summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-09-26 03:23:25 -0400
committerDax Raad <[email protected]>2025-09-26 03:23:25 -0400
commit70310a37b323f1c55a73ed3391dd956f1e0ae0ce (patch)
treee4dad22d84e4a1a1f31598de42e1721f51f3a582
parenteb7f4e20df365550541358d6274f1499a22d3758 (diff)
downloadopencode-70310a37b323f1c55a73ed3391dd956f1e0ae0ce.tar.gz
opencode-70310a37b323f1c55a73ed3391dd956f1e0ae0ce.zip
validate config directory
-rw-r--r--packages/opencode/src/config/config.ts239
1 files changed, 130 insertions, 109 deletions
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 84ee9a162..c25c6ea04 100644
--- a/packages/opencode/src/config/config.ts
+++ b/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"),