fix: ensure plurals are properly handled

This commit is contained in:
Aiden Cline
2026-01-12 15:19:17 -06:00
parent e47f383137
commit aaa14d4865
4 changed files with 237 additions and 25 deletions

View File

@@ -209,6 +209,19 @@ export namespace Config {
await BunProc.run(["install"], { cwd: dir }).catch(() => {})
}
function rel(item: string, patterns: string[]) {
for (const pattern of patterns) {
const index = item.indexOf(pattern)
if (index === -1) continue
return item.slice(index + pattern.length)
}
}
function trim(file: string) {
const ext = path.extname(file)
return ext.length ? file.slice(0, -ext.length) : file
}
const COMMAND_GLOB = new Bun.Glob("{command,commands}/**/*.md")
async function loadCommand(dir: string) {
const result: Record<string, Command> = {}
@@ -221,16 +234,9 @@ export namespace Config {
const md = await ConfigMarkdown.parse(item)
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 patterns = ["/.opencode/command/", "/.opencode/commands/", "/command/", "/commands/"]
const file = rel(item, patterns) ?? path.basename(item)
const name = trim(file)
const config = {
name,
@@ -260,20 +266,9 @@ export namespace Config {
const md = await ConfigMarkdown.parse(item)
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 patterns = ["/.opencode/agent/", "/.opencode/agents/", "/agent/", "/agents/"]
const file = rel(item, patterns) ?? path.basename(item)
const agentName = trim(file)
const config = {
name: agentName,

View File

@@ -31,7 +31,7 @@ export namespace ToolRegistry {
export const state = Instance.state(async () => {
const custom = [] as Tool.Info[]
const glob = new Bun.Glob("tool/*.{js,ts}")
const glob = new Bun.Glob("{tool,tools}/*.{js,ts}")
for (const dir of await Config.directories()) {
for await (const match of glob.scan({

View File

@@ -334,6 +334,147 @@ Test agent prompt`,
})
})
test("loads agents from .opencode/agents (plural)", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const opencodeDir = path.join(dir, ".opencode")
await fs.mkdir(opencodeDir, { recursive: true })
const agentsDir = path.join(opencodeDir, "agents")
await fs.mkdir(path.join(agentsDir, "nested"), { recursive: true })
await Bun.write(
path.join(agentsDir, "helper.md"),
`---
model: test/model
mode: subagent
---
Helper agent prompt`,
)
await Bun.write(
path.join(agentsDir, "nested", "child.md"),
`---
model: test/model
mode: subagent
---
Nested agent prompt`,
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await Config.get()
expect(config.agent?.["helper"]).toMatchObject({
name: "helper",
model: "test/model",
mode: "subagent",
prompt: "Helper agent prompt",
})
expect(config.agent?.["nested/child"]).toMatchObject({
name: "nested/child",
model: "test/model",
mode: "subagent",
prompt: "Nested agent prompt",
})
},
})
})
test("loads commands from .opencode/command (singular)", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const opencodeDir = path.join(dir, ".opencode")
await fs.mkdir(opencodeDir, { recursive: true })
const commandDir = path.join(opencodeDir, "command")
await fs.mkdir(path.join(commandDir, "nested"), { recursive: true })
await Bun.write(
path.join(commandDir, "hello.md"),
`---
description: Test command
---
Hello from singular command`,
)
await Bun.write(
path.join(commandDir, "nested", "child.md"),
`---
description: Nested command
---
Nested command template`,
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await Config.get()
expect(config.command?.["hello"]).toEqual({
description: "Test command",
template: "Hello from singular command",
})
expect(config.command?.["nested/child"]).toEqual({
description: "Nested command",
template: "Nested command template",
})
},
})
})
test("loads commands from .opencode/commands (plural)", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const opencodeDir = path.join(dir, ".opencode")
await fs.mkdir(opencodeDir, { recursive: true })
const commandsDir = path.join(opencodeDir, "commands")
await fs.mkdir(path.join(commandsDir, "nested"), { recursive: true })
await Bun.write(
path.join(commandsDir, "hello.md"),
`---
description: Test command
---
Hello from plural commands`,
)
await Bun.write(
path.join(commandsDir, "nested", "child.md"),
`---
description: Nested command
---
Nested command template`,
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const config = await Config.get()
expect(config.command?.["hello"]).toEqual({
description: "Test command",
template: "Hello from plural commands",
})
expect(config.command?.["nested/child"]).toEqual({
description: "Nested command",
template: "Nested command template",
})
},
})
})
test("updates config and writes to file", async () => {
await using tmp = await tmpdir()
await Instance.provide({

View File

@@ -0,0 +1,76 @@
import { describe, expect, test } from "bun:test"
import path from "path"
import fs from "fs/promises"
import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
import { ToolRegistry } from "../../src/tool/registry"
describe("tool.registry", () => {
test("loads tools from .opencode/tool (singular)", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const opencodeDir = path.join(dir, ".opencode")
await fs.mkdir(opencodeDir, { recursive: true })
const toolDir = path.join(opencodeDir, "tool")
await fs.mkdir(toolDir, { recursive: true })
await Bun.write(
path.join(toolDir, "hello.ts"),
[
"export default {",
" description: 'hello tool',",
" args: {},",
" execute: async () => {",
" return 'hello world'",
" },",
"}",
"",
].join("\n"),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const ids = await ToolRegistry.ids()
expect(ids).toContain("hello")
},
})
})
test("loads tools from .opencode/tools (plural)", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const opencodeDir = path.join(dir, ".opencode")
await fs.mkdir(opencodeDir, { recursive: true })
const toolsDir = path.join(opencodeDir, "tools")
await fs.mkdir(toolsDir, { recursive: true })
await Bun.write(
path.join(toolsDir, "hello.ts"),
[
"export default {",
" description: 'hello tool',",
" args: {},",
" execute: async () => {",
" return 'hello world'",
" },",
"}",
"",
].join("\n"),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const ids = await ToolRegistry.ids()
expect(ids).toContain("hello")
},
})
})
})