Files
oh-my-opencode/src/shared/bun-which-shim.ts
T
YeonGyu-Kim 4394f34225 feat(shim): add bun-file/hash/which shims with Node fallbacks
The plugin builds with `bun build --target bun` and runs under OpenCode
CLI (Bun SEA) but also under OpenCode Desktop (Electron / Node V8) where
`globalThis.Bun` does not exist. Mirror the existing `bun-spawn-shim.ts`
pattern for three more Bun runtime APIs:

- bun-file-shim: bunFile()/bunWrite() backed by node:fs/promises with
  ArrayBuffer slicing to avoid Node Buffer pool exposure
- bun-hash-shim: pure-JS XXH32, bit-exact with Bun.hash.xxHash32 verified
  by 1200-pair fuzz comparison so existing hashline LINE#ID tags remain
  stable across runtimes
- bun-which-shim: synchronous PATH walker with Windows .exe/.cmd/.bat/.com
  extensions plus isUnsafeCommandName guard that rejects path separators,
  parent traversal, drive letters and null bytes before any probe

Each shim uses the canonical `runtime.Bun !== undefined` detection and
delegates to native Bun under IS_BUN, otherwise uses Node primitives.
Each ships with a co-located test that exercises both branches via the
`node:vm.runInNewContext` pattern from bun-hash-shim.test.ts.
2026-05-12 12:46:50 +09:00

59 lines
1.8 KiB
TypeScript

import { accessSync, constants } from "node:fs"
import { delimiter, join } from "node:path"
type BunWhichRuntime = { which(commandName: string): string | null }
const runtime = globalThis as typeof globalThis & { Bun?: BunWhichRuntime }
const IS_BUN = typeof runtime.Bun !== "undefined"
function isUnsafeCommandName(commandName: string): boolean {
if (commandName.includes("/") || commandName.includes("\\")) return true
if (commandName === "." || commandName === ".." || commandName.includes("..")) return true
if (/^[a-zA-Z]:/.test(commandName)) return true
if (commandName.includes("\0")) return true
return false
}
function isExecutable(filePath: string): boolean {
try {
accessSync(filePath, constants.X_OK)
return true
} catch {
return false
}
}
function resolvePathValue(): string | undefined {
if (process.platform === "win32") return process.env.Path ?? process.env.PATH
return process.env.PATH
}
function getWindowsCandidates(commandName: string): string[] {
if (process.platform !== "win32") return [commandName]
return [commandName, `${commandName}.exe`, `${commandName}.cmd`, `${commandName}.bat`, `${commandName}.com`]
}
export function bunWhich(commandName: string): string | null {
if (!commandName) return null
if (isUnsafeCommandName(commandName)) return null
if (IS_BUN) return runtime.Bun?.which(commandName) ?? null
const pathValue = resolvePathValue()
if (!pathValue) return null
const pathEntries = pathValue.split(delimiter).filter((pathEntry) => pathEntry.length > 0)
if (pathEntries.length === 0) return null
const candidateNames = getWindowsCandidates(commandName)
for (const pathEntry of pathEntries) {
for (const candidateName of candidateNames) {
const candidatePath = join(pathEntry, candidateName)
if (isExecutable(candidatePath)) return candidatePath
}
}
return null
}