From 75b1dddf910706bb548d6b23e416528f2fc74506 Mon Sep 17 00:00:00 2001 From: Parth Sareen Date: Tue, 3 Feb 2026 02:03:33 -0500 Subject: [PATCH] cmd: launch extra params (#14039) --- cmd/config/claude.go | 12 ++-- cmd/config/claude_test.go | 12 ++-- cmd/config/codex.go | 7 +- cmd/config/codex_test.go | 11 +-- cmd/config/droid.go | 4 +- cmd/config/integrations.go | 45 +++++++++--- cmd/config/integrations_test.go | 120 ++++++++++++++++++++++++++++++-- cmd/config/openclaw.go | 4 +- cmd/config/opencode.go | 4 +- 9 files changed, 182 insertions(+), 37 deletions(-) diff --git a/cmd/config/claude.go b/cmd/config/claude.go index bc183a6ac..80a72f564 100644 --- a/cmd/config/claude.go +++ b/cmd/config/claude.go @@ -15,11 +15,13 @@ type Claude struct{} func (c *Claude) String() string { return "Claude Code" } -func (c *Claude) args(model string) []string { +func (c *Claude) args(model string, extra []string) []string { + var args []string if model != "" { - return []string{"--model", model} + args = append(args, "--model", model) } - return nil + args = append(args, extra...) + return args } func (c *Claude) findPath() (string, error) { @@ -41,13 +43,13 @@ func (c *Claude) findPath() (string, error) { return fallback, nil } -func (c *Claude) Run(model string) error { +func (c *Claude) Run(model string, args []string) error { claudePath, err := c.findPath() if err != nil { return fmt.Errorf("claude is not installed, install from https://code.claude.com/docs/en/quickstart") } - cmd := exec.Command(claudePath, c.args(model)...) + cmd := exec.Command(claudePath, c.args(model, args)...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/cmd/config/claude_test.go b/cmd/config/claude_test.go index a1f7cbc2a..dbbaee769 100644 --- a/cmd/config/claude_test.go +++ b/cmd/config/claude_test.go @@ -84,17 +84,21 @@ func TestClaudeArgs(t *testing.T) { tests := []struct { name string model string + args []string want []string }{ - {"with model", "llama3.2", []string{"--model", "llama3.2"}}, - {"empty model", "", nil}, + {"with model", "llama3.2", nil, []string{"--model", "llama3.2"}}, + {"empty model", "", nil, nil}, + {"with model and verbose", "llama3.2", []string{"--verbose"}, []string{"--model", "llama3.2", "--verbose"}}, + {"empty model with help", "", []string{"--help"}, []string{"--help"}}, + {"with allowed tools", "llama3.2", []string{"--allowedTools", "Read,Write,Bash"}, []string{"--model", "llama3.2", "--allowedTools", "Read,Write,Bash"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := c.args(tt.model) + got := c.args(tt.model, tt.args) if !slices.Equal(got, tt.want) { - t.Errorf("args(%q) = %v, want %v", tt.model, got, tt.want) + t.Errorf("args(%q, %v) = %v, want %v", tt.model, tt.args, got, tt.want) } }) } diff --git a/cmd/config/codex.go b/cmd/config/codex.go index f421e1f6a..f9c52f61d 100644 --- a/cmd/config/codex.go +++ b/cmd/config/codex.go @@ -14,20 +14,21 @@ type Codex struct{} func (c *Codex) String() string { return "Codex" } -func (c *Codex) args(model string) []string { +func (c *Codex) args(model string, extra []string) []string { args := []string{"--oss"} if model != "" { args = append(args, "-m", model) } + args = append(args, extra...) return args } -func (c *Codex) Run(model string) error { +func (c *Codex) Run(model string, args []string) error { if err := checkCodexVersion(); err != nil { return err } - cmd := exec.Command("codex", c.args(model)...) + cmd := exec.Command("codex", c.args(model, args)...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/cmd/config/codex_test.go b/cmd/config/codex_test.go index 2fe614211..9c18910be 100644 --- a/cmd/config/codex_test.go +++ b/cmd/config/codex_test.go @@ -11,17 +11,20 @@ func TestCodexArgs(t *testing.T) { tests := []struct { name string model string + args []string want []string }{ - {"with model", "llama3.2", []string{"--oss", "-m", "llama3.2"}}, - {"empty model", "", []string{"--oss"}}, + {"with model", "llama3.2", nil, []string{"--oss", "-m", "llama3.2"}}, + {"empty model", "", nil, []string{"--oss"}}, + {"with model and profile", "qwen3-coder", []string{"-p", "myprofile"}, []string{"--oss", "-m", "qwen3-coder", "-p", "myprofile"}}, + {"with sandbox flag", "llama3.2", []string{"--sandbox", "workspace-write"}, []string{"--oss", "-m", "llama3.2", "--sandbox", "workspace-write"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := c.args(tt.model) + got := c.args(tt.model, tt.args) if !slices.Equal(got, tt.want) { - t.Errorf("args(%q) = %v, want %v", tt.model, got, tt.want) + t.Errorf("args(%q, %v) = %v, want %v", tt.model, tt.args, got, tt.want) } }) } diff --git a/cmd/config/droid.go b/cmd/config/droid.go index 1e2a853a9..340cda4fc 100644 --- a/cmd/config/droid.go +++ b/cmd/config/droid.go @@ -39,7 +39,7 @@ type modelEntry struct { func (d *Droid) String() string { return "Droid" } -func (d *Droid) Run(model string) error { +func (d *Droid) Run(model string, args []string) error { if _, err := exec.LookPath("droid"); err != nil { return fmt.Errorf("droid is not installed, install from https://docs.factory.ai/cli/getting-started/quickstart") } @@ -53,7 +53,7 @@ func (d *Droid) Run(model string) error { return fmt.Errorf("setup failed: %w", err) } - cmd := exec.Command("droid") + cmd := exec.Command("droid", args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/cmd/config/integrations.go b/cmd/config/integrations.go index fcf596efd..fb13202d5 100644 --- a/cmd/config/integrations.go +++ b/cmd/config/integrations.go @@ -22,7 +22,7 @@ import ( // Runner can run an integration with a model. type Runner interface { - Run(model string) error + Run(model string, args []string) error // String returns the human-readable name of the integration String() string } @@ -233,13 +233,13 @@ func selectModels(ctx context.Context, name, current string) ([]string, error) { return selected, nil } -func runIntegration(name, modelName string) error { +func runIntegration(name, modelName string, args []string) error { r, ok := integrations[name] if !ok { return fmt.Errorf("unknown integration: %s", name) } fmt.Fprintf(os.Stderr, "\nLaunching %s with %s...\n", r, modelName) - return r.Run(modelName) + return r.Run(modelName, args) } // LaunchCmd returns the cobra command for launching integrations. @@ -248,7 +248,7 @@ func LaunchCmd(checkServerHeartbeat func(cmd *cobra.Command, args []string) erro var configFlag bool cmd := &cobra.Command{ - Use: "launch [INTEGRATION]", + Use: "launch [INTEGRATION] [-- [EXTRA_ARGS...]]", Short: "Launch an integration with Ollama", Long: `Launch an integration configured with Ollama models. @@ -263,14 +263,37 @@ Examples: ollama launch ollama launch claude ollama launch claude --model - ollama launch droid --config (does not auto-launch)`, - Args: cobra.MaximumNArgs(1), + ollama launch droid --config (does not auto-launch) + ollama launch codex -- -p myprofile (pass extra args to integration) + ollama launch codex -- --sandbox workspace-write`, + Args: cobra.ArbitraryArgs, PreRunE: checkServerHeartbeat, RunE: func(cmd *cobra.Command, args []string) error { + // Extract integration name and args to pass through using -- separator var name string - if len(args) > 0 { - name = args[0] + var passArgs []string + dashIdx := cmd.ArgsLenAtDash() + + if dashIdx == -1 { + // No "--" separator: only allow 0 or 1 args (integration name) + if len(args) > 1 { + return fmt.Errorf("unexpected arguments: %v\nUse '--' to pass extra arguments to the integration", args[1:]) + } + if len(args) == 1 { + name = args[0] + } } else { + // "--" was used: args before it = integration name, args after = passthrough + if dashIdx > 1 { + return fmt.Errorf("expected at most 1 integration name before '--', got %d", dashIdx) + } + if dashIdx == 1 { + name = args[0] + } + passArgs = args[dashIdx:] + } + + if name == "" { var err error name, err = selectIntegration() if errors.Is(err, errCancelled) { @@ -289,7 +312,7 @@ Examples: // If launching without --model, use saved config if available if !configFlag && modelFlag == "" { if config, err := loadIntegration(name); err == nil && len(config.Models) > 0 { - return runIntegration(name, config.Models[0]) + return runIntegration(name, config.Models[0], passArgs) } } @@ -350,13 +373,13 @@ Examples: if configFlag { if launch, _ := confirmPrompt(fmt.Sprintf("\nLaunch %s now?", r)); launch { - return runIntegration(name, models[0]) + return runIntegration(name, models[0], passArgs) } fmt.Fprintf(os.Stderr, "Run 'ollama launch %s' to start with %s\n", strings.ToLower(name), models[0]) return nil } - return runIntegration(name, models[0]) + return runIntegration(name, models[0], passArgs) }, } diff --git a/cmd/config/integrations_test.go b/cmd/config/integrations_test.go index 48f357db3..e460142c4 100644 --- a/cmd/config/integrations_test.go +++ b/cmd/config/integrations_test.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "slices" "strings" "testing" @@ -90,8 +91,8 @@ func TestLaunchCmd(t *testing.T) { cmd := LaunchCmd(mockCheck) t.Run("command structure", func(t *testing.T) { - if cmd.Use != "launch [INTEGRATION]" { - t.Errorf("Use = %q, want %q", cmd.Use, "launch [INTEGRATION]") + if cmd.Use != "launch [INTEGRATION] [-- [EXTRA_ARGS...]]" { + t.Errorf("Use = %q, want %q", cmd.Use, "launch [INTEGRATION] [-- [EXTRA_ARGS...]]") } if cmd.Short == "" { t.Error("Short description should not be empty") @@ -121,7 +122,7 @@ func TestLaunchCmd(t *testing.T) { } func TestRunIntegration_UnknownIntegration(t *testing.T) { - err := runIntegration("unknown-integration", "model") + err := runIntegration("unknown-integration", "model", nil) if err == nil { t.Error("expected error for unknown integration, got nil") } @@ -182,7 +183,118 @@ func TestAllIntegrations_HaveRequiredMethods(t *testing.T) { // Test Run() exists (we can't call it without actually running the command) // Just verify the method is available - var _ func(string) error = r.Run + var _ func(string, []string) error = r.Run + }) + } +} + +func TestParseArgs(t *testing.T) { + // Tests reflect cobra's ArgsLenAtDash() semantics: + // - cobra strips "--" from args + // - ArgsLenAtDash() returns the index where "--" was, or -1 + tests := []struct { + name string + args []string // args as cobra delivers them (no "--") + dashIdx int // what ArgsLenAtDash() returns + wantName string + wantArgs []string + wantErr bool + }{ + { + name: "no extra args, no dash", + args: []string{"claude"}, + dashIdx: -1, + wantName: "claude", + }, + { + name: "with extra args after --", + args: []string{"codex", "-p", "myprofile"}, + dashIdx: 1, + wantName: "codex", + wantArgs: []string{"-p", "myprofile"}, + }, + { + name: "extra args only after --", + args: []string{"codex", "--sandbox", "workspace-write"}, + dashIdx: 1, + wantName: "codex", + wantArgs: []string{"--sandbox", "workspace-write"}, + }, + { + name: "-- at end with no args after", + args: []string{"claude"}, + dashIdx: 1, + wantName: "claude", + }, + { + name: "-- with no integration name", + args: []string{"--verbose"}, + dashIdx: 0, + wantName: "", + wantArgs: []string{"--verbose"}, + }, + { + name: "multiple args before -- is error", + args: []string{"claude", "codex", "--verbose"}, + dashIdx: 2, + wantErr: true, + }, + { + name: "multiple args without -- is error", + args: []string{"claude", "codex"}, + dashIdx: -1, + wantErr: true, + }, + { + name: "no args, no dash", + args: []string{}, + dashIdx: -1, + wantName: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Simulate the parsing logic from LaunchCmd using dashIdx + var name string + var parsedArgs []string + var err error + + dashIdx := tt.dashIdx + args := tt.args + + if dashIdx == -1 { + if len(args) > 1 { + err = fmt.Errorf("unexpected arguments: %v", args[1:]) + } else if len(args) == 1 { + name = args[0] + } + } else { + if dashIdx > 1 { + err = fmt.Errorf("expected at most 1 integration name before '--', got %d", dashIdx) + } else { + if dashIdx == 1 { + name = args[0] + } + parsedArgs = args[dashIdx:] + } + } + + if tt.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if name != tt.wantName { + t.Errorf("name = %q, want %q", name, tt.wantName) + } + if !slices.Equal(parsedArgs, tt.wantArgs) { + t.Errorf("args = %v, want %v", parsedArgs, tt.wantArgs) + } }) } } diff --git a/cmd/config/openclaw.go b/cmd/config/openclaw.go index 613b032f0..a1e4a537d 100644 --- a/cmd/config/openclaw.go +++ b/cmd/config/openclaw.go @@ -19,7 +19,7 @@ func (c *Openclaw) String() string { return "OpenClaw" } const ansiGreen = "\033[32m" -func (c *Openclaw) Run(model string) error { +func (c *Openclaw) Run(model string, args []string) error { bin := "openclaw" if _, err := exec.LookPath(bin); err != nil { bin = "clawdbot" @@ -52,7 +52,7 @@ func (c *Openclaw) Run(model string) error { } // Onboarding completed: run gateway - cmd := exec.Command(bin, "gateway") + cmd := exec.Command(bin, append([]string{"gateway"}, args...)...) cmd.Stdin = os.Stdin // Capture output to detect "already running" message diff --git a/cmd/config/opencode.go b/cmd/config/opencode.go index 736acfec5..06fce2743 100644 --- a/cmd/config/opencode.go +++ b/cmd/config/opencode.go @@ -18,7 +18,7 @@ type OpenCode struct{} func (o *OpenCode) String() string { return "OpenCode" } -func (o *OpenCode) Run(model string) error { +func (o *OpenCode) Run(model string, args []string) error { if _, err := exec.LookPath("opencode"); err != nil { return fmt.Errorf("opencode is not installed, install from https://opencode.ai") } @@ -32,7 +32,7 @@ func (o *OpenCode) Run(model string) error { return fmt.Errorf("setup failed: %w", err) } - cmd := exec.Command("opencode") + cmd := exec.Command("opencode", args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr