validate config directory

This commit is contained in:
Dax Raad
2025-09-26 03:23:25 -04:00
parent eb7f4e20df
commit 70310a37b3

View File

@@ -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"),