Dax Raad 4 месяцев назад
Родитель
Сommit
70310a37b3
1 измененных файлов с 130 добавлено и 109 удалено
  1. 130 109
      packages/opencode/src/config/config.ts

+ 130 - 109
packages/opencode/src/config/config.ts

@@ -49,6 +49,8 @@ export namespace Config {
     }
 
     result.agent = result.agent || {}
+    result.mode = result.mode || {}
+    result.plugin = result.plugin || []
 
     const directories = [
       Global.Path.config,
@@ -57,109 +59,14 @@ export namespace Config {
       )),
     ]
 
-    const markdownAgents = [
-      ...(await Filesystem.globUp("agent/**/*.md", Global.Path.config, Global.Path.config)),
-      ...(await Filesystem.globUp(".opencode/agent/**/*.md", Instance.directory, Instance.worktree)),
-    ]
-    for (const item of markdownAgents) {
-      const content = await Bun.file(item).text()
-      const md = matter(content)
-      if (!md.data) continue
-
-      // Extract relative path from agent folder for nested agents
-      let agentName = path.basename(item, ".md")
-      const agentFolderPath = item.includes("/.opencode/agent/")
-        ? item.split("/.opencode/agent/")[1]
-        : item.includes("/agent/")
-          ? item.split("/agent/")[1]
-          : agentName + ".md"
-
-      // If agent is in a subfolder, include folder path in name
-      if (agentFolderPath.includes("/")) {
-        const relativePath = agentFolderPath.replace(".md", "")
-        const pathParts = relativePath.split("/")
-        agentName = pathParts.slice(0, -1).join("/") + "/" + pathParts[pathParts.length - 1]
-      }
-
-      const config = {
-        name: agentName,
-        ...md.data,
-        prompt: md.content.trim(),
-      }
-      const parsed = Agent.safeParse(config)
-      if (parsed.success) {
-        result.agent = mergeDeep(result.agent, {
-          [config.name]: parsed.data,
-        })
-        continue
-      }
-      throw new InvalidError({ path: item }, { cause: parsed.error })
+    for (const dir of directories) {
+      await assertValid(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))
+      result.plugin.push(...(await loadPlugin(dir)))
     }
 
-    // Load mode markdown files
-    result.mode = result.mode || {}
-    const markdownModes = [
-      ...(await Filesystem.globUp("mode/*.md", Global.Path.config, Global.Path.config)),
-      ...(await Filesystem.globUp(".opencode/mode/*.md", Instance.directory, Instance.worktree)),
-    ]
-    for (const item of markdownModes) {
-      const content = await Bun.file(item).text()
-      const md = matter(content)
-      if (!md.data) continue
-
-      const config = {
-        name: path.basename(item, ".md"),
-        ...md.data,
-        prompt: md.content.trim(),
-      }
-      const parsed = Agent.safeParse(config)
-      if (parsed.success) {
-        result.agent = mergeDeep(result.mode, {
-          [config.name]: {
-            ...parsed.data,
-            mode: "primary" as const,
-          },
-        })
-        continue
-      }
-    }
-
-    // Load command markdown files
-    result.command = result.command || {}
-    const markdownCommands = [
-      ...(await Filesystem.globUp("command/**/*.md", Global.Path.config, Global.Path.config)),
-      ...(await Filesystem.globUp(".opencode/command/**/*.md", Instance.directory, Instance.worktree)),
-    ]
-    for (const item of markdownCommands) {
-      const content = await Bun.file(item).text()
-      const md = matter(content)
-      if (!md.data) continue
-
-      const name = (() => {
-        const patterns = ["/.opencode/command/", "/command/"]
-        const pattern = patterns.find((p) => item.includes(p))
-
-        if (pattern) {
-          const index = item.indexOf(pattern)
-          return item.slice(index + pattern.length, -3)
-        }
-        return path.basename(item, ".md")
-      })()
-
-      const config = {
-        name,
-        ...md.data,
-        template: md.content.trim(),
-      }
-      const parsed = Command.safeParse(config)
-      if (parsed.success) {
-        result.command = mergeDeep(result.command, {
-          [config.name]: parsed.data,
-        })
-        continue
-      }
-      throw new InvalidError({ path: item }, { cause: parsed.error })
-    }
     // Migrate deprecated mode field to agent field
     for (const [name, mode] of Object.entries(result.mode)) {
       result.agent = mergeDeep(result.agent ?? {}, {
@@ -170,14 +77,6 @@ export namespace Config {
       })
     }
 
-    result.plugin = result.plugin || []
-    result.plugin.push(
-      ...[
-        ...(await Filesystem.globUp("plugin/*.{ts,js}", Global.Path.config, Global.Path.config)),
-        ...(await Filesystem.globUp(".opencode/plugin/*.{ts,js}", Instance.directory, Instance.worktree)),
-      ].map((x) => "file://" + x),
-    )
-
     if (Flag.OPENCODE_PERMISSION) {
       result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION))
     }
@@ -218,6 +117,128 @@ export namespace Config {
     }
   })
 
+  async function assertValid(dir: string) {
+    const ALLOWED_DIRS = new Set(["agent", "command", "mode", "plugin"])
+    const UNEXPECTED_DIR_GLOB = new Bun.Glob("*/")
+    for await (const item of UNEXPECTED_DIR_GLOB.scan({ absolute: true, cwd: dir, onlyFiles: false })) {
+      const dirname = path.basename(item)
+      if (!ALLOWED_DIRS.has(dirname)) {
+        throw new InvalidError({
+          path: dir,
+          message: `Unexpected directory "${dirname}" found in "${dir}". Only ${ALLOWED_DIRS.values().toArray().join(", ")} directories are allowed.`,
+        })
+      }
+    }
+  }
+
+  const COMMAND_GLOB = new Bun.Glob("command/**/*.md")
+  async function loadCommand(dir: string) {
+    const result: Record<string, Command> = {}
+    for await (const item of COMMAND_GLOB.scan({ absolute: true, cwd: dir })) {
+      const content = await Bun.file(item).text()
+      const md = matter(content)
+      if (!md.data) continue
+
+      const name = (() => {
+        const patterns = ["/.opencode/command/", "/command/"]
+        const pattern = patterns.find((p) => item.includes(p))
+
+        if (pattern) {
+          const index = item.indexOf(pattern)
+          return item.slice(index + pattern.length, -3)
+        }
+        return path.basename(item, ".md")
+      })()
+
+      const config = {
+        name,
+        ...md.data,
+        template: md.content.trim(),
+      }
+      const parsed = Command.safeParse(config)
+      if (parsed.success) {
+        result[config.name] = parsed.data
+        continue
+      }
+      throw new InvalidError({ path: item }, { cause: parsed.error })
+    }
+    return result
+  }
+
+  const AGENT_GLOB = new Bun.Glob("agent/**/*.md")
+  async function loadAgent(dir: string) {
+    const result: Record<string, Agent> = {}
+
+    for await (const item of AGENT_GLOB.scan({ absolute: true, cwd: dir })) {
+      const content = await Bun.file(item).text()
+      const md = matter(content)
+      if (!md.data) continue
+
+      // Extract relative path from agent folder for nested agents
+      let agentName = path.basename(item, ".md")
+      const agentFolderPath = item.includes("/.opencode/agent/")
+        ? item.split("/.opencode/agent/")[1]
+        : item.includes("/agent/")
+          ? item.split("/agent/")[1]
+          : agentName + ".md"
+
+      // If agent is in a subfolder, include folder path in name
+      if (agentFolderPath.includes("/")) {
+        const relativePath = agentFolderPath.replace(".md", "")
+        const pathParts = relativePath.split("/")
+        agentName = pathParts.slice(0, -1).join("/") + "/" + pathParts[pathParts.length - 1]
+      }
+
+      const config = {
+        name: agentName,
+        ...md.data,
+        prompt: md.content.trim(),
+      }
+      const parsed = Agent.safeParse(config)
+      if (parsed.success) {
+        result[config.name] = parsed.data
+        continue
+      }
+      throw new InvalidError({ path: item }, { cause: parsed.error })
+    }
+    return result
+  }
+
+  const MODE_GLOB = new Bun.Glob("mode/*.md")
+  async function loadMode(dir: string) {
+    const result: Record<string, Agent> = {}
+    for await (const item of MODE_GLOB.scan({ absolute: true, cwd: dir })) {
+      const content = await Bun.file(item).text()
+      const md = matter(content)
+      if (!md.data) continue
+
+      const config = {
+        name: path.basename(item, ".md"),
+        ...md.data,
+        prompt: md.content.trim(),
+      }
+      const parsed = Agent.safeParse(config)
+      if (parsed.success) {
+        result[config.name] = {
+          ...parsed.data,
+          mode: "primary" as const,
+        }
+        continue
+      }
+    }
+    return result
+  }
+
+  const PLUGIN_GLOB = new Bun.Glob("plugin/*.{ts,js}")
+  async function loadPlugin(dir: string) {
+    const plugins: string[] = []
+
+    for await (const item of PLUGIN_GLOB.scan({ absolute: true, cwd: dir })) {
+      plugins.push("file://" + item)
+    }
+    return plugins
+  }
+
   export const McpLocal = z
     .object({
       type: z.literal("local").describe("Type of MCP server connection"),