feat(formatters): render all terminal output via marked-terminal#297
feat(formatters): render all terminal output via marked-terminal#297
Conversation
Semver Impact of This PR🟡 Minor (new features) 📋 Changelog PreviewThis is how your changes will appear in the changelog. New Features ✨
Bug Fixes 🐛
🤖 This preview updates automatically when you update the PR. |
Codecov Results 📊✅ 2162 passed | Total: 2162 | Pass Rate: 100% | Execution Time: 0ms All tests are passing successfully. ✅ Patch coverage is 97.34%. Project has 3202 uncovered lines. Files with missing lines (13)
Coverage diff@@ Coverage Diff @@
## main #PR +/-##
==========================================
+ Coverage 76.42% 79.05% +2.63%
==========================================
Files 117 118 +1
Lines 15298 15286 -12
Branches 0 0 —
==========================================
+ Hits 11691 12084 +393
- Misses 3607 3202 -405
- Partials 0 0 —Generated by Codecov Action |
|
Re: merging Good point — they do overlap in responsibility. Merging them would create a ~1600-line file. I think the current split works well: That said, if you feel strongly about it I can merge — just want to flag the file size concern. |
Add marked-terminal as the rendering engine for all human-readable CLI output. All detail views (issue, event, org, project, log, trace, Seer) now build markdown internally and render through marked-terminal, producing Unicode box-drawing tables and styled headings. Streaming list output (log/trace rows) stays row-by-row for compatibility with follow/watch modes. - Add src/lib/formatters/markdown.ts with central renderMarkdown() utility - Rewrite formatters to return rendered string instead of string[] - Convert all detail sections to markdown tables (| **Label** | value | pattern) - Convert Seer root cause and solution output to fenced code blocks - Remove divider(), formatTable() dead code from human.ts - Escape SDK names in code spans to prevent markdown transformation of underscores - Update all formatter tests for new string return type and content-based checks
…ble cells Add escapeMarkdownCell() helper to markdown.ts that escapes backslashes first, then pipe characters — fixing CodeQL 'Incomplete string escaping' alerts where the previous \| replacement left backslashes unescaped.
…var overrides Gate all markdown rendering behind isPlainOutput(): skip marked.parse() and return raw CommonMark when stdout is not a TTY. Override with env vars: - SENTRY_PLAIN_OUTPUT=1/0 — explicit project-specific control (highest priority) - NO_COLOR=1/0 — widely-supported standard (secondary) - process.stdout.isTTY — auto-detect (fallback) Both env vars treat '0', 'false', '' as falsy; everything else as truthy (case-insensitive). SENTRY_PLAIN_OUTPUT takes precedence over NO_COLOR. Add renderInlineMarkdown() using marked.parseInline() for inline-only rendering of individual cell values without block wrapping. Migrate streaming formatters to dual-mode output: - formatLogRow / formatLogsHeader: plain emits markdown table rows/header - formatTraceRow / formatTracesHeader: same This means piping to a file produces valid CommonMark; live TTY sessions get the existing ANSI-rendered output unchanged.
Markdown table cells use '**Slug**' and '**DSN**' without trailing colons. The E2E runner has no TTY so output is plain markdown — update assertions to match.
- Extract LOG_TABLE_COLS and TRACE_TABLE_COLS module-level constants - Add mdTableHeader() and divider() helpers to markdown.ts - Reduce duplication in formatLogRow/formatTraceRow by building markdown cell values once, then branching only on isPlainOutput() - Use middle truncation for log IDs (abc...123 vs abcdef...) - Fix package.json repository URL to use git+ prefix
…n defs - Add mdRow() helper: shared streaming row renderer used by both formatLogRow and formatTraceRow (eliminates duplicated isPlainOutput branching) - Add mdKvTable() helper: builds key-value detail tables from [label, value] tuples (replaces manual string concatenation in formatLogDetails sections) - Simplify mdTableHeader() column alignment: use ':' suffix convention (e.g. 'Duration:') instead of [name, 'right'] tuples - Make formatLogsHeader/formatTracesHeader consistent: both modes now emit markdown table rows via mdRow() instead of diverging formats
…, table helpers 57 new tests covering: - formatEventDetails with all conditional sections: stack traces, breadcrumbs, request, user/geo, environment, replay, tags, SDK - formatSolution with steps, empty steps, markdown in descriptions - formatLogTable and formatTraceTable batch rendering - mdRow, mdKvTable, mdTableHeader, escapeMarkdownCell helpers
- NO_COLOR: follow no-color.org spec (any non-empty value disables color, including '0' and 'false'; only empty string leaves color on). Previously isTruthyEnv() was used which treated '0'/'false' as falsy, breaking interoperability with standard CI tooling. - buildMarkdownTable: escape cell values via escapeMarkdownCell() so pipe and backslash characters in API-supplied names/URLs don't corrupt table structure (consistent with formatLogTable, formatTraceTable, buildBreadcrumbsMarkdown). - formatIssueDetails: escape newlines in metadata.value blockquote with .replace(/\n/g, '\n> ') so multi-line error messages render correctly (same pattern already used in formatLogDetails).
- escapeMarkdownCell: replace newlines with space to prevent multi-line values from splitting a markdown table row across multiple lines (affects log messages, breadcrumb messages, tag values, any user content) - formatLogTable / formatTraceTable: wire into the non-follow batch list paths (log/list.ts executeSingleFetch, trace/list.ts) — replaces the formatLogsHeader + formatLogRow loop so batch mode gets proper Unicode-bordered table rendering, streaming/follow mode still uses rows - project/view.ts: restore muted() styling on the multi-project divider by importing and calling divider(60) instead of bare "─".repeat(60)
1ad7523 to
2ff821a
Compare
- package.json: restore version to 0.14.0-dev.0, accidentally clobbered to 0.13.0-dev.0 during rebase conflict resolution - human.ts: restore green()/yellow()/muted() colors to STATUS_ICONS and STATUS_LABELS, lost during the markdown migration; unknown statuses already used statusColor() so known statuses must also be colored to avoid the inconsistency - markdown.ts mdRow: after renderInlineMarkdown(), replace residual | with U+2502 (│) so CommonMark backslash-escapes like \| that marked unescapes back to | don't visually break the pipe-delimited row layout in streaming follow mode - trace.ts: extract buildTraceRowCells() private helper shared by both formatTraceRow (streaming) and formatTraceTable (batch) so cell formatting stays consistent; re-export formatTraceRow and formatTracesHeader to make them available for future streaming use cases and keep the existing tests valid
… in trace summary - formatEventDetails (human.ts): wrap SDK name in backtick code span so names like sentry.python.aws_lambda don't render with markdown emphasis (same fix already applied to log.ts SDK field in a previous commit) - formatTraceSummary (trace.ts): wrap rootTransaction through escapeMarkdownCell() — rootOp was already in backticks but rootTransaction was embedded raw, allowing pipe/underscore characters to break the table or produce unintended emphasis
- markdown.ts: add safeCodeSpan() helper for backtick-wrapped values inside table cells — replaces | with U+2502 and newlines with space since CommonMark code spans treat backslash as literal (not escape), making \| ineffective at preventing table splitting - markdown.ts mdKvTable: apply escapeMarkdownCell() to every value so all key-value detail sections (org, project, log context, etc.) are safe against user-supplied | and newline characters - human.ts: apply escapeMarkdownCell() to all bare user-supplied values in formatIssueDetails (platform, type, assignee, project name, release versions, permalink) and buildUserMarkdown (name, email, username, id, ip, geo location) and buildEnvironmentMarkdown (browser, OS, device) - human.ts: use safeCodeSpan() for backtick-wrapped values (culprit, project slug, event location, trace ID) instead of raw template literal backtick wrapping
…cell sanitisation
mdKvTable unconditionally applied escapeMarkdownCell() to all values,
including those pre-formatted as code spans (e.g. `${logId}`,
`${codeFunction}`). escapeMarkdownCell() escapes backslashes first
(\→\\), which inside a CommonMark code span is literal, producing
double-backslashes in the rendered output (e.g. src\\lib\\parser.ts).
Replace escapeMarkdownCell with a new sanitizeKvCell() private helper
that only substitutes | with │ (U+2502 BOX DRAWINGS LIGHT VERTICAL,
visually identical, never a table delimiter) and newlines with a space.
This avoids:
- Double-escaping backslashes inside code spans
- Reintroducing CodeQL 'incomplete escaping' concern (no backslash
escape chain is used at all — character substitution only)
- Table structure corruption from user-supplied pipe characters
The commit message for 2b7b534 described replacing residual | with U+2502 in mdRow's rendered path, but the actual code change to markdown.ts was never committed — the mdRow implementation still called renderInlineMarkdown without the post-render sanitisation. Apply the fix: after renderInlineMarkdown(), replace all | (including those that marked.parseInline unescapes from CommonMark \| backslash-escapes) with │ (U+2502, BOX DRAWINGS LIGHT VERTICAL) so pipe characters in log messages, transaction names, and other user content cannot visually corrupt the pipe-delimited column structure in streaming TTY output.
…ove unused minWidth - markdown.ts: add escapeMarkdownInline() which escapes \, *, _, `, [, ] — the characters that CommonMark processes as inline emphasis, strong, code spans, and links. Prevents Python/JS exception titles like 'TypeError in __init__' from losing underscores (rendered as empty emphasis) when passed through renderMarkdown() in headings/blockquotes. - human.ts formatIssueDetails: apply escapeMarkdownInline() to issue.title in the '## shortId: title' heading and to metadata.value in blockquotes - human.ts formatEventDetails: apply escapeMarkdownInline() to the event header string used in '## header (`eventId`)' headings - table.ts: remove minWidth from Column<T> interface — buildMarkdownTable never used it (auto-sized by cli-table3), so the property was dead configuration; remove corresponding dead minWidth values from repo/list.ts and team/list.ts callers
…ngling JSDoc - log.ts: formatLogRow was using '**LEVEL**' (uniform bold, no color) for the severity cell, while formatLogTable correctly used formatSeverity() for per-level ANSI color (red/error, yellow/warning, cyan/info, muted/debug). Restore formatSeverity() in formatLogRow to keep streaming and batch modes consistent. - table.ts: remove the dangling JSDoc comment '/** Minimum column width... */' left behind when the minWidth property was removed from Column<T> in the previous commit.
…sage blockquotes - human.ts formatOrgDetails: escape org.name with escapeMarkdownCell() in the kv table row and escapeMarkdownInline() in the '## slug: name' heading - human.ts formatProjectDetails: escape project.name, project.platform, and project.organization.name with escapeMarkdownCell(); use safeCodeSpan() for project.organization.slug; apply escapeMarkdownInline() to heading - log.ts formatLogDetails: escape log.message with escapeMarkdownInline() before embedding in the blockquote — same pattern already applied to issue.metadata.value in human.ts; prevents __, *, and backtick chars in Python tracebacks or format strings from corrupting the rendered output
…es from breaking code spans - markdown.ts safeCodeSpan: also replace ` with ˋ (U+02CB MODIFIER LETTER GRAVE ACCENT) — visually identical in monospace but never a code-span delimiter. Prevents JS exception messages like 'Unexpected token `' from prematurely closing the backtick-delimited code span. - human.ts formatStackFrameMarkdown: use safeCodeSpan() for the full 'at fn (file:line:col)' string instead of raw template-literal backticks - human.ts formatExceptionValueMarkdown: use safeCodeSpan() for the 'type: value' header instead of raw backticks
…in headings
- issue/list.ts: replace inline muted("─".repeat(n)) with divider(n) from
markdown.ts — the helper already exists and is re-exported via the
formatters index; inlining it duplicated the helper logic
- seer.ts buildRootCauseMarkdown: escape cause.description with
escapeMarkdownInline() before embedding in the '### Cause #N: ...' heading
- seer.ts formatSolution: escape solution.data.one_line_summary with
escapeMarkdownInline() before embedding in '**Summary:** ...' bold text
— Seer AI-generated text can contain underscores, asterisks, and backticks
which the markdown parser would interpret as emphasis or code spans
src/lib/formatters/seer.ts
Outdated
| lines.push(...formatReproductionStep(step, i)); | ||
| } | ||
| for (const step of cause.root_cause_reproduction) { | ||
| lines.push(`**${step.title}**`); |
There was a problem hiding this comment.
Unescaped step titles break bold markdown formatting
Medium Severity
step.title is embedded directly inside **...** bold markers without escapeMarkdownInline(). In contrast, cause.description (line 111) and solution.data.one_line_summary (line 263) in the same file are properly escaped. If Seer generates a title containing **, _, or * (plausible for code-related descriptions like "Fix __init__ handling"), the bold delimiters would be corrupted, producing garbled rendered output.
Additional Locations (1)
src/lib/formatters/output.ts
Outdated
| /** Function to format data as human-readable lines */ | ||
| formatHuman: (data: T) => string[]; | ||
| /** Function to format data as a rendered string or string array */ | ||
| formatHuman: (data: T) => string | string[]; |
There was a problem hiding this comment.
Dead backward-compat shim in formatHuman return type
Low Severity
The formatHuman return type was widened from string[] to string | string[] and an Array.isArray() branch was added. Since all formatters were migrated to return string in this PR, the string[] path is dead code. The reviewer's stated preference is to fix callers directly rather than keep backward-compat shims.
Additional Locations (1)
src/lib/formatters/seer.ts
Outdated
| if (step) { | ||
| lines.push(` ${i + 1}. ${bold(step.title)}`); | ||
| lines.push(` ${muted(step.description)}`); | ||
| lines.push(`${i + 1}. **${step.title}**`); |
There was a problem hiding this comment.
Unescaped step titles break markdown bold formatting
Medium Severity
step.title is embedded directly inside **...** bold markers without escaping via escapeMarkdownInline(). If the Seer API returns a title containing markdown special characters like *, _, or `, the bold formatting breaks and the output renders incorrectly. This is a regression — the old code used chalk's bold() which applied ANSI codes directly, making special characters inert. The same issue exists in buildRootCauseMarkdown at the other location.
Additional Locations (1)
…ring[] shim in writeOutput
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.


Summary
marked-terminalas the rendering engine for all human-readable CLI output — detail views for issues, events, orgs, projects, logs, traces, and Seer output now build markdown internally and render throughmarked-terminal| **Label** | value |pattern) and styled headings; Seer root cause / solution output uses fenced code blocksformatLogRow,formatTraceRow) stays row-by-row for compatibility with follow/watch polling loopsChanges
src/lib/formatters/markdown.ts— centralrenderMarkdown()utility with Sentinel color palettestring[]→string(rendered markdown)divider()andformatTable()dead code fromhuman.tslog.ts(underscores/dashes in names likesentry.pythonwere consumed by markdown emphasis rules — now wrapped in code spans)stringreturn type using content-based.toContain()checks