[object Object]

← back to Cli Printing Press

fix(cli): generator template improvements — ID typing, dead imports, README cookbook (#102)

82a3614844064fb71ba9da7be67e2cbcbd2f7ef1 · 2026-04-01 00:59:18 -0700 · Trevin Chow

* fix(cli): generator template improvements — ID typing, dead imports, README cookbook

Three template improvements from the Steam retro:

1. ID params override int→string: Parameters named *id/*ids now generate
   StringVar instead of IntVar. SteamID64 (17 digits) overflows int64,
   and zero-value int confusion makes IntVar unsuitable for identifiers.
   Affects goType, cobraFlagFunc, defaultVal, and zeroVal — all have
   param-aware variants that check isIDParam().

2. Remove dead import markers: The `var _ = strings.ReplaceAll` and
   similar lines are no longer emitted. Removed unused `strings` import.
   Made `io` import conditional on POST/PUT/PATCH methods.

3. README Cookbook section: readme.md.tmpl now emits a Cookbook section
   with 5 common workflow examples (JSON output, field filtering,
   dry-run, sync+search, export). Scorecard README scorer gives +2 for
   this section.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli): scope ID-to-string override to query/path params only, deterministic cookbook

Codex review fixes:

1. ID-to-string override now only applies to query/path flag params, not
   request body fields. Body fields keep their original int type to
   preserve the JSON wire format — sending "123" instead of 123 would
   break strict APIs. Added defaultValForParam() alongside defaultVal().

2. firstResource() sorts map keys before selecting, making the README
   cookbook examples deterministic across runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 82a3614844064fb71ba9da7be67e2cbcbd2f7ef1
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Wed Apr 1 00:59:18 2026 -0700

    fix(cli): generator template improvements — ID typing, dead imports, README cookbook (#102)
    
    * fix(cli): generator template improvements — ID typing, dead imports, README cookbook
    
    Three template improvements from the Steam retro:
    
    1. ID params override int→string: Parameters named *id/*ids now generate
       StringVar instead of IntVar. SteamID64 (17 digits) overflows int64,
       and zero-value int confusion makes IntVar unsuitable for identifiers.
       Affects goType, cobraFlagFunc, defaultVal, and zeroVal — all have
       param-aware variants that check isIDParam().
    
    2. Remove dead import markers: The `var _ = strings.ReplaceAll` and
       similar lines are no longer emitted. Removed unused `strings` import.
       Made `io` import conditional on POST/PUT/PATCH methods.
    
    3. README Cookbook section: readme.md.tmpl now emits a Cookbook section
       with 5 common workflow examples (JSON output, field filtering,
       dry-run, sync+search, export). Scorecard README scorer gives +2 for
       this section.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    
    * fix(cli): scope ID-to-string override to query/path params only, deterministic cookbook
    
    Codex review fixes:
    
    1. ID-to-string override now only applies to query/path flag params, not
       request body fields. Body fields keep their original int type to
       preserve the JSON wire format — sending "123" instead of 123 would
       break strict APIs. Added defaultValForParam() alongside defaultVal().
    
    2. firstResource() sorts map keys before selecting, making the README
       cookbook examples deterministic across runs.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
 internal/generator/generator.go                    | 86 +++++++++++++++++++---
 .../generator/templates/command_endpoint.go.tmpl   | 15 ++--
 internal/generator/templates/readme.md.tmpl        | 22 ++++++
 3 files changed, 101 insertions(+), 22 deletions(-)

diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 2b5389d7..11182ce9 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -6,6 +6,7 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
+	"sort"
 	"strconv"
 	"strings"
 	"text/template"
@@ -68,18 +69,27 @@ func New(s *spec.APISpec, outputDir string) *Generator {
 		templates: make(map[string]*template.Template),
 	}
 	g.funcs = template.FuncMap{
-		"title":              cases.Title(language.English).String,
-		"lower":              strings.ToLower,
-		"upper":              strings.ToUpper,
-		"join":               strings.Join,
-		"camel":              toCamel,
-		"snake":              toSnake,
-		"pascal":             toPascal,
-		"goType":             goType,
-		"goStoreType":        goStoreType,
-		"cobraFlagFunc":      cobraFlagFunc,
-		"defaultVal":         defaultVal,
-		"zeroVal":            zeroVal,
+		"title":                 cases.Title(language.English).String,
+		"lower":                 strings.ToLower,
+		"upper":                 strings.ToUpper,
+		"join":                  strings.Join,
+		"camel":                 toCamel,
+		"snake":                 toSnake,
+		"pascal":                toPascal,
+		"goType":                goType,
+		"goTypeForParam":        goTypeForParam,
+		"goStoreType":           goStoreType,
+		"cobraFlagFunc":         cobraFlagFunc,
+		"cobraFlagFuncForParam": cobraFlagFuncForParam,
+		"defaultVal":            defaultVal,
+		"defaultValForParam":    defaultValForParam,
+		"zeroVal":               zeroVal,
+		"zeroValForParam": func(name, t string) string {
+			if isIDParam(name) && t == "int" {
+				return `""`
+			}
+			return zeroVal(t)
+		},
 		"positionalArgs":     positionalArgs,
 		"configTag":          configTag,
 		"camelToJSON":        camelToJSON,
@@ -97,6 +107,17 @@ func New(s *spec.APISpec, outputDir string) *Generator {
 		"modulePath":         func() string { return naming.CLI(s.Name) },
 		"kebab":              toKebab,
 		"envName":            func(s string) string { return strings.ToUpper(strings.ReplaceAll(s, "-", "_")) },
+		"firstResource": func(resources map[string]spec.Resource) string {
+			var names []string
+			for name := range resources {
+				names = append(names, name)
+			}
+			sort.Strings(names)
+			if len(names) > 0 {
+				return names[0]
+			}
+			return "resource"
+		},
 	}
 	return g
 }
@@ -618,6 +639,17 @@ func toPascal(s string) string {
 	return strings.Join(parts, "")
 }
 
+// isIDParam returns true if the parameter name suggests it's an identifier
+// that should be typed as string regardless of the spec's declared type.
+// IDs like steamid (17-digit number) overflow int64, and zero-value confusion
+// makes IntVar unsuitable for identifiers.
+func isIDParam(name string) bool {
+	lower := strings.ToLower(name)
+	return strings.HasSuffix(lower, "id") || strings.HasSuffix(lower, "ids") ||
+		strings.HasSuffix(lower, "_id") || strings.HasSuffix(lower, "_ids") ||
+		lower == "steamid" || lower == "steamids"
+}
+
 func goType(t string) string {
 	switch t {
 	case "string":
@@ -708,6 +740,36 @@ func cobraFlagFunc(t string) string {
 	}
 }
 
+// goTypeForParam returns the Go type for a parameter, overriding int→string
+// for ID-like parameters to avoid overflow and zero-value confusion.
+func goTypeForParam(name, t string) string {
+	if isIDParam(name) && t == "int" {
+		return "string"
+	}
+	return goType(t)
+}
+
+// cobraFlagFuncForParam returns the cobra flag function, overriding IntVar→StringVar
+// for ID-like parameters.
+func cobraFlagFuncForParam(name, t string) string {
+	if isIDParam(name) && t == "int" {
+		return "StringVar"
+	}
+	return cobraFlagFunc(t)
+}
+
+// defaultValForParam returns the default value for a flag parameter,
+// overriding int→string for ID-like parameters.
+func defaultValForParam(p spec.Param) string {
+	if isIDParam(p.Name) && p.Type == "int" {
+		if p.Default != nil {
+			return fmt.Sprintf("%q", fmt.Sprintf("%v", p.Default))
+		}
+		return `""`
+	}
+	return defaultVal(p)
+}
+
 func defaultVal(p spec.Param) string {
 	if p.Default != nil {
 		// Coerce the default value to match the declared param type
diff --git a/internal/generator/templates/command_endpoint.go.tmpl b/internal/generator/templates/command_endpoint.go.tmpl
index a3f83537..b27c210b 100644
--- a/internal/generator/templates/command_endpoint.go.tmpl
+++ b/internal/generator/templates/command_endpoint.go.tmpl
@@ -6,23 +6,18 @@ package cli
 import (
 	"encoding/json"
 	"fmt"
+{{- if or (eq .Endpoint.Method "POST") (eq .Endpoint.Method "PUT") (eq .Endpoint.Method "PATCH")}}
 	"io"
+{{- end}}
 	"os"
-	"strings"
 
 	"github.com/spf13/cobra"
 )
 
-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}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra.Command {
 {{- range .Endpoint.Params}}
 {{- if not .Positional}}
-	var flag{{camel .Name}} {{goType .Type}}
+	var flag{{camel .Name}} {{goTypeForParam .Name .Type}}
 {{- end}}
 {{- end}}
 {{- range .Endpoint.Body}}
@@ -78,7 +73,7 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
 			params := map[string]string{}
 {{- range .Endpoint.Params}}
 {{- if not .Positional}}
-			if flag{{camel .Name}} != {{zeroVal .Type}} {
+			if flag{{camel .Name}} != {{zeroValForParam .Name .Type}} {
 				params["{{.Name}}"] = fmt.Sprintf("%v", flag{{camel .Name}})
 			}
 {{- end}}
@@ -239,7 +234,7 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
 
 {{- range .Endpoint.Params}}
 {{- if not .Positional}}
-	cmd.Flags().{{cobraFlagFunc .Type}}(&flag{{camel .Name}}, "{{flagName .Name}}", {{defaultVal .}}, "{{oneline .Description}}")
+	cmd.Flags().{{cobraFlagFuncForParam .Name .Type}}(&flag{{camel .Name}}, "{{flagName .Name}}", {{defaultValForParam .}}, "{{oneline .Description}}")
 {{- if .Required}}
 	_ = cmd.MarkFlagRequired("{{flagName .Name}}")
 {{- end}}
diff --git a/internal/generator/templates/readme.md.tmpl b/internal/generator/templates/readme.md.tmpl
index f2248df2..0dc1df26 100644
--- a/internal/generator/templates/readme.md.tmpl
+++ b/internal/generator/templates/readme.md.tmpl
@@ -135,6 +135,28 @@ This CLI is designed for AI agent consumption:
 
 Exit codes: `0` success, `2` usage error, `3` not found, `4` auth error, `5` API error, `7` rate limited, `10` config error.
 
+## Cookbook
+
+Common workflows and recipes:
+
+```bash
+# List resources as JSON for scripting
+{{.Name}}-pp-cli {{firstResource .Resources}} list --json
+
+# Filter to specific fields
+{{.Name}}-pp-cli {{firstResource .Resources}} list --json --select id,name,status
+
+# Dry run to preview the request
+{{.Name}}-pp-cli {{firstResource .Resources}} list --dry-run
+
+# Sync data locally for offline search
+{{.Name}}-pp-cli sync
+{{.Name}}-pp-cli search "query"
+
+# Export for backup
+{{.Name}}-pp-cli export --format jsonl > backup.jsonl
+```
+
 ## Health Check
 
 ```bash

← 8a5d01cc fix(cli): scorer behavioral detection — path validity, insig  ·  back to Cli Printing Press  ·  fix(ci): add post-merge hook to rebuild binary after git pul 3af5d2d7 →