diff --git a/.cursor/rules/bun-cli.mdc b/.cursor/rules/bun-cli.mdc deleted file mode 100644 index f6715e26f..000000000 --- a/.cursor/rules/bun-cli.mdc +++ /dev/null @@ -1,304 +0,0 @@ ---- -description: Bun CLI Development Standards - Leveraging Bun's native APIs for optimal performance -globs: "packages/cli/**/*.{ts,tsx}" -alwaysApply: true ---- - -# Bun CLI Development Standards - -This project uses **Bun** as both runtime and build tool. Always prefer Bun-native APIs over Node.js equivalents for better performance and cleaner code. - -## Bun API Reference - -Full documentation: https://bun.sh/docs - -## File Operations - -**Use Bun's file APIs instead of `node:fs` for reading/writing:** - -```typescript -// Reading files -const file = Bun.file(filepath); -if (await file.exists()) { - const text = await file.text(); // Read as string - const json = await file.json(); // Parse JSON directly - const buffer = await file.bytes(); // Read as Uint8Array -} - -// Writing files -await Bun.write(filepath, content); // String or Buffer -await Bun.write(filepath, Bun.file(other)); // Copy file - -// File metadata -const stats = await Bun.file(filepath).stat(); -``` - -**Exception:** Use `node:fs` for directory creation with specific permissions: -```typescript -import { mkdirSync } from "node:fs"; -mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 }); -``` - -Docs: https://bun.sh/docs/api/file-io - -## Process Spawning - -**Use `Bun.spawn()` and `Bun.which()` instead of `node:child_process`:** - -```typescript -// Find executable -const git = Bun.which("git"); // Returns path or null - -// Spawn process -const proc = Bun.spawn(["git", "status"], { - cwd: "/path/to/repo", - stdout: "pipe", - stderr: "pipe", - env: { ...process.env, CUSTOM: "value" }, -}); - -// Read output -const stdout = await Bun.readableStreamToText(proc.stdout); -const exitCode = await proc.exited; -``` - -Docs: https://bun.sh/docs/api/spawn - -## Shell Commands (Scripting) - -**For build scripts and automation, use `Bun.$`:** - -```typescript -import { $ } from "bun"; - -// Tagged template shell commands -await $`git add . && git commit -m "message"`; -const sha = await $`git rev-parse HEAD`.text(); - -// With error handling -const result = await $`npm test`.nothrow(); -if (result.exitCode !== 0) { - console.error(result.stderr.toString()); -} -``` - -Docs: https://bun.sh/docs/runtime/shell - -## Glob Pattern Matching - -**Use `Bun.Glob` for file discovery:** - -```typescript -const glob = new Bun.Glob("**/*.{ts,js}"); - -for await (const file of glob.scan({ cwd: "./src", onlyFiles: true })) { - console.log(file); -} - -// Check if path matches pattern -if (glob.match("src/index.ts")) { - // ... -} -``` - -Docs: https://bun.sh/docs/api/glob - -## HTTP Server (if needed) - -**Use `Bun.serve()` for local servers (OAuth callbacks, etc.):** - -```typescript -const server = Bun.serve({ - port: 0, // Auto-assign port - fetch(req) { - const url = new URL(req.url); - if (url.pathname === "/callback") { - return new Response("Success!"); - } - return new Response("Not found", { status: 404 }); - }, -}); - -console.log(`Server running on port ${server.port}`); -server.stop(); // When done -``` - -Docs: https://bun.sh/docs/api/http - -## Building Binaries - -**Use `Bun.build()` with `compile` option for standalone executables:** - -```typescript -await Bun.build({ - entrypoints: ["./src/bin.ts"], - compile: { - target: "bun-darwin-arm64", // or linux-x64, windows-x64, etc. - outfile: "dist/sentry", - }, - define: { - CLI_VERSION: JSON.stringify(version), - // Inject secrets at build time for npm distribution - SENTRY_CLIENT_ID_BUILD: JSON.stringify(process.env.SENTRY_CLIENT_ID ?? ""), - }, - sourcemap: "external", -}); -``` - -**Build-time secrets pattern:** - -For values that need to be baked into the binary (like OAuth client IDs): - -1. Read from env in build script: `process.env.SENTRY_CLIENT_ID` -2. Inject via `define`: `SENTRY_CLIENT_ID_BUILD: JSON.stringify(value)` -3. Use in code with runtime override support: - -```typescript -// Declare the build-time constant -declare const SENTRY_CLIENT_ID_BUILD: string | undefined; - -// Allow runtime override (for self-hosted), fall back to build-time value -const CLIENT_ID = - process.env.SENTRY_CLIENT_ID ?? - (typeof SENTRY_CLIENT_ID_BUILD !== "undefined" ? SENTRY_CLIENT_ID_BUILD : ""); -``` - -**Build command with secrets:** -```bash -SENTRY_CLIENT_ID=xxx bun run build:all -``` - -Supported targets: -- `bun-darwin-arm64`, `bun-darwin-x64`, `bun-darwin-x64-baseline` -- `bun-linux-x64`, `bun-linux-arm64`, `bun-linux-x64-baseline` -- `bun-linux-x64-musl`, `bun-linux-arm64-musl` -- `bun-windows-x64`, `bun-windows-x64-baseline` - -Baseline variants target older CPUs without AVX2 support. - -Docs: https://bun.sh/docs/bundler/executables - -## Utilities - -```typescript -// Sleep -await Bun.sleep(1000); // 1 second - -// Fast hashing -const hash = Bun.hash.xxHash32(data); - -// TOML parsing -const config = Bun.TOML.parse(content); - -// Module resolution -const path = await Bun.resolve("package/file", import.meta.dir); - -// Environment -const value = Bun.env.MY_VAR; - -// Direct I/O -Bun.stderr.write("Error message\n"); -const input = await Bun.stdin.text(); -``` - -## Testing - -**Use `bun:test` for all tests:** - -```typescript -import { describe, expect, test, mock, beforeEach } from "bun:test"; - -describe("feature", () => { - test("should work", async () => { - expect(await someFunction()).toBe(expected); - }); -}); - -// Mocking -mock.module("./some-module", () => ({ - default: () => "mocked", -})); -``` - -Run tests: `bun test` - -Docs: https://bun.sh/docs/cli/test - -## What NOT to Use - -Avoid these Node.js APIs when Bun equivalents exist: - -| Avoid | Use Instead | -|-------|-------------| -| `fs.readFileSync()` | `await Bun.file(path).text()` | -| `fs.writeFileSync()` | `await Bun.write(path, content)` | -| `fs.existsSync()` | `await Bun.file(path).exists()` | -| `child_process.spawn()` | `Bun.spawn()` | -| `child_process.exec()` | `Bun.$\`command\`` | -| `which` package | `Bun.which()` | -| `glob` package | `new Bun.Glob()` | -| `fast-glob` | `new Bun.Glob()` | - -**Keep using `node:fs` for:** -- Directory creation with permissions (`mkdirSync` with `mode`) -- Operations that need sync behavior in specific contexts - -## CLI Framework - -This project uses **Stricli** (`@stricli/core`) for CLI command definitions. Key patterns: - -```typescript -import { buildCommand, buildRouteMap } from "@stricli/core"; - -export const myCommand = buildCommand({ - docs: { - brief: "Short description", - fullDescription: "Detailed description with examples", - }, - parameters: { - flags: { - json: { kind: "boolean", brief: "Output as JSON", default: false }, - limit: { kind: "parsed", parse: Number, brief: "Max items", default: 10 }, - }, - }, - async func(this: SentryContext, flags) { - // Implementation - all config functions are async, use await - }, -}); -``` - -## Validation - -Use **Zod** for runtime validation of configs and API responses: - -```typescript -import { z } from "zod"; - -const ConfigSchema = z.object({ - token: z.string(), - org: z.string().optional(), -}); - -type Config = z.infer; - -// Validates and throws if invalid -const config = ConfigSchema.parse(rawData); - -// Returns { success: boolean, data?, error? } -const result = ConfigSchema.safeParse(rawData); -``` - -Docs: https://zod.dev - -## Async Patterns - -All config functions in this project are async. Always await them: - -```typescript -// Config operations -const token = await getAuthToken(); -const isAuth = await isAuthenticated(); -const org = await getDefaultOrganization(); -await setAuthToken(token, expiresIn); -await clearAuth(); -``` diff --git a/.cursor/rules/ultracite.mdc b/.cursor/rules/ultracite.mdc deleted file mode 100644 index ad163e550..000000000 --- a/.cursor/rules/ultracite.mdc +++ /dev/null @@ -1,101 +0,0 @@ ---- -description: Ultracite Rules - AI-Ready Formatter and Linter -globs: "**/*.{ts,tsx,js,jsx,json,jsonc,html,vue,svelte,astro,css,yaml,yml,graphql,gql,md,mdx,grit}" -alwaysApply: false ---- - -# Ultracite Code Standards - -This project uses **Ultracite**, a zero-config Biome preset that enforces strict code quality standards through automated formatting and linting. - -## Quick Reference - -- **Format code**: `npx ultracite fix` -- **Check for issues**: `npx ultracite check` -- **Diagnose setup**: `npx ultracite doctor` - -Biome (the underlying engine) provides extremely fast Rust-based linting and formatting. Most issues are automatically fixable. - ---- - -## Core Principles - -Write code that is **accessible, performant, type-safe, and maintainable**. Focus on clarity and explicit intent over brevity. - -### Type Safety & Explicitness - -- Use explicit types for function parameters and return values when they enhance clarity -- Prefer `unknown` over `any` when the type is genuinely unknown -- Use const assertions (`as const`) for immutable values and literal types -- Leverage TypeScript's type narrowing instead of type assertions -- Use meaningful variable names instead of magic numbers - extract constants with descriptive names - -### Modern JavaScript/TypeScript - -- Use arrow functions for callbacks and short functions -- Prefer `for...of` loops over `.forEach()` and indexed `for` loops -- Use optional chaining (`?.`) and nullish coalescing (`??`) for safer property access -- Prefer template literals over string concatenation -- Use destructuring for object and array assignments -- Use `const` by default, `let` only when reassignment is needed, never `var` - -### Async & Promises - -- Always `await` promises in async functions - don't forget to use the return value -- Use `async/await` syntax instead of promise chains for better readability -- Handle errors appropriately in async code with try-catch blocks -- Don't use async functions as Promise executors - -### Error Handling & Debugging - -- Remove `console.log`, `debugger`, and `alert` statements from production code -- Throw `Error` objects with descriptive messages, not strings or other values -- Use `try-catch` blocks meaningfully - don't catch errors just to rethrow them -- Prefer early returns over nested conditionals for error cases - -### Code Organization - -- Keep functions focused and under reasonable cognitive complexity limits -- Extract complex conditions into well-named boolean variables -- Use early returns to reduce nesting -- Prefer simple conditionals over nested ternary operators -- Group related code together and separate concerns - -### Security - -- Add `rel="noopener"` when using `target="_blank"` on links -- Avoid `dangerouslySetInnerHTML` unless absolutely necessary -- Don't use `eval()` or assign directly to `document.cookie` -- Validate and sanitize user input - -### Performance - -- Avoid spread syntax in accumulators within loops -- Use top-level regex literals instead of creating them in loops -- Prefer specific imports over namespace imports -- Avoid barrel files (index files that re-export everything) -- Use proper image components (e.g., Next.js ``) over `` tags - ---- - -## Testing - -- Write assertions inside `it()` or `test()` blocks -- Avoid done callbacks in async tests - use async/await instead -- Don't use `.only` or `.skip` in committed code -- Keep test suites reasonably flat - avoid excessive `describe` nesting - -## When Biome Can't Help - -Biome's linter will catch most issues automatically. Focus your attention on: - -1. **Business logic correctness** - Biome can't validate your algorithms -2. **Meaningful naming** - Use descriptive names for functions, variables, and types -3. **Architecture decisions** - Component structure, data flow, and API design -4. **Edge cases** - Handle boundary conditions and error states -5. **User experience** - Accessibility, performance, and usability considerations -6. **Documentation** - Add comments for complex logic, but prefer self-documenting code - ---- - -Most formatting and common issues are automatically fixed by Biome. Run `npx ultracite fix` before committing to ensure compliance. diff --git a/AGENTS.md b/AGENTS.md index a36c3ec76..490504e46 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,1156 +1,62 @@ -# AGENTS.md - -Guidelines for AI agents working in this codebase. - -## Project Overview - -**Sentry CLI** is a command-line interface for [Sentry](https://sentry.io), built with [Bun](https://bun.sh) and [Stricli](https://bloomberg.github.io/stricli/). - -### Goals - -- **Zero-config experience** - Auto-detect project context from DSNs in source code and env files -- **AI-powered debugging** - Integrate Seer AI for root cause analysis and fix plans -- **Developer-friendly** - Follow `gh` CLI conventions for intuitive UX -- **Agent-friendly** - JSON output and predictable behavior for AI coding agents -- **Fast** - Native binaries via Bun, SQLite caching for API responses - -### Key Features - -- **DSN Auto-Detection** - Scans `.env` files and source code (JS, Python, Go, Java, Ruby, PHP) to find Sentry DSNs -- **Project Root Detection** - Walks up from CWD to find project boundaries using VCS, language, and build markers -- **Directory Name Inference** - Fallback project matching using bidirectional word boundary matching -- **Multi-Region Support** - Automatic region detection with fan-out to regional APIs (us.sentry.io, de.sentry.io) -- **Monorepo Support** - Generates short aliases for multiple projects -- **Seer AI Integration** - `issue explain` and `issue plan` commands for AI analysis -- **OAuth Device Flow** - Secure authentication without browser redirects - -## Cursor Rules (Important!) - -Before working on this codebase, read the Cursor rules: - -- **`.cursor/rules/bun-cli.mdc`** - Bun API usage, file I/O, process spawning, testing -- **`.cursor/rules/ultracite.mdc`** - Code style, formatting, linting rules - -## Quick Reference: Commands - -> **Note**: Always check `package.json` for the latest scripts. - -```bash -# Development -bun install # Install dependencies -bun run dev # Run CLI in dev mode -bun run --env-file=.env.local src/bin.ts # Dev with env vars - -# Build -bun run build # Build for current platform -bun run build:all # Build for all platforms - -# Type Checking -bun run typecheck # Check types - -# Linting & Formatting -bun run lint # Check for issues -bun run lint:fix # Auto-fix issues (run before committing) - -# Testing -bun test # Run all tests -bun test path/to/file.test.ts # Run single test file -bun test --watch # Watch mode -bun test --filter "test name" # Run tests matching pattern -bun run test:unit # Run unit tests only -bun run test:e2e # Run e2e tests only -``` - -## Rules: No Runtime Dependencies - -**CRITICAL**: All packages must be in `devDependencies`, never `dependencies`. Everything is bundled at build time via esbuild. CI enforces this with `bun run check:deps`. - -When adding a package, always use `bun add -d ` (the `-d` flag). - -When the `@sentry/api` SDK provides types for an API response, import them directly from `@sentry/api` instead of creating redundant Zod schemas in `src/types/sentry.ts`. - -## Rules: Use Bun APIs - -**CRITICAL**: This project uses Bun as runtime. Always prefer Bun-native APIs over Node.js equivalents. - -Read the full guidelines in `.cursor/rules/bun-cli.mdc`. - -**Bun Documentation**: https://bun.sh/docs - Consult these docs when unsure about Bun APIs. - -### Quick Bun API Reference - -| Task | Use This | NOT This | -|------|----------|----------| -| Read file | `await Bun.file(path).text()` | `fs.readFileSync()` | -| Write file | `await Bun.write(path, content)` | `fs.writeFileSync()` | -| Check file exists | `await Bun.file(path).exists()` | `fs.existsSync()` | -| Spawn process | `Bun.spawn()` | `child_process.spawn()` | -| Shell commands | `Bun.$\`command\`` ⚠️ | `child_process.exec()` | -| Find executable | `Bun.which("git")` | `which` package | -| Glob patterns | `new Bun.Glob()` | `glob` / `fast-glob` packages | -| Sleep | `await Bun.sleep(ms)` | `setTimeout` with Promise | -| Parse JSON file | `await Bun.file(path).json()` | Read + JSON.parse | - -**Exception**: Use `node:fs` for directory creation with permissions: -```typescript -import { mkdirSync } from "node:fs"; -mkdirSync(dir, { recursive: true, mode: 0o700 }); -``` - -**Exception**: `Bun.$` (shell tagged template) has no shim in `script/node-polyfills.ts` and will crash on the npm/node distribution. Until a shim is added, use `execSync` from `node:child_process` for shell commands that must work in both runtimes: -```typescript -import { execSync } from "node:child_process"; -const result = execSync("id -u username", { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }); -``` - -## Architecture - -``` -cli/ -├── src/ -│ ├── bin.ts # Entry point -│ ├── app.ts # Stricli application setup -│ ├── context.ts # Dependency injection context -│ ├── commands/ # CLI commands -│ │ ├── auth/ # login, logout, refresh, status, token, whoami -│ │ ├── cli/ # defaults, feedback, fix, setup, upgrade -│ │ ├── dashboard/ # list, view, create, widget (add, edit, delete) -│ │ ├── event/ # list, view -│ │ ├── issue/ # list, view, events, explain, plan, resolve, unresolve, merge -│ │ ├── log/ # list, view -│ │ ├── org/ # list, view -│ │ ├── project/ # list, view, create, delete -│ │ ├── 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 # Direct API access command -│ │ ├── help.ts # Help command -│ │ ├── init.ts # Initialize Sentry in your project (experimental) -│ │ └── schema.ts # Browse the Sentry API schema -│ ├── lib/ # Shared utilities -│ │ ├── command.ts # buildCommand wrapper (telemetry + output) -│ │ ├── api-client.ts # Barrel re-export for API modules -│ │ ├── api/ # Domain API modules -│ │ │ ├── infrastructure.ts # Shared helpers, types, raw requests -│ │ │ ├── organizations.ts -│ │ │ ├── projects.ts -│ │ │ ├── issues.ts -│ │ │ ├── events.ts -│ │ │ ├── traces.ts # Trace + span listing -│ │ │ ├── logs.ts -│ │ │ ├── seer.ts -│ │ │ └── trials.ts -│ │ ├── region.ts # Multi-region resolution -│ │ ├── telemetry.ts # Sentry SDK instrumentation -│ │ ├── sentry-urls.ts # URL builders for Sentry -│ │ ├── hex-id.ts # Hex ID validation (32-char + 16-char span) -│ │ ├── trace-id.ts # Trace ID validation wrapper -│ │ ├── db/ # SQLite database layer -│ │ │ ├── instance.ts # Database singleton -│ │ │ ├── schema.ts # Table definitions -│ │ │ ├── migration.ts # Schema migrations -│ │ │ ├── utils.ts # SQL helpers (upsert) -│ │ │ ├── auth.ts # Token storage -│ │ │ ├── user.ts # User info cache -│ │ │ ├── regions.ts # Org→region URL cache -│ │ │ ├── defaults.ts # Default org/project -│ │ │ ├── pagination.ts # Cursor pagination storage -│ │ │ ├── dsn-cache.ts # DSN resolution cache -│ │ │ ├── project-cache.ts # Project data cache -│ │ │ ├── project-root-cache.ts # Project root cache -│ │ │ ├── project-aliases.ts # Monorepo alias mappings -│ │ │ └── version-check.ts # Version check cache -│ │ ├── dsn/ # DSN detection system -│ │ │ ├── detector.ts # High-level detection API -│ │ │ ├── scanner.ts # File scanning logic -│ │ │ ├── code-scanner.ts # Code file DSN extraction -│ │ │ ├── project-root.ts # Project root detection -│ │ │ ├── parser.ts # DSN parsing utilities -│ │ │ ├── resolver.ts # DSN to org/project resolution -│ │ │ ├── fs-utils.ts # File system helpers -│ │ │ ├── env.ts # Environment variable detection -│ │ │ ├── env-file.ts # .env file parsing -│ │ │ ├── errors.ts # DSN-specific errors -│ │ │ ├── types.ts # Type definitions -│ │ │ └── languages/ # Per-language DSN extractors -│ │ │ ├── javascript.ts -│ │ │ ├── python.ts -│ │ │ ├── go.ts -│ │ │ ├── java.ts -│ │ │ ├── ruby.ts -│ │ │ └── php.ts -│ │ ├── formatters/ # Output formatting -│ │ │ ├── human.ts # Human-readable output -│ │ │ ├── json.ts # JSON output -│ │ │ ├── output.ts # Output utilities -│ │ │ ├── seer.ts # Seer AI response formatting -│ │ │ ├── colors.ts # Terminal colors -│ │ │ ├── markdown.ts # Markdown → ANSI renderer -│ │ │ ├── trace.ts # Trace/span formatters -│ │ │ ├── time-utils.ts # Shared time/duration utils -│ │ │ ├── table.ts # Table rendering -│ │ │ └── log.ts # Log entry formatting -│ │ ├── oauth.ts # OAuth device flow -│ │ ├── errors.ts # Error classes -│ │ ├── resolve-target.ts # Org/project resolution -│ │ ├── resolve-issue.ts # Issue ID resolution -│ │ ├── issue-id.ts # Issue ID parsing utilities -│ │ ├── arg-parsing.ts # Argument parsing helpers -│ │ ├── alias.ts # Alias generation -│ │ ├── promises.ts # Promise utilities -│ │ ├── polling.ts # Polling utilities -│ │ ├── upgrade.ts # CLI upgrade functionality -│ │ ├── version-check.ts # Version checking -│ │ ├── browser.ts # Open URLs in browser -│ │ ├── clipboard.ts # Clipboard access -│ │ └── qrcode.ts # QR code generation -│ └── types/ # TypeScript types and Zod schemas -│ ├── sentry.ts # Sentry API types -│ ├── config.ts # Configuration types -│ ├── oauth.ts # OAuth types -│ └── seer.ts # Seer AI types -├── test/ # Test files (mirrors src/ structure) -│ ├── lib/ # Unit tests for lib/ -│ │ ├── *.test.ts # Standard unit tests -│ │ ├── *.property.test.ts # Property-based tests -│ │ └── db/ -│ │ ├── *.test.ts # DB unit tests -│ │ └── *.model-based.test.ts # Model-based tests -│ ├── model-based/ # Model-based testing helpers -│ │ └── helpers.ts # Isolated DB context, constants -│ ├── commands/ # Unit tests for commands/ -│ ├── e2e/ # End-to-end tests -│ ├── fixtures/ # Test fixtures -│ └── mocks/ # Test mocks -├── docs/ # Documentation site (Astro + Starlight) -├── script/ # Build and utility scripts -├── .cursor/rules/ # Cursor AI rules (read these!) -└── biome.jsonc # Linting config (extends ultracite) +# Agent Instructions + +## Project +- Sentry CLI is a Bun + Stricli command-line client for Sentry. +- Product goals: zero-config project detection, `gh`-style UX, reliable JSON for agents, fast bundled binaries, and Seer-powered debugging flows. +- Keep this file as the always-loaded router. Put large durable context in `policies/`, repeatable workflows in `playbooks/`, and design plans in `specs/`. + +## Package Manager +- Use **Bun**: `bun install`, `bun run dev`, `bun run test`, `bun run typecheck`. +- Add packages with `bun add -d ` only; this repo does not use runtime `dependencies`. + +## Commands +- Setup: `bun install` +- Run CLI: `bun run dev -- ` +- Run with local env: `bun run --env-file=.env.local src/bin.ts ` +- Typecheck: `bun run typecheck` +- Lint: `bun run lint`; fix with `bun run lint:fix` +- Unit tests: `bun run test:unit` +- E2E tests: `bun run test:e2e` +- One test file: `bun test path/to/file.test.ts --timeout 15000 --isolate` +- Metadata checks: `bun run check:fragments`; `bun run check:errors`; `bun run check:deps` + +## Policies +- Runtime APIs, packages, Node distribution: `policies/runtime-and-deps.md` +- Commands, routes, mutations: `policies/cli-command-design.md` +- Human output, JSON output, errors: `policies/output-and-errors.md` +- Cursor pagination for list commands: `policies/pagination.md` +- Test style and isolation: `policies/testing.md` +- Generated docs, skills, schemas: `policies/generated-artifacts.md` +- Edge-case implementation notes: `policies/implementation-notes.md` + +## Playbooks +- Local CLI smoke testing: `playbooks/local-cli-testing.md` + +## Key Conventions +- Command code uses repo wrappers: `buildCommand`, `buildListCommand`, `buildDeleteCommand`, and `buildRouteMap`. +- Command output goes through `CommandOutput`; the wrappers own `--json` and `--fields`. +- Required entity IDs are positional args, not flags. +- Use shared `CliError` subclasses from `src/lib/errors.ts`. +- Production `catch` blocks must log, rethrow, or explain the fallback. +- Local ESM imports use `.js` extensions; type-only imports use `import type`. +- Prefer `@sentry/api` response types when available instead of duplicating API schemas. + +## File Map +- Commands: `src/commands//` +- API modules: `src/lib/api/` +- Formatters: `src/lib/formatters/` +- Shared command helpers: `src/lib/command.ts`, `src/lib/list-command.ts`, `src/lib/mutate-command.ts` +- Org/project resolution: `src/lib/resolve-target.ts`, `src/lib/org-list.ts` +- DSN detection: `src/lib/dsn/` +- SQLite/cache code: `src/lib/db/` +- Types and schemas: `src/types/` +- Tests: `test/` mirrors `src/` +- Command doc fragments: `docs/src/fragments/commands/` +- Generated plugin skill: `plugins/sentry-cli/skills/sentry-cli/` + +## Commit Attribution +AI commits MUST include: + +```text +Co-Authored-By: OpenAI Codex ``` - -## Key Patterns - -### CLI Commands (Stricli) - -Commands use [Stricli](https://bloomberg.github.io/stricli/docs/getting-started/principles) wrapped by `src/lib/command.ts`. - -**CRITICAL**: Import `buildCommand` from `../../lib/command.js`, **NEVER** from `@stricli/core` directly — the wrapper adds telemetry, `--json`/`--fields` injection, and output rendering. - -Pattern: - -```typescript -import { buildCommand } from "../../lib/command.js"; -import type { SentryContext } from "../../context.js"; -import { CommandOutput } from "../../lib/formatters/output.js"; - -export const myCommand = buildCommand({ - docs: { - brief: "Short description", - fullDescription: "Detailed description", - }, - output: { - human: formatMyData, // (data: T) => string - jsonTransform: jsonTransformMyData, // optional: (data: T, fields?) => unknown - jsonExclude: ["humanOnlyField"], // optional: strip keys from JSON - }, - parameters: { - flags: { - limit: { kind: "parsed", parse: Number, brief: "Max items", default: 10 }, - }, - }, - async *func(this: SentryContext, flags) { - const data = await fetchData(); - yield new CommandOutput(data); - return { hint: "Tip: use --json for machine-readable output" }; - }, -}); -``` - -**Key rules:** -- Functions are `async *func()` generators — yield `new CommandOutput(data)`, return `{ hint }`. -- `output.human` receives the same data object that gets serialized to JSON — no divergent-data paths. -- The wrapper auto-injects `--json` and `--fields` flags. Do NOT add your own `json` flag. -- Do NOT use `stdout.write()` or `if (flags.json)` branching — the wrapper handles it. - -### Command File Structure - -Command files in `src/commands/` should focus on three concerns: -1. **Argument parsing** — positional args, flags, URL detection -2. **API orchestration** — fetching data, error handling, enrichment -3. **Output dispatch** — `yield new CommandOutput(data)` - -Formatting and rendering logic belongs in `src/lib/formatters/.ts`. If a command file exceeds ~400 lines, extract formatting helpers into a dedicated formatter module. - -Reference: `src/lib/formatters/replay.ts` (extracted from `replay/view.ts`), `src/lib/formatters/trace.ts`, `src/lib/formatters/human.ts`. - -Lint enforcement: `stderr.write()` is banned in command files (GritQL rule). Use `logger` for diagnostics and `CommandOutput` for data output. - -### Route Maps (Stricli) - -Route groups use Stricli's `buildRouteMap` wrapped by `src/lib/route-map.ts`. - -**CRITICAL**: Import `buildRouteMap` from `../../lib/route-map.js`, **NEVER** from `@stricli/core` directly — the wrapper auto-injects standard subcommand aliases based on which route keys exist: - -| Route | Auto-aliases | -|----------|----------------| -| `list` | `ls` | -| `view` | `show` | -| `delete` | `remove`, `rm` | -| `create` | `new` | - -Manually specified aliases in `aliases` are merged with (and take precedence over) auto-generated ones. Do NOT manually add aliases that are already in the standard set above. - -```typescript -import { buildRouteMap } from "../../lib/route-map.js"; - -export const myRoute = buildRouteMap({ - routes: { - list: listCommand, - view: viewCommand, - create: createCommand, - }, - defaultCommand: "view", - // No need for aliases — ls, show, and new are auto-injected. - // Only add aliases for non-standard mappings: - // aliases: { custom: "list" }, - docs: { - brief: "Manage my resources", - }, -}); -``` - -### Positional Arguments - -Use `parseSlashSeparatedArg` from `src/lib/arg-parsing.ts` for the standard `[//]` pattern. Required identifiers (trace IDs, span IDs) should be **positional args**, not flags. - -```typescript -import { parseSlashSeparatedArg, parseOrgProjectArg } from "../../lib/arg-parsing.js"; - -// "my-org/my-project/abc123" → { id: "abc123", targetArg: "my-org/my-project" } -const { id, targetArg } = parseSlashSeparatedArg(first, "Trace ID", USAGE_HINT); -const parsed = parseOrgProjectArg(targetArg); -// parsed.type: "auto-detect" | "explicit" | "project-search" | "org-all" -``` - -Reference: `span/list.ts`, `trace/view.ts`, `event/view.ts` - -### Markdown Rendering - -All non-trivial human output must use the markdown rendering pipeline: - -- Build markdown strings with helpers: `mdKvTable()`, `colorTag()`, `escapeMarkdownCell()`, `renderMarkdown()` -- **NEVER** use raw `muted()` / chalk in output strings — use `colorTag("muted", text)` inside markdown -- Tree-structured output (box-drawing characters) that can't go through `renderMarkdown()` should use the `plainSafeMuted` pattern: `isPlainOutput() ? text : muted(text)` -- `isPlainOutput()` precedence: `SENTRY_PLAIN_OUTPUT` > `NO_COLOR` > `FORCE_COLOR` (TTY only) > `!isTTY` -- `isPlainOutput()` lives in `src/lib/formatters/plain-detect.ts` (re-exported from `markdown.ts` for compat) - -Reference: `formatters/trace.ts` (`formatAncestorChain`), `formatters/human.ts` (`plainSafeMuted`) - -### Create & Delete Command Standards - -Mutation (create/delete) commands use shared infrastructure from `src/lib/mutate-command.ts`, -paralleling `list-command.ts` for list commands. - -**Delete commands** MUST use `buildDeleteCommand()` instead of `buildCommand()`. It: -1. Auto-injects `--yes`, `--force`, `--dry-run` flags with `-y`, `-f`, `-n` aliases -2. Runs a non-interactive safety guard before `func()` — refuses to proceed if - stdin is not a TTY and `--yes`/`--force` was not passed (dry-run bypasses) -3. Options to skip specific injections (`noForceFlag`, `noDryRunFlag`, `noNonInteractiveGuard`) - -```typescript -import { buildDeleteCommand, confirmByTyping, isConfirmationBypassed, requireExplicitTarget } from "../../lib/mutate-command.js"; - -export const deleteCommand = buildDeleteCommand({ - // Same args as buildCommand — flags/aliases auto-injected - async *func(this: SentryContext, flags, target) { - requireExplicitTarget(parsed, "Entity", "sentry entity delete "); - if (flags["dry-run"]) { yield preview; return; } - if (!isConfirmationBypassed(flags)) { - if (!await confirmByTyping(expected, promptMessage)) return; - } - await doDelete(); - }, -}); -``` - -**Create commands** import `DRY_RUN_FLAG` and `DRY_RUN_ALIASES` for consistent dry-run support: - -```typescript -import { DRY_RUN_FLAG, DRY_RUN_ALIASES } from "../../lib/mutate-command.js"; - -// In parameters: -flags: { "dry-run": DRY_RUN_FLAG, team: { ... } }, -aliases: { ...DRY_RUN_ALIASES, t: "team" }, -``` - -**Key utilities** in `mutate-command.ts`: -- `isConfirmationBypassed(flags)` — true if `--yes` or `--force` is set -- `guardNonInteractive(flags)` — throws in non-interactive mode without `--yes` -- `confirmByTyping(expected, message)` — type-out confirmation prompt -- `requireExplicitTarget(parsed, entityType, usage)` — blocks auto-detect for safety -- `DESTRUCTIVE_FLAGS` / `DESTRUCTIVE_ALIASES` — spreadable bundles for manual use - -### List Command Pagination - -All list commands with API pagination MUST use the shared cursor-stack -infrastructure for **bidirectional** pagination (`-c next` / `-c prev`): - -```typescript -import { LIST_CURSOR_FLAG } from "../../lib/list-command.js"; -import { - buildPaginationContextKey, resolveCursor, - advancePaginationState, hasPreviousPage, -} from "../../lib/db/pagination.js"; - -export const PAGINATION_KEY = "my-entity-list"; - -// In buildCommand: -flags: { cursor: LIST_CURSOR_FLAG }, -aliases: { c: "cursor" }, - -// In func(): -const contextKey = buildPaginationContextKey("entity", `${org}/${project}`, { - sort: flags.sort, q: flags.query, -}); -const { cursor, direction } = resolveCursor(flags.cursor, PAGINATION_KEY, contextKey); -const { data, nextCursor } = await listEntities(org, project, { cursor, ... }); -advancePaginationState(PAGINATION_KEY, contextKey, direction, nextCursor); -const hasPrev = hasPreviousPage(PAGINATION_KEY, contextKey); -const hasMore = !!nextCursor; -``` - -**Cursor stack model:** The DB stores a JSON array of page-start cursors -plus a page index. Each entry is an opaque string — plain API cursors, -compound cursors (issue list), or extended cursors with mid-page bookmarks -(dashboard list). `-c next` increments the index, `-c prev` decrements it, -`-c first` resets to 0. The stack truncates on back-then-forward to avoid -stale entries. `"last"` is a silent alias for `"next"`. - -**Hint rules:** Show `-c prev` when `hasPreviousPage()` returns true. -Show `-c next` when `hasMore` is true. Include both `nextCursor` and -`hasPrev` in the JSON envelope. - -**Navigation hint generation:** Use `paginationHint()` from -`src/lib/list-command.ts` to build bidirectional navigation strings. -Pass it pre-built `prevHint`/`nextHint` command strings and it returns -the combined `"Prev: X | Next: Y"` string (or single-direction, or `""`). -Do NOT assemble `navParts` arrays manually — the shared helper ensures -consistent formatting across all list commands. - -```typescript -import { paginationHint } from "../../lib/list-command.js"; - -const nav = paginationHint({ - hasPrev, - hasMore, - prevHint: `sentry entity list ${org}/ -c prev`, - nextHint: `sentry entity list ${org}/ -c next`, -}); -if (items.length === 0 && nav) { - hint = `No entities on this page. ${nav}`; -} else if (hasMore) { - header = `Showing ${items.length} entities (more available)\n${nav}`; -} else if (nav) { - header = `Showing ${items.length} entities\n${nav}`; -} -``` - -**Three abstraction levels for list commands** (prefer the highest level -that fits your use case): - -1. **`buildOrgListCommand`** (team/repo list) — Fully automatic. Pagination - hints, cursor management, JSON envelope, and human formatting are all - handled internally. New simple org-scoped list commands should use this. - -2. **`dispatchOrgScopedList` with overrides** (project/issue list) — Automatic - for most modes; custom `"org-all"` override calls `resolveCursor` + - `advancePaginationState` + `paginationHint` manually. - -3. **`buildListCommand` with manual pagination** (trace/span/dashboard list) — - Command manages its own pagination loop. Must call `resolveCursor`, - `advancePaginationState`, `hasPreviousPage`, and `paginationHint` directly. - -**Auto-pagination for large limits:** - -When `--limit` exceeds `API_MAX_PER_PAGE` (100), list commands MUST transparently -fetch multiple pages to fill the requested limit. Cap `perPage` at -`Math.min(flags.limit, API_MAX_PER_PAGE)` and loop until `results.length >= limit` -or pages are exhausted. This matches the `listIssuesAllPages` pattern. - -```typescript -const perPage = Math.min(flags.limit, API_MAX_PER_PAGE); -for (let page = 0; page < MAX_PAGINATION_PAGES; page++) { - const { data, nextCursor } = await listPaginated(org, { perPage, cursor }); - results.push(...data); - if (results.length >= flags.limit || !nextCursor) break; - cursor = nextCursor; -} -``` - -Never pass a `per_page` value larger than `API_MAX_PER_PAGE` to the API — the -server silently caps it, causing the command to return fewer items than requested. - -Reference template: `trace/list.ts`, `span/list.ts`, `dashboard/list.ts` - -### ID Validation - -Use shared validators from `src/lib/hex-id.ts`: -- `validateHexId(value, label)` — 32-char hex IDs (trace IDs, log IDs). Auto-strips UUID dashes. -- `validateSpanId(value)` — 16-char hex span IDs. Auto-strips dashes. -- `validateTraceId(value)` — thin wrapper around `validateHexId` in `src/lib/trace-id.ts`. - -All normalize to lowercase. Throw `ValidationError` on invalid input. - -### Sort Convention - -Use `"date"` for timestamp-based sort (not `"time"`). Export sort types from the API layer (e.g., `SpanSortValue` from `api/traces.ts`), import in commands. This matches `issue list`, `trace list`, and `span list`. - -### Generated Docs & Skills - -All command docs and skill files are generated via `bun run generate:docs` (which runs `generate:command-docs` then `generate:skill`). This runs automatically as part of `dev`, `build`, `typecheck`, and `test` scripts. - -- **Command docs** (`docs/src/content/docs/commands/*.md`) are **gitignored** and generated from CLI metadata + hand-written fragments in `docs/src/fragments/commands/`. -- **Skill files** (`plugins/sentry-cli/skills/sentry-cli/`) are **committed** (consumed by external plugin systems) and auto-committed by CI when stale. -- Edit fragments in `docs/src/fragments/commands/` for custom examples and guides. -- `bun run check:fragments` validates fragment ↔ route consistency. -- Positional `placeholder` values must be descriptive: `"org/project/trace-id"` not `"args"`. - -### Zod Schemas for Validation - -All config and API types use Zod schemas: - -```typescript -import { z } from "zod"; - -export const MySchema = z.object({ - field: z.string(), - optional: z.number().optional(), -}); - -export type MyType = z.infer; - -// Validate data -const result = MySchema.safeParse(data); -if (result.success) { - // result.data is typed -} -``` - -### Type Organization - -- Define Zod schemas alongside types in `src/types/*.ts` -- Key type files: `sentry.ts` (API types), `config.ts` (configuration), `oauth.ts` (auth flow), `seer.ts` (Seer AI) -- Re-export from `src/types/index.ts` -- Use `type` imports: `import type { MyType } from "../types/index.js"` - -### SQL Utilities - -Use the `upsert()` helper from `src/lib/db/utils.ts` to reduce SQL boilerplate: - -```typescript -import { upsert, runUpsert } from "../db/utils.js"; - -// Generate UPSERT statement -const { sql, values } = upsert("table", { id: 1, name: "foo" }, ["id"]); -db.query(sql).run(...values); - -// Or use convenience wrapper -runUpsert(db, "table", { id: 1, name: "foo" }, ["id"]); - -// Exclude columns from update -const { sql, values } = upsert( - "users", - { id: 1, name: "Bob", created_at: now }, - ["id"], - { excludeFromUpdate: ["created_at"] } -); -``` - -### Error Handling - -All CLI errors extend the `CliError` base class from `src/lib/errors.ts`: - -```typescript -// Error hierarchy in src/lib/errors.ts -// Exit codes are defined in the EXIT constant object — use EXIT.* constants -// when constructing errors, never hardcode numeric exit codes outside errors.ts. -CliError (base, exitCode=1) -├── HostScopeError (exitCode=13) -├── ApiError (exitCode=30 — HTTP/API failures) -├── AuthError (exitCode=10–12 by reason — 'not_authenticated' | 'expired' | 'invalid') -├── ConfigError (exitCode=20 — configuration/DSN) -├── OutputError (exitCode=60 — data rendered, but operation failed) -├── ContextError (exitCode=22 — missing context) -├── ResolutionError (exitCode=23 — value provided but not found) -├── ValidationError (exitCode=21 — input validation) -├── DeviceFlowError (exitCode=51 — OAuth flow) -├── SeerError (exitCode=40–42 by reason — 'not_enabled' | 'no_budget' | 'ai_disabled') -├── TimeoutError (exitCode=31 — operation timed out) -├── UpgradeError (exitCode=50 — upgrade failures) -└── WizardError (exitCode=61–64 by workflow step — init wizard error) -``` - -> Exit code ranges: 1x=auth, 2x=input/config, 3x=API/network, 4x=feature/billing, -> 5x=operations, 6x=command-specific. See `EXIT` in `src/lib/errors.ts` and -> https://cli.sentry.dev/exit-codes/ for the full reference. - -**Choosing between ContextError, ResolutionError, and ValidationError:** - -| Scenario | Error Class | Example | -|----------|-------------|---------| -| User **omitted** a required value | `ContextError` | No org/project provided | -| User **provided** a value that wasn't found | `ResolutionError` | Project 'cli' not found | -| User input is **malformed** | `ValidationError` | Invalid hex ID format | - -**ContextError rules:** -- `command` must be a **single-line** CLI usage example (e.g., `"sentry org view "`) -- Constructor throws if `command` contains `\n` (catches misuse in tests) -- Pass `alternatives: []` when defaults are irrelevant (e.g., for missing Trace ID, Event ID) -- Use `" and "` in `resource` for plural grammar: `"Trace ID and span ID"` → "are required" - -**CI enforcement:** `bun run check:errors` scans for `ContextError` with multiline commands and `CliError` with ad-hoc "Try:" strings. - -```typescript -// Usage examples -throw new ContextError("Organization", "sentry org view "); -throw new ContextError("Trace ID", "sentry trace view ", []); // no alternatives -throw new ResolutionError("Project 'cli'", "not found", "sentry issue list /cli", [ - "No project with this slug found in any accessible organization", -]); -throw new ValidationError("Invalid trace ID format", "traceId"); -``` - -**Fuzzy suggestions in resolution errors:** - -When a user-provided name/title doesn't match any entity, use `fuzzyMatch()` from -`src/lib/fuzzy.ts` to suggest similar candidates instead of listing all entities -(which can be overwhelming). Show at most 5 fuzzy matches. - -Reference: `resolveDashboardId()` in `src/commands/dashboard/resolve.ts`. - -### Catch Block Logging - -Silent `catch` blocks are prohibited in `src/` production code. Biome's `noEmptyBlockStatements` catches syntactically empty `catch {}` blocks, but blocks with only a `return` statement and no logging are equally problematic — errors vanish silently, making debugging impossible. - -Every `catch` block must either: -1. Re-throw the error -2. Log with `log.debug()` or `log.warn()` for diagnostic visibility -3. Return a fallback value **with** a `log.debug()` call explaining the suppression - -```typescript -// WRONG — error vanishes silently -try { data = await fetchOptionalData(); } -catch { return []; } - -// RIGHT — error is visible in debug logs -try { data = await fetchOptionalData(); } -catch (error) { - log.debug("Failed to fetch optional data", error); - return []; -} -``` - -Use `logger.withTag("command-name")` for tagged logging in command files. - -### Auto-Recovery for Wrong Entity Types - -When a user provides the wrong type of identifier (e.g., an issue short ID -where a trace ID is expected), commands should **auto-recover** when the -user's intent is unambiguous: - -1. **Detect** the actual entity type using helpers like `looksLikeIssueShortId()`, - `SPAN_ID_RE`, `HEX_ID_RE`, or non-hex character checks. -2. **Resolve** the input to the correct type (e.g., issue → latest event → trace ID). -3. **Warn** via `log.warn()` explaining what happened. -4. **Show** the result with a return `hint` nudging toward the correct command. - -When recovery is **ambiguous or impossible**, keep the existing error but add -entity-aware suggestions (e.g., "This looks like a span ID"). - -**Detection helpers:** -- `looksLikeIssueShortId(value)` — uppercase dash-separated (e.g., `CLI-G5`) -- `SPAN_ID_RE.test(value)` — 16-char hex (span ID) -- `HEX_ID_RE.test(value)` — 32-char hex (trace/event/log ID) -- `/[^0-9a-f]/.test(normalized)` — non-hex characters → likely a slug/name - -**Reference implementations:** -- `event/view.ts` — issue short ID → latest event redirect -- `span/view.ts` — `traceId/spanId` slash format → auto-split -- `trace/view.ts` — issue short ID → issue's trace redirect -- `hex-id.ts` — entity-aware error hints in `validateHexId`/`validateSpanId` - -### Async Config Functions - -All config operations are async. Always await: - -```typescript -const token = await getAuthToken(); -const isAuth = await isAuthenticated(); -await setAuthToken(token, expiresIn); -``` - -### Adding New Utility Files - -Before creating a new `src/lib/*.ts` utility file, check whether existing shared modules already cover your use case: - -| If you need... | Check first... | -|----------------|---------------| -| Duration formatting | `src/lib/formatters/time-utils.ts` (`formatDurationCompact`, `formatDurationVerbose`) | -| Hex ID validation/normalization | `src/lib/hex-id.ts` (`validateHexId`, `tryNormalizeHexId`, `normalizeHexId`) | -| Relative time display | `src/lib/formatters/time-utils.ts` (`formatRelativeTime`) | -| Table/markdown output | `src/lib/formatters/` directory | -| Pagination | `src/lib/db/pagination.ts`, `src/lib/list-command.ts` | -| Error classes | `src/lib/errors.ts` (never create ad-hoc error types) | -| Search query building | `src/lib/search-query.ts`, `src/lib/arg-parsing.ts` | - -If an existing module covers ≥80% of what you need, extend it with new exported functions rather than creating a new file. New files are appropriate when the domain is genuinely new (e.g., `replay-search.ts` for replay-specific field resolution). - -Every new `src/lib/**/*.ts` file must start with a module-level JSDoc comment describing the module's purpose. - -### Imports - -- Use `.js` extension for local imports (ESM requirement) -- Group: external packages first, then local imports -- Use `type` keyword for type-only imports - -```typescript -import { z } from "zod"; -import { buildCommand } from "../../lib/command.js"; -import type { SentryContext } from "../../context.js"; -import { getAuthToken } from "../../lib/config.js"; -``` - -### List Command Infrastructure - -Two abstraction levels exist for list commands: - -1. **`src/lib/list-command.ts`** — `buildOrgListCommand` factory + shared Stricli parameter constants (`LIST_TARGET_POSITIONAL`, `LIST_JSON_FLAG`, `LIST_CURSOR_FLAG`, `buildListLimitFlag`). Use this for simple entity lists like `team list` and `repo list`. - -2. **`src/lib/org-list.ts`** — `dispatchOrgScopedList` with `OrgListConfig` and a 4-mode handler map: `auto-detect`, `explicit`, `org-all`, `project-search`. Complex commands (`project list`, `issue list`) call `dispatchOrgScopedList` with an `overrides` map directly instead of using `buildOrgListCommand`. - -Key rules when writing overrides: -- Each mode handler receives a `HandlerContext` with the narrowed `parsed` plus shared I/O (`stdout`, `cwd`, `flags`). Access parsed fields via `ctx.parsed.org`, `ctx.parsed.projectSlug`, etc. — no manual `Extract<>` casts needed. -- Commands with extra fields (e.g., `stderr`, `setContext`) spread the context and add them: `(ctx) => handle({ ...ctx, flags, stderr, setContext })`. Override `ctx.flags` with the command-specific flags type when needed. -- `resolveCursor()` must be called **inside** the `org-all` override closure, not before `dispatchOrgScopedList`, so that `--cursor` validation errors fire correctly for non-org-all modes. -- `handleProjectSearch` errors must use `"Project"` as the `ContextError` resource, not `config.entityName`. -- Always set `orgSlugMatchBehavior` on `dispatchOrgScopedList` to declare how bare-slug org matches are handled. Use `"redirect"` for commands where listing all entities in the org makes sense (e.g., `project list`, `team list`, `issue list`). Use `"error"` for commands where org-all redirect is inappropriate. The pre-check uses cached orgs to avoid N API calls — when the cache is cold, the handler's own org-slug check serves as a safety net (throws `ResolutionError` with a hint). - -3. **Standalone list commands** (e.g., `span list`, `trace list`) that don't use org-scoped dispatch wire pagination directly in `func()`. See the "List Command Pagination" section above for the pattern. - -### Project Filtering in API Calls - -Different Sentry API endpoints use different project filtering mechanisms. Never apply both simultaneously: - -| API Endpoint | Project filter | Helper | -|-------------|---------------|--------| -| Discover/Events (`queryEvents`) | `project:` in query string | `buildProjectQuery()` | -| Replay index (`listReplays`) | `projectSlugs` parameter | Direct parameter | -| Issue index (`listIssuesPaginated`) | `project` parameter or query string | Varies by mode | - -When adding a new dataset to `explore`, verify which filtering mechanism the underlying API expects and handle it in `resolveDatasetConfig`. The `explore` command centralizes dataset-specific behavior (sort, query, fetch, field validation) in `resolveDatasetConfig` — add new datasets there rather than scattering `if (dataset === ...)` checks through the `func` body. - -## Commenting & Documentation (JSDoc-first) - -### Default Rule -- **Prefer JSDoc over inline comments.** -- Code should be readable without narrating what it already says. - -### Required: JSDoc -Add JSDoc comments on: -- **Every exported function, class, and type** (and important internal ones). -- **Types/interfaces**: document each field/property (what it represents, units, allowed values, meaning of `null`, defaults). - -Include in JSDoc: -- What it does -- Key business rules / constraints -- Assumptions and edge cases -- Side effects -- Why it exists (when non-obvious) - -### Inline Comments (rare) -Inline comments are **allowed only** when they add information the code cannot express: -- **"Why"** - business reason, constraint, historical context -- **Non-obvious behavior** - surprising edge cases -- **Workarounds** - bugs in dependencies, platform quirks -- **Hardcoded values** - why hardcoded, what would break if changed - -Inline comments are **NOT allowed** if they just restate the code: -```typescript -// Bad: -if (!person) // if no person -i++ // increment i -return result // return result - -// Good: -// Required by GDPR Article 17 - user requested deletion -await deleteUserData(userId) -``` - -### Prohibited Comment Styles -- **ASCII art section dividers** - Do not use decorative box-drawing characters like `─────────` to create section headers. Use standard JSDoc comments or simple `// Section Name` comments instead. - -### Goal -Minimal comments, maximum clarity. Comments explain **intent and reasoning**, not syntax. - -## Testing (bun:test + fast-check) - -**Prefer property-based and model-based testing** over traditional unit tests. These approaches find edge cases automatically and provide better coverage with less code. - -**fast-check Documentation**: https://fast-check.dev/docs/core-blocks/arbitraries/ - -### Testing Hierarchy (in order of preference) - -1. **Model-Based Tests** - For stateful systems (database, caches, state machines) -2. **Property-Based Tests** - For pure functions, parsing, validation, transformations -3. **Unit Tests** - Only for trivial cases or when properties are hard to express - -### Test File Naming - -| Type | Pattern | Location | -|------|---------|----------| -| Property-based | `*.property.test.ts` | `test/lib/` | -| Model-based | `*.model-based.test.ts` | `test/lib/db/` | -| Unit tests | `*.test.ts` | `test/` (mirrors `src/`) | -| E2E tests | `*.test.ts` | `test/e2e/` | - -### Test Environment Isolation (CRITICAL) - -Tests that need a database or config directory **must** use `useTestConfigDir()` from `test/helpers.ts`. This helper: -- Creates a unique temp directory in `beforeEach` -- Sets `SENTRY_CONFIG_DIR` to point at it -- **Restores** (never deletes) the env var in `afterEach` -- Closes the database and cleans up temp files - -**NEVER** do any of these in test files: -- `delete process.env.SENTRY_CONFIG_DIR` — This pollutes other test files that load after yours -- `const baseDir = process.env[CONFIG_DIR_ENV_VAR]!` at module scope — This captures a value that may be stale -- Manual `beforeEach`/`afterEach` that sets/deletes `SENTRY_CONFIG_DIR` - -**Why**: Bun's test runner uses `--isolate --parallel` (see `test:unit` in `package.json`), so each test file runs in a fresh global environment within a worker process. That bounds most cross-file leaks to a single worker, but `process.env` is still shared within a file's lifecycle — if your `afterEach` deletes the env var, the next describe/test's module-level code (or a beforeEach that re-reads env) gets `undefined`, causing `TypeError: The "paths[0]" property must be of type string`. Also, `TEST_TMP_DIR` is namespaced by `BUN_TEST_WORKER_ID` in `test/constants.ts` so parallel workers don't wipe each other's temp state during preload. - -```typescript -// CORRECT: Use the helper -import { useTestConfigDir } from "../helpers.js"; - -const getConfigDir = useTestConfigDir("my-test-prefix-"); - -// If you need the directory path in a test: -test("example", () => { - const dir = getConfigDir(); -}); - -// WRONG: Manual env var management -beforeEach(() => { process.env.SENTRY_CONFIG_DIR = tmpDir; }); -afterEach(() => { delete process.env.SENTRY_CONFIG_DIR; }); // BUG! -``` - -### Property-Based Testing - -Use property-based tests when verifying invariants that should hold for **any valid input**. - -```typescript -import { describe, expect, test } from "bun:test"; -import { constantFrom, assert as fcAssert, property, tuple } from "fast-check"; -import { DEFAULT_NUM_RUNS } from "../model-based/helpers.js"; - -// Define arbitraries (random data generators) -const slugArb = array(constantFrom(..."abcdefghijklmnopqrstuvwxyz0123456789".split("")), { - minLength: 1, - maxLength: 15, -}).map((chars) => chars.join("")); - -describe("property: myFunction", () => { - test("is symmetric", () => { - fcAssert( - property(slugArb, slugArb, (a, b) => { - // Properties should always hold regardless of input - expect(myFunction(a, b)).toBe(myFunction(b, a)); - }), - { numRuns: DEFAULT_NUM_RUNS } - ); - }); - - test("round-trip: encode then decode returns original", () => { - fcAssert( - property(validInputArb, (input) => { - const encoded = encode(input); - const decoded = decode(encoded); - expect(decoded).toEqual(input); - }), - { numRuns: DEFAULT_NUM_RUNS } - ); - }); -}); -``` - -**Good candidates for property-based testing:** -- Parsing functions (DSN, issue IDs, aliases) -- Encoding/decoding (round-trip invariant) -- Symmetric operations (a op b = b op a) -- Idempotent operations (f(f(x)) = f(x)) -- Validation functions (valid inputs accepted, invalid rejected) - -**See examples:** `test/lib/dsn.property.test.ts`, `test/lib/alias.property.test.ts`, `test/lib/issue-id.property.test.ts` - -### Model-Based Testing - -Use model-based tests for **stateful systems** where sequences of operations should maintain invariants. - -```typescript -import { describe, expect, test } from "bun:test"; -import { - type AsyncCommand, - asyncModelRun, - asyncProperty, - commands, - assert as fcAssert, -} from "fast-check"; -import { createIsolatedDbContext, DEFAULT_NUM_RUNS } from "../../model-based/helpers.js"; - -// Define a simplified model of expected state -type DbModel = { - entries: Map; -}; - -// Define commands that operate on both model and real system -class SetCommand implements AsyncCommand { - constructor(readonly key: string, readonly value: string) {} - - check = () => true; - - async run(model: DbModel, real: RealDb): Promise { - // Apply to real system - await realSet(this.key, this.value); - - // Update model - model.entries.set(this.key, this.value); - } - - toString = () => `set("${this.key}", "${this.value}")`; -} - -class GetCommand implements AsyncCommand { - constructor(readonly key: string) {} - - check = () => true; - - async run(model: DbModel, real: RealDb): Promise { - const realValue = await realGet(this.key); - const expectedValue = model.entries.get(this.key); - - // Verify real system matches model - expect(realValue).toBe(expectedValue); - } - - toString = () => `get("${this.key}")`; -} - -describe("model-based: database", () => { - test("random sequences maintain consistency", () => { - fcAssert( - asyncProperty(commands(allCommandArbs), async (cmds) => { - const cleanup = createIsolatedDbContext(); - try { - await asyncModelRun( - () => ({ model: { entries: new Map() }, real: {} }), - cmds - ); - } finally { - cleanup(); - } - }), - { numRuns: DEFAULT_NUM_RUNS } - ); - }); -}); -``` - -**Good candidates for model-based testing:** -- Database operations (auth, caches, regions) -- Stateful caches with invalidation -- Systems with cross-cutting invariants (e.g., clearAuth also clears regions) - -**See examples:** `test/lib/db/model-based.test.ts`, `test/lib/db/dsn-cache.model-based.test.ts` - -### Test Helpers - -Use `test/model-based/helpers.ts` for shared utilities: - -```typescript -import { createIsolatedDbContext, DEFAULT_NUM_RUNS } from "../model-based/helpers.js"; - -// Create isolated DB for each test run (prevents interference) -const cleanup = createIsolatedDbContext(); -try { - // ... test code -} finally { - cleanup(); -} - -// Use consistent number of runs across tests -fcAssert(property(...), { numRuns: DEFAULT_NUM_RUNS }); // 50 runs -``` - -### When to Use Unit Tests - -Use traditional unit tests only when: -- Testing trivial logic with obvious expected values -- Properties are difficult to express or would be tautological -- Testing error messages or specific output formatting -- Integration with external systems (E2E tests) - -### Avoiding Unit/Property Test Duplication - -When a `*.property.test.ts` file exists for a module, **do not add unit tests that re-check the same invariants** with hardcoded examples. Before adding a unit test, check whether the companion property file already generates random inputs for that invariant. - -**Unit tests that belong alongside property tests:** -- Edge cases outside the property generator's range (e.g., self-hosted DSNs when the arbitrary only produces SaaS ones) -- Specific output format documentation (exact strings, column layouts, rendered vs plain mode) -- Concurrency/timing behavior that property tests cannot express -- Integration tests exercising multiple functions together (e.g., `writeJsonList` envelope shape) - -**Unit tests to avoid when property tests exist:** -- "returns true for valid input" / "returns false for invalid input" — the property test already covers this with random inputs -- Basic round-trip assertions — property tests check `decode(encode(x)) === x` for all `x` -- Hardcoded examples of invariants like idempotency, symmetry, or subset relationships - -When adding property tests for a function that already has unit tests, **remove the unit tests that become redundant**. Add a header comment to the unit test file noting which invariants live in the property file: - -```typescript -/** - * Note: Core invariants (round-trips, validation, ordering) are tested via - * property-based tests in foo.property.test.ts. These tests focus on edge - * cases and specific output formatting not covered by property generators. - */ -``` - -```typescript -import { describe, expect, test, mock } from "bun:test"; - -describe("feature", () => { - test("should return specific value", async () => { - expect(await someFunction("input")).toBe("expected output"); - }); -}); - -// Mock modules when needed -mock.module("./some-module", () => ({ - default: () => "mocked", -})); -``` - -## File Locations - -| What | Where | -|------|-------| -| Add new command | `src/commands//` | -| Add API types | `src/types/sentry.ts` | -| Add config types | `src/types/config.ts` | -| Add Seer types | `src/types/seer.ts` | -| Add utility | `src/lib/` | -| Add DSN language support | `src/lib/dsn/languages/` | -| Add DB operations | `src/lib/db/` | -| Build scripts | `script/` | -| Add property tests | `test/lib/.property.test.ts` | -| Add model-based tests | `test/lib/db/.model-based.test.ts` | -| Add unit tests | `test/` (mirror `src/` structure) | -| Add E2E tests | `test/e2e/` | -| Test helpers | `test/model-based/helpers.ts` | -| Add documentation | `docs/src/content/docs/` | -| Hand-written command doc content | `docs/src/fragments/commands/` | - - -## Long-term Knowledge - -### Architecture - - -* **Issue resolve --in grammar: release + @next + @commit sentinels**: \`sentry issue resolve --in\` grammar: (a) omitted→immediate resolve, (b) \`\\`→\`inRelease\` (monorepo \`spotlight@1.2.3\` pass-through), (c) \`@next\`→\`inNextRelease\`, (d) \`@commit\`→auto-detect git HEAD + match Sentry repos, (e) \`@commit:\@\\`→explicit. Sentinel matching case-insensitive; unknown \`@\`-prefixed tokens throw \`ValidationError\`. \`parseResolveSpec\` splits on LAST \`@\` to handle scoped names like \`@acme/web\`. \`resolveCommitSpec\` uses \`getHeadCommit\`/\`getRepositoryName\` from \`src/lib/git.ts\`, matching Sentry repo \`externalSlug\` or \`name\` via \`listRepositoriesCached\`. API requires \`statusDetails.inCommit: {commit, repository}\` — not bare SHA. - - -* **npm bundle requires Node.js >= 22 due to node:sqlite polyfill**: The npm package (dist/bin.cjs) requires Node.js >= 22 because the bun:sqlite polyfill uses \`node:sqlite\`. A runtime version guard in the esbuild banner catches this early. When writing esbuild banner strings in TS template literals, double-escape: \`\\\\\\\n\` in TS → \`\\\n\` in output → newline at runtime. Single \`\\\n\` produces a literal newline inside a JS string, causing SyntaxError. - - -* **repo\_cache SQLite table for offline Sentry repo lookups**: Schema v14 adds \`repo\_cache\` table in \`src/lib/db/schema.ts\` + helpers in \`src/lib/db/repo-cache.ts\` (7-day TTL). \`listAllRepositories(org)\` in \`src/lib/api/repositories.ts\` paginates through \`listRepositoriesPaginated\` using \`API\_MAX\_PER\_PAGE\` and \`MAX\_PAGINATION\_PAGES\` — never use the unpaginated \`listRepositories\` for cache-backed lookups (silently caps at ~25). \`listRepositoriesCached(org)\` wraps it with cache-first lookup and a try/catch around \`setCachedRepos\` so read-only databases (macOS \`sudo brew install\`) don't crash commands whose API fetch already succeeded. Used by \`@commit\` resolver to match git origin \`owner/repo\` against Sentry repo \`externalSlug\` or \`name\`. - - -* **Response cache hit invisibility — synthetic Response carries no marker**: Response cache hit invisibility — synthetic Response from \`getCachedResponse()\` in \`src/lib/response-cache.ts\` is indistinguishable from network. Solved via module-level \`lastCacheHitAgeMs\`: set on hit, cleared at top of \`authenticatedFetch()\` per-call (single-process CLI = race-free). \`src/lib/cache-hint.ts\` provides \`formatCacheHint()\` (\`"cached · 3m ago · use -f to refresh"\`) and \`appendCacheHint(existingHint)\` (joins with \` | \`). Wired in \`buildCommand\` (\`src/lib/command.ts\`): \`appendCacheHint(returned?.hint)\` runs only when generator returns a \`CommandReturn\` — bare \`return;\` paths (e.g. \`--web\`) skip the hint. Same chokepoint can host future cross-cutting hint decorators. Test-only \`\_setLastCacheHitAgeForTesting(ms)\` exposes state. - - -* **Seer trial prompt uses middleware layering in bin.ts error handling chain**: Seer trial prompt via error middleware layering: \`bin.ts\` chain is \`main() → executeWithAutoAuth() → executeWithSeerTrialPrompt() → runCommand()\`. Seer trial prompts (\`no\_budget\`/\`not\_enabled\`) caught by inner wrapper; auth errors bubble to outer. Trial API: \`GET /api/0/customers/{org}/\` → \`productTrials\[]\` (prefer \`seerUsers\`, fallback \`seerAutofix\`). Start: \`PUT /api/0/customers/{org}/product-trial/\`. SaaS-only; self-hosted 404s gracefully. \`ai\_disabled\` excluded. \`startSeerTrial\` accepts \`category\` from trial object — don't hardcode. - -### Decision - - -* **Raw markdown output for non-interactive terminals, rendered for TTY**: Markdown-first output pipeline: custom renderer in \`src/lib/formatters/markdown.ts\` walks \`marked\` tokens to produce ANSI-styled output. Commands build CommonMark using helpers (\`mdKvTable()\`, \`mdRow()\`, \`colorTag()\`, \`escapeMarkdownCell()\`, \`safeCodeSpan()\`) and pass through \`renderMarkdown()\`. \`isPlainOutput()\` precedence: \`SENTRY\_PLAIN\_OUTPUT\` > \`NO\_COLOR\` > \`FORCE\_COLOR\` > \`!isTTY\`. \`--json\` always outputs JSON. Colors defined in \`COLORS\` object in \`colors.ts\`. Tests run non-TTY so assertions match raw CommonMark; use \`stripAnsi()\` helper for rendered-mode assertions. - -### Gotcha - - -* **--json schema stability: collapse=organization drops nested org fields**: --json schema + response cache gotchas: (1) \`?collapse=organization\` shrinks \`organization\` to \`{id, slug}\` — silent --json regression. \`jsonTransform\` re-hydrates \`organization.name\` via \`resolveOrgDisplayName\` against \`org\_regions\` cache. (2) \`buildCacheKey()\` normalizes URL with sorted query params, so \`invalidateCachedResponse(baseUrl)\` misses entries with query suffixes. Use \`invalidateCachedResponsesMatching(prefix)\` (raw \`startsWith()\`); \`buildApiUrl()\` always emits trailing slash → safe prefix. (3) When \`jsonTransform\` is set, \`jsonExclude\` and \`filterFields\` are NOT applied — transform must call \`filterFields(result, fields)\` and omit excluded keys itself. - - -* **@sentry/api SDK passes Request object to custom fetch — headers lost on Node.js**: @sentry/api SDK calls \`\_fetch(request)\` with no init object. In \`authenticatedFetch\`, \`init\` is undefined → \`prepareHeaders\` creates empty headers, stripping Content-Type on Node.js (HTTP 415). Fix: fall back to \`input.headers\` when \`init\` is undefined. Use \`unwrapPaginatedResult\` (not \`unwrapResult\`) to access Link header for pagination. \`per\_page\` not in SDK types — cast query at runtime. SDK returns \`data={}\` (not \`\[]\`) for empty/204/missing Content-Type responses — always guard with \`Array.isArray(data)\` before \`.map()\`. Self-hosted instances behind reverse proxies commonly trigger this. - - -* **API tests must use useTestConfigDir to isolate disk response cache**: API tests that mock \`globalThis.fetch\` MUST call \`useTestConfigDir()\` from \`test/helpers.ts\` + \`setAuthToken()\`. The \`authenticatedFetch\` singleton in \`src/lib/sentry-client.ts\` checks a filesystem-based response cache (\`~/.sentry/cache/responses/\`, see \`response-cache.ts\`) BEFORE calling fetch. Without per-test config dirs, test N's API response gets cached to disk and served to test N+1 — fetch mock never fires, assertion sees stale data. TTL tiers in \`classifyUrl()\`: stable=5min (default), volatile=60s (issues, logs), immutable=24h (events/traces by ID). Symptom: test expects fresh mock value, receives prior test's value. Reference: \`test/lib/api/issues.test.ts\` (correct pattern), \`test/lib/api/repositories.test.ts\` regression fixed by adding \`useTestConfigDir("repo-cache-")\` + \`setAuthToken("test-token", 3600, "test-refresh")\` in beforeEach. - - -* **Biome noUselessUndefined also rejects () => {} empty arrow callbacks**: Biome lint traps: (1) \`noUselessUndefined\` rejects \`() => undefined\` AND \`noEmptyBlockStatements\` rejects \`() => {}\` — use top-level \`function noop(): void {}\`. (2) \`noExcessiveCognitiveComplexity\` caps at 15. (3) \`expect(() => fn()).toThrow(X)\` must be one line. (4) Plugin forbids raw \`metadata\` table queries — use \`getMetadata\`/\`setMetadata\`/\`clearMetadata\`. (5) Also enforced: \`useBlockStatements\`, \`noNestedTernary\`, \`useAtIndex\`, \`noStaticOnlyClass\`, \`useSimplifiedLogicExpression\`, \`noShadow\`. Namespace imports forbidden. (6) \`useYield\` fires on \`async \*func()\` with statements but not empty bodies — only add \`biome-ignore\` to generators with statements. \`lint:fix\` differs from CI \`lint\`: auto-fix hides \`noPrecisionLoss\` on >2^53 literals, \`noIncrementDecrement\`, import ordering. Always \`bun run lint\` before pushing. - - -* **Bun --isolate coverage inflates LF count for files with verbose comments/JSDoc**: Bun --isolate coverage inflates LF count: under \`bun test --isolate --parallel\` (CI's \`test:unit\`), Bun's coverage instrumentation counts comments, blank lines, type annotations, and closing braces as 'executable'. E.g. \`zstd-transport.ts\` LF=165 locally → 210 under --isolate, dropping coverage 99%→78%. Codecov sees inflated number. Workaround: trim verbose inline comments inside function bodies (move rationale to JSDoc above function or module-level doc). Statement coverage stays 100% — 'missing' lines are non-executable. - - -* **Bun 1.3.11 tty.ReadStream leaks libuv handle — process.stdin.unref is undefined**: Bun 1.3.11 macOS TTY bug: \`process.stdin\` via kqueue \`EVFILT\_READ\` on reopened non-session-leader TTY fd fails to deliver keystrokes when fd 0 inherited via \`exec bin \ -* **MastraClient has no dispose API — use AbortController for cleanup**: MastraClient has no \`close()\`/\`dispose()\` API — cleanup via \`ClientOptions.abortSignal\` (constructor) or per-prompt \`signal\`. Without explicit abort, Bun's fetch dispatcher keep-alive sockets hold the event loop alive past natural exit. Pattern in \`src/lib/init/wizard-runner.ts\`: create \`AbortController\` per \`runWizard\`, pass \`abortSignal: controller.signal\` to \`new MastraClient(...)\`, abort via \`using \_ = { \[Symbol.dispose]: () => controller.abort() }\`. Custom \`fetch\` wrapper must preserve \`init.signal\` via spread. Tests capture \`ClientOptions\` via \`spyOn(MastraClient.prototype, 'getWorkflow').mockImplementation(function() { capturedOpts.push(this.options); ... })\`. - - -* **Multi-region fan-out: distinguish all-403 from empty orgs with hasSuccessfulRegion flag**: In \`listOrganizationsUncached\` (\`src/lib/api/organizations.ts\`), \`Promise.allSettled\` collects multi-region results. Don't use \`flatResults.length === 0\` to detect all-regions-failed — a region returning 200 OK with zero orgs pushes nothing into \`flatResults\`. Track a \`hasSuccessfulRegion\` boolean on any \`"fulfilled"\` settlement. Only re-throw 403 \`ApiError\` when \`!hasSuccessfulRegion && lastScopeError\`. - -### Pattern - - -* **CLI-1D3 Windows download visibility race: poll statSync with exponential backoff**: Windows upgrade download visibility race (CLI-1D3): \`waitForBinaryVisible\` in \`src/lib/upgrade.ts\` polls \`statSync\` with exponential backoff (6 attempts, 5 sleeps: 100+200+400+800+1600 = 3.1s). Loop breaks BEFORE final sleep — \`VERIFY\_MAX\_ATTEMPTS=N\` yields N-1 sleeps (off-by-one trap). Covers Windows + Bun 1.3.9 race where \`Bun.file().writer().end()\` returns before OS surfaces file by path → opaque \`Executable not found in $PATH\` from \`Bun.spawn\`. Safety net \`isEnoentSpawnError()\` in \`src/commands/cli/upgrade.ts\` detects both \`code==='ENOENT'\` and Bun's path-string error → \`UpgradeError('execution\_failed')\`. Race-free delayed-write tests: writer must POLL until bad state exists THEN overwrite. - - -* **Cross-compile sentry-cli with patched Bun: drop compile.target to use selfExePath**: Cross-compile sentry-cli with patched Bun: \`Bun.build({compile})\` downloads stock Bun from npm when \`compile.target\` is set. Workaround in \`script/build.ts\`: omit \`target\` entirely so Bun hits \`isDefault()\` branch → uses \`selfExePath()\` = the running Bun as embed runtime. Only works when host OS/arch matches desired output. Escape hatch: place file at \`$CWD/bun-\-\-v\\` (e.g. \`bun-darwin-arm64-v1.3.13\`) picked up via \`bun.FD.cwd().existsAt(version\_str)\` in \`src/compile\_target.zig:exePath\`. Build also requires \`SENTRY\_CLIENT\_ID\` env var. - - -* **Dedupe resolved entity IDs in batch operations before API call**: Batch issue merge (src/commands/issue/merge.ts): (1) Dedupe by resolved numeric ID after \`Promise.all(args.map(resolveIssue))\`, not raw input (users pass same entity as \`CLI-K9\`, \`my-org/CLI-K9\`, \`123\`). Throw ValidationError if \`new Set(ids).size < 2\`. (2) Reject undefined orgs in cross-org check — bare numeric IDs without DSN/config resolve with \`org: undefined\`; filtering them out lets mixed-org merges slip through. (3) Pass \`--into\` through \`resolveIssue()\` for alias/org-qualified parity; compare by numeric \`id\`, not \`shortId\`. (4) Sentry bulk merge API picks canonical parent by event count — \`--into\` is preference only; warn when API's \`parent\` differs. Empty results return 204. - - -* **findProjectsByPattern as fuzzy fallback for exact slug misses**: When \`findProjectsBySlug\` returns empty (no exact match), use \`findProjectsByPattern\` as a fallback to suggest similar projects. \`findProjectsByPattern\` does bidirectional word-boundary matching (\`matchesWordBoundary\`) against all projects in all orgs — the same logic used for directory name inference. In the \`project-search\` handler, call it after the exact miss, format matches as \`\/\\` suggestions in the \`ResolutionError\`. This avoids a dead-end error for typos like 'patagonai' when 'patagon-ai' exists. Note: \`findProjectsByPattern\` makes additional API calls (lists all projects per org), so only call it on the failure path. - - -* **Grouped widget --limit auto-default via applyGroupLimitAutoDefault helper**: Dashboard widget flag normalization: (1) Dataset aliases (errors→error-events) normalize ONCE at top of \`func()\` via \`normalizeDataset()\` in \`src/commands/dashboard/resolve.ts\`. In \`edit.ts\`, pass \`normalizedFlags\` to \`buildReplacement\` — \`validateAggregateNames\` reads \`flags.dataset\` and rejects valid aggregates like \`failure\_rate\` if it sees raw alias. (2) Grouped widgets need \`limit\` (API rejects). \`applyGroupLimitAutoDefault\` defaults to \`DEFAULT\_GROUP\_BY\_LIMIT=5\` only when user passed \`--group-by\` without \`--limit\`; skip for auto-defaulted columns like \`\["issue"]\`. (3) Tests asserting \`--limit\` >10 survives into PUT body must use \`display: "line"\` — \`prepareWidgetQueries\` clamps bar/table to max=10. - - -* **Hidden --org/--project compat flags via mergeGlobalFlags**: Hidden global \`--org\`/\`--project\` flags accept old \`sentry-cli\` syntax. Defined in \`GLOBAL\_FLAGS\` (global-flags.ts) so argv-hoist relocates them. \`mergeGlobalFlags()\` in command.ts injects hidden flag shapes (skip if command owns the flag — e.g. \`release create --project -p\`) and returns \`stripKeys\` set used by \`cleanRawFlags\`. \`applyOrgProjectFlags()\` writes values to \`SENTRY\_ORG\`/\`SENTRY\_PROJECT\` via \`getEnv()\` before auth guard, overwriting existing env vars (explicit CLI > env var). Resolution chain in resolve-target.ts picks them up at priority #2. No short aliases (\`-p\` conflicts). The helper extraction was needed to keep \`buildCommand\` under Biome's cognitive complexity limit of 15. - - -* **Preserve ApiError type so classifySilenced can silence 4xx errors**: Preserve ApiError type for classifySilenced: \`classifySilenced\` (src/lib/error-reporting.ts) only silences \`ApiError\` with status 401-499 — wrapping in generic \`CliError\` loses \`status\` and causes 403s to be captured. Re-throw via \`new ApiError(msg, error.status, error.detail, error.endpoint)\` with terse message (\`ApiError.format()\` appends detail/endpoint). \`ValidationError\` without \`field\` collapses unfielded errors into one fingerprint; always pass \`field\`. Fingerprint rule changes don't retroactively re-fingerprint — manually merge new groups into canonical old parents. \`ApiError\` rule keys by \`api\_status + command\`. - - -* **Sentry SDK tree-shaking patches must be regenerated via bun patch workflow**: Sentry SDK tree-shaking via bun patch: \`patchedDependencies\` in \`package.json\` strips unused exports from \`@sentry/core\` and \`@sentry/node-core\`. Non-light root of \`@sentry/node-core\` pulls uninstalled \`@opentelemetry/instrumentation\` — \*\*always import from \`@sentry/node-core/light\`\*\* (subpaths: \`.\`, \`./light\`, \`./light/otlp\`, \`./init\`, \`./loader\`, \`./import\`). No supported import for \`HttpsProxyAgent\`. Bumping SDK: remove old patches, \`rm -rf ~/.bun/install/cache/@sentry\`, \`bun install\`, \`bun patch @sentry/core\`, edit, \`bun patch --commit\`; repeat for node-core. Preserved: \`\_INTERNAL\_safeUnref\`, \`\_INTERNAL\_safeDateNow\`, \`nodeRuntimeMetricsIntegration\`. Before stripping any core export, grep \`node-core/build/{cjs,esm}/light/sdk.js\` for runtime usage (e.g. \`spanStreamingIntegration\` when \`traceLifecycle === 'stream'\`). Remove \`.bun-tag-\*\` hunks from generated patches. Manual \`git diff\` patches fail. - - -* **Shared pagination infrastructure: buildPaginationContextKey and parseCursorFlag**: Bidirectional pagination via cursor stack in \`src/lib/db/pagination.ts\`. \`resolveCursor(flag, key, contextKey)\` maps keywords (next/prev/first/last) to \`{cursor, direction}\`. \`advancePaginationState\` manages stack — back-then-forward truncates stale entries. \`hasPreviousPage\` checks \`page\_index > 0\`. \`paginationHint()\` builds nav strings. All list commands use this. Critical: \`resolveCursor()\` must be called inside \`org-all\` override closures, not before \`dispatchOrgScopedList\`. - - -* **Telemetry instrumentation pattern: withTracingSpan + captureException for handled errors**: For graceful-fallback operations, use \`withTracingSpan\` from \`src/lib/telemetry.ts\` for child spans and \`captureException\` from \`@sentry/bun\` (named import — Biome forbids namespace imports) with \`level: 'warning'\` for non-fatal errors. \`withTracingSpan\` uses \`onlyIfParent: true\` — no-op without active transaction. User-visible fallbacks use \`log.warn()\` not \`log.debug()\`. Several commands bypass telemetry by importing \`buildCommand\` from \`@stricli/core\` directly instead of \`../../lib/command.js\` (trace/list, trace/view, log/view, api.ts, help.ts). - - -* **Testing Stricli command func() bodies via spyOn mocking**: Testing Stricli command func() bodies: (1) \`const func = await cmd.loader(); func.call(mockContext, flags, ...args)\` with mock \`stdout\`, \`stderr\`, \`cwd\`, \`setContext\`. \`loader()\` return type union causes \`.call()\` LSP false-positives that pass \`tsc --noEmit\`. (2) When API functions are renamed, update both spy target AND mock return shape. (3) \`normalizeSlug\` replaces \`\_\`→\`-\` but does NOT lowercase. (4) Bun \`mockFetch()\` replaces \`globalThis.fetch\` — use one unified mock dispatching by URL. (5) \`mock.module()\` pollutes module registry for ALL subsequent files — put in \`test/isolated/\` and run via \`test:isolated\`. (6) For \`Bun.spawn\`, use direct property assignment in \`beforeEach\`/\`afterEach\`. - -### Preference - - -* **Bot review triage: distinguish real bugs from SDK-mirroring false positives**: When Sentry Seer or Cursor Bugbot flags 'unusual' code that intentionally mirrors upstream SDK behavior (e.g., \`http\_proxy\` as last-resort fallback for HTTPS URLs — deliberate in \`@sentry/node-core\` \`applyNoProxyOption\`), decline with a written rationale referencing the SDK source rather than silently changing behavior. Removing the mirror creates a divergence where users get different proxy semantics from our transport vs. the SDK default. BYK's pattern: verify against \`node\_modules/@sentry/node-core/build/esm/transports/http.js\`, post a reply explaining the precedent, and resolve the thread. Real bugs (uppercase env var support, whitespace trimming, wildcard handling) get fixed; SDK-mirroring 'bugs' get explained and dismissed. - diff --git a/biome.jsonc b/biome.jsonc index 1cb2f9146..326b0a857 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -7,7 +7,9 @@ "./lint-rules/no-raw-metadata-queries.grit", "./lint-rules/no-manual-transactions.grit", "./lint-rules/no-inline-touch-cache.grit", - "./lint-rules/no-stderr-write-in-commands.grit" + "./lint-rules/no-stderr-write-in-commands.grit", + "./lint-rules/no-wrapper-owned-command-flags.grit", + "./lint-rules/delete-command-wrapper.grit" ], "files": { "includes": ["!docs", "!test/init-eval/templates"] @@ -112,8 +114,7 @@ "options": { "paths": { "@stricli/core": { - "importNames": ["buildCommand", "buildRouteMap"], - "message": "Import buildCommand from '../lib/command.js' and buildRouteMap from '../lib/route-map.js' instead. The wrappers inject telemetry and standard subcommand aliases." + "message": "Command files must use the repo command wrappers from lib/command.js, lib/list-command.js, lib/mutate-command.js, and lib/route-map.js instead of importing @stricli/core directly." }, "chalk": { "message": "Use colorTag() from formatters/markdown.js for colored output. Raw chalk bypasses the plain-output pipeline (NO_COLOR, SENTRY_PLAIN_OUTPUT)." diff --git a/lint-rules/delete-command-wrapper.grit b/lint-rules/delete-command-wrapper.grit new file mode 100644 index 000000000..d6d7270e4 --- /dev/null +++ b/lint-rules/delete-command-wrapper.grit @@ -0,0 +1,6 @@ +file($name, $body) where { + $name <: r".*src/commands/.*/delete\.ts$", + $body <: contains `buildCommand($args)` as $match, + $body <: not contains `buildDeleteCommand($deleteArgs)`, + register_diagnostic(span=$match, message="Delete command files must use buildDeleteCommand() from src/lib/mutate-command.ts.") +} diff --git a/lint-rules/no-wrapper-owned-command-flags.grit b/lint-rules/no-wrapper-owned-command-flags.grit new file mode 100644 index 000000000..a693e0971 --- /dev/null +++ b/lint-rules/no-wrapper-owned-command-flags.grit @@ -0,0 +1,10 @@ +file($name, $body) where { + $name <: r".*src/commands/.*", + $body <: contains or { + `json: { kind: $kind, $rest }` as $match, + `fields: { kind: $kind, $rest }` as $match, + `"json": { kind: $kind, $rest }` as $match, + `"fields": { kind: $kind, $rest }` as $match + }, + register_diagnostic(span=$match, message="Do not define --json or --fields in command flags. The command wrappers inject them.") +} diff --git a/playbooks/README.md b/playbooks/README.md new file mode 100644 index 000000000..bc56cdba4 --- /dev/null +++ b/playbooks/README.md @@ -0,0 +1,16 @@ +# Playbooks + +Playbooks are repeatable workflows. + +Use a playbook when the same task needs commands, ordering, checks, or known failure modes. + +## Rules +- Keep steps executable and short. +- Include exact commands. +- State expected results only when they guide debugging. +- Link policies instead of restating them. + +## Current Playbooks +| File | Purpose | +|------|---------| +| `local-cli-testing.md` | Run and smoke-test the CLI locally | diff --git a/playbooks/local-cli-testing.md b/playbooks/local-cli-testing.md new file mode 100644 index 000000000..64d63ed1b --- /dev/null +++ b/playbooks/local-cli-testing.md @@ -0,0 +1,59 @@ +# Local CLI Testing + +## Goal +Run the CLI locally with the smallest command that exercises the changed behavior. + +## Fast Path +```bash +bun run --env-file=.env.local src/bin.ts +``` + +Examples: +```bash +bun run --env-file=.env.local src/bin.ts auth whoami +bun run --env-file=.env.local src/bin.ts issue list / --limit 5 +bun run --env-file=.env.local src/bin.ts trace view --json +``` + +## Dev Script +Use this when generated docs/schema/sdk must be refreshed first: + +```bash +bun run dev -- +``` + +## Built Binary Smoke Test +Use this for packaging, startup, or Node-distribution-sensitive changes: + +```bash +bun run build +./dist/bin.cjs +``` + +## Auth And Env +- Put local OAuth client config in `.env.local`. +- Use `SENTRY_AUTH_TOKEN` for non-interactive API smoke tests. +- Use `SENTRY_HOST` for self-hosted testing. +- Use `SENTRY_LOG_LEVEL=debug` when checking diagnostics. + +## Output Checks +```bash +bun run --env-file=.env.local src/bin.ts --json +bun run --env-file=.env.local src/bin.ts --fields id,slug --json +SENTRY_PLAIN_OUTPUT=1 bun run --env-file=.env.local src/bin.ts +``` + +## Test Selection +| Change | Start With | +|--------|------------| +| Command parser or output | `bun test test/commands//.test.ts --timeout 15000 --isolate` | +| Shared lib | `bun test test/lib/.test.ts --timeout 15000 --isolate` | +| Parsing/validation invariant | matching `*.property.test.ts` | +| DB/cache behavior | matching `*.model-based.test.ts` | +| Full CLI behavior | `bun run test:e2e` | + +## Final Checks +```bash +bun run typecheck +bun run lint +``` diff --git a/policies/README.md b/policies/README.md new file mode 100644 index 000000000..362dbe495 --- /dev/null +++ b/policies/README.md @@ -0,0 +1,26 @@ +# Policies + +Policies are short repo-wide defaults. + +Use a policy when the repo needs a durable "how we do this here" rule without a full design doc or workflow. + +## Rules +- Keep policy docs small: intent, default rule, meaningful exceptions. +- Prefer bullets, tables, and examples over paragraphs. +- Link to implementation files instead of copying large code samples. +- Move repeatable procedures to `playbooks/`. +- Move proposed designs, tradeoffs, and migration plans to `specs/`. +- Update `AGENTS.md` when a policy becomes broadly required reading. + +## Current Policies +| File | Scope | +|------|-------| +| `TEMPLATE.md` | Copy this shape for new policy docs | +| `code-comments.md` | Comments, docstrings, and JSDoc | +| `runtime-and-deps.md` | Bun APIs, dependency rules, Node distribution exceptions | +| `cli-command-design.md` | Command, route, and mutation command conventions | +| `output-and-errors.md` | Human output, JSON output, errors, logging | +| `pagination.md` | Cursor-stack pagination for list commands | +| `testing.md` | Test style, isolation, property/model-based tests | +| `generated-artifacts.md` | Generated docs, skills, fragments, schemas | +| `implementation-notes.md` | Conditional edge-case notes for specific domains | diff --git a/policies/TEMPLATE.md b/policies/TEMPLATE.md new file mode 100644 index 000000000..cf1497296 --- /dev/null +++ b/policies/TEMPLATE.md @@ -0,0 +1,14 @@ +# Policy Title + +## Intent + +One short paragraph on why this policy exists. + +## Policy + +- Default rule +- Second rule if needed + +## Exceptions + +- Only list real exceptions diff --git a/policies/cli-command-design.md b/policies/cli-command-design.md new file mode 100644 index 000000000..e19af071f --- /dev/null +++ b/policies/cli-command-design.md @@ -0,0 +1,35 @@ +# CLI Command Design + +## Intent +Commands should follow `gh`-style ergonomics while preserving consistent telemetry, JSON output, and error handling. + +## Command Wrappers +- Import `buildCommand` from `src/lib/command.ts`. +- Import `buildRouteMap` from `src/lib/route-map.ts`. +- Do not import either wrapper directly from `@stricli/core`. +- The wrappers inject telemetry, `--json`, `--fields`, output rendering, and standard route aliases. + +## Command Shape +- Command `func` implementations are `async *` generators. +- Yield `new CommandOutput(data)` for data output. +- Return `{ hint }` for follow-up guidance. +- Keep command files focused on argument parsing, API orchestration, and output dispatch. +- Put rendering logic in `src/lib/formatters/.ts`. +- Avoid command files over roughly 400 lines; extract helpers when command logic stops being scannable. + +## Arguments And Routes +- Required identifiers are positional args, not flags. +- Use `parseSlashSeparatedArg` and `parseOrgProjectArg` for `[//]`. +- Use `"date"` for timestamp sort values, not `"time"`. +- Route aliases for `list`, `view`, `delete`, and `create` are auto-injected by `buildRouteMap`. + +## Mutations +- Delete commands use `buildDeleteCommand()` from `src/lib/mutate-command.ts`. +- Create commands reuse `DRY_RUN_FLAG` and `DRY_RUN_ALIASES` when dry-run is supported. +- Destructive commands require explicit targets unless a helper policy says otherwise. + +## Shared Helpers +- Check existing `src/lib/*` modules before adding new utilities. +- Extend an existing helper when it covers most of the need. +- Use `upsert()` / `runUpsert()` from `src/lib/db/utils.ts` for SQLite UPSERTs. +- Use shared hex validators from `src/lib/hex-id.ts` and `src/lib/trace-id.ts`. diff --git a/policies/code-comments.md b/policies/code-comments.md new file mode 100644 index 000000000..ba38bd2d1 --- /dev/null +++ b/policies/code-comments.md @@ -0,0 +1,19 @@ +# Code Comments + +## Intent +Comments are for non-obvious intent, invariants, policy decisions, and tradeoffs. + +They are not a narration layer for obvious code. + +## Policy +- Add comments when behavior is easy to misread, policy-driven, or tied to a non-obvious invariant. +- Exported functions, classes, and types must have brief JSDoc explaining intent. +- Document fields when units, nullability, defaults, or allowed values are not obvious from the type. +- Prefer short JSDoc on tricky local helpers when future readers need context to change them safely. +- Keep comments concrete. Explain why the code exists or what boundary it protects. +- Delete or rewrite stale comments in the same change that alters behavior. + +## Exceptions +- Do not comment obvious transformations or control flow. +- Do not restate code in English. +- Do not add decorative ASCII or box-drawing section dividers. diff --git a/policies/generated-artifacts.md b/policies/generated-artifacts.md new file mode 100644 index 000000000..df35814a1 --- /dev/null +++ b/policies/generated-artifacts.md @@ -0,0 +1,14 @@ +# Generated Artifacts + +## Intent +Generated docs, schemas, and plugin skill files should be updated through the existing generators. + +## Rules +- `bun run generate:docs` runs command docs, parser generation, skill generation, and docs sections. +- `dev`, `build`, `typecheck`, and test scripts run the relevant generators automatically. +- Command reference docs under `docs/src/content/docs/commands/*.md` are generated and gitignored. +- Edit custom command docs in `docs/src/fragments/commands/`. +- Run `bun run check:fragments` after fragment or route changes. +- Skill files under `plugins/sentry-cli/skills/sentry-cli/` are generated and committed. +- Positional placeholders must be descriptive, such as `org/project/trace-id`, not `args`. +- API schema changes should use `bun run generate:schema`. diff --git a/policies/implementation-notes.md b/policies/implementation-notes.md new file mode 100644 index 000000000..6680a8fcf --- /dev/null +++ b/policies/implementation-notes.md @@ -0,0 +1,22 @@ +# Implementation Notes + +Load this only when touching one of the listed areas. These notes capture edge +cases that are too specific for always-loaded agent instructions. + +## Domain Notes +- Issue flows: `issue resolve --in` accepts versions, `@next`, `@commit`, and `@commit:@`; split explicit commit specs on the last `@` and send `{commit, repository}` to the API. For issue merge, dedupe by resolved numeric IDs, reject unresolved orgs in cross-org checks, and treat `--into` as a preference. +- Repository lookup: `repo_cache` backs offline Sentry repo matching for `@commit`; use paginated `listAllRepositories()` and tolerate read-only cache writes. +- Response cache: cached synthetic `Response` objects carry no marker; `authenticatedFetch()` owns `lastCacheHitAgeMs`, and `buildCommand()` appends cache hints only when a command returns a `CommandReturn`. +- JSON and markdown output: `collapse=organization` can drop nested org fields, so `jsonTransform` must rehydrate needed fields, apply `filterFields()`, and handle exclusions itself. Tests run non-TTY and usually assert raw CommonMark. +- API SDK fetch: `@sentry/api` may pass a `Request` without `init`; preserve request headers, use `unwrapPaginatedResult()` when headers matter, and guard empty responses with `Array.isArray()`. +- API and command tests: API tests that mock `globalThis.fetch` need `useTestConfigDir()` plus `setAuthToken()`. Test command `func` bodies via `await cmd.loader()` and `.call(mockContext, flags, ...args)`; keep `mock.module()` pollution in isolated test files. +- Multi-region orgs: in `listOrganizationsUncached()`, track any fulfilled region separately from result count so empty 200s are not treated as all-region 403 failures. +- Seer and init: `bin.ts` layers auth outside Seer trial prompting; trial start uses the server-provided category and treats self-hosted 404s gracefully. `MastraClient` has no dispose API; pass and abort an `AbortController`, and preserve `init.signal` in custom fetch wrappers. +- TTY and upgrade/build: macOS Bun TTY reopening uses `/dev/tty` plus `tty.ReadStream`; keep the explicit exit safety net and skip it under `NODE_ENV=test`. Windows upgrade verification polls file visibility before spawn. Patched Bun cross-compile omits `compile.target` and requires `SENTRY_CLIENT_ID`. +- npm/node distribution: `dist/bin.cjs` requires Node.js >= 22 because the SQLite polyfill uses `node:sqlite`; double-escape newline continuations in esbuild banner template strings. +- SDK tree-shaking: Sentry SDK patches come from `bun patch`; import `@sentry/node-core/light` subpaths and regenerate patches instead of hand-editing diffs. +- Lint and coverage traps: use named imports instead of namespace imports, define top-level `noop()` helpers instead of empty arrows, and run `bun run lint` after `lint:fix`. Bun `--isolate --parallel` coverage may count comments, type lines, and braces. +- Hidden globals: hidden `--org` / `--project` compatibility flags are merged in `buildCommand()` and applied before auth; no short aliases because `-p` conflicts. +- Error reporting and telemetry: preserve `ApiError` status when rethrowing so 4xx API errors stay silenced; pass `field` to `ValidationError`. Graceful fallbacks should use `withTracingSpan()` plus named `captureException` imports at warning level; user-visible fallbacks use `log.warn()`. +- Dashboard and project lookup: normalize dashboard dataset aliases once, pass normalized flags through replacement builders, and use grouped-widget limit auto-defaulting only when the user omitted `--limit`. On exact project slug misses, `findProjectsByPattern()` is the failure-path suggestion mechanism. +- Bot review triage: when bot feedback conflicts with mirrored upstream SDK behavior, verify against the SDK source and explain the precedent instead of diverging silently. diff --git a/policies/output-and-errors.md b/policies/output-and-errors.md new file mode 100644 index 000000000..ef1c6c5f0 --- /dev/null +++ b/policies/output-and-errors.md @@ -0,0 +1,27 @@ +# Output And Errors + +## Human Output +- Non-trivial human output goes through the markdown rendering pipeline. +- Build markdown with helpers such as `mdKvTable()`, `colorTag()`, `escapeMarkdownCell()`, and `renderMarkdown()`. +- Do not put raw `muted()` or chalk calls inside output strings. +- Tree output that cannot go through markdown should use the plain-safe muted pattern. + +## JSON Output +- The command wrapper owns `--json` and `--fields`. +- `output.human` receives the same data object that JSON serialization receives. +- Do not branch on `flags.json` inside command bodies. +- List JSON envelopes preserve pagination fields such as `hasMore`, `hasPrev`, and `nextCursor`. + +## Errors +- All CLI errors extend `CliError` from `src/lib/errors.ts`. +- Use `ContextError` when the user omitted required context. +- Use `ResolutionError` when a provided value was not found. +- Use `ValidationError` when input is malformed. +- Use `EXIT.*` constants in error definitions; do not hardcode exit codes elsewhere. +- `ContextError.command` must be one single-line usage example. + +## Diagnostics +- Production `catch` blocks must log, rethrow, or explain the fallback with `log.debug()` / `log.warn()`. +- Use `logger.withTag("command-name")` in command files. +- Use fuzzy suggestions for user-provided names when listing every candidate would be noisy. +- Auto-recover wrong entity types when intent is unambiguous; warn and return a hint. diff --git a/policies/pagination.md b/policies/pagination.md new file mode 100644 index 000000000..29dd39648 --- /dev/null +++ b/policies/pagination.md @@ -0,0 +1,28 @@ +# Pagination + +## Intent +List commands use one cursor-stack model so `-c next`, `-c prev`, and JSON pagination stay consistent. + +## Required Helpers +- `LIST_CURSOR_FLAG` from `src/lib/list-command.ts` +- `buildPaginationContextKey()` from `src/lib/db/pagination.ts` +- `resolveCursor()` from `src/lib/db/pagination.ts` +- `advancePaginationState()` from `src/lib/db/pagination.ts` +- `hasPreviousPage()` from `src/lib/db/pagination.ts` +- `paginationHint()` from `src/lib/list-command.ts` + +## Rules +- Use `-c` as the `cursor` alias. +- Include `hasPrev`, `hasMore`, and `nextCursor` where applicable in JSON envelopes. +- Build navigation hints with `paginationHint()`, not manual string arrays. +- Treat `"last"` as the existing silent alias for `"next"`. +- Call `resolveCursor()` inside mode-specific handlers when cursor support is mode-specific. +- Never pass `per_page` larger than `API_MAX_PER_PAGE`. +- When `--limit` exceeds `API_MAX_PER_PAGE`, fetch multiple pages until the limit is filled or pages run out. + +## Abstraction Choice +| Use | When | +|-----|------| +| `buildOrgListCommand` | Simple org-scoped lists such as teams or repos | +| `dispatchOrgScopedList` | Commands with custom auto-detect, org-all, or project-search behavior | +| Manual cursor wiring | Standalone list commands such as traces, spans, dashboards, replays, or explore | diff --git a/policies/runtime-and-deps.md b/policies/runtime-and-deps.md new file mode 100644 index 000000000..f85479cd1 --- /dev/null +++ b/policies/runtime-and-deps.md @@ -0,0 +1,28 @@ +# Runtime And Dependencies + +## Intent +The CLI ships as bundled Bun binaries and an npm/node distribution. Runtime choices must work in both paths. + +## Dependency Policy +- All packages go in `devDependencies`, never `dependencies`. +- Add packages with `bun add -d `. +- Run `bun run check:deps` after dependency changes. +- Use types from `@sentry/api` when the SDK exposes the API response shape. + +## Bun APIs +| Task | Prefer | Avoid | +|------|--------|-------| +| Read file | `await Bun.file(path).text()` | `fs.readFileSync()` | +| Write file | `await Bun.write(path, content)` | `fs.writeFileSync()` | +| Check file exists | `await Bun.file(path).exists()` | `fs.existsSync()` | +| Spawn process | `Bun.spawn()` | `child_process.spawn()` | +| Find executable | `Bun.which("git")` | `which` package | +| Glob files | `new Bun.Glob()` | `glob` / `fast-glob` | +| Sleep | `await Bun.sleep(ms)` | Promise-wrapped `setTimeout` | +| Parse JSON file | `await Bun.file(path).json()` | read + `JSON.parse` | + +## Exceptions +- Use `node:fs` for directory creation that needs permissions: `mkdirSync(dir, { recursive: true, mode: 0o700 })`. +- Do not use `Bun.$` in code that must run in the npm/node distribution; `script/node-polyfills.ts` does not shim it. +- For shell commands that must work under both runtimes, use `execSync` from `node:child_process`. +- `Bun.$` is acceptable in Bun-only scripts. diff --git a/policies/testing.md b/policies/testing.md new file mode 100644 index 000000000..4fade0bf4 --- /dev/null +++ b/policies/testing.md @@ -0,0 +1,32 @@ +# Testing + +## Intent +Tests should be isolated, targeted, and biased toward generated coverage for invariant-heavy logic. + +## Commands +| Task | Command | +|------|---------| +| Single file | `bun test path/to/file.test.ts --timeout 15000 --isolate` | +| Unit suite | `bun run test:unit` | +| E2E suite | `bun run test:e2e` | +| Changed tests | `bun run test:changed` | + +## Test Types +| Type | Pattern | Use For | +|------|---------|---------| +| Unit | `*.test.ts` | Specific outputs, formatting, integration edges | +| Property | `*.property.test.ts` | Parsing, validation, transforms, invariants | +| Model-based | `*.model-based.test.ts` | DB state, caches, state machines | +| E2E | `test/e2e/*.test.ts` | Full CLI behavior | + +## Isolation +- Tests that touch config dirs, auth, SQLite, or response cache use `useTestConfigDir()` from `test/helpers.ts`. +- Do not manually delete `process.env.SENTRY_CONFIG_DIR` in tests. +- Do not capture config-dir env values at module scope. +- API tests that mock `globalThis.fetch` also need isolated config and an auth token. + +## Property And Model Tests +- Prefer property tests for reusable pure logic with clear invariants. +- Prefer model-based tests for stateful systems. +- Use `DEFAULT_NUM_RUNS` from `test/model-based/helpers.ts`. +- Do not duplicate invariants in unit tests when a property test already covers them. diff --git a/specs/README.md b/specs/README.md new file mode 100644 index 000000000..188b982e5 --- /dev/null +++ b/specs/README.md @@ -0,0 +1,28 @@ +# Specs + +Specs are short design notes for material changes. + +Use a spec when work needs a proposed shape, tradeoffs, migration steps, or open questions before code changes. + +## Rules +- One spec per feature, migration, or decision. +- Start with status: `draft`, `accepted`, `implemented`, or `superseded`. +- Prefer bullets and tables. +- Link related policies, playbooks, issues, and PRs. +- Move repeatable commands to `playbooks/`. + +## Template +```markdown +# Title + +Status: draft + +## Goal +- ... + +## Plan +- ... + +## Open Questions +- ... +```