← back to Cli Printing Press
fix(cli): emit structured JSON error from parents invoked without a subcommand in --agent mode (#1373)
fb9125e86965b875d878c2b8460087b8656a896b · 2026-05-14 01:06:03 -0700 · Trevin Chow
* fix(cli): emit structured JSON error from parents invoked without a subcommand in --agent / --json mode
Generated parent commands (resource parents and the framework auth /
profile / jobs / workflow / share groupings) had no RunE wired, so
cobra fell through to its default help renderer. In --agent / --json
mode that put human-readable help on stdout and exited 0 — agents
piping the output through `jq` got a parse error with no signal that
the invocation was incomplete.
Adds a shared parentNoSubcommandRunE helper in helpers.go.tmpl that
emits {"error":"subcommand required","valid_subcommands":[...]} on
stdout and returns usageErr (exit 2) when flags.asJSON is set, falling
back to cmd.Help() for human mode. Wired into command_parent.go.tmpl
and the framework parents that shared the same shape.
Human-mode behavior is unchanged: help still renders, exit 0.
Closes #1138
* test(cli): extend parent-RunE wiring test to cover workflow and share parents
Files touched
M internal/generator/generator_test.goM internal/generator/templates/auth.go.tmplM internal/generator/templates/auth_browser.go.tmplM internal/generator/templates/auth_client_credentials.go.tmplM internal/generator/templates/auth_simple.go.tmplM internal/generator/templates/channel_workflow.go.tmplM internal/generator/templates/command_parent.go.tmplM internal/generator/templates/helpers.go.tmplM internal/generator/templates/jobs.go.tmplM internal/generator/templates/profile.go.tmplM internal/generator/templates/share_commands.go.tmplM testdata/golden/expected/generate-embedded-paged-api/embedded-paged-api/internal/cli/helpers.goM testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/cli/auth.goM testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/auth.goM testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.goM testdata/golden/expected/generate-golden-api/dogfood.jsonM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/auth.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/profile.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_avatar.go
Diff
commit fb9125e86965b875d878c2b8460087b8656a896b
Author: Trevin Chow <trevin@trevinchow.com>
Date: Thu May 14 01:06:03 2026 -0700
fix(cli): emit structured JSON error from parents invoked without a subcommand in --agent mode (#1373)
* fix(cli): emit structured JSON error from parents invoked without a subcommand in --agent / --json mode
Generated parent commands (resource parents and the framework auth /
profile / jobs / workflow / share groupings) had no RunE wired, so
cobra fell through to its default help renderer. In --agent / --json
mode that put human-readable help on stdout and exited 0 — agents
piping the output through `jq` got a parse error with no signal that
the invocation was incomplete.
Adds a shared parentNoSubcommandRunE helper in helpers.go.tmpl that
emits {"error":"subcommand required","valid_subcommands":[...]} on
stdout and returns usageErr (exit 2) when flags.asJSON is set, falling
back to cmd.Help() for human mode. Wired into command_parent.go.tmpl
and the framework parents that shared the same shape.
Human-mode behavior is unchanged: help still renders, exit 0.
Closes #1138
* test(cli): extend parent-RunE wiring test to cover workflow and share parents
---
internal/generator/generator_test.go | 63 ++++++++++++++++++++++
internal/generator/templates/auth.go.tmpl | 1 +
internal/generator/templates/auth_browser.go.tmpl | 1 +
.../templates/auth_client_credentials.go.tmpl | 1 +
internal/generator/templates/auth_simple.go.tmpl | 1 +
.../generator/templates/channel_workflow.go.tmpl | 1 +
.../generator/templates/command_parent.go.tmpl | 1 +
internal/generator/templates/helpers.go.tmpl | 26 +++++++++
internal/generator/templates/jobs.go.tmpl | 1 +
internal/generator/templates/profile.go.tmpl | 1 +
.../generator/templates/share_commands.go.tmpl | 1 +
.../embedded-paged-api/internal/cli/helpers.go | 26 +++++++++
.../printing-press-oauth2-cc/internal/cli/auth.go | 1 +
.../printing-press-rich-auth/internal/cli/auth.go | 1 +
.../internal/cli/helpers.go | 26 +++++++++
.../expected/generate-golden-api/dogfood.json | 2 +-
.../printing-press-golden/internal/cli/auth.go | 1 +
.../printing-press-golden/internal/cli/helpers.go | 26 +++++++++
.../printing-press-golden/internal/cli/profile.go | 1 +
.../internal/cli/projects_avatar.go | 1 +
20 files changed, 182 insertions(+), 1 deletion(-)
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index afb3263d..ef4de667 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -9898,3 +9898,66 @@ func TestGenerateEndpointTemplateEnvOverridesWireThrough(t *testing.T) {
assert.Contains(t, string(readme), "ST_TENANT_ID",
"README must surface the override env var name in the runtime endpoint instructions")
}
+
+// TestGenerateParentNoSubcommandRunE_WiredOnResourceParents: every generated
+// resource parent command must wire the parentNoSubcommandRunE helper so
+// invocations without a subcommand in --agent / --json mode get a structured
+// JSON error instead of human-readable cobra help on stdout. Before the fix,
+// agents driving the CLI silently received help text and exit 0. The helper
+// also has to be defined exactly once in helpers.go so the generated package
+// compiles. Multi-endpoint resources are exercised so a parent is actually
+// emitted (single-endpoint resources get promoted to a direct subcommand).
+func TestGenerateParentNoSubcommandRunE_WiredOnResourceParents(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("parent-runE-wired")
+ apiSpec.Resources = map[string]spec.Resource{
+ "items": {
+ Description: "Manage items",
+ Endpoints: map[string]spec.Endpoint{
+ "list": {Method: "GET", Path: "/items", Description: "List items"},
+ "get": {Method: "GET", Path: "/items/{id}", Description: "Get one item"},
+ },
+ },
+ }
+ apiSpec.Share = spec.ShareConfig{Enabled: true, SnapshotTables: []string{"items"}}
+
+ outputDir := filepath.Join(t.TempDir(), naming.CLI(apiSpec.Name))
+ gen := New(apiSpec, outputDir)
+ gen.VisionSet = VisionTemplateSet{Store: true, Sync: true}
+ require.NoError(t, gen.Generate())
+
+ helpersSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "helpers.go"))
+ require.NoError(t, err)
+ assert.Contains(t, string(helpersSrc), "func parentNoSubcommandRunE(flags *rootFlags)",
+ "helpers.go must define parentNoSubcommandRunE so generated parents can wire it")
+ assert.Contains(t, string(helpersSrc), `"subcommand required"`,
+ "the helper must emit a structured JSON error envelope")
+ assert.Contains(t, string(helpersSrc), `"valid_subcommands"`,
+ "the JSON envelope must list valid subcommands so agents can self-correct")
+
+ parentSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "items.go"))
+ require.NoError(t, err)
+ assert.Regexp(t, `RunE:\s+parentNoSubcommandRunE\(flags\)`, string(parentSrc),
+ "the resource parent must call the shared helper instead of falling through to cobra's default help")
+
+ authSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "auth.go"))
+ require.NoError(t, err)
+ assert.Regexp(t, `RunE:\s+parentNoSubcommandRunE\(flags\)`, string(authSrc),
+ "the auth parent shares the same bug class and must wire the helper too")
+
+ profileSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "profile.go"))
+ require.NoError(t, err)
+ assert.Regexp(t, `RunE:\s+parentNoSubcommandRunE\(flags\)`, string(profileSrc),
+ "the profile parent shares the same bug class and must wire the helper too")
+
+ workflowSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "channel_workflow.go"))
+ require.NoError(t, err)
+ assert.Regexp(t, `RunE:\s+parentNoSubcommandRunE\(flags\)`, string(workflowSrc),
+ "the workflow parent shares the same bug class and must wire the helper too")
+
+ shareSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "share_commands.go"))
+ require.NoError(t, err)
+ assert.Regexp(t, `RunE:\s+parentNoSubcommandRunE\(flags\)`, string(shareSrc),
+ "the share parent shares the same bug class and must wire the helper too")
+}
diff --git a/internal/generator/templates/auth.go.tmpl b/internal/generator/templates/auth.go.tmpl
index 1565bfa1..1b05b7a2 100644
--- a/internal/generator/templates/auth.go.tmpl
+++ b/internal/generator/templates/auth.go.tmpl
@@ -29,6 +29,7 @@ func newAuthCmd(flags *rootFlags) *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: {{printf "%q" (authCommandShort .APISpec)}},
+ RunE: parentNoSubcommandRunE(flags),
}
cmd.AddCommand(newAuthSetupCmd(flags))
diff --git a/internal/generator/templates/auth_browser.go.tmpl b/internal/generator/templates/auth_browser.go.tmpl
index a23b4f42..de2dda92 100644
--- a/internal/generator/templates/auth_browser.go.tmpl
+++ b/internal/generator/templates/auth_browser.go.tmpl
@@ -51,6 +51,7 @@ func newAuthCmd(flags *rootFlags) *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: {{printf "%q" (authCommandShort .APISpec)}},
+ RunE: parentNoSubcommandRunE(flags),
}
cmd.AddCommand(newAuthLoginCmd(flags))
diff --git a/internal/generator/templates/auth_client_credentials.go.tmpl b/internal/generator/templates/auth_client_credentials.go.tmpl
index 1cfd7d01..14415de4 100644
--- a/internal/generator/templates/auth_client_credentials.go.tmpl
+++ b/internal/generator/templates/auth_client_credentials.go.tmpl
@@ -26,6 +26,7 @@ func newAuthCmd(flags *rootFlags) *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: {{printf "%q" (authCommandShort .APISpec)}},
+ RunE: parentNoSubcommandRunE(flags),
}
cmd.AddCommand(newAuthLoginCmd(flags))
diff --git a/internal/generator/templates/auth_simple.go.tmpl b/internal/generator/templates/auth_simple.go.tmpl
index d6e0bc92..2370ee9c 100644
--- a/internal/generator/templates/auth_simple.go.tmpl
+++ b/internal/generator/templates/auth_simple.go.tmpl
@@ -24,6 +24,7 @@ func newAuthCmd(flags *rootFlags) *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: {{printf "%q" (authCommandShort .APISpec)}},
+ RunE: parentNoSubcommandRunE(flags),
}
cmd.AddCommand(newAuthSetupCmd(flags))
diff --git a/internal/generator/templates/channel_workflow.go.tmpl b/internal/generator/templates/channel_workflow.go.tmpl
index 1c262a3a..27640132 100644
--- a/internal/generator/templates/channel_workflow.go.tmpl
+++ b/internal/generator/templates/channel_workflow.go.tmpl
@@ -25,6 +25,7 @@ func newWorkflowCmd(flags *rootFlags) *cobra.Command {
cmd := &cobra.Command{
Use: "workflow",
Short: "Compound workflows that combine multiple API operations",
+ RunE: parentNoSubcommandRunE(flags),
}
{{- if .SyncEnabled}}
diff --git a/internal/generator/templates/command_parent.go.tmpl b/internal/generator/templates/command_parent.go.tmpl
index 198b884f..76d8dff8 100644
--- a/internal/generator/templates/command_parent.go.tmpl
+++ b/internal/generator/templates/command_parent.go.tmpl
@@ -14,6 +14,7 @@ func new{{camel .FuncPrefix}}Cmd(flags *rootFlags) *cobra.Command {
{{- if .Hidden}}
Hidden: true,
{{- end}}
+ RunE: parentNoSubcommandRunE(flags),
}
{{range $eName, $endpoint := .Resource.Endpoints}}
cmd.AddCommand(new{{camel $.FuncPrefix}}{{camel $eName}}Cmd(flags))
diff --git a/internal/generator/templates/helpers.go.tmpl b/internal/generator/templates/helpers.go.tmpl
index 7d58d0d8..65074cf5 100644
--- a/internal/generator/templates/helpers.go.tmpl
+++ b/internal/generator/templates/helpers.go.tmpl
@@ -140,6 +140,32 @@ func dryRunOK(flags *rootFlags) bool {
return flags != nil && flags.dryRun
}
+// parentNoSubcommandRunE returns a RunE that handles parents invoked without a
+// subcommand. In machine output (--json/--agent) the parent emits a structured
+// error to stdout listing valid subcommands and exits 2; otherwise cobra's
+// default help text is printed. Without this, agents driving the CLI in
+// --agent mode received only human-readable help on stdout and exit 0, with no
+// signal that the invocation was incomplete.
+func parentNoSubcommandRunE(flags *rootFlags) func(*cobra.Command, []string) error {
+ return func(cmd *cobra.Command, args []string) error {
+ if flags != nil && flags.asJSON {
+ subs := make([]string, 0, len(cmd.Commands()))
+ for _, c := range cmd.Commands() {
+ if c.IsAvailableCommand() && c.Name() != "help" {
+ subs = append(subs, c.Name())
+ }
+ }
+ sort.Strings(subs)
+ _ = json.NewEncoder(cmd.OutOrStdout()).Encode(map[string]any{
+ "error": "subcommand required",
+ "valid_subcommands": subs,
+ })
+ return usageErr(fmt.Errorf("subcommand required for %q", cmd.CommandPath()))
+ }
+ return cmd.Help()
+ }
+}
+
{{- if .HasDataLayer}}
// accessWarning describes an API access-denial that sync converts into a
diff --git a/internal/generator/templates/jobs.go.tmpl b/internal/generator/templates/jobs.go.tmpl
index 9a95944b..de80b4d0 100644
--- a/internal/generator/templates/jobs.go.tmpl
+++ b/internal/generator/templates/jobs.go.tmpl
@@ -217,6 +217,7 @@ func newJobsCmd(flags *rootFlags) *cobra.Command {
Submit an async endpoint with --wait to block until completion; submit
without --wait to get the job ID back immediately and track it later.`,
+ RunE: parentNoSubcommandRunE(flags),
}
cmd.AddCommand(newJobsListCmd(flags))
cmd.AddCommand(newJobsGetCmd(flags))
diff --git a/internal/generator/templates/profile.go.tmpl b/internal/generator/templates/profile.go.tmpl
index 1296e472..513f1d49 100644
--- a/internal/generator/templates/profile.go.tmpl
+++ b/internal/generator/templates/profile.go.tmpl
@@ -153,6 +153,7 @@ agent can invoke the same command with the same configuration each run.
Use --profile <name> on any command to apply that profile's values.
Explicit flags override profile values.`,
+ RunE: parentNoSubcommandRunE(flags),
}
cmd.AddCommand(newProfileSaveCmd(flags))
cmd.AddCommand(newProfileUseCmd(flags))
diff --git a/internal/generator/templates/share_commands.go.tmpl b/internal/generator/templates/share_commands.go.tmpl
index 378a7274..ea3e12e1 100644
--- a/internal/generator/templates/share_commands.go.tmpl
+++ b/internal/generator/templates/share_commands.go.tmpl
@@ -27,6 +27,7 @@ One teammate with API credentials runs 'share publish' on a cadence; the
rest run 'share subscribe <remote>' once, then read from the cache
without needing credentials. Auto-refresh picks up new snapshots on the
next read command.`,
+ RunE: parentNoSubcommandRunE(flags),
}
cmd.AddCommand(newShareExportCmd(flags))
cmd.AddCommand(newShareImportCmd(flags))
diff --git a/testdata/golden/expected/generate-embedded-paged-api/embedded-paged-api/internal/cli/helpers.go b/testdata/golden/expected/generate-embedded-paged-api/embedded-paged-api/internal/cli/helpers.go
index 146efdb7..73b178a8 100644
--- a/testdata/golden/expected/generate-embedded-paged-api/embedded-paged-api/internal/cli/helpers.go
+++ b/testdata/golden/expected/generate-embedded-paged-api/embedded-paged-api/internal/cli/helpers.go
@@ -124,6 +124,32 @@ func dryRunOK(flags *rootFlags) bool {
return flags != nil && flags.dryRun
}
+// parentNoSubcommandRunE returns a RunE that handles parents invoked without a
+// subcommand. In machine output (--json/--agent) the parent emits a structured
+// error to stdout listing valid subcommands and exits 2; otherwise cobra's
+// default help text is printed. Without this, agents driving the CLI in
+// --agent mode received only human-readable help on stdout and exit 0, with no
+// signal that the invocation was incomplete.
+func parentNoSubcommandRunE(flags *rootFlags) func(*cobra.Command, []string) error {
+ return func(cmd *cobra.Command, args []string) error {
+ if flags != nil && flags.asJSON {
+ subs := make([]string, 0, len(cmd.Commands()))
+ for _, c := range cmd.Commands() {
+ if c.IsAvailableCommand() && c.Name() != "help" {
+ subs = append(subs, c.Name())
+ }
+ }
+ sort.Strings(subs)
+ _ = json.NewEncoder(cmd.OutOrStdout()).Encode(map[string]any{
+ "error": "subcommand required",
+ "valid_subcommands": subs,
+ })
+ return usageErr(fmt.Errorf("subcommand required for %q", cmd.CommandPath()))
+ }
+ return cmd.Help()
+ }
+}
+
// accessWarning describes an API access-denial that sync converts into a
// non-fatal warning. It carries enough structured data for the sync_warning
// JSON event without parsing free-form error strings downstream.
diff --git a/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/cli/auth.go b/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/cli/auth.go
index 84e90080..787745a0 100644
--- a/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/cli/auth.go
+++ b/testdata/golden/expected/generate-golden-api-oauth2-cc/printing-press-oauth2-cc/internal/cli/auth.go
@@ -26,6 +26,7 @@ func newAuthCmd(flags *rootFlags) *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: "Manage authentication for Printing Press Oauth2",
+ RunE: parentNoSubcommandRunE(flags),
}
cmd.AddCommand(newAuthLoginCmd(flags))
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/auth.go b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/auth.go
index 08fb4068..a9f3791f 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/auth.go
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/auth.go
@@ -14,6 +14,7 @@ func newAuthCmd(flags *rootFlags) *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: "Manage authentication for Printing Press Rich",
+ RunE: parentNoSubcommandRunE(flags),
}
cmd.AddCommand(newAuthSetupCmd(flags))
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.go b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.go
index f6db72c0..33ef4ad3 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.go
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.go
@@ -123,6 +123,32 @@ func dryRunOK(flags *rootFlags) bool {
return flags != nil && flags.dryRun
}
+// parentNoSubcommandRunE returns a RunE that handles parents invoked without a
+// subcommand. In machine output (--json/--agent) the parent emits a structured
+// error to stdout listing valid subcommands and exits 2; otherwise cobra's
+// default help text is printed. Without this, agents driving the CLI in
+// --agent mode received only human-readable help on stdout and exit 0, with no
+// signal that the invocation was incomplete.
+func parentNoSubcommandRunE(flags *rootFlags) func(*cobra.Command, []string) error {
+ return func(cmd *cobra.Command, args []string) error {
+ if flags != nil && flags.asJSON {
+ subs := make([]string, 0, len(cmd.Commands()))
+ for _, c := range cmd.Commands() {
+ if c.IsAvailableCommand() && c.Name() != "help" {
+ subs = append(subs, c.Name())
+ }
+ }
+ sort.Strings(subs)
+ _ = json.NewEncoder(cmd.OutOrStdout()).Encode(map[string]any{
+ "error": "subcommand required",
+ "valid_subcommands": subs,
+ })
+ return usageErr(fmt.Errorf("subcommand required for %q", cmd.CommandPath()))
+ }
+ return cmd.Help()
+ }
+}
+
// accessWarning describes an API access-denial that sync converts into a
// non-fatal warning. It carries enough structured data for the sync_warning
// JSON event without parsing free-form error strings downstream.
diff --git a/testdata/golden/expected/generate-golden-api/dogfood.json b/testdata/golden/expected/generate-golden-api/dogfood.json
index f755805e..697673ba 100644
--- a/testdata/golden/expected/generate-golden-api/dogfood.json
+++ b/testdata/golden/expected/generate-golden-api/dogfood.json
@@ -16,7 +16,7 @@
},
"dead_functions": {
"dead": 0,
- "total": 58
+ "total": 59
},
"dir": "<ARTIFACT_DIR>/generate-golden-api/printing-press-golden",
"example_check": {
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/auth.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/auth.go
index a9c90486..d6dfc242 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/auth.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/auth.go
@@ -14,6 +14,7 @@ func newAuthCmd(flags *rootFlags) *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: "Manage authentication for Printing Press Studio",
+ RunE: parentNoSubcommandRunE(flags),
}
cmd.AddCommand(newAuthSetupCmd(flags))
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
index feba1e1e..3dcd6f7b 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
@@ -124,6 +124,32 @@ func dryRunOK(flags *rootFlags) bool {
return flags != nil && flags.dryRun
}
+// parentNoSubcommandRunE returns a RunE that handles parents invoked without a
+// subcommand. In machine output (--json/--agent) the parent emits a structured
+// error to stdout listing valid subcommands and exits 2; otherwise cobra's
+// default help text is printed. Without this, agents driving the CLI in
+// --agent mode received only human-readable help on stdout and exit 0, with no
+// signal that the invocation was incomplete.
+func parentNoSubcommandRunE(flags *rootFlags) func(*cobra.Command, []string) error {
+ return func(cmd *cobra.Command, args []string) error {
+ if flags != nil && flags.asJSON {
+ subs := make([]string, 0, len(cmd.Commands()))
+ for _, c := range cmd.Commands() {
+ if c.IsAvailableCommand() && c.Name() != "help" {
+ subs = append(subs, c.Name())
+ }
+ }
+ sort.Strings(subs)
+ _ = json.NewEncoder(cmd.OutOrStdout()).Encode(map[string]any{
+ "error": "subcommand required",
+ "valid_subcommands": subs,
+ })
+ return usageErr(fmt.Errorf("subcommand required for %q", cmd.CommandPath()))
+ }
+ return cmd.Help()
+ }
+}
+
// accessWarning describes an API access-denial that sync converts into a
// non-fatal warning. It carries enough structured data for the sync_warning
// JSON event without parsing free-form error strings downstream.
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/profile.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/profile.go
index f3b60421..66d695b4 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/profile.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/profile.go
@@ -153,6 +153,7 @@ agent can invoke the same command with the same configuration each run.
Use --profile <name> on any command to apply that profile's values.
Explicit flags override profile values.`,
+ RunE: parentNoSubcommandRunE(flags),
}
cmd.AddCommand(newProfileSaveCmd(flags))
cmd.AddCommand(newProfileUseCmd(flags))
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_avatar.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_avatar.go
index bb6503f1..01f7bd3f 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_avatar.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_avatar.go
@@ -11,6 +11,7 @@ func newProjectsAvatarCmd(flags *rootFlags) *cobra.Command {
cmd := &cobra.Command{
Use: "avatar",
Short: "Manage avatar",
+ RunE: parentNoSubcommandRunE(flags),
}
cmd.AddCommand(newProjectsAvatarUploadProjectCmd(flags))
← d44587e9 test(cli): regression tests for body-flag identifier collisi
·
back to Cli Printing Press
·
fix(cli): drop shell line comments + skip happy_path on miss 295fd03f →