Files
oh-my-opencode/src/shared/bun-spawn-shim.ts
T
2026-05-06 14:30:29 +09:00

169 lines
4.8 KiB
TypeScript

import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "node:child_process"
import { Readable, Writable } from "node:stream"
type AnyRecord = Record<string, unknown>
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
signal?: AbortSignal
}
export interface SpawnedProcess {
readonly exitCode: number | null
readonly exited: Promise<number>
readonly stdout: ReadableStream<Uint8Array>
readonly stderr: ReadableStream<Uint8Array>
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<Uint8Array> {
return new ReadableStream<Uint8Array>({
start(controller) {
controller.close()
},
})
}
function toReadableStream(stream: NodeJS.ReadableStream | null): ReadableStream<Uint8Array> {
if (!stream) return emptyReadableStream()
return Readable.toWeb(stream as Readable) as ReadableStream<Uint8Array>
}
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<typeof nodeSpawn>): SpawnedProcess {
let exitCode: number | null = null
const exited = new Promise<number>((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,
signal: options.signal,
})
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,
}
}