refactor: replace tsx with bun for TypeScript execution (#278)

This commit is contained in:
Ayaan Zaidi
2026-01-06 12:44:08 +05:30
committed by GitHub
parent b472143882
commit 7a48b908e4
13 changed files with 64 additions and 67 deletions

View File

@@ -52,32 +52,21 @@ jobs:
exit 1
- name: Setup Node.js
if: matrix.runtime == 'node'
uses: actions/setup-node@v4
with:
node-version: 24
check-latest: true
- name: Setup Bun
if: matrix.runtime == 'bun'
uses: oven-sh/setup-bun@v2
with:
# bun.sh downloads currently fail with:
# "Failed to list releases from GitHub: 401" -> "Unexpected HTTP response: 400"
bun-download-url: "https://github.com/oven-sh/bun/releases/latest/download/bun-linux-x64.zip"
- name: Setup Node.js (tooling for bun)
if: matrix.runtime == 'bun'
uses: actions/setup-node@v4
with:
node-version: 24
check-latest: true
bun-version: latest
- name: Runtime versions
run: |
node -v
npm -v
if [ "${{ matrix.runtime }}" = "bun" ]; then bun -v; fi
bun -v
- name: Capture node path
run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV"

View File

@@ -7,7 +7,7 @@
## Build, Test, and Development Commands
- Install deps: `pnpm install`
- Run CLI in dev: `pnpm clawdbot ...` (tsx entry) or `pnpm dev` for `src/index.ts`.
- Run CLI in dev: `pnpm clawdbot ...` (bun entry) or `pnpm dev` for `src/index.ts`.
- Type-check/build: `pnpm build` (tsc)
- Lint/format: `pnpm lint` (biome check), `pnpm format` (biome format)
- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage`

View File

@@ -14,7 +14,7 @@ read_when:
Script: `scripts/bench-model.ts`
Usage:
- `source ~/.profile && pnpm tsx scripts/bench-model.ts --runs 10`
- `source ~/.profile && bun scripts/bench-model.ts --runs 10`
- Optional env: `MINIMAX_API_KEY`, `MINIMAX_BASE_URL`, `MINIMAX_MODEL`, `ANTHROPIC_API_KEY`
- Default prompt: “Reply with a single word: ok. No punctuation or extra text.”

View File

@@ -91,7 +91,7 @@ Mapping options (summary):
- `hooks.mappings` lets you define `match`, `action`, and templates in config.
- `hooks.transformsDir` + `transform.module` loads a JS/TS module for custom logic.
- Use `match.source` to keep a generic ingest endpoint (payload-driven routing).
- TS transforms require a TS loader (e.g. `tsx`) or precompiled `.js` at runtime.
- TS transforms require a TS loader (e.g. `bun`) or precompiled `.js` at runtime.
- `clawdbot hooks gmail setup` writes `hooks.gmail` config for `clawdbot hooks gmail run`.
## Responses

View File

@@ -44,20 +44,20 @@
"LICENSE"
],
"scripts": {
"dev": "tsx src/entry.ts",
"dev": "bun src/entry.ts",
"postinstall": "node scripts/postinstall.js",
"docs:list": "tsx scripts/docs-list.ts",
"docs:list": "bun scripts/docs-list.ts",
"docs:dev": "cd docs && mint dev",
"docs:build": "cd docs && pnpm dlx mint broken-links",
"build": "tsc -p tsconfig.json && tsx scripts/canvas-a2ui-copy.ts",
"release:check": "tsx scripts/release-check.ts",
"build": "tsc -p tsconfig.json && bun scripts/canvas-a2ui-copy.ts",
"release:check": "bun scripts/release-check.ts",
"ui:install": "pnpm -C ui install",
"ui:dev": "pnpm -C ui dev",
"ui:build": "pnpm -C ui build",
"start": "tsx src/entry.ts",
"clawdbot": "tsx src/entry.ts",
"gateway:watch": "tsx watch --clear-screen=false --include 'src/**/*.ts' src/entry.ts gateway --force",
"clawdbot:rpc": "tsx src/entry.ts agent --mode rpc --json",
"start": "bun src/entry.ts",
"clawdbot": "bun src/entry.ts",
"gateway:watch": "bun --watch src/entry.ts gateway --force",
"clawdbot:rpc": "bun src/entry.ts agent --mode rpc --json",
"lint": "biome check src test && oxlint --type-aware src test --ignore-pattern src/canvas-host/a2ui/a2ui.bundle.js",
"lint:swift": "swiftlint lint --config .swiftlint.yml && (cd apps/ios && swiftlint lint --config .swiftlint.yml)",
"lint:fix": "biome check --write --unsafe src && biome format --write src",
@@ -65,12 +65,12 @@
"format:swift": "swiftformat --lint --config .swiftformat apps/macos/Sources apps/ios/Sources apps/shared/ClawdbotKit/Sources",
"format:fix": "biome format src --write",
"test": "vitest",
"test:force": "tsx scripts/test-force.ts",
"test:force": "bun scripts/test-force.ts",
"test:coverage": "vitest run --coverage",
"test:e2e": "vitest run --config vitest.e2e.config.ts",
"test:docker:qr": "bash scripts/e2e/qr-import-docker.sh",
"protocol:gen": "tsx scripts/protocol-gen.ts",
"protocol:gen:swift": "tsx scripts/protocol-gen-swift.ts",
"protocol:gen": "bun scripts/protocol-gen.ts",
"protocol:gen:swift": "bun scripts/protocol-gen-swift.ts",
"protocol:check": "pnpm protocol:gen && pnpm protocol:gen:swift && git diff --exit-code -- dist/protocol.schema.json apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift",
"canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh"
},
@@ -142,7 +142,6 @@
"quicktype-core": "^23.2.6",
"rolldown": "1.0.0-beta.58",
"signal-utils": "^0.21.1",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vitest": "^4.0.16",
"wireit": "^0.14.12"

8
pnpm-lock.yaml generated
View File

@@ -194,9 +194,6 @@ importers:
signal-utils:
specifier: ^0.21.1
version: 0.21.1(signal-polyfill@0.2.2)
tsx:
specifier: ^4.21.0
version: 4.21.0
typescript:
specifier: ^5.9.3
version: 5.9.3
@@ -4792,6 +4789,7 @@ snapshots:
get-tsconfig@4.13.0:
dependencies:
resolve-pkg-maps: 1.0.0
optional: true
glob-parent@5.1.2:
dependencies:
@@ -5553,7 +5551,8 @@ snapshots:
require-from-string@2.0.2: {}
resolve-pkg-maps@1.0.0: {}
resolve-pkg-maps@1.0.0:
optional: true
retry@0.12.0: {}
@@ -5877,6 +5876,7 @@ snapshots:
get-tsconfig: 4.13.0
optionalDependencies:
fsevents: 2.3.3
optional: true
type-is@2.0.1:
dependencies:

2
scripts/docs-list.ts Normal file → Executable file
View File

@@ -1,4 +1,4 @@
#!/usr/bin/env tsx
#!/usr/bin/env bun
import { readdirSync, readFileSync } from 'node:fs';
import { join, relative } from 'node:path';

2
scripts/release-check.ts Normal file → Executable file
View File

@@ -1,4 +1,4 @@
#!/usr/bin/env tsx
#!/usr/bin/env bun
import { execSync } from "node:child_process";

2
scripts/test-force.ts Normal file → Executable file
View File

@@ -1,4 +1,4 @@
#!/usr/bin/env tsx
#!/usr/bin/env bun
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";

View File

@@ -15,7 +15,7 @@ let resolvingA2uiRoot: Promise<string | null> | null = null;
async function resolveA2uiRoot(): Promise<string | null> {
const here = path.dirname(fileURLToPath(import.meta.url));
const candidates = [
// Running from source (tsx) or dist (tsc + copied assets).
// Running from source (bun) or dist (tsc + copied assets).
path.resolve(here, "a2ui"),
// Running from dist without copied assets (fallback to source).
path.resolve(here, "../../src/canvas-host/a2ui"),

View File

@@ -90,10 +90,8 @@ describe("gateway SIGTERM", () => {
const err: string[] = [];
child = spawn(
process.execPath,
"bun",
[
"--import",
"tsx",
"src/index.ts",
"gateway",
"--port",

View File

@@ -11,6 +11,11 @@ function isNodeRuntime(execPath: string): boolean {
return base === "node" || base === "node.exe";
}
function isBunRuntime(execPath: string): boolean {
const base = path.basename(execPath).toLowerCase();
return base === "bun" || base === "bun.exe";
}
async function resolveCliEntrypointPathForService(): Promise<string> {
const argv1 = process.argv[1];
if (!argv1) throw new Error("Unable to resolve CLI entrypoint path");
@@ -108,16 +113,16 @@ function resolveRepoRootForDev(): string {
return parts.slice(0, srcIndex).join(path.sep);
}
async function resolveTsxCliPath(repoRoot: string): Promise<string> {
const candidate = path.join(
repoRoot,
"node_modules",
"tsx",
"dist",
"cli.mjs",
);
await fs.access(candidate);
return candidate;
async function resolveBunPath(): Promise<string> {
// Bun is expected to be in PATH, resolve via which/where
const { execSync } = await import("node:child_process");
try {
const bunPath = execSync("which bun", { encoding: "utf8" }).trim();
await fs.access(bunPath);
return bunPath;
} catch {
throw new Error("Bun not found in PATH. Install bun: https://bun.sh");
}
}
export async function resolveGatewayProgramArguments(params: {
@@ -125,28 +130,40 @@ export async function resolveGatewayProgramArguments(params: {
dev?: boolean;
}): Promise<GatewayProgramArgs> {
const gatewayArgs = ["gateway-daemon", "--port", String(params.port)];
const nodePath = process.execPath;
const execPath = process.execPath;
if (!params.dev) {
try {
const cliEntrypointPath = await resolveCliEntrypointPathForService();
return {
programArguments: [nodePath, cliEntrypointPath, ...gatewayArgs],
programArguments: [execPath, cliEntrypointPath, ...gatewayArgs],
};
} catch (error) {
if (!isNodeRuntime(nodePath)) {
return { programArguments: [nodePath, ...gatewayArgs] };
// If running under bun or another runtime that can execute TS directly
if (!isNodeRuntime(execPath)) {
return { programArguments: [execPath, ...gatewayArgs] };
}
throw error;
}
}
// Dev mode: use bun to run TypeScript directly
const repoRoot = resolveRepoRootForDev();
const tsxCliPath = await resolveTsxCliPath(repoRoot);
const devCliPath = path.join(repoRoot, "src", "index.ts");
await fs.access(devCliPath);
// If already running under bun, use current execPath
if (isBunRuntime(execPath)) {
return {
programArguments: [execPath, devCliPath, ...gatewayArgs],
workingDirectory: repoRoot,
};
}
// Otherwise resolve bun from PATH
const bunPath = await resolveBunPath();
return {
programArguments: [nodePath, tsxCliPath, devCliPath, ...gatewayArgs],
programArguments: [bunPath, devCliPath, ...gatewayArgs],
workingDirectory: repoRoot,
};
}

View File

@@ -118,10 +118,8 @@ const spawnGatewayInstance = async (name: string): Promise<GatewayInstance> => {
try {
child = spawn(
process.execPath,
"bun",
[
"--import",
"tsx",
"src/index.ts",
"gateway",
"--port",
@@ -218,15 +216,11 @@ const runCliJson = async (
): Promise<unknown> => {
const stdout: string[] = [];
const stderr: string[] = [];
const child = spawn(
process.execPath,
["--import", "tsx", "src/index.ts", ...args],
{
cwd: process.cwd(),
env: { ...process.env, ...env },
stdio: ["ignore", "pipe", "pipe"],
},
);
const child = spawn("bun", ["src/index.ts", ...args], {
cwd: process.cwd(),
env: { ...process.env, ...env },
stdio: ["ignore", "pipe", "pipe"],
});
child.stdout?.setEncoding("utf8");
child.stderr?.setEncoding("utf8");
child.stdout?.on("data", (d) => stdout.push(String(d)));