Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 41 additions & 5 deletions cmd/sandbox/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,13 +327,13 @@ func runProcessShell(c *cli.Context) error {
if err != nil {
return err
}
envs, err := parseEnvFlags(c.StringSlice("env"))
envs, err := parseEnvFlags(processStringSliceFlag(c, subcommand, "env"))
if err != nil {
return err
}
req := api.ProcessCreateRequest{
Cmd: c.String("cmd"),
Cwd: strings.TrimSpace(c.String("cwd")),
Cmd: processStringFlag(c, subcommand, "cmd"),
Cwd: strings.TrimSpace(processStringFlag(c, subcommand, "cwd")),
Env: envs,
PTY: ptyOptionsFromFlags(c, !output.IsJSON(c) && !processBoolFlag(c, subcommand, "no-attach")),
}
Expand Down Expand Up @@ -671,7 +671,7 @@ func processClientSandboxAndOptionalProcess(c *cli.Context) (*api.SandboxClient,

func processCreateRequestFromCLI(c *cli.Context, shellMode bool) (api.ProcessCreateRequest, error) {
subcommand := c.Command.Name
envs, err := parseEnvFlags(c.StringSlice("env"))
envs, err := parseEnvFlags(processStringSliceFlag(c, subcommand, "env"))
if err != nil {
return api.ProcessCreateRequest{}, err
}
Expand All @@ -683,7 +683,7 @@ func processCreateRequestFromCLI(c *cli.Context, shellMode bool) (api.ProcessCre
req := api.ProcessCreateRequest{
Cmd: cmd,
Args: args,
Cwd: strings.TrimSpace(c.String("cwd")),
Cwd: strings.TrimSpace(processStringFlag(c, subcommand, "cwd")),
Env: envs,
}
if processBoolFlag(c, subcommand, "pty") {
Expand Down Expand Up @@ -1575,6 +1575,14 @@ func processInt64Flag(c *cli.Context, subcommand, name string) int64 {
return v
}

// Repeatable flags need every occurrence, not just the first.
func processStringSliceFlag(c *cli.Context, subcommand, name string) []string {
if v := c.StringSlice(name); len(v) > 0 {
return v
}
return rawProcessFlagValues(subcommand, name)
}

func processDurationFlag(c *cli.Context, subcommand, name string) time.Duration {
if v := c.Duration(name); v != 0 {
return v
Expand Down Expand Up @@ -1608,6 +1616,34 @@ func rawProcessFlagPresent(subcommand, name string) bool {
return false
}

func rawProcessFlagValues(subcommand, name string) []string {
start := processSubcommandArgIndex(subcommand)
if start < 0 {
return nil
}
target := "--" + name
prefix := target + "="
var out []string
for i := start + 1; i < len(os.Args); i++ {
arg := os.Args[i]
if arg == "--" {
break
}
if strings.HasPrefix(arg, prefix) {
out = append(out, strings.TrimPrefix(arg, prefix))
continue
}
if arg == target && i+1 < len(os.Args) {
next := os.Args[i+1]
if !strings.HasPrefix(next, "-") {
out = append(out, next)
i++
}
}
}
return out
}

func rawProcessFlagValue(subcommand, name string) string {
start := processSubcommandArgIndex(subcommand)
if start < 0 {
Expand Down
68 changes: 68 additions & 0 deletions cmd/sandbox/process_flags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package sandbox

import (
"os"
"testing"
)

// urfave/cli stops parsing flags at the first positional argument, so a flag
// written after the sandbox name never reaches the cli.Context. The raw helpers
// recover it from os.Args. Values after "--" belong to the sandbox command and
// must never be read as flags.

func withArgs(t *testing.T, args []string) {
t.Helper()
saved := os.Args
os.Args = args
t.Cleanup(func() { os.Args = saved })
}

func TestRawProcessFlagValueAfterPositional(t *testing.T) {
withArgs(t, []string{"createos", "sandbox", "process", "start", "my-box", "--cwd", "/workspace", "--", "claude"})
if got := rawProcessFlagValue("start", "cwd"); got != "/workspace" {
t.Fatalf("cwd = %q, want /workspace", got)
}
}

func TestRawProcessFlagValueEqualsForm(t *testing.T) {
withArgs(t, []string{"createos", "sandbox", "process", "start", "my-box", "--cwd=/workspace", "--", "claude"})
if got := rawProcessFlagValue("start", "cwd"); got != "/workspace" {
t.Fatalf("cwd = %q, want /workspace", got)
}
}

func TestRawProcessFlagValueStopsAtDoubleDash(t *testing.T) {
withArgs(t, []string{"createos", "sandbox", "process", "run", "my-box", "--", "sh", "-c", "--cwd", "/evil"})
if got := rawProcessFlagValue("run", "cwd"); got != "" {
t.Fatalf("cwd = %q, want empty: values after -- are the sandbox command", got)
}
}

func TestRawProcessFlagValuesCollectsEveryOccurrence(t *testing.T) {
withArgs(t, []string{"createos", "sandbox", "process", "start", "my-box", "--env", "A=1", "--env=B=2", "--", "claude"})
got := rawProcessFlagValues("start", "env")
want := []string{"A=1", "B=2"}
if len(got) != len(want) {
t.Fatalf("env = %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("env = %v, want %v", got, want)
}
}
}

func TestRawProcessFlagValuesStopsAtDoubleDash(t *testing.T) {
withArgs(t, []string{"createos", "sandbox", "process", "run", "my-box", "--env", "A=1", "--", "env", "--env", "B=2"})
got := rawProcessFlagValues("run", "env")
if len(got) != 1 || got[0] != "A=1" {
t.Fatalf("env = %v, want [A=1]", got)
}
}

func TestRawProcessFlagValuesEmptyWhenAbsent(t *testing.T) {
withArgs(t, []string{"createos", "sandbox", "process", "start", "my-box", "--", "claude"})
if got := rawProcessFlagValues("start", "env"); len(got) != 0 {
t.Fatalf("env = %v, want empty", got)
}
}