Skip to content

Commit 4bddd1f

Browse files
betegonclaude
andcommitted
merge: resolve api-client.ts conflict with main's modular refactor
Port deleteProject into src/lib/api/projects.ts and re-export from the barrel file, aligning with main's split of api-client into domain modules under src/lib/api/. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2 parents 37ed511 + a6d62d7 commit 4bddd1f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+6930
-3702
lines changed

AGENTS.md

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,9 @@ mock.module("./some-module", () => ({
628628

629629
### Architecture
630630

631+
<!-- lore:019ce2be-39f1-7ad9-a4c5-4506b62f689c -->
632+
* **api-client.ts split into domain modules under src/lib/api/**: The original monolithic \`src/lib/api-client.ts\` (1,977 lines) was split into 12 focused domain modules under \`src/lib/api/\`: infrastructure.ts (shared helpers, types, raw requests), organizations.ts, projects.ts, teams.ts, repositories.ts, issues.ts, events.ts, traces.ts, logs.ts, seer.ts, trials.ts, users.ts. The original \`api-client.ts\` was converted to a ~100-line barrel re-export file preserving all existing import paths. The \`biome.jsonc\` override for \`noBarrelFile\` already includes \`api-client.ts\`. When adding new API functions, place them in the appropriate domain module under \`src/lib/api/\`, not in the barrel file.
633+
631634
<!-- lore:019cb8ea-c6f0-75d8-bda7-e32b4e217f92 -->
632635
* **CLI telemetry DSN is public write-only — safe to embed in install script**: The CLI's Sentry DSN (\`SENTRY\_CLI\_DSN\` in \`src/lib/constants.ts\`) is a public write-only ingest key already baked into every binary. Safe to hardcode in install scripts. Opt-out: \`SENTRY\_CLI\_NO\_TELEMETRY=1\`.
633636
@@ -659,15 +662,9 @@ mock.module("./some-module", () => ({
659662
<!-- lore:019c8ab6-d119-7365-9359-98ecf464b704 -->
660663
* **@sentry/api SDK passes Request object to custom fetch — headers lost on Node.js**: @sentry/api SDK calls \`\_fetch(request)\` with no init object. In \`authenticatedFetch\`, \`init\` is undefined so \`prepareHeaders\` creates empty headers — on Node.js this strips Content-Type (HTTP 415). Fix: fall back to \`input.headers\` when \`init\` is undefined. Use \`unwrapPaginatedResult\` (not \`unwrapResult\`) to access the Response's Link header for pagination. \`per\_page\` is not in SDK types; cast query to pass it at runtime.
661664
662-
<!-- lore:019cc484-f0e1-7016-a851-177fb9ad2cc4 -->
663-
* **AGENTS.md must be excluded from markdown linters**: AGENTS.md is auto-managed by lore and uses \`\*\` list markers and long lines that violate typical remark-lint rules (unordered-list-marker-style, maximum-line-length). When a project uses remark with \`--frail\` (warnings become errors), AGENTS.md will fail CI. Fix: add \`AGENTS.md\` to \`.remarkignore\`. This applies to any lore-managed project with markdown linting.
664-
665665
<!-- lore:019c9e98-7af4-7e25-95f4-fc06f7abf564 -->
666666
* **Bun binary build requires SENTRY\_CLIENT\_ID env var**: The build script (\`script/bundle.ts\`) requires \`SENTRY\_CLIENT\_ID\` environment variable and exits with code 1 if missing. When building locally, use \`bun run --env-file=.env.local build\` or set the env var explicitly. The binary build (\`bun run build\`) also needs it. Without it you get: \`Error: SENTRY\_CLIENT\_ID environment variable is required.\`
667667
668-
<!-- lore:019cc40e-e56e-71e9-bc5d-545f97df732b -->
669-
* **Consola prompt cancel returns truthy Symbol, not false**: When a user cancels a \`consola\` / \`@clack/prompts\` confirmation prompt (Ctrl+C), the return value is \`Symbol(clack:cancel)\`, not \`false\`. Since Symbols are truthy in JavaScript, checking \`!confirmed\` will be \`false\` and the code falls through as if the user confirmed. Fix: use \`confirmed !== true\` (strict equality) instead of \`!confirmed\` to correctly handle cancel, false, and any other non-true values.
670-
671668
<!-- lore:019c9776-e3dd-7632-88b8-358a19506218 -->
672669
* **GitHub immutable releases prevent rolling nightly tag pattern**: getsentry/cli has immutable GitHub releases — assets can't be modified and tags can NEVER be reused. Nightly builds publish to GHCR with versioned tags like \`nightly-0.14.0-dev.1772661724\`, not GitHub Releases or npm. \`fetchManifest()\` throws \`UpgradeError("network\_error")\` for both network failures and non-200 — callers must check message for HTTP 404/403. Craft with no \`preReleaseCommand\` silently skips \`bump-version.sh\` if only target is \`github\`.
673670
@@ -680,16 +677,13 @@ mock.module("./some-module", () => ({
680677
<!-- lore:019c9741-d78e-73b1-87c2-e360ef6c7475 -->
681678
* **useTestConfigDir without isolateProjectRoot causes DSN scanning of repo tree**: \`useTestConfigDir()\` creates temp dirs under \`.test-tmp/\` in the repo tree. Without \`{ isolateProjectRoot: true }\`, \`findProjectRoot\` walks up and finds the repo's \`.git\`, causing DSN detection to scan real source code and trigger network calls against test mocks (timeouts). Always pass \`isolateProjectRoot: true\` when tests exercise \`resolveOrg\`, \`detectDsn\`, or \`findProjectRoot\`.
682679
683-
<!-- lore:019cc303-e397-75b9-9762-6f6ad108f50a -->
684-
* **Zod z.coerce.number() converts null to 0 silently**: Zod gotchas in this codebase: (1) \`z.coerce.number()\` passes input through \`Number()\`, so \`null\` silently becomes \`0\`. Be aware if \`null\` vs \`0\` distinction matters. (2) Zod v4 \`.default({})\` short-circuits — it returns the default value without parsing through inner schema defaults. So \`.object({ enabled: z.boolean().default(true) }).default({})\` returns \`{}\`, not \`{ enabled: true }\`. Fix: provide fully-populated default objects. This affected nested config sections in src/config.ts during the v3→v4 upgrade.
685-
686680
### Pattern
687681
688682
<!-- lore:019c972c-9f11-7c0d-96ce-3f8cc2641175 -->
689683
* **Org-scoped SDK calls follow getOrgSdkConfig + unwrapResult pattern**: All org-scoped API calls in src/lib/api-client.ts: (1) call \`getOrgSdkConfig(orgSlug)\` for regional URL + SDK config, (2) spread into SDK function: \`{ ...config, path: { organization\_id\_or\_slug: orgSlug, ... } }\`, (3) pass to \`unwrapResult(result, errorContext)\`. Shared helpers \`resolveAllTargets\`/\`resolveOrgAndProject\` must NOT call \`fetchProjectId\` — commands that need it enrich targets themselves.
690684
691685
<!-- lore:5ac4e219-ea1f-41cb-8e97-7e946f5848c0 -->
692-
* **PR workflow: wait for Seer and Cursor BugBot before resolving**: After pushing a PR in getsentry/cli, CI includes Seer Code Review and Cursor Bugbot as advisory checks (~2-3 min). They may not trigger on draft PRs — use \`gh pr ready\` first. Workflow: push → \`gh pr checks \<PR> --watch\` → check for bot review comments → fix → repeat. Use GraphQL \`reviewThreads\` query with \`isResolved\` filter to find unresolved comments. Reply to bot comments after fixing.
686+
* **PR workflow: wait for Seer and Cursor BugBot before resolving**: After pushing a PR in the getsentry/cli repo, the CI pipeline includes Seer Code Review and Cursor Bugbot as advisory checks. Both typically take 2-3 minutes but may not trigger on draft PRs — only ready-for-review PRs reliably get bot reviews. The workflow is: push → wait for all CI (including npm build jobs which test the actual bundle) → check for inline review comments from Seer/BugBot → fix if needed → repeat. Use \`gh pr checks \<PR> --watch\` to monitor. Review comments are fetched via \`gh api repos/OWNER/REPO/pulls/NUM/comments\` and \`gh api repos/OWNER/REPO/pulls/NUM/reviews\`.
693687
694688
<!-- lore:019cb162-d3ad-7b05-ab4f-f87892d517a6 -->
695689
* **Shared pagination infrastructure: buildPaginationContextKey and parseCursorFlag**: List commands with cursor pagination use \`buildPaginationContextKey(type, identifier, flags)\` for composite context keys and \`parseCursorFlag(value)\` accepting \`"last"\` magic value. Critical: \`resolveCursor()\` must be called inside the \`org-all\` override closure, not before \`dispatchOrgScopedList\` — otherwise cursor validation errors fire before the correct mode-specific error.

DEVELOPMENT.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,11 @@ When creating your Sentry OAuth application:
6767

6868
## Environment Variables
6969

70-
| Variable | Description | Default |
71-
| ------------------ | ------------------------------------ | -------------------- |
72-
| `SENTRY_CLIENT_ID` | Sentry OAuth app client ID | (required) |
73-
| `SENTRY_URL` | Sentry instance URL (for self-hosted)| `https://sentry.io` |
70+
| Variable | Description | Default |
71+
| ------------------ | ----------------------------------------------------- | -------------------- |
72+
| `SENTRY_CLIENT_ID` | Sentry OAuth app client ID | (required) |
73+
| `SENTRY_HOST` | Sentry instance URL (for self-hosted, takes precedence) | `https://sentry.io` |
74+
| `SENTRY_URL` | Alias for `SENTRY_HOST` | `https://sentry.io` |
7475

7576
## Building
7677

biome.jsonc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@
5656
},
5757
{
5858
// db/index.ts exports db connection utilities - not a barrel file but triggers the rule
59-
"includes": ["src/lib/db/index.ts"],
59+
// api-client.ts is a barrel that re-exports from src/lib/api/ domain modules
60+
// to preserve the existing import path for all consumers
61+
"includes": ["src/lib/db/index.ts", "src/lib/api-client.ts"],
6062
"linter": {
6163
"rules": {
6264
"performance": {

docs/src/content/docs/commands/auth.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,18 +84,18 @@ sentry auth refresh
8484

8585
This is typically handled automatically when tokens expire.
8686

87-
## Configuration
87+
## Credential Storage
8888

89-
Credentials are stored in `~/.sentry/config.json` with restricted file permissions (mode 600).
89+
Credentials are stored in a SQLite database at `~/.sentry/cli.db` with restricted file permissions (mode 600).
9090

91-
**Config structure:**
91+
Use `sentry auth token` to retrieve your current access token, or `sentry auth status` to check authentication state.
9292

93-
```json
94-
{
95-
"auth": {
96-
"token": "...",
97-
"refreshToken": "...",
98-
"expiresAt": "2024-12-31T00:00:00Z"
99-
}
100-
}
101-
```
93+
### Environment Variable Precedence
94+
95+
The CLI checks for auth tokens in the following order, using the first one found:
96+
97+
1. `SENTRY_AUTH_TOKEN` environment variable (legacy)
98+
2. `SENTRY_TOKEN` environment variable
99+
3. The stored token in the SQLite database
100+
101+
When a token comes from an environment variable, the CLI skips expiry checks and automatic refresh.

docs/src/content/docs/configuration.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,22 @@ The Sentry CLI can be configured through environment variables and a local datab
77

88
## Environment Variables
99

10-
### `SENTRY_URL`
10+
### `SENTRY_HOST`
1111

1212
Base URL of your Sentry instance. **Only needed for [self-hosted Sentry](./self-hosted/).** SaaS users (sentry.io) should not set this.
1313

1414
```bash
15-
export SENTRY_URL=https://sentry.example.com
15+
export SENTRY_HOST=https://sentry.example.com
1616
```
1717

1818
When set, all API requests (including OAuth login) are directed to this URL instead of `https://sentry.io`. The CLI also sets this automatically when you pass a self-hosted Sentry URL as a command argument.
1919

20+
`SENTRY_HOST` takes precedence over `SENTRY_URL`. Both work identically — use whichever you prefer.
21+
22+
### `SENTRY_URL`
23+
24+
Alias for `SENTRY_HOST`. If both are set, `SENTRY_HOST` takes precedence.
25+
2026
### `SENTRY_ORG`
2127

2228
Default organization slug. Skips organization auto-detection.
@@ -138,9 +144,11 @@ The `sentry api` command also uses `--verbose` to show full HTTP request/respons
138144

139145
## Credential Storage
140146

141-
Credentials are stored in a SQLite database at `~/.sentry/` (or the path set by `SENTRY_CONFIG_DIR`) with restricted file permissions (mode 600) for security. The database also caches:
147+
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:
142148

143149
- Organization and project defaults
144150
- DSN resolution results
145151
- Region URL mappings
146152
- Project aliases (for monorepo support)
153+
154+
See [Credential Storage](./commands/auth/#credential-storage) in the auth command docs for more details.

docs/src/content/docs/self-hosted.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ title: Self-Hosted Sentry
33
description: Using the Sentry CLI with a self-hosted Sentry instance
44
---
55

6-
The CLI works with self-hosted Sentry instances. Set the `SENTRY_URL` environment variable to point at your instance:
6+
The CLI works with self-hosted Sentry instances. Set the `SENTRY_HOST` (or `SENTRY_URL`) environment variable to point at your instance:
77

88
```bash
9-
export SENTRY_URL=https://sentry.example.com
9+
export SENTRY_HOST=https://sentry.example.com
1010
```
1111

1212
## Authenticating
@@ -27,14 +27,14 @@ The OAuth device flow requires **Sentry 26.1.0 or later** and a public OAuth app
2727
Pass your instance URL and the client ID:
2828

2929
```bash
30-
SENTRY_URL=https://sentry.example.com SENTRY_CLIENT_ID=your-client-id sentry auth login
30+
SENTRY_HOST=https://sentry.example.com SENTRY_CLIENT_ID=your-client-id sentry auth login
3131
```
3232

3333
:::tip
3434
You can export both variables in your shell profile so every CLI invocation picks them up:
3535

3636
```bash
37-
export SENTRY_URL=https://sentry.example.com
37+
export SENTRY_HOST=https://sentry.example.com
3838
export SENTRY_CLIENT_ID=your-client-id
3939
```
4040
:::
@@ -48,7 +48,7 @@ If your instance is on an older version or you prefer not to create an OAuth app
4848
3. Pass it to the CLI:
4949

5050
```bash
51-
SENTRY_URL=https://sentry.example.com sentry auth login --token YOUR_TOKEN
51+
SENTRY_HOST=https://sentry.example.com sentry auth login --token YOUR_TOKEN
5252
```
5353

5454
## After Login
@@ -66,7 +66,8 @@ If you pass a self-hosted Sentry URL as a command argument (e.g., an issue or ev
6666

6767
| Variable | Description |
6868
|----------|-------------|
69-
| `SENTRY_URL` | Base URL of your Sentry instance |
69+
| `SENTRY_HOST` | Base URL of your Sentry instance (takes precedence over `SENTRY_URL`) |
70+
| `SENTRY_URL` | Alias for `SENTRY_HOST` |
7071
| `SENTRY_CLIENT_ID` | Client ID of your public OAuth application |
7172
| `SENTRY_ORG` | Default organization slug |
7273
| `SENTRY_PROJECT` | Default project slug (supports `org/project` format) |

plugins/sentry-cli/skills/sentry-cli/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Authenticate with Sentry
4444
**Flags:**
4545
- `--token <value> - Authenticate using an API token instead of OAuth`
4646
- `--timeout <value> - Timeout for OAuth flow in seconds (default: 900) - (default: "900")`
47+
- `--force - Re-authenticate without prompting`
4748

4849
**Examples:**
4950

src/commands/auth/login.ts

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isatty } from "node:tty";
12
import type { SentryContext } from "../../context.js";
23
import { getCurrentUser, getUserRegions } from "../../lib/api-client.js";
34
import { buildCommand, numberParser } from "../../lib/command.js";
@@ -9,7 +10,7 @@ import {
910
setAuthToken,
1011
} from "../../lib/db/auth.js";
1112
import { getDbPath } from "../../lib/db/index.js";
12-
import { setUserInfo } from "../../lib/db/user.js";
13+
import { getUserInfo, setUserInfo } from "../../lib/db/user.js";
1314
import { AuthError } from "../../lib/errors.js";
1415
import { formatUserIdentity } from "../../lib/formatters/human.js";
1516
import { runInteractiveLogin } from "../../lib/interactive-login.js";
@@ -21,8 +22,58 @@ const log = logger.withTag("auth.login");
2122
type LoginFlags = {
2223
readonly token?: string;
2324
readonly timeout: number;
25+
readonly force: boolean;
2426
};
2527

28+
/**
29+
* Handle the case where the user is already authenticated.
30+
*
31+
* Returns `true` if the login flow should proceed (credentials cleared),
32+
* or `false` if the command should exit early.
33+
*
34+
* - Env-var auth: always blocks re-auth (user must unset the var).
35+
* - `--force`: clears auth silently and proceeds.
36+
* - Interactive TTY: prompts user to confirm re-authentication.
37+
* - Non-interactive without `--force`: prints a message and blocks.
38+
*/
39+
async function handleExistingAuth(force: boolean): Promise<boolean> {
40+
if (isEnvTokenActive()) {
41+
const envVar = getActiveEnvVarName();
42+
log.info(
43+
`Authentication is provided via ${envVar} environment variable. ` +
44+
`Unset ${envVar} to use OAuth-based login instead.`
45+
);
46+
return false;
47+
}
48+
49+
if (!force) {
50+
// Non-interactive (piped, CI): print message and block
51+
if (!isatty(0)) {
52+
log.info(
53+
"You are already authenticated. Use '--force' or 'sentry auth logout' first to re-authenticate."
54+
);
55+
return false;
56+
}
57+
58+
// Interactive TTY: prompt user to confirm re-authentication
59+
const userInfo = getUserInfo();
60+
const identity = userInfo ? formatUserIdentity(userInfo) : "current user";
61+
const confirmed = await log.prompt(
62+
`Already authenticated as ${identity}. Re-authenticate?`,
63+
{ type: "confirm", initial: false }
64+
);
65+
66+
// Symbol(clack:cancel) is truthy — strict equality check
67+
if (confirmed !== true) {
68+
return false;
69+
}
70+
}
71+
72+
// Clear existing credentials and caches before re-authenticating
73+
await clearAuth();
74+
return true;
75+
}
76+
2677
export const loginCommand = buildCommand({
2778
docs: {
2879
brief: "Authenticate with Sentry",
@@ -46,23 +97,20 @@ export const loginCommand = buildCommand({
4697
// Stricli requires string defaults (raw CLI input); numberParser converts to number
4798
default: "900",
4899
},
100+
force: {
101+
kind: "boolean",
102+
brief: "Re-authenticate without prompting",
103+
default: false,
104+
},
49105
},
50106
},
51107
async func(this: SentryContext, flags: LoginFlags): Promise<void> {
52-
// Check if already authenticated
108+
// Check if already authenticated and handle re-authentication
53109
if (await isAuthenticated()) {
54-
if (isEnvTokenActive()) {
55-
const envVar = getActiveEnvVarName();
56-
log.info(
57-
`Authentication is provided via ${envVar} environment variable. ` +
58-
`Unset ${envVar} to use OAuth-based login instead.`
59-
);
60-
} else {
61-
log.info(
62-
"You are already authenticated. Use 'sentry auth logout' first to re-authenticate."
63-
);
110+
const shouldProceed = await handleExistingAuth(flags.force);
111+
if (!shouldProceed) {
112+
return;
64113
}
65-
return;
66114
}
67115

68116
// Token-based authentication

src/commands/auth/logout.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const logoutCommand = buildCommand({
3030
docs: {
3131
brief: "Log out of Sentry",
3232
fullDescription:
33-
"Remove stored authentication credentials from the configuration file.",
33+
"Remove stored authentication credentials from the local database.",
3434
},
3535
output: { json: true, human: formatLogoutResult },
3636
parameters: {

0 commit comments

Comments
 (0)