Skip to content

Commit 556adad

Browse files
authored
fix: wait for dependencies before loading custom tools and plugins (anomalyco#12227)
1 parent 843bbc9 commit 556adad

File tree

4 files changed

+71
-16
lines changed

4 files changed

+71
-16
lines changed

packages/opencode/src/config/config.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { GlobalBus } from "@/bus/global"
3030
import { Event } from "../server/event"
3131
import { PackageRegistry } from "@/bun/registry"
3232
import { proxied } from "@/util/proxied"
33+
import { iife } from "@/util/iife"
3334

3435
export namespace Config {
3536
const log = Log.create({ service: "config" })
@@ -144,6 +145,8 @@ export namespace Config {
144145
log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
145146
}
146147

148+
const deps = []
149+
147150
for (const dir of unique(directories)) {
148151
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
149152
for (const file of ["opencode.jsonc", "opencode.json"]) {
@@ -156,10 +159,12 @@ export namespace Config {
156159
}
157160
}
158161

159-
const shouldInstall = await needsInstall(dir)
160-
if (shouldInstall) {
161-
await installDependencies(dir)
162-
}
162+
deps.push(
163+
iife(async () => {
164+
const shouldInstall = await needsInstall(dir)
165+
if (shouldInstall) await installDependencies(dir)
166+
}),
167+
)
163168

164169
result.command = mergeDeep(result.command ?? {}, await loadCommand(dir))
165170
result.agent = mergeDeep(result.agent, await loadAgent(dir))
@@ -233,9 +238,15 @@ export namespace Config {
233238
return {
234239
config: result,
235240
directories,
241+
deps,
236242
}
237243
})
238244

245+
export async function waitForDependencies() {
246+
const deps = await state().then((x) => x.deps)
247+
await Promise.all(deps)
248+
}
249+
239250
export async function installDependencies(dir: string) {
240251
const pkg = path.join(dir, "package.json")
241252
const targetVersion = Installation.isLocal() ? "latest" : Installation.VERSION

packages/opencode/src/plugin/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export namespace Plugin {
4444
}
4545

4646
const plugins = [...(config.plugin ?? [])]
47+
if (plugins.length) await Config.waitForDependencies()
4748
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
4849
plugins.push(...BUILTIN)
4950
}

packages/opencode/src/tool/registry.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,15 @@ export namespace ToolRegistry {
3535
const custom = [] as Tool.Info[]
3636
const glob = new Bun.Glob("{tool,tools}/*.{js,ts}")
3737

38-
for (const dir of await Config.directories()) {
39-
for await (const match of glob.scan({
40-
cwd: dir,
41-
absolute: true,
42-
followSymlinks: true,
43-
dot: true,
44-
})) {
45-
const namespace = path.basename(match, path.extname(match))
46-
const mod = await import(match)
47-
for (const [id, def] of Object.entries<ToolDefinition>(mod)) {
48-
custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def))
49-
}
38+
const matches = await Config.directories().then((dirs) =>
39+
dirs.flatMap((dir) => [...glob.scanSync({ cwd: dir, absolute: true, followSymlinks: true, dot: true })]),
40+
)
41+
if (matches.length) await Config.waitForDependencies()
42+
for (const match of matches) {
43+
const namespace = path.basename(match, path.extname(match))
44+
const mod = await import(match)
45+
for (const [id, def] of Object.entries<ToolDefinition>(mod)) {
46+
custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def))
5047
}
5148
}
5249

packages/opencode/test/tool/registry.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,50 @@ describe("tool.registry", () => {
7373
},
7474
})
7575
})
76+
77+
test("loads tools with external dependencies without crashing", async () => {
78+
await using tmp = await tmpdir({
79+
init: async (dir) => {
80+
const opencodeDir = path.join(dir, ".opencode")
81+
await fs.mkdir(opencodeDir, { recursive: true })
82+
83+
const toolsDir = path.join(opencodeDir, "tools")
84+
await fs.mkdir(toolsDir, { recursive: true })
85+
86+
await Bun.write(
87+
path.join(opencodeDir, "package.json"),
88+
JSON.stringify({
89+
name: "custom-tools",
90+
dependencies: {
91+
"@opencode-ai/plugin": "^0.0.0",
92+
cowsay: "^1.6.0",
93+
},
94+
}),
95+
)
96+
97+
await Bun.write(
98+
path.join(toolsDir, "cowsay.ts"),
99+
[
100+
"import { say } from 'cowsay'",
101+
"export default {",
102+
" description: 'tool that imports cowsay at top level',",
103+
" args: { text: { type: 'string' } },",
104+
" execute: async ({ text }: { text: string }) => {",
105+
" return say({ text })",
106+
" },",
107+
"}",
108+
"",
109+
].join("\n"),
110+
)
111+
},
112+
})
113+
114+
await Instance.provide({
115+
directory: tmp.path,
116+
fn: async () => {
117+
const ids = await ToolRegistry.ids()
118+
expect(ids).toContain("cowsay")
119+
},
120+
})
121+
})
76122
})

0 commit comments

Comments
 (0)