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 exit 1
- name: Setup Node.js - name: Setup Node.js
if: matrix.runtime == 'node'
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 24 node-version: 24
check-latest: true check-latest: true
- name: Setup Bun - name: Setup Bun
if: matrix.runtime == 'bun'
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2
with: with:
# bun.sh downloads currently fail with: bun-version: latest
# "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
- name: Runtime versions - name: Runtime versions
run: | run: |
node -v node -v
npm -v npm -v
if [ "${{ matrix.runtime }}" = "bun" ]; then bun -v; fi bun -v
- name: Capture node path - name: Capture node path
run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV" run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV"

View File

@@ -7,7 +7,7 @@
## Build, Test, and Development Commands ## Build, Test, and Development Commands
- Install deps: `pnpm install` - 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) - Type-check/build: `pnpm build` (tsc)
- Lint/format: `pnpm lint` (biome check), `pnpm format` (biome format) - Lint/format: `pnpm lint` (biome check), `pnpm format` (biome format)
- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage` - Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage`

View File

@@ -14,7 +14,7 @@ read_when:
Script: `scripts/bench-model.ts` Script: `scripts/bench-model.ts`
Usage: 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` - 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.” - 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.mappings` lets you define `match`, `action`, and templates in config.
- `hooks.transformsDir` + `transform.module` loads a JS/TS module for custom logic. - `hooks.transformsDir` + `transform.module` loads a JS/TS module for custom logic.
- Use `match.source` to keep a generic ingest endpoint (payload-driven routing). - 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`. - `clawdbot hooks gmail setup` writes `hooks.gmail` config for `clawdbot hooks gmail run`.
## Responses ## Responses

View File

@@ -44,20 +44,20 @@
"LICENSE" "LICENSE"
], ],
"scripts": { "scripts": {
"dev": "tsx src/entry.ts", "dev": "bun src/entry.ts",
"postinstall": "node scripts/postinstall.js", "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:dev": "cd docs && mint dev",
"docs:build": "cd docs && pnpm dlx mint broken-links", "docs:build": "cd docs && pnpm dlx mint broken-links",
"build": "tsc -p tsconfig.json && tsx scripts/canvas-a2ui-copy.ts", "build": "tsc -p tsconfig.json && bun scripts/canvas-a2ui-copy.ts",
"release:check": "tsx scripts/release-check.ts", "release:check": "bun scripts/release-check.ts",
"ui:install": "pnpm -C ui install", "ui:install": "pnpm -C ui install",
"ui:dev": "pnpm -C ui dev", "ui:dev": "pnpm -C ui dev",
"ui:build": "pnpm -C ui build", "ui:build": "pnpm -C ui build",
"start": "tsx src/entry.ts", "start": "bun src/entry.ts",
"clawdbot": "tsx src/entry.ts", "clawdbot": "bun src/entry.ts",
"gateway:watch": "tsx watch --clear-screen=false --include 'src/**/*.ts' src/entry.ts gateway --force", "gateway:watch": "bun --watch src/entry.ts gateway --force",
"clawdbot:rpc": "tsx src/entry.ts agent --mode rpc --json", "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": "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: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", "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:swift": "swiftformat --lint --config .swiftformat apps/macos/Sources apps/ios/Sources apps/shared/ClawdbotKit/Sources",
"format:fix": "biome format src --write", "format:fix": "biome format src --write",
"test": "vitest", "test": "vitest",
"test:force": "tsx scripts/test-force.ts", "test:force": "bun scripts/test-force.ts",
"test:coverage": "vitest run --coverage", "test:coverage": "vitest run --coverage",
"test:e2e": "vitest run --config vitest.e2e.config.ts", "test:e2e": "vitest run --config vitest.e2e.config.ts",
"test:docker:qr": "bash scripts/e2e/qr-import-docker.sh", "test:docker:qr": "bash scripts/e2e/qr-import-docker.sh",
"protocol:gen": "tsx scripts/protocol-gen.ts", "protocol:gen": "bun scripts/protocol-gen.ts",
"protocol:gen:swift": "tsx scripts/protocol-gen-swift.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", "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" "canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh"
}, },
@@ -142,7 +142,6 @@
"quicktype-core": "^23.2.6", "quicktype-core": "^23.2.6",
"rolldown": "1.0.0-beta.58", "rolldown": "1.0.0-beta.58",
"signal-utils": "^0.21.1", "signal-utils": "^0.21.1",
"tsx": "^4.21.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vitest": "^4.0.16", "vitest": "^4.0.16",
"wireit": "^0.14.12" "wireit": "^0.14.12"

8
pnpm-lock.yaml generated
View File

@@ -194,9 +194,6 @@ importers:
signal-utils: signal-utils:
specifier: ^0.21.1 specifier: ^0.21.1
version: 0.21.1(signal-polyfill@0.2.2) version: 0.21.1(signal-polyfill@0.2.2)
tsx:
specifier: ^4.21.0
version: 4.21.0
typescript: typescript:
specifier: ^5.9.3 specifier: ^5.9.3
version: 5.9.3 version: 5.9.3
@@ -4792,6 +4789,7 @@ snapshots:
get-tsconfig@4.13.0: get-tsconfig@4.13.0:
dependencies: dependencies:
resolve-pkg-maps: 1.0.0 resolve-pkg-maps: 1.0.0
optional: true
glob-parent@5.1.2: glob-parent@5.1.2:
dependencies: dependencies:
@@ -5553,7 +5551,8 @@ snapshots:
require-from-string@2.0.2: {} require-from-string@2.0.2: {}
resolve-pkg-maps@1.0.0: {} resolve-pkg-maps@1.0.0:
optional: true
retry@0.12.0: {} retry@0.12.0: {}
@@ -5877,6 +5876,7 @@ snapshots:
get-tsconfig: 4.13.0 get-tsconfig: 4.13.0
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
optional: true
type-is@2.0.1: type-is@2.0.1:
dependencies: 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 { readdirSync, readFileSync } from 'node:fs';
import { join, relative } from 'node:path'; 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"; 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 os from "node:os";
import path from "node:path"; import path from "node:path";
import { spawnSync } from "node:child_process"; 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> { async function resolveA2uiRoot(): Promise<string | null> {
const here = path.dirname(fileURLToPath(import.meta.url)); const here = path.dirname(fileURLToPath(import.meta.url));
const candidates = [ const candidates = [
// Running from source (tsx) or dist (tsc + copied assets). // Running from source (bun) or dist (tsc + copied assets).
path.resolve(here, "a2ui"), path.resolve(here, "a2ui"),
// Running from dist without copied assets (fallback to source). // Running from dist without copied assets (fallback to source).
path.resolve(here, "../../src/canvas-host/a2ui"), path.resolve(here, "../../src/canvas-host/a2ui"),

View File

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

View File

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

View File

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