2
0
Эх сурвалжийг харах

chore: revert changes overlapping with #18308

Dax Raad 4 долоо хоног өмнө
parent
commit
0293a8bb80

+ 6 - 15
packages/opencode/package.json

@@ -8,7 +8,7 @@
   "scripts": {
     "prepare": "effect-language-service patch || true",
     "typecheck": "tsgo --noEmit",
-    "test": "bun test --timeout 30000 registry",
+    "test": "bun test --timeout 30000",
     "build": "bun run script/build.ts",
     "dev": "bun run --conditions=browser ./src/index.ts",
     "random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'",
@@ -26,15 +26,9 @@
   "exports": {
     "./*": "./src/*.ts"
   },
-  "imports": {
-    "#db": {
-      "bun": "./src/storage/db.bun.ts",
-      "node": "./src/storage/db.node.ts",
-      "default": "./src/storage/db.bun.ts"
-    }
-  },
   "devDependencies": {
     "@babel/core": "7.28.4",
+    "@effect/language-service": "0.79.0",
     "@octokit/webhooks-types": "7.6.1",
     "@opencode-ai/script": "workspace:*",
     "@parcel/watcher-darwin-arm64": "2.5.1",
@@ -51,14 +45,13 @@
     "@types/bun": "catalog:",
     "@types/cross-spawn": "6.0.6",
     "@types/mime-types": "3.0.1",
-    "@types/npmcli__arborist": "6.3.3",
     "@types/semver": "^7.5.8",
     "@types/turndown": "5.0.5",
     "@types/which": "3.0.4",
     "@types/yargs": "17.0.33",
     "@typescript/native-preview": "catalog:",
-    "effect": "catalog:",
-    "drizzle-kit": "catalog:",
+    "drizzle-kit": "1.0.0-beta.16-ea816b6",
+    "drizzle-orm": "1.0.0-beta.16-ea816b6",
     "typescript": "catalog:",
     "vscode-languageserver-types": "3.17.5",
     "why-is-node-running": "3.2.2",
@@ -91,12 +84,9 @@
     "@clack/prompts": "1.0.0-alpha.1",
     "@gitlab/gitlab-ai-provider": "3.6.0",
     "@gitlab/opencode-gitlab-auth": "1.3.3",
-    "@hono/node-server": "1.19.11",
-    "@hono/node-ws": "1.3.0",
     "@hono/standard-validator": "0.1.5",
     "@hono/zod-validator": "catalog:",
     "@modelcontextprotocol/sdk": "1.25.2",
-    "@npmcli/arborist": "9.4.0",
     "@octokit/graphql": "9.0.2",
     "@octokit/rest": "catalog:",
     "@openauthjs/openauth": "catalog:",
@@ -123,7 +113,8 @@
     "cross-spawn": "^7.0.6",
     "decimal.js": "10.5.0",
     "diff": "catalog:",
-    "drizzle-orm": "catalog:",
+    "drizzle-orm": "1.0.0-beta.16-ea816b6",
+    "effect": "catalog:",
     "fuzzysort": "3.1.0",
     "glob": "13.0.5",
     "google-auth-library": "10.5.0",

+ 127 - 0
packages/opencode/src/bun/index.ts

@@ -0,0 +1,127 @@
+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 { Lock } from "../util/lock"
+import { PackageRegistry } from "./registry"
+import { proxied } from "@/util/proxied"
+import { Process } from "../util/process"
+
+export namespace BunProc {
+  const log = Log.create({ service: "bun" })
+
+  export async function run(cmd: string[], options?: Process.RunOptions) {
+    const full = [which(), ...cmd]
+    log.info("running", {
+      cmd: full,
+      ...options,
+    })
+    const result = await Process.run(full, {
+      cwd: options?.cwd,
+      abort: options?.abort,
+      kill: options?.kill,
+      timeout: options?.timeout,
+      nothrow: options?.nothrow,
+      env: {
+        ...process.env,
+        ...options?.env,
+        BUN_BE_BUN: "1",
+      },
+    })
+    log.info("done", {
+      code: result.code,
+      stdout: result.stdout.toString(),
+      stderr: result.stderr.toString(),
+    })
+    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<string, string> }>(pkgjsonPath).catch(async () => {
+      const result = { dependencies: {} as Record<string, string> }
+      await Filesystem.writeJson(pkgjsonPath, result)
+      return result
+    })
+    if (!parsed.dependencies) parsed.dependencies = {} as Record<string, string>
+    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() || process.env.CI ? ["--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
+  }
+}

+ 44 - 0
packages/opencode/src/bun/registry.ts

@@ -0,0 +1,44 @@
+import semver from "semver"
+import { Log } from "../util/log"
+import { Process } from "../util/process"
+
+export namespace PackageRegistry {
+  const log = Log.create({ service: "bun" })
+
+  function which() {
+    return process.execPath
+  }
+
+  export async function info(pkg: string, field: string, cwd?: string): Promise<string | null> {
+    const { code, stdout, stderr } = await Process.run([which(), "info", pkg, field], {
+      cwd,
+      env: {
+        ...process.env,
+        BUN_BE_BUN: "1",
+      },
+      nothrow: true,
+    })
+
+    if (code !== 0) {
+      log.warn("bun info failed", { pkg, field, code, stderr: stderr.toString() })
+      return null
+    }
+
+    const value = stdout.toString().trim()
+    if (!value) return null
+    return value
+  }
+
+  export async function isOutdated(pkg: string, cachedVersion: string, cwd?: string): Promise<boolean> {
+    const latestVersion = await info(pkg, "version", cwd)
+    if (!latestVersion) {
+      log.warn("Failed to resolve latest version, using cached", { pkg, cachedVersion })
+      return false
+    }
+
+    const isRange = /[\s^~*xX<>|=]/.test(cachedVersion)
+    if (isRange) return !semver.satisfies(latestVersion, cachedVersion)
+
+    return semver.lt(cachedVersion, latestVersion)
+  }
+}

+ 77 - 13
packages/opencode/src/config/config.ts

@@ -1,6 +1,6 @@
 import { Log } from "../util/log"
 import path from "path"
-import { pathToFileURL } from "url"
+import { pathToFileURL, fileURLToPath } from "url"
 import { createRequire } from "module"
 import os from "os"
 import z from "zod"
@@ -22,6 +22,7 @@ import {
 } from "jsonc-parser"
 import { Instance } from "../project/instance"
 import { LSPServer } from "../lsp/server"
+import { BunProc } from "@/bun"
 import { Installation } from "@/installation"
 import { ConfigMarkdown } from "./markdown"
 import { constants, existsSync } from "fs"
@@ -29,11 +30,14 @@ import { Bus } from "@/bus"
 import { GlobalBus } from "@/bus/global"
 import { Event } from "../server/event"
 import { Glob } from "../util/glob"
+import { PackageRegistry } from "@/bun/registry"
+import { proxied } from "@/util/proxied"
 import { iife } from "@/util/iife"
 import { Account } from "@/account"
 import { ConfigPaths } from "./paths"
 import { Filesystem } from "@/util/filesystem"
-import { Npm } from "@/npm"
+import { Process } from "@/util/process"
+import { Lock } from "@/util/lock"
 
 export namespace Config {
   const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" })
@@ -150,7 +154,8 @@ export namespace Config {
 
       deps.push(
         iife(async () => {
-          await installDependencies(dir)
+          const shouldInstall = await needsInstall(dir)
+          if (shouldInstall) await installDependencies(dir)
         }),
       )
 
@@ -266,10 +271,6 @@ export namespace Config {
   }
 
   export async function installDependencies(dir: string) {
-    if (!(await isWritable(dir))) {
-      log.info("config dir is not writable, skipping dependency install", { dir })
-      return
-    }
     const pkg = path.join(dir, "package.json")
     const targetVersion = Installation.isLocal() ? "*" : Installation.VERSION
 
@@ -283,15 +284,43 @@ export namespace Config {
     await Filesystem.writeJson(pkg, json)
 
     const gitignore = path.join(dir, ".gitignore")
-    if (!(await Filesystem.exists(gitignore)))
-      await Filesystem.write(
-        gitignore,
-        ["node_modules", "plans", "package.json", "bun.lock", ".gitignore", "package-lock.json"].join("\n"),
-      )
+    const hasGitIgnore = await Filesystem.exists(gitignore)
+    if (!hasGitIgnore)
+      await Filesystem.write(gitignore, ["node_modules", "package.json", "bun.lock", ".gitignore"].join("\n"))
 
     // Install any additional dependencies defined in the package.json
     // This allows local plugins and custom tools to use external packages
-    await Npm.install(dir)
+    using _ = await Lock.write("bun-install")
+    await BunProc.run(
+      [
+        "install",
+        // TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936)
+        ...(proxied() || process.env.CI ? ["--no-cache"] : []),
+      ],
+      { cwd: dir },
+    ).catch((err) => {
+      if (err instanceof Process.RunFailedError) {
+        const detail = {
+          dir,
+          cmd: err.cmd,
+          code: err.code,
+          stdout: err.stdout.toString(),
+          stderr: err.stderr.toString(),
+        }
+        if (Flag.OPENCODE_STRICT_CONFIG_DEPS) {
+          log.error("failed to install dependencies", detail)
+          throw err
+        }
+        log.warn("failed to install dependencies", detail)
+        return
+      }
+
+      if (Flag.OPENCODE_STRICT_CONFIG_DEPS) {
+        log.error("failed to install dependencies", { dir, error: err })
+        throw err
+      }
+      log.warn("failed to install dependencies", { dir, error: err })
+    })
   }
 
   async function isWritable(dir: string) {
@@ -303,6 +332,41 @@ export namespace Config {
     }
   }
 
+  export async function needsInstall(dir: string) {
+    // Some config dirs may be read-only.
+    // Installing deps there will fail; skip installation in that case.
+    const writable = await isWritable(dir)
+    if (!writable) {
+      log.debug("config dir is not writable, skipping dependency install", { dir })
+      return false
+    }
+
+    const nodeModules = path.join(dir, "node_modules")
+    if (!existsSync(nodeModules)) return true
+
+    const pkg = path.join(dir, "package.json")
+    const pkgExists = await Filesystem.exists(pkg)
+    if (!pkgExists) return true
+
+    const parsed = await Filesystem.readJson<{ dependencies?: Record<string, string> }>(pkg).catch(() => null)
+    const dependencies = parsed?.dependencies ?? {}
+    const depVersion = dependencies["@opencode-ai/plugin"]
+    if (!depVersion) return true
+
+    const targetVersion = Installation.isLocal() ? "latest" : Installation.VERSION
+    if (targetVersion === "latest") {
+      const isOutdated = await PackageRegistry.isOutdated("@opencode-ai/plugin", depVersion, dir)
+      if (!isOutdated) return false
+      log.info("Cached version is outdated, proceeding with install", {
+        pkg: "@opencode-ai/plugin",
+        cachedVersion: depVersion,
+      })
+      return true
+    }
+    if (depVersion === targetVersion) return false
+    return true
+  }
+
   function rel(item: string, patterns: string[]) {
     const normalizedItem = item.replaceAll("\\", "/")
     for (const pattern of patterns) {

+ 65 - 83
packages/opencode/src/format/formatter.ts

@@ -1,40 +1,40 @@
 import { text } from "node:stream/consumers"
+import { BunProc } from "../bun"
 import { Instance } from "../project/instance"
 import { Filesystem } from "../util/filesystem"
 import { Process } from "../util/process"
 import { which } from "../util/which"
 import { Flag } from "@/flag/flag"
-import { Npm } from "@/npm"
 
 export interface Info {
   name: string
+  command: string[]
   environment?: Record<string, string>
   extensions: string[]
-  enabled(): Promise<string[] | false>
+  enabled(): Promise<boolean>
 }
 
 export const gofmt: Info = {
   name: "gofmt",
+  command: ["gofmt", "-w", "$FILE"],
   extensions: [".go"],
   async enabled() {
-    const p = which("gofmt")
-    if (p === null) return false
-    return [p, "-w", "$FILE"]
+    return which("gofmt") !== null
   },
 }
 
 export const mix: Info = {
   name: "mix",
+  command: ["mix", "format", "$FILE"],
   extensions: [".ex", ".exs", ".eex", ".heex", ".leex", ".neex", ".sface"],
   async enabled() {
-    const p = which("mix")
-    if (p === null) return false
-    return [p, "format", "$FILE"]
+    return which("mix") !== null
   },
 }
 
 export const prettier: Info = {
   name: "prettier",
+  command: [BunProc.which(), "x", "prettier", "--write", "$FILE"],
   environment: {
     BUN_BE_BUN: "1",
   },
@@ -73,9 +73,8 @@ export const prettier: Info = {
         dependencies?: Record<string, string>
         devDependencies?: Record<string, string>
       }>(item)
-      if (json.dependencies?.prettier || json.devDependencies?.prettier) {
-        return [await Npm.which("prettier"), "--write", "$FILE"]
-      }
+      if (json.dependencies?.prettier) return true
+      if (json.devDependencies?.prettier) return true
     }
     return false
   },
@@ -83,6 +82,7 @@ export const prettier: Info = {
 
 export const oxfmt: Info = {
   name: "oxfmt",
+  command: [BunProc.which(), "x", "oxfmt", "$FILE"],
   environment: {
     BUN_BE_BUN: "1",
   },
@@ -95,9 +95,8 @@ export const oxfmt: Info = {
         dependencies?: Record<string, string>
         devDependencies?: Record<string, string>
       }>(item)
-      if (json.dependencies?.oxfmt || json.devDependencies?.oxfmt) {
-        return [await Npm.which("oxfmt"), "$FILE"]
-      }
+      if (json.dependencies?.oxfmt) return true
+      if (json.devDependencies?.oxfmt) return true
     }
     return false
   },
@@ -105,6 +104,7 @@ export const oxfmt: Info = {
 
 export const biome: Info = {
   name: "biome",
+  command: [BunProc.which(), "x", "@biomejs/biome", "check", "--write", "$FILE"],
   environment: {
     BUN_BE_BUN: "1",
   },
@@ -141,7 +141,7 @@ export const biome: Info = {
     for (const config of configs) {
       const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree)
       if (found.length > 0) {
-        return [await Npm.which("@biomejs/biome"), "check", "--write", "$FILE"]
+        return true
       }
     }
     return false
@@ -150,49 +150,47 @@ export const biome: Info = {
 
 export const zig: Info = {
   name: "zig",
+  command: ["zig", "fmt", "$FILE"],
   extensions: [".zig", ".zon"],
   async enabled() {
-    const p = which("zig")
-    if (p === null) return false
-    return [p, "fmt", "$FILE"]
+    return which("zig") !== null
   },
 }
 
 export const clang: Info = {
   name: "clang-format",
+  command: ["clang-format", "-i", "$FILE"],
   extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"],
   async enabled() {
     const items = await Filesystem.findUp(".clang-format", Instance.directory, Instance.worktree)
-    if (items.length === 0) return false
-    return ["clang-format", "-i", "$FILE"]
+    return items.length > 0
   },
 }
 
 export const ktlint: Info = {
   name: "ktlint",
+  command: ["ktlint", "-F", "$FILE"],
   extensions: [".kt", ".kts"],
   async enabled() {
-    const p = which("ktlint")
-    if (p === null) return false
-    return [p, "-F", "$FILE"]
+    return which("ktlint") !== null
   },
 }
 
 export const ruff: Info = {
   name: "ruff",
+  command: ["ruff", "format", "$FILE"],
   extensions: [".py", ".pyi"],
   async enabled() {
-    const p = which("ruff")
-    if (p === null) return false
+    if (!which("ruff")) return false
     const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"]
     for (const config of configs) {
       const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree)
       if (found.length > 0) {
         if (config === "pyproject.toml") {
           const content = await Filesystem.readText(found[0])
-          if (content.includes("[tool.ruff]")) return [p, "format", "$FILE"]
+          if (content.includes("[tool.ruff]")) return true
         } else {
-          return [p, "format", "$FILE"]
+          return true
         }
       }
     }
@@ -201,7 +199,7 @@ export const ruff: Info = {
       const found = await Filesystem.findUp(dep, Instance.directory, Instance.worktree)
       if (found.length > 0) {
         const content = await Filesystem.readText(found[0])
-        if (content.includes("ruff")) return [p, "format", "$FILE"]
+        if (content.includes("ruff")) return true
       }
     }
     return false
@@ -210,13 +208,14 @@ export const ruff: Info = {
 
 export const rlang: Info = {
   name: "air",
+  command: ["air", "format", "$FILE"],
   extensions: [".R"],
   async enabled() {
     const airPath = which("air")
     if (airPath == null) return false
 
     try {
-      const proc = Process.spawn([airPath, "--help"], {
+      const proc = Process.spawn(["air", "--help"], {
         stdout: "pipe",
         stderr: "pipe",
       })
@@ -228,10 +227,7 @@ export const rlang: Info = {
       const firstLine = output.split("\n")[0]
       const hasR = firstLine.includes("R language")
       const hasFormatter = firstLine.includes("formatter")
-      if (hasR && hasFormatter) {
-        return [airPath, "format", "$FILE"]
-      }
-      return false
+      return hasR && hasFormatter
     } catch (error) {
       return false
     }
@@ -240,14 +236,14 @@ export const rlang: Info = {
 
 export const uvformat: Info = {
   name: "uv",
+  command: ["uv", "format", "--", "$FILE"],
   extensions: [".py", ".pyi"],
   async enabled() {
     if (await ruff.enabled()) return false
-    const uvPath = which("uv")
-    if (uvPath !== null) {
-      const proc = Process.spawn([uvPath, "format", "--help"], { stderr: "pipe", stdout: "pipe" })
+    if (which("uv") !== null) {
+      const proc = Process.spawn(["uv", "format", "--help"], { stderr: "pipe", stdout: "pipe" })
       const code = await proc.exited
-      if (code === 0) return [uvPath, "format", "--", "$FILE"]
+      return code === 0
     }
     return false
   },
@@ -255,118 +251,108 @@ export const uvformat: Info = {
 
 export const rubocop: Info = {
   name: "rubocop",
+  command: ["rubocop", "--autocorrect", "$FILE"],
   extensions: [".rb", ".rake", ".gemspec", ".ru"],
   async enabled() {
-    const path = which("rubocop")
-    if (path === null) return false
-    return [path, "--autocorrect", "$FILE"]
+    return which("rubocop") !== null
   },
 }
 
 export const standardrb: Info = {
   name: "standardrb",
+  command: ["standardrb", "--fix", "$FILE"],
   extensions: [".rb", ".rake", ".gemspec", ".ru"],
   async enabled() {
-    const path = which("standardrb")
-    if (path === null) return false
-    return [path, "--fix", "$FILE"]
+    return which("standardrb") !== null
   },
 }
 
 export const htmlbeautifier: Info = {
   name: "htmlbeautifier",
+  command: ["htmlbeautifier", "$FILE"],
   extensions: [".erb", ".html.erb"],
   async enabled() {
-    const path = which("htmlbeautifier")
-    if (path === null) return false
-    return [path, "$FILE"]
+    return which("htmlbeautifier") !== null
   },
 }
 
 export const dart: Info = {
   name: "dart",
+  command: ["dart", "format", "$FILE"],
   extensions: [".dart"],
   async enabled() {
-    const path = which("dart")
-    if (path === null) return false
-    return [path, "format", "$FILE"]
+    return which("dart") !== null
   },
 }
 
 export const ocamlformat: Info = {
   name: "ocamlformat",
+  command: ["ocamlformat", "-i", "$FILE"],
   extensions: [".ml", ".mli"],
   async enabled() {
-    const path = which("ocamlformat")
-    if (!path) return false
+    if (!which("ocamlformat")) return false
     const items = await Filesystem.findUp(".ocamlformat", Instance.directory, Instance.worktree)
-    if (items.length === 0) return false
-    return [path, "-i", "$FILE"]
+    return items.length > 0
   },
 }
 
 export const terraform: Info = {
   name: "terraform",
+  command: ["terraform", "fmt", "$FILE"],
   extensions: [".tf", ".tfvars"],
   async enabled() {
-    const path = which("terraform")
-    if (path === null) return false
-    return [path, "fmt", "$FILE"]
+    return which("terraform") !== null
   },
 }
 
 export const latexindent: Info = {
   name: "latexindent",
+  command: ["latexindent", "-w", "-s", "$FILE"],
   extensions: [".tex"],
   async enabled() {
-    const path = which("latexindent")
-    if (path === null) return false
-    return [path, "-w", "-s", "$FILE"]
+    return which("latexindent") !== null
   },
 }
 
 export const gleam: Info = {
   name: "gleam",
+  command: ["gleam", "format", "$FILE"],
   extensions: [".gleam"],
   async enabled() {
-    const path = which("gleam")
-    if (path === null) return false
-    return [path, "format", "$FILE"]
+    return which("gleam") !== null
   },
 }
 
 export const shfmt: Info = {
   name: "shfmt",
+  command: ["shfmt", "-w", "$FILE"],
   extensions: [".sh", ".bash"],
   async enabled() {
-    const path = which("shfmt")
-    if (path === null) return false
-    return [path, "-w", "$FILE"]
+    return which("shfmt") !== null
   },
 }
 
 export const nixfmt: Info = {
   name: "nixfmt",
+  command: ["nixfmt", "$FILE"],
   extensions: [".nix"],
   async enabled() {
-    const path = which("nixfmt")
-    if (path === null) return false
-    return [path, "$FILE"]
+    return which("nixfmt") !== null
   },
 }
 
 export const rustfmt: Info = {
   name: "rustfmt",
+  command: ["rustfmt", "$FILE"],
   extensions: [".rs"],
   async enabled() {
-    const path = which("rustfmt")
-    if (path === null) return false
-    return [path, "$FILE"]
+    return which("rustfmt") !== null
   },
 }
 
 export const pint: Info = {
   name: "pint",
+  command: ["./vendor/bin/pint", "$FILE"],
   extensions: [".php"],
   async enabled() {
     const items = await Filesystem.findUp("composer.json", Instance.directory, Instance.worktree)
@@ -375,9 +361,8 @@ export const pint: Info = {
         require?: Record<string, string>
         "require-dev"?: Record<string, string>
       }>(item)
-      if (json.require?.["laravel/pint"] || json["require-dev"]?.["laravel/pint"]) {
-        return ["./vendor/bin/pint", "$FILE"]
-      }
+      if (json.require?.["laravel/pint"]) return true
+      if (json["require-dev"]?.["laravel/pint"]) return true
     }
     return false
   },
@@ -385,30 +370,27 @@ export const pint: Info = {
 
 export const ormolu: Info = {
   name: "ormolu",
+  command: ["ormolu", "-i", "$FILE"],
   extensions: [".hs"],
   async enabled() {
-    const path = which("ormolu")
-    if (path === null) return false
-    return [path, "-i", "$FILE"]
+    return which("ormolu") !== null
   },
 }
 
 export const cljfmt: Info = {
   name: "cljfmt",
+  command: ["cljfmt", "fix", "--quiet", "$FILE"],
   extensions: [".clj", ".cljs", ".cljc", ".edn"],
   async enabled() {
-    const path = which("cljfmt")
-    if (path === null) return false
-    return [path, "fix", "--quiet", "$FILE"]
+    return which("cljfmt") !== null
   },
 }
 
 export const dfmt: Info = {
   name: "dfmt",
+  command: ["dfmt", "-i", "$FILE"],
   extensions: [".d"],
   async enabled() {
-    const path = which("dfmt")
-    if (path === null) return false
-    return [path, "-i", "$FILE"]
+    return which("dfmt") !== null
   },
 }

+ 6 - 15
packages/opencode/src/format/index.ts

@@ -37,7 +37,7 @@ export namespace Format {
     Effect.gen(function* () {
       const instance = yield* InstanceContext
 
-      const enabled: Record<string, string[] | false> = {}
+      const enabled: Record<string, boolean> = {}
       const formatters: Record<string, Formatter.Info> = {}
 
       const cfg = yield* Effect.promise(() => Config.get())
@@ -62,7 +62,7 @@ export namespace Format {
           formatters[name] = {
             ...info,
             name,
-            enabled: async () => info.command,
+            enabled: async () => true,
           }
         }
       } else {
@@ -79,22 +79,13 @@ export namespace Format {
       }
 
       async function getFormatter(ext: string) {
-        const result: Array<{
-          name: string
-          command: string[]
-          environment?: Record<string, string>
-        }> = []
+        const result = []
         for (const item of Object.values(formatters)) {
           log.info("checking", { name: item.name, ext })
           if (!item.extensions.includes(ext)) continue
-          const cmd = await isEnabled(item)
-          if (!cmd) continue
+          if (!(await isEnabled(item))) continue
           log.info("enabled", { name: item.name, ext })
-          result.push({
-            name: item.name,
-            command: cmd,
-            environment: item.environment,
-          })
+          result.push(item)
         }
         return result
       }
@@ -150,7 +141,7 @@ export namespace Format {
           result.push({
             name: formatter.name,
             extensions: formatter.extensions,
-            enabled: !!isOn,
+            enabled: isOn,
           })
         }
         return result

+ 173 - 30
packages/opencode/src/lsp/server.ts

@@ -3,6 +3,7 @@ import path from "path"
 import os from "os"
 import { Global } from "../global"
 import { Log } from "../util/log"
+import { BunProc } from "../bun"
 import { text } from "node:stream/consumers"
 import fs from "fs/promises"
 import { Filesystem } from "../util/filesystem"
@@ -13,7 +14,6 @@ import { Process } from "../util/process"
 import { which } from "../util/which"
 import { Module } from "@opencode-ai/util/module"
 import { spawn } from "./launch"
-import { Npm } from "@/npm"
 
 export namespace LSPServer {
   const log = Log.create({ service: "lsp.server" })
@@ -103,7 +103,7 @@ export namespace LSPServer {
       const tsserver = Module.resolve("typescript/lib/tsserver.js", Instance.directory)
       log.info("typescript server", { tsserver })
       if (!tsserver) return
-      const proc = spawn(await Npm.which("typescript-language-server"), ["--stdio"], {
+      const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
         cwd: root,
         env: {
           ...process.env,
@@ -129,8 +129,29 @@ export namespace LSPServer {
       let binary = which("vue-language-server")
       const args: string[] = []
       if (!binary) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        binary = await Npm.which("@vue/language-server")
+        const js = path.join(
+          Global.Path.bin,
+          "node_modules",
+          "@vue",
+          "language-server",
+          "bin",
+          "vue-language-server.js",
+        )
+        if (!(await Filesystem.exists(js))) {
+          if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+          await Process.spawn([BunProc.which(), "install", "@vue/language-server"], {
+            cwd: Global.Path.bin,
+            env: {
+              ...process.env,
+              BUN_BE_BUN: "1",
+            },
+            stdout: "pipe",
+            stderr: "pipe",
+            stdin: "pipe",
+          }).exited
+        }
+        binary = BunProc.which()
+        args.push("run", js)
       }
       args.push("--stdio")
       const proc = spawn(binary, args, {
@@ -193,7 +214,7 @@ export namespace LSPServer {
         log.info("installed VS Code ESLint server", { serverPath })
       }
 
-      const proc = spawn(await Npm.which("tsx"), [serverPath, "--stdio"], {
+      const proc = spawn(BunProc.which(), [serverPath, "--stdio"], {
         cwd: root,
         env: {
           ...process.env,
@@ -324,8 +345,8 @@ export namespace LSPServer {
       if (!bin) {
         const resolved = Module.resolve("biome", root)
         if (!resolved) return
-        bin = await Npm.which("biome")
-        args = ["lsp-proxy", "--stdio"]
+        bin = BunProc.which()
+        args = ["x", "biome", "lsp-proxy", "--stdio"]
       }
 
       const proc = spawn(bin, args, {
@@ -351,7 +372,9 @@ export namespace LSPServer {
     },
     extensions: [".go"],
     async spawn(root) {
-      let bin = which("gopls")
+      let bin = which("gopls", {
+        PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
+      })
       if (!bin) {
         if (!which("go")) return
         if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
@@ -386,7 +409,9 @@ export namespace LSPServer {
     root: NearestRoot(["Gemfile"]),
     extensions: [".rb", ".rake", ".gemspec", ".ru"],
     async spawn(root) {
-      let bin = which("rubocop")
+      let bin = which("rubocop", {
+        PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
+      })
       if (!bin) {
         const ruby = which("ruby")
         const gem = which("gem")
@@ -491,8 +516,19 @@ export namespace LSPServer {
       let binary = which("pyright-langserver")
       const args = []
       if (!binary) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        binary = await Npm.which("pyright")
+        const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js")
+        if (!(await Filesystem.exists(js))) {
+          if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+          await Process.spawn([BunProc.which(), "install", "pyright"], {
+            cwd: Global.Path.bin,
+            env: {
+              ...process.env,
+              BUN_BE_BUN: "1",
+            },
+          }).exited
+        }
+        binary = BunProc.which()
+        args.push(...["run", js])
       }
       args.push("--stdio")
 
@@ -594,7 +630,9 @@ export namespace LSPServer {
     extensions: [".zig", ".zon"],
     root: NearestRoot(["build.zig"]),
     async spawn(root) {
-      let bin = which("zls")
+      let bin = which("zls", {
+        PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
+      })
 
       if (!bin) {
         const zig = which("zig")
@@ -704,7 +742,9 @@ export namespace LSPServer {
     root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]),
     extensions: [".cs"],
     async spawn(root) {
-      let bin = which("csharp-ls")
+      let bin = which("csharp-ls", {
+        PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
+      })
       if (!bin) {
         if (!which("dotnet")) {
           log.error(".NET SDK is required to install csharp-ls")
@@ -741,7 +781,9 @@ export namespace LSPServer {
     root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]),
     extensions: [".fs", ".fsi", ".fsx", ".fsscript"],
     async spawn(root) {
-      let bin = which("fsautocomplete")
+      let bin = which("fsautocomplete", {
+        PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
+      })
       if (!bin) {
         if (!which("dotnet")) {
           log.error(".NET SDK is required to install fsautocomplete")
@@ -1007,8 +1049,22 @@ export namespace LSPServer {
       let binary = which("svelteserver")
       const args: string[] = []
       if (!binary) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        binary = await Npm.which("svelte-language-server")
+        const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js")
+        if (!(await Filesystem.exists(js))) {
+          if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+          await Process.spawn([BunProc.which(), "install", "svelte-language-server"], {
+            cwd: Global.Path.bin,
+            env: {
+              ...process.env,
+              BUN_BE_BUN: "1",
+            },
+            stdout: "pipe",
+            stderr: "pipe",
+            stdin: "pipe",
+          }).exited
+        }
+        binary = BunProc.which()
+        args.push("run", js)
       }
       args.push("--stdio")
       const proc = spawn(binary, args, {
@@ -1040,8 +1096,22 @@ export namespace LSPServer {
       let binary = which("astro-ls")
       const args: string[] = []
       if (!binary) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        binary = await Npm.which("@astrojs/language-server")
+        const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js")
+        if (!(await Filesystem.exists(js))) {
+          if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+          await Process.spawn([BunProc.which(), "install", "@astrojs/language-server"], {
+            cwd: Global.Path.bin,
+            env: {
+              ...process.env,
+              BUN_BE_BUN: "1",
+            },
+            stdout: "pipe",
+            stderr: "pipe",
+            stdin: "pipe",
+          }).exited
+        }
+        binary = BunProc.which()
+        args.push("run", js)
       }
       args.push("--stdio")
       const proc = spawn(binary, args, {
@@ -1290,8 +1360,31 @@ export namespace LSPServer {
       let binary = which("yaml-language-server")
       const args: string[] = []
       if (!binary) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        binary = await Npm.which("yaml-language-server")
+        const js = path.join(
+          Global.Path.bin,
+          "node_modules",
+          "yaml-language-server",
+          "out",
+          "server",
+          "src",
+          "server.js",
+        )
+        const exists = await Filesystem.exists(js)
+        if (!exists) {
+          if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+          await Process.spawn([BunProc.which(), "install", "yaml-language-server"], {
+            cwd: Global.Path.bin,
+            env: {
+              ...process.env,
+              BUN_BE_BUN: "1",
+            },
+            stdout: "pipe",
+            stderr: "pipe",
+            stdin: "pipe",
+          }).exited
+        }
+        binary = BunProc.which()
+        args.push("run", js)
       }
       args.push("--stdio")
       const proc = spawn(binary, args, {
@@ -1320,7 +1413,9 @@ export namespace LSPServer {
     ]),
     extensions: [".lua"],
     async spawn(root) {
-      let bin = which("lua-language-server")
+      let bin = which("lua-language-server", {
+        PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
+      })
 
       if (!bin) {
         if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
@@ -1456,8 +1551,22 @@ export namespace LSPServer {
       let binary = which("intelephense")
       const args: string[] = []
       if (!binary) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        binary = await Npm.which("intelephense")
+        const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js")
+        if (!(await Filesystem.exists(js))) {
+          if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+          await Process.spawn([BunProc.which(), "install", "intelephense"], {
+            cwd: Global.Path.bin,
+            env: {
+              ...process.env,
+              BUN_BE_BUN: "1",
+            },
+            stdout: "pipe",
+            stderr: "pipe",
+            stdin: "pipe",
+          }).exited
+        }
+        binary = BunProc.which()
+        args.push("run", js)
       }
       args.push("--stdio")
       const proc = spawn(binary, args, {
@@ -1539,8 +1648,22 @@ export namespace LSPServer {
       let binary = which("bash-language-server")
       const args: string[] = []
       if (!binary) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        binary = await Npm.which("bash-language-server")
+        const js = path.join(Global.Path.bin, "node_modules", "bash-language-server", "out", "cli.js")
+        if (!(await Filesystem.exists(js))) {
+          if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+          await Process.spawn([BunProc.which(), "install", "bash-language-server"], {
+            cwd: Global.Path.bin,
+            env: {
+              ...process.env,
+              BUN_BE_BUN: "1",
+            },
+            stdout: "pipe",
+            stderr: "pipe",
+            stdin: "pipe",
+          }).exited
+        }
+        binary = BunProc.which()
+        args.push("run", js)
       }
       args.push("start")
       const proc = spawn(binary, args, {
@@ -1561,7 +1684,9 @@ export namespace LSPServer {
     extensions: [".tf", ".tfvars"],
     root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"]),
     async spawn(root) {
-      let bin = which("terraform-ls")
+      let bin = which("terraform-ls", {
+        PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
+      })
 
       if (!bin) {
         if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
@@ -1642,7 +1767,9 @@ export namespace LSPServer {
     extensions: [".tex", ".bib"],
     root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
     async spawn(root) {
-      let bin = which("texlab")
+      let bin = which("texlab", {
+        PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
+      })
 
       if (!bin) {
         if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
@@ -1733,8 +1860,22 @@ export namespace LSPServer {
       let binary = which("docker-langserver")
       const args: string[] = []
       if (!binary) {
-        if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
-        binary = await Npm.which("dockerfile-language-server-nodejs")
+        const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js")
+        if (!(await Filesystem.exists(js))) {
+          if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+          await Process.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], {
+            cwd: Global.Path.bin,
+            env: {
+              ...process.env,
+              BUN_BE_BUN: "1",
+            },
+            stdout: "pipe",
+            stderr: "pipe",
+            stdin: "pipe",
+          }).exited
+        }
+        binary = BunProc.which()
+        args.push("run", js)
       }
       args.push("--stdio")
       const proc = spawn(binary, args, {
@@ -1825,7 +1966,9 @@ export namespace LSPServer {
     extensions: [".typ", ".typc"],
     root: NearestRoot(["typst.toml"]),
     async spawn(root) {
-      let bin = which("tinymist")
+      let bin = which("tinymist", {
+        PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
+      })
 
       if (!bin) {
         if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return

+ 0 - 160
packages/opencode/src/npm/index.ts

@@ -1,160 +0,0 @@
-// Workaround: Bun on Windows does not support the UV_FS_O_FILEMAP flag that
-// the `tar` package uses for files < 512KB (fs.open returns EINVAL).
-// tar silently swallows the error and skips writing files, leaving only empty
-// directories. Setting __FAKE_PLATFORM__ makes tar fall back to the plain 'w'
-// flag. See tar's get-write-flag.js.
-// Must be set before @npmcli/arborist is imported since tar caches the flag
-// at module evaluation time — so we use a dynamic import() below.
-if (process.platform === "win32") {
-  process.env.__FAKE_PLATFORM__ = "linux"
-}
-
-import semver from "semver"
-import z from "zod"
-import { NamedError } from "@opencode-ai/util/error"
-import { Global } from "../global"
-import { Lock } from "../util/lock"
-import { Log } from "../util/log"
-import path from "path"
-import { readdir } from "fs/promises"
-import { Filesystem } from "@/util/filesystem"
-
-const { Arborist } = await import("@npmcli/arborist")
-
-export namespace Npm {
-  const log = Log.create({ service: "npm" })
-
-  export const InstallFailedError = NamedError.create(
-    "NpmInstallFailedError",
-    z.object({
-      pkg: z.string(),
-    }),
-  )
-
-  function directory(pkg: string) {
-    return path.join(Global.Path.cache, "packages", pkg)
-  }
-
-  export async function outdated(pkg: string, cachedVersion: string): Promise<boolean> {
-    const response = await fetch(`https://registry.npmjs.org/${pkg}`)
-    if (!response.ok) {
-      log.warn("Failed to resolve latest version, using cached", { pkg, cachedVersion })
-      return false
-    }
-
-    const data = (await response.json()) as { "dist-tags"?: { latest?: string } }
-    const latestVersion = data?.["dist-tags"]?.latest
-    if (!latestVersion) {
-      log.warn("No latest version found, using cached", { pkg, cachedVersion })
-      return false
-    }
-
-    const isRange = /[\s^~*xX<>|=]/.test(cachedVersion)
-    if (isRange) return !semver.satisfies(latestVersion, cachedVersion)
-
-    return semver.lt(cachedVersion, latestVersion)
-  }
-
-  export async function add(pkg: string) {
-    using _ = await Lock.write("npm-install")
-    log.info("installing package", {
-      pkg,
-    })
-    const hash = pkg
-    const dir = directory(hash)
-
-    const arborist = new Arborist({
-      path: dir,
-      binLinks: true,
-      progress: false,
-      savePrefix: "",
-    })
-    const tree = await arborist.loadVirtual().catch(() => {})
-    if (tree) {
-      const first = tree.edgesOut.values().next().value?.to
-      if (first) {
-        log.info("package already installed", { pkg })
-        return first.path
-      }
-    }
-
-    const result = await arborist
-      .reify({
-        add: [pkg],
-        save: true,
-        saveType: "prod",
-      })
-      .catch((cause) => {
-        throw new InstallFailedError(
-          { pkg },
-          {
-            cause,
-          },
-        )
-      })
-
-    const first = result.edgesOut.values().next().value?.to
-    if (!first) throw new InstallFailedError({ pkg })
-    return first.path
-  }
-
-  export async function install(dir: string) {
-    log.info("checking dependencies", { dir })
-
-    const reify = async () => {
-      const arb = new Arborist({
-        path: dir,
-        binLinks: true,
-        progress: false,
-        savePrefix: "",
-      })
-      await arb.reify().catch(() => {})
-    }
-
-    if (!(await Filesystem.exists(path.join(dir, "node_modules")))) {
-      log.info("node_modules missing, reifying")
-      await reify()
-      return
-    }
-
-    const pkg = await Filesystem.readJson(path.join(dir, "package.json")).catch(() => ({}))
-    const lock = await Filesystem.readJson(path.join(dir, "package-lock.json")).catch(() => ({}))
-
-    const declared = new Set([
-      ...Object.keys(pkg.dependencies || {}),
-      ...Object.keys(pkg.devDependencies || {}),
-      ...Object.keys(pkg.peerDependencies || {}),
-      ...Object.keys(pkg.optionalDependencies || {}),
-    ])
-
-    const root = lock.packages?.[""] || {}
-    const locked = new Set([
-      ...Object.keys(root.dependencies || {}),
-      ...Object.keys(root.devDependencies || {}),
-      ...Object.keys(root.peerDependencies || {}),
-      ...Object.keys(root.optionalDependencies || {}),
-    ])
-
-    for (const name of declared) {
-      if (!locked.has(name)) {
-        log.info("dependency not in lock file, reifying", { name })
-        await reify()
-        return
-      }
-    }
-
-    log.info("dependencies in sync")
-  }
-
-  export async function which(pkg: string) {
-    const dir = path.join(directory(pkg), "node_modules", ".bin")
-    const files = await readdir(dir).catch(() => [])
-    if (!files.length) {
-      await add(pkg)
-      const retry = await readdir(dir).catch(() => [])
-      if (!retry.length) throw new Error(`No binary found for package "${pkg}" after install`)
-      return path.join(dir, retry[0])
-    }
-    return path.join(dir, files[0])
-  }
-}

+ 8 - 8
packages/opencode/src/plugin/index.ts

@@ -4,7 +4,7 @@ import { Bus } from "../bus"
 import { Log } from "../util/log"
 import { createOpencodeClient } from "@opencode-ai/sdk"
 import { Server } from "../server/server"
-import { Npm } from "../npm"
+import { BunProc } from "../bun"
 import { Instance } from "../project/instance"
 import { Flag } from "../flag/flag"
 import { CodexAuthPlugin } from "./codex"
@@ -30,9 +30,7 @@ export namespace Plugin {
         : undefined,
       fetch: async (...args) => Server.Default().fetch(...args),
     })
-    log.info("loading config")
     const config = await Config.get()
-    log.info("config loaded")
     const hooks: Hooks[] = []
     const input: PluginInput = {
       client,
@@ -42,8 +40,7 @@ export namespace Plugin {
       get serverUrl(): URL {
         return Server.url ?? new URL("http://localhost:4096")
       },
-      // @ts-expect-error
-      $: typeof Bun === "undefined" ? undefined : Bun.$,
+      $: Bun.$,
     }
 
     for (const plugin of INTERNAL_PLUGINS) {
@@ -62,13 +59,16 @@ export namespace Plugin {
       if (plugin.includes("opencode-openai-codex-auth") || plugin.includes("opencode-copilot-auth")) continue
       log.info("loading plugin", { path: plugin })
       if (!plugin.startsWith("file://")) {
-        plugin = await Npm.add(plugin).catch((err) => {
+        const lastAtIndex = plugin.lastIndexOf("@")
+        const pkg = lastAtIndex > 0 ? plugin.substring(0, lastAtIndex) : plugin
+        const version = lastAtIndex > 0 ? plugin.substring(lastAtIndex + 1) : "latest"
+        plugin = await BunProc.install(pkg, version).catch((err) => {
           const cause = err instanceof Error ? err.cause : err
           const detail = cause instanceof Error ? cause.message : String(cause ?? err)
-          log.error("failed to install plugin", { plugin, error: detail })
+          log.error("failed to install plugin", { pkg, version, error: detail })
           Bus.publish(Session.Event.Error, {
             error: new NamedError.Unknown({
-              message: `Failed to install plugin ${plugin}: ${detail}`,
+              message: `Failed to install plugin ${pkg}@${version}: ${detail}`,
             }).toObject(),
           })
           return ""

+ 2 - 2
packages/opencode/src/provider/provider.ts

@@ -5,7 +5,7 @@ import { Config } from "../config/config"
 import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda"
 import { NoSuchModelError, type Provider as SDK } from "ai"
 import { Log } from "../util/log"
-import { Npm } from "../npm"
+import { BunProc } from "../bun"
 import { Hash } from "../util/hash"
 import { Plugin } from "../plugin"
 import { NamedError } from "@opencode-ai/util/error"
@@ -1187,7 +1187,7 @@ export namespace Provider {
 
       let installedPath: string
       if (!model.api.npm.startsWith("file://")) {
-        installedPath = await Npm.add(model.api.npm)
+        installedPath = await BunProc.install(model.api.npm, "latest")
       } else {
         log.info("loading local provider", { pkg: model.api.npm })
         installedPath = model.api.npm

+ 53 - 0
packages/opencode/test/bun.test.ts

@@ -0,0 +1,53 @@
+import { describe, expect, test } from "bun:test"
+import fs from "fs/promises"
+import path from "path"
+
+describe("BunProc registry configuration", () => {
+  test("should not contain hardcoded registry parameters", async () => {
+    // Read the bun/index.ts file
+    const bunIndexPath = path.join(__dirname, "../src/bun/index.ts")
+    const content = await fs.readFile(bunIndexPath, "utf-8")
+
+    // Verify that no hardcoded registry is present
+    expect(content).not.toContain("--registry=")
+    expect(content).not.toContain("hasNpmRcConfig")
+    expect(content).not.toContain("NpmRc")
+  })
+
+  test("should use Bun's default registry resolution", async () => {
+    // Read the bun/index.ts file
+    const bunIndexPath = path.join(__dirname, "../src/bun/index.ts")
+    const content = await fs.readFile(bunIndexPath, "utf-8")
+
+    // Verify that it uses Bun's default resolution
+    expect(content).toContain("Bun's default registry resolution")
+    expect(content).toContain("Bun will use them automatically")
+    expect(content).toContain("No need to pass --registry flag")
+  })
+
+  test("should have correct command structure without registry", async () => {
+    // Read the bun/index.ts file
+    const bunIndexPath = path.join(__dirname, "../src/bun/index.ts")
+    const content = await fs.readFile(bunIndexPath, "utf-8")
+
+    // Extract the install function
+    const installFunctionMatch = content.match(/export async function install[\s\S]*?^  }/m)
+    expect(installFunctionMatch).toBeTruthy()
+
+    if (installFunctionMatch) {
+      const installFunction = installFunctionMatch[0]
+
+      // Verify expected arguments are present
+      expect(installFunction).toContain('"add"')
+      expect(installFunction).toContain('"--force"')
+      expect(installFunction).toContain('"--exact"')
+      expect(installFunction).toContain('"--cwd"')
+      expect(installFunction).toContain("Global.Path.cache")
+      expect(installFunction).toContain('pkg + "@" + version')
+
+      // Verify no registry argument is added
+      expect(installFunction).not.toContain('"--registry"')
+      expect(installFunction).not.toContain('args.push("--registry')
+    }
+  })
+})

+ 35 - 1
packages/opencode/test/config/config.test.ts

@@ -1,4 +1,4 @@
-import { test, expect, describe, mock, afterEach } from "bun:test"
+import { test, expect, describe, mock, afterEach, spyOn } from "bun:test"
 import { Config } from "../../src/config/config"
 import { Instance } from "../../src/project/instance"
 import { Auth } from "../../src/auth"
@@ -10,6 +10,7 @@ import { pathToFileURL } from "url"
 import { Global } from "../../src/global"
 import { ProjectID } from "../../src/project/schema"
 import { Filesystem } from "../../src/util/filesystem"
+import { BunProc } from "../../src/bun"
 
 // Get managed config directory from environment (set in preload.ts)
 const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR!
@@ -763,6 +764,39 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => {
   }
 })
 
+test("serializes concurrent config dependency installs", async () => {
+  await using tmp = await tmpdir()
+  const dirs = [path.join(tmp.path, "a"), path.join(tmp.path, "b")]
+  await Promise.all(dirs.map((dir) => fs.mkdir(dir, { recursive: true })))
+
+  const seen: string[] = []
+  let active = 0
+  let max = 0
+  const run = spyOn(BunProc, "run").mockImplementation(async (_cmd, opts) => {
+    active++
+    max = Math.max(max, active)
+    seen.push(opts?.cwd ?? "")
+    await new Promise((resolve) => setTimeout(resolve, 25))
+    active--
+    return {
+      code: 0,
+      stdout: Buffer.alloc(0),
+      stderr: Buffer.alloc(0),
+    }
+  })
+
+  try {
+    await Promise.all(dirs.map((dir) => Config.installDependencies(dir)))
+  } finally {
+    run.mockRestore()
+  }
+
+  expect(max).toBe(1)
+  expect(seen.toSorted()).toEqual(dirs.toSorted())
+  expect(await Filesystem.exists(path.join(dirs[0], "package.json"))).toBe(true)
+  expect(await Filesystem.exists(path.join(dirs[1], "package.json"))).toBe(true)
+})
+
 test("resolves scoped npm plugins in config", async () => {
   await using tmp = await tmpdir({
     init: async (dir) => {