Ver Fonte

fix(config): install plugin deps per config directory

Dax Raad há 2 meses atrás
pai
commit
440eb76e52
1 ficheiros alterados com 43 adições e 18 exclusões
  1. 43 18
      packages/opencode/src/config/config.ts

+ 43 - 18
packages/opencode/src/config/config.ts

@@ -145,11 +145,20 @@ export namespace Config {
       log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
     }
 
-    const deps = []
+    const deps: Promise<void>[] = []
 
     for (const dir of unique(directories)) {
-      if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
-        for (const file of ["opencode.jsonc", "opencode.json"]) {
+      const files = ["opencode.jsonc", "opencode.json"]
+      const isConfigDir = dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR || dir === Global.Path.config
+      const configs = isConfigDir
+        ? await Promise.all(files.map((file) => loadFile(path.join(dir, file), { resolvePlugins: false })))
+        : []
+      const plugins = configs.flatMap((config) => config.plugin ?? [])
+      const shouldInstall = await needsInstall(dir, plugins)
+      if (shouldInstall) await installDependencies(dir, plugins)
+
+      if (isConfigDir) {
+        for (const file of files) {
           log.debug(`loading config from ${path.join(dir, file)}`)
           result = mergeConfigConcatArrays(result, await loadFile(path.join(dir, file)))
           // to satisfy the type checker
@@ -159,13 +168,6 @@ export namespace Config {
         }
       }
 
-      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))
       result.agent = mergeDeep(result.agent, await loadMode(dir))
@@ -247,7 +249,7 @@ export namespace Config {
     await Promise.all(deps)
   }
 
-  export async function installDependencies(dir: string) {
+  export async function installDependencies(dir: string, plugins: string[] = []) {
     const pkg = path.join(dir, "package.json")
     const targetVersion = Installation.isLocal() ? "*" : Installation.VERSION
 
@@ -258,6 +260,10 @@ export namespace Config {
       ...json.dependencies,
       "@opencode-ai/plugin": targetVersion,
     }
+    const pluginDeps = deps(plugins)
+    for (const [name, version] of Object.entries(pluginDeps)) {
+      json.dependencies[name] = version
+    }
     await Bun.write(pkg, JSON.stringify(json, null, 2))
     await new Promise((resolve) => setTimeout(resolve, 3000))
 
@@ -286,7 +292,7 @@ export namespace Config {
     }
   }
 
-  async function needsInstall(dir: string) {
+  async function needsInstall(dir: string, plugins: string[] = []) {
     // Some config dirs may be read-only.
     // Installing deps there will fail; skip installation in that case.
     const writable = await isWritable(dir)
@@ -318,8 +324,27 @@ export namespace Config {
       })
       return true
     }
-    if (depVersion === targetVersion) return false
-    return true
+    if (depVersion !== targetVersion) return true
+
+    const pluginDeps = deps(plugins)
+    for (const [name, version] of Object.entries(pluginDeps)) {
+      if (dependencies[name] !== version) return true
+    }
+    return false
+  }
+
+  function deps(items: string[]) {
+    const result: Record<string, string> = {}
+    for (const item of items) {
+      if (!item) continue
+      if (item.startsWith("file://")) continue
+      if (item.startsWith("./") || item.startsWith("../") || item.startsWith("/") || item.startsWith("~")) continue
+      const lastAt = item.lastIndexOf("@")
+      const pkg = lastAt > 0 ? item.substring(0, lastAt) : item
+      const version = lastAt > 0 ? item.substring(lastAt + 1) : "latest"
+      result[pkg] = version
+    }
+    return result
   }
 
   function rel(item: string, patterns: string[]) {
@@ -1216,7 +1241,7 @@ export namespace Config {
     return result
   })
 
-  async function loadFile(filepath: string): Promise<Info> {
+  async function loadFile(filepath: string, options: { resolvePlugins?: boolean } = {}): Promise<Info> {
     log.info("loading", { path: filepath })
     let text = await Bun.file(filepath)
       .text()
@@ -1225,10 +1250,10 @@ export namespace Config {
         throw new JsonError({ path: filepath }, { cause: err })
       })
     if (!text) return {}
-    return load(text, filepath)
+    return load(text, filepath, options)
   }
 
-  async function load(text: string, configFilepath: string) {
+  async function load(text: string, configFilepath: string, options: { resolvePlugins?: boolean } = {}) {
     const original = text
     text = text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
       return process.env[varName] || ""
@@ -1304,7 +1329,7 @@ export namespace Config {
         await Bun.write(configFilepath, updated).catch(() => {})
       }
       const data = parsed.data
-      if (data.plugin) {
+      if (data.plugin && options.resolvePlugins !== false) {
         for (let i = 0; i < data.plugin.length; i++) {
           const plugin = data.plugin[i]
           try {