Преглед на файлове

feat(tui): discover project plugins

Dax Raad преди 1 седмица
родител
ревизия
068c32df39

+ 16 - 0
.opencode/plugins/tui/discovery-smoke.ts

@@ -0,0 +1,16 @@
+import type { Context } from "../../../packages/plugin/src/tui/context"
+
+export default {
+  id: "test.tui-discovery-smoke",
+  setup(context: Context) {
+    const timer = setTimeout(() => {
+      context.ui.toast.show({
+        title: "TUI plugin discovery works",
+        message: "Loaded .opencode/plugins/tui/discovery-smoke.ts",
+        variant: "success",
+        duration: 30_000,
+      })
+    }, 1_000)
+    return () => clearTimeout(timer)
+  },
+}

+ 2 - 1
packages/tui/src/plugin/context.tsx

@@ -33,6 +33,7 @@ import { useToast } from "../ui/toast"
 import { useAttention } from "../context/attention"
 import { abbreviateHome } from "../util/path-format"
 import { builtins } from "./builtins"
+import { discoverTuiPlugins } from "./discovery"
 
 export interface PackageResolver {
   readonly resolve: (spec: string) => Promise<string | undefined>
@@ -353,7 +354,7 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver }>
         .filter(([, registration]) => registration.active)
         .map(([id]) => deactivate(id)),
     )
-    const entries = config.data.plugins ?? []
+    const entries = [...(await discoverTuiPlugins(paths.cwd)), ...(config.data.plugins ?? [])]
     batch(() => {
       setStore("registrations", reconcileStore({}))
       setStore("states", [])

+ 16 - 0
packages/tui/src/plugin/discovery.ts

@@ -0,0 +1,16 @@
+import { readdir } from "node:fs/promises"
+import path from "node:path"
+
+const extensions = new Set([".cjs", ".cts", ".js", ".jsx", ".mjs", ".mts", ".ts", ".tsx"])
+
+export async function discoverTuiPlugins(cwd: string) {
+  const directory = path.join(cwd, ".opencode", "plugins", "tui")
+  const entries = await readdir(directory, { withFileTypes: true }).catch((error: unknown) => {
+    if (error && typeof error === "object" && Reflect.get(error, "code") === "ENOENT") return []
+    return Promise.reject(error)
+  })
+  return entries
+    .filter((entry) => (entry.isFile() || entry.isSymbolicLink()) && extensions.has(path.extname(entry.name)))
+    .map((entry) => path.join(directory, entry.name))
+    .sort()
+}

+ 27 - 0
packages/tui/test/plugin-discovery.test.ts

@@ -0,0 +1,27 @@
+import { mkdir, writeFile } from "node:fs/promises"
+import path from "node:path"
+import { expect, test } from "bun:test"
+import { discoverTuiPlugins } from "../src/plugin/discovery"
+import { tmpdir } from "./fixture/fixture"
+
+test("discovers project TUI plugin files in stable order", async () => {
+  await using tmp = await tmpdir()
+  const directory = path.join(tmp.path, ".opencode", "plugins", "tui")
+  await mkdir(path.join(directory, "nested"), { recursive: true })
+  await Promise.all([
+    writeFile(path.join(directory, "second.tsx"), "export default {}"),
+    writeFile(path.join(directory, "first.js"), "export default {}"),
+    writeFile(path.join(directory, "ignored.json"), "{}"),
+    writeFile(path.join(directory, "nested", "ignored.ts"), "export default {}"),
+  ])
+
+  expect(await discoverTuiPlugins(tmp.path)).toEqual([
+    path.join(directory, "first.js"),
+    path.join(directory, "second.tsx"),
+  ])
+})
+
+test("returns no project TUI plugins when the directory is absent", async () => {
+  await using tmp = await tmpdir()
+  expect(await discoverTuiPlugins(tmp.path)).toEqual([])
+})