[object Object]

← back to Cli Printing Press

feat(templates): add --select flag, error hints, README rewrite, and Owner variable

780c6b51715d663f9fc70909f84d2723e960e18f · 2026-03-24 22:51:01 -0700 · Matt Van Horn

Phase 0: Template quality push (4.9 -> 6.9 on Steinberger scale)

- Add --select flag for JSON field filtering (e.g. --select id,name,status)
  with filterFields/printOutputFiltered functions in helpers.go.tmpl
- Add actionable error hints: 401 says "run doctor to verify auth",
  404 says "run list to see available items"
- Rewrite readme.md.tmpl: quickstart section, output format examples,
  agent usage section, troubleshooting, dogfood marker comments
- Replace hardcoded USER placeholder with {{.Owner}} in all 13 templates
  (go.mod, main, root, client, doctor, auth, config, types, command,
  helpers, goreleaser, readme). Add Owner field to spec.APISpec.
- Add "Code generated by CLI Printing Press. DO NOT EDIT." to all 9
  .go.tmpl files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files touched

Diff

commit 780c6b51715d663f9fc70909f84d2723e960e18f
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date:   Tue Mar 24 22:51:01 2026 -0700

    feat(templates): add --select flag, error hints, README rewrite, and Owner variable
    
    Phase 0: Template quality push (4.9 -> 6.9 on Steinberger scale)
    
    - Add --select flag for JSON field filtering (e.g. --select id,name,status)
      with filterFields/printOutputFiltered functions in helpers.go.tmpl
    - Add actionable error hints: 401 says "run doctor to verify auth",
      404 says "run list to see available items"
    - Rewrite readme.md.tmpl: quickstart section, output format examples,
      agent usage section, troubleshooting, dogfood marker comments
    - Replace hardcoded USER placeholder with {{.Owner}} in all 13 templates
      (go.mod, main, root, client, doctor, auth, config, types, command,
      helpers, goreleaser, readme). Add Owner field to spec.APISpec.
    - Add "Code generated by CLI Printing Press. DO NOT EDIT." to all 9
      .go.tmpl files
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
 internal/generator/generator.go                   |   4 +
 internal/generator/templates/auth.go.tmpl         |   4 +-
 internal/generator/templates/client.go.tmpl       |   4 +-
 internal/generator/templates/command.go.tmpl      |   2 +
 internal/generator/templates/config.go.tmpl       |   2 +
 internal/generator/templates/doctor.go.tmpl       |   4 +-
 internal/generator/templates/go.mod.tmpl          |   2 +-
 internal/generator/templates/goreleaser.yaml.tmpl |   6 +-
 internal/generator/templates/helpers.go.tmpl      |  75 +++++++++++++---
 internal/generator/templates/main.go.tmpl         |   4 +-
 internal/generator/templates/readme.md.tmpl       | 104 +++++++++++++++++++---
 internal/generator/templates/root.go.tmpl         |   8 +-
 internal/generator/templates/types.go.tmpl        |   2 +
 internal/spec/spec.go                             |   1 +
 14 files changed, 186 insertions(+), 36 deletions(-)

diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index c597b7d3..6bbb1abb 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -22,6 +22,10 @@ type Generator struct {
 }
 
 func New(s *spec.APISpec, outputDir string) *Generator {
+	// Default Owner to "USER" for backward compatibility
+	if s.Owner == "" {
+		s.Owner = "USER"
+	}
 	g := &Generator{Spec: s, OutputDir: outputDir}
 	g.funcs = template.FuncMap{
 		"title":             strings.Title,
diff --git a/internal/generator/templates/auth.go.tmpl b/internal/generator/templates/auth.go.tmpl
index 66b6a2cf..89c92c93 100644
--- a/internal/generator/templates/auth.go.tmpl
+++ b/internal/generator/templates/auth.go.tmpl
@@ -1,3 +1,5 @@
+// Code generated by CLI Printing Press. DO NOT EDIT.
+
 package cli
 
 import (
@@ -15,7 +17,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/USER/{{.Name}}-cli/internal/config"
+	"github.com/{{.Owner}}/{{.Name}}-cli/internal/config"
 	"github.com/spf13/cobra"
 )
 
diff --git a/internal/generator/templates/client.go.tmpl b/internal/generator/templates/client.go.tmpl
index 95e8ce72..8f88baa2 100644
--- a/internal/generator/templates/client.go.tmpl
+++ b/internal/generator/templates/client.go.tmpl
@@ -1,3 +1,5 @@
+// Code generated by CLI Printing Press. DO NOT EDIT.
+
 package client
 
 import (
@@ -12,7 +14,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/USER/{{.Name}}-cli/internal/config"
+	"github.com/{{.Owner}}/{{.Name}}-cli/internal/config"
 )
 
 type Client struct {
diff --git a/internal/generator/templates/command.go.tmpl b/internal/generator/templates/command.go.tmpl
index 0f3db719..d6a86849 100644
--- a/internal/generator/templates/command.go.tmpl
+++ b/internal/generator/templates/command.go.tmpl
@@ -1,3 +1,5 @@
+// Code generated by CLI Printing Press. DO NOT EDIT.
+
 package cli
 
 import (
diff --git a/internal/generator/templates/config.go.tmpl b/internal/generator/templates/config.go.tmpl
index 5329aaac..23cb4454 100644
--- a/internal/generator/templates/config.go.tmpl
+++ b/internal/generator/templates/config.go.tmpl
@@ -1,3 +1,5 @@
+// Code generated by CLI Printing Press. DO NOT EDIT.
+
 package config
 
 import (
diff --git a/internal/generator/templates/doctor.go.tmpl b/internal/generator/templates/doctor.go.tmpl
index 99e7d9ce..4ed07865 100644
--- a/internal/generator/templates/doctor.go.tmpl
+++ b/internal/generator/templates/doctor.go.tmpl
@@ -1,3 +1,5 @@
+// Code generated by CLI Printing Press. DO NOT EDIT.
+
 package cli
 
 import (
@@ -6,7 +8,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/USER/{{.Name}}-cli/internal/config"
+	"github.com/{{.Owner}}/{{.Name}}-cli/internal/config"
 	"github.com/spf13/cobra"
 )
 
diff --git a/internal/generator/templates/go.mod.tmpl b/internal/generator/templates/go.mod.tmpl
index b78cba70..ec21fff4 100644
--- a/internal/generator/templates/go.mod.tmpl
+++ b/internal/generator/templates/go.mod.tmpl
@@ -1,4 +1,4 @@
-module github.com/USER/{{.Name}}-cli
+module github.com/{{.Owner}}/{{.Name}}-cli
 
 go 1.23
 
diff --git a/internal/generator/templates/goreleaser.yaml.tmpl b/internal/generator/templates/goreleaser.yaml.tmpl
index 43bdd384..49becd2e 100644
--- a/internal/generator/templates/goreleaser.yaml.tmpl
+++ b/internal/generator/templates/goreleaser.yaml.tmpl
@@ -9,7 +9,7 @@ builds:
     env:
       - CGO_ENABLED=0
     ldflags:
-      - -s -w -X github.com/USER/{{.Name}}-cli/internal/cli.version={{"{{"}} .Version {{"}}"}}
+      - -s -w -X github.com/{{.Owner}}/{{.Name}}-cli/internal/cli.version={{"{{"}} .Version {{"}}"}}
     targets:
       - darwin_amd64
       - darwin_arm64
@@ -28,9 +28,9 @@ checksum:
 brews:
   - name: {{.Name}}-cli
     repository:
-      owner: USER
+      owner: {{.Owner}}
       name: homebrew-tap
-    homepage: "https://github.com/USER/{{.Name}}-cli"
+    homepage: "https://github.com/{{.Owner}}/{{.Name}}-cli"
     description: "{{.Description}}"
     install: |
       bin.install "{{.Name}}-cli"
diff --git a/internal/generator/templates/helpers.go.tmpl b/internal/generator/templates/helpers.go.tmpl
index 66d826d1..8a7425ca 100644
--- a/internal/generator/templates/helpers.go.tmpl
+++ b/internal/generator/templates/helpers.go.tmpl
@@ -1,3 +1,5 @@
+// Code generated by CLI Printing Press. DO NOT EDIT.
+
 package cli
 
 import (
@@ -74,27 +76,17 @@ func apiErr(err error) error      { return &cliError{code: 5, err: err} }
 func configErr(err error) error   { return &cliError{code: 10, err: err} }
 func rateLimitErr(err error) error { return &cliError{code: 7, err: err} }
 
-// classifyAPIError maps API errors to structured exit codes.
+// classifyAPIError maps API errors to structured exit codes with actionable hints.
 func classifyAPIError(err error) error {
-	var apiError interface{ Error() string }
-	// Check if it has StatusCode field (client.APIError)
-	type statusCoder interface {
-		Error() string
-	}
-	type withStatus interface {
-		statusCoder
-	}
-	// Use reflection-free approach: check error message for HTTP status
 	msg := err.Error()
 	switch {
 	case strings.Contains(msg, "HTTP 401") || strings.Contains(msg, "HTTP 403"):
-		return authErr(err)
+		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"):
-		return notFoundErr(err)
+		return notFoundErr(fmt.Errorf("%w\nhint: resource not found. Run the 'list' command to see available items", err))
 	case strings.Contains(msg, "HTTP 429"):
 		return rateLimitErr(err)
 	default:
-		_ = apiError
 		return apiErr(err)
 	}
 }
@@ -207,6 +199,63 @@ func paginatedGet(c interface {
 	return json.RawMessage(result), nil
 }
 
+// selectFields is set from the --select flag; used by printOutputFiltered.
+var selectFieldsGlobal string
+
+// printOutputFiltered applies --select field filtering before rendering.
+func printOutputFiltered(w io.Writer, data json.RawMessage, asJSON bool, selectExpr string) error {
+	if selectExpr != "" {
+		data = filterFields(data, selectExpr)
+	}
+	return printOutput(w, data, asJSON)
+}
+
+// filterFields keeps only the specified comma-separated fields from JSON objects/arrays.
+func filterFields(data json.RawMessage, fields string) json.RawMessage {
+	wanted := map[string]bool{}
+	for _, f := range strings.Split(fields, ",") {
+		f = strings.TrimSpace(f)
+		if f != "" {
+			wanted[f] = true
+		}
+	}
+	if len(wanted) == 0 {
+		return data
+	}
+
+	// Try array of objects
+	var items []map[string]json.RawMessage
+	if json.Unmarshal(data, &items) == nil {
+		filtered := make([]map[string]json.RawMessage, len(items))
+		for i, item := range items {
+			m := map[string]json.RawMessage{}
+			for k, v := range item {
+				if wanted[k] {
+					m[k] = v
+				}
+			}
+			filtered[i] = m
+		}
+		out, _ := json.Marshal(filtered)
+		return out
+	}
+
+	// Try single object
+	var obj map[string]json.RawMessage
+	if json.Unmarshal(data, &obj) == nil {
+		m := map[string]json.RawMessage{}
+		for k, v := range obj {
+			if wanted[k] {
+				m[k] = v
+			}
+		}
+		out, _ := json.Marshal(m)
+		return out
+	}
+
+	return data
+}
+
 // printOutput auto-detects arrays and renders as tables, or prints raw JSON for objects.
 func printOutput(w io.Writer, data json.RawMessage, asJSON bool) error {
 	if asJSON {
diff --git a/internal/generator/templates/main.go.tmpl b/internal/generator/templates/main.go.tmpl
index b765938e..bce66af4 100644
--- a/internal/generator/templates/main.go.tmpl
+++ b/internal/generator/templates/main.go.tmpl
@@ -1,10 +1,12 @@
+// Code generated by CLI Printing Press. DO NOT EDIT.
+
 package main
 
 import (
 	"fmt"
 	"os"
 
-	"github.com/USER/{{.Name}}-cli/internal/cli"
+	"github.com/{{.Owner}}/{{.Name}}-cli/internal/cli"
 )
 
 func main() {
diff --git a/internal/generator/templates/readme.md.tmpl b/internal/generator/templates/readme.md.tmpl
index 8094f59b..33df4181 100644
--- a/internal/generator/templates/readme.md.tmpl
+++ b/internal/generator/templates/readme.md.tmpl
@@ -4,35 +4,97 @@
 
 ## Install
 
+### Homebrew
+
 ```
-go install github.com/USER/{{.Name}}-cli/cmd/{{.Name}}-cli@latest
+brew install {{.Owner}}/tap/{{.Name}}-cli
 ```
 
-## Usage
+### Go
 
 ```
-{{.Name}}-cli --help
-{{.Name}}-cli --version
+go install github.com/{{.Owner}}/{{.Name}}-cli/cmd/{{.Name}}-cli@latest
+```
+
+### Binary
+
+Download from [Releases](https://github.com/{{.Owner}}/{{.Name}}-cli/releases).
+
+## Quick Start
+
+```bash
+# 1. Set your API credentials
+export {{index .Auth.EnvVars 0}}="your-key-here"
+
+# 2. Verify everything works
 {{.Name}}-cli doctor
+
+# 3. Start using it
+{{- range $name, $resource := .Resources}}
+{{$.Name}}-cli {{$name}} --help
+{{- break}}
+{{- end}}
 ```
 
-### Commands
+## Usage
+
+<!-- HELP_OUTPUT -->
+
+## Commands
 {{range $name, $resource := .Resources}}
-#### {{$name}}
+### {{$name}}
 
 {{$resource.Description}}
 {{range $eName, $endpoint := $resource.Endpoints}}
-- `{{$.Name}}-cli {{$name}} {{$eName}}` - {{$endpoint.Description}}
+- **`{{$.Name}}-cli {{$name}} {{$eName}}`** - {{$endpoint.Description}}
 {{- end}}
 {{end}}
 
-### Output Formats
+## Output Formats
+
+```bash
+# Human-readable table (default)
+{{.Name}}-cli {{range $name, $_ := .Resources}}{{$name}}{{break}}{{end}} list
 
-- Default: human-readable table
-- `--json`: JSON output
-- `--plain`: tab-separated text for piping
+# JSON for scripting and agents
+{{.Name}}-cli {{range $name, $_ := .Resources}}{{$name}}{{break}}{{end}} list --json
+
+# Filter specific fields
+{{.Name}}-cli {{range $name, $_ := .Resources}}{{$name}}{{break}}{{end}} list --json --select id,name,status
+
+# Plain tab-separated for piping
+{{.Name}}-cli {{range $name, $_ := .Resources}}{{$name}}{{break}}{{end}} list --plain
+
+# Dry run (show request without sending)
+{{.Name}}-cli {{range $name, $_ := .Resources}}{{$name}}{{break}}{{end}} list --dry-run
+```
 
-### Configuration
+## Agent Usage
+
+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
+```
+
+Exit codes: `0` success, `2` usage error, `3` not found, `4` auth error, `5` API error, `7` rate limited, `10` config error.
+
+## Health Check
+
+```bash
+{{.Name}}-cli doctor
+```
+
+<!-- DOCTOR_OUTPUT -->
+
+## Configuration
 
 Config file: `{{.Config.Path}}`
 
@@ -41,4 +103,20 @@ Environment variables:
 - `{{.}}`
 {{- end}}
 
-## Generated by CLI Printing Press
+## Troubleshooting
+
+**Authentication errors (exit code 4)**
+- Run `{{.Name}}-cli doctor` to check credentials
+- Verify the environment variable is set: `echo ${{index .Auth.EnvVars 0}}`
+
+**Not found errors (exit code 3)**
+- Check the resource ID is correct
+- Run the `list` command to see available items
+
+**Rate limit errors (exit code 7)**
+- The CLI auto-retries with exponential backoff
+- If persistent, wait a few minutes and try again
+
+---
+
+Generated by [CLI Printing Press](https://github.com/mvanhorn/cli-printing-press)
diff --git a/internal/generator/templates/root.go.tmpl b/internal/generator/templates/root.go.tmpl
index 96e87562..66daf7ef 100644
--- a/internal/generator/templates/root.go.tmpl
+++ b/internal/generator/templates/root.go.tmpl
@@ -1,3 +1,5 @@
+// Code generated by CLI Printing Press. DO NOT EDIT.
+
 package cli
 
 import (
@@ -6,8 +8,8 @@ import (
 	"text/tabwriter"
 	"time"
 
-	"github.com/USER/{{.Name}}-cli/internal/client"
-	"github.com/USER/{{.Name}}-cli/internal/config"
+	"github.com/{{.Owner}}/{{.Name}}-cli/internal/client"
+	"github.com/{{.Owner}}/{{.Name}}-cli/internal/config"
 	"github.com/spf13/cobra"
 )
 
@@ -18,6 +20,7 @@ type rootFlags struct {
 	plain      bool
 	quiet      bool
 	dryRun     bool
+	selectFields string
 	configPath string
 	timeout    time.Duration
 }
@@ -40,6 +43,7 @@ func Execute() error {
 	rootCmd.PersistentFlags().StringVar(&flags.configPath, "config", "", "Config file path")
 	rootCmd.PersistentFlags().DurationVar(&flags.timeout, "timeout", 30*time.Second, "Request timeout")
 	rootCmd.PersistentFlags().BoolVar(&flags.dryRun, "dry-run", false, "Show request without sending")
+	rootCmd.PersistentFlags().StringVar(&flags.selectFields, "select", "", "Comma-separated fields to include in output (e.g. --select id,name,status)")
 	rootCmd.PersistentFlags().BoolVar(&noColor, "no-color", false, "Disable colored output")
 
 {{- range $name, $resource := .Resources}}
diff --git a/internal/generator/templates/types.go.tmpl b/internal/generator/templates/types.go.tmpl
index 5c6b7456..af7903b4 100644
--- a/internal/generator/templates/types.go.tmpl
+++ b/internal/generator/templates/types.go.tmpl
@@ -1,3 +1,5 @@
+// Code generated by CLI Printing Press. DO NOT EDIT.
+
 package types
 {{range $name, $typeDef := .Types}}
 type {{safeTypeName $name}} struct {
diff --git a/internal/spec/spec.go b/internal/spec/spec.go
index c7d98457..f1085fdd 100644
--- a/internal/spec/spec.go
+++ b/internal/spec/spec.go
@@ -13,6 +13,7 @@ type APISpec struct {
 	Version     string              `yaml:"version"`
 	BaseURL     string              `yaml:"base_url"`
 	BasePath    string              `yaml:"base_path,omitempty"`
+	Owner       string              `yaml:"owner,omitempty"` // GitHub owner for import paths and Homebrew tap
 	Auth        AuthConfig          `yaml:"auth"`
 	Config      ConfigSpec          `yaml:"config"`
 	Resources   map[string]Resource `yaml:"resources"`

← 1f5871bd feat(pipeline): add comparative analysis scoring and GoRelea  ·  back to Cli Printing Press  ·  fix(templates): guard readme.md.tmpl against empty Auth.EnvV 2370b45b →