Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions apps/cli-docs/src/content/docs/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,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

Expand Down Expand Up @@ -177,7 +178,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

Expand Down
15 changes: 14 additions & 1 deletion apps/cli-docs/src/fragments/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,24 @@ 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
- Region URL mappings
- 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` 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` 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.
2 changes: 1 addition & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/install
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
139 changes: 138 additions & 1 deletion packages/cli/src/commands/cli/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@
*/

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";
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 {
Expand All @@ -28,6 +31,7 @@ import {
getAgentSkillsPreference,
setAgentSkillsPreference,
} from "../../lib/db/defaults.js";
import { closeDatabase, resolveXdgConfigDir } from "../../lib/db/index.js";
import { setInstallInfo } from "../../lib/db/install-info.js";
import {
parseReleaseChannel,
Expand Down Expand Up @@ -80,6 +84,111 @@ function formatSetupResult(result: SetupResult): string {
return result.messages.join("\n");
}

/**
* Migrate `cli.db` (+ WAL sidecars) and the old `config.json` out of the legacy
* `~/.sentry` directory into the XDG config directory.
*
* 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.
*/
async function migrateLegacyConfig(
homeDir: string,
env: NodeJS.ProcessEnv,
emit: Logger
): Promise<void> {
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 (samePath(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();
await mkdir(targetConfigDir, { recursive: true, mode: 0o700 });
for (const name of configFiles) {
const from = join(legacyDir, name);
if (existsSync(from)) {
await rename(from, join(targetConfigDir, name));
}
Comment thread
cursor[bot] marked this conversation as resolved.
}
emit(`Config: Migrated ${legacyDir} → ${targetConfigDir}`);
}

/**
* 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 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
): Promise<string | undefined> {
const filename = getBinaryFilename();
const targetDir = determineInstallDir(homeDir, env);
const targetBin = join(targetDir, filename);
if (existsSync(targetBin)) {
return;
}

const legacyBin = findMigratableBinary(homeDir, targetDir, filename);
if (!legacyBin) {
Comment thread
jared-outpost[bot] marked this conversation as resolved.
return;
}

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.
await chmod(targetBin, 0o755);
try {
await unlink(legacyBin);
} catch (error) {
// Leave the old binary in place if it can't be removed — the new copy
Comment thread
jared-outpost[bot] marked this conversation as resolved.
// 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}`);
return targetBin;
}

/**
* Handle binary installation from a temp location.
*
Expand Down Expand Up @@ -556,7 +665,35 @@ 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. Config and binary migrations
// 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)
Comment thread
cursor[bot] marked this conversation as resolved.
if (flags.install) {
const result = await handleInstall(
process.execPath,
Expand Down
49 changes: 46 additions & 3 deletions packages/cli/src/commands/cli/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@

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 {
determineInstallDir,
isDowngrade,
LEGACY_INSTALL_SUBDIR,
releaseLock,
samePath,
} from "../../lib/binary.js";
import { buildCommand } from "../../lib/command.js";
import { CLI_VERSION } from "../../lib/constants.js";
Expand All @@ -42,6 +44,7 @@ import {
type ChangelogSummary,
fetchChangelog,
} from "../../lib/release-notes.js";
import { isInPath } from "../../lib/shell.js";
import {
detectInstallationMethod,
executeUpgrade,
Expand Down Expand Up @@ -564,6 +567,41 @@ 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(), LEGACY_INSTALL_SUBDIR);
if (!samePath(currentInstallDir, legacyBinDir)) {
Comment thread
jared-outpost[bot] marked this conversation as resolved.
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 (
!samePath(xdgInstallDir, legacyBinDir) &&
isInPath(xdgInstallDir, pathEnv)
) {
return xdgInstallDir;
}
return currentInstallDir;
Comment thread
jared-outpost[bot] marked this conversation as resolved.
}

/**
* Execute the standard upgrade path: download via curl or package manager,
* then run setup on the new binary.
Expand Down Expand Up @@ -615,17 +653,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,
});
Expand Down
Loading
Loading