← back to Cli Printing Press
test(cli): add golden output harness (#310)
0a1e80d9da986fcd85c3f303b337a4f6608e4731 · 2026-04-26 13:21:04 -0700 · Trevin Chow
* test(cli): add golden output harness
* test(cli): strengthen golden artifact coverage
* test(cli): preserve raw golden Go artifacts
* test(cli): pin golden generation owner
* test(cli): snapshot golden verification outputs
Files touched
A .github/workflows/golden.ymlM .gitignoreM AGENTS.mdM README.mdM lefthook.ymlA scripts/golden.shA testdata/golden/cases/browser-sniff-sample/artifacts.txtA testdata/golden/cases/browser-sniff-sample/command.txtA testdata/golden/cases/catalog-list/artifacts.txtA testdata/golden/cases/catalog-list/command.txtA testdata/golden/cases/catalog-show-petstore/artifacts.txtA testdata/golden/cases/catalog-show-petstore/command.txtA testdata/golden/cases/generate-golden-api/artifacts.txtA testdata/golden/cases/generate-golden-api/command.txtA testdata/golden/expected/browser-sniff-sample/exit.txtA testdata/golden/expected/browser-sniff-sample/spec.yamlA testdata/golden/expected/browser-sniff-sample/stderr.txtA testdata/golden/expected/browser-sniff-sample/stdout.txtA testdata/golden/expected/browser-sniff-sample/traffic-analysis.jsonA testdata/golden/expected/catalog-list/exit.txtA testdata/golden/expected/catalog-list/stderr.txtA testdata/golden/expected/catalog-list/stdout.txtA testdata/golden/expected/catalog-show-petstore/exit.txtA testdata/golden/expected/catalog-show-petstore/stderr.txtA testdata/golden/expected/catalog-show-petstore/stdout.txtA testdata/golden/expected/generate-golden-api/dogfood.jsonA testdata/golden/expected/generate-golden-api/exit.txtA testdata/golden/expected/generate-golden-api/printing-press-golden/.printing-press.jsonA testdata/golden/expected/generate-golden-api/printing-press-golden/cmd/printing-press-golden-pp-cli/main.goA testdata/golden/expected/generate-golden-api/printing-press-golden/go.modA testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/auth.goA testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.goA testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.goA testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.goA testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.goA testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.goA testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/root.goA testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.goA testdata/golden/expected/generate-golden-api/printing-press-golden/internal/config/config.goA testdata/golden/expected/generate-golden-api/printing-press-golden/internal/mcp/tools.goA testdata/golden/expected/generate-golden-api/printing-press-golden/internal/types/types.goA testdata/golden/expected/generate-golden-api/scorecard.jsonA testdata/golden/expected/generate-golden-api/stderr.txtA testdata/golden/expected/generate-golden-api/stdout.txtA testdata/golden/fixtures/golden-api.yaml
Diff
commit 0a1e80d9da986fcd85c3f303b337a4f6608e4731
Author: Trevin Chow <trevin@trevinchow.com>
Date: Sun Apr 26 13:21:04 2026 -0700
test(cli): add golden output harness (#310)
* test(cli): add golden output harness
* test(cli): strengthen golden artifact coverage
* test(cli): preserve raw golden Go artifacts
* test(cli): pin golden generation owner
* test(cli): snapshot golden verification outputs
---
.github/workflows/golden.yml | 21 +
.gitignore | 1 +
AGENTS.md | 20 +
README.md | 18 +
lefthook.yml | 8 +-
scripts/golden.sh | 282 +++++++++++
.../cases/browser-sniff-sample/artifacts.txt | 2 +
.../golden/cases/browser-sniff-sample/command.txt | 1 +
testdata/golden/cases/catalog-list/artifacts.txt | 1 +
testdata/golden/cases/catalog-list/command.txt | 1 +
.../cases/catalog-show-petstore/artifacts.txt | 1 +
.../golden/cases/catalog-show-petstore/command.txt | 1 +
.../golden/cases/generate-golden-api/artifacts.txt | 16 +
.../golden/cases/generate-golden-api/command.txt | 4 +
.../golden/expected/browser-sniff-sample/exit.txt | 1 +
.../golden/expected/browser-sniff-sample/spec.yaml | 54 +++
.../expected/browser-sniff-sample/stderr.txt | 0
.../expected/browser-sniff-sample/stdout.txt | 3 +
.../browser-sniff-sample/traffic-analysis.json | 229 +++++++++
testdata/golden/expected/catalog-list/exit.txt | 1 +
testdata/golden/expected/catalog-list/stderr.txt | 0
testdata/golden/expected/catalog-list/stdout.txt | 41 ++
.../golden/expected/catalog-show-petstore/exit.txt | 1 +
.../expected/catalog-show-petstore/stderr.txt | 0
.../expected/catalog-show-petstore/stdout.txt | 11 +
.../expected/generate-golden-api/dogfood.json | 79 +++
.../golden/expected/generate-golden-api/exit.txt | 1 +
.../printing-press-golden/.printing-press.json | 20 +
.../cmd/printing-press-golden-pp-cli/main.go | 18 +
.../printing-press-golden/go.mod | 10 +
.../printing-press-golden/internal/cli/auth.go | 104 ++++
.../internal/cli/projects_create.go | 134 ++++++
.../internal/cli/projects_list.go | 96 ++++
.../internal/cli/projects_tasks_list-project.go | 101 ++++
.../internal/cli/projects_tasks_update-project.go | 137 ++++++
.../internal/cli/promoted_public.go | 85 ++++
.../printing-press-golden/internal/cli/root.go | 236 +++++++++
.../internal/client/client.go | 536 +++++++++++++++++++++
.../internal/config/config.go | 122 +++++
.../printing-press-golden/internal/mcp/tools.go | 344 +++++++++++++
.../printing-press-golden/internal/types/types.go | 33 ++
.../expected/generate-golden-api/scorecard.json | 48 ++
.../golden/expected/generate-golden-api/stderr.txt | 1 +
.../golden/expected/generate-golden-api/stdout.txt | 0
testdata/golden/fixtures/golden-api.yaml | 224 +++++++++
45 files changed, 3045 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/golden.yml b/.github/workflows/golden.yml
new file mode 100644
index 00000000..785e5195
--- /dev/null
+++ b/.github/workflows/golden.yml
@@ -0,0 +1,21 @@
+name: Golden
+
+on:
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ golden:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: actions/setup-go@v6
+ with:
+ go-version-file: go.mod
+ cache: true
+
+ - name: Verify golden outputs
+ run: scripts/golden.sh verify
diff --git a/.gitignore b/.gitignore
index 1a07ac56..aec884e0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
.DS_Store
.cache/
+.gotmp/
.claude/worktrees/
/printing-press
library/
diff --git a/AGENTS.md b/AGENTS.md
index d4d79c72..83cbc9ff 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -51,6 +51,26 @@ A pre-commit hook runs `gofmt -w` on staged Go files automatically. A pre-push h
**IMPORTANT: Always use relative paths for build output.** Never build to `/tmp` or any shared absolute path. Multiple worktrees run concurrently and will stomp on each other. Use `./printing-press` exactly as shown above.
+## Golden Output Harness
+
+The golden harness is a byte-level behavior check for deterministic, offline `printing-press` commands and generated artifacts. It complements unit tests by catching user-visible output drift and printed CLI artifact drift.
+
+Run `scripts/golden.sh verify` whenever a change may affect CLI command output, catalog rendering, browser-sniff or crowd-sniff output, generated specs or generated printed CLI files, templates under `internal/generator/templates/`, naming, endpoint derivation, auth emission, manifest generation, scorecard output, or pipeline artifacts.
+
+If a refactor changes machine code but claims behavior is identical, `scripts/golden.sh verify` should pass without fixture updates.
+
+Run `scripts/golden.sh update` only when the behavior/output change is intentional. After updating, inspect the diffs manually and explain in the final response why the golden changes are expected. Never update goldens just to make a failing check pass.
+
+Golden cases must be deterministic, offline, and auth-free. Do not add cases that depend on network access, user credentials or env vars, `~/printing-press`, wall-clock timestamps unless normalized, machine-specific absolute paths unless normalized, or large generated printed CLI trees unless the compared subset is intentional.
+
+To add a case, create `testdata/golden/cases/<case-name>/`, add expected outputs under `testdata/golden/expected/<case-name>/`, and list behaviorally important generated files in `artifacts.txt` when the command creates artifacts. Prefer a small, high-signal artifact subset over snapshotting huge trees.
+
+Maintain `testdata/golden/fixtures/golden-api.yaml` as the purpose-built generated-CLI fixture for the Printing Press. When the machine gains deterministic generation capabilities that should survive major refactors — for example new auth shapes, pagination contracts, MCP surfaces, manifest fields, or endpoint naming rules — extend this fixture and add the smallest useful artifact comparison that proves the capability. Do not mutate this fixture for one printed CLI's edge case unless it represents a general machine behavior.
+
+If `verify` fails, inspect `.gotmp/golden/actual/<case-name>/` and the generated `.diff` files. Decide whether the change is a regression or an intentional behavior change. If it is a regression, fix code. If it is intentional, run `scripts/golden.sh update`, review fixture diffs, and mention the golden update in the final summary.
+
+Golden verification does not replace `go test ./...`, `go vet ./...`, `golangci-lint run ./...`, or `go build -o ./printing-press ./cmd/printing-press`. It is an additional check for behavior-sensitive changes and runs in CI as a separate `Golden` workflow, not as part of `go test ./...`.
+
## Project Structure
- `cmd/printing-press/` - CLI entry point
diff --git a/README.md b/README.md
index e85dfdc0..d9dda42a 100644
--- a/README.md
+++ b/README.md
@@ -456,6 +456,24 @@ golangci-lint run ./...
A pre-push lefthook hook runs `golangci-lint` on changed files; the same config (`.golangci.yml`) runs in CI. Install hooks with `brew install lefthook && lefthook install`. To test local skill changes, run `claude --plugin-dir .` so `/printing-press` loads from your working copy. See [AGENTS.md](AGENTS.md) for full conventions, glossary, and release flow.
+### Golden Output Harness
+
+Golden output checks compare deterministic, offline `printing-press` commands against committed stdout, stderr, exit-code, and selected artifact fixtures:
+
+```bash
+scripts/golden.sh verify
+```
+
+Use update mode only after an intentional behavior change:
+
+```bash
+scripts/golden.sh update
+```
+
+The harness rebuilds `./printing-press`, writes actual outputs under `.gotmp/golden/actual`, and compares them to `testdata/golden/expected`. Cases live under `testdata/golden/cases/<case-name>/`; `command.txt` defines the offline command, and `artifacts.txt` lists behaviorally important generated files to compare. Normalization is intentionally narrow: machine-specific paths, deterministic JSON formatting, and known provenance fields like generated timestamps. CI runs this as a separate `Golden` workflow, not inside `go test ./...`.
+
+The generated-CLI golden uses `testdata/golden/fixtures/golden-api.yaml`, a purpose-built OpenAPI fixture for the Printing Press. Extend that fixture when the machine gains new deterministic generation capabilities that should be protected by artifact goldens. Update mode refuses dirty worktrees unless `GOLDEN_ALLOW_DIRTY=1` is set, so fixture churn stays intentional.
+
## Credits
- Peter Steinberger ([@steipete](https://github.com/steipete)). [discrawl](https://github.com/steipete/discrawl) and [gogcli](https://github.com/steipete/gogcli) set the bar. The quality scoring system is inspired by his work; discrawl's sync architecture directly influenced the printing press templates.
diff --git a/lefthook.yml b/lefthook.yml
index a49a6e2f..3881c3c1 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -22,14 +22,18 @@ pre-commit:
commands:
fmt:
glob: "*.go"
- run: gofmt -w {staged_files} && git add {staged_files}
+ run: |
+ files="$(printf '%s\n' {staged_files} | grep -v '^testdata/golden/expected/' || true)"
+ test -z "$files" || { printf '%s\n' "$files" | xargs gofmt -w; printf '%s\n' "$files" | xargs git add; }
stage_fixed: true
pre-push:
commands:
fmt:
glob: "*.go"
- run: test -z "$(gofmt -l .)" || { gofmt -l .; exit 1; }
+ run: |
+ unformatted="$(git ls-files '*.go' ':!:testdata/golden/expected/**' | xargs gofmt -l)"
+ test -z "$unformatted" || { printf '%s\n' "$unformatted"; exit 1; }
fail_text: "gofmt failed. Run 'gofmt -w .' to fix."
lint:
glob: "*.go"
diff --git a/scripts/golden.sh b/scripts/golden.sh
new file mode 100755
index 00000000..a1826783
--- /dev/null
+++ b/scripts/golden.sh
@@ -0,0 +1,282 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+mode="${1:-verify}"
+if [[ "$mode" != "verify" && "$mode" != "update" && "$mode" != "list" && "$mode" != "--list" ]]; then
+ echo "usage: scripts/golden.sh [verify|update|list]" >&2
+ exit 2
+fi
+
+repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$repo_root"
+
+binary="./printing-press"
+cases_root="testdata/golden/cases"
+expected_root="testdata/golden/expected"
+actual_root=".gotmp/golden/actual"
+actual_abs="$repo_root/$actual_root"
+golden_owner="printing-press-golden"
+
+escape_sed() {
+ printf "%s" "$1" | sed -e 's/[\/&|]/\\&/g'
+}
+
+repo_root_pattern="$(escape_sed "$repo_root")"
+home_pattern="$(escape_sed "$HOME")"
+actual_root_pattern="$(escape_sed "$actual_root")"
+actual_abs_pattern="$(escape_sed "$actual_abs")"
+
+# Keep normalization intentionally narrow. These substitutions remove
+# machine-specific paths while preserving behaviorally meaningful output.
+normalize_text() {
+ sed \
+ -e "s|$actual_abs_pattern|<ARTIFACT_DIR>|g" \
+ -e "s|$actual_root_pattern|<ARTIFACT_DIR>|g" \
+ -e "s|$repo_root_pattern|<REPO>|g" \
+ -e "s|$home_pattern|<HOME>|g"
+}
+
+normalize_json() {
+ local file="$1"
+
+ case "$file" in
+ */.printing-press.json)
+ jq -S '
+ .generated_at = "<GENERATED_AT>"
+ | .printing_press_version = "<PRINTING_PRESS_VERSION>"
+ ' "$file" | normalize_text
+ ;;
+ *)
+ jq -S . "$file" | normalize_text
+ ;;
+ esac
+}
+
+case_names() {
+ find "$cases_root" -mindepth 1 -maxdepth 1 -type d -print |
+ sed "s|^$cases_root/||" |
+ sort
+}
+
+artifact_list() {
+ local case_name="$1"
+ local manifest="$cases_root/$case_name/artifacts.txt"
+
+ if [[ ! -f "$manifest" ]]; then
+ return 0
+ fi
+
+ sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' "$manifest"
+}
+
+require_update_allowed() {
+ if [[ "${GOLDEN_ALLOW_DIRTY:-}" == "1" ]]; then
+ return 0
+ fi
+
+ if [[ -n "$(git status --porcelain --untracked-files=all)" ]]; then
+ echo "Refusing to update golden fixtures with a dirty worktree." >&2
+ echo "Review or commit changes first, or set GOLDEN_ALLOW_DIRTY=1 intentionally." >&2
+ exit 1
+ fi
+}
+
+list_cases() {
+ local case_name
+ local artifacts
+
+ for case_name in $(case_names); do
+ echo "$case_name"
+ sed 's/^/ /' "$cases_root/$case_name/command.txt"
+
+ artifacts="$(artifact_list "$case_name")"
+ if [[ -n "$artifacts" ]]; then
+ echo " artifacts:"
+ printf "%s\n" "$artifacts" | sed 's/^/ /'
+ fi
+ done
+}
+
+run_case() {
+ local case_name="$1"
+ local case_dir="$cases_root/$case_name"
+ local out_dir="$actual_root/$case_name"
+ local raw_stdout="$out_dir/stdout.raw"
+ local raw_stderr="$out_dir/stderr.raw"
+ local exit_file="$out_dir/exit.txt"
+ local exit_code=0
+ local command_text
+
+ if [[ ! -f "$case_dir/command.txt" ]]; then
+ echo "missing command.txt for golden case: $case_name" >&2
+ return 2
+ fi
+
+ mkdir -p "$out_dir"
+ command_text="$(cat "$case_dir/command.txt")"
+
+ BINARY="$binary" CASE_ACTUAL_DIR="$out_dir" REPO_ROOT="$repo_root" \
+ GIT_CONFIG_COUNT=2 \
+ GIT_CONFIG_KEY_0=github.user \
+ GIT_CONFIG_VALUE_0="$golden_owner" \
+ GIT_CONFIG_KEY_1=user.name \
+ GIT_CONFIG_VALUE_1="$golden_owner" \
+ bash -c "$command_text" >"$raw_stdout" 2>"$raw_stderr" || exit_code=$?
+
+ normalize_text <"$raw_stdout" >"$out_dir/stdout.txt"
+ normalize_text <"$raw_stderr" >"$out_dir/stderr.txt"
+ printf "%s\n" "$exit_code" >"$exit_file"
+}
+
+normalize_artifacts() {
+ local case_name="$1"
+ local actual_dir="$actual_root/$case_name"
+ local normalized_dir="$actual_dir/_normalized"
+ local artifact
+ local source
+ local target
+
+ rm -rf "$normalized_dir"
+
+ while IFS= read -r artifact; do
+ source="$actual_dir/$artifact"
+ target="$normalized_dir/$artifact"
+
+ if [[ ! -f "$source" ]]; then
+ echo "missing artifact for $case_name: $artifact" >&2
+ return 1
+ fi
+
+ mkdir -p "$(dirname "$target")"
+ case "$artifact" in
+ *.json)
+ normalize_json "$source" >"$target"
+ ;;
+ *)
+ normalize_text <"$source" >"$target"
+ ;;
+ esac
+ done < <(artifact_list "$case_name")
+}
+
+compare_file() {
+ local case_name="$1"
+ local label="$2"
+ local expected="$3"
+ local actual="$4"
+ local diff_path="$5"
+
+ mkdir -p "$(dirname "$diff_path")"
+ if ! diff -u "$expected" "$actual" >"$diff_path"; then
+ echo "FAIL $case_name $label"
+ echo " diff: $diff_path"
+ return 1
+ fi
+
+ rm -f "$diff_path"
+ return 0
+}
+
+compare_case() {
+ local case_name="$1"
+ local failed=0
+ local actual_dir="$actual_root/$case_name"
+ local expected_dir="$expected_root/$case_name"
+ local artifact
+
+ for file in stdout.txt stderr.txt exit.txt; do
+ compare_file "$case_name" "$file" "$expected_dir/$file" "$actual_dir/$file" "$actual_dir/$file.diff" || failed=1
+ done
+
+ while IFS= read -r artifact; do
+ compare_file \
+ "$case_name" \
+ "$artifact" \
+ "$expected_dir/$artifact" \
+ "$actual_dir/_normalized/$artifact" \
+ "$actual_dir/$artifact.diff" || failed=1
+ done < <(artifact_list "$case_name")
+
+ return "$failed"
+}
+
+update_case() {
+ local case_name="$1"
+ local actual_dir="$actual_root/$case_name"
+ local expected_dir="$expected_root/$case_name"
+ local artifact
+
+ mkdir -p "$expected_dir"
+ cp "$actual_dir/stdout.txt" "$expected_dir/stdout.txt"
+ cp "$actual_dir/stderr.txt" "$expected_dir/stderr.txt"
+ cp "$actual_dir/exit.txt" "$expected_dir/exit.txt"
+
+ while IFS= read -r artifact; do
+ mkdir -p "$(dirname "$expected_dir/$artifact")"
+ cp "$actual_dir/_normalized/$artifact" "$expected_dir/$artifact"
+ done < <(artifact_list "$case_name")
+}
+
+if [[ "$mode" == "list" || "$mode" == "--list" ]]; then
+ list_cases
+ exit 0
+fi
+
+if [[ "$mode" == "update" ]]; then
+ require_update_allowed
+fi
+
+if find "$cases_root" -name artifacts.txt -print -quit | grep -q . && ! command -v jq >/dev/null 2>&1; then
+ echo "jq is required for golden artifact normalization" >&2
+ exit 1
+fi
+
+echo "Building $binary"
+go build -o "$binary" ./cmd/printing-press
+
+rm -rf "$actual_root"
+mkdir -p "$actual_root"
+
+failures=0
+case_count=0
+for case_name in $(case_names); do
+ case_count=$((case_count + 1))
+ run_case "$case_name"
+
+ if ! normalize_artifacts "$case_name"; then
+ failures=$((failures + 1))
+ continue
+ fi
+
+ if [[ "$mode" == "update" ]]; then
+ update_case "$case_name"
+ echo "UPDATED $case_name"
+ continue
+ fi
+
+ if compare_case "$case_name"; then
+ echo "PASS $case_name"
+ else
+ failures=$((failures + 1))
+ fi
+done
+
+if [[ "$mode" == "update" ]]; then
+ if [[ "$failures" -gt 0 ]]; then
+ echo "Golden update failed: $failures case(s) could not be normalized."
+ echo "Actual outputs: $actual_root"
+ exit 1
+ fi
+
+ echo "Golden fixtures updated for $case_count case(s)."
+ exit 0
+fi
+
+if [[ "$failures" -gt 0 ]]; then
+ echo "Golden verify failed: $failures case(s) changed."
+ echo "Actual outputs: $actual_root"
+ echo "Run scripts/golden.sh update only for intentional behavior changes."
+ exit 1
+fi
+
+echo "Golden verify passed: $case_count case(s)."
diff --git a/testdata/golden/cases/browser-sniff-sample/artifacts.txt b/testdata/golden/cases/browser-sniff-sample/artifacts.txt
new file mode 100644
index 00000000..7028e073
--- /dev/null
+++ b/testdata/golden/cases/browser-sniff-sample/artifacts.txt
@@ -0,0 +1,2 @@
+spec.yaml
+traffic-analysis.json
diff --git a/testdata/golden/cases/browser-sniff-sample/command.txt b/testdata/golden/cases/browser-sniff-sample/command.txt
new file mode 100644
index 00000000..2a0f8712
--- /dev/null
+++ b/testdata/golden/cases/browser-sniff-sample/command.txt
@@ -0,0 +1 @@
+"$BINARY" browser-sniff --har testdata/sniff/sample.har --output "$CASE_ACTUAL_DIR/spec.yaml" --analysis-output "$CASE_ACTUAL_DIR/traffic-analysis.json"
diff --git a/testdata/golden/cases/catalog-list/artifacts.txt b/testdata/golden/cases/catalog-list/artifacts.txt
new file mode 100644
index 00000000..a96ba8e2
--- /dev/null
+++ b/testdata/golden/cases/catalog-list/artifacts.txt
@@ -0,0 +1 @@
+# no extra artifacts
diff --git a/testdata/golden/cases/catalog-list/command.txt b/testdata/golden/cases/catalog-list/command.txt
new file mode 100644
index 00000000..28b615d6
--- /dev/null
+++ b/testdata/golden/cases/catalog-list/command.txt
@@ -0,0 +1 @@
+"$BINARY" catalog list
diff --git a/testdata/golden/cases/catalog-show-petstore/artifacts.txt b/testdata/golden/cases/catalog-show-petstore/artifacts.txt
new file mode 100644
index 00000000..a96ba8e2
--- /dev/null
+++ b/testdata/golden/cases/catalog-show-petstore/artifacts.txt
@@ -0,0 +1 @@
+# no extra artifacts
diff --git a/testdata/golden/cases/catalog-show-petstore/command.txt b/testdata/golden/cases/catalog-show-petstore/command.txt
new file mode 100644
index 00000000..143982f7
--- /dev/null
+++ b/testdata/golden/cases/catalog-show-petstore/command.txt
@@ -0,0 +1 @@
+"$BINARY" catalog show petstore
diff --git a/testdata/golden/cases/generate-golden-api/artifacts.txt b/testdata/golden/cases/generate-golden-api/artifacts.txt
new file mode 100644
index 00000000..bed42101
--- /dev/null
+++ b/testdata/golden/cases/generate-golden-api/artifacts.txt
@@ -0,0 +1,16 @@
+printing-press-golden/.printing-press.json
+printing-press-golden/go.mod
+printing-press-golden/cmd/printing-press-golden-pp-cli/main.go
+printing-press-golden/internal/client/client.go
+printing-press-golden/internal/cli/root.go
+printing-press-golden/internal/cli/auth.go
+printing-press-golden/internal/cli/projects_list.go
+printing-press-golden/internal/cli/projects_create.go
+printing-press-golden/internal/cli/projects_tasks_list-project.go
+printing-press-golden/internal/cli/projects_tasks_update-project.go
+printing-press-golden/internal/cli/promoted_public.go
+printing-press-golden/internal/config/config.go
+printing-press-golden/internal/mcp/tools.go
+printing-press-golden/internal/types/types.go
+dogfood.json
+scorecard.json
diff --git a/testdata/golden/cases/generate-golden-api/command.txt b/testdata/golden/cases/generate-golden-api/command.txt
new file mode 100644
index 00000000..818d3445
--- /dev/null
+++ b/testdata/golden/cases/generate-golden-api/command.txt
@@ -0,0 +1,4 @@
+set -euo pipefail
+"$BINARY" generate --spec testdata/golden/fixtures/golden-api.yaml --output "$CASE_ACTUAL_DIR/printing-press-golden" --force --spec-url file://testdata/golden/fixtures/golden-api.yaml --validate=false
+"$BINARY" dogfood --dir "$CASE_ACTUAL_DIR/printing-press-golden" --json > "$CASE_ACTUAL_DIR/dogfood.json"
+"$BINARY" scorecard --dir "$CASE_ACTUAL_DIR/printing-press-golden" --json > "$CASE_ACTUAL_DIR/scorecard.json"
diff --git a/testdata/golden/expected/browser-sniff-sample/exit.txt b/testdata/golden/expected/browser-sniff-sample/exit.txt
new file mode 100644
index 00000000..573541ac
--- /dev/null
+++ b/testdata/golden/expected/browser-sniff-sample/exit.txt
@@ -0,0 +1 @@
+0
diff --git a/testdata/golden/expected/browser-sniff-sample/spec.yaml b/testdata/golden/expected/browser-sniff-sample/spec.yaml
new file mode 100644
index 00000000..44d2bda2
--- /dev/null
+++ b/testdata/golden/expected/browser-sniff-sample/spec.yaml
@@ -0,0 +1,54 @@
+name: httpbin
+description: Discovered API spec for httpbin
+version: 0.1.0
+base_url: https://httpbin.org
+spec_source: sniffed
+auth:
+ type: none
+ header: ""
+ format: ""
+ env_vars: []
+config:
+ format: toml
+ path: ~/.config/httpbin-pp-cli/config.toml
+resources:
+ get:
+ description: Operations on get
+ endpoints:
+ list_get:
+ method: GET
+ path: /get
+ description: GET /get
+ params: []
+ body: []
+ response:
+ type: object
+ item: get
+ pagination: null
+ headers:
+ description: Operations on headers
+ endpoints:
+ list_headers:
+ method: GET
+ path: /headers
+ description: GET /headers
+ params: []
+ body: []
+ response:
+ type: object
+ item: headers
+ pagination: null
+ ip:
+ description: Operations on ip
+ endpoints:
+ list_ip:
+ method: GET
+ path: /ip
+ description: GET /ip
+ params: []
+ body: []
+ response:
+ type: object
+ item: ip
+ pagination: null
+types: {}
diff --git a/testdata/golden/expected/browser-sniff-sample/stderr.txt b/testdata/golden/expected/browser-sniff-sample/stderr.txt
new file mode 100644
index 00000000..e69de29b
diff --git a/testdata/golden/expected/browser-sniff-sample/stdout.txt b/testdata/golden/expected/browser-sniff-sample/stdout.txt
new file mode 100644
index 00000000..54df07e0
--- /dev/null
+++ b/testdata/golden/expected/browser-sniff-sample/stdout.txt
@@ -0,0 +1,3 @@
+Spec written to <ARTIFACT_DIR>/browser-sniff-sample/spec.yaml (3 endpoints across 3 resources)
+Traffic analysis written to <ARTIFACT_DIR>/browser-sniff-sample/traffic-analysis.json
+Run 'printing-press generate --spec <ARTIFACT_DIR>/browser-sniff-sample/spec.yaml' to build the CLI
diff --git a/testdata/golden/expected/browser-sniff-sample/traffic-analysis.json b/testdata/golden/expected/browser-sniff-sample/traffic-analysis.json
new file mode 100644
index 00000000..2950265e
--- /dev/null
+++ b/testdata/golden/expected/browser-sniff-sample/traffic-analysis.json
@@ -0,0 +1,229 @@
+{
+ "auth": {},
+ "candidate_commands": [
+ {
+ "confidence": 0.55,
+ "evidence": [
+ {
+ "content_type": "application/json",
+ "entry_index": 1,
+ "host": "httpbin.org",
+ "method": "GET",
+ "path": "/get",
+ "reason": "endpoint cluster member",
+ "status": 200
+ }
+ ],
+ "name": "list_get",
+ "rationale": "Derived from observed GET /get traffic.",
+ "resource": "get"
+ },
+ {
+ "confidence": 0.55,
+ "evidence": [
+ {
+ "content_type": "application/json",
+ "entry_index": 3,
+ "host": "httpbin.org",
+ "method": "GET",
+ "path": "/headers",
+ "reason": "endpoint cluster member",
+ "status": 200
+ }
+ ],
+ "name": "list_headers",
+ "rationale": "Derived from observed GET /headers traffic.",
+ "resource": "headers"
+ },
+ {
+ "confidence": 0.55,
+ "evidence": [
+ {
+ "content_type": "application/json",
+ "entry_index": 4,
+ "host": "httpbin.org",
+ "method": "GET",
+ "path": "/ip",
+ "reason": "endpoint cluster member",
+ "status": 200
+ }
+ ],
+ "name": "list_ip",
+ "rationale": "Derived from observed GET /ip traffic.",
+ "resource": "ip"
+ }
+ ],
+ "endpoint_clusters": [
+ {
+ "content_types": [
+ "application/json"
+ ],
+ "count": 1,
+ "evidence": [
+ {
+ "content_type": "application/json",
+ "entry_index": 1,
+ "host": "httpbin.org",
+ "method": "GET",
+ "path": "/get",
+ "reason": "endpoint cluster member",
+ "status": 200
+ }
+ ],
+ "host": "httpbin.org",
+ "method": "GET",
+ "path": "/get",
+ "request_shape": {},
+ "response_shape": {},
+ "size_class": "empty",
+ "statuses": [
+ 200
+ ]
+ },
+ {
+ "content_types": [
+ "application/json"
+ ],
+ "count": 1,
+ "evidence": [
+ {
+ "content_type": "application/json",
+ "entry_index": 3,
+ "host": "httpbin.org",
+ "method": "GET",
+ "path": "/headers",
+ "reason": "endpoint cluster member",
+ "status": 200
+ }
+ ],
+ "host": "httpbin.org",
+ "method": "GET",
+ "path": "/headers",
+ "request_shape": {},
+ "response_shape": {},
+ "size_class": "empty",
+ "statuses": [
+ 200
+ ]
+ },
+ {
+ "content_types": [
+ "application/json"
+ ],
+ "count": 1,
+ "evidence": [
+ {
+ "content_type": "application/json",
+ "entry_index": 4,
+ "host": "httpbin.org",
+ "method": "GET",
+ "path": "/ip",
+ "reason": "endpoint cluster member",
+ "status": 200
+ }
+ ],
+ "host": "httpbin.org",
+ "method": "GET",
+ "path": "/ip",
+ "request_shape": {},
+ "response_shape": {},
+ "size_class": "empty",
+ "statuses": [
+ 200
+ ]
+ }
+ ],
+ "protocols": [
+ {
+ "confidence": 0.75,
+ "evidence": [
+ {
+ "content_type": "application/json",
+ "entry_index": 1,
+ "host": "httpbin.org",
+ "method": "GET",
+ "path": "/get",
+ "reason": "JSON HTTP request/response without specialized protocol markers",
+ "status": 200
+ },
+ {
+ "content_type": "application/json",
+ "entry_index": 3,
+ "host": "httpbin.org",
+ "method": "GET",
+ "path": "/headers",
+ "reason": "JSON HTTP request/response without specialized protocol markers",
+ "status": 200
+ },
+ {
+ "content_type": "application/json",
+ "entry_index": 4,
+ "host": "httpbin.org",
+ "method": "GET",
+ "path": "/ip",
+ "reason": "JSON HTTP request/response without specialized protocol markers",
+ "status": 200
+ }
+ ],
+ "label": "rest_json"
+ }
+ ],
+ "reachability": {
+ "confidence": 0.65,
+ "mode": "standard_http",
+ "reasons": [
+ "no browser-only reachability signals observed"
+ ]
+ },
+ "request_sequences": [
+ {
+ "confidence": 0.65,
+ "evidence": [
+ {
+ "content_type": "application/json",
+ "entry_index": 1,
+ "host": "httpbin.org",
+ "method": "GET",
+ "path": "/get",
+ "reason": "observed API request order",
+ "status": 200
+ },
+ {
+ "content_type": "application/json",
+ "entry_index": 3,
+ "host": "httpbin.org",
+ "method": "GET",
+ "path": "/headers",
+ "reason": "observed API request order",
+ "status": 200
+ },
+ {
+ "content_type": "application/json",
+ "entry_index": 4,
+ "host": "httpbin.org",
+ "method": "GET",
+ "path": "/ip",
+ "reason": "observed API request order",
+ "status": 200
+ }
+ ],
+ "label": "observed_api_flow",
+ "notes": [
+ "HAR timing available for at least one request."
+ ]
+ }
+ ],
+ "summary": {
+ "api_entry_count": 3,
+ "entry_count": 5,
+ "host_distribution": {
+ "data": 1,
+ "httpbin.org": 4
+ },
+ "noise_entry_count": 2,
+ "target_url": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwcHgiICBoZWlnaHQ9IjIwMHB4IiAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiIGNsYXNzPSJsZHMtcm9sbGluZyIgc3R5bGU9ImJhY2tncm91bmQtaW1hZ2U6IG5vbmU7IGJhY2tncm91bmQtcG9zaXRpb246IGluaXRpYWwgaW5pdGlhbDsgYmFja2dyb3VuZC1yZXBlYXQ6IGluaXRpYWwgaW5pdGlhbDsiPjxjaXJjbGUgY3g9IjUwIiBjeT0iNTAiIGZpbGw9Im5vbmUiIG5nLWF0dHItc3Ryb2tlPSJ7e2NvbmZpZy5jb2xvcn19IiBuZy1hdHRyLXN0cm9rZS13aWR0aD0ie3tjb25maWcud2lkdGh9fSIgbmctYXR0ci1yPSJ7e2NvbmZpZy5yYWRpdXN9fSIgbmctYXR0ci1zdHJva2UtZGFzaGFycmF5PSJ7e2NvbmZpZy5kYXNoYXJyYXl9fSIgc3Ryb2tlPSIjNTU1NTU1IiBzdHJva2Utd2lkdGg9IjEwIiByPSIzNSIgc3Ryb2tlLWRhc2hhcnJheT0iMTY0LjkzMzYxNDMxMzQ2NDE1IDU2Ljk3Nzg3MTQzNzgyMTM4Ij48YW5pbWF0ZVRyYW5zZm9ybSBhdHRyaWJ1dGVOYW1lPSJ0cmFuc2Zvcm0iIHR5cGU9InJvdGF0ZSIgY2FsY01vZGU9ImxpbmVhciIgdmFsdWVzPSIwIDUwIDUwOzM2MCA1MCA1MCIga2V5VGltZXM9IjA7MSIgZHVyPSIxcyIgYmVnaW49IjBzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSI+PC9hbmltYXRlVHJhbnNmb3JtPjwvY2lyY2xlPjwvc3ZnPgo=",
+ "time_end": "2026-03-28T22:47:12.3217152Z",
+ "time_start": "2026-03-28T22:46:42.118844928Z"
+ },
+ "version": "1"
+}
diff --git a/testdata/golden/expected/catalog-list/exit.txt b/testdata/golden/expected/catalog-list/exit.txt
new file mode 100644
index 00000000..573541ac
--- /dev/null
+++ b/testdata/golden/expected/catalog-list/exit.txt
@@ -0,0 +1 @@
+0
diff --git a/testdata/golden/expected/catalog-list/stderr.txt b/testdata/golden/expected/catalog-list/stderr.txt
new file mode 100644
index 00000000..e69de29b
diff --git a/testdata/golden/expected/catalog-list/stdout.txt b/testdata/golden/expected/catalog-list/stdout.txt
new file mode 100644
index 00000000..81058c07
--- /dev/null
+++ b/testdata/golden/expected/catalog-list/stdout.txt
@@ -0,0 +1,41 @@
+auth:
+ stytch Authentication and user management API
+
+cloud:
+ digitalocean Cloud infrastructure and developer platform API
+
+developer-tools:
+ github Software development platform API
+ launchdarkly Feature flag management - flags, environments, segments, experiments, audit logs
+ postman-explore Public API network directory for discovering community collections, workspaces, and APIs
+
+example:
+ petstore Canonical OpenAPI example - pet store management
+
+marketing:
+ producthunt Find, monitor, and export Product Hunt launches for launch research, scouting, and competitive tracking without requiring a Product Hunt API application.
+
+monitoring:
+ sentry Error tracking and performance monitoring - projects, issues, events, releases
+
+other:
+ google-flights Flight search via Google Flights. No public API — reached through community-maintained reverse-engineered wrappers.
+ kayak Flight, hotel, and car rental aggregator. No public API and no maintained community wrappers — reached by scraping embedded JSON from server-rendered pages.
+
+payments:
+ plaid Banking API for account linking, transactions, identity verification, and income
+ stripe Payment processing and financial infrastructure API
+
+project-management:
+ asana Work management and project tracking API
+
+sales-and-crm:
+ hubspot CRM contacts API
+ pipedrive CRM for sales teams - deals, contacts, pipelines, activities, and organizations
+
+social-and-messaging:
+ discord Chat and community platform API
+ front Customer communication platform API
+ telegram Telegram Bot API for building bots - send messages, manage chats, webhooks, stickers
+ twilio Communication APIs for SMS, voice, and messaging
+
diff --git a/testdata/golden/expected/catalog-show-petstore/exit.txt b/testdata/golden/expected/catalog-show-petstore/exit.txt
new file mode 100644
index 00000000..573541ac
--- /dev/null
+++ b/testdata/golden/expected/catalog-show-petstore/exit.txt
@@ -0,0 +1 @@
+0
diff --git a/testdata/golden/expected/catalog-show-petstore/stderr.txt b/testdata/golden/expected/catalog-show-petstore/stderr.txt
new file mode 100644
index 00000000..e69de29b
diff --git a/testdata/golden/expected/catalog-show-petstore/stdout.txt b/testdata/golden/expected/catalog-show-petstore/stdout.txt
new file mode 100644
index 00000000..96a8579d
--- /dev/null
+++ b/testdata/golden/expected/catalog-show-petstore/stdout.txt
@@ -0,0 +1,11 @@
+Name: petstore
+Display Name: Petstore
+Description: Canonical OpenAPI example - pet store management
+Category: example
+Tier: official
+Spec URL: https://petstore3.swagger.io/api/v3/openapi.yaml
+Spec Format: yaml
+OpenAPI: 3.0
+Homepage: https://petstore3.swagger.io
+Notes: None.
+Verified: 2026-03-23
diff --git a/testdata/golden/expected/generate-golden-api/dogfood.json b/testdata/golden/expected/generate-golden-api/dogfood.json
new file mode 100644
index 00000000..92e12e61
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/dogfood.json
@@ -0,0 +1,79 @@
+{
+ "auth_check": {
+ "detail": "spec not provided; auth protocol check skipped",
+ "generated_format": "",
+ "match": true,
+ "spec_scheme": ""
+ },
+ "browser_session_check": {
+ "detail": "spec not provided; browser-session auth check skipped",
+ "pass": true,
+ "required": false
+ },
+ "dead_flags": {
+ "dead": 0,
+ "total": 17
+ },
+ "dead_functions": {
+ "dead": 1,
+ "items": [
+ "wrapResultsWithFreshness"
+ ],
+ "total": 50
+ },
+ "dir": "<ARTIFACT_DIR>/generate-golden-api/printing-press-golden",
+ "example_check": {
+ "detail": "could not build CLI binary: exit status 1",
+ "skipped": true,
+ "tested": 0,
+ "valid_examples": 0,
+ "with_examples": 0
+ },
+ "issues": [
+ "1 dead helper functions found",
+ "example check skipped: could not build CLI binary: exit status 1"
+ ],
+ "naming_check": {
+ "checked": 27
+ },
+ "novel_features_check": {
+ "found": 0,
+ "planned": 0,
+ "skipped": true
+ },
+ "path_check": {
+ "tested": 0,
+ "valid": 0,
+ "valid_pct": 0
+ },
+ "pipeline_check": {
+ "detail": "sync uses domain-specific Upsert methods; search methods not found; 1 domain tables found",
+ "domain_tables": 1,
+ "search_calls_domain": false,
+ "sync_calls_domain": true
+ },
+ "reimplementation_check": {
+ "checked": 0,
+ "exempted_via_store": 0,
+ "skipped": true
+ },
+ "test_presence": {
+ "checked": 0
+ },
+ "verdict": "WARN",
+ "wiring_check": {
+ "command_tree": {
+ "defined": 35,
+ "registered": 35
+ },
+ "config_consistency": {
+ "consistent": true
+ },
+ "workflow_completeness": {
+ "detail": "no workflow_verify.yaml found",
+ "mapped_steps": 0,
+ "skipped": true,
+ "total_steps": 0
+ }
+ }
+}
diff --git a/testdata/golden/expected/generate-golden-api/exit.txt b/testdata/golden/expected/generate-golden-api/exit.txt
new file mode 100644
index 00000000..573541ac
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/exit.txt
@@ -0,0 +1 @@
+0
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/.printing-press.json b/testdata/golden/expected/generate-golden-api/printing-press-golden/.printing-press.json
new file mode 100644
index 00000000..79dd3895
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/.printing-press.json
@@ -0,0 +1,20 @@
+{
+ "api_name": "printing-press-golden",
+ "api_version": "2026.04",
+ "auth_env_vars": [
+ "PRINTING_PRESS_GOLDEN_API_KEY_AUTH"
+ ],
+ "auth_type": "api_key",
+ "cli_name": "printing-press-golden-pp-cli",
+ "generated_at": "<GENERATED_AT>",
+ "mcp_binary": "printing-press-golden-pp-mcp",
+ "mcp_public_tool_count": 1,
+ "mcp_ready": "full",
+ "mcp_tool_count": 6,
+ "printing_press_version": "<PRINTING_PRESS_VERSION>",
+ "schema_version": 1,
+ "spec_checksum": "sha256:afe8312a5660273a91a5ae54b4b7402a9c993b12ebf3b969fd63522f25d68836",
+ "spec_format": "openapi3",
+ "spec_path": "testdata/golden/fixtures/golden-api.yaml",
+ "spec_url": "file://testdata/golden/fixtures/golden-api.yaml"
+}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/cmd/printing-press-golden-pp-cli/main.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/cmd/printing-press-golden-pp-cli/main.go
new file mode 100644
index 00000000..6a5aa9d7
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/cmd/printing-press-golden-pp-cli/main.go
@@ -0,0 +1,18 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "printing-press-golden-pp-cli/internal/cli"
+)
+
+func main() {
+ if err := cli.Execute(); err != nil {
+ fmt.Fprintln(os.Stderr, err.Error())
+ os.Exit(cli.ExitCode(err))
+ }
+}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/go.mod b/testdata/golden/expected/generate-golden-api/printing-press-golden/go.mod
new file mode 100644
index 00000000..4dd91bda
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/go.mod
@@ -0,0 +1,10 @@
+module printing-press-golden-pp-cli
+
+go 1.23
+
+require (
+ github.com/spf13/cobra v1.9.1
+ github.com/pelletier/go-toml/v2 v2.2.4
+)
+require modernc.org/sqlite v1.37.0
+require github.com/mark3labs/mcp-go v0.47.0
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/auth.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/auth.go
new file mode 100644
index 00000000..a2409194
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/auth.go
@@ -0,0 +1,104 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package cli
+
+import (
+ "fmt"
+ "os"
+
+ "printing-press-golden-pp-cli/internal/config"
+ "github.com/spf13/cobra"
+)
+
+func newAuthCmd(flags *rootFlags) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "auth",
+ Short: "Manage PRINTING_PRESS_GOLDEN_API_KEY_AUTH credentials",
+ }
+
+ cmd.AddCommand(newAuthStatusCmd(flags))
+ cmd.AddCommand(newAuthSetTokenCmd(flags))
+ cmd.AddCommand(newAuthLogoutCmd(flags))
+
+ return cmd
+}
+
+func newAuthStatusCmd(flags *rootFlags) *cobra.Command {
+ return &cobra.Command{
+ Use: "status",
+ Short: "Show authentication status",
+ Example: " printing-press-golden-pp-cli auth status",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ cfg, err := config.Load(flags.configPath)
+ if err != nil {
+ return configErr(err)
+ }
+
+ w := cmd.OutOrStdout()
+ header := cfg.AuthHeader()
+ if header == "" {
+ fmt.Fprintln(w, red("Not authenticated"))
+ fmt.Fprintln(w, "")
+ fmt.Fprintln(w, "Set your token:")
+ fmt.Fprintln(w, " export PRINTING_PRESS_GOLDEN_API_KEY_AUTH=\"your-token-here\"")
+ fmt.Fprintf(w, " printing-press-golden-pp-cli auth set-token <token>\n")
+ return authErr(fmt.Errorf("no credentials configured"))
+ }
+
+ fmt.Fprintln(w, green("Authenticated"))
+ fmt.Fprintf(w, " Source: %s\n", cfg.AuthSource)
+ fmt.Fprintf(w, " Config: %s\n", cfg.Path)
+ return nil
+ },
+ }
+}
+
+func newAuthSetTokenCmd(flags *rootFlags) *cobra.Command {
+ return &cobra.Command{
+ Use: "set-token <token>",
+ Short: "Save an API token to the config file",
+ Example: " printing-press-golden-pp-cli auth set-token sk_live_abc123",
+ Args: cobra.ExactArgs(1),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ cfg, err := config.Load(flags.configPath)
+ if err != nil {
+ return configErr(err)
+ }
+
+ // Save the token directly via the config's save mechanism
+ if err := cfg.SaveTokens("", "", args[0], "", cfg.TokenExpiry); err != nil {
+ return configErr(fmt.Errorf("saving token: %w", err))
+ }
+
+ fmt.Fprintf(cmd.OutOrStdout(), "Token saved to %s\n", cfg.Path)
+ return nil
+ },
+ }
+}
+
+func newAuthLogoutCmd(flags *rootFlags) *cobra.Command {
+ return &cobra.Command{
+ Use: "logout",
+ Short: "Clear stored credentials",
+ Example: " printing-press-golden-pp-cli auth logout",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ cfg, err := config.Load(flags.configPath)
+ if err != nil {
+ return configErr(err)
+ }
+
+ if err := cfg.ClearTokens(); err != nil {
+ return configErr(fmt.Errorf("clearing tokens: %w", err))
+ }
+
+ // Warn if env vars still set
+ if os.Getenv("PRINTING_PRESS_GOLDEN_API_KEY_AUTH") != "" {
+ fmt.Fprintf(cmd.OutOrStdout(), "Config cleared. Note: PRINTING_PRESS_GOLDEN_API_KEY_AUTH env var is still set.\n")
+ return nil
+ }
+ fmt.Fprintln(cmd.OutOrStdout(), "Logged out. Credentials cleared.")
+ return nil
+ },
+ }
+}
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
new file mode 100644
index 00000000..d78d192c
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.go
@@ -0,0 +1,134 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package cli
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/spf13/cobra"
+)
+
+func newProjectsCreateCmd(flags *rootFlags) *cobra.Command {
+ var bodyName string
+ var bodyOwnerEmail string
+ var bodyVisibility string
+ var stdinBody bool
+
+ cmd := &cobra.Command{
+ Use: "create",
+ Short: "Create project",
+ Example: " printing-press-golden-pp-cli projects create --name example-resource",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if !stdinBody {
+ if !cmd.Flags().Changed("name") && !flags.dryRun {
+ return fmt.Errorf("required flag \"%s\" not set", "name")
+ }
+ if !cmd.Flags().Changed("visibility") && !flags.dryRun {
+ return fmt.Errorf("required flag \"%s\" not set", "visibility")
+ }
+ }
+ c, err := flags.newClient()
+ if err != nil {
+ return err
+ }
+
+ path := "/projects"
+ 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{}
+ if bodyName != "" {
+ body["name"] = bodyName
+ }
+ if bodyOwnerEmail != "" {
+ body["owner_email"] = bodyOwnerEmail
+ }
+ if bodyVisibility != "" {
+ body["visibility"] = bodyVisibility
+ }
+ }
+ data, statusCode, err := c.Post(path, body)
+ if err != nil {
+ return classifyAPIError(err)
+ }
+ if wantsHumanTable(cmd.OutOrStdout(), flags) {
+ // Check if response contains an array (directly or wrapped in "data")
+ var items []map[string]any
+ if json.Unmarshal(data, &items) == nil && len(items) > 0 {
+ if err := printAutoTable(cmd.OutOrStdout(), items); err != nil {
+ fmt.Fprintf(os.Stderr, "warning: table rendering failed, falling back to JSON: %v\n", err)
+ } else {
+ return nil
+ }
+ } else {
+ var wrapped struct{ Data []map[string]any `json:"data"` }
+ if json.Unmarshal(data, &wrapped) == nil && len(wrapped.Data) > 0 {
+ if err := printAutoTable(cmd.OutOrStdout(), wrapped.Data); err != nil {
+ fmt.Fprintf(os.Stderr, "warning: table rendering failed, falling back to JSON: %v\n", err)
+ } else {
+ return nil
+ }
+ }
+ }
+ }
+ if flags.asJSON || !isTerminal(cmd.OutOrStdout()) {
+ if flags.quiet {
+ return nil
+ }
+ // Apply --compact and --select to the API response before wrapping.
+ // --select wins when both are set: explicit field choice trumps the
+ // generic high-gravity allow-list. Otherwise --compact still applies
+ // when --agent is on but the user did not name fields.
+ filtered := data
+ if flags.selectFields != "" {
+ filtered = filterFields(filtered, flags.selectFields)
+ } else if flags.compact {
+ filtered = compactFields(filtered)
+ }
+ envelope := map[string]any{
+ "action": "post",
+ "resource": "projects",
+ "path": path,
+ "status": statusCode,
+ "success": statusCode >= 200 && statusCode < 300,
+ }
+ if flags.dryRun {
+ envelope["dry_run"] = true
+ envelope["status"] = 0
+ envelope["success"] = false
+ }
+ if len(filtered) > 0 {
+ var parsed any
+ if err := json.Unmarshal(filtered, &parsed); err == nil {
+ envelope["data"] = parsed
+ }
+ }
+ envelopeJSON, err := json.Marshal(envelope)
+ if err != nil {
+ return err
+ }
+ return printOutput(cmd.OutOrStdout(), json.RawMessage(envelopeJSON), true)
+ }
+ return printOutputWithFlags(cmd.OutOrStdout(), data, flags)
+ },
+ }
+ cmd.Flags().StringVar(&bodyName, "name", "", "Name")
+ cmd.Flags().StringVar(&bodyOwnerEmail, "owner-email", "", "Owner email")
+ cmd.Flags().StringVar(&bodyVisibility, "visibility", "", "Visibility")
+ cmd.Flags().BoolVar(&stdinBody, "stdin", false, "Read request body as JSON from stdin")
+
+ return cmd
+}
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
new file mode 100644
index 00000000..b40808a0
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.go
@@ -0,0 +1,96 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package cli
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "github.com/spf13/cobra"
+)
+
+func newProjectsListCmd(flags *rootFlags) *cobra.Command {
+ var flagStatus string
+ var flagLimit int
+ var flagCursor string
+ var flagAll bool
+
+ cmd := &cobra.Command{
+ Use: "list",
+ Short: "List projects",
+ Example: " printing-press-golden-pp-cli projects list",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if cmd.Flags().Changed("status") {
+ allowedStatus := []string{ "draft", "active", "archived" }
+ validStatus := false
+ for _, v := range allowedStatus {
+ if flagStatus == v {
+ validStatus = true
+ break
+ }
+ }
+ if !validStatus {
+ fmt.Fprintf(os.Stderr, "warning: --%s %q not in allowed set %v\n", "status", flagStatus, allowedStatus)
+ }
+ }
+ c, err := flags.newClient()
+ if err != nil {
+ return err
+ }
+
+ path := "/projects"
+ data, prov, err := resolvePaginatedRead(c, flags, "projects", path, map[string]string{
+ "status": fmt.Sprintf("%v", flagStatus),
+ "limit": fmt.Sprintf("%v", flagLimit),
+ "cursor": fmt.Sprintf("%v", flagCursor),
+ }, flagAll, "cursor", "", "")
+ if err != nil {
+ return classifyAPIError(err)
+ }
+ // Print provenance to stderr for human-facing output
+ {
+ var countItems []json.RawMessage
+ _ = json.Unmarshal(data, &countItems)
+ printProvenance(cmd, len(countItems), prov)
+ }
+ // For JSON output, wrap with provenance envelope before passing through flags.
+ // --select wins over --compact when both are set; --compact only runs when
+ // no explicit fields were requested.
+ if flags.asJSON || !isTerminal(cmd.OutOrStdout()) {
+ filtered := data
+ if flags.selectFields != "" {
+ filtered = filterFields(filtered, flags.selectFields)
+ } else if flags.compact {
+ filtered = compactFields(filtered)
+ }
+ wrapped, wrapErr := wrapWithProvenance(filtered, prov)
+ if wrapErr != nil {
+ return wrapErr
+ }
+ return printOutput(cmd.OutOrStdout(), wrapped, true)
+ }
+ // For all other output modes (table, csv, plain, quiet), use the standard pipeline
+ if wantsHumanTable(cmd.OutOrStdout(), flags) {
+ var items []map[string]any
+ if json.Unmarshal(data, &items) == nil && len(items) > 0 {
+ if err := printAutoTable(cmd.OutOrStdout(), items); err != nil {
+ return err
+ }
+ if len(items) >= 25 {
+ fmt.Fprintf(os.Stderr, "\nShowing %d results. To narrow: add --limit, --json --select, or filter flags.\n", len(items))
+ }
+ return nil
+ }
+ }
+ return printOutputWithFlags(cmd.OutOrStdout(), data, flags)
+ },
+ }
+ cmd.Flags().StringVar(&flagStatus, "status", "", "Status (one of: draft, active, archived)")
+ cmd.Flags().IntVar(&flagLimit, "limit", 25, "Limit")
+ cmd.Flags().StringVar(&flagCursor, "cursor", "", "Cursor")
+ cmd.Flags().BoolVar(&flagAll, "all", false, "Fetch all pages")
+
+ return cmd
+}
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
new file mode 100644
index 00000000..cadb8191
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.go
@@ -0,0 +1,101 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package cli
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "github.com/spf13/cobra"
+)
+
+func newProjectsTasksListProjectCmd(flags *rootFlags) *cobra.Command {
+ var flagPriority string
+ var flagLimit int
+ var flagCursor string
+ var flagAll bool
+
+ cmd := &cobra.Command{
+ Use: "list-project <projectId>",
+ Aliases: []string{"get"},
+ Short: "List project tasks",
+ Example: " printing-press-golden-pp-cli projects tasks list-project example-value",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if len(args) == 0 {
+ return cmd.Help()
+ }
+ if cmd.Flags().Changed("priority") {
+ allowedPriority := []string{ "low", "normal", "high" }
+ validPriority := false
+ for _, v := range allowedPriority {
+ if flagPriority == v {
+ validPriority = true
+ break
+ }
+ }
+ if !validPriority {
+ fmt.Fprintf(os.Stderr, "warning: --%s %q not in allowed set %v\n", "priority", flagPriority, allowedPriority)
+ }
+ }
+ c, err := flags.newClient()
+ if err != nil {
+ return err
+ }
+
+ path := "/projects/{projectId}/tasks"
+ path = replacePathParam(path, "projectId", args[0])
+ data, prov, err := resolvePaginatedRead(c, flags, "tasks", path, map[string]string{
+ "priority": fmt.Sprintf("%v", flagPriority),
+ "limit": fmt.Sprintf("%v", flagLimit),
+ "cursor": fmt.Sprintf("%v", flagCursor),
+ }, flagAll, "cursor", "", "")
+ if err != nil {
+ return classifyAPIError(err)
+ }
+ // Print provenance to stderr for human-facing output
+ {
+ var countItems []json.RawMessage
+ _ = json.Unmarshal(data, &countItems)
+ printProvenance(cmd, len(countItems), prov)
+ }
+ // For JSON output, wrap with provenance envelope before passing through flags.
+ // --select wins over --compact when both are set; --compact only runs when
+ // no explicit fields were requested.
+ if flags.asJSON || !isTerminal(cmd.OutOrStdout()) {
+ filtered := data
+ if flags.selectFields != "" {
+ filtered = filterFields(filtered, flags.selectFields)
+ } else if flags.compact {
+ filtered = compactFields(filtered)
+ }
+ wrapped, wrapErr := wrapWithProvenance(filtered, prov)
+ if wrapErr != nil {
+ return wrapErr
+ }
+ return printOutput(cmd.OutOrStdout(), wrapped, true)
+ }
+ // For all other output modes (table, csv, plain, quiet), use the standard pipeline
+ if wantsHumanTable(cmd.OutOrStdout(), flags) {
+ var items []map[string]any
+ if json.Unmarshal(data, &items) == nil && len(items) > 0 {
+ if err := printAutoTable(cmd.OutOrStdout(), items); err != nil {
+ return err
+ }
+ if len(items) >= 25 {
+ fmt.Fprintf(os.Stderr, "\nShowing %d results. To narrow: add --limit, --json --select, or filter flags.\n", len(items))
+ }
+ return nil
+ }
+ }
+ return printOutputWithFlags(cmd.OutOrStdout(), data, flags)
+ },
+ }
+ cmd.Flags().StringVar(&flagPriority, "priority", "", "Priority (one of: low, normal, high)")
+ cmd.Flags().IntVar(&flagLimit, "limit", 50, "Limit")
+ cmd.Flags().StringVar(&flagCursor, "cursor", "", "Cursor")
+ cmd.Flags().BoolVar(&flagAll, "all", false, "Fetch all pages")
+
+ return cmd
+}
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
new file mode 100644
index 00000000..492bb4c1
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go
@@ -0,0 +1,137 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package cli
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/spf13/cobra"
+)
+
+func newProjectsTasksUpdateProjectCmd(flags *rootFlags) *cobra.Command {
+ var bodyCompleted bool
+ var bodyPriority string
+ var bodyTitle string
+ var stdinBody bool
+
+ cmd := &cobra.Command{
+ Use: "update-project <projectId> <taskId>",
+ Aliases: []string{"update"},
+ Short: "Update project task",
+ Example: " printing-press-golden-pp-cli projects tasks update-project example-value example-value",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if len(args) == 0 {
+ return cmd.Help()
+ }
+ if !stdinBody {
+ }
+ c, err := flags.newClient()
+ if err != nil {
+ return err
+ }
+
+ path := "/projects/{projectId}/tasks/{taskId}"
+ path = replacePathParam(path, "projectId", args[0])
+ if len(args) < 2 {
+ return usageErr(fmt.Errorf("taskId is required\nUsage: %s %s <%s>", cmd.Root().Name(), cmd.CommandPath(), "taskId"))
+ }
+ path = replacePathParam(path, "taskId", args[1])
+ 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{}
+ if bodyCompleted != false {
+ body["completed"] = bodyCompleted
+ }
+ if bodyPriority != "" {
+ body["priority"] = bodyPriority
+ }
+ if bodyTitle != "" {
+ body["title"] = bodyTitle
+ }
+ }
+ data, statusCode, err := c.Patch(path, body)
+ if err != nil {
+ return classifyAPIError(err)
+ }
+ if wantsHumanTable(cmd.OutOrStdout(), flags) {
+ // Check if response contains an array (directly or wrapped in "data")
+ var items []map[string]any
+ if json.Unmarshal(data, &items) == nil && len(items) > 0 {
+ if err := printAutoTable(cmd.OutOrStdout(), items); err != nil {
+ fmt.Fprintf(os.Stderr, "warning: table rendering failed, falling back to JSON: %v\n", err)
+ } else {
+ return nil
+ }
+ } else {
+ var wrapped struct{ Data []map[string]any `json:"data"` }
+ if json.Unmarshal(data, &wrapped) == nil && len(wrapped.Data) > 0 {
+ if err := printAutoTable(cmd.OutOrStdout(), wrapped.Data); err != nil {
+ fmt.Fprintf(os.Stderr, "warning: table rendering failed, falling back to JSON: %v\n", err)
+ } else {
+ return nil
+ }
+ }
+ }
+ }
+ if flags.asJSON || !isTerminal(cmd.OutOrStdout()) {
+ if flags.quiet {
+ return nil
+ }
+ // Apply --compact and --select to the API response before wrapping.
+ // --select wins when both are set: explicit field choice trumps the
+ // generic high-gravity allow-list. Otherwise --compact still applies
+ // when --agent is on but the user did not name fields.
+ filtered := data
+ if flags.selectFields != "" {
+ filtered = filterFields(filtered, flags.selectFields)
+ } else if flags.compact {
+ filtered = compactFields(filtered)
+ }
+ envelope := map[string]any{
+ "action": "patch",
+ "resource": "tasks",
+ "path": path,
+ "status": statusCode,
+ "success": statusCode >= 200 && statusCode < 300,
+ }
+ if flags.dryRun {
+ envelope["dry_run"] = true
+ envelope["status"] = 0
+ envelope["success"] = false
+ }
+ if len(filtered) > 0 {
+ var parsed any
+ if err := json.Unmarshal(filtered, &parsed); err == nil {
+ envelope["data"] = parsed
+ }
+ }
+ envelopeJSON, err := json.Marshal(envelope)
+ if err != nil {
+ return err
+ }
+ return printOutput(cmd.OutOrStdout(), json.RawMessage(envelopeJSON), true)
+ }
+ return printOutputWithFlags(cmd.OutOrStdout(), data, flags)
+ },
+ }
+ cmd.Flags().BoolVar(&bodyCompleted, "completed", false, "Completed")
+ cmd.Flags().StringVar(&bodyPriority, "priority", "", "Priority")
+ cmd.Flags().StringVar(&bodyTitle, "title", "", "Title")
+ cmd.Flags().BoolVar(&stdinBody, "stdin", false, "Read request body as JSON from stdin")
+
+ return cmd
+}
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
new file mode 100644
index 00000000..1f6de6a5
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.go
@@ -0,0 +1,85 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package cli
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "github.com/spf13/cobra"
+)
+
+func newPublicPromotedCmd(flags *rootFlags) *cobra.Command {
+
+ cmd := &cobra.Command{
+ Use: "public",
+ Short: "Get public service status",
+ Long: "Shortcut for 'public get-status'. Get public service status",
+ Example: " printing-press-golden-pp-cli public",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ c, err := flags.newClient()
+ if err != nil {
+ return err
+ }
+
+ path := "/public/status"
+ params := map[string]string{}
+ data, prov, err := resolveRead(c, flags, "public", false, path, params)
+ if err != nil {
+ return classifyAPIError(err)
+ }
+ // Unwrap API response envelopes (e.g. {"status":"success","data":[...]})
+ // so output helpers see the inner data, not the wrapper.
+ data = extractResponseData(data)
+
+ // Print provenance to stderr
+ {
+ var countItems []json.RawMessage
+ if json.Unmarshal(data, &countItems) != nil {
+ // Single object, not an array
+ countItems = []json.RawMessage{data}
+ }
+ printProvenance(cmd, len(countItems), prov)
+ }
+ // CSV bypasses JSON pipe path so --csv works when piped
+ if flags.csv {
+ return printOutputWithFlags(cmd.OutOrStdout(), data, flags)
+ }
+ // For JSON output, wrap with provenance envelope. --select wins over
+ // --compact when both are set; --compact only runs when no explicit
+ // fields were requested.
+ if flags.asJSON || !isTerminal(cmd.OutOrStdout()) {
+ filtered := data
+ if flags.selectFields != "" {
+ filtered = filterFields(filtered, flags.selectFields)
+ } else if flags.compact {
+ filtered = compactFields(filtered)
+ }
+ wrapped, wrapErr := wrapWithProvenance(filtered, prov)
+ if wrapErr != nil {
+ return wrapErr
+ }
+ return printOutput(cmd.OutOrStdout(), wrapped, true)
+ }
+ if wantsHumanTable(cmd.OutOrStdout(), flags) {
+ var items []map[string]any
+ if json.Unmarshal(data, &items) == nil && len(items) > 0 {
+ if err := printAutoTable(cmd.OutOrStdout(), items); err != nil {
+ return err
+ }
+ if len(items) >= 25 {
+ fmt.Fprintf(os.Stderr, "\nShowing %d results. To narrow: add --limit, --json --select, or filter flags.\n", len(items))
+ }
+ return nil
+ }
+ }
+ return printOutputWithFlags(cmd.OutOrStdout(), data, flags)
+ },
+ }
+
+ // Wire sibling endpoints and sub-resources as subcommands
+
+ return cmd
+}
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
new file mode 100644
index 00000000..af791954
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/root.go
@@ -0,0 +1,236 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package cli
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+ "text/tabwriter"
+ "time"
+
+ "printing-press-golden-pp-cli/internal/client"
+ "printing-press-golden-pp-cli/internal/config"
+ "github.com/spf13/cobra"
+)
+
+var version = "1.0.0"
+
+type rootFlags struct {
+ asJSON bool
+ compact bool
+ csv bool
+ plain bool
+ quiet bool
+ dryRun bool
+ noCache bool
+ noInput bool
+ yes bool
+ agent bool
+ selectFields string
+ configPath string
+ profileName string
+ deliverSpec string
+ timeout time.Duration
+ rateLimit float64
+ dataSource string
+ freshnessMeta any
+
+ // deliverBuf captures command output when --deliver is set to a
+ // non-stdout sink. Flushed to the sink after Execute returns.
+ deliverBuf *bytes.Buffer
+ deliverSink DeliverSink
+}
+
+// Execute runs the CLI in non-interactive mode: never prompts, all values via flags or stdin.
+func Execute() error {
+ var flags rootFlags
+
+ rootCmd := &cobra.Command{
+ Use: "printing-press-golden-pp-cli",
+ Short: "Manage printing-press-golden resources via the printing-press-golden API",
+ Long: `Manage printing-press-golden resources via the printing-press-golden API.
+
+Add --agent to any command for JSON output + non-interactive mode.
+Run 'printing-press-golden-pp-cli doctor' to verify auth and connectivity.`,
+ SilenceUsage: true,
+ Version: version,
+ }
+ rootCmd.SetVersionTemplate("printing-press-golden-pp-cli {{ .Version }}\n")
+
+ rootCmd.PersistentFlags().BoolVar(&flags.asJSON, "json", false, "Output as JSON")
+ rootCmd.PersistentFlags().BoolVar(&flags.compact, "compact", false, "Return only key fields (id, name, status, timestamps) for minimal token usage")
+ rootCmd.PersistentFlags().BoolVar(&flags.csv, "csv", false, "Output as CSV (table and array responses)")
+ rootCmd.PersistentFlags().BoolVar(&flags.plain, "plain", false, "Output as plain tab-separated text")
+ rootCmd.PersistentFlags().BoolVar(&flags.quiet, "quiet", false, "Bare output, one value per line")
+ 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().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().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")
+ rootCmd.PersistentFlags().BoolVar(&humanFriendly, "human-friendly", false, "Enable colored output and rich formatting")
+ rootCmd.PersistentFlags().BoolVar(&flags.agent, "agent", false, "Set all agent-friendly defaults (--json --compact --no-input --no-color --yes)")
+ rootCmd.PersistentFlags().StringVar(&flags.dataSource, "data-source", "auto", "Data source for read commands: auto (live with local fallback), live (API only), local (synced data only)")
+ rootCmd.PersistentFlags().StringVar(&flags.profileName, "profile", "", "Apply values from a saved profile (see 'printing-press-golden-pp-cli profile list')")
+ rootCmd.PersistentFlags().StringVar(&flags.deliverSpec, "deliver", "", "Route output to a sink: stdout (default), file:<path>, webhook:<url>")
+ rootCmd.PersistentFlags().Float64Var(&flags.rateLimit, "rate-limit", 0, "Max requests per second (0 to disable)")
+
+ rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
+ if flags.deliverSpec != "" {
+ sink, err := ParseDeliverSink(flags.deliverSpec)
+ if err != nil {
+ return err
+ }
+ flags.deliverSink = sink
+ if sink.Scheme != "stdout" && sink.Scheme != "" {
+ flags.deliverBuf = &bytes.Buffer{}
+ cmd.SetOut(io.MultiWriter(os.Stdout, flags.deliverBuf))
+ }
+ }
+ if flags.profileName != "" {
+ profile, err := GetProfile(flags.profileName)
+ if err != nil {
+ return err
+ }
+ if profile == nil {
+ available := ListProfileNames()
+ if len(available) == 0 {
+ return fmt.Errorf("profile %q not found (no profiles saved yet; run '%s profile save <name> --<flag> <value>')", flags.profileName, cmd.Root().Name())
+ }
+ return fmt.Errorf("profile %q not found; available: %s", flags.profileName, strings.Join(available, ", "))
+ }
+ if err := ApplyProfileToFlags(cmd, profile); err != nil {
+ return err
+ }
+ }
+ if flags.agent {
+ if !cmd.Flags().Changed("json") {
+ flags.asJSON = true
+ }
+ if !cmd.Flags().Changed("compact") {
+ flags.compact = true
+ }
+ if !cmd.Flags().Changed("no-input") {
+ flags.noInput = true
+ }
+ if !cmd.Flags().Changed("yes") {
+ flags.yes = true
+ }
+ if !cmd.Flags().Changed("no-color") {
+ noColor = true
+ }
+ }
+ switch flags.dataSource {
+ case "auto", "live", "local":
+ // valid
+ default:
+ return fmt.Errorf("invalid --data-source value %q: must be auto, live, or local", flags.dataSource)
+ }
+ return nil
+ }
+ rootCmd.AddCommand(newProjectsCmd(&flags))
+ rootCmd.AddCommand(newDoctorCmd(&flags))
+ rootCmd.AddCommand(newAuthCmd(&flags))
+ rootCmd.AddCommand(newAgentContextCmd(rootCmd))
+ rootCmd.AddCommand(newProfileCmd(&flags))
+ rootCmd.AddCommand(newFeedbackCmd(&flags))
+ rootCmd.AddCommand(newWhichCmd(&flags))
+ rootCmd.AddCommand(newExportCmd(&flags))
+ rootCmd.AddCommand(newImportCmd(&flags))
+ rootCmd.AddCommand(newSyncCmd(&flags))
+ rootCmd.AddCommand(newAnalyticsCmd(&flags))
+ rootCmd.AddCommand(newWorkflowCmd(&flags))
+ rootCmd.AddCommand(newStaleCmd(&flags))
+ rootCmd.AddCommand(newOrphansCmd(&flags))
+ rootCmd.AddCommand(newLoadCmd(&flags))
+ rootCmd.AddCommand(newAPICmd(&flags))
+ rootCmd.AddCommand(newPublicPromotedCmd(&flags))
+ rootCmd.AddCommand(newVersionCliCmd())
+
+ err := rootCmd.Execute()
+ if err != nil && strings.Contains(err.Error(), "unknown flag") {
+ msg := err.Error()
+ // Extract the flag name from the error message (e.g., "unknown flag: --foob")
+ if idx := strings.Index(msg, "unknown flag: "); idx >= 0 {
+ flagStr := strings.TrimSpace(msg[idx+len("unknown flag: "):])
+ if suggestion := suggestFlag(flagStr, rootCmd); suggestion != "" {
+ return fmt.Errorf("%w\nhint: did you mean --%s?", err, suggestion)
+ }
+ }
+ }
+ if err == nil && flags.deliverBuf != nil {
+ if derr := Deliver(flags.deliverSink, flags.deliverBuf.Bytes(), flags.compact); derr != nil {
+ fmt.Fprintf(os.Stderr, "warning: deliver to %s:%s failed: %v\n", flags.deliverSink.Scheme, flags.deliverSink.Target, derr)
+ return derr
+ }
+ }
+ return err
+}
+
+func ExitCode(err error) int {
+ var codeErr *cliError
+ if As(err, &codeErr) {
+ return codeErr.code
+ }
+ return 1
+}
+
+func (f *rootFlags) newClient() (*client.Client, error) {
+ cfg, err := config.Load(f.configPath)
+ if err != nil {
+ return nil, configErr(err)
+ }
+ c := client.New(cfg, f.timeout, f.rateLimit)
+ c.DryRun = f.dryRun
+ c.NoCache = f.noCache
+ return c, nil
+}
+
+func (f *rootFlags) printJSON(w *cobra.Command, v any) error {
+ enc := json.NewEncoder(w.OutOrStdout())
+ enc.SetIndent("", " ")
+ return enc.Encode(v)
+}
+
+func (f *rootFlags) printTable(w *cobra.Command, headers []string, rows [][]string) error {
+ if f.asJSON {
+ return fmt.Errorf("use printJSON for JSON output")
+ }
+ tw := tabwriter.NewWriter(w.OutOrStdout(), 2, 4, 2, ' ', 0)
+ header := ""
+ for i, h := range headers {
+ if i > 0 {
+ header += "\t"
+ }
+ header += h
+ }
+ fmt.Fprintln(tw, header)
+ for _, row := range rows {
+ line := ""
+ for i, cell := range row {
+ if i > 0 {
+ line += "\t"
+ }
+ line += cell
+ }
+ fmt.Fprintln(tw, line)
+ }
+ return tw.Flush()
+}
+
+func newVersionCliCmd() *cobra.Command {
+ return &cobra.Command{
+ Use: "version",
+ Short: "Print version",
+ Run: func(cmd *cobra.Command, args []string) {
+ fmt.Printf("printing-press-golden-pp-cli %s\n", version)
+ },
+ }
+}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go
new file mode 100644
index 00000000..b7c1059c
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/client/client.go
@@ -0,0 +1,536 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package client
+
+import (
+ "bytes"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "io"
+ "math"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+ "printing-press-golden-pp-cli/internal/config"
+)
+
+type Client struct {
+ BaseURL string
+ Config *config.Config
+ HTTPClient *http.Client
+ DryRun bool
+ NoCache bool
+ cacheDir string
+ limiter *adaptiveLimiter
+}
+
+// adaptiveLimiter provides proactive rate limiting with adaptive ceiling discovery.
+// Starts at a conservative floor rate and ramps up after consecutive successes.
+// On 429, halves the rate and records a ceiling. Per-session only — not persisted.
+// Thread-safe: all methods are guarded by a mutex for concurrent sync workers.
+type adaptiveLimiter struct {
+ mu sync.Mutex
+ rate float64 // current requests per second
+ floor float64 // starting/minimum rate
+ ceiling float64 // discovered ceiling (0 = unknown)
+ successes int // consecutive successes since last 429
+ rampAfter int // successes needed before increasing rate
+ lastRequest time.Time // zero-value on init — first call skips wait (intentional)
+}
+
+func newAdaptiveLimiter(ratePerSec float64) *adaptiveLimiter {
+ if ratePerSec <= 0 {
+ return nil
+ }
+ return &adaptiveLimiter{
+ rate: ratePerSec,
+ floor: ratePerSec,
+ rampAfter: 10,
+ }
+}
+
+// Wait blocks until the rate limiter allows the next request.
+func (l *adaptiveLimiter) Wait() {
+ if l == nil {
+ return
+ }
+ l.mu.Lock()
+ delay := time.Duration(float64(time.Second) / l.rate)
+ elapsed := time.Since(l.lastRequest)
+ l.mu.Unlock()
+ if elapsed < delay {
+ time.Sleep(delay - elapsed)
+ }
+ l.mu.Lock()
+ l.lastRequest = time.Now()
+ l.mu.Unlock()
+}
+
+// OnSuccess records a successful request and ramps up the rate after enough consecutive successes.
+func (l *adaptiveLimiter) OnSuccess() {
+ if l == nil {
+ return
+ }
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ l.successes++
+ if l.successes >= l.rampAfter {
+ newRate := l.rate * 1.25
+ if l.ceiling > 0 && newRate > l.ceiling*0.9 {
+ newRate = l.ceiling * 0.9
+ }
+ l.rate = newRate
+ l.successes = 0
+ }
+}
+
+// OnRateLimit records a 429 response — halves the rate and discovers the ceiling.
+func (l *adaptiveLimiter) OnRateLimit() {
+ if l == nil {
+ return
+ }
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ l.ceiling = l.rate
+ l.rate = l.rate / 2
+ if l.rate < 0.5 {
+ l.rate = 0.5 // absolute minimum: 1 request per 2 seconds
+ }
+ l.successes = 0
+}
+
+// Rate returns the current rate in requests per second. Returns 0 if the limiter is nil.
+func (l *adaptiveLimiter) Rate() float64 {
+ if l == nil {
+ return 0
+ }
+ l.mu.Lock()
+ defer l.mu.Unlock()
+ return l.rate
+}
+
+
+
+// APIError carries HTTP status information for structured exit codes.
+type APIError struct {
+ Method string
+ Path string
+ StatusCode int
+ Body string
+}
+
+func (e *APIError) Error() string {
+ return fmt.Sprintf("%s %s returned HTTP %d: %s", e.Method, e.Path, e.StatusCode, e.Body)
+}
+
+func newHTTPClient(timeout time.Duration, jar http.CookieJar) *http.Client {
+ return &http.Client{Timeout: timeout, Jar: jar}
+}
+
+func New(cfg *config.Config, timeout time.Duration, rateLimit float64) *Client {
+ homeDir, _ := os.UserHomeDir()
+ cacheDir := filepath.Join(homeDir, ".cache", "printing-press-golden-pp-cli")
+ httpClient := newHTTPClient(timeout, nil)
+ return &Client{
+ BaseURL: strings.TrimRight(cfg.BaseURL, "/"),
+ Config: cfg,
+ HTTPClient: httpClient,
+ cacheDir: cacheDir,
+ limiter: newAdaptiveLimiter(rateLimit),
+ }
+}
+
+// RateLimit returns the current effective rate limit in req/s. Returns 0 if disabled.
+func (c *Client) RateLimit() float64 {
+ return c.limiter.Rate()
+}
+
+func (c *Client) Get(path string, params map[string]string) (json.RawMessage, error) {
+ return c.GetWithHeaders(path, params, nil)
+}
+
+func (c *Client) GetWithHeaders(path string, params map[string]string, headers map[string]string) (json.RawMessage, error) {
+ // Check cache for GET requests
+ if !c.NoCache && !c.DryRun && c.cacheDir != "" {
+ if cached, ok := c.readCache(path, params); ok {
+ return cached, nil
+ }
+ }
+ result, _, err := c.do("GET", path, params, nil, headers)
+ if err == nil && !c.NoCache && !c.DryRun && c.cacheDir != "" {
+ c.writeCache(path, params, result)
+ }
+ return result, err
+}
+
+func (c *Client) ProbeGet(path string) (int, error) {
+ _, status, err := c.do("GET", path, nil, nil, nil)
+ return status, err
+}
+
+func (c *Client) cacheKey(path string, params map[string]string) string {
+ key := path
+ for k, v := range params {
+ key += k + "=" + v
+ }
+ h := sha256.Sum256([]byte(key))
+ return hex.EncodeToString(h[:8])
+}
+
+func (c *Client) readCache(path string, params map[string]string) (json.RawMessage, bool) {
+ cacheFile := filepath.Join(c.cacheDir, c.cacheKey(path, params)+".json")
+ info, err := os.Stat(cacheFile)
+ if err != nil || time.Since(info.ModTime()) > 5*time.Minute {
+ return nil, false
+ }
+ data, err := os.ReadFile(cacheFile)
+ if err != nil {
+ return nil, false
+ }
+ return json.RawMessage(data), true
+}
+
+func (c *Client) writeCache(path string, params map[string]string, data json.RawMessage) {
+ os.MkdirAll(c.cacheDir, 0o755)
+ cacheFile := filepath.Join(c.cacheDir, c.cacheKey(path, params)+".json")
+ os.WriteFile(cacheFile, []byte(data), 0o644)
+}
+
+func (c *Client) Post(path string, body any) (json.RawMessage, int, error) {
+ return c.do("POST", path, nil, body, nil)
+}
+
+func (c *Client) PostWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("POST", path, nil, body, headers)
+}
+
+func (c *Client) Delete(path string) (json.RawMessage, int, error) {
+ return c.do("DELETE", path, nil, nil, nil)
+}
+
+func (c *Client) DeleteWithHeaders(path string, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("DELETE", path, nil, nil, headers)
+}
+
+func (c *Client) Put(path string, body any) (json.RawMessage, int, error) {
+ return c.do("PUT", path, nil, body, nil)
+}
+
+func (c *Client) PutWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("PUT", path, nil, body, headers)
+}
+
+func (c *Client) Patch(path string, body any) (json.RawMessage, int, error) {
+ return c.do("PATCH", path, nil, body, nil)
+}
+
+func (c *Client) PatchWithHeaders(path string, body any, headers map[string]string) (json.RawMessage, int, error) {
+ return c.do("PATCH", path, nil, body, headers)
+}
+
+// do executes an HTTP request. headerOverrides, when non-nil, override global
+// RequiredHeaders for this specific request (used for per-endpoint API versioning).
+func (c *Client) do(method, path string, params map[string]string, body any, headerOverrides map[string]string) (json.RawMessage, int, error) {
+ targetURL := c.BaseURL + path
+
+ var bodyBytes []byte
+ if body != nil {
+ b, err := json.Marshal(body)
+ if err != nil {
+ return nil, 0, fmt.Errorf("marshaling body: %w", err)
+ }
+ bodyBytes = b
+ }
+
+ // Resolve auth material before the dry-run branch so --dry-run can preview
+ // exactly what would be sent. Uses only cached credentials; a token that
+ // requires a network refresh will be re-fetched on the live request path,
+ // not during dry-run.
+ authHeader, err := c.authHeader()
+ if err != nil {
+ return nil, 0, err
+ }
+
+ // Build the request for dry-run display or actual execution
+ if c.DryRun {
+ return c.dryRun(method, targetURL, path, params, bodyBytes, headerOverrides, authHeader)
+ }
+
+ const maxRetries = 3
+ var lastErr error
+
+ for attempt := 0; attempt <= maxRetries; attempt++ {
+ // Proactive rate limiting — wait before sending
+ c.limiter.Wait()
+ var bodyReader io.Reader
+ if bodyBytes != nil {
+ bodyReader = strings.NewReader(string(bodyBytes))
+ }
+
+ req, err := http.NewRequest(method, targetURL, bodyReader)
+ if err != nil {
+ return nil, 0, fmt.Errorf("creating request: %w", err)
+ }
+ if bodyBytes != nil {
+ req.Header.Set("Content-Type", "application/json")
+ }
+
+ if params != nil {
+ q := req.URL.Query()
+ for k, v := range params {
+ if v != "" {
+ q.Set(k, v)
+ }
+ }
+ req.URL.RawQuery = q.Encode()
+ }
+
+ if authHeader != "" {
+ req.Header.Set("X-API-Key", authHeader)
+ }
+ req.Header.Set("X-Api-Version", "2026-04-01")
+ // Per-endpoint header overrides (e.g., different API version per resource)
+ for k, v := range headerOverrides {
+ req.Header.Set(k, v)
+ }
+ req.Header.Set("User-Agent", "printing-press-golden-pp-cli/2026.04")
+
+ resp, err := c.HTTPClient.Do(req)
+ if err != nil {
+ lastErr = fmt.Errorf("%s %s: %w", method, path, err)
+ continue
+ }
+
+ respBody, err := io.ReadAll(resp.Body)
+ resp.Body.Close()
+ if err != nil {
+ return nil, 0, fmt.Errorf("reading response: %w", err)
+ }
+ respBody = sanitizeJSONResponse(respBody)
+
+ // Success
+ if resp.StatusCode < 400 {
+ c.limiter.OnSuccess()
+ return json.RawMessage(respBody), resp.StatusCode, nil
+ }
+
+ apiErr := &APIError{
+ Method: method,
+ Path: path,
+ StatusCode: resp.StatusCode,
+ Body: truncateBody(respBody),
+ }
+
+ // Rate limited - adjust adaptive limiter and retry
+ if resp.StatusCode == 429 && attempt < maxRetries {
+ c.limiter.OnRateLimit()
+ wait := retryAfter(resp)
+ fmt.Fprintf(os.Stderr, "rate limited, waiting %s (attempt %d/%d, rate adjusted to %.1f req/s)\n", wait, attempt+1, maxRetries, c.limiter.Rate())
+ time.Sleep(wait)
+ lastErr = apiErr
+ continue
+ }
+
+ // Server error - retry with backoff
+ if resp.StatusCode >= 500 && attempt < maxRetries {
+ wait := time.Duration(math.Pow(2, float64(attempt))) * time.Second
+ fmt.Fprintf(os.Stderr, "server error %d, retrying in %s (attempt %d/%d)\n", resp.StatusCode, wait, attempt+1, maxRetries)
+ time.Sleep(wait)
+ lastErr = apiErr
+ continue
+ }
+
+ // Client error or retries exhausted - return the error
+ return nil, resp.StatusCode, apiErr
+ }
+
+ return nil, 0, lastErr
+}
+
+// dryRun prints the outgoing request exactly as the live path would send it,
+// using the auth material already resolved in `do()`. Never triggers a network
+// call — the caller is responsible for passing cached auth material only.
+func (c *Client) dryRun(method, targetURL, path string, params map[string]string, body []byte, headerOverrides map[string]string, authHeader string) (json.RawMessage, int, error) {
+ fmt.Fprintf(os.Stderr, "%s %s\n", method, targetURL)
+ if params != nil {
+ for k, v := range params {
+ if v != "" {
+ fmt.Fprintf(os.Stderr, " ?%s=%s\n", k, v)
+ }
+ }
+ }
+ if body != nil {
+ var pretty json.RawMessage
+ if json.Unmarshal(body, &pretty) == nil {
+ enc := json.NewEncoder(os.Stderr)
+ enc.SetIndent(" ", " ")
+ fmt.Fprintf(os.Stderr, " Body:\n")
+ enc.Encode(pretty)
+ }
+ }
+ if authHeader != "" {
+ fmt.Fprintf(os.Stderr, " %s: %s\n", "X-API-Key", maskToken(authHeader))
+ }
+ fmt.Fprintf(os.Stderr, "\n(dry run - no request sent)\n")
+ return json.RawMessage(`{"dry_run": true}`), 0, nil
+}
+
+func (c *Client) ConfiguredTimeout() time.Duration {
+ if c.HTTPClient != nil && c.HTTPClient.Timeout > 0 {
+ return c.HTTPClient.Timeout
+ }
+ return 30 * time.Second
+}
+
+func (c *Client) authHeader() (string, error) {
+ if c.Config == nil {
+ return "", nil
+ }
+ if c.Config.AccessToken != "" && !c.Config.TokenExpiry.IsZero() && time.Now().After(c.Config.TokenExpiry) && c.Config.RefreshToken != "" {
+ if err := c.refreshAccessToken(); err != nil {
+ return "", err
+ }
+ }
+ return c.Config.AuthHeader(), nil
+}
+
+func (c *Client) refreshAccessToken() error {
+ if c.Config == nil {
+ return nil
+ }
+ if c.Config.RefreshToken == "" {
+ return nil
+ }
+
+ tokenURL := ""
+ if tokenURL == "" {
+ return nil
+ }
+
+ params := url.Values{
+ "grant_type": {"refresh_token"},
+ "refresh_token": {c.Config.RefreshToken},
+ "client_id": {c.Config.ClientID},
+ }
+ if c.Config.ClientSecret != "" {
+ params.Set("client_secret", c.Config.ClientSecret)
+ }
+
+ resp, err := c.HTTPClient.PostForm(tokenURL, params)
+ if err != nil {
+ return fmt.Errorf("refreshing access token: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= 400 {
+ body, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("refreshing access token: HTTP %d: %s", resp.StatusCode, truncateBody(body))
+ }
+
+ var tokenResp struct {
+ AccessToken string `json:"access_token"`
+ RefreshToken string `json:"refresh_token"`
+ ExpiresIn int `json:"expires_in"`
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
+ return fmt.Errorf("parsing refresh response: %w", err)
+ }
+ if tokenResp.AccessToken == "" {
+ return fmt.Errorf("refreshing access token: no access token in response")
+ }
+
+ refreshToken := c.Config.RefreshToken
+ if tokenResp.RefreshToken != "" {
+ refreshToken = tokenResp.RefreshToken
+ }
+
+ expiry := time.Time{}
+ if tokenResp.ExpiresIn > 0 {
+ expiry = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
+ }
+
+ if err := c.Config.SaveTokens(c.Config.ClientID, c.Config.ClientSecret, tokenResp.AccessToken, refreshToken, expiry); err != nil {
+ return fmt.Errorf("saving refreshed token: %w", err)
+ }
+
+ return nil
+}
+
+const maxRetryWait = 60 * time.Second
+
+func retryAfter(resp *http.Response) time.Duration {
+ header := resp.Header.Get("Retry-After")
+ if header == "" {
+ return 5 * time.Second
+ }
+ if seconds, err := strconv.Atoi(header); err == nil {
+ d := time.Duration(seconds) * time.Second
+ if d > maxRetryWait {
+ return maxRetryWait
+ }
+ return d
+ }
+ if t, err := http.ParseTime(header); err == nil {
+ wait := time.Until(t)
+ if wait > maxRetryWait {
+ return maxRetryWait
+ }
+ if wait > 0 {
+ return wait
+ }
+ }
+ return 5 * time.Second
+}
+
+// sanitizeJSONResponse strips known JSONP/XSSI prefixes and UTF-8 BOM from
+// response bodies so that downstream JSON parsing succeeds. For clean JSON
+// responses these checks are no-ops.
+func sanitizeJSONResponse(body []byte) []byte {
+ // UTF-8 BOM
+ body = bytes.TrimPrefix(body, []byte("\xEF\xBB\xBF"))
+
+ // JSONP/XSSI prefixes, ordered longest-first where prefixes overlap
+ prefixes := [][]byte{
+ []byte(")]}'\n"),
+ []byte(")]}'"),
+ []byte("{}&&"),
+ []byte("for(;;);"),
+ []byte("while(1);"),
+ }
+ for _, p := range prefixes {
+ if bytes.HasPrefix(body, p) {
+ body = bytes.TrimPrefix(body, p)
+ body = bytes.TrimLeft(body, " \t\r\n")
+ break
+ }
+ }
+ return body
+}
+
+
+// maskToken redacts all but the last 4 characters of a token for safe display.
+func maskToken(token string) string {
+ if token == "" {
+ return ""
+ }
+ if len(token) <= 4 {
+ return "****"
+ }
+ return "****" + token[len(token)-4:]
+}
+
+func truncateBody(b []byte) string {
+ s := string(b)
+ if len(s) > 200 {
+ return s[:200] + "..."
+ }
+ return s
+}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/config/config.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/config/config.go
new file mode 100644
index 00000000..612e5921
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/config/config.go
@@ -0,0 +1,122 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package config
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/pelletier/go-toml/v2"
+)
+
+type Config struct {
+ BaseURL string `toml:"base_url"`
+ AuthHeaderVal string `toml:"auth_header"`
+ AuthSource string `toml:"-"`
+ AccessToken string `toml:"access_token"`
+ RefreshToken string `toml:"refresh_token"`
+ TokenExpiry time.Time `toml:"token_expiry"`
+ ClientID string `toml:"client_id"`
+ ClientSecret string `toml:"client_secret"`
+ Path string `toml:"-"`
+ PrintingPressGoldenApiKeyAuth string `toml:"press_golden_api_key_auth"`
+}
+
+func Load(configPath string) (*Config, error) {
+ cfg := &Config{
+ BaseURL: "https://api.golden.example/v1",
+ }
+
+ // Resolve config path
+ path := configPath
+ if path == "" {
+ path = os.Getenv("PRINTING_PRESS_GOLDEN_CONFIG")
+ }
+ if path == "" {
+ home, _ := os.UserHomeDir()
+ path = filepath.Join(home, ".config", "printing-press-golden-pp-cli", "config.toml")
+ }
+ cfg.Path = path
+
+ // Try to load config file
+ data, err := os.ReadFile(path)
+ if err == nil {
+ if err := toml.Unmarshal(data, cfg); err != nil {
+ return nil, fmt.Errorf("parsing config %s: %w", path, err)
+ }
+ }
+
+ // Env var overrides
+ if v := os.Getenv("PRINTING_PRESS_GOLDEN_API_KEY_AUTH"); v != "" {
+ cfg.PrintingPressGoldenApiKeyAuth = v
+ cfg.AuthSource = "env:PRINTING_PRESS_GOLDEN_API_KEY_AUTH"
+ }
+
+ // Base URL override (used by printing-press verify to point at mock/test servers)
+ if v := os.Getenv("PRINTING_PRESS_GOLDEN_BASE_URL"); v != "" {
+ cfg.BaseURL = v
+ }
+ return cfg, nil
+}
+
+func (c *Config) AuthHeader() string {
+ if c.AuthHeaderVal != "" {
+ return c.AuthHeaderVal
+ }
+ token := c.PrintingPressGoldenApiKeyAuth
+ if token == "" {
+ return ""
+ }
+ if c.PrintingPressGoldenApiKeyAuth == "" {
+ return ""
+ }
+ return token
+}
+
+func applyAuthFormat(format string, replacements map[string]string) string {
+ if format == "" {
+ return ""
+ }
+ for key, value := range replacements {
+ format = strings.ReplaceAll(format, "{"+key+"}", value)
+ }
+ if strings.Contains(format, "{") {
+ return ""
+ }
+ return format
+}
+
+func (c *Config) SaveTokens(clientID, clientSecret, accessToken, refreshToken string, expiry time.Time) error {
+ c.ClientID = clientID
+ c.ClientSecret = clientSecret
+ c.AccessToken = accessToken
+ c.RefreshToken = refreshToken
+ c.TokenExpiry = expiry
+ return c.save()
+}
+
+func (c *Config) ClearTokens() error {
+ c.AccessToken = ""
+ c.RefreshToken = ""
+ c.TokenExpiry = time.Time{}
+ return c.save()
+}
+
+func (c *Config) save() error {
+ dir := filepath.Dir(c.Path)
+ if err := os.MkdirAll(dir, 0o700); err != nil {
+ return fmt.Errorf("creating config dir: %w", err)
+ }
+ data, err := toml.Marshal(c)
+ if err != nil {
+ return fmt.Errorf("marshaling config: %w", err)
+ }
+ return os.WriteFile(c.Path, data, 0o600)
+}
+
+// Ensure strings import is used
+var _ = strings.ReplaceAll
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/mcp/tools.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/mcp/tools.go
new file mode 100644
index 00000000..8346b0dd
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/mcp/tools.go
@@ -0,0 +1,344 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package mcp
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "time"
+
+ mcplib "github.com/mark3labs/mcp-go/mcp"
+ "github.com/mark3labs/mcp-go/server"
+ "printing-press-golden-pp-cli/internal/client"
+ "printing-press-golden-pp-cli/internal/config"
+ "printing-press-golden-pp-cli/internal/store"
+)
+// looksLikeAuthError checks if an error message body contains auth-related keywords.
+func looksLikeAuthError(msg string) bool {
+ lower := strings.ToLower(msg)
+ patterns := []string{
+ `\bkey\b`,
+ `\btoken\b`,
+ `\bunauthorized\b`,
+ `\bapi_key\b`,
+ `missing.{0,20}key`,
+ `required.{0,20}key`,
+ `\bforbidden\b`,
+ `\bauthenticat`,
+ `\bcredential`,
+ }
+ for _, p := range patterns {
+ if matched, _ := regexp.MatchString(p, lower); matched {
+ return true
+ }
+ }
+ return false
+}
+
+// sanitizeErrorBody truncates and strips credential-shaped strings from error output.
+func sanitizeErrorBody(msg string) string {
+ if len(msg) > 200 {
+ msg = msg[:200] + "..."
+ }
+ credPatterns := regexp.MustCompile(`(?i)(sk-[a-zA-Z0-9]{8,}|sk_live_[a-zA-Z0-9]+|Bearer\s+[a-zA-Z0-9._\-]+|key=[a-zA-Z0-9._\-]+)`)
+ msg = credPatterns.ReplaceAllString(msg, "[REDACTED]")
+ return msg
+}
+
+// RegisterTools registers all API operations as MCP tools.
+func RegisterTools(s *server.MCPServer) {
+ s.AddTool(
+ mcplib.NewTool("projects_create",
+ mcplib.WithDescription("Create project Returns Project."),
+ ),
+ makeAPIHandler("POST", "/projects", []string{ }),
+ )
+ s.AddTool(
+ mcplib.NewTool("projects_get",
+ mcplib.WithDescription("Get project"),
+ mcplib.WithString("projectId", mcplib.Required(), mcplib.Description("Project id")),
+ ),
+ makeAPIHandler("GET", "/projects/{projectId}", []string{"projectId", }),
+ )
+ s.AddTool(
+ mcplib.NewTool("projects_list",
+ mcplib.WithDescription("List projects Returns array of Project."),
+ mcplib.WithString("status", mcplib.Description("Status")),
+ mcplib.WithString("limit", mcplib.Description("Limit")),
+ mcplib.WithString("cursor", mcplib.Description("Cursor")),
+ ),
+ makeAPIHandler("GET", "/projects", []string{ }),
+ )
+ s.AddTool(
+ mcplib.NewTool("projects_tasks_list-project",
+ mcplib.WithDescription("List project tasks Returns array of Task."),
+ mcplib.WithString("projectId", mcplib.Required(), mcplib.Description("Project id")),
+ mcplib.WithString("priority", mcplib.Description("Priority")),
+ mcplib.WithString("limit", mcplib.Description("Limit")),
+ mcplib.WithString("cursor", mcplib.Description("Cursor")),
+ ),
+ makeAPIHandler("GET", "/projects/{projectId}/tasks", []string{"projectId", }),
+ )
+ s.AddTool(
+ mcplib.NewTool("projects_tasks_update-project",
+ mcplib.WithDescription("Update project task Partial update."),
+ mcplib.WithString("projectId", mcplib.Required(), mcplib.Description("Project id")),
+ mcplib.WithString("taskId", mcplib.Required(), mcplib.Description("Task id")),
+ ),
+ makeAPIHandler("PATCH", "/projects/{projectId}/tasks/{taskId}", []string{"projectId","taskId", }),
+ )
+ s.AddTool(
+ mcplib.NewTool("public_get-status",
+ mcplib.WithDescription("Get public service status (public)"),
+ ),
+ makeAPIHandler("GET", "/public/status", []string{ }),
+ )
+ // Sync tool — populates local database for offline search and sql queries
+ s.AddTool(
+ mcplib.NewTool("sync",
+ mcplib.WithDescription("Sync API data to local SQLite database. Run this before using search or sql tools. Supports incremental sync."),
+ mcplib.WithString("resources", mcplib.Description("Comma-separated resource types to sync (omit for all)")),
+ mcplib.WithString("since", mcplib.Description("Incremental sync since duration (e.g. 7d, 24h, 1w)")),
+ mcplib.WithBoolean("full", mcplib.Description("Full resync ignoring checkpoints")),
+ ),
+ handleSync,
+ )
+ // SQL tool — ad-hoc analysis on synced data without API calls
+ s.AddTool(
+ mcplib.NewTool("sql",
+ mcplib.WithDescription("Run read-only SQL against local database. Use for ad-hoc analysis, aggregations, and joins across synced resources. Requires sync first."),
+ mcplib.WithString("query", mcplib.Required(), mcplib.Description("SQL query (SELECT only). Tables match resource names.")),
+ ),
+ handleSQL,
+ )
+
+ // Context tool — front-loaded domain knowledge for agents.
+ // Call this first to understand the API taxonomy, query patterns, and capabilities.
+ s.AddTool(
+ mcplib.NewTool("context",
+ mcplib.WithDescription("Get API domain context: resource taxonomy, auth requirements, query tips, and unique capabilities. Call this first."),
+ ),
+ handleContext,
+ )
+}
+
+// makeAPIHandler creates a generic MCP tool handler for an API endpoint.
+func makeAPIHandler(method, pathTemplate string, positionalParams []string) server.ToolHandlerFunc {
+ return func(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
+ c, err := newMCPClient()
+ if err != nil {
+ return mcplib.NewToolResultError(err.Error()), nil
+ }
+
+ // mcp-go v0.47+ made CallToolParams.Arguments an `any` to support
+ // non-map payloads; GetArguments() returns the map[string]any shape
+ // we rely on here (or an empty map when the payload is something else).
+ args := req.GetArguments()
+
+ // Build path by substituting positional params
+ path := pathTemplate
+ for _, p := range positionalParams {
+ if v, ok := args[p]; ok {
+ path = strings.Replace(path, "{"+p+"}", fmt.Sprintf("%v", v), 1)
+ }
+ }
+
+ // Collect non-positional params as query params
+ params := make(map[string]string)
+ for k, v := range args {
+ isPositional := false
+ for _, p := range positionalParams {
+ if k == p {
+ isPositional = true
+ break
+ }
+ }
+ if !isPositional {
+ params[k] = fmt.Sprintf("%v", v)
+ }
+ }
+
+ var data json.RawMessage
+ switch method {
+ case "GET":
+ data, err = c.Get(path, params)
+ case "POST":
+ body, _ := json.Marshal(args)
+ data, _, err = c.Post(path, body)
+ case "PUT":
+ body, _ := json.Marshal(args)
+ data, _, err = c.Put(path, body)
+ case "PATCH":
+ body, _ := json.Marshal(args)
+ data, _, err = c.Patch(path, body)
+ case "DELETE":
+ data, _, err = c.Delete(path)
+ default:
+ return mcplib.NewToolResultError("unsupported method: " + method), nil
+ }
+
+ if err != nil {
+ msg := err.Error()
+ switch {
+ case strings.Contains(msg, "HTTP 409"):
+ return mcplib.NewToolResultText("already exists (no-op)"), nil
+ case strings.Contains(msg, "HTTP 400") && looksLikeAuthError(msg):
+ return mcplib.NewToolResultError("authentication error: " + sanitizeErrorBody(msg) +
+ "\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_AUTH=<your-key>" +
+ "\n Run 'printing-press-golden-pp-cli doctor' to check auth status."), nil
+ case strings.Contains(msg, "HTTP 401"):
+ return mcplib.NewToolResultError("authentication failed: " + sanitizeErrorBody(msg) +
+ "\nhint: check your API key." +
+ "\n Set it with: export PRINTING_PRESS_GOLDEN_API_KEY_AUTH=<your-key>" +
+ "\n Run 'printing-press-golden-pp-cli doctor' to check auth status."), nil
+ case strings.Contains(msg, "HTTP 403"):
+ return mcplib.NewToolResultError("permission denied: " + sanitizeErrorBody(msg) +
+ "\nhint: your credentials are valid but lack access to this resource." +
+ "\n Set it with: export PRINTING_PRESS_GOLDEN_API_KEY_AUTH=<your-key>" +
+ "\n Run 'printing-press-golden-pp-cli doctor' to check auth status."), nil
+ case strings.Contains(msg, "HTTP 404"):
+ if method == "DELETE" {
+ return mcplib.NewToolResultText("already deleted (no-op)"), nil
+ }
+ return mcplib.NewToolResultError("not found: " + msg), nil
+ case strings.Contains(msg, "HTTP 429"):
+ return mcplib.NewToolResultError("rate limited: " + msg), nil
+ default:
+ return mcplib.NewToolResultError(msg), nil
+ }
+ }
+
+ // For GET responses, wrap bare arrays with count metadata
+ if method == "GET" {
+ trimmed := strings.TrimSpace(string(data))
+ if len(trimmed) > 0 && trimmed[0] == '[' {
+ var items []json.RawMessage
+ if json.Unmarshal(data, &items) == nil {
+ wrapped := map[string]any{
+ "count": len(items),
+ "items": items,
+ }
+ out, _ := json.Marshal(wrapped)
+ return mcplib.NewToolResultText(string(out)), nil
+ }
+ }
+ }
+ return mcplib.NewToolResultText(string(data)), nil
+ }
+}
+
+func newMCPClient() (*client.Client, error) {
+ home, _ := os.UserHomeDir()
+ cfgPath := filepath.Join(home, ".config", "printing-press-golden-pp-cli", "config.toml")
+ cfg, err := config.Load(cfgPath)
+ if err != nil {
+ return nil, fmt.Errorf("loading config: %w", err)
+ }
+ return client.New(cfg, 30*time.Second, 0), nil
+}
+
+func dbPath() string {
+ home, _ := os.UserHomeDir()
+ return filepath.Join(home, ".local", "share", "printing-press-golden-pp-cli", "data.db")
+}
+// Note: MCP tools use their own dbPath() because they are in a separate package (main, not cli).
+// The CLI's defaultDBPath() in the cli package uses the same canonical path.
+
+func handleSync(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
+ return mcplib.NewToolResultText("sync not yet implemented via MCP - use the CLI: printing-press-golden-pp-cli sync"), nil
+}
+
+func handleSQL(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
+ args := req.GetArguments()
+ query, ok := args["query"].(string)
+ if !ok || query == "" {
+ return mcplib.NewToolResultError("query is required"), nil
+ }
+
+ // Block write operations
+ upper := strings.ToUpper(strings.TrimSpace(query))
+ for _, prefix := range []string{"INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "CREATE"} {
+ if strings.HasPrefix(upper, prefix) {
+ return mcplib.NewToolResultError("only SELECT queries are allowed"), nil
+ }
+ }
+
+ db, err := store.Open(dbPath())
+ if err != nil {
+ return mcplib.NewToolResultError(fmt.Sprintf("opening database: %v", err)), nil
+ }
+ defer db.Close()
+
+ rows, err := db.Query(query)
+ if err != nil {
+ return mcplib.NewToolResultError(fmt.Sprintf("query failed: %v", err)), nil
+ }
+ defer rows.Close()
+
+ cols, _ := rows.Columns()
+ var results []map[string]any
+ for rows.Next() {
+ values := make([]any, len(cols))
+ ptrs := make([]any, len(cols))
+ for i := range values {
+ ptrs[i] = &values[i]
+ }
+ rows.Scan(ptrs...)
+ row := make(map[string]any)
+ for i, col := range cols {
+ row[col] = values[i]
+ }
+ results = append(results, row)
+ }
+
+ data, _ := json.MarshalIndent(results, "", " ")
+ return mcplib.NewToolResultText(string(data)), nil
+}
+
+func handleContext(_ context.Context, _ mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
+ ctx := map[string]any{
+ "api": "printing-press-golden",
+ "description": "Purpose-built fixture for golden generation coverage.",
+ "archetype": "project-management",
+ "tool_count": 6,
+ "auth": map[string]any{
+ "type": "api_key",
+ "env_vars": []string{"PRINTING_PRESS_GOLDEN_API_KEY_AUTH", },
+ },
+ "resources": []map[string]any{
+ {
+ "name": "projects",
+ "description": "Manage projects",
+ "endpoints": []string{"create", "get", "list", },
+ "syncable": true,
+ "searchable": true,
+ },
+ {
+ "name": "public",
+ "description": "Manage public",
+ "endpoints": []string{"get-status", },
+ },
+ },
+ "query_tips": []string{
+ "Pagination uses cursor-based paging. Pass cursor parameter for subsequent pages.",
+ "Control page size with the limit parameter (default 100).",
+ "Use the sql tool for ad-hoc analysis on synced data. Run sync first to populate the local database.",
+ "Use the search tool for full-text search across all synced resources. Faster than iterating list endpoints.",
+ "Prefer sql/search over repeated API calls when the data is already synced.",
+ },
+ "playbook": []map[string]string{
+ {"topic": "Finding stale work", "insight": "Use the stale command or sql query to find items not updated recently. More reliable than scanning list results manually."},
+ {"topic": "Load analysis", "insight": "When analyzing team workload, filter by assignee and status. Raw counts without status filtering are misleading."},
+ {"topic": "Bulk operations", "insight": "For bulk status changes, prefer update endpoints over delete+create. Most PM APIs track history on updates."},
+ },
+ }
+ data, _ := json.MarshalIndent(ctx, "", " ")
+ return mcplib.NewToolResultText(string(data)), nil
+}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/types/types.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/types/types.go
new file mode 100644
index 00000000..9f57cd46
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/types/types.go
@@ -0,0 +1,33 @@
+// Copyright 2026 printing-press-golden. Licensed under Apache-2.0. See LICENSE.
+// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+
+package types
+
+
+type CreateProjectRequest struct {
+ Name string `json:"name"`
+ OwnerEmail string `json:"owner_email"`
+ Visibility string `json:"visibility"`
+}
+
+type Project struct {
+ Id string `json:"id"`
+ Name string `json:"name"`
+ Status string `json:"status"`
+ Visibility string `json:"visibility"`
+}
+
+type Task struct {
+ DueAt string `json:"due_at"`
+ Id string `json:"id"`
+ Priority string `json:"priority"`
+ ProjectId string `json:"project_id"`
+ Title string `json:"title"`
+}
+
+type UpdateTaskRequest struct {
+ Completed bool `json:"completed"`
+ Priority string `json:"priority"`
+ Title string `json:"title"`
+}
+
diff --git a/testdata/golden/expected/generate-golden-api/scorecard.json b/testdata/golden/expected/generate-golden-api/scorecard.json
new file mode 100644
index 00000000..2a63d525
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/scorecard.json
@@ -0,0 +1,48 @@
+{
+ "scorecard": {
+ "api_name": "printing-press-golden",
+ "competitor_scores": null,
+ "gap_report": [
+ "breadth scored 4/10 - needs improvement",
+ "MCP: 6 tools (1 public, 5 auth-required) — readiness: full"
+ ],
+ "overall_grade": "A",
+ "steinberger": {
+ "agent_native": 10,
+ "agent_workflow_readiness": 9,
+ "auth": 10,
+ "auth_protocol": 0,
+ "breadth": 4,
+ "cache_freshness": 5,
+ "data_pipeline_integrity": 7,
+ "dead_code": 4,
+ "doctor": 10,
+ "error_handling": 10,
+ "insight": 8,
+ "live_api_verification": 0,
+ "local_cache": 10,
+ "mcp_quality": 10,
+ "mcp_remote_transport": 5,
+ "mcp_surface_strategy": 0,
+ "mcp_token_efficiency": 7,
+ "mcp_tool_design": 0,
+ "output_modes": 10,
+ "path_validity": 0,
+ "percentage": 81,
+ "readme": 8,
+ "sync_correctness": 10,
+ "terminal_ux": 8,
+ "total": 81,
+ "type_fidelity": 3,
+ "vision": 7,
+ "workflows": 10
+ },
+ "unscored_dimensions": [
+ "mcp_tool_design",
+ "mcp_surface_strategy",
+ "path_validity",
+ "auth_protocol",
+ "live_api_verification"
+ ]
+ }
+}
diff --git a/testdata/golden/expected/generate-golden-api/stderr.txt b/testdata/golden/expected/generate-golden-api/stderr.txt
new file mode 100644
index 00000000..5c2fa18c
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/stderr.txt
@@ -0,0 +1 @@
+Generated printing-press-golden at <ARTIFACT_DIR>/generate-golden-api/printing-press-golden
diff --git a/testdata/golden/expected/generate-golden-api/stdout.txt b/testdata/golden/expected/generate-golden-api/stdout.txt
new file mode 100644
index 00000000..e69de29b
diff --git a/testdata/golden/fixtures/golden-api.yaml b/testdata/golden/fixtures/golden-api.yaml
new file mode 100644
index 00000000..79864a06
--- /dev/null
+++ b/testdata/golden/fixtures/golden-api.yaml
@@ -0,0 +1,224 @@
+openapi: "3.0.3"
+info:
+ title: Printing Press Golden API
+ version: "2026.04"
+ description: Purpose-built fixture for golden generation coverage.
+servers:
+ - url: https://api.golden.example/v1
+security:
+ - ApiKeyAuth: []
+components:
+ securitySchemes:
+ ApiKeyAuth:
+ type: apiKey
+ in: header
+ name: X-API-Key
+ parameters:
+ ApiVersion:
+ name: X-Api-Version
+ in: header
+ required: true
+ schema:
+ type: string
+ default: "2026-04-01"
+ description: Required API version header.
+ RequestID:
+ name: X-Request-Id
+ in: header
+ required: false
+ schema:
+ type: string
+ description: Optional per-request correlation ID.
+ schemas:
+ Project:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ status:
+ type: string
+ enum: [draft, active, archived]
+ visibility:
+ type: string
+ enum: [private, team, public]
+ Task:
+ type: object
+ properties:
+ id:
+ type: string
+ project_id:
+ type: string
+ title:
+ type: string
+ priority:
+ type: string
+ enum: [low, normal, high]
+ due_at:
+ type: string
+ format: date-time
+ CreateProjectRequest:
+ type: object
+ required: [name, visibility]
+ properties:
+ name:
+ type: string
+ visibility:
+ type: string
+ enum: [private, team, public]
+ owner_email:
+ type: string
+ format: email
+ UpdateTaskRequest:
+ type: object
+ properties:
+ title:
+ type: string
+ priority:
+ type: string
+ enum: [low, normal, high]
+ completed:
+ type: boolean
+paths:
+ /public/status:
+ get:
+ tags: [status]
+ operationId: getPublicStatus
+ summary: Get public service status
+ security: []
+ responses:
+ "200":
+ description: OK
+ /projects:
+ get:
+ tags: [projects]
+ operationId: listProjects
+ summary: List projects
+ parameters:
+ - $ref: "#/components/parameters/ApiVersion"
+ - name: status
+ in: query
+ required: false
+ schema:
+ type: string
+ enum: [draft, active, archived]
+ - name: limit
+ in: query
+ required: false
+ schema:
+ type: integer
+ default: 25
+ - name: cursor
+ in: query
+ required: false
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Project"
+ post:
+ tags: [projects]
+ operationId: createProject
+ summary: Create project
+ parameters:
+ - $ref: "#/components/parameters/ApiVersion"
+ - $ref: "#/components/parameters/RequestID"
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/CreateProjectRequest"
+ responses:
+ "201":
+ description: Created
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Project"
+ /projects/{projectId}:
+ get:
+ tags: [projects]
+ operationId: getProject
+ summary: Get project
+ parameters:
+ - $ref: "#/components/parameters/ApiVersion"
+ - name: projectId
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ /projects/{projectId}/tasks:
+ get:
+ tags: [tasks]
+ operationId: listProjectTasks
+ summary: List project tasks
+ parameters:
+ - $ref: "#/components/parameters/ApiVersion"
+ - name: projectId
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: priority
+ in: query
+ required: false
+ schema:
+ type: string
+ enum: [low, normal, high]
+ - name: limit
+ in: query
+ required: false
+ schema:
+ type: integer
+ default: 50
+ - name: cursor
+ in: query
+ required: false
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Task"
+ /projects/{projectId}/tasks/{taskId}:
+ patch:
+ tags: [tasks]
+ operationId: updateProjectTask
+ summary: Update project task
+ parameters:
+ - $ref: "#/components/parameters/ApiVersion"
+ - name: projectId
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: taskId
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/UpdateTaskRequest"
+ responses:
+ "200":
+ description: OK
← f33d8e55 fix(cli): harden store migrations for generated identifiers
·
back to Cli Printing Press
·
refactor(cli): simplify refactor-safe helpers (#311) fd2ea4f4 →