Просмотр исходного кода

fix: wait for dependencies before loading custom tools and plugins (#12227)

Dax 2 месяцев назад
Родитель
Сommit
556adad67b

+ 15 - 4
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

+ 1 - 0
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)
     }

+ 9 - 12
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))
       }
     }
 

+ 46 - 0
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")
+      },
+    })
+  })
 })