← back to Cli Printing Press
fix(cli): require explicit retry no-ops (#635)
cb3ef87308c006da61b3b5e7305b0b2095540f15 · 2026-05-05 13:43:50 -0700 · Trevin Chow
* fix(cli): require explicit retry no-ops
* docs(cli): document generated helper contracts
Files touched
M docs/solutions/design-patterns/http-client-cache-invalidate-on-mutation-2026-05-05.mdA docs/solutions/logic-errors/generated-cli-idempotent-noops-and-export-validation-2026-05-05.mdM internal/generator/generator.goM internal/generator/generator_test.goM internal/generator/templates/command_endpoint.go.tmplM internal/generator/templates/command_promoted.go.tmplM internal/generator/templates/export.go.tmplM internal/generator/templates/helpers.go.tmplM internal/generator/templates/readme.md.tmplM internal/generator/templates/root.go.tmplM internal/generator/templates/search.go.tmplM internal/generator/templates/skill.md.tmplM internal/generator/vision_templates.goM testdata/golden/expected/generate-golden-api/dogfood.jsonM testdata/golden/expected/generate-golden-api/printing-press-golden/README.mdM testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.mdM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/root.goM testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_enterprise.goM testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_list.goM testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_premium.go
Diff
commit cb3ef87308c006da61b3b5e7305b0b2095540f15
Author: Trevin Chow <trevin@trevinchow.com>
Date: Tue May 5 13:43:50 2026 -0700
fix(cli): require explicit retry no-ops (#635)
* fix(cli): require explicit retry no-ops
* docs(cli): document generated helper contracts
---
...ient-cache-invalidate-on-mutation-2026-05-05.md | 2 +-
...otent-noops-and-export-validation-2026-05-05.md | 165 ++++++++++++++++++
internal/generator/generator.go | 19 +-
internal/generator/generator_test.go | 192 +++++++++++++++++++++
.../generator/templates/command_endpoint.go.tmpl | 4 +-
.../generator/templates/command_promoted.go.tmpl | 2 +-
internal/generator/templates/export.go.tmpl | 20 ++-
internal/generator/templates/helpers.go.tmpl | 45 +++--
internal/generator/templates/readme.md.tmpl | 2 +-
internal/generator/templates/root.go.tmpl | 8 +
internal/generator/templates/search.go.tmpl | 2 +-
internal/generator/templates/skill.md.tmpl | 2 +
internal/generator/vision_templates.go | 35 ++++
.../expected/generate-golden-api/dogfood.json | 4 +-
.../printing-press-golden/README.md | 2 +-
.../printing-press-golden/SKILL.md | 1 +
.../printing-press-golden/internal/cli/helpers.go | 34 +++-
.../internal/cli/projects_create.go | 2 +-
.../internal/cli/projects_list.go | 2 +-
.../internal/cli/projects_tasks_list-project.go | 2 +-
.../internal/cli/projects_tasks_update-project.go | 2 +-
.../internal/cli/promoted_public.go | 2 +-
.../printing-press-golden/internal/cli/root.go | 2 +
.../internal/cli/items_enterprise.go | 2 +-
.../tier-routing-golden/internal/cli/items_list.go | 2 +-
.../internal/cli/items_premium.go | 2 +-
26 files changed, 518 insertions(+), 39 deletions(-)
diff --git a/docs/solutions/design-patterns/http-client-cache-invalidate-on-mutation-2026-05-05.md b/docs/solutions/design-patterns/http-client-cache-invalidate-on-mutation-2026-05-05.md
index f8dda5c2..40554e03 100644
--- a/docs/solutions/design-patterns/http-client-cache-invalidate-on-mutation-2026-05-05.md
+++ b/docs/solutions/design-patterns/http-client-cache-invalidate-on-mutation-2026-05-05.md
@@ -139,5 +139,5 @@ Both legs (MCP-side `NoCache=true` from PR #521 (session history) and CLI-side `
- **PR #213** (`mvanhorn/printing-press-library`) — bulk backfill of `NoCache=true` across 38 published CLIs that pre-dated PR #521. (session history)
- **Issue #515** — Dub retro that originally surfaced the MCP-side stale-read on Create → warm-cache → Delete → stale 200. (session history)
- **PR #237** (`mvanhorn/printing-press-library`) commit `d26a2862` — the cal-com CLI-side `invalidateCache()` fix this doc captures. Printed-CLI-level patch; does NOT yet propagate to other CLIs.
-- **Generator-template gap** (open) — emit `invalidateCache()` and the `do()` success-branch hook in every printed CLI's `client.go` template so future CLIs inherit it without per-CLI patching. Worth filing as a P1 retro candidate alongside the cal-com retro #597 series; the same template that PR #521 touched is the obvious landing site.
+- **Issue #603** (closed 2026-05-05) — emits `invalidateCache()` and the `do()` success-branch hook in every printed CLI's `client.go` template so future CLIs inherit it without per-CLI patching.
- **MEMORY.md note** "MCP server should default to NoCache=true" (auto memory [claude]) — captures the MCP-side workaround. With this fix, the note's "agents driving the API through MCP get stale snapshots after DELETE/PATCH" caveat is now resolved at the client level; `NoCache=true` remains the right MCP default for freshness reasons unrelated to mutations.
diff --git a/docs/solutions/logic-errors/generated-cli-idempotent-noops-and-export-validation-2026-05-05.md b/docs/solutions/logic-errors/generated-cli-idempotent-noops-and-export-validation-2026-05-05.md
new file mode 100644
index 00000000..d1b524bc
--- /dev/null
+++ b/docs/solutions/logic-errors/generated-cli-idempotent-noops-and-export-validation-2026-05-05.md
@@ -0,0 +1,165 @@
+---
+title: Generated CLI retry no-ops and export commands require explicit API-shape checks
+date: 2026-05-05
+category: logic-errors
+module: cli-printing-press-generator
+problem_type: logic_error
+component: tooling
+symptoms:
+ - "Generated helpers silently swallowed HTTP 409 create conflicts and DELETE 404s"
+ - "Generated export was emitted unconditionally and accepted resources without a bare GET collection endpoint"
+ - "Export typo arguments fell through to upstream HTTP responses instead of failing as usage errors"
+root_cause: missing_validation
+resolution_type: code_fix
+severity: high
+tags:
+ - generated-cli
+ - idempotency
+ - export
+ - http-errors
+ - json-output
+ - generator-templates
+---
+
+# Generated CLI retry no-ops and export commands require explicit API-shape checks
+
+## Problem
+
+The Printing Press generator emitted two helper surfaces that treated API state and API shape as implicit:
+
+- Create helpers converted HTTP 409 conflicts into successful no-ops without the caller opting into retry semantics.
+- Delete helpers converted HTTP 404 responses into successful no-ops without the caller saying a missing target was acceptable.
+- The generic `export` command was generated for API specs that had resources but no bare `GET /<resource>` collection endpoint.
+
+Those behaviors look convenient in one retry path, but they compound poorly across every printed CLI. A real create conflict or delete miss becomes indistinguishable from a successful mutation, and an export command can exist for a resource the API cannot actually export.
+
+## Symptoms
+
+- A generated create command could exit 0 after an upstream HTTP 409, masking "already exists" as success.
+- A generated delete command could exit 0 after an upstream HTTP 404, masking "already deleted" as success.
+- Under `--json`, the no-op path did not have a structured envelope that machines could distinguish from a normal success payload.
+- `export <resource>` accepted arbitrary resource names before checking generated metadata, so typos could fall through into config loading, client setup, and upstream HTTP responses.
+- In session history, the Hacker News reprint surfaced the same export-shape bug: `export stories` returned Google sign-in HTML because the Firebase API uses `/topstories.json` and `/item/<id>.json`, not a bare `GET /stories` collection endpoint.
+
+## What Didn't Work
+
+Fixing one printed CLI would not compound. The issue was in the generator templates, so every future CLI needed the safer default.
+
+Helper-level error classification alone was too implicit. The templates could detect "HTTP 409" or "HTTP 404", but without a root flag they had no signal that the caller was intentionally running an idempotent retry flow.
+
+Dogfood evidence alone was noisy. Placeholder-heavy printed CLIs can fail export in several ways; the generator rule needed to be tied to actual endpoint shape, not just one API's confusing response body.
+
+Generic resource-name export overfit the resource inventory. A resource list says what the API talks about; it does not prove the API has a collection endpoint suitable for streaming/export.
+
+## Solution
+
+### 1. Require an explicit root flag before returning a retry no-op
+
+Generated CLIs now expose opt-in root flags for the retry cases:
+
+```go
+rootCmd.PersistentFlags().BoolVar(&flags.idempotent, "idempotent", false, "Treat already-existing create results as a successful no-op")
+rootCmd.PersistentFlags().BoolVar(&flags.ignoreMissing, "ignore-missing", false, "Treat missing delete targets as a successful no-op")
+```
+
+The generated helpers thread the root flags into the error classifiers. A conflict only becomes exit 0 when the user opted in:
+
+```go
+case strings.Contains(msg, "HTTP 409"):
+ if flags != nil && flags.idempotent {
+ return writeNoop(flags, "already_exists", "already exists (no-op)")
+ }
+ classified := apiErr(err)
+ writeAPIErrorEnvelope(flags, classified, ExitCode(classified))
+ return classified
+```
+
+Delete uses the same shape for missing targets:
+
+```go
+if strings.Contains(msg, "HTTP 404") && flags != nil && flags.ignoreMissing {
+ return writeNoop(flags, "already_deleted", "already deleted (no-op)")
+}
+```
+
+With `--json`, those opt-in no-ops emit a machine-readable envelope:
+
+```json
+{"status":"noop","reason":"already_deleted","message":"already deleted (no-op)"}
+```
+
+### 2. Emit export only for actual bare collection endpoints
+
+The export template is now conditional. It is emitted only when the spec has at least one resource backed by a bare `GET /<resource>` endpoint:
+
+```go
+func constrainVisionTemplates(api *spec.APISpec, set VisionTemplateSet) VisionTemplateSet {
+ if set.Export && len(exportableResources(api)) == 0 {
+ set.Export = false
+ }
+ return set
+}
+```
+
+The resource discovery intentionally checks endpoint shape, not just resource names:
+
+```go
+if endpoint.Method == "GET" && endpoint.Path == "/"+resourceName {
+ resources = append(resources, resourceName)
+}
+```
+
+### 3. Validate export arguments before config and API work
+
+Generated export commands build a static valid-resource set from the exportable endpoints and reject unknown resources at argument validation time:
+
+```go
+validResources := map[string]bool{
+ "items": true,
+}
+
+if !validResources[resource] {
+ return usageErr("unknown export resource %q (valid: %s)", resource, strings.Join(exportableResources, ", "))
+}
+```
+
+That keeps typos and unsupported resources in CLI-usage territory instead of turning them into auth, config, network, or upstream-content failures.
+
+### 4. Lock the behavior at the generator layer
+
+Cover the new contract with generator/template tests plus goldens:
+
+- `TestGeneratedHelpers_IdempotentNoopsRequireOptIn`
+- `TestGeneratedExport_ValidatesResourceArgument`
+- `TestGeneratedExport_OmittedWithoutBareCollectionEndpoint`
+- `scripts/golden.sh verify`
+
+The tests matter because both fixes are emitted behavior. A passing Go unit test on one helper is not enough if a future template path can silently regenerate the old surface.
+
+## Why This Works
+
+The fix separates "the HTTP request succeeded" from "the caller accepts this retry state." A create conflict and a delete miss remain errors by default; explicit retry flags are the only path that turns them into successful no-ops.
+
+The JSON no-op envelope gives agents a stable branch that is distinct from a normal success payload. They can treat `status: "noop"` as intentional control flow instead of inferring semantics from free-form text or exit code alone.
+
+The export command now mirrors the API contract rather than the resource inventory. If the API has no bare collection endpoint, the generated CLI does not promise a generic export workflow it cannot honor.
+
+Early argument validation also avoids avoidable side effects. When a resource can be validated from generated metadata, do that before loading config, building clients, or making network calls.
+
+## Prevention
+
+- For generated helpers, require an explicit root flag before converting an upstream 4xx into exit 0.
+- Gate generated command emission on actual endpoint capability, not inferred resource names.
+- Validate arguments backed solely by generated metadata before config, client, auth, or API work.
+- When a generator change affects CLI surface or machine-readable output, add focused generator tests and golden coverage in the same PR.
+- Use printed-CLI dogfood to decide whether a finding is systemic or API-specific, then land systemic fixes in generator templates.
+
+## Related
+
+- [Issue #599](https://github.com/mvanhorn/cli-printing-press/issues/599) - idempotent helpers respect `--json` and require opt-in no-ops.
+- [Issue #593](https://github.com/mvanhorn/cli-printing-press/issues/593) - export validates resource arguments and emits conditionally.
+- [Issue #597](https://github.com/mvanhorn/cli-printing-press/issues/597) - retro series that included the retry no-op finding.
+- [Issue #586](https://github.com/mvanhorn/cli-printing-press/issues/586) - retro series that included the export-shape finding.
+- [PR #635](https://github.com/mvanhorn/cli-printing-press/pull/635) - generator fix for both contracts.
+- [HTTP client response cache must invalidate on successful mutations](../design-patterns/http-client-cache-invalidate-on-mutation-2026-05-05.md) - neighboring generator-template mutation-safety pattern.
+- [Dry-run is the default for mutator probes in generated test harnesses](../design-patterns/dry-run-default-for-mutator-probes-in-test-harnesses-2026-05-05.md) - sibling pattern for making mutation intent explicit.
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 8bf275b9..56b9c00b 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -244,12 +244,13 @@ func New(s *spec.APISpec, outputDir string) *Generator {
"graphqlFieldSelection": func(typeName string, types map[string]spec.TypeDef) []string {
return graphqlFieldSelection(typeName, types)
},
- "isGraphQL": isGraphQLSpec,
- "backtick": func() string { return "`" },
- "kebab": toKebab,
- "humanName": naming.HumanName,
- "envPrefix": naming.EnvPrefix,
- "mcpToolName": naming.SnakeIdentifier,
+ "isGraphQL": isGraphQLSpec,
+ "exportableResources": exportableResources,
+ "backtick": func() string { return "`" },
+ "kebab": toKebab,
+ "humanName": naming.HumanName,
+ "envPrefix": naming.EnvPrefix,
+ "mcpToolName": naming.SnakeIdentifier,
"lookupEndpoint": func(api *spec.APISpec, ref string) templateEndpoint {
e, _ := lookupEndpointForTemplate(api, ref)
return e
@@ -645,6 +646,7 @@ type readmeTemplateData struct {
HasDataLayer bool
HasAsyncJobs bool
HasWriteCommands bool
+ HasDelete bool
HasAuth bool
FreshnessCommands []string
TrafficAnalysis *trafficAnalysisTemplateData
@@ -699,6 +701,7 @@ func (g *Generator) readmeData() *readmeTemplateData {
HasDataLayer: g.VisionSet.Store,
HasAsyncJobs: len(g.AsyncJobs) > 0,
HasWriteCommands: hasWriteCommands(g.Spec.Resources),
+ HasDelete: computeHelperFlags(g.Spec).HasDelete,
HasAuth: hasAuth(g.Spec.Auth),
FreshnessCommands: g.freshnessCommandPaths(),
TrafficAnalysis: g.trafficAnalysisData(),
@@ -1199,6 +1202,7 @@ func (g *Generator) prepareOutput() error {
if g.profile == nil {
g.profile = profiler.Profile(g.Spec)
}
+ g.VisionSet = constrainVisionTemplates(g.Spec, g.VisionSet)
if err := g.validateFreshnessCommandCoverage(); err != nil {
return err
}
@@ -2070,6 +2074,7 @@ func (g *Generator) renderRootProjectFiles(promotedCommands []PromotedCommand, p
// rootCmd.AddCommand(newAuthCmd) so the root binary does not reference an
// undefined symbol when auth.go was skipped.
hasAuthCommand := g.shouldEmitAuth()
+ helperFlags := computeHelperFlags(g.Spec)
rootData := struct {
*spec.APISpec
@@ -2085,6 +2090,7 @@ func (g *Generator) renderRootProjectFiles(promotedCommands []PromotedCommand, p
HasAsyncJobs bool
AsyncJobCount int
HasAuthCommand bool
+ HasDelete bool
}{
APISpec: g.Spec,
VisionSet: g.VisionSet,
@@ -2099,6 +2105,7 @@ func (g *Generator) renderRootProjectFiles(promotedCommands []PromotedCommand, p
HasAsyncJobs: len(g.AsyncJobs) > 0,
AsyncJobCount: len(g.AsyncJobs),
HasAuthCommand: hasAuthCommand,
+ HasDelete: helperFlags.HasDelete,
}
if err := g.renderTemplate("root.go.tmpl", filepath.Join("internal", "cli", "root.go"), rootData); err != nil {
return fmt.Errorf("rendering root: %w", err)
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index ac20bb17..0f36e59e 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -2,6 +2,7 @@ package generator
import (
"encoding/json"
+ "errors"
"fmt"
"go/parser"
"go/token"
@@ -2879,6 +2880,197 @@ func TestGeneratedHelpers_ConditionalClassifyDeleteError(t *testing.T) {
})
}
+func TestGeneratedHelpers_IdempotentNoopsRequireOptIn(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := &spec.APISpec{
+ Name: "testidempotent",
+ Version: "0.1.0",
+ BaseURL: "https://api.example.com",
+ Resources: map[string]spec.Resource{
+ "teams": {
+ Description: "Manage teams",
+ Endpoints: map[string]spec.Endpoint{
+ "create": {Method: "POST", Path: "/teams", Description: "Create team"},
+ "delete": {Method: "DELETE", Path: "/teams/{id}", Description: "Delete team"},
+ },
+ },
+ },
+ }
+
+ outputDir := filepath.Join(t.TempDir(), "testidempotent-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ testPath := filepath.Join(outputDir, "internal", "cli", "idempotent_helpers_test.go")
+ inlineTest := `package cli
+
+import (
+ "encoding/json"
+ "errors"
+ "io"
+ "os"
+ "testing"
+)
+
+func captureStdoutStderr(t *testing.T, fn func() error) (string, string, error) {
+ t.Helper()
+ oldOut := os.Stdout
+ oldErr := os.Stderr
+ outR, outW, err := os.Pipe()
+ if err != nil {
+ t.Fatal(err)
+ }
+ errR, errW, err := os.Pipe()
+ if err != nil {
+ t.Fatal(err)
+ }
+ os.Stdout = outW
+ os.Stderr = errW
+ callErr := fn()
+ _ = outW.Close()
+ _ = errW.Close()
+ os.Stdout = oldOut
+ os.Stderr = oldErr
+ out, err := io.ReadAll(outR)
+ if err != nil {
+ t.Fatal(err)
+ }
+ errOut, err := io.ReadAll(errR)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return string(out), string(errOut), callErr
+}
+
+func requireNoopJSON(t *testing.T, body, reason string) {
+ t.Helper()
+ var got map[string]string
+ if err := json.Unmarshal([]byte(body), &got); err != nil {
+ t.Fatalf("noop output must be JSON: %v; body=%q", err, body)
+ }
+ if got["status"] != "noop" || got["reason"] != reason {
+ t.Fatalf("unexpected noop envelope: %#v", got)
+ }
+}
+
+func TestClassifyAPIError409RequiresIdempotent(t *testing.T) {
+ err := classifyAPIError(errors.New("HTTP 409: conflict"), &rootFlags{})
+ if err == nil {
+ t.Fatal("409 without --idempotent must be an error")
+ }
+ if ExitCode(err) != 5 {
+ t.Fatalf("409 should classify as API error, got exit %d", ExitCode(err))
+ }
+
+ stdout, stderr, err := captureStdoutStderr(t, func() error {
+ return classifyAPIError(errors.New("HTTP 409: conflict"), &rootFlags{idempotent: true, asJSON: true})
+ })
+ if err != nil {
+ t.Fatalf("idempotent 409 returned error: %v", err)
+ }
+ if stderr != "" {
+ t.Fatalf("json noop should not write stderr, got %q", stderr)
+ }
+ requireNoopJSON(t, stdout, "already_exists")
+}
+
+func TestClassifyDeleteError404RequiresIgnoreMissing(t *testing.T) {
+ err := classifyDeleteError(errors.New("HTTP 404: not found"), &rootFlags{})
+ if err == nil {
+ t.Fatal("404 delete without --ignore-missing must be an error")
+ }
+ if ExitCode(err) != 3 {
+ t.Fatalf("404 should classify as not found, got exit %d", ExitCode(err))
+ }
+
+ stdout, stderr, err := captureStdoutStderr(t, func() error {
+ return classifyDeleteError(errors.New("HTTP 404: not found"), &rootFlags{ignoreMissing: true, asJSON: true})
+ })
+ if err != nil {
+ t.Fatalf("ignore-missing 404 returned error: %v", err)
+ }
+ if stderr != "" {
+ t.Fatalf("json noop should not write stderr, got %q", stderr)
+ }
+ requireNoopJSON(t, stdout, "already_deleted")
+}
+`
+ require.NoError(t, os.WriteFile(testPath, []byte(inlineTest), 0o644))
+
+ runGoCommandRequired(t, outputDir, "test", "./internal/cli")
+}
+
+func TestGeneratedExport_ValidatesResourceArgument(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := &spec.APISpec{
+ Name: "testexport",
+ Version: "0.1.0",
+ BaseURL: "https://api.example.com",
+ Config: spec.ConfigSpec{Format: "toml", Path: "~/.config/testexport-pp-cli/config.toml"},
+ Resources: map[string]spec.Resource{
+ "stories": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/stories"}}},
+ "items": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/items"}}},
+ "users": {Endpoints: map[string]spec.Endpoint{"list": {Method: "GET", Path: "/users"}}},
+ },
+ }
+
+ outputDir := filepath.Join(t.TempDir(), "testexport-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ rootGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "root.go"))
+ require.NoError(t, err)
+ assert.Contains(t, string(rootGo), "rootCmd.AddCommand(newExportCmd(flags))")
+
+ exportGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "export.go"))
+ require.NoError(t, err)
+ exportContent := string(exportGo)
+ assert.Contains(t, exportContent, `"items": true`)
+ assert.Contains(t, exportContent, `"stories": true`)
+ assert.Contains(t, exportContent, `"users": true`)
+ assert.Contains(t, exportContent, `unknown resource %q; valid: %s`)
+
+ runGoCommandRequired(t, outputDir, "build", "-o", "./testexport-pp-cli", "./cmd/testexport-pp-cli")
+ cmd := exec.Command(filepath.Join(outputDir, "testexport-pp-cli"), "export", "storiez")
+ out, err := cmd.CombinedOutput()
+ require.Error(t, err)
+ assert.Contains(t, string(out), `unknown resource "storiez"; valid: items, stories, users`)
+ if exitErr, ok := err.(*exec.ExitError); ok {
+ assert.Equal(t, 2, exitErr.ExitCode())
+ } else {
+ t.Fatalf("expected ExitError, got %T", err)
+ }
+}
+
+func TestGeneratedExport_OmittedWithoutBareCollectionEndpoint(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := &spec.APISpec{
+ Name: "testnoexport",
+ Version: "0.1.0",
+ BaseURL: "https://api.example.com",
+ Config: spec.ConfigSpec{Format: "toml", Path: "~/.config/testnoexport-pp-cli/config.toml"},
+ Resources: map[string]spec.Resource{
+ "stories": {Endpoints: map[string]spec.Endpoint{"get": {Method: "GET", Path: "/api/v1/stories/{id}"}}},
+ "items": {Endpoints: map[string]spec.Endpoint{"get": {Method: "GET", Path: "/api/v1/items/{id}"}}},
+ },
+ }
+
+ outputDir := filepath.Join(t.TempDir(), "testnoexport-pp-cli")
+ gen := New(apiSpec, outputDir)
+ gen.VisionSet = VisionTemplateSet{Export: true}
+ require.NoError(t, gen.Generate())
+
+ rootGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "root.go"))
+ require.NoError(t, err)
+ assert.NotContains(t, string(rootGo), "newExportCmd")
+
+ _, err = os.Stat(filepath.Join(outputDir, "internal", "cli", "export.go"))
+ assert.True(t, errors.Is(err, os.ErrNotExist), "export.go should not be emitted for APIs without bare collection endpoints")
+
+ runGoCommandRequired(t, outputDir, "build", "./cmd/testnoexport-pp-cli")
+}
+
func TestGeneratedHelpers_ConditionalDataLayerFunctions(t *testing.T) {
t.Parallel()
diff --git a/internal/generator/templates/command_endpoint.go.tmpl b/internal/generator/templates/command_endpoint.go.tmpl
index 92c62328..3dce55e0 100644
--- a/internal/generator/templates/command_endpoint.go.tmpl
+++ b/internal/generator/templates/command_endpoint.go.tmpl
@@ -320,11 +320,11 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
{{- end}}
{{- if eq .Endpoint.Method "DELETE"}}
if err != nil {
- return classifyDeleteError(err)
+ return classifyDeleteError(err, flags)
}
{{- else}}
if err != nil {
- return classifyAPIError(err)
+ return classifyAPIError(err, flags)
}
{{- end}}
{{- if endpointNeedsClientLimit .Endpoint}}
diff --git a/internal/generator/templates/command_promoted.go.tmpl b/internal/generator/templates/command_promoted.go.tmpl
index ef768ddb..9cc6a463 100644
--- a/internal/generator/templates/command_promoted.go.tmpl
+++ b/internal/generator/templates/command_promoted.go.tmpl
@@ -191,7 +191,7 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
{{- end}}
{{- end}}
if err != nil {
- return classifyAPIError(err)
+ return classifyAPIError(err, flags)
}
{{- if .Endpoint.UsesHTMLResponse}}
diff --git a/internal/generator/templates/export.go.tmpl b/internal/generator/templates/export.go.tmpl
index 7d9c3870..3b2eaa0e 100644
--- a/internal/generator/templates/export.go.tmpl
+++ b/internal/generator/templates/export.go.tmpl
@@ -8,10 +8,12 @@ import (
"encoding/json"
"fmt"
"os"
+ "strings"
"github.com/spf13/cobra"
)
+{{- $exportableResources := exportableResources .}}
func newExportCmd(flags *rootFlags) *cobra.Command {
var format string
var outputFile string
@@ -34,6 +36,21 @@ large datasets as it has no memory pressure.`,
{{.Name}}-pp-cli export <resource> --format jsonl | jq '.id'`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
+ validResources := map[string]bool{
+{{- range $exportableResources}}
+ {{printf "%q" .}}: true,
+{{- end}}
+ }
+ validResourceList := []string{
+{{- range $exportableResources}}
+ {{printf "%q" .}},
+{{- end}}
+ }
+ resource := args[0]
+ if !validResources[resource] {
+ return usageErr(fmt.Errorf("unknown resource %q; valid: %s", resource, strings.Join(validResourceList, ", ")))
+ }
+
c, err := flags.newClient()
if err != nil {
return err
@@ -42,7 +59,6 @@ large datasets as it has no memory pressure.`,
c.NoCache = true
}
- resource := args[0]
path := "/" + resource
if len(args) > 1 {
path += "/" + args[1]
@@ -64,7 +80,7 @@ large datasets as it has no memory pressure.`,
data, err := c.Get(path, nil)
if err != nil {
- return classifyAPIError(err)
+ return classifyAPIError(err, flags)
}
switch format {
diff --git a/internal/generator/templates/helpers.go.tmpl b/internal/generator/templates/helpers.go.tmpl
index af888476..3464f62c 100644
--- a/internal/generator/templates/helpers.go.tmpl
+++ b/internal/generator/templates/helpers.go.tmpl
@@ -265,14 +265,40 @@ func extractGraphQLObject(data json.RawMessage, field string) (json.RawMessage,
}
{{- end}}
+type noopResult struct {
+ Status string `json:"status"`
+ Reason string `json:"reason"`
+}
+
+func writeNoop(flags *rootFlags, reason, prose string) error {
+ if flags != nil && flags.asJSON {
+ return json.NewEncoder(os.Stdout).Encode(noopResult{Status: "noop", Reason: reason})
+ }
+ fmt.Fprintln(os.Stderr, prose)
+ return nil
+}
+
+func writeAPIErrorEnvelope(flags *rootFlags, err error, code int) {
+ if flags == nil || !flags.asJSON {
+ return
+ }
+ _ = json.NewEncoder(os.Stdout).Encode(map[string]any{
+ "error": err.Error(),
+ "code": code,
+ })
+}
+
// classifyAPIError maps API errors to structured exit codes with actionable hints.
-func classifyAPIError(err error) error {
+func classifyAPIError(err error, flags *rootFlags) error {
msg := err.Error()
switch {
case strings.Contains(msg, "HTTP 409"):
- // 409 Conflict = resource already exists. For agents retrying creates, this is success.
- fmt.Fprintln(os.Stderr, "already exists (no-op)")
- return nil
+ if flags != nil && flags.idempotent {
+ return writeNoop(flags, "already_exists", "already exists (no-op)")
+ }
+ classified := apiErr(err)
+ writeAPIErrorEnvelope(flags, classified, ExitCode(classified))
+ return classified
{{- if and .Auth.Type (ne .Auth.Type "none")}}
case strings.Contains(msg, "HTTP 400") && cliutil.LooksLikeAuthError(msg):
return authErr(fmt.Errorf("%w\nhint: the API rejected the request — this usually means auth is missing or invalid."+
@@ -357,14 +383,13 @@ func classifyAPIError(err error) error {
}
{{- if .HasDelete}}
-// classifyDeleteError treats 404 as success for DELETE (already deleted = idempotent no-op).
-func classifyDeleteError(err error) error {
+// classifyDeleteError maps DELETE errors and supports explicit idempotent no-op handling.
+func classifyDeleteError(err error, flags *rootFlags) error {
msg := err.Error()
- if strings.Contains(msg, "HTTP 404") {
- fmt.Fprintln(os.Stderr, "already deleted (no-op)")
- return nil
+ if strings.Contains(msg, "HTTP 404") && flags != nil && flags.ignoreMissing {
+ return writeNoop(flags, "already_deleted", "already deleted (no-op)")
}
- return classifyAPIError(err)
+ return classifyAPIError(err, flags)
}
{{- end}}
diff --git a/internal/generator/templates/readme.md.tmpl b/internal/generator/templates/readme.md.tmpl
index 2d43cd24..9184b500 100644
--- a/internal/generator/templates/readme.md.tmpl
+++ b/internal/generator/templates/readme.md.tmpl
@@ -246,7 +246,7 @@ This CLI is designed for AI agent consumption:
- **Filterable** - `--select id,name` returns only fields you need
- **Previewable** - `--dry-run` shows the request without sending
{{- if .HasWriteCommands}}
-- **Retryable** - creates return "already exists" on retry, deletes return "already deleted"
+- **Explicit retries** - add `--idempotent` to create retries{{if .HasDelete}} and `--ignore-missing` to delete retries{{end}} when a no-op success is acceptable
- **Confirmable** - `--yes` for explicit confirmation of destructive actions
- **Piped input** - write commands can accept structured input when their help lists `--stdin`
{{- else}}
diff --git a/internal/generator/templates/root.go.tmpl b/internal/generator/templates/root.go.tmpl
index ca935e29..73f759a2 100644
--- a/internal/generator/templates/root.go.tmpl
+++ b/internal/generator/templates/root.go.tmpl
@@ -29,6 +29,10 @@ type rootFlags struct {
dryRun bool
noCache bool
noInput bool
+ idempotent bool
+{{- if .HasDelete}}
+ ignoreMissing bool
+{{- end}}
yes bool
agent bool
selectFields string
@@ -147,6 +151,10 @@ Run '{{.Name}}-pp-cli doctor' to verify auth and connectivity.{{backtick}},
rootCmd.PersistentFlags().BoolVar(&flags.dryRun, "dry-run", false, "Show request without sending")
rootCmd.PersistentFlags().BoolVar(&flags.noCache, "no-cache", false, "Bypass response cache")
rootCmd.PersistentFlags().BoolVar(&flags.noInput, "no-input", false, "Disable all interactive prompts (for CI/agents)")
+ rootCmd.PersistentFlags().BoolVar(&flags.idempotent, "idempotent", false, "Treat already-existing create results as a successful no-op")
+{{- if .HasDelete}}
+ rootCmd.PersistentFlags().BoolVar(&flags.ignoreMissing, "ignore-missing", false, "Treat missing delete targets as a successful no-op")
+{{- end}}
rootCmd.PersistentFlags().StringVar(&flags.selectFields, "select", "", "Comma-separated fields to include in output (e.g. --select id,name,status)")
rootCmd.PersistentFlags().BoolVar(&flags.yes, "yes", false, "Skip confirmation prompts (for agents and scripts)")
rootCmd.PersistentFlags().BoolVar(&noColor, "no-color", false, "Disable colored output")
diff --git a/internal/generator/templates/search.go.tmpl b/internal/generator/templates/search.go.tmpl
index bb67679f..142ffc9f 100644
--- a/internal/generator/templates/search.go.tmpl
+++ b/internal/generator/templates/search.go.tmpl
@@ -141,7 +141,7 @@ In local mode: searches locally synced data only.`,
}
// Check if it's a network error for auto-mode fallback
if flags.dataSource == "live" || !isNetworkError(getErr) {
- return classifyAPIError(getErr)
+ return classifyAPIError(getErr, flags)
}
// auto mode + network error: fall through to local FTS
fmt.Fprintf(cmd.ErrOrStderr(), "API unreachable, falling back to local search.\n")
diff --git a/internal/generator/templates/skill.md.tmpl b/internal/generator/templates/skill.md.tmpl
index 0509b2a5..0066e2dc 100644
--- a/internal/generator/templates/skill.md.tmpl
+++ b/internal/generator/templates/skill.md.tmpl
@@ -245,6 +245,8 @@ Add `--agent` to any command. Expands to: `--json --compact --no-input --no-colo
- **Non-interactive** — never prompts, every input is a flag
{{- if not .HasWriteCommands}}
- **Read-only** — do not use this CLI for create, update, delete, publish, comment, upvote, invite, order, send, or other mutating requests
+{{- else}}
+- **Explicit retries** — use `--idempotent` only when an already-existing create should count as success{{if .HasDelete}}, and `--ignore-missing` only when a missing delete target should count as success{{end}}
{{- end}}
{{- if .HasDataLayer}}
diff --git a/internal/generator/vision_templates.go b/internal/generator/vision_templates.go
index db3d50e4..e9d78ac1 100644
--- a/internal/generator/vision_templates.go
+++ b/internal/generator/vision_templates.go
@@ -1,6 +1,10 @@
package generator
import (
+ "sort"
+ "strings"
+
+ "github.com/mvanhorn/cli-printing-press/v3/internal/spec"
"github.com/mvanhorn/cli-printing-press/v3/internal/vision"
)
@@ -157,6 +161,37 @@ func SelectVisionTemplates(plan *vision.VisionaryPlan) VisionTemplateSet {
return set
}
+func constrainVisionTemplates(api *spec.APISpec, set VisionTemplateSet) VisionTemplateSet {
+ if set.Export && len(exportableResources(api)) == 0 {
+ set.Export = false
+ }
+ return set
+}
+
+func exportableResources(api *spec.APISpec) []string {
+ if api == nil {
+ return nil
+ }
+ names := make([]string, 0, len(api.Resources))
+ for name, resource := range api.Resources {
+ if hasBareCollectionEndpoint(name, resource) {
+ names = append(names, name)
+ }
+ }
+ sort.Strings(names)
+ return names
+}
+
+func hasBareCollectionEndpoint(resourceName string, resource spec.Resource) bool {
+ wantPath := "/" + resourceName
+ for _, endpoint := range resource.Endpoints {
+ if strings.EqualFold(endpoint.Method, "GET") && endpoint.Path == wantPath {
+ return true
+ }
+ }
+ return false
+}
+
func (s VisionTemplateSet) HasWorkflows() bool {
return len(s.Workflows) > 0
}
diff --git a/testdata/golden/expected/generate-golden-api/dogfood.json b/testdata/golden/expected/generate-golden-api/dogfood.json
index 20ee0049..8e10242c 100644
--- a/testdata/golden/expected/generate-golden-api/dogfood.json
+++ b/testdata/golden/expected/generate-golden-api/dogfood.json
@@ -12,11 +12,11 @@
},
"dead_flags": {
"dead": 0,
- "total": 17
+ "total": 18
},
"dead_functions": {
"dead": 0,
- "total": 49
+ "total": 51
},
"dir": "<ARTIFACT_DIR>/generate-golden-api/printing-press-golden",
"example_check": {
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md b/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md
index 8f3e4e8b..20fbee42 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md
@@ -103,7 +103,7 @@ This CLI is designed for AI agent consumption:
- **Pipeable** - `--json` output to stdout, errors to stderr
- **Filterable** - `--select id,name` returns only fields you need
- **Previewable** - `--dry-run` shows the request without sending
-- **Retryable** - creates return "already exists" on retry, deletes return "already deleted"
+- **Explicit retries** - add `--idempotent` to create retries when a no-op success is acceptable
- **Confirmable** - `--yes` for explicit confirmation of destructive actions
- **Piped input** - write commands can accept structured input when their help lists `--stdin`
- **Offline-friendly** - sync/search commands can use the local SQLite store when available
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md b/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md
index 5c368b91..5a2e3a37 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md
@@ -73,6 +73,7 @@ Add `--agent` to any command. Expands to: `--json --compact --no-input --no-colo
- **Previewable** — `--dry-run` shows the request without sending
- **Offline-friendly** — sync/search commands can use the local SQLite store when available
- **Non-interactive** — never prompts, every input is a flag
+- **Explicit retries** — use `--idempotent` only when an already-existing create should count as success
### Response envelope
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
index a5704a99..e8ebdddf 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
@@ -194,14 +194,40 @@ func isSyncAccessWarning(err error) (*accessWarning, bool) {
return nil, false
}
+type noopResult struct {
+ Status string `json:"status"`
+ Reason string `json:"reason"`
+}
+
+func writeNoop(flags *rootFlags, reason, prose string) error {
+ if flags != nil && flags.asJSON {
+ return json.NewEncoder(os.Stdout).Encode(noopResult{Status: "noop", Reason: reason})
+ }
+ fmt.Fprintln(os.Stderr, prose)
+ return nil
+}
+
+func writeAPIErrorEnvelope(flags *rootFlags, err error, code int) {
+ if flags == nil || !flags.asJSON {
+ return
+ }
+ _ = json.NewEncoder(os.Stdout).Encode(map[string]any{
+ "error": err.Error(),
+ "code": code,
+ })
+}
+
// classifyAPIError maps API errors to structured exit codes with actionable hints.
-func classifyAPIError(err error) error {
+func classifyAPIError(err error, flags *rootFlags) error {
msg := err.Error()
switch {
case strings.Contains(msg, "HTTP 409"):
- // 409 Conflict = resource already exists. For agents retrying creates, this is success.
- fmt.Fprintln(os.Stderr, "already exists (no-op)")
- return nil
+ if flags != nil && flags.idempotent {
+ return writeNoop(flags, "already_exists", "already exists (no-op)")
+ }
+ classified := apiErr(err)
+ writeAPIErrorEnvelope(flags, classified, ExitCode(classified))
+ return classified
case strings.Contains(msg, "HTTP 400") && cliutil.LooksLikeAuthError(msg):
return authErr(fmt.Errorf("%w\nhint: the API rejected the request — this usually means auth is missing or invalid."+
"\n Set your API key: export PRINTING_PRESS_GOLDEN_API_KEY=<your-key>"+
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.go
index 578330a7..3f038212 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.go
@@ -63,7 +63,7 @@ func newProjectsCreateCmd(flags *rootFlags) *cobra.Command {
}
data, statusCode, err := c.Post(path, body)
if err != nil {
- return classifyAPIError(err)
+ return classifyAPIError(err, flags)
}
if wantsHumanTable(cmd.OutOrStdout(), flags) {
// Check if response contains an array (directly or wrapped in "data")
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.go
index 1384bd9f..03b564fc 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.go
@@ -48,7 +48,7 @@ func newProjectsListCmd(flags *rootFlags) *cobra.Command {
"cursor": fmt.Sprintf("%v", flagCursor),
}, nil, flagAll, "cursor", "", "")
if err != nil {
- return classifyAPIError(err)
+ return classifyAPIError(err, flags)
}
// Print provenance to stderr for human-facing output
{
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.go
index ff35ec4f..1d3ad90e 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.go
@@ -53,7 +53,7 @@ func newProjectsTasksListProjectCmd(flags *rootFlags) *cobra.Command {
"cursor": fmt.Sprintf("%v", flagCursor),
}, nil, flagAll, "cursor", "", "")
if err != nil {
- return classifyAPIError(err)
+ return classifyAPIError(err, flags)
}
// Print provenance to stderr for human-facing output
{
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go
index 0587b7d1..42660f7b 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go
@@ -66,7 +66,7 @@ func newProjectsTasksUpdateProjectCmd(flags *rootFlags) *cobra.Command {
}
data, statusCode, err := c.Patch(path, body)
if err != nil {
- return classifyAPIError(err)
+ return classifyAPIError(err, flags)
}
if wantsHumanTable(cmd.OutOrStdout(), flags) {
// Check if response contains an array (directly or wrapped in "data")
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.go
index 57fce0ef..e56bc33d 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.go
@@ -29,7 +29,7 @@ func newPublicPromotedCmd(flags *rootFlags) *cobra.Command {
params := map[string]string{}
data, prov, err := resolveRead(cmd.Context(), c, flags, "public", false, path, params, nil)
if err != nil {
- return classifyAPIError(err)
+ return classifyAPIError(err, flags)
}
// Unwrap API response envelopes (e.g. {"status":"success","data":[...]})
// so output helpers see the inner data, not the wrapper.
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/root.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/root.go
index eb51e521..96aa19de 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/root.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/root.go
@@ -29,6 +29,7 @@ type rootFlags struct {
dryRun bool
noCache bool
noInput bool
+ idempotent bool
yes bool
agent bool
selectFields string
@@ -101,6 +102,7 @@ Run 'printing-press-golden-pp-cli doctor' to verify auth and connectivity.`,
rootCmd.PersistentFlags().BoolVar(&flags.dryRun, "dry-run", false, "Show request without sending")
rootCmd.PersistentFlags().BoolVar(&flags.noCache, "no-cache", false, "Bypass response cache")
rootCmd.PersistentFlags().BoolVar(&flags.noInput, "no-input", false, "Disable all interactive prompts (for CI/agents)")
+ rootCmd.PersistentFlags().BoolVar(&flags.idempotent, "idempotent", false, "Treat already-existing create results as a successful no-op")
rootCmd.PersistentFlags().StringVar(&flags.selectFields, "select", "", "Comma-separated fields to include in output (e.g. --select id,name,status)")
rootCmd.PersistentFlags().BoolVar(&flags.yes, "yes", false, "Skip confirmation prompts (for agents and scripts)")
rootCmd.PersistentFlags().BoolVar(&noColor, "no-color", false, "Disable colored output")
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_enterprise.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_enterprise.go
index c8bd6924..67184800 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_enterprise.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_enterprise.go
@@ -29,7 +29,7 @@ func newItemsEnterpriseCmd(flags *rootFlags) *cobra.Command {
params := map[string]string{}
data, prov, err := resolveRead(cmd.Context(), c, flags, "items", false, path, params, nil)
if err != nil {
- return classifyAPIError(err)
+ return classifyAPIError(err, flags)
}
// Print provenance to stderr for human-facing output
{
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_list.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_list.go
index e8376e6e..3fe6400e 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_list.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_list.go
@@ -30,7 +30,7 @@ func newItemsListCmd(flags *rootFlags) *cobra.Command {
data, prov, err := resolvePaginatedRead(cmd.Context(), c, flags, "items", path, map[string]string{
}, nil, flagAll, "cursor", "", "")
if err != nil {
- return classifyAPIError(err)
+ return classifyAPIError(err, flags)
}
// Print provenance to stderr for human-facing output
{
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_premium.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_premium.go
index 1a594ba2..249ba74a 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_premium.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_premium.go
@@ -29,7 +29,7 @@ func newItemsPremiumCmd(flags *rootFlags) *cobra.Command {
params := map[string]string{}
data, prov, err := resolveRead(cmd.Context(), c, flags, "items", false, path, params, nil)
if err != nil {
- return classifyAPIError(err)
+ return classifyAPIError(err, flags)
}
// Print provenance to stderr for human-facing output
{
← b4a95cb7 fix(cli): infer bearer auth from inline params (#634)
·
back to Cli Printing Press
·
feat(skills): data-driven Phase 6 menu and hold-path support 0310d3fb →