diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2501035f..449218b6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,8 +43,11 @@ jobs: - 'src/**' - 'docs/**' - 'package.json' + - 'README.md' + - 'DEVELOPMENT.md' - 'script/generate-skill.ts' - 'script/generate-command-docs.ts' + - 'script/generate-docs-sections.ts' - 'script/eval-skill.ts' - 'test/skill-eval/**' code: @@ -130,17 +133,26 @@ jobs: echo "stale=true" >> "$GITHUB_OUTPUT" echo "Skill files are out of date" fi - - name: Auto-commit regenerated skill files - if: steps.check-skill.outputs.stale == 'true' && steps.token.outcome == 'success' + - name: Check docs sections + id: check-sections + run: | + if git diff --quiet DEVELOPMENT.md docs/src/content/docs/contributing.md docs/src/content/docs/self-hosted.md; then + echo "Docs sections are up to date" + else + echo "stale=true" >> "$GITHUB_OUTPUT" + echo "Docs sections are out of date" + fi + - name: Auto-commit regenerated files + if: (steps.check-skill.outputs.stale == 'true' || steps.check-sections.outputs.stale == 'true') && steps.token.outcome == 'success' run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add plugins/sentry-cli/skills/sentry-cli/ - git diff --cached --quiet || (git commit -m "chore: regenerate skill files" && git push) - - name: Fail for fork PRs with stale skill files - if: steps.check-skill.outputs.stale == 'true' && steps.token.outcome != 'success' + git add plugins/sentry-cli/skills/sentry-cli/ DEVELOPMENT.md docs/src/content/docs/contributing.md docs/src/content/docs/self-hosted.md + git diff --cached --quiet || (git commit -m "chore: regenerate docs" && git push) + - name: Fail for fork PRs with stale generated files + if: (steps.check-skill.outputs.stale == 'true' || steps.check-sections.outputs.stale == 'true') && steps.token.outcome != 'success' run: | - echo "::error::Skill files are out of date. Run 'bun run generate:docs' locally and commit the result." + echo "::error::Generated files are out of date. Run 'bun run generate:docs' locally and commit the result." exit 1 lint: diff --git a/.gitignore b/.gitignore index b29ae9dad..8c2df2fb1 100644 --- a/.gitignore +++ b/.gitignore @@ -58,8 +58,9 @@ src/generated/ src/sdk.generated.ts src/sdk.generated.d.cts -# Generated command docs (rebuilt from fragments + CLI introspection) +# Generated docs pages (rebuilt from fragments + CLI introspection / env registry) docs/src/content/docs/commands/ +docs/src/content/docs/configuration.md # Generated discovery manifest (rebuilt by generate:skill, served via symlinked skill files) docs/public/.well-known/skills/index.json diff --git a/AGENTS.md b/AGENTS.md index 284cb0211..141c3abc3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -550,18 +550,6 @@ const { sql, values } = upsert( ); ``` -### UX Philosophy: Intent-First Correction - -When the user's intent is unambiguous, **do what they meant** instead of rejecting with an error. Show a `log.warn()` notice explaining what was corrected and how to do it properly next time. Only reject when intent is ambiguous or the input is genuinely dangerous (e.g., path traversal). - -This applies across the CLI: -- **Input cleanup** — strip copy-paste artifacts (line breaks, indentation) and warn, rather than throwing `ValidationError` -- **Entity type recovery** — resolve wrong-type identifiers to the correct type (see "Auto-Recovery" below) -- **Argument order swapping** — fix swapped positional args with a stderr warning -- **Slug/org matching** — redirect bare slugs to the most likely intent - -When recovery is **ambiguous or impossible**, keep the error but add entity-aware suggestions and fuzzy matches. - ### Error Handling All CLI errors extend the `CliError` base class from `src/lib/errors.ts`: @@ -996,90 +984,95 @@ mock.module("./some-module", () => ({ ### Architecture - -* **Auth token env var override pattern: SENTRY\_AUTH\_TOKEN > SENTRY\_TOKEN > SQLite**: Auth in \`src/lib/db/auth.ts\` follows layered precedence: \`SENTRY\_AUTH\_TOKEN\` > \`SENTRY\_TOKEN\` > SQLite OAuth token. \`getEnvToken()\` trims env vars (empty/whitespace = unset). \`AuthSource\` tracks provenance. \`ENV\_SOURCE\_PREFIX = "env:"\` — use \`.length\` not hardcoded 4. Env tokens bypass refresh/expiry. \`isEnvTokenActive()\` guards auth commands. Logout must NOT clear stored auth when env token active. These functions stay in \`db/auth.ts\` despite not touching DB because they're tightly coupled with token retrieval. - - -* **Consola chosen as CLI logger with Sentry createConsolaReporter integration**: Consola is the CLI logger with Sentry \`createConsolaReporter\` integration. Two reporters: FancyReporter (stderr) + Sentry structured logs. Level via \`SENTRY\_LOG\_LEVEL\`. \`buildCommand\` injects hidden \`--log-level\`/\`--verbose\` flags. \`withTag()\` creates independent instances; \`setLogLevel()\` propagates via registry. All user-facing output must use consola, not raw stderr. \`HandlerContext\` intentionally omits stderr. + +* **API client wraps all errors as CliError subclasses — no raw exceptions escape**: The API client (src/lib/api-client.ts) wraps ALL errors as CliError subclasses (ApiError or AuthError) — no raw exceptions escape. Commands don't need try-catch for error display; the central handler in app.ts formats CliError cleanly. Only add try-catch when a command needs to handle errors specially (e.g., login continuing despite user-info fetch failure). - -* **defaults table is write-orphaned — setDefaults() has no production caller**: The \`defaults\` single-row table was dropped (schema v13) and consolidated into the \`metadata\` KV table. Keys: \`defaults.org\`, \`defaults.project\`, \`defaults.telemetry\` ("on"/"off"), \`defaults.url\`. Individual getters/setters in \`src/lib/db/defaults.ts\`: \`getDefaultOrganization()\`, \`setDefaultOrganization(value | null)\`, etc. \`getAllDefaults()\` returns full state, \`clearAllDefaults()\` deletes all keys. \`getTelemetryPreference()\` returns \`boolean | undefined\`. Migration 13 copies any existing defaults-table data to metadata then drops the table. JSON migration (\`migration.ts\`) writes directly to metadata. \`metadata.key\` is \`PRIMARY KEY\` — O(log n) lookups, no extra index needed. + +* **Completion fast-path skips Sentry SDK via SENTRY\_CLI\_NO\_TELEMETRY and SQLite telemetry queue**: Shell completions (\`\_\_complete\`) set \`SENTRY\_CLI\_NO\_TELEMETRY=1\` in \`bin.ts\` before any imports, skipping \`createTracedDatabase\` wrapper and avoiding \`@sentry/node-core/light\` load (~85ms). Completion timing queued to \`completion\_telemetry\_queue\` SQLite table (~1ms). Normal runs drain via \`DELETE FROM ... RETURNING\` and emit as \`Sentry.metrics.distribution\`. Fast-path achieves ~60ms dev / ~140ms CI, 200ms e2e budget. - -* **Input validation layer: src/lib/input-validation.ts guards CLI arg parsing**: Four validators in \`src/lib/input-validation.ts\` guard against agent-hallucinated inputs: \`rejectControlChars\` (ASCII < 0x20), \`rejectPreEncoded\` (%XX), \`validateResourceId\` (rejects ?, #, %, whitespace), \`validateEndpoint\` (rejects \`..\` traversal). Applied in \`parseSlashOrgProject\`, bare-slug path in \`parseOrgProjectArg\`, \`parseIssueArg\`, and \`normalizeEndpoint\` (api.ts). NOT applied in \`parseSlashSeparatedArg\` for no-slash plain IDs — those may contain structural separators (newlines for log view batch IDs) that callers split downstream. Validation targets user-facing parse boundaries only; env vars and DB cache values are trusted. + +* **Craft artifact discovery uses commit SHA and workflow name, not tags**: Craft artifact discovery and release pipeline: Craft (\`getsentry/craft@v2\`) finds artifacts by commit SHA + workflow \*\*name\*\* (not tags). Jobs in \`ci.yml\` (named \`Build\`) auto-discovered; artifacts matching \`/^sentry-.\*$/\` picked up without \`.craft.yml\` changes. GitHub Releases are immutable post-publish — all assets (including delta patches) must be workflow artifacts before craft runs. Delta patches use TRDIFF10 (zig-bsdiff + zstd). \`generate-patches\` job runs on \`main\`/\`release/\*\*\` with \`continue-on-error\`. Nightly: old binary from GHCR via ORAS. Stable: \`gh release download\`. Binary download steps use \`sentry-\*-\*\` pattern to avoid matching \`sentry-patches\`. Bash glob loops need \`shopt -s nullglob\`. - -* **Magic @ selectors resolve issues dynamically via sort-based list API queries**: Magic \`@\` selectors (\`@latest\`, \`@most\_frequent\`) in \`parseIssueArg\` are detected early (before \`validateResourceId\`) because \`@\` is not in the forbidden charset. \`SELECTOR\_MAP\` provides case-insensitive matching with common variations (\`@mostfrequent\`, \`@most-frequent\`). Resolution in \`resolveSelector\` (issue/utils.ts) maps selectors to \`IssueSort\` values (\`date\`, \`freq\`), calls \`listIssuesPaginated\` with \`perPage: 1\` and \`query: 'is:unresolved'\`. Supports org-prefixed form: \`sentry/@latest\`. Unrecognized \`@\`-prefixed strings fall through to suffix-only parsing (not an error). The \`ParsedIssueArg\` union includes \`{ type: 'selector'; selector: IssueSelector; org?: string }\`. + +* **Sentry API: events require org+project, issues have legacy global endpoint**: Sentry API scoping: Events require org+project in URL path (\`/projects/{org}/{project}/events/{id}/\`). Issues use legacy global endpoint (\`/api/0/issues/{id}/\`) without org context. Traces need only org (\`/organizations/{org}/trace/{traceId}/\`). Two-step lookup for events: fetch issue → extract org/project from response → fetch event. Cross-project event search possible via Discover endpoint \`/organizations/{org}/events/\` with \`query=id:{eventId}\`. - -* **Sentry SDK uses @sentry/node-core/light instead of @sentry/bun to avoid OTel overhead**: The CLI uses \`@sentry/node-core/light\` instead of \`@sentry/bun\` to avoid loading the full OpenTelemetry stack (~150ms, 24MB). \`@sentry/core\` barrel is patched via \`bun patch\` to remove ~32 unused exports saving ~13ms. Key gotcha: \`LightNodeClient\` constructor hardcodes \`runtime: { name: 'node' }\` AFTER spreading user options, so passing \`runtime\` in \`Sentry.init()\` is silently overwritten. Fix: patch \`client.getOptions().runtime\` post-init (returns mutable ref). The CLI does this in \`telemetry.ts\` to report \`bun\` runtime when running as binary. Trade-offs: transport falls back to Node's \`http\` module instead of native \`fetch\`. Upstream issues: getsentry/sentry-javascript#19885 and #19886. + +* **Sentry CLI authenticated fetch architecture with response caching**: \`createAuthenticatedFetch()\` wraps fetch with auth headers, 30s timeout, retry (max 2), 401 token refresh, and span tracing. Response caching via \`http-cache-semantics\` (RFC 7234) stored at \`~/.sentry/cache/responses/\`. Fallback TTL tiers: immutable (24hr), stable (5min), volatile (60s), no-cache (0). Only GET 2xx cached. \`--fresh\` and \`SENTRY\_NO\_CACHE=1\` bypass cache. Cache cleared on login/logout. - -* **Telemetry opt-out is env-var-only — no persistent preference or DO\_NOT\_TRACK**: Telemetry opt-out now uses a 4-level priority chain via \`isTelemetryEnabled()\` in \`telemetry.ts\`: (1) \`SENTRY\_CLI\_NO\_TELEMETRY=1\` → disabled, (2) \`DO\_NOT\_TRACK=1\` → disabled (consoledonottrack.com), (3) \`metadata\` key \`defaults.telemetry\` → "on"/"off" persistent preference set via \`sentry cli defaults telemetry on/off\`, (4) default → enabled. The DB read is wrapped in try/catch since \`withTelemetry\` may run before DB init. \`getTelemetryPreference()\` from \`db/defaults.ts\` returns \`boolean | undefined\`. The env var is still force-set to \`"1"\` in completion fast-path, DB tracing gate, feedback guard, and test preloads. + +* **Sentry CLI has two distribution channels with different runtimes**: Sentry CLI ships two ways: (1) Standalone binary via \`Bun.build()\` with \`compile: true\`. (2) npm package via esbuild producing CJS \`dist/bin.cjs\` for Node 22+, with Bun API polyfills from \`script/node-polyfills.ts\`. \`Bun.$\` has NO polyfill — use \`execSync\` instead. SDK is \`@sentry/node-core/light\` (not \`@sentry/bun\`). - -* **Zod schema on OutputConfig enables self-documenting JSON fields in help and SKILL.md**: List commands register a \`schema?: ZodType\` on \`OutputConfig\\` (in \`src/lib/formatters/output.ts\`). \`extractSchemaFields()\` walks Zod object shapes to produce \`SchemaFieldInfo\[]\` (name, type, description, optional). In \`command.ts\`, \`buildFieldsFlag()\` enriches the \`--fields\` flag brief with available names; \`enrichDocsWithSchema()\` appends a fields section to \`fullDescription\`. The schema is exposed as \`\_\_jsonSchema\` on the built command for introspection — \`introspect.ts\` reads it into \`CommandInfo.jsonFields\`. \`help.ts\` renders fields in \`sentry help \\` output. \`generate-skill.ts\` renders a markdown table in reference docs. For \`buildOrgListCommand\`/\`dispatchOrgScopedList\`, pass \`schema\` via \`OrgListConfig\` — \`list-command.ts\` forwards it to \`OutputConfig\`. + +* **Sentry CLI resolve-target cascade has 5 priority levels with env var support**: Resolve-target cascade (src/lib/resolve-target.ts) has 5 priority levels: (1) Explicit CLI flags, (2) SENTRY\_ORG/SENTRY\_PROJECT env vars, (3) SQLite config defaults, (4) DSN auto-detection, (5) Directory name inference. SENTRY\_PROJECT supports combo notation \`org/project\` — when used, SENTRY\_ORG is ignored. If combo parse fails (e.g. \`org/\`), the entire value is discarded. The \`resolveFromEnvVars()\` helper is injected into all four resolution functions. ### Decision - -* **All view subcommands should use \ \ positional pattern**: All \`\* view\` subcommands follow a consistent \`\ \\` positional pattern where target is the optional \`org/project\` specifier. During migration, use opportunistic argument swapping with a stderr warning when args are in wrong order. This is an instance of the broader "Intent-First Correction" UX philosophy: when user intent is unambiguous, do what they meant and show a \`log.warn()\` notice — don't reject with an error. Safe when input is already invalid, correction is unambiguous, warning goes to stderr. Normalize at command level, keep parsers pure. Model after \`gh\` CLI conventions. The \`auth\` route uses \`defaultCommand: "status"\` (not \`view\`) since it has no viewable entity — \`sentry auth\` shows auth status directly. + +* **Issue list global limit with fair per-project distribution and representation guarantees**: \`issue list --limit\` is a global total across all detected projects. \`fetchWithBudget\` Phase 1 divides evenly, Phase 2 redistributes surplus via cursor resume. \`trimWithProjectGuarantee\` ensures at least 1 issue per project before filling remaining slots. JSON output wraps in \`{ data, hasMore }\` with optional \`errors\` array. Compound cursor (pipe-separated) enables \`-c last\` for multi-target pagination, keyed by sorted target fingerprint. - -* **Sentry-derived terminal color palette tuned for dual-background contrast**: The CLI's chart/dashboard palette uses 10 colors derived from Sentry's categorical chart hues (\`static/app/utils/theme/scraps/tokens/color.tsx\` in getsentry/sentry), each adjusted to mid-luminance to achieve ≥3:1 contrast on both dark (#1e1e1e) and light (#f0f0f0) backgrounds. Key adjustments: orange darkened from #FF9838→#C06F20, green #67C800→#3D8F09, yellow #FFD00E→#9E8B18, purple lightened #5D3EB2→#8B6AC8, indigo #50219C→#7B50D0. Blurple (#7553FF), pink (#F0369A), magenta (#B82D90) used as-is. Teal (#228A83) added to fill a hue gap. ANSI 16-color codes were considered but rejected in favor of hex since the mid-luminance hex values provide guaranteed contrast regardless of terminal theme configuration. + +* **Sentry CLI config dir should stay at ~/.sentry/, not move to XDG**: Config dir stays at \`~/.sentry/\` (not XDG). The readonly DB errors on macOS are from \`sudo brew install\` creating root-owned files. Fixes: (1) bestEffort() makes setup steps non-fatal, (2) tryRepairReadonly() detects root-owned files and prints \`sudo chown\` instructions, (3) \`sentry cli fix\` handles ownership repair. Ownership must be checked BEFORE permissions — root-owned files cause chmod to EPERM. ### Gotcha - -* **Biome lint bans process.stdout in commands — use isPlainOutput() and yield tokens instead**: A custom lint rule prevents \`process.stdout\` usage in command files — all output must go through \`yield CommandOutput\` or the Stricli context's \`this.stdout\`. For TTY detection, use \`isPlainOutput()\` from \`src/lib/formatters/plain-detect.ts\` instead of \`process.stdout.isTTY\`. For ANSI control sequences (screen clear, cursor movement), yield a \`ClearScreen\` token and let the \`buildCommand\` wrapper handle it. This keeps commands decoupled from stdout details. + +* **Biome noExcessiveCognitiveComplexity max 15 requires extracting helpers from command handlers**: Biome lint rules that frequently trip developers in this codebase: (1) \`noExcessiveCognitiveComplexity\` max 15 — extract helpers from Stricli \`func()\` handlers to stay under limit; improves testability too. (2) \`useBlockStatements\` — always use braces, no braceless \`if/return/break\`. Nested ternaries banned (\`noNestedTernary\`) — use if/else or sequential assignments. (3) \`useAtIndex\` — use \`arr.at(-1)\` not \`arr\[arr.length - 1]\`. (4) \`noStaticOnlyClass\` — use branded instances instead of static-only classes. - -* **Dashboard tracemetrics dataset uses comma-separated aggregate format**: SDK v10+ custom metrics (, , ) emit envelope items. Dashboard widgets for these MUST use with aggregate format — e.g., . The parameter must match the SDK emission exactly: if no unit specified, for memory metrics, for uptime. only supports , , , , display types — no or . Widgets with always require . Sort expressions must reference aggregates present in . + +* **brew is not in VALID\_METHODS but Homebrew formula passes --method brew**: Homebrew install: \`isHomebrewInstall()\` detects via Cellar realpath (checked before stored install info). Upgrade command tells users \`brew upgrade getsentry/tools/sentry\`. Formula runs \`sentry cli setup --method brew --no-modify-path\` as post\_install. Version pinning throws 'unsupported\_operation'. Uses .gz artifacts. Tap at getsentry/tools. - -* **Dot-notation field filtering is ambiguous for keys containing dots**: The \`filterFields\` function in \`src/lib/formatters/json.ts\` uses dot-notation to address nested fields (e.g., \`metadata.value\`). This means object keys that literally contain dots are ambiguous and cannot be addressed. Property-based tests for this function must generate field name arbitraries that exclude dots — use a restricted charset like \`\[a-zA-Z0-9\_]\` in fast-check arbitraries. Counterexample found by fast-check: \`{"a":{".":false}}\` with path \`"a."\` splits into \`\["a", ""]\` and fails to resolve. + +* **Bun mock.module() leaks globally across test files in same process**: Bun's mock.module() replaces modules globally and leaks across test files in the same process. Solution: tests using mock.module() must run in a separate \`bun test\` invocation. In package.json, use \`bun run test:unit && bun run test:isolated\` instead of \`bun test\`. The \`test/isolated/\` directory exists for these tests. This was the root cause of ~100 test failures (getsentry/cli#258). - -* **Git worktree blocks branch checkout of branches used in other worktrees**: \`git checkout main\` fails with "already used by worktree at ..." when another worktree has that branch checked out. In this repo's worktree setup, use \`git checkout origin/main --detach\` or create feature branches from \`origin/main\` directly: \`git checkout -b fix/foo origin/main\`. This is a standard git worktree constraint but catches people off guard in the CLI repo which uses worktrees for parallel development. + +* **CI paths-filter for skill regeneration misses package.json version changes**: The \`check-generated\` job in \`.github/workflows/ci.yml\` gates on \`dorny/paths-filter\` with a \`skill\` filter matching \`src/\*\*\`, \`docs/\*\*\`, and generator scripts. But \`generate-skill.ts\` reads the version from \`package.json\` and embeds it in skill file frontmatter. Version bump commits (from Craft) only touch \`package.json\` and \`plugin.json\` — neither matches the filter, so skill regeneration is skipped and files go stale. Fix: add \`package.json\` to the \`skill\` paths-filter. The \`release/\*\*\` branch fallback doesn't help because the version bump lands on \`main\`. GitHub App token (sentry-release-bot) correctly triggers CI — the skip is purely from paths-filter mismatch. - -* **Spinner stdout/stderr collision: log messages inside withProgress appear on spinner line**: The \`withProgress\` spinner in \`src/lib/polling.ts\` writes to stdout using \`\r\x1b\[K\` (no trailing newline). Consola logger writes to stderr. On a shared terminal, any \`log.info()\` called \*\*inside\*\* the \`withProgress\` callback appears on the same line as the spinner text because stderr doesn't know about stdout's carriage-return positioning. Fix pattern: propagate data out of the callback via return value, then call \`log.info()\` \*\*after\*\* \`withProgress\` completes (when the \`finally\` block has already cleared the spinner line). This affected \`downloadBinaryToTemp\` in \`upgrade.ts\` where \`log.info('Applied delta patch...')\` fired inside the spinner callback. + +* **Making clearAuth() async breaks model-based tests — use non-async Promise\ return instead**: Making \`clearAuth()\` \`async\` breaks fast-check model-based tests — real async yields during \`asyncModelRun\` cause \`createIsolatedDbContext\` cleanup to interleave. Fix: keep non-async, return \`clearResponseCache().catch(...)\` directly. Also: model-based tests need explicit timeouts (e.g., \`30\_000\`) — Bun's default 5s causes false failures during shrinking. - -* **Stricli FLAG\_NAME\_PATTERN requires 2+ chars after double-dash**: Stricli's \`FLAG\_NAME\_PATTERN\` is \`/^--(\[a-z]\[a-z-.\d\_]+)$/i\` — the \`+\` quantifier requires at least 2 characters after \`--\`. Single-char flags like \`--x\` or \`--y\` silently fail to match and are consumed as positional arguments, causing confusing parse errors. Always use flag names with 2+ characters (e.g., \`--col\`, \`--row\`). Single-char shortcuts must use the alias system (\`-x\` maps to a longer flag name). This bit the dashboard widget \`--x\`/\`--y\` flags which were DOA from introduction. + +* **Multiregion mock must include all control silo API routes**: When changing which Sentry API endpoint a function uses, mock routes must be updated in BOTH \`test/mocks/routes.ts\` (single-region) AND \`test/mocks/multiregion.ts\` \`createControlSiloRoutes()\`. Missing the multiregion mock causes 404s in multi-region test scenarios. - -* **Stricli rejects unknown flags — pre-parsed global flags must be consumed from argv**: Stricli's arg parser is strict: any \`--flag\` not registered on a command throws \`No flag registered for --flag\`. Global flags (parsed before Stricli in bin.ts) MUST be spliced out of argv. \`--log-level\` was correctly consumed but \`--verbose\` was intentionally left in (for the \`api\` command's own \`--verbose\`). This breaks every other command. Also, \`argv.indexOf('--flag')\` doesn't match \`--flag=value\` form — must check both space-separated and equals-sign forms when pre-parsing. A Biome \`noRestrictedImports\` lint rule in \`biome.jsonc\` now blocks \`import { buildCommand } from "@stricli/core"\` at error level — only \`src/lib/command.ts\` is exempted. Other \`@stricli/core\` exports (\`buildRouteMap\`, \`run\`, etc.) are allowed. + +* **Sentry /users/me/ endpoint returns 403 for OAuth tokens — use /auth/ instead**: The Sentry \`/users/me/\` endpoint returns 403 for OAuth tokens. Use \`/auth/\` instead — it works with ALL token types and lives on the control silo. In the CLI, \`getControlSiloUrl()\` handles routing correctly. \`SentryUserSchema\` (with \`.passthrough()\`) handles the \`/auth/\` response since it only requires \`id\`. -### Pattern + +* **Sentry chunk upload API returns camelCase not snake\_case**: The Sentry chunk upload options endpoint (\`/api/0/organizations/{org}/chunk-upload/\`) returns camelCase keys (\`chunkSize\`, \`chunksPerRequest\`, \`maxRequestSize\`, \`hashAlgorithm\`), NOT snake\_case. Zod schemas for these responses should use camelCase field names. This is an exception to the typical Sentry API convention of snake\_case. Verified by direct API query. The \`AssembleResponse\` also uses camelCase (\`missingChunks\`). + + +* **Source Map v3 spec allows null entries in sources array**: The Source Map v3 spec allows \`null\` entries in the \`sources\` array, and bundlers like esbuild actually produce them. Any code iterating over \`sources\` and calling string methods (e.g., \`.replaceAll()\`) must guard against null: \`map.sources.map((s) => typeof s === "string" ? s.replaceAll("\\\\", "/") : s)\`. Without the guard, \`null.replaceAll()\` throws \`TypeError\`. This applies to \`src/lib/sourcemap/debug-id.ts\` and any future sourcemap manipulation code. + + +* **Stricli command context uses this.stdout not this.process.stdout**: In Stricli command \`func()\` handlers, use \`this.stdout\` and \`this.stderr\` directly — NOT \`this.process.stdout\`. The \`SentryContext\` interface has both \`process\` and \`stdout\`/\`stderr\` as separate top-level properties. Test mock contexts typically provide \`stdout\` but not a full \`process\` object, so \`this.process.stdout\` causes \`TypeError: undefined is not an object\` at runtime in tests even though TypeScript doesn't flag it. - -* **Auth route defaultCommand is status, not view**: The \`auth\` route in \`src/commands/auth/index.ts\` uses \`defaultCommand: "status"\` — the only route group with a non-\`view\` default. All other entity routes (issue, event, org, project, dashboard, trace, span, log) default to \`view\`. The auth route has no viewable entity, so \`sentry auth\` shows auth status directly. This was added to handle the common \`sentry info\` intent (users wanting account info). Routes without any default: \`cli\`, \`sourcemap\`, \`repo\`, \`team\`, \`trial\`, \`release\`, \`dashboard/widget\`. + +* **Stricli defaultCommand blends default command flags into route completions**: When a Stricli route map has \`defaultCommand\` set, requesting completions for that route (e.g. \`\["issues", ""]\`) returns both the subcommand names AND the default command's flags/positional completions. This means completion tests that compare against \`extractCommandTree()\` subcommand lists will fail for groups with defaultCommand, since the actual completions include extra entries like \`--limit\`, \`--query\`, etc. Solution: track \`hasDefaultCommand\` in the command tree and skip strict subcommand-matching assertions for those groups. - -* **ClearScreen yield token for in-place terminal refresh in buildCommand wrapper**: Commands needing in-place refresh yield a \`ClearScreen\` token from \`src/lib/formatters/output.ts\`. The \`handleYieldedValue\` function in \`buildCommand\` sets a \`pendingClear\` flag; when the next \`CommandOutput\` is rendered, \`renderCommandOutput\` prepends \`\x1b\[H\x1b\[J\` and writes everything in a \*\*single \`stdout.write()\` call\*\* — no flicker. In JSON/plain modes the clear is silently ignored. Pattern: \`yield ClearScreen()\` then \`yield CommandOutput(data)\`. Critical: never split clear and content into separate writes. Also: never add a redundant clear-screen inside a \`HumanRenderer.render()\` method — the \`ClearScreen\` token is the sole mechanism. The dashboard renderer originally had its own \`\x1b\[2J\x1b\[H\` prepend on re-renders, causing double clears; this was removed. + +* **Upgrade command tests are flaky when run in full suite due to test ordering**: The \`upgradeCommand.func\` tests in \`test/commands/cli/upgrade.test.ts\` sometimes fail when run as part of the full \`bun run test:unit\` suite but pass consistently in isolation (\`bun test test/commands/cli/upgrade.test.ts\`). This is a test-ordering/state-leak issue, not a code bug. Don't chase these failures when your changes are unrelated to the upgrade command. + +### Pattern - -* **CLI init sequence: preloadProjectContext → sentryclirc shim → runCli → withTelemetry**: \`startCli()\` in \`cli.ts\`: (1) fast-path \`\_\_complete\` dispatch, (2) \`preloadProjectContext(cwd)\` — walks up from CWD finding project root AND \`.sentryclirc\`, then calls \`applySentryCliRcEnvShim()\` to set env vars from config, (3) \`runCli(args)\` — dynamically imports all heavy modules, calls \`withTelemetry()\`. Persistent defaults should be applied between steps 2 and 3: after \`.sentryclirc\` shim (so project-local config takes precedence) but before telemetry reads the preference. \`preloadProjectContext\` is wrapped in try/catch — failures are non-fatal. Both functions use \`await import()\` for heavy modules to keep the completion fast-path lightweight. + +* **Bun global installs use .bun path segment for detection**: Bun global package installs place scripts under \`~/.bun/install/global/node\_modules/\`. The \`.bun\` directory in the path distinguishes bun from npm/yarn installs. In \`detectPackageManagerFromPath()\`, check \`segments.includes('.bun')\` before the npm fallback. Detection order: \`.pnpm\` → pnpm, \`.bun\` → bun, other \`node\_modules\` → npm. Yarn classic uses same flat layout as npm so falls through to npm detection — this is acceptable because path-based detection is a \*\*fallback\*\* after subprocess calls (which correctly identify yarn). Path detection must NOT be authoritative/override stored DB info, only serve as fallback when subprocess detection fails (e.g., Windows where .cmd files cause ENOENT). - -* **parseBoolValue utility for CLI boolean flag parsing**: \`src/lib/parse-bool.ts\` exports \`parseBoolValue(input: string): boolean | null\` — case-insensitive, whitespace-trimmed parser. True: on/yes/true/1/t/y. False: off/no/false/0/f/n. Unrecognized: null. Used by \`sentry cli defaults telemetry\` for accepting human-friendly boolean values. Adapted from Sentry JS SDK's \`envToBool\` pattern. Property-tested in \`test/lib/parse-bool.property.test.ts\`. + +* **Non-essential DB cache writes should be guarded with try-catch**: Non-essential DB cache writes (e.g., \`setUserInfo()\`, \`setInstallInfo()\`) must be wrapped in try-catch so a broken/read-only DB doesn't crash a command whose primary operation succeeded. Pattern: \`try { setInstallInfo(...) } catch { log.debug(...) }\`. In login.ts, \`getCurrentUser()\` failure after token save must not block auth — log warning, continue. In upgrade.ts, \`setInstallInfo\` after legacy detection is guarded same way. Exception: \`getUserRegions()\` failure should \`clearAuth()\` and fail hard (indicates invalid token). This is enforced by BugBot reviews — any \`setInstallInfo\`/\`setUserInfo\` call outside setup.ts's \`bestEffort()\` wrapper needs its own try-catch. - -* **Property-based tests for input validators use stringMatching for forbidden char coverage**: In \`test/lib/input-validation.property.test.ts\`, forbidden-character arbitraries are built with \`stringMatching\` targeting specific regex patterns (e.g., \`/^\[^\x00-\x1f]\*\[\x00-\x1f]\[^\x00-\x1f]\*$/\` for control chars). This ensures fast-check generates strings that always contain the forbidden character while varying surrounding content. The \`biome-ignore lint/suspicious/noControlCharactersInRegex\` suppression is needed on the control char regex constant in \`input-validation.ts\`. + +* **Sentry CLI command docs are auto-generated from Stricli route tree with CI freshness check**: Sentry CLI command docs are auto-generated from Stricli route tree: Docs in \`docs/src/content/docs/commands/\*.md\` and skill files in \`plugins/sentry-cli/skills/sentry-cli/references/\*.md\` are generated via \`bun run generate:docs\`. Content between \`\\` markers is regenerated; hand-written examples go in \`docs/src/fragments/commands/\`. CI checks \`check:command-docs\` and \`check:skill\` fail if stale. Run generators after changing command parameters/flags/docs. - -* **sentry cli defaults command uses variadic positional args for key-value dispatch**: \`src/commands/cli/defaults.ts\` uses Stricli's \`kind: 'array'\` positional parameter to receive \`\[key, value?]\` args. Dispatch: no args → show all; 1 arg → show specific key; 2 args → set; \`--clear\` without args → clear all (with \`guardNonInteractive\` + confirmation); \`--clear\` with key → clear specific. Canonical key aliases: \`org\`/\`organization\` → \`org\`, \`project\` → \`project\`, \`telemetry\` → \`telemetry\`, \`url\` → \`url\`. \`auth: false\` since defaults don't require authentication. \`computeTelemetryEffective()\` returns the resolved source for display. + +* **Stricli buildCommand output config injects json flag into func params**: When a Stricli command uses \`output: { json: true, human: formatFn }\`, \`--json\` and \`--fields\` flags are auto-injected. The \`func\` handler receives these in its first parameter — type explicitly (e.g., \`flags: { json?: boolean }\`) to access them. Commands with interactive side effects (browser prompts, QR codes) should check \`flags.json\` and skip them when true. - -* **Sentry's official color token source location in getsentry/sentry repo**: Sentry's canonical color palette lives in \`static/app/utils/theme/scraps/tokens/color.tsx\` in getsentry/sentry. It defines \`categorical.light\` and \`categorical.dark\` palettes with named colors (blurple, purple, indigo, plum, magenta, pink, salmon, orange, yellow, lime, green). Chart palettes are built in \`static/app/utils/theme/theme.tsx\` using \`CHART\_PALETTE\_LIGHT\` and \`CHART\_PALETTE\_DARK\` arrays that progressively add colors as series count grows (1→blurple, 6→blurple/indigo/pink/orange/yellow/green, etc.). GitHub API tree endpoint (\`/git/trees/master?recursive=1\`) can locate files without needing authenticated code search. + +* **ZipWriter and file handle cleanup: always use try/finally with close()**: ZipWriter in \`src/lib/sourcemap/zip.ts\` has \`close()\` for error cleanup; \`finalize()\` uses try/finally for \`fh.close()\`. Callers must wrap usage in try/catch and call \`zip.close()\` in catch. Pattern: create zip → try { add entries + finalize } catch { zip.close(); throw }. Prevents file handle leaks on partial failures. - -* **Shared flag constants in list-command.ts for cross-command consistency**: \`src/lib/list-command.ts\` exports shared Stricli flag definitions (\`FIELDS\_FLAG\`, \`FRESH\_FLAG\`, \`FRESH\_ALIASES\`) reused across all commands. When adding a new global-ish flag to multiple commands, define it once here as a const satisfying Stricli's flag shape, then spread into each command's \`flags\` object. The \`--fields\` flag is \`{ kind: 'parsed', parse: String, brief: '...', optional: true }\`. \`parseFieldsList()\` in \`formatters/json.ts\` handles comma-separated parsing with trim/dedup. \`writeJson()\` accepts an optional \`fields\` array and calls \`filterFields()\` before serialization. +### Preference - -* **SKILL.md generator must filter hidden Stricli flags**: \`script/generate-skill.ts\` introspects Stricli's route tree to auto-generate SKILL.md. \`FlagDef\` must include \`hidden?: boolean\`; \`extractFlags\` propagates it so \`generateCommandDoc\` filters out hidden flags alongside \`help\`/\`helpAll\`. Hidden flags from \`buildCommand\` (\`--log-level\`, \`--verbose\`) appear globally in \`docs/src/content/docs/commands/index.md\` Global Options section, pulled into SKILL.md via \`loadCommandsOverview\`. When \`cmd.jsonFields\` is present (from Zod schema registration), \`generateFullCommandDoc\` renders a markdown "JSON Fields" table with field name, type, and description columns in reference docs. + +* **Code style: Array.from() over spread for iterators, allowlist not whitelist**: User prefers \`Array.from(map.keys())\` over \`\[...map.keys()]\` for converting iterators to arrays (avoids intermediate spread). Use "allowlist" terminology instead of "whitelist" in comments and variable names. When a reviewer asks "Why not .filter() here?" — it may be a question, not a change request; the \`for..of\` loop may be intentionally more efficient. Confirm intent before changing. - -* **URL resolution chain and persistent default integration point**: \`getConfiguredSentryUrl()\` in \`constants.ts\` checks \`SENTRY\_HOST || SENTRY\_URL\` env vars. The \`.sentryclirc\` shim writes \`\[defaults] url\` to \`env.SENTRY\_URL\` via \`applySentryCliRcEnvShim()\`. A persistent URL default (\`metadata\` key \`defaults.url\`) is now applied in \`preloadProjectContext()\` after the \`.sentryclirc\` shim, writing to \`env.SENTRY\_URL\` only if both \`SENTRY\_HOST\` and \`SENTRY\_URL\` are unset. Full priority: SENTRY\_HOST > SENTRY\_URL > .sentryclirc > metadata defaults.url > sentry.io. The DB read is try/catch wrapped since DB may not be available during early init. + +* **PR workflow: address review comments, resolve threads, wait for CI**: PR workflow: (1) Wait for CI, (2) Check unresolved review comments via \`gh api\`, (3) Fix in follow-up commits (not amends), (4) Reply explaining fix, (5) Resolve thread via \`gh api graphql\` \`resolveReviewThread\` mutation, (6) Push and re-check CI, (7) Final sweep. Use \`git notes add\` for implementation plans. Branch naming: \`fix/descriptive-slug\` or \`feat/descriptive-slug\`. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 9752df857..5a088f91c 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -59,11 +59,13 @@ When creating your Sentry OAuth application: - **Redirect URI**: Not required for device flow - **Scopes**: The CLI requests these scopes: + - `project:read`, `project:write`, `project:admin` - `org:read` - `event:read`, `event:write` - `member:read` - - `team:read` + - `team:read`, `team:write` + ## Environment Variables diff --git a/README.md b/README.md index b0fff6054..6a9c3c6c9 100644 --- a/README.md +++ b/README.md @@ -68,24 +68,7 @@ sentry issue plan PROJ-ABC ## Commands -| Command | Description | -|---------|-------------| -| `sentry auth` | Login, logout, check authentication status | -| `sentry org` | List and view organizations | -| `sentry project` | List, view, create, and delete projects | -| `sentry issue` | List, view, explain, and plan issues | -| `sentry event` | View event details | -| `sentry trace` | List and view distributed traces | -| `sentry span` | List and view spans | -| `sentry log` | List and view logs (with streaming) | -| `sentry dashboard` | List, view, and create dashboards with widgets | -| `sentry sourcemap` | Inject debug IDs and upload sourcemaps | -| `sentry init` | Initialize Sentry in your project | -| `sentry schema` | Browse the Sentry API schema | -| `sentry api` | Make direct API requests | -| `sentry cli` | Upgrade, setup, fix, and send feedback | - -For detailed documentation, visit [cli.sentry.dev](https://cli.sentry.dev). +Run `sentry --help` to see all available commands, or browse the [command reference](https://cli.sentry.dev/commands/). ## Configuration diff --git a/docs/src/content/docs/configuration.md b/docs/src/content/docs/configuration.md deleted file mode 100644 index 9cfe7807a..000000000 --- a/docs/src/content/docs/configuration.md +++ /dev/null @@ -1,252 +0,0 @@ ---- -title: Configuration -description: Environment variables, config files, and configuration options for the Sentry CLI ---- - -The Sentry CLI can be configured through config files, environment variables, and a local database. Most users don't need to set any of these — the CLI auto-detects your project from your codebase and stores credentials locally after `sentry auth login`. - -## Configuration File (`.sentryclirc`) - -The CLI supports a `.sentryclirc` config file using standard INI syntax. This is the same format used by the legacy `sentry-cli` tool, so existing config files are automatically picked up. - -### How It Works - -The CLI looks for `.sentryclirc` files by walking up from your current directory toward the filesystem root. If multiple files are found, values from the closest file take priority, with `~/.sentryclirc` serving as a global fallback. - -```ini -[defaults] -org = my-org -project = my-project - -[auth] -token = sntrys_... -``` - -### Supported Fields - -| Section | Key | Description | -|---------|-----|-------------| -| `[defaults]` | `org` | Default organization slug | -| `[defaults]` | `project` | Default project slug | -| `[defaults]` | `url` | Sentry base URL (for self-hosted) | -| `[auth]` | `token` | Auth token (mapped to `SENTRY_AUTH_TOKEN`) | - -### Monorepo Setup - -In monorepos, place a `.sentryclirc` at the repo root with your org, then add per-package configs with just the project: - -``` -my-monorepo/ - .sentryclirc # [defaults] org = my-company - packages/ - frontend/ - .sentryclirc # [defaults] project = frontend-web - backend/ - .sentryclirc # [defaults] project = backend-api -``` - -When you run a command from `packages/frontend/`, the CLI resolves `org = my-company` from the root and `project = frontend-web` from the closest file. - -### Resolution Priority - -When the CLI needs to determine your org and project, it checks these sources in order: - -1. **Explicit CLI arguments** — `sentry issue list my-org/my-project` -2. **Environment variables** — `SENTRY_ORG` / `SENTRY_PROJECT` -3. **`.sentryclirc` config file** — walked up from CWD, merged with `~/.sentryclirc` -4. **DSN auto-detection** — scans source code and `.env` files -5. **Directory name inference** — matches your directory name against project slugs - -The first source that provides both org and project wins. For org-only commands, only the org is needed. - -### Backward Compatibility - -If you previously used the legacy `sentry-cli` and have a `~/.sentryclirc` file, the new CLI reads it automatically. The `[defaults]` and `[auth]` sections are fully compatible. The `[auth] token` value is mapped to the `SENTRY_AUTH_TOKEN` environment variable internally (only if the env var is not already set). - -## Environment Variables - -### `SENTRY_AUTH_TOKEN` - -Authentication token for the Sentry API. This is the primary way to authenticate in CI/CD pipelines and scripts where interactive login is not possible. - -```bash -export SENTRY_AUTH_TOKEN=sntrys_YOUR_TOKEN_HERE -``` - -You can create auth tokens in your [Sentry account settings](https://sentry.io/settings/account/api/auth-tokens/). When set, this takes precedence over any stored OAuth token from `sentry auth login`. - -### `SENTRY_TOKEN` - -Legacy alias for `SENTRY_AUTH_TOKEN`. If both are set, `SENTRY_AUTH_TOKEN` takes precedence. - -### `SENTRY_HOST` - -Base URL of your Sentry instance. **Only needed for [self-hosted Sentry](./self-hosted/).** SaaS users (sentry.io) should not set this. - -```bash -export SENTRY_HOST=https://sentry.example.com -``` - -When set, all API requests (including OAuth login) are directed to this URL instead of `https://sentry.io`. The CLI also sets this automatically when you pass a self-hosted Sentry URL as a command argument. - -`SENTRY_HOST` takes precedence over `SENTRY_URL`. Both work identically — use whichever you prefer. - -### `SENTRY_URL` - -Alias for `SENTRY_HOST`. If both are set, `SENTRY_HOST` takes precedence. - -### `SENTRY_ORG` - -Default organization slug. Skips organization auto-detection. - -```bash -export SENTRY_ORG=my-org -``` - -### `SENTRY_PROJECT` - -Default project slug. Can also include the org in `org/project` format. - -```bash -# Project only (requires SENTRY_ORG or auto-detection for the org) -export SENTRY_PROJECT=my-project - -# Org and project together -export SENTRY_PROJECT=my-org/my-project -``` - -When using the `org/project` combo format, `SENTRY_ORG` is ignored. - -### `SENTRY_DSN` - -Sentry DSN for project auto-detection. This is the same DSN you use in `Sentry.init()`. The CLI resolves it to determine your organization and project. - -```bash -export SENTRY_DSN=https://key@o123.ingest.us.sentry.io/456 -``` - -The CLI also detects DSNs from `.env` files and source code automatically — see [DSN Auto-Detection](./features/#dsn-auto-detection). - -### `SENTRY_CLIENT_ID` - -Client ID of a public OAuth application on your Sentry instance. **Required for [self-hosted Sentry](./self-hosted/)** (26.1.0+) to use `sentry auth login` with the device flow. See the [Self-Hosted guide](./self-hosted/#1-create-a-public-oauth-application) for how to create one. - -```bash -export SENTRY_CLIENT_ID=your-oauth-client-id -``` - -### `SENTRY_CONFIG_DIR` - -Override the directory where the CLI stores its database (credentials, caches, defaults). Defaults to `~/.sentry/`. - -```bash -export SENTRY_CONFIG_DIR=/path/to/config -``` - -### `SENTRY_VERSION` - -Pin a specific version for the [install script](./getting-started/#install-script). Accepts a version number (e.g., `0.19.0`) or `nightly`. The `--version` flag takes precedence if both are set. - -```bash -SENTRY_VERSION=nightly curl https://cli.sentry.dev/install -fsS | bash -``` - -This is useful in CI/CD pipelines and Dockerfiles where you want reproducible installations without inline flags. - -### `SENTRY_PLAIN_OUTPUT` - -Force plain text output (no colors or ANSI formatting). Takes precedence over `NO_COLOR`. - -```bash -export SENTRY_PLAIN_OUTPUT=1 -``` - -### `NO_COLOR` - -Standard convention to disable color output. See [no-color.org](https://no-color.org/). Respected when `SENTRY_PLAIN_OUTPUT` is not set. - -```bash -export NO_COLOR=1 -``` - -### `SENTRY_CLI_NO_TELEMETRY` - -Disable CLI telemetry (error tracking for the CLI itself). The CLI sends anonymized error reports to help improve reliability — set this to opt out. - -```bash -export SENTRY_CLI_NO_TELEMETRY=1 -``` - -### `SENTRY_LOG_LEVEL` - -Controls the verbosity of diagnostic output. Defaults to `info`. - -Valid values: `error`, `warn`, `log`, `info`, `debug`, `trace` - -```bash -export SENTRY_LOG_LEVEL=debug -``` - -Equivalent to passing `--log-level debug` on the command line. CLI flags take precedence over the environment variable. - -### `SENTRY_CLI_NO_UPDATE_CHECK` - -Disable the automatic update check that runs periodically in the background. - -```bash -export SENTRY_CLI_NO_UPDATE_CHECK=1 -``` - -### `SENTRY_INSTALL_DIR` - -Override the directory where the CLI binary is installed. Used by the install script and `sentry cli upgrade` to control the binary location. - -```bash -export SENTRY_INSTALL_DIR=/usr/local/bin -``` - -### `SENTRY_NO_CACHE` - -Disable API response caching. When set, the CLI will not cache API responses and will always make fresh requests. - -```bash -export SENTRY_NO_CACHE=1 -``` - -## Global Options - -These flags are accepted by every command. They are not shown in individual command `--help` output, but are always available. - -### `--log-level ` - -Set the log verbosity level. Accepts: `error`, `warn`, `log`, `info`, `debug`, `trace`. - -```bash -sentry issue list --log-level debug -sentry --log-level=trace cli upgrade -``` - -Overrides `SENTRY_LOG_LEVEL` when both are set. - -### `--verbose` - -Shorthand for `--log-level debug`. Enables debug-level diagnostic output. - -```bash -sentry issue list --verbose -``` - -:::note -The `sentry api` command also uses `--verbose` to show full HTTP request/response details. When used with `sentry api`, it serves both purposes (debug logging + HTTP output). -::: - -## Credential Storage - -We store credentials and caches in a SQLite database (`cli.db`) inside the config directory (`~/.sentry/` by default, overridable via `SENTRY_CONFIG_DIR`). The database file and its WAL side-files are created with restricted permissions (mode 600) so that only the current user can read them. The database also caches: - -- Organization and project defaults -- DSN resolution results -- Region URL mappings -- Project aliases (for monorepo support) - -See [Credential Storage](./commands/auth/#credential-storage) in the auth command docs for more details. diff --git a/docs/src/content/docs/contributing.md b/docs/src/content/docs/contributing.md index 9bf97e350..a41ccc372 100644 --- a/docs/src/content/docs/contributing.md +++ b/docs/src/content/docs/contributing.md @@ -41,6 +41,7 @@ Edit `.env.local` with your development credentials. ## Project Structure + ``` cli/ ├── src/ @@ -48,17 +49,33 @@ cli/ │ ├── app.ts # Stricli application setup │ ├── context.ts # Dependency injection context │ ├── commands/ # CLI commands -│ │ ├── auth/ # Authentication commands -│ │ ├── org/ # Organization commands -│ │ ├── project/ # Project commands -│ │ ├── issue/ # Issue commands -│ │ └── event/ # Event commands +│ │ ├── auth/ # login, logout, refresh, status, token, whoami +│ │ ├── cli/ # defaults, feedback, fix, setup, upgrade +│ │ ├── dashboard/ # list, view, create, add, edit, delete +│ │ ├── event/ # view, list +│ │ ├── issue/ # list, events, explain, plan, view +│ │ ├── log/ # list, view +│ │ ├── org/ # list, view +│ │ ├── project/ # create, delete, list, view +│ │ ├── release/ # list, view, create, finalize, delete, deploy, deploys, set-commits, propose-version +│ │ ├── repo/ # list +│ │ ├── sourcemap/ # inject, upload +│ │ ├── span/ # list, view +│ │ ├── team/ # list +│ │ ├── trace/ # list, view, logs +│ │ ├── trial/ # list, start +│ │ ├── api.ts # Make an authenticated API request +│ │ ├── help.ts # Help command +│ │ ├── init.ts # Initialize Sentry in your project (experimental) +│ │ └── schema.ts # Browse the Sentry API schema │ ├── lib/ # Shared utilities │ └── types/ # TypeScript types and Zod schemas -├── test/ # Test files -├── script/ # Build scripts -└── docs/ # Documentation site +├── test/ # Test files (mirrors src/ structure) +├── script/ # Build and utility scripts +├── plugins/ # Agent skill files +└── docs/ # Documentation site (Astro + Starlight) ``` + ## Building diff --git a/docs/src/content/docs/self-hosted.md b/docs/src/content/docs/self-hosted.md index 00fc0e9fd..5f768338f 100644 --- a/docs/src/content/docs/self-hosted.md +++ b/docs/src/content/docs/self-hosted.md @@ -44,7 +44,10 @@ export SENTRY_CLIENT_ID=your-client-id If your instance is on an older version or you prefer not to create an OAuth application, you can use an API token instead: 1. Go to **Settings → Developer Settings → Personal Tokens** in your Sentry instance (or visit `https://sentry.example.com/settings/account/api/auth-tokens/new-token/`) -2. Create a new token with the following scopes: `project:read`, `project:write`, `project:admin`, `org:read`, `event:read`, `event:write`, `member:read`, `team:read` +2. Create a new token with the following scopes: + +`project:read`, `project:write`, `project:admin`, `org:read`, `event:read`, `event:write`, `member:read`, `team:read`, `team:write` + 3. Pass it to the CLI: ```bash diff --git a/docs/src/fragments/configuration.md b/docs/src/fragments/configuration.md new file mode 100644 index 000000000..f28662cce --- /dev/null +++ b/docs/src/fragments/configuration.md @@ -0,0 +1,113 @@ + + +## Configuration File (`.sentryclirc`) + +The CLI supports a `.sentryclirc` config file using standard INI syntax. This is the same format used by the legacy `sentry-cli` tool, so existing config files are automatically picked up. + +### How It Works + +The CLI looks for `.sentryclirc` files by walking up from your current directory toward the filesystem root. If multiple files are found, values from the closest file take priority, with `~/.sentryclirc` serving as a global fallback. + +```ini +[defaults] +org = my-org +project = my-project + +[auth] +token = sntrys_... +``` + +### Supported Fields + +| Section | Key | Description | +|---------|-----|-------------| +| `[defaults]` | `org` | Default organization slug | +| `[defaults]` | `project` | Default project slug | +| `[defaults]` | `url` | Sentry base URL (for self-hosted) | +| `[auth]` | `token` | Auth token (mapped to `SENTRY_AUTH_TOKEN`) | + +### Monorepo Setup + +In monorepos, place a `.sentryclirc` at the repo root with your org, then add per-package configs with just the project: + +``` +my-monorepo/ + .sentryclirc # [defaults] org = my-company + packages/ + frontend/ + .sentryclirc # [defaults] project = frontend-web + backend/ + .sentryclirc # [defaults] project = backend-api +``` + +When you run a command from `packages/frontend/`, the CLI resolves `org = my-company` from the root and `project = frontend-web` from the closest file. + +### Resolution Priority + +When the CLI needs to determine your org and project, it checks these sources in order: + +1. **Explicit CLI arguments** — `sentry issue list my-org/my-project` +2. **Environment variables** — `SENTRY_ORG` / `SENTRY_PROJECT` +3. **`.sentryclirc` config file** — walked up from CWD, merged with `~/.sentryclirc` +4. **Persistent defaults** — set via `sentry cli defaults` +5. **DSN auto-detection** — scans source code and `.env` files +6. **Directory name inference** — matches your directory name against project slugs + +The first source that provides both org and project wins. For org-only commands, only the org is needed. + +### Backward Compatibility + +If you previously used the legacy `sentry-cli` and have a `~/.sentryclirc` file, the new CLI reads it automatically. The `[defaults]` and `[auth]` sections are fully compatible. The `[auth] token` value is mapped to the `SENTRY_AUTH_TOKEN` environment variable internally (only if the env var is not already set). + +## Persistent Defaults + +Use `sentry cli defaults` to set persistent defaults for organization, project, URL, and telemetry. These are stored in the CLI's local database and apply to all commands. + +```bash +sentry cli defaults org my-org # Set default organization +sentry cli defaults project my-project # Set default project +sentry cli defaults url https://... # Set Sentry URL (self-hosted) +sentry cli defaults telemetry off # Disable telemetry +sentry cli defaults # Show all current defaults +sentry cli defaults org --clear # Clear a specific default +``` + +See [`sentry cli defaults`](./commands/cli/#sentry-cli-defaults) for full usage. + +## Global Options + +These flags are accepted by every command. They are not shown in individual command `--help` output, but are always available. + +### `--log-level ` + +Set the log verbosity level. Accepts: `error`, `warn`, `log`, `info`, `debug`, `trace`. + +```bash +sentry issue list --log-level debug +sentry --log-level=trace cli upgrade +``` + +Overrides `SENTRY_LOG_LEVEL` when both are set. + +### `--verbose` + +Shorthand for `--log-level debug`. Enables debug-level diagnostic output. + +```bash +sentry issue list --verbose +``` + +:::note +The `sentry api` command also uses `--verbose` to show full HTTP request/response details. When used with `sentry api`, it serves both purposes (debug logging + HTTP output). +::: + +## Credential Storage + +We store credentials and caches in a SQLite database (`cli.db`) inside the config directory (`~/.sentry/` by default, overridable via `SENTRY_CONFIG_DIR`). The database file and its WAL side-files are created with restricted permissions (mode 600) so that only the current user can read them. The database also caches: + +- Organization and project defaults +- DSN resolution results +- Region URL mappings +- Project aliases (for monorepo support) + +See [Credential Storage](./commands/auth/#credential-storage) in the auth command docs for more details. diff --git a/package.json b/package.json index 11e2047a2..a83515e19 100644 --- a/package.json +++ b/package.json @@ -84,13 +84,15 @@ "test:init-eval": "bun test test/init-eval --timeout 600000 --concurrency 6", "generate:sdk": "bun run script/generate-sdk.ts", "generate:skill": "bun run script/generate-skill.ts", - "generate:docs": "bun run generate:command-docs && bun run generate:skill", + "generate:docs": "bun run generate:command-docs && bun run generate:skill && bun run generate:docs-sections", + "generate:docs-sections": "bun run script/generate-docs-sections.ts", "generate:schema": "bun run script/generate-api-schema.ts", "generate:command-docs": "bun run script/generate-command-docs.ts", "eval:skill": "bun run script/eval-skill.ts", "check:fragments": "bun run script/check-fragments.ts", "check:deps": "bun run script/check-no-deps.ts", - "check:errors": "bun run script/check-error-patterns.ts" + "check:errors": "bun run script/check-error-patterns.ts", + "check:docs-sections": "bun run script/generate-docs-sections.ts --check" }, "type": "module", "types": "./dist/index.d.cts" diff --git a/script/check-fragments.ts b/script/check-fragments.ts index 44fcf661b..afe36a9e3 100644 --- a/script/check-fragments.ts +++ b/script/check-fragments.ts @@ -1,12 +1,12 @@ #!/usr/bin/env bun /** - * Validate command doc fragment files. + * Validate doc fragment files. * - * Ensures fragment files in docs/src/fragments/commands/ stay consistent - * with the CLI route tree: + * Ensures fragment files stay consistent with the CLI route tree: * 1. Every route has a corresponding fragment file (+ index.md) * 2. Every fragment file corresponds to an existing route (or is index.md) * 3. Fragment files don't accidentally contain frontmatter or the generated marker + * 4. Top-level fragments (e.g., configuration.md) exist * * Usage: * bun run script/check-fragments.ts @@ -97,6 +97,28 @@ for (const file of fragmentFiles) { } } +// --------------------------------------------------------------------------- +// Check 4: Top-level fragments (non-command generated pages) +// --------------------------------------------------------------------------- + +const TOP_LEVEL_FRAGMENTS_DIR = "docs/src/fragments"; + +/** Top-level fragment files that must exist (for generated doc pages) */ +const REQUIRED_TOP_LEVEL_FRAGMENTS = ["configuration"]; + +for (const name of REQUIRED_TOP_LEVEL_FRAGMENTS) { + const path = `${TOP_LEVEL_FRAGMENTS_DIR}/${name}.md`; + if (!(await Bun.file(path).exists())) { + errors.push( + `Missing top-level fragment: ${path} (required for generated ${name}.md page)` + ); + } +} + +// --------------------------------------------------------------------------- +// Results +// --------------------------------------------------------------------------- + if (errors.length > 0) { console.error(`Found ${errors.length} fragment validation error(s):\n`); for (const err of errors) { @@ -106,5 +128,8 @@ if (errors.length > 0) { } console.log( - `All ${actualFragments.size} fragment files valid (${routeNames.size} routes + index)` + `All ${actualFragments.size} command fragment files valid (${routeNames.size} routes + index)` +); +console.log( + `All ${REQUIRED_TOP_LEVEL_FRAGMENTS.length} top-level fragment(s) valid` ); diff --git a/script/generate-command-docs.ts b/script/generate-command-docs.ts index 5568fcfc6..fedfbb73c 100644 --- a/script/generate-command-docs.ts +++ b/script/generate-command-docs.ts @@ -1,14 +1,15 @@ #!/usr/bin/env bun /** - * Generate Command Reference Documentation from Stricli Command Metadata + * Generate Command Reference & Configuration Documentation * - * Introspects the CLI's route tree to generate accurate command reference - * pages for the documentation website. Flags, arguments, and aliases are - * extracted directly from source code, preventing documentation drift. + * Introspects the CLI's route tree and env var registry to generate + * accurate reference pages for the documentation website. Flags, arguments, + * aliases, and environment variables are extracted directly from source code, + * preventing documentation drift. * * Each generated page combines: - * 1. Auto-generated reference — flags, args, descriptions (from CLI metadata) - * 2. Hand-written fragment — examples, guides, tips (from docs/src/fragments/commands/) + * 1. Auto-generated reference — from CLI metadata or env registry + * 2. Hand-written fragment — examples, guides, tips (from docs/src/fragments/) * * The generated output is gitignored. Fragment files are the committed * source of truth for custom content. @@ -17,8 +18,9 @@ * bun run script/generate-command-docs.ts * * Output: - * docs/src/content/docs/commands/{route}.md (one per visible route) - * docs/src/content/docs/commands/index.md (commands overview table) + * docs/src/content/docs/commands/{route}.md (one per visible route) + * docs/src/content/docs/commands/index.md (commands overview table) + * docs/src/content/docs/configuration.md (env var reference + config fragment) */ import { mkdirSync, rmSync } from "node:fs"; @@ -37,6 +39,7 @@ if (!(await Bun.file(SKILL_CONTENT_PATH).exists())) { await Bun.write(SKILL_CONTENT_PATH, SKILL_CONTENT_STUB); } +import type { EnvVarEntry } from "../src/lib/env-registry.js"; import type { CommandInfo, FlagInfo, @@ -47,10 +50,14 @@ import type { const { routes } = await import("../src/app.js"); const { extractAllRoutes } = await import("../src/lib/introspect.js"); +const { ENV_VAR_REGISTRY } = await import("../src/lib/env-registry.js"); const DOCS_DIR = "docs/src/content/docs/commands"; +const DOCS_CONTENT_DIR = "docs/src/content/docs"; const FRAGMENTS_DIR = "docs/src/fragments/commands"; +const FRAGMENTS_ROOT = "docs/src/fragments"; const INDEX_PATH = `${DOCS_DIR}/index.md`; +const CONFIG_PATH = `${DOCS_CONTENT_DIR}/configuration.md`; /** * Marker comment separating auto-generated reference content from @@ -281,12 +288,76 @@ function generateCommandsTable(allRoutes: RouteInfo[]): string { return lines.join("\n"); } +// --------------------------------------------------------------------------- +// Configuration Page Generation +// --------------------------------------------------------------------------- + +/** + * Generate a `### \`VAR_NAME\`` section for a single environment variable. + * Matches the existing hand-written format in configuration.md. + */ +function generateEnvVarSection(entry: EnvVarEntry): string { + const lines: string[] = []; + lines.push(`### \`${entry.name}\``); + lines.push(""); + lines.push(entry.description); + + if (entry.example !== undefined) { + lines.push(""); + lines.push("```bash"); + lines.push(`export ${entry.name}=${entry.example}`); + lines.push("```"); + } + + return lines.join("\n"); +} + +/** + * Generate the full auto-generated portion of the configuration page. + * + * Includes frontmatter, intro paragraph, and per-variable reference + * sections. The fragment (config file docs, global options, credential + * storage) is appended after the end marker. + */ +function generateConfigurationPage(registry: readonly EnvVarEntry[]): string { + const lines: string[] = []; + + // YAML frontmatter + lines.push("---"); + lines.push("title: Configuration"); + lines.push( + "description: Environment variables, config files, and configuration options for the Sentry CLI" + ); + lines.push("---"); + lines.push(""); + + // Intro + lines.push( + "The Sentry CLI can be configured through config files, environment variables, and a local database. Most users don't need to set any of these — the CLI auto-detects your project from your codebase and stores credentials locally after `sentry auth login`." + ); + lines.push(""); + + // Environment Variables section + lines.push("## Environment Variables"); + lines.push(""); + + for (const entry of registry) { + lines.push(generateEnvVarSection(entry)); + lines.push(""); + } + + // End marker + lines.push(GENERATED_END_MARKER); + + return lines.join("\n"); +} + // --------------------------------------------------------------------------- // Custom Content (Fragment Files) // --------------------------------------------------------------------------- /** - * Read hand-written custom content from a fragment file. + * Read hand-written custom content from a command fragment file. * Fragment files live in docs/src/fragments/commands/ and contain only * the custom sections (examples, guides) — no frontmatter or generated content. * Returns empty string if the fragment file doesn't exist. @@ -299,6 +370,19 @@ async function readCustomContent(fragmentName: string): Promise { } } +/** + * Read hand-written custom content from a top-level fragment file. + * Top-level fragments live in docs/src/fragments/ (not the commands/ subdirectory). + * Returns empty string if the fragment file doesn't exist. + */ +async function readTopLevelFragment(fragmentName: string): Promise { + try { + return await Bun.file(`${FRAGMENTS_ROOT}/${fragmentName}.md`).text(); + } catch { + return ""; + } +} + // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- @@ -357,6 +441,18 @@ const indexContent = indexCustomContent await Bun.write(INDEX_PATH, indexContent); +// --------------------------------------------------------------------------- +// Generate configuration.md (env var reference + fragment) +// --------------------------------------------------------------------------- + +const configGenerated = generateConfigurationPage(ENV_VAR_REGISTRY); +const configFragment = await readTopLevelFragment("configuration"); +const configContent = configFragment + ? configGenerated + configFragment + : `${configGenerated}\n`; + +await Bun.write(CONFIG_PATH, configContent); + console.log( - `Generated ${generatedFiles.length} command doc pages + ${INDEX_PATH}` + `Generated ${generatedFiles.length} command doc pages + ${INDEX_PATH} + ${CONFIG_PATH}` ); diff --git a/script/generate-docs-sections.ts b/script/generate-docs-sections.ts new file mode 100644 index 000000000..d195ebaa8 --- /dev/null +++ b/script/generate-docs-sections.ts @@ -0,0 +1,295 @@ +#!/usr/bin/env bun +/** + * Generate Documentation Sections (Marker-Based) + * + * Injects auto-generated content into hand-written documentation files + * between and markers. + * + * Sections: + * - contributing.md: project structure tree (from route tree + filesystem) + * - DEVELOPMENT.md: OAuth scopes bullet list (from oauth.ts) + * - self-hosted.md: OAuth scopes inline list (from oauth.ts) + * + * Unlike generate-command-docs.ts (which produces gitignored files from scratch), + * this script edits committed files in-place between marker pairs. + * + * Usage: + * bun run script/generate-docs-sections.ts # Generate (write) + * bun run script/generate-docs-sections.ts --check # Dry-run, exit 1 if stale + */ + +import { mkdirSync } from "node:fs"; + +// Bootstrap: skill-content stub (same pattern as generate-command-docs.ts) +const SKILL_CONTENT_PATH = "src/generated/skill-content.ts"; +const SKILL_CONTENT_STUB = + "export const SKILL_FILES: [string, string][] = [];\n"; +if (!(await Bun.file(SKILL_CONTENT_PATH).exists())) { + mkdirSync("src/generated", { recursive: true }); + await Bun.write(SKILL_CONTENT_PATH, SKILL_CONTENT_STUB); +} + +import type { RouteInfo, RouteMap } from "../src/lib/introspect.js"; + +const { routes } = await import("../src/app.js"); +const { extractAllRoutes } = await import("../src/lib/introspect.js"); +const { OAUTH_SCOPES } = await import("../src/lib/oauth.js"); + +const isCheck = process.argv.includes("--check"); + +// --------------------------------------------------------------------------- +// Marker Replacement +// --------------------------------------------------------------------------- + +/** + * Replace content between named marker pairs in a string. + * + * Expects exactly one pair of markers: + * + * ...content to replace... + * + * + * Returns the string with the content between markers replaced by `generated`. + * Throws if markers are missing or out of order. + */ +function replaceMarkerSection( + content: string, + sectionName: string, + generated: string +): string { + const startTag = ``; + const endTag = ``; + + const startIdx = content.indexOf(startTag); + const endIdx = content.indexOf(endTag); + + if (startIdx === -1 || endIdx === -1) { + throw new Error( + `Missing markers for section "${sectionName}": ` + + `start=${startIdx !== -1}, end=${endIdx !== -1}` + ); + } + if (startIdx > endIdx) { + throw new Error(`Markers out of order for section "${sectionName}"`); + } + + const before = content.slice(0, startIdx + startTag.length); + const after = content.slice(endIdx); + return `${before}\n${generated}\n${after}`; +} + +// --------------------------------------------------------------------------- +// Section: Project Structure (contributing.md) +// --------------------------------------------------------------------------- + +/** Routes that are excluded from documentation pages and the project tree */ +const SKIP_ROUTES = new Set(["help"]); + +/** + * Determine if a route is a standalone command (not a group with subcommands). + * Standalone commands live as .ts files directly in src/commands/, + * while groups are subdirectories. + */ +function isStandaloneCommand(route: RouteInfo): boolean { + return ( + route.commands.length === 1 && + route.commands[0].path === `sentry ${route.name}` + ); +} + +/** + * Get subcommand names for a route group (e.g., "list, view, create"). + * Extracts the last path segment from each command's path. + */ +function getSubcommandNames(route: RouteInfo): string[] { + return route.commands.map((cmd) => { + const parts = cmd.path.split(" "); + return parts.at(-1) ?? route.name; + }); +} + +/** + * Generate the project structure tree for contributing.md. + * + * Combines static entries (bin.ts, app.ts, etc.) with dynamic + * command directories/files extracted from the route tree. + */ +function generateProjectStructure(allRoutes: RouteInfo[]): string { + const lines: string[] = []; + lines.push("```"); + lines.push("cli/"); + lines.push("├── src/"); + lines.push("│ ├── bin.ts # Entry point"); + lines.push("│ ├── app.ts # Stricli application setup"); + lines.push("│ ├── context.ts # Dependency injection context"); + lines.push("│ ├── commands/ # CLI commands"); + + // Separate routes into groups (directories) and standalone (files) + const groups: RouteInfo[] = []; + const standalones: RouteInfo[] = []; + for (const route of allRoutes) { + if (SKIP_ROUTES.has(route.name)) { + continue; + } + if (isStandaloneCommand(route)) { + standalones.push(route); + } else { + groups.push(route); + } + } + + // Sort both alphabetically + groups.sort((a, b) => a.name.localeCompare(b.name)); + standalones.sort((a, b) => a.name.localeCompare(b.name)); + + // Render group directories (always use ├── since standalones follow) + for (const route of groups) { + const subcmds = getSubcommandNames(route).join(", "); + lines.push(`│ │ ├── ${`${route.name}/`.padEnd(13)}# ${subcmds}`); + } + + // Combine standalone commands with help.ts (which is in SKIP_ROUTES + // for doc generation but still exists in the filesystem). + // Add help before sorting so it lands in correct alphabetical position. + const allStandaloneEntries: { name: string; brief: string }[] = + standalones.map((r) => ({ name: r.name, brief: r.commands[0].brief })); + allStandaloneEntries.push({ name: "help", brief: "Help command" }); + allStandaloneEntries.sort((a, b) => a.name.localeCompare(b.name)); + + // Render standalone command files + for (let i = 0; i < allStandaloneEntries.length; i += 1) { + const entry = allStandaloneEntries[i]; + const isLast = i === allStandaloneEntries.length - 1; + const prefix = isLast ? "└──" : "├──"; + lines.push( + `│ │ ${prefix} ${`${entry.name}.ts`.padEnd(13)}# ${entry.brief}` + ); + } + + lines.push("│ ├── lib/ # Shared utilities"); + lines.push("│ └── types/ # TypeScript types and Zod schemas"); + lines.push("├── test/ # Test files (mirrors src/ structure)"); + lines.push("├── script/ # Build and utility scripts"); + lines.push("├── plugins/ # Agent skill files"); + lines.push( + "└── docs/ # Documentation site (Astro + Starlight)" + ); + lines.push("```"); + + return lines.join("\n"); +} + +// --------------------------------------------------------------------------- +// Section: OAuth Scopes (DEVELOPMENT.md, self-hosted.md) +// --------------------------------------------------------------------------- + +/** + * Group OAuth scopes by resource prefix (the part before the colon). + * Returns groups in insertion order. + */ +function groupScopesByResource( + scopes: readonly string[] +): Map { + const groups = new Map(); + for (const scope of scopes) { + const resource = scope.split(":")[0]; + const existing = groups.get(resource); + if (existing) { + existing.push(scope); + } else { + groups.set(resource, [scope]); + } + } + return groups; +} + +/** + * Generate scopes as a bullet list for DEVELOPMENT.md. + * Groups by resource prefix, each group on one line. + */ +function generateScopesBulletList(scopes: readonly string[]): string { + const grouped = groupScopesByResource(scopes); + const lines: string[] = []; + for (const scopeGroup of grouped.values()) { + const formatted = scopeGroup.map((s) => `\`${s}\``).join(", "); + lines.push(` - ${formatted}`); + } + return lines.join("\n"); +} + +/** + * Generate scopes as an inline comma-separated list for self-hosted.md. + */ +function generateScopesInline(scopes: readonly string[]): string { + return scopes.map((s) => `\`${s}\``).join(", "); +} + +// --------------------------------------------------------------------------- +// Section Definitions +// --------------------------------------------------------------------------- + +type SectionDef = { + filePath: string; + sectionName: string; + generate: () => string; +}; + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +const routeMap = routes as unknown as RouteMap; +const routeInfos = extractAllRoutes(routeMap); + +const sections: SectionDef[] = [ + { + filePath: "docs/src/content/docs/contributing.md", + sectionName: "project-structure", + generate: () => generateProjectStructure(routeInfos), + }, + { + filePath: "DEVELOPMENT.md", + sectionName: "oauth-scopes", + generate: () => generateScopesBulletList(OAUTH_SCOPES), + }, + { + filePath: "docs/src/content/docs/self-hosted.md", + sectionName: "oauth-scopes", + generate: () => generateScopesInline(OAUTH_SCOPES), + }, +]; + +let staleCount = 0; + +for (const section of sections) { + const original = await Bun.file(section.filePath).text(); + const generated = section.generate(); + const updated = replaceMarkerSection( + original, + section.sectionName, + generated + ); + + if (updated !== original) { + if (isCheck) { + console.error(`STALE: ${section.filePath} [${section.sectionName}]`); + staleCount += 1; + } else { + await Bun.write(section.filePath, updated); + console.log(`Updated: ${section.filePath} [${section.sectionName}]`); + } + } else { + console.log(`Up to date: ${section.filePath} [${section.sectionName}]`); + } +} + +if (isCheck && staleCount > 0) { + console.error( + `\n${staleCount} section(s) are stale. Run: bun run generate:docs` + ); + process.exit(1); +} + +if (!isCheck) { + console.log("All docs sections generated."); +} diff --git a/src/lib/env-registry.ts b/src/lib/env-registry.ts new file mode 100644 index 000000000..b285f0ba1 --- /dev/null +++ b/src/lib/env-registry.ts @@ -0,0 +1,184 @@ +/** + * Environment Variable Registry + * + * Centralized metadata catalog for all environment variables recognized + * by the CLI. Used by doc generators to produce configuration.md — NOT + * used at runtime for env var access (existing `getEnv()` patterns remain). + */ + +/** Metadata for a single environment variable */ +export type EnvVarEntry = { + /** Variable name (e.g., "SENTRY_AUTH_TOKEN") */ + name: string; + /** Multi-sentence description (markdown OK). Rendered as body text under the heading. */ + description: string; + /** Example value shown in a bash code block. If omitted, no code block is generated. */ + example?: string; + /** Default value, mentioned in the description when provided. */ + defaultValue?: string; + /** Install-script-only variable (not used at runtime by the CLI binary). */ + installOnly?: boolean; +}; + +/** + * All user-facing environment variables recognized by the Sentry CLI. + * + * Ordered by documentation priority: auth → targeting → URL → paths → + * install → display → logging/telemetry → cache/pagination → database. + * The generator preserves this order in the output. + */ +export const ENV_VAR_REGISTRY: readonly EnvVarEntry[] = [ + // -- Auth -- + { + name: "SENTRY_AUTH_TOKEN", + description: + "Authentication token for the Sentry API. This is the primary way to authenticate in CI/CD pipelines and scripts where interactive login is not possible.\n\nYou can create auth tokens in your [Sentry account settings](https://sentry.io/settings/account/api/auth-tokens/). When set, this takes precedence over any stored OAuth token from `sentry auth login`.", + example: "sntrys_YOUR_TOKEN_HERE", + }, + { + name: "SENTRY_TOKEN", + description: + "Legacy alias for `SENTRY_AUTH_TOKEN`. If both are set, `SENTRY_AUTH_TOKEN` takes precedence.", + }, + { + name: "SENTRY_FORCE_ENV_TOKEN", + description: + "When set, environment variable tokens (`SENTRY_AUTH_TOKEN` / `SENTRY_TOKEN`) take precedence over the stored OAuth token from `sentry auth login`. By default, the stored OAuth token takes priority because it supports automatic refresh. Set this if you want to ensure the environment variable token is always used, which is useful for self-hosted setups or CI environments.", + example: "1", + }, + // -- Targeting -- + { + name: "SENTRY_ORG", + description: + "Default organization slug. Skips organization auto-detection.", + example: "my-org", + }, + { + name: "SENTRY_PROJECT", + description: + "Default project slug. Can also include the org in `org/project` format.\n\nWhen using the `org/project` combo format, `SENTRY_ORG` is ignored.", + example: "my-org/my-project", + }, + { + name: "SENTRY_DSN", + description: + "Sentry DSN for project auto-detection. This is the same DSN you use in `Sentry.init()`. The CLI resolves it to determine your organization and project.\n\nThe CLI also detects DSNs from `.env` files and source code automatically — see [DSN Auto-Detection](./features/#dsn-auto-detection).", + example: "https://key@o123.ingest.us.sentry.io/456", + }, + // -- URL -- + { + name: "SENTRY_HOST", + description: + "Base URL of your Sentry instance. **Only needed for [self-hosted Sentry](./self-hosted/).** SaaS users (sentry.io) should not set this.\n\nWhen set, all API requests (including OAuth login) are directed to this URL instead of `https://sentry.io`. The CLI also sets this automatically when you pass a self-hosted Sentry URL as a command argument.\n\n`SENTRY_HOST` takes precedence over `SENTRY_URL`. Both work identically — use whichever you prefer.", + example: "https://sentry.example.com", + defaultValue: "https://sentry.io", + }, + { + name: "SENTRY_URL", + description: + "Alias for `SENTRY_HOST`. If both are set, `SENTRY_HOST` takes precedence.", + defaultValue: "https://sentry.io", + }, + { + name: "SENTRY_CLIENT_ID", + description: + "Client ID of a public OAuth application on your Sentry instance. **Required for [self-hosted Sentry](./self-hosted/)** (26.1.0+) to use `sentry auth login` with the device flow. See the [Self-Hosted guide](./self-hosted/#1-create-a-public-oauth-application) for how to create one.", + example: "your-oauth-client-id", + }, + // -- Paths -- + { + name: "SENTRY_CONFIG_DIR", + description: + "Override the directory where the CLI stores its database (credentials, caches, defaults). Defaults to `~/.sentry/`.", + example: "/path/to/config", + defaultValue: "~/.sentry/", + }, + { + name: "SENTRY_INSTALL_DIR", + description: + "Override the directory where the CLI binary is installed. Used by the install script and `sentry cli upgrade` to control the binary location.", + example: "/usr/local/bin", + installOnly: true, + }, + // -- Install -- + { + name: "SENTRY_VERSION", + description: + "Pin a specific version for the [install script](./getting-started/#install-script). Accepts a version number (e.g., `0.19.0`) or `nightly`. The `--version` flag takes precedence if both are set.\n\nThis is useful in CI/CD pipelines and Dockerfiles where you want reproducible installations without inline flags.", + example: "nightly", + installOnly: true, + }, + { + name: "SENTRY_INIT", + description: + "Used with the install script. When set to `1`, the installer runs `sentry init` after installing the binary to guide you through project setup.", + example: "1", + installOnly: true, + }, + // -- Display -- + { + name: "SENTRY_PLAIN_OUTPUT", + description: + "Force plain text output (no colors or ANSI formatting). Takes precedence over `NO_COLOR`.", + example: "1", + }, + { + name: "NO_COLOR", + description: + "Standard convention to disable color output. See [no-color.org](https://no-color.org/). Respected when `SENTRY_PLAIN_OUTPUT` is not set.", + example: "1", + }, + { + name: "FORCE_COLOR", + description: + "Force color output on interactive terminals. Only takes effect when stdout is a TTY. Set to `0` to force plain output, `1` to force color. Ignored when stdout is piped.", + example: "1", + }, + { + name: "SENTRY_OUTPUT_FORMAT", + description: + "Force the output format for all commands. Currently only `json` is supported. This is primarily used by the [library API](./library-usage/) (`createSentrySDK()`) to get JSON output without passing `--json` flags.", + example: "json", + }, + // -- Logging & telemetry -- + { + name: "SENTRY_LOG_LEVEL", + description: + "Controls the verbosity of diagnostic output. Defaults to `info`.\n\nValid values: `error`, `warn`, `log`, `info`, `debug`, `trace`\n\nEquivalent to passing `--log-level debug` on the command line. CLI flags take precedence over the environment variable.", + example: "debug", + defaultValue: "info", + }, + { + name: "SENTRY_CLI_NO_TELEMETRY", + description: + "Disable CLI telemetry (error tracking for the CLI itself). The CLI sends anonymized error reports to help improve reliability — set this to opt out.", + example: "1", + }, + { + name: "SENTRY_CLI_NO_UPDATE_CHECK", + description: + "Disable the automatic update check that runs periodically in the background.", + example: "1", + }, + // -- Cache & pagination -- + { + name: "SENTRY_NO_CACHE", + description: + "Disable API response caching. When set, the CLI will not cache API responses and will always make fresh requests.", + example: "1", + }, + { + name: "SENTRY_MAX_PAGINATION_PAGES", + description: + "Cap the maximum number of pages fetched during auto-pagination. Useful for limiting API calls when using large `--limit` values.", + example: "10", + defaultValue: "50", + }, + // -- Database -- + { + name: "SENTRY_CLI_NO_AUTO_REPAIR", + description: + "Disable automatic database schema repair. By default, the CLI automatically repairs its SQLite database when it detects schema drift. Set this to `1` to prevent auto-repair.", + example: "1", + }, +]; diff --git a/src/lib/oauth.ts b/src/lib/oauth.ts index ba635dd9d..294bf1263 100644 --- a/src/lib/oauth.ts +++ b/src/lib/oauth.ts @@ -49,8 +49,8 @@ function getClientId(): string { ); } -// OAuth scopes requested for the CLI -const SCOPES = [ +/** OAuth scopes requested by the CLI. Exported for doc generation. */ +export const OAUTH_SCOPES: readonly string[] = [ "project:read", "project:write", "project:admin", @@ -60,7 +60,10 @@ const SCOPES = [ "member:read", "team:read", "team:write", -].join(" "); +]; + +/** Space-joined scope string for OAuth requests */ +const SCOPES = OAUTH_SCOPES.join(" "); type DeviceFlowCallbacks = { onUserCode: (