mirror of
https://github.com/aljazceru/opencode.git
synced 2026-01-07 18:04:54 +01:00
validate config directory
This commit is contained in:
@@ -49,6 +49,8 @@ export namespace Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
result.agent = result.agent || {}
|
result.agent = result.agent || {}
|
||||||
|
result.mode = result.mode || {}
|
||||||
|
result.plugin = result.plugin || []
|
||||||
|
|
||||||
const directories = [
|
const directories = [
|
||||||
Global.Path.config,
|
Global.Path.config,
|
||||||
@@ -57,109 +59,14 @@ export namespace Config {
|
|||||||
)),
|
)),
|
||||||
]
|
]
|
||||||
|
|
||||||
const markdownAgents = [
|
for (const dir of directories) {
|
||||||
...(await Filesystem.globUp("agent/**/*.md", Global.Path.config, Global.Path.config)),
|
await assertValid(dir)
|
||||||
...(await Filesystem.globUp(".opencode/agent/**/*.md", Instance.directory, Instance.worktree)),
|
result.command = mergeDeep(result.command ?? {}, await loadCommand(dir))
|
||||||
]
|
result.agent = mergeDeep(result.agent, await loadAgent(dir))
|
||||||
for (const item of markdownAgents) {
|
result.agent = mergeDeep(result.agent, await loadMode(dir))
|
||||||
const content = await Bun.file(item).text()
|
result.plugin.push(...(await loadPlugin(dir)))
|
||||||
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 })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// Migrate deprecated mode field to agent field
|
||||||
for (const [name, mode] of Object.entries(result.mode)) {
|
for (const [name, mode] of Object.entries(result.mode)) {
|
||||||
result.agent = mergeDeep(result.agent ?? {}, {
|
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) {
|
if (Flag.OPENCODE_PERMISSION) {
|
||||||
result.permission = mergeDeep(result.permission ?? {}, JSON.parse(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
|
export const McpLocal = z
|
||||||
.object({
|
.object({
|
||||||
type: z.literal("local").describe("Type of MCP server connection"),
|
type: z.literal("local").describe("Type of MCP server connection"),
|
||||||
|
|||||||
Reference in New Issue
Block a user