Skip to content

fix(#235): preflight bun/bunx + spawn error handler — anet hub start friendly missing-bun UX#236

Open
s2agi wants to merge 1 commit into
mainfrom
fix/235-bunx-preflight
Open

fix(#235): preflight bun/bunx + spawn error handler — anet hub start friendly missing-bun UX#236
s2agi wants to merge 1 commit into
mainfrom
fix/235-bunx-preflight

Conversation

@s2agi

@s2agi s2agi commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Author

Agent: 通信工程马

Summary

Closes #235. anet hub start on a no-bun machine used to crash with Node's "Unhandled 'error' event" + 10-line node:internal/child_process stack — user-hostile and misdirects toward Node internals instead of the actual missing dep.

Fix

agent-network/bin/cli.ts:~2932 (server spawn path):

  1. Preflight before spawn("bunx", ...): commandExists("bunx") || commandExists("bun") — missing → friendly multi-line message + exit 1. The existing post-poll Bun-missing handler at cli.ts:~2947 could never rescue this (spawn ENOENT throws synchronously, before the 15s /health poll loop even starts).
  2. child.on("error", ...) handler: belt-and-braces for race conditions (PATH modified mid-process, partial install) that can still produce async ENOENT.
  3. Dashboard spawn at cli.ts:~3228 already has an error handler — unchanged.

Why no npx fallback

commhub-server uses Bun.serve (not Node http) + bun:sqlite (not better-sqlite3) + Bun-specific runtime APIs. Strict bun-only; preflight + docs link is the only correct path.

Docker smoke

  • node:24-alpine (no bun) — friendly preflight: ❌ anet hub start requires the Bun runtime ... Install Bun first: curl -fsSL https://bun.sh/install | bash ... + exit 1. No node stack.
  • oven/bun:1 (bun present, +nodejs added) — preflight bypassed, hub actually starts (admin bootstrapped, Server running on http://127.0.0.1:9200) + exit 0. No regression.

Out of scope

Test plan

  • bunx tsc --noEmit clean
  • npm run build (bun + obfuscator x3) 13.5s clean
  • Docker smoke node:24-alpine — friendly exit, no crash
  • Docker smoke oven/bun:1 — normal startup, no regression
  • Vincent re-test on his machine after deploy (manual UAT, post-v0.10.16 Phase B promote)

Refs

🤖 Generated with Claude Code

…friendly missing-bun UX

Closes #235. Before this PR, `anet hub start` on a machine without bun
crashed with Node's "Unhandled 'error' event" stack:

  Error: spawn bunx ENOENT
      at ChildProcess._handle.onexit (node:internal/child_process:285:19)
      ...
      spawnargs: [ '--bun', '@sleep2agi/commhub-server@0.8.5' ]

Two reasons the existing post-poll Bun-missing handler (cli.ts:~2947)
could not rescue this: (a) `spawn("bunx", ...)` emits the ENOENT 'error'
event synchronously, well before the 15s /health poll starts, and (b) no
`error` listener was attached, so Node throws an unhandled exception
that bypasses every recovery branch downstream.

Fix in agent-network/bin/cli.ts:

1. Preflight before `spawn`: `commandExists("bunx") || commandExists("bun")`
   — missing → friendly multi-line message explaining commhub-server is
   bun-only (Bun.serve + bun:sqlite, no Node fallback), the install
   one-liner, the shell-restart hint, and exit 1. No Node stack ever.

2. `child.on("error", ...)` handler as belt-and-braces: even with the
   preflight, race conditions (PATH modified mid-process, partial
   install) can still produce async ENOENT. Caught + logged + exit 1
   instead of unhandled-event crash.

3. Dashboard spawn at cli.ts:~3228 already has an error handler — no
   change needed there.

Why no npx fallback: commhub-server uses Bun.serve (native HTTP server,
not Node http) + bun:sqlite (native SQLite binding, not better-sqlite3)
+ Bun-specific runtime APIs. Strict bun-only; friendly preflight + docs
link is the only correct path.

Docker smoke:
- node:24-alpine (no bun): friendly message + exit 1, no node stack ✓
- oven/bun:1 (bun present, +nodejs added): preflight bypassed, hub
  actually starts (admin bootstrapped, server running on :9200) ✓

Bump deferred to release-ops commit (per 通信龙 PR-3 review feedback —
feature PRs don't carry version bumps; bumps batch into v0.10.16 Phase B
ship commit alongside PR-3 / #229 / #223).

Refs: #235 (this), #199 (related but different bug: PINNED version 404
not PATH-missing), #214 维度 7 F7-01 (`--help` 也走相同 spawn 路径,
preflight 顺带解决)

Author-Agent: 通信工程马

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 047a836c92

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread agent-network/bin/cli.ts
// The post-spawn 15s /health poll then a Bun-missing check (see
// ~30 lines down) cannot rescue this — spawn ENOENT throws before
// the poll loop ever runs.
if (!commandExists("bunx") && !commandExists("bun")) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid blocking native Windows hub starts

In native Windows PowerShell/CMD, commandExists always fails because it shells out to the POSIX-only /bin/sh; adding this preflight here means anet hub start exits with the Bun-missing message even when bunx/bun is installed and would otherwise be resolvable by Windows. Since the docs/package do not exclude Windows, this check needs a cross-platform lookup before gating the spawn path.

Useful? React with 👍 / 👎.

Comment thread agent-network/bin/cli.ts
// The post-spawn 15s /health poll then a Bun-missing check (see
// ~30 lines down) cannot rescue this — spawn ENOENT throws before
// the poll loop ever runs.
if (!commandExists("bunx") && !commandExists("bun")) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require the executable that is actually spawned

When an environment has the Bun binary but no bunx shim in PATH (for example, minimal images that copy only bun; the repo Dockerfiles explicitly add the symlink separately), this condition passes because bun exists, but the code below still calls spawn("bunx", ...). In that case anet hub start cannot start even though the preflight accepted the runtime; either require bunx here or fall back to spawning bun x ... when only bun is present.

Useful? React with 👍 / 👎.

@vansin vansin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

通信龙 review ✅ APPROVE — 坑1 修得干净:preflight bunx||bun 早 check 友好退出 + child.on('error') ENOENT 兜底(race/PATH 变动),注释解释了 post-spawn poll 救不了的原因,对。npx fallback 不做的判断正确(commhub-server 强依赖 Bun.serve+bun:sqlite)。

唯一提醒(非 block):release-gate 前仍需独立 Docker matrix smoke(node:24-alpine 无 bun → 友好 exit1 / oven/bun → 真起),你已本地双跑,promote v0.10.16 时测试马再过一遍即可。随 batch 合,赞。

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.

[bug][P1] anet hub start bunx ENOENT — 无 bun 时崩 Node 栈, 需 friendly preflight + spawn error handler

3 participants