فهرست منبع

refactor(opencode): replace Bun.which with npm which (#15012)

Dax 1 ماه پیش
والد
کامیت
74effa8eec

+ 14 - 2
bun.lock

@@ -373,6 +373,7 @@
         "ulid": "catalog:",
         "ulid": "catalog:",
         "vscode-jsonrpc": "8.2.1",
         "vscode-jsonrpc": "8.2.1",
         "web-tree-sitter": "0.25.10",
         "web-tree-sitter": "0.25.10",
+        "which": "6.0.1",
         "xdg-basedir": "5.1.0",
         "xdg-basedir": "5.1.0",
         "yargs": "18.0.0",
         "yargs": "18.0.0",
         "zod": "catalog:",
         "zod": "catalog:",
@@ -395,6 +396,7 @@
         "@types/bun": "catalog:",
         "@types/bun": "catalog:",
         "@types/mime-types": "3.0.1",
         "@types/mime-types": "3.0.1",
         "@types/turndown": "5.0.5",
         "@types/turndown": "5.0.5",
+        "@types/which": "3.0.4",
         "@types/yargs": "17.0.33",
         "@types/yargs": "17.0.33",
         "@typescript/native-preview": "catalog:",
         "@typescript/native-preview": "catalog:",
         "drizzle-kit": "1.0.0-beta.12-a5629fb",
         "drizzle-kit": "1.0.0-beta.12-a5629fb",
@@ -2120,6 +2122,8 @@
 
 
     "@types/whatwg-mimetype": ["@types/[email protected]", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
     "@types/whatwg-mimetype": ["@types/[email protected]", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
 
 
+    "@types/which": ["@types/[email protected]", "", {}, "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w=="],
+
     "@types/ws": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
     "@types/ws": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
 
 
     "@types/yargs": ["@types/[email protected]", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="],
     "@types/yargs": ["@types/[email protected]", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="],
@@ -3236,7 +3240,7 @@
 
 
     "isbinaryfile": ["[email protected]", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="],
     "isbinaryfile": ["[email protected]", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="],
 
 
-    "isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
+    "isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="],
 
 
     "isomorphic-ws": ["[email protected]", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="],
     "isomorphic-ws": ["[email protected]", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="],
 
 
@@ -4586,7 +4590,7 @@
 
 
     "when-exit": ["[email protected]", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="],
     "when-exit": ["[email protected]", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="],
 
 
-    "which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
+    "which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
 
 
     "which-boxed-primitive": ["[email protected]", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
     "which-boxed-primitive": ["[email protected]", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
 
 
@@ -5202,6 +5206,8 @@
 
 
     "app-builder-lib/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A=="],
     "app-builder-lib/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A=="],
 
 
+    "app-builder-lib/which": ["[email protected]", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
+
     "archiver-utils/glob": ["[email protected]", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
     "archiver-utils/glob": ["[email protected]", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
 
 
     "archiver-utils/is-stream": ["[email protected]", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
     "archiver-utils/is-stream": ["[email protected]", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
@@ -5388,6 +5394,8 @@
 
 
     "node-gyp/nopt": ["[email protected]", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="],
     "node-gyp/nopt": ["[email protected]", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="],
 
 
+    "node-gyp/which": ["[email protected]", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
+
     "npm-run-path/path-key": ["[email protected]", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
     "npm-run-path/path-key": ["[email protected]", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
 
 
     "nypm/citty": ["[email protected]", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="],
     "nypm/citty": ["[email protected]", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="],
@@ -5918,6 +5926,8 @@
 
 
     "app-builder-lib/@electron/get/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
     "app-builder-lib/@electron/get/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
 
 
+    "app-builder-lib/which/isexe": ["[email protected]", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
+
     "archiver-utils/glob/jackspeak": ["[email protected]", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
     "archiver-utils/glob/jackspeak": ["[email protected]", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
 
 
     "archiver-utils/glob/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
     "archiver-utils/glob/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
@@ -6000,6 +6010,8 @@
 
 
     "node-gyp/nopt/abbrev": ["[email protected]", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="],
     "node-gyp/nopt/abbrev": ["[email protected]", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="],
 
 
+    "node-gyp/which/isexe": ["[email protected]", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
+
     "opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
     "opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
 
 
     "opencode/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
     "opencode/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],

+ 2 - 0
packages/opencode/package.json

@@ -43,6 +43,7 @@
     "@types/mime-types": "3.0.1",
     "@types/mime-types": "3.0.1",
     "@types/turndown": "5.0.5",
     "@types/turndown": "5.0.5",
     "@types/yargs": "17.0.33",
     "@types/yargs": "17.0.33",
+    "@types/which": "3.0.4",
     "@typescript/native-preview": "catalog:",
     "@typescript/native-preview": "catalog:",
     "drizzle-kit": "1.0.0-beta.12-a5629fb",
     "drizzle-kit": "1.0.0-beta.12-a5629fb",
     "drizzle-orm": "1.0.0-beta.12-a5629fb",
     "drizzle-orm": "1.0.0-beta.12-a5629fb",
@@ -127,6 +128,7 @@
     "ulid": "catalog:",
     "ulid": "catalog:",
     "vscode-jsonrpc": "8.2.1",
     "vscode-jsonrpc": "8.2.1",
     "web-tree-sitter": "0.25.10",
     "web-tree-sitter": "0.25.10",
+    "which": "6.0.1",
     "xdg-basedir": "5.1.0",
     "xdg-basedir": "5.1.0",
     "yargs": "18.0.0",
     "yargs": "18.0.0",
     "zod": "catalog:",
     "zod": "catalog:",

+ 3 - 2
packages/opencode/src/cli/cmd/session.ts

@@ -9,6 +9,7 @@ import { Filesystem } from "../../util/filesystem"
 import { Process } from "../../util/process"
 import { Process } from "../../util/process"
 import { EOL } from "os"
 import { EOL } from "os"
 import path from "path"
 import path from "path"
+import { which } from "../../util/which"
 
 
 function pagerCmd(): string[] {
 function pagerCmd(): string[] {
   const lessOptions = ["-R", "-S"]
   const lessOptions = ["-R", "-S"]
@@ -17,7 +18,7 @@ function pagerCmd(): string[] {
   }
   }
 
 
   // user could have less installed via other options
   // user could have less installed via other options
-  const lessOnPath = Bun.which("less")
+  const lessOnPath = which("less")
   if (lessOnPath) {
   if (lessOnPath) {
     if (Filesystem.stat(lessOnPath)?.size) return [lessOnPath, ...lessOptions]
     if (Filesystem.stat(lessOnPath)?.size) return [lessOnPath, ...lessOptions]
   }
   }
@@ -27,7 +28,7 @@ function pagerCmd(): string[] {
     if (Filesystem.stat(less)?.size) return [less, ...lessOptions]
     if (Filesystem.stat(less)?.size) return [less, ...lessOptions]
   }
   }
 
 
-  const git = Bun.which("git")
+  const git = which("git")
   if (git) {
   if (git) {
     const less = path.join(git, "..", "..", "usr", "bin", "less.exe")
     const less = path.join(git, "..", "..", "usr", "bin", "less.exe")
     if (Filesystem.stat(less)?.size) return [less, ...lessOptions]
     if (Filesystem.stat(less)?.size) return [less, ...lessOptions]

+ 5 - 4
packages/opencode/src/cli/cmd/tui/util/clipboard.ts

@@ -6,6 +6,7 @@ import { tmpdir } from "os"
 import path from "path"
 import path from "path"
 import { Filesystem } from "../../../../util/filesystem"
 import { Filesystem } from "../../../../util/filesystem"
 import { Process } from "../../../../util/process"
 import { Process } from "../../../../util/process"
+import { which } from "../../../../util/which"
 
 
 /**
 /**
  * Writes text to clipboard via OSC 52 escape sequence.
  * Writes text to clipboard via OSC 52 escape sequence.
@@ -76,7 +77,7 @@ export namespace Clipboard {
   const getCopyMethod = lazy(() => {
   const getCopyMethod = lazy(() => {
     const os = platform()
     const os = platform()
 
 
-    if (os === "darwin" && Bun.which("osascript")) {
+    if (os === "darwin" && which("osascript")) {
       console.log("clipboard: using osascript")
       console.log("clipboard: using osascript")
       return async (text: string) => {
       return async (text: string) => {
         const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
         const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
@@ -85,7 +86,7 @@ export namespace Clipboard {
     }
     }
 
 
     if (os === "linux") {
     if (os === "linux") {
-      if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-copy")) {
+      if (process.env["WAYLAND_DISPLAY"] && which("wl-copy")) {
         console.log("clipboard: using wl-copy")
         console.log("clipboard: using wl-copy")
         return async (text: string) => {
         return async (text: string) => {
           const proc = Process.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
           const proc = Process.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
@@ -95,7 +96,7 @@ export namespace Clipboard {
           await proc.exited.catch(() => {})
           await proc.exited.catch(() => {})
         }
         }
       }
       }
-      if (Bun.which("xclip")) {
+      if (which("xclip")) {
         console.log("clipboard: using xclip")
         console.log("clipboard: using xclip")
         return async (text: string) => {
         return async (text: string) => {
           const proc = Process.spawn(["xclip", "-selection", "clipboard"], {
           const proc = Process.spawn(["xclip", "-selection", "clipboard"], {
@@ -109,7 +110,7 @@ export namespace Clipboard {
           await proc.exited.catch(() => {})
           await proc.exited.catch(() => {})
         }
         }
       }
       }
-      if (Bun.which("xsel")) {
+      if (which("xsel")) {
         console.log("clipboard: using xsel")
         console.log("clipboard: using xsel")
         return async (text: string) => {
         return async (text: string) => {
           const proc = Process.spawn(["xsel", "--clipboard", "--input"], {
           const proc = Process.spawn(["xsel", "--clipboard", "--input"], {

+ 2 - 1
packages/opencode/src/file/ripgrep.ts

@@ -8,6 +8,7 @@ import { lazy } from "../util/lazy"
 import { $ } from "bun"
 import { $ } from "bun"
 import { Filesystem } from "../util/filesystem"
 import { Filesystem } from "../util/filesystem"
 import { Process } from "../util/process"
 import { Process } from "../util/process"
+import { which } from "../util/which"
 import { text } from "node:stream/consumers"
 import { text } from "node:stream/consumers"
 
 
 import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
 import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
@@ -126,7 +127,7 @@ export namespace Ripgrep {
   )
   )
 
 
   const state = lazy(async () => {
   const state = lazy(async () => {
-    const system = Bun.which("rg")
+    const system = which("rg")
     if (system) {
     if (system) {
       const stat = await fs.stat(system).catch(() => undefined)
       const stat = await fs.stat(system).catch(() => undefined)
       if (stat?.isFile()) return { filepath: system }
       if (stat?.isFile()) return { filepath: system }

+ 22 - 21
packages/opencode/src/format/formatter.ts

@@ -3,6 +3,7 @@ import { BunProc } from "../bun"
 import { Instance } from "../project/instance"
 import { Instance } from "../project/instance"
 import { Filesystem } from "../util/filesystem"
 import { Filesystem } from "../util/filesystem"
 import { Process } from "../util/process"
 import { Process } from "../util/process"
+import { which } from "../util/which"
 import { Flag } from "@/flag/flag"
 import { Flag } from "@/flag/flag"
 
 
 export interface Info {
 export interface Info {
@@ -18,7 +19,7 @@ export const gofmt: Info = {
   command: ["gofmt", "-w", "$FILE"],
   command: ["gofmt", "-w", "$FILE"],
   extensions: [".go"],
   extensions: [".go"],
   async enabled() {
   async enabled() {
-    return Bun.which("gofmt") !== null
+    return which("gofmt") !== null
   },
   },
 }
 }
 
 
@@ -27,7 +28,7 @@ export const mix: Info = {
   command: ["mix", "format", "$FILE"],
   command: ["mix", "format", "$FILE"],
   extensions: [".ex", ".exs", ".eex", ".heex", ".leex", ".neex", ".sface"],
   extensions: [".ex", ".exs", ".eex", ".heex", ".leex", ".neex", ".sface"],
   async enabled() {
   async enabled() {
-    return Bun.which("mix") !== null
+    return which("mix") !== null
   },
   },
 }
 }
 
 
@@ -152,7 +153,7 @@ export const zig: Info = {
   command: ["zig", "fmt", "$FILE"],
   command: ["zig", "fmt", "$FILE"],
   extensions: [".zig", ".zon"],
   extensions: [".zig", ".zon"],
   async enabled() {
   async enabled() {
-    return Bun.which("zig") !== null
+    return which("zig") !== null
   },
   },
 }
 }
 
 
@@ -171,7 +172,7 @@ export const ktlint: Info = {
   command: ["ktlint", "-F", "$FILE"],
   command: ["ktlint", "-F", "$FILE"],
   extensions: [".kt", ".kts"],
   extensions: [".kt", ".kts"],
   async enabled() {
   async enabled() {
-    return Bun.which("ktlint") !== null
+    return which("ktlint") !== null
   },
   },
 }
 }
 
 
@@ -180,7 +181,7 @@ export const ruff: Info = {
   command: ["ruff", "format", "$FILE"],
   command: ["ruff", "format", "$FILE"],
   extensions: [".py", ".pyi"],
   extensions: [".py", ".pyi"],
   async enabled() {
   async enabled() {
-    if (!Bun.which("ruff")) return false
+    if (!which("ruff")) return false
     const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"]
     const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"]
     for (const config of configs) {
     for (const config of configs) {
       const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree)
       const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree)
@@ -210,7 +211,7 @@ export const rlang: Info = {
   command: ["air", "format", "$FILE"],
   command: ["air", "format", "$FILE"],
   extensions: [".R"],
   extensions: [".R"],
   async enabled() {
   async enabled() {
-    const airPath = Bun.which("air")
+    const airPath = which("air")
     if (airPath == null) return false
     if (airPath == null) return false
 
 
     try {
     try {
@@ -239,7 +240,7 @@ export const uvformat: Info = {
   extensions: [".py", ".pyi"],
   extensions: [".py", ".pyi"],
   async enabled() {
   async enabled() {
     if (await ruff.enabled()) return false
     if (await ruff.enabled()) return false
-    if (Bun.which("uv") !== null) {
+    if (which("uv") !== null) {
       const proc = Process.spawn(["uv", "format", "--help"], { stderr: "pipe", stdout: "pipe" })
       const proc = Process.spawn(["uv", "format", "--help"], { stderr: "pipe", stdout: "pipe" })
       const code = await proc.exited
       const code = await proc.exited
       return code === 0
       return code === 0
@@ -253,7 +254,7 @@ export const rubocop: Info = {
   command: ["rubocop", "--autocorrect", "$FILE"],
   command: ["rubocop", "--autocorrect", "$FILE"],
   extensions: [".rb", ".rake", ".gemspec", ".ru"],
   extensions: [".rb", ".rake", ".gemspec", ".ru"],
   async enabled() {
   async enabled() {
-    return Bun.which("rubocop") !== null
+    return which("rubocop") !== null
   },
   },
 }
 }
 
 
@@ -262,7 +263,7 @@ export const standardrb: Info = {
   command: ["standardrb", "--fix", "$FILE"],
   command: ["standardrb", "--fix", "$FILE"],
   extensions: [".rb", ".rake", ".gemspec", ".ru"],
   extensions: [".rb", ".rake", ".gemspec", ".ru"],
   async enabled() {
   async enabled() {
-    return Bun.which("standardrb") !== null
+    return which("standardrb") !== null
   },
   },
 }
 }
 
 
@@ -271,7 +272,7 @@ export const htmlbeautifier: Info = {
   command: ["htmlbeautifier", "$FILE"],
   command: ["htmlbeautifier", "$FILE"],
   extensions: [".erb", ".html.erb"],
   extensions: [".erb", ".html.erb"],
   async enabled() {
   async enabled() {
-    return Bun.which("htmlbeautifier") !== null
+    return which("htmlbeautifier") !== null
   },
   },
 }
 }
 
 
@@ -280,7 +281,7 @@ export const dart: Info = {
   command: ["dart", "format", "$FILE"],
   command: ["dart", "format", "$FILE"],
   extensions: [".dart"],
   extensions: [".dart"],
   async enabled() {
   async enabled() {
-    return Bun.which("dart") !== null
+    return which("dart") !== null
   },
   },
 }
 }
 
 
@@ -289,7 +290,7 @@ export const ocamlformat: Info = {
   command: ["ocamlformat", "-i", "$FILE"],
   command: ["ocamlformat", "-i", "$FILE"],
   extensions: [".ml", ".mli"],
   extensions: [".ml", ".mli"],
   async enabled() {
   async enabled() {
-    if (!Bun.which("ocamlformat")) return false
+    if (!which("ocamlformat")) return false
     const items = await Filesystem.findUp(".ocamlformat", Instance.directory, Instance.worktree)
     const items = await Filesystem.findUp(".ocamlformat", Instance.directory, Instance.worktree)
     return items.length > 0
     return items.length > 0
   },
   },
@@ -300,7 +301,7 @@ export const terraform: Info = {
   command: ["terraform", "fmt", "$FILE"],
   command: ["terraform", "fmt", "$FILE"],
   extensions: [".tf", ".tfvars"],
   extensions: [".tf", ".tfvars"],
   async enabled() {
   async enabled() {
-    return Bun.which("terraform") !== null
+    return which("terraform") !== null
   },
   },
 }
 }
 
 
@@ -309,7 +310,7 @@ export const latexindent: Info = {
   command: ["latexindent", "-w", "-s", "$FILE"],
   command: ["latexindent", "-w", "-s", "$FILE"],
   extensions: [".tex"],
   extensions: [".tex"],
   async enabled() {
   async enabled() {
-    return Bun.which("latexindent") !== null
+    return which("latexindent") !== null
   },
   },
 }
 }
 
 
@@ -318,7 +319,7 @@ export const gleam: Info = {
   command: ["gleam", "format", "$FILE"],
   command: ["gleam", "format", "$FILE"],
   extensions: [".gleam"],
   extensions: [".gleam"],
   async enabled() {
   async enabled() {
-    return Bun.which("gleam") !== null
+    return which("gleam") !== null
   },
   },
 }
 }
 
 
@@ -327,7 +328,7 @@ export const shfmt: Info = {
   command: ["shfmt", "-w", "$FILE"],
   command: ["shfmt", "-w", "$FILE"],
   extensions: [".sh", ".bash"],
   extensions: [".sh", ".bash"],
   async enabled() {
   async enabled() {
-    return Bun.which("shfmt") !== null
+    return which("shfmt") !== null
   },
   },
 }
 }
 
 
@@ -336,7 +337,7 @@ export const nixfmt: Info = {
   command: ["nixfmt", "$FILE"],
   command: ["nixfmt", "$FILE"],
   extensions: [".nix"],
   extensions: [".nix"],
   async enabled() {
   async enabled() {
-    return Bun.which("nixfmt") !== null
+    return which("nixfmt") !== null
   },
   },
 }
 }
 
 
@@ -345,7 +346,7 @@ export const rustfmt: Info = {
   command: ["rustfmt", "$FILE"],
   command: ["rustfmt", "$FILE"],
   extensions: [".rs"],
   extensions: [".rs"],
   async enabled() {
   async enabled() {
-    return Bun.which("rustfmt") !== null
+    return which("rustfmt") !== null
   },
   },
 }
 }
 
 
@@ -372,7 +373,7 @@ export const ormolu: Info = {
   command: ["ormolu", "-i", "$FILE"],
   command: ["ormolu", "-i", "$FILE"],
   extensions: [".hs"],
   extensions: [".hs"],
   async enabled() {
   async enabled() {
-    return Bun.which("ormolu") !== null
+    return which("ormolu") !== null
   },
   },
 }
 }
 
 
@@ -381,7 +382,7 @@ export const cljfmt: Info = {
   command: ["cljfmt", "fix", "--quiet", "$FILE"],
   command: ["cljfmt", "fix", "--quiet", "$FILE"],
   extensions: [".clj", ".cljs", ".cljc", ".edn"],
   extensions: [".clj", ".cljs", ".cljc", ".edn"],
   async enabled() {
   async enabled() {
-    return Bun.which("cljfmt") !== null
+    return which("cljfmt") !== null
   },
   },
 }
 }
 
 
@@ -390,6 +391,6 @@ export const dfmt: Info = {
   command: ["dfmt", "-i", "$FILE"],
   command: ["dfmt", "-i", "$FILE"],
   extensions: [".d"],
   extensions: [".d"],
   async enabled() {
   async enabled() {
-    return Bun.which("dfmt") !== null
+    return which("dfmt") !== null
   },
   },
 }
 }

+ 45 - 44
packages/opencode/src/lsp/server.ts

@@ -12,6 +12,7 @@ import { Instance } from "../project/instance"
 import { Flag } from "../flag/flag"
 import { Flag } from "../flag/flag"
 import { Archive } from "../util/archive"
 import { Archive } from "../util/archive"
 import { Process } from "../util/process"
 import { Process } from "../util/process"
+import { which } from "../util/which"
 
 
 export namespace LSPServer {
 export namespace LSPServer {
   const log = Log.create({ service: "lsp.server" })
   const log = Log.create({ service: "lsp.server" })
@@ -75,7 +76,7 @@ export namespace LSPServer {
     },
     },
     extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs"],
     extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs"],
     async spawn(root) {
     async spawn(root) {
-      const deno = Bun.which("deno")
+      const deno = which("deno")
       if (!deno) {
       if (!deno) {
         log.info("deno not found, please install deno first")
         log.info("deno not found, please install deno first")
         return
         return
@@ -122,7 +123,7 @@ export namespace LSPServer {
     extensions: [".vue"],
     extensions: [".vue"],
     root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
     root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
     async spawn(root) {
     async spawn(root) {
-      let binary = Bun.which("vue-language-server")
+      let binary = which("vue-language-server")
       const args: string[] = []
       const args: string[] = []
       if (!binary) {
       if (!binary) {
         const js = path.join(
         const js = path.join(
@@ -260,7 +261,7 @@ export namespace LSPServer {
 
 
       let lintBin = await resolveBin(lintTarget)
       let lintBin = await resolveBin(lintTarget)
       if (!lintBin) {
       if (!lintBin) {
-        const found = Bun.which("oxlint")
+        const found = which("oxlint")
         if (found) lintBin = found
         if (found) lintBin = found
       }
       }
 
 
@@ -281,7 +282,7 @@ export namespace LSPServer {
 
 
       let serverBin = await resolveBin(serverTarget)
       let serverBin = await resolveBin(serverTarget)
       if (!serverBin) {
       if (!serverBin) {
-        const found = Bun.which("oxc_language_server")
+        const found = which("oxc_language_server")
         if (found) serverBin = found
         if (found) serverBin = found
       }
       }
       if (serverBin) {
       if (serverBin) {
@@ -332,7 +333,7 @@ export namespace LSPServer {
       let bin: string | undefined
       let bin: string | undefined
       if (await Filesystem.exists(localBin)) bin = localBin
       if (await Filesystem.exists(localBin)) bin = localBin
       if (!bin) {
       if (!bin) {
-        const found = Bun.which("biome")
+        const found = which("biome")
         if (found) bin = found
         if (found) bin = found
       }
       }
 
 
@@ -368,11 +369,11 @@ export namespace LSPServer {
     },
     },
     extensions: [".go"],
     extensions: [".go"],
     async spawn(root) {
     async spawn(root) {
-      let bin = Bun.which("gopls", {
+      let bin = which("gopls", {
         PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
         PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
       })
       })
       if (!bin) {
       if (!bin) {
-        if (!Bun.which("go")) return
+        if (!which("go")) return
         if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
         if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
 
 
         log.info("installing gopls")
         log.info("installing gopls")
@@ -405,12 +406,12 @@ export namespace LSPServer {
     root: NearestRoot(["Gemfile"]),
     root: NearestRoot(["Gemfile"]),
     extensions: [".rb", ".rake", ".gemspec", ".ru"],
     extensions: [".rb", ".rake", ".gemspec", ".ru"],
     async spawn(root) {
     async spawn(root) {
-      let bin = Bun.which("rubocop", {
+      let bin = which("rubocop", {
         PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
         PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
       })
       })
       if (!bin) {
       if (!bin) {
-        const ruby = Bun.which("ruby")
-        const gem = Bun.which("gem")
+        const ruby = which("ruby")
+        const gem = which("gem")
         if (!ruby || !gem) {
         if (!ruby || !gem) {
           log.info("Ruby not found, please install Ruby first")
           log.info("Ruby not found, please install Ruby first")
           return
           return
@@ -457,7 +458,7 @@ export namespace LSPServer {
         return undefined
         return undefined
       }
       }
 
 
-      let binary = Bun.which("ty")
+      let binary = which("ty")
 
 
       const initialization: Record<string, string> = {}
       const initialization: Record<string, string> = {}
 
 
@@ -509,7 +510,7 @@ export namespace LSPServer {
     extensions: [".py", ".pyi"],
     extensions: [".py", ".pyi"],
     root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]),
     root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]),
     async spawn(root) {
     async spawn(root) {
-      let binary = Bun.which("pyright-langserver")
+      let binary = which("pyright-langserver")
       const args = []
       const args = []
       if (!binary) {
       if (!binary) {
         const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js")
         const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js")
@@ -563,7 +564,7 @@ export namespace LSPServer {
     extensions: [".ex", ".exs"],
     extensions: [".ex", ".exs"],
     root: NearestRoot(["mix.exs", "mix.lock"]),
     root: NearestRoot(["mix.exs", "mix.lock"]),
     async spawn(root) {
     async spawn(root) {
-      let binary = Bun.which("elixir-ls")
+      let binary = which("elixir-ls")
       if (!binary) {
       if (!binary) {
         const elixirLsPath = path.join(Global.Path.bin, "elixir-ls")
         const elixirLsPath = path.join(Global.Path.bin, "elixir-ls")
         binary = path.join(
         binary = path.join(
@@ -574,7 +575,7 @@ export namespace LSPServer {
         )
         )
 
 
         if (!(await Filesystem.exists(binary))) {
         if (!(await Filesystem.exists(binary))) {
-          const elixir = Bun.which("elixir")
+          const elixir = which("elixir")
           if (!elixir) {
           if (!elixir) {
             log.error("elixir is required to run elixir-ls")
             log.error("elixir is required to run elixir-ls")
             return
             return
@@ -625,12 +626,12 @@ export namespace LSPServer {
     extensions: [".zig", ".zon"],
     extensions: [".zig", ".zon"],
     root: NearestRoot(["build.zig"]),
     root: NearestRoot(["build.zig"]),
     async spawn(root) {
     async spawn(root) {
-      let bin = Bun.which("zls", {
+      let bin = which("zls", {
         PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
         PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
       })
       })
 
 
       if (!bin) {
       if (!bin) {
-        const zig = Bun.which("zig")
+        const zig = which("zig")
         if (!zig) {
         if (!zig) {
           log.error("Zig is required to use zls. Please install Zig first.")
           log.error("Zig is required to use zls. Please install Zig first.")
           return
           return
@@ -737,11 +738,11 @@ export namespace LSPServer {
     root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]),
     root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]),
     extensions: [".cs"],
     extensions: [".cs"],
     async spawn(root) {
     async spawn(root) {
-      let bin = Bun.which("csharp-ls", {
+      let bin = which("csharp-ls", {
         PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
         PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
       })
       })
       if (!bin) {
       if (!bin) {
-        if (!Bun.which("dotnet")) {
+        if (!which("dotnet")) {
           log.error(".NET SDK is required to install csharp-ls")
           log.error(".NET SDK is required to install csharp-ls")
           return
           return
         }
         }
@@ -776,11 +777,11 @@ export namespace LSPServer {
     root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]),
     root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]),
     extensions: [".fs", ".fsi", ".fsx", ".fsscript"],
     extensions: [".fs", ".fsi", ".fsx", ".fsscript"],
     async spawn(root) {
     async spawn(root) {
-      let bin = Bun.which("fsautocomplete", {
+      let bin = which("fsautocomplete", {
         PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
         PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
       })
       })
       if (!bin) {
       if (!bin) {
-        if (!Bun.which("dotnet")) {
+        if (!which("dotnet")) {
           log.error(".NET SDK is required to install fsautocomplete")
           log.error(".NET SDK is required to install fsautocomplete")
           return
           return
         }
         }
@@ -817,7 +818,7 @@ export namespace LSPServer {
     async spawn(root) {
     async spawn(root) {
       // Check if sourcekit-lsp is available in the PATH
       // Check if sourcekit-lsp is available in the PATH
       // This is installed with the Swift toolchain
       // This is installed with the Swift toolchain
-      const sourcekit = Bun.which("sourcekit-lsp")
+      const sourcekit = which("sourcekit-lsp")
       if (sourcekit) {
       if (sourcekit) {
         return {
         return {
           process: spawn(sourcekit, {
           process: spawn(sourcekit, {
@@ -828,7 +829,7 @@ export namespace LSPServer {
 
 
       // If sourcekit-lsp not found, check if xcrun is available
       // If sourcekit-lsp not found, check if xcrun is available
       // This is specific to macOS where sourcekit-lsp is typically installed with Xcode
       // This is specific to macOS where sourcekit-lsp is typically installed with Xcode
-      if (!Bun.which("xcrun")) return
+      if (!which("xcrun")) return
 
 
       const lspLoc = await $`xcrun --find sourcekit-lsp`.quiet().nothrow()
       const lspLoc = await $`xcrun --find sourcekit-lsp`.quiet().nothrow()
 
 
@@ -877,7 +878,7 @@ export namespace LSPServer {
     },
     },
     extensions: [".rs"],
     extensions: [".rs"],
     async spawn(root) {
     async spawn(root) {
-      const bin = Bun.which("rust-analyzer")
+      const bin = which("rust-analyzer")
       if (!bin) {
       if (!bin) {
         log.info("rust-analyzer not found in path, please install it")
         log.info("rust-analyzer not found in path, please install it")
         return
         return
@@ -896,7 +897,7 @@ export namespace LSPServer {
     extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
     extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
     async spawn(root) {
     async spawn(root) {
       const args = ["--background-index", "--clang-tidy"]
       const args = ["--background-index", "--clang-tidy"]
-      const fromPath = Bun.which("clangd")
+      const fromPath = which("clangd")
       if (fromPath) {
       if (fromPath) {
         return {
         return {
           process: spawn(fromPath, args, {
           process: spawn(fromPath, args, {
@@ -1041,7 +1042,7 @@ export namespace LSPServer {
     extensions: [".svelte"],
     extensions: [".svelte"],
     root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
     root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
     async spawn(root) {
     async spawn(root) {
-      let binary = Bun.which("svelteserver")
+      let binary = which("svelteserver")
       const args: string[] = []
       const args: string[] = []
       if (!binary) {
       if (!binary) {
         const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js")
         const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js")
@@ -1088,7 +1089,7 @@ export namespace LSPServer {
       }
       }
       const tsdk = path.dirname(tsserver)
       const tsdk = path.dirname(tsserver)
 
 
-      let binary = Bun.which("astro-ls")
+      let binary = which("astro-ls")
       const args: string[] = []
       const args: string[] = []
       if (!binary) {
       if (!binary) {
         const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js")
         const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js")
@@ -1132,7 +1133,7 @@ export namespace LSPServer {
     root: NearestRoot(["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"]),
     root: NearestRoot(["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"]),
     extensions: [".java"],
     extensions: [".java"],
     async spawn(root) {
     async spawn(root) {
-      const java = Bun.which("java")
+      const java = which("java")
       if (!java) {
       if (!java) {
         log.error("Java 21 or newer is required to run the JDTLS. Please install it first.")
         log.error("Java 21 or newer is required to run the JDTLS. Please install it first.")
         return
         return
@@ -1324,7 +1325,7 @@ export namespace LSPServer {
     extensions: [".yaml", ".yml"],
     extensions: [".yaml", ".yml"],
     root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
     root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
     async spawn(root) {
     async spawn(root) {
-      let binary = Bun.which("yaml-language-server")
+      let binary = which("yaml-language-server")
       const args: string[] = []
       const args: string[] = []
       if (!binary) {
       if (!binary) {
         const js = path.join(
         const js = path.join(
@@ -1380,7 +1381,7 @@ export namespace LSPServer {
     ]),
     ]),
     extensions: [".lua"],
     extensions: [".lua"],
     async spawn(root) {
     async spawn(root) {
-      let bin = Bun.which("lua-language-server", {
+      let bin = which("lua-language-server", {
         PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
         PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
       })
       })
 
 
@@ -1512,7 +1513,7 @@ export namespace LSPServer {
     extensions: [".php"],
     extensions: [".php"],
     root: NearestRoot(["composer.json", "composer.lock", ".php-version"]),
     root: NearestRoot(["composer.json", "composer.lock", ".php-version"]),
     async spawn(root) {
     async spawn(root) {
-      let binary = Bun.which("intelephense")
+      let binary = which("intelephense")
       const args: string[] = []
       const args: string[] = []
       if (!binary) {
       if (!binary) {
         const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js")
         const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js")
@@ -1556,7 +1557,7 @@ export namespace LSPServer {
     extensions: [".prisma"],
     extensions: [".prisma"],
     root: NearestRoot(["schema.prisma", "prisma/schema.prisma", "prisma"], ["package.json"]),
     root: NearestRoot(["schema.prisma", "prisma/schema.prisma", "prisma"], ["package.json"]),
     async spawn(root) {
     async spawn(root) {
-      const prisma = Bun.which("prisma")
+      const prisma = which("prisma")
       if (!prisma) {
       if (!prisma) {
         log.info("prisma not found, please install prisma")
         log.info("prisma not found, please install prisma")
         return
         return
@@ -1574,7 +1575,7 @@ export namespace LSPServer {
     extensions: [".dart"],
     extensions: [".dart"],
     root: NearestRoot(["pubspec.yaml", "analysis_options.yaml"]),
     root: NearestRoot(["pubspec.yaml", "analysis_options.yaml"]),
     async spawn(root) {
     async spawn(root) {
-      const dart = Bun.which("dart")
+      const dart = which("dart")
       if (!dart) {
       if (!dart) {
         log.info("dart not found, please install dart first")
         log.info("dart not found, please install dart first")
         return
         return
@@ -1592,7 +1593,7 @@ export namespace LSPServer {
     extensions: [".ml", ".mli"],
     extensions: [".ml", ".mli"],
     root: NearestRoot(["dune-project", "dune-workspace", ".merlin", "opam"]),
     root: NearestRoot(["dune-project", "dune-workspace", ".merlin", "opam"]),
     async spawn(root) {
     async spawn(root) {
-      const bin = Bun.which("ocamllsp")
+      const bin = which("ocamllsp")
       if (!bin) {
       if (!bin) {
         log.info("ocamllsp not found, please install ocaml-lsp-server")
         log.info("ocamllsp not found, please install ocaml-lsp-server")
         return
         return
@@ -1609,7 +1610,7 @@ export namespace LSPServer {
     extensions: [".sh", ".bash", ".zsh", ".ksh"],
     extensions: [".sh", ".bash", ".zsh", ".ksh"],
     root: async () => Instance.directory,
     root: async () => Instance.directory,
     async spawn(root) {
     async spawn(root) {
-      let binary = Bun.which("bash-language-server")
+      let binary = which("bash-language-server")
       const args: string[] = []
       const args: string[] = []
       if (!binary) {
       if (!binary) {
         const js = path.join(Global.Path.bin, "node_modules", "bash-language-server", "out", "cli.js")
         const js = path.join(Global.Path.bin, "node_modules", "bash-language-server", "out", "cli.js")
@@ -1648,7 +1649,7 @@ export namespace LSPServer {
     extensions: [".tf", ".tfvars"],
     extensions: [".tf", ".tfvars"],
     root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"]),
     root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"]),
     async spawn(root) {
     async spawn(root) {
-      let bin = Bun.which("terraform-ls", {
+      let bin = which("terraform-ls", {
         PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
         PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
       })
       })
 
 
@@ -1731,7 +1732,7 @@ export namespace LSPServer {
     extensions: [".tex", ".bib"],
     extensions: [".tex", ".bib"],
     root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
     root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
     async spawn(root) {
     async spawn(root) {
-      let bin = Bun.which("texlab", {
+      let bin = which("texlab", {
         PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
         PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
       })
       })
 
 
@@ -1821,7 +1822,7 @@ export namespace LSPServer {
     extensions: [".dockerfile", "Dockerfile"],
     extensions: [".dockerfile", "Dockerfile"],
     root: async () => Instance.directory,
     root: async () => Instance.directory,
     async spawn(root) {
     async spawn(root) {
-      let binary = Bun.which("docker-langserver")
+      let binary = which("docker-langserver")
       const args: string[] = []
       const args: string[] = []
       if (!binary) {
       if (!binary) {
         const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js")
         const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js")
@@ -1860,7 +1861,7 @@ export namespace LSPServer {
     extensions: [".gleam"],
     extensions: [".gleam"],
     root: NearestRoot(["gleam.toml"]),
     root: NearestRoot(["gleam.toml"]),
     async spawn(root) {
     async spawn(root) {
-      const gleam = Bun.which("gleam")
+      const gleam = which("gleam")
       if (!gleam) {
       if (!gleam) {
         log.info("gleam not found, please install gleam first")
         log.info("gleam not found, please install gleam first")
         return
         return
@@ -1878,9 +1879,9 @@ export namespace LSPServer {
     extensions: [".clj", ".cljs", ".cljc", ".edn"],
     extensions: [".clj", ".cljs", ".cljc", ".edn"],
     root: NearestRoot(["deps.edn", "project.clj", "shadow-cljs.edn", "bb.edn", "build.boot"]),
     root: NearestRoot(["deps.edn", "project.clj", "shadow-cljs.edn", "bb.edn", "build.boot"]),
     async spawn(root) {
     async spawn(root) {
-      let bin = Bun.which("clojure-lsp")
+      let bin = which("clojure-lsp")
       if (!bin && process.platform === "win32") {
       if (!bin && process.platform === "win32") {
-        bin = Bun.which("clojure-lsp.exe")
+        bin = which("clojure-lsp.exe")
       }
       }
       if (!bin) {
       if (!bin) {
         log.info("clojure-lsp not found, please install clojure-lsp first")
         log.info("clojure-lsp not found, please install clojure-lsp first")
@@ -1909,7 +1910,7 @@ export namespace LSPServer {
       return Instance.directory
       return Instance.directory
     },
     },
     async spawn(root) {
     async spawn(root) {
-      const nixd = Bun.which("nixd")
+      const nixd = which("nixd")
       if (!nixd) {
       if (!nixd) {
         log.info("nixd not found, please install nixd first")
         log.info("nixd not found, please install nixd first")
         return
         return
@@ -1930,7 +1931,7 @@ export namespace LSPServer {
     extensions: [".typ", ".typc"],
     extensions: [".typ", ".typc"],
     root: NearestRoot(["typst.toml"]),
     root: NearestRoot(["typst.toml"]),
     async spawn(root) {
     async spawn(root) {
-      let bin = Bun.which("tinymist", {
+      let bin = which("tinymist", {
         PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
         PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
       })
       })
 
 
@@ -2024,7 +2025,7 @@ export namespace LSPServer {
     extensions: [".hs", ".lhs"],
     extensions: [".hs", ".lhs"],
     root: NearestRoot(["stack.yaml", "cabal.project", "hie.yaml", "*.cabal"]),
     root: NearestRoot(["stack.yaml", "cabal.project", "hie.yaml", "*.cabal"]),
     async spawn(root) {
     async spawn(root) {
-      const bin = Bun.which("haskell-language-server-wrapper")
+      const bin = which("haskell-language-server-wrapper")
       if (!bin) {
       if (!bin) {
         log.info("haskell-language-server-wrapper not found, please install haskell-language-server")
         log.info("haskell-language-server-wrapper not found, please install haskell-language-server")
         return
         return
@@ -2042,7 +2043,7 @@ export namespace LSPServer {
     extensions: [".jl"],
     extensions: [".jl"],
     root: NearestRoot(["Project.toml", "Manifest.toml", "*.jl"]),
     root: NearestRoot(["Project.toml", "Manifest.toml", "*.jl"]),
     async spawn(root) {
     async spawn(root) {
-      const julia = Bun.which("julia")
+      const julia = which("julia")
       if (!julia) {
       if (!julia) {
         log.info("julia not found, please install julia first (https://julialang.org/downloads/)")
         log.info("julia not found, please install julia first (https://julialang.org/downloads/)")
         return
         return

+ 2 - 1
packages/opencode/src/project/project.ts

@@ -14,6 +14,7 @@ import { GlobalBus } from "@/bus/global"
 import { existsSync } from "fs"
 import { existsSync } from "fs"
 import { git } from "../util/git"
 import { git } from "../util/git"
 import { Glob } from "../util/glob"
 import { Glob } from "../util/glob"
+import { which } from "../util/which"
 
 
 export namespace Project {
 export namespace Project {
   const log = Log.create({ service: "project" })
   const log = Log.create({ service: "project" })
@@ -97,7 +98,7 @@ export namespace Project {
       if (dotgit) {
       if (dotgit) {
         let sandbox = path.dirname(dotgit)
         let sandbox = path.dirname(dotgit)
 
 
-        const gitBinary = Bun.which("git")
+        const gitBinary = which("git")
 
 
         // cached id calculation
         // cached id calculation
         let id = await Filesystem.readText(path.join(dotgit, "opencode"))
         let id = await Filesystem.readText(path.join(dotgit, "opencode"))

+ 3 - 2
packages/opencode/src/shell/shell.ts

@@ -1,6 +1,7 @@
 import { Flag } from "@/flag/flag"
 import { Flag } from "@/flag/flag"
 import { lazy } from "@/util/lazy"
 import { lazy } from "@/util/lazy"
 import { Filesystem } from "@/util/filesystem"
 import { Filesystem } from "@/util/filesystem"
+import { which } from "@/util/which"
 import path from "path"
 import path from "path"
 import { spawn, type ChildProcess } from "child_process"
 import { spawn, type ChildProcess } from "child_process"
 import { setTimeout as sleep } from "node:timers/promises"
 import { setTimeout as sleep } from "node:timers/promises"
@@ -40,7 +41,7 @@ export namespace Shell {
   function fallback() {
   function fallback() {
     if (process.platform === "win32") {
     if (process.platform === "win32") {
       if (Flag.OPENCODE_GIT_BASH_PATH) return Flag.OPENCODE_GIT_BASH_PATH
       if (Flag.OPENCODE_GIT_BASH_PATH) return Flag.OPENCODE_GIT_BASH_PATH
-      const git = Bun.which("git")
+      const git = which("git")
       if (git) {
       if (git) {
         // git.exe is typically at: C:\Program Files\Git\cmd\git.exe
         // git.exe is typically at: C:\Program Files\Git\cmd\git.exe
         // bash.exe is at: C:\Program Files\Git\bin\bash.exe
         // bash.exe is at: C:\Program Files\Git\bin\bash.exe
@@ -50,7 +51,7 @@ export namespace Shell {
       return process.env.COMSPEC || "cmd.exe"
       return process.env.COMSPEC || "cmd.exe"
     }
     }
     if (process.platform === "darwin") return "/bin/zsh"
     if (process.platform === "darwin") return "/bin/zsh"
-    const bash = Bun.which("bash")
+    const bash = which("bash")
     if (bash) return bash
     if (bash) return bash
     return "/bin/sh"
     return "/bin/sh"
   }
   }

+ 10 - 0
packages/opencode/src/util/which.ts

@@ -0,0 +1,10 @@
+import whichPkg from "which"
+
+export function which(cmd: string, env?: NodeJS.ProcessEnv) {
+  const result = whichPkg.sync(cmd, {
+    nothrow: true,
+    path: env?.PATH,
+    pathExt: env?.PATHEXT,
+  })
+  return typeof result === "string" ? result : null
+}

+ 82 - 0
packages/opencode/test/util/which.test.ts

@@ -0,0 +1,82 @@
+import { describe, expect, test } from "bun:test"
+import fs from "fs/promises"
+import path from "path"
+import { which } from "../../src/util/which"
+import { tmpdir } from "../fixture/fixture"
+
+async function cmd(dir: string, name: string, exec = true) {
+  const ext = process.platform === "win32" ? ".cmd" : ""
+  const file = path.join(dir, name + ext)
+  const body = process.platform === "win32" ? "@echo off\r\n" : "#!/bin/sh\n"
+  await fs.writeFile(file, body)
+  if (process.platform !== "win32") {
+    await fs.chmod(file, exec ? 0o755 : 0o644)
+  }
+  return file
+}
+
+function env(PATH: string): NodeJS.ProcessEnv {
+  return {
+    PATH,
+    PATHEXT: process.env["PATHEXT"],
+  }
+}
+
+function same(a: string | null, b: string) {
+  if (process.platform === "win32") {
+    expect(a?.toLowerCase()).toBe(b.toLowerCase())
+    return
+  }
+
+  expect(a).toBe(b)
+}
+
+describe("util.which", () => {
+  test("returns null when command is missing", () => {
+    expect(which("opencode-missing-command-for-test")).toBeNull()
+  })
+
+  test("finds a command from PATH override", async () => {
+    await using tmp = await tmpdir()
+    const bin = path.join(tmp.path, "bin")
+    await fs.mkdir(bin)
+    const file = await cmd(bin, "tool")
+
+    same(which("tool", env(bin)), file)
+  })
+
+  test("uses first PATH match", async () => {
+    await using tmp = await tmpdir()
+    const a = path.join(tmp.path, "a")
+    const b = path.join(tmp.path, "b")
+    await fs.mkdir(a)
+    await fs.mkdir(b)
+    const first = await cmd(a, "dupe")
+    await cmd(b, "dupe")
+
+    same(which("dupe", env([a, b].join(path.delimiter))), first)
+  })
+
+  test("returns null for non-executable file on unix", async () => {
+    if (process.platform === "win32") return
+
+    await using tmp = await tmpdir()
+    const bin = path.join(tmp.path, "bin")
+    await fs.mkdir(bin)
+    await cmd(bin, "noexec", false)
+
+    expect(which("noexec", env(bin))).toBeNull()
+  })
+
+  test("uses PATHEXT on windows", async () => {
+    if (process.platform !== "win32") return
+
+    await using tmp = await tmpdir()
+    const bin = path.join(tmp.path, "bin")
+    await fs.mkdir(bin)
+    const file = path.join(bin, "pathext.CMD")
+    await fs.writeFile(file, "@echo off\r\n")
+
+    expect(which("pathext", { PATH: bin, PATHEXT: ".CMD" })).toBe(file)
+  })
+})