Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
70 changes: 20 additions & 50 deletions AGENTS.md

Large diffs are not rendered by default.

2 changes: 2 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
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(process.stdin);

if (loginSuccess) {
process.stderr.write("\nRetrying command...\n\n");
Expand Down
23 changes: 12 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, stateless } 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: { json: true, human: stateless(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 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,7 @@ export const apiCommand = buildCommand({
throw new OutputError(response.body);
}

return { data: response.body };
yield commandOutput(response.body);
return;
},
});
89 changes: 55 additions & 34 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, stateless } 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: { json: true, human: stateless(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,41 @@ 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 = {
name: user.name,
email: user.email,
username: user.username,
id: user.id,
};
} 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()}`);
yield commandOutput(result);
return;
}

// 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(process.stdin, {
timeout: flags.timeout * 1000,
});

if (!loginSuccess) {
// Error already displayed by runInteractiveLogin - just set exit code
if (result) {
yield commandOutput(result);
} else {
// Error already displayed by runInteractiveLogin
process.exitCode = 1;
}
},
Expand Down
24 changes: 13 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, stateless } from "../../lib/formatters/output.js";

/** Structured result of the logout operation */
export type LogoutResult = {
Expand All @@ -32,15 +33,17 @@ export const logoutCommand = buildCommand({
fullDescription:
"Remove stored authentication credentials from the local database.",
},
output: { json: true, human: formatLogoutResult },
output: { json: true, human: stateless(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." },
};
yield commandOutput({
loggedOut: false,
message: "Not currently authenticated.",
});
return;
}

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

return {
data: {
loggedOut: true,
configPath,
},
};
yield commandOutput({
loggedOut: true,
configPath,
});
return;
},
});
8 changes: 5 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, stateless } 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: { json: true, human: stateless(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,7 @@ Examples:
: undefined,
};

return { data: payload };
yield commandOutput(payload);
return;
},
});
8 changes: 5 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, stateless } 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: { json: true, human: stateless(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,7 @@ export const statusCommand = buildCommand({
verification: await verifyCredentials(),
};

return { data };
yield commandOutput(data);
return;
},
});
4 changes: 3 additions & 1 deletion src/commands/auth/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ export const tokenCommand = buildCommand({
"when stdout is not a TTY (e.g., when piped).",
},
parameters: {},
func(this: SentryContext): void {
// biome-ignore lint/correctness/useYield: void generator — writes to stdout directly
// biome-ignore lint/suspicious/useAwait: sync body but async generator required by buildCommand
async *func(this: SentryContext) {
const { stdout } = this;

const token = getAuthToken();
Expand Down
8 changes: 5 additions & 3 deletions src/commands/auth/whoami.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { isAuthenticated } from "../../lib/db/auth.js";
import { setUserInfo } from "../../lib/db/user.js";
import { AuthError } from "../../lib/errors.js";
import { formatUserIdentity } from "../../lib/formatters/index.js";
import { commandOutput, stateless } from "../../lib/formatters/output.js";
import {
applyFreshFlag,
FRESH_ALIASES,
Expand All @@ -35,15 +36,15 @@ export const whoamiCommand = buildCommand({
},
output: {
json: true,
human: formatUserIdentity,
human: stateless(formatUserIdentity),
},
parameters: {
flags: {
fresh: FRESH_FLAG,
},
aliases: FRESH_ALIASES,
},
async func(this: SentryContext, flags: WhoamiFlags) {
async *func(this: SentryContext, flags: WhoamiFlags) {
applyFreshFlag(flags);

if (!(await isAuthenticated())) {
Expand All @@ -65,6 +66,7 @@ export const whoamiCommand = buildCommand({
// Cache update failure is non-essential — user identity was already fetched.
}

return { data: user };
yield commandOutput(user);
return;
},
});
Loading
Loading