diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6577bfcb..062e5736c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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" diff --git a/AGENTS.md b/AGENTS.md index a2b576c72..740d607d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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` diff --git a/docs/test.md b/docs/test.md index b31a57fbb..6670cd1cf 100644 --- a/docs/test.md +++ b/docs/test.md @@ -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.” diff --git a/docs/webhook.md b/docs/webhook.md index ae17f8925..c0b3b1925 100644 --- a/docs/webhook.md +++ b/docs/webhook.md @@ -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 diff --git a/package.json b/package.json index 61c60d401..4e30de34a 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82dcf8793..7b5468092 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/scripts/docs-list.ts b/scripts/docs-list.ts old mode 100644 new mode 100755 index a631726cf..7fad2594a --- a/scripts/docs-list.ts +++ b/scripts/docs-list.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env tsx +#!/usr/bin/env bun import { readdirSync, readFileSync } from 'node:fs'; import { join, relative } from 'node:path'; diff --git a/scripts/release-check.ts b/scripts/release-check.ts old mode 100644 new mode 100755 index d9e0b43a3..3863a9d11 --- a/scripts/release-check.ts +++ b/scripts/release-check.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env tsx +#!/usr/bin/env bun import { execSync } from "node:child_process"; diff --git a/scripts/test-force.ts b/scripts/test-force.ts old mode 100644 new mode 100755 index 9845fbd69..4e6e3bf9e --- a/scripts/test-force.ts +++ b/scripts/test-force.ts @@ -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"; diff --git a/src/canvas-host/a2ui.ts b/src/canvas-host/a2ui.ts index 0c55c3c54..ff871aec2 100644 --- a/src/canvas-host/a2ui.ts +++ b/src/canvas-host/a2ui.ts @@ -15,7 +15,7 @@ let resolvingA2uiRoot: Promise | null = null; async function resolveA2uiRoot(): Promise { 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"), diff --git a/src/cli/gateway.sigterm.test.ts b/src/cli/gateway.sigterm.test.ts index 5d722cf16..533cd06f4 100644 --- a/src/cli/gateway.sigterm.test.ts +++ b/src/cli/gateway.sigterm.test.ts @@ -90,10 +90,8 @@ describe("gateway SIGTERM", () => { const err: string[] = []; child = spawn( - process.execPath, + "bun", [ - "--import", - "tsx", "src/index.ts", "gateway", "--port", diff --git a/src/daemon/program-args.ts b/src/daemon/program-args.ts index b2dddfb6c..bd530a789 100644 --- a/src/daemon/program-args.ts +++ b/src/daemon/program-args.ts @@ -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 { 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 { - const candidate = path.join( - repoRoot, - "node_modules", - "tsx", - "dist", - "cli.mjs", - ); - await fs.access(candidate); - return candidate; +async function resolveBunPath(): Promise { + // 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 { 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, }; } diff --git a/test/gateway.multi.e2e.test.ts b/test/gateway.multi.e2e.test.ts index f3510f6be..7e15e96c0 100644 --- a/test/gateway.multi.e2e.test.ts +++ b/test/gateway.multi.e2e.test.ts @@ -118,10 +118,8 @@ const spawnGatewayInstance = async (name: string): Promise => { try { child = spawn( - process.execPath, + "bun", [ - "--import", - "tsx", "src/index.ts", "gateway", "--port", @@ -218,15 +216,11 @@ const runCliJson = async ( ): Promise => { 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)));