summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorShoubhit Dash <[email protected]>2026-03-13 17:58:00 +0530
committerGitHub <[email protected]>2026-03-13 17:58:00 +0530
commitd4ae13f2a0e7748dd8f3a94ec21ee05050ec2cf7 (patch)
tree3c0406310f27f679af56952ce2d916674088ac75
parentf4804dac85b325c7d075384c246ae81ca43bc3a7 (diff)
downloadopencode-d4ae13f2a0e7748dd8f3a94ec21ee05050ec2cf7.tar.gz
opencode-d4ae13f2a0e7748dd8f3a94ec21ee05050ec2cf7.zip
fix(opencode): serialize config bun installs (#17342)
-rw-r--r--packages/opencode/src/config/config.ts2
-rw-r--r--packages/opencode/test/config/config.test.ts36
2 files changed, 37 insertions, 1 deletions
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index f3d0d0b7a..27ba4e186 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -37,6 +37,7 @@ import { Account } from "@/account"
import { ConfigPaths } from "./paths"
import { Filesystem } from "@/util/filesystem"
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" })
@@ -289,6 +290,7 @@ export namespace Config {
// Install any additional dependencies defined in the package.json
// This allows local plugins and custom tools to use external packages
+ using _ = await Lock.write("bun-install")
await BunProc.run(
[
"install",
diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts
index 90727cf8a..baf209d86 100644
--- a/packages/opencode/test/config/config.test.ts
+++ b/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) => {