← back to Cli Printing Press
feat(cli): auth.optional spec field — doctor INFO not FAIL, README framing, auth cmd names env var (#211)
831040d58463ecc3734ac663fd43988ed149106d · 2026-04-13 14:28:12 -0700 · Trevin Chow
Introduces `auth.optional: true` on internal YAML specs for APIs where the
API key enhances a subset of features (USDA FoodData Central, YouTube Data
API v3, OpenWeather paid tier) rather than gating core functionality. The
generator now emits honest UX for these cases instead of the misleading
"FAIL Auth: not configured" that greeted every new user.
## Problem
A CLI like recipe-goat-pp-cli uses USDA_FDC_API_KEY only for the
`recipe get --nutrition` backfill. Every other command works anonymously.
But the spec schema has no way to declare this, so doctor shows:
FAIL Auth: not configured
hint: export USDA_FDC_API_KEY=<your-key>
A new user reads "FAIL" and assumes the CLI is broken. The README puts
the section under a plain "## Authentication" header that implies
requirement, and `auth --help` says "Manage authentication tokens" with
no hint which token matters.
## Fix
- `internal/spec/spec.go`: new `AuthConfig.Optional bool` field.
- `internal/generator/templates/doctor.go.tmpl`:
- Emits `"optional — not configured"` when `Auth.Optional` and no key set.
- Status-indicator logic has a new `strings.HasPrefix(s, "optional")` case
that renders as yellow INFO instead of red FAIL.
- Also reorders the switch so `s == "not required"` matches before the
generic `"not "` substring check — fixes a latent issue where no-auth
CLIs (like weather-goat) showed WARN on a perfectly healthy state.
- `internal/generator/templates/auth.go.tmpl` and `auth_simple.go.tmpl`:
The `auth` parent command's Short description now names the specific
env var (`"Manage OPT_AUTH_KEY credentials"` or `"Manage the optional
OPT_AUTH_KEY (enables features that require it; not needed for core
commands)"` when optional). Was generic "Manage authentication".
- `internal/generator/templates/readme.md.tmpl`: when `Auth.Optional`
the Authentication section header becomes `## Optional: API Key` and
prepends "**All core commands work without setup.** The API key below
is only needed to unlock additional features."
## Usage
```yaml
auth:
type: api_key
header: X-API-KEY
env_vars: [USDA_FDC_API_KEY]
optional: true # new
key_url: https://fdc.nal.usda.gov/api-key-signup
```
## Testing
`internal/generator/auth_optional_test.go` pins seven behaviors:
- `TestAuthOptional_DoctorReportsOptional` — doctor emits the
optional-prefixed status when `auth.optional` is set.
- `TestAuthNotOptional_DoctorReportsFailure` — default specs still emit
the plain not-configured message that renders as FAIL.
- `TestAuthOptional_AuthCmdShortNamesEnvVar` — optional variant mentions
the env var + optional framing.
- `TestAuthRequired_AuthCmdShortNamesEnvVar` — required variant names the
env var without optional framing.
- `TestAuthOptional_ReadmeFramesAsOptional` — README uses `## Optional:
API Key` + reassurance preamble.
- `TestAuthNotOptional_ReadmeKeepsAuthenticationHeader` — required-auth
specs keep the plain `## Authentication` heading.
- `TestAuthConfig_Optional_ZeroValue` — schema sanity: Optional defaults
to false.
All 17 packages pass. (An unrelated pre-existing flake in
TestIntEnumParamSkipped is confirmed to exist on main.)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Files touched
A internal/generator/auth_optional_test.goM internal/generator/templates/auth.go.tmplM internal/generator/templates/auth_simple.go.tmplM internal/generator/templates/doctor.go.tmplM internal/generator/templates/readme.md.tmplM internal/spec/spec.go
Diff
commit 831040d58463ecc3734ac663fd43988ed149106d
Author: Trevin Chow <trevin@trevinchow.com>
Date: Mon Apr 13 14:28:12 2026 -0700
feat(cli): auth.optional spec field — doctor INFO not FAIL, README framing, auth cmd names env var (#211)
Introduces `auth.optional: true` on internal YAML specs for APIs where the
API key enhances a subset of features (USDA FoodData Central, YouTube Data
API v3, OpenWeather paid tier) rather than gating core functionality. The
generator now emits honest UX for these cases instead of the misleading
"FAIL Auth: not configured" that greeted every new user.
## Problem
A CLI like recipe-goat-pp-cli uses USDA_FDC_API_KEY only for the
`recipe get --nutrition` backfill. Every other command works anonymously.
But the spec schema has no way to declare this, so doctor shows:
FAIL Auth: not configured
hint: export USDA_FDC_API_KEY=<your-key>
A new user reads "FAIL" and assumes the CLI is broken. The README puts
the section under a plain "## Authentication" header that implies
requirement, and `auth --help` says "Manage authentication tokens" with
no hint which token matters.
## Fix
- `internal/spec/spec.go`: new `AuthConfig.Optional bool` field.
- `internal/generator/templates/doctor.go.tmpl`:
- Emits `"optional — not configured"` when `Auth.Optional` and no key set.
- Status-indicator logic has a new `strings.HasPrefix(s, "optional")` case
that renders as yellow INFO instead of red FAIL.
- Also reorders the switch so `s == "not required"` matches before the
generic `"not "` substring check — fixes a latent issue where no-auth
CLIs (like weather-goat) showed WARN on a perfectly healthy state.
- `internal/generator/templates/auth.go.tmpl` and `auth_simple.go.tmpl`:
The `auth` parent command's Short description now names the specific
env var (`"Manage OPT_AUTH_KEY credentials"` or `"Manage the optional
OPT_AUTH_KEY (enables features that require it; not needed for core
commands)"` when optional). Was generic "Manage authentication".
- `internal/generator/templates/readme.md.tmpl`: when `Auth.Optional`
the Authentication section header becomes `## Optional: API Key` and
prepends "**All core commands work without setup.** The API key below
is only needed to unlock additional features."
## Usage
```yaml
auth:
type: api_key
header: X-API-KEY
env_vars: [USDA_FDC_API_KEY]
optional: true # new
key_url: https://fdc.nal.usda.gov/api-key-signup
```
## Testing
`internal/generator/auth_optional_test.go` pins seven behaviors:
- `TestAuthOptional_DoctorReportsOptional` — doctor emits the
optional-prefixed status when `auth.optional` is set.
- `TestAuthNotOptional_DoctorReportsFailure` — default specs still emit
the plain not-configured message that renders as FAIL.
- `TestAuthOptional_AuthCmdShortNamesEnvVar` — optional variant mentions
the env var + optional framing.
- `TestAuthRequired_AuthCmdShortNamesEnvVar` — required variant names the
env var without optional framing.
- `TestAuthOptional_ReadmeFramesAsOptional` — README uses `## Optional:
API Key` + reassurance preamble.
- `TestAuthNotOptional_ReadmeKeepsAuthenticationHeader` — required-auth
specs keep the plain `## Authentication` heading.
- `TestAuthConfig_Optional_ZeroValue` — schema sanity: Optional defaults
to false.
All 17 packages pass. (An unrelated pre-existing flake in
TestIntEnumParamSkipped is confirmed to exist on main.)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
internal/generator/auth_optional_test.go | 146 +++++++++++++++++++++++
internal/generator/templates/auth.go.tmpl | 8 ++
internal/generator/templates/auth_simple.go.tmpl | 8 ++
internal/generator/templates/doctor.go.tmpl | 14 ++-
internal/generator/templates/readme.md.tmpl | 6 +-
internal/spec/spec.go | 7 +-
6 files changed, 181 insertions(+), 8 deletions(-)
diff --git a/internal/generator/auth_optional_test.go b/internal/generator/auth_optional_test.go
new file mode 100644
index 00000000..784c3516
--- /dev/null
+++ b/internal/generator/auth_optional_test.go
@@ -0,0 +1,146 @@
+package generator
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/mvanhorn/cli-printing-press/internal/spec"
+ "github.com/stretchr/testify/require"
+)
+
+// TestAuthOptional_DoctorReportsOptional verifies that a spec with
+// `auth.optional: true` produces a doctor that emits "optional — not configured"
+// (rendered as INFO by the indicator switch) instead of "not configured" (FAIL).
+// A Grade-A CLI with a completely healthy doctor for its optional-auth state
+// is now possible. Regression guard for #211.
+func TestAuthOptional_DoctorReportsOptional(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("opt-auth")
+ apiSpec.Auth.Optional = true
+
+ outputDir := filepath.Join(t.TempDir(), "opt-auth-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ doctorSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "doctor.go"))
+ require.NoError(t, err)
+ require.Contains(t, string(doctorSrc), `"optional — not configured"`,
+ "doctor should emit optional-prefixed status when auth.optional is set")
+}
+
+// TestAuthNotOptional_DoctorReportsFailure guards the default branch: when
+// auth.optional is unset, doctor emits the plain "not configured" string that
+// renders as FAIL. Prevents over-broad application of the optional path.
+func TestAuthNotOptional_DoctorReportsFailure(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("req-auth")
+ // Optional left at zero-value false.
+
+ outputDir := filepath.Join(t.TempDir(), "req-auth-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ doctorSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "doctor.go"))
+ require.NoError(t, err)
+ require.Contains(t, string(doctorSrc), `"not configured"`,
+ "doctor emits the standard not-configured message for required auth")
+ require.NotContains(t, string(doctorSrc), `"optional — not configured"`,
+ "default spec must not emit the optional-prefixed status")
+}
+
+// TestAuthOptional_AuthCmdShortNamesEnvVar verifies the `auth` subcommand's
+// help Short description names the specific env var and flags the optionality
+// when auth.optional is set.
+func TestAuthOptional_AuthCmdShortNamesEnvVar(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("opt-auth-short")
+ apiSpec.Auth.Optional = true
+ apiSpec.Auth.EnvVars = []string{"OPT_AUTH_KEY"}
+
+ outputDir := filepath.Join(t.TempDir(), "opt-auth-short-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ authSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "auth.go"))
+ require.NoError(t, err)
+ require.Contains(t, string(authSrc), `"Manage the optional OPT_AUTH_KEY`,
+ "auth parent command Short must name the env var and flag optionality")
+}
+
+// TestAuthRequired_AuthCmdShortNamesEnvVar verifies the default (required)
+// branch still names the env var — just without the "optional" flag.
+func TestAuthRequired_AuthCmdShortNamesEnvVar(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("req-auth-short")
+ apiSpec.Auth.EnvVars = []string{"REQ_AUTH_KEY"}
+
+ outputDir := filepath.Join(t.TempDir(), "req-auth-short-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ authSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "auth.go"))
+ require.NoError(t, err)
+ require.Contains(t, string(authSrc), `"Manage REQ_AUTH_KEY credentials"`,
+ "required-auth parent command Short names the env var without optional framing")
+}
+
+// TestAuthOptional_ReadmeFramesAsOptional verifies the README template
+// uses "Optional: API Key" + the "all core commands work without setup"
+// preamble when auth.optional is true and a narrative auth_narrative is set.
+func TestAuthOptional_ReadmeFramesAsOptional(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("opt-auth-readme")
+ apiSpec.Auth.Optional = true
+
+ outputDir := filepath.Join(t.TempDir(), "opt-auth-readme-pp-cli")
+ gen := New(apiSpec, outputDir)
+ gen.Narrative = &ReadmeNarrative{
+ AuthNarrative: "Use the OPTAUTH_KEY to unlock bonus features.",
+ }
+ require.NoError(t, gen.Generate())
+
+ readme, err := os.ReadFile(filepath.Join(outputDir, "README.md"))
+ require.NoError(t, err)
+ body := string(readme)
+ require.Contains(t, body, "## Optional: API Key",
+ "README must use 'Optional: API Key' heading when auth.optional is set")
+ require.Contains(t, body, "All core commands work without setup",
+ "README must reassure users that core commands need no setup")
+}
+
+// TestAuthNotOptional_ReadmeKeepsAuthenticationHeader guards the default branch:
+// when auth.optional is unset, README keeps the plain "## Authentication" heading.
+func TestAuthNotOptional_ReadmeKeepsAuthenticationHeader(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("req-auth-readme")
+ // Optional left false.
+
+ outputDir := filepath.Join(t.TempDir(), "req-auth-readme-pp-cli")
+ gen := New(apiSpec, outputDir)
+ gen.Narrative = &ReadmeNarrative{
+ AuthNarrative: "Export REQ_AUTH_KEY to access the API.",
+ }
+ require.NoError(t, gen.Generate())
+
+ readme, err := os.ReadFile(filepath.Join(outputDir, "README.md"))
+ require.NoError(t, err)
+ body := string(readme)
+ require.Contains(t, body, "## Authentication",
+ "README must keep the Authentication heading for required auth")
+ require.NotContains(t, body, "## Optional: API Key",
+ "required-auth README must not use the optional framing")
+ require.NotContains(t, body, "All core commands work without setup",
+ "required-auth README must not claim core commands work without setup")
+}
+
+// Sanity check that my spec field round-trips. Not really testing anything
+// new after the previous tests; here to make the schema intent explicit.
+func TestAuthConfig_Optional_ZeroValue(t *testing.T) {
+ a := spec.AuthConfig{}
+ require.False(t, a.Optional, "Optional must default to false")
+ a.Optional = true
+ require.True(t, a.Optional)
+}
diff --git a/internal/generator/templates/auth.go.tmpl b/internal/generator/templates/auth.go.tmpl
index 21ec0b02..fccd05a8 100644
--- a/internal/generator/templates/auth.go.tmpl
+++ b/internal/generator/templates/auth.go.tmpl
@@ -25,7 +25,15 @@ import (
func newAuthCmd(flags *rootFlags) *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
+{{- if .Auth.EnvVars}}
+{{- if .Auth.Optional}}
+ Short: "Manage the optional {{index .Auth.EnvVars 0}} (enables features that require it; not needed for core commands)",
+{{- else}}
+ Short: "Manage {{index .Auth.EnvVars 0}} credentials",
+{{- end}}
+{{- else}}
Short: "Manage authentication",
+{{- end}}
}
cmd.AddCommand(newAuthLoginCmd(flags))
diff --git a/internal/generator/templates/auth_simple.go.tmpl b/internal/generator/templates/auth_simple.go.tmpl
index 4d1decc6..c7d51c40 100644
--- a/internal/generator/templates/auth_simple.go.tmpl
+++ b/internal/generator/templates/auth_simple.go.tmpl
@@ -16,7 +16,15 @@ import (
func newAuthCmd(flags *rootFlags) *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
+{{- if .Auth.EnvVars}}
+{{- if .Auth.Optional}}
+ Short: "Manage the optional {{index .Auth.EnvVars 0}} (enables features that require it; not needed for core commands)",
+{{- else}}
+ Short: "Manage {{index .Auth.EnvVars 0}} credentials",
+{{- end}}
+{{- else}}
Short: "Manage authentication tokens",
+{{- end}}
}
cmd.AddCommand(newAuthStatusCmd(flags))
diff --git a/internal/generator/templates/doctor.go.tmpl b/internal/generator/templates/doctor.go.tmpl
index a24baa05..9d8edfc3 100644
--- a/internal/generator/templates/doctor.go.tmpl
+++ b/internal/generator/templates/doctor.go.tmpl
@@ -85,7 +85,11 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
if cfg != nil {
header := cfg.AuthHeader()
if header == "" {
+{{- if .Auth.Optional}}
+ report["auth"] = "optional — not configured"
+{{- else}}
report["auth"] = "not configured"
+{{- end}}
{{- if .Auth.EnvVars}}
report["auth_hint"] = "export {{index .Auth.EnvVars 0}}=<your-key>"
{{- end}}
@@ -209,12 +213,16 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
}
s := fmt.Sprintf("%v", v)
indicator := green("OK")
- if strings.Contains(s, "error") || strings.Contains(s, "not configured") || strings.Contains(s, "unreachable") || strings.Contains(s, "invalid") {
+ switch {
+ case strings.HasPrefix(s, "optional"):
+ // Optional-auth CLI with no key set — informational, not a failure.
+ indicator = yellow("INFO")
+ case strings.Contains(s, "error") || strings.Contains(s, "not configured") || strings.Contains(s, "unreachable") || strings.Contains(s, "invalid"):
indicator = red("FAIL")
- } else if s == "not required" {
+ case s == "not required":
// Public APIs: no auth needed is a healthy state, not a warning.
indicator = green("OK")
- } else if strings.Contains(s, "not ") || strings.Contains(s, "skipped") || strings.Contains(s, "inferred") {
+ 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)
diff --git a/internal/generator/templates/readme.md.tmpl b/internal/generator/templates/readme.md.tmpl
index b89ec884..56e650ee 100644
--- a/internal/generator/templates/readme.md.tmpl
+++ b/internal/generator/templates/readme.md.tmpl
@@ -28,9 +28,11 @@ go install github.com/mvanhorn/printing-press-library/library/{{if .Category}}{{
Download from [Releases](https://github.com/mvanhorn/printing-press-library/releases).
{{- if and .Narrative .Narrative.AuthNarrative}}
-## Authentication
+{{if .Auth.Optional}}## Optional: API Key{{else}}## Authentication{{end}}
-{{.Narrative.AuthNarrative}}
+{{if .Auth.Optional}}**All core commands work without setup.** The API key below is only needed to unlock additional features.
+
+{{end}}{{.Narrative.AuthNarrative}}
{{- end}}
## Quick Start
diff --git a/internal/spec/spec.go b/internal/spec/spec.go
index ce2e7007..78d1885c 100644
--- a/internal/spec/spec.go
+++ b/internal/spec/spec.go
@@ -59,9 +59,10 @@ type AuthConfig struct {
Header string `yaml:"header" json:"header"`
Format string `yaml:"format" json:"format"`
EnvVars []string `yaml:"env_vars" json:"env_vars"`
- Scheme string `yaml:"scheme,omitempty" json:"scheme,omitempty"` // OpenAPI security scheme name
- In string `yaml:"in,omitempty" json:"in,omitempty"` // header, query, cookie
- KeyURL string `yaml:"key_url,omitempty" json:"key_url,omitempty"` // URL where users can register for an API key
+ Optional bool `yaml:"optional,omitempty" json:"optional,omitempty"` // true when the key enhances a subset of features (e.g., USDA nutrition backfill) rather than gating core functionality; doctor treats unconfigured optional auth as INFO not FAIL and README frames the section as "Optional"
+ Scheme string `yaml:"scheme,omitempty" json:"scheme,omitempty"` // OpenAPI security scheme name
+ In string `yaml:"in,omitempty" json:"in,omitempty"` // header, query, cookie
+ KeyURL string `yaml:"key_url,omitempty" json:"key_url,omitempty"` // URL where users can register for an API key
AuthorizationURL string `yaml:"authorization_url,omitempty" json:"authorization_url,omitempty"`
TokenURL string `yaml:"token_url,omitempty" json:"token_url,omitempty"`
Scopes []string `yaml:"scopes,omitempty" json:"scopes,omitempty"`
← df242da4 feat(cli): scorecard --live-check samples novel-feature exam
·
back to Cli Printing Press
·
feat(cli): printing-press verify-skill + Phase 4 wiring + Ph 18cb521e →