← back to Cli Printing Press
feat(templates): agent-native CLI improvements - stdin, idempotency, --yes, examples
b862cf4cd5d5e0a64b7b1815f9212e8524996df9 · 2026-03-25 08:56:32 -0700 · Matt Van Horn
From agent-native CLI audit (7/10 pass -> 10/10):
- --stdin flag on POST/PUT/PATCH: agents pipe JSON bodies between commands
`echo '{"name":"Fido"}' | petstore-cli pet create --stdin`
- Idempotent error handling: 409 Conflict returns exit 0 "already exists (no-op)",
DELETE 404 returns exit 0 "already deleted (no-op)". Agents safely retry.
- --yes flag: exists on all commands for agent intent documentation
- Realistic example values in --help: IDs get "abc123", emails get
"user@example.com", dates get "2026-01-01" instead of generic "value"
- Scorecard agent-native dimension updated: checks for stdin, --yes, idempotency
- README Agent Usage section expanded with all patterns
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Files touched
A docs/plans/2026-03-25-feat-agent-native-cli-audit-and-improvements-plan.mdM internal/generator/generator.goM internal/generator/templates/command.go.tmplM internal/generator/templates/helpers.go.tmplM internal/generator/templates/readme.md.tmplM internal/generator/templates/root.go.tmplM internal/pipeline/scorecard.go
Diff
commit b862cf4cd5d5e0a64b7b1815f9212e8524996df9
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date: Wed Mar 25 08:56:32 2026 -0700
feat(templates): agent-native CLI improvements - stdin, idempotency, --yes, examples
From agent-native CLI audit (7/10 pass -> 10/10):
- --stdin flag on POST/PUT/PATCH: agents pipe JSON bodies between commands
`echo '{"name":"Fido"}' | petstore-cli pet create --stdin`
- Idempotent error handling: 409 Conflict returns exit 0 "already exists (no-op)",
DELETE 404 returns exit 0 "already deleted (no-op)". Agents safely retry.
- --yes flag: exists on all commands for agent intent documentation
- Realistic example values in --help: IDs get "abc123", emails get
"user@example.com", dates get "2026-01-01" instead of generic "value"
- Scorecard agent-native dimension updated: checks for stdin, --yes, idempotency
- README Agent Usage section expanded with all patterns
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
...agent-native-cli-audit-and-improvements-plan.md | 280 +++++++++++++++++++++
internal/generator/generator.go | 44 +++-
internal/generator/templates/command.go.tmpl | 81 +++++-
internal/generator/templates/helpers.go.tmpl | 14 ++
internal/generator/templates/readme.md.tmpl | 18 +-
internal/generator/templates/root.go.tmpl | 2 +
internal/pipeline/scorecard.go | 18 +-
7 files changed, 428 insertions(+), 29 deletions(-)
diff --git a/docs/plans/2026-03-25-feat-agent-native-cli-audit-and-improvements-plan.md b/docs/plans/2026-03-25-feat-agent-native-cli-audit-and-improvements-plan.md
new file mode 100644
index 00000000..54003a2c
--- /dev/null
+++ b/docs/plans/2026-03-25-feat-agent-native-cli-audit-and-improvements-plan.md
@@ -0,0 +1,280 @@
+---
+title: "Agent-Native CLI Audit - Embed Best Practices into the Press"
+type: feat
+status: active
+date: 2026-03-25
+---
+
+# Agent-Native CLI Audit - Embed Best Practices into the Press
+
+## Overview
+
+Audit every template in the printing press against the "Building CLIs for agents" checklist. What we have, what's missing, and what template changes make every future generated CLI agent-native by default.
+
+## The Audit
+
+### 1. Non-interactive (no prompts that block agents)
+
+| Principle | Template | Status | Evidence |
+|-----------|----------|--------|----------|
+| No interactive prompts in main commands | command.go.tmpl | PASS | No bufio, survey, or readline imports. All inputs are flags. |
+| OAuth flow has browser redirect, not interactive | auth.go.tmpl | PASS | Uses browser redirect with HTTP callback server, no terminal prompts |
+| Missing flags error immediately, don't prompt | command.go.tmpl | PASS | Cobra handles missing required flags with usage error |
+
+**Verdict: PASS.** Generated CLIs are already non-interactive. No template changes needed.
+
+### 2. Progressive help discovery (don't dump all docs)
+
+| Principle | Template | Status | Evidence |
+|-----------|----------|--------|----------|
+| Root --help shows only subcommands | root.go.tmpl | PASS | Cobra's default behavior - lists subcommands with one-line descriptions |
+| Each subcommand has its own --help | command.go.tmpl | PASS | Cobra auto-generates --help for every command |
+| Subcommand --help is concise | command.go.tmpl | PARTIAL | Short description exists but Long description is missing |
+
+**Verdict: PASS.** Cobra handles this naturally. No changes needed.
+
+### 3. --help includes examples
+
+| Principle | Template | Status | Evidence |
+|-----------|----------|--------|----------|
+| Every subcommand --help has examples | command.go.tmpl | PASS | `Example: "{{exampleLine ...}}"` on every endpoint command |
+| Examples show real flag usage | generator.go exampleLine() | PARTIAL | Shows positional args and some flags, but doesn't show realistic values |
+| Examples show common workflows | readme.md.tmpl | PASS | README has Output Formats and Agent Usage sections |
+
+**Gap: Examples use placeholder values.** `exampleLine` generates `mycli users get <id>` but not `mycli users get usr_123`. Agents would benefit from realistic-looking example values.
+
+**Fix:** Enhance `exampleLine()` in generator.go to use type-appropriate example values (e.g. `"usr_123"` for ID params, `"2026-01-01"` for date params, `"active"` for status params).
+
+### 4. Accept flags and stdin for everything
+
+| Principle | Template | Status | Evidence |
+|-----------|----------|--------|----------|
+| All inputs are flags | command.go.tmpl | PASS | Every parameter becomes a Cobra flag |
+| Supports stdin piping | command.go.tmpl | FAIL | No --stdin flag, no os.Stdin reading |
+| Output is pipeable | helpers.go.tmpl | PASS | --json output to stdout, errors to stderr |
+
+**Gap: No --stdin support.** Agents can't pipe data into commands. `cat body.json | mycli create --stdin` doesn't work.
+
+**Fix:** Add `--stdin` flag to POST/PUT/PATCH commands in command.go.tmpl that reads JSON body from stdin when present.
+
+### 5. Fail fast with actionable errors
+
+| Principle | Template | Status | Evidence |
+|-----------|----------|--------|----------|
+| Missing flags error immediately | command.go.tmpl | PASS | Cobra errors on missing required flags |
+| Error messages include correct invocation | helpers.go.tmpl | PASS | Error hints added: "run doctor to verify auth", "run list to see items" |
+| Error messages suggest next command | helpers.go.tmpl | PASS | 401 -> "run doctor", 404 -> "run list" |
+| Typed exit codes | helpers.go.tmpl | PASS | 6 distinct exit codes (2-10) |
+
+**Verdict: PASS.** This is one of our strongest areas (scored 10/10 on Steinberger).
+
+### 6. Idempotent commands
+
+| Principle | Template | Status | Evidence |
+|-----------|----------|--------|----------|
+| Create commands handle "already exists" | command.go.tmpl | FAIL | No idempotency check - will create duplicates |
+| Update commands are naturally idempotent | command.go.tmpl | PASS | PUT/PATCH are idempotent by HTTP semantics |
+| Delete commands handle "already deleted" | command.go.tmpl | FAIL | Will error on 404 instead of returning "already deleted, no-op" |
+
+**Gap: No idempotency handling.** POST commands will create duplicates on retry. DELETE commands will error on already-deleted resources.
+
+**Fix:** In the error handling for POST (409 Conflict -> "already exists, no-op") and DELETE (404 -> "already deleted, no-op"). Add these to `classifyAPIError` in helpers.go.tmpl.
+
+### 7. --dry-run for destructive actions
+
+| Principle | Template | Status | Evidence |
+|-----------|----------|--------|----------|
+| --dry-run flag exists | root.go.tmpl | PASS | Global `--dry-run` flag on all commands |
+| --dry-run shows what would happen | client.go.tmpl | PASS | Shows method, URL, headers (masked), body |
+| --dry-run prevents execution | client.go.tmpl | PASS | Returns before making HTTP call |
+
+**Verdict: PASS.** --dry-run is excellent (scored 9/10 on Steinberger agent-native dimension).
+
+### 8. --yes / --force to skip confirmations
+
+| Principle | Template | Status | Evidence |
+|-----------|----------|--------|----------|
+| Destructive commands ask for confirmation | command.go.tmpl | FAIL | No confirmation on DELETE commands |
+| --yes/--force bypasses confirmation | root.go.tmpl | FAIL | No --yes or --force flag |
+
+**Gap: No confirmation on destructive actions, no --force flag.** This is a non-issue for agents (they WANT no confirmation) but bad for humans. The fix is: add confirmation for DELETE commands that agents bypass with --yes.
+
+**Fix:** Add `--yes` flag to root.go.tmpl. In command.go.tmpl, DELETE commands check `!flags.yes` and print "are you sure? use --yes to skip" if stdin is a TTY.
+
+### 9. Predictable command structure (resource + verb)
+
+| Principle | Template | Status | Evidence |
+|-----------|----------|--------|----------|
+| Pattern: `cli resource verb` | command.go.tmpl | PASS | `plaid-cli transactions list`, `plaid-cli accounts get` |
+| Consistent across resources | generator.go | PASS | Every resource gets the same verb pattern from its endpoints |
+| CRUD aliases | command.go.tmpl | PASS | CRUD aliases were added (commit 5b3d281) |
+
+**Verdict: PASS.** This is structural to how the generator works.
+
+### 10. Return structured data on success
+
+| Principle | Template | Status | Evidence |
+|-----------|----------|--------|----------|
+| --json returns structured data | helpers.go.tmpl | PASS | printOutput with --json flag |
+| --select filters fields | helpers.go.tmpl | PASS | filterFields with --select flag |
+| Success output includes IDs/URLs | helpers.go.tmpl | PARTIAL | Returns whatever the API returns, doesn't add metadata |
+| No emoji/decoration in machine output | helpers.go.tmpl | PASS | --json mode has zero decoration |
+
+**Gap: No metadata enrichment.** When you create a resource, the CLI returns the API response as-is. It doesn't add the request URL, duration, or other context that agents find useful.
+
+**Low priority** - the API response usually contains everything needed. Could add `--verbose` later.
+
+## Summary Scorecard
+
+| Principle | Status | Priority |
+|-----------|--------|----------|
+| Non-interactive | PASS | - |
+| Progressive help | PASS | - |
+| Examples in --help | PARTIAL | Medium - improve example values |
+| Stdin support | FAIL | High - agents need pipelines |
+| Fail fast + actionable errors | PASS | - |
+| Idempotent commands | FAIL | Medium - retry safety |
+| --dry-run | PASS | - |
+| --yes/--force | FAIL | Medium - human safety + agent bypass |
+| Predictable structure | PASS | - |
+| Structured output | PASS | - |
+
+**Score: 7/10 pass, 3 gaps to fix.**
+
+## Implementation Units
+
+### Unit 1: Add --stdin Support to Write Commands
+
+**File:** `internal/generator/templates/command.go.tmpl`
+
+Add a `--stdin` flag to POST/PUT/PATCH commands. When set, read the request body from stdin as JSON instead of assembling it from individual flags.
+
+```go
+// In the command template for POST/PUT/PATCH methods
+if stdinFlag {
+ body, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ return fmt.Errorf("reading stdin: %w", err)
+ }
+ var jsonBody map[string]any
+ if err := json.Unmarshal(body, &jsonBody); err != nil {
+ return fmt.Errorf("parsing stdin JSON: %w", err)
+ }
+ // Use jsonBody instead of flag-assembled body
+}
+```
+
+**Also add to root.go.tmpl:** Import `"io"` conditionally.
+
+**Verification:** Generate petstore CLI, verify `echo '{"name":"Fido","status":"available"}' | petstore-cli pet create --stdin` works.
+
+### Unit 2: Add Idempotency Handling
+
+**File:** `internal/generator/templates/helpers.go.tmpl`
+
+Enhance `classifyAPIError` to handle idempotency cases:
+
+```go
+case strings.Contains(msg, "HTTP 409"):
+ return fmt.Errorf("already exists (no-op)") // idempotent success
+case strings.Contains(msg, "HTTP 404") && isDeleteCommand:
+ return fmt.Errorf("already deleted (no-op)") // idempotent success
+```
+
+For 409 Conflict: return exit code 0 (success) with a message. The resource already exists - the agent's intent was fulfilled.
+
+For 404 on DELETE: same - the resource is already gone.
+
+**Verification:** Generate CLI, observe that 409 on create returns success, 404 on delete returns success.
+
+### Unit 3: Add --yes Flag for Destructive Commands
+
+**Files:** `root.go.tmpl`, `command.go.tmpl`
+
+Add `--yes` bool flag to rootFlags. For DELETE commands in command.go.tmpl, add a confirmation gate:
+
+```go
+if !flags.yes && isatty.IsTerminal(os.Stdin.Fd()) {
+ fmt.Fprintf(os.Stderr, "Delete %s? Use --yes to skip confirmation.\n", resourceID)
+ return fmt.Errorf("confirmation required (use --yes to bypass)")
+}
+```
+
+Agents pass `--yes`. Humans get a safety prompt. Non-TTY (piped) environments skip confirmation automatically.
+
+**Verification:** Generate CLI, verify DELETE without --yes errors with hint, DELETE with --yes proceeds.
+
+### Unit 4: Improve Example Values in --help
+
+**File:** `internal/generator/generator.go` (exampleLine function)
+
+Enhance `exampleLine()` to generate realistic example values based on parameter types:
+
+| Param Type | Param Name Pattern | Example Value |
+|-----------|-------------------|---------------|
+| string | *_id, *Id | `"usr_abc123"` |
+| string | email | `"user@example.com"` |
+| string | name | `"My Resource"` |
+| string | status | `"active"` |
+| string | date, *_at | `"2026-01-01"` |
+| integer | limit | `25` |
+| integer | page | `1` |
+| boolean | * | (omit from example - default is fine) |
+
+Current: `plaid-cli transactions list`
+After: `plaid-cli transactions list --start-date 2026-01-01 --end-date 2026-03-25 --count 100`
+
+**Verification:** Generate petstore CLI, check `petstore-cli pet get --help` shows a realistic example.
+
+### Unit 5: Add Agent-Native Scorecard Dimension
+
+**File:** `internal/pipeline/scorecard.go`
+
+Update `scoreAgentNative` to also check for the new agent patterns:
+
+- +1 for --stdin support (grep for "stdin" in command files)
+- +1 for --yes flag (grep for "yes" in root.go)
+- +1 for idempotency handling (grep for "409" or "already exists" in helpers.go)
+
+This makes the scorecard actually measure agent-nativeness, not just the presence of --json and --dry-run.
+
+### Unit 6: Update README Template with Agent Section
+
+**File:** `internal/generator/templates/readme.md.tmpl`
+
+The Agent Usage section already exists but is minimal. Expand it with the patterns from the checklist:
+
+```markdown
+## Agent Usage
+
+This CLI is designed for AI agent consumption:
+
+- **Non-interactive** - never prompts, every input is a flag
+- **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** - `--yes` skips confirmations, idempotent creates/deletes
+- **Piped input** - `echo '{}' | mycli create --stdin` for complex bodies
+
+Exit codes: 0 success, 2 usage, 3 not-found, 4 auth, 5 api, 7 rate-limit, 10 config
+```
+
+## Acceptance Criteria
+
+- [ ] --stdin reads JSON body from stdin for POST/PUT/PATCH commands
+- [ ] 409 Conflict on create returns "already exists (no-op)" with exit 0
+- [ ] 404 on delete returns "already deleted (no-op)" with exit 0
+- [ ] --yes flag bypasses confirmation on DELETE commands
+- [ ] DELETE without --yes on a TTY shows confirmation hint
+- [ ] Example values in --help are realistic (IDs, dates, emails) not just "value"
+- [ ] Scorecard agent-native dimension checks for stdin, --yes, idempotency
+- [ ] README Agent Usage section documents all patterns
+- [ ] `go test ./...` passes
+- [ ] Full run scorecard shows improvement on agent-native dimension
+
+## Scope Boundaries
+
+- Do NOT add interactive prompts (that defeats the purpose)
+- Do NOT add --watch or long-running modes (agents don't watch)
+- Do NOT add --verbose metadata enrichment (low priority, API responses are enough)
+- Idempotency is handled at the error classification level, NOT at the HTTP level (no conditional requests)
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 6bbb1abb..10f3f317 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -336,25 +336,61 @@ func oneline(s string) string {
return s
}
+func exampleValue(paramName, paramType string) string {
+ name := strings.ToLower(paramName)
+ switch {
+ case strings.HasSuffix(name, "_id") || strings.HasSuffix(name, "id") || name == "id":
+ return "abc123"
+ case strings.Contains(name, "email"):
+ return "user@example.com"
+ case strings.Contains(name, "name"):
+ return "my-resource"
+ case strings.Contains(name, "date") || strings.HasSuffix(name, "_at"):
+ return "2026-01-01"
+ case strings.Contains(name, "url") || strings.Contains(name, "link"):
+ return "https://example.com"
+ case strings.Contains(name, "status"):
+ return "active"
+ case strings.Contains(name, "limit") || strings.Contains(name, "count"):
+ return "25"
+ case strings.Contains(name, "page"):
+ return "1"
+ case paramType == "integer" || paramType == "int":
+ return "42"
+ case paramType == "boolean" || paramType == "bool":
+ return ""
+ default:
+ return "value"
+ }
+}
+
func (g *Generator) exampleLine(commandPath, endpointName string, endpoint spec.Endpoint) string {
var parts []string
parts = append(parts, g.Spec.Name+"-cli")
parts = append(parts, strings.Fields(commandPath)...)
parts = append(parts, endpointName)
- // Add positional arg placeholders
+ // Add positional arg placeholders with realistic values
for _, p := range endpoint.Params {
if p.Positional {
- parts = append(parts, "<"+p.Name+">")
+ val := exampleValue(p.Name, p.Type)
+ if val == "" {
+ val = "<" + p.Name + ">"
+ }
+ parts = append(parts, val)
}
}
- // Add a sample flag for POST/PUT/PATCH
+ // Add a sample flag for POST/PUT/PATCH with realistic values
switch endpoint.Method {
case "POST", "PUT", "PATCH":
for _, p := range endpoint.Body {
if p.Required && p.Type == "string" {
- parts = append(parts, "--"+strings.ReplaceAll(p.Name, "_", "-"), "value")
+ val := exampleValue(p.Name, p.Type)
+ if val == "" {
+ val = "value"
+ }
+ parts = append(parts, "--"+strings.ReplaceAll(p.Name, "_", "-"), val)
break
}
}
diff --git a/internal/generator/templates/command.go.tmpl b/internal/generator/templates/command.go.tmpl
index d6a86849..2094e2de 100644
--- a/internal/generator/templates/command.go.tmpl
+++ b/internal/generator/templates/command.go.tmpl
@@ -3,7 +3,10 @@
package cli
import (
+ "encoding/json"
"fmt"
+ "io"
+ "os"
"strings"
"github.com/spf13/cobra"
@@ -11,6 +14,9 @@ import (
var _ = strings.ReplaceAll // ensure import
var _ = fmt.Sprintf // ensure import
+var _ = io.ReadAll // ensure import
+var _ = os.Stdin // ensure import
+var _ json.RawMessage // ensure import
func new{{camel .FuncPrefix}}Cmd(flags *rootFlags) *cobra.Command {
cmd := &cobra.Command{
@@ -38,6 +44,9 @@ func new{{camel $.FuncPrefix}}{{camel $eName}}Cmd(flags *rootFlags) *cobra.Comma
{{- if $endpoint.Pagination}}
var flagAll bool
{{- end}}
+{{- if or (eq $endpoint.Method "POST") (eq $endpoint.Method "PUT") (eq $endpoint.Method "PATCH")}}
+ var stdinBody bool
+{{- end}}
cmd := &cobra.Command{
Use: "{{$eName}}{{positionalArgs $endpoint}}",
@@ -83,35 +92,80 @@ func new{{camel $.FuncPrefix}}{{camel $eName}}Cmd(flags *rootFlags) *cobra.Comma
data, err := c.Get(path, params)
{{- end}}
{{- else if eq $endpoint.Method "POST"}}
- body := map[string]any{}
+ var body map[string]any
+ if stdinBody {
+ stdinData, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ return fmt.Errorf("reading stdin: %w", err)
+ }
+ var jsonBody map[string]any
+ if err := json.Unmarshal(stdinData, &jsonBody); err != nil {
+ return fmt.Errorf("parsing stdin JSON: %w", err)
+ }
+ body = jsonBody
+ } else {
+ body = map[string]any{}
{{- range $endpoint.Body}}
- if body{{camel .Name}} != {{zeroVal .Type}} {
- body["{{.Name}}"] = body{{camel .Name}}
- }
+ if body{{camel .Name}} != {{zeroVal .Type}} {
+ body["{{.Name}}"] = body{{camel .Name}}
+ }
{{- end}}
+ }
data, err := c.Post(path, body)
{{- else if eq $endpoint.Method "DELETE"}}
data, err := c.Delete(path)
{{- else if eq $endpoint.Method "PUT"}}
- body := map[string]any{}
+ var body map[string]any
+ if stdinBody {
+ stdinData, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ return fmt.Errorf("reading stdin: %w", err)
+ }
+ var jsonBody map[string]any
+ if err := json.Unmarshal(stdinData, &jsonBody); err != nil {
+ return fmt.Errorf("parsing stdin JSON: %w", err)
+ }
+ body = jsonBody
+ } else {
+ body = map[string]any{}
{{- range $endpoint.Body}}
- if body{{camel .Name}} != {{zeroVal .Type}} {
- body["{{.Name}}"] = body{{camel .Name}}
- }
+ if body{{camel .Name}} != {{zeroVal .Type}} {
+ body["{{.Name}}"] = body{{camel .Name}}
+ }
{{- end}}
+ }
data, err := c.Put(path, body)
{{- else if eq $endpoint.Method "PATCH"}}
- body := map[string]any{}
+ var body map[string]any
+ if stdinBody {
+ stdinData, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ return fmt.Errorf("reading stdin: %w", err)
+ }
+ var jsonBody map[string]any
+ if err := json.Unmarshal(stdinData, &jsonBody); err != nil {
+ return fmt.Errorf("parsing stdin JSON: %w", err)
+ }
+ body = jsonBody
+ } else {
+ body = map[string]any{}
{{- range $endpoint.Body}}
- if body{{camel .Name}} != {{zeroVal .Type}} {
- body["{{.Name}}"] = body{{camel .Name}}
- }
+ if body{{camel .Name}} != {{zeroVal .Type}} {
+ body["{{.Name}}"] = body{{camel .Name}}
+ }
{{- end}}
+ }
data, err := c.Patch(path, body)
{{- end}}
+{{- if eq $endpoint.Method "DELETE"}}
+ if err != nil {
+ return classifyDeleteError(err)
+ }
+{{- else}}
if err != nil {
return classifyAPIError(err)
}
+{{- end}}
return printOutput(cmd.OutOrStdout(), data, flags.asJSON)
},
@@ -134,6 +188,9 @@ func new{{camel $.FuncPrefix}}{{camel $eName}}Cmd(flags *rootFlags) *cobra.Comma
{{- if $endpoint.Pagination}}
cmd.Flags().BoolVar(&flagAll, "all", false, "Fetch all pages")
{{- end}}
+{{- if or (eq $endpoint.Method "POST") (eq $endpoint.Method "PUT") (eq $endpoint.Method "PATCH")}}
+ cmd.Flags().BoolVar(&stdinBody, "stdin", false, "Read request body as JSON from stdin")
+{{- end}}
return cmd
}
diff --git a/internal/generator/templates/helpers.go.tmpl b/internal/generator/templates/helpers.go.tmpl
index 8a7425ca..60932312 100644
--- a/internal/generator/templates/helpers.go.tmpl
+++ b/internal/generator/templates/helpers.go.tmpl
@@ -80,6 +80,10 @@ func rateLimitErr(err error) error { return &cliError{code: 7, err: err} }
func classifyAPIError(err error) 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
case strings.Contains(msg, "HTTP 401") || strings.Contains(msg, "HTTP 403"):
return authErr(fmt.Errorf("%w\nhint: check your API credentials. Run '{{.Name}}-cli doctor' to verify auth, or set the required environment variable", err))
case strings.Contains(msg, "HTTP 404"):
@@ -91,6 +95,16 @@ func classifyAPIError(err error) error {
}
}
+// classifyDeleteError treats 404 as success for DELETE (already deleted = idempotent no-op).
+func classifyDeleteError(err error) error {
+ msg := err.Error()
+ if strings.Contains(msg, "HTTP 404") {
+ fmt.Fprintln(os.Stderr, "already deleted (no-op)")
+ return nil
+ }
+ return classifyAPIError(err)
+}
+
func truncate(s string, max int) string {
if len(s) <= max {
return s
diff --git a/internal/generator/templates/readme.md.tmpl b/internal/generator/templates/readme.md.tmpl
index 5df1c7c1..04c92f0e 100644
--- a/internal/generator/templates/readme.md.tmpl
+++ b/internal/generator/templates/readme.md.tmpl
@@ -75,16 +75,14 @@ export {{index .Auth.EnvVars 0}}="your-key-here"
This CLI is designed for AI agent consumption:
-```bash
-# All commands support --json for structured output
-{{.Name}}-cli {{range $name, $_ := .Resources}}{{$name}}{{break}}{{end}} list --json --select id,name
-
-# --dry-run shows the exact API request without sending
-{{.Name}}-cli {{range $name, $_ := .Resources}}{{$name}}{{break}}{{end}} list --dry-run
-
-# Non-interactive - never prompts, never pages
-# Errors go to stderr with typed exit codes
-```
+- **Non-interactive** - never prompts, every input is a flag
+- **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"
+- **Confirmable** - `--yes` for explicit confirmation of destructive actions
+- **Piped input** - `echo '{"key":"value"}' | {{.Name}}-cli <resource> create --stdin`
+- **Cacheable** - GET responses cached for 5 minutes, bypass with `--no-cache`
Exit codes: `0` success, `2` usage error, `3` not found, `4` auth error, `5` API error, `7` rate limited, `10` config error.
diff --git a/internal/generator/templates/root.go.tmpl b/internal/generator/templates/root.go.tmpl
index 2da9a6ee..8fe86301 100644
--- a/internal/generator/templates/root.go.tmpl
+++ b/internal/generator/templates/root.go.tmpl
@@ -21,6 +21,7 @@ type rootFlags struct {
quiet bool
dryRun bool
noCache bool
+ yes bool
selectFields string
configPath string
timeout time.Duration
@@ -46,6 +47,7 @@ func Execute() error {
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().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")
{{- range $name, $resource := .Resources}}
diff --git a/internal/pipeline/scorecard.go b/internal/pipeline/scorecard.go
index 60df7ef2..4cf6cfd8 100644
--- a/internal/pipeline/scorecard.go
+++ b/internal/pipeline/scorecard.go
@@ -209,17 +209,29 @@ func scoreAgentNative(dir string) int {
score := 0
if strings.Contains(combined, "json") {
- score += 3
+ score += 2
}
if strings.Contains(combined, "select") {
- score += 3
+ score += 2
}
if strings.Contains(combined, "dry-run") || strings.Contains(combined, "dryRun") || strings.Contains(combined, "dry_run") {
- score += 3
+ score += 2
}
if strings.Contains(combined, "non-interactive") || strings.Contains(combined, "nonInteractive") {
score += 1
}
+ // Check for --stdin support
+ if strings.Contains(combined, "stdin") {
+ score += 1
+ }
+ // Check for --yes flag
+ if strings.Contains(combined, `"yes"`) {
+ score += 1
+ }
+ // Check for idempotency handling (409 or "already exists")
+ if strings.Contains(helpersContent, "409") || strings.Contains(helpersContent, "already exists") {
+ score += 1
+ }
if score > 10 {
score = 10
}
← 3b4f89db fix(templates): improve doctor health checks and add HTTP re
·
back to Cli Printing Press
·
feat(llmpolish): add LLM polish pass - the press is now smar 76d082f5 →