summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax <[email protected]>2026-02-04 22:25:43 -0500
committerGitHub <[email protected]>2026-02-05 03:25:43 +0000
commit556adad67bcdec0d29b878705175e9ebca544574 (patch)
treea425580ce549d9ea937ddeb24f73eda6af936bc2
parent843bbc973a86229fe75a021032b816890342d500 (diff)
downloadopencode-556adad67bcdec0d29b878705175e9ebca544574.tar.gz
opencode-556adad67bcdec0d29b878705175e9ebca544574.zip
fix: wait for dependencies before loading custom tools and plugins (#12227)
-rw-r--r--packages/opencode/src/config/config.ts19
-rw-r--r--packages/opencode/src/plugin/index.ts1
-rw-r--r--packages/opencode/src/tool/registry.ts21
-rw-r--r--packages/opencode/test/tool/registry.test.ts46
4 files changed, 71 insertions, 16 deletions
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 7c7f1cc43..dfb86dbe2 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -30,6 +30,7 @@ import { GlobalBus } from "@/bus/global"
import { Event } from "../server/event"
import { PackageRegistry } from "@/bun/registry"
import { proxied } from "@/util/proxied"
+import { iife } from "@/util/iife"
export namespace Config {
const log = Log.create({ service: "config" })
@@ -144,6 +145,8 @@ export namespace Config {
log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
}
+ const deps = []
+
for (const dir of unique(directories)) {
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
for (const file of ["opencode.jsonc", "opencode.json"]) {
@@ -156,10 +159,12 @@ export namespace Config {
}
}
- const shouldInstall = await needsInstall(dir)
- if (shouldInstall) {
- await installDependencies(dir)
- }
+ deps.push(
+ iife(async () => {
+ const shouldInstall = await needsInstall(dir)
+ if (shouldInstall) await installDependencies(dir)
+ }),
+ )
result.command = mergeDeep(result.command ?? {}, await loadCommand(dir))
result.agent = mergeDeep(result.agent, await loadAgent(dir))
@@ -233,9 +238,15 @@ export namespace Config {
return {
config: result,
directories,
+ deps,
}
})
+ export async function waitForDependencies() {
+ const deps = await state().then((x) => x.deps)
+ await Promise.all(deps)
+ }
+
export async function installDependencies(dir: string) {
const pkg = path.join(dir, "package.json")
const targetVersion = Installation.isLocal() ? "latest" : Installation.VERSION
diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts
index 6032935f8..4e5776e51 100644
--- a/packages/opencode/src/plugin/index.ts
+++ b/packages/opencode/src/plugin/index.ts
@@ -44,6 +44,7 @@ export namespace Plugin {
}
const plugins = [...(config.plugin ?? [])]
+ if (plugins.length) await Config.waitForDependencies()
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
plugins.push(...BUILTIN)
}
diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts
index 3cb771594..5ed5a879b 100644
--- a/packages/opencode/src/tool/registry.ts
+++ b/packages/opencode/src/tool/registry.ts
@@ -35,18 +35,15 @@ export namespace ToolRegistry {
const custom = [] as Tool.Info[]
const glob = new Bun.Glob("{tool,tools}/*.{js,ts}")
- for (const dir of await Config.directories()) {
- for await (const match of glob.scan({
- cwd: dir,
- absolute: true,
- followSymlinks: true,
- dot: true,
- })) {
- const namespace = path.basename(match, path.extname(match))
- const mod = await import(match)
- for (const [id, def] of Object.entries<ToolDefinition>(mod)) {
- custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def))
- }
+ const matches = await Config.directories().then((dirs) =>
+ dirs.flatMap((dir) => [...glob.scanSync({ cwd: dir, absolute: true, followSymlinks: true, dot: true })]),
+ )
+ if (matches.length) await Config.waitForDependencies()
+ for (const match of matches) {
+ const namespace = path.basename(match, path.extname(match))
+ const mod = await import(match)
+ for (const [id, def] of Object.entries<ToolDefinition>(mod)) {
+ custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def))
}
}
diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts
index aea8b7088..706a9e12c 100644
--- a/packages/opencode/test/tool/registry.test.ts
+++ b/packages/opencode/test/tool/registry.test.ts
@@ -73,4 +73,50 @@ describe("tool.registry", () => {
},
})
})
+
+ test("loads tools with external dependencies without crashing", async () => {
+ await using tmp = await tmpdir({
+ init: async (dir) => {
+ const opencodeDir = path.join(dir, ".opencode")
+ await fs.mkdir(opencodeDir, { recursive: true })
+
+ const toolsDir = path.join(opencodeDir, "tools")
+ await fs.mkdir(toolsDir, { recursive: true })
+
+ await Bun.write(
+ path.join(opencodeDir, "package.json"),
+ JSON.stringify({
+ name: "custom-tools",
+ dependencies: {
+ "@opencode-ai/plugin": "^0.0.0",
+ cowsay: "^1.6.0",
+ },
+ }),
+ )
+
+ await Bun.write(
+ path.join(toolsDir, "cowsay.ts"),
+ [
+ "import { say } from 'cowsay'",
+ "export default {",
+ " description: 'tool that imports cowsay at top level',",
+ " args: { text: { type: 'string' } },",
+ " execute: async ({ text }: { text: string }) => {",
+ " return say({ text })",
+ " },",
+ "}",
+ "",
+ ].join("\n"),
+ )
+ },
+ })
+
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const ids = await ToolRegistry.ids()
+ expect(ids).toContain("cowsay")
+ },
+ })
+ })
})