From 51dc54947e025d97d0063a07923f769d5b1d9aa4 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Fri, 28 Aug 2026 17:56:20 +0000 Subject: [PATCH 01/17] feat(config): follow XDG Base Directory spec for config location Resolve the config/data directory via the XDG Base Directory specification instead of always using `~/.sentry`. Precedence: 1. `SENTRY_CONFIG_DIR` override (unchanged, highest priority) 2. Legacy `~/.sentry` when it already exists (no breakage for existing installs) 3. `/sentry`, defaulting to `~/.config/sentry` A non-absolute `XDG_CONFIG_HOME` is ignored per the spec. This keeps credentials and caches out of the home directory root, which also unblocks environments (e.g. coding agents) that restrict writes to `/root`. Fixes #1502 --- .../src/content/docs/getting-started.mdx | 7 +-- apps/cli-docs/src/fragments/configuration.md | 2 +- packages/cli/DEVELOPMENT.md | 2 +- packages/cli/README.md | 2 +- packages/cli/src/lib/db/index.ts | 46 +++++++++++++--- packages/cli/test/lib/config.test.ts | 54 ++++++++++++++++++- 6 files changed, 99 insertions(+), 14 deletions(-) diff --git a/apps/cli-docs/src/content/docs/getting-started.mdx b/apps/cli-docs/src/content/docs/getting-started.mdx index 101bb2fe61..6449b76016 100644 --- a/apps/cli-docs/src/content/docs/getting-started.mdx +++ b/apps/cli-docs/src/content/docs/getting-started.mdx @@ -112,8 +112,9 @@ sentry auth You'll be given a URL and a code to enter. Once you authorize the application in your browser, the CLI stores the OAuth credentials. When the server provides a refresh token, the CLI refreshes the access token automatically. Persist the -Sentry CLI configuration directory (`~/.sentry/` by default, overridable with -`SENTRY_CONFIG_DIR`) across runs to keep automatic refresh working. +Sentry CLI configuration directory (`$XDG_CONFIG_HOME/sentry/`, defaulting to +`~/.config/sentry/`, overridable with `SENTRY_CONFIG_DIR`) across runs to keep +automatic refresh working. ### API Token @@ -156,7 +157,7 @@ See the [Self-Hosted](../self-hosted/) guide for full setup details. ## Configuration -Credentials are stored in a SQLite database at `~/.sentry/` with restricted file permissions (mode 600) for security. See [Configuration](../configuration/) for environment variables and customization options. +Credentials are stored in a SQLite database under `$XDG_CONFIG_HOME/sentry/` (defaulting to `~/.config/sentry/`) with restricted file permissions (mode 600) for security. See [Configuration](../configuration/) for environment variables and customization options. ## Next Steps diff --git a/apps/cli-docs/src/fragments/configuration.md b/apps/cli-docs/src/fragments/configuration.md index f28662cce0..41716d6dfd 100644 --- a/apps/cli-docs/src/fragments/configuration.md +++ b/apps/cli-docs/src/fragments/configuration.md @@ -103,7 +103,7 @@ The `sentry api` command also uses `--verbose` to show full HTTP request/respons ## Credential Storage -We store credentials and caches in a SQLite database (`cli.db`) inside the config directory (`~/.sentry/` by default, overridable via `SENTRY_CONFIG_DIR`). The database file and its WAL side-files are created with restricted permissions (mode 600) so that only the current user can read them. The database also caches: +We store credentials and caches in a SQLite database (`cli.db`) inside the config directory. The location follows the [XDG Base Directory specification](https://specifications.freedesktop.org/basedir/latest/): by default the CLI uses `$XDG_CONFIG_HOME/sentry` (i.e. `~/.config/sentry/` when `XDG_CONFIG_HOME` is unset), and you can override it with `SENTRY_CONFIG_DIR`. For backward compatibility, if a legacy `~/.sentry/` directory already exists it continues to be used. The database file and its WAL side-files are created with restricted permissions (mode 600) so that only the current user can read them. The database also caches: - Organization and project defaults - DSN resolution results diff --git a/packages/cli/DEVELOPMENT.md b/packages/cli/DEVELOPMENT.md index 4bc3f3a3d5..7e37772481 100644 --- a/packages/cli/DEVELOPMENT.md +++ b/packages/cli/DEVELOPMENT.md @@ -85,7 +85,7 @@ The table below lists the most common development variables. For the complete re | `SENTRY_HOST` | Sentry instance URL (for self-hosted, takes precedence) | `https://sentry.io` | | `SENTRY_URL` | Alias for `SENTRY_HOST` | `https://sentry.io` | | `SENTRY_CLIENT_ID` | Sentry OAuth app client ID | (required for build) | -| `SENTRY_CONFIG_DIR` | Override credentials/cache directory | `~/.sentry/` | +| `SENTRY_CONFIG_DIR` | Override credentials/cache directory | `$XDG_CONFIG_HOME/sentry/` (`~/.config/sentry/`), or legacy `~/.sentry/` if it exists | | `SENTRY_LOG_LEVEL` | Diagnostic log level (`error`, `warn`, `log`, `info`, `debug`, `trace`) | `info` | | `SENTRY_CLI_NO_TELEMETRY` | Disable CLI telemetry (error tracking) | — | diff --git a/packages/cli/README.md b/packages/cli/README.md index 515665c536..e3d41cce4f 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -78,7 +78,7 @@ Run `sentry --help` to see all available commands, or browse the [command refere ## Configuration -Credentials are stored in `~/.sentry/` with restricted permissions (mode 600). +Credentials are stored in `$XDG_CONFIG_HOME/sentry/` (defaulting to `~/.config/sentry/`) with restricted permissions (mode 600). A pre-existing legacy `~/.sentry/` directory is still honored, and the location can be overridden with `SENTRY_CONFIG_DIR`. ## Library Usage diff --git a/packages/cli/src/lib/db/index.ts b/packages/cli/src/lib/db/index.ts index 46cfc7ca1f..7818f8a713 100644 --- a/packages/cli/src/lib/db/index.ts +++ b/packages/cli/src/lib/db/index.ts @@ -4,10 +4,10 @@ * bundled WASM driver (`node-sqlite3-wasm`, Node < 22.15) behind one API. */ -import { chmodSync, mkdirSync } from "node:fs"; +import { chmodSync, existsSync, mkdirSync } from "node:fs"; import { createRequire } from "node:module"; import { homedir } from "node:os"; -import { join } from "node:path"; +import { isAbsolute, join } from "node:path"; import { getEnv } from "../env.js"; import { logger } from "../logger.js"; @@ -21,7 +21,11 @@ import { Database } from "./sqlite.js"; export const CONFIG_DIR_ENV_VAR = "SENTRY_CONFIG_DIR"; -const DEFAULT_CONFIG_DIR_NAME = ".sentry"; +/** Legacy config directory name under the user's home directory (`~/.sentry`). */ +const LEGACY_CONFIG_DIR_NAME = ".sentry"; + +/** Sub-directory used under the XDG config base directory. */ +const XDG_CONFIG_SUBDIR = "sentry"; const DB_FILENAME = "cli.db"; @@ -69,10 +73,40 @@ function registerExitHandler(): void { }); } +/** + * Resolve the config directory from an environment and home directory. + * + * Precedence: + * 1. `SENTRY_CONFIG_DIR` — explicit override, always wins. + * 2. Legacy `~/.sentry` — used when it already exists, so existing installs + * keep working without migration. + * 3. XDG base directory — `$XDG_CONFIG_HOME/sentry`, falling back to + * `~/.config/sentry`. Per the XDG spec, a non-absolute `XDG_CONFIG_HOME` + * is ignored. + * + * Pure and side-effect free so it can be unit-tested directly. + */ +export function resolveConfigDir(env: NodeJS.ProcessEnv, home: string): string { + const override = env[CONFIG_DIR_ENV_VAR]; + if (override) { + return override; + } + + const legacyDir = join(home, LEGACY_CONFIG_DIR_NAME); + if (existsSync(legacyDir)) { + return legacyDir; + } + + const xdgConfigHome = env.XDG_CONFIG_HOME; + const configHome = + xdgConfigHome && isAbsolute(xdgConfigHome) + ? xdgConfigHome + : join(home, ".config"); + return join(configHome, XDG_CONFIG_SUBDIR); +} + export function getConfigDir(): string { - return ( - getEnv()[CONFIG_DIR_ENV_VAR] || join(homedir(), DEFAULT_CONFIG_DIR_NAME) - ); + return resolveConfigDir(getEnv(), homedir()); } export function getDbPath(): string { diff --git a/packages/cli/test/lib/config.test.ts b/packages/cli/test/lib/config.test.ts index 496d2bb087..8e8491044c 100644 --- a/packages/cli/test/lib/config.test.ts +++ b/packages/cli/test/lib/config.test.ts @@ -4,8 +4,9 @@ * Integration tests for SQLite-based config storage. */ -import { writeFileSync } from "node:fs"; -import { access } from "node:fs/promises"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { access, mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { @@ -26,6 +27,7 @@ import { CONFIG_DIR_ENV_VAR, closeDatabase, getDbPath, + resolveConfigDir, } from "../../src/lib/db/index.js"; import { clearProjectAliases, @@ -561,6 +563,54 @@ describe("getDbPath", () => { }); }); +describe("resolveConfigDir", () => { + let home: string; + + beforeEach(async () => { + home = await mkdtemp(join(tmpdir(), "resolve-config-home-")); + }); + + afterEach(async () => { + await rm(home, { recursive: true, force: true }); + }); + + test("prefers the SENTRY_CONFIG_DIR override over everything", () => { + const override = join(home, "custom-config"); + mkdirSync(join(home, ".sentry")); + expect( + resolveConfigDir( + { + [CONFIG_DIR_ENV_VAR]: override, + XDG_CONFIG_HOME: join(home, "xdg"), + }, + home + ) + ).toBe(override); + }); + + test("uses the legacy ~/.sentry directory when it already exists", () => { + mkdirSync(join(home, ".sentry")); + expect(resolveConfigDir({}, home)).toBe(join(home, ".sentry")); + }); + + test("uses XDG_CONFIG_HOME/sentry when set to an absolute path", () => { + const xdg = join(home, "xdg-config"); + expect(resolveConfigDir({ XDG_CONFIG_HOME: xdg }, home)).toBe( + join(xdg, "sentry") + ); + }); + + test("falls back to ~/.config/sentry when XDG_CONFIG_HOME is unset", () => { + expect(resolveConfigDir({}, home)).toBe(join(home, ".config", "sentry")); + }); + + test("ignores a non-absolute XDG_CONFIG_HOME per the XDG spec", () => { + expect(resolveConfigDir({ XDG_CONFIG_HOME: "relative/path" }, home)).toBe( + join(home, ".config", "sentry") + ); + }); +}); + // ───────────────────────────────────────────────────────────────────────────── // JSON Migration // ───────────────────────────────────────────────────────────────────────────── From 2c52b8d99cd02eb6ae0e870af6a9ecf7db7031b5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 28 Aug 2026 17:57:20 +0000 Subject: [PATCH 02/17] chore: regenerate docs --- packages/cli/DEVELOPMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/DEVELOPMENT.md b/packages/cli/DEVELOPMENT.md index 7e37772481..4bc3f3a3d5 100644 --- a/packages/cli/DEVELOPMENT.md +++ b/packages/cli/DEVELOPMENT.md @@ -85,7 +85,7 @@ The table below lists the most common development variables. For the complete re | `SENTRY_HOST` | Sentry instance URL (for self-hosted, takes precedence) | `https://sentry.io` | | `SENTRY_URL` | Alias for `SENTRY_HOST` | `https://sentry.io` | | `SENTRY_CLIENT_ID` | Sentry OAuth app client ID | (required for build) | -| `SENTRY_CONFIG_DIR` | Override credentials/cache directory | `$XDG_CONFIG_HOME/sentry/` (`~/.config/sentry/`), or legacy `~/.sentry/` if it exists | +| `SENTRY_CONFIG_DIR` | Override credentials/cache directory | `~/.sentry/` | | `SENTRY_LOG_LEVEL` | Diagnostic log level (`error`, `warn`, `log`, `info`, `debug`, `trace`) | `info` | | `SENTRY_CLI_NO_TELEMETRY` | Disable CLI telemetry (error tracking) | — | From b64aaad2ad495cd7012fc50e0f77e72e0a7abbfe Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Fri, 28 Aug 2026 18:03:52 +0000 Subject: [PATCH 03/17] fix(config): only honor legacy ~/.sentry when it contains cli.db or config.json A bare ~/.sentry/bin created by the curl installer should not prevent new XDG-based installs. The legacy check now requires the presence of the actual database or the old JSON config file. Fixes the Cursor Bugbot report on PR #1503. --- packages/cli/src/lib/db/index.ts | 9 ++++++++- packages/cli/test/lib/config.test.ts | 12 ++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/lib/db/index.ts b/packages/cli/src/lib/db/index.ts index 7818f8a713..938fb89f0f 100644 --- a/packages/cli/src/lib/db/index.ts +++ b/packages/cli/src/lib/db/index.ts @@ -93,7 +93,14 @@ export function resolveConfigDir(env: NodeJS.ProcessEnv, home: string): string { } const legacyDir = join(home, LEGACY_CONFIG_DIR_NAME); - if (existsSync(legacyDir)) { + // Only treat the legacy directory as a prior config install when it + // contains the actual database or the old JSON config. A bare + // `~/.sentry/bin` created by the curl installer should not block XDG. + if ( + existsSync(legacyDir) && + (existsSync(join(legacyDir, DB_FILENAME)) || + existsSync(join(legacyDir, "config.json"))) + ) { return legacyDir; } diff --git a/packages/cli/test/lib/config.test.ts b/packages/cli/test/lib/config.test.ts index 8e8491044c..31bd445f46 100644 --- a/packages/cli/test/lib/config.test.ts +++ b/packages/cli/test/lib/config.test.ts @@ -589,8 +589,16 @@ describe("resolveConfigDir", () => { }); test("uses the legacy ~/.sentry directory when it already exists", () => { - mkdirSync(join(home, ".sentry")); - expect(resolveConfigDir({}, home)).toBe(join(home, ".sentry")); + const legacy = join(home, ".sentry"); + mkdirSync(legacy); + writeFileSync(join(legacy, "cli.db"), ""); // simulate a prior config install + expect(resolveConfigDir({}, home)).toBe(legacy); + }); + + test("ignores a bare ~/.sentry/bin (installer artifact) and falls back to XDG", () => { + const legacy = join(home, ".sentry"); + mkdirSync(join(legacy, "bin"), { recursive: true }); + expect(resolveConfigDir({}, home)).toBe(join(home, ".config", "sentry")); }); test("uses XDG_CONFIG_HOME/sentry when set to an absolute path", () => { From 90c9cb2f97811d9488f7307e3c9e65222eb4b75e Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Fri, 4 Sep 2026 17:21:05 +0000 Subject: [PATCH 04/17] feat(config): follow XDG spec for binary install path and migrate legacy layout Extend the XDG work to the binary install directory and add automatic migration of the legacy ~/.sentry layout in `sentry cli setup`. - determineInstallDir now honors an absolute XDG_BIN_HOME (after SENTRY_INSTALL_DIR) and falls back to ~/.local/bin instead of ~/.sentry/bin. - setup migrates an existing ~/.sentry/bin binary and legacy config (cli.db, config.json) into the XDG locations on first run; skipped when the target already exists. - upgrade's known-curl-path detection and fallback install path track the same XDG-aware resolution. - install script and getCurlInstallPaths recognize XDG_BIN_HOME. - resolveXdgConfigDir exposes the XDG target (bypassing legacy detection) so migration doesn't no-op. - docs: document binary install location + migration. Addresses review feedback on #1503. --- apps/cli-docs/src/fragments/configuration.md | 11 +++ packages/cli/install | 2 +- packages/cli/src/commands/cli/setup.ts | 79 ++++++++++++++++- packages/cli/src/lib/binary.ts | 23 +++-- packages/cli/src/lib/db/index.ts | 19 +++++ packages/cli/src/lib/upgrade.ts | 22 +++-- packages/cli/test/commands/cli/setup.test.ts | 90 ++++++++++++++++++++ packages/cli/test/lib/binary.test.ts | 60 +++++++++++-- 8 files changed, 283 insertions(+), 23 deletions(-) diff --git a/apps/cli-docs/src/fragments/configuration.md b/apps/cli-docs/src/fragments/configuration.md index 41716d6dfd..c47d504e10 100644 --- a/apps/cli-docs/src/fragments/configuration.md +++ b/apps/cli-docs/src/fragments/configuration.md @@ -111,3 +111,14 @@ We store credentials and caches in a SQLite database (`cli.db`) inside the confi - Project aliases (for monorepo support) See [Credential Storage](./commands/auth/#credential-storage) in the auth command docs for more details. + +## Binary Install Location + +When installed via the install script, the CLI binary is placed in an XDG-aligned directory. `sentry cli setup` resolves the location in this order: + +1. `SENTRY_INSTALL_DIR` — explicit override +2. `$XDG_BIN_HOME` — used when set to an absolute path, per the XDG spec +3. `~/.local/bin` or `~/bin` — when either already exists and is on your `PATH` +4. `~/.local/bin` — default fallback + +Older installs placed the binary in `~/.sentry/bin`. Running `sentry cli setup` (including via `sentry upgrade`) migrates an existing `~/.sentry/bin` binary and any legacy `~/.sentry` config data (`cli.db`, `config.json`) into the new XDG locations automatically. The migration is skipped when a binary or config already exists at the target. diff --git a/packages/cli/install b/packages/cli/install index 467b13b3dd..03c7ac2d85 100755 --- a/packages/cli/install +++ b/packages/cli/install @@ -337,7 +337,7 @@ trap - EXIT # interactively — when piped (curl | bash), stdin is the pipe. if [[ "${SENTRY_INIT:-}" == "1" ]]; then sentry_bin="" - for dir in "${SENTRY_INSTALL_DIR:-}" "$HOME/.local/bin" "$HOME/bin" "$HOME/.sentry/bin"; do + for dir in "${SENTRY_INSTALL_DIR:-}" "${XDG_BIN_HOME:-}" "$HOME/.local/bin" "$HOME/bin" "$HOME/.sentry/bin"; do [[ -z "$dir" ]] && continue if [[ -x "${dir}/sentry" ]]; then sentry_bin="${dir}/sentry" diff --git a/packages/cli/src/commands/cli/setup.ts b/packages/cli/src/commands/cli/setup.ts index 7a5ce6b0ee..dd575a5c9e 100644 --- a/packages/cli/src/commands/cli/setup.ts +++ b/packages/cli/src/commands/cli/setup.ts @@ -6,7 +6,13 @@ * and the upgrade command for curl-based installs). */ -import { existsSync, unlinkSync } from "node:fs"; +import { + copyFileSync, + existsSync, + mkdirSync, + renameSync, + unlinkSync, +} from "node:fs"; import { dirname, join } from "node:path"; import { captureException } from "@sentry/node-core/light"; import type { SentryContext } from "../../context.js"; @@ -28,6 +34,7 @@ import { getAgentSkillsPreference, setAgentSkillsPreference, } from "../../lib/db/defaults.js"; +import { resolveXdgConfigDir } from "../../lib/db/index.js"; import { setInstallInfo } from "../../lib/db/install-info.js"; import { parseReleaseChannel, @@ -80,6 +87,66 @@ function formatSetupResult(result: SetupResult): string { return result.messages.join("\n"); } +/** + * Migrate config data and the binary out of the legacy `~/.sentry` layout into + * the XDG-compliant locations. + * + * Runs before install/configuration so the rest of setup sees the new paths. + * Each part is independent and best-effort: a failure to move the binary must + * not prevent config migration, and vice versa. + */ +function migrateLegacyLayout( + homeDir: string, + env: NodeJS.ProcessEnv, + emit: Logger +): void { + const legacyDir = join(homeDir, ".sentry"); + + // Config data: cli.db (+ sidecars) and the old config.json. Target the XDG + // location directly — resolveConfigDir keeps returning the legacy dir while + // it still holds cli.db, which would make migration a no-op. + const targetConfigDir = resolveXdgConfigDir(env, homeDir); + if (targetConfigDir !== legacyDir) { + const configFiles = ["cli.db", "cli.db-wal", "cli.db-shm", "config.json"]; + const hasLegacyConfig = configFiles.some((name) => + existsSync(join(legacyDir, name)) + ); + const primary = join(targetConfigDir, "cli.db"); + if (hasLegacyConfig && !existsSync(primary)) { + mkdirSync(targetConfigDir, { recursive: true, mode: 0o700 }); + for (const name of configFiles) { + const from = join(legacyDir, name); + if (existsSync(from)) { + renameSync(from, join(targetConfigDir, name)); + } + } + emit(`Config: Migrated ${legacyDir} → ${targetConfigDir}`); + } + } + + // Binary: ~/.sentry/bin/ → XDG-aware install dir. + const filename = getBinaryFilename(); + const legacyBin = join(legacyDir, "bin", filename); + const targetDir = determineInstallDir(homeDir, env); + const targetBin = join(targetDir, filename); + if ( + existsSync(legacyBin) && + !existsSync(targetBin) && + targetDir !== join(legacyDir, "bin") + ) { + mkdirSync(targetDir, { recursive: true, mode: 0o755 }); + copyFileSync(legacyBin, targetBin); + try { + unlinkSync(legacyBin); + } catch { + // Leave the old binary in place if it can't be removed — the new copy + // is authoritative and setInstallInfo points upgrades at it. + } + setInstallInfo({ method: "curl", path: targetBin, version: CLI_VERSION }); + emit(`Binary: Migrated ${legacyBin} → ${targetBin}`); + } +} + /** * Handle binary installation from a temp location. * @@ -555,7 +622,15 @@ export const setupCommand = buildCommand({ let binaryDir = dirname(binaryPath); let freshInstall = false; - // 0. Install binary from temp location (when --install is set) + // 0. Migrate any legacy ~/.sentry config/binary into XDG locations first, + // so the steps below operate on the new paths. + try { + migrateLegacyLayout(homeDir, process.env, emit); + } catch (error) { + warn("Legacy migration", error); + } + + // 1. Install binary from temp location (when --install is set) if (flags.install) { const result = await handleInstall( process.execPath, diff --git a/packages/cli/src/lib/binary.ts b/packages/cli/src/lib/binary.ts index 215d18669f..fe696670a1 100644 --- a/packages/cli/src/lib/binary.ts +++ b/packages/cli/src/lib/binary.ts @@ -15,7 +15,7 @@ import { writeFileSync, } from "node:fs"; import { chmod, copyFile, mkdir, realpath, unlink } from "node:fs/promises"; -import { delimiter, dirname, join, resolve } from "node:path"; +import { delimiter, dirname, isAbsolute, join, resolve } from "node:path"; import { compare as semverCompare } from "semver"; import { getUserAgent } from "./constants.js"; import { @@ -226,10 +226,11 @@ export function getBinaryPaths(installPath: string): { * Determine the install directory for a curl-installed binary. * * Priority: - * 1. $SENTRY_INSTALL_DIR environment variable (if set and writable) - * 2. ~/.local/bin (if exists AND in $PATH) - * 3. ~/bin (if exists AND in $PATH) - * 4. ~/.sentry/bin (fallback; setup will handle PATH modification) + * 1. $SENTRY_INSTALL_DIR environment variable + * 2. $XDG_BIN_HOME (if set to an absolute path, per the XDG spec) + * 3. ~/.local/bin (if exists AND in $PATH) + * 4. ~/bin (if exists AND in $PATH) + * 5. ~/.local/bin (XDG-aligned fallback; setup handles PATH modification) * * @param homeDir - User's home directory * @param env - Process environment variables @@ -246,7 +247,13 @@ export function determineInstallDir( return env.SENTRY_INSTALL_DIR; } - // 2-3. Check well-known directories that are already in PATH + // 2. XDG_BIN_HOME override — honored only when absolute, per the XDG spec + const xdgBinHome = env.XDG_BIN_HOME; + if (xdgBinHome && isAbsolute(xdgBinHome)) { + return xdgBinHome; + } + + // 3-4. Check well-known directories that are already in PATH const candidates = [join(homeDir, ".local", "bin"), join(homeDir, "bin")]; for (const dir of candidates) { @@ -255,8 +262,8 @@ export function determineInstallDir( } } - // 4. Fallback — setup will handle adding this to PATH - return join(homeDir, ".sentry", "bin"); + // 5. XDG-aligned fallback — setup will handle adding this to PATH + return join(homeDir, ".local", "bin"); } /** diff --git a/packages/cli/src/lib/db/index.ts b/packages/cli/src/lib/db/index.ts index 938fb89f0f..1034770ce9 100644 --- a/packages/cli/src/lib/db/index.ts +++ b/packages/cli/src/lib/db/index.ts @@ -104,6 +104,25 @@ export function resolveConfigDir(env: NodeJS.ProcessEnv, home: string): string { return legacyDir; } + return resolveXdgConfigDir(env, home); +} + +/** + * Resolve the XDG-compliant config directory, ignoring any legacy `~/.sentry` + * install. This is the migration *target*: `resolveConfigDir` keeps returning + * the legacy dir while it holds `cli.db`, so migration must compute the new + * location directly. Honors `SENTRY_CONFIG_DIR` and an absolute + * `XDG_CONFIG_HOME`, otherwise defaults to `~/.config/sentry`. + */ +export function resolveXdgConfigDir( + env: NodeJS.ProcessEnv, + home: string +): string { + const override = env[CONFIG_DIR_ENV_VAR]; + if (override) { + return override; + } + const xdgConfigHome = env.XDG_CONFIG_HOME; const configHome = xdgConfigHome && isAbsolute(xdgConfigHome) diff --git a/packages/cli/src/lib/upgrade.ts b/packages/cli/src/lib/upgrade.ts index 00a8301bf2..0f5d69257a 100644 --- a/packages/cli/src/lib/upgrade.ts +++ b/packages/cli/src/lib/upgrade.ts @@ -19,11 +19,12 @@ import { } from "node:fs"; import { writeFile } from "node:fs/promises"; import { homedir } from "node:os"; -import { dirname, join, sep } from "node:path"; +import { dirname, isAbsolute, join, sep } from "node:path"; import { setTimeout } from "node:timers/promises"; import { acquireLock, cleanupOldBinary, + determineInstallDir, fetchWithUpgradeError, GITHUB_RELEASES_URL, getBinaryDownloadUrl, @@ -100,7 +101,15 @@ export const VERSION_PREFIX_REGEX = /^v/; */ let _knownCurlPaths: string[] | undefined; function getKnownCurlPaths(): string[] { - _knownCurlPaths ??= KNOWN_CURL_DIRS.map((dir) => join(homedir(), dir) + sep); + if (_knownCurlPaths === undefined) { + const paths = KNOWN_CURL_DIRS.map((dir) => join(homedir(), dir) + sep); + // Honor an absolute XDG_BIN_HOME, matching determineInstallDir's precedence + const xdgBinHome = process.env.XDG_BIN_HOME; + if (xdgBinHome && isAbsolute(xdgBinHome)) { + paths.push(xdgBinHome + sep); + } + _knownCurlPaths = paths; + } return _knownCurlPaths; } @@ -111,7 +120,7 @@ function getKnownCurlPaths(): string[] { * 1. Stored install path from DB (if method is curl AND its directory still * exists — a stale path whose directory was purged is skipped) * 2. process.execPath if it's in a known curl install location - * 3. Default to ~/.sentry/bin/sentry (fallback for fresh installs) + * 3. Default to the XDG-aware install dir (fallback for fresh installs) * * @returns Object with install, temp, old, and lock file paths */ @@ -128,7 +137,7 @@ export function getCurlInstallPaths(): { // `ENOENT ... open '.../sentry.lock'` (reported in #discuss-cli). // // existsSync also returns false on EACCES / a transiently-unmounted parent, - // in which case we fall through to execPath / the ~/.sentry/bin fallback + // in which case we fall through to execPath / the default-install fallback // rather than erroring. That tradeoff is acceptable: the running binary's // own directory (execPath) is by definition accessible, so a genuine install // is still found; only an unreadable *stored hint* is ignored. @@ -149,7 +158,10 @@ export function getCurlInstallPaths(): { } // Fallback to default path (for fresh installs or non-curl runs like tests) - const defaultPath = join(homedir(), ".sentry", "bin", getBinaryFilename()); + const defaultPath = join( + determineInstallDir(homedir(), process.env), + getBinaryFilename() + ); return getBinaryPaths(defaultPath); } diff --git a/packages/cli/test/commands/cli/setup.test.ts b/packages/cli/test/commands/cli/setup.test.ts index ade1849393..ae4b9815cc 100644 --- a/packages/cli/test/commands/cli/setup.test.ts +++ b/packages/cli/test/commands/cli/setup.test.ts @@ -1015,6 +1015,96 @@ describe("sentry cli setup", () => { }); }); +describe("sentry cli setup — legacy migration", () => { + let testHome: string; + let restoreStderr: (() => void) | undefined; + + beforeEach(() => { + testHome = join( + "/tmp", + `setup-mig-home-${Date.now()}-${Math.random().toString(36).slice(2)}` + ); + mkdirSync(testHome, { recursive: true }); + }); + + afterEach(() => { + restoreStderr?.(); + restoreStderr = undefined; + rmSync(testHome, { recursive: true, force: true }); + }); + + const setupArgs = [ + "cli", + "setup", + "--quiet", + "--no-modify-path", + "--no-completions", + "--no-agent-skills", + ]; + + test("migrates legacy ~/.sentry config into the XDG config dir", async () => { + const configDir = join(testHome, "config", "sentry"); + const legacyDir = join(testHome, ".sentry"); + mkdirSync(legacyDir, { recursive: true }); + writeFileSync(join(legacyDir, "cli.db"), "legacy-db"); + + const { context, restore } = createMockContext({ + homeDir: testHome, + env: { SENTRY_CONFIG_DIR: configDir }, + }); + restoreStderr = restore; + + await run(app, setupArgs, context); + + const moved = join(configDir, "cli.db"); + expect(existsSync(moved)).toBe(true); + expect(await readFile(moved, "utf8")).toBe("legacy-db"); + expect(existsSync(join(legacyDir, "cli.db"))).toBe(false); + }); + + test("migrates a legacy ~/.sentry/bin binary to the install dir", async () => { + const installDir = join(testHome, "install", "bin"); + const legacyBinDir = join(testHome, ".sentry", "bin"); + mkdirSync(legacyBinDir, { recursive: true }); + writeFileSync(join(legacyBinDir, "sentry"), "legacy-binary"); + + const { context, restore } = createMockContext({ + homeDir: testHome, + env: { SENTRY_INSTALL_DIR: installDir }, + }); + restoreStderr = restore; + + await run(app, setupArgs, context); + + const moved = join(installDir, "sentry"); + expect(existsSync(moved)).toBe(true); + expect(await readFile(moved, "utf8")).toBe("legacy-binary"); + expect(existsSync(join(legacyBinDir, "sentry"))).toBe(false); + }); + + test("does not overwrite an existing binary at the target", async () => { + const installDir = join(testHome, "install", "bin"); + mkdirSync(installDir, { recursive: true }); + writeFileSync(join(installDir, "sentry"), "current-binary"); + + const legacyBinDir = join(testHome, ".sentry", "bin"); + mkdirSync(legacyBinDir, { recursive: true }); + writeFileSync(join(legacyBinDir, "sentry"), "legacy-binary"); + + const { context, restore } = createMockContext({ + homeDir: testHome, + env: { SENTRY_INSTALL_DIR: installDir }, + }); + restoreStderr = restore; + + await run(app, setupArgs, context); + + expect(await readFile(join(installDir, "sentry"), "utf8")).toBe( + "current-binary" + ); + }); +}); + describe("sentry cli setup — --channel flag", () => { useTestConfigDir("test-setup-channel-"); diff --git a/packages/cli/test/lib/binary.test.ts b/packages/cli/test/lib/binary.test.ts index 8254eac7b7..a9a255c20c 100644 --- a/packages/cli/test/lib/binary.test.ts +++ b/packages/cli/test/lib/binary.test.ts @@ -127,15 +127,15 @@ describe("determineInstallDir", () => { expect(result).toBe(homeBin); }); - test("falls back to ~/.sentry/bin when no candidates are in PATH", () => { + test("falls back to ~/.local/bin when no candidates are in PATH", () => { const result = determineInstallDir(testDir, { PATH: "/usr/bin:/bin", }); - expect(result).toBe(join(testDir, ".sentry", "bin")); + expect(result).toBe(join(testDir, ".local", "bin")); }); - test("skips ~/.local/bin when it exists but is not in PATH", () => { + test("falls back to ~/.local/bin when it exists but is not in PATH", () => { const localBin = join(testDir, ".local", "bin"); mkdirSync(localBin, { recursive: true }); @@ -143,8 +143,7 @@ describe("determineInstallDir", () => { PATH: "/usr/bin:/bin", }); - // Should fall back to ~/.sentry/bin, not use ~/.local/bin - expect(result).toBe(join(testDir, ".sentry", "bin")); + expect(result).toBe(localBin); }); test("handles empty PATH", () => { @@ -152,13 +151,60 @@ describe("determineInstallDir", () => { PATH: "", }); - expect(result).toBe(join(testDir, ".sentry", "bin")); + expect(result).toBe(join(testDir, ".local", "bin")); }); test("handles undefined PATH", () => { const result = determineInstallDir(testDir, {}); - expect(result).toBe(join(testDir, ".sentry", "bin")); + expect(result).toBe(join(testDir, ".local", "bin")); + }); + + test("uses XDG_BIN_HOME when set to an absolute path", () => { + const xdgBin = join(testDir, "xdg", "bin"); + + const result = determineInstallDir(testDir, { + XDG_BIN_HOME: xdgBin, + PATH: "/usr/bin", + }); + + expect(result).toBe(xdgBin); + }); + + test("ignores a non-absolute XDG_BIN_HOME per the XDG spec", () => { + const result = determineInstallDir(testDir, { + XDG_BIN_HOME: "relative/bin", + PATH: "/usr/bin", + }); + + expect(result).toBe(join(testDir, ".local", "bin")); + }); + + test("XDG_BIN_HOME takes priority over ~/.local/bin in PATH", () => { + const localBin = join(testDir, ".local", "bin"); + mkdirSync(localBin, { recursive: true }); + const xdgBin = join(testDir, "xdg", "bin"); + + const result = determineInstallDir(testDir, { + XDG_BIN_HOME: xdgBin, + PATH: `/usr/bin:${localBin}`, + }); + + expect(result).toBe(xdgBin); + }); + + test("SENTRY_INSTALL_DIR takes priority over XDG_BIN_HOME", () => { + const xdgBin = join(testDir, "xdg", "bin"); + const customDir = join(testDir, "custom"); + mkdirSync(customDir, { recursive: true }); + + const result = determineInstallDir(testDir, { + SENTRY_INSTALL_DIR: customDir, + XDG_BIN_HOME: xdgBin, + PATH: "/usr/bin", + }); + + expect(result).toBe(customDir); }); test("SENTRY_INSTALL_DIR takes priority over ~/.local/bin", () => { From 531840b28c1c9e254324fe2fd8fbba94558f95d3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 4 Sep 2026 17:22:07 +0000 Subject: [PATCH 05/17] chore: regenerate docs --- .../skills/sentry-cli/references/agent-conversation.md | 2 +- .../sentry-cli/skills/sentry-cli/references/dashboard.md | 2 +- .../plugins/sentry-cli/skills/sentry-cli/references/event.md | 2 +- .../sentry-cli/skills/sentry-cli/references/explore.md | 2 +- .../sentry-cli/skills/sentry-cli/references/feedback.md | 2 +- .../plugins/sentry-cli/skills/sentry-cli/references/issue.md | 4 ++-- .../plugins/sentry-cli/skills/sentry-cli/references/log.md | 2 +- .../plugins/sentry-cli/skills/sentry-cli/references/replay.md | 2 +- .../plugins/sentry-cli/skills/sentry-cli/references/span.md | 2 +- .../plugins/sentry-cli/skills/sentry-cli/references/trace.md | 4 ++-- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/agent-conversation.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/agent-conversation.md index e7fa5ccd3b..a9fff08682 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/agent-conversation.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/agent-conversation.md @@ -18,7 +18,7 @@ List recent agent conversations **Flags:** - `-n, --limit - Number of conversations (1-1000) - (default: "25")` - `-q, --query - Search query` -- `-t, --period - Time range: "7d", "2026-07-01..2026-08-01", ">=2026-07-01" - (default: "7d")` +- `-t, --period - Time range: "7d", "2026-08-01..2026-09-01", ">=2026-08-01" - (default: "7d")` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `-c, --cursor - Navigate pages: "next", "prev", "first" (or raw cursor string)` diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md index 8c318169be..1a2a94c552 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md @@ -42,7 +42,7 @@ View a dashboard - `-w, --web - Open in browser` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `-r, --refresh - Auto-refresh interval in seconds (default: 60, min: 10)` -- `-t, --period - Time range: "7d", "2026-07-01..2026-08-01", ">=2026-07-01"` +- `-t, --period - Time range: "7d", "2026-08-01..2026-09-01", ">=2026-08-01"` **Examples:** diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/event.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/event.md index e831b344ec..adce7c3e29 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/event.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/event.md @@ -37,7 +37,7 @@ List events for an issue - `-n, --limit - Number of events (1-1000) - (default: "25")` - `-q, --query - Search query (Sentry search syntax)` - `--full - Include full event body (stacktraces)` -- `-t, --period - Time range: "7d", "2026-07-01..2026-08-01", ">=2026-07-01" - (default: "7d")` +- `-t, --period - Time range: "7d", "2026-08-01..2026-09-01", ">=2026-08-01" - (default: "7d")` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `-c, --cursor - Navigate pages: "next", "prev", "first" (or raw cursor string)` diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md index a0ab45d3f6..9e2338cfcd 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md @@ -24,7 +24,7 @@ Query aggregate event data (Explore) - `-s, --sort - Sort field (prefix with - for desc, e.g., "-count()")` - `-e, --environment ... - Environment filter (repeatable, comma-separated)` - `-n, --limit - Number of rows (1-1000) - (default: "25")` -- `-t, --period - Time range: "7d", "2026-07-01..2026-08-01", ">=2026-07-01" - (default: "24h")` +- `-t, --period - Time range: "7d", "2026-08-01..2026-09-01", ">=2026-08-01" - (default: "24h")` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `-c, --cursor - Navigate pages: "next", "prev", "first" (or raw cursor string)` diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/feedback.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/feedback.md index ae6dd5281d..0e5397ec8c 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/feedback.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/feedback.md @@ -19,7 +19,7 @@ List and search User Feedback - `--status - Mailbox: unresolved, resolved, spam, or all - (default: "unresolved")` - `-n, --limit - Number of feedback items (1-1000) - (default: "25")` - `-q, --query - Search query (Sentry issue search syntax)` -- `-t, --period - Time range: "7d", "2026-07-01..2026-08-01", ">=2026-07-01" - (default: "14d")` +- `-t, --period - Time range: "7d", "2026-08-01..2026-09-01", ">=2026-08-01" - (default: "14d")` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `-c, --cursor - Navigate pages: "next", "prev", "first" (or raw cursor string)` diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md index 00386311d1..d1c554d385 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md @@ -19,7 +19,7 @@ List issues in a project - `-q, --query - Search query (Sentry syntax, implicit AND, no OR operator)` - `-n, --limit - Maximum number of issues to list - (default: "25")` - `-s, --sort - Sort by: recommended, date, new, freq, user (default: recommended on sentry.io, else date)` -- `-t, --period - Time range: "7d", "2026-07-01..2026-08-01", ">=2026-07-01" - (default: "90d")` +- `-t, --period - Time range: "7d", "2026-08-01..2026-09-01", ">=2026-08-01" - (default: "90d")` - `-c, --cursor - Pagination cursor (use "next" for next page, "prev" for previous)` - `--compact - Single-line rows for compact output (auto-detects if omitted)` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` @@ -92,7 +92,7 @@ List events for a specific issue - `-n, --limit - Number of events (1-1000) - (default: "25")` - `-q, --query - Search query (Sentry search syntax)` - `--full - Include full event body (stacktraces)` -- `-t, --period - Time range: "7d", "2026-07-01..2026-08-01", ">=2026-07-01" - (default: "7d")` +- `-t, --period - Time range: "7d", "2026-08-01..2026-09-01", ">=2026-08-01" - (default: "7d")` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `-c, --cursor - Navigate pages: "next", "prev", "first" (or raw cursor string)` diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/log.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/log.md index 3cffc06556..28d0d0eabb 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/log.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/log.md @@ -19,7 +19,7 @@ List logs from a project - `-n, --limit - Number of log entries (1-1000) - (default: "100")` - `-q, --query - Filter query (e.g., "severity:error", "project:backend", "project:[a,b]")` - `-f, --follow - Stream logs (optionally specify poll interval in seconds)` -- `-t, --period - Time range: "7d", "2026-07-01..2026-08-01", ">=2026-07-01"` +- `-t, --period - Time range: "7d", "2026-08-01..2026-09-01", ">=2026-08-01"` - `-s, --sort - Sort order: "newest" (default) or "oldest" - (default: "newest")` - `--fresh - Bypass cache, re-detect projects, and fetch fresh data` diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/replay.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/replay.md index 4b89e399f5..c444ffaa2a 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/replay.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/replay.md @@ -20,7 +20,7 @@ List recent Session Replays - `-q, --query - Search query (Sentry replay search syntax)` - `-e, --environment ... - Filter by environment (repeatable, comma-separated)` - `-s, --sort - Sort by: date, oldest, duration, errors, activity, or a raw replay sort field - (default: "date")` -- `-t, --period - Time range: "7d", "2026-07-01..2026-08-01", ">=2026-07-01" - (default: "7d")` +- `-t, --period - Time range: "7d", "2026-08-01..2026-09-01", ">=2026-08-01" - (default: "7d")` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `-c, --cursor - Navigate pages: "next", "prev", "first" (or raw cursor string)` diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/span.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/span.md index f9dc233099..ff4cb412a3 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/span.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/span.md @@ -19,7 +19,7 @@ List spans in a project or trace - `-n, --limit - Number of spans (<=1000) - (default: "25")` - `-q, --query - Filter spans (e.g., "op:db", "project:backend", "project:[cli,api]")` - `-s, --sort - Sort order: date, duration - (default: "date")` -- `-t, --period - Time range: "7d", "2026-07-01..2026-08-01", ">=2026-07-01" - (default: "7d")` +- `-t, --period - Time range: "7d", "2026-08-01..2026-09-01", ">=2026-08-01" - (default: "7d")` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `-c, --cursor - Navigate pages: "next", "prev", "first" (or raw cursor string)` diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trace.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trace.md index 0072977777..53bb750bbf 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trace.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trace.md @@ -19,7 +19,7 @@ List recent traces in a project - `-n, --limit - Number of traces (1-1000) - (default: "25")` - `-q, --query - Search query (Sentry search syntax)` - `-s, --sort - Sort by: date, duration - (default: "date")` -- `-t, --period - Time range: "7d", "2026-07-01..2026-08-01", ">=2026-07-01" - (default: "7d")` +- `-t, --period - Time range: "7d", "2026-08-01..2026-09-01", ">=2026-08-01" - (default: "7d")` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `-c, --cursor - Navigate pages: "next", "prev", "first" (or raw cursor string)` @@ -91,7 +91,7 @@ View logs associated with a trace **Flags:** - `-w, --web - Open trace in browser` -- `-t, --period - Time range: "7d", "2026-07-01..2026-08-01", ">=2026-07-01" - (default: "14d")` +- `-t, --period - Time range: "7d", "2026-08-01..2026-09-01", ">=2026-08-01" - (default: "14d")` - `-n, --limit - Number of log entries (<=1000) - (default: "100")` - `-q, --query - Filter query (e.g., "severity:error", "project:backend", "project:[a,b]")` - `-s, --sort - Sort order: "newest" (default) or "oldest" - (default: "newest")` From 78fd927df2b81dfeb7c7b0f115ace5909bf4d0a8 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Fri, 4 Sep 2026 18:11:41 +0000 Subject: [PATCH 06/17] fix(setup): address bugbot findings on binary/config migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reevaluated after scope expanded to include binary install paths. - Close the SQLite DB before renaming cli.db/WAL sidecars. The DB is opened at startup (cleanup-old-binary), and an open file cannot be renamed on Windows — previously the whole migration threw and was swallowed. Split config and binary migration into separate functions with independent try/catch so one failing can't skip the other. - migrateLegacyBinary now returns the new path; setup adopts it as binaryPath/binaryDir so PATH modification and setInstallInfo point at the migrated binary instead of the deleted legacy path (a --method setInstallInfo no longer overwrites it with the old location). - docs: clarify that 'sentry upgrade' keeps the binary in place (pins SENTRY_INSTALL_DIR) and only 'sentry cli setup' relocates it; config data still migrates on upgrade. - test: record-new-path regression covering the stale-path fix. Addresses cursor[bot] review on #1503. --- apps/cli-docs/src/fragments/configuration.md | 4 +- packages/cli/src/commands/cli/setup.ts | 116 ++++++++++++------- packages/cli/test/commands/cli/setup.test.ts | 61 ++++++++++ 3 files changed, 137 insertions(+), 44 deletions(-) diff --git a/apps/cli-docs/src/fragments/configuration.md b/apps/cli-docs/src/fragments/configuration.md index c47d504e10..7cb853ef5d 100644 --- a/apps/cli-docs/src/fragments/configuration.md +++ b/apps/cli-docs/src/fragments/configuration.md @@ -121,4 +121,6 @@ When installed via the install script, the CLI binary is placed in an XDG-aligne 3. `~/.local/bin` or `~/bin` — when either already exists and is on your `PATH` 4. `~/.local/bin` — default fallback -Older installs placed the binary in `~/.sentry/bin`. Running `sentry cli setup` (including via `sentry upgrade`) migrates an existing `~/.sentry/bin` binary and any legacy `~/.sentry` config data (`cli.db`, `config.json`) into the new XDG locations automatically. The migration is skipped when a binary or config already exists at the target. +Older installs placed the binary in `~/.sentry/bin`. Running `sentry cli setup` moves an existing `~/.sentry/bin` binary into the resolved install directory (updating your `PATH` and recorded install metadata to match) and migrates any legacy `~/.sentry` config data (`cli.db`, `config.json`) into the XDG config directory. Both migrations are skipped when a binary or config already exists at the target. + +`sentry upgrade` keeps the binary where it currently lives — it pins `SENTRY_INSTALL_DIR` to the existing install directory so an in-place update never relocates a binary that is already on your `PATH`. Legacy config data is still migrated on upgrade; to move the binary itself to the XDG location, run `sentry cli setup` (optionally with `SENTRY_INSTALL_DIR` or `XDG_BIN_HOME` set). diff --git a/packages/cli/src/commands/cli/setup.ts b/packages/cli/src/commands/cli/setup.ts index dd575a5c9e..935390f19a 100644 --- a/packages/cli/src/commands/cli/setup.ts +++ b/packages/cli/src/commands/cli/setup.ts @@ -34,7 +34,7 @@ import { getAgentSkillsPreference, setAgentSkillsPreference, } from "../../lib/db/defaults.js"; -import { resolveXdgConfigDir } from "../../lib/db/index.js"; +import { closeDatabase, resolveXdgConfigDir } from "../../lib/db/index.js"; import { setInstallInfo } from "../../lib/db/install-info.js"; import { parseReleaseChannel, @@ -88,63 +88,81 @@ function formatSetupResult(result: SetupResult): string { } /** - * Migrate config data and the binary out of the legacy `~/.sentry` layout into - * the XDG-compliant locations. + * Migrate `cli.db` (+ WAL sidecars) and the old `config.json` out of the legacy + * `~/.sentry` directory into the XDG config directory. * - * Runs before install/configuration so the rest of setup sees the new paths. - * Each part is independent and best-effort: a failure to move the binary must - * not prevent config migration, and vice versa. + * The database is opened at CLI startup (cleanup-old-binary reads install + * info), so it must be closed before the files are moved — an open SQLite file + * cannot be renamed on Windows. Closing also invalidates the cached handle, so + * the next `getDatabase()` reopens at the new path. */ -function migrateLegacyLayout( +function migrateLegacyConfig( homeDir: string, env: NodeJS.ProcessEnv, emit: Logger ): void { const legacyDir = join(homeDir, ".sentry"); - - // Config data: cli.db (+ sidecars) and the old config.json. Target the XDG - // location directly — resolveConfigDir keeps returning the legacy dir while - // it still holds cli.db, which would make migration a no-op. + // Target the XDG location directly — resolveConfigDir keeps returning the + // legacy dir while it still holds cli.db, which would make migration a no-op. const targetConfigDir = resolveXdgConfigDir(env, homeDir); - if (targetConfigDir !== legacyDir) { - const configFiles = ["cli.db", "cli.db-wal", "cli.db-shm", "config.json"]; - const hasLegacyConfig = configFiles.some((name) => - existsSync(join(legacyDir, name)) - ); - const primary = join(targetConfigDir, "cli.db"); - if (hasLegacyConfig && !existsSync(primary)) { - mkdirSync(targetConfigDir, { recursive: true, mode: 0o700 }); - for (const name of configFiles) { - const from = join(legacyDir, name); - if (existsSync(from)) { - renameSync(from, join(targetConfigDir, name)); - } - } - emit(`Config: Migrated ${legacyDir} → ${targetConfigDir}`); + if (targetConfigDir === legacyDir) { + return; + } + + const configFiles = ["cli.db", "cli.db-wal", "cli.db-shm", "config.json"]; + const hasLegacyConfig = configFiles.some((name) => + existsSync(join(legacyDir, name)) + ); + if (!hasLegacyConfig || existsSync(join(targetConfigDir, "cli.db"))) { + return; + } + + closeDatabase(); + mkdirSync(targetConfigDir, { recursive: true, mode: 0o700 }); + for (const name of configFiles) { + const from = join(legacyDir, name); + if (existsSync(from)) { + renameSync(from, join(targetConfigDir, name)); } } + emit(`Config: Migrated ${legacyDir} → ${targetConfigDir}`); +} - // Binary: ~/.sentry/bin/ → XDG-aware install dir. +/** + * Migrate the binary out of the legacy `~/.sentry/bin` into the XDG-aware + * install dir. Returns the new binary path when a move happened, so the caller + * can point PATH setup and recorded install info at the new location instead of + * the now-deleted legacy path. + */ +function migrateLegacyBinary( + homeDir: string, + env: NodeJS.ProcessEnv, + emit: Logger +): string | undefined { + const legacyDir = join(homeDir, ".sentry"); const filename = getBinaryFilename(); const legacyBin = join(legacyDir, "bin", filename); const targetDir = determineInstallDir(homeDir, env); const targetBin = join(targetDir, filename); if ( - existsSync(legacyBin) && - !existsSync(targetBin) && - targetDir !== join(legacyDir, "bin") + !existsSync(legacyBin) || + existsSync(targetBin) || + targetDir === join(legacyDir, "bin") ) { - mkdirSync(targetDir, { recursive: true, mode: 0o755 }); - copyFileSync(legacyBin, targetBin); - try { - unlinkSync(legacyBin); - } catch { - // Leave the old binary in place if it can't be removed — the new copy - // is authoritative and setInstallInfo points upgrades at it. - } - setInstallInfo({ method: "curl", path: targetBin, version: CLI_VERSION }); - emit(`Binary: Migrated ${legacyBin} → ${targetBin}`); + return; } + + mkdirSync(targetDir, { recursive: true, mode: 0o755 }); + copyFileSync(legacyBin, targetBin); + try { + unlinkSync(legacyBin); + } catch { + // Leave the old binary in place if it can't be removed — the new copy + // is authoritative and setInstallInfo points upgrades at it. + } + setInstallInfo({ method: "curl", path: targetBin, version: CLI_VERSION }); + emit(`Binary: Migrated ${legacyBin} → ${targetBin}`); + return targetBin; } /** @@ -623,11 +641,23 @@ export const setupCommand = buildCommand({ let freshInstall = false; // 0. Migrate any legacy ~/.sentry config/binary into XDG locations first, - // so the steps below operate on the new paths. + // so the steps below operate on the new paths. Config and binary migrations + // are independent — a failure in one must not skip the other. try { - migrateLegacyLayout(homeDir, process.env, emit); + migrateLegacyConfig(homeDir, process.env, emit); + } catch (error) { + warn("Legacy config migration", error); + } + try { + const migratedBinary = migrateLegacyBinary(homeDir, process.env, emit); + // Adopt the new location so PATH setup and recorded install info point at + // the migrated binary rather than the deleted legacy path. + if (migratedBinary) { + binaryPath = migratedBinary; + binaryDir = dirname(migratedBinary); + } } catch (error) { - warn("Legacy migration", error); + warn("Legacy binary migration", error); } // 1. Install binary from temp location (when --install is set) diff --git a/packages/cli/test/commands/cli/setup.test.ts b/packages/cli/test/commands/cli/setup.test.ts index ae4b9815cc..609ead1469 100644 --- a/packages/cli/test/commands/cli/setup.test.ts +++ b/packages/cli/test/commands/cli/setup.test.ts @@ -33,6 +33,10 @@ import { getAgentSkillsPreference, setAgentSkillsPreference, } from "../../../src/lib/db/defaults.js"; +import { + clearInstallInfo, + getInstallInfo, +} from "../../../src/lib/db/install-info.js"; import { getReleaseChannel } from "../../../src/lib/db/release-channel.js"; // biome-ignore lint/performance/noNamespaceImport: dynamic setup imports are mocked at the module boundary import * as interactiveLogin from "../../../src/lib/interactive-login.js"; @@ -1105,6 +1109,63 @@ describe("sentry cli setup — legacy migration", () => { }); }); +describe("sentry cli setup — legacy migration records new path", () => { + // Isolate the DB so getInstallInfo() reflects this test's writes. + useTestConfigDir("test-setup-migration-info-"); + + let testHome: string; + let restoreStderr: (() => void) | undefined; + + beforeEach(() => { + testHome = join( + "/tmp", + `setup-mig-info-${Date.now()}-${Math.random().toString(36).slice(2)}` + ); + mkdirSync(testHome, { recursive: true }); + }); + + afterEach(() => { + restoreStderr?.(); + restoreStderr = undefined; + clearInstallInfo(); + rmSync(testHome, { recursive: true, force: true }); + }); + + test("records the migrated binary path, not the legacy location", async () => { + const installDir = join(testHome, "install", "bin"); + const legacyBinDir = join(testHome, ".sentry", "bin"); + mkdirSync(legacyBinDir, { recursive: true }); + writeFileSync(join(legacyBinDir, "sentry"), "legacy-binary"); + + const { context, restore } = createMockContext({ + homeDir: testHome, + env: { + SENTRY_INSTALL_DIR: installDir, + SENTRY_CONFIG_DIR: process.env.SENTRY_CONFIG_DIR, + }, + }); + restoreStderr = restore; + + await run( + app, + [ + "cli", + "setup", + "--quiet", + "--method", + "curl", + "--no-modify-path", + "--no-completions", + "--no-agent-skills", + ], + context + ); + + const recorded = getInstallInfo(); + expect(recorded?.path).toBe(join(installDir, "sentry")); + }); +}); + describe("sentry cli setup — --channel flag", () => { useTestConfigDir("test-setup-channel-"); From 5927aefb9ef9cc0c8e083920962256749417ea4b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 4 Sep 2026 18:15:32 +0000 Subject: [PATCH 07/17] chore: regenerate docs --- .../sentry-cli/skills/sentry-cli/SKILL.md | 25 ++- .../references/agent-conversation.md | 29 +++ .../skills/sentry-cli/references/alert.md | 88 ++++++++ .../skills/sentry-cli/references/api.md | 42 ++++ .../skills/sentry-cli/references/auth.md | 54 +++++ .../skills/sentry-cli/references/build.md | 29 +++ .../skills/sentry-cli/references/cli.md | 125 +++++++++++ .../sentry-cli/references/code-mappings.md | 16 ++ .../sentry-cli/references/dart-symbol-map.md | 13 ++ .../skills/sentry-cli/references/dashboard.md | 109 ++++++++++ .../sentry-cli/references/debug-files.md | 50 +++++ .../skills/sentry-cli/references/docs.md | 12 ++ .../skills/sentry-cli/references/event.md | 70 +++++++ .../skills/sentry-cli/references/explore.md | 57 +++++ .../skills/sentry-cli/references/feedback.md | 43 ++++ .../skills/sentry-cli/references/info.md | 13 ++ .../skills/sentry-cli/references/init.md | 28 +++ .../skills/sentry-cli/references/issue.md | 195 ++++++++++++++++++ .../skills/sentry-cli/references/local.md | 27 +++ .../skills/sentry-cli/references/log.md | 39 ++++ .../skills/sentry-cli/references/monitor.md | 23 +++ .../skills/sentry-cli/references/org.md | 16 ++ .../skills/sentry-cli/references/platform.md | 16 ++ .../skills/sentry-cli/references/proguard.md | 10 + .../skills/sentry-cli/references/project.md | 43 ++++ .../sentry-cli/references/react-native.md | 19 ++ .../skills/sentry-cli/references/release.md | 59 ++++++ .../skills/sentry-cli/references/replay.md | 36 ++++ .../skills/sentry-cli/references/repo.md | 13 ++ .../skills/sentry-cli/references/schema.md | 19 ++ .../skills/sentry-cli/references/snapshots.md | 28 +++ .../skills/sentry-cli/references/sourcemap.md | 39 ++++ .../skills/sentry-cli/references/span.md | 41 ++++ .../skills/sentry-cli/references/status.md | 13 ++ .../skills/sentry-cli/references/team.md | 13 ++ .../skills/sentry-cli/references/trace.md | 54 +++++ .../skills/sentry-cli/references/trial.md | 19 ++ 37 files changed, 1523 insertions(+), 2 deletions(-) diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md index 6761e0c40a..4b8b785ba6 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -699,8 +699,29 @@ Browse the Sentry API schema → Full flags and examples: `references/schema.md` +## Global Options + +All commands support the following global options: + +- `--help` - Show help for the command +- `--version` - Show CLI version +- `--log-level ` - Set log verbosity (`error`, `warn`, `log`, `info`, `debug`, `trace`). Overrides `SENTRY_LOG_LEVEL` +- `--verbose` - Shorthand for `--log-level debug` + ## Output Formats -Most commands support `--json` flag for JSON output, making it easy to integrate with other tools. +### JSON Output -View commands support `-w` or `--web` flag to open the resource in your browser. +Most list and view commands support `--json` flag for JSON output, making it easy to integrate with other tools: + +```bash +sentry org list --json | jq '.[] | .slug' +``` + +### Opening in Browser + +View commands support `-w` or `--web` flag to open the resource in your browser: + +```bash +sentry issue view PROJ-123 -w +``` diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/agent-conversation.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/agent-conversation.md index 6803ac0ce1..a9fff08682 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/agent-conversation.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/agent-conversation.md @@ -44,6 +44,25 @@ List recent agent conversations | `toolNames` | array | | | `toolErrors` | number | | +**Examples:** + +```bash +# List recent agent conversations +sentry agent-conversation list + +# Explicit organization +sentry agent-conversation list my-org + +# Show more, last 24 hours +sentry agent-conversation list --limit 50 --period 24h + +# Filter conversations +sentry agent-conversation list -q "has:errors" + +# Paginate through results +sentry agent-conversation list my-org -c next +``` + ### `sentry agent-conversation view ` View an agent conversation transcript @@ -51,4 +70,14 @@ View an agent conversation transcript **Flags:** - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` +**Examples:** + +```bash +# View full transcript +sentry agent-conversation view my-org conv-123 + +# JSON output +sentry agent-conversation view my-org conv-123 --json +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/alert.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/alert.md index 800847134b..60f3eabf68 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/alert.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/alert.md @@ -22,6 +22,16 @@ List issue alert rules - `-c, --cursor - Pagination cursor (use "next" for next page, "prev" for previous)` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` +**Examples:** + +```bash +# List issue alert rules for a project +sentry alert issues list my-org/my-project + +# Filter rules by name +sentry alert issues list my-org/my-project --query "spike" +``` + ### `sentry alert issues view ` View an issue alert rule @@ -29,6 +39,16 @@ View an issue alert rule **Flags:** - `-w, --web - Open issue alert rules page in browser` +**Examples:** + +```bash +# View by ID +sentry alert issues view my-org/my-project/12345 + +# View by name +sentry alert issues view my-org/my-project/"Error Spike" +``` + ### `sentry alert issues create ` Create an issue alert rule @@ -44,6 +64,16 @@ Create an issue alert rule - `--owner - Owner (team:user style value accepted by Sentry API)` - `-n, --dry-run - Show what would happen without making changes` +**Examples:** + +```bash +# Create an issue alert rule with inline JSON condition/action +sentry alert issues create my-org/my-project \ + --name "Error Spike" \ + --condition '{"type":"first_seen_event","comparison":true,"conditionResult":true}' \ + --action '{"type":"email","data":{},"config":{"targetType":"team","targetIdentifier":"1"}}' +``` + ### `sentry alert issues delete ` Delete an issue alert rule @@ -53,6 +83,13 @@ Delete an issue alert rule - `-f, --force - Force the operation without confirmation` - `-n, --dry-run - Show what would happen without making changes` +**Examples:** + +```bash +# Delete with preview +sentry alert issues delete my-org/my-project/12345 --dry-run +``` + ### `sentry alert issues edit ` Edit an issue alert rule @@ -68,6 +105,13 @@ Edit an issue alert rule - `-m, --filter-match - Filter match mode: all or any` - `--owner - Owner value (pass empty string to clear)` +**Examples:** + +```bash +# Edit issue alert name/status +sentry alert issues edit my-org/my-project/12345 --name "Prod Error Spike" --status disabled +``` + ### `sentry alert metrics list ` List metric alert rules @@ -79,6 +123,13 @@ List metric alert rules - `-c, --cursor - Pagination cursor (use "next" for next page, "prev" for previous)` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` +**Examples:** + +```bash +# List metric alert rules for an organization +sentry alert metrics list my-org/ +``` + ### `sentry alert metrics view ` View a metric alert rule @@ -86,6 +137,16 @@ View a metric alert rule **Flags:** - `-w, --web - Open metric alert rules page in browser` +**Examples:** + +```bash +# View by ID +sentry alert metrics view my-org/67890 + +# View by name +sentry alert metrics view my-org/"P95 latency alert" +``` + ### `sentry alert metrics create ` Create a metric alert rule @@ -102,6 +163,19 @@ Create a metric alert rule - `--owner - Owner value accepted by Sentry API` - `-n, --dry-run - Show what would happen without making changes` +**Examples:** + +```bash +# Create an organization metric alert rule +sentry alert metrics create my-org \ + --name "P95 Latency" \ + --query "environment:prod" \ + --aggregate "p95(span.duration)" \ + --dataset spans \ + --time-window 5 \ + --trigger '{"alertThreshold":500,"actions":[{"id":"sentry.mail.actions.NotifyEmailAction","targetType":"Team","targetIdentifier":1}]}' +``` + ### `sentry alert metrics delete ` Delete a metric alert rule @@ -111,6 +185,13 @@ Delete a metric alert rule - `-f, --force - Force the operation without confirmation` - `-n, --dry-run - Show what would happen without making changes` +**Examples:** + +```bash +# Delete without prompt +sentry alert metrics delete my-org/67890 --yes +``` + ### `sentry alert metrics edit ` Edit a metric alert rule @@ -127,4 +208,11 @@ Edit a metric alert rule - `--environment - Environment value (pass empty string to clear)` - `--owner - Owner value (pass empty string to clear)` +**Examples:** + +```bash +# Edit metric alert query/window +sentry alert metrics edit my-org/67890 --query "environment:prod event.type:error" --time-window 15 +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/api.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/api.md index 125af2a971..3d2c55038c 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/api.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/api.md @@ -26,4 +26,46 @@ Make an authenticated API request - `--verbose - Include full HTTP request and response in the output` - `-n, --dry-run - Show the resolved request without sending it` +**Examples:** + +```bash +# List organizations +sentry api organizations/ + +# Get a specific issue +sentry api issues/123456789/ + +# Create a release +sentry api organizations/my-org/releases/ \ + -X POST -F version=1.0.0 + +# With inline JSON body +sentry api issues/123456789/ \ + -X POST -d '{"status": "resolved"}' + +# Update an issue status +sentry api issues/123456789/ \ + -X PUT -F status=resolved + +# Assign an issue +sentry api issues/123456789/ \ + -X PUT --field assignedTo="user@example.com" + +sentry api projects/my-org/my-project/ -X DELETE + +# Add custom headers +sentry api organizations/ -H "X-Custom: value" + +# Read body from a file +sentry api projects/my-org/my-project/releases/ -X POST --input release.json + +# Verbose mode (shows full HTTP request/response) +sentry api organizations/ --verbose + +# Preview the request without sending +sentry api organizations/ --dry-run + +sentry api "projects/my-org/my-project/events/EVENT_ID/attachments/ATTACHMENT_ID/?download=1" > screenshot.png +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/auth.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/auth.md index 99289ebf4f..746f8ef333 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/auth.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/auth.md @@ -23,10 +23,34 @@ Authenticate with Sentry - `--read-only - Request only read-only OAuth scopes (project:read, org:read, event:read, member:read, team:read). Useful for handing tokens to AI agents or CI jobs that should not be able to mutate Sentry state.` - `-s, --scope ... - Request specific OAuth scopes (repeatable, comma-separated). E.g. --scope project:read --scope org:read. Overrides the default scope set.` +**Examples:** + +```bash +sentry auth + +sentry auth --token YOUR_SENTRY_API_TOKEN + +sentry auth --read-only + +sentry auth --scope project:read --scope org:read +sentry auth --scope project:read,event:read + +sentry auth --url https://sentry.example.com +SENTRY_URL=https://sentry.example.com sentry auth + +sentry auth --token YOUR_TOKEN --url https://sentry.example.com +``` + ### `sentry auth logout` Log out of Sentry +**Examples:** + +```bash +sentry auth logout +``` + ### `sentry auth refresh` Refresh your OAuth access token @@ -36,6 +60,18 @@ Refresh your OAuth access token - `--read-only - Re-authenticate with read-only OAuth scopes (project:read, org:read, event:read, member:read, team:read)` - `-s, --scope ... - Re-authenticate with specific OAuth scopes (repeatable, comma-separated). E.g. --scope project:read --scope org:read` +**Examples:** + +```bash +sentry auth refresh + +# Refresh with read-only scopes +sentry auth refresh --read-only + +# Refresh with specific scopes +sentry auth refresh --scope project:read --scope org:read +``` + ### `sentry auth status` View authentication status @@ -44,10 +80,28 @@ View authentication status - `--show-token - Show the stored token (masked by default)` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` +**Examples:** + +```bash +sentry auth status + +# Show the raw token +sentry auth status --show-token + +# View current user +sentry auth whoami +``` + ### `sentry auth token` Print the stored authentication token +**Examples:** + +```bash +sentry auth token +``` + ### `sentry auth whoami` Show the currently authenticated identity diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/build.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/build.md index 8ccabe4616..93a1399eda 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/build.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/build.md @@ -37,4 +37,33 @@ Download a build artifact **Flags:** - `-o, --output - Output path (default: preprod_artifact_. in the current directory)` +**Examples:** + +```bash +# Upload an Android build (APK or AAB) for size analysis +sentry build upload ./app-release.apk + +# Upload an iOS build (XCArchive directory or IPA) +sentry build upload ./MyApp.xcarchive +sentry build upload ./MyApp.ipa + +# Upload with a build configuration and release notes +sentry build upload ./app.aab --build-configuration Release --release-notes "Nightly" + +# Tag a build with install groups (repeatable) +sentry build upload ./app.aab --install-group qa --install-group beta + +# Attach explicit git metadata (otherwise auto-collected in CI) +sentry build upload ./app.aab --head-sha "$GIT_SHA" --pr-number 42 --base-ref main + +# Download a build artifact by ID +sentry build download 1234567890 + +# Download to a specific path +sentry build download 1234567890 --output ./app.ipa + +# Output the result as JSON +sentry build download 1234567890 --json +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/cli.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/cli.md index 8102908724..05510325ce 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/cli.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/cli.md @@ -15,6 +15,18 @@ CLI-related commands Print the shell completion script +**Examples:** + +```bash +# Print completions for your current shell (auto-detected from $SHELL) +sentry cli completion + +# Generate for a specific shell +sentry cli completion zsh > ~/.local/share/zsh/site-functions/_sentry +eval "$(sentry cli completion bash)" +sentry cli completion fish > ~/.config/fish/completions/sentry.fish +``` + ### `sentry cli defaults ` View and manage default settings @@ -24,10 +36,51 @@ View and manage default settings - `-y, --yes - Skip confirmation prompt` - `-f, --force - Force the operation without confirmation` +**Examples:** + +```bash +# Show all current defaults +sentry cli defaults + +# Set default organization +sentry cli defaults org my-org + +# Set default project +sentry cli defaults project my-project + +# Set default Sentry URL (self-hosted) +sentry cli defaults url https://sentry.example.com + +# Set custom HTTP headers (self-hosted, e.g. for IAP/proxies) +sentry cli defaults headers "X-IAP: token" + +# Set a custom CA certificate (self-hosted, behind a TLS proxy) +sentry cli defaults ca-cert /path/to/ca.pem + +# Disable telemetry +sentry cli defaults telemetry off + +# Clear a single default +sentry cli defaults org --clear + +# Clear all defaults +sentry cli defaults --clear +``` + ### `sentry cli feedback ` Send feedback about the CLI +**Examples:** + +```bash +# Send positive feedback +sentry cli feedback i love this tool + +# Report an issue +sentry cli feedback the issue view is confusing +``` + ### `sentry cli fix` Diagnose and repair CLI database issues @@ -35,6 +88,12 @@ Diagnose and repair CLI database issues **Flags:** - `--dry-run - Show what would be fixed without making changes` +**Examples:** + +```bash +sentry cli fix +``` + ### `sentry cli import` Import settings from legacy .sentryclirc files @@ -45,6 +104,25 @@ Import settings from legacy .sentryclirc files - `--url - Explicitly trust this URL (bypasses same-file trust check)` - `--skip-validation - Skip token validation against the Sentry API` +**Examples:** + +```bash +# Auto-detect and import .sentryclirc +sentry cli import + +# Preview what would be imported +sentry cli import --dry-run + +# Skip confirmation prompt +sentry cli import --yes + +# Explicitly trust a self-hosted URL +sentry cli import --url https://sentry.example.com + +# Skip API validation of the imported token +sentry cli import --skip-validation +``` + ### `sentry cli setup` Configure shell integration @@ -58,6 +136,19 @@ Configure shell integration - `--no-agent-skills - Skip agent skill installation for AI coding assistants` - `--quiet - Suppress output (for scripted usage)` +**Examples:** + +```bash +# Run full setup (PATH, completions, agent skills) +sentry cli setup + +# Skip agent skill installation +sentry cli setup --no-agent-skills + +# Skip PATH and completion modifications +sentry cli setup --no-modify-path --no-completions +``` + ### `sentry cli uninstall` Uninstall Sentry CLI @@ -68,6 +159,19 @@ Uninstall Sentry CLI - `-f, --force - Force the operation without confirmation` - `-n, --dry-run - Show what would happen without making changes` +**Examples:** + +```bash +# Show what would be removed (dry run) +sentry cli uninstall --dry-run + +# Uninstall, keeping config directory +sentry cli uninstall --yes --keep-config + +# Full uninstall with confirmation +sentry cli uninstall +``` + ### `sentry cli upgrade ` Update the Sentry CLI to the latest version @@ -79,4 +183,25 @@ Update the Sentry CLI to the latest version - `--no-agent-skills - Skip agent skill installation for AI coding assistants` - `--method - Installation method to use (curl, brew, npm, pnpm, bun, yarn)` +**Examples:** + +```bash +sentry cli upgrade --check + +# Upgrade to latest stable +sentry cli upgrade + +# Upgrade to a specific version +sentry cli upgrade 0.5.0 + +# Force re-download +sentry cli upgrade --force + +# Switch to nightly builds +sentry cli upgrade nightly + +# Switch back to stable +sentry cli upgrade stable +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/code-mappings.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/code-mappings.md index ef41dcd35e..47b0d12f03 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/code-mappings.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/code-mappings.md @@ -19,4 +19,20 @@ Upload code mappings for stack trace linking - `--repo - Repository name (e.g., owner/repo). Auto-detected from git remote if omitted.` - `--default-branch - Default branch name. Auto-detected from git remote HEAD if omitted.` +**Examples:** + +```bash +# Upload code mappings from a JSON file +sentry code-mappings upload mappings.json + +# Specify repository explicitly +sentry code-mappings upload mappings.json --repo owner/repo + +# Specify repository and default branch +sentry code-mappings upload mappings.json --repo owner/repo --default-branch develop + +# Output as JSON +sentry code-mappings upload mappings.json --json +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dart-symbol-map.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dart-symbol-map.md index e5bc7e9e4a..27d6b1f945 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dart-symbol-map.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dart-symbol-map.md @@ -19,4 +19,17 @@ Upload a Dart/Flutter symbol map to Sentry - `-d, --debug-id - Debug ID (UUID) from the companion native debug file` - `--no-upload - Validate the file without uploading (dry-run)` +**Examples:** + +```bash +# Upload a dart symbol map with a debug ID +sentry dart-symbol-map upload --debug-id 12345678-1234-1234-1234-123456789abc mapping.json + +# Validate without uploading +sentry dart-symbol-map upload --debug-id 12345678-1234-1234-1234-123456789abc mapping.json --no-upload + +# Output as JSON +sentry dart-symbol-map upload --debug-id 12345678-1234-1234-1234-123456789abc mapping.json --json +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md index b95939120f..05753b22db 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/dashboard.md @@ -21,6 +21,19 @@ List dashboards - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `-c, --cursor - Navigate pages: "next", "prev", "first" (or raw cursor string)` +**Examples:** + +```bash +# List all dashboards +sentry dashboard list + +# Filter by name pattern +sentry dashboard list "Backend*" + +# Open dashboard list in browser +sentry dashboard list -w +``` + ### `sentry dashboard view ` View a dashboard @@ -33,10 +46,32 @@ View a dashboard - `--renderer - Graphics renderer (defaults to auto; falls back to auto when unavailable) - (default: "auto")` - `--no-graphics-cap - Use the terminal-native graphics width (may fall back to ASCII for very large dashboards)` +**Examples:** + +```bash +# View by title +sentry dashboard view 'Frontend Performance' + +# View by ID +sentry dashboard view 12345 + +# Auto-refresh every 30 seconds +sentry dashboard view "Backend Performance" --refresh 30 + +# Open in browser +sentry dashboard view 12345 -w +``` + ### `sentry dashboard create ` Create a dashboard +**Examples:** + +```bash +sentry dashboard create 'Frontend Performance' +``` + ### `sentry dashboard widget add ` Add a widget to a dashboard @@ -55,6 +90,31 @@ Add a widget to a dashboard - `--height - Widget height in grid rows (min 1)` - `-l, --layout - Layout mode: sequential (append in order) or dense (fill gaps) - (default: "sequential")` +**Examples:** + +```bash +# Simple counter widget +sentry dashboard widget add 'My Dashboard' "Error Count" \ + --display big_number --query count + +# Line chart with group-by +sentry dashboard widget add 'My Dashboard' "Errors by Browser" \ + --display line --query count --group-by browser.name + +# Table with multiple aggregates, sorted descending +sentry dashboard widget add 'My Dashboard' "Top Endpoints" \ + --display table \ + --query count --query p95:span.duration \ + --group-by transaction \ + --sort -count --limit 10 + +# With search filter +sentry dashboard widget add 'My Dashboard' "Slow Requests" \ + --display bar --query p95:span.duration \ + --where "span.op:http.client" \ + --group-by span.description +``` + ### `sentry dashboard widget edit ` Edit a widget in a dashboard @@ -75,6 +135,19 @@ Edit a widget in a dashboard - `--width - Widget width in grid columns (1–6)` - `--height - Widget height in grid rows (min 1)` +**Examples:** + +```bash +# Change display type +sentry dashboard widget edit 12345 --title 'Error Count' --display bar + +# Rename a widget +sentry dashboard widget edit 'My Dashboard' --index 0 --new-title 'Total Errors' + +# Change the query +sentry dashboard widget edit 12345 --title 'Error Rate' --query p95:span.duration +``` + ### `sentry dashboard widget delete ` Delete a widget from a dashboard @@ -86,6 +159,16 @@ Delete a widget from a dashboard - `-f, --force - Force the operation without confirmation` - `-n, --dry-run - Show what would happen without making changes` +**Examples:** + +```bash +# Delete by title +sentry dashboard widget delete 'My Dashboard' --title 'Error Count' + +# Delete by index +sentry dashboard widget delete 12345 --index 2 +``` + ### `sentry dashboard revisions ` List dashboard revisions @@ -94,6 +177,19 @@ List dashboard revisions - `-n, --limit - Maximum number of revisions to list - (default: "25")` - `-c, --cursor - Navigate pages: "next", "prev", "first" (or raw cursor string)` +**Examples:** + +```bash +# List revisions by dashboard title +sentry dashboard revisions 'Frontend Performance' + +# List revisions by dashboard ID +sentry dashboard revisions 12345 + +# With explicit org +sentry dashboard revisions my-org 12345 +``` + ### `sentry dashboard restore ` Restore a dashboard revision @@ -101,4 +197,17 @@ Restore a dashboard revision **Flags:** - `-r, --revision - Revision ID to restore` +**Examples:** + +```bash +# Restore by dashboard title and revision number +sentry dashboard restore 'Frontend Performance' --revision 3 + +# Restore by dashboard ID +sentry dashboard restore 12345 --revision 1 + +# With explicit org +sentry dashboard restore my-org 12345 --revision 1 +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/debug-files.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/debug-files.md index 567710bf17..3bc053f70a 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/debug-files.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/debug-files.md @@ -64,4 +64,54 @@ Create a JVM source bundle for source context - `-d, --debug-id - Debug ID (UUID) to stamp on the bundle` - `-e, --exclude ... - Additional directory names to exclude (repeatable)` +**Examples:** + +```bash +# Inspect a debug information file (auto-detects the format) +sentry debug-files check ./libexample.so +sentry debug-files check MyApp.dSYM/Contents/Resources/DWARF/MyApp +sentry debug-files check ./app.pdb --json + +# List the source files a debug file references (and whether they're available) +sentry debug-files print-sources ./libexample.so +sentry debug-files print-sources ./app.pdb --json + +# Locate debug files for one or more debug identifiers on disk +sentry debug-files find +sentry debug-files find --type dsym --path ./build +sentry debug-files find --no-cwd --no-well-known -p /symbols --json + +# Bundle a debug file's referenced source files (run on the build machine) +sentry debug-files bundle-sources ./libexample.so +sentry debug-files bundle-sources ./app.pdb --output ./app.src.zip + +# Bundle JVM sources with a debug ID +sentry debug-files bundle-jvm --output ./out --debug-id ./src + +# Exclude additional directories +sentry debug-files bundle-jvm --output ./out --debug-id --exclude generated --exclude build-tools ./src + +# Output as JSON +sentry debug-files bundle-jvm --output ./out --debug-id --json ./src + +# Upload debug information files (scans directories recursively) +sentry debug-files upload ./build +sentry debug-files upload ./libexample.so --include-sources + +# .zip archives are scanned in place; use --no-zips to skip them +sentry debug-files upload ./symbols.zip +sentry debug-files upload ./build --no-zips + +# Restrict by type or debug id, and wait for server-side processing +sentry debug-files upload ./dsyms --type dsym --wait +sentry debug-files upload ./build --id --require-all + +# Unity: upload IL2CPP line mappings (optionally with referenced C# sources) +sentry debug-files upload ./build --il2cpp-mapping +sentry debug-files upload ./build --il2cpp-mapping --include-sources + +# Preview what would be uploaded without uploading (no credentials needed) +sentry debug-files upload ./build --no-upload +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/docs.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/docs.md index 250e8773d0..3713ec9e64 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/docs.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/docs.md @@ -18,8 +18,20 @@ Find Sentry documentation pages by keyword **Flags:** - `-n, --limit - Maximum matches to return (1-20) - (default: "8")` +**Examples:** + +```bash +sentry docs list "source maps" +``` + ### `sentry docs query ` Ask a cited question about Sentry documentation +**Examples:** + +```bash +sentry docs "How do I configure tracing in Next.js?" +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/event.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/event.md index 0a62929a20..adce7c3e29 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/event.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/event.md @@ -20,6 +20,15 @@ View details of one or more events - `--spans - Span tree depth limit (number, "all" for unlimited, "no" to disable) - (default: "3")` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` +**Examples:** + +```bash +sentry event view abc123def456abc123def456abc12345 + +# Open in browser +sentry event view abc123def456abc123def456abc12345 -w +``` + ### `sentry event list ` List events for an issue @@ -52,6 +61,32 @@ List events for an issue | `crashFile` | string \| null | Crash file URL | | `metadata` | object | Event metadata | +**Examples:** + +```bash +# List events for an issue (using short ID) +sentry event list PROJ-ABC + +# List events for an issue (using numeric ID) +sentry event list 123456789 + +# Filter by search query +sentry event list PROJ-ABC --query "browser:Chrome" + +# Include full event bodies (stacktraces) +sentry event list PROJ-ABC --full + +# Limit results and time range +sentry event list PROJ-ABC --limit 50 --period 24h + +# Paginate through results +sentry event list PROJ-ABC -c next +sentry event list PROJ-ABC -c prev + +# Output as JSON +sentry event list PROJ-ABC --json +``` + ### `sentry event send ` Send a Sentry event @@ -75,4 +110,39 @@ Send a Sentry event - `--with-categories - Parse 'CATEGORY: message' prefixes from logfile breadcrumbs` - `--raw - Send file contents as-is without parsing` +**Examples:** + +```bash +# Send an error event (default level) +sentry event send -m "Something went wrong" + +# Specify level, release, and environment +sentry event send -m "Deploy check" -l info -r 1.0.0 -E production + +# Add tags and extra data +sentry event send -m "Payment failed" --tag env:prod --tag region:us-east --extra amount:99.99 + +# Set user context +sentry event send -m "Login error" --user id:42 --user email:alice@example.com + +# Custom fingerprint to group related events together +sentry event send -m "DB timeout" --fingerprint db-timeout --fingerprint {{ default }} + +# Send a serialized Sentry Event object +sentry event send ./crash.json + +# Send without re-parsing (raw mode — also supports pre-built envelopes) +sentry event send --raw ./crash.json +sentry event send --raw ./captured.envelope + +# Explicit DSN +sentry event send -m "Test" --dsn "https://key@o123.ingest.us.sentry.io/456" + +# Via environment variable +export SENTRY_DSN="https://key@o123.ingest.us.sentry.io/456" +sentry event send -m "Test" + +sentry send-event # same as: sentry event send +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md index 71ed5eef9b..9e2338cfcd 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/explore.md @@ -28,4 +28,61 @@ Query aggregate event data (Explore) - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `-c, --cursor - Navigate pages: "next", "prev", "first" (or raw cursor string)` +**Examples:** + +```bash +# Top errors in the last 24 hours, scoped to a project +sentry explore my-org/cli + +# All projects in an org +sentry explore my-org/ + +# Bare project slug (searches across orgs) +sentry explore cli + +# Auto-detect from DSN/config +sentry explore + +# Errors with user impact for a specific UTC window +sentry explore my-org/cli -F title -F "count()" -F "count_unique(user)" \ + --period "2024-01-15T00:00:00Z/2024-01-16T00:00:00Z" + +# Filter by specific error type (combines with auto-injected project filter) +sentry explore my-org/cli -F title -F "count()" \ + -q "error.type:TypeError" --period 1h + +# Span operation latency by route +sentry explore my-org/cli -F span.op -F "p50(span.duration)" \ + -F "p95(span.duration)" --dataset spans --period 1h + +# Top spans by count +sentry explore my-org/cli -F span.op -F "count()" \ + --dataset spans --sort "-count()" + +# Sum a custom metric (e.g., LLM token usage) across an org +sentry explore my-org/ -m llm.token_usage --dataset metrics --period 7d + +# Break down by a tag column (e.g., model name) +sentry explore my-org/seer -F gen_ai.request.model \ + -m llm.token_usage --dataset metrics --period 7d + +# Use a different aggregation (default is sum) +sentry explore my-org/ -m cache.hit_rate --agg avg --dataset metrics + +sentry explore my-org/ \ + -F "sum(value,llm.token_usage,distribution,none)" \ + --dataset metrics --period 7d + +# Log severity counts in the last hour +sentry explore my-org/cli -F severity -F "count()" \ + --dataset logs --period 1h + +# Pipe to jq for filtering +sentry explore my-org/cli -F title -F "count()" --json | jq '.data[:5]' + +# Get raw data for analysis +sentry explore my-org/cli -F title -F "count()" -F "count_unique(user)" \ + --json --limit 100 +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/feedback.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/feedback.md index 53229e4013..0e5397ec8c 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/feedback.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/feedback.md @@ -51,6 +51,27 @@ List and search User Feedback | `hasSeen` | boolean | Whether the feedback has been read | | `latestEventHasAttachments` | boolean | Whether the latest event has attachments | +**Examples:** + +```bash +# Auto-detect the organization from the current project +sentry feedback list + +# List Feedback for one project +sentry feedback list my-org/frontend + +# List Feedback across every project in an organization +sentry feedback list my-org/ + +# Search for a project across accessible organizations +sentry feedback list frontend + +sentry feedback list my-org/frontend --status resolved +sentry feedback list my-org/frontend --status spam +sentry feedback list my-org/frontend --status all --period 90d +sentry feedback list my-org/frontend --query "message:*checkout*" +``` + ### `sentry feedback view ` View a User Feedback item @@ -91,4 +112,26 @@ View a User Feedback item | `replayIds` | array | Related Session Replay IDs | | `attachments` | array | Attachments on the latest feedback event | +**Examples:** + +```bash +# Most recent unresolved Feedback, with detected or explicit organization +sentry feedback view @latest +sentry feedback view my-org/@latest + +# Short ID or numeric ID +sentry feedback view FRONTEND-2SDJ +sentry feedback view 5146636313 + +# Explicit organization +sentry feedback view my-org/FRONTEND-2SDJ + +# `view` is the default command; `show` is an alias +sentry feedback my-org/FRONTEND-2SDJ +sentry feedback show my-org/FRONTEND-2SDJ + +# Open the Feedback item in Sentry +sentry feedback view my-org/FRONTEND-2SDJ --web +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/info.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/info.md index 5f37751c40..9cab963ee7 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/info.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/info.md @@ -19,4 +19,17 @@ Print configuration and verify authentication - `--config-status-json - Emit configuration + auth status as JSON (for external tooling); always exits 0` - `--no-defaults - Verify only authentication, without requiring a default org/project` +**Examples:** + +```bash +# Print the resolved config and verify authentication +sentry info + +# Verify only authentication (don't require a default org/project) +sentry info --no-defaults + +# Machine-readable status for external tooling (always exits 0) +sentry info --config-status-json +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/init.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/init.md index 18ec87f3b4..50c0efe039 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/init.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/init.md @@ -23,4 +23,32 @@ Initialize Sentry in your project (experimental) - `--app - App to initialize in a monorepo (required with --yes when multiple apps are detected)` - `--tui - Use the Ink-based interactive UI (default). Pass --no-tui to fall back to plain log output.` +**Examples:** + +```bash +# Interactive setup +sentry init + +# Non-interactive agent/CI setup +sentry init --yes --features errors,tracing,replay + +# Dry run to preview changes +sentry init --dry-run + +# Target a subdirectory +sentry init ./my-app + +# Use a specific org (auto-detect project) +sentry init acme/ + +# Use a specific org and project +sentry init acme/my-app + +# Assign a team when creating a new project +sentry init acme/ --team backend + +# Enable specific features +sentry init --features profiling,replay +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md index bbc48d7a51..d1c554d385 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md @@ -48,6 +48,42 @@ List issues in a project | `isUnhandled` | boolean | Whether the issue is unhandled | | `seerFixabilityScore` | number \| null | Seer AI fixability score (0-1) | +**Examples:** + +```bash +# List issues in a specific project +sentry issue list my-org/frontend + +# All projects in an org +sentry issue list my-org/ + +# Search for a project across organizations +sentry issue list frontend + +# Show only unresolved issues +sentry issue list my-org/frontend --query "is:unresolved" + +# Show resolved issues +sentry issue list my-org/frontend --query "is:resolved" + +# Sort by frequency +sentry issue list my-org/frontend --sort freq --limit 20 + +# Sort by Sentry's "recommended" relevance ranking (the default on sentry.io; +# self-hosted instances default to "date" and require a recent Sentry version +# to accept --sort recommended) +sentry issue list my-org/frontend --sort recommended + +# Multiple filters (space-separated = implicit AND) +sentry issue list --query "is:unresolved level:error assigned:me" + +# Negation and wildcards +sentry issue list --query "!browser:Chrome message:*timeout*" + +# Match multiple values for one key (in-list syntax) +sentry issue list --query "browser:[Chrome,Firefox]" +``` + ### `sentry issue events ` List events for a specific issue @@ -80,6 +116,25 @@ List events for a specific issue | `crashFile` | string \| null | Crash file URL | | `metadata` | object | Event metadata | +**Examples:** + +```bash +# List recent events for an issue +sentry issue events FRONT-ABC + +# Filter events by search query +sentry issue events FRONT-ABC --query "browser:Chrome" + +# Show full event details +sentry issue events FRONT-ABC --full + +# Limit results and filter by time period +sentry issue events FRONT-ABC --limit 50 --period 24h + +# Paginate through results +sentry issue events FRONT-ABC -c next +``` + ### `sentry issue explain ` Analyze an issue's root cause using Seer AI @@ -88,6 +143,34 @@ Analyze an issue's root cause using Seer AI - `--force - Force new analysis even if one exists` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` +**Examples:** + +```bash +# View the most recent issue +sentry issue view @latest + +# Explain the most frequently occurring issue +sentry issue explain @most_frequent + +# Generate a fix plan for the latest issue +sentry issue plan @latest + +# Analyze root cause (may take a few minutes for new issues) +sentry issue explain 123456789 + +# By short ID with org prefix +sentry issue explain my-org/MYPROJECT-ABC + +# Force a fresh analysis +sentry issue explain 123456789 --force + +# Generate a fix plan (automatically runs explain if needed) +sentry issue plan 123456789 + +# Force a fresh plan even if one already exists +sentry issue plan 123456789 --force +``` + ### `sentry issue plan ` Generate a solution plan using Seer AI @@ -133,6 +216,42 @@ View details of a specific issue | `replayIds` | array | Related Session Replay IDs | | `trace` | object \| null | Trace context from the latest event's span tree | +**Examples:** + +```bash +sentry issue view FRONT-ABC + +# Open in browser +sentry issue view FRONT-ABC -w + +# GitHub-style identifiers work too (the "#" replaces the final slash) +sentry issue view my-org/my-project#FRONT-ABC +sentry issue view my-project#FRONT-ABC + +# Full JSON (issue fields + latest event + trace/replay context) +sentry issue view FRONT-ABC --json + +# Select specific top-level fields to keep output small +sentry issue view FRONT-ABC --json --fields shortId,title,culprit,count,userCount,permalink + +# Pull named fields off the latest event instead of the whole `event` object — +# the event's `request` entry can include live session data (cookies, headers, +# body), so extract only what you need +sentry issue view FRONT-ABC --json --fields event.id,event.title,event.dateCreated + +# Issue summary +sentry issue view FRONT-ABC --json | jq '{shortId, title, count, userCount, permalink}' + +# Latest event id + culprit +sentry issue view FRONT-ABC --json | jq '{event: .event.id, culprit}' + +# Just the request URL and method (avoids the full request/session blob) +sentry issue view FRONT-ABC --json | jq '.event.entries[] | select(.type == "request") | .data | {url, method}' + +# Exception type and value from the latest event +sentry issue view FRONT-ABC --json | jq '.event.entries[] | select(.type == "exception") | .data.values[0] | {type, value}' +``` + ### `sentry issue resolve ` Mark an issue as resolved @@ -144,6 +263,35 @@ Mark an issue as resolved Reopen a resolved issue +**Examples:** + +```bash +# Resolve immediately (no regression tracking) +sentry issue resolve CLI-G5 + +# Resolve in a specific release — future events on newer releases are +# regression-flagged +sentry issue resolve CLI-G5 --in 0.26.1 + +# Monorepo-style releases work too (no special parsing) +sentry issue resolve CLI-G5 --in spotlight@1.2.3 + +# Resolve in the next release (tied to current HEAD) +sentry issue resolve CLI-G5 --in @next +sentry issue resolve CLI-G5 -i @next + +# Resolve in the current git HEAD — auto-detects the Sentry repo from +# your git origin remote (hard-errors if it can't) +sentry issue resolve CLI-G5 --in @commit + +# Explicit commit + repo (no git inspection; repo must be registered in Sentry) +sentry issue resolve CLI-G5 --in @commit:getsentry/cli@abc123def + +# Reopen a resolved issue +sentry issue unresolve CLI-G5 +sentry issue reopen CLI-G5 # alias +``` + ### `sentry issue archive ` Archive (ignore) an issue @@ -151,6 +299,37 @@ Archive (ignore) an issue **Flags:** - `-u, --until - Condition for unarchival: forever, auto, 30m, 10x, 10u, 10x/5m, etc.` +**Examples:** + +```bash +# Archive forever (fully silenced) +sentry issue archive CLI-G5 + +# Smart detection — unarchives when Sentry detects a spike in event frequency +sentry issue archive CLI-G5 --until auto + +# Duration-based +sentry issue archive CLI-G5 --until 1h # 1 hour +sentry issue archive CLI-G5 --until 7d # 7 days +sentry issue archive CLI-G5 --until 2026-12-31 # specific date + +# Count-based — unarchive after N more events +sentry issue archive CLI-G5 --until 100x + +# User-based — unarchive after N more users affected +sentry issue archive CLI-G5 --until 10u + +# Compound — count within a time window +sentry issue archive CLI-G5 --until 100x/1h # 100 events within 1 hour +sentry issue archive CLI-G5 --until 10u/1d # 10 users within 1 day + +# Verbose forms also work +sentry issue archive CLI-G5 --until 10events/2hours + +# 'ignore' is an alias for 'archive' +sentry issue ignore CLI-G5 --until auto +``` + ### `sentry issue merge ` Merge 2+ issues into a single canonical group @@ -158,4 +337,20 @@ Merge 2+ issues into a single canonical group **Flags:** - `-i, --into - Prefer this issue as the canonical parent (included in the merge if not already listed)` +**Examples:** + +```bash +# Let Sentry auto-pick the parent (typically the largest by event count) +sentry issue merge CLI-K9 CLI-15H CLI-15N + +# Pin the canonical parent explicitly — accepts the same formats as +# positional args, including org-qualified and project-alias forms +sentry issue merge CLI-K9 CLI-15H CLI-15N --into CLI-K9 +sentry issue merge my-org/CLI-K9 my-org/CLI-15H --into my-org/CLI-K9 +sentry issue merge cli-k9 cli-15h --into cli-k9 # alias form + +# Cross-org merges are rejected — all issues must share an organization +# Non-error issue types (performance, info, etc.) cannot be merged +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/local.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/local.md index ef7f2760ac..062e5b9d94 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/local.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/local.md @@ -33,4 +33,31 @@ Run a command with the local dev server enabled - `-V, --verify - Verify SDK sends events, then exit` - `-t, --timeout - Kill the child after N seconds (0 = no timeout; defaults to 30 s in --verify mode) - (default: "0")` +**Examples:** + +```bash +# Start the server and tail events (default) +sentry local + +# Run your app with the local server auto-enabled +sentry local run -- npm run dev +sentry local run -- python manage.py runserver + +# Use a custom port +sentry local --port 9000 + +# Only show errors and logs (filter out transactions) +sentry local -f error -f log + +# Run quietly (suppress per-envelope tail output) +sentry local --quiet + +sentry local -f error -f log # only errors and logs + +sentry local -f ai # only AI/agent spans +sentry local -f ai -f error # agent spans and errors + +sentry local --format json +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/log.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/log.md index 6a4613ca3c..28d0d0eabb 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/log.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/log.md @@ -34,6 +34,33 @@ List logs from a project | `severity` | string \| null | Severity level (error, warning, info, debug) | | `trace` | string \| null | Trace ID for correlation | +**Examples:** + +```bash +# List last 100 logs (default) +sentry log list + +# Show only error logs +sentry log list -q 'severity:error' + +# Filter by message content +sentry log list -q 'database' + +# Limit results +sentry log list --limit 50 + +# Stream with default 2-second poll interval +sentry log list -f + +# Stream with custom 5-second poll interval +sentry log list -f 5 + +# Stream error logs from a specific project +sentry log list my-org/backend -f -q 'severity:error' + +sentry log list --json | jq '.data[] | select(.severity == "error")' +``` + ### `sentry log view ` View details of one or more log entries @@ -42,4 +69,16 @@ View details of one or more log entries - `-w, --web - Open in browser` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` +**Examples:** + +```bash +sentry log view 968c763c740cfda8b6728f27fb9e9b01 + +# With explicit project +sentry log view my-org/backend 968c763c740cfda8b6728f27fb9e9b01 + +# Open in browser +sentry log view 968c763c740cfda8b6728f27fb9e9b01 -w +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/monitor.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/monitor.md index 3044403459..822cc8440f 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/monitor.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/monitor.md @@ -47,4 +47,27 @@ List cron monitors | `dateCreated` | string | Creation date (ISO 8601) | | `project` | object | Owning project | +**Examples:** + +```bash +# Wrap a command with cron monitor check-ins (DSN-based) +SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0 \ + sentry monitor run nightly-job -- python manage.py cron + +# The -- separator is optional when the command has no flags +sentry monitor run nightly-job npm run task + +# Create/update the monitor on the first check-in via --schedule (crontab) +sentry monitor run nightly-job -s "0 0 * * *" --max-runtime 30 --timezone UTC -- ./backup.sh + +# List cron monitors in an org +sentry monitor list my-org/ + +# Paginate through monitors +sentry monitor list my-org/ -c next + +# Output as JSON +sentry monitor list --json +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/org.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/org.md index b9fbff0fe3..fc8fd64dc3 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/org.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/org.md @@ -27,4 +27,20 @@ View details of an organization - `-w, --web - Open in browser` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` +**Examples:** + +```bash +# List organizations +sentry org list + +# View organization details +sentry org view my-org + +# Open in browser +sentry org view my-org -w + +# JSON output +sentry org list --json +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/platform.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/platform.md index 636d74b456..268555e791 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/platform.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/platform.md @@ -18,4 +18,20 @@ List all valid Sentry platform identifiers **Flags:** - `-q, --search - Filter platforms by substring` +**Examples:** + +```bash +# List all valid Sentry platform identifiers +sentry platform list + +# Filter by substring +sentry platform list --search python + +# Shortcut for `sentry platform list` +sentry platforms + +# Output as JSON +sentry platform list --json +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/proguard.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/proguard.md index 8ae6e0cbde..ad99c15e41 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/proguard.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/proguard.md @@ -24,4 +24,14 @@ Upload ProGuard/R8 mapping files to Sentry Compute the UUID for a ProGuard mapping file +**Examples:** + +```bash +# Compute the UUID for a ProGuard/R8 mapping file +sentry proguard uuid ./app/build/outputs/mapping/release/mapping.txt + +# Output as JSON (includes the file path) +sentry proguard uuid mapping.txt --json +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/project.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/project.md index d1be5a24f9..ce3ed2f8fe 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/project.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/project.md @@ -19,6 +19,23 @@ Create one or more projects - `-t, --team - Team to create the project under` - `-n, --dry-run - Show what would happen without making changes` +**Examples:** + +```bash +# Every project is a name:platform pair; project names cannot contain whitespace +# Create a new project +sentry project create my-new-app:javascript-nextjs + +# Create several projects with their own platforms +sentry project create web:javascript api:python-django worker:node + +# Create under a specific org and team +sentry project create my-org/my-new-app:python --team backend-team + +# Preview without creating +sentry project create my-new-app:node --dry-run +``` + ### `sentry project delete ` Delete a project @@ -28,6 +45,16 @@ Delete a project - `-f, --force - Force the operation without confirmation` - `-n, --dry-run - Show what would happen without making changes` +**Examples:** + +```bash +# Delete a project (will prompt for confirmation) +sentry project delete my-org/old-project + +# Delete without confirmation +sentry project delete my-org/old-project --yes +``` + ### `sentry project list ` List projects @@ -46,4 +73,20 @@ View details of a project - `-w, --web - Open in browser` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` +**Examples:** + +```bash +# List all projects in an org +sentry project list my-org/ + +# Filter by platform +sentry project list my-org/ --platform javascript + +# View project details +sentry project view my-org/frontend + +# Open project in browser +sentry project view my-org/frontend -w +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/react-native.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/react-native.md index 6cd8b45764..ca182269ab 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/react-native.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/react-native.md @@ -38,4 +38,23 @@ Upload React Native sourcemaps (Xcode build step) - `--no-auto-release - Don't read the release from Xcode project files` - `--allow-xcode-infoplist-preprocessing - Run the C preprocessor over Info.plist (INFOPLIST_PREPROCESS)` +**Examples:** + +```bash +# Upload a bundle + sourcemap by debug ID (called by the Gradle plugin) +sentry react-native gradle \ + --bundle index.android.bundle \ + --sourcemap index.android.bundle.map + +# Also associate with a release and distribution(s) +sentry react-native gradle \ + --bundle index.android.bundle \ + --sourcemap index.android.bundle.map \ + --release com.example.app@1.0.0 \ + --dist 1000 + +# Xcode build phase (usually added automatically to your build script) +../node_modules/.bin/sentry-cli react-native xcode +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/release.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/release.md index b996bfdc19..8c8da262ad 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/release.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/release.md @@ -106,4 +106,63 @@ Set commits for a release Propose a release version +**Examples:** + +```bash +# List releases (auto-detect org) +sentry release list + +# List releases in a specific org +sentry release list my-org/ + +# View release details +sentry release view 1.0.0 +sentry release view my-org/1.0.0 + +# Create and finalize a release +sentry release create 1.0.0 --finalize + +# Create a release, then finalize separately +sentry release create 1.0.0 +sentry release set-commits 1.0.0 --auto +sentry release finalize 1.0.0 + +# Set commits from local git history +sentry release set-commits 1.0.0 --local + +# Create a deploy +sentry release deploy 1.0.0 production +sentry release deploy 1.0.0 staging "Deploy #42" + +# Propose a version from git HEAD +sentry release create $(sentry release propose-version) + +# List deploys for a release +sentry release deploys 1.0.0 +sentry release deploys my-org/1.0.0 + +# Archive a release (hide it from the default list, but keep it) +sentry release archive 1.0.0 +sentry release archive my-org/1.0.0 --dry-run # Preview without archiving + +# Restore a previously archived release +sentry release restore 1.0.0 +sentry release restore my-org/1.0.0 + +# Delete a release +sentry release delete my-org/1.0.0 +sentry release delete my-org/1.0.0 --yes # Skip confirmation +sentry release delete my-org/1.0.0 --dry-run # Preview without deleting + +# Output as JSON +sentry release list --json +sentry release view 1.0.0 --json + +# Full release workflow with explicit org +sentry release create my-org/1.0.0 --project my-project +sentry release set-commits my-org/1.0.0 --auto +sentry release finalize my-org/1.0.0 +sentry release deploy my-org/1.0.0 production +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/replay.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/replay.md index 9183285c39..c444ffaa2a 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/replay.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/replay.md @@ -60,6 +60,26 @@ List recent Session Replays | `user` | object \| null | User metadata | | `warning_ids` | array | Linked warning event IDs | +**Examples:** + +```bash +# List recent replays for a project +sentry replay list my-org/frontend + +# Search across all projects in an org +sentry replay list my-org/ --query "environment:production" + +# Change the time window and sort +sentry replay list my-org/frontend --period 24h --sort errors + +# Paginate through results +sentry replay list my-org/frontend -c next +sentry replay list my-org/frontend -c prev + +# Output machine-readable data +sentry replay list my-org/frontend --json +``` + ### `sentry replay view ` View a Session Replay @@ -109,4 +129,20 @@ View a Session Replay | `relatedIssues` | array | Replay-related issues | | `relatedTraces` | array | Replay-related traces | +**Examples:** + +```bash +# View a replay by ID using auto-detected org/project context +sentry replay view 346789a703f6454384f1de473b8b9fcc + +# View a replay with an explicit org +sentry replay view my-org/346789a703f6454384f1de473b8b9fcc + +# View a replay with explicit org/project context +sentry replay view my-org/frontend/346789a703f6454384f1de473b8b9fcc + +# Open a replay in the browser +sentry replay view my-org/346789a703f6454384f1de473b8b9fcc --web +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/repo.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/repo.md index 53a37ea0c5..5d01b91da2 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/repo.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/repo.md @@ -34,4 +34,17 @@ List repositories | `externalSlug` | string \| null | External slug (e.g. org/repo) | | `externalId` | string \| null | External ID | +**Examples:** + +```bash +# List repositories (auto-detect org) +sentry repo list + +# List repos in a specific org with pagination +sentry repo list my-org/ -c next + +# Output as JSON +sentry repo list --json +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/schema.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/schema.md index 5ac57be8c7..02b1151fbe 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/schema.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/schema.md @@ -19,4 +19,23 @@ Browse the Sentry API schema - `--all - Show all endpoints in a flat list` - `-q, --search - Search endpoints by keyword` +**Examples:** + +```bash +# List all API resources +sentry schema + +# Browse issue endpoints +sentry schema issues + +# View details for a specific operation +sentry schema issues list + +# Search for monitoring-related endpoints +sentry schema --search monitor + +# Flat list of every endpoint +sentry schema --all +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/snapshots.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/snapshots.md index 5b9feb6f85..4b44cba4e5 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/snapshots.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/snapshots.md @@ -53,4 +53,32 @@ Upload snapshots to a project - `--force-git-metadata - Force collecting git metadata even outside CI (conflicts with --no-git-metadata)` - `--no-git-metadata - Disable automatic git metadata collection` +**Examples:** + +```bash +# Upload a folder of screenshots as a snapshot for an app +sentry snapshots upload ./screenshots --app-id com.example.app + +# Upload only a subset of images (removals/renames not inferred on PRs) +sentry snapshots upload ./screenshots --app-id my-app --selective + +# Only flag images that differ by more than 1% +sentry snapshots upload ./screenshots --app-id my-app --diff-threshold 0.01 + +# Compare two directories of snapshot images locally +sentry snapshots diff ./baseline ./head + +# Fail (non-zero exit) if any images changed, with a custom threshold +sentry snapshots diff ./baseline ./head --fail-on-diff --threshold 0.02 + +# Download a specific baseline snapshot by ID +sentry snapshots download --snapshot-id 1234567890 + +# Download the latest baseline for an app, filtered by branch +sentry snapshots download --app-id my-app --branch main + +# Extract images to a specific directory +sentry snapshots download --app-id my-app --output ./baseline/ +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/sourcemap.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/sourcemap.md index f8157e54fa..fa852c299b 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/sourcemap.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/sourcemap.md @@ -22,6 +22,19 @@ Inject debug IDs into JavaScript files and sourcemaps - `--dry-run - Show what would be modified without writing` - `--allow-empty - Exit successfully when no JS + sourcemap pairs are found (default: error out to catch silent build misconfigurations)` +**Examples:** + +```bash +# Inject debug IDs into all JS files in dist/ +sentry sourcemap inject ./dist + +# Preview changes without writing +sentry sourcemap inject ./dist --dry-run + +# Only process specific extensions +sentry sourcemap inject ./build --ext .js,.mjs +``` + ### `sentry sourcemap upload ` Upload sourcemaps to Sentry @@ -38,6 +51,21 @@ Upload sourcemaps to Sentry - `--no-rewrite - Upload files as-is without injecting debug IDs` - `--allow-empty - Exit successfully when no JS + sourcemap pairs are found (default: error out to catch silent build misconfigurations)` +**Examples:** + +```bash +# Upload sourcemaps from dist/ +sentry sourcemap upload ./dist + +# Associate with a release +sentry sourcemap upload ./dist --release 1.0.0 + +# Set a custom URL prefix +sentry sourcemap upload ./dist --url-prefix '~/static/js/' + +sentry sourcemap upload ./dist --allow-empty +``` + ### `sentry sourcemap resolve ` Resolve and report sourcemap linkage for JavaScript files @@ -47,4 +75,15 @@ Resolve and report sourcemap linkage for JavaScript files - `--ignore - Comma-separated glob patterns to exclude (gitignore-style)` - `--ignore-file - Path to a file with gitignore-style patterns to exclude` +**Examples:** + +```bash +# Report how each JS file's sourcemap resolves and whether a debug ID +# has been injected (read-only — never modifies files) +sentry sourcemap resolve ./dist + +# Machine-readable output +sentry sourcemap resolve ./dist --json +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/span.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/span.md index 85f75bb6f8..ff4cb412a3 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/span.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/span.md @@ -37,6 +37,34 @@ List spans in a project or trace | `transaction` | string \| null | Transaction name | | `trace` | string | Trace ID | +**Examples:** + +```bash +# List recent spans in the current project +sentry span list + +# Find all DB spans +sentry span list -q "op:db" + +# Slow spans in the last 24 hours +sentry span list -q "duration:>100ms" --period 24h + +# List spans within a specific trace +sentry span list abc123def456abc123def456abc12345 + +# Paginate through results +sentry span list -c next + +# Show only spans from one project within a trace +sentry span list my-org/cli-server/abc123def456abc123def456abc12345 + +# Or use --query to filter by project +sentry span list abc123def456abc123def456abc12345 -q "project:cli-server" + +# Multiple projects at once +sentry span list abc123def456abc123def456abc12345 -q "project:[cli-server,api]" +``` + ### `sentry span view ` View details of specific spans @@ -45,4 +73,17 @@ View details of specific spans - `--spans - Span tree depth limit (number, "all" for unlimited, "no" to disable) - (default: "3")` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` +**Examples:** + +```bash +# View a single span +sentry span view abc123def456abc123def456abc12345 a1b2c3d4e5f67890 + +# View multiple spans at once +sentry span view abc123def456abc123def456abc12345 a1b2c3d4e5f67890 b2c3d4e5f6789012 + +# With explicit org/project +sentry span view my-org/backend/abc123def456abc123def456abc12345 a1b2c3d4e5f67890 +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/status.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/status.md index 317ade042e..4aa93c4c10 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/status.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/status.md @@ -18,4 +18,17 @@ Show Sentry service status **Flags:** - `--url - Status page base URL to query - (default: "https://status.sentry.io")` +**Examples:** + +```bash +# Show the current status of Sentry's services +sentry status + +# Get machine-readable status (useful in scripts) +sentry status --json + +# Check a self-hosted or regional status page (Statuspage CNAME) +sentry status --url https://status.acme.com +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/team.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/team.md index 5805ef3dc3..4d1152d483 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/team.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/team.md @@ -32,4 +32,17 @@ List teams | `teamRole` | string \| null | Your role in the team | | `memberCount` | number | Number of members | +**Examples:** + +```bash +# List teams +sentry team list my-org/ + +# Paginate through teams +sentry team list my-org/ -c next + +# Output as JSON +sentry team list --json +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trace.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trace.md index 00172e674e..53bb750bbf 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trace.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trace.md @@ -34,6 +34,22 @@ List recent traces in a project | `transaction.duration` | number | Duration (ms) | | `project` | string | Project slug | +**Examples:** + +```bash +# List last 20 traces (default) +sentry trace list + +# Sort by slowest first +sentry trace list --sort duration + +# Filter by transaction name, last 24 hours +sentry trace list -q "transaction:GET /api/users" --period 24h + +# Paginate through results +sentry trace list my-org/backend -c next +``` + ### `sentry trace view ` View details of a specific trace @@ -44,6 +60,31 @@ View details of a specific trace - `--spans - Span tree depth limit (number, "all" for unlimited, "no" to disable) - (default: "3")` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` +**Examples:** + +```bash +# View trace details with span tree +sentry trace view abc123def456abc123def456abc12345 + +# Open trace in browser +sentry trace view abc123def456abc123def456abc12345 -w + +# Auto-recover from an issue short ID +sentry trace view PROJ-123 + +# Filter trace view to one project's spans +sentry trace view my-org/cli-server/abc123def456abc123def456abc12345 + +# Full trace across all projects (default) +sentry trace view my-org/abc123def456abc123def456abc12345 + +# Filter trace logs by project +sentry trace logs my-org/cli-server/abc123def456abc123def456abc12345 + +# Multiple projects via --query +sentry trace logs abc123def456abc123def456abc12345 -q "project:[cli-server,api]" +``` + ### `sentry trace logs ` View logs associated with a trace @@ -56,4 +97,17 @@ View logs associated with a trace - `-s, --sort - Sort order: "newest" (default) or "oldest" - (default: "newest")` - `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` +**Examples:** + +```bash +# View logs for a trace +sentry trace logs abc123def456abc123def456abc12345 + +# Search with a longer time window +sentry trace logs --period 30d abc123def456abc123def456abc12345 + +# Filter logs within a trace +sentry trace logs -q 'severity:error' abc123def456abc123def456abc12345 +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trial.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trial.md index bcfaff527d..a53d48d350 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trial.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/trial.md @@ -30,4 +30,23 @@ List product trials Start a product trial +**Examples:** + +```bash +# List all trials for the current org +sentry trial list + +# List trials for a specific org +sentry trial list my-org + +# Start a Seer trial +sentry trial start seer + +# Start a trial for a specific org +sentry trial start replays my-org + +# Start a Business plan trial (opens browser) +sentry trial start plan +``` + All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. From 4bedf8e681af4f82939282672f0343b7b416d87a Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Fri, 4 Sep 2026 20:43:00 +0000 Subject: [PATCH 08/17] fix(setup): chmod migrated binary executable as defense-in-depth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit copyFileSync already preserves the source mode, so a migrated ~/.sentry binary stays executable in practice. Add an explicit chmodSync(0o755) after the copy anyway — mirrors installBinary — so the exec bit is never in doubt even if the legacy copy's permissions were stripped. Covered by a new accessSync(X_OK) assertion in the migration test. Also drain the now-flagged silent catch around unlinkSync with a log.debug. --- packages/cli/src/commands/cli/setup.ts | 8 +++++++- packages/cli/test/commands/cli/setup.test.ts | 13 ++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/cli/setup.ts b/packages/cli/src/commands/cli/setup.ts index 65e20982f7..5ec75a5b0b 100644 --- a/packages/cli/src/commands/cli/setup.ts +++ b/packages/cli/src/commands/cli/setup.ts @@ -7,6 +7,7 @@ */ import { + chmodSync, copyFileSync, existsSync, mkdirSync, @@ -154,11 +155,16 @@ function migrateLegacyBinary( mkdirSync(targetDir, { recursive: true, mode: 0o755 }); copyFileSync(legacyBin, targetBin); + // copyFileSync already preserves the source mode, but assert the exec bit + // explicitly — mirrors installBinary — so the migrated binary is runnable + // even if the legacy copy's permissions were somehow stripped. + chmodSync(targetBin, 0o755); try { unlinkSync(legacyBin); - } catch { + } catch (error) { // Leave the old binary in place if it can't be removed — the new copy // is authoritative and setInstallInfo points upgrades at it. + logger.withTag("cli.setup").debug("Failed to remove legacy binary", error); } setInstallInfo({ method: "curl", path: targetBin, version: CLI_VERSION }); emit(`Binary: Migrated ${legacyBin} → ${targetBin}`); diff --git a/packages/cli/test/commands/cli/setup.test.ts b/packages/cli/test/commands/cli/setup.test.ts index 609ead1469..0219a95650 100644 --- a/packages/cli/test/commands/cli/setup.test.ts +++ b/packages/cli/test/commands/cli/setup.test.ts @@ -7,7 +7,14 @@ * via a spy on process.stderr.write and assert on the collected output. */ -import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { + accessSync, + constants, + existsSync, + mkdirSync, + rmSync, + writeFileSync, +} from "node:fs"; import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { run } from "@stricli/core"; @@ -1084,6 +1091,10 @@ describe("sentry cli setup — legacy migration", () => { expect(existsSync(moved)).toBe(true); expect(await readFile(moved, "utf8")).toBe("legacy-binary"); expect(existsSync(join(legacyBinDir, "sentry"))).toBe(false); + // The migrated binary must remain executable. + if (process.platform !== "win32") { + expect(() => accessSync(moved, constants.X_OK)).not.toThrow(); + } }); test("does not overwrite an existing binary at the target", async () => { From 5c07acb7cf058a15d9864989fed138cb068adca3 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Fri, 4 Sep 2026 22:08:09 +0000 Subject: [PATCH 09/17] feat(upgrade): relocate legacy ~/.sentry/bin to XDG dir when safe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously 'sentry upgrade' hard-pinned SENTRY_INSTALL_DIR to the current install dir, so a legacy ~/.sentry/bin user could never migrate off it via upgrade — the docs' claim that upgrade keeps the binary in place was technically true but meant curl users never reached the XDG location. Now resolveUpgradeInstallDir relocates a legacy ~/.sentry/bin install to the XDG install dir when that dir is already on PATH (upgrade runs --no-modify-path, so it must not move a binary to a dir that isn't on PATH). setup's migrateLegacyBinary then moves the old binary and removes it before --install writes the new version; the legacy dir is left empty so nothing shadows. Non-legacy installs and off-PATH XDG dirs stay put. - resolveUpgradeInstallDir helper + unit tests (relocate / stay-put / undefined PATH / non-legacy). - docs: describe the conditional relocation on upgrade accurately. --- apps/cli-docs/src/fragments/configuration.md | 2 +- packages/cli/src/commands/cli/upgrade.ts | 44 +++++++++++++- .../cli/test/commands/cli/upgrade.test.ts | 60 ++++++++++++++++++- 3 files changed, 100 insertions(+), 6 deletions(-) diff --git a/apps/cli-docs/src/fragments/configuration.md b/apps/cli-docs/src/fragments/configuration.md index 7cb853ef5d..b9673a9333 100644 --- a/apps/cli-docs/src/fragments/configuration.md +++ b/apps/cli-docs/src/fragments/configuration.md @@ -123,4 +123,4 @@ When installed via the install script, the CLI binary is placed in an XDG-aligne Older installs placed the binary in `~/.sentry/bin`. Running `sentry cli setup` moves an existing `~/.sentry/bin` binary into the resolved install directory (updating your `PATH` and recorded install metadata to match) and migrates any legacy `~/.sentry` config data (`cli.db`, `config.json`) into the XDG config directory. Both migrations are skipped when a binary or config already exists at the target. -`sentry upgrade` keeps the binary where it currently lives — it pins `SENTRY_INSTALL_DIR` to the existing install directory so an in-place update never relocates a binary that is already on your `PATH`. Legacy config data is still migrated on upgrade; to move the binary itself to the XDG location, run `sentry cli setup` (optionally with `SENTRY_INSTALL_DIR` or `XDG_BIN_HOME` set). +`sentry upgrade` runs `setup` on the new binary, so it migrates too — but conservatively, because upgrade never edits your `PATH`. A legacy `~/.sentry/bin` binary is relocated to the XDG install directory **only when that directory is already on your `PATH`**, so the moved binary stays discoverable. If the XDG directory isn't on `PATH`, upgrade leaves the binary in place (a mislocated binary that vanished from `PATH` would break the command); run `sentry cli setup` explicitly to relocate it and update `PATH`. Legacy config data is migrated on upgrade regardless. diff --git a/packages/cli/src/commands/cli/upgrade.ts b/packages/cli/src/commands/cli/upgrade.ts index f760f80ba9..69cc87ff04 100644 --- a/packages/cli/src/commands/cli/upgrade.ts +++ b/packages/cli/src/commands/cli/upgrade.ts @@ -16,7 +16,7 @@ import { spawn } from "node:child_process"; import { homedir } from "node:os"; -import { dirname } from "node:path"; +import { dirname, join } from "node:path"; import { setTimeout } from "node:timers/promises"; import type { SentryContext } from "../../context.js"; import { @@ -42,6 +42,7 @@ import { type ChangelogSummary, fetchChangelog, } from "../../lib/release-notes.js"; +import { isInPath } from "../../lib/shell.js"; import { detectInstallationMethod, executeUpgrade, @@ -564,6 +565,38 @@ function resolveUpdatedCliPath( return whichSync("sentry", { PATH: pathEnv }) ?? entryPath ?? execPath; } +/** + * Decide which directory a curl upgrade should install into. + * + * Normally the binary stays where it currently lives — pinning the install + * dir keeps an in-place update from relocating a binary that is already on + * the user's `PATH` (upgrade runs setup with `--no-modify-path`, so it can't + * add a new directory to `PATH`). + * + * The one exception is a legacy `~/.sentry/bin` install: those should move to + * the XDG-aligned location so users actually migrate off `~/.sentry`. We only + * relocate when the XDG target directory is *already* on `PATH`, so the moved + * binary stays discoverable without any `PATH` edit. When it isn't, we keep + * the binary in place and leave relocation to an explicit `sentry cli setup`. + */ +export function resolveUpgradeInstallDir( + currentInstallDir: string, + pathEnv: string | undefined +): string { + const legacyBinDir = join(homedir(), ".sentry", "bin"); + if (currentInstallDir !== legacyBinDir) { + return currentInstallDir; + } + + // determineInstallDir with the legacy pin removed yields the XDG target. + const { SENTRY_INSTALL_DIR: _pinned, ...envWithoutPin } = process.env; + const xdgInstallDir = determineInstallDir(homedir(), envWithoutPin); + if (xdgInstallDir !== legacyBinDir && isInPath(xdgInstallDir, pathEnv)) { + return xdgInstallDir; + } + return currentInstallDir; +} + /** * Execute the standard upgrade path: download via curl or package manager, * then run setup on the new binary. @@ -615,17 +648,22 @@ async function executeStandardUpgrade(opts: { if (downloadResult) { // Curl: new binary is at temp path, setup --install will place it. // Pin the install directory via SENTRY_INSTALL_DIR so the child's - // determineInstallDir() doesn't relocate to a different directory. + // determineInstallDir() doesn't relocate to a directory that isn't on + // PATH. A legacy ~/.sentry/bin install is relocated to the XDG dir when + // that dir is already on PATH (see resolveUpgradeInstallDir); setup's + // legacy-binary migration then moves the old binary and removes it before + // --install writes the new one. // Release the download lock after the child exits — if the child used // the same lock path (ppid takeover), this is a harmless no-op. const currentInstallDir = dirname(getCurlInstallPaths().installPath); + const installDir = resolveUpgradeInstallDir(currentInstallDir, pathEnv); try { await runSetupOnNewBinary({ binaryPath: downloadResult.tempBinaryPath, method, channel, install: true, - installDir: currentInstallDir, + installDir, ensureAuthScopes: !json, noAgentSkills, }); diff --git a/packages/cli/test/commands/cli/upgrade.test.ts b/packages/cli/test/commands/cli/upgrade.test.ts index 47f4f372f1..7d098ecf2d 100644 --- a/packages/cli/test/commands/cli/upgrade.test.ts +++ b/packages/cli/test/commands/cli/upgrade.test.ts @@ -13,7 +13,8 @@ import * as child_process from "node:child_process"; import { chmodSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { unlink } from "node:fs/promises"; -import { join } from "node:path"; +import { homedir } from "node:os"; +import { delimiter, join } from "node:path"; import { gzipSync } from "node:zlib"; import { run } from "@stricli/core"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; @@ -25,7 +26,10 @@ vi.mock("node:child_process", async (importOriginal) => { }); import { app } from "../../../src/app.js"; -import { isEbusyError } from "../../../src/commands/cli/upgrade.js"; +import { + isEbusyError, + resolveUpgradeInstallDir, +} from "../../../src/commands/cli/upgrade.js"; import type { SentryContext } from "../../../src/context.js"; import { CLI_VERSION } from "../../../src/lib/constants.js"; import { @@ -1129,3 +1133,55 @@ describe("isEbusyError", () => { expect(isEbusyError(new Error("some error"))).toBe(false); }); }); + +describe("resolveUpgradeInstallDir", () => { + const home = homedir(); + const legacyBinDir = join(home, ".sentry", "bin"); + const xdgBinDir = join(home, ".local", "bin"); + let savedInstallDir: string | undefined; + let savedXdgBinHome: string | undefined; + + beforeEach(() => { + savedInstallDir = process.env.SENTRY_INSTALL_DIR; + savedXdgBinHome = process.env.XDG_BIN_HOME; + delete process.env.SENTRY_INSTALL_DIR; + delete process.env.XDG_BIN_HOME; + }); + + afterEach(() => { + if (savedInstallDir === undefined) { + delete process.env.SENTRY_INSTALL_DIR; + } else { + process.env.SENTRY_INSTALL_DIR = savedInstallDir; + } + if (savedXdgBinHome === undefined) { + delete process.env.XDG_BIN_HOME; + } else { + process.env.XDG_BIN_HOME = savedXdgBinHome; + } + }); + + test("keeps a non-legacy install dir unchanged", () => { + const current = join(home, "bin"); + expect( + resolveUpgradeInstallDir(current, `${current}${delimiter}/usr/bin`) + ).toBe(current); + }); + + test("relocates a legacy ~/.sentry/bin install when the XDG dir is on PATH", () => { + const pathEnv = `${xdgBinDir}${delimiter}/usr/bin`; + expect(resolveUpgradeInstallDir(legacyBinDir, pathEnv)).toBe(xdgBinDir); + }); + + test("keeps the legacy dir when the XDG dir is not on PATH", () => { + expect(resolveUpgradeInstallDir(legacyBinDir, "/usr/bin:/bin")).toBe( + legacyBinDir + ); + }); + + test("keeps the legacy dir when PATH is undefined", () => { + expect(resolveUpgradeInstallDir(legacyBinDir, undefined)).toBe( + legacyBinDir + ); + }); +}); From f2bac2a63d6ba8fd16bf0beddb913b4b577ddaa1 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Fri, 4 Sep 2026 23:51:37 +0000 Subject: [PATCH 10/17] fix(upgrade): compare install dirs case-insensitively on Windows/macOS resolveUpgradeInstallDir compared a stored install path against a freshly computed ~/.sentry/bin using strict ===. On case-insensitive filesystems (Windows, macOS) the two can differ only in casing (e.g. C:\Users\User vs C:\Users\user) yet point at the same directory, so a legacy install would fail the equality check and never relocate to the XDG dir. Add a samePath() helper that lowercases on win32/darwin and use it for both the legacy-dir and XDG-target comparisons. Test asserts a mixed-case legacy dir is still treated as legacy on case-insensitive platforms. Addresses sentry[bot] review on #1503. --- packages/cli/src/commands/cli/upgrade.ts | 21 +++++++++++++++++-- .../cli/test/commands/cli/upgrade.test.ts | 15 +++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/cli/upgrade.ts b/packages/cli/src/commands/cli/upgrade.ts index 69cc87ff04..29d870306c 100644 --- a/packages/cli/src/commands/cli/upgrade.ts +++ b/packages/cli/src/commands/cli/upgrade.ts @@ -565,6 +565,20 @@ function resolveUpdatedCliPath( return whichSync("sentry", { PATH: pathEnv }) ?? entryPath ?? execPath; } +/** + * Compare two directory paths for equality, case-insensitively on + * case-insensitive filesystems (Windows, macOS). A stored install path can + * differ in casing from a freshly computed one (e.g. `C:\Users\User` vs + * `C:\Users\user`) yet point at the same directory, so a strict `===` would + * wrongly treat them as different. + */ +function samePath(a: string, b: string): boolean { + if (process.platform === "win32" || process.platform === "darwin") { + return a.toLowerCase() === b.toLowerCase(); + } + return a === b; +} + /** * Decide which directory a curl upgrade should install into. * @@ -584,14 +598,17 @@ export function resolveUpgradeInstallDir( pathEnv: string | undefined ): string { const legacyBinDir = join(homedir(), ".sentry", "bin"); - if (currentInstallDir !== legacyBinDir) { + if (!samePath(currentInstallDir, legacyBinDir)) { return currentInstallDir; } // determineInstallDir with the legacy pin removed yields the XDG target. const { SENTRY_INSTALL_DIR: _pinned, ...envWithoutPin } = process.env; const xdgInstallDir = determineInstallDir(homedir(), envWithoutPin); - if (xdgInstallDir !== legacyBinDir && isInPath(xdgInstallDir, pathEnv)) { + if ( + !samePath(xdgInstallDir, legacyBinDir) && + isInPath(xdgInstallDir, pathEnv) + ) { return xdgInstallDir; } return currentInstallDir; diff --git a/packages/cli/test/commands/cli/upgrade.test.ts b/packages/cli/test/commands/cli/upgrade.test.ts index 7d098ecf2d..ca9688e467 100644 --- a/packages/cli/test/commands/cli/upgrade.test.ts +++ b/packages/cli/test/commands/cli/upgrade.test.ts @@ -1184,4 +1184,19 @@ describe("resolveUpgradeInstallDir", () => { legacyBinDir ); }); + + test("treats a differently-cased legacy dir as legacy on case-insensitive filesystems", () => { + // On Windows/macOS a stored install path can differ only in casing from + // the freshly computed legacy dir yet point at the same directory; it must + // still be recognized as the legacy install so relocation can trigger. + const mixedCaseLegacy = legacyBinDir.toUpperCase(); + const pathEnv = `${xdgBinDir}${delimiter}/usr/bin`; + const result = resolveUpgradeInstallDir(mixedCaseLegacy, pathEnv); + if (process.platform === "win32" || process.platform === "darwin") { + expect(result).toBe(xdgBinDir); + } else { + // Case-sensitive filesystem: a different-cased path is a different dir. + expect(result).toBe(mixedCaseLegacy); + } + }); }); From b03db3bf1d1940f0dbc77a4f6ebcf9f94197e961 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Sat, 5 Sep 2026 12:17:58 +0000 Subject: [PATCH 11/17] refactor(setup): address BYK review on legacy migration - Binary migration is no longer hardcoded to ~/.sentry/bin: findMigratableBinary scans getKnownInstallDirs() (derived from KNOWN_CURL_DIRS) for a binary in any known install dir other than the resolved target. - Deduplicate path logic: samePath and LEGACY_INSTALL_SUBDIR now live in binary.ts and are shared by setup and upgrade (removed the duplicate samePath in upgrade.ts). - Use async node:fs/promises (mkdir/copyFile/chmod/rename/unlink) in the migration paths. - Report migration failures to Sentry: both migrations now run through bestEffort(), which warns the user and captureException()s while never aborting setup. - Add an e2e migration test (test/e2e/migration.test.ts) that spawns the real CLI and verifies both the config DB and a legacy ~/.sentry/bin binary migrate to the XDG locations. Plus unit tests for samePath / getKnownInstallDirs. Addresses BYK's CHANGES_REQUESTED review on #1503. --- packages/cli/src/commands/cli/setup.ts | 122 ++++++++++++++--------- packages/cli/src/commands/cli/upgrade.ts | 18 +--- packages/cli/src/lib/binary.ts | 29 ++++++ packages/cli/test/e2e/migration.test.ts | 116 +++++++++++++++++++++ packages/cli/test/lib/binary.test.ts | 36 +++++++ 5 files changed, 258 insertions(+), 63 deletions(-) create mode 100644 packages/cli/test/e2e/migration.test.ts diff --git a/packages/cli/src/commands/cli/setup.ts b/packages/cli/src/commands/cli/setup.ts index 5ec75a5b0b..8453390193 100644 --- a/packages/cli/src/commands/cli/setup.ts +++ b/packages/cli/src/commands/cli/setup.ts @@ -6,14 +6,8 @@ * and the upgrade command for curl-based installs). */ -import { - chmodSync, - copyFileSync, - existsSync, - mkdirSync, - renameSync, - unlinkSync, -} from "node:fs"; +import { existsSync, unlinkSync } from "node:fs"; +import { chmod, copyFile, mkdir, rename, unlink } from "node:fs/promises"; import { dirname, join } from "node:path"; import { captureException } from "@sentry/node-core/light"; import type { SentryContext } from "../../context.js"; @@ -21,9 +15,11 @@ import { installAgentSkills } from "../../lib/agent-skills.js"; import { determineInstallDir, getBinaryFilename, + getKnownInstallDirs, type InstallationMethod, installBinary, parseInstallationMethod, + samePath, } from "../../lib/binary.js"; import { buildCommand } from "../../lib/command.js"; import { @@ -97,16 +93,16 @@ function formatSetupResult(result: SetupResult): string { * cannot be renamed on Windows. Closing also invalidates the cached handle, so * the next `getDatabase()` reopens at the new path. */ -function migrateLegacyConfig( +async function migrateLegacyConfig( homeDir: string, env: NodeJS.ProcessEnv, emit: Logger -): void { +): Promise { const legacyDir = join(homeDir, ".sentry"); // Target the XDG location directly — resolveConfigDir keeps returning the // legacy dir while it still holds cli.db, which would make migration a no-op. const targetConfigDir = resolveXdgConfigDir(env, homeDir); - if (targetConfigDir === legacyDir) { + if (samePath(targetConfigDir, legacyDir)) { return; } @@ -119,48 +115,70 @@ function migrateLegacyConfig( } closeDatabase(); - mkdirSync(targetConfigDir, { recursive: true, mode: 0o700 }); + await mkdir(targetConfigDir, { recursive: true, mode: 0o700 }); for (const name of configFiles) { const from = join(legacyDir, name); if (existsSync(from)) { - renameSync(from, join(targetConfigDir, name)); + await rename(from, join(targetConfigDir, name)); } } emit(`Config: Migrated ${legacyDir} → ${targetConfigDir}`); } /** - * Migrate the binary out of the legacy `~/.sentry/bin` into the XDG-aware - * install dir. Returns the new binary path when a move happened, so the caller - * can point PATH setup and recorded install info at the new location instead of - * the now-deleted legacy path. + * Find a previously-installed binary in a known install directory other than + * the resolved target, so it can be migrated. The curl installer may have + * placed the binary in any of {@link getKnownInstallDirs} (e.g. `~/.sentry/bin` + * on older installs), so migration is not limited to `~/.sentry`. */ -function migrateLegacyBinary( +function findMigratableBinary( + homeDir: string, + targetDir: string, + filename: string +): string | undefined { + for (const dir of getKnownInstallDirs(homeDir)) { + if (samePath(dir, targetDir)) { + continue; + } + const candidate = join(dir, filename); + if (existsSync(candidate)) { + return candidate; + } + } + return; +} + +/** + * Migrate an existing binary out of a known legacy install directory into the + * XDG-aware install dir. Returns the new binary path when a move happened, so + * the caller can point PATH setup and recorded install info at the new location + * instead of the now-deleted legacy path. + */ +async function migrateLegacyBinary( homeDir: string, env: NodeJS.ProcessEnv, emit: Logger -): string | undefined { - const legacyDir = join(homeDir, ".sentry"); +): Promise { const filename = getBinaryFilename(); - const legacyBin = join(legacyDir, "bin", filename); const targetDir = determineInstallDir(homeDir, env); const targetBin = join(targetDir, filename); - if ( - !existsSync(legacyBin) || - existsSync(targetBin) || - targetDir === join(legacyDir, "bin") - ) { + if (existsSync(targetBin)) { + return; + } + + const legacyBin = findMigratableBinary(homeDir, targetDir, filename); + if (!legacyBin) { return; } - mkdirSync(targetDir, { recursive: true, mode: 0o755 }); - copyFileSync(legacyBin, targetBin); - // copyFileSync already preserves the source mode, but assert the exec bit + await mkdir(targetDir, { recursive: true, mode: 0o755 }); + await copyFile(legacyBin, targetBin); + // copyFile already preserves the source mode, but assert the exec bit // explicitly — mirrors installBinary — so the migrated binary is runnable // even if the legacy copy's permissions were somehow stripped. - chmodSync(targetBin, 0o755); + await chmod(targetBin, 0o755); try { - unlinkSync(legacyBin); + await unlink(legacyBin); } catch (error) { // Leave the old binary in place if it can't be removed — the new copy // is authoritative and setInstallInfo points upgrades at it. @@ -649,23 +667,31 @@ export const setupCommand = buildCommand({ // 0. Migrate any legacy ~/.sentry config/binary into XDG locations first, // so the steps below operate on the new paths. Config and binary migrations - // are independent — a failure in one must not skip the other. - try { - migrateLegacyConfig(homeDir, process.env, emit); - } catch (error) { - warn("Legacy config migration", error); - } - try { - const migratedBinary = migrateLegacyBinary(homeDir, process.env, emit); - // Adopt the new location so PATH setup and recorded install info point at - // the migrated binary rather than the deleted legacy path. - if (migratedBinary) { - binaryPath = migratedBinary; - binaryDir = dirname(migratedBinary); - } - } catch (error) { - warn("Legacy binary migration", error); - } + // are independent — a failure in one must not skip the other, and both are + // best-effort: warnings surface to the user and errors are reported to + // Sentry, but a failure never aborts setup. + await bestEffort( + "Legacy config migration", + () => migrateLegacyConfig(homeDir, process.env, emit), + warn + ); + await bestEffort( + "Legacy binary migration", + async () => { + const migratedBinary = await migrateLegacyBinary( + homeDir, + process.env, + emit + ); + // Adopt the new location so PATH setup and recorded install info point + // at the migrated binary rather than the deleted legacy path. + if (migratedBinary) { + binaryPath = migratedBinary; + binaryDir = dirname(migratedBinary); + } + }, + warn + ); // 1. Install binary from temp location (when --install is set) if (flags.install) { diff --git a/packages/cli/src/commands/cli/upgrade.ts b/packages/cli/src/commands/cli/upgrade.ts index 29d870306c..8d82823683 100644 --- a/packages/cli/src/commands/cli/upgrade.ts +++ b/packages/cli/src/commands/cli/upgrade.ts @@ -22,7 +22,9 @@ import type { SentryContext } from "../../context.js"; import { determineInstallDir, isDowngrade, + LEGACY_INSTALL_SUBDIR, releaseLock, + samePath, } from "../../lib/binary.js"; import { buildCommand } from "../../lib/command.js"; import { CLI_VERSION } from "../../lib/constants.js"; @@ -565,20 +567,6 @@ function resolveUpdatedCliPath( return whichSync("sentry", { PATH: pathEnv }) ?? entryPath ?? execPath; } -/** - * Compare two directory paths for equality, case-insensitively on - * case-insensitive filesystems (Windows, macOS). A stored install path can - * differ in casing from a freshly computed one (e.g. `C:\Users\User` vs - * `C:\Users\user`) yet point at the same directory, so a strict `===` would - * wrongly treat them as different. - */ -function samePath(a: string, b: string): boolean { - if (process.platform === "win32" || process.platform === "darwin") { - return a.toLowerCase() === b.toLowerCase(); - } - return a === b; -} - /** * Decide which directory a curl upgrade should install into. * @@ -597,7 +585,7 @@ export function resolveUpgradeInstallDir( currentInstallDir: string, pathEnv: string | undefined ): string { - const legacyBinDir = join(homedir(), ".sentry", "bin"); + const legacyBinDir = join(homedir(), LEGACY_INSTALL_SUBDIR); if (!samePath(currentInstallDir, legacyBinDir)) { return currentInstallDir; } diff --git a/packages/cli/src/lib/binary.ts b/packages/cli/src/lib/binary.ts index ffa57f1f59..e884ac22d3 100644 --- a/packages/cli/src/lib/binary.ts +++ b/packages/cli/src/lib/binary.ts @@ -29,6 +29,35 @@ import { isProcessRunning } from "./process-utils.js"; /** Known directories where the curl installer may place the binary */ export const KNOWN_CURL_DIRS = [".local/bin", "bin", ".sentry/bin"]; +/** + * Legacy install directory (relative to home) that predates the XDG layout. + * The curl installer used to drop the binary here; migration moves it out. + */ +export const LEGACY_INSTALL_SUBDIR = join(".sentry", "bin"); + +/** + * Compare two filesystem paths for equality, case-insensitively on + * case-insensitive filesystems (Windows, macOS). A stored path can differ in + * casing from a freshly computed one (e.g. `C:\Users\User` vs `C:\Users\user`) + * yet point at the same location, so a strict `===` would wrongly differ. + */ +export function samePath(a: string, b: string): boolean { + if (process.platform === "win32" || process.platform === "darwin") { + return a.toLowerCase() === b.toLowerCase(); + } + return a === b; +} + +/** + * Candidate directories a previously-installed binary may live in, in priority + * order. Used by migration to find a binary to move into the resolved install + * directory. Derived from {@link KNOWN_CURL_DIRS} so setup and upgrade agree on + * what counts as a prior install location. + */ +export function getKnownInstallDirs(homeDir: string): string[] { + return KNOWN_CURL_DIRS.map((dir) => join(homeDir, dir)); +} + /** * How the CLI was installed. Determines the upgrade strategy. * diff --git a/packages/cli/test/e2e/migration.test.ts b/packages/cli/test/e2e/migration.test.ts new file mode 100644 index 0000000000..d9ef3bb891 --- /dev/null +++ b/packages/cli/test/e2e/migration.test.ts @@ -0,0 +1,116 @@ +/** + * Legacy Layout Migration E2E Tests + * + * Spawns the real CLI to verify that `sentry cli setup` migrates an existing + * `~/.sentry` layout — the SQLite config DB and a curl-installed binary — into + * the XDG-compliant locations. Exercises the full startup + migration path + * (DB open, close, file moves) as a user would hit it, not just the unit-level + * helpers. + */ + +import { + existsSync, + mkdirSync, + mkdtempSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { getBinaryFilename } from "../../src/lib/binary.js"; +import { runCli } from "../fixture.js"; + +const binName = getBinaryFilename(); + +let home: string; + +/** Env that isolates a spawned CLI to a throwaway home directory. */ +function homeEnv(extra: Record = {}): Record { + return { + HOME: home, + USERPROFILE: home, + // Drop any config-dir pin from the parent (preload sets it) so the CLI + // resolves paths from HOME like a real install. + SENTRY_CONFIG_DIR: "", + XDG_CONFIG_HOME: "", + XDG_BIN_HOME: "", + SENTRY_CLI_NO_TELEMETRY: "1", + ...extra, + }; +} + +/** Args that keep setup non-interactive and side-effect free beyond migration. */ +const setupArgs = [ + "cli", + "setup", + "--quiet", + "--no-modify-path", + "--no-completions", + "--no-agent-skills", +]; + +describe("e2e: legacy ~/.sentry migration", () => { + beforeEach(() => { + home = mkdtempSync(join(tmpdir(), "sentry-migrate-e2e-")); + }); + + afterEach(() => { + rmSync(home, { recursive: true, force: true }); + }); + + test( + "migrates the config DB from ~/.sentry to ~/.config/sentry", + { timeout: 60_000 }, + async () => { + // Seed a real SQLite config DB in the legacy location by running the CLI + // once pinned at ~/.sentry (this creates cli.db via getDatabase()). + const legacyDir = join(home, ".sentry"); + mkdirSync(legacyDir, { recursive: true, mode: 0o700 }); + // `auth status` opens (and thus creates) cli.db; a non-zero "not logged + // in" exit is fine — we only need the DB file to exist. + await runCli(["auth", "status"], { + env: homeEnv({ SENTRY_CONFIG_DIR: legacyDir }), + }); + expect(existsSync(join(legacyDir, "cli.db"))).toBe(true); + + // Now run setup with no config pin — it should migrate ~/.sentry/cli.db + // into the XDG config dir (~/.config/sentry). + const result = await runCli(setupArgs, { env: homeEnv() }); + expect(result.exitCode).toBe(0); + + const xdgDb = join(home, ".config", "sentry", "cli.db"); + expect(existsSync(xdgDb)).toBe(true); + expect(existsSync(join(legacyDir, "cli.db"))).toBe(false); + } + ); + + test( + "migrates a legacy ~/.sentry/bin binary onto the XDG install dir", + { timeout: 60_000 }, + async () => { + // Legacy curl layout: binary under ~/.sentry/bin, and ~/.local/bin on PATH + // so the resolved install dir is the XDG location. + const legacyBinDir = join(home, ".sentry", "bin"); + mkdirSync(legacyBinDir, { recursive: true }); + const legacyBin = join(legacyBinDir, binName); + writeFileSync(legacyBin, "#!/bin/sh\necho legacy\n", { mode: 0o755 }); + + const xdgBinDir = join(home, ".local", "bin"); + mkdirSync(xdgBinDir, { recursive: true }); + + const result = await runCli(setupArgs, { + env: homeEnv({ + PATH: `${xdgBinDir}:${process.env.PATH ?? ""}`, + }), + }); + expect(result.exitCode).toBe(0); + + const movedBin = join(xdgBinDir, binName); + expect(existsSync(movedBin)).toBe(true); + expect(await readFile(movedBin, "utf8")).toBe("#!/bin/sh\necho legacy\n"); + expect(existsSync(legacyBin)).toBe(false); + } + ); +}); diff --git a/packages/cli/test/lib/binary.test.ts b/packages/cli/test/lib/binary.test.ts index a9a255c20c..168c761962 100644 --- a/packages/cli/test/lib/binary.test.ts +++ b/packages/cli/test/lib/binary.test.ts @@ -25,12 +25,14 @@ import { getBinaryDownloadUrl, getBinaryFilename, getBinaryPaths, + getKnownInstallDirs, getPlatformBinaryName, installBinary, isDowngrade, isMusl, releaseLock, replaceBinarySync, + samePath, } from "../../src/lib/binary.js"; import { UpgradeError } from "../../src/lib/errors.js"; @@ -78,6 +80,40 @@ describe("getBinaryPaths", () => { }); }); +describe("samePath", () => { + test("matches identical paths", () => { + expect(samePath("/home/user/.local/bin", "/home/user/.local/bin")).toBe( + true + ); + }); + + test("distinguishes genuinely different paths", () => { + expect(samePath("/home/user/.local/bin", "/home/user/.sentry/bin")).toBe( + false + ); + }); + + test("case sensitivity follows the platform", () => { + const result = samePath("/Home/User/bin", "/home/user/bin"); + if (process.platform === "win32" || process.platform === "darwin") { + expect(result).toBe(true); + } else { + expect(result).toBe(false); + } + }); +}); + +describe("getKnownInstallDirs", () => { + test("returns the known curl dirs resolved against home", () => { + const dirs = getKnownInstallDirs("/home/user"); + expect(dirs).toEqual([ + join("/home/user", ".local", "bin"), + join("/home/user", "bin"), + join("/home/user", ".sentry", "bin"), + ]); + }); +}); + describe("determineInstallDir", () => { let testDir: string; From 77a0d212189959d0cd842dc7e28c345c686699b3 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Sat, 5 Sep 2026 21:58:54 +0000 Subject: [PATCH 12/17] fix(setup): only migrate binaries out of the legacy ~/.sentry/bin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address BugBot + Seer findings on the latest revision. - BugBot (high): findMigratableBinary scanned all known install dirs, so a stray binary in ~/.local/bin or ~/bin — both valid *current* XDG targets — could be moved out and the original deleted, leaving the real ~/.sentry/bin install behind. Replace getKnownInstallDirs with getLegacyInstallDirs, which is limited to the genuinely pre-XDG ~/.sentry/bin. Add setup tests asserting binaries in ~/.local/bin and ~/bin are never relocated. - Seer (medium): isInPath used a case-sensitive membership check; on Windows/ macOS a PATH entry can differ only in casing from a computed dir. Compare case-insensitively on those platforms, matching samePath. Add isInPath tests. Unit + e2e migration tests green; tsc + biome clean. --- packages/cli/src/commands/cli/setup.ts | 13 +++--- packages/cli/src/lib/binary.ts | 14 ++++--- packages/cli/src/lib/shell.ts | 6 +++ packages/cli/test/commands/cli/setup.test.ts | 42 ++++++++++++++++++++ packages/cli/test/lib/binary.test.ts | 17 ++++---- packages/cli/test/lib/shell.test.ts | 29 ++++++++++++++ 6 files changed, 100 insertions(+), 21 deletions(-) diff --git a/packages/cli/src/commands/cli/setup.ts b/packages/cli/src/commands/cli/setup.ts index 8453390193..fc14919640 100644 --- a/packages/cli/src/commands/cli/setup.ts +++ b/packages/cli/src/commands/cli/setup.ts @@ -15,7 +15,7 @@ import { installAgentSkills } from "../../lib/agent-skills.js"; import { determineInstallDir, getBinaryFilename, - getKnownInstallDirs, + getLegacyInstallDirs, type InstallationMethod, installBinary, parseInstallationMethod, @@ -126,17 +126,18 @@ async function migrateLegacyConfig( } /** - * Find a previously-installed binary in a known install directory other than - * the resolved target, so it can be migrated. The curl installer may have - * placed the binary in any of {@link getKnownInstallDirs} (e.g. `~/.sentry/bin` - * on older installs), so migration is not limited to `~/.sentry`. + * Find a binary in a *legacy* install directory (see {@link getLegacyInstallDirs}) + * that should be migrated into the resolved install target. Only genuinely + * pre-XDG locations are considered — `~/.local/bin` and `~/bin` are valid + * current targets and must never be treated as migration sources, or a working + * binary could be relocated out of an active directory. */ function findMigratableBinary( homeDir: string, targetDir: string, filename: string ): string | undefined { - for (const dir of getKnownInstallDirs(homeDir)) { + for (const dir of getLegacyInstallDirs(homeDir)) { if (samePath(dir, targetDir)) { continue; } diff --git a/packages/cli/src/lib/binary.ts b/packages/cli/src/lib/binary.ts index e884ac22d3..c0675d58cb 100644 --- a/packages/cli/src/lib/binary.ts +++ b/packages/cli/src/lib/binary.ts @@ -49,13 +49,15 @@ export function samePath(a: string, b: string): boolean { } /** - * Candidate directories a previously-installed binary may live in, in priority - * order. Used by migration to find a binary to move into the resolved install - * directory. Derived from {@link KNOWN_CURL_DIRS} so setup and upgrade agree on - * what counts as a prior install location. + * Directories a *legacy* install may have placed the binary in and that + * migration is allowed to move it out of. This is deliberately limited to the + * pre-XDG `~/.sentry/bin` — `~/.local/bin` and `~/bin` (also in + * {@link KNOWN_CURL_DIRS}) are valid *current* XDG install targets, so treating + * them as migration sources would relocate a working binary out of an active + * directory. Kept as a list so genuinely-legacy locations can be added later. */ -export function getKnownInstallDirs(homeDir: string): string[] { - return KNOWN_CURL_DIRS.map((dir) => join(homeDir, dir)); +export function getLegacyInstallDirs(homeDir: string): string[] { + return [join(homeDir, LEGACY_INSTALL_SUBDIR)]; } /** diff --git a/packages/cli/src/lib/shell.ts b/packages/cli/src/lib/shell.ts index a53603c77e..2e704fd378 100644 --- a/packages/cli/src/lib/shell.ts +++ b/packages/cli/src/lib/shell.ts @@ -172,6 +172,12 @@ export function isInPath( return false; } const paths = pathEnv.split(delimiter); + // Case-insensitive on case-insensitive filesystems (Windows, macOS): PATH + // entries can differ in casing from a computed directory yet be the same dir. + if (process.platform === "win32" || process.platform === "darwin") { + const target = directory.toLowerCase(); + return paths.some((p) => p.toLowerCase() === target); + } return paths.includes(directory); } diff --git a/packages/cli/test/commands/cli/setup.test.ts b/packages/cli/test/commands/cli/setup.test.ts index 0219a95650..82ce6ba2b1 100644 --- a/packages/cli/test/commands/cli/setup.test.ts +++ b/packages/cli/test/commands/cli/setup.test.ts @@ -1118,6 +1118,48 @@ describe("sentry cli setup — legacy migration", () => { "current-binary" ); }); + + test("does not migrate a binary out of ~/.local/bin (a valid target)", async () => { + // ~/.local/bin is a current XDG install target, not a legacy source: a + // binary there must never be relocated, even if it isn't the resolved dir. + const installDir = join(testHome, "install", "bin"); + const localBin = join(testHome, ".local", "bin"); + mkdirSync(localBin, { recursive: true }); + writeFileSync(join(localBin, "sentry"), "local-binary"); + + const { context, restore } = createMockContext({ + homeDir: testHome, + env: { SENTRY_INSTALL_DIR: installDir }, + }); + restoreStderr = restore; + + await run(app, setupArgs, context); + + // The ~/.local/bin binary stays put; nothing is copied to the target. + expect(existsSync(join(localBin, "sentry"))).toBe(true); + expect(await readFile(join(localBin, "sentry"), "utf8")).toBe( + "local-binary" + ); + expect(existsSync(join(installDir, "sentry"))).toBe(false); + }); + + test("does not migrate a binary out of ~/bin (a valid target)", async () => { + const installDir = join(testHome, "install", "bin"); + const homeBin = join(testHome, "bin"); + mkdirSync(homeBin, { recursive: true }); + writeFileSync(join(homeBin, "sentry"), "home-bin-binary"); + + const { context, restore } = createMockContext({ + homeDir: testHome, + env: { SENTRY_INSTALL_DIR: installDir }, + }); + restoreStderr = restore; + + await run(app, setupArgs, context); + + expect(existsSync(join(homeBin, "sentry"))).toBe(true); + expect(existsSync(join(installDir, "sentry"))).toBe(false); + }); }); describe("sentry cli setup — legacy migration records new path", () => { diff --git a/packages/cli/test/lib/binary.test.ts b/packages/cli/test/lib/binary.test.ts index 168c761962..1bc218ea17 100644 --- a/packages/cli/test/lib/binary.test.ts +++ b/packages/cli/test/lib/binary.test.ts @@ -25,7 +25,7 @@ import { getBinaryDownloadUrl, getBinaryFilename, getBinaryPaths, - getKnownInstallDirs, + getLegacyInstallDirs, getPlatformBinaryName, installBinary, isDowngrade, @@ -103,14 +103,13 @@ describe("samePath", () => { }); }); -describe("getKnownInstallDirs", () => { - test("returns the known curl dirs resolved against home", () => { - const dirs = getKnownInstallDirs("/home/user"); - expect(dirs).toEqual([ - join("/home/user", ".local", "bin"), - join("/home/user", "bin"), - join("/home/user", ".sentry", "bin"), - ]); +describe("getLegacyInstallDirs", () => { + test("returns only the pre-XDG ~/.sentry/bin, not current XDG targets", () => { + const dirs = getLegacyInstallDirs("/home/user"); + expect(dirs).toEqual([join("/home/user", ".sentry", "bin")]); + // ~/.local/bin and ~/bin are valid current targets, never migration sources + expect(dirs).not.toContain(join("/home/user", ".local", "bin")); + expect(dirs).not.toContain(join("/home/user", "bin")); }); }); diff --git a/packages/cli/test/lib/shell.test.ts b/packages/cli/test/lib/shell.test.ts index 6ddc6d0b60..a5e0571db9 100644 --- a/packages/cli/test/lib/shell.test.ts +++ b/packages/cli/test/lib/shell.test.ts @@ -17,6 +17,7 @@ import { findExistingConfigFile, getConfigCandidates, isBashAvailable, + isInPath, } from "../../src/lib/shell.js"; import { whichSync } from "../../src/lib/which.js"; @@ -359,6 +360,34 @@ describe("shell utilities", () => { }); }); +describe("isInPath", () => { + const sep = process.platform === "win32" ? ";" : ":"; + + test("returns true for an exact match", () => { + expect(isInPath("/usr/local/bin", `/usr/bin${sep}/usr/local/bin`)).toBe( + true + ); + }); + + test("returns false when the directory is absent", () => { + expect(isInPath("/opt/bin", `/usr/bin${sep}/usr/local/bin`)).toBe(false); + }); + + test("returns false for undefined or empty PATH", () => { + expect(isInPath("/usr/bin", undefined)).toBe(false); + expect(isInPath("/usr/bin", "")).toBe(false); + }); + + test("case sensitivity follows the platform", () => { + const result = isInPath("/Users/User/.local/bin", "/users/user/.local/bin"); + if (process.platform === "win32" || process.platform === "darwin") { + expect(result).toBe(true); + } else { + expect(result).toBe(false); + } + }); +}); + describe("isBashAvailable", () => { test("returns true when bash is in PATH", () => { // Point PATH at the directory containing bash From 325b4f23c22b71cb5a6f3c6aaacc8de9cc02bdee Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Mon, 7 Sep 2026 17:21:16 +0000 Subject: [PATCH 13/17] refactor: simplify samePath, dedupe isInPath, make legacy dirs a real list Address BYK review comments. - samePath: collapse to a single expression using a module-level CASE_INSENSITIVE_PLATFORMS Set instead of an if/branch. - isInPath (shell.ts): reuse samePath from binary.ts instead of duplicating the case-insensitive PATH comparison. - getLegacyInstallDirs: back it with a LEGACY_INSTALL_SUBDIRS array (currently the single pre-XDG ~/.sentry/bin) so the 'list' is real and extensible, matching the doc comment. No behavior change; tsc + biome clean, unit + e2e migration tests green. --- packages/cli/src/lib/binary.ts | 35 +++++++++++++++++++++++----------- packages/cli/src/lib/shell.ts | 12 ++++-------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/packages/cli/src/lib/binary.ts b/packages/cli/src/lib/binary.ts index c0675d58cb..0bd4ac2a45 100644 --- a/packages/cli/src/lib/binary.ts +++ b/packages/cli/src/lib/binary.ts @@ -29,12 +29,28 @@ import { isProcessRunning } from "./process-utils.js"; /** Known directories where the curl installer may place the binary */ export const KNOWN_CURL_DIRS = [".local/bin", "bin", ".sentry/bin"]; +/** Platforms whose filesystems are case-insensitive by default. */ +const CASE_INSENSITIVE_PLATFORMS = new Set([ + "win32", + "darwin", +]); + /** * Legacy install directory (relative to home) that predates the XDG layout. * The curl installer used to drop the binary here; migration moves it out. */ export const LEGACY_INSTALL_SUBDIR = join(".sentry", "bin"); +/** + * Legacy install sub-directories (relative to home) that predate the XDG + * layout and that migration is allowed to move a binary out of. Deliberately + * limited to the pre-XDG `~/.sentry/bin`: `~/.local/bin` and `~/bin` (also in + * {@link KNOWN_CURL_DIRS}) are valid *current* XDG install targets, so treating + * them as migration sources would relocate a working binary out of an active + * directory. An array so more legacy locations can be added if they ever exist. + */ +export const LEGACY_INSTALL_SUBDIRS = [LEGACY_INSTALL_SUBDIR]; + /** * Compare two filesystem paths for equality, case-insensitively on * case-insensitive filesystems (Windows, macOS). A stored path can differ in @@ -42,22 +58,19 @@ export const LEGACY_INSTALL_SUBDIR = join(".sentry", "bin"); * yet point at the same location, so a strict `===` would wrongly differ. */ export function samePath(a: string, b: string): boolean { - if (process.platform === "win32" || process.platform === "darwin") { - return a.toLowerCase() === b.toLowerCase(); - } - return a === b; + return ( + a === b || + (CASE_INSENSITIVE_PLATFORMS.has(process.platform) && + a.toLowerCase() === b.toLowerCase()) + ); } /** - * Directories a *legacy* install may have placed the binary in and that - * migration is allowed to move it out of. This is deliberately limited to the - * pre-XDG `~/.sentry/bin` — `~/.local/bin` and `~/bin` (also in - * {@link KNOWN_CURL_DIRS}) are valid *current* XDG install targets, so treating - * them as migration sources would relocate a working binary out of an active - * directory. Kept as a list so genuinely-legacy locations can be added later. + * Absolute legacy install directories for the given home. See + * {@link LEGACY_INSTALL_SUBDIRS} for why this is scoped to pre-XDG locations. */ export function getLegacyInstallDirs(homeDir: string): string[] { - return [join(homeDir, LEGACY_INSTALL_SUBDIR)]; + return LEGACY_INSTALL_SUBDIRS.map((dir) => join(homeDir, dir)); } /** diff --git a/packages/cli/src/lib/shell.ts b/packages/cli/src/lib/shell.ts index 2e704fd378..2e9b971c3c 100644 --- a/packages/cli/src/lib/shell.ts +++ b/packages/cli/src/lib/shell.ts @@ -8,6 +8,7 @@ import { existsSync } from "node:fs"; import { access, readFile, writeFile } from "node:fs/promises"; import { basename, delimiter, join } from "node:path"; +import { samePath } from "./binary.js"; import { logger } from "./logger.js"; import { whichSync } from "./which.js"; @@ -171,14 +172,9 @@ export function isInPath( if (!pathEnv) { return false; } - const paths = pathEnv.split(delimiter); - // Case-insensitive on case-insensitive filesystems (Windows, macOS): PATH - // entries can differ in casing from a computed directory yet be the same dir. - if (process.platform === "win32" || process.platform === "darwin") { - const target = directory.toLowerCase(); - return paths.some((p) => p.toLowerCase() === target); - } - return paths.includes(directory); + // samePath handles case-insensitive filesystems (Windows, macOS), where a + // PATH entry can differ in casing from a computed directory yet be the same. + return pathEnv.split(delimiter).some((p) => samePath(p, directory)); } /** From edce9f6900f451e1da36f5eedc703bff7bc90bb8 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Mon, 7 Sep 2026 17:31:11 +0000 Subject: [PATCH 14/17] fix(binary): case-insensitive PATH match in determineInstallDir The ~/.local/bin and ~/bin candidate check used pathDirs.includes(dir), a case-sensitive comparison. On Windows/macOS a PATH entry that differs only in casing from the computed dir would miss, falling back to ~/.local/bin and prompting a PATH edit when a valid dir was already present. Use samePath (case-insensitive on win32/darwin) for consistency with isInPath and the rest of the PR. Add a test covering the mixed-case PATH entry. --- packages/cli/src/lib/binary.ts | 6 ++++-- packages/cli/test/lib/binary.test.ts | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/lib/binary.ts b/packages/cli/src/lib/binary.ts index 0bd4ac2a45..76e16be851 100644 --- a/packages/cli/src/lib/binary.ts +++ b/packages/cli/src/lib/binary.ts @@ -297,11 +297,13 @@ export function determineInstallDir( return xdgBinHome; } - // 3-4. Check well-known directories that are already in PATH + // 3-4. Check well-known directories that are already in PATH. samePath keeps + // the membership check case-insensitive on Windows/macOS, where a PATH entry + // can differ in casing from the computed directory yet be the same dir. const candidates = [join(homeDir, ".local", "bin"), join(homeDir, "bin")]; for (const dir of candidates) { - if (existsSync(dir) && pathDirs.includes(dir)) { + if (existsSync(dir) && pathDirs.some((p) => samePath(p, dir))) { return dir; } } diff --git a/packages/cli/test/lib/binary.test.ts b/packages/cli/test/lib/binary.test.ts index 1bc218ea17..ca682c6eeb 100644 --- a/packages/cli/test/lib/binary.test.ts +++ b/packages/cli/test/lib/binary.test.ts @@ -151,6 +151,24 @@ describe("determineInstallDir", () => { expect(result).toBe(localBin); }); + test("matches a PATH entry case-insensitively on Windows/macOS", () => { + // Use ~/bin so the result is distinguishable from the ~/.local/bin fallback. + const homeBin = join(testDir, "bin"); + mkdirSync(homeBin, { recursive: true }); + + const result = determineInstallDir(testDir, { + PATH: `/usr/bin:${homeBin.toUpperCase()}`, + }); + + if (process.platform === "win32" || process.platform === "darwin") { + // Case-insensitive FS: the upper-cased PATH entry still matches ~/bin. + expect(result).toBe(homeBin); + } else { + // Case-sensitive FS: no match, so it falls back to the XDG default. + expect(result).toBe(join(testDir, ".local", "bin")); + } + }); + test("uses ~/bin when it exists and is in PATH but ~/.local/bin is not", () => { const homeBin = join(testDir, "bin"); mkdirSync(homeBin, { recursive: true }); From 3a26eb701d3aacef8a7f5bc5e9068a4709c5a0d4 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Mon, 7 Sep 2026 18:41:36 +0000 Subject: [PATCH 15/17] fix(upgrade): normalize trailing slash on XDG_BIN_HOME in known curl paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getKnownCurlPaths appended `sep` to a raw XDG_BIN_HOME, so a value with a trailing slash (e.g. /custom/bin/) produced /custom/bin// — a double separator that made process.execPath.startsWith(dir) miss, breaking curl install-method detection and upgrade path resolution. Extract the logic into a pure, testable buildKnownCurlPaths(homeDir, env) and normalize the XDG entry with join(xdgBinHome, '.') + sep so exactly one trailing separator is emitted. Add unit tests covering the trailing-slash, absolute, and non-absolute cases. --- packages/cli/src/lib/upgrade.ts | 42 ++++++++++++++++----------- packages/cli/test/lib/upgrade.test.ts | 37 ++++++++++++++++++++++- 2 files changed, 61 insertions(+), 18 deletions(-) diff --git a/packages/cli/src/lib/upgrade.ts b/packages/cli/src/lib/upgrade.ts index 990213186d..945adc48d9 100644 --- a/packages/cli/src/lib/upgrade.ts +++ b/packages/cli/src/lib/upgrade.ts @@ -90,26 +90,34 @@ export const VERSION_PREFIX_REGEX = /^v/; // Curl Binary Helpers /** - * Known directories where the curl installer may place the binary. - * Resolved at runtime against the user's home directory. - * Used for legacy detection (when no install info is stored). - * Trailing separator ensures startsWith matches a directory boundary - * (e.g. ~/.local/bin/ won't match ~/.local/binaries/). - * - * Computed lazily (not at module load) to avoid TDZ issues from circular - * imports — `KNOWN_CURL_DIRS` must be fully initialized before access. + * Build the list of known curl install directories the binary may live in, + * each with a trailing separator so `startsWith` matches a directory boundary + * (e.g. `~/.local/bin/` won't match `~/.local/binaries/`). Pure — takes home + * and env — so it can be unit-tested; `getKnownCurlPaths` memoizes the result. + */ +export function buildKnownCurlPaths( + homeDir: string, + env: NodeJS.ProcessEnv +): string[] { + const paths = KNOWN_CURL_DIRS.map((dir) => join(homeDir, dir) + sep); + // Honor an absolute XDG_BIN_HOME, matching determineInstallDir's precedence. + const xdgBinHome = env.XDG_BIN_HOME; + if (xdgBinHome && isAbsolute(xdgBinHome)) { + // join(dir, ".") strips any trailing separator so we don't emit a double + // separator (e.g. `/custom/bin//`) that would break the startsWith checks. + paths.push(join(xdgBinHome, ".") + sep); + } + return paths; +} + +/** + * Memoized known curl paths. Computed lazily (not at module load) to avoid TDZ + * issues from circular imports — `KNOWN_CURL_DIRS` must be fully initialized + * before access. */ let _knownCurlPaths: string[] | undefined; function getKnownCurlPaths(): string[] { - if (_knownCurlPaths === undefined) { - const paths = KNOWN_CURL_DIRS.map((dir) => join(homedir(), dir) + sep); - // Honor an absolute XDG_BIN_HOME, matching determineInstallDir's precedence - const xdgBinHome = process.env.XDG_BIN_HOME; - if (xdgBinHome && isAbsolute(xdgBinHome)) { - paths.push(xdgBinHome + sep); - } - _knownCurlPaths = paths; - } + _knownCurlPaths ??= buildKnownCurlPaths(homedir(), process.env); return _knownCurlPaths; } diff --git a/packages/cli/test/lib/upgrade.test.ts b/packages/cli/test/lib/upgrade.test.ts index 7ea0520dd4..7c6f63a4bb 100644 --- a/packages/cli/test/lib/upgrade.test.ts +++ b/packages/cli/test/lib/upgrade.test.ts @@ -19,7 +19,7 @@ import { } from "node:fs"; import { access, readFile, unlink, writeFile } from "node:fs/promises"; import { homedir, platform } from "node:os"; -import { join } from "node:path"; +import { join, sep } from "node:path"; import { setTimeout as sleep } from "node:timers/promises"; import { gzipSync } from "node:zlib"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; @@ -129,6 +129,7 @@ import { UpgradeError } from "../../src/lib/errors.js"; import { isProcessRunning } from "../../src/lib/process-utils.js"; const { + buildKnownCurlPaths, detectInstallationMethod, detectPackageManagerFromPath, downloadBinaryToTemp, @@ -986,6 +987,40 @@ describe("getBinaryDownloadUrl", () => { }); }); +describe("buildKnownCurlPaths", () => { + test("appends a trailing separator to each known dir", () => { + const paths = buildKnownCurlPaths("/home/user", {}); + expect(paths).toContain(join("/home/user", ".local", "bin") + sep); + expect(paths).toContain(join("/home/user", ".sentry", "bin") + sep); + expect(paths.every((p) => p.endsWith(sep))).toBe(true); + }); + + test("includes an absolute XDG_BIN_HOME", () => { + const xdgBin = join(homedir(), "custom", "bin"); + const paths = buildKnownCurlPaths("/home/user", { XDG_BIN_HOME: xdgBin }); + expect(paths).toContain(xdgBin + sep); + }); + + test("normalizes a trailing slash on XDG_BIN_HOME (no double separator)", () => { + const xdgBin = join(homedir(), "custom", "bin"); + const paths = buildKnownCurlPaths("/home/user", { + // Trailing separator on the configured dir must be normalized away. + XDG_BIN_HOME: xdgBin + sep, + }); + // Must end with a single sep, never a double sep which would break + // process.execPath.startsWith() directory-boundary checks. + expect(paths).toContain(xdgBin + sep); + expect(paths.some((p) => p.includes(sep + sep))).toBe(false); + }); + + test("ignores a non-absolute XDG_BIN_HOME", () => { + const paths = buildKnownCurlPaths("/home/user", { + XDG_BIN_HOME: join("relative", "bin"), + }); + expect(paths.some((p) => p.includes(`relative${sep}bin`))).toBe(false); + }); +}); + describe("getCurlInstallPaths", () => { test("returns all required paths", () => { const paths = getCurlInstallPaths(); From 9a845302492bbae302e7c561051fc14a4e8cea8c Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Mon, 7 Sep 2026 20:35:06 +0000 Subject: [PATCH 16/17] refactor(binary): resolve case-fold once; make samePath separator-tolerant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address BYK's review plus an independent adversarial review pass. - BYK: process.platform never changes at runtime, so compute IS_CASE_INSENSITIVE_FS once at module load (dropping the Set) and pick the case-folding step statically — no per-call platform check. - Adversarial review: samePath now strips a trailing separator (never from a bare root) before comparing, so a PATH entry like ~/.local/bin/ matches the computed ~/.local/bin. This fixes false negatives across all samePath call sites (isInPath, determineInstallDir PATH matching, upgrade relocation). - install script: only honor XDG_BIN_HOME when absolute, mirroring the Node determineInstallDir logic. - Tests: trailing-separator and root-not-stripped cases for samePath. tsc + biome clean; unit + e2e migration tests green. --- packages/cli/install | 6 +++- packages/cli/src/lib/binary.ts | 44 ++++++++++++++++++---------- packages/cli/test/lib/binary.test.ts | 15 +++++++++- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/packages/cli/install b/packages/cli/install index 03c7ac2d85..4e4eef8caf 100755 --- a/packages/cli/install +++ b/packages/cli/install @@ -337,7 +337,11 @@ trap - EXIT # interactively — when piped (curl | bash), stdin is the pipe. if [[ "${SENTRY_INIT:-}" == "1" ]]; then sentry_bin="" - for dir in "${SENTRY_INSTALL_DIR:-}" "${XDG_BIN_HOME:-}" "$HOME/.local/bin" "$HOME/bin" "$HOME/.sentry/bin"; do + # Only honor XDG_BIN_HOME when absolute, per the XDG spec (matches the Node + # determineInstallDir logic); a relative value is ignored. + xdg_bin_home="${XDG_BIN_HOME:-}" + [[ "$xdg_bin_home" != /* ]] && xdg_bin_home="" + for dir in "${SENTRY_INSTALL_DIR:-}" "$xdg_bin_home" "$HOME/.local/bin" "$HOME/bin" "$HOME/.sentry/bin"; do [[ -z "$dir" ]] && continue if [[ -x "${dir}/sentry" ]]; then sentry_bin="${dir}/sentry" diff --git a/packages/cli/src/lib/binary.ts b/packages/cli/src/lib/binary.ts index 76e16be851..3ebfcf3781 100644 --- a/packages/cli/src/lib/binary.ts +++ b/packages/cli/src/lib/binary.ts @@ -15,7 +15,7 @@ import { writeFileSync, } from "node:fs"; import { chmod, copyFile, mkdir, realpath, unlink } from "node:fs/promises"; -import { delimiter, dirname, isAbsolute, join, resolve } from "node:path"; +import { delimiter, dirname, isAbsolute, join, resolve, sep } from "node:path"; import { compare as semverCompare } from "semver"; import { getUserAgent } from "./constants.js"; import { @@ -29,11 +29,13 @@ import { isProcessRunning } from "./process-utils.js"; /** Known directories where the curl installer may place the binary */ export const KNOWN_CURL_DIRS = [".local/bin", "bin", ".sentry/bin"]; -/** Platforms whose filesystems are case-insensitive by default. */ -const CASE_INSENSITIVE_PLATFORMS = new Set([ - "win32", - "darwin", -]); +/** + * Whether the current platform's filesystem is case-insensitive by default + * (Windows, macOS). Resolved once at module load — `process.platform` never + * changes at runtime. + */ +const IS_CASE_INSENSITIVE_FS = + process.platform === "win32" || process.platform === "darwin"; /** * Legacy install directory (relative to home) that predates the XDG layout. @@ -52,17 +54,29 @@ export const LEGACY_INSTALL_SUBDIR = join(".sentry", "bin"); export const LEGACY_INSTALL_SUBDIRS = [LEGACY_INSTALL_SUBDIR]; /** - * Compare two filesystem paths for equality, case-insensitively on - * case-insensitive filesystems (Windows, macOS). A stored path can differ in - * casing from a freshly computed one (e.g. `C:\Users\User` vs `C:\Users\user`) - * yet point at the same location, so a strict `===` would wrongly differ. + * Strip a trailing path separator (but never from a bare root like `/`) so a + * PATH entry such as `~/.local/bin/` compares equal to `~/.local/bin`. + */ +function stripTrailingSep(p: string): string { + return p.length > 1 && p.endsWith(sep) ? p.slice(0, -1) : p; +} + +/** + * Compare two filesystem paths for equality. Tolerates a trailing separator on + * either side, and is case-insensitive on case-insensitive filesystems + * (Windows, macOS) — a stored path can differ in casing from a freshly computed + * one (e.g. `C:\Users\User` vs `C:\Users\user`) yet point at the same location, + * so a strict `===` would wrongly differ. + * + * The case-folding step is chosen once at module load from + * {@link IS_CASE_INSENSITIVE_FS} so there is no per-call platform check. */ +const foldCase: (p: string) => string = IS_CASE_INSENSITIVE_FS + ? (p) => p.toLowerCase() + : (p) => p; + export function samePath(a: string, b: string): boolean { - return ( - a === b || - (CASE_INSENSITIVE_PLATFORMS.has(process.platform) && - a.toLowerCase() === b.toLowerCase()) - ); + return foldCase(stripTrailingSep(a)) === foldCase(stripTrailingSep(b)); } /** diff --git a/packages/cli/test/lib/binary.test.ts b/packages/cli/test/lib/binary.test.ts index ca682c6eeb..6fcfb3b1aa 100644 --- a/packages/cli/test/lib/binary.test.ts +++ b/packages/cli/test/lib/binary.test.ts @@ -15,7 +15,7 @@ import { writeFileSync, } from "node:fs"; import { access, readFile, writeFile } from "node:fs/promises"; -import { join } from "node:path"; +import { join, sep } from "node:path"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { acquireLock, @@ -101,6 +101,19 @@ describe("samePath", () => { expect(result).toBe(false); } }); + + test("tolerates a trailing separator on either side", () => { + const dir = join("/home/user", ".local", "bin"); + expect(samePath(dir + sep, dir)).toBe(true); + expect(samePath(dir, dir + sep)).toBe(true); + expect(samePath(dir + sep, dir + sep)).toBe(true); + }); + + test("does not treat root as equal to empty after stripping", () => { + // A bare root separator must not be stripped to "". + expect(samePath(sep, sep)).toBe(true); + expect(samePath(sep, "")).toBe(false); + }); }); describe("getLegacyInstallDirs", () => { From 9da4417d57ea7abb6d8b1c4039fc202756f2a71d Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Mon, 7 Sep 2026 22:31:10 +0000 Subject: [PATCH 17/17] fix(install): accept Windows-absolute XDG_BIN_HOME; inline samePath variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BugBot: the install-script absolute-path guard only accepted POSIX /… paths, dropping a Windows drive-letter XDG_BIN_HOME (C:\… or C:/…) while Node's determineInstallDir (isAbsolute) still installs there — so SENTRY_INIT=1 couldn't find the binary. Accept drive-letter absolute paths too. - Per review: drop the foldCase helper and select the whole samePath implementation statically from IS_CASE_INSENSITIVE_FS (toLowerCase variant vs plain comparison). --- packages/cli/install | 8 ++++++-- packages/cli/src/lib/binary.ts | 14 ++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/cli/install b/packages/cli/install index 4e4eef8caf..e05cef2018 100755 --- a/packages/cli/install +++ b/packages/cli/install @@ -338,9 +338,13 @@ trap - EXIT if [[ "${SENTRY_INIT:-}" == "1" ]]; then sentry_bin="" # Only honor XDG_BIN_HOME when absolute, per the XDG spec (matches the Node - # determineInstallDir logic); a relative value is ignored. + # determineInstallDir logic); a relative value is ignored. Accept both POSIX + # (/…) and Windows drive-letter (C:\… or C:/…) absolute paths so a Windows + # XDG_BIN_HOME isn't dropped while Node still installs there. xdg_bin_home="${XDG_BIN_HOME:-}" - [[ "$xdg_bin_home" != /* ]] && xdg_bin_home="" + if [[ "$xdg_bin_home" != /* && ! "$xdg_bin_home" =~ ^[A-Za-z]:[\\/] ]]; then + xdg_bin_home="" + fi for dir in "${SENTRY_INSTALL_DIR:-}" "$xdg_bin_home" "$HOME/.local/bin" "$HOME/bin" "$HOME/.sentry/bin"; do [[ -z "$dir" ]] && continue if [[ -x "${dir}/sentry" ]]; then diff --git a/packages/cli/src/lib/binary.ts b/packages/cli/src/lib/binary.ts index 3ebfcf3781..75582612a3 100644 --- a/packages/cli/src/lib/binary.ts +++ b/packages/cli/src/lib/binary.ts @@ -68,16 +68,14 @@ function stripTrailingSep(p: string): string { * one (e.g. `C:\Users\User` vs `C:\Users\user`) yet point at the same location, * so a strict `===` would wrongly differ. * - * The case-folding step is chosen once at module load from + * The implementation is chosen once at module load from * {@link IS_CASE_INSENSITIVE_FS} so there is no per-call platform check. */ -const foldCase: (p: string) => string = IS_CASE_INSENSITIVE_FS - ? (p) => p.toLowerCase() - : (p) => p; - -export function samePath(a: string, b: string): boolean { - return foldCase(stripTrailingSep(a)) === foldCase(stripTrailingSep(b)); -} +export const samePath: (a: string, b: string) => boolean = + IS_CASE_INSENSITIVE_FS + ? (a, b) => + stripTrailingSep(a).toLowerCase() === stripTrailingSep(b).toLowerCase() + : (a, b) => stripTrailingSep(a) === stripTrailingSep(b); /** * Absolute legacy install directories for the given home. See