← back to Cli Printing Press
fix(cli): gate orphan token scaffolding on auth surface (#704)
35d7e842f618b0abcaa7127960344267e9f79c71 · 2026-05-08 00:22:37 -0700 · Trevin Chow
* fix(cli): gate orphan token scaffolding on auth surface
When `auth.type: "none"` (no securitySchemes, no OAuth, no GraphQL
persisted-query hint), the generator skips emitting `internal/cli/auth.go`
but `config.go` still emitted `AccessToken`, `RefreshToken`, `TokenExpiry`,
`ClientID`, `ClientSecret`, `SaveTokens`, and `ClearTokens`, and `client.go`
still emitted `refreshAccessToken()` plus an `AccessToken`-driven refresh
check inside `authHeader()`. Nothing on a no-auth CLI populates these,
which made triagers misread the shape as "OAuth-shaped config disagreeing
with auth-template selection" (issue #695). They aren't OAuth-shaped —
those fields are the universal token storage used by every non-none auth
flow — but unconditional emission left them dangling for no-auth CLIs.
Plumb a `HasAuthSurface bool` (mirroring `Generator.shouldEmitAuth()`)
through `clientTemplateData` and a new `configTemplateData`, then gate
the token fields, helpers, refresh function, and the now-unused `net/url`
and `time` imports on it. The same predicate is what gates `auth.go`
emission, the root-command registration, and the scorecard's no-auth
exemption — all four call sites now agree.
Refs #695
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(cli): align HasAuthCommand naming and table-drive token-scaffolding test
- Rename HasAuthSurface -> HasAuthCommand on clientTemplateData and
configTemplateData so the five call sites that mirror shouldEmitAuth
(auth.go emission, root.go registration, scorecard exemption, config
emission, client emission) all use one term, matching the doc comment.
- Trim the field comment to a single load-bearing sentence.
- Collapse the two assertion-only tests into one table-driven test with
shared symbol slices, dropping the ticket-number prefaces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(cli): pin all three shouldEmitAuth predicate branches and gate compile
Code review surfaced four issues:
- The test's claim of "pinning both directions" only covered one of three
shouldEmitAuth() branches. A regression that short-circuited the gate to
only check Auth.Type would still pass. Add table cases for the other
two arms: Auth.Type=="none" + AuthorizationURL set (oauth) and
Auth.Type=="none" + graphql_persisted_query traffic-analysis hint.
- Symbol-absence string asserts can't catch import-gating mistakes (an
orphan time.Time{} after TokenExpiry is removed would pass the asserts
but ship a non-buildable CLI). Add runGoCommand mod tidy + build steps
on the generated output, gated by -short via the existing helper so
unit-lane runs stay fast and full-lane runs catch the bug class.
- The api_key case relied on minimalSpec's silent default plus a
conditional override; make the case set Auth explicitly so each row
self-documents.
- The HasAuthCommand field comment in clientTemplateData restated the
field name (AGENTS.md "no field-name restatement"). Reword to lead
with the predicate source instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(cli): document auth-none dead-code emission and co-gating fix
Captures PR #704's fix in docs/solutions/logic-errors/. Records the
predicate-co-gating invariant (five sites must mirror shouldEmitAuth),
the rejected community-proposed parser fallback (wouldn't have helped
GitHub's spec — verified empirically), and the test pattern that pins
all three predicate branches plus a generated-CLI go-build step to
catch import-gating mistakes that string-presence asserts miss.
Cross-links the inverse-direction parser doc and the auth.Type
branching idiom doc for future readers tracing the co-gating pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
A docs/solutions/logic-errors/auth-none-dead-code-in-config-client-2026-05-08.mdA internal/generator/auth_none_dead_code_test.goM internal/generator/generator.goM internal/generator/templates/client.go.tmplM internal/generator/templates/config.go.tmpl
Diff
commit 35d7e842f618b0abcaa7127960344267e9f79c71
Author: Trevin Chow <trevin@trevinchow.com>
Date: Fri May 8 00:22:37 2026 -0700
fix(cli): gate orphan token scaffolding on auth surface (#704)
* fix(cli): gate orphan token scaffolding on auth surface
When `auth.type: "none"` (no securitySchemes, no OAuth, no GraphQL
persisted-query hint), the generator skips emitting `internal/cli/auth.go`
but `config.go` still emitted `AccessToken`, `RefreshToken`, `TokenExpiry`,
`ClientID`, `ClientSecret`, `SaveTokens`, and `ClearTokens`, and `client.go`
still emitted `refreshAccessToken()` plus an `AccessToken`-driven refresh
check inside `authHeader()`. Nothing on a no-auth CLI populates these,
which made triagers misread the shape as "OAuth-shaped config disagreeing
with auth-template selection" (issue #695). They aren't OAuth-shaped —
those fields are the universal token storage used by every non-none auth
flow — but unconditional emission left them dangling for no-auth CLIs.
Plumb a `HasAuthSurface bool` (mirroring `Generator.shouldEmitAuth()`)
through `clientTemplateData` and a new `configTemplateData`, then gate
the token fields, helpers, refresh function, and the now-unused `net/url`
and `time` imports on it. The same predicate is what gates `auth.go`
emission, the root-command registration, and the scorecard's no-auth
exemption — all four call sites now agree.
Refs #695
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(cli): align HasAuthCommand naming and table-drive token-scaffolding test
- Rename HasAuthSurface -> HasAuthCommand on clientTemplateData and
configTemplateData so the five call sites that mirror shouldEmitAuth
(auth.go emission, root.go registration, scorecard exemption, config
emission, client emission) all use one term, matching the doc comment.
- Trim the field comment to a single load-bearing sentence.
- Collapse the two assertion-only tests into one table-driven test with
shared symbol slices, dropping the ticket-number prefaces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(cli): pin all three shouldEmitAuth predicate branches and gate compile
Code review surfaced four issues:
- The test's claim of "pinning both directions" only covered one of three
shouldEmitAuth() branches. A regression that short-circuited the gate to
only check Auth.Type would still pass. Add table cases for the other
two arms: Auth.Type=="none" + AuthorizationURL set (oauth) and
Auth.Type=="none" + graphql_persisted_query traffic-analysis hint.
- Symbol-absence string asserts can't catch import-gating mistakes (an
orphan time.Time{} after TokenExpiry is removed would pass the asserts
but ship a non-buildable CLI). Add runGoCommand mod tidy + build steps
on the generated output, gated by -short via the existing helper so
unit-lane runs stay fast and full-lane runs catch the bug class.
- The api_key case relied on minimalSpec's silent default plus a
conditional override; make the case set Auth explicitly so each row
self-documents.
- The HasAuthCommand field comment in clientTemplateData restated the
field name (AGENTS.md "no field-name restatement"). Reword to lead
with the predicate source instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(cli): document auth-none dead-code emission and co-gating fix
Captures PR #704's fix in docs/solutions/logic-errors/. Records the
predicate-co-gating invariant (five sites must mirror shouldEmitAuth),
the rejected community-proposed parser fallback (wouldn't have helped
GitHub's spec — verified empirically), and the test pattern that pins
all three predicate branches plus a generated-CLI go-build step to
catch import-gating mistakes that string-presence asserts miss.
Cross-links the inverse-direction parser doc and the auth.Type
branching idiom doc for future readers tracing the co-gating pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
...h-none-dead-code-in-config-client-2026-05-08.md | 162 +++++++++++++++++++++
internal/generator/auth_none_dead_code_test.go | 115 +++++++++++++++
internal/generator/generator.go | 17 +++
internal/generator/templates/client.go.tmpl | 7 +-
internal/generator/templates/config.go.tmpl | 7 +
5 files changed, 307 insertions(+), 1 deletion(-)
diff --git a/docs/solutions/logic-errors/auth-none-dead-code-in-config-client-2026-05-08.md b/docs/solutions/logic-errors/auth-none-dead-code-in-config-client-2026-05-08.md
new file mode 100644
index 00000000..b3cfdfd2
--- /dev/null
+++ b/docs/solutions/logic-errors/auth-none-dead-code-in-config-client-2026-05-08.md
@@ -0,0 +1,162 @@
+---
+module: internal/generator
+date: "2026-05-08"
+problem_type: logic_error
+component: tooling
+severity: medium
+symptoms:
+ - "config.go emitted AccessToken/RefreshToken/TokenExpiry/ClientID/ClientSecret fields when auth.type is none"
+ - "client.go emitted refreshAccessToken() and AccessToken-driven refresh check when shouldEmitAuth() returned false"
+ - "Unused time and net/url imports emitted in config.go and client.go for no-auth specs"
+root_cause: logic_error
+resolution_type: code_fix
+tags:
+ - generator
+ - auth
+ - dead-code
+ - templates
+ - shouldEmitAuth
+ - no-auth
+ - co-gating
+ - test-coverage
+---
+
+## Problem
+
+The Printing Press generator gates `auth.go` emission, the `root.go` `HasAuthCommand` registration, and the scorecard's `scoreAuth` exemption on `Generator.shouldEmitAuth()` (`internal/generator/generator.go:1808`). Two additional artifacts were never wired through the same gate: `config.go.tmpl` and `client.go.tmpl`.
+
+For any spec where `shouldEmitAuth()` returns false — `auth.type: "none"` AND no `AuthorizationURL` AND no `graphql_persisted_query` traffic-analysis hint — the generator emitted:
+
+- **`config.go`**: `AccessToken`, `RefreshToken`, `TokenExpiry`, `ClientID`, `ClientSecret` fields; `SaveTokens()` and `ClearTokens()` methods; the `time` import those methods use.
+- **`client.go`**: `refreshAccessToken()` function; an `AccessToken`-driven refresh check inside `authHeader()`; the `net/url` import that function uses.
+
+None of these symbols had any caller in the emitted CLI. They were dead code with no path to populate them — `auth.go` (the only caller of `SaveTokens`/`ClearTokens` on the CLI surface) was correctly suppressed.
+
+The predicate:
+
+```go
+// internal/generator/generator.go
+func (g *Generator) shouldEmitAuth() bool {
+ return g.Spec.Auth.Type != "none" ||
+ g.Spec.Auth.AuthorizationURL != "" ||
+ g.hasTrafficAnalysisHint("graphql_persisted_query")
+}
+```
+
+The predicate's doc comment enumerates the call sites that "must agree". Before PR #704, only three of the five did.
+
+## Symptoms
+
+- Generated CLIs for no-auth specs compiled successfully but shipped dead `AccessToken`/`RefreshToken`/`TokenExpiry`/`ClientID`/`ClientSecret` fields, `SaveTokens`/`ClearTokens` methods, and `refreshAccessToken()` with no callers.
+- Triage of one such generated CLI concluded "config emission disagrees with auth-template selection — must be a parser bug" (issue #695). The diagnosis was a misread (the fields are universal, not OAuth-specific), but the underlying orphan emission was real.
+- No compile error. Dead code is valid Go. The defect was invisible without inspecting generated output against the auth gate predicate.
+
+## What Didn't Work
+
+Two community-proposed fixes were evaluated and rejected before the correct fix was identified.
+
+**Parser-side fallback (rejected).** The proposal was to scan `info.description` for `Authorization: Bearer` prose and operation parameter examples for `*-Token` headers, with the goal of synthesizing a bearer scheme when the spec lacks `securitySchemes`. Verified against the motivating spec (GitHub's upstream OpenAPI):
+
+- `info.description` is six words: `"GitHub's v3 REST API."` — no auth keywords.
+- Zero `Authorization` header parameters in any operation.
+- Zero top-level `security` blocks; zero `components.securitySchemes`.
+
+`internal/openapi/parser.go`'s existing `inferDescriptionAuth` already covers `bearer`, `access token`, `auth token`, `api key`, `api_key`, `authorization header`. None appear in GitHub's spec. A new regex over the same source would not fire.
+
+**Generator-side fail-fast (rejected).** This proposal built on the misread: it assumed the emitted fields were OAuth-specific and proposed an error when "config has OAuth shape" but no auth template fires. The fields were not OAuth-specific — they were the universal token storage emitted unconditionally for every auth type. A check on "OAuth shape" would either always fire (false positive) or require gating the fields first — at which point the gating *is* the fix, not a check.
+
+**Triage of the originating retro.** The user pain that motivated issue #695 (a generated GitHub CLI with no `auth` command for an API that requires auth) was a workflow execution failure, not a generator bug. `skills/printing-press/SKILL.md:1456` already mandates a Pre-Generation Auth Enrichment step that should have edited the spec when research finds auth signals. The retro that filed #695 noted "10 transcendence features approved at Phase 1.5; 0 implemented" — the enrichment step was skipped. The community-proposed parser fix would have papered over a missed workflow step rather than fixing the generator (auto memory [claude]: "retro default is don't-file — file only when the machine could have raised the floor AND it's generalizable"). The narrower cleanup (this fix) *was* generalizable across every no-auth spec, which is why it landed; the originally-proposed parser fix was not.
+
+## Solution
+
+[PR #704](https://github.com/mvanhorn/cli-printing-press/pull/704). Added `HasAuthCommand bool` to two template-data structs and gated the orphan symbols on that field.
+
+**New struct + field** in `internal/generator/generator.go`:
+
+```go
+type configTemplateData struct {
+ *spec.APISpec
+ HasAuthCommand bool
+}
+
+type clientTemplateData struct {
+ *spec.APISpec
+ HasGraphQLPersistedQueries bool
+ // Populated by Generator.shouldEmitAuth() so this template gate stays in
+ // sync with auth.go emission, root.go registration, and scoreAuth.
+ HasAuthCommand bool
+}
+```
+
+**Population in `renderSingleFiles`** (`internal/generator/generator.go`):
+
+```go
+case "client.go.tmpl":
+ data = &clientTemplateData{
+ APISpec: g.Spec,
+ HasGraphQLPersistedQueries: g.hasTrafficAnalysisHint("graphql_persisted_query"),
+ HasAuthCommand: g.shouldEmitAuth(),
+ }
+case "config.go.tmpl":
+ data = &configTemplateData{
+ APISpec: g.Spec,
+ HasAuthCommand: g.shouldEmitAuth(),
+ }
+```
+
+**`config.go.tmpl` gate** (`internal/generator/templates/config.go.tmpl`): `time` import, all five token fields, and `SaveTokens`/`ClearTokens` are wrapped in `{{- if .HasAuthCommand}} ... {{- end}}`.
+
+**`client.go.tmpl` gate** (`internal/generator/templates/client.go.tmpl`): `net/url` import, `refreshAccessToken()`, and the `AccessToken`-driven refresh check inside `authHeader()` are gated. The `authHeader()` block extends the existing `client_credentials` chain to `{{- else if .HasAuthCommand}}` so the OR-logic matches `shouldEmitAuth()`'s three OR-branches.
+
+**Whitespace gymnastic.** `{{- if .HasAuthCommand}}\n\nfunc...` (with an embedded blank line inside the block) preserves the blank-line separator between functions when the gate fires. The leading `{{- ` strips the preceding blank line in the template source; the embedded blank line replaces it on emission. Required to satisfy `gofmt` and avoid a golden-output diff.
+
+## Why This Works
+
+`Generator.shouldEmitAuth()` is the canonical predicate. Plumbing its return value into both template-data structs makes the templates gate on the exact same condition that gates `auth.go`. All five call sites enumerated in the predicate's doc comment now agree by construction.
+
+When `shouldEmitAuth()` is false, no token field, no token method, no refresh function, and no associated import reach the emitted CLI. When it is true, the full scaffolding emits as before. No caller in either branch is left without its callee.
+
+## Prevention
+
+**1. Predicates whose doc comment says "must agree" need every call site visible at the predicate definition.** Adding a sixth gated artifact in the future is at risk of silently skipping the gate by passing `g.Spec` directly via the `default` arm of the `renderSingleFiles` switch (residual risk surfaced by the maintainability reviewer in PR #704's code review). Prefer explicit `case` arms for every template that needs the gate value, even when the only reason for the arm is to attach the predicate output to template data.
+
+**2. Conditional-emission tests must pin every branch of the predicate, not just the common case.** A regression that short-circuits `shouldEmitAuth()` to one OR-arm passes a single-case test. The test added in PR #704 (`internal/generator/auth_none_dead_code_test.go`) is table-driven with one row per OR-branch:
+
+```go
+tests := []struct {
+ name string
+ auth spec.AuthConfig
+ trafficAnalysisHints []string
+ expect func(t *testing.T, src, sym string)
+}{
+ {name: "no_auth_omits_scaffolding",
+ auth: spec.AuthConfig{Type: "none"},
+ expect: func(t *testing.T, src, sym string) { assert.NotContains(t, src, sym) }},
+ {name: "api_key_keeps_scaffolding",
+ auth: spec.AuthConfig{Type: "api_key", Header: "Authorization", Format: "Bearer {token}", EnvVars: []string{"MYAPI_TOKEN"}},
+ expect: func(t *testing.T, src, sym string) { assert.Contains(t, src, sym) }},
+ {name: "none_with_authorization_url_keeps_scaffolding",
+ auth: spec.AuthConfig{Type: "none", AuthorizationURL: "https://example.com/oauth/authorize"},
+ expect: func(t *testing.T, src, sym string) { assert.Contains(t, src, sym) }},
+ {name: "none_with_graphql_persisted_query_keeps_scaffolding",
+ auth: spec.AuthConfig{Type: "none"},
+ trafficAnalysisHints: []string{"graphql_persisted_query"},
+ expect: func(t *testing.T, src, sym string) { assert.Contains(t, src, sym) }},
+}
+```
+
+**3. String-presence tests on generated source can't catch import-gating mistakes.** `assert.NotContains(t, src, "TokenExpiry")` passes even if a stray `time.Time{}` reference left elsewhere in the template would prevent the generated CLI from compiling (unused import or undefined symbol). After gating symbols out of a template, pair the string-presence assert with `go build` on the generated output:
+
+```go
+runGoCommand(t, outputDir, "mod", "tidy")
+runGoCommand(t, outputDir, "build", "./...")
+```
+
+Use the existing `runGoCommand` helper (`internal/generator/generator_test.go:981`); it auto-skips under `-short` so the unit lane stays fast and full CI catches the regression.
+
+## Related
+
+- [`docs/solutions/logic-errors/inline-authorization-param-bearer-inference-2026-05-05.md`](../logic-errors/inline-authorization-param-bearer-inference-2026-05-05.md) — same broad problem class (auth-emission gates drifting across files) but the inverse failure mode: parser *under-inferring* bearer auth and producing `auth.type: "none"` for an API that needs auth. Together the two docs establish the co-gating invariant from both directions of failure.
+- [`docs/solutions/design-patterns/avoid-classification-when-failure-is-asymmetric-2026-05-06.md`](../design-patterns/avoid-classification-when-failure-is-asymmetric-2026-05-06.md) — establishes `auth.Type` branching as the team's accepted idiom for auth-conditional template emission. PR #704 extends that idiom to structural code files (`config.go`, `client.go`) via the same predicate.
+- [`docs/solutions/best-practices/cross-repo-coordination-with-printing-press-library-2026-05-06.md`](../best-practices/cross-repo-coordination-with-printing-press-library-2026-05-06.md) — template changes that alter the `config.go` / `client.go` field set may break printing-press-library CI; coordinate landing.
+- Issue #695 (open) — the originating triage filing. PR #704 closes the dead-code half; the user pain (no `auth` command on a generated GitHub CLI) is workflow-level, not a generator bug. See PR #704 description for full triage.
diff --git a/internal/generator/auth_none_dead_code_test.go b/internal/generator/auth_none_dead_code_test.go
new file mode 100644
index 00000000..ff42d7fd
--- /dev/null
+++ b/internal/generator/auth_none_dead_code_test.go
@@ -0,0 +1,115 @@
+package generator
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/mvanhorn/cli-printing-press/v4/internal/browsersniff"
+ "github.com/mvanhorn/cli-printing-press/v4/internal/spec"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// Symbols that exist solely to support an `auth` subcommand. They must
+// disappear from generated config.go / client.go for `auth.type: "none"`
+// CLIs (no caller can populate them) and stay for any non-none auth flow.
+var (
+ configTokenScaffolding = []string{
+ "AccessToken",
+ "RefreshToken",
+ "TokenExpiry",
+ "ClientID",
+ "ClientSecret",
+ "SaveTokens",
+ "ClearTokens",
+ }
+ clientTokenScaffolding = []string{
+ "refreshAccessToken",
+ "c.Config.AccessToken",
+ "c.Config.RefreshToken",
+ "c.Config.TokenExpiry",
+ }
+)
+
+// TestTokenScaffoldingFollowsAuthSurface pins all three branches of
+// shouldEmitAuth(): Auth.Type != "none", Auth.AuthorizationURL != "", and a
+// graphql_persisted_query traffic-analysis hint each independently keep the
+// OAuth-shape token scaffolding emitting; only when all three are absent do
+// the symbols disappear (otherwise they would be dead code with no caller on
+// the CLI surface). The cases also verify that the generated CLI still
+// compiles with the gating in place — gated imports falling out of sync with
+// gated function bodies would otherwise pass the symbol-absence assertions
+// but ship a non-buildable CLI.
+func TestTokenScaffoldingFollowsAuthSurface(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ auth spec.AuthConfig
+ trafficAnalysisHints []string
+ expect func(t *testing.T, src, sym string)
+ }{
+ {
+ name: "no_auth_omits_scaffolding",
+ auth: spec.AuthConfig{Type: "none"},
+ expect: func(t *testing.T, src, sym string) { assert.NotContains(t, src, sym) },
+ },
+ {
+ name: "api_key_keeps_scaffolding",
+ auth: spec.AuthConfig{
+ Type: "api_key",
+ Header: "Authorization",
+ Format: "Bearer {token}",
+ EnvVars: []string{"MYAPI_TOKEN"},
+ },
+ expect: func(t *testing.T, src, sym string) { assert.Contains(t, src, sym) },
+ },
+ {
+ name: "none_with_authorization_url_keeps_scaffolding",
+ auth: spec.AuthConfig{
+ Type: "none",
+ AuthorizationURL: "https://example.com/oauth/authorize",
+ },
+ expect: func(t *testing.T, src, sym string) { assert.Contains(t, src, sym) },
+ },
+ {
+ name: "none_with_graphql_persisted_query_keeps_scaffolding",
+ auth: spec.AuthConfig{Type: "none"},
+ trafficAnalysisHints: []string{"graphql_persisted_query"},
+ expect: func(t *testing.T, src, sym string) { assert.Contains(t, src, sym) },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec(tt.name)
+ apiSpec.Auth = tt.auth
+
+ outputDir := filepath.Join(t.TempDir(), tt.name+"-pp-cli")
+ gen := New(apiSpec, outputDir)
+ if len(tt.trafficAnalysisHints) > 0 {
+ gen.TrafficAnalysis = &browsersniff.TrafficAnalysis{GenerationHints: tt.trafficAnalysisHints}
+ }
+ require.NoError(t, gen.Generate())
+
+ configSrc := readGeneratedFile(t, outputDir, "internal", "config", "config.go")
+ for _, sym := range configTokenScaffolding {
+ tt.expect(t, configSrc, sym)
+ }
+
+ clientSrc := readGeneratedFile(t, outputDir, "internal", "client", "client.go")
+ for _, sym := range clientTokenScaffolding {
+ tt.expect(t, clientSrc, sym)
+ }
+
+ // Catch import-gating mistakes: an orphan reference to a gated
+ // symbol (e.g., a stray time.Time{} after TokenExpiry is removed)
+ // would pass the substring asserts but produce non-buildable
+ // generated source. Skipped under -short / unit lane via runGoCommand.
+ runGoCommand(t, outputDir, "mod", "tidy")
+ runGoCommand(t, outputDir, "build", "./...")
+ })
+ }
+}
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 989472d3..98782599 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -628,6 +628,17 @@ type authTemplateData struct {
type clientTemplateData struct {
*spec.APISpec
HasGraphQLPersistedQueries bool
+ // Populated by Generator.shouldEmitAuth() so this template gate stays in
+ // sync with auth.go emission, root.go registration, and scoreAuth.
+ HasAuthCommand bool
+}
+
+// configTemplateData wraps APISpec with a precomputed auth-surface flag so
+// config.go.tmpl can gate token-management fields and helpers on the same
+// predicate the auth-command emission and root.go registration use.
+type configTemplateData struct {
+ *spec.APISpec
+ HasAuthCommand bool
}
// endpointTemplateData is the data passed to command_endpoint.go.tmpl
@@ -1372,6 +1383,12 @@ func (g *Generator) renderSingleFiles() error {
data = &clientTemplateData{
APISpec: g.Spec,
HasGraphQLPersistedQueries: g.hasTrafficAnalysisHint("graphql_persisted_query"),
+ HasAuthCommand: g.shouldEmitAuth(),
+ }
+ case "config.go.tmpl":
+ data = &configTemplateData{
+ APISpec: g.Spec,
+ HasAuthCommand: g.shouldEmitAuth(),
}
case "agent_context.go.tmpl":
data = g.templateData()
diff --git a/internal/generator/templates/client.go.tmpl b/internal/generator/templates/client.go.tmpl
index d134ab75..f3b58cdd 100644
--- a/internal/generator/templates/client.go.tmpl
+++ b/internal/generator/templates/client.go.tmpl
@@ -12,7 +12,9 @@ import (
"io"
"math"
"net/http"
+{{- if .HasAuthCommand}}
"net/url"
+{{- end}}
"os"
"path/filepath"
{{- if ne .ClientPattern "proxy-envelope"}}
@@ -1019,7 +1021,7 @@ func (c *Client) authHeader() (string, error) {
}
c.ccMu.Unlock()
}
-{{- else}}
+{{- else if .HasAuthCommand}}
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
@@ -1103,6 +1105,8 @@ func (c *Client) mintClientCredentials(clientID, clientSecret string) error {
}
{{- end}}
+{{- if .HasAuthCommand}}
+
func (c *Client) refreshAccessToken() error {
if c.Config == nil {
return nil
@@ -1164,6 +1168,7 @@ func (c *Client) refreshAccessToken() error {
return nil
}
+{{- end}}
{{if .HasResourceBaseURLOverride -}}
// isAbsoluteURL reports whether path is already a full URL (per-resource
diff --git a/internal/generator/templates/config.go.tmpl b/internal/generator/templates/config.go.tmpl
index 8bc7f9ea..ecb95d63 100644
--- a/internal/generator/templates/config.go.tmpl
+++ b/internal/generator/templates/config.go.tmpl
@@ -11,7 +11,9 @@ import (
"os"
"path/filepath"
"strings"
+{{- if .HasAuthCommand}}
"time"
+{{- end}}
{{- if eq .Config.Format "toml"}}
"github.com/pelletier/go-toml/v2"
@@ -22,6 +24,7 @@ type Config struct {
BaseURL string `{{configTag .Config.Format}}:"base_url"`
AuthHeaderVal string `{{configTag .Config.Format}}:"auth_header"`
AuthSource string `{{configTag .Config.Format}}:"-"`
+{{- if .HasAuthCommand}}
AccessToken string `{{configTag .Config.Format}}:"access_token"`
RefreshToken string `{{configTag .Config.Format}}:"refresh_token"`
TokenExpiry time.Time `{{configTag .Config.Format}}:"token_expiry"`
@@ -30,6 +33,7 @@ type Config struct {
{{- end}}
ClientID string `{{configTag .Config.Format}}:"client_id"`
ClientSecret string `{{configTag .Config.Format}}:"client_secret"`
+{{- end}}
Path string `{{configTag .Config.Format}}:"-"`
{{- if .Auth.EnvVarSpecs}}
{{- range .Auth.EnvVarSpecs}}
@@ -350,6 +354,8 @@ func applyAuthFormat(format string, replacements map[string]string) string {
return format
}
+{{- if .HasAuthCommand}}
+
func (c *Config) SaveTokens(clientID, clientSecret, accessToken, refreshToken string, expiry time.Time) error {
c.ClientID = clientID
c.ClientSecret = clientSecret
@@ -394,6 +400,7 @@ func (c *Config) ClearTokens() error {
{{- end}}
return c.save()
}
+{{- end}}
func (c *Config) save() error {
dir := filepath.Dir(c.Path)
← 41c87cf8 fix(cli): parse x-mcp extension in OpenAPI parser (#702)
·
back to Cli Printing Press
·
fix(cli): dedupe TypeField identifiers in same struct (#705) d1c3371e →