292 lines
6.9 KiB
Go
292 lines
6.9 KiB
Go
package zellij
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/aoagents/agent-orchestrator/backend/internal/ports"
|
|
)
|
|
|
|
const (
|
|
agentPaneName = "agent"
|
|
defaultChunkBytes = 16 * 1024
|
|
)
|
|
|
|
func versionArgs() []string {
|
|
return []string{"--version"}
|
|
}
|
|
|
|
func createSessionArgs(id, layoutPath string) []string {
|
|
return []string{
|
|
"attach", "--create-background", id,
|
|
"options",
|
|
"--default-layout", layoutPath,
|
|
"--pane-frames", "false",
|
|
"--session-serialization", "false",
|
|
"--show-startup-tips", "false",
|
|
"--show-release-notes", "false",
|
|
}
|
|
}
|
|
|
|
func listPanesArgs(id string) []string {
|
|
return []string{"--session", id, "action", "list-panes", "--all", "--json"}
|
|
}
|
|
|
|
func pasteArgs(id, paneID, chunk string) []string {
|
|
return []string{"--session", id, "action", "paste", "--pane-id", paneID, chunk}
|
|
}
|
|
|
|
func sendEnterArgs(id, paneID string) []string {
|
|
return []string{"--session", id, "action", "send-keys", "--pane-id", paneID, "Enter"}
|
|
}
|
|
|
|
func dumpScreenArgs(id, paneID string) []string {
|
|
return []string{"--session", id, "action", "dump-screen", "--pane-id", paneID, "--full"}
|
|
}
|
|
|
|
func listSessionsArgs() []string {
|
|
return []string{"list-sessions", "--no-formatting"}
|
|
}
|
|
|
|
func killSessionArgs(id string) []string {
|
|
return []string{"kill-session", id}
|
|
}
|
|
|
|
func attachArgs(id string) []string {
|
|
return []string{"attach", id}
|
|
}
|
|
|
|
func handleIDValue(sessionID, paneID string) string {
|
|
return sessionID + "/" + paneID
|
|
}
|
|
|
|
func terminalPaneID(id int) string {
|
|
return fmt.Sprintf("terminal_%d", id)
|
|
}
|
|
|
|
func buildLayout(cfg ports.RuntimeConfig, shellPath string) string {
|
|
spec := shellLaunchSpecFor(shellPath)
|
|
shellCommand := shellLaunchCommand(cfg, shellPath, spec)
|
|
return layoutString(cfg.WorkspacePath, shellPath, spec.args, shellCommand)
|
|
}
|
|
|
|
type shellLaunchSpec struct {
|
|
args []string
|
|
}
|
|
|
|
func shellLaunchSpecFor(shellPath string) shellLaunchSpec {
|
|
base := strings.ToLower(filepathBase(shellPath))
|
|
if strings.Contains(base, "cmd") {
|
|
return shellLaunchSpec{args: []string{"/D", "/S", "/K"}}
|
|
}
|
|
if strings.Contains(base, "powershell") || strings.Contains(base, "pwsh") {
|
|
return shellLaunchSpec{args: []string{"-NoLogo", "-NoProfile", "-NoExit", "-Command"}}
|
|
}
|
|
return shellLaunchSpec{args: []string{"-lc"}}
|
|
}
|
|
|
|
func layoutString(workspacePath, shellPath string, shellArgs []string, shellCommand string) string {
|
|
return "layout {\n" +
|
|
" cwd " + kdlQuote(workspacePath) + "\n" +
|
|
" pane command=" + kdlQuote(shellPath) + " name=" + kdlQuote(agentPaneName) + " {\n" +
|
|
" args " + kdlJoin(shellArgs) + " " + kdlQuote(shellCommand) + "\n" +
|
|
" }\n" +
|
|
"}\n"
|
|
}
|
|
|
|
func shellLaunchCommand(cfg ports.RuntimeConfig, shellPath string, spec shellLaunchSpec) string {
|
|
if len(spec.args) > 0 && spec.args[0] == "-NoLogo" {
|
|
return wrapLaunchCommandPowerShell(cfg)
|
|
}
|
|
if len(spec.args) > 0 && spec.args[0] == "/D" {
|
|
return wrapLaunchCommandCmd(cfg)
|
|
}
|
|
return wrapLaunchCommandUnix(cfg, shellPath)
|
|
}
|
|
|
|
func wrapLaunchCommandUnix(cfg ports.RuntimeConfig, shellPath string) string {
|
|
path := cfg.Env["PATH"]
|
|
if path == "" {
|
|
path = getenv("PATH")
|
|
}
|
|
|
|
var b strings.Builder
|
|
for _, key := range sortedKeys(cfg.Env) {
|
|
if key == "PATH" {
|
|
continue
|
|
}
|
|
b.WriteString("export ")
|
|
b.WriteString(key)
|
|
b.WriteString("=")
|
|
b.WriteString(shellQuote(cfg.Env[key]))
|
|
b.WriteString("; ")
|
|
}
|
|
if path != "" {
|
|
b.WriteString("export PATH=")
|
|
b.WriteString(shellQuote(path))
|
|
b.WriteString("; ")
|
|
}
|
|
b.WriteString(quoteArgvUnix(cfg.Argv))
|
|
b.WriteString("; exec ")
|
|
b.WriteString(shellQuote(shellPath))
|
|
b.WriteString(" -i")
|
|
return b.String()
|
|
}
|
|
|
|
func wrapLaunchCommandPowerShell(cfg ports.RuntimeConfig) string {
|
|
path := cfg.Env["PATH"]
|
|
if path == "" {
|
|
path = getenv("PATH")
|
|
}
|
|
|
|
var b strings.Builder
|
|
for _, key := range sortedKeys(cfg.Env) {
|
|
if key == "PATH" {
|
|
continue
|
|
}
|
|
b.WriteString("$env:")
|
|
b.WriteString(key)
|
|
b.WriteString(" = ")
|
|
b.WriteString(psQuote(cfg.Env[key]))
|
|
b.WriteString("; ")
|
|
}
|
|
if path != "" {
|
|
b.WriteString("$env:PATH = ")
|
|
b.WriteString(psQuote(path))
|
|
b.WriteString("; ")
|
|
}
|
|
b.WriteString(quoteArgvPowerShell(cfg.Argv))
|
|
return b.String()
|
|
}
|
|
|
|
func wrapLaunchCommandCmd(cfg ports.RuntimeConfig) string {
|
|
path := cfg.Env["PATH"]
|
|
if path == "" {
|
|
path = getenv("PATH")
|
|
}
|
|
|
|
var b strings.Builder
|
|
for _, key := range sortedKeys(cfg.Env) {
|
|
if key == "PATH" {
|
|
continue
|
|
}
|
|
b.WriteString("set \"")
|
|
b.WriteString(key)
|
|
b.WriteString("=")
|
|
b.WriteString(cmdQuote(cfg.Env[key]))
|
|
b.WriteString("\" && ")
|
|
}
|
|
if path != "" {
|
|
b.WriteString("set \"PATH=")
|
|
b.WriteString(cmdQuote(path))
|
|
b.WriteString("\" && ")
|
|
}
|
|
b.WriteString(quoteArgvCmd(cfg.Argv))
|
|
return b.String()
|
|
}
|
|
|
|
func validateEnvKeys(env map[string]string) error {
|
|
for key := range env {
|
|
if !validEnvKey(key) {
|
|
return fmt.Errorf("zellij runtime: invalid env key %q", key)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validEnvKey(key string) bool {
|
|
if key == "" {
|
|
return false
|
|
}
|
|
for i, r := range key {
|
|
if r == '_' || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
|
|
continue
|
|
}
|
|
if i > 0 && r >= '0' && r <= '9' {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func sortedKeys(m map[string]string) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
func shellQuote(s string) string {
|
|
return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
|
|
}
|
|
|
|
func psQuote(s string) string {
|
|
return "'" + strings.ReplaceAll(s, "'", "''") + "'"
|
|
}
|
|
|
|
func cmdQuote(s string) string {
|
|
return strings.ReplaceAll(s, "\"", "\"\"")
|
|
}
|
|
|
|
// quoteArgvUnix renders argv as a POSIX-shell command, single-quoting each
|
|
// argument so a value with spaces stays one word under `sh -lc`.
|
|
func quoteArgvUnix(argv []string) string {
|
|
parts := make([]string, len(argv))
|
|
for i, a := range argv {
|
|
parts[i] = shellQuote(a)
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
// quoteArgvPowerShell renders argv for `powershell -Command`. The call operator
|
|
// `&` is required so a quoted first token is invoked as a command rather than
|
|
// echoed as a string literal.
|
|
func quoteArgvPowerShell(argv []string) string {
|
|
if len(argv) == 0 {
|
|
return ""
|
|
}
|
|
parts := make([]string, len(argv))
|
|
for i, a := range argv {
|
|
parts[i] = psQuote(a)
|
|
}
|
|
return "& " + strings.Join(parts, " ")
|
|
}
|
|
|
|
// quoteArgvCmd renders argv for cmd.exe, wrapping each argument in double quotes
|
|
// (doubling any embedded quote) so spaces don't split a single argument.
|
|
func quoteArgvCmd(argv []string) string {
|
|
parts := make([]string, len(argv))
|
|
for i, a := range argv {
|
|
parts[i] = "\"" + strings.ReplaceAll(a, "\"", "\"\"") + "\""
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
func kdlQuote(s string) string {
|
|
return strconv.Quote(s)
|
|
}
|
|
|
|
func kdlJoin(args []string) string {
|
|
parts := make([]string, 0, len(args))
|
|
for _, arg := range args {
|
|
parts = append(parts, kdlQuote(arg))
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
func filepathBase(path string) string {
|
|
if path == "" {
|
|
return ""
|
|
}
|
|
i := strings.LastIndexAny(path, `/\`)
|
|
if i < 0 {
|
|
return path
|
|
}
|
|
return path[i+1:]
|
|
}
|