Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
876fdf6
refactor: unify all command functions as async generators
BYK Mar 12, 2026
0fe387a
refactor: brand CommandOutput with Symbol and move hints to generator…
BYK Mar 13, 2026
edc3ef3
refactor: remove stdout/stderr plumbing from commands
BYK Mar 13, 2026
68c59da
refactor(auth/login): yield LoginResult instead of using logger for o…
BYK Mar 13, 2026
2c18734
refactor(log/list): remove LogStreamChunk — func yields data, not ren…
BYK Mar 13, 2026
839105e
chore: regenerate SKILL.md
github-actions[bot] Mar 13, 2026
2cd03d3
refactor: OutputConfig.human becomes a factory returning HumanRenderer
BYK Mar 13, 2026
792fe0e
refactor(log/list): use HumanRenderer factory, remove module-level state
BYK Mar 13, 2026
bfc89fb
test: add tests for HumanRenderer factory and finalize()
BYK Mar 13, 2026
c2ca94c
fix: update command.test.ts for stateless() wrapper
BYK Mar 13, 2026
afd68ba
fix: address review findings from BugBot and Seer
BYK Mar 13, 2026
3c07efc
fix: update help.test.ts for printCustomHelp return-based API
BYK Mar 13, 2026
945956a
fix: apply filterFields per-element in formatTraceLogs
BYK Mar 13, 2026
5c4c1b1
fix: finalization on error and empty-state hint rendering
BYK Mar 13, 2026
fc3e594
fix: address PR #416 review comments
BYK Mar 14, 2026
51bc242
fix: address round 2 review findings
BYK Mar 14, 2026
e5dc4b0
refactor: address round 3 review comments
BYK Mar 14, 2026
42c6bbb
refactor: address round 4 review comments
BYK Mar 15, 2026
3df286c
chore: regenerate SKILL.md
github-actions[bot] Mar 15, 2026
db8c84a
fix: guard seenWithoutTs dedup against undefined l.id
BYK Mar 15, 2026
349b8bb
refactor: address round 5 review comments
BYK Mar 15, 2026
77db155
fix: include warnings in cli setup JSON output
BYK Mar 15, 2026
e92a851
fix: return empty array for log list --json with no results, add init…
BYK Mar 15, 2026
d56ced1
refactor: log/list yields bare items in follow mode for proper JSONL
BYK Mar 16, 2026
fd69e7f
fix: trim trailing newlines in help and trace logs formatters
BYK Mar 16, 2026
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
70 changes: 20 additions & 50 deletions AGENTS.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Authenticate with Sentry
- `--token <value> - Authenticate using an API token instead of OAuth`
- `--timeout <value> - Timeout for OAuth flow in seconds (default: 900) - (default: "900")`
- `--force - Re-authenticate without prompting`
- `--json - Output as JSON`
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`

**Examples:**

Expand Down Expand Up @@ -109,6 +111,10 @@ sentry auth status

Print the stored authentication token

**Flags:**
- `--json - Output as JSON`
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`

#### `sentry auth whoami`

Show the currently authenticated user
Expand Down Expand Up @@ -485,6 +491,8 @@ Configure shell integration
- `--no-completions - Skip shell completion installation`
- `--no-agent-skills - Skip agent skill installation for AI coding assistants`
- `--quiet - Suppress output (for scripted usage)`
- `--json - Output as JSON`
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`

#### `sentry cli upgrade <version>`

Expand Down
6 changes: 1 addition & 5 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,7 @@ const autoAuthMiddleware: ErrorMiddleware = async (next, args) => {
: "Authentication required. Starting login flow...\n\n"
);

const loginSuccess = await runInteractiveLogin(
process.stdout,
process.stderr,
process.stdin
);
const loginSuccess = await runInteractiveLogin();

if (loginSuccess) {
process.stderr.write("\nRetrying command...\n\n");
Expand Down
22 changes: 11 additions & 11 deletions src/commands/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { SentryContext } from "../context.js";
import { buildSearchParams, rawApiRequest } from "../lib/api-client.js";
import { buildCommand } from "../lib/command.js";
import { OutputError, ValidationError } from "../lib/errors.js";
import { CommandOutput } from "../lib/formatters/output.js";
import { validateEndpoint } from "../lib/input-validation.js";
import { logger } from "../lib/logger.js";
import { getDefaultSdkConfig } from "../lib/sentry-client.js";
Expand Down Expand Up @@ -1052,7 +1053,7 @@ function logResponse(response: { status: number; headers: Headers }): void {
}

export const apiCommand = buildCommand({
output: { json: true, human: formatApiResponse },
output: { human: formatApiResponse },
docs: {
brief: "Make an authenticated API request",
fullDescription:
Expand Down Expand Up @@ -1155,7 +1156,7 @@ export const apiCommand = buildCommand({
n: "dry-run",
},
},
async func(this: SentryContext, flags: ApiFlags, endpoint: string) {
async *func(this: SentryContext, flags: ApiFlags, endpoint: string) {
const { stdin } = this;

const normalizedEndpoint = normalizeEndpoint(endpoint);
Expand All @@ -1168,14 +1169,13 @@ export const apiCommand = buildCommand({

// Dry-run mode: preview the request that would be sent
if (flags["dry-run"]) {
return {
data: {
method: flags.method,
url: resolveRequestUrl(normalizedEndpoint, params),
headers: resolveEffectiveHeaders(headers, body),
body: body ?? null,
},
};
yield new CommandOutput({
method: flags.method,
url: resolveRequestUrl(normalizedEndpoint, params),
headers: resolveEffectiveHeaders(headers, body),
body: body ?? null,
});
return;
}

const verbose = flags.verbose && !flags.silent;
Expand Down Expand Up @@ -1210,6 +1210,6 @@ export const apiCommand = buildCommand({
throw new OutputError(response.body);
}

return { data: response.body };
return yield new CommandOutput(response.body);
},
});
85 changes: 50 additions & 35 deletions src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,38 @@ import {
import { getDbPath } from "../../lib/db/index.js";
import { getUserInfo, setUserInfo } from "../../lib/db/user.js";
import { AuthError } from "../../lib/errors.js";
import { formatUserIdentity } from "../../lib/formatters/human.js";
import { success } from "../../lib/formatters/colors.js";
import {
formatDuration,
formatUserIdentity,
} from "../../lib/formatters/human.js";
import { CommandOutput } from "../../lib/formatters/output.js";
import type { LoginResult } from "../../lib/interactive-login.js";
import { runInteractiveLogin } from "../../lib/interactive-login.js";
import { logger } from "../../lib/logger.js";
import { clearResponseCache } from "../../lib/response-cache.js";

const log = logger.withTag("auth.login");

/** Format a {@link LoginResult} for human-readable terminal output. */
function formatLoginResult(result: LoginResult): string {
const lines: string[] = [];
lines.push(
success(
`✔ ${result.method === "token" ? "Authenticated with API token" : "Authentication successful!"}`
)
);
if (result.user) {
lines.push(` Logged in as: ${formatUserIdentity(result.user)}`);
}
lines.push(` Config saved to: ${result.configPath}`);
if (result.expiresIn) {
lines.push(` Token expires in: ${formatDuration(result.expiresIn)}`);
}
lines.push(""); // trailing newline
return lines.join("\n");
}

type LoginFlags = {
readonly token?: string;
readonly timeout: number;
Expand Down Expand Up @@ -104,7 +129,8 @@ export const loginCommand = buildCommand({
},
},
},
async func(this: SentryContext, flags: LoginFlags): Promise<void> {
output: { human: formatLoginResult },
async *func(this: SentryContext, flags: LoginFlags) {
// Check if already authenticated and handle re-authentication
if (await isAuthenticated()) {
const shouldProceed = await handleExistingAuth(flags.force);
Expand All @@ -113,15 +139,15 @@ export const loginCommand = buildCommand({
}
}

// Clear stale cached responses from a previous session
try {
await clearResponseCache();
} catch {
// Non-fatal: cache directory may not exist
}

// Token-based authentication
if (flags.token) {
// Clear stale cached responses from a previous session
try {
await clearResponseCache();
} catch {
// Non-fatal: cache directory may not exist
}

// Save token first, then validate by fetching user regions
await setAuthToken(flags.token);

Expand All @@ -139,46 +165,35 @@ export const loginCommand = buildCommand({

// Fetch and cache user info via /auth/ (works with all token types).
// A transient failure here must not block login — the token is already valid.
let user: Awaited<ReturnType<typeof getCurrentUser>> | undefined;
const result: LoginResult = {
method: "token",
configPath: getDbPath(),
};
try {
user = await getCurrentUser();
const user = await getCurrentUser();
setUserInfo({
userId: user.id,
email: user.email,
username: user.username,
name: user.name,
});
result.user = user;
} catch {
// Non-fatal: user info is supplementary. Token remains stored and valid.
}

log.success("Authenticated with API token");
if (user) {
log.info(`Logged in as: ${formatUserIdentity(user)}`);
}
log.info(`Config saved to: ${getDbPath()}`);
return;
return yield new CommandOutput(result);
}

// Clear stale cached responses from a previous session
try {
await clearResponseCache();
} catch {
// Non-fatal: cache directory may not exist
}

const { stdout, stderr } = this;
const loginSuccess = await runInteractiveLogin(
stdout,
stderr,
process.stdin,
{
timeout: flags.timeout * 1000,
}
);
// OAuth device flow
const result = await runInteractiveLogin({
timeout: flags.timeout * 1000,
});

if (!loginSuccess) {
// Error already displayed by runInteractiveLogin - just set exit code
if (result) {
yield new CommandOutput(result);
} else {
// Error already displayed by runInteractiveLogin
process.exitCode = 1;
}
},
Expand Down
22 changes: 11 additions & 11 deletions src/commands/auth/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { getDbPath } from "../../lib/db/index.js";
import { AuthError } from "../../lib/errors.js";
import { formatLogoutResult } from "../../lib/formatters/human.js";
import { CommandOutput } from "../../lib/formatters/output.js";

/** Structured result of the logout operation */
export type LogoutResult = {
Expand All @@ -32,15 +33,16 @@ export const logoutCommand = buildCommand({
fullDescription:
"Remove stored authentication credentials from the local database.",
},
output: { json: true, human: formatLogoutResult },
output: { human: formatLogoutResult },
parameters: {
flags: {},
},
async func(this: SentryContext): Promise<{ data: LogoutResult }> {
async *func(this: SentryContext) {
if (!(await isAuthenticated())) {
return {
data: { loggedOut: false, message: "Not currently authenticated." },
};
return yield new CommandOutput({
loggedOut: false,
message: "Not currently authenticated.",
});
}

if (isEnvTokenActive()) {
Expand All @@ -55,11 +57,9 @@ export const logoutCommand = buildCommand({
const configPath = getDbPath();
await clearAuth();

return {
data: {
loggedOut: true,
configPath,
},
};
return yield new CommandOutput({
loggedOut: true,
configPath,
});
},
});
7 changes: 4 additions & 3 deletions src/commands/auth/refresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { AuthError } from "../../lib/errors.js";
import { success } from "../../lib/formatters/colors.js";
import { formatDuration } from "../../lib/formatters/human.js";
import { CommandOutput } from "../../lib/formatters/output.js";

type RefreshFlags = {
readonly json: boolean;
Expand Down Expand Up @@ -58,7 +59,7 @@ Examples:
{"success":true,"refreshed":true,"expiresIn":3600,"expiresAt":"..."}
`.trim(),
},
output: { json: true, human: formatRefreshResult },
output: { human: formatRefreshResult },
parameters: {
flags: {
force: {
Expand All @@ -68,7 +69,7 @@ Examples:
},
},
},
async func(this: SentryContext, flags: RefreshFlags) {
async *func(this: SentryContext, flags: RefreshFlags) {
// Env var tokens can't be refreshed
if (isEnvTokenActive()) {
const envVar = getActiveEnvVarName();
Expand Down Expand Up @@ -104,6 +105,6 @@ Examples:
: undefined,
};

return { data: payload };
return yield new CommandOutput(payload);
},
});
7 changes: 4 additions & 3 deletions src/commands/auth/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { getDbPath } from "../../lib/db/index.js";
import { getUserInfo } from "../../lib/db/user.js";
import { AuthError, stringifyUnknown } from "../../lib/errors.js";
import { formatAuthStatus, maskToken } from "../../lib/formatters/human.js";
import { CommandOutput } from "../../lib/formatters/output.js";
import {
applyFreshFlag,
FRESH_ALIASES,
Expand Down Expand Up @@ -143,7 +144,7 @@ export const statusCommand = buildCommand({
"Display information about your current authentication status, " +
"including whether you're logged in and your default organization/project settings.",
},
output: { json: true, human: formatAuthStatus },
output: { human: formatAuthStatus },
parameters: {
flags: {
"show-token": {
Expand All @@ -155,7 +156,7 @@ export const statusCommand = buildCommand({
},
aliases: FRESH_ALIASES,
},
async func(this: SentryContext, flags: StatusFlags) {
async *func(this: SentryContext, flags: StatusFlags) {
applyFreshFlag(flags);

const auth = getAuthConfig();
Expand Down Expand Up @@ -189,6 +190,6 @@ export const statusCommand = buildCommand({
verification: await verifyCredentials(),
};

return { data };
return yield new CommandOutput(data);
},
});
15 changes: 6 additions & 9 deletions src/commands/auth/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,25 @@ import type { SentryContext } from "../../context.js";
import { buildCommand } from "../../lib/command.js";
import { getAuthToken } from "../../lib/db/auth.js";
import { AuthError } from "../../lib/errors.js";
import { CommandOutput } from "../../lib/formatters/output.js";

export const tokenCommand = buildCommand({
docs: {
brief: "Print the stored authentication token",
fullDescription:
"Print the stored authentication token to stdout.\n\n" +
"This outputs the raw token without any formatting, making it suitable for " +
"piping to other commands or scripts. The token is printed without a trailing newline " +
"when stdout is not a TTY (e.g., when piped).",
"piping to other commands or scripts.",
},
parameters: {},
func(this: SentryContext): void {
const { stdout } = this;

output: { human: (token: string) => token },
// biome-ignore lint/suspicious/useAwait: sync body but async generator required by buildCommand
async *func(this: SentryContext) {
const token = getAuthToken();
if (!token) {
throw new AuthError("not_authenticated");
}

// Add newline only if stdout is a TTY (interactive terminal)
// When piped, omit newline for cleaner output
const suffix = process.stdout.isTTY ? "\n" : "";
stdout.write(`${token}${suffix}`);
return yield new CommandOutput(token);
},
});
Loading
Loading