Skip to content

Commit 34d07fe

Browse files
committed
feat: improve markdown rendering styles
Part 1: Inline code spans now render with cyan text on dark teal background with space padding for a 'pill' look, replacing the previous plain yellow text. Part 2: h1/h2 headings now display a colored divider bar (━) underneath, clamped to 30 chars. h3+ headings are now bold cyan (previously non-bold). Part 3: Update Terminal.astro hero demo and index.mdx feature terminal to show current column layout (LEVEL, SHORT ID, COUNT, SEEN, FIXABILITY, TITLE). Remove ALIAS column and .accent CSS class. Part 4: Update tests to assert new code span padding, heading divider bars, divider clamping, and h3+ no-divider behavior.
1 parent becab6f commit 34d07fe

6 files changed

Lines changed: 80 additions & 47 deletions

File tree

AGENTS.md

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -629,7 +629,7 @@ mock.module("./some-module", () => ({
629629
### Architecture
630630

631631
<!-- lore:019cb8ea-c6f0-75d8-bda7-e32b4e217f92 -->
632-
* **CLI telemetry DSN is public write-onlysafe to embed in install script**: The CLI's own Sentry DSN (\`SENTRY\_CLI\_DSN\` in \`src/lib/constants.ts\`) is a public write-only ingest key (\`1188a86f...@o1.ingest.us.sentry.io/4510776311808000\`). It's already baked into every distributed binary. Safe to hardcode in the bash install script for error reporting via the envelope APIno secrets needed. Opt-out via \`SENTRY\_CLI\_NO\_TELEMETRY=1\` (same env var the binary checks). Envelope endpoint: \`https://o1.ingest.us.sentry.io/api/{PROJECT\_ID}/envelope/\` with \`x-sentry-auth\` header containing the public key.
632+
* **CLI telemetry DSN is public write-onlysafe to embed in install script**: The CLI's Sentry DSN (\`SENTRY\_CLI\_DSN\` in \`src/lib/constants.ts\`) is a public write-only ingest key already baked into every binary. Safe to hardcode in install scripts. Opt-out: \`SENTRY\_CLI\_NO\_TELEMETRY=1\`.
633633

634634
<!-- lore:019c978a-18b5-7a0d-a55f-b72f7789bdac -->
635635
* **cli.sentry.dev is served from gh-pages branch via GitHub Pages**: \`cli.sentry.dev\` is served from gh-pages branch via GitHub Pages. Craft's gh-pages target runs \`git rm -r -f .\` before extracting docs — persist extra files via \`postReleaseCommand\` in \`.craft.yml\`. Install script supports \`--channel nightly\`, downloading from the \`nightly\` release tag directly. version.json is only used by upgrade/version-check flow.
@@ -640,22 +640,13 @@ mock.module("./some-module", () => ({
640640
<!-- lore:019c972c-9f0f-75cd-9e24-9bdbb1ac03d6 -->
641641
* **Numeric issue ID resolution returns org:undefined despite API success**: Numeric issue ID resolution in \`resolveNumericIssue()\`: (1) try DSN/env/config for org, (2) if found use \`getIssueInOrg(org, id)\` with region routing, (3) else fall back to unscoped \`getIssue(id)\`, (4) extract org from \`issue.permalink\` via \`parseSentryUrl\` as final fallback. The \`explicit-org-numeric\` case uses \`getIssueInOrg\`. \`resolveOrgAndIssueId\` no longer throws for bare numeric IDs when permalink contains the org slug.
642642
643-
<!-- lore:019cb950-9b7b-731a-9832-b7f6cfb6a6a2 -->
644-
* **Self-hosted OAuth device flow requires Sentry 26.1.0+ and SENTRY\_CLIENT\_ID**: The OAuth device flow for self-hosted Sentry requires version 26.1.0+ (PR #105675 added device flow, #106169 added public client support). Users must create a public OAuth application in Settings → Developer Settings → New Public Integration, then set both \`SENTRY\_URL\` and \`SENTRY\_CLIENT\_ID\`. The client ID is NOT optional for self-hosted — without it, \`sentry auth login\` cannot use the device flow. For older instances, the fallback is \`sentry auth login --token\`. The \`getSentryUrl()\` and \`getClientId()\` functions in \`src/lib/oauth.ts\` read these lazily (not at module load) so URL parsing from arguments can set \`SENTRY\_URL\` after import.
645-
646-
<!-- lore:019ca9c3-989c-7c8d-bcd0-9f308fd2c3d7 -->
647-
* **Sentry CLI markdown-first formatting pipeline replaces ad-hoc ANSI**: Formatters build CommonMark strings; \`renderMarkdown()\` renders to ANSI for TTY or raw markdown for non-TTY. Key helpers: \`colorTag()\`, \`mdKvTable()\`, \`mdRow()\`, \`mdTableHeader()\` (\`:\` suffix = right-aligned), \`renderTextTable()\`. \`isPlainOutput()\` checks \`SENTRY\_PLAIN\_OUTPUT\` > \`NO\_COLOR\` > \`!isTTY\`. Batch path: \`formatXxxTable()\`. Streaming path: \`StreamingTable\` (TTY) or raw markdown rows (plain). Both share \`buildXxxRowCells()\`.
648-
649643
<!-- lore:019c972c-9f0d-7c8e-95b1-7beda99c36a8 -->
650644
* **parseSentryUrl does not handle subdomain-style SaaS URLs**: parseSentryUrl in src/lib/sentry-url-parser.ts handles both path-based (\`/organizations/{org}/...\`) and subdomain-style (\`https://{org}.sentry.io/issues/123/\`) URLs. \`matchSubdomainOrg()\` extracts org from hostname ending in \`.sentry.io\`. Region subdomains (\`us\`, \`de\`) filtered by requiring org slug length > 2. Supports \`/issues/{id}/\`, \`/issues/{id}/events/{eventId}/\`, and \`/traces/{traceId}/\` paths. Self-hosted uses path-based only.
651645
652-
<!-- lore:019cc000-0000-0000-0000-consola00001 -->
653-
* **Consola chosen as CLI logger with Sentry createConsolaReporter integration**: The CLI uses \`consola\` (v3.x, ~33KB) for structured logging. Two reporters: (1) built-in FancyReporter writes to stderr with color/icons, (2) \`Sentry.createConsolaReporter()\` from \`@sentry/bun\` auto-forwards all logs to Sentry structured logs (requires \`enableLogs: true\` in Sentry.init, already set). Consola respects \`NO_COLOR\` env var natively. Level controlled by \`SENTRY_LOG_LEVEL\` env var mapped to consola numeric levels: error=0, warn=1, info=3 (default), debug=4, trace=5. Use \`consola.withTag('upgrade')\` for domain-scoped logging. User-facing messages use \`logger.info()\`/\`logger.success()\`; diagnostics use \`logger.debug()\`/\`logger.trace()\`. Global \`--verbose\`/\`--log-level\` flags pre-parsed from argv before Stricli, then set \`logger.level\` directly.
654-
655646
### Decision
656647
657648
<!-- lore:019c99d5-69f2-74eb-8c86-411f8512801d -->
658-
* **Raw markdown output for non-interactive terminals, rendered for TTY**: Output raw CommonMark when stdout is not a TTY; render through marked-terminal only for TTY. Detection: \`process.stdout.isTTY\`. Override precedence: \`SENTRY\_PLAIN\_OUTPUT\` > \`NO\_COLOR\` > auto-detect. \`--json\` always outputs JSON. Streaming formatters (log/trace) use ANSI-colored text for TTY, markdown table rows for non-TTY.
649+
* **Raw markdown output for non-interactive terminals, rendered for TTY**: Markdown-first output pipeline: marked-terminal was replaced by custom renderer in \`src/lib/formatters/markdown.ts\`. Commands build CommonMark using helpers (\`mdKvTable()\`, \`mdRow()\`, \`colorTag()\`, \`escapeMarkdownCell()\`, \`safeCodeSpan()\`) and pass through \`renderMarkdown()\`. \`isPlainOutput()\` precedence: \`SENTRY\_PLAIN\_OUTPUT\` > \`NO\_COLOR\` > \`FORCE\_COLOR\` > \`!isTTY\`. \`--json\` always outputs JSON. When migrating: replace ad-hoc \`stdout.write()\` with \`formatXxx()\` returning \`renderMarkdown(lines.join('\n'))\`. Tests run non-TTY so assertions match raw CommonMark (\`\*\*bold\*\*\`, backtick code spans).
659650
660651
<!-- lore:00166785-609d-4ab5-911e-ee205d17b90c -->
661652
* **whoami should be separate from auth status command**: The \`sentry auth whoami\` command should be a dedicated command separate from \`sentry auth status\`. They serve different purposes: \`status\` shows everything about auth state (token, expiry, defaults, org verification), while \`whoami\` just shows user identity (name, email, username, ID) by fetching live from \`/auth/\` endpoint. \`sentry whoami\` should be a top-level alias (like \`sentry issues\`\`sentry issue list\`). \`whoami\` should support \`--json\` for machine consumption and be lightweight — no credential verification, no defaults listing.
@@ -669,13 +660,13 @@ mock.module("./some-module", () => ({
669660
* **Bun binary build requires SENTRY\_CLIENT\_ID env var**: The build script (\`script/bundle.ts\`) requires \`SENTRY\_CLIENT\_ID\` environment variable and exits with code 1 if missing. When building locally, use \`bun run --env-file=.env.local build\` or set the env var explicitly. The binary build (\`bun run build\`) also needs it. Without it you get: \`Error: SENTRY\_CLIENT\_ID environment variable is required.\`
670661
671662
<!-- lore:019c9776-e3dd-7632-88b8-358a19506218 -->
672-
* **GitHub immutable releases prevent rolling nightly tag pattern**: getsentry/cli has immutable GitHub releases — assets can't be modified and tags can NEVER be reused. Nightly uses per-version tags (e.g., \`0.13.0-dev.1772062077\`) with API-based latest discovery; deletes all existing assets before uploading. Craft minVersion >= 2.21.0 with no \`preReleaseCommand\` silently skips \`bump-version.sh\` if the only target is \`github\`. Fix: explicitly set \`preReleaseCommand: bash scripts/bump-version.sh\`.
663+
* **GitHub immutable releases prevent rolling nightly tag pattern**: getsentry/cli has immutable GitHub releases — assets can't be modified and tags can NEVER be reused. Nightly uses per-version tags (e.g., \`0.13.0-dev.1772062077\`) with API-based latest discovery. Craft with no \`preReleaseCommand\` silently skips \`bump-version.sh\` if the only target is \`github\` — must explicitly set it.
673664
674665
<!-- lore:019cb8c2-d7b5-780c-8a9f-d20001bc198f -->
675-
* **Install script: BSD sed and awk JSON parsing breaks OCI digest extraction**: The install script parses OCI manifests with awk (no jq dependency). Key trap: \`sed 's/},{/}\n{/g'\` doesn't insert newlines on macOS BSD sed (\`\n\` is literal). Also, the first layer shares a line with the config block after \`\[{\` split. Fix: use a single awk pass tracking last-seen \`"digest"\` value, printing it when \`"org.opencontainers.image.title"\` matches target. Works because \`digest\` always precedes \`annotations\` within each OCI layer object. This avoids sed entirely and handles both GNU/BSD awk. The config digest (\`sha256:44136fa...\`) is a 2-byte \`{}\` blob — downloading it instead of the real binary causes \`gunzip: unexpected end of file\`. The install script now has fire-and-forget Sentry telemetry via \`die()\` + ERR trap, which would catch such failures automatically.
666+
* **Install script: BSD sed and awk JSON parsing breaks OCI digest extraction**: The install script parses OCI manifests with awk (no jq). Key trap: BSD sed \`\n\` is literal, not newline. Fix: single awk pass tracking last-seen \`"digest"\`, printing when \`"org.opencontainers.image.title"\` matches target. The config digest (\`sha256:44136fa...\`) is a 2-byte \`{}\` blob — downloading it instead of the real binary causes \`gunzip: unexpected end of file\`.
676667
677668
<!-- lore:019cb963-cb63-722d-9365-b34336f4766d -->
678-
* **macOS SIGKILL on MAP\_SHARED mmap of signed Mach-O binaries**: macOS AMFI (code signing enforcement) sends SIGKILL when \`MAP\_SHARED\` with \`PROT\_WRITE\` is used on a code-signed Mach-O binary. \`Bun.mmap()\` defaults to \`{ shared: true }\` (MAP\_SHARED). In \`src/lib/bspatch.ts\`, \`Bun.mmap(process.execPath)\` kills the process on macOS during delta upgrades because the running CLI binary is ad-hoc signed (all Bun binaries are). Fix: pass \`{ shared: false }\` for MAP\_PRIVATE. Since the mapping is read-only in practice, no COW pages are allocated — identical performance. Linux ELF binaries have no such restriction.
669+
* **macOS SIGKILL on MAP\_SHARED mmap of signed Mach-O binaries**: macOS AMFI (code signing enforcement) sends uncatchable SIGKILL when \`Bun.mmap()\` is used on code-signed Mach-O binaries. \`Bun.mmap()\` always requests PROT\_WRITE regardless of the \`shared\` flag, and macOS rejects ANY writable mapping (MAP\_SHARED or MAP\_PRIVATE) on signed Mach-O. PR #339's MAP\_PRIVATE fix was insufficient. Fixed by replacing \`Bun.mmap(oldPath)\` with \`new Uint8Array(await Bun.file(oldPath).arrayBuffer())\` in bspatch.ts. Costs ~100 MB heap for the old binary but avoids the uncatchable SIGKILL entirely. Sentry telemetry confirmed: zero successful macOS upgrade traces from delta-enabled versions, while Linux worked fine.
679670
680671
<!-- lore:019c969a-1c90-7041-88a8-4e4d9a51ebed -->
681672
* **Multiple mockFetch calls replace each other — use unified mocks for multi-endpoint tests**: Bun test mocking gotchas: (1) \`mockFetch()\` replaces \`globalThis.fetch\` — calling it twice replaces the first mock. Use a single unified fetch mock dispatching by URL pattern. (2) \`mock.module()\` pollutes the module registry for ALL subsequent test files. Tests using it must live in \`test/isolated/\` and run via \`test:isolated\`. (3) For \`Bun.spawn\`, use direct property assignment in \`beforeEach\`/\`afterEach\`.
@@ -688,9 +679,6 @@ mock.module("./some-module", () => ({
688679
689680
### Pattern
690681
691-
<!-- lore:019c9793-fb1c-7986-936e-57949e9a30d0 -->
692-
* **Markdown table structure for marked-terminal: blank header row + separator + data rows**: Markdown tables for marked-terminal: blank header row (\`| | |\`), separator (\`|---|---|\`), then data rows (\`| \*\*Label\*\* | value |\`). Data rows before separator produce malformed output. Escape user content via \`escapeMarkdownCell()\` in \`src/lib/formatters/markdown.ts\` — backslashes first, then pipes. CodeQL flags incomplete escaping as high severity.
693-
694682
<!-- lore:019c972c-9f11-7c0d-96ce-3f8cc2641175 -->
695683
* **Org-scoped SDK calls follow getOrgSdkConfig + unwrapResult pattern**: All org-scoped API calls in src/lib/api-client.ts: (1) call \`getOrgSdkConfig(orgSlug)\` for regional URL + SDK config, (2) spread into SDK function: \`{ ...config, path: { organization\_id\_or\_slug: orgSlug, ... } }\`, (3) pass to \`unwrapResult(result, errorContext)\`. Shared helpers \`resolveAllTargets\`/\`resolveOrgAndProject\` must NOT call \`fetchProjectId\` — commands that need it enrich targets themselves.
696684
@@ -703,5 +691,5 @@ mock.module("./some-module", () => ({
703691
### Preference
704692
705693
<!-- lore:019c9700-0fc3-730c-82c3-a290d5ecc2ea -->
706-
* **CI scripts: prefer jq/sed over node -e for JSON manipulation**: Reviewer (BYK) prefers using standard Unix tools (\`jq\`, \`sed\`, \`awk\`) over \`node -e\` for simple JSON manipulation in CI workflow scripts. For example, reading/modifying package.json version: \`jq -r .version package.json\` to read, \`jq --arg v "$NEW" '.version = $v' package.json > tmp && mv tmp package.json\` to write. This avoids requiring Node.js to be installed in CI steps that only need basic JSON operations, and is more readable for shell-centric workflows.
694+
* **CI scripts: prefer jq/sed over node -e for JSON manipulation**: Prefer \`jq\`/\`sed\`/\`awk\` over \`node -e\` for JSON manipulation in CI scripts. Example: \`jq -r .version package.json\` to read, \`jq --arg v "$NEW" '.version = $v' package.json > tmp && mv tmp package.json\` to write.
707695
<!-- End lore-managed section -->

docs/src/components/Terminal.astro

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -51,32 +51,36 @@ if (background) {
5151
<div class="line output dimmed small">Issues from 3 projects:</div>
5252
<div class="line spacer-sm"></div>
5353
<div class="line output table-header">
54+
<span class="col-level">LEVEL</span>
5455
<span class="col-shortid">SHORT ID</span>
55-
<span class="col-alias">ALIAS</span>
56-
<span class="col-title">TITLE</span>
5756
<span class="col-count">COUNT</span>
58-
<span class="col-fix">FIX</span>
57+
<span class="col-seen">SEEN</span>
58+
<span class="col-fix">FIXABILITY</span>
59+
<span class="col-title">TITLE</span>
5960
</div>
6061
<div class="line output table-row">
62+
<span class="col-level red-text">ERROR</span>
6163
<span class="col-shortid dimmed">FRONTEND-79</span>
62-
<span class="col-alias accent">f-79</span>
63-
<span class="col-title">TypeError: Cannot read property 'map'...</span>
6464
<span class="col-count">2.4k</span>
65+
<span class="col-seen dimmed">2h</span>
6566
<span class="col-fix green-text">92%</span>
67+
<span class="col-title">TypeError: Cannot read property 'map'...</span>
6668
</div>
6769
<div class="line output table-row">
70+
<span class="col-level yellow-text">WARN</span>
6871
<span class="col-shortid dimmed">API-2F</span>
69-
<span class="col-alias accent">a-2f</span>
70-
<span class="col-title">ReferenceError: user is not defined</span>
7172
<span class="col-count">891</span>
73+
<span class="col-seen dimmed">1d</span>
7274
<span class="col-fix yellow-text">67%</span>
75+
<span class="col-title">ReferenceError: user is not defined</span>
7376
</div>
7477
<div class="line output table-row">
78+
<span class="col-level red-text">ERROR</span>
7579
<span class="col-shortid dimmed">MOBILE-4D</span>
76-
<span class="col-alias accent">m-4d</span>
77-
<span class="col-title">NetworkError: Failed to fetch</span>
7880
<span class="col-count">456</span>
81+
<span class="col-seen dimmed">5d</span>
7982
<span class="col-fix red-text">23%</span>
83+
<span class="col-title">NetworkError: Failed to fetch</span>
8084
</div>
8185

8286
<div class="line spacer"></div>
@@ -231,10 +235,6 @@ if (background) {
231235
color: #22d3ee;
232236
}
233237

234-
.accent {
235-
color: #a78bfa;
236-
}
237-
238238
.dimmed {
239239
color: rgba(255, 255, 255, 0.4);
240240
}
@@ -286,8 +286,14 @@ if (background) {
286286
min-width: 95px;
287287
}
288288

289-
.col-alias {
290-
min-width: 45px;
289+
.col-level {
290+
min-width: 50px;
291+
font-weight: 500;
292+
}
293+
294+
.col-seen {
295+
min-width: 30px;
296+
text-align: right;
291297
}
292298

293299
.col-title {
@@ -325,9 +331,5 @@ if (background) {
325331
.col-shortid {
326332
display: none;
327333
}
328-
329-
.col-alias {
330-
min-width: 40px;
331-
}
332334
}
333335
</style>

docs/src/content/docs/index.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,21 +52,25 @@ import sectionBg3 from '../../assets/section-bg-3.png';
5252
<div class="line dimmed">Detected project: my-app (from .env)</div>
5353
<div class="spacer"></div>
5454
<div class="line table-header">
55+
<span class="col-level">LEVEL</span>
5556
<span class="col-id">SHORT ID</span>
5657
<span class="col-title">TITLE</span>
5758
<span class="col-count">COUNT</span>
5859
</div>
5960
<div class="line table-row">
61+
<span class="col-level red-text">ERROR</span>
6062
<span class="col-id highlight">WQ</span>
6163
<span class="col-title">TypeError: Cannot read property 'map'...</span>
6264
<span class="col-count">142</span>
6365
</div>
6466
<div class="line table-row">
67+
<span class="col-level yellow-text">WARN</span>
6568
<span class="col-id highlight">X3</span>
6669
<span class="col-title">Failed to fetch user data from API</span>
6770
<span class="col-count">89</span>
6871
</div>
6972
<div class="line table-row">
73+
<span class="col-level red-text">ERROR</span>
7074
<span class="col-id highlight">R7</span>
7175
<span class="col-title">Connection timeout after 30s</span>
7276
<span class="col-count">34</span>

src/lib/formatters/colors.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export const COLORS = {
1818
white: "#f9f8f9",
1919
cyan: "#79B8FF",
2020
muted: "#898294",
21+
/** Background tint for inline code spans (dark teal, pairs with cyan text) */
22+
codeBg: "#1a2f3a",
23+
/** Foreground color for inline code spans */
24+
codeFg: "#22d3ee",
2125
} as const;
2226

2327
// Base Color Functions

src/lib/formatters/markdown.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import chalk from "chalk";
2727
import { highlight as cliHighlight } from "cli-highlight";
2828
import { marked, type Token, type Tokens } from "marked";
29+
import stringWidth from "string-width";
2930
import { COLORS, muted, terminalLink } from "./colors.js";
3031
import { type Alignment, renderTextTable } from "./text-table.js";
3132

@@ -328,7 +329,9 @@ function renderOneInline(token: Token): string {
328329
case "em":
329330
return chalk.italic(renderInline((token as Tokens.Em).tokens));
330331
case "codespan":
331-
return chalk.hex(COLORS.yellow)((token as Tokens.Codespan).text);
332+
return chalk.bgHex(COLORS.codeBg).hex(COLORS.codeFg)(
333+
` ${(token as Tokens.Codespan).text} `
334+
);
332335
case "link": {
333336
const link = token as Tokens.Link;
334337
let linkText = renderInline(link.tokens);
@@ -440,12 +443,18 @@ function renderBlocks(tokens: Token[]): string {
440443
case "heading": {
441444
const t = token as Tokens.Heading;
442445
const text = renderInline(t.tokens);
443-
// h1/h2 → bold cyan; h3+ → plain cyan (less prominent)
444-
const styled =
445-
t.depth <= 2
446-
? chalk.hex(COLORS.cyan).bold(text)
447-
: chalk.hex(COLORS.cyan)(text);
448-
parts.push(styled);
446+
if (t.depth <= 2) {
447+
// h1/h2 → bold cyan with colored divider bar for visual weight
448+
parts.push(chalk.hex(COLORS.cyan).bold(text));
449+
parts.push(
450+
chalk.hex(COLORS.cyan)(
451+
"\u2501".repeat(Math.min(stringWidth(text), 30))
452+
)
453+
);
454+
} else {
455+
// h3+ → bold cyan (less prominent, no divider)
456+
parts.push(chalk.hex(COLORS.cyan).bold(text));
457+
}
449458
parts.push("");
450459
break;
451460
}

0 commit comments

Comments
 (0)