Files
typebasedio b75b82e676 feat(v0.6): launch path carries ResolvedAgent (command + args + env)
LaunchOpts.Agent (string) and WorkspaceEntryOptions.Agent (string) are
replaced by *agent.Resolved, carrying Command, Args, and Env. The five
entry commands (new, resume, last, open, attach) each construct an
AgentSpec from the workspace metadata, apply --agent as a one-shot
agent.command override (Open Q 1 — keeps muscle memory for users
passing executable paths), call agent.Resolve, and pass the result
through. resolveEntryAgent centralises the resume/last/open/attach path.

shell.ExecAgent and shell.ExecTmuxAgent gain an args parameter; agent.env
is merged into the env map at the session.Run launch switch, AFTER
ctask's exported CTASK_* vars (per spec §5: agent.env wins on collision).
mergeAgentEnv is the centralised merge.

Lease, manifest, write lock, heartbeat, summary, and provisional
cleanup are unchanged. The Agent string fields on Lease, SessionSummary,
and SessionInfo continue to record the launched command for diagnostics.
2026-05-15 11:08:03 -04:00

127 lines
3.5 KiB
Go

package shell
import (
"fmt"
"os"
"os/exec"
"runtime"
)
// DefaultShell returns the platform-appropriate interactive shell.
func DefaultShell() string {
if runtime.GOOS == "windows" {
if _, err := exec.LookPath("powershell.exe"); err == nil {
return "powershell.exe"
}
return "cmd.exe"
}
shell := os.Getenv("SHELL")
if shell != "" {
return shell
}
return "bash"
}
// BuildEnvList merges ctask env vars into the current process environment.
func BuildEnvList(vars map[string]string) []string {
env := os.Environ()
for k, v := range vars {
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
return env
}
// PromptPrefix returns the shell prompt prefix for --shell mode.
func PromptPrefix(slug, mode string) string {
return fmt.Sprintf("(ctask:%s|%s) ", slug, mode)
}
// BannerLines returns the launch banner lines for agent mode.
// Adds a "project dir:" line when launchDir is non-empty.
func BannerLines(mode, slug, wsPath, launchDir string) []string {
lines := []string{
fmt.Sprintf("[ctask] %s :: %s", mode, slug),
fmt.Sprintf("[ctask] %s", wsPath),
}
if launchDir != "" {
lines = append(lines, fmt.Sprintf("[ctask] project dir: %s/", launchDir))
}
return lines
}
// ContainerNotice returns the v0.1 deferred container mode message.
func ContainerNotice() string {
return "[ctask] container mode is not available in v0.1. Use local mode or see docs for manual container setup."
}
// execAgentArgs is the pure helper: returns the (path, argv) pair that
// ExecAgent will hand to exec.Command. Extracted so unit tests can
// assert argv shape without spawning a real child.
func execAgentArgs(agent string, args []string) (string, []string, error) {
path, err := exec.LookPath(agent)
if err != nil {
return "", nil, fmt.Errorf("agent command not found: %s", agent)
}
return path, args, nil
}
// ExecAgent launches the agent command in the workspace directory.
// args is appended after the executable; envVars is merged into the
// process environment via BuildEnvList (which keeps os.Environ() as the
// base — agent.env entries layered into envVars by the caller take
// precedence on collision, per v0.6 spec §5).
func ExecAgent(agent string, args []string, wsDir string, envVars map[string]string) error {
path, argv, err := execAgentArgs(agent, args)
if err != nil {
return err
}
cmd := exec.Command(path, argv...)
cmd.Dir = wsDir
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = BuildEnvList(envVars)
return cmd.Run()
}
// ExecShell launches an interactive shell in the workspace directory.
func ExecShell(wsDir string, envVars map[string]string, slug, mode string) error {
shellCmd := DefaultShell()
prefix := PromptPrefix(slug, mode)
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
if shellCmd == "powershell.exe" {
cmd = exec.Command(shellCmd, "-NoExit", "-Command",
fmt.Sprintf("function prompt { '%s' + (Get-Location).Path + '> ' }", prefix))
} else {
// cmd.exe
cmd = exec.Command(shellCmd)
}
} else {
cmd = exec.Command(shellCmd)
}
cmd.Dir = wsDir
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
env := BuildEnvList(envVars)
if runtime.GOOS == "windows" && shellCmd == "cmd.exe" {
env = append(env, fmt.Sprintf("PROMPT=%s$P$G", prefix))
} else if runtime.GOOS != "windows" {
existingPS1 := os.Getenv("PS1")
if existingPS1 == "" {
existingPS1 = "\\u@\\h:\\w\\$ "
}
env = append(env, fmt.Sprintf("PS1=%s%s", prefix, existingPS1))
}
cmd.Env = env
return cmd.Run()
}