Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e53a1c0
feat(formatters): render all terminal output via marked-terminal
BYK Feb 26, 2026
cd7301f
fix(deps): move marked/marked-terminal to devDependencies
BYK Feb 26, 2026
dcb8576
fix(formatters): properly escape backslashes and pipes in markdown ta…
BYK Feb 26, 2026
befef7e
fix(formatters): add missing escapeMarkdownCell imports in trace.ts a…
BYK Feb 26, 2026
78dd5e1
feat(formatters): add plain output mode with isTTY detection and env …
BYK Feb 26, 2026
0d179ef
fix(e2e): update org/project view label checks for markdown table format
BYK Feb 26, 2026
98f1bb7
refactor(formatters): address PR review feedback
BYK Feb 26, 2026
7f55c53
refactor(formatters): extract mdRow/mdKvTable helpers, simplify colum…
BYK Feb 26, 2026
5412424
test(formatters): add coverage for formatEventDetails, formatSolution…
BYK Feb 26, 2026
e56b843
fix(formatters): address Seer code review feedback
BYK Feb 26, 2026
f21ad65
fix(formatters): address second round of Seer/BugBot review feedback
BYK Feb 26, 2026
cda4525
fix(formatters): address third round of bot review feedback
BYK Feb 26, 2026
7161a3d
fix(formatters): escape SDK name in event details and rootTransaction…
BYK Feb 26, 2026
c78b433
fix(formatters): escape all user-supplied values in markdown table cells
BYK Feb 26, 2026
1dc5d49
fix(formatters): mdKvTable: use U+2502 instead of backslash-pipe for …
BYK Feb 26, 2026
7234d74
fix(formatters): apply missing mdRow pipe-bleed-through guard
BYK Feb 26, 2026
faa9086
fix(formatters): escape markdown inline chars in issue titles and rem…
BYK Feb 26, 2026
b9df73d
fix(formatters): restore severity color in formatLogRow and remove da…
BYK Feb 26, 2026
6d51cf2
fix(formatters): escape org/project names in detail views and log mes…
BYK Feb 26, 2026
1d4d72c
fix(formatters): prevent backticks in exception values and stack fram…
BYK Feb 26, 2026
0a6f0ba
fix(formatters): use divider() in issue/list.ts and escape Seer text …
BYK Feb 26, 2026
42adad3
fix(e2e): increase bundle test timeout to handle cold CI runner startups
BYK Feb 26, 2026
34da275
fix(formatters): escape step titles in Seer output and remove dead st…
BYK Feb 26, 2026
dbd19a2
refactor(formatters): migrate org/project list to markdown tables, st…
BYK Feb 27, 2026
8b48be0
refactor(formatters): migrate issue list to markdown table via writeI…
BYK Feb 27, 2026
cecc46c
refactor(formatters): replace marked-terminal with custom renderer + …
BYK Feb 27, 2026
ce59be9
fix(formatters): restore in-cell markdown rendering and add terminal …
BYK Feb 27, 2026
c6a5519
fix(formatters): don't run cell values through markdown parser in wri…
BYK Feb 27, 2026
0e59671
refactor(formatters): use markdown in table cells, render links as OS…
BYK Feb 27, 2026
744e944
refactor(formatters): convert formatShortId to markdown output, fix t…
BYK Feb 27, 2026
e0184ce
fix(e2e): strip markdown bold markers when asserting on issue short IDs
BYK Feb 27, 2026
f0a7f8a
feat(formatters): add semantic HTML color tags for log levels and iss…
BYK Feb 27, 2026
85ff846
fix(formatters): restore cross-org alias slash extraction in formatSh…
BYK Feb 27, 2026
47832a8
test(formatters): add coverage for text-table, markdown blocks/inline…
BYK Feb 27, 2026
d468172
fix(table): add truncate and minWidths options to prevent row wrapping
BYK Feb 27, 2026
1dd9808
fix(table): use shrinkable: false for SHORT ID instead of blanket tru…
BYK Feb 27, 2026
600c954
fix(formatters): address all PR review comments
BYK Feb 28, 2026
a083022
fix: deduplicate AGENTS.md lore sections and COLORS constant
BYK Feb 28, 2026
0088ec5
fix(formatters): strip color tags in plain output, escape angle brack…
BYK Feb 28, 2026
6afd84b
chore: remove duplicate Long-term Knowledge section from AGENTS.md
BYK Feb 28, 2026
6f6fc17
refactor(formatters): DRY up manual markdown table building with mdKv…
BYK Feb 28, 2026
302bb2f
feat(formatters): add StreamingTable for bordered streaming log/trace…
BYK Feb 28, 2026
63ec308
fix(formatters): escape release versions and rootTransaction, fix bat…
BYK Feb 28, 2026
2b1f0ba
fix(test): reorder stripAnsi to strip color tags before ANSI codes
BYK Feb 28, 2026
09dfd52
fix(formatters): restore bold+underline for alias indicators in short…
BYK Feb 28, 2026
dc879a9
fix(formatters): use colorTag bu for alias bold+underline instead of …
BYK Feb 28, 2026
7f35261
fix(formatters): escape mdKvTable values, remove dead trace streaming…
BYK Feb 28, 2026
171410f
fix(formatters): render color tags in streaming log cells, hoist regex
BYK Feb 28, 2026
735b8f8
fix(formatters): prevent double-escaping of issue titles in plain mode
BYK Feb 28, 2026
a65000a
fix(formatters): preserve markdown links in escapeMarkdownCell
BYK Feb 28, 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
82 changes: 82 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

60 changes: 32 additions & 28 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,29 +1,9 @@
{
"name": "sentry",
"version": "0.14.0-dev.0",
"description": "Sentry CLI - A command-line interface for using Sentry built by robots and humans for robots and humans",
"type": "module",
"bin": {
"sentry": "./dist/bin.cjs"
},
"files": [
"dist/bin.cjs"
],
"scripts": {
"dev": "bun run src/bin.ts",
"build": "bun run script/build.ts --single",
"build:all": "bun run script/build.ts",
"bundle": "bun run script/bundle.ts",
"typecheck": "tsc --noEmit",
"lint": "bunx ultracite check",
"lint:fix": "bunx ultracite fix",
"test": "bun run test:unit && bun run test:isolated",
"test:unit": "bun test test/lib test/commands test/types --coverage --coverage-reporter=lcov",
"test:isolated": "bun test test/isolated",
"test:e2e": "bun test test/e2e",
"generate:skill": "bun run script/generate-skill.ts",
"check:skill": "bun run script/check-skill.ts",
"check:deps": "bun run script/check-no-deps.ts"
"repository": {
"type": "git",
"url": "git+https://github.com/getsentry/cli.git"
},
"devDependencies": {
"@biomejs/biome": "2.3.8",
Expand All @@ -39,29 +19,53 @@
"@types/semver": "^7.7.1",
"binpunch": "^1.0.0",
"chalk": "^5.6.2",
"cli-highlight": "^2.1.11",
"esbuild": "^0.25.0",
"fast-check": "^4.5.3",
"ignore": "^7.0.5",
"marked": "^15",
"p-limit": "^7.2.0",
"pretty-ms": "^9.3.0",
"qrcode-terminal": "^0.12.0",
"semver": "^7.7.3",
"string-width": "^8.2.0",
"tinyglobby": "^0.2.15",
"typescript": "^5",
"ultracite": "6.3.10",
"uuidv7": "^1.1.0",
"wrap-ansi": "^10.0.0",
"zod": "^3.24.0"
},
"repository": {
"type": "git",
"url": "https://github.com/getsentry/cli.git"
"bin": {
"sentry": "./dist/bin.cjs"
},
"license": "FSL-1.1-Apache-2.0",
"description": "Sentry CLI - A command-line interface for using Sentry built by robots and humans for robots and humans",
"engines": {
"node": ">=22"
},
"files": [
"dist/bin.cjs"
],
"license": "FSL-1.1-Apache-2.0",
"packageManager": "bun@1.3.9",
"patchedDependencies": {
"@stricli/core@1.2.5": "patches/@stricli%2Fcore@1.2.5.patch"
}
},
"scripts": {
"dev": "bun run src/bin.ts",
"build": "bun run script/build.ts --single",
"build:all": "bun run script/build.ts",
"bundle": "bun run script/bundle.ts",
"typecheck": "tsc --noEmit",
"lint": "bunx ultracite check",
"lint:fix": "bunx ultracite fix",
"test": "bun run test:unit && bun run test:isolated",
"test:unit": "bun test test/lib test/commands test/types --coverage --coverage-reporter=lcov",
"test:isolated": "bun test test/isolated",
"test:e2e": "bun test test/e2e",
"generate:skill": "bun run script/generate-skill.ts",
"check:skill": "bun run script/check-skill.ts",
"check:deps": "bun run script/check-no-deps.ts"
},
"type": "module"
}
6 changes: 1 addition & 5 deletions src/commands/event/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,7 @@ type HumanOutputOptions = {
function writeHumanOutput(stdout: Writer, options: HumanOutputOptions): void {
const { event, detectedFrom, spanTreeLines } = options;

const lines = formatEventDetails(event, `Event ${event.eventID}`);

// Skip leading empty line for standalone display
const output = lines.slice(1);
stdout.write(`${output.join("\n")}\n`);
stdout.write(`${formatEventDetails(event, `Event ${event.eventID}`)}\n`);

if (spanTreeLines && spanTreeLines.length > 0) {
stdout.write(`${spanTreeLines.join("\n")}\n`);
Expand Down
3 changes: 1 addition & 2 deletions src/commands/issue/explain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,7 @@ export const explainCommand = buildCommand({
}

// Human-readable output
const lines = formatRootCauseList(causes);
stdout.write(`${lines.join("\n")}\n`);
stdout.write(`${formatRootCauseList(causes)}\n`);
writeFooter(
stdout,
`To create a plan, run: sentry issue plan ${issueArg}`
Expand Down
55 changes: 11 additions & 44 deletions src/commands/issue/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,9 @@ import {
ValidationError,
} from "../../lib/errors.js";
import {
divider,
type FormatShortIdOptions,
formatIssueListHeader,
formatIssueRow,
type IssueTableRow,
muted,
writeIssueTable,
writeJson,
} from "../../lib/formatters/index.js";
import {
Expand Down Expand Up @@ -109,37 +107,9 @@ function parseSort(value: string): SortValue {
*
* @param stdout - Output writer
* @param title - Section title
* @param isMultiProject - Whether to show ALIAS column for multi-project mode
*/
function writeListHeader(
stdout: Writer,
title: string,
isMultiProject = false
): void {
function writeListHeader(stdout: Writer, title: string): void {
stdout.write(`${title}:\n\n`);
stdout.write(muted(`${formatIssueListHeader(isMultiProject)}\n`));
stdout.write(`${divider(isMultiProject ? 96 : 80)}\n`);
}

/** Issue with formatting options attached */
/** @internal */ export type IssueWithOptions = {
issue: SentryIssue;
/** Org slug — used as part of the per-project key in trimWithProjectGuarantee. */
orgSlug: string;
formatOptions: FormatShortIdOptions;
};

/**
* Write formatted issue rows to stdout.
*/
function writeIssueRows(
stdout: Writer,
issues: IssueWithOptions[],
termWidth: number
): void {
for (const { issue, formatOptions } of issues) {
stdout.write(`${formatIssueRow(issue, termWidth, formatOptions)}\n`);
}
}

/**
Expand Down Expand Up @@ -234,7 +204,7 @@ function attachFormatOptions(
results: IssueListResult[],
aliasMap: Map<string, string>,
isMultiProject: boolean
): IssueWithOptions[] {
): IssueTableRow[] {
return results.flatMap((result) =>
result.issues.map((issue) => {
const key = `${result.target.org}/${result.target.project}`;
Expand Down Expand Up @@ -591,9 +561,9 @@ async function fetchWithBudget(
* @returns Trimmed array in the same sorted order
*/
function trimWithProjectGuarantee(
issues: IssueWithOptions[],
issues: IssueTableRow[],
limit: number
): IssueWithOptions[] {
): IssueTableRow[] {
if (issues.length <= limit) {
return issues;
}
Expand Down Expand Up @@ -797,9 +767,8 @@ async function handleOrgAllIssues(options: OrgAllIssuesOptions): Promise<void> {

// isMultiProject=true: org-all shows issues from every project, so the ALIAS
// column is needed to identify which project each issue belongs to.
writeListHeader(stdout, `Issues in ${org}`, true);
const termWidth = process.stdout.columns || 80;
const issuesWithOpts = issues.map((issue) => ({
writeListHeader(stdout, `Issues in ${org}`);
const issuesWithOpts: IssueTableRow[] = issues.map((issue) => ({
issue,
// org-all: org context comes from the `org` param; issue.organization may be absent
orgSlug: org,
Expand All @@ -808,7 +777,7 @@ async function handleOrgAllIssues(options: OrgAllIssuesOptions): Promise<void> {
isMultiProject: true,
},
}));
writeIssueRows(stdout, issuesWithOpts, termWidth);
writeIssueTable(stdout, issuesWithOpts, true);

if (hasMore) {
stdout.write(`\nShowing ${issues.length} issues (more available)\n`);
Expand Down Expand Up @@ -1059,10 +1028,8 @@ async function handleResolvedTargets(
? `Issues in ${firstTarget.orgDisplay}/${firstTarget.projectDisplay}`
: `Issues from ${validResults.length} projects`;

writeListHeader(stdout, title, isMultiProject);

const termWidth = process.stdout.columns || 80;
writeIssueRows(stdout, issuesWithOptions, termWidth);
writeListHeader(stdout, title);
writeIssueTable(stdout, issuesWithOptions, isMultiProject);

let footerMode: "single" | "multi" | "none" = "none";
if (isMultiProject) {
Expand Down
3 changes: 1 addition & 2 deletions src/commands/issue/plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,7 @@ function outputSolution(options: OutputSolutionOptions): void {
}

if (solution) {
const lines = formatSolution(solution);
stdout.write(`${lines.join("\n")}\n`);
stdout.write(`${formatSolution(solution)}\n`);
} else {
stderr.write("No solution found. Check the Sentry web UI for details.\n");
}
Expand Down
10 changes: 3 additions & 7 deletions src/commands/issue/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,13 @@ type HumanOutputOptions = {
function writeHumanOutput(stdout: Writer, options: HumanOutputOptions): void {
const { issue, event, spanTreeLines } = options;

const issueLines = formatIssueDetails(issue);
stdout.write(`${issueLines.join("\n")}\n`);
stdout.write(`${formatIssueDetails(issue)}\n`);

if (event) {
// Pass issue permalink for constructing replay links
const eventLines = formatEventDetails(
event,
"Latest Event",
issue.permalink
stdout.write(
`${formatEventDetails(event, "Latest Event", issue.permalink)}\n`
);
stdout.write(`${eventLines.join("\n")}\n`);
}

if (spanTreeLines && spanTreeLines.length > 0) {
Expand Down
47 changes: 37 additions & 10 deletions src/commands/log/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@ import { listLogs } from "../../lib/api-client.js";
import { validateLimit } from "../../lib/arg-parsing.js";
import { AuthError, stringifyUnknown } from "../../lib/errors.js";
import {
buildLogRowCells,
createLogStreamingTable,
formatLogRow,
formatLogsHeader,
formatLogTable,
isPlainOutput,
writeFooter,
writeJson,
} from "../../lib/formatters/index.js";
import { renderInlineMarkdown } from "../../lib/formatters/markdown.js";
import {
buildListCommand,
TARGET_PATTERN_NOTE,
Expand Down Expand Up @@ -73,12 +78,24 @@ function parseFollow(value: string): number {

/**
* Write logs to output in the appropriate format.
*
* When a StreamingTable is provided (TTY mode), renders rows through the
* bordered table. Otherwise falls back to plain markdown rows.
*/
function writeLogs(stdout: Writer, logs: SentryLog[], asJson: boolean): void {
function writeLogs(
stdout: Writer,
logs: SentryLog[],
asJson: boolean,
table?: import("../../lib/formatters/text-table.js").StreamingTable
): void {
if (asJson) {
for (const log of logs) {
writeJson(stdout, log);
}
} else if (table) {
for (const log of logs) {
stdout.write(table.row(buildLogRowCells(log).map(renderInlineMarkdown)));
}
} else {
for (const log of logs) {
stdout.write(formatLogRow(log));
Expand Down Expand Up @@ -115,10 +132,7 @@ async function executeSingleFetch(
// Reverse for chronological order (API returns newest first, tail shows oldest first)
const chronological = [...logs].reverse();

stdout.write(formatLogsHeader());
for (const log of chronological) {
stdout.write(formatLogRow(log));
}
stdout.write(`${formatLogTable(chronological)}\n`);

// Show footer with tip if we hit the limit
const hasMore = logs.length >= flags.limit;
Expand Down Expand Up @@ -160,7 +174,12 @@ async function executeFollowMode(options: FollowModeOptions): Promise<void> {
stderr.write("\n");
}

// Track if header has been printed (for human mode)
// In TTY mode, use a bordered StreamingTable for aligned columns.
// In plain mode, use raw markdown rows for pipe-friendly output.
const plain = flags.json || isPlainOutput();
const table = plain ? undefined : createLogStreamingTable();

// Track if header has been printed (for human/plain mode)
let headerPrinted = false;

// Initial fetch: only last minute for follow mode (we want recent logs, not historical)
Expand All @@ -172,13 +191,21 @@ async function executeFollowMode(options: FollowModeOptions): Promise<void> {

// Print header before initial logs (human mode only)
if (!flags.json && initialLogs.length > 0) {
stdout.write(formatLogsHeader());
stdout.write(table ? table.header() : formatLogsHeader());
headerPrinted = true;
}

// Reverse for chronological order (API returns newest first, tail -f shows oldest first)
const chronologicalInitial = [...initialLogs].reverse();
writeLogs(stdout, chronologicalInitial, flags.json);
writeLogs(stdout, chronologicalInitial, flags.json, table);

// Print bottom border on Ctrl+C so the table closes cleanly
if (table) {
process.once("SIGINT", () => {
stdout.write(table.footer());
process.exit(0);
});
}

// Track newest timestamp (logs are sorted -timestamp, so first is newest)
// Use current time as fallback to avoid fetching old logs when initial fetch is empty
Expand All @@ -202,13 +229,13 @@ async function executeFollowMode(options: FollowModeOptions): Promise<void> {
if (newestLog) {
// Print header before first logs if not already printed
if (!(flags.json || headerPrinted)) {
stdout.write(formatLogsHeader());
stdout.write(table ? table.header() : formatLogsHeader());
headerPrinted = true;
}

// Reverse for chronological order (oldest first for tail -f style)
const chronologicalNew = [...newLogs].reverse();
writeLogs(stdout, chronologicalNew, flags.json);
writeLogs(stdout, chronologicalNew, flags.json, table);

// Update timestamp AFTER successful write to avoid losing logs on write failure
lastTimestamp = newestLog.timestamp_precise;
Expand Down
3 changes: 1 addition & 2 deletions src/commands/log/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,7 @@ function writeHumanOutput(
orgSlug: string,
detectedFrom?: string
): void {
const lines = formatLogDetails(log, orgSlug);
stdout.write(`${lines.join("\n")}\n`);
stdout.write(`${formatLogDetails(log, orgSlug)}\n`);

if (detectedFrom) {
stdout.write(`\nDetected from ${detectedFrom}\n`);
Expand Down
Loading
Loading