import z from "zod" import { Global } from "../global" import { Log } from "../util/log" import path from "path" import { Filesystem } from "../util/filesystem" import { NamedError } from "@opencode-ai/util/error" import { readableStreamToText } from "bun" import { Lock } from "../util/lock" import { PackageRegistry } from "./registry" import { proxied } from "@/util/proxied" export namespace BunProc { const log = Log.create({ service: "bun" }) export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject) { log.info("running", { cmd: [which(), ...cmd], ...options, }) const result = Bun.spawn([which(), ...cmd], { ...options, stdout: "pipe", stderr: "pipe", env: { ...process.env, ...options?.env, BUN_BE_BUN: "1", }, }) const code = await result.exited const stdout = result.stdout ? typeof result.stdout === "number" ? result.stdout : await readableStreamToText(result.stdout) : undefined const stderr = result.stderr ? typeof result.stderr === "number" ? result.stderr : await readableStreamToText(result.stderr) : undefined log.info("done", { code, stdout, stderr, }) if (code !== 0) { throw new Error(`Command failed with exit code ${result.exitCode}`) } return result } export function which() { return process.execPath } export const InstallFailedError = NamedError.create( "BunInstallFailedError", z.object({ pkg: z.string(), version: z.string(), }), ) export async function install(pkg: string, version = "latest") { // Use lock to ensure only one install at a time using _ = await Lock.write("bun-install") const mod = path.join(Global.Path.cache, "node_modules", pkg) const pkgjsonPath = path.join(Global.Path.cache, "package.json") const parsed = await Filesystem.readJson<{ dependencies: Record }>(pkgjsonPath).catch(async () => { const result = { dependencies: {} as Record } await Filesystem.writeJson(pkgjsonPath, result) return result }) if (!parsed.dependencies) parsed.dependencies = {} as Record const dependencies = parsed.dependencies const modExists = await Filesystem.exists(mod) const cachedVersion = dependencies[pkg] if (!modExists || !cachedVersion) { // continue to install } else if (version !== "latest" && cachedVersion === version) { return mod } else if (version === "latest") { const isOutdated = await PackageRegistry.isOutdated(pkg, cachedVersion, Global.Path.cache) if (!isOutdated) return mod log.info("Cached version is outdated, proceeding with install", { pkg, cachedVersion }) } // Build command arguments const args = [ "add", "--force", "--exact", // TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936) ...(proxied() ? ["--no-cache"] : []), "--cwd", Global.Path.cache, pkg + "@" + version, ] // Let Bun handle registry resolution: // - If .npmrc files exist, Bun will use them automatically // - If no .npmrc files exist, Bun will default to https://registry.npmjs.org // - No need to pass --registry flag log.info("installing package using Bun's default registry resolution", { pkg, version, }) await BunProc.run(args, { cwd: Global.Path.cache, }).catch((e) => { throw new InstallFailedError( { pkg, version }, { cause: e, }, ) }) // Resolve actual version from installed package when using "latest" // This ensures subsequent starts use the cached version until explicitly updated let resolvedVersion = version if (version === "latest") { const installedPkg = await Filesystem.readJson<{ version?: string }>(path.join(mod, "package.json")).catch( () => null, ) if (installedPkg?.version) { resolvedVersion = installedPkg.version } } parsed.dependencies[pkg] = resolvedVersion await Filesystem.writeJson(pkgjsonPath, parsed) return mod } }