Files
ctask/internal/shell/launch.go
T
typebasedio 103f2cd33e feat(v0.5): launch agent inside project subdirectory via launch_dir
LaunchOpts gains LaunchDir. session.Run resolves it via
workspace.ResolveLaunch, prints any fallback warning, and passes the
absolute path as the child process's working directory. Security
violations (absolute paths, .. escape) abort the session. The banner
gains a 'project dir: <name>/' line when launch_dir is set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 19:49:29 -04:00

112 lines
2.8 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."
}
// ExecAgent launches the agent command in the workspace directory.
func ExecAgent(agent string, wsDir string, envVars map[string]string) error {
path, err := exec.LookPath(agent)
if err != nil {
return fmt.Errorf("agent command not found: %s", agent)
}
cmd := exec.Command(path)
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()
}