diff --git a/package.json b/package.json index ffd572371..3f7156672 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "./schema.json": "./dist/oh-my-opencode.schema.json" }, "scripts": { - "build": "bun build src/index.ts --outdir dist --target bun --format esm --external @ast-grep/napi --external zod && tsc --emitDeclarationOnly && bun build src/cli/index.ts --outdir dist/cli --target bun --format esm --external @ast-grep/napi && bun run build:schema", + "build": "bun build src/index.ts --outdir dist --target bun --format esm --external @ast-grep/napi --external zod && bun run build:node-require-shim && tsc --emitDeclarationOnly && bun build src/cli/index.ts --outdir dist/cli --target bun --format esm --external @ast-grep/napi && bun run build:schema", + "build:node-require-shim": "bun run script/patch-node-require-shim.ts", "build:all": "bun run build && bun run build:binaries", "build:binaries": "bun run script/build-binaries.ts", "build:schema": "bun run script/build-schema.ts", diff --git a/script/patch-node-require-shim.ts b/script/patch-node-require-shim.ts new file mode 100644 index 000000000..a2e39f0a5 --- /dev/null +++ b/script/patch-node-require-shim.ts @@ -0,0 +1,27 @@ +#!/usr/bin/env bun + +import { readFileSync, writeFileSync } from "node:fs" +import { dirname, join } from "node:path" +import { fileURLToPath } from "node:url" + +const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) +const DIST_PATH = join(SCRIPT_DIR, "..", "dist", "index.js") +const IMPORT_LINE = 'import { createRequire as __omoCreateRequire } from "node:module";' +const BUN_REQUIRE_LINE = "var __require = import.meta.require;" +const NODE_SAFE_REQUIRE_LINE = 'var __require = typeof import.meta.require === "function" ? import.meta.require : __omoCreateRequire(import.meta.url);' + +const original = readFileSync(DIST_PATH, "utf-8") + +if (original.includes(NODE_SAFE_REQUIRE_LINE)) { + console.log("Node/Electron require shim already present in dist/index.js, skipping.") + process.exit(0) +} + +if (!original.includes(BUN_REQUIRE_LINE)) { + throw new Error(`Expected Bun require helper not found in ${DIST_PATH}`) +} + +const patched = original.replace(BUN_REQUIRE_LINE, `${IMPORT_LINE}\n${NODE_SAFE_REQUIRE_LINE}`) + +writeFileSync(DIST_PATH, patched, "utf-8") +console.log("Patched Node/Electron require shim in dist/index.js") diff --git a/src/features/team-mode/team-worktree/cleanup.ts b/src/features/team-mode/team-worktree/cleanup.ts index 673ecebc1..72649cc31 100644 --- a/src/features/team-mode/team-worktree/cleanup.ts +++ b/src/features/team-mode/team-worktree/cleanup.ts @@ -2,9 +2,10 @@ import fs from "node:fs/promises" import path from "node:path" import type { TeamModeConfig } from "./manager" +import { spawn as bunSpawn } from "../../../shared/bun-spawn-shim" async function runGit(args: string[]): Promise<{ code: number; stderr: string }> { - const process = Bun.spawn({ cmd: ["git", ...args], stdout: "pipe", stderr: "pipe" }) + const process = bunSpawn({ cmd: ["git", ...args], stdout: "pipe", stderr: "pipe" }) const [exitCode, stderrText] = await Promise.all([process.exited, new Response(process.stderr).text()]) return { code: exitCode, stderr: stderrText } } @@ -12,7 +13,7 @@ async function runGit(args: string[]): Promise<{ code: number; stderr: string }> export async function removeWorktree(worktreePath: string): Promise { await fs.rm(worktreePath, { recursive: true, force: true }) - const rootLookup = await Bun.spawn({ + const rootLookup = bunSpawn({ cmd: ["git", "-C", worktreePath, "rev-parse", "--show-superproject-working-tree"], stdout: "pipe", stderr: "pipe", diff --git a/src/features/team-mode/team-worktree/manager.ts b/src/features/team-mode/team-worktree/manager.ts index 359df4cd0..0f01bed71 100644 --- a/src/features/team-mode/team-worktree/manager.ts +++ b/src/features/team-mode/team-worktree/manager.ts @@ -1,4 +1,5 @@ import path from "node:path" +import { spawn as bunSpawn } from "../../../shared/bun-spawn-shim" export type TeamModeConfig = { worktreeBaseDir?: string @@ -16,7 +17,7 @@ function countParentSegments(spec: string): number { } async function runGit(args: string[], cwd?: string): Promise<{ code: number; stderr: string }> { - const process = Bun.spawn({ cmd: ["git", ...args], cwd, stdout: "pipe", stderr: "pipe" }) + const process = bunSpawn({ cmd: ["git", ...args], cwd, stdout: "pipe", stderr: "pipe" }) const [exitCode, stderrBytes] = await Promise.all([process.exited, new Response(process.stderr).text()]) return { code: exitCode, stderr: stderrBytes } } diff --git a/src/features/tmux-subagent/pane-state-querier.ts b/src/features/tmux-subagent/pane-state-querier.ts index e2ac9bfd1..0d01b03c0 100644 --- a/src/features/tmux-subagent/pane-state-querier.ts +++ b/src/features/tmux-subagent/pane-state-querier.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun" +import { spawn } from "../../shared/bun-spawn-shim" import type { WindowState, TmuxPaneInfo } from "./types" import { parsePaneStateOutput } from "./pane-state-parser" import { getTmuxPath } from "../../tools/interactive-bash/tmux-path-resolver" diff --git a/src/hooks/comment-checker/cli.ts b/src/hooks/comment-checker/cli.ts index e0ca21475..14a128d49 100644 --- a/src/hooks/comment-checker/cli.ts +++ b/src/hooks/comment-checker/cli.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun" +import { spawn } from "../../shared/bun-spawn-shim" import { createRequire } from "module" import { dirname, join } from "path" import { existsSync } from "fs" diff --git a/src/hooks/session-notification-sender.ts b/src/hooks/session-notification-sender.ts index 4d33bed77..8849e1af8 100644 --- a/src/hooks/session-notification-sender.ts +++ b/src/hooks/session-notification-sender.ts @@ -33,6 +33,21 @@ export function getDefaultSoundPath(platform: Platform): string { } } +type ShellCommand = Promise & { + quiet?: () => Promise + nothrow?: () => ShellCommand +} + +async function runQuietNothrow(command: ShellCommand): Promise { + const safeCommand = typeof command.nothrow === "function" ? command.nothrow() : command + if (typeof safeCommand.quiet === "function") { + await safeCommand.quiet() + return + } + + await safeCommand +} + export async function sendSessionNotification( ctx: PluginInput, platform: Platform, @@ -72,14 +87,14 @@ export async function sendSessionNotification( const escapedTitle = escapeAppleScriptText(title) const escapedMessage = escapeAppleScriptText(message) - await ctx.$`${osascriptPath} -e ${"display notification \"" + escapedMessage + "\" with title \"" + escapedTitle + "\""}`.nothrow().quiet() + await runQuietNothrow(ctx.$`${osascriptPath} -e ${"display notification \"" + escapedMessage + "\" with title \"" + escapedTitle + "\""}`) break } case "linux": { const notifySendPath = await getNotifySendPath() if (!notifySendPath) return - await ctx.$`${notifySendPath} ${title} ${message} 2>/dev/null`.nothrow().quiet() + await runQuietNothrow(ctx.$`${notifySendPath} ${title} ${message} 2>/dev/null`) break } case "win32": { @@ -87,7 +102,7 @@ export async function sendSessionNotification( if (!powershellPath) return const toastScript = buildWindowsToastScript(title, message) - await ctx.$`${powershellPath} -Command ${toastScript}`.nothrow().quiet() + await runQuietNothrow(ctx.$`${powershellPath} -Command ${toastScript}`) break } } @@ -102,17 +117,17 @@ export async function playSessionNotificationSound( case "darwin": { const afplayPath = await getAfplayPath() if (!afplayPath) return - ctx.$`${afplayPath} ${soundPath}`.nothrow().quiet() + await runQuietNothrow(ctx.$`${afplayPath} ${soundPath}`) break } case "linux": { const paplayPath = await getPaplayPath() if (paplayPath) { - ctx.$`${paplayPath} ${soundPath} 2>/dev/null`.nothrow().quiet() + await runQuietNothrow(ctx.$`${paplayPath} ${soundPath} 2>/dev/null`) } else { const aplayPath = await getAplayPath() if (aplayPath) { - ctx.$`${aplayPath} ${soundPath} 2>/dev/null`.nothrow().quiet() + await runQuietNothrow(ctx.$`${aplayPath} ${soundPath} 2>/dev/null`) } } break @@ -121,7 +136,7 @@ export async function playSessionNotificationSound( const powershellPath = await getPowershellPath() if (!powershellPath) return const escaped = escapePowerShellSingleQuotedText(soundPath) - ctx.$`${powershellPath} -Command ${"(New-Object Media.SoundPlayer '" + escaped + "').PlaySync()"}`.nothrow().quiet() + await runQuietNothrow(ctx.$`${powershellPath} -Command ${"(New-Object Media.SoundPlayer '" + escaped + "').PlaySync()"}`) break } } diff --git a/src/openclaw/dispatcher.ts b/src/openclaw/dispatcher.ts index 5971f371d..97643958f 100644 --- a/src/openclaw/dispatcher.ts +++ b/src/openclaw/dispatcher.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun" +import { spawn } from "../shared/bun-spawn-shim" import { validateGatewayUrl } from "./gateway-url-validation" import type { OpenClawGateway, WakeResult } from "./types" diff --git a/src/openclaw/reply-listener-process.ts b/src/openclaw/reply-listener-process.ts index f6309f168..305601edd 100644 --- a/src/openclaw/reply-listener-process.ts +++ b/src/openclaw/reply-listener-process.ts @@ -1,5 +1,5 @@ import { readFileSync } from "fs" -import { spawn } from "bun" +import { spawn } from "../shared/bun-spawn-shim" export const REPLY_LISTENER_DAEMON_IDENTITY_MARKER = "--openclaw-reply-listener-daemon" diff --git a/src/openclaw/reply-listener-spawn.ts b/src/openclaw/reply-listener-spawn.ts index 1cd0a1818..9d6b6cfbb 100644 --- a/src/openclaw/reply-listener-spawn.ts +++ b/src/openclaw/reply-listener-spawn.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun" +import { spawn } from "../shared/bun-spawn-shim" import { createReplyListenerDaemonEnv, REPLY_LISTENER_DAEMON_IDENTITY_MARKER, diff --git a/src/openclaw/tmux.ts b/src/openclaw/tmux.ts index 9bdb6212a..47b04c45a 100644 --- a/src/openclaw/tmux.ts +++ b/src/openclaw/tmux.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun" +import { spawn } from "../shared/bun-spawn-shim" export function getCurrentTmuxSession(): string | null { const env = process.env.TMUX diff --git a/src/shared/binary-downloader.ts b/src/shared/binary-downloader.ts index bb6918c30..16a8ff60b 100644 --- a/src/shared/binary-downloader.ts +++ b/src/shared/binary-downloader.ts @@ -1,6 +1,6 @@ import { chmodSync, existsSync, mkdirSync, unlinkSync } from "node:fs"; import * as path from "node:path"; -import { spawn } from "bun"; +import { spawn } from "./bun-spawn-shim"; import { validateArchiveEntries, type ArchiveEntry } from "./archive-entry-validator"; import { extractZip } from "./zip-extractor"; diff --git a/src/shared/bun-spawn-shim.test.ts b/src/shared/bun-spawn-shim.test.ts new file mode 100644 index 000000000..238cfb1b1 --- /dev/null +++ b/src/shared/bun-spawn-shim.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, test } from "bun:test" + +import { spawn, spawnSync } from "./bun-spawn-shim" + +describe("bun-spawn-shim", () => { + test("#given array command #when spawn exits successfully #then exited resolves to zero", async () => { + const proc = spawn(["bun", "--version"], { stdout: "pipe", stderr: "pipe" }) + + const exitCode = await proc.exited + + expect(exitCode).toBe(0) + expect(proc.exitCode).toBe(0) + }) + + test("#given piped stdout #when spawn writes output #then stdout is readable", async () => { + const proc = spawn(["bun", "--print", "'shim-ok'"], { stdout: "pipe", stderr: "pipe" }) + + const [exitCode, stdout] = await Promise.all([ + proc.exited, + new Response(proc.stdout).text(), + ]) + + expect(exitCode).toBe(0) + expect(stdout.trim()).toBe("shim-ok") + }) + + test("#given detached object command #when spawn starts #then process exposes daemon controls", async () => { + const proc = spawn({ + cmd: ["bun", "--print", "'detached-ok'"], + stdout: "pipe", + stderr: "pipe", + detached: true, + }) + + proc.unref() + const exitCode = await proc.exited + + expect(exitCode).toBe(0) + expect(typeof proc.ref).toBe("function") + expect(typeof proc.unref).toBe("function") + expect(proc.pid).toBeGreaterThan(0) + }) + + test("#given stdio tuple #when spawn runs #then ignored streams are still safe to read", async () => { + const proc = spawn({ + cmd: ["bun", "--print", "'ignored'"], + stdio: ["ignore", "ignore", "ignore"], + }) + + const exitCode = await proc.exited + const stdout = await new Response(proc.stdout).text() + + expect(exitCode).toBe(0) + expect(stdout).toBe("") + }) + + test("#given spawnSync command #when it writes output #then stdout and exit code match", () => { + const result = spawnSync(["bun", "--print", "'sync-ok'"], { stdout: "pipe", stderr: "pipe" }) + + expect(result.exitCode).toBe(0) + expect(result.success).toBe(true) + expect(result.stdout).toBeDefined() + expect(Buffer.from(result.stdout!).toString().trim()).toBe("sync-ok") + }) + + test("#given spawnSync command #when it completes #then result.pid is a positive number", () => { + const result = spawnSync(["bun", "--version"], { stdout: "pipe", stderr: "pipe" }) + + expect(result.pid).toBeGreaterThan(0) + }) + + test("#given default stdio #when child reads stdin #then it does not hang waiting for input", async () => { + const proc = spawn(["cat"], { stdout: "pipe", stderr: "pipe" }) + + const exitCode = await proc.exited + + expect(exitCode).toBe(0) + }) + + test("#given missing executable #when spawn invoked #then the error is surfaced to the caller", async () => { + let observedError: unknown + try { + const proc = spawn(["__omo-shim-missing-binary__"], { stdout: "pipe", stderr: "pipe" }) + await proc.exited + } catch (error) { + observedError = error + } + + expect(observedError).toBeDefined() + }) +}) diff --git a/src/shared/bun-spawn-shim.ts b/src/shared/bun-spawn-shim.ts new file mode 100644 index 000000000..de756793d --- /dev/null +++ b/src/shared/bun-spawn-shim.ts @@ -0,0 +1,166 @@ +import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "node:child_process" +import { Readable, Writable } from "node:stream" + +type AnyRecord = Record +type StdioMode = "pipe" | "inherit" | "ignore" +type StdioTuple = [StdioMode, StdioMode, StdioMode] + +export interface SpawnOptions { + cmd?: string[] + cwd?: string + env?: NodeJS.ProcessEnv + stdin?: StdioMode + stdout?: StdioMode + stderr?: StdioMode + stdio?: StdioTuple + detached?: boolean +} + +export interface SpawnedProcess { + readonly exitCode: number | null + readonly exited: Promise + readonly stdout: ReadableStream + readonly stderr: ReadableStream + readonly stdin: NodeJS.WritableStream + readonly pid: number | undefined + kill(signal?: NodeJS.Signals): void + ref(): void + unref(): void +} + +export interface SpawnSyncResult { + readonly exitCode: number + readonly stdout: Buffer | undefined + readonly stderr: Buffer | undefined + readonly success: boolean + readonly pid: number +} + +type BunSpawnRuntime = { + spawn(command: string[], options?: SpawnOptions): SpawnedProcess + spawn(options: SpawnOptions & { cmd: string[] }): SpawnedProcess + spawnSync(command: string[], options?: SpawnOptions): SpawnSyncResult + spawnSync(options: SpawnOptions & { cmd: string[] }): SpawnSyncResult +} + +const runtime = globalThis as typeof globalThis & { Bun?: BunSpawnRuntime } +const IS_BUN = typeof runtime.Bun !== "undefined" + +function emptyReadableStream(): ReadableStream { + return new ReadableStream({ + start(controller) { + controller.close() + }, + }) +} + +function toReadableStream(stream: NodeJS.ReadableStream | null): ReadableStream { + if (!stream) return emptyReadableStream() + + return Readable.toWeb(stream as Readable) as ReadableStream +} + +function emptyWritableStream(): Writable { + return new Writable({ + write(_chunk, _encoding, callback) { + callback() + }, + }) +} + +function resolveCommand(cmdOrOpts: unknown, optsArg?: unknown): { cmd: string[]; opts: SpawnOptions } { + const isObj = !Array.isArray(cmdOrOpts) + const opts = isObj ? (cmdOrOpts as SpawnOptions) : ((optsArg ?? {}) as SpawnOptions) + + return { + cmd: isObj ? ((cmdOrOpts as AnyRecord).cmd as string[]) : (cmdOrOpts as string[]), + opts, + } +} + +function resolveStdio(options: SpawnOptions): StdioTuple { + if (options.stdio) return options.stdio + + return [options.stdin ?? "ignore", options.stdout ?? "pipe", options.stderr ?? "inherit"] +} + +function wrapNodeProcess(proc: ReturnType): SpawnedProcess { + let exitCode: number | null = null + const exited = new Promise((resolve, reject) => { + proc.on("exit", (code) => { + exitCode = code ?? 1 + resolve(exitCode) + }) + proc.on("error", (error) => { + if (exitCode === null) { + exitCode = 1 + reject(error) + } + }) + }) + + return { + get exitCode() { + return exitCode + }, + exited, + stdout: toReadableStream(proc.stdout), + stderr: toReadableStream(proc.stderr), + stdin: proc.stdin ?? emptyWritableStream(), + kill(signal?: NodeJS.Signals) { + if (proc.killed || exitCode !== null) return + + try { + proc.kill(signal) + } catch (error) { + if (!String(error).includes("kill")) throw error + } + }, + pid: proc.pid, + ref() { + proc.ref() + }, + unref() { + proc.unref() + }, + } +} + +export function spawn(command: string[], options?: SpawnOptions): SpawnedProcess +export function spawn(options: SpawnOptions & { cmd: string[] }): SpawnedProcess +export function spawn(cmdOrOpts: unknown, opts?: unknown): SpawnedProcess { + if (IS_BUN) return runtime.Bun!.spawn(cmdOrOpts as string[] & SpawnOptions & { cmd: string[] }, opts as SpawnOptions) + + const { cmd, opts: options } = resolveCommand(cmdOrOpts, opts) + const [bin, ...args] = cmd + const proc = nodeSpawn(bin, args, { + cwd: options.cwd, + env: options.env, + stdio: resolveStdio(options), + detached: options.detached, + }) + + return wrapNodeProcess(proc) +} + +export function spawnSync(command: string[], options?: SpawnOptions): SpawnSyncResult +export function spawnSync(options: SpawnOptions & { cmd: string[] }): SpawnSyncResult +export function spawnSync(cmdOrOpts: unknown, opts?: unknown): SpawnSyncResult { + if (IS_BUN) return runtime.Bun!.spawnSync(cmdOrOpts as string[] & SpawnOptions & { cmd: string[] }, opts as SpawnOptions) + + const { cmd, opts: options } = resolveCommand(cmdOrOpts, opts) + const [bin, ...args] = cmd + const result = nodeSpawnSync(bin, args, { + cwd: options.cwd, + env: options.env, + stdio: resolveStdio(options), + }) + + return { + exitCode: result.status ?? 1, + stdout: result.stdout ?? undefined, + stderr: result.stderr ?? undefined, + success: (result.status ?? 1) === 0, + pid: result.pid ?? -1, + } +} diff --git a/src/shared/dist-bundle-bun-globals.test.ts b/src/shared/dist-bundle-bun-globals.test.ts new file mode 100644 index 000000000..4d8b7a1d8 --- /dev/null +++ b/src/shared/dist-bundle-bun-globals.test.ts @@ -0,0 +1,65 @@ +import { existsSync } from "node:fs" +import { describe, expect, test } from "bun:test" + +const DIST_INDEX = "dist/index.js" +const GLOBAL_BUN_DESTRUCTURE = /^\s*(?:var|let|const)\s*\{[^}]*\}\s*=\s*globalThis\.Bun/gm +const TOP_LEVEL_REQUIRE_CALL = "__require(" + +describe("dist bundle Bun globals", () => { + test.skipIf(!existsSync(DIST_INDEX))("#given dist bundle #when scanned #then no globalThis.Bun destructures remain", async () => { + const dist = await Bun.file(DIST_INDEX).text() + + const matches = dist.match(GLOBAL_BUN_DESTRUCTURE) ?? [] + + expect(matches).toEqual([]) + }) + + test.skipIf(!existsSync(DIST_INDEX))("#given dist bundle #when scanned #then no top-level __require call remains", async () => { + const dist = await Bun.file(DIST_INDEX).text() + const offending: string[] = [] + let depth = 0 + + for (const [index, line] of dist.split("\n").entries()) { + if (depth === 0 && line.includes(TOP_LEVEL_REQUIRE_CALL)) { + offending.push(`${index + 1}: ${line.trim()}`) + } + + for (const char of line) { + if (char === "{") { + depth += 1 + } else if (char === "}") { + depth -= 1 + if (depth < 0) depth = 0 + } + } + } + + expect(offending).toEqual([]) + }) + + test.skipIf(!existsSync(DIST_INDEX))("#given dist bundle #when imported under node --input-type=module #then it loads without error", async () => { + const node = Bun.which("node") + if (!node) return + + const proc = Bun.spawn({ + cmd: [node, "--input-type=module", "-e", "await import('./dist/index.js'); console.log('node-esm-load-ok')"], + cwd: process.cwd(), + stdout: "pipe", + stderr: "pipe", + }) + + const stdout = await new Response(proc.stdout).text() + const stderr = await new Response(proc.stderr).text() + const exitCode = await proc.exited + + expect({ + exitCode, + stdout: stdout.trim(), + stderr: stderr.trim(), + }).toEqual({ + exitCode: 0, + stdout: "node-esm-load-ok", + stderr: "", + }) + }) +}) diff --git a/src/shared/spawn-with-windows-hide.ts b/src/shared/spawn-with-windows-hide.ts index 7da9ed086..f6fec2a7e 100644 --- a/src/shared/spawn-with-windows-hide.ts +++ b/src/shared/spawn-with-windows-hide.ts @@ -1,4 +1,4 @@ -import { spawn as bunSpawn } from "bun" +import { spawn as bunSpawn } from "./bun-spawn-shim" import { spawn as nodeSpawn, type ChildProcess } from "node:child_process" import { Readable } from "node:stream" @@ -75,7 +75,7 @@ export function spawnWithWindowsHide(command: string[], options: SpawnOptions): const proc = nodeSpawn(cmd, args, { cwd: options.cwd, env: options.env, - stdio: [options.stdin ?? "pipe", options.stdout ?? "pipe", options.stderr ?? "pipe"], + stdio: [options.stdin ?? "ignore", options.stdout ?? "pipe", options.stderr ?? "inherit"], windowsHide: true, shell: true, }) diff --git a/src/shared/tmux/tmux-utils/layout.ts b/src/shared/tmux/tmux-utils/layout.ts index 5ac82ee58..259498899 100644 --- a/src/shared/tmux/tmux-utils/layout.ts +++ b/src/shared/tmux/tmux-utils/layout.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun" +import { spawn } from "../../bun-spawn-shim" import type { TmuxLayout } from "../../../config/schema" import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver" diff --git a/src/shared/tmux/tmux-utils/pane-dimensions.ts b/src/shared/tmux/tmux-utils/pane-dimensions.ts index a11ad2602..d16238e85 100644 --- a/src/shared/tmux/tmux-utils/pane-dimensions.ts +++ b/src/shared/tmux/tmux-utils/pane-dimensions.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun" +import { spawn } from "../../bun-spawn-shim" import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver" export interface PaneDimensions { diff --git a/src/shared/tmux/tmux-utils/pane-replace.ts b/src/shared/tmux/tmux-utils/pane-replace.ts index 271ad79eb..12ee33116 100644 --- a/src/shared/tmux/tmux-utils/pane-replace.ts +++ b/src/shared/tmux/tmux-utils/pane-replace.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun" +import { spawn } from "../../bun-spawn-shim" import type { TmuxConfig } from "../../../config/schema" import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver" import type { SpawnPaneResult } from "../types" diff --git a/src/shared/tmux/tmux-utils/pane-spawn.ts b/src/shared/tmux/tmux-utils/pane-spawn.ts index 2713eafbc..779da3031 100644 --- a/src/shared/tmux/tmux-utils/pane-spawn.ts +++ b/src/shared/tmux/tmux-utils/pane-spawn.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun" +import { spawn } from "../../bun-spawn-shim" import type { TmuxConfig } from "../../../config/schema" import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver" import type { SpawnPaneResult } from "../types" diff --git a/src/shared/tmux/tmux-utils/session-spawn.ts b/src/shared/tmux/tmux-utils/session-spawn.ts index a6fd15d2d..f5f3bbf7e 100644 --- a/src/shared/tmux/tmux-utils/session-spawn.ts +++ b/src/shared/tmux/tmux-utils/session-spawn.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun" +import { spawn } from "../../bun-spawn-shim" import type { TmuxConfig } from "../../../config/schema" import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver" import type { SpawnPaneResult } from "../types" diff --git a/src/shared/tmux/tmux-utils/spawn-process.ts b/src/shared/tmux/tmux-utils/spawn-process.ts index c75826cab..f1bb66d32 100644 --- a/src/shared/tmux/tmux-utils/spawn-process.ts +++ b/src/shared/tmux/tmux-utils/spawn-process.ts @@ -1 +1 @@ -export { spawn } from "bun" +export { spawn } from "../../bun-spawn-shim" diff --git a/src/shared/tmux/tmux-utils/window-spawn.ts b/src/shared/tmux/tmux-utils/window-spawn.ts index 45c0ee315..222dd2de9 100644 --- a/src/shared/tmux/tmux-utils/window-spawn.ts +++ b/src/shared/tmux/tmux-utils/window-spawn.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun" +import { spawn } from "../../bun-spawn-shim" import type { TmuxConfig } from "../../../config/schema" import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver" import type { SpawnPaneResult } from "../types" diff --git a/src/shared/zip-entry-listing/powershell-zip-entry-listing.ts b/src/shared/zip-entry-listing/powershell-zip-entry-listing.ts index 9169f510b..a77213899 100644 --- a/src/shared/zip-entry-listing/powershell-zip-entry-listing.ts +++ b/src/shared/zip-entry-listing/powershell-zip-entry-listing.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun" +import { spawn } from "../bun-spawn-shim" import type { ArchiveEntry } from "../archive-entry-validator" diff --git a/src/shared/zip-entry-listing/python-zip-entry-listing.ts b/src/shared/zip-entry-listing/python-zip-entry-listing.ts index 8c94442aa..4cdd71610 100644 --- a/src/shared/zip-entry-listing/python-zip-entry-listing.ts +++ b/src/shared/zip-entry-listing/python-zip-entry-listing.ts @@ -1,4 +1,4 @@ -import { spawn, spawnSync } from "bun" +import { spawn, spawnSync } from "../bun-spawn-shim" import type { ArchiveEntry } from "../archive-entry-validator" diff --git a/src/shared/zip-entry-listing/read-zip-symlink-target.ts b/src/shared/zip-entry-listing/read-zip-symlink-target.ts index 59eb6098c..2b6b9ab8d 100644 --- a/src/shared/zip-entry-listing/read-zip-symlink-target.ts +++ b/src/shared/zip-entry-listing/read-zip-symlink-target.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun" +import { spawn } from "../bun-spawn-shim" export async function readZipSymlinkTarget( archivePath: string, diff --git a/src/shared/zip-entry-listing/tar-zip-entry-listing.ts b/src/shared/zip-entry-listing/tar-zip-entry-listing.ts index 10b231905..f346b6552 100644 --- a/src/shared/zip-entry-listing/tar-zip-entry-listing.ts +++ b/src/shared/zip-entry-listing/tar-zip-entry-listing.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun" +import { spawn } from "../bun-spawn-shim" import type { ArchiveEntry } from "../archive-entry-validator" import { log } from "../logger" diff --git a/src/shared/zip-entry-listing/zipinfo-zip-entry-listing.ts b/src/shared/zip-entry-listing/zipinfo-zip-entry-listing.ts index 2fd638525..926e8b5da 100644 --- a/src/shared/zip-entry-listing/zipinfo-zip-entry-listing.ts +++ b/src/shared/zip-entry-listing/zipinfo-zip-entry-listing.ts @@ -1,4 +1,4 @@ -import { spawn, spawnSync } from "bun" +import { spawn, spawnSync } from "../bun-spawn-shim" import type { ArchiveEntry } from "../archive-entry-validator" import { readZipSymlinkTarget } from "./read-zip-symlink-target" diff --git a/src/shared/zip-extractor.ts b/src/shared/zip-extractor.ts index 77ac26b3d..cdc61fecc 100644 --- a/src/shared/zip-extractor.ts +++ b/src/shared/zip-extractor.ts @@ -1,4 +1,4 @@ -import { spawn, spawnSync } from "bun" +import { spawn, spawnSync } from "./bun-spawn-shim" import { release } from "os" import { validateArchiveEntries } from "./archive-entry-validator" diff --git a/src/tools/ast-grep/cli.ts b/src/tools/ast-grep/cli.ts index 86dc211ee..2d76975f7 100644 --- a/src/tools/ast-grep/cli.ts +++ b/src/tools/ast-grep/cli.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun" +import { spawn } from "../../shared/bun-spawn-shim" import { existsSync } from "fs" import { getSgCliPath, diff --git a/src/tools/glob/cli.ts b/src/tools/glob/cli.ts index 996133383..9ba34c32a 100644 --- a/src/tools/glob/cli.ts +++ b/src/tools/glob/cli.ts @@ -1,5 +1,5 @@ import { resolve } from "node:path" -import { spawn } from "bun" +import { spawn } from "../../shared/bun-spawn-shim" import { resolveGrepCli, type GrepBackend, diff --git a/src/tools/grep/cli.ts b/src/tools/grep/cli.ts index 9f55b1d27..4b9684c66 100644 --- a/src/tools/grep/cli.ts +++ b/src/tools/grep/cli.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun" +import { spawn } from "../../shared/bun-spawn-shim" import { resolveGrepCli, type ResolvedCli, diff --git a/src/tools/hashline-edit/formatter-trigger.ts b/src/tools/hashline-edit/formatter-trigger.ts index 370015844..72a755e63 100644 --- a/src/tools/hashline-edit/formatter-trigger.ts +++ b/src/tools/hashline-edit/formatter-trigger.ts @@ -1,5 +1,6 @@ import path from "path" import { log } from "../../shared" +import { spawn as bunSpawn } from "../../shared/bun-spawn-shim" interface FormatterConfig { disabled?: boolean @@ -106,7 +107,7 @@ export async function runFormattersForFile( const cmd = buildFormatterCommand(formatter.command, filePath) try { log("[formatter-trigger] Running formatter", { command: cmd, file: filePath }) - const proc = Bun.spawn(cmd, { + const proc = bunSpawn(cmd, { cwd: directory, env: { ...process.env, ...formatter.environment }, stdout: "ignore", diff --git a/src/tools/interactive-bash/tmux-path-resolver.ts b/src/tools/interactive-bash/tmux-path-resolver.ts index 1aa346235..1187fdef0 100644 --- a/src/tools/interactive-bash/tmux-path-resolver.ts +++ b/src/tools/interactive-bash/tmux-path-resolver.ts @@ -1,4 +1,4 @@ -import { spawn } from "bun" +import { spawn } from "../../shared/bun-spawn-shim" let tmuxPath: string | null = null let initPromise: Promise | null = null diff --git a/src/tools/lsp/lsp-process.ts b/src/tools/lsp/lsp-process.ts index 3f7b769a2..91d940b94 100644 --- a/src/tools/lsp/lsp-process.ts +++ b/src/tools/lsp/lsp-process.ts @@ -1,4 +1,4 @@ -import { spawn as bunSpawn } from "bun" +import { spawn as bunSpawn } from "../../shared/bun-spawn-shim" import { spawn as nodeSpawn, type ChildProcess } from "node:child_process" import { existsSync, statSync } from "fs" import { log } from "../../shared/logger"