From 2d54da377a218fbee0b09e7a432011b0d19495a8 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 21:05:05 +0000 Subject: [PATCH 1/6] feat(auth): Make bare sentry auth run login Default the auth route to login so `sentry auth` matches `sentry auth login`, and point installer/onboarding copy at the shorter command. Co-Authored-By: David Cramer --- apps/cli-docs/src/components/Terminal.astro | 2 +- .../src/content/docs/getting-started.mdx | 6 +-- apps/cli-docs/src/fragments/commands/auth.md | 18 ++++--- apps/cli-docs/src/fragments/commands/init.md | 2 +- packages/cli/README.md | 2 +- .../sentry-cli/skills/sentry-cli/SKILL.md | 4 +- .../skills/sentry-cli/references/auth.md | 16 +++--- .../cli/script/generate-skill-markdown.ts | 20 +++++-- packages/cli/script/generate-skill.ts | 53 ++++++++++++++++--- packages/cli/src/commands/auth/index.ts | 4 +- packages/cli/src/commands/cli/setup.ts | 4 +- packages/cli/src/lib/help.ts | 2 +- packages/cli/test/commands/cli/setup.test.ts | 4 +- .../cli/test/lib/command-suggestions.test.ts | 11 +++- packages/cli/test/lib/help.test.ts | 2 +- .../script/generate-skill-markdown.test.ts | 26 +++++++++ 16 files changed, 131 insertions(+), 45 deletions(-) diff --git a/apps/cli-docs/src/components/Terminal.astro b/apps/cli-docs/src/components/Terminal.astro index 1cb79c0a88..e2a694e5c0 100644 --- a/apps/cli-docs/src/components/Terminal.astro +++ b/apps/cli-docs/src/components/Terminal.astro @@ -31,7 +31,7 @@ if (background) {
$ - sentry auth login + sentry auth
diff --git a/apps/cli-docs/src/content/docs/getting-started.mdx b/apps/cli-docs/src/content/docs/getting-started.mdx index ee60060583..603bb42f4d 100644 --- a/apps/cli-docs/src/content/docs/getting-started.mdx +++ b/apps/cli-docs/src/content/docs/getting-started.mdx @@ -106,7 +106,7 @@ Or run directly without installing: The easiest way to authenticate is via OAuth device flow: ```bash -sentry auth login +sentry auth ``` You'll be given a URL and a code to enter. Once you authorize the application @@ -120,7 +120,7 @@ Sentry CLI configuration directory (`~/.sentry/` by default, overridable with Alternatively, you can use an API token directly: ```bash -sentry auth login --token YOUR_SENTRY_API_TOKEN +sentry auth --token YOUR_SENTRY_API_TOKEN ``` You can create API tokens in your [Sentry account settings](https://sentry.io/settings/account/api/auth-tokens/). @@ -149,7 +149,7 @@ sentry auth logout Using a self-hosted Sentry instance? Set `SENTRY_URL` to point at it: ```bash -SENTRY_URL=https://sentry.example.com sentry auth login +SENTRY_URL=https://sentry.example.com sentry auth ``` See the [Self-Hosted](../self-hosted/) guide for full setup details. diff --git a/apps/cli-docs/src/fragments/commands/auth.md b/apps/cli-docs/src/fragments/commands/auth.md index 51395146eb..e8ab19ab52 100644 --- a/apps/cli-docs/src/fragments/commands/auth.md +++ b/apps/cli-docs/src/fragments/commands/auth.md @@ -5,9 +5,11 @@ ### OAuth login (recommended) ```bash -sentry auth login +sentry auth ``` +`sentry auth login` is equivalent. + 1. A URL and device code will be displayed 2. Open the URL in your browser 3. Enter the code when prompted @@ -18,7 +20,7 @@ sentry auth login ### Token login ```bash -sentry auth login --token YOUR_SENTRY_API_TOKEN +sentry auth --token YOUR_SENTRY_API_TOKEN ``` ### Read-only OAuth login @@ -27,7 +29,7 @@ Request only read-only scopes — useful for tokens handed to AI agents or CI jobs that should not mutate Sentry state: ```bash -sentry auth login --read-only +sentry auth --read-only ``` ### Custom OAuth scopes @@ -35,8 +37,8 @@ sentry auth login --read-only Request specific scopes (repeatable, comma-separated): ```bash -sentry auth login --scope project:read --scope org:read -sentry auth login --scope project:read,event:read +sentry auth --scope project:read --scope org:read +sentry auth --scope project:read,event:read ``` ### Self-hosted Sentry @@ -44,14 +46,14 @@ sentry auth login --scope project:read,event:read Use `--url` (recommended) or the `SENTRY_URL` environment variable: ```bash -sentry auth login --url https://sentry.example.com -SENTRY_URL=https://sentry.example.com sentry auth login +sentry auth --url https://sentry.example.com +SENTRY_URL=https://sentry.example.com sentry auth ``` For token-based auth with self-hosted: ```bash -sentry auth login --token YOUR_TOKEN --url https://sentry.example.com +sentry auth --token YOUR_TOKEN --url https://sentry.example.com ``` See [Self-Hosted Sentry](../self-hosted/) for details. diff --git a/apps/cli-docs/src/fragments/commands/init.md b/apps/cli-docs/src/fragments/commands/init.md index e7e80d9d74..adc6e6f95c 100644 --- a/apps/cli-docs/src/fragments/commands/init.md +++ b/apps/cli-docs/src/fragments/commands/init.md @@ -2,7 +2,7 @@ > **Experimental:** `sentry init` is experimental and may modify your source files. Always review changes before committing. -**Prerequisites:** You must be authenticated first. Run `sentry auth login` if you haven't already. +**Prerequisites:** You must be authenticated first. Run `sentry auth` if you haven't already. ## Examples diff --git a/packages/cli/README.md b/packages/cli/README.md index 052c1d534a..86a1d85082 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -52,7 +52,7 @@ bunx sentry --help ```bash # Authenticate with Sentry -sentry auth login +sentry auth # List issues (auto-detects project from your codebase) sentry issue list diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md index 2e2ac49f34..c1294f269c 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -284,8 +284,8 @@ npm install -g sentry ### Authentication ```bash -sentry auth login -sentry auth login --token YOUR_SENTRY_API_TOKEN +sentry auth +sentry auth --token YOUR_SENTRY_API_TOKEN sentry auth status sentry auth logout ``` diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/auth.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/auth.md index 26b90c01ae..766e0f9034 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/auth.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/auth.md @@ -26,19 +26,19 @@ Authenticate with Sentry **Examples:** ```bash -sentry auth login +sentry auth -sentry auth login --token YOUR_SENTRY_API_TOKEN +sentry auth --token YOUR_SENTRY_API_TOKEN -sentry auth login --read-only +sentry auth --read-only -sentry auth login --scope project:read --scope org:read -sentry auth login --scope project:read,event:read +sentry auth --scope project:read --scope org:read +sentry auth --scope project:read,event:read -sentry auth login --url https://sentry.example.com -SENTRY_URL=https://sentry.example.com sentry auth login +sentry auth --url https://sentry.example.com +SENTRY_URL=https://sentry.example.com sentry auth -sentry auth login --token YOUR_TOKEN --url https://sentry.example.com +sentry auth --token YOUR_TOKEN --url https://sentry.example.com ``` ### `sentry auth logout` diff --git a/packages/cli/script/generate-skill-markdown.ts b/packages/cli/script/generate-skill-markdown.ts index fd2f22b696..eb19074f46 100644 --- a/packages/cli/script/generate-skill-markdown.ts +++ b/packages/cli/script/generate-skill-markdown.ts @@ -18,10 +18,20 @@ export function extractCommandPathFromHeading( export function matchExampleToCommand( code: string, commandPaths: readonly string[], - groupFallback: string + groupFallback: string, + defaultCommandPath?: string ): string | undefined { - return ( - commandPaths.find((path) => code.includes(path)) ?? - (code.includes(groupFallback) ? groupFallback : undefined) - ); + // Prefer the longest path so `sentry auth login` wins over bare `sentry auth` + // when both would otherwise match via includes(). + const byLengthDesc = [...commandPaths].sort((a, b) => b.length - a.length); + const matched = byLengthDesc.find((path) => code.includes(path)); + if (matched) { + return matched; + } + if (!code.includes(groupFallback)) { + return; + } + // Bare group examples (`sentry auth`) belong on the default subcommand when + // one exists (login), not on a synthetic group-only path. + return defaultCommandPath ?? groupFallback; } diff --git a/packages/cli/script/generate-skill.ts b/packages/cli/script/generate-skill.ts index fc7df43dfe..ee9207e39f 100644 --- a/packages/cli/script/generate-skill.ts +++ b/packages/cli/script/generate-skill.ts @@ -60,6 +60,23 @@ import { isRouteMap, } from "../src/lib/introspect.js"; +/** Resolve the default subcommand name for a route map, if configured. */ +function findDefaultCommandName(routeMap: RouteMap): string | undefined { + if (!routeMap.getDefaultCommand) { + return; + } + const defaultCmd = routeMap.getDefaultCommand(); + if (!defaultCmd) { + return; + } + for (const sub of routeMap.getAllEntries()) { + if (sub.target === defaultCmd) { + return sub.name.original; + } + } + return; +} + const SKILL_DIR = "plugins/sentry-cli/skills/sentry-cli"; const OUTPUT_PATH = `${SKILL_DIR}/SKILL.md`; const REFERENCES_DIR = `${SKILL_DIR}/references`; @@ -290,10 +307,10 @@ npm install -g sentry \`\`\`bash # OAuth login (recommended) -sentry auth login +sentry auth # Or use an API token -sentry auth login --token YOUR_SENTRY_API_TOKEN +sentry auth --token YOUR_SENTRY_API_TOKEN # Check auth status sentry auth status @@ -343,7 +360,8 @@ function associateCodeBlocks( tokens: Token[], commandPaths: string[], commandGroup: string, - examples: Map + examples: Map, + defaultCommandPath?: string ): void { const groupFallback = `sentry ${commandGroup}`; let currentCmd: string | null = null; @@ -359,7 +377,12 @@ function associateCodeBlocks( if (currentCmd && examples.has(currentCmd)) { appendExample(examples, currentCmd, code); } else { - const target = matchExampleToCommand(code, commandPaths, groupFallback); + const target = matchExampleToCommand( + code, + commandPaths, + groupFallback, + defaultCommandPath + ); if (target) { appendExample(examples, target, code); } @@ -377,7 +400,8 @@ function associateCodeBlocks( * by matching code blocks to commands via heading context or content analysis. */ async function loadCommandExamples( - commandGroup: string + commandGroup: string, + defaultCommandName?: string ): Promise> { const docContent = await loadDoc(`commands/${commandGroup}.md`); if (!docContent) { @@ -387,7 +411,16 @@ async function loadCommandExamples( const tokens = marked.lexer(docContent); const examples = new Map(); const commandPaths = collectCommandPaths(tokens, examples); - associateCodeBlocks(tokens, commandPaths, commandGroup, examples); + const defaultCommandPath = defaultCommandName + ? `sentry ${commandGroup} ${defaultCommandName}` + : undefined; + associateCodeBlocks( + tokens, + commandPaths, + commandGroup, + examples, + defaultCommandPath + ); return examples; } @@ -435,7 +468,13 @@ async function extractRoutes(routeMap: RouteMap): Promise { } const routeName = entry.name.original; const target = entry.target; - const docExamples = await loadCommandExamples(routeName); + const defaultCommandName = isRouteMap(target) + ? findDefaultCommandName(target) + : undefined; + const docExamples = await loadCommandExamples( + routeName, + defaultCommandName + ); if (isRouteMap(target)) { result.push({ name: routeName, diff --git a/packages/cli/src/commands/auth/index.ts b/packages/cli/src/commands/auth/index.ts index 21dcd3a1a1..4b8c1916d6 100644 --- a/packages/cli/src/commands/auth/index.ts +++ b/packages/cli/src/commands/auth/index.ts @@ -15,11 +15,11 @@ export const authRoute = buildRouteMap({ token: tokenCommand, whoami: whoamiCommand, }, - defaultCommand: "status", + defaultCommand: "login", docs: { brief: "Authenticate with Sentry", fullDescription: - "Manage authentication with Sentry. Use 'sentry auth login' to authenticate, " + + "Manage authentication with Sentry. Use 'sentry auth' (or 'sentry auth login') to authenticate, " + "'sentry auth logout' to remove credentials, 'sentry auth refresh' to manually refresh your OAuth access token, " + "'sentry auth status' to check your authentication status, " + "'sentry auth whoami' to show your current user identity, " + diff --git a/packages/cli/src/commands/cli/setup.ts b/packages/cli/src/commands/cli/setup.ts index f930a97d92..2da4367244 100644 --- a/packages/cli/src/commands/cli/setup.ts +++ b/packages/cli/src/commands/cli/setup.ts @@ -315,8 +315,8 @@ function printWelcomeMessage( emit(`Installed sentry v${version} to ${binaryPath}`); emit(""); emit("Get started:"); - emit(" sentry auth login Authenticate with Sentry"); - emit(" sentry --help See all available commands"); + emit(" sentry auth Authenticate with Sentry"); + emit(" sentry --help See all available commands"); emit(""); emit("https://cli.sentry.dev"); } diff --git a/packages/cli/src/lib/help.ts b/packages/cli/src/lib/help.ts index 53a429466f..5be8830266 100644 --- a/packages/cli/src/lib/help.ts +++ b/packages/cli/src/lib/help.ts @@ -135,7 +135,7 @@ function generateCommands(): HelpCommand[] { }); } -const EXAMPLE_LOGGED_OUT = "sentry auth login"; +const EXAMPLE_LOGGED_OUT = "sentry auth"; const EXAMPLE_LOGGED_IN = "sentry issue list"; const DOCS_URL = "https://cli.sentry.dev/getting-started/"; diff --git a/packages/cli/test/commands/cli/setup.test.ts b/packages/cli/test/commands/cli/setup.test.ts index 2127f3c40a..6591a67fed 100644 --- a/packages/cli/test/commands/cli/setup.test.ts +++ b/packages/cli/test/commands/cli/setup.test.ts @@ -549,7 +549,7 @@ describe("sentry cli setup", () => { // Should show welcome message, not "Setup complete!" expect(combined).toContain("Installed sentry v"); expect(combined).toContain("Get started:"); - expect(combined).toContain("sentry auth login"); + expect(combined).toContain("sentry auth"); expect(combined).toContain("sentry --help"); expect(combined).toContain("cli.sentry.dev"); expect(combined).not.toContain("Setup complete!"); @@ -642,7 +642,7 @@ describe("sentry cli setup", () => { expect(combined).toContain("Binary: Installed to"); // But welcome/getting-started is suppressed for upgrades expect(combined).not.toContain("Get started:"); - expect(combined).not.toContain("sentry auth login"); + expect(combined).not.toContain("Authenticate with Sentry"); }); test("--install with --quiet suppresses all output", async () => { diff --git a/packages/cli/test/lib/command-suggestions.test.ts b/packages/cli/test/lib/command-suggestions.test.ts index ff7d511956..0f8ff98a8a 100644 --- a/packages/cli/test/lib/command-suggestions.test.ts +++ b/packages/cli/test/lib/command-suggestions.test.ts @@ -144,8 +144,17 @@ describe("routes with defaultCommand", () => { } }); - test("auth route group has defaultCommand (status)", () => { + test("auth route group has defaultCommand (login)", () => { expect(routesWithDefault.has("auth")).toBe(true); + const authEntry = routes + .getAllEntries() + .find((e) => e.name.original === "auth"); + const authMap = authEntry?.target as unknown as RouteMap; + const defaultCmd = authMap.getDefaultCommand?.(); + const defaultName = authMap + .getAllEntries() + .find((e) => e.target === defaultCmd)?.name.original; + expect(defaultName).toBe("login"); }); test("route groups without defaultCommand", () => { diff --git a/packages/cli/test/lib/help.test.ts b/packages/cli/test/lib/help.test.ts index a94cf0c057..37955e7ac9 100644 --- a/packages/cli/test/lib/help.test.ts +++ b/packages/cli/test/lib/help.test.ts @@ -112,7 +112,7 @@ describe("printCustomHelp", () => { test("shows login example when not authenticated", async () => { // useTestConfigDir provides a clean env with no auth token const output = stripAnsi(printCustomHelp()); - expect(output).toContain("sentry auth login"); + expect(output).toContain("sentry auth"); }); test("includes an Environment Variables section with top-level vars", () => { diff --git a/packages/cli/test/script/generate-skill-markdown.test.ts b/packages/cli/test/script/generate-skill-markdown.test.ts index 6bce18c8b3..c5f4fefa8c 100644 --- a/packages/cli/test/script/generate-skill-markdown.test.ts +++ b/packages/cli/test/script/generate-skill-markdown.test.ts @@ -40,6 +40,32 @@ describe("matchExampleToCommand", () => { ).toBe("sentry project create"); }); + test("maps bare group examples to the default subcommand", () => { + expect( + matchExampleToCommand( + "sentry auth\nsentry auth --token YOUR_SENTRY_API_TOKEN", + [ + "sentry auth login", + "sentry auth logout", + "sentry auth status", + ], + "sentry auth", + "sentry auth login" + ) + ).toBe("sentry auth login"); + }); + + test("prefers longer command paths over shorter prefixes", () => { + expect( + matchExampleToCommand( + "sentry auth login --token TOKEN", + ["sentry auth login", "sentry auth status"], + "sentry auth", + "sentry auth login" + ) + ).toBe("sentry auth login"); + }); + test("the generated project reference retains create examples", async () => { const reference = await readFile( "plugins/sentry-cli/skills/sentry-cli/references/project.md", From b1310a3b342391d7546a191937d46a9f81e1fa4d Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 22:52:51 +0000 Subject: [PATCH 2/6] feat(auth): Route bare sentry auth by login state Bare `sentry auth` now logs in when logged out and shows status when already authenticated. Login-only flags still force the login path. Co-Authored-By: David Cramer --- apps/cli-docs/src/fragments/commands/auth.md | 3 +- packages/cli/script/generate-skill.ts | 15 +- packages/cli/src/commands/auth/default.ts | 159 ++++++++++++++++++ packages/cli/src/commands/auth/index.ts | 17 +- .../cli/test/commands/auth/default.test.ts | 155 +++++++++++++++++ .../cli/test/lib/command-suggestions.test.ts | 9 +- 6 files changed, 345 insertions(+), 13 deletions(-) create mode 100644 packages/cli/src/commands/auth/default.ts create mode 100644 packages/cli/test/commands/auth/default.test.ts diff --git a/apps/cli-docs/src/fragments/commands/auth.md b/apps/cli-docs/src/fragments/commands/auth.md index e8ab19ab52..9157ccb78f 100644 --- a/apps/cli-docs/src/fragments/commands/auth.md +++ b/apps/cli-docs/src/fragments/commands/auth.md @@ -8,7 +8,8 @@ sentry auth ``` -`sentry auth login` is equivalent. +Bare `sentry auth` logs in when you're logged out and shows status when you're +already authenticated. `sentry auth login` always starts the login flow. 1. A URL and device code will be displayed 2. Open the URL in your browser diff --git a/packages/cli/script/generate-skill.ts b/packages/cli/script/generate-skill.ts index ee9207e39f..1dc4673952 100644 --- a/packages/cli/script/generate-skill.ts +++ b/packages/cli/script/generate-skill.ts @@ -60,7 +60,12 @@ import { isRouteMap, } from "../src/lib/introspect.js"; -/** Resolve the default subcommand name for a route map, if configured. */ +/** + * Resolve the default subcommand name for a route map, if configured. + * + * Hidden dispatcher routes (e.g. auth's `default`) map bare group examples + * onto the user-facing command they primarily document (login). + */ function findDefaultCommandName(routeMap: RouteMap): string | undefined { if (!routeMap.getDefaultCommand) { return; @@ -71,7 +76,13 @@ function findDefaultCommandName(routeMap: RouteMap): string | undefined { } for (const sub of routeMap.getAllEntries()) { if (sub.target === defaultCmd) { - return sub.name.original; + const name = sub.name.original; + // Auth uses a hidden "default" dispatcher; bare `sentry auth` examples + // document the login path. + if (name === "default") { + return "login"; + } + return name; } } return; diff --git a/packages/cli/src/commands/auth/default.ts b/packages/cli/src/commands/auth/default.ts new file mode 100644 index 0000000000..5e5f883812 --- /dev/null +++ b/packages/cli/src/commands/auth/default.ts @@ -0,0 +1,159 @@ +/** + * Bare `sentry auth` dispatcher. + * + * Routes to login when logged out (or when login-only flags are present), + * and to status when already authenticated. + */ + +import type { SentryContext } from "../../context.js"; +import { + buildCommand, + FIELDS_FLAG, + JSON_FLAG, + numberParser, +} from "../../lib/command.js"; +import { isAuthenticated } from "../../lib/db/auth.js"; +import { FRESH_ALIASES, FRESH_FLAG } from "../../lib/list-command.js"; +import { loginCommand, parseLoginUrl } from "./login.js"; +import { statusCommand } from "./status.js"; + +type AuthDefaultFlags = { + readonly token?: string; + readonly timeout: number; + readonly force: boolean; + readonly url?: string; + readonly "read-only": boolean; + readonly scope?: readonly string[]; + readonly "show-token": boolean; + readonly fresh: boolean; + readonly json?: boolean; + readonly fields?: string[]; +}; + +/** Login-only flags that force the bare command into the login path. */ +const LOGIN_ONLY_FLAGS = [ + "token", + "force", + "url", + "read-only", + "scope", +] as const; + +/** + * Choose login vs status for bare `sentry auth`. + * + * - Login-only flags always select login (even when already authenticated). + * - Otherwise: logged out → login, logged in → status. + */ +export function resolveAuthDefaultTarget( + flags: Readonly>, + authenticated = isAuthenticated() +): "login" | "status" { + for (const name of LOGIN_ONLY_FLAGS) { + const value = flags[name]; + if (value === undefined || value === false) { + continue; + } + if (Array.isArray(value) && value.length === 0) { + continue; + } + return "login"; + } + return authenticated ? "status" : "login"; +} + +async function invokeCommand( + command: typeof loginCommand | typeof statusCommand, + context: SentryContext, + flags: Record +): Promise { + const loaded = await command.loader(); + const func = typeof loaded === "function" ? loaded : loaded.default; + await func.call(context, flags); +} + +/** + * Hidden default for bare `sentry auth`. + * + * Accepts the union of login and status flags so either path works without + * an explicit subcommand. Rendering is delegated to the selected command. + */ +export const authDefaultCommand = buildCommand({ + auth: false, + skipRcUrlCheck: true, + docs: { + brief: "Authenticate with Sentry or show auth status", + fullDescription: + "When not authenticated, starts the login flow. When already authenticated, shows auth status. " + + "Equivalent to `sentry auth login` or `sentry auth status` depending on current credentials. " + + "Login-only flags (e.g. --token, --url) force the login path.", + }, + parameters: { + flags: { + token: { + kind: "parsed", + parse: String, + brief: "Authenticate using an API token instead of OAuth", + optional: true, + }, + timeout: { + kind: "parsed", + parse: numberParser, + brief: "Timeout for OAuth flow in seconds (default: 900)", + default: "900", + }, + force: { + kind: "boolean", + brief: "Re-authenticate without prompting", + default: false, + }, + url: { + kind: "parsed", + parse: parseLoginUrl, + brief: + "Sentry instance URL to authenticate against (e.g. https://sentry.example.com). " + + "Required for self-hosted; defaults to SaaS (https://sentry.io).", + optional: true, + }, + "read-only": { + kind: "boolean", + brief: + "Request only read-only OAuth scopes (project:read, org:read, event:read, member:read, team:read). " + + "Useful for handing tokens to AI agents or CI jobs that should not be able to mutate Sentry state.", + default: false, + }, + scope: { + kind: "parsed", + parse: String, + brief: + "Request specific OAuth scopes (repeatable, comma-separated). " + + "E.g. --scope project:read --scope org:read. Overrides the default scope set.", + variadic: true, + optional: true, + }, + "show-token": { + kind: "boolean", + brief: "Show the stored token (masked by default)", + default: false, + }, + fresh: FRESH_FLAG, + // Declared explicitly because this dispatcher has no output config of + // its own — login/status own rendering, but bare `sentry auth --json` + // still needs to parse these flags. + json: JSON_FLAG, + fields: FIELDS_FLAG, + }, + aliases: { s: "scope", ...FRESH_ALIASES }, + }, + // No output config: the selected login/status command owns rendering. + // biome-ignore lint/correctness/useYield: dispatcher awaits nested command handlers + async *func(this: SentryContext, flags: AuthDefaultFlags) { + const target = resolveAuthDefaultTarget(flags); + const command = target === "login" ? loginCommand : statusCommand; + await invokeCommand( + command, + this, + flags as unknown as Record + ); + }, +}); diff --git a/packages/cli/src/commands/auth/index.ts b/packages/cli/src/commands/auth/index.ts index 4b8c1916d6..56fb3ed49f 100644 --- a/packages/cli/src/commands/auth/index.ts +++ b/packages/cli/src/commands/auth/index.ts @@ -1,4 +1,5 @@ import { buildRouteMap } from "../../lib/route-map.js"; +import { authDefaultCommand } from "./default.js"; import { loginCommand } from "./login.js"; import { logoutCommand } from "./logout.js"; import { refreshCommand } from "./refresh.js"; @@ -8,6 +9,8 @@ import { whoamiCommand } from "./whoami.js"; export const authRoute = buildRouteMap({ routes: { + // Hidden smart default: login when logged out, status when logged in. + default: authDefaultCommand, login: loginCommand, logout: logoutCommand, refresh: refreshCommand, @@ -15,14 +18,16 @@ export const authRoute = buildRouteMap({ token: tokenCommand, whoami: whoamiCommand, }, - defaultCommand: "login", + defaultCommand: "default", docs: { brief: "Authenticate with Sentry", fullDescription: - "Manage authentication with Sentry. Use 'sentry auth' (or 'sentry auth login') to authenticate, " + - "'sentry auth logout' to remove credentials, 'sentry auth refresh' to manually refresh your OAuth access token, " + - "'sentry auth status' to check your authentication status, " + - "'sentry auth whoami' to show your current user identity, " + - "and 'sentry auth token' to print your token for use in scripts.", + "Manage authentication with Sentry. Use 'sentry auth' to log in when logged out " + + "or show status when logged in. Explicit subcommands: 'sentry auth login', " + + "'sentry auth logout', 'sentry auth refresh', 'sentry auth status', " + + "'sentry auth whoami', and 'sentry auth token'.", + hideRoute: { + default: true, + }, }, }); diff --git a/packages/cli/test/commands/auth/default.test.ts b/packages/cli/test/commands/auth/default.test.ts new file mode 100644 index 0000000000..9d35da679f --- /dev/null +++ b/packages/cli/test/commands/auth/default.test.ts @@ -0,0 +1,155 @@ +/** + * Tests for bare `sentry auth` smart default routing. + */ + +import { describe, expect, test, vi } from "vitest"; +import { + authDefaultCommand, + resolveAuthDefaultTarget, +} from "../../../src/commands/auth/default.js"; + +describe("resolveAuthDefaultTarget", () => { + test("logged out → login", () => { + expect(resolveAuthDefaultTarget({}, false)).toBe("login"); + }); + + test("logged in → status", () => { + expect(resolveAuthDefaultTarget({}, true)).toBe("status"); + }); + + test("login-only flags force login even when authenticated", () => { + expect(resolveAuthDefaultTarget({ token: "t" }, true)).toBe("login"); + expect(resolveAuthDefaultTarget({ force: true }, true)).toBe("login"); + expect( + resolveAuthDefaultTarget({ url: "https://sentry.example.com" }, true) + ).toBe("login"); + expect(resolveAuthDefaultTarget({ "read-only": true }, true)).toBe("login"); + expect(resolveAuthDefaultTarget({ scope: ["org:read"] }, true)).toBe( + "login" + ); + }); + + test("status-only flags keep status when authenticated", () => { + expect( + resolveAuthDefaultTarget({ "show-token": true, fresh: true }, true) + ).toBe("status"); + }); + + test("empty scope array does not force login", () => { + expect(resolveAuthDefaultTarget({ scope: [] }, true)).toBe("status"); + }); +}); + +describe("authDefaultCommand", () => { + test("dispatches to login when logged out", async () => { + const isAuthenticated = vi.spyOn( + await import("../../../src/lib/db/auth.js"), + "isAuthenticated" + ); + isAuthenticated.mockReturnValue(false); + + const loginLoader = vi.fn(async () => + vi.fn(async () => { + // login path + }) + ); + const statusLoader = vi.fn(async () => + vi.fn(async () => { + // status path + }) + ); + + const { loginCommand } = await import( + "../../../src/commands/auth/login.js" + ); + const { statusCommand } = await import( + "../../../src/commands/auth/status.js" + ); + const loginSpy = vi + .spyOn(loginCommand, "loader") + .mockImplementation(loginLoader); + const statusSpy = vi + .spyOn(statusCommand, "loader") + .mockImplementation(statusLoader); + + const context = { + stdout: { write: vi.fn() }, + stderr: { write: vi.fn() }, + cwd: "/tmp", + env: process.env, + }; + + const func = await authDefaultCommand.loader(); + await func.call(context as never, { + timeout: 900, + force: false, + "read-only": false, + "show-token": false, + fresh: false, + json: false, + }); + + expect(loginSpy).toHaveBeenCalledOnce(); + expect(statusSpy).not.toHaveBeenCalled(); + + loginSpy.mockRestore(); + statusSpy.mockRestore(); + isAuthenticated.mockRestore(); + }); + + test("dispatches to status when logged in", async () => { + const isAuthenticated = vi.spyOn( + await import("../../../src/lib/db/auth.js"), + "isAuthenticated" + ); + isAuthenticated.mockReturnValue(true); + + const loginLoader = vi.fn(async () => + vi.fn(async () => { + // login path + }) + ); + const statusLoader = vi.fn(async () => + vi.fn(async () => { + // status path + }) + ); + + const { loginCommand } = await import( + "../../../src/commands/auth/login.js" + ); + const { statusCommand } = await import( + "../../../src/commands/auth/status.js" + ); + const loginSpy = vi + .spyOn(loginCommand, "loader") + .mockImplementation(loginLoader); + const statusSpy = vi + .spyOn(statusCommand, "loader") + .mockImplementation(statusLoader); + + const context = { + stdout: { write: vi.fn() }, + stderr: { write: vi.fn() }, + cwd: "/tmp", + env: process.env, + }; + + const func = await authDefaultCommand.loader(); + await func.call(context as never, { + timeout: 900, + force: false, + "read-only": false, + "show-token": false, + fresh: false, + json: false, + }); + + expect(statusSpy).toHaveBeenCalledOnce(); + expect(loginSpy).not.toHaveBeenCalled(); + + loginSpy.mockRestore(); + statusSpy.mockRestore(); + isAuthenticated.mockRestore(); + }); +}); diff --git a/packages/cli/test/lib/command-suggestions.test.ts b/packages/cli/test/lib/command-suggestions.test.ts index 0f8ff98a8a..8dc8c067c9 100644 --- a/packages/cli/test/lib/command-suggestions.test.ts +++ b/packages/cli/test/lib/command-suggestions.test.ts @@ -144,17 +144,18 @@ describe("routes with defaultCommand", () => { } }); - test("auth route group has defaultCommand (login)", () => { + test("auth route group has defaultCommand (smart default)", () => { expect(routesWithDefault.has("auth")).toBe(true); const authEntry = routes .getAllEntries() .find((e) => e.name.original === "auth"); const authMap = authEntry?.target as unknown as RouteMap; const defaultCmd = authMap.getDefaultCommand?.(); - const defaultName = authMap + const defaultEntry = authMap .getAllEntries() - .find((e) => e.target === defaultCmd)?.name.original; - expect(defaultName).toBe("login"); + .find((e) => e.target === defaultCmd); + expect(defaultEntry?.name.original).toBe("default"); + expect(defaultEntry?.hidden).toBe(true); }); test("route groups without defaultCommand", () => { From 448bd4b124fe37a338e811a1c93426297e6fa559 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 22:54:35 +0000 Subject: [PATCH 3/6] fix(cli): Align installer welcome command columns Pad the shorter `sentry auth` line so descriptions line up with `sentry --help` in the post-install Get started output. Co-Authored-By: David Cramer --- packages/cli/src/commands/cli/setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/cli/setup.ts b/packages/cli/src/commands/cli/setup.ts index 2da4367244..c7a7bf1747 100644 --- a/packages/cli/src/commands/cli/setup.ts +++ b/packages/cli/src/commands/cli/setup.ts @@ -315,7 +315,7 @@ function printWelcomeMessage( emit(`Installed sentry v${version} to ${binaryPath}`); emit(""); emit("Get started:"); - emit(" sentry auth Authenticate with Sentry"); + emit(" sentry auth Authenticate with Sentry"); emit(" sentry --help See all available commands"); emit(""); emit("https://cli.sentry.dev"); From 0945ac0acf89f15413afb7e998603389ccae0046 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 6 Aug 2026 23:11:46 +0000 Subject: [PATCH 4/6] chore: regenerate docs --- .../sentry-cli/skills/sentry-cli/references/issue.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md index 2324b3b93d..99f49ca221 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md @@ -236,6 +236,10 @@ Mark an issue as resolved **Flags:** - `-i, --in - Resolve in a release, next release, or commit ('' | '@next' | '@commit' | '@commit:@')` +### `sentry issue unresolve ` + +Reopen a resolved issue + **Examples:** ```bash @@ -265,10 +269,6 @@ sentry issue unresolve CLI-G5 sentry issue reopen CLI-G5 # alias ``` -### `sentry issue unresolve ` - -Reopen a resolved issue - ### `sentry issue archive ` Archive (ignore) an issue From 234adb9781e3da7a1c852ab90720ca40e375e05b Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Thu, 6 Aug 2026 23:13:22 +0000 Subject: [PATCH 5/6] fix(skill): satisfy biome lint on skill example matching Group associateCodeBlocks doc-context params into an options object to clear useMaxParams, and reflow the test array to biome's formatting. --- packages/cli/script/generate-skill.ts | 18 ++++++++++-------- .../script/generate-skill-markdown.test.ts | 6 +----- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/cli/script/generate-skill.ts b/packages/cli/script/generate-skill.ts index 1dc4673952..e87189dbb6 100644 --- a/packages/cli/script/generate-skill.ts +++ b/packages/cli/script/generate-skill.ts @@ -369,11 +369,14 @@ function collectCommandPaths( */ function associateCodeBlocks( tokens: Token[], - commandPaths: string[], - commandGroup: string, - examples: Map, - defaultCommandPath?: string + context: { + commandPaths: string[]; + commandGroup: string; + examples: Map; + defaultCommandPath?: string; + } ): void { + const { commandPaths, commandGroup, examples, defaultCommandPath } = context; const groupFallback = `sentry ${commandGroup}`; let currentCmd: string | null = null; @@ -425,13 +428,12 @@ async function loadCommandExamples( const defaultCommandPath = defaultCommandName ? `sentry ${commandGroup} ${defaultCommandName}` : undefined; - associateCodeBlocks( - tokens, + associateCodeBlocks(tokens, { commandPaths, commandGroup, examples, - defaultCommandPath - ); + defaultCommandPath, + }); return examples; } diff --git a/packages/cli/test/script/generate-skill-markdown.test.ts b/packages/cli/test/script/generate-skill-markdown.test.ts index c5f4fefa8c..374ee8d5bc 100644 --- a/packages/cli/test/script/generate-skill-markdown.test.ts +++ b/packages/cli/test/script/generate-skill-markdown.test.ts @@ -44,11 +44,7 @@ describe("matchExampleToCommand", () => { expect( matchExampleToCommand( "sentry auth\nsentry auth --token YOUR_SENTRY_API_TOKEN", - [ - "sentry auth login", - "sentry auth logout", - "sentry auth status", - ], + ["sentry auth login", "sentry auth logout", "sentry auth status"], "sentry auth", "sentry auth login" ) From 64c4a6da57340e457df33148b937b316b414482c Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 23:20:26 +0000 Subject: [PATCH 6/6] fix(auth): Avoid double command pipeline on bare sentry auth Dispatch into unwrapped login/status generators via __rawFunc so auth guards, rc-URL trust checks, telemetry, and output rendering run once under the bare-auth command settings. --- packages/cli/src/commands/auth/default.ts | 88 +++++++---- packages/cli/src/commands/auth/login.ts | 2 +- packages/cli/src/lib/command.ts | 5 + .../cli/test/commands/auth/default.test.ts | 149 +++++++++--------- 4 files changed, 137 insertions(+), 107 deletions(-) diff --git a/packages/cli/src/commands/auth/default.ts b/packages/cli/src/commands/auth/default.ts index 5e5f883812..aba8ed2122 100644 --- a/packages/cli/src/commands/auth/default.ts +++ b/packages/cli/src/commands/auth/default.ts @@ -3,19 +3,22 @@ * * Routes to login when logged out (or when login-only flags are present), * and to status when already authenticated. + * + * Dispatches into the *unwrapped* login/status generators so the command + * pipeline (auth guard, rc-URL trust check, telemetry, output rendering) + * runs exactly once under this command's settings. */ +import type { Command } from "@stricli/core"; import type { SentryContext } from "../../context.js"; -import { - buildCommand, - FIELDS_FLAG, - JSON_FLAG, - numberParser, -} from "../../lib/command.js"; +import { buildCommand, numberParser } from "../../lib/command.js"; import { isAuthenticated } from "../../lib/db/auth.js"; +import { formatAuthStatus } from "../../lib/formatters/human.js"; +import { CommandOutput } from "../../lib/formatters/output.js"; +import type { LoginResult } from "../../lib/interactive-login.js"; import { FRESH_ALIASES, FRESH_FLAG } from "../../lib/list-command.js"; -import { loginCommand, parseLoginUrl } from "./login.js"; -import { statusCommand } from "./status.js"; +import { formatLoginResult, loginCommand, parseLoginUrl } from "./login.js"; +import { type AuthStatusData, statusCommand } from "./status.js"; type AuthDefaultFlags = { readonly token?: string; @@ -26,8 +29,6 @@ type AuthDefaultFlags = { readonly scope?: readonly string[]; readonly "show-token": boolean; readonly fresh: boolean; - readonly json?: boolean; - readonly fields?: string[]; }; /** Login-only flags that force the bare command into the login path. */ @@ -62,24 +63,51 @@ export function resolveAuthDefaultTarget( return authenticated ? "status" : "login"; } -async function invokeCommand( - command: typeof loginCommand | typeof statusCommand, - context: SentryContext, - flags: Record -): Promise { - const loaded = await command.loader(); - const func = typeof loaded === "function" ? loaded : loaded.default; - await func.call(context, flags); +type RawCommandFunc = ( + this: SentryContext, + flags: Record, + ...args: unknown[] +) => AsyncGenerator; + +/** Read the unwrapped generator attached by {@link buildCommand}. */ +function getRawFunc(command: Command): RawCommandFunc { + const raw = (command as unknown as { __rawFunc?: RawCommandFunc }).__rawFunc; + if (!raw) { + throw new Error( + "Command is missing __rawFunc; expected buildCommand output" + ); + } + return raw; +} + +function isLoginResult(data: unknown): data is LoginResult { + return ( + !!data && + typeof data === "object" && + "method" in data && + "configPath" in data + ); +} + +function formatAuthDefaultOutput(data: LoginResult | AuthStatusData): string { + if (isLoginResult(data)) { + return formatLoginResult(data); + } + return formatAuthStatus(data); } /** * Hidden default for bare `sentry auth`. * * Accepts the union of login and status flags so either path works without - * an explicit subcommand. Rendering is delegated to the selected command. + * an explicit subcommand. Renders through this command's single pipeline. */ export const authDefaultCommand = buildCommand({ auth: false, + // Login must run even with a poisoned .sentryclirc URL. Status is also + // reached through this path when already authenticated — matching the + // onboarding intent of bare `sentry auth` rather than re-applying the + // stricter explicit-status rc gate via a nested wrapper. skipRcUrlCheck: true, docs: { brief: "Authenticate with Sentry or show auth status", @@ -137,23 +165,27 @@ export const authDefaultCommand = buildCommand({ default: false, }, fresh: FRESH_FLAG, - // Declared explicitly because this dispatcher has no output config of - // its own — login/status own rendering, but bare `sentry auth --json` - // still needs to parse these flags. - json: JSON_FLAG, - fields: FIELDS_FLAG, }, aliases: { s: "scope", ...FRESH_ALIASES }, }, - // No output config: the selected login/status command owns rendering. - // biome-ignore lint/correctness/useYield: dispatcher awaits nested command handlers + output: { human: formatAuthDefaultOutput }, async *func(this: SentryContext, flags: AuthDefaultFlags) { const target = resolveAuthDefaultTarget(flags); const command = target === "login" ? loginCommand : statusCommand; - await invokeCommand( - command, + const raw = getRawFunc(command as Command); + const generator = raw.call( this, flags as unknown as Record ); + + // Re-yield CommandOutput values so this command's wrapper renders once. + let step = await generator.next(); + while (!step.done) { + if (step.value instanceof CommandOutput) { + yield step.value; + } + step = await generator.next(); + } + return step.value as { hint?: string } | undefined; }, }); diff --git a/packages/cli/src/commands/auth/login.ts b/packages/cli/src/commands/auth/login.ts index 133da8791f..a2621886c4 100644 --- a/packages/cli/src/commands/auth/login.ts +++ b/packages/cli/src/commands/auth/login.ts @@ -54,7 +54,7 @@ import { registerLoginTrustAnchor } from "../../lib/token-host.js"; const log = logger.withTag("auth.login"); /** Format a {@link LoginResult} for human-readable terminal output. */ -function formatLoginResult(result: LoginResult): string { +export function formatLoginResult(result: LoginResult): string { const lines: string[] = []; lines.push( success( diff --git a/packages/cli/src/lib/command.ts b/packages/cli/src/lib/command.ts index 956fb53ab2..9b5efea99f 100644 --- a/packages/cli/src/lib/command.ts +++ b/packages/cli/src/lib/command.ts @@ -847,5 +847,10 @@ export function buildCommand< outputConfig.schema; } + // Expose the unwrapped generator for internal dispatchers (e.g. bare + // `sentry auth`) that must run a subcommand without re-entering the + // telemetry / auth-guard / rc-trust / output pipeline. + (cmd as unknown as Record).__rawFunc = originalFunc; + return cmd; } diff --git a/packages/cli/test/commands/auth/default.test.ts b/packages/cli/test/commands/auth/default.test.ts index 9d35da679f..2f7448667f 100644 --- a/packages/cli/test/commands/auth/default.test.ts +++ b/packages/cli/test/commands/auth/default.test.ts @@ -7,6 +7,9 @@ import { authDefaultCommand, resolveAuthDefaultTarget, } from "../../../src/commands/auth/default.js"; +import { loginCommand } from "../../../src/commands/auth/login.js"; +import { statusCommand } from "../../../src/commands/auth/status.js"; +import { CommandOutput } from "../../../src/lib/formatters/output.js"; describe("resolveAuthDefaultTarget", () => { test("logged out → login", () => { @@ -41,36 +44,29 @@ describe("resolveAuthDefaultTarget", () => { }); describe("authDefaultCommand", () => { - test("dispatches to login when logged out", async () => { + test("dispatches to login raw func when logged out", async () => { const isAuthenticated = vi.spyOn( await import("../../../src/lib/db/auth.js"), "isAuthenticated" ); isAuthenticated.mockReturnValue(false); - const loginLoader = vi.fn(async () => - vi.fn(async () => { - // login path - }) - ); - const statusLoader = vi.fn(async () => - vi.fn(async () => { - // status path - }) - ); + const loginRaw = vi.fn(async function* () { + yield new CommandOutput({ + method: "token", + configPath: "/tmp/sentry.db", + }); + }); + const statusRaw = vi.fn(async function* () { + yield new CommandOutput({ authenticated: true, source: "oauth" }); + }); - const { loginCommand } = await import( - "../../../src/commands/auth/login.js" - ); - const { statusCommand } = await import( - "../../../src/commands/auth/status.js" - ); - const loginSpy = vi - .spyOn(loginCommand, "loader") - .mockImplementation(loginLoader); - const statusSpy = vi - .spyOn(statusCommand, "loader") - .mockImplementation(statusLoader); + const loginCmd = loginCommand as unknown as { __rawFunc: unknown }; + const statusCmd = statusCommand as unknown as { __rawFunc: unknown }; + const prevLogin = loginCmd.__rawFunc; + const prevStatus = statusCmd.__rawFunc; + loginCmd.__rawFunc = loginRaw; + statusCmd.__rawFunc = statusRaw; const context = { stdout: { write: vi.fn() }, @@ -79,54 +75,49 @@ describe("authDefaultCommand", () => { env: process.env, }; - const func = await authDefaultCommand.loader(); - await func.call(context as never, { - timeout: 900, - force: false, - "read-only": false, - "show-token": false, - fresh: false, - json: false, - }); - - expect(loginSpy).toHaveBeenCalledOnce(); - expect(statusSpy).not.toHaveBeenCalled(); - - loginSpy.mockRestore(); - statusSpy.mockRestore(); - isAuthenticated.mockRestore(); + try { + const func = await authDefaultCommand.loader(); + await func.call(context as never, { + timeout: 900, + force: false, + "read-only": false, + "show-token": false, + fresh: false, + json: false, + }); + + expect(loginRaw).toHaveBeenCalledOnce(); + expect(statusRaw).not.toHaveBeenCalled(); + } finally { + loginCmd.__rawFunc = prevLogin; + statusCmd.__rawFunc = prevStatus; + isAuthenticated.mockRestore(); + } }); - test("dispatches to status when logged in", async () => { + test("dispatches to status raw func when logged in", async () => { const isAuthenticated = vi.spyOn( await import("../../../src/lib/db/auth.js"), "isAuthenticated" ); isAuthenticated.mockReturnValue(true); - const loginLoader = vi.fn(async () => - vi.fn(async () => { - // login path - }) - ); - const statusLoader = vi.fn(async () => - vi.fn(async () => { - // status path - }) - ); + const loginRaw = vi.fn(async function* () { + yield new CommandOutput({ + method: "token", + configPath: "/tmp/sentry.db", + }); + }); + const statusRaw = vi.fn(async function* () { + yield new CommandOutput({ authenticated: true, source: "oauth" }); + }); - const { loginCommand } = await import( - "../../../src/commands/auth/login.js" - ); - const { statusCommand } = await import( - "../../../src/commands/auth/status.js" - ); - const loginSpy = vi - .spyOn(loginCommand, "loader") - .mockImplementation(loginLoader); - const statusSpy = vi - .spyOn(statusCommand, "loader") - .mockImplementation(statusLoader); + const loginCmd = loginCommand as unknown as { __rawFunc: unknown }; + const statusCmd = statusCommand as unknown as { __rawFunc: unknown }; + const prevLogin = loginCmd.__rawFunc; + const prevStatus = statusCmd.__rawFunc; + loginCmd.__rawFunc = loginRaw; + statusCmd.__rawFunc = statusRaw; const context = { stdout: { write: vi.fn() }, @@ -135,21 +126,23 @@ describe("authDefaultCommand", () => { env: process.env, }; - const func = await authDefaultCommand.loader(); - await func.call(context as never, { - timeout: 900, - force: false, - "read-only": false, - "show-token": false, - fresh: false, - json: false, - }); - - expect(statusSpy).toHaveBeenCalledOnce(); - expect(loginSpy).not.toHaveBeenCalled(); - - loginSpy.mockRestore(); - statusSpy.mockRestore(); - isAuthenticated.mockRestore(); + try { + const func = await authDefaultCommand.loader(); + await func.call(context as never, { + timeout: 900, + force: false, + "read-only": false, + "show-token": false, + fresh: false, + json: false, + }); + + expect(statusRaw).toHaveBeenCalledOnce(); + expect(loginRaw).not.toHaveBeenCalled(); + } finally { + loginCmd.__rawFunc = prevLogin; + statusCmd.__rawFunc = prevStatus; + isAuthenticated.mockRestore(); + } }); });