4394f34225
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.
59 lines
1.8 KiB
TypeScript
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
|
|
}
|