Skip to content
Merged
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
18 changes: 15 additions & 3 deletions .lore.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
* **Auth token env var override pattern: SENTRY\_AUTH\_TOKEN > SENTRY\_TOKEN > SQLite**: Auth token precedence in \`src/lib/db/auth.ts\`: \`SENTRY\_AUTH\_TOKEN\` > \`SENTRY\_TOKEN\` > SQLite OAuth token. \`getEnvToken()\` trims env vars (empty/whitespace = unset). \`AuthSource\` tracks provenance. \`ENV\_SOURCE\_PREFIX = "env:"\` — use \`.length\` not hardcoded 4. Env tokens bypass refresh/expiry. \`isEnvTokenActive()\` guards auth commands. Logout must NOT clear stored auth when env token active. \`runInteractiveLogin\` catches OAuth flow errors internally and returns falsy on failure; login command sets \`process.exitCode = 1\` and returns normally (does NOT reject). Tests expecting \`rejects.toThrow()\` will fail — assert via fetch-call inspection instead. \`requestDeviceCode\` requires \`SENTRY\_CLIENT\_ID\` env var.

<!-- lore:019e4f9d-b0e0-72b0-ab0f-1740206c9d80 -->
* **Binary build pipeline: esbuild → fossilize → Node SEA (replacing Bun.build compile)**: Binary build pipeline: \`src/bin.ts → \[esbuild CJS, node24 target] → dist-build/bin.js → \[fossilize --no-bundle] → Node SEA binary → \[binpunch] → gzip\`. Keep inputs in \`dist-build/\` (separate from fossilize's \`--out-dir dist-bin/\` — fossilize always \`rm -rf dist-bin/\` on start). Single fossilize invocation for all platforms via \`FOSSILIZE\_PLATFORMS\`. Post-process: rename \`sentry-win-x64.exe\`→\`sentry-windows-x64.exe\`. Platform matrix: darwin-arm64, darwin-x64, linux-x64, linux-arm64, win32-x64 (no musl). Ink sidecar: \`--assets dist-build/ink-app.js\`. \`NODE\_VERSION=24\` (Node 24 LTS 'Krypton', upgraded from 22). \`FOSSILIZE\_SIGN=y\` on push to main/release. esbuild: \`format:'cjs'\` + \`target:'node24'\` + \`minify:true\` + \`treeShaking:true\` + inject \`import-meta-url.js\` shim. Build script: \`pnpm tsx script/build.ts\`. Gzip only when \`RELEASE\_BUILD=1\`. binpunch always runs. Sourcemap uploaded to Sentry, never shipped.
* **Binary build pipeline: esbuild → fossilize → Node SEA (replacing Bun.build compile)**: Binary build pipeline: \`src/bin.ts → \[esbuild CJS, node24 target] → dist-build/bin.js → \[fossilize --no-bundle] → Node SEA binary → \[binpunch ICU hole-punch] → gzip\`. Strip debug symbols handled INSIDE fossilize (as of fossilize #16) — fossilize strips the copied binary BEFORE postject injection. Strip MUST happen before injection — after SEA injection, \`strip\` fails ('section .text can't be allocated in segment 2'). macOS: \`strip -x\` on unsigned copy (fossilize unsigns before copy); cross-strip from Linux silently fails (caught). Windows: skipped (no debug symbols). NODE\_VERSION='lts'. ALL\_TARGETS: darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-x64. Post-process: rename \`sentry-win-x64.exe\`→\`sentry-windows-x64.exe\`. UPX RULED OUT — destroys ELF notes. \`FOSSILIZE\_SIGN=y\` on push to main/release. Gzip only when \`RELEASE\_BUILD=1\`. Sourcemap uploaded to Sentry, never shipped.

<!-- lore:019e56e2-f89c-7846-8953-1e47fb470cbf -->
* **Binary size breakdown: 94.5% is Node.js runtime — bundled code is ~6.3 MiB**: Binary composition (linux-x64, Node 24 LTS 'Krypton'): Node.js runtime=121 MiB (~97%), JS bundle=3.7 MiB, Ink sidecar=2.6 MiB. Total raw=125 MiB, compressed=34 MiB. Node 24 LTS vs Node 22: startup ~41% faster (~1.0s vs ~1.7s), completion ~47% faster (~0.16s vs ~0.3s), JS bundle 5% smaller (3.7 vs 3.9 MiB). esbuild: \`minify: true\` enables all three modes (whitespace, syntax, identifiers) — identifier mangling already active. \`treeShaking: true\` explicit (esbuild enables by default with \`bundle: true\`, but made explicit). Size reduction levers: (1) small-ICU Node build saves ~20 MiB raw but unavailable from official releases; (2) Ink sidecar (2.6 MiB) only used by \`sentry init\` — could be CDN-loaded but adds network dependency; (3) more aggressive Sentry SDK tree-shaking saves ~200 KB. Verdict: none worth pursuing — compressed download is already reasonable and ~97% is uncontrollable Node runtime.
* **Binary size breakdown: 94.5% is Node.js runtime — bundled code is ~6.3 MiB**: Binary composition (linux-x64, Node 24 LTS): Node.js runtime=121 MiB (ships with debug symbols). \`strip --strip-unneeded\` → 101 MiB (-17 MiB raw, -4 MiB compressed). Strip built into fossilize #16 — happens on the copied binary BEFORE postject injection. After strip+SEA+binpunch: ~108 MiB raw, ~30 MiB gzip (vs 125 MiB / 34 MiB unstripped). .rodata=52.5 MB: V8 snapshot ~12 MB, ICU full-icu data ~28 MB. UPX compresses to 25 MiB but DESTROYS ELF notes — ruled out. Slim Node build flags: \`--with-intl=small-icu\`, \`--without-inspector\`, \`--without-sqlite\`, \`--without-npm\`, \`--enable-lto\` (high CI cost, deferred). Cross-compilation from Linux to darwin not officially supported. Final vs Bun: download 30 MiB (Bun: 32 MiB), \`--version\` ~1.0s (Bun: ~1.9s), completions ~0.16s (Bun: ~0.42s).

<!-- lore:019e51fe-5a57-7a60-a9a6-7e68ffb4f169 -->
* **CI build-binary matrix: PR=2 targets, main/release=7 targets**: \`ci.yml\` \`build-binary\` job: PRs build only \`linux-x64\` (can-test:true) + \`linux-x64-musl\` (can-test:false). Main/release/workflow\_call builds all 7: darwin-arm64, linux-x64, linux-x64-musl, windows-x64, darwin-x64, linux-arm64, linux-arm64-musl. Build command: \`bun run build --target ${{ matrix.target }}\`. \`test-e2e\` downloads \`sentry-linux-x64\` artifact and sets \`SENTRY\_CLI\_BINARY\`. \`build-npm\` matrix: Node 22+24; smoke test is only \`node dist/bin.cjs --help\`. \`SENTRY\_CLIENT\_ID\` defaults to \`ci-fork-pr-dummy\` for fork PRs (can't read repo vars). Gzip artifacts only on non-PR runs. \`FOSSILIZE\_SIGN=y\` on push to main/release.
Expand Down Expand Up @@ -81,6 +81,9 @@
<!-- lore:019cc2ef-9be5-722d-bc9f-b07a8197eeed -->
* **All view subcommands should use \<target> \<id> positional pattern**: All \`\* view\` subcommands use \`\<target> \<id>\` positional pattern (Intent-First Correction UX): target is optional \`org/project\`. Use opportunistic arg swapping with \`log.warn()\` when args are wrong order — when intent is unambiguous, do what they meant. Normalize at command level, keep parsers pure. Model after \`gh\` CLI. Exception: \`auth\` uses \`defaultCommand: "status"\` (no viewable entity). Routes without defaults: \`cli\`, \`sourcemap\`, \`repo\`, \`team\`, \`trial\`, \`release\`, \`dashboard/widget\`.

<!-- lore:019e56fb-b48b-7625-a591-85cdc2df10d3 -->
* **Node.js slim build flags for SEA binary size reduction**: Node.js configure.py size-reduction flags relevant to SEA builds: \`--with-intl=small-icu\` (English-only ICU, no download needed; saves ~20 MB .rodata); \`--with-intl=none\` (disables Intl/String.normalize entirely); \`--without-inspector\` (removes V8 inspector protocol); \`--without-sqlite\` (removes SQLite + Web Storage API); \`--without-npm\`/\`--without-corepack\` (already excluded by default); \`--enable-lto\` (LTO, GCC 5.4.1+ or Clang 3.9.1+); \`--without-node-snapshot\`/\`--without-node-code-cache\` (removes V8 snapshot/code cache). AVOID: \`--disable-single-executable-application\` (removes SEA support), \`--v8-lite-mode\` (no JIT, much slower), \`--without-ssl\` (breaks crypto/https). Cross-compilation from Linux to darwin NOT officially supported — darwin builds require macOS runners with Xcode >= 16.4.

<!-- lore:019c99d5-69f2-74eb-8c86-411f8512801d -->
* **Raw markdown output for non-interactive terminals, rendered for TTY**: Markdown-first output pipeline: custom renderer in \`src/lib/formatters/markdown.ts\` walks \`marked\` tokens to produce ANSI-styled output. 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. Colors defined in \`COLORS\` object in \`colors.ts\`. Tests run non-TTY so assertions match raw CommonMark; use \`stripAnsi()\` helper for rendered-mode assertions.

Expand Down Expand Up @@ -113,6 +116,12 @@
<!-- lore:019e471c-df3f-7e78-9df7-6b36d2d258db -->
* **SQLite transaction() ROLLBACK can throw, discarding original error**: (gotcha) SQLite transaction ROLLBACK error-swallowing trap: In \`src/lib/db/sqlite.ts\`, \`transaction()\` catches errors and runs \`this.db.exec('ROLLBACK')\`. If ROLLBACK itself throws, the original error is lost. Fix: \`const origErr = e; try { this.db.exec('ROLLBACK'); } catch (rbErr) { log.debug(...); } throw origErr;\`

<!-- lore:019e5719-0102-7bdd-82e9-b39f8cb96244 -->
* **strip fails on Node SEA binaries — must strip BEFORE fossilize injection**: Strip debug symbols must happen BEFORE fossilize SEA injection. Trap: \`strip --strip-unneeded\` on a plain Node binary saves ~17 MiB and still runs — looks like it should work on the final SEA binary too. But after postject injects the SEA blob, \`strip\` fails: 'section .text can't be allocated in segment 2'. Fix: as of fossilize #16, stripping is built into fossilize itself — it strips the copied binary (already unsigned for macOS/Windows) BEFORE calling postject. Cross-strip from Linux to macOS silently fails (caught); native macOS runners strip correctly with \`strip -x\`. Windows skipped (no debug symbols). No need for \`stripCachedNodeBinaries\` in the CLI build script — fossilize handles it.

<!-- lore:019e56fb-b485-73b0-8ab3-848bb3649b6e -->
* **UPX destroys ELF notes — incompatible with Node SEA binaries**: Trap: UPX compresses Node binaries from 99 MiB to 25 MiB and the compressed binary still runs — looks like a huge win. But UPX rewrites the entire ELF structure: original binary has 2 ELF notes (NT\_GNU\_BUILD\_ID + NT\_GNU\_ABI\_TAG), UPX'd binary has 0 notes and 0 sections. NODE\_SEA\_BLOB is stored as an ELF note — UPX destroys it. Fix: use \`strip --strip-unneeded\` instead, BUT only on the plain Node binary BEFORE fossilize SEA injection. After injection, \`strip\` fails with 'section .text can't be allocated in segment 2' — the SEA blob corrupts the ELF section-to-segment mapping. Strip the \`.node-cache/\` binaries before calling fossilize. Saves ~17 MB raw / ~4 MB compressed. Strip is idempotent — already-stripped binaries are unchanged. Recommended order: strip cached Node → fossilize (inject) → binpunch → gzip.

<!-- lore:019e51e9-4a9c-7dde-814e-7c8904ee2833 -->
* **useTestConfigDir afterEach: never delete CONFIG\_DIR\_ENV\_VAR — always restore previous value**: Trap: deleting \`process.env.SENTRY\_CONFIG\_DIR\` in \`afterEach\` looks like proper cleanup. But \`preload.ts\` always sets \`SENTRY\_CONFIG\_DIR\`, so \`savedConfigDir\` is always defined — deleting it causes subsequent test files' module-level code or \`beforeEach\` hooks to read \`undefined\`. Fix: always restore the previous value, never delete. The \`else { delete process.env\[CONFIG\_DIR\_ENV\_VAR] }\` branch is intentionally omitted in \`test/helpers.ts\` \`useTestConfigDir\`. Same principle applies in \`test/fixture.ts\` \`setAuthToken()\` finally block — the delete there is acceptable only because it's a scoped try/finally restore, not a test lifecycle hook.

Expand Down Expand Up @@ -205,7 +214,7 @@
* **Always pull from origin/main before starting any exploration or work in getsentry/cli**: Before beginning any exploration, investigation, or implementation work in the getsentry/cli repository, always run \`git pull origin/main\` first. If there are local changes (e.g., in \`.lore.md\`) that block the pull, stash them, complete the pull, resolve any conflicts by checking out the index version, then drop the stash. This is an explicit user directive that applies at the very start of every session involving this repo, regardless of what work is planned.

<!-- lore:019e5276-c2e3-7f64-a545-8dc3a65798bf -->
* **Always pursue native runner builds to enable platform-specific optimizations**: When the user discovers that cross-compilation from a non-native runner is blocking optimizations (e.g., code cache, smoke testing, codesigning), they consistently push to move builds to native runners for each target platform. This applies to macOS targets moving from ubuntu-latest to macos-latest, and extends to investigating per-platform features like useCodeCache and useSnapshot in fossilize. When the assistant identifies a cross-compilation limitation, the user expects a concrete plan to restructure CI matrix jobs to use native runners, and will follow up to explore related optimizations (e.g., cloning upstream repos to check feasibility). Always check whether current CI runners match the target platform and propose native runner alternatives when they don't.
* **Always pursue native runner builds to enable platform-specific optimizations**: When the user discovers that cross-compilation from a non-native runner is blocking optimizations (e.g., code cache, codesigning, strip+resign), they consistently push to move builds to native runners for each target platform. macOS targets require macOS runners (Xcode >= 16.4 for Node.js builds; \`strip -x\` on Mach-O requires re-codesigning). Linux cross-compilation to darwin is NOT officially supported by Node.js. The user will switch to per-platform native builds if bytecode (\`useCodeCache\`) yields meaningful startup improvement. Always check whether current CI runners match the target platform and propose native runner alternatives when they don't.

<!-- lore:019e56e7-1757-7b5c-9d3b-059491b69f0e -->
* **Always reference external tools and prior art when exploring build/size optimization approaches**: When investigating build pipeline improvements or binary size reduction, the user consistently references specific external tools, repos, and contacts (e.g., Vercel's build-binary.mjs, binpunch, fossilize, Melkey's work) as starting points for evaluation. They expect the assistant to analyze whether each referenced approach actually applies to their specific setup before recommending it. The user wants a clear breakdown of what's relevant vs. irrelevant given their actual architecture (e.g., 'we already use esbuild full bundling, so node\_modules stripping doesn't apply'), followed by concrete alternative opportunities ranked by impact.
Expand All @@ -216,6 +225,9 @@
<!-- lore:019e56f4-d4d9-7d15-b729-2f11a05dd23d -->
* **Always research technical approaches thoroughly before implementation**: When facing a significant technical decision or migration, the user consistently requests deep research into multiple approaches before writing any code. This includes: fetching specific upstream documentation/source files (e.g., BUILDING.md, configure.py), identifying concrete flags/options, estimating build times, and evaluating cross-compilation feasibility. The user wants tradeoffs between paths laid out explicitly. Only after research is complete does implementation begin. When presenting research, include specific flags, URLs, estimated costs (time/size), and platform constraints.

<!-- lore:019e56f7-21d3-70f8-a5cb-f7d6464899ff -->
* **Always track migration progress with explicit completion criteria and remaining blockers**: The Bun→Node migration is complete only when \`Bun.build({ compile: true })\` is replaced by fossilize in \`script/build.ts\`. As of the current session, \`script/build.ts\` already uses fossilize (\`--no-bundle\`, \`--out-dir dist-bin\`, \`--node-version lts\`) with esbuild for bundling — the migration is complete. NODE\_VERSION='lts' in build.ts. The user expects the assistant to track this state across sessions and confirm the migration is done. When resuming sessions, verify \`script/build.ts\` does not contain \`Bun.build({ compile: true })\` before declaring migration complete.

<!-- lore:019e51ed-de34-73e5-9b09-56df6f0ebbc0 -->
* **Always update dependencies promptly after releasing new versions**: When the user releases a new version of a tool they own (e.g., fossilize), they immediately update dependent projects to use that new version. This includes bumping the version in package files, creating a dedicated branch with a descriptive name (e.g., \`chore/tool-x.y.z\`), and opening a pull request. The commit message follows conventional commit format: \`chore: update \<tool> to \<version> (\<key feature>)\`. The assistant should proactively handle the full update workflow: fetch latest main, create the branch, update the dependency, commit, push, and open a PR.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"consola": "^3.4.2",
"esbuild": "^0.25.0",
"fast-check": "^4.5.3",
"fossilize": "^0.6.0",
"fossilize": "^0.7.0",
"hono": "^4.12.15",
"http-cache-semantics": "^4.2.0",
"ignore": "^7.0.5",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 0 additions & 54 deletions script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,54 +53,6 @@ const REQUIRE_ALIAS_FILTER =
/(?:db[\\/](?:index|schema)|list-command|telemetry)\.ts$/;
const REQUIRE_ALIAS_RE = /\b_require\(/g;

/** Fossilize's Node binary cache directory. */
const NODE_CACHE_DIR = ".node-cache";

/**
* Strip debug symbols from cached Node binaries BEFORE fossilize uses them.
*
* Node.js ships with full symbol tables (~17 MiB on linux-x64) that aren't
* needed at runtime. Stripping must happen before SEA injection because
* postject corrupts the ELF section-to-segment mapping, making strip fail.
*
* Stripping is idempotent — already-stripped binaries are unchanged.
* Windows PE binaries are skipped (no debug symbols in release builds).
*/
async function stripCachedNodeBinaries(platforms: string[]): Promise<void> {
if (!existsSync(NODE_CACHE_DIR)) {
return;
}

const { readdirSync } = await import("node:fs");
const files = readdirSync(NODE_CACHE_DIR);

for (const platform of platforms) {
// Skip Windows — no debug symbols to strip
if (platform.startsWith("win")) {
continue;
}

// Strip ALL cached binaries for this platform (multiple versions may exist)
const matches = files.filter(
(f) => f.endsWith(`-${platform}`) || f.endsWith(`-${platform}.exe`)
);

for (const match of matches) {
const cached = join(NODE_CACHE_DIR, match);
try {
const stripCmd = platform.startsWith("darwin")
? `strip -x "${cached}"`
: `strip --strip-unneeded "${cached}"`;
execSync(stripCmd, { stdio: "ignore" });
console.log(` Stripped ${match}`);
} catch {
// Non-fatal: stripping may fail on cross-platform binaries (e.g.,
// stripping a macOS Mach-O on Linux). The binary still works unstripped.
}
}
}
}

/** Build-time constants injected into the binary */
const SENTRY_CLIENT_ID = process.env.SENTRY_CLIENT_ID ?? "";

Expand Down Expand Up @@ -362,12 +314,6 @@ async function compileAllTargets(
` Step 2: Compiling ${platforms.length} target(s) (Node SEA via fossilize)...`
);

// Pre-strip Node binaries in fossilize's cache to reduce final binary size.
// strip removes debug symbols and unneeded sections (~17 MiB on linux-x64).
// Must run BEFORE fossilize's SEA injection — postject corrupts ELF section
// layout, making strip fail on the injected binary.
await stripCachedNodeBinaries(platforms);

const fossilizeBin = join("node_modules", ".bin", "fossilize");

try {
Expand Down
Loading