diff --git a/.changesets/1790084751-8116f03c.yaml b/.changesets/1790084751-8116f03c.yaml new file mode 100644 index 00000000..d896c541 --- /dev/null +++ b/.changesets/1790084751-8116f03c.yaml @@ -0,0 +1,10 @@ +id: 1790084751-8116f03c +features: + - core +targets: + - cli +type: fix +bump: patch +description: planned commands report CLI_UNAVAILABLE for arbitrary positional arguments and unknown flags +author: 2ynn +date: "2026-09-22" diff --git a/templates/templates/cli/intents.go.stmpl b/templates/templates/cli/intents.go.stmpl index 2f56a53b..99fd58e0 100644 --- a/templates/templates/cli/intents.go.stmpl +++ b/templates/templates/cli/intents.go.stmpl @@ -114,9 +114,10 @@ func initIntentCmds(rootCmd *cobra.Command) error { func newPlannedCmd(name, short, long, groupID, note string) *cobra.Command { cmd := &cobra.Command{ - Use: name, - Short: short, - Args: cobra.NoArgs, + Use: name, + Short: short, + Args: cobra.ArbitraryArgs, + FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true}, RunE: func(cmd *cobra.Command, args []string) error { if usage.UsageRequested(cmd) { return usage.EmitSchema(cmd, cmd.OutOrStdout()) diff --git a/templates/templates/cli/tests/primary/clierrors_test.go.stmpl b/templates/templates/cli/tests/primary/clierrors_test.go.stmpl index 7a8b9bb7..6c691e27 100644 --- a/templates/templates/cli/tests/primary/clierrors_test.go.stmpl +++ b/templates/templates/cli/tests/primary/clierrors_test.go.stmpl @@ -64,6 +64,32 @@ func TestCLIErrorAgentModeMatrix(t *testing.T) { } } +func TestCLIErrorPlannedCommandIgnoresArgv(t *testing.T) { + for _, args := range [][]string{ + {"--agent-mode", "archive"}, + {"--agent-mode", "archive", "hello world"}, + {"--agent-mode", "archive", "--count", "3"}, + {"--agent-mode", "archive", "hello", "--count", "3"}, + } { + t.Run(strings.Join(args[1:], " "), func(t *testing.T) { + resetAgentMode(t) + h := NewCLITestHarness(t) + err := h.RunRaw(args) + require.Error(t, err) + assert.True(t, output.IsRendered(err), "a CLI error rendered in the envelope must be marked so main prints nothing further") + + envelope := agentEnvelope(t, h.GetStderr()) + assert.Equal(t, "unsupported_error", envelope["error_type"]) + assert.Equal(t, "CLI_UNAVAILABLE", envelope["error_reason"]) + assert.Equal(t, float64(1), envelope["exit_code"]) + assert.NotContains(t, envelope["error"], "unknown command") + assert.NotContains(t, envelope["error"], "unknown flag") + assert.NotContains(t, h.GetStderr(), "--dry-run") + assert.Empty(t, strings.TrimSpace(h.GetStdout())) + }) + } +} + // TestCLIErrorFlagValuesAreNotRenderingFlags: a flag-looking VALUE of a real // flag must not switch the rendering mode: `--body --agent-mode` is a body of // "--agent-mode" (invalid JSON, plain error), not agent mode; `--body -o json` diff --git a/templates/templates/cli/tests/primary/exitcodes_test.go.stmpl b/templates/templates/cli/tests/primary/exitcodes_test.go.stmpl index 238732cd..11c17b46 100644 --- a/templates/templates/cli/tests/primary/exitcodes_test.go.stmpl +++ b/templates/templates/cli/tests/primary/exitcodes_test.go.stmpl @@ -166,6 +166,8 @@ func TestExitCodesAcrossRenderingModes(t *testing.T) { {name: "missing required flag", args: []string{"custom-required-flags"}, want: 2}, {name: "malformed body", args: []string{"request-bodies", "request-body-post-flag-defaults-body-merge", "--body", "{"}, want: 2}, {name: "planned command", args: []string{"archive"}, want: 1}, + {name: "planned command with argument", args: []string{"archive", "hello world"}, want: 1}, + {name: "planned command with unknown flag", args: []string{"archive", "--count", "3"}, want: 1}, {name: "builtin stray argument", args: []string{"version", "extra"}, want: 2}, {name: "HTTP 400", args: []string{"errors", "status-get-error", "--status-code", "400"}, serverURL: s.URL, want: 2}, {name: "HTTP 401", args: []string{"errors", "status-get-error", "--status-code", "401"}, serverURL: s.URL, want: 3}, diff --git a/templates/templates/cli/usage_test.go.stmpl b/templates/templates/cli/usage_test.go.stmpl index 76edfffd..4acaef90 100644 --- a/templates/templates/cli/usage_test.go.stmpl +++ b/templates/templates/cli/usage_test.go.stmpl @@ -9,6 +9,9 @@ {{- addImport "github.com/spf13/pflag" -}} {{- addImport "internal/cli" true -}} {{- end -}} +{{- if templatePlannedUsageTestEnabled -}} + {{- addImport "internal/clierrors" true -}} +{{- end -}} package tests {{genImports}} @@ -311,6 +314,23 @@ func TestUsageSchema_PlannedCommand(t *testing.T) { assert.Contains(t, stdout, "cmd \"{{templatePlannedUsageCmdName}}\"") assert.Empty(t, stderr) } + +func TestPlannedCommand_ArgvReportsUnavailable(t *testing.T) { + for _, extra := range [][]string{ + {"hello"}, + {"--count", "3"}, + } { + t.Run(strings.Join(extra, " "), func(t *testing.T) { + h := NewCLITestHarness(t) + err := h.RunBare(append([]string{ {{templatePlannedUsagePathArgs}} }, extra...)) + require.Error(t, err) + + assert.Equal(t, clierrors.ExitRuntime, clierrors.ExitCode(err)) + assert.Contains(t, h.GetStderr(), `"error_reason": "CLI_UNAVAILABLE"`) + assert.Empty(t, strings.TrimSpace(h.GetStdout())) + }) + } +} {{- end }} {{- if $custom }} diff --git a/zSDKs/sdk-cli/.speakeasy/gen.lock b/zSDKs/sdk-cli/.speakeasy/gen.lock index 4b50cc13..f8c1205f 100644 --- a/zSDKs/sdk-cli/.speakeasy/gen.lock +++ b/zSDKs/sdk-cli/.speakeasy/gen.lock @@ -126,7 +126,7 @@ trackedFiles: internal/cli/intent_say.go: last_write_checksum: sha1:d7137e29ad2375390c175b9e79f0a557341b2e95 internal/cli/intents.go: - last_write_checksum: sha1:11c1961232079be8b4d3b5e5498c7e61c2810c03 + last_write_checksum: sha1:997b951af6d215576d30574a54022937beedd890 internal/cli/intents_order_test.go: last_write_checksum: sha1:16b6e0b6b7a669f7d12e25d02079fcb6c1bdb878 internal/cli/login.go: @@ -1086,7 +1086,7 @@ trackedFiles: tests/types_test.go: last_write_checksum: sha1:5ce31f08a3214779ed561d0f37279eb4d5202723 tests/usage_test.go: - last_write_checksum: sha1:318b3a28489fe849a72321a9bab87713b12112f2 + last_write_checksum: sha1:7968373b2942b3a209b6f001753ef565e79dad79 examples: _operation_with_leading_and_trailing_underscores_: speakeasy-default-operation-with-leading-and-trailing-underscores: diff --git a/zSDKs/sdk-cli/internal/cli/intents.go b/zSDKs/sdk-cli/internal/cli/intents.go index 762636f7..0d96eec5 100644 --- a/zSDKs/sdk-cli/internal/cli/intents.go +++ b/zSDKs/sdk-cli/internal/cli/intents.go @@ -84,9 +84,10 @@ func initIntentCmds(rootCmd *cobra.Command) error { func newPlannedCmd(name, short, long, groupID, note string) *cobra.Command { cmd := &cobra.Command{ - Use: name, - Short: short, - Args: cobra.NoArgs, + Use: name, + Short: short, + Args: cobra.ArbitraryArgs, + FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true}, RunE: func(cmd *cobra.Command, args []string) error { if usage.UsageRequested(cmd) { return usage.EmitSchema(cmd, cmd.OutOrStdout()) diff --git a/zSDKs/sdk-cli/tests/usage_test.go b/zSDKs/sdk-cli/tests/usage_test.go index ec55673c..6c7b8ea4 100644 --- a/zSDKs/sdk-cli/tests/usage_test.go +++ b/zSDKs/sdk-cli/tests/usage_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "openapi/internal/cli" + "openapi/internal/clierrors" "regexp" "strings" "testing" @@ -282,6 +283,23 @@ func TestUsageSchema_PlannedCommand(t *testing.T) { assert.Empty(t, stderr) } +func TestPlannedCommand_ArgvReportsUnavailable(t *testing.T) { + for _, extra := range [][]string{ + {"hello"}, + {"--count", "3"}, + } { + t.Run(strings.Join(extra, " "), func(t *testing.T) { + h := NewCLITestHarness(t) + err := h.RunBare(append([]string{"archive"}, extra...)) + require.Error(t, err) + + assert.Equal(t, clierrors.ExitRuntime, clierrors.ExitCode(err)) + assert.Contains(t, h.GetStderr(), `"error_reason": "CLI_UNAVAILABLE"`) + assert.Empty(t, strings.TrimSpace(h.GetStdout())) + }) + } +} + func TestUsageSchema_CustomCommand(t *testing.T) { h := NewCLITestHarness(t) err := h.RunBare([]string{"hello", "--usage"})