From ee380e9ab90affeeeeddf2c2ecc43b3d2d0d7542 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 18 Jan 2026 18:21:13 +0000 Subject: [PATCH] fix: run cli scripts via node build runner --- docs/debug/node-issue.md | 65 +++++++++++++++++++++++++++++++++ package.json | 18 ++++----- scripts/repro/tsx-name-repro.ts | 3 ++ scripts/run-node.mjs | 38 +++++++++++++++++++ scripts/watch-node.mjs | 52 ++++++++++++++++++++++++++ 5 files changed, 167 insertions(+), 9 deletions(-) create mode 100644 docs/debug/node-issue.md create mode 100644 scripts/repro/tsx-name-repro.ts create mode 100644 scripts/run-node.mjs create mode 100644 scripts/watch-node.mjs diff --git a/docs/debug/node-issue.md b/docs/debug/node-issue.md new file mode 100644 index 000000000..d061170d1 --- /dev/null +++ b/docs/debug/node-issue.md @@ -0,0 +1,65 @@ +# Node + tsx "__name is not a function" crash + +## Summary +Running Clawdbot via Node with `tsx` fails at startup with: + +``` +[clawdbot] Failed to start CLI: TypeError: __name is not a function + at createSubsystemLogger (.../src/logging/subsystem.ts:203:25) + at .../src/agents/auth-profiles/constants.ts:25:20 +``` + +This began after switching dev scripts from Bun to `tsx` (commit `2871657e`, 2026-01-06). The same runtime path worked with Bun. + +## Environment +- Node: v25.x (observed on v25.3.0) +- tsx: 4.21.0 +- OS: macOS (repro also likely on other platforms that run Node 25) + +## Repro (Node-only) +```bash +# in repo root +node --version +pnpm install +node --import tsx src/entry.ts status +``` + +## Minimal repro in repo +```bash +node --import tsx scripts/repro/tsx-name-repro.ts +``` + +## Node version check +- Node 25.3.0: fails +- Node 22.22.0 (Homebrew `node@22`): fails +- Node 24: not installed here yet; needs verification + +## Notes / hypothesis +- `tsx` uses esbuild to transform TS/ESM. esbuild’s `keepNames` emits a `__name` helper and wraps function definitions with `__name(...)`. +- The crash indicates `__name` exists but is not a function at runtime, which implies the helper is missing or overwritten for this module in the Node 25 loader path. +- Similar `__name` helper issues have been reported in other esbuild consumers when the helper is missing or rewritten. + +## Regression history +- `2871657e` (2026-01-06): scripts changed from Bun to tsx to make Bun optional. +- Before that (Bun path), `pnpm clawdbot status` and `gateway:watch` worked. + +## Workarounds +- Use Bun for dev scripts (current temporary revert). +- Use Node + tsc watch, then run compiled output: + ```bash + pnpm exec tsc --watch --preserveWatchOutput + node --watch dist/entry.js status + ``` +- Confirmed locally: `pnpm exec tsc -p tsconfig.json` + `node dist/entry.js status` works on Node 25. +- Disable esbuild keepNames in the TS loader if possible (prevents `__name` helper insertion); tsx does not currently expose this. +- Test Node LTS (22/24) with `tsx` to see if the issue is Node 25–specific. + +## References +- https://opennext.js.org/cloudflare/howtos/keep_names +- https://esbuild.github.io/api/#keep-names +- https://github.com/evanw/esbuild/issues/1031 + +## Next steps +- Repro on Node 22/24 to confirm Node 25 regression. +- Test `tsx` nightly or pin to earlier version if a known regression exists. +- If reproduces on Node LTS, file a minimal repro upstream with the `__name` stack trace. diff --git a/package.json b/package.json index 5d6da168c..2352c6d75 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "dist/whatsapp/**" ], "scripts": { - "dev": "bun src/entry.ts", + "dev": "node scripts/run-node.mjs", "postinstall": "node scripts/postinstall.js", "prepack": "pnpm build", "docs:list": "bun scripts/docs-list.ts", @@ -78,14 +78,14 @@ "ui:install": "node scripts/ui.js install", "ui:dev": "node scripts/ui.js dev", "ui:build": "node scripts/ui.js build", - "start": "bun src/entry.ts", - "clawdbot": "bun src/entry.ts", - "gateway:watch": "bun --watch src/entry.ts gateway --force", - "gateway:dev": "CLAWDBOT_SKIP_CHANNELS=1 bun src/entry.ts --dev gateway", - "gateway:dev:reset": "CLAWDBOT_SKIP_CHANNELS=1 bun src/entry.ts --dev gateway --reset", - "tui": "bun src/entry.ts tui", - "tui:dev": "CLAWDBOT_PROFILE=dev bun src/entry.ts tui", - "clawdbot:rpc": "bun src/entry.ts agent --mode rpc --json", + "start": "node scripts/run-node.mjs", + "clawdbot": "node scripts/run-node.mjs", + "gateway:watch": "node scripts/watch-node.mjs gateway --force", + "gateway:dev": "CLAWDBOT_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway", + "gateway:dev:reset": "CLAWDBOT_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway --reset", + "tui": "node scripts/run-node.mjs tui", + "tui:dev": "CLAWDBOT_PROFILE=dev node scripts/run-node.mjs tui", + "clawdbot:rpc": "node scripts/run-node.mjs agent --mode rpc --json", "ios:gen": "cd apps/ios && xcodegen generate", "ios:open": "cd apps/ios && xcodegen generate && open Clawdbot.xcodeproj", "ios:build": "bash -lc 'cd apps/ios && xcodegen generate && xcodebuild -project Clawdbot.xcodeproj -scheme Clawdbot -destination \"${IOS_DEST:-platform=iOS Simulator,name=iPhone 17}\" -configuration Debug build'", diff --git a/scripts/repro/tsx-name-repro.ts b/scripts/repro/tsx-name-repro.ts new file mode 100644 index 000000000..7f5161d40 --- /dev/null +++ b/scripts/repro/tsx-name-repro.ts @@ -0,0 +1,3 @@ +import "../../src/logging/subsystem.js"; + +console.log("tsx-name-repro: loaded logging/subsystem"); diff --git a/scripts/run-node.mjs b/scripts/run-node.mjs new file mode 100644 index 000000000..d833a8ac0 --- /dev/null +++ b/scripts/run-node.mjs @@ -0,0 +1,38 @@ +#!/usr/bin/env node +import { spawn } from "node:child_process"; +import process from "node:process"; + +const args = process.argv.slice(2); +const env = { ...process.env }; +const cwd = process.cwd(); + +const build = spawn("pnpm", ["exec", "tsc", "-p", "tsconfig.json"], { + cwd, + env, + stdio: "inherit", +}); + +build.on("exit", (code, signal) => { + if (signal) { + process.exit(1); + return; + } + if (code !== 0 && code !== null) { + process.exit(code); + return; + } + + const nodeProcess = spawn(process.execPath, ["dist/entry.js", ...args], { + cwd, + env, + stdio: "inherit", + }); + + nodeProcess.on("exit", (exitCode, exitSignal) => { + if (exitSignal) { + process.exit(1); + return; + } + process.exit(exitCode ?? 1); + }); +}); diff --git a/scripts/watch-node.mjs b/scripts/watch-node.mjs new file mode 100644 index 000000000..c91a389af --- /dev/null +++ b/scripts/watch-node.mjs @@ -0,0 +1,52 @@ +#!/usr/bin/env node +import { spawn, spawnSync } from "node:child_process"; +import process from "node:process"; + +const args = process.argv.slice(2); +const env = { ...process.env }; +const cwd = process.cwd(); + +const initialBuild = spawnSync("pnpm", ["exec", "tsc", "-p", "tsconfig.json"], { + cwd, + env, + stdio: "inherit", +}); + +if (initialBuild.status !== 0) { + process.exit(initialBuild.status ?? 1); +} + +const tsc = spawn("pnpm", ["exec", "tsc", "--watch", "--preserveWatchOutput"], { + cwd, + env, + stdio: "inherit", +}); + +const nodeProcess = spawn(process.execPath, ["--watch", "dist/entry.js", ...args], { + cwd, + env, + stdio: "inherit", +}); + +let exiting = false; + +function cleanup(code = 0) { + if (exiting) return; + exiting = true; + nodeProcess.kill("SIGTERM"); + tsc.kill("SIGTERM"); + process.exit(code); +} + +process.on("SIGINT", () => cleanup(130)); +process.on("SIGTERM", () => cleanup(143)); + +tsc.on("exit", (code) => { + if (exiting) return; + cleanup(code ?? 1); +}); + +nodeProcess.on("exit", (code, signal) => { + if (signal || exiting) return; + cleanup(code ?? 1); +});