diff --git a/.changesets/1791385567-923326e3.yaml b/.changesets/1791385567-923326e3.yaml new file mode 100644 index 000000000..4c2888822 --- /dev/null +++ b/.changesets/1791385567-923326e3.yaml @@ -0,0 +1,10 @@ +id: 1791385567-923326e3 +features: + - cliCommands +targets: + - cli +type: fix +bump: patch +description: resume async intents with their polling and artifact output and preserve raw JSON on stream-capable operations +author: ThomasRooney +date: "2026-10-07" diff --git a/internal/extensions/cli_commands.go b/internal/extensions/cli_commands.go index 7c7908ba0..24ec9a9f3 100644 --- a/internal/extensions/cli_commands.go +++ b/internal/extensions/cli_commands.go @@ -424,6 +424,7 @@ var cliReservedFlagNames = map[string]bool{ // adding an async recipe cannot turn a previously valid flag into a // generated pflag collision. "async": true, + "resume": true, "poll-interval": true, "poll-timeout": true, } diff --git a/internal/extensions/cli_commands_test.go b/internal/extensions/cli_commands_test.go index 9f95cb7a1..ad359379e 100644 --- a/internal/extensions/cli_commands_test.go +++ b/internal/extensions/cli_commands_test.go @@ -2759,7 +2759,7 @@ commands: out: {to: $.engine} `, `flag name "out" is reserved`) - for _, name := range []string{"async", "poll-interval", "poll-timeout"} { + for _, name := range []string{"async", "resume", "poll-interval", "poll-timeout"} { t.Run("reserved async flag "+name, func(t *testing.T) { requireDecodeError(t, fmt.Sprintf(` version: 1 diff --git a/templates/templates/cli/README.md b/templates/templates/cli/README.md index 02c9cc68a..9e6aad6ac 100644 --- a/templates/templates/cli/README.md +++ b/templates/templates/cli/README.md @@ -1046,6 +1046,8 @@ Operation-declared names are checked against generated parameter/body/security f The projection annotation is attached only to the operation command. SSE-overload operations need no decode-time branch: `StreamResult` projects a real stream and already falls back to normal `Result` output when `--stream=false` produces JSON (including completed-interaction replay APIs). Generated long help explains raw projection, `--stream=false`, and `-o json`; static KDL and live `--usage` include operation-declared flags, and Cobra-generated docs inherit the same flags/help. +Explicit JSON/jq output prepares raw response handling before calling stream-capable operations, so a JSON response can be printed even when its known fields no longer match the generated schema. Actual SSE and JSONL responses still use their streaming decoders. + Ordinary typed `Result` extraction never treats a value with `Next` and `Value` methods as renderable response content; stream wrappers belong exclusively to `StreamResult`. This also keeps a streaming dry-run silent in implicit pretty mode when the dry-run client creates an empty synthetic stream. Explicit JSON dry-runs print the `{"dry_run":true,"request":{...}}` preview object like every other operation. #### Artifact intent output (`output.artifact`) @@ -1073,6 +1075,29 @@ Artifact intents register `--out` and `--raw-response`. When a missing required The returned MIME type controls a file destination's extension. A matching extension stays unchanged; a missing extension is appended silently; a conflicting extension is rewritten and stderr receives one line such as `Note: wrote image/png as photo.png (requested photo.jpg)`. Like the human-only `Wrote image to ...` status, the note is suppressed in machine modes (agent mode, `--jq`, non-pretty/table formats): the envelope already carries the final path and success keeps stderr silent there. `--out` is output routing only and never participates in request-body source/merge rules. +### Resuming async intents + +`intentcmd.go.stmpl` registers `--resume ` for commands with an async recipe. +It polls the existing operation using the recipe's parameters, states and timing, +then uses the same output and artifact handling as a normal create-and-wait call. +It never creates another operation. Create arguments, request flags and `--async` +are incompatible with resume; body files and stdin are not read, and create-input +forms are not opened. Authentication, server selection, output flags, +`--poll-interval` and `--poll-timeout` remain available. + +`auxiliary/internal/output/async.go.stmpl` shares the polling loop between creation +and resume. `--dry-run --resume ` previews one poll request without network +calls or artifact writes. Human `--async` output prints the handle on stdout and +a resume command on stderr. Structured modes retain their existing payload and +silent stderr. Timeout/error hints also name the original intent with `--resume` +and shell-quote the handle. Output paths are not retained in hints: pass `--out` +again when resuming to select the destination. + +Artifact output supports inline base64 content. URI-only content retains the +explicit unsupported-download error and does not create an artifact file. +`--raw-response` prints the terminal payload instead of writing an artifact. +The `--resume` flag description in help and static/live `--usage` describes polling an existing operation by ID instead of creating one. Conflict errors explain incompatible inputs. + ### Binary Downloads **Files**: `opcmd.go.stmpl` (flag registration + branching), `auxiliary/internal/output/output.go.stmpl` (binary detection in `Result()`), `auxiliary/internal/output/binary.go.stmpl` (`WriteToFile`, TTY detection), `includes/templating.ts` (`hasBinaryResponse`, `isOnlyBinaryResponse`, `anyOperationHasBinary`) diff --git a/templates/templates/cli/async_test.go.stmpl b/templates/templates/cli/async_test.go.stmpl index b0cf3a9dc..47542d080 100644 --- a/templates/templates/cli/async_test.go.stmpl +++ b/templates/templates/cli/async_test.go.stmpl @@ -3,14 +3,15 @@ package tests import ( - {{- if $a.AsyncPendingTestJSON}} "context" - {{- end}} "encoding/json" + "fmt" "net/http" "net/http/httptest" - {{- if $a.ArtifactJSON}} + "os/exec" + "runtime" "os" + {{- if $a.ArtifactJSON}} "path/filepath" {{- end}} "strings" @@ -21,8 +22,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - {{- if $a.AsyncPendingTestJSON}} "{{getGolangPackage}}/internal/cli" + {{- if isInteractiveModeEnabled}} + "github.com/spf13/cobra" + "{{getGolangPackage}}/internal/interactive" {{- end}} "{{getGolangPackage}}/internal/flagutil" "{{getGolangPackage}}/internal/sdk/types" @@ -191,7 +194,7 @@ func TestAsyncIntent_EscapeReturnsCreateResponseWithoutPolling(t *testing.T) { h := NewCLITestHarness(t) require.NoError(t, runAsyncIntent(t, h, stub.server.URL, "--async")) assert.Equal(t, asyncTestHandle, strings.TrimSpace(h.GetStdout())) - assert.Empty(t, h.GetStderr()) + assert.Contains(t, h.GetStderr(), "To resume polling: "+{{goStringLiteral $a.AsyncResume}}+" "+asyncTestHandle) _, polls, _ := stub.counts() assert.Zero(t, polls) @@ -433,4 +436,426 @@ func TestAsyncIntent_DeclaredJQAppliesOnlyToTerminalResponse(t *testing.T) { assert.JSONEq(t, {{goStringLiteral $j.AsyncSuccessTestJSON}}, h.GetStdout()) } {{- end}} + +func runAsyncResume(t *testing.T, h *CLITestHarness, serverURL string, extra ...string) error { + t.Helper() + args := []string{ {{templateAsyncTestPathArgs}}, "--resume", asyncTestHandle, "--poll-interval", "20ms", "--poll-timeout", "5s" } + return h.RunWithStdinServerRaw(serverURL, append(args, extra...), "") +} + +func TestAsyncResume_SavesTerminalResultWithoutCreate(t *testing.T) { + stub := newAsyncStub(t, asyncCreateTestBody{{if $a.AsyncPendingTestJSON}}, {{goStringLiteral $a.AsyncPendingTestJSON}}{{end}}, asyncSuccessTestBody) + h := NewCLITestHarness(t) + {{- if $a.ArtifactJSON}} + target := filepath.Join(t.TempDir(), "resumed{{templateArtifactTestExt}}") + require.NoError(t, runAsyncResume(t, h, stub.server.URL, "--out", target)) + data, err := os.ReadFile(strings.TrimSpace(h.GetStdout())) + require.NoError(t, err) + assert.Equal(t, "async-artifact", string(data)) + {{- else}} + require.NoError(t, runAsyncResume(t, h, stub.server.URL, "--output-format", "json")) + assert.JSONEq(t, asyncSuccessTestBody, h.GetStdout()) + {{- end}} + creates, polls, _ := stub.counts() + assert.Zero(t, creates) + assert.GreaterOrEqual(t, polls, 1) +} + +func TestAsyncResume_RejectsConflictsBeforeRequests(t *testing.T) { + cases := [][]string{ + {"--resume="}, {"--resume", " "}, {"unexpected prompt"}, {"--async"}, + {"--poll-interval", "0s"}, {"--poll-timeout", "1ms"}, + {{- if $a.BodyFlag}} + {"--{{$a.BodyFlag}}", "@/nonexistent-resume-input.json"}, {"--{{$a.BodyFlag}}", "@-"}, + {{- end}} + {{- range $a.Flags}} + {"--{{.Name}}={{if eq .Type "bool"}}true{{else if or (eq .Type "int") (eq .Type "float")}}1{{else}}conflicting{{end}}"}, + {{- end}} + {{- if $a.ArtifactJSON}} + {"--out", "unused", "--raw-response"}, + {{- end}} + } + for _, extra := range cases { + t.Run(strings.Join(extra, " "), func(t *testing.T) { + stub := newAsyncStub(t, asyncCreateTestBody, asyncSuccessTestBody) + h := NewCLITestHarness(t) + err := runAsyncResume(t, h, stub.server.URL, extra...) + require.Error(t, err) + assert.NotContains(t, err.Error(), "no such file") + creates, polls, _ := stub.counts() + assert.Zero(t, creates) + assert.Zero(t, polls) + }) + } +} + +func TestAsyncResume_DryRunPreviewsOnlyPoll(t *testing.T) { + stub := newAsyncStub(t, asyncCreateTestBody, asyncSuccessTestBody) + h := NewCLITestHarness(t) + {{- if $a.ArtifactJSON}} + dir := withAsyncWorkingDir(t) + {{- end}} + require.NoError(t, runAsyncResume(t, h, stub.server.URL, "--dry-run", "--output-format", "json")) + var preview struct { + DryRun bool `json:"dry_run"` + Request struct { Method string `json:"method"`; URL string `json:"url"` } `json:"request"` + } + require.NoError(t, json.Unmarshal([]byte(h.GetStdout()), &preview)) + assert.True(t, preview.DryRun) + assert.Equal(t, "GET", preview.Request.Method) + assert.Contains(t, preview.Request.URL, asyncTestHandle) + {{- range $a.AsyncParams}} + {{- if eq .In "query"}} + assert.Contains(t, preview.Request.URL, {{goStringLiteral .Name}}+"="+{{goStringLiteral .Expected}}) + {{- end}} + {{- end}} + creates, polls, _ := stub.counts() + assert.Zero(t, creates) + assert.Zero(t, polls) + {{- if $a.ArtifactJSON}} + files, err := os.ReadDir(dir) + require.NoError(t, err) + assert.Empty(t, files) + {{- end}} +} + +func TestAsyncResume_DoesNotReadOpenStdin(t *testing.T) { + stub := newAsyncStub(t, asyncCreateTestBody, asyncSuccessTestBody) + h := NewCLITestHarness(t) + h.resetAndSetupEnv() + root, err := cli.NewRootCommand() + require.NoError(t, err) + root.SetOut(h.stdout) + root.SetErr(h.stderr) + read, write, err := os.Pipe() + require.NoError(t, err) + defer read.Close() + defer write.Close() + original := os.Stdin + os.Stdin = read + defer func() { os.Stdin = original }() + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + stopWatchdog := make(chan struct{}) + defer close(stopWatchdog) + go func() { + select { case <-ctx.Done(): _ = write.Close(); case <-stopWatchdog: } + }() + err = cli.ExecuteRoot(ctx, root, []string{ {{templateAsyncTestPathArgs}}, "--resume", asyncTestHandle, "--dry-run", "--output-format", "json", "--server-url", stub.server.URL}) + require.NoError(t, err) + require.NoError(t, ctx.Err()) + creates, polls, _ := stub.counts() + assert.Zero(t, creates) + assert.Zero(t, polls) +} + +func TestAsyncResume_HelpAndUsageDescribeAlternateInput(t *testing.T) { + for _, flag := range []string{"--help", "--usage"} { + h := NewCLITestHarness(t) + require.NoError(t, h.RunWithStdinServerRaw("http://localhost:1", []string{ {{templateAsyncTestPathArgs}}, flag }, "")) + assert.Contains(t, h.GetStdout(), "Resume polling an existing operation by ID instead of creating one") + } +} + +{{- if $a.ArtifactJSON}} +func TestAsyncResume_OutputModes(t *testing.T) { + for _, extra := range [][]string{ {"--output-format", "json"}, {"--agent-mode", "--output-format", "json"}, {"--jq", "."}, {"--raw-response"}} { + t.Run(strings.Join(extra, " "), func(t *testing.T) { + stub := newAsyncStub(t, asyncCreateTestBody, asyncSuccessTestBody) + dir := withAsyncWorkingDir(t) + h := NewCLITestHarness(t) + require.NoError(t, runAsyncResume(t, h, stub.server.URL, extra...)) + files, err := os.ReadDir(dir) + require.NoError(t, err) + if extra[0] == "--raw-response" { + assert.Empty(t, files) + assert.JSONEq(t, asyncSuccessTestBody, h.GetStdout()) + } else { + require.Len(t, files, 1) + data, err := os.ReadFile(filepath.Join(dir, files[0].Name())) + require.NoError(t, err) + assert.Equal(t, "async-artifact", string(data)) + { + var envelope map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(h.GetStdout()), &envelope)) + resolved, err := filepath.EvalSymlinks(filepath.Join(dir, files[0].Name())) + require.NoError(t, err) + assert.Equal(t, resolved, envelope["path"]) + assert.Equal(t, float64(len(data)), envelope["size_bytes"]) + } + assert.Empty(t, h.GetStderr()) + } + creates, polls, _ := stub.counts() + assert.Zero(t, creates) + assert.Equal(t, 1, polls) + }) + } +} +{{- end}} + +{{- if isInteractiveModeEnabled}} +type asyncResumePrompter struct { calls int } +func (p *asyncResumePrompter) Prompt(_ *cobra.Command, _ []interactive.PromptField) ([]interactive.PromptAnswer, error) { + p.calls++ + return nil, context.Canceled +} + +func TestAsyncResume_BypassesInjectedCreatePrompter(t *testing.T) { + for _, extra := range [][]string{nil, {"--resume="}{{if $a.BodyFlag}}, {"--{{$a.BodyFlag}}", "@/nonexistent-resume-input.json"}{{end}}} { + stub := newAsyncStub(t, asyncCreateTestBody, asyncSuccessTestBody) + h := NewCLITestHarness(t) + h.resetAndSetupEnv() + root, err := cli.NewRootCommand() + require.NoError(t, err) + root.SetOut(h.stdout) + root.SetErr(h.stderr) + p := &asyncResumePrompter{} + args := []string{ {{templateAsyncTestPathArgs}}, "--resume", asyncTestHandle, "--dry-run", "--interactive", "--server-url", stub.server.URL} + err = cli.ExecuteRoot(interactive.WithPrompter(context.Background(), p), root, append(args, extra...)) + if len(extra) == 0 { require.NoError(t, err) } else { require.Error(t, err); assert.NotContains(t, err.Error(), "no such file") } + assert.Zero(t, p.calls) + creates, polls, _ := stub.counts() + assert.Zero(t, creates) + assert.Zero(t, polls) + } +} +{{- end}} + + +{{- $j := templateAsyncJQTestCommand}} +{{- if $j}} +func TestAsyncResume_NonArtifactDeclaredJQ(t *testing.T) { + stub := newAsyncStubWithStatus(t, {{$j.AsyncCreateResponseCode}}, {{$j.AsyncResponseCode}}, {{goStringLiteral $j.AsyncCreateTestJSON}}, {{goStringLiteral $j.AsyncSuccessTestJSON}}) + h := NewCLITestHarness(t) + args := []string{ {{templateIntentPathArgs $j}}, "--resume", asyncTestHandle, "--poll-interval", "20ms" } + require.NoError(t, h.RunWithStdinServerRaw(stub.server.URL, args, "")) + assert.NotContains(t, h.GetStdout(), "{") + assert.NotEmpty(t, h.GetStdout()) + {{- if eq $j.JQ ".status"}} + expected, err := json.Marshal(jsonGetString({{goStringLiteral $j.AsyncSuccessTestJSON}}, "status")) + require.NoError(t, err) + assert.Equal(t, string(expected), strings.TrimSpace(h.GetStdout())) + {{- end}} + creates, polls, _ := stub.counts() + assert.Zero(t, creates) + assert.Equal(t, 1, polls) + for _, flag := range []string{"--help", "--usage"} { + h = NewCLITestHarness(t) + require.NoError(t, h.RunWithStdinServerRaw(stub.server.URL, []string{ {{templateIntentPathArgs $j}}, flag }, "")) + assert.NotContains(t, h.GetStdout(), "--out again") + } +} +{{- end}} + +func TestAsyncResume_TerminalErrorsAndHandoff(t *testing.T) { + for name, test := range map[string]struct { body, reason string; timeout bool }{ + "unknown": {asyncUnknownTestBody, "CLI_ASYNC_UNKNOWN_STATE", false}, + "missing": {asyncMissingStateBody, "CLI_PROTOCOL", false}, + {{- if $a.AsyncFailureTestJSON}} + "failure": { {{goStringLiteral $a.AsyncFailureTestJSON}}, "CLI_ASYNC_FAILED", false }, + {{- end}} + {{- if $a.AsyncHandoffTestJSON}} + "handoff": { {{goStringLiteral $a.AsyncHandoffTestJSON}}, "", false }, + {{- end}} + {{- if $a.AsyncPendingTestJSON}} + "timeout": { {{goStringLiteral $a.AsyncPendingTestJSON}}, "CLI_ASYNC_TIMEOUT", true }, + {{- end}} + } { + t.Run(name, func(t *testing.T) { + stub := newAsyncStub(t, asyncCreateTestBody, test.body) + {{- if $a.ArtifactJSON}} + dir := withAsyncWorkingDir(t) + {{- end}} + h := NewCLITestHarness(t) + extra := []string{"--output-format", "json", "--agent-mode"} + if test.timeout { extra = append(extra, "--poll-timeout", "50ms") } + err := runAsyncResume(t, h, stub.server.URL, extra...) + if test.reason == "" { require.NoError(t, err); assert.JSONEq(t, test.body, h.GetStdout()) } else { + require.Error(t, err) + assert.Equal(t, test.reason, jsonGetString(h.GetStderr(), "error_reason")) + assert.Equal(t, {{goStringLiteral $a.AsyncResume}}+" "+asyncTestHandle, jsonGetString(h.GetStderr(), "resume")) + } + creates, _, _ := stub.counts() + assert.Zero(t, creates) + {{- if $a.ArtifactJSON}} + files, err := os.ReadDir(dir) + require.NoError(t, err) + assert.Empty(t, files) + {{- end}} + }) + } +} + +{{- if $a.ArtifactJSON}} +func TestAsyncResume_URIIsExplicitlyUnsupported(t *testing.T) { + var payload interface{} + require.NoError(t, json.Unmarshal([]byte(asyncSuccessTestBody), &payload)) + var cfg map[string]interface{} + require.NoError(t, json.Unmarshal([]byte({{goStringLiteral $a.ArtifactJSON}}), &cfg)) + field := func(name, fallback string) string { if value, ok := cfg[name].(string); ok { return value }; return fallback } + var visit func(interface{}) + visit = func(v interface{}) { + switch value := v.(type) { + case map[string]interface{}: + if value[field("typeField", "type")] == cfg["kind"] { + delete(value, field("dataField", "data")) + value[field("uriField", "uri")] = "https://example.invalid/artifact" + } + for _, child := range value { visit(child) } + case []interface{}: + for _, child := range value { visit(child) } + } + } + visit(payload) + body, err := json.Marshal(payload) + require.NoError(t, err) + stub := newAsyncStub(t, asyncCreateTestBody, string(body)) + dir := withAsyncWorkingDir(t) + h := NewCLITestHarness(t) + err = runAsyncResume(t, h, stub.server.URL) + require.Error(t, err) + assert.Contains(t, err.Error(), "delivered by URI") + files, err := os.ReadDir(dir) + require.NoError(t, err) + assert.Empty(t, files) +} +{{- end}} + +{{- if templateReviewAsyncPollTestEnabled}} +func TestAsyncPoll_RawSchemaMismatchAndSSE(t *testing.T) { + for _, mode := range []string{"json", "jq", "sse"} { + t.Run(mode, func(t *testing.T) { + calls := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + if mode == "sse" { + w.Header().Set("Content-Type", "text/event-stream") + fmt.Fprintf(w, "data: %s\n\n", asyncSuccessTestBody) + } else { + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ "id": 123, "status": "completed" }`) + } + })) + defer server.Close() + h := NewCLITestHarness(t) + args := []string{ {{templateAsyncPollTestArgs}}, "--{{$a.AsyncParameterName}}", asyncTestHandle } + {{- range $a.AsyncParams}} + {{- if eq .In "query"}} + args = append(args, "--{{.Name}}={{.Expected}}") + {{- end}} + {{- end}} + if mode == "jq" { args = append(args, "--jq", ".id") } else { args = append(args, "--output-format", "json") } + require.NoError(t, h.RunWithStdinServerRaw(server.URL, args, "")) + assert.Equal(t, 1, calls) + if mode == "json" { assert.JSONEq(t, `{ "id":123, "status":"completed" }`, h.GetStdout()) } + if mode == "jq" { assert.Equal(t, "123", strings.TrimSpace(h.GetStdout())) } + if mode == "sse" { + var event struct { + Data json.RawMessage `json:"data"` + } + require.NoError(t, json.Unmarshal([]byte(h.GetStdout()), &event)) + assert.JSONEq(t, asyncSuccessTestBody, string(event.Data)) + } + }) + } +} + + +{{- end}} + +func TestAsyncResume_PrintedHintQuotesHandleAndExecutes(t *testing.T) { + handle := asyncTestHandle + " '$(echo unexpected); spaced" + creates, polls := 0, 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if r.Method == http.MethodGet { + polls++ + {{- if eq $a.AsyncParameterIn "query"}} + assert.Equal(t, handle, r.URL.Query().Get({{goStringLiteral $a.AsyncParameterName}})) + {{- else}} + assert.Contains(t, r.URL.Path, handle) + {{- end}} + fmt.Fprint(w, strings.ReplaceAll(asyncSuccessTestBody, asyncTestHandle, handle)) + } else { + creates++ + fmt.Fprint(w, strings.ReplaceAll(asyncCreateTestBody, asyncTestHandle, handle)) + } + })) + defer server.Close() + {{- if $a.ArtifactJSON}} + withAsyncWorkingDir(t) + {{- end}} + h := NewCLITestHarness(t) + require.NoError(t, runAsyncIntent(t, h, server.URL, "--async")) + assert.Contains(t, h.GetStderr(), "To resume polling: ") + assert.Contains(t, h.GetStderr(), " --resume ") + assert.Equal(t, 1, creates) + assert.Zero(t, polls) + hint := strings.TrimSpace(strings.TrimPrefix(h.GetStderr(), "To resume polling: ")) + if runtime.GOOS == "windows" { + t.Skip("POSIX shell quoting requires sh") + } + parsed, err := exec.Command("sh", "-c", "set -- "+hint+"; printf '%s\\0' \"$@\"").Output() + require.NoError(t, err) + argv := strings.Split(strings.TrimSuffix(string(parsed), string(rune(0))), string(rune(0))) + require.Greater(t, len(argv), 2) + assert.Equal(t, handle, argv[len(argv)-1]) + h = NewCLITestHarness(t) + require.NoError(t, h.RunWithStdinServerRaw(server.URL, append(argv[1:], "--poll-interval", "20ms"), "")) + assert.Equal(t, 1, creates) + assert.Equal(t, 1, polls) + {{- if $a.ArtifactJSON}} + data, err := os.ReadFile(strings.TrimSpace(h.GetStdout())) + require.NoError(t, err) + assert.Equal(t, "async-artifact", string(data)) + {{- end}} +} + +{{- if isInteractiveModeEnabled}} +func TestAsyncResume_DoesNotSuppressOrdinaryCreatePrompt(t *testing.T) { + h := NewCLITestHarness(t) + h.resetAndSetupEnv() + root, err := cli.NewRootCommand() + require.NoError(t, err) + root.SetOut(h.stdout) + root.SetErr(h.stderr) + p := &asyncResumePrompter{} + err = cli.ExecuteRoot(interactive.WithPrompter(context.Background(), p), root, []string{ {{templateAsyncTestPathArgs}}, "--interactive", "--dry-run" }) + {{- if gt $a.MinArgs 0}} + require.Error(t, err) + assert.Equal(t, 1, p.calls) + {{- else}} + _ = err + {{- end}} +} +{{- end}} + +func TestAsyncResume_RequestFlagDefaultsAndExplicitConflicts(t *testing.T) { + for _, explicit := range []bool{false, true} { + stub := newAsyncStub(t, asyncCreateTestBody, asyncSuccessTestBody) + h := NewCLITestHarness(t).WithEnv("TEST_CREATE_PRIORITY", "from-environment") + h.resetAndSetupEnv() + root, err := cli.NewRootCommand() + require.NoError(t, err) + root.SetOut(h.stdout) + root.SetErr(h.stderr) + cmd, _, err := root.Find([]string{ {{templateAsyncTestPathArgs}} }) + require.NoError(t, err) + cmd.Flags().String("create-priority", os.Getenv("TEST_CREATE_PRIORITY"), "Create priority") + flagutil.MarkRequestInput(cmd, "create-priority") + args := []string{ {{templateAsyncTestPathArgs}}, "--resume", asyncTestHandle, "--dry-run", "--output-format", "json", "--server-url", stub.server.URL } + if explicit { args = append(args, "--create-priority=from-argv") } + err = cli.ExecuteRoot(context.Background(), root, args) + if explicit { + require.Error(t, err) + assert.Contains(t, err.Error(), "--create-priority") + } else { + require.NoError(t, err) + assert.Contains(t, h.GetStdout(), stub.server.URL) + } + creates, polls, _ := stub.counts() + assert.Zero(t, creates) + assert.Zero(t, polls) + } +} {{- end}} diff --git a/templates/templates/cli/auxiliary/internal/flagutil/flags.go.stmpl b/templates/templates/cli/auxiliary/internal/flagutil/flags.go.stmpl index 8da6134a2..a4d36688f 100644 --- a/templates/templates/cli/auxiliary/internal/flagutil/flags.go.stmpl +++ b/templates/templates/cli/auxiliary/internal/flagutil/flags.go.stmpl @@ -34,6 +34,10 @@ func FlagChanged(cmd *cobra.Command, name string) bool { return false } +func IsAsyncResume(cmd *cobra.Command) bool { + return cmd != nil && cmd.Annotations["speakeasy_async"] != "" && FlagChanged(cmd, "resume") +} + // AnyFlagChanged reports whether any of the named flags was explicitly set. func AnyFlagChanged(cmd *cobra.Command, names ...string) bool { for _, name := range names { diff --git a/templates/templates/cli/auxiliary/internal/interactive/interactive.go.stmpl b/templates/templates/cli/auxiliary/internal/interactive/interactive.go.stmpl index 61eb3b236..5ae75e8fa 100644 --- a/templates/templates/cli/auxiliary/internal/interactive/interactive.go.stmpl +++ b/templates/templates/cli/auxiliary/internal/interactive/interactive.go.stmpl @@ -191,7 +191,7 @@ func interceptCommand(cmd *cobra.Command) { } bodyTookControl := false - if !usage.UsageRequested(runCmd) && !documentationRequested(runCmd) && Resolve(runCmd).PromptRequiredInputs() { + if !usage.UsageRequested(runCmd) && !documentationRequested(runCmd) && !flagutil.IsAsyncResume(runCmd) && Resolve(runCmd).PromptRequiredInputs() { supplied, err := wholeBodySupplied(runCmd) if err != nil { return err diff --git a/templates/templates/cli/auxiliary/internal/output/async.go.stmpl b/templates/templates/cli/auxiliary/internal/output/async.go.stmpl index 5170f185b..82eb5e40c 100644 --- a/templates/templates/cli/auxiliary/internal/output/async.go.stmpl +++ b/templates/templates/cli/auxiliary/internal/output/async.go.stmpl @@ -14,6 +14,7 @@ import ( "time" "github.com/spf13/cobra" + "github.com/spf13/pflag" "{{getGolangPackage}}/internal/flagutil" ) @@ -164,9 +165,62 @@ func AsyncResult(cmd *cobra.Command, createResponse interface{}, newPoll AsyncPo return renderAsyncSnapshot(cmd, create, false) } fmt.Fprintln(cmd.OutOrStdout(), id) + fmt.Fprintln(cmd.ErrOrStderr(), "To resume polling: "+resume) return nil } + return pollAsyncResult(cmd, cfg, timings, id, started, newPoll) +} + +func ValidateAsyncResume(cmd *cobra.Command, args []string) error { + id, _ := flagutil.GetStringFlag(cmd, "resume") + if strings.TrimSpace(id) == "" { + return flagutil.WithCLIValidation(errors.New("--resume requires a non-empty operation handle")) + } + if len(args) > 0 { + return flagutil.WithCLIValidation(errors.New("--resume cannot be combined with create arguments")) + } + if escape, _ := flagutil.GetBoolFlag(cmd, "async"); escape { + return flagutil.WithCLIValidation(errors.New("--resume cannot be combined with --async")) + } + bodyFlag := cmd.Annotations[flagutil.AnnotationWholeBodyFlag] + var conflict string + cmd.LocalFlags().VisitAll(func(f *pflag.Flag) { + if conflict == "" && f.Changed && (f.Name == bodyFlag || len(f.Annotations[flagutil.AnnotationRequestInput]) > 0) { + conflict = f.Name + } + }) + if conflict != "" { + return flagutil.WithCLIValidation(fmt.Errorf("--resume cannot be combined with create request flag --%s", conflict)) + } + return ValidateAsyncFlags(cmd) +} + +func ResumeAsync(cmd *cobra.Command, id string, newPoll AsyncPollFactory) error { + cfg, err := activeAsync(cmd) + if err != nil { + return WithCLIReason(err, ReasonCLIProtocol) + } + timings, err := effectiveAsyncTimings(cmd, cfg) + if err != nil { + return err + } + if dryRun, _ := flagutil.GetBoolFlag(cmd, "dry-run"); dryRun { + poll, err := newPoll(cmd, id, cfg.Params) + if err != nil { + return Error(cmd, err) + } + _, err = poll(cmd.Context()) + if err != nil { + return Error(cmd, err) + } + return nil + } + return pollAsyncResult(cmd, cfg, timings, id, time.Now(), newPoll) +} + +func pollAsyncResult(cmd *cobra.Command, cfg *asyncConfig, timings asyncTimings, id string, started time.Time, newPoll AsyncPollFactory) error { + resume := asyncResume(cfg.Resume, id) poll, err := newPoll(cmd, id, cfg.Params) if err != nil { return Error(cmd, err) diff --git a/templates/templates/cli/includes/flags.ts b/templates/templates/cli/includes/flags.ts index 780a885db..8b7df5bf0 100644 --- a/templates/templates/cli/includes/flags.ts +++ b/templates/templates/cli/includes/flags.ts @@ -47,6 +47,7 @@ const reservedFlagNames = [ "out", "raw-response", "async", + "resume", "poll-interval", "poll-timeout", ]; diff --git a/templates/templates/cli/includes/intents.ts b/templates/templates/cli/includes/intents.ts index f3f0a19ec..f2cfdb402 100644 --- a/templates/templates/cli/includes/intents.ts +++ b/templates/templates/cli/includes/intents.ts @@ -531,7 +531,7 @@ function validateIntentFlagNames( } } if (cmd.Async) { - for (const owned of ["async", "poll-interval", "poll-timeout"]) { + for (const owned of ["async", "resume", "poll-interval", "poll-timeout"]) { if (generatedFlags.has(owned)) { throw new Error( `x-speakeasy-cli-commands: command "${cmdKey}" declares async, ` + @@ -1086,10 +1086,7 @@ function collectIntentManifest(): IntentManifestCtx { } to the generated poll request`, ); } - const pollFlag = sanitizeFlagNameWithReserved(target.Field.Name); - const resumePrefix = `${sanitizeCliName()} ${getCLICommandPath( - pollFound.op, - ).join(" ")} --${pollFlag}`; + const resumePrefix = `${sanitizeCliName()} ${path.join(" ")} --resume`; asyncParams = (cmd.Async.ResolvedParams || []).map((param: any) => ({ In: param.In, Name: param.Name, @@ -1840,6 +1837,43 @@ function templateAsyncTestArgs(): string { } registerTemplateFunc("templateAsyncTestArgs", templateAsyncTestArgs); +function templateAsyncTestPathArgs(): string { + const cmd = getAsyncTestCommand(); + if (!cmd) return ""; + return [...cmd.ParentPath, firstUseWord(cmd.Use)] + .map((p) => goStringLiteral(p)) + .join(", "); +} +registerTemplateFunc("templateAsyncTestPathArgs", templateAsyncTestPathArgs); +function templateIntentPathArgs(cmd: IntentCmdCtx): string { + return [...cmd.ParentPath, firstUseWord(cmd.Use)] + .map((p) => goStringLiteral(p)) + .join(", "); +} +registerTemplateFunc("templateIntentPathArgs", templateIntentPathArgs); +function templateAsyncPollTestArgs(): string { + const cmd = getAsyncTestCommand(); + return cmd?.PollOp + ? getCLICommandPath(cmd.PollOp) + .map((p) => goStringLiteral(p)) + .join(", ") + : ""; +} +registerTemplateFunc("templateAsyncPollTestArgs", templateAsyncPollTestArgs); +function templateReviewAsyncPollTestEnabled(): boolean { + const cmd = getAsyncTestCommand(); + return ( + (context.Global.AST as any).OpenAPIDocument?.Info?.Title === "SDK Review" && + cmd?.PollOp?.OriginalID === "getAsset" && + cmd.AsyncParameterName === "id" && + getStreamingFieldName(cmd.PollOp) === "AssetStatusStream" + ); +} +registerTemplateFunc( + "templateReviewAsyncPollTestEnabled", + templateReviewAsyncPollTestEnabled, +); + function templateIntentAsyncTestArgs(cmd: IntentCmdCtx | null): string { if (!cmd) return ""; const parts = [...cmd.ParentPath, firstUseWord(cmd.Use)]; diff --git a/templates/templates/cli/includes/usage.ts b/templates/templates/cli/includes/usage.ts index 6f945c399..215049fd3 100644 --- a/templates/templates/cli/includes/usage.ts +++ b/templates/templates/cli/includes/usage.ts @@ -1075,6 +1075,10 @@ function buildIntentUsageCommand(intent: IntentCmdCtx): UsageCommandDef { }); } if (intent.AsyncJSON) { + flags.push({ + spec: usageFlagSpec("resume", undefined, "id"), + help: "Resume polling an existing operation by ID instead of creating one", + }); flags.push({ spec: usageFlagSpec("async"), help: "Return the operation handle without waiting for a terminal response", @@ -1095,8 +1099,12 @@ function buildIntentUsageCommand(intent: IntentCmdCtx): UsageCommandDef { args: intent.Args.map((arg) => { const usage: UsageArgDef = { name: arg.Name || "input", - help: arg.Summary, - required: Boolean(arg.Required && !arg.PresetCovered), + help: intent.AsyncJSON + ? `${arg.Summary} (create input; omit with --resume)` + : arg.Summary, + required: Boolean( + arg.Required && !arg.PresetCovered && !intent.AsyncJSON, + ), variadic: Boolean(arg.Variadic), }; if (arg.Suggestions && arg.Suggestions.length > 0) { diff --git a/templates/templates/cli/intentcmd.go.stmpl b/templates/templates/cli/intentcmd.go.stmpl index f29cd636a..32d42277f 100644 --- a/templates/templates/cli/intentcmd.go.stmpl +++ b/templates/templates/cli/intentcmd.go.stmpl @@ -152,6 +152,8 @@ func InitIntent{{$c.FuncName}}(parent *cobra.Command) error { cmd.Flags().Bool("raw-response", false, "Print the raw API response instead of writing the {{$c.ArtifactKind}} to a file") {{- end}} {{- if $c.AsyncJSON}} + cmd.Flags().String("resume", "", "Resume polling an existing operation by ID instead of creating one") + _ = flagutil.AnnotatePromptFlag(cmd, "resume", flagutil.PromptFlagSpec{Kind: "string"}) cmd.Flags().Bool("async", false, "Return the operation handle without waiting for a terminal response") cmd.Flags().String("poll-interval", "", "Override the initial polling interval (positive Go duration, for example 500ms or 2s)") cmd.Flags().String("poll-timeout", "", "Override the overall polling deadline (positive Go duration, at least the effective poll interval)") @@ -250,6 +252,20 @@ func runIntent{{$c.FuncName}}Cmd(cmd *cobra.Command, args []string) error { return flagutil.WithCLIValidation(fmt.Errorf("--raw-response prints the raw API response and cannot be combined with --out")) } {{- end}} + {{- if $c.AsyncJSON}} + if flagutil.IsAsyncResume(cmd) { + if err := output.ValidateAsyncResume(cmd, args); err != nil { + return err + } + {{- if $c.JQ}} + if f := cmd.Flag("jq"); f != nil && !f.Changed { + _ = f.Value.Set("{{escapeGoString $c.JQ}}") + } + {{- end}} + id, _ := flagutil.GetStringFlag(cmd, "resume") + return output.ResumeAsync(cmd, id, newPollIntent{{$c.FuncName}}) + } + {{- end}} {{- if $c.Dispatch}} {{- if or (gt $c.MinArgs 0) $c.PromptFlags}} if len(args) == 0{{range $c.Flags}} && !flagutil.FlagChanged(cmd, "{{.Name}}"){{end}} { diff --git a/templates/templates/cli/opcmd.go.stmpl b/templates/templates/cli/opcmd.go.stmpl index 5c6b8d69a..2cb69cd8f 100644 --- a/templates/templates/cli/opcmd.go.stmpl +++ b/templates/templates/cli/opcmd.go.stmpl @@ -332,6 +332,11 @@ func {{$runFuncName}}(cmd *cobra.Command, args []string) error { }) } {{- end}} + {{- if and (not (hasResponseTransform $op)) (not (hasBinaryResponse $op))}} + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + {{- end}} {{- if hasStreamingResponse $op}} // Streaming response — iterate events and output incrementally. // Skip streaming iteration in dry-run mode (synthetic response has no stream). @@ -343,11 +348,6 @@ func {{$runFuncName}}(cmd *cobra.Command, args []string) error { return output.StreamResult(cmd, res, "{{getStreamingFieldName $op}}") } {{- end}} - {{- if and (not (hasResponseTransform $op)) (not (hasBinaryResponse $op))}} - if output.WantsRawJSON(cmd) { - sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) - } - {{- end}} {{- if not (operationHasDeferredBody $op)}} stopProgress := output.StartRequestProgress(cmd) defer stopProgress() @@ -481,6 +481,11 @@ func {{$executeFuncName}}(cmd *cobra.Command, args []string, asyncIntent bool) ( return nil, nil } {{- end}} + {{- if and (not (hasResponseTransform $op)) (not (hasBinaryResponse $op))}} + if asyncIntent || output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } + {{- end}} {{- if hasStreamingResponse $op}} if !client.IsDryRun(cmd) && !asyncIntent { res, err := s{{.Local.SubSDKAccessor}}.{{sanitizeMethodName $op}}({{templateSDKCallArgs $op $reqObjName}}) @@ -493,11 +498,6 @@ func {{$executeFuncName}}(cmd *cobra.Command, args []string, asyncIntent bool) ( return nil, nil } {{- end}} - {{- if and (not (hasResponseTransform $op)) (not (hasBinaryResponse $op))}} - if asyncIntent || output.WantsRawJSON(cmd) { - sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) - } - {{- end}} {{- if not (operationHasDeferredBody $op)}} stopProgress := func() {} if !asyncIntent { diff --git a/templates/templates/cli/tests/primary/jsonlstreaming_test.go.stmpl b/templates/templates/cli/tests/primary/jsonlstreaming_test.go.stmpl index b8aa118c9..a11d60751 100644 --- a/templates/templates/cli/tests/primary/jsonlstreaming_test.go.stmpl +++ b/templates/templates/cli/tests/primary/jsonlstreaming_test.go.stmpl @@ -176,3 +176,20 @@ func TestJsonlStreamWithJq(t *testing.T) { require.NoError(t, json.Unmarshal([]byte(strings.TrimSpace(lines[1])), &name1)) assert.Equal(t, "John", name1) } + +func TestJsonlStreamExplicitJSON(t *testing.T) { + calls := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls++ + w.Header().Set("Content-Type", "application/jsonl") + _, _ = w.Write([]byte("{\"name\":\"Peter\",\"skills\":[\"Go\"]}\n{\"name\":\"John\",\"skills\":[\"Rust\"]}\n")) + })) + defer server.Close() + h := NewCLITestHarness(t) + require.NoError(t, h.RunWithStdinServerRaw(server.URL, []string{"jsonl", "jsonl-stream", "--output-format", "json"}, "")) + assert.Equal(t, 1, calls) + events := parseNDJSON(t, h.GetStdout()) + require.Len(t, events, 2) + assert.Equal(t, map[string]interface{}{"name": "Peter", "skills": []interface{}{"Go"}}, events[0]) + assert.Equal(t, map[string]interface{}{"name": "John", "skills": []interface{}{"Rust"}}, events[1]) +} diff --git a/zSDKs/sdk-cli/.speakeasy/gen.lock b/zSDKs/sdk-cli/.speakeasy/gen.lock index ba46d959c..8980590aa 100644 --- a/zSDKs/sdk-cli/.speakeasy/gen.lock +++ b/zSDKs/sdk-cli/.speakeasy/gen.lock @@ -64,7 +64,7 @@ trackedFiles: internal/cli/binaryandstringupload.go: last_write_checksum: sha1:20d8028b7618e73dcf1763372f9145ea325b32b0 internal/cli/chat.go: - last_write_checksum: sha1:d5ccc0970afac880e050719fe49c883432062904 + last_write_checksum: sha1:11730e18c9c6f403253ba394cb00e5b6e8beec59 internal/cli/configure.go: last_write_checksum: sha1:81f421f92ba05428359b905815e1dd23c64e7539 internal/cli/createuser.go: @@ -74,7 +74,7 @@ trackedFiles: internal/cli/deleteuser.go: last_write_checksum: sha1:e6876685816745a290da3903a03a3afe8f0812e7 internal/cli/getasset.go: - last_write_checksum: sha1:de1608c08a8516f477d77a1d138a590462fa1f0f + last_write_checksum: sha1:5a5d0784d2880dd248401b62b0579e6b21ff7c04 internal/cli/getbinarydefaultresponse.go: last_write_checksum: sha1:75df919b8b943ca85ed7299d14cd2c5ee7cb7b56 internal/cli/getduplicateexportcollision.go: @@ -120,9 +120,9 @@ trackedFiles: internal/cli/intent_invite.go: last_write_checksum: sha1:793f73ebb8278cf397e5a6a0fefb0ce1d34ffb08 internal/cli/intent_observe.go: - last_write_checksum: sha1:a431c4a089387daea5b9f974ae5b4d1663414026 + last_write_checksum: sha1:73bb5c7809ee73702a3284ab1ac1bc74c064f4f7 internal/cli/intent_produce.go: - last_write_checksum: sha1:41e8095d62f9bab26b77c3be790b4ed1eac7f8f1 + last_write_checksum: sha1:bd40954296ab4b5dd4265987e5c8918d46209528 internal/cli/intent_render.go: last_write_checksum: sha1:9b211c0615b4df7cad91ca9434669eebcc7282e3 internal/cli/intent_say.go: @@ -180,7 +180,7 @@ trackedFiles: internal/cli/postfile.go: last_write_checksum: sha1:f67d5ddcaa3ea921513ee6f97fe5eb8db9580aae internal/cli/renderasset.go: - last_write_checksum: sha1:7b64189912eaa0e2c11bdbc91f39d3d035bbc8b3 + last_write_checksum: sha1:c72bde224ebd3246cce2e14e5f04b1e3ed854ba2 internal/cli/root.go: last_write_checksum: sha1:dad2cd122f67c8465ec46ab90e7e74f9b1f0ce3d internal/cli/tag1/auth.go: @@ -244,13 +244,13 @@ trackedFiles: internal/flagutil/dispatch.go: last_write_checksum: sha1:fecd48dd69b4743ff5c6c1329a2071fe49d9423e internal/flagutil/flags.go: - last_write_checksum: sha1:dcaf9e57c7fbfc01e64568da488aacc6ee9d158f + last_write_checksum: sha1:d03aca42136fca769c6a6613e27a0001911e66d0 internal/flagutil/metadata.go: last_write_checksum: sha1:1936c82d8e36a6612364827ffc05dcbea0e1dd8f internal/flagutil/preset.go: last_write_checksum: sha1:a6cf90b7dd09d11ffbd438fc3aba76806de5752a internal/interactive/interactive.go: - last_write_checksum: sha1:ba6979695c50a3c2385bc74a4676dfe88814a984 + last_write_checksum: sha1:cb2abdd024fe02c87a0e558c1573cdc91832edfc internal/interactive/policy.go: last_write_checksum: sha1:3cf3eec72867230610aa5e7c308fddcaf916b81b internal/interactive/policy_test.go: @@ -260,7 +260,7 @@ trackedFiles: internal/output/artifact.go: last_write_checksum: sha1:e1dfc09cbda1012a953fe33fece7efaddaf3f03b internal/output/async.go: - last_write_checksum: sha1:dd97e2b19a859b376462dd1beacb31d7d3668a79 + last_write_checksum: sha1:a21d71edaa274da8d45b79d6a7511424057c143f internal/output/binary.go: last_write_checksum: sha1:843f1fafc38ee03782e978f617a07bb543f06d3b internal/output/classify.go: @@ -646,7 +646,7 @@ trackedFiles: internal/usage/bodyschemas.go: last_write_checksum: sha1:775249b69ed405ad072c16a76fe6113f375b3818 internal/usage/schema.go: - last_write_checksum: sha1:e010e10d24f913bd1c3b5429cb85b74e9fefae1d + last_write_checksum: sha1:1320ae9b96590ef597a1423e64da8993a90925fc scripts/install.ps1: last_write_checksum: sha1:9dcf40ea00ecb8ceda83d5056f57235b43cf658a scripts/install.sh: @@ -654,7 +654,7 @@ trackedFiles: tests/artifact_test.go: last_write_checksum: sha1:0f30f25b5dc69938e9ef91371d4b3497bb87f62e tests/async_test.go: - last_write_checksum: sha1:fb5dafb085f988d482359c61cdb2b800528ceed0 + last_write_checksum: sha1:6ba96fcf11fc74d77e99c81cea4e3a1cd6dfe297 tests/cli_harness_test.go: last_write_checksum: sha1:b347035c99b9f95ac8e82a7e5d30afe78ed92c23 tests/common_helpers_test.go: diff --git a/zSDKs/sdk-cli/README.md b/zSDKs/sdk-cli/README.md index dc30adb63..d5bbf1b88 100644 --- a/zSDKs/sdk-cli/README.md +++ b/zSDKs/sdk-cli/README.md @@ -272,7 +272,7 @@ Commands that produce media write the file and print only its path on stdout (`- cli render "a lighthouse at sunset" --out ./output/ ``` -Long-running commands poll to a terminal response; human progress goes to stderr and machine-mode success keeps stderr silent. Add `--async` to `cli produce "a lighthouse at sunrise"` to return its handle immediately, or tune foreground polling with `--poll-interval ` and `--poll-timeout `. Resume an escaped or timed-out operation with `cli get-asset --id `. +Long-running commands poll to a terminal response; human progress goes to stderr and machine-mode success keeps stderr silent. Add `--async` to `cli produce "a lighthouse at sunrise"` to return its handle immediately, or tune foreground polling with `--poll-interval ` and `--poll-timeout `. Resume an escaped or timed-out operation with `cli produce --resume `. diff --git a/zSDKs/sdk-cli/docs/cli_observe.md b/zSDKs/sdk-cli/docs/cli_observe.md index 09ca8c5b4..1e8ce3b1a 100644 --- a/zSDKs/sdk-cli/docs/cli_observe.md +++ b/zSDKs/sdk-cli/docs/cli_observe.md @@ -25,6 +25,7 @@ cli observe [prompt] [flags] -h, --help help for observe --poll-interval string Override the initial polling interval (positive Go duration, for example 500ms or 2s) --poll-timeout string Override the overall polling deadline (positive Go duration, at least the effective poll interval) + --resume string Resume polling an existing operation by ID instead of creating one --schema Print the exact JSON Schema of the request body and exit ``` diff --git a/zSDKs/sdk-cli/docs/cli_produce.md b/zSDKs/sdk-cli/docs/cli_produce.md index ba7087da1..3ca6d8d99 100644 --- a/zSDKs/sdk-cli/docs/cli_produce.md +++ b/zSDKs/sdk-cli/docs/cli_produce.md @@ -30,6 +30,7 @@ cli produce [prompt] [flags] --poll-interval string Override the initial polling interval (positive Go duration, for example 500ms or 2s) --poll-timeout string Override the overall polling deadline (positive Go duration, at least the effective poll interval) --raw-response Print the raw API response instead of writing the image to a file + --resume string Resume polling an existing operation by ID instead of creating one --schema Print the exact JSON Schema of the request body and exit ``` diff --git a/zSDKs/sdk-cli/internal/cli/chat.go b/zSDKs/sdk-cli/internal/cli/chat.go index 2aacdf837..1ed19e26b 100644 --- a/zSDKs/sdk-cli/internal/cli/chat.go +++ b/zSDKs/sdk-cli/internal/cli/chat.go @@ -105,6 +105,9 @@ func runChatCmd(cmd *cobra.Command, args []string) error { if err := output.ValidateGlobalServerIndex(cmd, len(sdk.ServerList)); err != nil { return err } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } // Streaming response — iterate events and output incrementally. // Skip streaming iteration in dry-run mode (synthetic response has no stream). if !client.IsDryRun(cmd) { @@ -114,9 +117,6 @@ func runChatCmd(cmd *cobra.Command, args []string) error { } return output.StreamResult(cmd, res, "ChatStream") } - if output.WantsRawJSON(cmd) { - sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) - } res, err := s.Chat(cmd.Context(), *request, sdkOpts...) if err != nil { return output.Error(cmd, err) diff --git a/zSDKs/sdk-cli/internal/cli/getasset.go b/zSDKs/sdk-cli/internal/cli/getasset.go index 8c5bcd0cb..f5269bf94 100644 --- a/zSDKs/sdk-cli/internal/cli/getasset.go +++ b/zSDKs/sdk-cli/internal/cli/getasset.go @@ -65,6 +65,9 @@ func runGetAssetCmd(cmd *cobra.Command, args []string) error { if err := output.ValidateGlobalServerIndex(cmd, len(sdk.ServerList)); err != nil { return err } + if output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } // Streaming response — iterate events and output incrementally. // Skip streaming iteration in dry-run mode (synthetic response has no stream). if !client.IsDryRun(cmd) { @@ -74,9 +77,6 @@ func runGetAssetCmd(cmd *cobra.Command, args []string) error { } return output.StreamResult(cmd, res, "AssetStatusStream") } - if output.WantsRawJSON(cmd) { - sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) - } res, err := s.GetAsset(cmd.Context(), *req, sdkOpts...) if err != nil { return output.Error(cmd, err) diff --git a/zSDKs/sdk-cli/internal/cli/intent_observe.go b/zSDKs/sdk-cli/internal/cli/intent_observe.go index d4a0140b0..9dfd76a48 100644 --- a/zSDKs/sdk-cli/internal/cli/intent_observe.go +++ b/zSDKs/sdk-cli/internal/cli/intent_observe.go @@ -29,7 +29,7 @@ func InitIntentObserve(parent *cobra.Command) error { "speakeasy_operation": "renderAsset", flagutil.AnnotationWholeBodyFlag: "body", "speakeasy_strict_body_keys": "true", - "speakeasy_async": "{\"idPointer\":\"/id\",\"statePointer\":\"/status\",\"states\":{\"completed\":\"success\",\"failed\":\"failure\",\"in_progress\":\"pending\",\"requires_action\":\"handoff\"},\"interval\":\"20ms\",\"backoff\":1.5,\"maxInterval\":\"60ms\",\"timeout\":\"5s\",\"command\":\"observe\",\"resume\":\"cli get-asset --id\",\"parameterIn\":\"path\",\"parameterName\":\"id\",\"params\":[{\"in\":\"query\",\"name\":\"stream\",\"value\":false}]}", + "speakeasy_async": "{\"idPointer\":\"/id\",\"statePointer\":\"/status\",\"states\":{\"completed\":\"success\",\"failed\":\"failure\",\"in_progress\":\"pending\",\"requires_action\":\"handoff\"},\"interval\":\"20ms\",\"backoff\":1.5,\"maxInterval\":\"60ms\",\"timeout\":\"5s\",\"command\":\"observe\",\"resume\":\"cli observe --resume\",\"parameterIn\":\"path\",\"parameterName\":\"id\",\"params\":[{\"in\":\"query\",\"name\":\"stream\",\"value\":false}]}", }, } intentMeta := flagutil.NonBodyMeta(renderAssetCmdMeta, "") @@ -49,6 +49,8 @@ func InitIntentObserve(parent *cobra.Command) error { }}); err != nil { return fmt.Errorf("declare interactive arguments for intent observe: %w", err) } + cmd.Flags().String("resume", "", "Resume polling an existing operation by ID instead of creating one") + _ = flagutil.AnnotatePromptFlag(cmd, "resume", flagutil.PromptFlagSpec{Kind: "string"}) cmd.Flags().Bool("async", false, "Return the operation handle without waiting for a terminal response") cmd.Flags().String("poll-interval", "", "Override the initial polling interval (positive Go duration, for example 500ms or 2s)") cmd.Flags().String("poll-timeout", "", "Override the overall polling deadline (positive Go duration, at least the effective poll interval)") @@ -77,6 +79,16 @@ func runIntentObserveCmd(cmd *cobra.Command, args []string) error { if requested, _ := cmd.Flags().GetBool("schema"); requested { return usage.EmitBodySchema(cmd.OutOrStdout(), "renderAsset") } + if flagutil.IsAsyncResume(cmd) { + if err := output.ValidateAsyncResume(cmd, args); err != nil { + return err + } + if f := cmd.Flag("jq"); f != nil && !f.Changed { + _ = f.Value.Set(".status") + } + id, _ := flagutil.GetStringFlag(cmd, "resume") + return output.ResumeAsync(cmd, id, newPollIntentObserve) + } bodySurfaces := []string{"body"} for _, surface := range bodySurfaces { if flagutil.FlagChanged(cmd, surface) { diff --git a/zSDKs/sdk-cli/internal/cli/intent_produce.go b/zSDKs/sdk-cli/internal/cli/intent_produce.go index 698d88bc4..aa41a2e31 100644 --- a/zSDKs/sdk-cli/internal/cli/intent_produce.go +++ b/zSDKs/sdk-cli/internal/cli/intent_produce.go @@ -31,7 +31,7 @@ func InitIntentProduce(parent *cobra.Command) error { "speakeasy_strict_body_keys": "true", "speakeasy_command_hints": "{\"CLI_ASYNC_FAILED\":[\"Inspect the terminal response and resume command\"],\"CLI_ASYNC_TIMEOUT\":[\"Resume polling with the returned handle\"],\"CLI_ASYNC_UNKNOWN_STATE\":[\"Regenerate the CLI against the current API schema\"]}", "speakeasy_artifact": "{\"pointer\":[{\"field\":\"steps\"},{\"wild\":true},{\"field\":\"content\"},{\"wild\":true}],\"kind\":\"image\",\"defaultPath\":\"produce-{timestamp}-{rand}.{ext}\"}", - "speakeasy_async": "{\"idPointer\":\"/id\",\"statePointer\":\"/status\",\"states\":{\"completed\":\"success\",\"failed\":\"failure\",\"in_progress\":\"pending\",\"requires_action\":\"handoff\"},\"interval\":\"20ms\",\"backoff\":1.5,\"maxInterval\":\"60ms\",\"timeout\":\"5s\",\"command\":\"produce\",\"resume\":\"cli get-asset --id\",\"parameterIn\":\"path\",\"parameterName\":\"id\",\"params\":[{\"in\":\"query\",\"name\":\"stream\",\"value\":false}]}", + "speakeasy_async": "{\"idPointer\":\"/id\",\"statePointer\":\"/status\",\"states\":{\"completed\":\"success\",\"failed\":\"failure\",\"in_progress\":\"pending\",\"requires_action\":\"handoff\"},\"interval\":\"20ms\",\"backoff\":1.5,\"maxInterval\":\"60ms\",\"timeout\":\"5s\",\"command\":\"produce\",\"resume\":\"cli produce --resume\",\"parameterIn\":\"path\",\"parameterName\":\"id\",\"params\":[{\"in\":\"query\",\"name\":\"stream\",\"value\":false}]}", }, } intentMeta := flagutil.NonBodyMeta(renderAssetCmdMeta, "") @@ -58,6 +58,8 @@ func InitIntentProduce(parent *cobra.Command) error { Kind: "string", }) cmd.Flags().Bool("raw-response", false, "Print the raw API response instead of writing the image to a file") + cmd.Flags().String("resume", "", "Resume polling an existing operation by ID instead of creating one") + _ = flagutil.AnnotatePromptFlag(cmd, "resume", flagutil.PromptFlagSpec{Kind: "string"}) cmd.Flags().Bool("async", false, "Return the operation handle without waiting for a terminal response") cmd.Flags().String("poll-interval", "", "Override the initial polling interval (positive Go duration, for example 500ms or 2s)") cmd.Flags().String("poll-timeout", "", "Override the overall polling deadline (positive Go duration, at least the effective poll interval)") @@ -89,6 +91,13 @@ func runIntentProduceCmd(cmd *cobra.Command, args []string) error { if flagutil.FlagChanged(cmd, "raw-response") && flagutil.FlagChanged(cmd, "out") { return flagutil.WithCLIValidation(fmt.Errorf("--raw-response prints the raw API response and cannot be combined with --out")) } + if flagutil.IsAsyncResume(cmd) { + if err := output.ValidateAsyncResume(cmd, args); err != nil { + return err + } + id, _ := flagutil.GetStringFlag(cmd, "resume") + return output.ResumeAsync(cmd, id, newPollIntentProduce) + } bodySurfaces := []string{"body"} for _, surface := range bodySurfaces { if flagutil.FlagChanged(cmd, surface) { diff --git a/zSDKs/sdk-cli/internal/cli/renderasset.go b/zSDKs/sdk-cli/internal/cli/renderasset.go index ca496bb24..ff5be470d 100644 --- a/zSDKs/sdk-cli/internal/cli/renderasset.go +++ b/zSDKs/sdk-cli/internal/cli/renderasset.go @@ -87,6 +87,9 @@ func executeRenderAssetCmd(cmd *cobra.Command, args []string, asyncIntent bool) if err := output.ValidateGlobalServerIndex(cmd, len(sdk.ServerList)); err != nil { return nil, err } + if asyncIntent || output.WantsRawJSON(cmd) { + sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) + } if !client.IsDryRun(cmd) && !asyncIntent { res, err := s.RenderAsset(cmd.Context(), *request, sdkOpts...) if err != nil { @@ -97,9 +100,6 @@ func executeRenderAssetCmd(cmd *cobra.Command, args []string, asyncIntent bool) } return nil, nil } - if asyncIntent || output.WantsRawJSON(cmd) { - sdkOpts = append(sdkOpts, operations.WithSkipDeserialization()) - } res, err := s.RenderAsset(cmd.Context(), *request, sdkOpts...) if err != nil { return nil, output.Error(cmd, err) diff --git a/zSDKs/sdk-cli/internal/flagutil/flags.go b/zSDKs/sdk-cli/internal/flagutil/flags.go index cda1dc19a..79f5db123 100644 --- a/zSDKs/sdk-cli/internal/flagutil/flags.go +++ b/zSDKs/sdk-cli/internal/flagutil/flags.go @@ -36,6 +36,10 @@ func FlagChanged(cmd *cobra.Command, name string) bool { return false } +func IsAsyncResume(cmd *cobra.Command) bool { + return cmd != nil && cmd.Annotations["speakeasy_async"] != "" && FlagChanged(cmd, "resume") +} + // AnyFlagChanged reports whether any of the named flags was explicitly set. func AnyFlagChanged(cmd *cobra.Command, names ...string) bool { for _, name := range names { diff --git a/zSDKs/sdk-cli/internal/interactive/interactive.go b/zSDKs/sdk-cli/internal/interactive/interactive.go index 45d471482..0a48a658e 100644 --- a/zSDKs/sdk-cli/internal/interactive/interactive.go +++ b/zSDKs/sdk-cli/internal/interactive/interactive.go @@ -192,7 +192,7 @@ func interceptCommand(cmd *cobra.Command) { } bodyTookControl := false - if !usage.UsageRequested(runCmd) && !documentationRequested(runCmd) && Resolve(runCmd).PromptRequiredInputs() { + if !usage.UsageRequested(runCmd) && !documentationRequested(runCmd) && !flagutil.IsAsyncResume(runCmd) && Resolve(runCmd).PromptRequiredInputs() { supplied, err := wholeBodySupplied(runCmd) if err != nil { return err diff --git a/zSDKs/sdk-cli/internal/output/async.go b/zSDKs/sdk-cli/internal/output/async.go index 5c653dc42..f6858cf0c 100644 --- a/zSDKs/sdk-cli/internal/output/async.go +++ b/zSDKs/sdk-cli/internal/output/async.go @@ -15,6 +15,7 @@ import ( "time" "github.com/spf13/cobra" + "github.com/spf13/pflag" "openapi/internal/flagutil" ) @@ -165,9 +166,62 @@ func AsyncResult(cmd *cobra.Command, createResponse interface{}, newPoll AsyncPo return renderAsyncSnapshot(cmd, create, false) } fmt.Fprintln(cmd.OutOrStdout(), id) + fmt.Fprintln(cmd.ErrOrStderr(), "To resume polling: "+resume) return nil } + return pollAsyncResult(cmd, cfg, timings, id, started, newPoll) +} + +func ValidateAsyncResume(cmd *cobra.Command, args []string) error { + id, _ := flagutil.GetStringFlag(cmd, "resume") + if strings.TrimSpace(id) == "" { + return flagutil.WithCLIValidation(errors.New("--resume requires a non-empty operation handle")) + } + if len(args) > 0 { + return flagutil.WithCLIValidation(errors.New("--resume cannot be combined with create arguments")) + } + if escape, _ := flagutil.GetBoolFlag(cmd, "async"); escape { + return flagutil.WithCLIValidation(errors.New("--resume cannot be combined with --async")) + } + bodyFlag := cmd.Annotations[flagutil.AnnotationWholeBodyFlag] + var conflict string + cmd.LocalFlags().VisitAll(func(f *pflag.Flag) { + if conflict == "" && f.Changed && (f.Name == bodyFlag || len(f.Annotations[flagutil.AnnotationRequestInput]) > 0) { + conflict = f.Name + } + }) + if conflict != "" { + return flagutil.WithCLIValidation(fmt.Errorf("--resume cannot be combined with create request flag --%s", conflict)) + } + return ValidateAsyncFlags(cmd) +} + +func ResumeAsync(cmd *cobra.Command, id string, newPoll AsyncPollFactory) error { + cfg, err := activeAsync(cmd) + if err != nil { + return WithCLIReason(err, ReasonCLIProtocol) + } + timings, err := effectiveAsyncTimings(cmd, cfg) + if err != nil { + return err + } + if dryRun, _ := flagutil.GetBoolFlag(cmd, "dry-run"); dryRun { + poll, err := newPoll(cmd, id, cfg.Params) + if err != nil { + return Error(cmd, err) + } + _, err = poll(cmd.Context()) + if err != nil { + return Error(cmd, err) + } + return nil + } + return pollAsyncResult(cmd, cfg, timings, id, time.Now(), newPoll) +} + +func pollAsyncResult(cmd *cobra.Command, cfg *asyncConfig, timings asyncTimings, id string, started time.Time, newPoll AsyncPollFactory) error { + resume := asyncResume(cfg.Resume, id) poll, err := newPoll(cmd, id, cfg.Params) if err != nil { return Error(cmd, err) diff --git a/zSDKs/sdk-cli/internal/usage/schema.go b/zSDKs/sdk-cli/internal/usage/schema.go index 1aa9c2b6f..d5cbd3b59 100644 --- a/zSDKs/sdk-cli/internal/usage/schema.go +++ b/zSDKs/sdk-cli/internal/usage/schema.go @@ -13,12 +13,12 @@ import ( ) var usageSchemas = map[string]string{ - "": "name \"cli\"\nbin \"cli\"\nabout \"\"\"\n This document will show case as many of our features as possible in as little operations/models as possible.\n This will then generate a SDK that we can more easily review than the test SDKs based on uber.yaml spec.\n \"\"\"\nversion \"0.0.1\"\nconfig {\n file \"~/.config/cli/config.yaml\"\n}\nflag \"--usage\" help=\"Print the CLI Usage schema in KDL format\" global=#true\nflag \"-v --version\" help=\"Print the CLI version\"\nflag \"-o --output-format \" help=\"Specify the output format. Options: pretty, json, yaml, table, toon.\" global=#true config=\"output_format\" default=\"pretty\"\nflag \"--color \" help=\"Control colored output: auto (color when output is a TTY), always, or never. Respects NO_COLOR and FORCE_COLOR env vars.\" global=#true default=\"auto\"\nflag \"-q --jq \" help=\"Filter and transform output using a jq expression (e.g., '.name', '.items[] | .id')\" global=#true\nflag \"--raw-output\" help=\"Write --jq string results as raw text instead of JSON strings (like jq -r); non-string results stay JSON\" global=#true default=#false\nflag \"--server-url \" help=\"Override the default server URL\" global=#true\nflag \"--server \" help=\"Select a server by index (for indexed servers) or name (for named servers)\" global=#true\nflag \"-H --header \" help=\"Set a custom HTTP request header (format: \\\"Key: Value\\\"). Can be specified multiple times.\" global=#true var=#true\nflag \"--include-headers\" help=\"Include HTTP response headers in the output\" global=#true default=#false\nflag \"--timeout \" help=\"HTTP request timeout (e.g., 30s, 5m, 100ms)\" global=#true config=\"timeout\"\nflag \"--interactive\" help=\"Prompt for missing inputs and open guided configure/auth forms (forms fall back to line prompts on stdin off-TTY)\" global=#true default=#false\nflag \"--no-interactive\" help=\"Disable all interactive features (auto-prompting, explorer auto-launch, TUI forms)\" global=#true default=#false\nflag \"--dry-run\" help=\"Preview API requests without sending them (no network, no OS keychain). Human preview on stderr; with -o json or --jq, one JSON object per request on stdout. Local mutation commands (auth login, auth logout and configure) make no request: they skip prompts and writes and report a no-op (stderr, or one JSON object on stdout in the machine form)\" global=#true default=#false\nflag \"-d --debug\" help=\"Log request and response diagnostics to stderr\" global=#true default=#false\nflag \"--agent-mode\" help=\"Enable structured errors and default TOON output for AI coding agents.\" global=#true default=#false\nflag \"--no-retries\" help=\"Disable automatic retries (default: retries enabled with exponential backoff)\" global=#true config=\"no_retries\" default=#false\nflag \"--retry-max-elapsed-time \" help=\"Maximum total time for retries (e.g., 30s, 5m). Default: 30s\" global=#true config=\"retry_max_elapsed_time\"\nflag \"--retry-connection-errors\" help=\"Retry on connection errors (EOF, reset, etc.)\" global=#true config=\"retry_connection_errors\" default=#false\nflag \"--retry-config \" help=\"Full retry config as JSON. Schema: {\\\"strategy\\\":\\\"backoff\\\",\\\"backoff\\\":{\\\"initialInterval\\\":500,\\\"maxInterval\\\":10000,\\\"exponent\\\":1.5,\\\"maxElapsedTime\\\":30000},\\\"retryConnectionErrors\\\":false}. Times are in milliseconds.\" global=#true config=\"retry_config\"\nflag \"--subdomain \" help=\"Server template variable: subdomain\" global=#true\nflag \"--version-param \" help=\"Server template variable: version\" global=#true\nflag \"--host-name \" help=\"Server template variable: HostName\" global=#true\nflag \"--port \" help=\"Server template variable: PORT\" global=#true\nflag \"--username \" help=\"HTTP Basic username\" global=#true env=\"CLI_USERNAME\" config=\"security.username\"\nflag \"--password \" help=\"HTTP Basic password\" global=#true env=\"CLI_PASSWORD\" config=\"security.password\"\nflag \"--bearer-auth \" help=\"HTTP Bearer\" global=#true env=\"CLI_BEARER_AUTH\" config=\"security.bearer_auth\"\nflag \"--my-api-key \" help=\"API Key\" global=#true env=\"CLI_MY_API_KEY\" config=\"security.my_api_key\"\nflag \"--oauth2 \" help=\"OAuth2 Authorization\" global=#true env=\"CLI_OAUTH2\" config=\"security.oauth2\"\nflag \"--app-id \" help=\"Custom authentication credential\" global=#true env=\"CLI_APP_ID\" config=\"security.app_id\"\nflag \"--secret \" help=\"Custom authentication credential\" global=#true env=\"CLI_SECRET\" config=\"security.secret\"\nflag \"--mobile-auth \" help=\"\"\"\n OAuth2 Password flow, a flow that is too\n long to describe in a single line.\n \"\"\" global=#true env=\"CLI_MOBILE_AUTH\" config=\"security.mobile_auth\"\nflag \"--client-id \" help=\"Client Credentials flow. client identifier\" global=#true env=\"CLI_CLIENT_ID\" config=\"security.client_id\"\nflag \"--client-secret \" help=\"Client Credentials flow. client secret\" global=#true env=\"CLI_CLIENT_SECRET\" config=\"security.client_secret\"\nflag \"--token-url \" help=\"Client Credentials flow. token URL\" global=#true env=\"CLI_TOKEN_URL\" config=\"security.token_url\"\nflag \"--no-keyring\" help=\"Never read or write the OS keychain; store secrets in the config file instead\" global=#true env=\"CLI_NO_KEYRING\" config=\"no_keyring\" default=#false\nflag \"--query-param1 \" help=\"A long winded, multi-line description\" global=#true env=\"CLI_QUERY_PARAM1\" config=\"globals.query_param1\"\nflag \"--deprecated-query-param1 \" help=\"A deprecated description\" global=#true env=\"CLI_DEPRECATED_QUERY_PARAM1\" config=\"globals.deprecated_query_param1\"\nflag \"--deprecated-query-param2 \" help=\"Global deprecated-query-param2 parameter\" global=#true env=\"CLI_DEPRECATED_QUERY_PARAM2\" config=\"globals.deprecated_query_param2\"\nflag \"--lone-query-param \" help=\"Global lone-query-param parameter\" global=#true env=\"CLI_LONE_QUERY_PARAM\" config=\"globals.lone_query_param\"\ncmd \"invite\" help=\"Invite a user by email\" {\n arg \"email\" help=\"Email address to invite\" required=#true var=#true\n flag \"--body \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--given-name \" help=\"First name to record\"\n flag \"--user-gender \" help=\"Gender to record (e.g. male, female, other)\" {\n suggestions \"male\" \"female\" \"other\"\n }\n}\ncmd \"enroll\" help=\"Enroll a user by email\" {\n flag \"--body \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--enrollment-email \" help=\"Email address to enroll\"\n}\ncmd \"render\" help=\"Render an image asset to a file\" {\n arg \"prompt\" help=\"Text prompt to render\" required=#true var=#true\n flag \"--body \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--out \" help=\"Write the image to this file (or into this directory). Default: ./render-{timestamp}-{rand}.{ext}\"\n flag \"--raw-response\" help=\"Print the raw API response instead of writing the image to a file\"\n}\ncmd \"produce\" help=\"Produce an image asset and wait for completion\" {\n arg \"prompt\" help=\"Text prompt to render\" required=#true var=#true\n flag \"--body \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--out \" help=\"Write the image to this file (or into this directory). Default: ./produce-{timestamp}-{rand}.{ext}\"\n flag \"--raw-response\" help=\"Print the raw API response instead of writing the image to a file\"\n flag \"--async\" help=\"Return the operation handle without waiting for a terminal response\"\n flag \"--poll-interval \" help=\"Override the initial polling interval (positive Go duration, for example 500ms or 2s)\"\n flag \"--poll-timeout \" help=\"Override the overall polling deadline (positive Go duration, at least the effective poll interval)\"\n}\ncmd \"observe\" help=\"Produce an asset and print its terminal status\" {\n arg \"prompt\" help=\"Text prompt to render\" required=#true var=#true\n flag \"--body \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--async\" help=\"Return the operation handle without waiting for a terminal response\"\n flag \"--poll-interval \" help=\"Override the initial polling interval (positive Go duration, for example 500ms or 2s)\"\n flag \"--poll-timeout \" help=\"Override the overall polling deadline (positive Go duration, at least the effective poll interval)\"\n}\ncmd \"say\" help=\"Stream a chat reply\" {\n arg \"prompt\" help=\"Prompt to send\" required=#true var=#true\n flag \"--request \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--stream\" help=\"Stream events as they arrive (default: true)\" default=#true\n}\ncmd \"archive\" help=\"Archive a user account\"\ncmd \"test-group\" help=\"Operations for test-group\" {\n alias \"tg\"\n cmd \"tag2\" help=\"Operations for tag2\" {\n cmd \"post-test\" help=\"Post Test2\" {\n alias \"pt\"\n flag \"--obj \" help=\"A simple object that uses all our supported primitive types and enums and has optional properties. [required]\"\n flag \"-t --type \" help=\"options: type1, type2\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--output-file \" help=\"Save the response body to a file path (recommended for binary/file responses)\"\n flag \"--output-b64\" help=\"Encode binary response as base64 and print to stdout\"\n }\n }\n cmd \"tag3\" help=\"Operations for tag3\" {\n cmd \"post-test\" help=\"Post Test2\" {\n alias \"pt\"\n flag \"--obj \" help=\"A simple object that uses all our supported primitive types and enums and has optional properties. [required]\"\n flag \"-t --type \" help=\"options: type1, type2\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--output-file \" help=\"Save the response body to a file path (recommended for binary/file responses)\"\n flag \"--output-b64\" help=\"Encode binary response as base64 and print to stdout\"\n }\n }\n}\ncmd \"hello\" help=\"Hand-written command that survives regeneration\"\ncmd \"auth\" help=\"Manage authentication credentials\" {\n cmd \"login\" help=\"Interactively configure authentication credentials\"\n cmd \"logout\" help=\"Clear all stored authentication credentials\"\n cmd \"whoami\" help=\"Display current authentication and global parameter configuration\"\n}\ncmd \"binary-and-string-upload\" help=\"Binary And String Upload\" {\n alias \"basu\"\n flag \"-b --binary \" help=\"binary data (file: or b64:)\"\n flag \"-s --string \" help=\"string value\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\ncmd \"chat\" help=\"Chat\" {\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--stream\" help=\"Stream events as they arrive\" default=#true\n flag \"--max-tokens \" help=\"Maximum number of tokens to generate\"\n flag \"--temperature \" help=\"Sampling temperature\"\n}\ncmd \"completion\" help=\"Generate the autocompletion script for the specified shell\" {\n cmd \"bash\" help=\"Generate the autocompletion script for bash\"\n cmd \"fish\" help=\"Generate the autocompletion script for fish\"\n cmd \"powershell\" help=\"Generate the autocompletion script for powershell\"\n cmd \"zsh\" help=\"Generate the autocompletion script for zsh\"\n}\ncmd \"configure\" help=\"Configure authentication, global parameters, and preferences\" {\n flag \"--default-output-format \" help=\"Store the default output format without opening the form. Options: pretty, json, yaml, table, toon. Pass an empty value to clear it.\" {\n suggestions \"pretty\" \"json\" \"yaml\" \"table\" \"toon\"\n }\n}\ncmd \"create-user\" help=\"Create User\" {\n alias \"cu\"\n flag \"-i --id \" help=\"string value\"\n flag \"-e --email \" help=\"[required]\"\n flag \"-f --first-name \" help=\"string value\"\n flag \"-l --last-name \" help=\"string value\"\n flag \"--age \" help=\"number value\"\n flag \"-p --postal-code \" help=\"string value\"\n flag \"-g --gender \" help=\"options: male, female, other\"\n flag \"--associated-ids \" help=\"list of values\" var=#true\n flag \"--metadata \" help=\"JSON object\"\n flag \"--metadata-any \" help=\"A metadata object with additionalProperties true (any type) - should be flattened\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\ncmd \"create-with-union\" help=\"Create with discriminated union request body\" {\n alias \"cwu\"\n flag \"--dry-run-param\" help=\"If true, validates without creating\"\n flag \"-n --name \" help=\"Name for the shape [required]\"\n flag \"-s --shape \" help=\"{ \\\"type\\\": string, \\\"radius\\\": number, ... } | { \\\"type\\\": string, \\\"width\\\": number, \\\"height\\\": number, ... }\"\n flag \"--shape.circle \" help=\"Circle variant as JSON\"\n flag \"--shape.circle.radius \" help=\"The radius of the circle [required]\"\n flag \"--shape.circle.color-param \" help=\"Optional color of the circle\"\n flag \"--shape.rectangle \" help=\"Rectangle variant as JSON\"\n flag \"--shape.rectangle.width \" help=\"The width of the rectangle [required]\"\n flag \"--shape.rectangle.height \" help=\"The height of the rectangle [required]\"\n flag \"--shape.rectangle.color-param \" help=\"Optional color of the rectangle\"\n flag \"--description \" help=\"Optional description\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\ncmd \"delete-user\" help=\"Delete User\" {\n alias \"du\"\n arg \"id\" help=\"string value (or use --id)\"\n flag \"-i --id \" help=\"string value (or pass it as the [id] argument)\"\n}\ncmd \"explore\" help=\"Interactively browse and run commands\"\ncmd \"get-asset\" help=\"Get Asset\" {\n alias \"ga\"\n flag \"-i --id \" help=\"[required]\"\n flag \"-s --stream\" help=\"boolean flag\" default=#true\n}\ncmd \"get-binary-default-response\" help=\"Get Binary Default Response\" {\n alias \"gbdr\"\n flag \"--output-file \" help=\"Save the response body to a file path (recommended for binary/file responses)\"\n flag \"--output-b64\" help=\"Encode binary response as base64 and print to stdout\"\n}\ncmd \"get-duplicate-export-collision\" help=\"Tests that a spec-defined error type colliding with a built-in SDK error name does not cause TS2308\" {\n alias \"gdec\"\n}\ncmd \"get-empty-object-error\" help=\"Get Empty Object Error\" {\n alias \"geoe\"\n}\ncmd \"get-error-in-union\" help=\"Get Error In Union\" {\n alias \"geiu\"\n}\ncmd \"get-error-only-example\" help=\"Operation with example only on error response\" {\n alias \"geoex\"\n}\ncmd \"get-fully-flattened-request\" help=\"Get Fully Flattened Request\" {\n alias \"gffr\"\n flag \"-l --lang \" help=\"[required]\"\n flag \"-m --max-length \" help=\"integer value\"\n flag \"-n --name \" help=\"[required]\"\n flag \"-e --emoji \" help=\"options: wave, smiley, thumbsup, tada\"\n flag \"-g --gif \" help=\"string value\"\n flag \"-r --reply\" help=\"boolean flag\"\n flag \"-p --private\" help=\"boolean flag\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\ncmd \"get-named-primitive-union\" help=\"Test named primitive union options using title and x-speakeasy-name-override\" {\n alias \"gnpu\"\n}\ncmd \"get-nested-integer-string\" help=\"Test nested struct with integer:string tag\" {\n alias \"gnis\"\n}\ncmd \"get-polymorphism\" help=\"Get Polymorphism\" {\n alias \"gp\"\n}\ncmd \"get-positional-default\" help=\"Get an item with a default path identifier\" {\n alias \"gpd\"\n arg \"id\" help=\"string value (or use --id)\"\n flag \"-i --id \" help=\"string value (or pass it as the [id] argument)\" default=\"default-id\"\n}\ncmd \"get-request-body-flattened-away\" help=\"Get Request Body Flattened Away\" {\n alias \"grbfa\"\n}\ncmd \"get-union-errors\" help=\"Get Union Errors\" {\n alias \"gue\"\n flag \"-p --page \" help=\"[required]\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n}\ncmd \"get-user\" help=\"Get User\" {\n alias \"gu\"\n arg \"id\" help=\"string value (or use --id)\"\n flag \"-i --id \" help=\"string value (or pass it as the [id] argument)\"\n}\ncmd \"group\" help=\"Operations for group\" {\n cmd \"root-group-op\" help=\"An operation at the group's root level\" {\n alias \"rgo\"\n }\n cmd \"sub\" help=\"Operations for sub-group\" {\n cmd \"empty\" help=\"Operations for empty\" {\n cmd \"tail\" help=\"Operations for tail\" {\n cmd \"nested-group-op\" help=\"An operation at the group's deepest level\" {\n alias \"ngo\"\n }\n }\n }\n cmd \"op\" help=\"An operation at the group's top level\"\n }\n}\ncmd \"help\" help=\"Help about any command\"\ncmd \"login\" help=\"Login\"\ncmd \"namespace-tests\" help=\"Operations for namespace-tests\" {\n alias \"nt\"\n cmd \"conflicts\" help=\"Operations for conflicts\" {\n cmd \"create-namespace\" help=\"Create Namespace Conflict Test\" {\n alias \"cn\"\n flag \"-i --id \" help=\"[required]\"\n flag \"-n --name \" help=\"[required]\"\n flag \"-s --species \" help=\"The species of the pet (e.g., dog, cat) [required]\"\n flag \"-m --models \" help=\"A field name that often collides\"\n flag \"-r --request \" help=\"A field name that often collides\"\n flag \"--operations \" help=\"A field name that often collides\"\n flag \"-e --errors \" help=\"A field name that often collides\"\n flag \"-u --utils \" help=\"A field name that often collides\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n cmd \"get-namespace\" help=\"Get Namespace Conflict Test\" {\n alias \"gn\"\n }\n cmd \"get-pet-owners\" help=\"Get Pet Owners\" {\n alias \"gpo\"\n }\n cmd \"get-triple-namespace\" help=\"Get Triple Namespace Conflict Test\" {\n alias \"gtn\"\n }\n cmd \"put-namespace\" help=\"Put Property Name Conflicts Behind\" {\n alias \"pn\"\n flag \"--direct \" help=\"JSON object\"\n flag \"-n --namespaced \" help=\"JSON object\"\n flag \"--overridden \" help=\"JSON object\"\n flag \"-a --allof \" help=\"JSON object\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n }\n cmd \"single-bar\" help=\"Operations for single-bar\" {\n alias \"sb\"\n cmd \"get-single-namespace-bar-pet\" help=\"Get Single Namespace Bar Pet\" {\n alias \"gsnbp\"\n }\n }\n cmd \"single-foo\" help=\"Operations for single-foo\" {\n alias \"sf\"\n cmd \"create-single-namespace-foo-pet\" help=\"Create Single Namespace Foo Pet\" {\n alias \"csnfp\"\n flag \"-i --id \" help=\"[required]\"\n flag \"-n --name \" help=\"[required]\"\n flag \"-s --species \" help=\"The species of the pet (e.g., dog, cat) [required]\"\n flag \"-m --models \" help=\"A field name that often collides\"\n flag \"-r --request \" help=\"A field name that often collides\"\n flag \"--operations \" help=\"A field name that often collides\"\n flag \"-e --errors \" help=\"A field name that often collides\"\n flag \"-u --utils \" help=\"A field name that often collides\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n cmd \"get-single-namespace-foo-pet\" help=\"Get Single Namespace Foo Pet\" {\n alias \"gsnfp\"\n }\n }\n cmd \"types\" help=\"Operations for types\" {\n cmd \"get-namespace\" help=\"Get Namespace Types Test\" {\n alias \"gn\"\n }\n cmd \"get-namespace-animal\" help=\"Get Namespace Animal (Discriminated Union)\" {\n alias \"gna\"\n }\n cmd \"get-namespace-organization\" help=\"Get Namespace Organization (Nested Inline Schemas)\" {\n alias \"gno\"\n }\n cmd \"get-namespace-vehicle\" help=\"Get Namespace Vehicle (Non-Discriminated Union)\" {\n alias \"gnv\"\n }\n }\n}\ncmd \"obsolete\" help=\"A subSDK in which all operations are deprecated\" {\n cmd \"deprecated1\" help=\"Deprecated Operation\"\n}\ncmd \"operation-with-leading-and-trailing-underscores\" help=\"Operation With Leading And Trailing Underscores\" {\n alias \"owlatu\"\n flag \"--qp1 \" help=\"This parameter will not be filled in with the queryParam1 global because it uses x-speakeasy-name-override which results in a non-matching name. [required]\"\n}\ncmd \"parentheses-in-path-allowed\" help=\"A string with \\\\x7b\\\\x7b double braces \\\\x7d\\\\x7d and { single braces }\\\\nand \\\\\\\\{\\\\\\\\{ escaped curlies \\\\\\\\}\\\\\\\\} and `backticks`.\\\\nand \\\\\\\\`escaped backticks\\\\\\\\` and double slashes\\\\\\\\\\\\\\\\\\\\nand 'single quotes' and \\\\\\\"double quotes\\\\\\\".\\\\nand \\\\\\\\'escaped single quotes\\\\\\\\' and \\\\\\\\\\\\\\\"escaped double quotes\\\\\\\\\\\\\\\".\" {\n alias \"pipa\"\n arg \"id\" help=\"string value (or use --id)\"\n flag \"-i --id \" help=\"string value (or pass it as the [id] argument)\"\n flag \"--field-with-braces-in-description \" help=\"\"\"\n A string with " + string([]byte{'{', '{'}) + " double braces " + string([]byte{'}', '}'}) + " and { single braces }\n and \\\\{\\\\{ escaped curlies \\\\}\\\\} and 'backticks'.\n and \\\\'escaped backticks\\\\' and double slashes\\\\\\\\\n and 'single quotes' and \"double quotes\".\n and \\\\'escaped single quotes\\\\' and \\\\\"escaped double quotes\\\\\".\n \"\"\"\n flag \"--field-with-braces-in-default \" help=\"string value\" default=\"\"\"\n A string with " + string([]byte{'{', '{'}) + " double braces " + string([]byte{'}', '}'}) + " and { single braces }\n and \\\\{\\\\{ escaped curlies \\\\}\\\\} and `backticks`.\n and \\\\`escaped backticks\\\\` and double slashes\\\\\\\\\n and 'single quotes' and \"double quotes\".\n and \\\\'escaped single quotes\\\\' and \\\\\"escaped double quotes\\\\\".\n\n \"\"\"\n flag \"--field-with-braces-in-title \" help=\"string value\"\n flag \"--field-with-braces-in-example \" help=\"string value\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\ncmd \"post-file\" help=\"Post File\" {\n alias \"pf\"\n flag \"-u --upload \" help=\"A file\"\n flag \"--output-file \" help=\"Save the response body to a file path (recommended for binary/file responses)\"\n flag \"--output-b64\" help=\"Encode binary response as base64 and print to stdout\"\n}\ncmd \"render-asset\" help=\"Render Asset\" {\n alias \"ra\"\n flag \"-p --prompt \" help=\"[required]\"\n flag \"--output-modalities \" help=\"options: text, image\" var=#true\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\ncmd \"tag1\" help=\"The first tag\" {\n cmd \"recent\" help=\"List recent test1 pages via tag1\" {\n flag \"-p --page \" help=\"[required]\"\n flag \"--query-param2 \" help=\"\"\"\n An [enum](https://enum.com) \"query parameter\"\n that is not easily described in a single line.\n\n **Available Values:**\n | Value | Description |\n |-------|-------------|\n | 0 | No data |\n | 1 | Partial |\n | 2 | Complete |\n (options: 0, 1, 2) [required]\n \"\"\"\n flag \"--header-param1 \" help=\"[required]\"\n }\n cmd \"auth\" help=\"\"\"\n This operation aims at testing available OAuth2 scopes collection:\n - only operation with oauth2 authorizationCode security flow\n - belongs to a subSDK\n \"\"\" {\n flag \"--access-token \" help=\"Security credential\"\n }\n cmd \"deprecated1\" help=\"Deprecated Operation\"\n cmd \"list-test1\" help=\"Get Test1\" {\n alias \"lt\"\n arg \"page\" help=\"integer value (or use --page)\"\n flag \"-p --page \" help=\"integer value (or pass it as the [page] argument)\"\n flag \"--query-param2 \" help=\"\"\"\n An [enum](https://enum.com) \"query parameter\"\n that is not easily described in a single line.\n\n **Available Values:**\n | Value | Description |\n |-------|-------------|\n | 0 | No data |\n | 1 | Partial |\n | 2 | Complete |\n (options: 0, 1, 2) [required]\n \"\"\"\n flag \"--header-param1 \" help=\"[required]\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n }\n cmd \"post-file-with-encoding\" help=\"Post File With Encoding\" {\n alias \"pfwe\"\n flag \"--file \" help=\"The file to upload (supports CSV, PNG, JPEG, or PDF)\"\n flag \"-a --attachment \" help=\"An optional binary attachment\"\n flag \"--file-name \" help=\"Optional custom file name\"\n flag \"--file-purpose \" help=\"Purpose of the file upload\"\n flag \"-m --metadata \" help=\"JSON metadata about the file\"\n }\n}\ncmd \"test-endpoint\" help=\"Test Endpoint\" {\n alias \"te\"\n arg \"test-name\" help=\"string value (or use --test-name)\"\n flag \"-t --test-name \" help=\"string value (or pass it as the [test-name] argument)\"\n flag \"--test \" help=\"[required]\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\ncmd \"test-enum-formats\" help=\"Test x-speakeasy-enums in different formats\" {\n alias \"tef\"\n flag \"--string-array-format \" help=\"String enum with x-speakeasy-enums as array (full coverage) (options: pending_review, approved_final, rejected_permanently, arch_x86, mobile_ios, custom_field) [required]\"\n flag \"--string-map-format \" help=\"String enum with x-speakeasy-enums as map (full coverage) (options: low_priority, medium_priority, high_priority, critical_urgent) [required]\"\n flag \"--string-partial-map-format \" help=\"String enum with x-speakeasy-enums as map (partial coverage) (options: draft_mode, under_review, published_live, archived_old) [required]\"\n flag \"--integer-map-format \" help=\"Integer enum with x-speakeasy-enums as map (full coverage) (options: 100, 200, 300, 400) [required]\"\n flag \"--integer-partial-map-format \" help=\"Integer enum with x-speakeasy-enums as map (partial coverage) (options: 1, 2, 3, 4, 5) [required]\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\ncmd \"update-user\" help=\"Update User\" {\n alias \"uu\"\n arg \"id\" help=\"string value (or use --id)\"\n flag \"-i --id \" help=\"string value (or pass it as the [id] argument)\"\n flag \"--user.id \" help=\"[required]\"\n flag \"--user.email \" help=\"[required]\"\n flag \"--user.first-name \" help=\"string value\"\n flag \"--user.last-name \" help=\"string value\"\n flag \"--user.age \" help=\"number value\"\n flag \"--user.postal-code \" help=\"string value\"\n flag \"--user.gender \" help=\"options: male, female, other\"\n flag \"--user.associated-ids \" help=\"list of values\" var=#true\n flag \"--user.metadata \" help=\"JSON object\"\n flag \"--user.metadata-any \" help=\"A metadata object with additionalProperties true (any type) - should be flattened\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\ncmd \"url-validation-stress-test\" help=\"Url Validation Stress Test\" {\n alias \"uvst\"\n}\ncmd \"validate\" help=\"Validate\"\ncmd \"version\" help=\"Print the CLI version\"\ncmd \"whoami\" help=\"Display current authentication and global parameter configuration\"\n", + "": "name \"cli\"\nbin \"cli\"\nabout \"\"\"\n This document will show case as many of our features as possible in as little operations/models as possible.\n This will then generate a SDK that we can more easily review than the test SDKs based on uber.yaml spec.\n \"\"\"\nversion \"0.0.1\"\nconfig {\n file \"~/.config/cli/config.yaml\"\n}\nflag \"--usage\" help=\"Print the CLI Usage schema in KDL format\" global=#true\nflag \"-v --version\" help=\"Print the CLI version\"\nflag \"-o --output-format \" help=\"Specify the output format. Options: pretty, json, yaml, table, toon.\" global=#true config=\"output_format\" default=\"pretty\"\nflag \"--color \" help=\"Control colored output: auto (color when output is a TTY), always, or never. Respects NO_COLOR and FORCE_COLOR env vars.\" global=#true default=\"auto\"\nflag \"-q --jq \" help=\"Filter and transform output using a jq expression (e.g., '.name', '.items[] | .id')\" global=#true\nflag \"--raw-output\" help=\"Write --jq string results as raw text instead of JSON strings (like jq -r); non-string results stay JSON\" global=#true default=#false\nflag \"--server-url \" help=\"Override the default server URL\" global=#true\nflag \"--server \" help=\"Select a server by index (for indexed servers) or name (for named servers)\" global=#true\nflag \"-H --header \" help=\"Set a custom HTTP request header (format: \\\"Key: Value\\\"). Can be specified multiple times.\" global=#true var=#true\nflag \"--include-headers\" help=\"Include HTTP response headers in the output\" global=#true default=#false\nflag \"--timeout \" help=\"HTTP request timeout (e.g., 30s, 5m, 100ms)\" global=#true config=\"timeout\"\nflag \"--interactive\" help=\"Prompt for missing inputs and open guided configure/auth forms (forms fall back to line prompts on stdin off-TTY)\" global=#true default=#false\nflag \"--no-interactive\" help=\"Disable all interactive features (auto-prompting, explorer auto-launch, TUI forms)\" global=#true default=#false\nflag \"--dry-run\" help=\"Preview API requests without sending them (no network, no OS keychain). Human preview on stderr; with -o json or --jq, one JSON object per request on stdout. Local mutation commands (auth login, auth logout and configure) make no request: they skip prompts and writes and report a no-op (stderr, or one JSON object on stdout in the machine form)\" global=#true default=#false\nflag \"-d --debug\" help=\"Log request and response diagnostics to stderr\" global=#true default=#false\nflag \"--agent-mode\" help=\"Enable structured errors and default TOON output for AI coding agents.\" global=#true default=#false\nflag \"--no-retries\" help=\"Disable automatic retries (default: retries enabled with exponential backoff)\" global=#true config=\"no_retries\" default=#false\nflag \"--retry-max-elapsed-time \" help=\"Maximum total time for retries (e.g., 30s, 5m). Default: 30s\" global=#true config=\"retry_max_elapsed_time\"\nflag \"--retry-connection-errors\" help=\"Retry on connection errors (EOF, reset, etc.)\" global=#true config=\"retry_connection_errors\" default=#false\nflag \"--retry-config \" help=\"Full retry config as JSON. Schema: {\\\"strategy\\\":\\\"backoff\\\",\\\"backoff\\\":{\\\"initialInterval\\\":500,\\\"maxInterval\\\":10000,\\\"exponent\\\":1.5,\\\"maxElapsedTime\\\":30000},\\\"retryConnectionErrors\\\":false}. Times are in milliseconds.\" global=#true config=\"retry_config\"\nflag \"--subdomain \" help=\"Server template variable: subdomain\" global=#true\nflag \"--version-param \" help=\"Server template variable: version\" global=#true\nflag \"--host-name \" help=\"Server template variable: HostName\" global=#true\nflag \"--port \" help=\"Server template variable: PORT\" global=#true\nflag \"--username \" help=\"HTTP Basic username\" global=#true env=\"CLI_USERNAME\" config=\"security.username\"\nflag \"--password \" help=\"HTTP Basic password\" global=#true env=\"CLI_PASSWORD\" config=\"security.password\"\nflag \"--bearer-auth \" help=\"HTTP Bearer\" global=#true env=\"CLI_BEARER_AUTH\" config=\"security.bearer_auth\"\nflag \"--my-api-key \" help=\"API Key\" global=#true env=\"CLI_MY_API_KEY\" config=\"security.my_api_key\"\nflag \"--oauth2 \" help=\"OAuth2 Authorization\" global=#true env=\"CLI_OAUTH2\" config=\"security.oauth2\"\nflag \"--app-id \" help=\"Custom authentication credential\" global=#true env=\"CLI_APP_ID\" config=\"security.app_id\"\nflag \"--secret \" help=\"Custom authentication credential\" global=#true env=\"CLI_SECRET\" config=\"security.secret\"\nflag \"--mobile-auth \" help=\"\"\"\n OAuth2 Password flow, a flow that is too\n long to describe in a single line.\n \"\"\" global=#true env=\"CLI_MOBILE_AUTH\" config=\"security.mobile_auth\"\nflag \"--client-id \" help=\"Client Credentials flow. client identifier\" global=#true env=\"CLI_CLIENT_ID\" config=\"security.client_id\"\nflag \"--client-secret \" help=\"Client Credentials flow. client secret\" global=#true env=\"CLI_CLIENT_SECRET\" config=\"security.client_secret\"\nflag \"--token-url \" help=\"Client Credentials flow. token URL\" global=#true env=\"CLI_TOKEN_URL\" config=\"security.token_url\"\nflag \"--no-keyring\" help=\"Never read or write the OS keychain; store secrets in the config file instead\" global=#true env=\"CLI_NO_KEYRING\" config=\"no_keyring\" default=#false\nflag \"--query-param1 \" help=\"A long winded, multi-line description\" global=#true env=\"CLI_QUERY_PARAM1\" config=\"globals.query_param1\"\nflag \"--deprecated-query-param1 \" help=\"A deprecated description\" global=#true env=\"CLI_DEPRECATED_QUERY_PARAM1\" config=\"globals.deprecated_query_param1\"\nflag \"--deprecated-query-param2 \" help=\"Global deprecated-query-param2 parameter\" global=#true env=\"CLI_DEPRECATED_QUERY_PARAM2\" config=\"globals.deprecated_query_param2\"\nflag \"--lone-query-param \" help=\"Global lone-query-param parameter\" global=#true env=\"CLI_LONE_QUERY_PARAM\" config=\"globals.lone_query_param\"\ncmd \"invite\" help=\"Invite a user by email\" {\n arg \"email\" help=\"Email address to invite\" required=#true var=#true\n flag \"--body \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--given-name \" help=\"First name to record\"\n flag \"--user-gender \" help=\"Gender to record (e.g. male, female, other)\" {\n suggestions \"male\" \"female\" \"other\"\n }\n}\ncmd \"enroll\" help=\"Enroll a user by email\" {\n flag \"--body \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--enrollment-email \" help=\"Email address to enroll\"\n}\ncmd \"render\" help=\"Render an image asset to a file\" {\n arg \"prompt\" help=\"Text prompt to render\" required=#true var=#true\n flag \"--body \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--out \" help=\"Write the image to this file (or into this directory). Default: ./render-{timestamp}-{rand}.{ext}\"\n flag \"--raw-response\" help=\"Print the raw API response instead of writing the image to a file\"\n}\ncmd \"produce\" help=\"Produce an image asset and wait for completion\" {\n arg \"prompt\" help=\"Text prompt to render (create input; omit with --resume)\" var=#true\n flag \"--body \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--out \" help=\"Write the image to this file (or into this directory). Default: ./produce-{timestamp}-{rand}.{ext}\"\n flag \"--raw-response\" help=\"Print the raw API response instead of writing the image to a file\"\n flag \"--resume \" help=\"Resume polling an existing operation by ID instead of creating one\"\n flag \"--async\" help=\"Return the operation handle without waiting for a terminal response\"\n flag \"--poll-interval \" help=\"Override the initial polling interval (positive Go duration, for example 500ms or 2s)\"\n flag \"--poll-timeout \" help=\"Override the overall polling deadline (positive Go duration, at least the effective poll interval)\"\n}\ncmd \"observe\" help=\"Produce an asset and print its terminal status\" {\n arg \"prompt\" help=\"Text prompt to render (create input; omit with --resume)\" var=#true\n flag \"--body \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--resume \" help=\"Resume polling an existing operation by ID instead of creating one\"\n flag \"--async\" help=\"Return the operation handle without waiting for a terminal response\"\n flag \"--poll-interval \" help=\"Override the initial polling interval (positive Go duration, for example 500ms or 2s)\"\n flag \"--poll-timeout \" help=\"Override the overall polling deadline (positive Go duration, at least the effective poll interval)\"\n}\ncmd \"say\" help=\"Stream a chat reply\" {\n arg \"prompt\" help=\"Prompt to send\" required=#true var=#true\n flag \"--request \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--stream\" help=\"Stream events as they arrive (default: true)\" default=#true\n}\ncmd \"archive\" help=\"Archive a user account\"\ncmd \"test-group\" help=\"Operations for test-group\" {\n alias \"tg\"\n cmd \"tag2\" help=\"Operations for tag2\" {\n cmd \"post-test\" help=\"Post Test2\" {\n alias \"pt\"\n flag \"--obj \" help=\"A simple object that uses all our supported primitive types and enums and has optional properties. [required]\"\n flag \"-t --type \" help=\"options: type1, type2\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--output-file \" help=\"Save the response body to a file path (recommended for binary/file responses)\"\n flag \"--output-b64\" help=\"Encode binary response as base64 and print to stdout\"\n }\n }\n cmd \"tag3\" help=\"Operations for tag3\" {\n cmd \"post-test\" help=\"Post Test2\" {\n alias \"pt\"\n flag \"--obj \" help=\"A simple object that uses all our supported primitive types and enums and has optional properties. [required]\"\n flag \"-t --type \" help=\"options: type1, type2\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--output-file \" help=\"Save the response body to a file path (recommended for binary/file responses)\"\n flag \"--output-b64\" help=\"Encode binary response as base64 and print to stdout\"\n }\n }\n}\ncmd \"hello\" help=\"Hand-written command that survives regeneration\"\ncmd \"auth\" help=\"Manage authentication credentials\" {\n cmd \"login\" help=\"Interactively configure authentication credentials\"\n cmd \"logout\" help=\"Clear all stored authentication credentials\"\n cmd \"whoami\" help=\"Display current authentication and global parameter configuration\"\n}\ncmd \"binary-and-string-upload\" help=\"Binary And String Upload\" {\n alias \"basu\"\n flag \"-b --binary \" help=\"binary data (file: or b64:)\"\n flag \"-s --string \" help=\"string value\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\ncmd \"chat\" help=\"Chat\" {\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--stream\" help=\"Stream events as they arrive\" default=#true\n flag \"--max-tokens \" help=\"Maximum number of tokens to generate\"\n flag \"--temperature \" help=\"Sampling temperature\"\n}\ncmd \"completion\" help=\"Generate the autocompletion script for the specified shell\" {\n cmd \"bash\" help=\"Generate the autocompletion script for bash\"\n cmd \"fish\" help=\"Generate the autocompletion script for fish\"\n cmd \"powershell\" help=\"Generate the autocompletion script for powershell\"\n cmd \"zsh\" help=\"Generate the autocompletion script for zsh\"\n}\ncmd \"configure\" help=\"Configure authentication, global parameters, and preferences\" {\n flag \"--default-output-format \" help=\"Store the default output format without opening the form. Options: pretty, json, yaml, table, toon. Pass an empty value to clear it.\" {\n suggestions \"pretty\" \"json\" \"yaml\" \"table\" \"toon\"\n }\n}\ncmd \"create-user\" help=\"Create User\" {\n alias \"cu\"\n flag \"-i --id \" help=\"string value\"\n flag \"-e --email \" help=\"[required]\"\n flag \"-f --first-name \" help=\"string value\"\n flag \"-l --last-name \" help=\"string value\"\n flag \"--age \" help=\"number value\"\n flag \"-p --postal-code \" help=\"string value\"\n flag \"-g --gender \" help=\"options: male, female, other\"\n flag \"--associated-ids \" help=\"list of values\" var=#true\n flag \"--metadata \" help=\"JSON object\"\n flag \"--metadata-any \" help=\"A metadata object with additionalProperties true (any type) - should be flattened\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\ncmd \"create-with-union\" help=\"Create with discriminated union request body\" {\n alias \"cwu\"\n flag \"--dry-run-param\" help=\"If true, validates without creating\"\n flag \"-n --name \" help=\"Name for the shape [required]\"\n flag \"-s --shape \" help=\"{ \\\"type\\\": string, \\\"radius\\\": number, ... } | { \\\"type\\\": string, \\\"width\\\": number, \\\"height\\\": number, ... }\"\n flag \"--shape.circle \" help=\"Circle variant as JSON\"\n flag \"--shape.circle.radius \" help=\"The radius of the circle [required]\"\n flag \"--shape.circle.color-param \" help=\"Optional color of the circle\"\n flag \"--shape.rectangle \" help=\"Rectangle variant as JSON\"\n flag \"--shape.rectangle.width \" help=\"The width of the rectangle [required]\"\n flag \"--shape.rectangle.height \" help=\"The height of the rectangle [required]\"\n flag \"--shape.rectangle.color-param \" help=\"Optional color of the rectangle\"\n flag \"--description \" help=\"Optional description\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\ncmd \"delete-user\" help=\"Delete User\" {\n alias \"du\"\n arg \"id\" help=\"string value (or use --id)\"\n flag \"-i --id \" help=\"string value (or pass it as the [id] argument)\"\n}\ncmd \"explore\" help=\"Interactively browse and run commands\"\ncmd \"get-asset\" help=\"Get Asset\" {\n alias \"ga\"\n flag \"-i --id \" help=\"[required]\"\n flag \"-s --stream\" help=\"boolean flag\" default=#true\n}\ncmd \"get-binary-default-response\" help=\"Get Binary Default Response\" {\n alias \"gbdr\"\n flag \"--output-file \" help=\"Save the response body to a file path (recommended for binary/file responses)\"\n flag \"--output-b64\" help=\"Encode binary response as base64 and print to stdout\"\n}\ncmd \"get-duplicate-export-collision\" help=\"Tests that a spec-defined error type colliding with a built-in SDK error name does not cause TS2308\" {\n alias \"gdec\"\n}\ncmd \"get-empty-object-error\" help=\"Get Empty Object Error\" {\n alias \"geoe\"\n}\ncmd \"get-error-in-union\" help=\"Get Error In Union\" {\n alias \"geiu\"\n}\ncmd \"get-error-only-example\" help=\"Operation with example only on error response\" {\n alias \"geoex\"\n}\ncmd \"get-fully-flattened-request\" help=\"Get Fully Flattened Request\" {\n alias \"gffr\"\n flag \"-l --lang \" help=\"[required]\"\n flag \"-m --max-length \" help=\"integer value\"\n flag \"-n --name \" help=\"[required]\"\n flag \"-e --emoji \" help=\"options: wave, smiley, thumbsup, tada\"\n flag \"-g --gif \" help=\"string value\"\n flag \"-r --reply\" help=\"boolean flag\"\n flag \"-p --private\" help=\"boolean flag\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\ncmd \"get-named-primitive-union\" help=\"Test named primitive union options using title and x-speakeasy-name-override\" {\n alias \"gnpu\"\n}\ncmd \"get-nested-integer-string\" help=\"Test nested struct with integer:string tag\" {\n alias \"gnis\"\n}\ncmd \"get-polymorphism\" help=\"Get Polymorphism\" {\n alias \"gp\"\n}\ncmd \"get-positional-default\" help=\"Get an item with a default path identifier\" {\n alias \"gpd\"\n arg \"id\" help=\"string value (or use --id)\"\n flag \"-i --id \" help=\"string value (or pass it as the [id] argument)\" default=\"default-id\"\n}\ncmd \"get-request-body-flattened-away\" help=\"Get Request Body Flattened Away\" {\n alias \"grbfa\"\n}\ncmd \"get-union-errors\" help=\"Get Union Errors\" {\n alias \"gue\"\n flag \"-p --page \" help=\"[required]\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n}\ncmd \"get-user\" help=\"Get User\" {\n alias \"gu\"\n arg \"id\" help=\"string value (or use --id)\"\n flag \"-i --id \" help=\"string value (or pass it as the [id] argument)\"\n}\ncmd \"group\" help=\"Operations for group\" {\n cmd \"root-group-op\" help=\"An operation at the group's root level\" {\n alias \"rgo\"\n }\n cmd \"sub\" help=\"Operations for sub-group\" {\n cmd \"empty\" help=\"Operations for empty\" {\n cmd \"tail\" help=\"Operations for tail\" {\n cmd \"nested-group-op\" help=\"An operation at the group's deepest level\" {\n alias \"ngo\"\n }\n }\n }\n cmd \"op\" help=\"An operation at the group's top level\"\n }\n}\ncmd \"help\" help=\"Help about any command\"\ncmd \"login\" help=\"Login\"\ncmd \"namespace-tests\" help=\"Operations for namespace-tests\" {\n alias \"nt\"\n cmd \"conflicts\" help=\"Operations for conflicts\" {\n cmd \"create-namespace\" help=\"Create Namespace Conflict Test\" {\n alias \"cn\"\n flag \"-i --id \" help=\"[required]\"\n flag \"-n --name \" help=\"[required]\"\n flag \"-s --species \" help=\"The species of the pet (e.g., dog, cat) [required]\"\n flag \"-m --models \" help=\"A field name that often collides\"\n flag \"-r --request \" help=\"A field name that often collides\"\n flag \"--operations \" help=\"A field name that often collides\"\n flag \"-e --errors \" help=\"A field name that often collides\"\n flag \"-u --utils \" help=\"A field name that often collides\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n cmd \"get-namespace\" help=\"Get Namespace Conflict Test\" {\n alias \"gn\"\n }\n cmd \"get-pet-owners\" help=\"Get Pet Owners\" {\n alias \"gpo\"\n }\n cmd \"get-triple-namespace\" help=\"Get Triple Namespace Conflict Test\" {\n alias \"gtn\"\n }\n cmd \"put-namespace\" help=\"Put Property Name Conflicts Behind\" {\n alias \"pn\"\n flag \"--direct \" help=\"JSON object\"\n flag \"-n --namespaced \" help=\"JSON object\"\n flag \"--overridden \" help=\"JSON object\"\n flag \"-a --allof \" help=\"JSON object\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n }\n cmd \"single-bar\" help=\"Operations for single-bar\" {\n alias \"sb\"\n cmd \"get-single-namespace-bar-pet\" help=\"Get Single Namespace Bar Pet\" {\n alias \"gsnbp\"\n }\n }\n cmd \"single-foo\" help=\"Operations for single-foo\" {\n alias \"sf\"\n cmd \"create-single-namespace-foo-pet\" help=\"Create Single Namespace Foo Pet\" {\n alias \"csnfp\"\n flag \"-i --id \" help=\"[required]\"\n flag \"-n --name \" help=\"[required]\"\n flag \"-s --species \" help=\"The species of the pet (e.g., dog, cat) [required]\"\n flag \"-m --models \" help=\"A field name that often collides\"\n flag \"-r --request \" help=\"A field name that often collides\"\n flag \"--operations \" help=\"A field name that often collides\"\n flag \"-e --errors \" help=\"A field name that often collides\"\n flag \"-u --utils \" help=\"A field name that often collides\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n }\n cmd \"get-single-namespace-foo-pet\" help=\"Get Single Namespace Foo Pet\" {\n alias \"gsnfp\"\n }\n }\n cmd \"types\" help=\"Operations for types\" {\n cmd \"get-namespace\" help=\"Get Namespace Types Test\" {\n alias \"gn\"\n }\n cmd \"get-namespace-animal\" help=\"Get Namespace Animal (Discriminated Union)\" {\n alias \"gna\"\n }\n cmd \"get-namespace-organization\" help=\"Get Namespace Organization (Nested Inline Schemas)\" {\n alias \"gno\"\n }\n cmd \"get-namespace-vehicle\" help=\"Get Namespace Vehicle (Non-Discriminated Union)\" {\n alias \"gnv\"\n }\n }\n}\ncmd \"obsolete\" help=\"A subSDK in which all operations are deprecated\" {\n cmd \"deprecated1\" help=\"Deprecated Operation\"\n}\ncmd \"operation-with-leading-and-trailing-underscores\" help=\"Operation With Leading And Trailing Underscores\" {\n alias \"owlatu\"\n flag \"--qp1 \" help=\"This parameter will not be filled in with the queryParam1 global because it uses x-speakeasy-name-override which results in a non-matching name. [required]\"\n}\ncmd \"parentheses-in-path-allowed\" help=\"A string with \\\\x7b\\\\x7b double braces \\\\x7d\\\\x7d and { single braces }\\\\nand \\\\\\\\{\\\\\\\\{ escaped curlies \\\\\\\\}\\\\\\\\} and `backticks`.\\\\nand \\\\\\\\`escaped backticks\\\\\\\\` and double slashes\\\\\\\\\\\\\\\\\\\\nand 'single quotes' and \\\\\\\"double quotes\\\\\\\".\\\\nand \\\\\\\\'escaped single quotes\\\\\\\\' and \\\\\\\\\\\\\\\"escaped double quotes\\\\\\\\\\\\\\\".\" {\n alias \"pipa\"\n arg \"id\" help=\"string value (or use --id)\"\n flag \"-i --id \" help=\"string value (or pass it as the [id] argument)\"\n flag \"--field-with-braces-in-description \" help=\"\"\"\n A string with " + string([]byte{'{', '{'}) + " double braces " + string([]byte{'}', '}'}) + " and { single braces }\n and \\\\{\\\\{ escaped curlies \\\\}\\\\} and 'backticks'.\n and \\\\'escaped backticks\\\\' and double slashes\\\\\\\\\n and 'single quotes' and \"double quotes\".\n and \\\\'escaped single quotes\\\\' and \\\\\"escaped double quotes\\\\\".\n \"\"\"\n flag \"--field-with-braces-in-default \" help=\"string value\" default=\"\"\"\n A string with " + string([]byte{'{', '{'}) + " double braces " + string([]byte{'}', '}'}) + " and { single braces }\n and \\\\{\\\\{ escaped curlies \\\\}\\\\} and `backticks`.\n and \\\\`escaped backticks\\\\` and double slashes\\\\\\\\\n and 'single quotes' and \"double quotes\".\n and \\\\'escaped single quotes\\\\' and \\\\\"escaped double quotes\\\\\".\n\n \"\"\"\n flag \"--field-with-braces-in-title \" help=\"string value\"\n flag \"--field-with-braces-in-example \" help=\"string value\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\ncmd \"post-file\" help=\"Post File\" {\n alias \"pf\"\n flag \"-u --upload \" help=\"A file\"\n flag \"--output-file \" help=\"Save the response body to a file path (recommended for binary/file responses)\"\n flag \"--output-b64\" help=\"Encode binary response as base64 and print to stdout\"\n}\ncmd \"render-asset\" help=\"Render Asset\" {\n alias \"ra\"\n flag \"-p --prompt \" help=\"[required]\"\n flag \"--output-modalities \" help=\"options: text, image\" var=#true\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\ncmd \"tag1\" help=\"The first tag\" {\n cmd \"recent\" help=\"List recent test1 pages via tag1\" {\n flag \"-p --page \" help=\"[required]\"\n flag \"--query-param2 \" help=\"\"\"\n An [enum](https://enum.com) \"query parameter\"\n that is not easily described in a single line.\n\n **Available Values:**\n | Value | Description |\n |-------|-------------|\n | 0 | No data |\n | 1 | Partial |\n | 2 | Complete |\n (options: 0, 1, 2) [required]\n \"\"\"\n flag \"--header-param1 \" help=\"[required]\"\n }\n cmd \"auth\" help=\"\"\"\n This operation aims at testing available OAuth2 scopes collection:\n - only operation with oauth2 authorizationCode security flow\n - belongs to a subSDK\n \"\"\" {\n flag \"--access-token \" help=\"Security credential\"\n }\n cmd \"deprecated1\" help=\"Deprecated Operation\"\n cmd \"list-test1\" help=\"Get Test1\" {\n alias \"lt\"\n arg \"page\" help=\"integer value (or use --page)\"\n flag \"-p --page \" help=\"integer value (or pass it as the [page] argument)\"\n flag \"--query-param2 \" help=\"\"\"\n An [enum](https://enum.com) \"query parameter\"\n that is not easily described in a single line.\n\n **Available Values:**\n | Value | Description |\n |-------|-------------|\n | 0 | No data |\n | 1 | Partial |\n | 2 | Complete |\n (options: 0, 1, 2) [required]\n \"\"\"\n flag \"--header-param1 \" help=\"[required]\"\n flag \"-a --all\" help=\"Automatically paginate and fetch all results (streams NDJSON for JSON output)\"\n flag \"--max-pages \" help=\"Maximum number of pages to fetch when using --all (0 = no limit)\" default=0\n }\n cmd \"post-file-with-encoding\" help=\"Post File With Encoding\" {\n alias \"pfwe\"\n flag \"--file \" help=\"The file to upload (supports CSV, PNG, JPEG, or PDF)\"\n flag \"-a --attachment \" help=\"An optional binary attachment\"\n flag \"--file-name \" help=\"Optional custom file name\"\n flag \"--file-purpose \" help=\"Purpose of the file upload\"\n flag \"-m --metadata \" help=\"JSON metadata about the file\"\n }\n}\ncmd \"test-endpoint\" help=\"Test Endpoint\" {\n alias \"te\"\n arg \"test-name\" help=\"string value (or use --test-name)\"\n flag \"-t --test-name \" help=\"string value (or pass it as the [test-name] argument)\"\n flag \"--test \" help=\"[required]\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\ncmd \"test-enum-formats\" help=\"Test x-speakeasy-enums in different formats\" {\n alias \"tef\"\n flag \"--string-array-format \" help=\"String enum with x-speakeasy-enums as array (full coverage) (options: pending_review, approved_final, rejected_permanently, arch_x86, mobile_ios, custom_field) [required]\"\n flag \"--string-map-format \" help=\"String enum with x-speakeasy-enums as map (full coverage) (options: low_priority, medium_priority, high_priority, critical_urgent) [required]\"\n flag \"--string-partial-map-format \" help=\"String enum with x-speakeasy-enums as map (partial coverage) (options: draft_mode, under_review, published_live, archived_old) [required]\"\n flag \"--integer-map-format \" help=\"Integer enum with x-speakeasy-enums as map (full coverage) (options: 100, 200, 300, 400) [required]\"\n flag \"--integer-partial-map-format \" help=\"Integer enum with x-speakeasy-enums as map (partial coverage) (options: 1, 2, 3, 4, 5) [required]\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\ncmd \"update-user\" help=\"Update User\" {\n alias \"uu\"\n arg \"id\" help=\"string value (or use --id)\"\n flag \"-i --id \" help=\"string value (or pass it as the [id] argument)\"\n flag \"--user.id \" help=\"[required]\"\n flag \"--user.email \" help=\"[required]\"\n flag \"--user.first-name \" help=\"string value\"\n flag \"--user.last-name \" help=\"string value\"\n flag \"--user.age \" help=\"number value\"\n flag \"--user.postal-code \" help=\"string value\"\n flag \"--user.gender \" help=\"options: male, female, other\"\n flag \"--user.associated-ids \" help=\"list of values\" var=#true\n flag \"--user.metadata \" help=\"JSON object\"\n flag \"--user.metadata-any \" help=\"A metadata object with additionalProperties true (any type) - should be flattened\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n}\ncmd \"url-validation-stress-test\" help=\"Url Validation Stress Test\" {\n alias \"uvst\"\n}\ncmd \"validate\" help=\"Validate\"\ncmd \"version\" help=\"Print the CLI version\"\ncmd \"whoami\" help=\"Display current authentication and global parameter configuration\"\n", "invite": "cmd \"invite\" help=\"Invite a user by email\" {\n arg \"email\" help=\"Email address to invite\" required=#true var=#true\n flag \"--body \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--given-name \" help=\"First name to record\"\n flag \"--user-gender \" help=\"Gender to record (e.g. male, female, other)\" {\n suggestions \"male\" \"female\" \"other\"\n }\n}\n", "enroll": "cmd \"enroll\" help=\"Enroll a user by email\" {\n flag \"--body \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--enrollment-email \" help=\"Email address to enroll\"\n}\n", "render": "cmd \"render\" help=\"Render an image asset to a file\" {\n arg \"prompt\" help=\"Text prompt to render\" required=#true var=#true\n flag \"--body \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--out \" help=\"Write the image to this file (or into this directory). Default: ./render-{timestamp}-{rand}.{ext}\"\n flag \"--raw-response\" help=\"Print the raw API response instead of writing the image to a file\"\n}\n", - "produce": "cmd \"produce\" help=\"Produce an image asset and wait for completion\" {\n arg \"prompt\" help=\"Text prompt to render\" required=#true var=#true\n flag \"--body \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--out \" help=\"Write the image to this file (or into this directory). Default: ./produce-{timestamp}-{rand}.{ext}\"\n flag \"--raw-response\" help=\"Print the raw API response instead of writing the image to a file\"\n flag \"--async\" help=\"Return the operation handle without waiting for a terminal response\"\n flag \"--poll-interval \" help=\"Override the initial polling interval (positive Go duration, for example 500ms or 2s)\"\n flag \"--poll-timeout \" help=\"Override the overall polling deadline (positive Go duration, at least the effective poll interval)\"\n}\n", - "observe": "cmd \"observe\" help=\"Produce an asset and print its terminal status\" {\n arg \"prompt\" help=\"Text prompt to render\" required=#true var=#true\n flag \"--body \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--async\" help=\"Return the operation handle without waiting for a terminal response\"\n flag \"--poll-interval \" help=\"Override the initial polling interval (positive Go duration, for example 500ms or 2s)\"\n flag \"--poll-timeout \" help=\"Override the overall polling deadline (positive Go duration, at least the effective poll interval)\"\n}\n", + "produce": "cmd \"produce\" help=\"Produce an image asset and wait for completion\" {\n arg \"prompt\" help=\"Text prompt to render (create input; omit with --resume)\" var=#true\n flag \"--body \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--out \" help=\"Write the image to this file (or into this directory). Default: ./produce-{timestamp}-{rand}.{ext}\"\n flag \"--raw-response\" help=\"Print the raw API response instead of writing the image to a file\"\n flag \"--resume \" help=\"Resume polling an existing operation by ID instead of creating one\"\n flag \"--async\" help=\"Return the operation handle without waiting for a terminal response\"\n flag \"--poll-interval \" help=\"Override the initial polling interval (positive Go duration, for example 500ms or 2s)\"\n flag \"--poll-timeout \" help=\"Override the overall polling deadline (positive Go duration, at least the effective poll interval)\"\n}\n", + "observe": "cmd \"observe\" help=\"Produce an asset and print its terminal status\" {\n arg \"prompt\" help=\"Text prompt to render (create input; omit with --resume)\" var=#true\n flag \"--body \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--resume \" help=\"Resume polling an existing operation by ID instead of creating one\"\n flag \"--async\" help=\"Return the operation handle without waiting for a terminal response\"\n flag \"--poll-interval \" help=\"Override the initial polling interval (positive Go duration, for example 500ms or 2s)\"\n flag \"--poll-timeout \" help=\"Override the overall polling deadline (positive Go duration, at least the effective poll interval)\"\n}\n", "say": "cmd \"say\" help=\"Stream a chat reply\" {\n arg \"prompt\" help=\"Prompt to send\" required=#true var=#true\n flag \"--request \" help=\"Request body as JSON (advanced; merges with intent inputs, rejecting duplicate keys). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--stream\" help=\"Stream events as they arrive (default: true)\" default=#true\n}\n", "archive": "cmd \"archive\" help=\"Archive a user account\"\n", "test-group": "cmd \"test-group\" help=\"Operations for test-group\" {\n alias \"tg\"\n cmd \"tag2\" help=\"Operations for tag2\" {\n cmd \"post-test\" help=\"Post Test2\" {\n alias \"pt\"\n flag \"--obj \" help=\"A simple object that uses all our supported primitive types and enums and has optional properties. [required]\"\n flag \"-t --type \" help=\"options: type1, type2\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--output-file \" help=\"Save the response body to a file path (recommended for binary/file responses)\"\n flag \"--output-b64\" help=\"Encode binary response as base64 and print to stdout\"\n }\n }\n cmd \"tag3\" help=\"Operations for tag3\" {\n cmd \"post-test\" help=\"Post Test2\" {\n alias \"pt\"\n flag \"--obj \" help=\"A simple object that uses all our supported primitive types and enums and has optional properties. [required]\"\n flag \"-t --type \" help=\"options: type1, type2\"\n flag \"--body \" help=\"Request body as JSON (alternative to individual flags). Can also be provided via stdin; @path reads a file, @- reads stdin to EOF. Use --schema to print the exact JSON Schema.\"\n flag \"--schema\" help=\"Print the exact JSON Schema of the request body and exit\"\n flag \"--output-file \" help=\"Save the response body to a file path (recommended for binary/file responses)\"\n flag \"--output-b64\" help=\"Encode binary response as base64 and print to stdout\"\n }\n }\n}\n", diff --git a/zSDKs/sdk-cli/tests/async_test.go b/zSDKs/sdk-cli/tests/async_test.go index e1b8939d0..fc56d9847 100644 --- a/zSDKs/sdk-cli/tests/async_test.go +++ b/zSDKs/sdk-cli/tests/async_test.go @@ -5,10 +5,13 @@ package tests import ( "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" "os" + "os/exec" "path/filepath" + "runtime" "strings" "sync" "testing" @@ -16,8 +19,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/spf13/cobra" "openapi/internal/cli" "openapi/internal/flagutil" + "openapi/internal/interactive" "openapi/internal/sdk/types" ) @@ -151,7 +157,7 @@ func TestAsyncIntent_EscapeReturnsCreateResponseWithoutPolling(t *testing.T) { h := NewCLITestHarness(t) require.NoError(t, runAsyncIntent(t, h, stub.server.URL, "--async")) assert.Equal(t, asyncTestHandle, strings.TrimSpace(h.GetStdout())) - assert.Empty(t, h.GetStderr()) + assert.Contains(t, h.GetStderr(), "To resume polling: "+"cli produce --resume"+" "+asyncTestHandle) _, polls, _ := stub.counts() assert.Zero(t, polls) @@ -194,7 +200,7 @@ func TestAsyncIntent_FailureUsesSharedMachineErrorEnvelope(t *testing.T) { require.Error(t, err) assert.Empty(t, h.GetStdout()) assert.Equal(t, asyncTestHandle, jsonGetString(h.GetStderr(), "id")) - assert.Contains(t, jsonGetString(h.GetStderr(), "resume"), "cli get-asset --id"+" "+asyncTestHandle) + assert.Contains(t, jsonGetString(h.GetStderr(), "resume"), "cli produce --resume"+" "+asyncTestHandle) if args[0] == "--agent-mode" { assert.Equal(t, "CLI_ASYNC_FAILED", jsonGetString(h.GetStderr(), "error_reason")) assert.Contains(t, h.GetStderr(), "Inspect the terminal response and resume command") @@ -207,7 +213,7 @@ func TestAsyncIntent_FailureUsesSharedMachineErrorEnvelope(t *testing.T) { require.Error(t, err) diagnostic := strings.TrimSpace(h.GetStderr()) assert.Contains(t, diagnostic, asyncTestHandle) - assert.Contains(t, diagnostic, "cli get-asset --id"+" "+asyncTestHandle) + assert.Contains(t, diagnostic, "cli produce --resume"+" "+asyncTestHandle) assert.Zero(t, strings.Count(diagnostic, string(rune(10)))) } @@ -224,7 +230,7 @@ func TestAsyncIntent_UnknownAndMissingStatesAreErrors(t *testing.T) { assert.Empty(t, h.GetStdout()) assert.Equal(t, test.reason, jsonGetString(h.GetStderr(), "error_reason")) assert.Equal(t, asyncTestHandle, jsonGetString(h.GetStderr(), "id")) - assert.Contains(t, jsonGetString(h.GetStderr(), "resume"), "cli get-asset --id"+" "+asyncTestHandle) + assert.Contains(t, jsonGetString(h.GetStderr(), "resume"), "cli produce --resume"+" "+asyncTestHandle) }) } } @@ -376,3 +382,396 @@ func TestAsyncIntent_DeclaredJQAppliesOnlyToTerminalResponse(t *testing.T) { require.NoError(t, h.RunWithStdinServerRaw(stub.server.URL, commandArgs, "")) assert.JSONEq(t, "{\"status\":\"completed\"}", h.GetStdout()) } + +func runAsyncResume(t *testing.T, h *CLITestHarness, serverURL string, extra ...string) error { + t.Helper() + args := []string{"produce", "--resume", asyncTestHandle, "--poll-interval", "20ms", "--poll-timeout", "5s"} + return h.RunWithStdinServerRaw(serverURL, append(args, extra...), "") +} + +func TestAsyncResume_SavesTerminalResultWithoutCreate(t *testing.T) { + stub := newAsyncStub(t, asyncCreateTestBody, "{\"status\":\"in_progress\"}", asyncSuccessTestBody) + h := NewCLITestHarness(t) + target := filepath.Join(t.TempDir(), "resumed.png") + require.NoError(t, runAsyncResume(t, h, stub.server.URL, "--out", target)) + data, err := os.ReadFile(strings.TrimSpace(h.GetStdout())) + require.NoError(t, err) + assert.Equal(t, "async-artifact", string(data)) + creates, polls, _ := stub.counts() + assert.Zero(t, creates) + assert.GreaterOrEqual(t, polls, 1) +} + +func TestAsyncResume_RejectsConflictsBeforeRequests(t *testing.T) { + cases := [][]string{ + {"--resume="}, {"--resume", " "}, {"unexpected prompt"}, {"--async"}, + {"--poll-interval", "0s"}, {"--poll-timeout", "1ms"}, + {"--body", "@/nonexistent-resume-input.json"}, {"--body", "@-"}, + {"--out", "unused", "--raw-response"}, + } + for _, extra := range cases { + t.Run(strings.Join(extra, " "), func(t *testing.T) { + stub := newAsyncStub(t, asyncCreateTestBody, asyncSuccessTestBody) + h := NewCLITestHarness(t) + err := runAsyncResume(t, h, stub.server.URL, extra...) + require.Error(t, err) + assert.NotContains(t, err.Error(), "no such file") + creates, polls, _ := stub.counts() + assert.Zero(t, creates) + assert.Zero(t, polls) + }) + } +} + +func TestAsyncResume_DryRunPreviewsOnlyPoll(t *testing.T) { + stub := newAsyncStub(t, asyncCreateTestBody, asyncSuccessTestBody) + h := NewCLITestHarness(t) + dir := withAsyncWorkingDir(t) + require.NoError(t, runAsyncResume(t, h, stub.server.URL, "--dry-run", "--output-format", "json")) + var preview struct { + DryRun bool `json:"dry_run"` + Request struct { + Method string `json:"method"` + URL string `json:"url"` + } `json:"request"` + } + require.NoError(t, json.Unmarshal([]byte(h.GetStdout()), &preview)) + assert.True(t, preview.DryRun) + assert.Equal(t, "GET", preview.Request.Method) + assert.Contains(t, preview.Request.URL, asyncTestHandle) + assert.Contains(t, preview.Request.URL, "stream"+"="+"false") + creates, polls, _ := stub.counts() + assert.Zero(t, creates) + assert.Zero(t, polls) + files, err := os.ReadDir(dir) + require.NoError(t, err) + assert.Empty(t, files) +} + +func TestAsyncResume_DoesNotReadOpenStdin(t *testing.T) { + stub := newAsyncStub(t, asyncCreateTestBody, asyncSuccessTestBody) + h := NewCLITestHarness(t) + h.resetAndSetupEnv() + root, err := cli.NewRootCommand() + require.NoError(t, err) + root.SetOut(h.stdout) + root.SetErr(h.stderr) + read, write, err := os.Pipe() + require.NoError(t, err) + defer read.Close() + defer write.Close() + original := os.Stdin + os.Stdin = read + defer func() { os.Stdin = original }() + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + stopWatchdog := make(chan struct{}) + defer close(stopWatchdog) + go func() { + select { + case <-ctx.Done(): + _ = write.Close() + case <-stopWatchdog: + } + }() + err = cli.ExecuteRoot(ctx, root, []string{"produce", "--resume", asyncTestHandle, "--dry-run", "--output-format", "json", "--server-url", stub.server.URL}) + require.NoError(t, err) + require.NoError(t, ctx.Err()) + creates, polls, _ := stub.counts() + assert.Zero(t, creates) + assert.Zero(t, polls) +} + +func TestAsyncResume_HelpAndUsageDescribeAlternateInput(t *testing.T) { + for _, flag := range []string{"--help", "--usage"} { + h := NewCLITestHarness(t) + require.NoError(t, h.RunWithStdinServerRaw("http://localhost:1", []string{"produce", flag}, "")) + assert.Contains(t, h.GetStdout(), "Resume polling an existing operation by ID instead of creating one") + } +} +func TestAsyncResume_OutputModes(t *testing.T) { + for _, extra := range [][]string{{"--output-format", "json"}, {"--agent-mode", "--output-format", "json"}, {"--jq", "."}, {"--raw-response"}} { + t.Run(strings.Join(extra, " "), func(t *testing.T) { + stub := newAsyncStub(t, asyncCreateTestBody, asyncSuccessTestBody) + dir := withAsyncWorkingDir(t) + h := NewCLITestHarness(t) + require.NoError(t, runAsyncResume(t, h, stub.server.URL, extra...)) + files, err := os.ReadDir(dir) + require.NoError(t, err) + if extra[0] == "--raw-response" { + assert.Empty(t, files) + assert.JSONEq(t, asyncSuccessTestBody, h.GetStdout()) + } else { + require.Len(t, files, 1) + data, err := os.ReadFile(filepath.Join(dir, files[0].Name())) + require.NoError(t, err) + assert.Equal(t, "async-artifact", string(data)) + { + var envelope map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(h.GetStdout()), &envelope)) + resolved, err := filepath.EvalSymlinks(filepath.Join(dir, files[0].Name())) + require.NoError(t, err) + assert.Equal(t, resolved, envelope["path"]) + assert.Equal(t, float64(len(data)), envelope["size_bytes"]) + } + assert.Empty(t, h.GetStderr()) + } + creates, polls, _ := stub.counts() + assert.Zero(t, creates) + assert.Equal(t, 1, polls) + }) + } +} + +type asyncResumePrompter struct{ calls int } + +func (p *asyncResumePrompter) Prompt(_ *cobra.Command, _ []interactive.PromptField) ([]interactive.PromptAnswer, error) { + p.calls++ + return nil, context.Canceled +} + +func TestAsyncResume_BypassesInjectedCreatePrompter(t *testing.T) { + for _, extra := range [][]string{nil, {"--resume="}, {"--body", "@/nonexistent-resume-input.json"}} { + stub := newAsyncStub(t, asyncCreateTestBody, asyncSuccessTestBody) + h := NewCLITestHarness(t) + h.resetAndSetupEnv() + root, err := cli.NewRootCommand() + require.NoError(t, err) + root.SetOut(h.stdout) + root.SetErr(h.stderr) + p := &asyncResumePrompter{} + args := []string{"produce", "--resume", asyncTestHandle, "--dry-run", "--interactive", "--server-url", stub.server.URL} + err = cli.ExecuteRoot(interactive.WithPrompter(context.Background(), p), root, append(args, extra...)) + if len(extra) == 0 { + require.NoError(t, err) + } else { + require.Error(t, err) + assert.NotContains(t, err.Error(), "no such file") + } + assert.Zero(t, p.calls) + creates, polls, _ := stub.counts() + assert.Zero(t, creates) + assert.Zero(t, polls) + } +} +func TestAsyncResume_NonArtifactDeclaredJQ(t *testing.T) { + stub := newAsyncStubWithStatus(t, 200, 200, "{\"id\":\"async-test-handle\"}", "{\"status\":\"completed\"}") + h := NewCLITestHarness(t) + args := []string{"observe", "--resume", asyncTestHandle, "--poll-interval", "20ms"} + require.NoError(t, h.RunWithStdinServerRaw(stub.server.URL, args, "")) + assert.NotContains(t, h.GetStdout(), "{") + assert.NotEmpty(t, h.GetStdout()) + expected, err := json.Marshal(jsonGetString("{\"status\":\"completed\"}", "status")) + require.NoError(t, err) + assert.Equal(t, string(expected), strings.TrimSpace(h.GetStdout())) + creates, polls, _ := stub.counts() + assert.Zero(t, creates) + assert.Equal(t, 1, polls) + for _, flag := range []string{"--help", "--usage"} { + h = NewCLITestHarness(t) + require.NoError(t, h.RunWithStdinServerRaw(stub.server.URL, []string{"observe", flag}, "")) + assert.NotContains(t, h.GetStdout(), "--out again") + } +} + +func TestAsyncResume_TerminalErrorsAndHandoff(t *testing.T) { + for name, test := range map[string]struct { + body, reason string + timeout bool + }{ + "unknown": {asyncUnknownTestBody, "CLI_ASYNC_UNKNOWN_STATE", false}, + "missing": {asyncMissingStateBody, "CLI_PROTOCOL", false}, + "failure": {"{\"status\":\"failed\",\"error\":{\"message\":\"async operation failed\"\x7d\x7d", "CLI_ASYNC_FAILED", false}, + "handoff": {"{\"status\":\"requires_action\",\"action\":{\"type\":\"continue\"\x7d\x7d", "", false}, + "timeout": {"{\"status\":\"in_progress\"}", "CLI_ASYNC_TIMEOUT", true}, + } { + t.Run(name, func(t *testing.T) { + stub := newAsyncStub(t, asyncCreateTestBody, test.body) + dir := withAsyncWorkingDir(t) + h := NewCLITestHarness(t) + extra := []string{"--output-format", "json", "--agent-mode"} + if test.timeout { + extra = append(extra, "--poll-timeout", "50ms") + } + err := runAsyncResume(t, h, stub.server.URL, extra...) + if test.reason == "" { + require.NoError(t, err) + assert.JSONEq(t, test.body, h.GetStdout()) + } else { + require.Error(t, err) + assert.Equal(t, test.reason, jsonGetString(h.GetStderr(), "error_reason")) + assert.Equal(t, "cli produce --resume"+" "+asyncTestHandle, jsonGetString(h.GetStderr(), "resume")) + } + creates, _, _ := stub.counts() + assert.Zero(t, creates) + files, err := os.ReadDir(dir) + require.NoError(t, err) + assert.Empty(t, files) + }) + } +} +func TestAsyncResume_URIIsExplicitlyUnsupported(t *testing.T) { + var payload interface{} + require.NoError(t, json.Unmarshal([]byte(asyncSuccessTestBody), &payload)) + var cfg map[string]interface{} + require.NoError(t, json.Unmarshal([]byte("{\"pointer\":[{\"field\":\"steps\"},{\"wild\":true},{\"field\":\"content\"},{\"wild\":true}],\"kind\":\"image\",\"defaultPath\":\"produce-{timestamp}-{rand}.{ext}\"}"), &cfg)) + field := func(name, fallback string) string { + if value, ok := cfg[name].(string); ok { + return value + } + return fallback + } + var visit func(interface{}) + visit = func(v interface{}) { + switch value := v.(type) { + case map[string]interface{}: + if value[field("typeField", "type")] == cfg["kind"] { + delete(value, field("dataField", "data")) + value[field("uriField", "uri")] = "https://example.invalid/artifact" + } + for _, child := range value { + visit(child) + } + case []interface{}: + for _, child := range value { + visit(child) + } + } + } + visit(payload) + body, err := json.Marshal(payload) + require.NoError(t, err) + stub := newAsyncStub(t, asyncCreateTestBody, string(body)) + dir := withAsyncWorkingDir(t) + h := NewCLITestHarness(t) + err = runAsyncResume(t, h, stub.server.URL) + require.Error(t, err) + assert.Contains(t, err.Error(), "delivered by URI") + files, err := os.ReadDir(dir) + require.NoError(t, err) + assert.Empty(t, files) +} +func TestAsyncPoll_RawSchemaMismatchAndSSE(t *testing.T) { + for _, mode := range []string{"json", "jq", "sse"} { + t.Run(mode, func(t *testing.T) { + calls := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + if mode == "sse" { + w.Header().Set("Content-Type", "text/event-stream") + fmt.Fprintf(w, "data: %s\n\n", asyncSuccessTestBody) + } else { + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `{ "id": 123, "status": "completed" }`) + } + })) + defer server.Close() + h := NewCLITestHarness(t) + args := []string{"get-asset", "--id", asyncTestHandle} + args = append(args, "--stream=false") + if mode == "jq" { + args = append(args, "--jq", ".id") + } else { + args = append(args, "--output-format", "json") + } + require.NoError(t, h.RunWithStdinServerRaw(server.URL, args, "")) + assert.Equal(t, 1, calls) + if mode == "json" { + assert.JSONEq(t, `{ "id":123, "status":"completed" }`, h.GetStdout()) + } + if mode == "jq" { + assert.Equal(t, "123", strings.TrimSpace(h.GetStdout())) + } + if mode == "sse" { + var event struct { + Data json.RawMessage `json:"data"` + } + require.NoError(t, json.Unmarshal([]byte(h.GetStdout()), &event)) + assert.JSONEq(t, asyncSuccessTestBody, string(event.Data)) + } + }) + } +} + +func TestAsyncResume_PrintedHintQuotesHandleAndExecutes(t *testing.T) { + handle := asyncTestHandle + " '$(echo unexpected); spaced" + creates, polls := 0, 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if r.Method == http.MethodGet { + polls++ + assert.Contains(t, r.URL.Path, handle) + fmt.Fprint(w, strings.ReplaceAll(asyncSuccessTestBody, asyncTestHandle, handle)) + } else { + creates++ + fmt.Fprint(w, strings.ReplaceAll(asyncCreateTestBody, asyncTestHandle, handle)) + } + })) + defer server.Close() + withAsyncWorkingDir(t) + h := NewCLITestHarness(t) + require.NoError(t, runAsyncIntent(t, h, server.URL, "--async")) + assert.Contains(t, h.GetStderr(), "To resume polling: ") + assert.Contains(t, h.GetStderr(), " --resume ") + assert.Equal(t, 1, creates) + assert.Zero(t, polls) + hint := strings.TrimSpace(strings.TrimPrefix(h.GetStderr(), "To resume polling: ")) + if runtime.GOOS == "windows" { + t.Skip("POSIX shell quoting requires sh") + } + parsed, err := exec.Command("sh", "-c", "set -- "+hint+"; printf '%s\\0' \"$@\"").Output() + require.NoError(t, err) + argv := strings.Split(strings.TrimSuffix(string(parsed), string(rune(0))), string(rune(0))) + require.Greater(t, len(argv), 2) + assert.Equal(t, handle, argv[len(argv)-1]) + h = NewCLITestHarness(t) + require.NoError(t, h.RunWithStdinServerRaw(server.URL, append(argv[1:], "--poll-interval", "20ms"), "")) + assert.Equal(t, 1, creates) + assert.Equal(t, 1, polls) + data, err := os.ReadFile(strings.TrimSpace(h.GetStdout())) + require.NoError(t, err) + assert.Equal(t, "async-artifact", string(data)) +} +func TestAsyncResume_DoesNotSuppressOrdinaryCreatePrompt(t *testing.T) { + h := NewCLITestHarness(t) + h.resetAndSetupEnv() + root, err := cli.NewRootCommand() + require.NoError(t, err) + root.SetOut(h.stdout) + root.SetErr(h.stderr) + p := &asyncResumePrompter{} + err = cli.ExecuteRoot(interactive.WithPrompter(context.Background(), p), root, []string{"produce", "--interactive", "--dry-run"}) + require.Error(t, err) + assert.Equal(t, 1, p.calls) +} + +func TestAsyncResume_RequestFlagDefaultsAndExplicitConflicts(t *testing.T) { + for _, explicit := range []bool{false, true} { + stub := newAsyncStub(t, asyncCreateTestBody, asyncSuccessTestBody) + h := NewCLITestHarness(t).WithEnv("TEST_CREATE_PRIORITY", "from-environment") + h.resetAndSetupEnv() + root, err := cli.NewRootCommand() + require.NoError(t, err) + root.SetOut(h.stdout) + root.SetErr(h.stderr) + cmd, _, err := root.Find([]string{"produce"}) + require.NoError(t, err) + cmd.Flags().String("create-priority", os.Getenv("TEST_CREATE_PRIORITY"), "Create priority") + flagutil.MarkRequestInput(cmd, "create-priority") + args := []string{"produce", "--resume", asyncTestHandle, "--dry-run", "--output-format", "json", "--server-url", stub.server.URL} + if explicit { + args = append(args, "--create-priority=from-argv") + } + err = cli.ExecuteRoot(context.Background(), root, args) + if explicit { + require.Error(t, err) + assert.Contains(t, err.Error(), "--create-priority") + } else { + require.NoError(t, err) + assert.Contains(t, h.GetStdout(), stub.server.URL) + } + creates, polls, _ := stub.counts() + assert.Zero(t, creates) + assert.Zero(t, polls) + } +}