← back to Cli Printing Press
feat(cli): point users at where to get a token (URL + instructions + auth setup --launch) (#871)
4fac827e8c3e90d77ef2c89017eab6f8c81cf747 · 2026-05-11 01:34:18 -0500 · Giuliano Pezzolo Giacaglia
* feat(cli): infer and surface auth key URL on token prompts
When a printed CLI prompts for a missing token, point the user at the page
where they can obtain one. Adds a parser inference fallback (security scheme
description -> info.description with auth cues -> externalDocs.url ->
info.contact.url), a new catalog auth_key_url field that overrides
inference, and backfilled URLs for 14 catalog entries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cli): add auth setup command with --launch and per-API instructions
Pairs the URL printed by auth prompts with one-line free-form guidance
("Settings -> Personal access tokens -> Generate new") and a new
`auth setup` command that prints the same steps. `--launch` opens the URL
in the user's default browser, short-circuited by cliutil.IsVerifyEnv()
so verifier subprocesses don't pop tabs.
Adds spec.Auth.Instructions, x-auth-instructions extension, catalog
auth_instructions field (overrides spec). Backfills the 14 catalog
entries that already declared auth_key_url. Goldens updated for the new
auth setup subcommand emitted by auth.go.tmpl and auth_simple.go.tmpl.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(cli): point skill template at auth setup for credential discovery
Per AGENTS.md Skill Authoring rule: when a machine change adds an
agent-relevant command, update the skill template in the same PR. The
api_key, oauth2, and bearer_token branches now name `auth setup` as the
discovery step before the existing token-storage commands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): stop labeling docs URLs as credential URLs
The previous inference fell back to externalDocs.url and info.contact.url
when picking Auth.KeyURL, which produced misleading "Get a key at: <docs-url>"
prompts (e.g. Figma's externalDocs is a docs landing page, not a credentials
page). Drop those two fallbacks. KeyURL now requires either an explicit
x-auth-key-url, a catalog auth_key_url override, or an auth-cue-anchored URL
extracted from a security scheme description or info.description.
When KeyURL is empty but the spec has a docs URL (already populated as
WebsiteURL from externalDocs/contact/x-website), the printed CLI surfaces
it under a separate `See API docs: <URL>` line in helpers, doctor,
mcp/tools, and the auth setup command. Same URL, honest framing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): address PR #871 review comments
- parser.go: scan for $ref now handles both JSON ("$ref": ...) and YAML
($ref: ...) formats so the InternalizeRefs skip optimization no longer
silently bypasses external-ref resolution for YAML specs.
- auth.go.tmpl: auth setup --launch now uses openSetupURL which returns
an error and prints a manual-fallback line, matching auth_simple's
behavior. openBrowser remains for auth login.
- docs/CATALOG.md: corrects the inference fallback list to match the
parser (security scheme description + info.description with cues only;
externalDocs.url and info.contact.url are intentionally not fallbacks).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): escape Auth.Instructions in generated Go string literals
Address greptile P1 (and Champworks' review): .Auth.Instructions was
embedded directly inside Go double-quoted string literals in three
templates, so any catalog auth_instructions or x-auth-instructions
value containing " or \ would produce a syntax error in the generated
CLI. The 14 backfilled catalog entries are plain ASCII so no current
output breaks, but the template path accepts arbitrary user text.
Switch all 9 sites to the same pattern mcp_tools.go.tmpl already uses:
{{printf "%q" (printf "<prefix>%s" .Auth.Instructions)}}. This is
byte-identical to the previous output for ASCII-safe values (no golden
drift) but properly escapes special characters.
Sites updated: auth.go.tmpl (1), auth_simple.go.tmpl (1),
helpers.go.tmpl (7 — 401/403 hint paths across api_key, bearer_token,
oauth2, and the no-auth fallback).
Add TestGeneratedHelpers_AuthInstructionsWithSpecialChars_Compiles as
a regression test: generates a CLI with Instructions containing both
" and \, and asserts the result compiles. Verified by temporarily
reverting one template — the test fails with three concrete syntax
errors in auth.go (unexpected name, unknown escape, newline in
string), confirming it catches the bug.
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
M AGENTS.mdM catalog/asana.yamlM catalog/digitalocean.yamlM catalog/discord.yamlM catalog/front.yamlM catalog/github.yamlM catalog/hubspot.yamlM catalog/launchdarkly.yamlM catalog/mercury.yamlM catalog/pipedrive.yamlM catalog/plaid.yamlM catalog/sentry.yamlM catalog/stripe.yamlM catalog/stytch.yamlM catalog/twilio.yamlM docs/CATALOG.mdM docs/SPEC-EXTENSIONS.mdM internal/catalog/catalog.goM internal/catalog/catalog_test.goM internal/cli/root.goM internal/generator/generator_test.goM internal/generator/templates/auth.go.tmplM internal/generator/templates/auth_simple.go.tmplM internal/generator/templates/doctor.go.tmplM internal/generator/templates/helpers.go.tmplM internal/generator/templates/mcp_tools.go.tmplM internal/generator/templates/skill.md.tmplM internal/openapi/parser.goM internal/openapi/parser_test.goM internal/spec/spec.goM testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/SKILL.mdM testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/auth.goM testdata/golden/expected/generate-golden-api/dogfood.jsonM testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.mdM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/auth.go
Diff
commit 4fac827e8c3e90d77ef2c89017eab6f8c81cf747
Author: Giuliano Pezzolo Giacaglia <raulzito234@gmail.com>
Date: Mon May 11 01:34:18 2026 -0500
feat(cli): point users at where to get a token (URL + instructions + auth setup --launch) (#871)
* feat(cli): infer and surface auth key URL on token prompts
When a printed CLI prompts for a missing token, point the user at the page
where they can obtain one. Adds a parser inference fallback (security scheme
description -> info.description with auth cues -> externalDocs.url ->
info.contact.url), a new catalog auth_key_url field that overrides
inference, and backfilled URLs for 14 catalog entries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(cli): add auth setup command with --launch and per-API instructions
Pairs the URL printed by auth prompts with one-line free-form guidance
("Settings -> Personal access tokens -> Generate new") and a new
`auth setup` command that prints the same steps. `--launch` opens the URL
in the user's default browser, short-circuited by cliutil.IsVerifyEnv()
so verifier subprocesses don't pop tabs.
Adds spec.Auth.Instructions, x-auth-instructions extension, catalog
auth_instructions field (overrides spec). Backfills the 14 catalog
entries that already declared auth_key_url. Goldens updated for the new
auth setup subcommand emitted by auth.go.tmpl and auth_simple.go.tmpl.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(cli): point skill template at auth setup for credential discovery
Per AGENTS.md Skill Authoring rule: when a machine change adds an
agent-relevant command, update the skill template in the same PR. The
api_key, oauth2, and bearer_token branches now name `auth setup` as the
discovery step before the existing token-storage commands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): stop labeling docs URLs as credential URLs
The previous inference fell back to externalDocs.url and info.contact.url
when picking Auth.KeyURL, which produced misleading "Get a key at: <docs-url>"
prompts (e.g. Figma's externalDocs is a docs landing page, not a credentials
page). Drop those two fallbacks. KeyURL now requires either an explicit
x-auth-key-url, a catalog auth_key_url override, or an auth-cue-anchored URL
extracted from a security scheme description or info.description.
When KeyURL is empty but the spec has a docs URL (already populated as
WebsiteURL from externalDocs/contact/x-website), the printed CLI surfaces
it under a separate `See API docs: <URL>` line in helpers, doctor,
mcp/tools, and the auth setup command. Same URL, honest framing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): address PR #871 review comments
- parser.go: scan for $ref now handles both JSON ("$ref": ...) and YAML
($ref: ...) formats so the InternalizeRefs skip optimization no longer
silently bypasses external-ref resolution for YAML specs.
- auth.go.tmpl: auth setup --launch now uses openSetupURL which returns
an error and prints a manual-fallback line, matching auth_simple's
behavior. openBrowser remains for auth login.
- docs/CATALOG.md: corrects the inference fallback list to match the
parser (security scheme description + info.description with cues only;
externalDocs.url and info.contact.url are intentionally not fallbacks).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): escape Auth.Instructions in generated Go string literals
Address greptile P1 (and Champworks' review): .Auth.Instructions was
embedded directly inside Go double-quoted string literals in three
templates, so any catalog auth_instructions or x-auth-instructions
value containing " or \ would produce a syntax error in the generated
CLI. The 14 backfilled catalog entries are plain ASCII so no current
output breaks, but the template path accepts arbitrary user text.
Switch all 9 sites to the same pattern mcp_tools.go.tmpl already uses:
{{printf "%q" (printf "<prefix>%s" .Auth.Instructions)}}. This is
byte-identical to the previous output for ASCII-safe values (no golden
drift) but properly escapes special characters.
Sites updated: auth.go.tmpl (1), auth_simple.go.tmpl (1),
helpers.go.tmpl (7 — 401/403 hint paths across api_key, bearer_token,
oauth2, and the no-auth fallback).
Add TestGeneratedHelpers_AuthInstructionsWithSpecialChars_Compiles as
a regression test: generates a CLI with Instructions containing both
" and \, and asserts the result compiles. Verified by temporarily
reverting one template — the test fails with three concrete syntax
errors in auth.go (unexpected name, unknown escape, newline in
string), confirming it catches the bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
AGENTS.md | 2 +
catalog/asana.yaml | 2 +
catalog/digitalocean.yaml | 2 +
catalog/discord.yaml | 2 +
catalog/front.yaml | 2 +
catalog/github.yaml | 2 +
catalog/hubspot.yaml | 2 +
catalog/launchdarkly.yaml | 2 +
catalog/mercury.yaml | 2 +
catalog/pipedrive.yaml | 2 +
catalog/plaid.yaml | 2 +
catalog/sentry.yaml | 2 +
catalog/stripe.yaml | 2 +
catalog/stytch.yaml | 2 +
catalog/twilio.yaml | 2 +
docs/CATALOG.md | 17 ++
docs/SPEC-EXTENSIONS.md | 36 ++++
internal/catalog/catalog.go | 16 ++
internal/catalog/catalog_test.go | 7 +
internal/cli/root.go | 6 +
internal/generator/generator_test.go | 41 +++++
internal/generator/templates/auth.go.tmpl | 73 ++++++++
internal/generator/templates/auth_simple.go.tmpl | 88 ++++++++++
internal/generator/templates/doctor.go.tmpl | 14 ++
internal/generator/templates/helpers.go.tmpl | 35 ++++
internal/generator/templates/mcp_tools.go.tmpl | 20 +++
internal/generator/templates/skill.md.tmpl | 6 +-
internal/openapi/parser.go | 123 +++++++++++++-
internal/openapi/parser_test.go | 186 +++++++++++++++++++++
internal/spec/spec.go | 11 +-
.../printing-press-rich-auth/SKILL.md | 2 +-
.../printing-press-rich-auth/internal/cli/auth.go | 32 +++-
.../expected/generate-golden-api/dogfood.json | 4 +-
.../printing-press-golden/SKILL.md | 2 +-
.../printing-press-golden/internal/cli/auth.go | 29 +++-
35 files changed, 763 insertions(+), 15 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 8e073654..fadfcf0c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -162,6 +162,8 @@ When adding or editing `catalog/*.yaml`, first decide whether the entry belongs
- `category` must be one of `ai`, `auth`, `cloud`, `commerce`, `developer-tools`, `devices`, `food-and-dining`, `marketing`, `media-and-entertainment`, `monitoring`, `payments`, `productivity`, `project-management`, `sales-and-crm`, `social-and-messaging`, `travel`, or `other`. The validator also accepts `example` as a test-only catch-all; do not use it for real catalog entries.
- `tier` must be `official` or `community`.
- `bearer_refresh`, when present, must include `bundle_url` and `pattern`; `bundle_url` must use HTTPS, and `pattern` must compile as a Go regexp.
+- `auth_key_url`, when present, must use HTTPS. It overrides any URL inferred from the spec and surfaces in the printed CLI as `Get a key at: <URL>`.
+- `auth_instructions`, when present, is a one-line string rendered under the URL. It overrides any `x-auth-instructions` value from the spec.
- Rebuild the binary after editing; `catalog.FS` is a Go embed.
See [`docs/CATALOG.md`](docs/CATALOG.md) for the inclusion rubric, evidence checklist, validation rationale, wrapper-only entry shape, and bearer-refresh metadata.
diff --git a/catalog/asana.yaml b/catalog/asana.yaml
index ec373e0e..187ee2e5 100644
--- a/catalog/asana.yaml
+++ b/catalog/asana.yaml
@@ -8,4 +8,6 @@ openapi_version: "3.0"
tier: official
verified_date: "2026-03-23"
homepage: https://developers.asana.com/reference
+auth_key_url: https://app.asana.com/0/my-apps
+auth_instructions: "Create a personal access token under Apps & Integrations."
notes: None.
diff --git a/catalog/digitalocean.yaml b/catalog/digitalocean.yaml
index fba59e7e..558f9836 100644
--- a/catalog/digitalocean.yaml
+++ b/catalog/digitalocean.yaml
@@ -8,4 +8,6 @@ openapi_version: "3.0"
tier: official
verified_date: "2026-03-23"
homepage: https://docs.digitalocean.com/reference/api
+auth_key_url: https://cloud.digitalocean.com/account/api/tokens
+auth_instructions: "Generate a personal access token with read or write scope as needed."
notes: None.
diff --git a/catalog/discord.yaml b/catalog/discord.yaml
index 879563a4..e801e266 100644
--- a/catalog/discord.yaml
+++ b/catalog/discord.yaml
@@ -8,4 +8,6 @@ openapi_version: "3.1"
tier: official
verified_date: "2026-03-23"
homepage: https://discord.com/developers/docs
+auth_key_url: https://discord.com/developers/applications
+auth_instructions: "Create an application, add a bot, and copy the bot token from the Bot tab."
notes: Very large spec (1MB+). Generator truncates to 50 resources / 20 endpoints per resource.
diff --git a/catalog/front.yaml b/catalog/front.yaml
index 1ee987e2..de0a5b4b 100644
--- a/catalog/front.yaml
+++ b/catalog/front.yaml
@@ -8,4 +8,6 @@ openapi_version: "3.0"
tier: official
verified_date: "2026-03-23"
homepage: https://dev.frontapp.com/reference
+auth_key_url: https://app.frontapp.com/settings/tokens
+auth_instructions: "Generate an API token under Settings -> Developers -> API tokens."
notes: None.
diff --git a/catalog/github.yaml b/catalog/github.yaml
index 69d9b507..192c113d 100644
--- a/catalog/github.yaml
+++ b/catalog/github.yaml
@@ -8,6 +8,8 @@ openapi_version: "3.0"
tier: official
verified_date: "2026-03-23"
homepage: https://docs.github.com/en/rest
+auth_key_url: https://github.com/settings/tokens
+auth_instructions: "Generate a fine-grained personal access token (or classic) with the scopes your workflow needs."
notes: Very large spec. Generator truncates significantly.
known_alternatives:
- name: gh
diff --git a/catalog/hubspot.yaml b/catalog/hubspot.yaml
index 36800e75..03e6502b 100644
--- a/catalog/hubspot.yaml
+++ b/catalog/hubspot.yaml
@@ -8,4 +8,6 @@ openapi_version: "3.0"
tier: official
verified_date: "2026-03-23"
homepage: https://developers.hubspot.com/docs/api
+auth_key_url: https://app.hubspot.com/private-apps
+auth_instructions: "Create a private app, grant CRM scopes, and copy the access token."
notes: Only covers CRM Contacts endpoint. HubSpot has many separate specs.
diff --git a/catalog/launchdarkly.yaml b/catalog/launchdarkly.yaml
index fdb99b18..feaec3dc 100644
--- a/catalog/launchdarkly.yaml
+++ b/catalog/launchdarkly.yaml
@@ -8,4 +8,6 @@ openapi_version: "3.0"
tier: community
verified_date: "2026-03-24"
homepage: https://launchdarkly.com
+auth_key_url: https://app.launchdarkly.com/settings/authorization
+auth_instructions: "Create an access token with the role and project scope your workflow needs."
notes: "221 paths, API key auth. Flat 'api' resource due to /api/v2/ prefix."
diff --git a/catalog/mercury.yaml b/catalog/mercury.yaml
index d16bbcbd..29565983 100644
--- a/catalog/mercury.yaml
+++ b/catalog/mercury.yaml
@@ -8,6 +8,8 @@ openapi_version: "3.0"
tier: official
verified_date: "2026-05-06"
homepage: https://docs.mercury.com/reference
+auth_key_url: https://app.mercury.com/settings/tokens
+auth_instructions: "Create an API token under Settings -> API and treat it as production credentials."
owner_name: Cathryn Lavery
mcp:
transport: [stdio, http]
diff --git a/catalog/pipedrive.yaml b/catalog/pipedrive.yaml
index f2bb3e3b..b85936aa 100644
--- a/catalog/pipedrive.yaml
+++ b/catalog/pipedrive.yaml
@@ -8,4 +8,6 @@ openapi_version: "3.0"
tier: official
verified_date: "2026-03-25"
homepage: https://developers.pipedrive.com
+auth_key_url: https://app.pipedrive.com/settings/personal/api
+auth_instructions: "Copy your personal API token from Personal preferences -> API."
notes: "Zero CLI competition. CRM from the terminal."
diff --git a/catalog/plaid.yaml b/catalog/plaid.yaml
index ffccb912..8cdb0375 100644
--- a/catalog/plaid.yaml
+++ b/catalog/plaid.yaml
@@ -8,6 +8,8 @@ openapi_version: "3.0"
tier: community
verified_date: "2026-03-25"
homepage: https://plaid.com/docs/api
+auth_key_url: https://dashboard.plaid.com/team/keys
+auth_instructions: "Use Sandbox keys (PLAID_ENV=sandbox) until you have completed Plaid review for production."
notes: "Fintech devs' go-to banking API. Abandoned community CLI (57 stars) proves demand."
known_alternatives:
- name: plaid-cli
diff --git a/catalog/sentry.yaml b/catalog/sentry.yaml
index fb4d786f..4ce05f4c 100644
--- a/catalog/sentry.yaml
+++ b/catalog/sentry.yaml
@@ -8,4 +8,6 @@ openapi_version: "3.0"
tier: community
verified_date: "2026-03-24"
homepage: https://sentry.io
+auth_key_url: https://sentry.io/settings/account/api/auth-tokens/
+auth_instructions: "Create an auth token with the scopes your workflow needs (project:read, event:read, etc.)."
notes: "126 paths, bearer auth. Templated base URL {region}.sentry.io needs manual config."
diff --git a/catalog/stripe.yaml b/catalog/stripe.yaml
index e278b23c..ae8aa08b 100644
--- a/catalog/stripe.yaml
+++ b/catalog/stripe.yaml
@@ -8,6 +8,8 @@ openapi_version: "3.0"
tier: official
verified_date: "2026-03-23"
homepage: https://stripe.com/docs/api
+auth_key_url: https://dashboard.stripe.com/apikeys
+auth_instructions: "Use a test mode key (sk_test_*) unless you intend to charge live cards."
notes: Very large spec (~500 endpoints). Generator truncates significantly.
known_alternatives:
- name: stripe-cli
diff --git a/catalog/stytch.yaml b/catalog/stytch.yaml
index d4133322..6f306256 100644
--- a/catalog/stytch.yaml
+++ b/catalog/stytch.yaml
@@ -8,4 +8,6 @@ openapi_version: "3.0"
tier: official
verified_date: "2026-03-23"
homepage: https://stytch.com/docs
+auth_key_url: https://stytch.com/dashboard/api-keys
+auth_instructions: "Use Test environment keys until you have switched the project to Live."
notes: None.
diff --git a/catalog/twilio.yaml b/catalog/twilio.yaml
index 2ceed19d..c6dc0777 100644
--- a/catalog/twilio.yaml
+++ b/catalog/twilio.yaml
@@ -8,4 +8,6 @@ openapi_version: "3.0"
tier: official
verified_date: "2026-03-23"
homepage: https://www.twilio.com/docs/usage/api
+auth_key_url: https://console.twilio.com/
+auth_instructions: "Copy your Account SID and Auth Token from the console homepage."
notes: None.
diff --git a/docs/CATALOG.md b/docs/CATALOG.md
index dd4492ad..6e167285 100644
--- a/docs/CATALOG.md
+++ b/docs/CATALOG.md
@@ -79,3 +79,20 @@ If the validator or enum values change, update both this doc and the inline `AGE
Catalog entries for browser-facing APIs with rotating public client bearer tokens may declare `bearer_refresh`. When present, both `bearer_refresh.bundle_url` and `bearer_refresh.pattern` are required, the bundle URL must use HTTPS, and the pattern must compile as a Go regexp.
The generator copies this metadata into the printed CLI so `doctor --refresh-bearer` and the agent-accessible `refresh-bearer` command can refresh the user's stored token from the live source bundle.
+
+## Auth key URL
+
+Catalog entries may declare `auth_key_url:` — an HTTPS page where the user can obtain credentials (personal access token, API key, OAuth client, etc.). The generator surfaces it in the printed CLI's auth prompts and `doctor` output as `Get a key at: <URL>`.
+
+Precedence:
+- Catalog `auth_key_url` overrides any URL from the spec.
+- Otherwise, an OpenAPI spec's [`x-auth-key-url`](SPEC-EXTENSIONS.md#x-auth-key-url) is used.
+- Otherwise, the parser infers a URL from the selected security scheme's `description`, then from `info.description` when the surrounding text mentions credential cues. `externalDocs.url` and `info.contact.url` are intentionally **not** fallbacks — those typically point at the docs landing page or company homepage, not the keys UI. When `KeyURL` is empty, the printed CLI surfaces those URLs under a separate `See API docs:` line instead. See [`SPEC-EXTENSIONS.md`](SPEC-EXTENSIONS.md#x-auth-key-url) for details.
+
+Set `auth_key_url:` when the inference would land on a generic homepage and you know the specific token-acquisition page. The validator only checks that the URL starts with `https://`; it does not probe reachability.
+
+## Auth instructions
+
+Catalog entries may also declare `auth_instructions:` — a one-line string of free-form guidance ("Settings → Personal access tokens → Generate new") that the printed CLI prints under the `Get a key at:` line. Use this when the URL lands on a docs page rather than the keys UI: the URL says where to start, the instruction says what to do once there.
+
+Catalog `auth_instructions` overrides any value from the spec's [`x-auth-instructions`](SPEC-EXTENSIONS.md#x-auth-instructions) extension. The printed CLI surfaces it in auth prompts, `doctor`, and the new `auth setup` command (which also takes `--launch` to open the URL in a browser).
diff --git a/docs/SPEC-EXTENSIONS.md b/docs/SPEC-EXTENSIONS.md
index d29933e3..517a2148 100644
--- a/docs/SPEC-EXTENSIONS.md
+++ b/docs/SPEC-EXTENSIONS.md
@@ -374,6 +374,42 @@ Rules:
- Leading and trailing whitespace is trimmed.
- The parser does not validate the URL shape.
+When the extension is absent and the spec has any auth, the parser falls back
+through the following sources in order and uses the first plausible HTTPS URL:
+
+1. The selected security scheme's `description` (extracted via regex).
+2. `info.description`, but only when the surrounding text mentions
+ credential-related cues (`token`, `api key`, `credential`, `register`,
+ `sign up`, etc.) so an unrelated URL doesn't get picked.
+
+`externalDocs.url` and `info.contact.url` are intentionally **not** fallbacks
+for `KeyURL`. Those almost always point at the API's docs landing page or the
+company homepage, neither of which is where users actually create a token.
+When `KeyURL` ends up empty, the printed CLI uses `WebsiteURL` (already
+populated from `externalDocs.url`, `info.contact.url`, and `x-website`) under
+a separate `See API docs: <URL>` line — honest framing for those URLs.
+
+Catalog YAML's `auth_key_url:` (see [`CATALOG.md`](CATALOG.md)) overrides the
+inference. The result drives the printed CLI's `Get a key at: <URL>` output in
+auth prompts and `doctor`.
+
+### `x-auth-instructions`
+
+Free-form one-line guidance shown alongside `x-auth-key-url`, e.g. "Settings →
+Personal access tokens → Generate new". The printed CLI surfaces this under
+the URL in auth prompts, `doctor`, and the `auth setup` command.
+
+Parsed field: `APISpec.Auth.Instructions`
+
+Rules:
+- Optional.
+- Must be a string.
+- Leading and trailing whitespace is trimmed.
+- Use this when `x-auth-key-url` lands on a docs page rather than the keys UI;
+ the URL says where to start, the instruction says what to do once there.
+
+Catalog YAML's `auth_instructions:` overrides any spec-supplied value.
+
### `x-auth-title`
Overrides the title shown for the credential field in install/config surfaces.
diff --git a/internal/catalog/catalog.go b/internal/catalog/catalog.go
index 46aa05e1..2fef309b 100644
--- a/internal/catalog/catalog.go
+++ b/internal/catalog/catalog.go
@@ -128,6 +128,19 @@ type Entry struct {
SpecSource string `yaml:"spec_source,omitempty"`
// AuthRequired indicates whether the API needs authentication. Empty means unknown.
AuthRequired *bool `yaml:"auth_required,omitempty"`
+ // AuthKeyURL is an HTTPS URL pointing the user at the page where they can
+ // obtain credentials (a personal access token, API key, OAuth client, etc.).
+ // Surfaces as "Get a key at: <URL>" in the printed CLI's auth prompts and
+ // doctor output. Overrides any URL inferred from the spec; the spec's
+ // x-auth-key-url and parser inference fallbacks are used when this is empty.
+ AuthKeyURL string `yaml:"auth_key_url,omitempty"`
+ // AuthInstructions is one-line free-form guidance shown alongside
+ // AuthKeyURL — e.g. "Settings → Personal access tokens → Generate new".
+ // Renders under the URL in auth prompts and doctor output, and is the
+ // human-readable companion to the URL when the URL is a generic docs page
+ // rather than a deep link to the keys UI. Overrides any spec-supplied
+ // x-auth-instructions value.
+ AuthInstructions string `yaml:"auth_instructions,omitempty"`
// ClientPattern describes the HTTP client pattern needed. Empty defaults to "rest".
// Values: rest, proxy-envelope, graphql.
ClientPattern string `yaml:"client_pattern,omitempty"`
@@ -301,6 +314,9 @@ func (e *Entry) Validate() error {
if err := validateBearerRefresh(e.BearerRefresh); err != nil {
return err
}
+ if e.AuthKeyURL != "" && !strings.HasPrefix(e.AuthKeyURL, "https://") {
+ return fmt.Errorf(`auth_key_url must start with "https://"`)
+ }
return nil
}
diff --git a/internal/catalog/catalog_test.go b/internal/catalog/catalog_test.go
index 31fa0691..cc14be63 100644
--- a/internal/catalog/catalog_test.go
+++ b/internal/catalog/catalog_test.go
@@ -167,6 +167,13 @@ func TestValidateEntry(t *testing.T) {
},
wantErr: "bearer_refresh.pattern is not a valid regexp",
},
+ {
+ name: "non https auth_key_url",
+ mutate: func(e *Entry) {
+ e.AuthKeyURL = "http://example.com/keys"
+ },
+ wantErr: `auth_key_url must start with "https://"`,
+ },
}
for _, tt := range tests {
diff --git a/internal/cli/root.go b/internal/cli/root.go
index 077e709a..033465f7 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -1589,6 +1589,12 @@ func enrichSpecFromCatalogEntry(apiSpec *spec.APISpec, entry *catalog.Entry) {
if entry.BearerRefresh.Pattern != "" && apiSpec.BearerRefresh.Pattern == "" {
apiSpec.BearerRefresh.Pattern = entry.BearerRefresh.Pattern
}
+ if entry.AuthKeyURL != "" && apiSpec.Auth.Type != "none" {
+ apiSpec.Auth.KeyURL = entry.AuthKeyURL
+ }
+ if entry.AuthInstructions != "" && apiSpec.Auth.Type != "none" {
+ apiSpec.Auth.Instructions = entry.AuthInstructions
+ }
}
func mcpConfigured(m spec.MCPConfig) bool {
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index cbb91ee1..184f755c 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -4627,6 +4627,47 @@ func TestGeneratedHelpers_AuthWithKeyURL_Compiles(t *testing.T) {
runGoCommand(t, outputDir, "build", "./...")
}
+// Adversarial Instructions values must not break generated Go. The auth setup,
+// helpers (401/403 hint paths), and MCP tool description templates all embed
+// .Auth.Instructions inside Go string literals; without %q escaping a value
+// containing " or \ produces a syntax error at template-render time.
+func TestGeneratedHelpers_AuthInstructionsWithSpecialChars_Compiles(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := &spec.APISpec{
+ Name: "instrescape",
+ Version: "0.1.0",
+ BaseURL: "https://api.example.com",
+ Auth: spec.AuthConfig{
+ Type: "api_key",
+ Header: "Authorization",
+ In: "header",
+ EnvVars: []string{"INSTRESCAPE_API_KEY"},
+ KeyURL: "https://example.com/keys",
+ Instructions: `Settings → "Personal access tokens" → \Generate new\`,
+ },
+ Config: spec.ConfigSpec{
+ Format: "toml",
+ Path: "~/.config/instrescape-pp-cli/config.toml",
+ },
+ Resources: map[string]spec.Resource{
+ "users": {
+ Description: "Manage users",
+ Endpoints: map[string]spec.Endpoint{
+ "list": {Method: "GET", Path: "/users", Description: "List users"},
+ },
+ },
+ },
+ }
+
+ outputDir := filepath.Join(t.TempDir(), "instrescape-pp-cli")
+ gen := New(apiSpec, outputDir)
+ require.NoError(t, gen.Generate())
+
+ runGoCommand(t, outputDir, "mod", "tidy")
+ runGoCommand(t, outputDir, "build", "./...")
+}
+
// --- Unit 4: Doctor Auth Hint Tests ---
func TestGeneratedDoctor_AuthHintsWithKeyURL(t *testing.T) {
diff --git a/internal/generator/templates/auth.go.tmpl b/internal/generator/templates/auth.go.tmpl
index 71bb8a30..f9eb72d2 100644
--- a/internal/generator/templates/auth.go.tmpl
+++ b/internal/generator/templates/auth.go.tmpl
@@ -18,6 +18,9 @@ import (
"strings"
"time"
+{{- if or .Auth.KeyURL .WebsiteURL}}
+ "{{modulePath}}/internal/cliutil"
+{{- end}}
"{{modulePath}}/internal/config"
"github.com/spf13/cobra"
)
@@ -28,6 +31,7 @@ func newAuthCmd(flags *rootFlags) *cobra.Command {
Short: {{printf "%q" (authCommandShort .APISpec)}},
}
+ cmd.AddCommand(newAuthSetupCmd(flags))
cmd.AddCommand(newAuthLoginCmd(flags))
cmd.AddCommand(newAuthStatusCmd(flags))
cmd.AddCommand(newAuthLogoutCmd(flags))
@@ -35,6 +39,75 @@ func newAuthCmd(flags *rootFlags) *cobra.Command {
return cmd
}
+// newAuthSetupCmd prints concrete steps for registering an OAuth app and
+// obtaining client credentials. Side-effect rule: print by default, --launch
+// to open the registration URL, short-circuit when the verifier is running.
+func newAuthSetupCmd(_ *rootFlags) *cobra.Command {
+ var launch bool
+ cmd := &cobra.Command{
+ Use: "setup",
+ Short: "Print steps for registering an OAuth app (use --launch to open the URL)",
+ Example: " {{.Name}}-pp-cli auth setup\n {{.Name}}-pp-cli auth setup --launch",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ w := cmd.OutOrStdout()
+{{- if .Auth.KeyURL}}
+ fmt.Fprintln(w, "Register an app and copy your credentials at: {{.Auth.KeyURL}}")
+{{- else if .WebsiteURL}}
+ fmt.Fprintln(w, "See API docs for OAuth app registration: {{.WebsiteURL}}")
+{{- else}}
+ fmt.Fprintln(w, "No setup URL is configured for this CLI; check the API's docs.")
+{{- end}}
+{{- if .Auth.Instructions}}
+ fmt.Fprintln(w, {{printf "%q" (printf " %s" .Auth.Instructions)}})
+{{- end}}
+ fmt.Fprintln(w, "")
+ fmt.Fprintln(w, "Then run:")
+ fmt.Fprintln(w, " {{.Name}}-pp-cli auth login --client-id <id> --client-secret <secret>")
+ if !launch {
+ return nil
+ }
+{{- if or .Auth.KeyURL .WebsiteURL}}
+ launchURL := {{if .Auth.KeyURL}}"{{.Auth.KeyURL}}"{{else}}"{{.WebsiteURL}}"{{end}}
+ if cliutil.IsVerifyEnv() {
+ fmt.Fprintf(w, "would launch: %s\n", launchURL)
+ return nil
+ }
+ if err := openSetupURL(launchURL); err != nil {
+ fmt.Fprintf(cmd.ErrOrStderr(), "could not open browser automatically: %v\nopen this URL manually: %s\n", err, launchURL)
+ }
+{{- else}}
+ fmt.Fprintln(cmd.ErrOrStderr(), "no setup URL configured; cannot launch")
+{{- end}}
+ return nil
+ },
+ }
+ cmd.Flags().BoolVar(&launch, "launch", false, "Open the setup URL in your default browser")
+ return cmd
+}
+
+{{- if or .Auth.KeyURL .WebsiteURL}}
+
+// openSetupURL opens url in the OS default browser and returns any error
+// from the launch. Distinct from openBrowser (used by `auth login`) because
+// `auth setup --launch` prints a manual-fallback message on failure rather
+// than silently failing. Per the side-effect rule, the caller short-circuits
+// with cliutil.IsVerifyEnv() before this is reached.
+func openSetupURL(url string) error {
+ var c *exec.Cmd
+ switch runtime.GOOS {
+ case "darwin":
+ c = exec.Command("open", url)
+ case "linux":
+ c = exec.Command("xdg-open", url)
+ case "windows":
+ c = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
+ default:
+ return fmt.Errorf("unsupported OS: %s", runtime.GOOS)
+ }
+ return c.Start()
+}
+{{- end}}
+
func newAuthLoginCmd(flags *rootFlags) *cobra.Command {
var clientID string
var clientSecret string
diff --git a/internal/generator/templates/auth_simple.go.tmpl b/internal/generator/templates/auth_simple.go.tmpl
index db6109ee..d6e0bc92 100644
--- a/internal/generator/templates/auth_simple.go.tmpl
+++ b/internal/generator/templates/auth_simple.go.tmpl
@@ -5,10 +5,17 @@ package cli
import (
"fmt"
+{{- if or .Auth.KeyURL .WebsiteURL}}
+ "os/exec"
+ "runtime"
+{{- end}}
{{- if .Auth.EnvVars}}
"os"
{{- end}}
+{{- if or .Auth.KeyURL .WebsiteURL}}
+ "{{modulePath}}/internal/cliutil"
+{{- end}}
"{{modulePath}}/internal/config"
"github.com/spf13/cobra"
)
@@ -19,6 +26,7 @@ func newAuthCmd(flags *rootFlags) *cobra.Command {
Short: {{printf "%q" (authCommandShort .APISpec)}},
}
+ cmd.AddCommand(newAuthSetupCmd(flags))
cmd.AddCommand(newAuthStatusCmd(flags))
cmd.AddCommand(newAuthSetTokenCmd(flags))
cmd.AddCommand(newAuthLogoutCmd(flags))
@@ -26,6 +34,86 @@ func newAuthCmd(flags *rootFlags) *cobra.Command {
return cmd
}
+// newAuthSetupCmd prints concrete steps for getting a credential. Side-effect
+// rule: print by default, --launch opt-in to open the URL, short-circuit when
+// the verifier is running this in a sandboxed subprocess.
+func newAuthSetupCmd(_ *rootFlags) *cobra.Command {
+ var launch bool
+ cmd := &cobra.Command{
+ Use: "setup",
+ Short: "Print steps for obtaining a credential (use --launch to open the URL)",
+ Example: " {{.Name}}-pp-cli auth setup\n {{.Name}}-pp-cli auth setup --launch",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ w := cmd.OutOrStdout()
+{{- if .Auth.KeyURL}}
+ fmt.Fprintln(w, "Get a key at: {{.Auth.KeyURL}}")
+{{- else if .WebsiteURL}}
+ fmt.Fprintln(w, "See API docs: {{.WebsiteURL}}")
+{{- else}}
+ fmt.Fprintln(w, "No setup URL is configured for this CLI; check the API's docs.")
+{{- end}}
+{{- if .Auth.Instructions}}
+ fmt.Fprintln(w, {{printf "%q" (printf " %s" .Auth.Instructions)}})
+{{- end}}
+{{- if .Auth.EnvVarSpecs}}
+ fmt.Fprintln(w, "")
+ fmt.Fprintln(w, "Then set:")
+{{- range .Auth.EnvVarSpecs}}
+{{- if isRequestAuthEnvVar .}}
+ fmt.Fprintln(w, " export {{.Name}}=\"<your-token>\"")
+{{- end}}
+{{- end}}
+ fmt.Fprintln(w, " {{.Name}}-pp-cli auth set-token <token>")
+{{- else if .Auth.EnvVars}}
+ fmt.Fprintln(w, "")
+ fmt.Fprintln(w, "Then set:")
+{{- range .Auth.EnvVars}}
+ fmt.Fprintln(w, " export {{.}}=\"<your-token>\"")
+{{- end}}
+ fmt.Fprintln(w, " {{.Name}}-pp-cli auth set-token <token>")
+{{- end}}
+ if !launch {
+ return nil
+ }
+{{- if or .Auth.KeyURL .WebsiteURL}}
+ launchURL := {{if .Auth.KeyURL}}"{{.Auth.KeyURL}}"{{else}}"{{.WebsiteURL}}"{{end}}
+ if cliutil.IsVerifyEnv() {
+ fmt.Fprintf(w, "would launch: %s\n", launchURL)
+ return nil
+ }
+ if err := openSetupURL(launchURL); err != nil {
+ fmt.Fprintf(cmd.ErrOrStderr(), "could not open browser automatically: %v\nopen this URL manually: %s\n", err, launchURL)
+ }
+{{- else}}
+ fmt.Fprintln(cmd.ErrOrStderr(), "no setup URL configured; cannot launch")
+{{- end}}
+ return nil
+ },
+ }
+ cmd.Flags().BoolVar(&launch, "launch", false, "Open the setup URL in your default browser")
+ return cmd
+}
+
+{{- if or .Auth.KeyURL .WebsiteURL}}
+
+// openSetupURL opens url in the OS default browser. Per the side-effect rule,
+// the caller short-circuits with cliutil.IsVerifyEnv() before this is reached.
+func openSetupURL(url string) error {
+ var c *exec.Cmd
+ switch runtime.GOOS {
+ case "darwin":
+ c = exec.Command("open", url)
+ case "linux":
+ c = exec.Command("xdg-open", url)
+ case "windows":
+ c = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
+ default:
+ return fmt.Errorf("unsupported OS: %s", runtime.GOOS)
+ }
+ return c.Start()
+}
+{{- end}}
+
func newAuthStatusCmd(flags *rootFlags) *cobra.Command {
return &cobra.Command{
Use: "status",
diff --git a/internal/generator/templates/doctor.go.tmpl b/internal/generator/templates/doctor.go.tmpl
index 99a88721..02be3b72 100644
--- a/internal/generator/templates/doctor.go.tmpl
+++ b/internal/generator/templates/doctor.go.tmpl
@@ -205,6 +205,11 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
{{- end}}
{{- if .Auth.KeyURL}}
report["auth_key_url"] = "{{.Auth.KeyURL}}"
+{{- else if .WebsiteURL}}
+ report["auth_docs_url"] = "{{.WebsiteURL}}"
+{{- end}}
+{{- if .Auth.Instructions}}
+ report["auth_instructions"] = {{printf "%q" .Auth.Instructions}}
{{- end}}
} else {
authConfigured = true
@@ -523,6 +528,15 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command {
if keyURL, ok := report["auth_key_url"]; ok {
fmt.Fprintf(w, " Get a key at: %v\n", keyURL)
}
+{{- else if .WebsiteURL}}
+ if docsURL, ok := report["auth_docs_url"]; ok {
+ fmt.Fprintf(w, " See API docs: %v\n", docsURL)
+ }
+{{- end}}
+{{- if .Auth.Instructions}}
+ if instructions, ok := report["auth_instructions"]; ok {
+ fmt.Fprintf(w, " %v\n", instructions)
+ }
{{- end}}
{{- if .HasStore}}
// Cache section: render after the primary health block so it
diff --git a/internal/generator/templates/helpers.go.tmpl b/internal/generator/templates/helpers.go.tmpl
index 969a6dcc..976511d3 100644
--- a/internal/generator/templates/helpers.go.tmpl
+++ b/internal/generator/templates/helpers.go.tmpl
@@ -310,6 +310,11 @@ func classifyAPIError(err error, flags *rootFlags) error {
{{- end}}
{{- if .Auth.KeyURL}}
"\n Get a key at: {{.Auth.KeyURL}}"+
+{{- else if .WebsiteURL}}
+ "\n See API docs: {{.WebsiteURL}}"+
+{{- end}}
+{{- if .Auth.Instructions}}
+ {{printf "%q" (printf "\n %s" .Auth.Instructions)}}+
{{- end}}
"\n Run '{{.Name}}-pp-cli doctor' to check auth status."+
"\n Response: "+cliutil.SanitizeErrorBody(msg), err))
@@ -324,6 +329,11 @@ func classifyAPIError(err error, flags *rootFlags) error {
{{- end}}
{{- if .Auth.KeyURL}}
"\n Get a key at: {{.Auth.KeyURL}}"+
+{{- else if .WebsiteURL}}
+ "\n See API docs: {{.WebsiteURL}}"+
+{{- end}}
+{{- if .Auth.Instructions}}
+ {{printf "%q" (printf "\n %s" .Auth.Instructions)}}+
{{- end}}
"\n Run '{{.Name}}-pp-cli doctor' to check auth status.", err))
{{- else if eq .Auth.Type "bearer_token"}}
@@ -335,6 +345,11 @@ func classifyAPIError(err error, flags *rootFlags) error {
{{- end}}
{{- if .Auth.KeyURL}}
"\n Get a key at: {{.Auth.KeyURL}}"+
+{{- else if .WebsiteURL}}
+ "\n See API docs: {{.WebsiteURL}}"+
+{{- end}}
+{{- if .Auth.Instructions}}
+ {{printf "%q" (printf "\n %s" .Auth.Instructions)}}+
{{- end}}
"\n Run '{{.Name}}-pp-cli doctor' to check auth status.", err))
{{- else if eq .Auth.Type "api_key"}}
@@ -346,6 +361,11 @@ func classifyAPIError(err error, flags *rootFlags) error {
{{- end}}
{{- if .Auth.KeyURL}}
"\n Get a key at: {{.Auth.KeyURL}}"+
+{{- else if .WebsiteURL}}
+ "\n See API docs: {{.WebsiteURL}}"+
+{{- end}}
+{{- if .Auth.Instructions}}
+ {{printf "%q" (printf "\n %s" .Auth.Instructions)}}+
{{- end}}
"\n Run '{{.Name}}-pp-cli doctor' to check auth status.", err))
{{- else}}
@@ -357,6 +377,11 @@ func classifyAPIError(err error, flags *rootFlags) error {
{{- end}}
{{- if .Auth.KeyURL}}
"\n Get a key at: {{.Auth.KeyURL}}"+
+{{- else if .WebsiteURL}}
+ "\n See API docs: {{.WebsiteURL}}"+
+{{- end}}
+{{- if .Auth.Instructions}}
+ {{printf "%q" (printf "\n %s" .Auth.Instructions)}}+
{{- end}}
"\n Run '{{.Name}}-pp-cli doctor' to check auth status.", err))
{{- end}}
@@ -370,6 +395,11 @@ func classifyAPIError(err error, flags *rootFlags) error {
{{- end}}
{{- if .Auth.KeyURL}}
"\n Get a key at: {{.Auth.KeyURL}}"+
+{{- else if .WebsiteURL}}
+ "\n See API docs: {{.WebsiteURL}}"+
+{{- end}}
+{{- if .Auth.Instructions}}
+ {{printf "%q" (printf "\n %s" .Auth.Instructions)}}+
{{- end}}
"\n Run '{{.Name}}-pp-cli doctor' to check auth status.", err))
{{- else if or (eq .Auth.Type "") (eq .Auth.Type "none")}}
@@ -385,6 +415,11 @@ func classifyAPIError(err error, flags *rootFlags) error {
{{- end}}
{{- if .Auth.KeyURL}}
"\n Get a key at: {{.Auth.KeyURL}}"+
+{{- else if .WebsiteURL}}
+ "\n See API docs: {{.WebsiteURL}}"+
+{{- end}}
+{{- if .Auth.Instructions}}
+ {{printf "%q" (printf "\n %s" .Auth.Instructions)}}+
{{- end}}
"\n Run '{{.Name}}-pp-cli doctor' to check auth status.", err))
{{- end}}
diff --git a/internal/generator/templates/mcp_tools.go.tmpl b/internal/generator/templates/mcp_tools.go.tmpl
index db8d5932..3d696d53 100644
--- a/internal/generator/templates/mcp_tools.go.tmpl
+++ b/internal/generator/templates/mcp_tools.go.tmpl
@@ -394,6 +394,11 @@ func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, pos
{{- end}}
{{- if .Auth.KeyURL}}
{{printf "%q" (printf "\n Get a key at: %s" .Auth.KeyURL)}} +
+{{- else if .WebsiteURL}}
+ {{printf "%q" (printf "\n See API docs: %s" .WebsiteURL)}} +
+{{- end}}
+{{- if .Auth.Instructions}}
+ {{printf "%q" (printf "\n %s" .Auth.Instructions)}} +
{{- end}}
{{printf "%q" (printf "\n Run '%s-pp-cli doctor' to check auth status." .Name)}}), nil
{{- end}}
@@ -413,6 +418,11 @@ func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, pos
{{- end}}
{{- if .Auth.KeyURL}}
{{printf "%q" (printf "\n Get a key at: %s" .Auth.KeyURL)}} +
+{{- else if .WebsiteURL}}
+ {{printf "%q" (printf "\n See API docs: %s" .WebsiteURL)}} +
+{{- end}}
+{{- if .Auth.Instructions}}
+ {{printf "%q" (printf "\n %s" .Auth.Instructions)}} +
{{- end}}
{{printf "%q" (printf "\n Run '%s-pp-cli doctor' to check auth status." .Name)}}), nil
case strings.Contains(msg, "HTTP 403"):
@@ -429,6 +439,11 @@ func makeAPIHandler(method, pathTemplate string, bindings []mcpParamBinding, pos
{{- end}}
{{- if .Auth.KeyURL}}
{{printf "%q" (printf "\n Get a key at: %s" .Auth.KeyURL)}} +
+{{- else if .WebsiteURL}}
+ {{printf "%q" (printf "\n See API docs: %s" .WebsiteURL)}} +
+{{- end}}
+{{- if .Auth.Instructions}}
+ {{printf "%q" (printf "\n %s" .Auth.Instructions)}} +
{{- end}}
{{printf "%q" (printf "\n Run '%s-pp-cli doctor' to check auth status." .Name)}}), nil
case strings.Contains(msg, "HTTP 404"):
@@ -658,6 +673,11 @@ func handleContext(_ context.Context, _ mcplib.CallToolRequest) (*mcplib.CallToo
{{- end}}
{{- if .Auth.KeyURL}}
"key_url": {{printf "%q" .Auth.KeyURL}},
+{{- else if .WebsiteURL}}
+ "docs_url": {{printf "%q" .WebsiteURL}},
+{{- end}}
+{{- if .Auth.Instructions}}
+ "instructions": {{printf "%q" .Auth.Instructions}},
{{- end}}
},
{{- end}}
diff --git a/internal/generator/templates/skill.md.tmpl b/internal/generator/templates/skill.md.tmpl
index 2bd2e9c2..9417618c 100644
--- a/internal/generator/templates/skill.md.tmpl
+++ b/internal/generator/templates/skill.md.tmpl
@@ -201,7 +201,7 @@ When you know what you want to do but not which command does it, ask the CLI dir
{{- else if eq .Auth.Type "api_key"}}
{{- with $canonicalEnvVar}}
-Set your API key via environment variable:
+Run `{{$.Name}}-pp-cli auth setup` to print the URL and steps for getting a key (add `--launch` to open the URL). Then set:
```bash
export {{.Name}}="<your-key>"
@@ -211,7 +211,7 @@ Or persist it in `{{$.Config.Path}}`.
{{- end}}
{{- else if eq .Auth.Type "oauth2"}}
-Authenticate via the browser:
+Run `{{.Name}}-pp-cli auth setup` for the registration URL and steps (add `--launch` to open the URL). Then authenticate:
```bash
{{.Name}}-pp-cli auth login
@@ -220,7 +220,7 @@ Authenticate via the browser:
Tokens are stored locally and refreshed automatically.
{{- else if eq .Auth.Type "bearer_token"}}
-Store your access token:
+Run `{{.Name}}-pp-cli auth setup` for the URL and steps to obtain a token (add `--launch` to open the URL). Then store it:
```bash
{{.Name}}-pp-cli auth set-token YOUR_TOKEN_HERE
diff --git a/internal/openapi/parser.go b/internal/openapi/parser.go
index 1f5151dc..ffdcf568 100644
--- a/internal/openapi/parser.go
+++ b/internal/openapi/parser.go
@@ -1,6 +1,7 @@
package openapi
import (
+ "bytes"
"context"
"encoding/json"
"fmt"
@@ -34,6 +35,7 @@ const (
extensionAuthVars = "x-auth-vars"
extensionAuthOptional = "x-auth-optional"
extensionAuthKeyURL = "x-auth-key-url"
+ extensionAuthInstructions = "x-auth-instructions"
extensionAuthTitle = "x-auth-title"
extensionAuthDescription = "x-auth-description"
extensionSpeakeasyExample = "x-speakeasy-example"
@@ -227,7 +229,47 @@ func parseWithLocation(data []byte, lenient bool, location *url.URL) (*spec.APIS
fmt.Fprintf(os.Stderr, "info: spec loaded after stripping broken references\n")
}
- doc.InternalizeRefs(context.Background(), nil)
+ // Skip InternalizeRefs when the spec has no external $refs (every $ref starts
+ // with '#'). The Render Public API spec has 314 cross-referenced schemas;
+ // kin-openapi's DefaultRefNameResolver-driven recursion runs > 10 minutes
+ // on it. Lazy ref resolution still works for in-document refs during access.
+ //
+ // Format-agnostic scan: matches both JSON (`"$ref": "..."`) and YAML
+ // (`$ref: '...'` or unquoted `$ref: ./schemas/foo.yaml`). A `$ref` whose
+ // value's first non-quote character is anything but '#' is external, and
+ // we must call InternalizeRefs to resolve it.
+ hasExternalRef := false
+ for off := 0; off < len(data); {
+ idx := bytes.Index(data[off:], []byte("$ref"))
+ if idx < 0 {
+ break
+ }
+ off += idx + len("$ref")
+ // Skip optional closing quote (JSON `"$ref"`) and whitespace before the colon.
+ for off < len(data) && (data[off] == '"' || data[off] == '\'' || data[off] == ' ' || data[off] == '\t') {
+ off++
+ }
+ if off >= len(data) || data[off] != ':' {
+ continue
+ }
+ off++
+ for off < len(data) && (data[off] == ' ' || data[off] == '\t') {
+ off++
+ }
+ if off < len(data) && (data[off] == '"' || data[off] == '\'') {
+ off++
+ }
+ if off >= len(data) {
+ break
+ }
+ if data[off] != '#' {
+ hasExternalRef = true
+ break
+ }
+ }
+ if hasExternalRef {
+ doc.InternalizeRefs(context.Background(), nil)
+ }
name := "api"
description := ""
@@ -325,6 +367,9 @@ func parseWithLocation(data []byte, lenient bool, location *url.URL) (*spec.APIS
if auth.Type != "none" && allOperationsAllowAnonymous(doc) {
auth = spec.AuthConfig{Type: "none"}
}
+ if auth.Type != "none" && auth.KeyURL == "" {
+ auth.KeyURL = inferAuthKeyURL(doc, auth.Scheme)
+ }
tierRouting, err := parseTypedExtension[spec.TierRoutingConfig](doc, extensionTierRouting)
if err != nil {
@@ -595,6 +640,79 @@ func allDigits(s string) bool {
return true
}
+// inferAuthKeyURL returns a best-effort HTTPS URL pointing the user at where
+// they can obtain a credential when x-auth-key-url is not set. Precedence:
+// 1. URL embedded in the selected security scheme's description
+// 2. URL embedded in info.description, but only when the surrounding text
+// mentions auth/credential cues (so we don't pick a URL describing an
+// unrelated feature)
+//
+// Returns "" when no plausible URL is found. The printed CLI surfaces the
+// result as "Get a key at: <URL>", so a wrong URL here is worse than no URL.
+// We deliberately do NOT fall back to externalDocs.url or info.contact.url —
+// those almost always point at the API's docs landing page or the company
+// homepage, neither of which is where users actually create a token. When this
+// returns "", the printed CLI falls back to a separate "See API docs: <URL>"
+// line driven by WebsiteURL, which is honest framing for those URLs.
+func inferAuthKeyURL(doc *openapi3.T, schemeName string) string {
+ if doc == nil {
+ return ""
+ }
+ if schemeName != "" && doc.Components != nil {
+ if ref, ok := doc.Components.SecuritySchemes[schemeName]; ok {
+ if scheme := securitySchemeValue(ref); scheme != nil {
+ if u := firstHTTPSURL(scheme.Description); u != "" {
+ return u
+ }
+ }
+ }
+ }
+ if doc.Info != nil {
+ if u := firstAuthRelatedURL(doc.Info.Description); u != "" {
+ return u
+ }
+ }
+ return ""
+}
+
+var httpsURLPattern = regexp.MustCompile(`https://[^\s)>\]"',]+`)
+
+// firstHTTPSURL returns the first https:// substring found in s, with trailing
+// sentence punctuation trimmed.
+func firstHTTPSURL(s string) string {
+ if s == "" {
+ return ""
+ }
+ m := httpsURLPattern.FindString(s)
+ return strings.TrimRight(m, ".,;:!?)")
+}
+
+// firstAuthRelatedURL returns the first HTTPS URL in s, but only when s also
+// contains language indicating the URL is about credentials. Avoids picking a
+// URL that happens to appear in a description of an unrelated feature.
+func firstAuthRelatedURL(s string) string {
+ if s == "" {
+ return ""
+ }
+ lower := strings.ToLower(s)
+ cues := []string{
+ "token", "api key", "api_key", "apikey",
+ "credential", "register", "sign up", "signup",
+ "create an app", "create an application", "personal access",
+ }
+ matched := false
+ for _, c := range cues {
+ if strings.Contains(lower, c) {
+ matched = true
+ break
+ }
+ }
+ if !matched {
+ return ""
+ }
+ return firstHTTPSURL(s)
+}
+
func applyAuthOverrideExtensions(auth *spec.AuthConfig, extensions map[string]any) {
if auth == nil || len(extensions) == 0 {
return
@@ -612,6 +730,9 @@ func applyAuthOverrideExtensions(auth *spec.AuthConfig, extensions map[string]an
if keyURL := stringExtension(extensions, extensionAuthKeyURL); keyURL != "" {
auth.KeyURL = keyURL
}
+ if instructions := stringExtension(extensions, extensionAuthInstructions); instructions != "" {
+ auth.Instructions = instructions
+ }
if title := stringExtension(extensions, extensionAuthTitle); title != "" {
auth.Title = title
}
diff --git a/internal/openapi/parser_test.go b/internal/openapi/parser_test.go
index 663bae21..eec0eb40 100644
--- a/internal/openapi/parser_test.go
+++ b/internal/openapi/parser_test.go
@@ -2140,6 +2140,7 @@ components:
- FLIGHTAWARE_API_KEY
x-auth-optional: true
x-auth-key-url: https://flightaware.com/commercial/aeroapi/
+ x-auth-instructions: Sign up for FlightAware AeroAPI and copy the personal API key.
x-auth-title: FlightAware AeroAPI Key
x-auth-description: Optional FlightAware AeroAPI credential for enriched flight data.
paths:
@@ -2156,10 +2157,195 @@ paths:
assert.Equal(t, []string{"FLIGHTAWARE_API_KEY"}, parsed.Auth.EnvVars)
assert.True(t, parsed.Auth.Optional)
assert.Equal(t, "https://flightaware.com/commercial/aeroapi/", parsed.Auth.KeyURL)
+ assert.Equal(t, "Sign up for FlightAware AeroAPI and copy the personal API key.", parsed.Auth.Instructions)
assert.Equal(t, "FlightAware AeroAPI Key", parsed.Auth.Title)
assert.Equal(t, "Optional FlightAware AeroAPI credential for enriched flight data.", parsed.Auth.Description)
}
+func TestOpenAPIAuthKeyURLInference(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ yaml string
+ expected string
+ }{
+ {
+ name: "explicit x-auth-key-url wins over inference",
+ yaml: `openapi: "3.0.3"
+info:
+ title: Example
+ version: "1.0.0"
+externalDocs:
+ url: https://docs.example.com/rest-api/
+servers:
+ - url: https://api.example.com
+components:
+ securitySchemes:
+ ApiKeyAuth:
+ type: apiKey
+ in: header
+ name: x-apikey
+ description: "Visit https://example.com/wrong-page to get a key"
+ x-auth-key-url: https://example.com/keys
+paths:
+ /ping:
+ get:
+ responses:
+ "200": { description: OK }
+`,
+ expected: "https://example.com/keys",
+ },
+ {
+ name: "url from security scheme description",
+ yaml: `openapi: "3.0.3"
+info:
+ title: Example
+ version: "1.0.0"
+externalDocs:
+ url: https://docs.example.com/rest-api/
+servers:
+ - url: https://api.example.com
+components:
+ securitySchemes:
+ ApiKeyAuth:
+ type: apiKey
+ in: header
+ name: x-apikey
+ description: "Generate a token at https://example.com/account/api-keys."
+paths:
+ /ping:
+ get:
+ responses:
+ "200": { description: OK }
+`,
+ expected: "https://example.com/account/api-keys",
+ },
+ {
+ name: "no inference when only externalDocs.url is set (docs URL is not a credentials page)",
+ yaml: `openapi: "3.0.3"
+info:
+ title: Figma API
+ version: "1.0.0"
+externalDocs:
+ url: https://developers.figma.com/docs/rest-api/
+servers:
+ - url: https://api.figma.com
+components:
+ securitySchemes:
+ PersonalAccessToken:
+ type: apiKey
+ in: header
+ name: X-Figma-Token
+paths:
+ /ping:
+ get:
+ responses:
+ "200": { description: OK }
+`,
+ expected: "",
+ },
+ {
+ name: "no inference when only info.contact.url is set (homepage is not a credentials page)",
+ yaml: `openapi: "3.0.3"
+info:
+ title: Example
+ version: "1.0.0"
+ contact:
+ url: https://example.com/developers
+servers:
+ - url: https://api.example.com
+components:
+ securitySchemes:
+ ApiKeyAuth:
+ type: apiKey
+ in: header
+ name: x-apikey
+paths:
+ /ping:
+ get:
+ responses:
+ "200": { description: OK }
+`,
+ expected: "",
+ },
+ {
+ name: "info.description URL only used when auth-related cues present",
+ yaml: `openapi: "3.0.3"
+info:
+ title: Example
+ version: "1.0.0"
+ description: "Generate an API key at https://example.com/account/keys before calling."
+servers:
+ - url: https://api.example.com
+components:
+ securitySchemes:
+ ApiKeyAuth:
+ type: apiKey
+ in: header
+ name: x-apikey
+paths:
+ /ping:
+ get:
+ responses:
+ "200": { description: OK }
+`,
+ expected: "https://example.com/account/keys",
+ },
+ {
+ name: "info.description URL ignored without auth cue",
+ yaml: `openapi: "3.0.3"
+info:
+ title: Example
+ version: "1.0.0"
+ description: "See https://example.com/changelog for release notes."
+ contact:
+ url: https://example.com/developers
+servers:
+ - url: https://api.example.com
+components:
+ securitySchemes:
+ ApiKeyAuth:
+ type: apiKey
+ in: header
+ name: x-apikey
+paths:
+ /ping:
+ get:
+ responses:
+ "200": { description: OK }
+`,
+ expected: "",
+ },
+ {
+ name: "no inference when auth.type is none",
+ yaml: `openapi: "3.0.3"
+info:
+ title: Example
+ version: "1.0.0"
+externalDocs:
+ url: https://docs.example.com/rest-api/
+servers:
+ - url: https://api.example.com
+paths:
+ /ping:
+ get:
+ responses:
+ "200": { description: OK }
+`,
+ expected: "",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ parsed, err := Parse([]byte(tc.yaml))
+ require.NoError(t, err)
+ assert.Equal(t, tc.expected, parsed.Auth.KeyURL)
+ })
+ }
+}
+
func TestOpenAPIAuthEnvVarsPopulateRichDefaults(t *testing.T) {
t.Parallel()
diff --git a/internal/spec/spec.go b/internal/spec/spec.go
index 82f0da2b..218a3412 100644
--- a/internal/spec/spec.go
+++ b/internal/spec/spec.go
@@ -447,11 +447,12 @@ type AuthConfig struct {
Format string `yaml:"format" json:"format"`
EnvVars []string `yaml:"env_vars" json:"env_vars"`
EnvVarSpecs []AuthEnvVar `yaml:"env_var_specs,omitempty" json:"env_var_specs,omitempty"`
- 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
- Title string `yaml:"title,omitempty" json:"title,omitempty"` // user-facing credential field title for install/config surfaces
+ 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
+ Instructions string `yaml:"instructions,omitempty" json:"instructions,omitempty"` // one-line guidance shown alongside KeyURL, e.g. "Settings → Personal access tokens → Generate new"
+ Title string `yaml:"title,omitempty" json:"title,omitempty"` // user-facing credential field title for install/config surfaces
Description string `yaml:"description,omitempty" json:"description,omitempty"`
AuthorizationURL string `yaml:"authorization_url,omitempty" json:"authorization_url,omitempty"`
TokenURL string `yaml:"token_url,omitempty" json:"token_url,omitempty"`
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/SKILL.md b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/SKILL.md
index 855f1005..4c02acbf 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/SKILL.md
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/SKILL.md
@@ -53,7 +53,7 @@ printing-press-rich-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:
+Run `printing-press-rich-pp-cli auth setup` to print the URL and steps for getting a key (add `--launch` to open the URL). Then set:
```bash
export RICH_AUTH_API_KEY="<your-key>"
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/auth.go b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/auth.go
index 7cbab816..6e4c71a6 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/auth.go
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/auth.go
@@ -6,7 +6,6 @@ package cli
import (
"fmt"
"os"
-
"printing-press-rich-pp-cli/internal/config"
"github.com/spf13/cobra"
)
@@ -17,6 +16,7 @@ func newAuthCmd(flags *rootFlags) *cobra.Command {
Short: "Manage authentication for Printing Press Rich",
}
+ cmd.AddCommand(newAuthSetupCmd(flags))
cmd.AddCommand(newAuthStatusCmd(flags))
cmd.AddCommand(newAuthSetTokenCmd(flags))
cmd.AddCommand(newAuthLogoutCmd(flags))
@@ -24,6 +24,36 @@ func newAuthCmd(flags *rootFlags) *cobra.Command {
return cmd
}
+// newAuthSetupCmd prints concrete steps for getting a credential. Side-effect
+// rule: print by default, --launch opt-in to open the URL, short-circuit when
+// the verifier is running this in a sandboxed subprocess.
+func newAuthSetupCmd(_ *rootFlags) *cobra.Command {
+ var launch bool
+ cmd := &cobra.Command{
+ Use: "setup",
+ Short: "Print steps for obtaining a credential (use --launch to open the URL)",
+ Example: " printing-press-rich-pp-cli auth setup\n printing-press-rich-pp-cli auth setup --launch",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ w := cmd.OutOrStdout()
+ fmt.Fprintln(w, "No setup URL is configured for this CLI; check the API's docs.")
+ fmt.Fprintln(w, "")
+ fmt.Fprintln(w, "Then set:")
+ fmt.Fprintln(w, " export RICH_AUTH_API_KEY=\"<your-token>\"")
+ fmt.Fprintln(w, " export RICH_AUTH_OPTIONAL_TOKEN=\"<your-token>\"")
+ fmt.Fprintln(w, " export RICH_AUTH_BOT_TOKEN=\"<your-token>\"")
+ fmt.Fprintln(w, " export RICH_AUTH_USER_TOKEN=\"<your-token>\"")
+ fmt.Fprintln(w, " printing-press-rich-pp-cli auth set-token <token>")
+ if !launch {
+ return nil
+ }
+ fmt.Fprintln(cmd.ErrOrStderr(), "no setup URL configured; cannot launch")
+ return nil
+ },
+ }
+ cmd.Flags().BoolVar(&launch, "launch", false, "Open the setup URL in your default browser")
+ return cmd
+}
+
func newAuthStatusCmd(flags *rootFlags) *cobra.Command {
return &cobra.Command{
Use: "status",
diff --git a/testdata/golden/expected/generate-golden-api/dogfood.json b/testdata/golden/expected/generate-golden-api/dogfood.json
index c144e26f..9961935d 100644
--- a/testdata/golden/expected/generate-golden-api/dogfood.json
+++ b/testdata/golden/expected/generate-golden-api/dogfood.json
@@ -71,8 +71,8 @@
"verdict": "WARN",
"wiring_check": {
"command_tree": {
- "defined": 40,
- "registered": 40
+ "defined": 41,
+ "registered": 41
},
"config_consistency": {
"consistent": true
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
index 260a1e42..a6e1aa68 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/SKILL.md
@@ -62,7 +62,7 @@ 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:
+Run `printing-press-golden-pp-cli auth setup` to print the URL and steps for getting a key (add `--launch` to open the URL). Then set:
```bash
export PRINTING_PRESS_GOLDEN_API_KEY="<your-key>"
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
index 7bb622d5..2503fdbe 100644
--- 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
@@ -6,7 +6,6 @@ package cli
import (
"fmt"
"os"
-
"printing-press-golden-pp-cli/internal/config"
"github.com/spf13/cobra"
)
@@ -17,6 +16,7 @@ func newAuthCmd(flags *rootFlags) *cobra.Command {
Short: "Manage authentication for Printing Press Studio",
}
+ cmd.AddCommand(newAuthSetupCmd(flags))
cmd.AddCommand(newAuthStatusCmd(flags))
cmd.AddCommand(newAuthSetTokenCmd(flags))
cmd.AddCommand(newAuthLogoutCmd(flags))
@@ -24,6 +24,33 @@ func newAuthCmd(flags *rootFlags) *cobra.Command {
return cmd
}
+// newAuthSetupCmd prints concrete steps for getting a credential. Side-effect
+// rule: print by default, --launch opt-in to open the URL, short-circuit when
+// the verifier is running this in a sandboxed subprocess.
+func newAuthSetupCmd(_ *rootFlags) *cobra.Command {
+ var launch bool
+ cmd := &cobra.Command{
+ Use: "setup",
+ Short: "Print steps for obtaining a credential (use --launch to open the URL)",
+ Example: " printing-press-golden-pp-cli auth setup\n printing-press-golden-pp-cli auth setup --launch",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ w := cmd.OutOrStdout()
+ fmt.Fprintln(w, "No setup URL is configured for this CLI; check the API's docs.")
+ fmt.Fprintln(w, "")
+ fmt.Fprintln(w, "Then set:")
+ fmt.Fprintln(w, " export PRINTING_PRESS_GOLDEN_API_KEY=\"<your-token>\"")
+ fmt.Fprintln(w, " printing-press-golden-pp-cli auth set-token <token>")
+ if !launch {
+ return nil
+ }
+ fmt.Fprintln(cmd.ErrOrStderr(), "no setup URL configured; cannot launch")
+ return nil
+ },
+ }
+ cmd.Flags().BoolVar(&launch, "launch", false, "Open the setup URL in your default browser")
+ return cmd
+}
+
func newAuthStatusCmd(flags *rootFlags) *cobra.Command {
return &cobra.Command{
Use: "status",
← a3e07d0a feat(cli): mechanical PII gate before promote/publish (#958)
·
back to Cli Printing Press
·
fix(generator): rename trailing '_test' stems to avoid Go te a03a7b81 →