From 29835a734540a55f0e5f8d6e7889ab8349c96fac Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Thu, 18 Jun 2026 17:01:18 +0000 Subject: [PATCH 1/5] feat: add `sentry token` subcommands (create, list, delete) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new `sentry token` command group for managing organization auth tokens: - `sentry token list [org]` — list active org auth tokens - `sentry token create [org] --name ` — create a new org auth token - `sentry token delete ` — delete (deactivate) a token Implementation: - API layer in src/lib/api/tokens.ts using control silo endpoints - OrgAuthToken Zod schema matching the Sentry API serializer - Delete command uses buildDeleteCommand with --yes/--force/--dry-run - Token resolution by ID or name for the delete command - Plural alias: `sentry tokens` → `sentry token list` Fixes #1110 --- src/app.ts | 6 ++ src/commands/token/create.ts | 112 ++++++++++++++++++++++ src/commands/token/delete.ts | 179 +++++++++++++++++++++++++++++++++++ src/commands/token/index.ts | 21 ++++ src/commands/token/list.ts | 98 +++++++++++++++++++ src/lib/api-client.ts | 5 + src/lib/api/tokens.ts | 82 ++++++++++++++++ src/types/index.ts | 2 + src/types/sentry.ts | 30 ++++++ 9 files changed, 535 insertions(+) create mode 100644 src/commands/token/create.ts create mode 100644 src/commands/token/delete.ts create mode 100644 src/commands/token/index.ts create mode 100644 src/commands/token/list.ts create mode 100644 src/lib/api/tokens.ts diff --git a/src/app.ts b/src/app.ts index 99168a749..5b6ea922f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -47,6 +47,8 @@ import { spanRoute } from "./commands/span/index.js"; import { listCommand as spanListCommand } from "./commands/span/list.js"; import { teamRoute } from "./commands/team/index.js"; import { listCommand as teamListCommand } from "./commands/team/list.js"; +import { tokenRoute } from "./commands/token/index.js"; +import { listCommand as tokenListCommand } from "./commands/token/list.js"; import { traceRoute } from "./commands/trace/index.js"; import { listCommand as traceListCommand } from "./commands/trace/list.js"; import { trialRoute } from "./commands/trial/index.js"; @@ -82,6 +84,7 @@ const PLURAL_TO_SINGULAR: Record = { releases: "release", repos: "repo", teams: "team", + tokens: "token", logs: "log", monitors: "monitor", replays: "replay", @@ -109,6 +112,7 @@ export const routes = buildRouteMap({ release: releaseRoute, repo: repoRoute, team: teamRoute, + token: tokenRoute, issue: issueRoute, event: eventRoute, events: eventListCommand, @@ -136,6 +140,7 @@ export const routes = buildRouteMap({ releases: releaseListCommand, repos: repoListCommand, teams: teamListCommand, + tokens: tokenListCommand, logs: logListCommand, monitors: monitorListCommand, spans: spanListCommand, @@ -159,6 +164,7 @@ export const routes = buildRouteMap({ releases: true, repos: true, teams: true, + tokens: true, logs: true, monitors: true, spans: true, diff --git a/src/commands/token/create.ts b/src/commands/token/create.ts new file mode 100644 index 000000000..5494fb7c3 --- /dev/null +++ b/src/commands/token/create.ts @@ -0,0 +1,112 @@ +/** + * sentry token create + * + * Create a new org auth token. The full token value is printed to stdout + * exactly once — it cannot be retrieved again after creation. + * + * Org auth tokens are scoped to `org:ci` and are intended for CI pipelines, + * release management, and other automated workflows. + */ + +import type { SentryContext } from "../../context.js"; +import { createOrgAuthToken } from "../../lib/api-client.js"; +import { buildCommand } from "../../lib/command.js"; +import { ContextError, ValidationError } from "../../lib/errors.js"; +import { success } from "../../lib/formatters/colors.js"; +import { CommandOutput } from "../../lib/formatters/output.js"; +import { resolveOrg } from "../../lib/resolve-target.js"; +import type { OrgAuthToken } from "../../types/index.js"; + +/** Result shape for output rendering. */ +type TokenCreateResult = { + token: OrgAuthToken; + orgSlug: string; +}; + +function formatTokenCreated(result: TokenCreateResult): string { + const lines: string[] = []; + lines.push( + success(`Created token '${result.token.name}' in ${result.orgSlug}`) + ); + lines.push(""); + if (result.token.token) { + lines.push(`Token: ${result.token.token}`); + lines.push(""); + lines.push("Save this token now — it will not be shown again."); + } + lines.push(`ID: ${result.token.id}`); + lines.push(`Scopes: ${result.token.scopes.join(", ")}`); + return lines.join("\n"); +} + +type CreateFlags = { + readonly name?: string; + readonly json: boolean; + readonly fields?: string[]; +}; + +export const createCommand = buildCommand({ + docs: { + brief: "Create an org auth token", + fullDescription: + "Create a new organization auth token with org:ci scope.\n\n" + + "The full token value is printed exactly once — save it immediately.\n" + + "Subsequent requests only show the last 4 characters.\n\n" + + "Examples:\n" + + " sentry token create my-org --name 'CI deploy token'\n" + + " sentry token create --name 'release-bot' # auto-detect org\n" + + " sentry token create my-org --name ci --json", + }, + output: { + human: formatTokenCreated, + }, + parameters: { + positional: { + kind: "tuple", + parameters: [ + { + placeholder: "org", + brief: "Organization slug", + parse: String, + optional: true, + }, + ], + }, + flags: { + name: { + kind: "parsed", + parse: String, + brief: "Name for the new token", + optional: true, + }, + }, + }, + async *func(this: SentryContext, flags: CreateFlags, orgArg?: string) { + const { cwd } = this; + + const resolved = await resolveOrg({ org: orgArg, cwd }); + if (!resolved) { + throw new ContextError( + "Organization", + "sentry token create --name ", + [] + ); + } + const orgSlug = resolved.org; + + const name = flags.name; + if (!name) { + throw new ValidationError( + "Token name is required. Use --name to specify a name.", + "name" + ); + } + + const token = await createOrgAuthToken(orgSlug, name); + + yield new CommandOutput({ token, orgSlug }); + return { + hint: "Save the token value now — it cannot be retrieved later.", + }; + }, +}); diff --git a/src/commands/token/delete.ts b/src/commands/token/delete.ts new file mode 100644 index 000000000..88e195850 --- /dev/null +++ b/src/commands/token/delete.ts @@ -0,0 +1,179 @@ +/** + * sentry token delete + * + * Delete (deactivate) an org auth token by ID. + * + * Uses `buildDeleteCommand` for standard --yes/--force/--dry-run flags + * and non-interactive safety guards. + */ + +import type { SentryContext } from "../../context.js"; +import { deleteOrgAuthToken, listOrgAuthTokens } from "../../lib/api-client.js"; +import { ContextError, ResolutionError } from "../../lib/errors.js"; +import { CommandOutput } from "../../lib/formatters/output.js"; +import { logger } from "../../lib/logger.js"; +import { + buildDeleteCommand, + confirmByTyping, + isConfirmationBypassed, +} from "../../lib/mutate-command.js"; +import { resolveOrg } from "../../lib/resolve-target.js"; + +const log = logger.withTag("token.delete"); + +/** Result shape for output rendering. */ +type TokenDeleteResult = { + tokenId: string; + tokenName: string; + orgSlug: string; + dryRun?: boolean; +}; + +function formatTokenDeleted(result: TokenDeleteResult): string { + if (result.dryRun) { + return `Would delete token '${result.tokenName}' (ID: ${result.tokenId}) from ${result.orgSlug}`; + } + return `Deleted token '${result.tokenName}' (ID: ${result.tokenId}) from ${result.orgSlug}`; +} + +type DeleteFlags = { + readonly yes: boolean; + readonly force: boolean; + readonly "dry-run": boolean; + readonly json: boolean; + readonly fields?: string[]; +}; + +/** + * Resolve a token by ID or name within an org's token list. + * + * Accepts either a numeric ID or a token name. When matching by name, + * requires an exact match (case-sensitive). + */ +async function resolveToken( + orgSlug: string, + tokenRef: string +): Promise<{ id: string; name: string }> { + const tokens = await listOrgAuthTokens(orgSlug); + + const byId = tokens.find((t) => t.id === tokenRef); + if (byId) { + return { id: byId.id, name: byId.name }; + } + + const byName = tokens.filter((t) => t.name === tokenRef); + if (byName.length === 1 && byName[0]) { + return { id: byName[0].id, name: byName[0].name }; + } + if (byName.length > 1) { + throw new ResolutionError( + `Token name '${tokenRef}'`, + `matches ${byName.length} tokens`, + "sentry token delete ", + byName.map( + (t) => `ID ${t.id}: ${t.name} (…${t.tokenLastCharacters ?? ""})` + ) + ); + } + + const hints = + tokens.length > 0 + ? tokens.slice(0, 5).map((t) => `${t.id}: ${t.name}`) + : ["No tokens found in this organization"]; + throw new ResolutionError( + `Token '${tokenRef}'`, + `not found in ${orgSlug}`, + `sentry token list ${orgSlug}`, + hints + ); +} + +export const deleteCommand = buildDeleteCommand({ + docs: { + brief: "Delete an org auth token", + fullDescription: + "Delete (deactivate) an organization auth token by ID or name.\n\n" + + "The token immediately stops working for API authentication.\n\n" + + "Examples:\n" + + " sentry token delete my-org 12345\n" + + " sentry token delete my-org 'CI deploy token'\n" + + " sentry token delete my-org 12345 --yes\n" + + " sentry token delete my-org 12345 --dry-run", + }, + output: { + human: formatTokenDeleted, + jsonTransform: (result: TokenDeleteResult) => ({ + deleted: !result.dryRun, + dryRun: result.dryRun ?? false, + tokenId: result.tokenId, + tokenName: result.tokenName, + org: result.orgSlug, + }), + }, + parameters: { + positional: { + kind: "tuple", + parameters: [ + { + placeholder: "org", + brief: "Organization slug", + parse: String, + }, + { + placeholder: "token-id", + brief: "Token ID or name", + parse: String, + }, + ], + }, + }, + async *func( + this: SentryContext, + flags: DeleteFlags, + orgArg: string, + tokenRef: string + ) { + const { cwd } = this; + + const resolved = await resolveOrg({ org: orgArg, cwd }); + if (!resolved) { + throw new ContextError( + "Organization", + "sentry token delete ", + [] + ); + } + const orgSlug = resolved.org; + + const token = await resolveToken(orgSlug, tokenRef); + + if (flags["dry-run"]) { + yield new CommandOutput({ + tokenId: token.id, + tokenName: token.name, + orgSlug, + dryRun: true, + }); + return; + } + + if (!isConfirmationBypassed(flags)) { + const confirmed = await confirmByTyping( + token.name, + `Type '${token.name}' to delete token (ID: ${token.id}):` + ); + if (!confirmed) { + log.info("Cancelled."); + return; + } + } + + await deleteOrgAuthToken(orgSlug, token.id); + + yield new CommandOutput({ + tokenId: token.id, + tokenName: token.name, + orgSlug, + }); + }, +}); diff --git a/src/commands/token/index.ts b/src/commands/token/index.ts new file mode 100644 index 000000000..99d590857 --- /dev/null +++ b/src/commands/token/index.ts @@ -0,0 +1,21 @@ +import { buildRouteMap } from "../../lib/route-map.js"; +import { createCommand } from "./create.js"; +import { deleteCommand } from "./delete.js"; +import { listCommand } from "./list.js"; + +export const tokenRoute = buildRouteMap({ + routes: { + create: createCommand, + delete: deleteCommand, + list: listCommand, + }, + defaultCommand: "list", + docs: { + brief: "Manage org auth tokens", + fullDescription: + "Create, list, and delete organization auth tokens.\n\n" + + "Org auth tokens are used for CI pipelines, release management,\n" + + "and other automated workflows. They are scoped to org:ci.\n\n" + + "Alias: `sentry tokens` → `sentry token list`", + }, +}); diff --git a/src/commands/token/list.ts b/src/commands/token/list.ts new file mode 100644 index 000000000..8d26c510d --- /dev/null +++ b/src/commands/token/list.ts @@ -0,0 +1,98 @@ +/** + * sentry token list + * + * List active org auth tokens for an organization. + * Tokens are sorted by last-used date (most recent first). + */ + +import type { SentryContext } from "../../context.js"; +import { listOrgAuthTokens } from "../../lib/api-client.js"; +import { buildCommand } from "../../lib/command.js"; +import { ContextError } from "../../lib/errors.js"; +import { escapeMarkdownCell } from "../../lib/formatters/markdown.js"; +import { CommandOutput } from "../../lib/formatters/output.js"; +import { type Column, formatTable } from "../../lib/formatters/table.js"; +import { formatRelativeTime } from "../../lib/formatters/time-utils.js"; +import { resolveOrg } from "../../lib/resolve-target.js"; +import type { OrgAuthToken } from "../../types/index.js"; + +/** Column definitions for the token table. */ +const TOKEN_COLUMNS: Column[] = [ + { header: "ID", value: (t) => t.id }, + { header: "NAME", value: (t) => escapeMarkdownCell(t.name) }, + { + header: "SCOPES", + value: (t) => t.scopes.join(", "), + }, + { + header: "LAST 4", + value: (t) => t.tokenLastCharacters ?? "", + }, + { + header: "LAST USED", + value: (t) => + t.dateLastUsed ? formatRelativeTime(t.dateLastUsed) : "never", + }, +]; + +/** Token list result for output rendering. */ +type TokenListResult = { + tokens: OrgAuthToken[]; + orgSlug: string; +}; + +function formatTokenList(result: TokenListResult): string { + if (result.tokens.length === 0) { + return `No active tokens found for ${result.orgSlug}.`; + } + const header = `${result.tokens.length} token${result.tokens.length === 1 ? "" : "s"} in ${result.orgSlug}`; + return `${header}\n\n${formatTable(result.tokens, TOKEN_COLUMNS)}`; +} + +type ListFlags = { + readonly json: boolean; + readonly fields?: string[]; +}; + +export const listCommand = buildCommand({ + docs: { + brief: "List org auth tokens", + fullDescription: + "List active organization auth tokens.\n\n" + + "Shows token ID, name, scopes, last 4 characters, and last-used date.\n\n" + + "Examples:\n" + + " sentry token list my-org\n" + + " sentry token list # auto-detect org\n" + + " sentry token list --json", + }, + output: { + human: formatTokenList, + jsonTransform: (result: TokenListResult) => result.tokens, + }, + parameters: { + positional: { + kind: "tuple", + parameters: [ + { + placeholder: "org", + brief: "Organization slug", + parse: String, + optional: true, + }, + ], + }, + }, + async *func(this: SentryContext, _flags: ListFlags, orgArg?: string) { + const { cwd } = this; + + const resolved = await resolveOrg({ org: orgArg, cwd }); + if (!resolved) { + throw new ContextError("Organization", "sentry token list ", []); + } + const orgSlug = resolved.org; + + const tokens = await listOrgAuthTokens(orgSlug); + + yield new CommandOutput({ tokens, orgSlug }); + }, +}); diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index 82ae7058b..69861115e 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -175,6 +175,11 @@ export { listTeams, listTeamsPaginated, } from "./api/teams.js"; +export { + createOrgAuthToken, + deleteOrgAuthToken, + listOrgAuthTokens, +} from "./api/tokens.js"; export type { FetchMultiSpanDetailsOptions, TraceItemAttribute, diff --git a/src/lib/api/tokens.ts b/src/lib/api/tokens.ts new file mode 100644 index 000000000..cbbb46a08 --- /dev/null +++ b/src/lib/api/tokens.ts @@ -0,0 +1,82 @@ +/** + * Org Auth Token API functions + * + * CRUD operations for organization-level authentication tokens. + * These endpoints live on the control silo and manage org auth tokens + * (used for CI, release management, and other automated workflows). + */ + +import { z } from "zod"; +import { type OrgAuthToken, OrgAuthTokenSchema } from "../../types/index.js"; +import { getControlSiloUrl } from "../sentry-client.js"; +import { + apiRequestToRegion, + apiRequestToRegionNoContent, +} from "./infrastructure.js"; + +/** + * List active org auth tokens for an organization. + * + * Returns tokens sorted by last-used date (most recent first), with + * never-used tokens sorted by name then creation date. + * + * @param orgSlug - Organization slug + * @returns Array of org auth tokens (without full token values) + */ +export async function listOrgAuthTokens( + orgSlug: string +): Promise { + const { data } = await apiRequestToRegion( + getControlSiloUrl(), + `/organizations/${orgSlug}/org-auth-tokens/`, + { schema: z.array(OrgAuthTokenSchema) } + ); + return data; +} + +/** + * Create a new org auth token. + * + * The response includes the full token value in the `token` field — this is + * the only time it is available. Subsequent GET requests only return the + * last 4 characters. + * + * @param orgSlug - Organization slug + * @param name - Human-readable name for the token + * @returns The created token (including the full token value) + */ +export async function createOrgAuthToken( + orgSlug: string, + name: string +): Promise { + const { data } = await apiRequestToRegion( + getControlSiloUrl(), + `/organizations/${orgSlug}/org-auth-tokens/`, + { + method: "POST", + body: { name }, + schema: OrgAuthTokenSchema, + } + ); + return data; +} + +/** + * Delete (deactivate) an org auth token. + * + * Tokens are soft-deleted by setting `date_deactivated`. The token + * immediately stops working for API authentication. + * + * @param orgSlug - Organization slug + * @param tokenId - Numeric token ID + */ +export async function deleteOrgAuthToken( + orgSlug: string, + tokenId: string +): Promise { + await apiRequestToRegionNoContent( + getControlSiloUrl(), + `/organizations/${orgSlug}/org-auth-tokens/${tokenId}/`, + { method: "DELETE" } + ); +} diff --git a/src/types/index.ts b/src/types/index.ts index c3defe907..97f491d7d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -117,6 +117,7 @@ export type { LogsResponse, Mechanism, MonitorConfig, + OrgAuthToken, OsContext, ProductTrial, ProjectKey, @@ -160,6 +161,7 @@ export { IssueViewOutputSchema, LogsResponseSchema, MonitorConfigSchema, + OrgAuthTokenSchema, ProductTrialSchema, RegionSchema, RepositoryProviderSchema, diff --git a/src/types/sentry.ts b/src/types/sentry.ts index e52e5f8a2..cfa38faa4 100644 --- a/src/types/sentry.ts +++ b/src/types/sentry.ts @@ -1291,3 +1291,33 @@ export const CustomerTrialInfoSchema = z.object({ }); export type CustomerTrialInfo = z.infer; + +// Org Auth Tokens + +/** + * Zod schema for an organization auth token as returned by the Sentry API. + * + * Matches the serializer in `sentry/api/serializers/models/orgauthtoken.py`. + * The `token` field is only present in the POST response (token creation) — + * it is the only time the full token value is available. + */ +export const OrgAuthTokenSchema = z.object({ + /** Numeric token ID (string-encoded) */ + id: z.string(), + /** Human-readable token name */ + name: z.string(), + /** Permission scopes granted to this token (e.g., ["org:ci"]) */ + scopes: z.array(z.string()), + /** Last 4 characters of the token (for identification) */ + tokenLastCharacters: z.string().nullable().optional(), + /** ISO 8601 date when the token was created */ + dateCreated: z.string(), + /** ISO 8601 date when the token was last used, null if never */ + dateLastUsed: z.string().nullable().optional(), + /** ID of the project where the token was last used, null if never */ + projectLastUsedId: z.string().nullable().optional(), + /** Full token value — only present in the creation response */ + token: z.string().optional(), +}); + +export type OrgAuthToken = z.infer; From 49d566c41918c037fd880e9d7942446114e8e9c1 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Thu, 18 Jun 2026 17:06:23 +0000 Subject: [PATCH 2/5] docs: add token command fragment for check:fragments validation --- docs/src/fragments/commands/token.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 docs/src/fragments/commands/token.md diff --git a/docs/src/fragments/commands/token.md b/docs/src/fragments/commands/token.md new file mode 100644 index 000000000..93f7fe2a7 --- /dev/null +++ b/docs/src/fragments/commands/token.md @@ -0,0 +1,21 @@ + + + +## Examples + +```bash +# List org auth tokens +sentry token list my-org + +# Create a new token +sentry token create my-org --name 'CI deploy token' + +# Delete a token by ID +sentry token delete my-org 12345 --yes + +# Delete a token (dry run) +sentry token delete my-org 12345 --dry-run + +# Output as JSON +sentry token list --json +``` From 886d86bf5f5d2ae50bb273549470c8dbd3537f70 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 18 Jun 2026 17:08:25 +0000 Subject: [PATCH 3/5] chore: regenerate docs --- docs/src/content/docs/contributing.md | 1 + plugins/sentry-cli/skills/sentry-cli/SKILL.md | 10 ++++ .../skills/sentry-cli/references/token.md | 53 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 plugins/sentry-cli/skills/sentry-cli/references/token.md diff --git a/docs/src/content/docs/contributing.md b/docs/src/content/docs/contributing.md index 4949ff1c1..1d0ec0721 100644 --- a/docs/src/content/docs/contributing.md +++ b/docs/src/content/docs/contributing.md @@ -73,6 +73,7 @@ cli/ │ │ ├── sourcemap/ # inject, resolve, upload │ │ ├── span/ # list, view │ │ ├── team/ # list +│ │ ├── token/ # create, delete, list │ │ ├── trace/ # list, logs, view │ │ ├── trial/ # list, start │ │ ├── api.ts # Make an authenticated API request diff --git a/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/plugins/sentry-cli/skills/sentry-cli/SKILL.md index e0dd8b4d3..49b5076bd 100644 --- a/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -481,6 +481,16 @@ Work with Sentry teams → Full flags and examples: `references/team.md` +### Token + +Manage org auth tokens + +- `sentry token create ` — Create an org auth token +- `sentry token delete ` — Delete an org auth token +- `sentry token list ` — List org auth tokens + +→ Full flags and examples: `references/token.md` + ### Explore Query aggregate event data (Explore) diff --git a/plugins/sentry-cli/skills/sentry-cli/references/token.md b/plugins/sentry-cli/skills/sentry-cli/references/token.md new file mode 100644 index 000000000..497692c64 --- /dev/null +++ b/plugins/sentry-cli/skills/sentry-cli/references/token.md @@ -0,0 +1,53 @@ +--- +name: sentry-cli-token +version: 0.38.0-dev.0 +description: Manage org auth tokens +requires: + bins: ["sentry"] + auth: true +--- + +# Token Commands + +Manage org auth tokens + +### `sentry token create ` + +Create an org auth token + +**Flags:** +- `--name - Name for the new token` + +### `sentry token delete ` + +Delete an org auth token + +**Flags:** +- `-y, --yes - Skip confirmation prompt` +- `-f, --force - Force the operation without confirmation` +- `-n, --dry-run - Show what would happen without making changes` + +### `sentry token list ` + +List org auth tokens + +**Examples:** + +```bash +# List org auth tokens +sentry token list my-org + +# Create a new token +sentry token create my-org --name 'CI deploy token' + +# Delete a token by ID +sentry token delete my-org 12345 --yes + +# Delete a token (dry run) +sentry token delete my-org 12345 --dry-run + +# Output as JSON +sentry token list --json +``` + +All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. From 42163e8683bc16d850ab08999a2ed2cd7c7148b1 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" <286517962+jared-outpost[bot]@users.noreply.github.com> Date: Sat, 20 Jun 2026 06:39:31 +0000 Subject: [PATCH 4/5] fix: add token commands to ORG_ONLY_COMMANDS for shell completions The completions property test verifies that every command with an org-related positional parameter is registered in either ORG_PROJECT_COMMANDS or ORG_ONLY_COMMANDS. The new token create/ delete/list commands accept an org slug but were missing from ORG_ONLY_COMMANDS, causing the test at line 469 to fail. --- src/lib/complete.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/complete.ts b/src/lib/complete.ts index 7da59f006..50aa8d86a 100644 --- a/src/lib/complete.ts +++ b/src/lib/complete.ts @@ -139,6 +139,9 @@ export const ORG_ONLY_COMMANDS = new Set([ "team list", "repo list", "monitor list", + "token create", + "token delete", + "token list", "trial list", "trial start", ]); From 07d740c2c9ccf6d941ffc817c3d2ad04049142f6 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" <286517962+jared-outpost[bot]@users.noreply.github.com> Date: Sat, 20 Jun 2026 06:47:52 +0000 Subject: [PATCH 5/5] fix: add token to groupsWithDefaultCommand in completions test The token route map has defaultCommand: "list", so the completions property test needs to know it returns flags (not subcommand names) when completing 'sentry token '. Without this, the test expects subcommand names but gets flags, causing two assertion failures. --- test/lib/completions.property.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/lib/completions.property.test.ts b/test/lib/completions.property.test.ts index 1e34785c8..671512d4f 100644 --- a/test/lib/completions.property.test.ts +++ b/test/lib/completions.property.test.ts @@ -196,6 +196,7 @@ describe("proposeCompletions: Stricli integration", () => { "log", "local", "monitor", + "token", ]); test("subcommands match extractCommandTree for each group without defaultCommand", async () => {