Skip to content

fix: ship a node-runtime CLI to actually fix stdout truncation (#20)#23

Merged
stephendolan merged 1 commit into
mainfrom
fix/issue-20-real-fix-bun-pipe-truncation
May 21, 2026
Merged

fix: ship a node-runtime CLI to actually fix stdout truncation (#20)#23
stephendolan merged 1 commit into
mainfrom
fix/issue-20-real-fix-bun-pipe-truncation

Conversation

@stephendolan
Copy link
Copy Markdown
Owner

Summary

v2.5.1 removed process.exit() calls but issue #20 reproduced anyway. Root cause is the #!/usr/bin/env bun shebang — bun drops queued stdout writes on exit even without an explicit process.exit(). The same bundled dist/cli.js:

  • under node: 10385 bytes piped ✅
  • under bun (current shebang): 512 bytes piped ❌

This PR:

  • Switches the shipped shebang to #!/usr/bin/env node (tsup emits plain ESM; no bun-runtime APIs in the bundle).
  • Drops engines.bun, adds engines.node: ">=20". Bun remains a dev dependency for TS execution, build, and tests.
  • Adds program.exitOverride() so commander's --help, --version, and parse-error paths don't call process.exit and race the pipe drain. The CommanderError.exitCode is forwarded via process.exitCode.
  • Adds src/lib/__tests__/pipe-truncation.test.ts, an end-to-end regression suite that spawns the built dist/cli.js under its real shebang and counts bytes received through a real cli | cat pipe. The v2.5.1 test passed because the error payload fit in one 512-byte pipe buffer — this one uses --help (~649 bytes) so the regression would now be caught.
  • CI: build before test so the integration suite has dist/cli.js to exercise.
  • CLAUDE.md: document the runtime split and the exitOverride() requirement.

Test plan

  • bun run typecheck clean
  • bun run lint clean
  • bun run test (vitest) — 24/24 pass
  • bun test (bun native, mirrors CI) — 24/24 pass
  • Manual: dist/cli.js inbox list 2>/dev/null | cat | wc -c → 10385 (was 512)
  • Manual: dist/cli.js --help | cat | wc -c → 649 (was 512)
  • Manual: error exit code 1 still propagates after the change

🤖 Generated with Claude Code

v2.5.1 removed all process.exit() calls but the truncation persisted:
the real cause is the `#!/usr/bin/env bun` shebang. Bun drops queued
stdout writes on exit even without an explicit process.exit. Direct
proof: the same bundled dist/cli.js produces 10385 bytes piped under
node and 512 bytes piped under bun.

Changes:
- Shebang `bun` -> `node`; tsup already emits plain ESM with no
  bun-specific runtime APIs, so no behavioral change beyond the fix.
- engines.bun -> engines.node ">=20". Bun stays as a dev dependency
  for TS execution, build, and tests; end users only need node.
- Add `program.exitOverride()` in cli.ts so commander's --help,
  --version, and parse-error paths don't call process.exit and race
  the pipe drain. The CommanderError exit code is forwarded via
  process.exitCode in the top-level catch.
- New regression suite `pipe-truncation.test.ts` spawns the built
  dist/cli.js under its real shebang and counts bytes received via a
  real pipe (`cli | cat`). The v2.5.1 fix passed because the error
  payload fit in one 512-byte pipe buffer; this test uses `--help`
  (~649 bytes) so the regression would now be caught.
- CI workflows: build before tests so the new integration suite has
  dist/cli.js to exercise.
- CLAUDE.md: document the runtime split (bun for dev, node for ship)
  and the commander.exitOverride() requirement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@stephendolan stephendolan merged commit bb754c5 into main May 21, 2026
1 check passed
@stephendolan stephendolan deleted the fix/issue-20-real-fix-bun-pipe-truncation branch May 21, 2026 14:05
@stephendolan stephendolan mentioned this pull request May 21, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant