← back to Cli Printing Press
test(cli): add golden coverage for generated artifacts (#345)
8617e06dc9336edd2a7987eab2f3c20167259637 · 2026-04-27 10:54:13 -0700 · Trevin Chow
Files touched
M testdata/golden/cases/generate-golden-api/artifacts.txtA testdata/golden/cases/schema-traffic-analysis/artifacts.txtA testdata/golden/cases/schema-traffic-analysis/command.txtA testdata/golden/expected/generate-golden-api/printing-press-golden/README.mdA testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.mdA testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/doctor.goA testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cliutil/verifyenv.goA testdata/golden/expected/schema-traffic-analysis/exit.txtA testdata/golden/expected/schema-traffic-analysis/stderr.txtA testdata/golden/expected/schema-traffic-analysis/stdout.txt
Diff
commit 8617e06dc9336edd2a7987eab2f3c20167259637
Author: Trevin Chow <trevin@trevinchow.com>
Date: Mon Apr 27 10:54:13 2026 -0700
test(cli): add golden coverage for generated artifacts (#345)
---
.../golden/cases/generate-golden-api/artifacts.txt | 4 +
.../cases/schema-traffic-analysis/artifacts.txt | 1 +
.../cases/schema-traffic-analysis/command.txt | 1 +
.../printing-press-golden/README.md | 155 +++++++
.../printing-press-golden/SKILL.md | 166 ++++++++
.../printing-press-golden/internal/cli/doctor.go | 451 +++++++++++++++++++++
.../internal/cliutil/verifyenv.go | 30 ++
.../expected/schema-traffic-analysis/exit.txt | 1 +
.../expected/schema-traffic-analysis/stderr.txt | 0
.../expected/schema-traffic-analysis/stdout.txt | 185 +++++++++
10 files changed, 994 insertions(+)
diff --git a/testdata/golden/cases/generate-golden-api/artifacts.txt b/testdata/golden/cases/generate-golden-api/artifacts.txt
index bd427844..35129396 100644
--- a/testdata/golden/cases/generate-golden-api/artifacts.txt
+++ b/testdata/golden/cases/generate-golden-api/artifacts.txt
@@ -16,3 +16,7 @@ printing-press-golden/internal/mcp/tools.go
printing-press-golden/internal/types/types.go
dogfood.json
scorecard.json
+printing-press-golden/README.md
+printing-press-golden/SKILL.md
+printing-press-golden/internal/cli/doctor.go
+printing-press-golden/internal/cliutil/verifyenv.go
diff --git a/testdata/golden/cases/schema-traffic-analysis/artifacts.txt b/testdata/golden/cases/schema-traffic-analysis/artifacts.txt
new file mode 100644
index 00000000..a96ba8e2
--- /dev/null
+++ b/testdata/golden/cases/schema-traffic-analysis/artifacts.txt
@@ -0,0 +1 @@
+# no extra artifacts
diff --git a/testdata/golden/cases/schema-traffic-analysis/command.txt b/testdata/golden/cases/schema-traffic-analysis/command.txt
new file mode 100644
index 00000000..de9c131b
--- /dev/null
+++ b/testdata/golden/cases/schema-traffic-analysis/command.txt
@@ -0,0 +1 @@
+"$BINARY" schema traffic-analysis
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md b/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md
new file mode 100644
index 00000000..58992484
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/README.md
@@ -0,0 +1,155 @@
+# Printing Press Golden CLI
+
+Purpose-built fixture for golden generation coverage.
+
+## Install
+
+### Go
+
+```
+go install github.com/mvanhorn/printing-press-library/library/other/printing-press-golden-pp-cli/cmd/printing-press-golden-pp-cli@latest
+```
+
+### Binary
+
+Download from [Releases](https://github.com/mvanhorn/printing-press-library/releases).
+
+## Quick Start
+
+### 1. Install
+
+See [Install](#install) above.
+
+### 2. Set Up Credentials
+
+Get your API key from your API provider's developer portal. The key typically looks like a long alphanumeric string.
+
+```bash
+export PRINTING_PRESS_GOLDEN_API_KEY_AUTH="<paste-your-key>"
+```
+
+You can also persist this in your config file at `~/.config/printing-press-golden-pp-cli/config.toml`.
+
+### 3. Verify Setup
+
+```bash
+printing-press-golden-pp-cli doctor
+```
+
+This checks your configuration and credentials.
+
+### 4. Try Your First Command
+
+```bash
+printing-press-golden-pp-cli projects list
+```
+
+## Usage
+
+Run `printing-press-golden-pp-cli --help` for the full command reference and flag list.
+
+## Commands
+
+### projects
+
+Manage projects
+
+- **`printing-press-golden-pp-cli projects create`** - Create project
+- **`printing-press-golden-pp-cli projects get`** - Get project
+- **`printing-press-golden-pp-cli projects list`** - List projects
+
+### public
+
+Manage public
+
+- **`printing-press-golden-pp-cli public get-status`** - Get public service status
+
+
+## Output Formats
+
+```bash
+# Human-readable table (default in terminal, JSON when piped)
+printing-press-golden-pp-cli projects list
+
+# JSON for scripting and agents
+printing-press-golden-pp-cli projects list --json
+
+# Filter to specific fields
+printing-press-golden-pp-cli projects list --json --select id,name,status
+
+# Dry run — show the request without sending
+printing-press-golden-pp-cli projects list --dry-run
+
+# Agent mode — JSON + compact + no prompts in one flag
+printing-press-golden-pp-cli projects list --agent
+```
+
+## Agent Usage
+
+This CLI is designed for AI agent consumption:
+
+- **Non-interactive** - never prompts, every input is a flag
+- **Pipeable** - `--json` output to stdout, errors to stderr
+- **Filterable** - `--select id,name` returns only fields you need
+- **Previewable** - `--dry-run` shows the request without sending
+- **Retryable** - creates return "already exists" on retry, deletes return "already deleted"
+- **Confirmable** - `--yes` for explicit confirmation of destructive actions
+- **Piped input** - write commands can accept structured input when their help lists `--stdin`
+- **Offline-friendly** - sync/search commands can use the local SQLite store when available
+- **Agent-safe by default** - no colors or formatting unless `--human-friendly` is set
+
+Exit codes: `0` success, `2` usage error, `3` not found, `4` auth error, `5` API error, `7` rate limited, `10` config error.
+
+## Use as MCP Server
+
+This CLI ships a companion MCP server for use with Claude Desktop, Cursor, and other MCP-compatible tools.
+
+### Claude Code
+
+```bash
+claude mcp add printing-press-golden printing-press-golden-pp-mcp -e PRINTING_PRESS_GOLDEN_API_KEY_AUTH=<your-key>
+```
+
+### Claude Desktop
+
+Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
+
+```json
+{
+ "mcpServers": {
+ "printing-press-golden": {
+ "command": "printing-press-golden-pp-mcp",
+ "env": {
+ "PRINTING_PRESS_GOLDEN_API_KEY_AUTH": "<your-key>"
+ }
+ }
+ }
+}
+```
+
+## Health Check
+
+```bash
+printing-press-golden-pp-cli doctor
+```
+
+Verifies configuration, credentials, and connectivity to the API.
+
+## Configuration
+
+Config file: `~/.config/printing-press-golden-pp-cli/config.toml`
+
+Environment variables:
+- `PRINTING_PRESS_GOLDEN_API_KEY_AUTH`
+
+## Troubleshooting
+**Authentication errors (exit code 4)**
+- Run `printing-press-golden-pp-cli doctor` to check credentials
+- Verify the environment variable is set: `echo $PRINTING_PRESS_GOLDEN_API_KEY_AUTH`
+**Not found errors (exit code 3)**
+- Check the resource ID is correct
+- Run the `list` command to see available items
+
+---
+
+Generated by [CLI Printing Press](https://github.com/mvanhorn/cli-printing-press)
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md b/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md
new file mode 100644
index 00000000..f247cdc3
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md
@@ -0,0 +1,166 @@
+---
+name: pp-printing-press-golden
+description: "Printing Press CLI for Printing Press Golden. Purpose-built fixture for golden generation coverage."
+argument-hint: "<command> [args] | install cli|mcp"
+allowed-tools: "Read Bash"
+metadata: '{"openclaw":{"requires":{"bins":["printing-press-golden-pp-cli"]},"install":[{"id":"go","kind":"shell","command":"go install github.com/mvanhorn/printing-press-library/library/other/printing-press-golden-pp-cli/cmd/printing-press-golden-pp-cli@latest","bins":["printing-press-golden-pp-cli"],"label":"Install via go install"}]}}'
+---
+
+# Printing Press Golden — Printing Press CLI
+
+Purpose-built fixture for golden generation coverage.
+
+## Command Reference
+
+**projects** — Manage projects
+
+- `printing-press-golden-pp-cli projects create` — Create project
+- `printing-press-golden-pp-cli projects get` — Get project
+- `printing-press-golden-pp-cli projects list` — List projects
+
+**public** — Manage public
+
+- `printing-press-golden-pp-cli public` — Get public service status
+
+
+### Finding the right command
+
+When you know what you want to do but not which command does it, ask the CLI directly:
+
+```bash
+printing-press-golden-pp-cli which "<capability in your own words>"
+```
+
+`which` resolves a natural-language capability query to the best matching command from this CLI's curated feature index. Exit code `0` means at least one match; exit code `2` means no confident match — fall back to `--help` or use a narrower query.
+
+## Auth Setup
+
+Set your API key via environment variable:
+
+```bash
+export PRINTING_PRESS_GOLDEN_API_KEY_AUTH="<your-key>"
+```
+
+Or persist it in `~/.config/printing-press-golden-pp-cli/config.toml`.
+
+Run `printing-press-golden-pp-cli doctor` to verify setup.
+
+## Agent Mode
+
+Add `--agent` to any command. Expands to: `--json --compact --no-input --no-color --yes`.
+
+- **Pipeable** — JSON on stdout, errors on stderr
+- **Filterable** — `--select` keeps a subset of fields. Dotted paths descend into nested structures; arrays traverse element-wise. Critical for keeping context small on verbose APIs:
+
+ ```bash
+ printing-press-golden-pp-cli projects list --agent --select id,name,status
+ ```
+- **Previewable** — `--dry-run` shows the request without sending
+- **Offline-friendly** — sync/search commands can use the local SQLite store when available
+- **Non-interactive** — never prompts, every input is a flag
+
+### Response envelope
+
+Commands that read from the local store or the API wrap output in a provenance envelope:
+
+```json
+{
+ "meta": {"source": "live" | "local", "synced_at": "...", "reason": "..."},
+ "results": <data>
+}
+```
+
+Parse `.results` for data and `.meta.source` to know whether it's live or local. A human-readable `N results (live)` summary is printed to stderr only when stdout is a terminal — piped/agent consumers get pure JSON on stdout.
+
+## Agent Feedback
+
+When you (or the agent) notice something off about this CLI, record it:
+
+```
+printing-press-golden-pp-cli feedback "the --since flag is inclusive but docs say exclusive"
+printing-press-golden-pp-cli feedback --stdin < notes.txt
+printing-press-golden-pp-cli feedback list --json --limit 10
+```
+
+Entries are stored locally at `~/.printing-press-golden-pp-cli/feedback.jsonl`. They are never POSTed unless `PRINTING_PRESS_GOLDEN_FEEDBACK_ENDPOINT` is set AND either `--send` is passed or `PRINTING_PRESS_GOLDEN_FEEDBACK_AUTO_SEND=true`. Default behavior is local-only.
+
+Write what *surprised* you, not a bug report. Short, specific, one line: that is the part that compounds.
+
+## Output Delivery
+
+Every command accepts `--deliver <sink>`. The output goes to the named sink in addition to (or instead of) stdout, so agents can route command results without hand-piping. Three sinks are supported:
+
+| Sink | Effect |
+|------|--------|
+| `stdout` | Default; write to stdout only |
+| `file:<path>` | Atomically write output to `<path>` (tmp + rename) |
+| `webhook:<url>` | POST the output body to the URL (`application/json` or `application/x-ndjson` when `--compact`) |
+
+Unknown schemes are refused with a structured error naming the supported set. Webhook failures return non-zero and log the URL + HTTP status on stderr.
+
+## Named Profiles
+
+A profile is a saved set of flag values, reused across invocations. Use it when a scheduled agent calls the same command every run with the same configuration - HeyGen's "Beacon" pattern.
+
+```
+printing-press-golden-pp-cli profile save briefing --json
+printing-press-golden-pp-cli --profile briefing projects list
+printing-press-golden-pp-cli profile list --json
+printing-press-golden-pp-cli profile show briefing
+printing-press-golden-pp-cli profile delete briefing --yes
+```
+
+Explicit flags always win over profile values; profile values win over defaults. `agent-context` lists all available profiles under `available_profiles` so introspecting agents discover them at runtime.
+
+## Exit Codes
+
+| Code | Meaning |
+|------|---------|
+| 0 | Success |
+| 2 | Usage error (wrong arguments) |
+| 3 | Resource not found |
+| 4 | Authentication required |
+| 5 | API error (upstream issue) |
+| 7 | Rate limited (wait and retry) |
+| 10 | Config error |
+
+## Argument Parsing
+
+Parse `$ARGUMENTS`:
+
+1. **Empty, `help`, or `--help`** → show `printing-press-golden-pp-cli --help` output
+2. **Starts with `install`** → ends with `mcp` → MCP installation; otherwise → CLI installation
+3. **Anything else** → Direct Use (execute as CLI command with `--agent`)
+
+## CLI Installation
+
+1. Check Go is installed: `go version` (requires Go 1.23+)
+2. Install:
+ ```bash
+ go install github.com/mvanhorn/printing-press-library/library/other/printing-press-golden-pp-cli/cmd/printing-press-golden-pp-cli@latest
+ ```
+3. Verify: `printing-press-golden-pp-cli --version`
+4. Ensure `$GOPATH/bin` (or `$HOME/go/bin`) is on `$PATH`.
+
+## MCP Server Installation
+
+1. Install the MCP server:
+ ```bash
+ go install github.com/mvanhorn/printing-press-library/library/other/printing-press-golden-pp-cli/cmd/printing-press-golden-pp-mcp@latest
+ ```
+2. Register with Claude Code:
+ ```bash
+ claude mcp add printing-press-golden-pp-mcp -- printing-press-golden-pp-mcp
+ ```
+3. Verify: `claude mcp list`
+
+## Direct Use
+
+1. Check if installed: `which printing-press-golden-pp-cli`
+ If not found, offer to install (see CLI Installation above).
+2. Match the user query to the best command from the Unique Capabilities and Command Reference above.
+3. Execute with the `--agent` flag:
+ ```bash
+ printing-press-golden-pp-cli <command> [subcommand] [args] --agent
+ ```
+4. If ambiguous, drill into subcommand help: `printing-press-golden-pp-cli <command> --help`.
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/doctor.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/doctor.go
new file mode 100644
index 00000000..5d7afdcd
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/doctor.go
@@ -0,0 +1,451 @@
+// 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 (
+ "database/sql"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+ "time"
+
+ "printing-press-golden-pp-cli/internal/client"
+ "printing-press-golden-pp-cli/internal/config"
+ "printing-press-golden-pp-cli/internal/store"
+ "github.com/spf13/cobra"
+)
+
+// looksLikeDoctorInterstitial reports whether the response body matches a known
+// bot-detection challenge page (Cloudflare, Akamai, Vercel, AWS WAF, DataDome,
+// PerimeterX). Only fires on the doctor probe — used to distinguish "transport
+// reached the wall" from "transport failed entirely." Returns the vendor name
+// when matched, or empty string when no match.
+//
+// Markers are anchored to <title> or vendor-specific strings to avoid
+// false-positives on benign content. For example, a recipe titled "Just A
+// Moment of Pause Cookies" must NOT match the Cloudflare challenge marker;
+// only "<title>just a moment" (the actual interstitial title) does.
+func looksLikeDoctorInterstitial(body []byte) string {
+ if len(body) == 0 {
+ return ""
+ }
+ limit := len(body)
+ if limit > 8192 {
+ limit = 8192
+ }
+ prefix := strings.ToLower(string(body[:limit]))
+ if !strings.Contains(prefix, "<title") {
+ // Every bot interstitial we recognize sets a <title>; bodies without
+ // one are body-only API responses, not challenge pages.
+ return ""
+ }
+ switch {
+ case strings.Contains(prefix, "<title>just a moment") || // CF JS challenge
+ strings.Contains(prefix, "challenges.cloudflare.com") || // CF Turnstile
+ (strings.Contains(prefix, "attention required") && strings.Contains(prefix, "cloudflare")):
+ return "Cloudflare"
+ case strings.Contains(prefix, "akamai") && (strings.Contains(prefix, "request unsuccessful") || strings.Contains(prefix, "access denied")):
+ return "Akamai"
+ case strings.Contains(prefix, "x-vercel-mitigated") || strings.Contains(prefix, "x-vercel-challenge-token") ||
+ (strings.Contains(prefix, "vercel") && strings.Contains(prefix, "challenge")):
+ return "Vercel"
+ case strings.Contains(prefix, "request blocked") && strings.Contains(prefix, "aws waf"):
+ return "AWS WAF"
+ case strings.Contains(prefix, "datadome") && (strings.Contains(prefix, "blocked") || strings.Contains(prefix, "captcha") || strings.Contains(prefix, "challenge")):
+ return "DataDome"
+ case strings.Contains(prefix, "perimeterx") || strings.Contains(prefix, "px-captcha"):
+ return "PerimeterX"
+ }
+ return ""
+}
+
+func newDoctorCmd(flags *rootFlags) *cobra.Command {
+ var failOn string
+ cmd := &cobra.Command{
+ Use: "doctor",
+ Short: "Check CLI health",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ report := map[string]any{}
+
+ // Check config
+ cfg, err := config.Load(flags.configPath)
+ if err != nil {
+ report["config"] = fmt.Sprintf("error: %s", err)
+ } else {
+ report["config"] = "ok"
+ report["config_path"] = cfg.Path
+ report["base_url"] = cfg.BaseURL
+ }
+
+ // Check auth
+ if cfg != nil {
+ header := cfg.AuthHeader()
+ if header == "" {
+ report["auth"] = "not configured"
+ report["auth_hint"] = "export PRINTING_PRESS_GOLDEN_API_KEY_AUTH=<your-key>"
+ } else {
+ report["auth"] = "configured"
+ report["auth_source"] = cfg.AuthSource
+ }
+ }
+
+ // Check auth environment variables
+ authEnvChecked := 0
+ authEnvSet := 0
+ authEnvChecked++
+ if os.Getenv("PRINTING_PRESS_GOLDEN_API_KEY_AUTH") != "" {
+ authEnvSet++
+ }
+ if authEnvSet == 0 {
+ report["env_vars"] = fmt.Sprintf("none set (checked %d)", authEnvChecked)
+ } else {
+ report["env_vars"] = fmt.Sprintf("%d/%d set", authEnvSet, authEnvChecked)
+ }
+
+ // Check API connectivity and validate credentials.
+ //
+ // The doctor uses the same client every other command uses --
+ // flags.newClient() returns a *client.Client wrapping whatever
+ // transport the spec declared (Surf for browser-chrome, stdlib
+ // for standard). A separate stdlib http.Client would silently
+ // bypass that choice and report false negatives against
+ // Cloudflare-fronted, Akamai-fronted, or otherwise bot-detected
+ // sites. By going through flags.newClient(), the doctor's
+ // reachability verdict matches what real commands experience.
+ if cfg != nil && cfg.BaseURL != "" {
+ c, clientErr := flags.newClient()
+ if clientErr != nil {
+ report["api"] = fmt.Sprintf("client init error: %s", clientErr)
+ } else {
+ // Step 1: Basic reachability via the configured transport.
+ reachBody, reachErr := c.Get("/", nil)
+ var reachAPIErr *client.APIError
+ switch {
+ case reachErr == nil:
+ // 2xx response — clearly reachable. Still inspect the
+ // body for a known interstitial; some bot walls return
+ // 200 with a JS challenge page.
+ if vendor := looksLikeDoctorInterstitial(reachBody); vendor != "" {
+ report["api"] = fmt.Sprintf("blocked by %s interstitial — the configured transport reached the wall. Try a different network, wait for the IP-level rate limit to clear, or check that the browser-chrome transport is bound correctly.", vendor)
+ } else {
+ report["api"] = "reachable"
+ }
+ case errors.As(reachErr, &reachAPIErr):
+ // Non-2xx from the server. The network reached, the
+ // server responded — that's "reachable" for our
+ // purposes. Inspect the response body for a known
+ // interstitial first; otherwise note the status.
+ status := reachAPIErr.StatusCode
+ if vendor := looksLikeDoctorInterstitial([]byte(reachAPIErr.Body)); vendor != "" {
+ report["api"] = fmt.Sprintf("blocked by %s interstitial (HTTP %d) — the configured transport reached the wall.", vendor, status)
+ } else {
+ report["api"] = fmt.Sprintf("reachable (HTTP %d at /)", status)
+ }
+ default:
+ // Network-level failure: DNS, connection refused, TLS,
+ // transport init, etc. The transport itself didn't
+ // connect.
+ report["api"] = fmt.Sprintf("unreachable: %s", reachErr)
+ }
+
+ // Step 2: Validate credentials with an authenticated probe.
+ authHeader := cfg.AuthHeader()
+ if authHeader == "" {
+ // No auth configured — skip credential validation
+ } else if reachErr != nil && !errors.As(reachErr, &reachAPIErr) {
+ report["credentials"] = "skipped (API unreachable)"
+ } else {
+ verifyPath := "/"
+ authParams := map[string]string{}
+ authHeaders := map[string]string{}
+ authHeaders["X-API-Key"] = authHeader
+ authHeaders["X-Api-Version"] = "2026-04-01"
+ authHeaders["User-Agent"] = "printing-press-golden-pp-cli"
+ _, authErr := c.GetWithHeaders(verifyPath, authParams, authHeaders)
+ var authAPIErr *client.APIError
+ switch {
+ case authErr == nil:
+ report["credentials"] = "valid"
+ case errors.As(authErr, &authAPIErr):
+ switch {
+ case authAPIErr.StatusCode == 401 || authAPIErr.StatusCode == 403:
+ // The probe hit the bare base URL because no auth.verify_path
+ // is configured in the spec. Many APIs return 401/403 from a
+ // bare versioned root regardless of token validity (the path
+ // isn't routed but the gateway still demands credentials).
+ // Don't claim invalid without certainty — set verify_path to
+ // a known-good authenticated GET (e.g. /me, /v1/account, /user)
+ // for a definitive verdict.
+ report["credentials"] = fmt.Sprintf("inconclusive (HTTP %d from base URL — set auth.verify_path in spec for a definitive probe)", authAPIErr.StatusCode)
+ default:
+ // Non-auth HTTP error (404, 500, etc.) — don't blame credentials
+ report["credentials"] = fmt.Sprintf("ok (HTTP %d from %s, but auth was accepted)", authAPIErr.StatusCode, verifyPath)
+ }
+ default:
+ report["credentials"] = fmt.Sprintf("error: %s", authErr)
+ }
+ }
+ }
+ } else if cfg != nil && cfg.BaseURL == "" {
+ report["api"] = "not configured (set base_url in config file)"
+ }
+ // Cache health: only reported when this CLI has a local store.
+ // Surfaces rows + last_synced_at per resource, schema version,
+ // and a fresh/stale/unknown verdict so agents can introspect
+ // whether to trust the cached data before issuing queries.
+ report["cache"] = collectCacheReport("")
+
+ report["version"] = version
+
+ if flags.asJSON {
+ if err := flags.printJSON(cmd, report); err != nil {
+ return err
+ }
+ return doctorExitForFailOn(failOn, report)
+ }
+
+ // Human-readable output with color
+ w := cmd.OutOrStdout()
+ checkKeys := []struct{ key, label string }{
+ {"config", "Config"},
+ {"auth", "Auth"},
+ {"api", "API"},
+ {"credentials", "Credentials"},
+ }
+ for _, ck := range checkKeys {
+ v, ok := report[ck.key]
+ if !ok {
+ continue
+ }
+ s := fmt.Sprintf("%v", v)
+ indicator := green("OK")
+ switch {
+ case strings.HasPrefix(s, "optional"):
+ // Optional-auth CLI with no key set — informational, not a failure.
+ indicator = yellow("INFO")
+ case strings.HasPrefix(s, "inconclusive"):
+ // The credential probe could not produce a definitive verdict
+ // (typically because the bare base URL returns 401/403 even for
+ // valid tokens). Surface as WARN, not FAIL — the user's actual
+ // commands will reveal a real auth failure if one exists.
+ indicator = yellow("WARN")
+ case strings.Contains(s, "error") || strings.Contains(s, "not configured") || strings.Contains(s, "unreachable") || strings.Contains(s, "invalid") || strings.Contains(s, "missing"):
+ indicator = red("FAIL")
+ case s == "not required":
+ // Public APIs: no auth needed is a healthy state, not a warning.
+ indicator = green("OK")
+ case strings.Contains(s, "not ") || strings.Contains(s, "skipped") || strings.Contains(s, "inferred"):
+ indicator = yellow("WARN")
+ }
+ fmt.Fprintf(w, " %s %s: %s\n", indicator, ck.label, s)
+ }
+ // Print info keys without status indicator
+ for _, key := range []string{"config_path", "base_url", "auth_source", "version"} {
+ if v, ok := report[key]; ok {
+ fmt.Fprintf(w, " %s: %v\n", key, v)
+ }
+ }
+ // Print auth setup hints (indented under Auth line)
+ if hint, ok := report["auth_hint"]; ok {
+ fmt.Fprintf(w, " hint: %v\n", hint)
+ }
+ // Cache section: render after the primary health block so it
+ // sits next to version info, mirroring the JSON report layout.
+ if cacheAny, ok := report["cache"]; ok {
+ if cacheRep, ok := cacheAny.(map[string]any); ok {
+ renderCacheReport(w, cacheRep)
+ }
+ }
+ return doctorExitForFailOn(failOn, report)
+ },
+ }
+ cmd.Flags().StringVar(&failOn, "fail-on", "", "Exit non-zero when a health level is reached: stale, error. Default is never.")
+ return cmd
+}
+
+// doctorExitForFailOn returns a non-nil error when the report's worst
+// status meets or exceeds the --fail-on threshold. "error" always trips
+// when any section reports an error; "stale" also trips when the cache
+// section is stale. The default empty string means never fail on status.
+func doctorExitForFailOn(failOn string, report map[string]any) error {
+ if failOn == "" {
+ return nil
+ }
+ worstError := false
+ worstStale := false
+ for _, v := range report {
+ s, ok := v.(string)
+ if ok {
+ if strings.Contains(s, "error") || strings.Contains(s, "unreachable") || strings.Contains(s, "invalid") || strings.Contains(s, "missing") {
+ worstError = true
+ }
+ }
+ if m, ok := v.(map[string]any); ok {
+ if st, _ := m["status"].(string); st == "error" {
+ worstError = true
+ } else if st == "stale" {
+ worstStale = true
+ }
+ }
+ }
+ switch failOn {
+ case "error":
+ if worstError {
+ return fmt.Errorf("doctor: --fail-on=error triggered")
+ }
+ case "stale":
+ if worstError || worstStale {
+ return fmt.Errorf("doctor: --fail-on=stale triggered")
+ }
+ default:
+ return fmt.Errorf("doctor: unknown --fail-on value %q (valid: stale, error)", failOn)
+ }
+ return nil
+}
+
+// collectCacheReport opens the local store, reads per-resource sync state,
+// and returns a map summarising cache health. Never panics on missing DB
+// or open failure; returns a map with status=unknown or status=error so the
+// caller can render and agents can interpret.
+//
+// staleAfterSpec is the CLI's configured threshold (e.g. "6h"); empty means
+// use the runtime default. The default is deliberately conservative (6h)
+// because the alternative is no freshness story at all.
+func collectCacheReport(staleAfterSpec string) map[string]any {
+ report := map[string]any{}
+ dbPath := defaultDBPath("printing-press-golden-pp-cli")
+ report["db_path"] = dbPath
+
+ fi, err := os.Stat(dbPath)
+ if err != nil {
+ if os.IsNotExist(err) {
+ report["status"] = "unknown"
+ report["hint"] = "Database not created yet; run 'printing-press-golden-pp-cli sync' to hydrate."
+ return report
+ }
+ report["status"] = "error"
+ report["error"] = err.Error()
+ return report
+ }
+ report["db_bytes"] = fi.Size()
+
+ s, err := store.Open(dbPath)
+ if err != nil {
+ report["status"] = "error"
+ report["error"] = err.Error()
+ return report
+ }
+ defer s.Close()
+
+ if v, verr := s.SchemaVersion(); verr == nil {
+ report["schema_version"] = v
+ }
+
+ staleAfter := 6 * time.Hour
+ if staleAfterSpec != "" {
+ if d, derr := time.ParseDuration(staleAfterSpec); derr == nil {
+ staleAfter = d
+ }
+ }
+
+ rows, qerr := s.DB().Query(`SELECT resource_type, COALESCE(total_count, 0), last_synced_at FROM sync_state ORDER BY resource_type`)
+ if qerr != nil {
+ // sync_state may not exist on a fresh DB that has migrated but not
+ // yet had any sync runs — treat as unknown rather than error.
+ report["status"] = "unknown"
+ report["hint"] = "No sync state recorded; run 'printing-press-golden-pp-cli sync' to populate."
+ return report
+ }
+ defer rows.Close()
+
+ var resources []map[string]any
+ fresh := true
+ haveAny := false
+ oldest := time.Duration(0)
+ for rows.Next() {
+ var rtype string
+ var count int64
+ var lastSynced sql.NullTime
+ if err := rows.Scan(&rtype, &count, &lastSynced); err != nil {
+ continue
+ }
+ r := map[string]any{"type": rtype, "rows": count}
+ if lastSynced.Valid {
+ haveAny = true
+ r["last_synced_at"] = lastSynced.Time.UTC().Format(time.RFC3339)
+ age := time.Since(lastSynced.Time)
+ r["staleness"] = age.Round(time.Minute).String()
+ if age > staleAfter {
+ fresh = false
+ }
+ if age > oldest {
+ oldest = age
+ }
+ } else {
+ r["staleness"] = "never"
+ fresh = false
+ }
+ resources = append(resources, r)
+ }
+ report["resources"] = resources
+ report["stale_after"] = staleAfter.String()
+
+ switch {
+ case !haveAny && len(resources) == 0:
+ report["status"] = "unknown"
+ report["hint"] = "sync_state is empty; run 'printing-press-golden-pp-cli sync' to hydrate."
+ case fresh:
+ report["status"] = "fresh"
+ default:
+ report["status"] = "stale"
+ report["oldest_age"] = oldest.Round(time.Minute).String()
+ report["hint"] = "Some resources are older than stale_after; run 'printing-press-golden-pp-cli sync' to refresh."
+ }
+ return report
+}
+
+func renderCacheReport(w io.Writer, rep map[string]any) {
+ status, _ := rep["status"].(string)
+ indicator := green("OK")
+ switch status {
+ case "stale":
+ indicator = yellow("WARN")
+ case "error":
+ indicator = red("FAIL")
+ case "unknown":
+ indicator = yellow("INFO")
+ }
+ fmt.Fprintf(w, " %s Cache: %s\n", indicator, status)
+ if v, ok := rep["db_path"]; ok {
+ fmt.Fprintf(w, " db_path: %v\n", v)
+ }
+ if v, ok := rep["schema_version"]; ok {
+ fmt.Fprintf(w, " schema_version: %v\n", v)
+ }
+ if v, ok := rep["db_bytes"]; ok {
+ fmt.Fprintf(w, " db_bytes: %v\n", v)
+ }
+ if v, ok := rep["stale_after"]; ok {
+ fmt.Fprintf(w, " stale_after: %v\n", v)
+ }
+ if v, ok := rep["oldest_age"]; ok {
+ fmt.Fprintf(w, " oldest_age: %v\n", v)
+ }
+ if resourcesAny, ok := rep["resources"]; ok {
+ if resources, ok := resourcesAny.([]map[string]any); ok && len(resources) > 0 {
+ fmt.Fprintf(w, " resources:\n")
+ for _, r := range resources {
+ rtype, _ := r["type"].(string)
+ rows := r["rows"]
+ staleness, _ := r["staleness"].(string)
+ fmt.Fprintf(w, " - %s: %v rows, %s\n", rtype, rows, staleness)
+ }
+ }
+ }
+ if hint, ok := rep["hint"]; ok {
+ fmt.Fprintf(w, " hint: %v\n", hint)
+ }
+}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cliutil/verifyenv.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cliutil/verifyenv.go
new file mode 100644
index 00000000..fc409acc
--- /dev/null
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cliutil/verifyenv.go
@@ -0,0 +1,30 @@
+// 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 cliutil
+
+import "os"
+
+// VerifyEnvVar is the env var the printing-press verifier sets in every
+// mock-mode subprocess. Generated commands that perform visible side
+// effects (open browser tabs, send notifications, dial out to OS
+// handlers) MUST short-circuit when this env var is "1" to avoid
+// spamming the user's environment during verify runs.
+const VerifyEnvVar = "PRINTING_PRESS_VERIFY"
+
+// IsVerifyEnv reports whether the current process is running under the
+// printing-press verifier in mock mode. Generated commands with side
+// effects pair this check with print-by-default + explicit opt-in
+// (--launch, --send, --play) so a verify pass on a fresh CLI does not
+// pop browser tabs or fire off real notifications.
+//
+// Defense-in-depth: even if the verifier's heuristic side-effect
+// classifier misses a command, this env-var short-circuit catches it.
+//
+// if cliutil.IsVerifyEnv() {
+// fmt.Fprintln(cmd.OutOrStdout(), "would launch:", url)
+// return nil
+// }
+func IsVerifyEnv() bool {
+ return os.Getenv(VerifyEnvVar) == "1"
+}
diff --git a/testdata/golden/expected/schema-traffic-analysis/exit.txt b/testdata/golden/expected/schema-traffic-analysis/exit.txt
new file mode 100644
index 00000000..573541ac
--- /dev/null
+++ b/testdata/golden/expected/schema-traffic-analysis/exit.txt
@@ -0,0 +1 @@
+0
diff --git a/testdata/golden/expected/schema-traffic-analysis/stderr.txt b/testdata/golden/expected/schema-traffic-analysis/stderr.txt
new file mode 100644
index 00000000..e69de29b
diff --git a/testdata/golden/expected/schema-traffic-analysis/stdout.txt b/testdata/golden/expected/schema-traffic-analysis/stdout.txt
new file mode 100644
index 00000000..ee5f4079
--- /dev/null
+++ b/testdata/golden/expected/schema-traffic-analysis/stdout.txt
@@ -0,0 +1,185 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://github.com/mvanhorn/cli-printing-press/schemas/traffic-analysis.schema.json",
+ "title": "CLI Printing Press traffic-analysis.json",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["version", "summary", "protocols", "auth", "endpoint_clusters"],
+ "properties": {
+ "version": {"type": "string"},
+ "summary": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["entry_count", "api_entry_count", "noise_entry_count"],
+ "properties": {
+ "target_url": {"type": "string"},
+ "captured_at": {"type": "string"},
+ "entry_count": {"type": "integer", "minimum": 0},
+ "api_entry_count": {"type": "integer", "minimum": 0},
+ "noise_entry_count": {"type": "integer", "minimum": 0},
+ "host_distribution": {"type": "object", "additionalProperties": {"type": "integer", "minimum": 0}},
+ "time_start": {"type": "string"},
+ "time_end": {"type": "string"}
+ }
+ },
+ "reachability": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["mode", "confidence"],
+ "properties": {
+ "mode": {"type": "string", "enum": ["standard_http", "browser_http", "browser_clearance_http", "browser_required", "blocked", "unknown"]},
+ "confidence": {"type": "number", "minimum": 0, "maximum": 1},
+ "reasons": {"type": "array", "items": {"type": "string"}},
+ "evidence": {"type": "array", "items": {"oneOf": [{"$ref": "#/$defs/evidence_ref"}, {"type": "string"}]}}
+ }
+ },
+ "protocols": {"type": "array", "items": {"$ref": "#/$defs/protocol_observation"}},
+ "auth": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "candidates": {"type": "array", "items": {"$ref": "#/$defs/auth_candidate"}}
+ }
+ },
+ "protections": {"type": "array", "items": {"$ref": "#/$defs/protection_observation"}},
+ "endpoint_clusters": {"type": "array", "items": {"$ref": "#/$defs/endpoint_cluster"}},
+ "request_sequences": {"type": "array", "items": {"$ref": "#/$defs/request_sequence"}},
+ "pagination": {"type": "array", "items": {"$ref": "#/$defs/pagination_signal"}},
+ "candidate_commands": {"type": "array", "items": {"$ref": "#/$defs/candidate_command"}},
+ "generation_hints": {"type": "array", "items": {"type": "string"}},
+ "warnings": {"type": "array", "items": {"$ref": "#/$defs/analysis_warning"}}
+ },
+ "$defs": {
+ "evidence_ref": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["entry_index"],
+ "properties": {
+ "entry_index": {"type": "integer", "minimum": 0},
+ "method": {"type": "string"},
+ "host": {"type": "string"},
+ "path": {"type": "string"},
+ "status": {"type": "integer"},
+ "content_type": {"type": "string"},
+ "reason": {"type": "string"}
+ }
+ },
+ "protocol_observation": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["label", "confidence"],
+ "properties": {
+ "label": {"type": "string"},
+ "confidence": {"type": "number", "minimum": 0, "maximum": 1},
+ "evidence": {"type": "array", "items": {"oneOf": [{"$ref": "#/$defs/evidence_ref"}, {"type": "string"}]}},
+ "details": {"type": "object", "additionalProperties": {"type": "string"}}
+ }
+ },
+ "auth_candidate": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["type", "confidence"],
+ "properties": {
+ "type": {"type": "string"},
+ "confidence": {"type": "number", "minimum": 0, "maximum": 1},
+ "header_names": {"type": "array", "items": {"type": "string"}},
+ "query_names": {"type": "array", "items": {"type": "string"}},
+ "cookie_names": {"type": "array", "items": {"type": "string"}},
+ "domain_hints": {"type": "array", "items": {"type": "string"}},
+ "evidence": {"type": "array", "items": {"$ref": "#/$defs/evidence_ref"}}
+ }
+ },
+ "protection_observation": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["label", "confidence"],
+ "properties": {
+ "label": {"type": "string"},
+ "confidence": {"type": "number", "minimum": 0, "maximum": 1},
+ "evidence": {"type": "array", "items": {"oneOf": [{"$ref": "#/$defs/evidence_ref"}, {"type": "string"}]}},
+ "notes": {"type": "array", "items": {"type": "string"}}
+ }
+ },
+ "endpoint_cluster": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["method", "path", "count"],
+ "properties": {
+ "host": {"type": "string"},
+ "method": {"type": "string"},
+ "path": {"type": "string"},
+ "count": {"type": "integer", "minimum": 0},
+ "statuses": {"type": "array", "items": {"type": "integer"}},
+ "content_types": {"type": "array", "items": {"type": "string"}},
+ "size_class": {"type": "string"},
+ "request_shape": {"$ref": "#/$defs/shape_summary"},
+ "response_shape": {"$ref": "#/$defs/shape_summary"},
+ "evidence": {"type": "array", "items": {"$ref": "#/$defs/evidence_ref"}}
+ }
+ },
+ "shape_summary": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "kind": {"type": "string"},
+ "fields": {"type": "array", "items": {"$ref": "#/$defs/shape_field"}}
+ }
+ },
+ "shape_field": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["name"],
+ "properties": {
+ "name": {"type": "string"},
+ "type": {"type": "string"},
+ "required": {"type": "boolean"},
+ "format": {"type": "string"}
+ }
+ },
+ "request_sequence": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["label", "confidence"],
+ "properties": {
+ "label": {"type": "string"},
+ "confidence": {"type": "number", "minimum": 0, "maximum": 1},
+ "evidence": {"type": "array", "items": {"oneOf": [{"$ref": "#/$defs/evidence_ref"}, {"type": "string"}]}},
+ "notes": {"type": "array", "items": {"type": "string"}}
+ }
+ },
+ "pagination_signal": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["location", "name", "confidence"],
+ "properties": {
+ "location": {"type": "string"},
+ "name": {"type": "string"},
+ "confidence": {"type": "number", "minimum": 0, "maximum": 1},
+ "evidence": {"type": "array", "items": {"$ref": "#/$defs/evidence_ref"}}
+ }
+ },
+ "candidate_command": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["name", "confidence"],
+ "properties": {
+ "name": {"type": "string"},
+ "resource": {"type": "string"},
+ "confidence": {"type": "number", "minimum": 0, "maximum": 1},
+ "rationale": {"type": "string"},
+ "evidence": {"type": "array", "items": {"$ref": "#/$defs/evidence_ref"}}
+ }
+ },
+ "analysis_warning": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["type", "message", "confidence"],
+ "properties": {
+ "type": {"type": "string"},
+ "message": {"type": "string"},
+ "confidence": {"type": "number", "minimum": 0, "maximum": 1},
+ "evidence": {"type": "array", "items": {"$ref": "#/$defs/evidence_ref"}}
+ }
+ }
+ }
+}
← d86d3af1 fix(cli): sync dogfood novel features into CLI artifacts (#3
·
back to Cli Printing Press
·
ci(ci): improve workflow coverage and guards (#346) 4c64ffe6 →