Skip to content

Commit 1196133

Browse files
committed
improve launch command
- Allow `docker model launch` with no args to list supported apps with descriptions and install status - Add `--config` flag to print configuration without launching - Properly handle `--` separator for passing extra args to integrations - Reject unexpected extra args without `--` separator with helpful error - Add app descriptions for all supported apps - Update documentation and tests Signed-off-by: Eric Curtin <eric.curtin@docker.com>
1 parent aa6b09e commit 1196133

4 files changed

Lines changed: 294 additions & 12 deletions

File tree

cmd/cli/commands/launch.go

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,24 +78,60 @@ var supportedApps = func() []string {
7878
return apps
7979
}()
8080

81+
// appDescriptions provides human-readable descriptions for supported apps.
82+
var appDescriptions = map[string]string{
83+
"anythingllm": "RAG platform with Docker Model Runner provider",
84+
"claude": "Claude Code AI assistant",
85+
"codex": "Codex CLI",
86+
"openclaw": "Open Claw AI assistant",
87+
"opencode": "Open Code AI code editor",
88+
"openwebui": "Open WebUI for models",
89+
}
90+
8191
func newLaunchCmd() *cobra.Command {
8292
var (
83-
port int
84-
image string
85-
detach bool
86-
dryRun bool
93+
port int
94+
image string
95+
detach bool
96+
dryRun bool
97+
configOnly bool
8798
)
8899
c := &cobra.Command{
89-
Use: "launch APP [-- APP_ARGS...]",
100+
Use: "launch [APP] [-- APP_ARGS...]",
90101
Short: "Launch an app configured to use Docker Model Runner",
91102
Long: fmt.Sprintf(`Launch an app configured to use Docker Model Runner.
92103
93-
Supported apps: %s`, strings.Join(supportedApps, ", ")),
94-
Args: requireMinArgs(1, "launch", "APP [-- APP_ARGS...]"),
104+
Without arguments, lists all supported apps.
105+
106+
Supported apps: %s
107+
108+
Examples:
109+
docker model launch
110+
docker model launch opencode
111+
docker model launch claude -- --help
112+
docker model launch openwebui --port 3000
113+
docker model launch claude --config`, strings.Join(supportedApps, ", ")),
95114
ValidArgs: supportedApps,
96115
RunE: func(cmd *cobra.Command, args []string) error {
116+
// No args - list supported apps
117+
if len(args) == 0 {
118+
return listSupportedApps(cmd)
119+
}
120+
97121
app := strings.ToLower(args[0])
98-
appArgs := args[1:]
122+
123+
// Extract passthrough args using -- separator
124+
var appArgs []string
125+
dashIdx := cmd.ArgsLenAtDash()
126+
if dashIdx == -1 {
127+
// No "--" separator
128+
if len(args) > 1 {
129+
return fmt.Errorf("unexpected arguments: %s\nUse '--' to pass extra arguments to the app", strings.Join(args[1:], " "))
130+
}
131+
} else {
132+
// "--" was used: args after it are passthrough
133+
appArgs = args[dashIdx:]
134+
}
99135

100136
runner, err := getStandaloneRunner(cmd.Context())
101137
if err != nil {
@@ -107,6 +143,11 @@ Supported apps: %s`, strings.Join(supportedApps, ", ")),
107143
return err
108144
}
109145

146+
// --config: print configuration without launching
147+
if configOnly {
148+
return printAppConfig(cmd, app, ep)
149+
}
150+
110151
if ca, ok := containerApps[app]; ok {
111152
return launchContainerApp(cmd, ca, ep.container, image, port, detach, appArgs, dryRun)
112153
}
@@ -120,9 +161,66 @@ Supported apps: %s`, strings.Join(supportedApps, ", ")),
120161
c.Flags().StringVar(&image, "image", "", "Override container image for containerized apps")
121162
c.Flags().BoolVar(&detach, "detach", false, "Run containerized app in background")
122163
c.Flags().BoolVar(&dryRun, "dry-run", false, "Print what would be executed without running it")
164+
c.Flags().BoolVar(&configOnly, "config", false, "Print configuration without launching")
123165
return c
124166
}
125167

168+
// listSupportedApps prints all supported apps with their descriptions and install status.
169+
func listSupportedApps(cmd *cobra.Command) error {
170+
cmd.Println("Supported apps:")
171+
cmd.Println()
172+
for _, name := range supportedApps {
173+
desc := appDescriptions[name]
174+
if desc == "" {
175+
desc = name
176+
}
177+
status := ""
178+
if _, ok := hostApps[name]; ok {
179+
if _, err := exec.LookPath(name); err != nil {
180+
status = " (not installed)"
181+
}
182+
}
183+
cmd.Printf(" %-15s %s%s\n", name, desc, status)
184+
}
185+
cmd.Println()
186+
cmd.Println("Usage: docker model launch APP [-- APP_ARGS...]")
187+
return nil
188+
}
189+
190+
// printAppConfig prints the configuration that would be used for the given app.
191+
func printAppConfig(cmd *cobra.Command, app string, ep engineEndpoints) error {
192+
if ca, ok := containerApps[app]; ok {
193+
cmd.Printf("Configuration for %s (container app):\n", app)
194+
cmd.Printf(" Image: %s\n", ca.defaultImage)
195+
cmd.Printf(" Container port: %d\n", ca.containerPort)
196+
cmd.Printf(" Host port: %d\n", ca.defaultHostPort)
197+
if ca.envFn != nil {
198+
cmd.Printf(" Environment:\n")
199+
for _, e := range ca.envFn(ep.container) {
200+
cmd.Printf(" %s\n", e)
201+
}
202+
}
203+
return nil
204+
}
205+
if cli, ok := hostApps[app]; ok {
206+
cmd.Printf("Configuration for %s (host app):\n", app)
207+
if cli.envFn != nil {
208+
cmd.Printf(" Environment:\n")
209+
for _, e := range cli.envFn(ep.host) {
210+
cmd.Printf(" %s\n", e)
211+
}
212+
}
213+
if cli.configInstructions != nil {
214+
cmd.Printf(" Manual configuration:\n")
215+
for _, line := range cli.configInstructions(ep.host) {
216+
cmd.Printf(" %s\n", line)
217+
}
218+
}
219+
return nil
220+
}
221+
return fmt.Errorf("unsupported app %q (supported: %s)", app, strings.Join(supportedApps, ", "))
222+
}
223+
126224
// resolveBaseEndpoints resolves the base URLs (without path) for both
127225
// container and host client locations.
128226
func resolveBaseEndpoints(runner *standaloneRunner) (engineEndpoints, error) {

cmd/cli/commands/launch_test.go

Lines changed: 158 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,13 +345,20 @@ func TestNewLaunchCmdValidArgs(t *testing.T) {
345345
require.Equal(t, supportedApps, cmd.ValidArgs)
346346
}
347347

348-
func TestNewLaunchCmdRequiresAtLeastOneArg(t *testing.T) {
348+
func TestNewLaunchCmdNoArgsListsApps(t *testing.T) {
349+
buf := new(bytes.Buffer)
349350
cmd := newLaunchCmd()
351+
cmd.SetOut(buf)
350352
cmd.SetArgs([]string{})
351353
err := cmd.Execute()
352354

353-
require.Error(t, err)
354-
require.Contains(t, err.Error(), "requires at least 1 arg")
355+
require.NoError(t, err)
356+
output := buf.String()
357+
require.Contains(t, output, "Supported apps:")
358+
for _, app := range supportedApps {
359+
require.Contains(t, output, app)
360+
}
361+
require.Contains(t, output, "Usage: docker model launch APP")
355362
}
356363

357364
func TestNewLaunchCmdDispatchContainerApp(t *testing.T) {
@@ -415,3 +422,151 @@ func TestNewLaunchCmdDispatchUnsupportedApp(t *testing.T) {
415422
require.Error(t, err)
416423
require.Contains(t, err.Error(), "unsupported app")
417424
}
425+
426+
func TestNewLaunchCmdConfigFlag(t *testing.T) {
427+
ctx, err := desktop.NewContextForTest(
428+
"http://localhost"+inference.ExperimentalEndpointsPrefix,
429+
nil,
430+
types.ModelRunnerEngineKindDesktop,
431+
)
432+
require.NoError(t, err)
433+
modelRunner = ctx
434+
435+
buf := new(bytes.Buffer)
436+
cmd := newLaunchCmd()
437+
cmd.SetOut(buf)
438+
cmd.SetArgs([]string{"openwebui", "--config"})
439+
440+
err = cmd.Execute()
441+
require.NoError(t, err)
442+
443+
output := buf.String()
444+
require.Contains(t, output, "Configuration for openwebui")
445+
require.Contains(t, output, "container app")
446+
require.Contains(t, output, "ghcr.io/open-webui/open-webui:latest")
447+
}
448+
449+
func TestNewLaunchCmdConfigFlagHostApp(t *testing.T) {
450+
ctx, err := desktop.NewContextForTest(
451+
"http://localhost"+inference.ExperimentalEndpointsPrefix,
452+
nil,
453+
types.ModelRunnerEngineKindDesktop,
454+
)
455+
require.NoError(t, err)
456+
modelRunner = ctx
457+
458+
buf := new(bytes.Buffer)
459+
cmd := newLaunchCmd()
460+
cmd.SetOut(buf)
461+
cmd.SetArgs([]string{"claude", "--config"})
462+
463+
err = cmd.Execute()
464+
require.NoError(t, err)
465+
466+
output := buf.String()
467+
require.Contains(t, output, "Configuration for claude")
468+
require.Contains(t, output, "host app")
469+
require.Contains(t, output, "ANTHROPIC_BASE_URL")
470+
require.Contains(t, output, "ANTHROPIC_API_KEY")
471+
}
472+
473+
func TestNewLaunchCmdRejectsExtraArgsWithoutDash(t *testing.T) {
474+
ctx, err := desktop.NewContextForTest(
475+
"http://localhost"+inference.ExperimentalEndpointsPrefix,
476+
nil,
477+
types.ModelRunnerEngineKindDesktop,
478+
)
479+
require.NoError(t, err)
480+
modelRunner = ctx
481+
482+
buf := new(bytes.Buffer)
483+
cmd := newLaunchCmd()
484+
cmd.SetOut(buf)
485+
cmd.SetArgs([]string{"opencode", "extra-arg"})
486+
487+
err = cmd.Execute()
488+
require.Error(t, err)
489+
require.Contains(t, err.Error(), "unexpected arguments")
490+
require.Contains(t, err.Error(), "Use '--'")
491+
}
492+
493+
func TestNewLaunchCmdPassthroughArgs(t *testing.T) {
494+
ctx, err := desktop.NewContextForTest(
495+
"http://localhost"+inference.ExperimentalEndpointsPrefix,
496+
nil,
497+
types.ModelRunnerEngineKindDesktop,
498+
)
499+
require.NoError(t, err)
500+
modelRunner = ctx
501+
502+
buf := new(bytes.Buffer)
503+
cmd := newLaunchCmd()
504+
cmd.SetOut(buf)
505+
cmd.SetArgs([]string{"openwebui", "--dry-run", "--", "--extra-flag"})
506+
507+
err = cmd.Execute()
508+
require.NoError(t, err)
509+
510+
output := buf.String()
511+
require.Contains(t, output, "Would run: docker")
512+
require.Contains(t, output, "--extra-flag")
513+
}
514+
515+
func TestAppDescriptionsExistForAllApps(t *testing.T) {
516+
for _, app := range supportedApps {
517+
require.NotEmpty(t, appDescriptions[app], "missing description for app %q", app)
518+
}
519+
}
520+
521+
func TestListSupportedApps(t *testing.T) {
522+
buf := new(bytes.Buffer)
523+
cmd := newTestCmd(buf)
524+
525+
err := listSupportedApps(cmd)
526+
require.NoError(t, err)
527+
528+
output := buf.String()
529+
require.Contains(t, output, "Supported apps:")
530+
require.Contains(t, output, "claude")
531+
require.Contains(t, output, "opencode")
532+
require.Contains(t, output, "openwebui")
533+
}
534+
535+
func TestPrintAppConfigContainerApp(t *testing.T) {
536+
buf := new(bytes.Buffer)
537+
cmd := newTestCmd(buf)
538+
539+
ep := engineEndpoints{container: testBaseURL, host: testBaseURL}
540+
err := printAppConfig(cmd, "openwebui", ep)
541+
require.NoError(t, err)
542+
543+
output := buf.String()
544+
require.Contains(t, output, "Configuration for openwebui")
545+
require.Contains(t, output, "container app")
546+
require.Contains(t, output, "ghcr.io/open-webui/open-webui:latest")
547+
require.Contains(t, output, "OPENAI_API_BASE")
548+
}
549+
550+
func TestPrintAppConfigHostApp(t *testing.T) {
551+
buf := new(bytes.Buffer)
552+
cmd := newTestCmd(buf)
553+
554+
ep := engineEndpoints{container: testBaseURL, host: testBaseURL}
555+
err := printAppConfig(cmd, "claude", ep)
556+
require.NoError(t, err)
557+
558+
output := buf.String()
559+
require.Contains(t, output, "Configuration for claude")
560+
require.Contains(t, output, "host app")
561+
require.Contains(t, output, "ANTHROPIC_BASE_URL")
562+
}
563+
564+
func TestPrintAppConfigUnsupported(t *testing.T) {
565+
buf := new(bytes.Buffer)
566+
cmd := newTestCmd(buf)
567+
568+
ep := engineEndpoints{container: testBaseURL, host: testBaseURL}
569+
err := printAppConfig(cmd, "bogus", ep)
570+
require.Error(t, err)
571+
require.Contains(t, err.Error(), "unsupported app")
572+
}

cmd/cli/docs/reference/docker_model_launch.yaml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,30 @@ short: Launch an app configured to use Docker Model Runner
33
long: |-
44
Launch an app configured to use Docker Model Runner.
55
6+
Without arguments, lists all supported apps.
7+
68
Supported apps: anythingllm, claude, codex, openclaw, opencode, openwebui
7-
usage: docker model launch APP [-- APP_ARGS...]
9+
10+
Examples:
11+
docker model launch
12+
docker model launch opencode
13+
docker model launch claude -- --help
14+
docker model launch openwebui --port 3000
15+
docker model launch claude --config
16+
usage: docker model launch [APP] [-- APP_ARGS...]
817
pname: docker model
918
plink: docker_model.yaml
1019
options:
20+
- option: config
21+
value_type: bool
22+
default_value: "false"
23+
description: Print configuration without launching
24+
deprecated: false
25+
hidden: false
26+
experimental: false
27+
experimentalcli: false
28+
kubernetes: false
29+
swarm: false
1130
- option: detach
1231
value_type: bool
1332
default_value: "false"

cmd/cli/docs/reference/model_launch.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,22 @@
33
<!---MARKER_GEN_START-->
44
Launch an app configured to use Docker Model Runner.
55

6+
Without arguments, lists all supported apps.
7+
68
Supported apps: anythingllm, claude, codex, openclaw, opencode, openwebui
79

10+
Examples:
11+
docker model launch
12+
docker model launch opencode
13+
docker model launch claude -- --help
14+
docker model launch openwebui --port 3000
15+
docker model launch claude --config
16+
817
### Options
918

1019
| Name | Type | Default | Description |
1120
|:------------|:---------|:--------|:------------------------------------------------|
21+
| `--config` | `bool` | | Print configuration without launching |
1222
| `--detach` | `bool` | | Run containerized app in background |
1323
| `--dry-run` | `bool` | | Print what would be executed without running it |
1424
| `--image` | `string` | | Override container image for containerized apps |

0 commit comments

Comments
 (0)