Sebastian Herrlinger 1 месяц назад
Родитель
Сommit
f98ad6e078
2 измененных файлов с 125 добавлено и 9 удалено
  1. 57 7
      packages/opencode/src/config/tui.ts
  2. 68 2
      packages/opencode/test/config/tui.test.ts

+ 57 - 7
packages/opencode/src/config/tui.ts

@@ -16,7 +16,36 @@ export namespace TuiConfig {
 
   export const Info = TuiInfo
 
-  export type Info = z.output<typeof Info>
+  export type PluginMeta = {
+    scope: "global" | "local"
+    source: string
+  }
+
+  type PluginEntry = {
+    item: Config.PluginSpec
+    meta: PluginMeta
+  }
+
+  export type Info = z.output<typeof Info> & {
+    plugin_meta?: Record<string, PluginMeta>
+  }
+
+  function scope(file: string): PluginMeta["scope"] {
+    if (Instance.containsPath(file)) return "local"
+    return "global"
+  }
+
+  function dedupePlugin(list: PluginEntry[]) {
+    const seen = new Set<string>()
+    const result: PluginEntry[] = []
+    for (const item of list.toReversed()) {
+      const name = Config.getPluginName(item.item)
+      if (seen.has(name)) continue
+      seen.add(name)
+      result.push(item)
+    }
+    return result.toReversed()
+  }
 
   function mergeInfo(target: Info, source: Info): Info {
     const merged = mergeDeep(target, source)
@@ -44,35 +73,56 @@ export namespace TuiConfig {
       : await ConfigPaths.projectFiles("tui", Instance.directory, Instance.worktree)
 
     let result: Info = {}
+    const plugin: PluginEntry[] = []
+
+    const apply = async (file: string) => {
+      const data = await loadFile(file)
+      result = mergeInfo(result, data)
+      if (!data.plugin?.length) return
+      const level = scope(file)
+      for (const item of data.plugin) {
+        plugin.push({
+          item,
+          meta: {
+            scope: level,
+            source: file,
+          },
+        })
+      }
+    }
 
     for (const file of ConfigPaths.fileInDirectory(Global.Path.config, "tui")) {
-      result = mergeInfo(result, await loadFile(file))
+      await apply(file)
     }
 
     if (custom) {
-      result = mergeInfo(result, await loadFile(custom))
+      await apply(custom)
       log.debug("loaded custom tui config", { path: custom })
     }
 
     for (const file of projectFiles) {
-      result = mergeInfo(result, await loadFile(file))
+      await apply(file)
     }
 
     for (const dir of unique(directories)) {
       if (!dir.endsWith(".opencode") && dir !== Flag.OPENCODE_CONFIG_DIR) continue
       for (const file of ConfigPaths.fileInDirectory(dir, "tui")) {
-        result = mergeInfo(result, await loadFile(file))
+        await apply(file)
       }
     }
 
     if (existsSync(managed)) {
       for (const file of ConfigPaths.fileInDirectory(managed, "tui")) {
-        result = mergeInfo(result, await loadFile(file))
+        await apply(file)
       }
     }
 
+    const merged = dedupePlugin(plugin)
     result.keybinds = Config.Keybinds.parse(result.keybinds ?? {})
-    result.plugin = Config.deduplicatePlugins(result.plugin ?? [])
+    result.plugin = merged.map((item) => item.item)
+    result.plugin_meta = merged.length
+      ? Object.fromEntries(merged.map((item) => [Config.getPluginName(item.item), item.meta]))
+      : undefined
 
     const deps: Promise<void>[] = []
     for (const dir of unique(directories)) {

+ 68 - 2
packages/opencode/test/config/tui.test.ts

@@ -458,9 +458,15 @@ test("applies file substitutions when first identical token is in a commented li
 test("loads managed tui config and gives it highest precedence", async () => {
   await using tmp = await tmpdir({
     init: async (dir) => {
-      await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ theme: "project-theme" }, null, 2))
+      await Bun.write(
+        path.join(dir, "tui.json"),
+        JSON.stringify({ theme: "project-theme", plugin: ["[email protected]"] }, null, 2),
+      )
       await fs.mkdir(managedConfigDir, { recursive: true })
-      await Bun.write(path.join(managedConfigDir, "tui.json"), JSON.stringify({ theme: "managed-theme" }, null, 2))
+      await Bun.write(
+        path.join(managedConfigDir, "tui.json"),
+        JSON.stringify({ theme: "managed-theme", plugin: ["[email protected]"] }, null, 2),
+      )
     },
   })
 
@@ -469,6 +475,13 @@ test("loads managed tui config and gives it highest precedence", async () => {
     fn: async () => {
       const config = await TuiConfig.get()
       expect(config.theme).toBe("managed-theme")
+      expect(config.plugin).toEqual(["[email protected]"])
+      expect(config.plugin_meta).toEqual({
+        "shared-plugin": {
+          scope: "global",
+          source: path.join(managedConfigDir, "tui.json"),
+        },
+      })
     },
   })
 })
@@ -526,6 +539,12 @@ test("supports tuple plugin specs with options in tui.json", async () => {
     fn: async () => {
       const config = await TuiConfig.get()
       expect(config.plugin).toEqual([["[email protected]", { enabled: true, label: "demo" }]])
+      expect(config.plugin_meta).toEqual({
+        "acme-plugin": {
+          scope: "local",
+          source: path.join(tmp.path, "tui.json"),
+        },
+      })
     },
   })
 })
@@ -559,6 +578,53 @@ test("deduplicates tuple plugin specs by name with higher precedence winning", a
         ["[email protected]", { source: "project" }],
         ["[email protected]", { source: "project" }],
       ])
+      expect(config.plugin_meta).toEqual({
+        "acme-plugin": {
+          scope: "local",
+          source: path.join(tmp.path, "tui.json"),
+        },
+        "second-plugin": {
+          scope: "local",
+          source: path.join(tmp.path, "tui.json"),
+        },
+      })
+    },
+  })
+})
+
+test("tracks global and local plugin metadata in merged tui config", async () => {
+  await using tmp = await tmpdir({
+    init: async (dir) => {
+      await Bun.write(
+        path.join(Global.Path.config, "tui.json"),
+        JSON.stringify({
+          plugin: ["[email protected]"],
+        }),
+      )
+      await Bun.write(
+        path.join(dir, "tui.json"),
+        JSON.stringify({
+          plugin: ["[email protected]"],
+        }),
+      )
+    },
+  })
+
+  await Instance.provide({
+    directory: tmp.path,
+    fn: async () => {
+      const config = await TuiConfig.get()
+      expect(config.plugin).toEqual(["[email protected]", "[email protected]"])
+      expect(config.plugin_meta).toEqual({
+        "global-plugin": {
+          scope: "global",
+          source: path.join(Global.Path.config, "tui.json"),
+        },
+        "local-plugin": {
+          scope: "local",
+          source: path.join(tmp.path, "tui.json"),
+        },
+      })
     },
   })
 })