Skip to content

Commit 154d87b

Browse files
authored
feat: expose CLI as a programmatic library (#565)
## Summary Exposes the Sentry CLI as a JavaScript/TypeScript library with a typed SDK that auto-generates methods for every CLI command. ```typescript import createSentrySDK from "sentry"; const sdk = createSentrySDK({ token: "sntrys_..." }); // Typed methods — use CLI route names directly const orgs = await sdk.org.list(); const issues = await sdk.issues.list({ org: "acme", project: "frontend", limit: 5 }); const issue = await sdk.issue.view({ issueId: "ACME-123" }); await sdk.issue.explain({ issueId: "ACME-123" }); // Nested commands await sdk.dashboard.widget.add({ ... }); // Escape hatch for any command const raw = await sdk.run("api", "/organizations/", "--method", "GET"); ``` 44 commands auto-discovered from the route tree. Zero manual config. ## API `createSentrySDK(options?)` is the **single entry point** (default export). | Option | Type | Default | Description | |--------|------|---------|-------------| | `token` | `string` | Auto-detected from env | Auth token (falls back to `SENTRY_AUTH_TOKEN` / `SENTRY_TOKEN`) | | `text` | `boolean` | `false` | Return human-readable text instead of parsed JSON | | `cwd` | `string` | `process.cwd()` | Working directory for DSN auto-detection | - Typed methods bypass Stricli's string dispatch — flags as objects, direct `Command.loader()` invocation - `sdk.run(...args)` escape hatch routes through Stricli for commands not yet typed or interactive workflows - Errors throw `SentryError` with `.exitCode` and `.stderr` ## Architecture ### Auto-generated SDK (`script/generate-sdk.ts`) Build-time codegen walks the route tree via Stricli introspection: - Discovers all commands recursively, skips hidden routes (plural aliases) - Uses CLI route names as-is: `org.list`, `issue.explain`, `dashboard.widget.add` - Extracts flag definitions → typed parameter interfaces - Reads `__jsonSchema` (#582) → typed return types from Zod schemas - Derives positional params from placeholder strings - Zero config — new commands are automatically included ### Direct command invocation (`src/lib/sdk-invoke.ts`) `buildInvoker()` resolves commands from the route tree (cached), calls `Command.loader()` directly with pre-built flag objects. `buildRunner()` provides the `sdk.run()` escape hatch via Stricli's `run()`. ### Env registry (`src/lib/env.ts`) `getEnv()`/`setEnv()` replaces all direct `process.env` reads (~14 files). Library mode creates an isolated env copy — consumer's `process.env` is never mutated. ### CLI extraction (`src/cli.ts`) CLI runner extracted from `bin.ts`. Both the npm bin wrapper and library share the same internals. ### Single npm bundle esbuild entry: `src/index.ts` → `dist/index.cjs`. Tiny `dist/bin.cjs` wrapper (~200 bytes) for CLI. Package size stays flat. ### Zero-copy return `captureObject` duck-type on Writer hands back in-memory objects directly. ### Library-safe telemetry `initSentry({ libraryMode: true })` strips global-polluting integrations. ### Lazy `node:sqlite` Polyfill defers `node:sqlite` import to first DB access, so `require("sentry")` doesn't crash. ## Tests - `test/lib/env.test.ts` — env registry (5 tests) - `test/lib/index.test.ts` — `createSentrySDK` + `sdk.run()` (7 tests) - `test/lib/sdk.test.ts` — typed SDK methods + namespaces (7 tests) - `test/e2e/library.test.ts` — bundled library via Node.js subprocesses (17 tests) ## Documentation - README "Library Usage" section - Full docs page at `docs/src/content/docs/library-usage.md` ## Related - #566 / #582 — Schema registration on OutputConfig (landed, used for return types)
1 parent d88e735 commit 154d87b

43 files changed

Lines changed: 2584 additions & 869 deletions

Some content is hidden

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

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ docs/.astro
5454

5555
# Generated files (rebuilt at build time)
5656
src/generated/
57+
src/sdk.generated.ts
58+
src/sdk.generated.d.cts
5759

5860
# OpenCode
5961
.opencode/

AGENTS.md

Lines changed: 30 additions & 65 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,38 @@ For detailed documentation, visit [cli.sentry.dev](https://cli.sentry.dev).
8383

8484
Credentials are stored in `~/.sentry/` with restricted permissions (mode 600).
8585

86+
## Library Usage
87+
88+
Use Sentry CLI programmatically in Node.js (≥22) or Bun without spawning a subprocess:
89+
90+
```typescript
91+
import createSentrySDK from "sentry";
92+
93+
const sdk = createSentrySDK({ token: "sntrys_..." });
94+
95+
// Typed methods for every CLI command
96+
const orgs = await sdk.org.list();
97+
const issues = await sdk.issue.list({ orgProject: "acme/frontend", limit: 5 });
98+
const issue = await sdk.issue.view({ issue: "ACME-123" });
99+
100+
// Nested commands
101+
await sdk.dashboard.widget.add({ display: "line", query: "count" }, "my-org/my-dashboard");
102+
103+
// Escape hatch for any CLI command
104+
const version = await sdk.run("--version");
105+
const text = await sdk.run("issue", "list", "-l", "5");
106+
```
107+
108+
Options (all optional):
109+
- `token` — Auth token. Falls back to `SENTRY_AUTH_TOKEN` / `SENTRY_TOKEN` env vars.
110+
- `url` — Sentry instance URL for self-hosted (e.g., `"sentry.example.com"`).
111+
- `org` — Default organization slug (avoids passing it on every call).
112+
- `project` — Default project slug.
113+
- `text` — Return human-readable string instead of parsed JSON (affects `run()` only).
114+
- `cwd` — Working directory for DSN auto-detection. Defaults to `process.cwd()`.
115+
116+
Errors are thrown as `SentryError` with `.exitCode` and `.stderr`.
117+
86118
---
87119

88120
## Development

biome.jsonc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,14 @@
6767
}
6868
},
6969
{
70+
// src/index.ts is the library entry point — re-exports SentryError, SentryOptions, SentrySDK
7071
// db/index.ts exports db connection utilities - not a barrel file but triggers the rule
7172
// api-client.ts is a barrel that re-exports from src/lib/api/ domain modules
7273
// to preserve the existing import path for all consumers
7374
// markdown.ts re-exports isPlainOutput from plain-detect.ts for backward compat
7475
// script/debug-id.ts re-exports from src/lib/sourcemap/debug-id.ts for build scripts
7576
"includes": [
77+
"src/index.ts",
7678
"src/lib/db/index.ts",
7779
"src/lib/api-client.ts",
7880
"src/lib/formatters/markdown.ts",

docs/astro.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ export default defineConfig({
199199
{ label: "Installation", slug: "getting-started" },
200200
{ label: "Self-Hosted", slug: "self-hosted" },
201201
{ label: "Configuration", slug: "configuration" },
202+
{ label: "Library Usage", slug: "library-usage" },
202203
],
203204
},
204205
{
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
---
2+
title: Library Usage
3+
description: Use the Sentry CLI programmatically in Node.js or Bun
4+
---
5+
6+
The Sentry CLI can be used as a JavaScript/TypeScript library, running commands
7+
in-process without spawning a subprocess. This is useful for AI coding agents,
8+
build tools, CI scripts, and other tools that want structured Sentry data.
9+
10+
## Installation
11+
12+
```bash
13+
npm install sentry
14+
```
15+
16+
## Quick Start
17+
18+
```typescript
19+
import createSentrySDK from "sentry";
20+
21+
const sdk = createSentrySDK({ token: "sntrys_..." });
22+
23+
// Typed methods for every CLI command
24+
const orgs = await sdk.org.list();
25+
const issues = await sdk.issue.list({ orgProject: "acme/frontend", limit: 5 });
26+
```
27+
28+
## Typed SDK
29+
30+
`createSentrySDK()` returns an object with typed methods for **every** CLI command,
31+
organized by the CLI route hierarchy:
32+
33+
```typescript
34+
import createSentrySDK from "sentry";
35+
36+
const sdk = createSentrySDK({ token: "sntrys_..." });
37+
```
38+
39+
### Organizations
40+
41+
```typescript
42+
const orgs = await sdk.org.list();
43+
const org = await sdk.org.view({ org: "acme" });
44+
```
45+
46+
### Projects
47+
48+
```typescript
49+
const projects = await sdk.project.list({ orgProject: "acme/" });
50+
const project = await sdk.project.view({ orgProject: "acme/frontend" });
51+
```
52+
53+
### Issues
54+
55+
```typescript
56+
const issues = await sdk.issue.list({
57+
orgProject: "acme/frontend",
58+
limit: 10,
59+
query: "is:unresolved",
60+
sort: "date",
61+
});
62+
63+
const issue = await sdk.issue.view({ issue: "ACME-123" });
64+
```
65+
66+
### Events, Traces, Spans
67+
68+
```typescript
69+
const event = await sdk.event.view({}, "abc123...");
70+
71+
const traces = await sdk.trace.list({ orgProject: "acme/frontend" });
72+
const trace = await sdk.trace.view({}, "abc123...");
73+
74+
const spans = await sdk.span.list({}, "acme/frontend");
75+
```
76+
77+
### Dashboards
78+
79+
```typescript
80+
const dashboards = await sdk.dashboard.list({}, "acme/");
81+
const dashboard = await sdk.dashboard.view({}, "acme/", "my-dashboard");
82+
83+
// Nested widget commands
84+
await sdk.dashboard.widget.add(
85+
{ display: "line", query: "count" },
86+
"acme/", "my-dashboard"
87+
);
88+
```
89+
90+
### Teams
91+
92+
```typescript
93+
const teams = await sdk.team.list({ orgProject: "acme/" });
94+
```
95+
96+
### Authentication
97+
98+
```typescript
99+
await sdk.auth.login();
100+
await sdk.auth.status();
101+
const whoami = await sdk.auth.whoami();
102+
```
103+
104+
The typed SDK invokes command handlers directly — bypassing CLI string parsing
105+
for zero overhead beyond the command's own logic.
106+
107+
## Escape Hatch: `run()`
108+
109+
For commands not easily expressed through the typed API, or when you want to
110+
pass raw CLI flags, use `sdk.run()`:
111+
112+
```typescript
113+
// Run any CLI command — returns parsed JSON by default
114+
const version = await sdk.run("--version");
115+
const issues = await sdk.run("issue", "list", "-l", "5");
116+
const help = await sdk.run("help", "issue");
117+
```
118+
119+
## Authentication
120+
121+
The `token` option provides an auth token for the current invocation. When
122+
omitted, it falls back to environment variables and stored credentials:
123+
124+
1. `token` option (highest priority)
125+
2. `SENTRY_AUTH_TOKEN` environment variable
126+
3. `SENTRY_TOKEN` environment variable
127+
4. Stored OAuth token from `sentry auth login`
128+
129+
```typescript
130+
// Explicit token
131+
const sdk = createSentrySDK({ token: "sntrys_..." });
132+
133+
// Or set the env var — it's picked up automatically
134+
process.env.SENTRY_AUTH_TOKEN = "sntrys_...";
135+
const sdk = createSentrySDK();
136+
```
137+
138+
## Options
139+
140+
All options are optional. Pass them when creating the SDK:
141+
142+
```typescript
143+
const sdk = createSentrySDK({ token: "...", text: true, cwd: "/my/project" });
144+
```
145+
146+
| Option | Type | Default | Description |
147+
|--------|------|---------|-------------|
148+
| `token` | `string` | Auto-detected | Auth token for this invocation |
149+
| `url` | `string` | `sentry.io` | Sentry instance URL for self-hosted |
150+
| `org` | `string` | Auto-detected | Default organization slug |
151+
| `project` | `string` | Auto-detected | Default project slug |
152+
| `text` | `boolean` | `false` | Return human-readable text instead of parsed JSON (`run()` only) |
153+
| `cwd` | `string` | `process.cwd()` | Working directory for DSN auto-detection |
154+
155+
## Return Values
156+
157+
Typed SDK methods return **parsed JavaScript objects** with zero serialization
158+
overhead (via zero-copy capture). The `run()` escape hatch returns parsed JSON
159+
by default, or a trimmed string for commands without JSON support.
160+
161+
```typescript
162+
// Typed methods → typed return
163+
const issues = await sdk.issue.list({ orgProject: "acme/frontend" });
164+
// IssueListResult type with known fields
165+
166+
// run() → parsed JSON or string
167+
const version = await sdk.run("--version");
168+
// "sentry 0.21.0"
169+
```
170+
171+
## Error Handling
172+
173+
Commands that fail throw a `SentryError`:
174+
175+
```typescript
176+
import createSentrySDK, { SentryError } from "sentry";
177+
178+
const sdk = createSentrySDK();
179+
180+
try {
181+
await sdk.issue.view({ issue: "NONEXISTENT-1" });
182+
} catch (err) {
183+
if (err instanceof SentryError) {
184+
console.error(err.message); // Clean error message (no ANSI codes)
185+
console.error(err.exitCode); // Non-zero exit code
186+
console.error(err.stderr); // Raw stderr output
187+
}
188+
}
189+
```
190+
191+
## Environment Isolation
192+
193+
The library never mutates `process.env`. Each invocation creates an isolated
194+
copy of the environment. This means:
195+
196+
- Your application's env vars are never touched
197+
- Multiple sequential calls are safe
198+
- Auth tokens passed via `token` don't leak to subsequent calls
199+
200+
:::note
201+
Concurrent calls are not supported in the current version.
202+
Calls should be sequential (awaited one at a time).
203+
:::
204+
205+
## Comparison with Subprocess
206+
207+
| | Library (`createSentrySDK()`) | Subprocess (`child_process`) |
208+
|---|---|---|
209+
| **Startup** | ~0ms (in-process) | ~200ms (process spawn + init) |
210+
| **Output** | Parsed object (zero-copy) | String (needs JSON.parse) |
211+
| **Errors** | `SentryError` with typed fields | Exit code + stderr string |
212+
| **Auth** | `token` option or env vars | Env vars only |
213+
| **Node.js** | >=22 required | Any version |
214+
215+
## Requirements
216+
217+
- **Node.js >= 22** (required for `node:sqlite`)
218+
- Or **Bun** (any recent version)
219+
220+
:::caution
221+
Streaming flags (`--refresh`, `--follow`) are not supported in library mode
222+
and will throw a `SentryError`. Use the CLI binary directly for live-streaming commands.
223+
:::

package.json

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,23 @@
4646
"bin": {
4747
"sentry": "./dist/bin.cjs"
4848
},
49+
"main": "./dist/index.cjs",
50+
"types": "./dist/index.d.cts",
51+
"exports": {
52+
".": {
53+
"types": "./dist/index.d.cts",
54+
"require": "./dist/index.cjs",
55+
"default": "./dist/index.cjs"
56+
}
57+
},
4958
"description": "Sentry CLI - A command-line interface for using Sentry built by robots and humans for robots and humans",
5059
"engines": {
5160
"node": ">=22"
5261
},
5362
"files": [
54-
"dist/bin.cjs"
63+
"dist/bin.cjs",
64+
"dist/index.cjs",
65+
"dist/index.d.cts"
5566
],
5667
"license": "FSL-1.1-Apache-2.0",
5768
"packageManager": "bun@1.3.11",
@@ -61,18 +72,19 @@
6172
"@sentry/node-core@10.44.0": "patches/@sentry%2Fnode-core@10.44.0.patch"
6273
},
6374
"scripts": {
64-
"dev": "bun run generate:schema && bun run src/bin.ts",
65-
"build": "bun run generate:schema && bun run script/build.ts --single",
66-
"build:all": "bun run generate:schema && bun run script/build.ts",
67-
"bundle": "bun run generate:schema && bun run script/bundle.ts",
68-
"typecheck": "tsc --noEmit",
75+
"dev": "bun run generate:schema && bun run generate:sdk && bun run src/bin.ts",
76+
"build": "bun run generate:schema && bun run generate:sdk && bun run script/build.ts --single",
77+
"build:all": "bun run generate:schema && bun run generate:sdk && bun run script/build.ts",
78+
"bundle": "bun run generate:schema && bun run generate:sdk && bun run script/bundle.ts",
79+
"typecheck": "bun run generate:sdk && tsc --noEmit",
6980
"lint": "bunx ultracite check",
7081
"lint:fix": "bunx ultracite fix",
7182
"test": "bun run test:unit && bun run test:isolated",
72-
"test:unit": "bun test --timeout 15000 test/lib test/commands test/types --coverage --coverage-reporter=lcov",
73-
"test:isolated": "bun test --timeout 15000 test/isolated",
74-
"test:e2e": "bun test --timeout 15000 test/e2e",
83+
"test:unit": "bun run generate:sdk && bun test --timeout 15000 test/lib test/commands test/types --coverage --coverage-reporter=lcov",
84+
"test:isolated": "bun run generate:sdk && bun test --timeout 15000 test/isolated",
85+
"test:e2e": "bun run generate:sdk && bun test --timeout 15000 test/e2e",
7586
"test:init-eval": "bun test test/init-eval --timeout 600000 --concurrency 6",
87+
"generate:sdk": "bun run script/generate-sdk.ts",
7688
"generate:skill": "bun run script/generate-skill.ts",
7789
"generate:schema": "bun run script/generate-api-schema.ts",
7890
"generate:command-docs": "bun run script/generate-command-docs.ts",

0 commit comments

Comments
 (0)