Compare commits

...

1 Commits

Author SHA1 Message Date
jmorganca
2f1930cfd6 feat: make ollama with no args launch integrations UI 2026-02-07 15:49:41 -08:00
3 changed files with 245 additions and 217 deletions

View File

@@ -1826,6 +1826,18 @@ func NewCLI() *cobra.Command {
return return
} }
// If no args, run launch to show interactive app selector
if len(args) == 0 {
if err := checkServerHeartbeat(cmd, args); err != nil {
cobra.CheckErr(err)
return
}
if err := config.RunLaunch(cmd, args, "", false); err != nil {
cobra.CheckErr(err)
}
return
}
cmd.Print(cmd.UsageString()) cmd.Print(cmd.UsageString())
}, },
} }

View File

@@ -59,6 +59,12 @@ var integrations = map[string]Runner{
"openclaw": &Openclaw{}, "openclaw": &Openclaw{},
} }
// IsIntegration returns true if the given name is a valid integration.
func IsIntegration(name string) bool {
_, ok := integrations[strings.ToLower(name)]
return ok
}
// recommendedModels are shown when the user has no models or as suggestions. // recommendedModels are shown when the user has no models or as suggestions.
// Order matters: local models first, then cloud models. // Order matters: local models first, then cloud models.
var recommendedModels = []selectItem{ var recommendedModels = []selectItem{
@@ -76,7 +82,7 @@ var integrationAliases = map[string]bool{
func selectIntegration() (string, error) { func selectIntegration() (string, error) {
if len(integrations) == 0 { if len(integrations) == 0 {
return "", fmt.Errorf("no integrations available") return "", fmt.Errorf("no apps available")
} }
names := slices.Sorted(maps.Keys(integrations)) names := slices.Sorted(maps.Keys(integrations))
@@ -93,14 +99,14 @@ func selectIntegration() (string, error) {
items = append(items, selectItem{Name: name, Description: description}) items = append(items, selectItem{Name: name, Description: description})
} }
return selectPrompt("Select integration:", items) return selectPrompt("Select app:", items)
} }
// selectModels lets the user select models for an integration // selectModels lets the user select models for an integration
func selectModels(ctx context.Context, name, current string) ([]string, error) { func selectModels(ctx context.Context, name, current string) ([]string, error) {
r, ok := integrations[name] r, ok := integrations[name]
if !ok { if !ok {
return nil, fmt.Errorf("unknown integration: %s", name) return nil, fmt.Errorf("unknown app: %s", name)
} }
client, err := api.ClientFromEnvironment() client, err := api.ClientFromEnvironment()
@@ -306,7 +312,7 @@ func ensureAuth(ctx context.Context, client *api.Client, cloudModels map[string]
func runIntegration(name, modelName string, args []string) error { func runIntegration(name, modelName string, args []string) error {
r, ok := integrations[name] r, ok := integrations[name]
if !ok { if !ok {
return fmt.Errorf("unknown integration: %s", name) return fmt.Errorf("unknown app: %s", name)
} }
fmt.Fprintf(os.Stderr, "\nLaunching %s with %s...\n", r, modelName) fmt.Fprintf(os.Stderr, "\nLaunching %s with %s...\n", r, modelName)
@@ -335,33 +341,10 @@ func syncAliases(ctx context.Context, client *api.Client, ac AliasConfigurer, na
return saveAliases(name, aliases) return saveAliases(name, aliases)
} }
// LaunchCmd returns the cobra command for launching integrations. // RunLaunch executes the launch logic for the given integration and arguments.
func LaunchCmd(checkServerHeartbeat func(cmd *cobra.Command, args []string) error) *cobra.Command { // This can be called directly from the root command (with empty modelFlag/configFlag)
var modelFlag string // or via the launch subcommand.
var configFlag bool func RunLaunch(cmd *cobra.Command, args []string, modelFlag string, configFlag bool) error {
cmd := &cobra.Command{
Use: "launch [INTEGRATION] [-- [EXTRA_ARGS...]]",
Short: "Launch an integration with Ollama",
Long: `Launch an integration configured with Ollama models.
Supported integrations:
claude Claude Code
codex Codex
droid Droid
opencode OpenCode
openclaw OpenClaw (aliases: clawdbot, moltbot)
Examples:
ollama launch
ollama launch claude
ollama launch claude --model <model>
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 // Extract integration name and args to pass through using -- separator
var name string var name string
var passArgs []string var passArgs []string
@@ -370,7 +353,7 @@ Examples:
if dashIdx == -1 { if dashIdx == -1 {
// No "--" separator: only allow 0 or 1 args (integration name) // No "--" separator: only allow 0 or 1 args (integration name)
if len(args) > 1 { if len(args) > 1 {
return fmt.Errorf("unexpected arguments: %v\nUse '--' to pass extra arguments to the integration", args[1:]) return fmt.Errorf("unexpected arguments: %v\nUse '--' to pass extra arguments to the app", args[1:])
} }
if len(args) == 1 { if len(args) == 1 {
name = args[0] name = args[0]
@@ -378,7 +361,7 @@ Examples:
} else { } else {
// "--" was used: args before it = integration name, args after = passthrough // "--" was used: args before it = integration name, args after = passthrough
if dashIdx > 1 { if dashIdx > 1 {
return fmt.Errorf("expected at most 1 integration name before '--', got %d", dashIdx) return fmt.Errorf("expected at most 1 app name before '--', got %d", dashIdx)
} }
if dashIdx == 1 { if dashIdx == 1 {
name = args[0] name = args[0]
@@ -399,7 +382,7 @@ Examples:
r, ok := integrations[strings.ToLower(name)] r, ok := integrations[strings.ToLower(name)]
if !ok { if !ok {
return fmt.Errorf("unknown integration: %s", name) return fmt.Errorf("unknown app: %s", name)
} }
// Handle AliasConfigurer integrations (claude, codex) // Handle AliasConfigurer integrations (claude, codex)
@@ -561,11 +544,44 @@ Examples:
if launch, _ := confirmPrompt(fmt.Sprintf("\nLaunch %s now?", r)); launch { if launch, _ := confirmPrompt(fmt.Sprintf("\nLaunch %s now?", r)); launch {
return runIntegration(name, models[0], passArgs) 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 nil
} }
return runIntegration(name, models[0], passArgs) if runner, isRunner := r.(Runner); isRunner {
return runner.Run(models[0], passArgs)
}
return nil
}
// LaunchCmd returns the cobra command for launching integrations.
func LaunchCmd(checkServerHeartbeat func(cmd *cobra.Command, args []string) error) *cobra.Command {
var modelFlag string
var configFlag bool
cmd := &cobra.Command{
Use: "launch [APP] [-- [EXTRA_ARGS...]]",
Short: "Launch an app with Ollama",
Long: `Launch an app configured with Ollama models.
Supported apps:
claude Claude Code
codex Codex
droid Droid
opencode OpenCode
openclaw OpenClaw (aliases: clawdbot, moltbot)
Examples:
ollama launch
ollama launch claude
ollama launch claude --model <model>
ollama launch droid --config (does not auto-launch)
ollama launch codex -- -p myprofile (pass extra args to app)
ollama launch codex -- --sandbox workspace-write`,
Args: cobra.ArbitraryArgs,
PreRunE: checkServerHeartbeat,
RunE: func(cmd *cobra.Command, args []string) error {
return RunLaunch(cmd, args, modelFlag, configFlag)
}, },
} }

View File

@@ -98,8 +98,8 @@ func TestLaunchCmd(t *testing.T) {
cmd := LaunchCmd(mockCheck) cmd := LaunchCmd(mockCheck)
t.Run("command structure", func(t *testing.T) { t.Run("command structure", func(t *testing.T) {
if cmd.Use != "launch [INTEGRATION] [-- [EXTRA_ARGS...]]" { if cmd.Use != "launch [APP] [-- [EXTRA_ARGS...]]" {
t.Errorf("Use = %q, want %q", cmd.Use, "launch [INTEGRATION] [-- [EXTRA_ARGS...]]") t.Errorf("Use = %q, want %q", cmd.Use, "launch [APP] [-- [EXTRA_ARGS...]]")
} }
if cmd.Short == "" { if cmd.Short == "" {
t.Error("Short description should not be empty") t.Error("Short description should not be empty")
@@ -133,8 +133,8 @@ func TestRunIntegration_UnknownIntegration(t *testing.T) {
if err == nil { if err == nil {
t.Error("expected error for unknown integration, got nil") t.Error("expected error for unknown integration, got nil")
} }
if !strings.Contains(err.Error(), "unknown integration") { if !strings.Contains(err.Error(), "unknown app") {
t.Errorf("error should mention 'unknown integration', got: %v", err) t.Errorf("error should mention 'unknown app', got: %v", err)
} }
} }