From 28bcab066e9d22bf4371052ea2d36bda4d2d4376 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 26 Mar 2026 11:22:00 +0900 Subject: [PATCH] fix(commands): load opencode command dirs from aliases Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../claude-code-command-loader/loader.test.ts | 70 +++++++++++++++++++ .../claude-code-command-loader/loader.ts | 26 ++++--- 2 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 src/features/claude-code-command-loader/loader.test.ts diff --git a/src/features/claude-code-command-loader/loader.test.ts b/src/features/claude-code-command-loader/loader.test.ts new file mode 100644 index 000000000..490a31f4a --- /dev/null +++ b/src/features/claude-code-command-loader/loader.test.ts @@ -0,0 +1,70 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test" +import { mkdirSync, rmSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { loadOpencodeGlobalCommands, loadOpencodeProjectCommands } from "./loader" + +const TEST_DIR = join(tmpdir(), `claude-code-command-loader-${Date.now()}`) + +function writeCommand(directory: string, name: string, description: string): void { + mkdirSync(directory, { recursive: true }) + writeFileSync( + join(directory, `${name}.md`), + `---\ndescription: ${description}\n---\nRun ${name}.\n`, + ) +} + +describe("claude-code command loader", () => { + let originalOpencodeConfigDir: string | undefined + + beforeEach(() => { + mkdirSync(TEST_DIR, { recursive: true }) + originalOpencodeConfigDir = process.env.OPENCODE_CONFIG_DIR + }) + + afterEach(() => { + if (originalOpencodeConfigDir === undefined) { + delete process.env.OPENCODE_CONFIG_DIR + } else { + process.env.OPENCODE_CONFIG_DIR = originalOpencodeConfigDir + } + rmSync(TEST_DIR, { recursive: true, force: true }) + }) + + it("#given a parent .opencode/commands directory #when loadOpencodeProjectCommands is called from child directory #then it loads the ancestor command", async () => { + // given + const projectDir = join(TEST_DIR, "project") + const childDir = join(projectDir, "apps", "desktop") + writeCommand(join(projectDir, ".opencode", "commands"), "ancestor", "Ancestor command") + + // when + const commands = await loadOpencodeProjectCommands(childDir) + + // then + expect(commands.ancestor?.description).toBe("(opencode-project) Ancestor command") + }) + + it("#given a .opencode/command directory #when loadOpencodeProjectCommands is called #then it loads the singular alias directory", async () => { + // given + writeCommand(join(TEST_DIR, ".opencode", "command"), "singular", "Singular command") + + // when + const commands = await loadOpencodeProjectCommands(TEST_DIR) + + // then + expect(commands.singular?.description).toBe("(opencode-project) Singular command") + }) + + it("#given a global .opencode/commands directory #when loadOpencodeGlobalCommands is called #then it loads the plural alias directory", async () => { + // given + const opencodeConfigDir = join(TEST_DIR, "opencode-config") + process.env.OPENCODE_CONFIG_DIR = opencodeConfigDir + writeCommand(join(opencodeConfigDir, "commands"), "global-plural", "Global plural command") + + // when + const commands = await loadOpencodeGlobalCommands() + + // then + expect(commands["global-plural"]?.description).toBe("(opencode) Global plural command") + }) +}) diff --git a/src/features/claude-code-command-loader/loader.ts b/src/features/claude-code-command-loader/loader.ts index adf2cc4c1..152bf21f9 100644 --- a/src/features/claude-code-command-loader/loader.ts +++ b/src/features/claude-code-command-loader/loader.ts @@ -3,7 +3,12 @@ import { join, basename } from "path" import { parseFrontmatter } from "../../shared/frontmatter" import { sanitizeModelField } from "../../shared/model-sanitizer" import { isMarkdownFile } from "../../shared/file-utils" -import { getClaudeConfigDir, getOpenCodeConfigDir } from "../../shared" +import { + findProjectOpencodeCommandDirs, + getClaudeConfigDir, + getOpenCodeCommandDirs, + getOpenCodeConfigDir, +} from "../../shared" import { log } from "../../shared/logger" import type { CommandScope, CommandDefinition, CommandFrontmatter, LoadedCommand } from "./types" @@ -121,16 +126,21 @@ export async function loadProjectCommands(directory?: string): Promise> { - const configDir = getOpenCodeConfigDir({ binary: "opencode" }) - const opencodeCommandsDir = join(configDir, "command") - const commands = await loadCommandsFromDir(opencodeCommandsDir, "opencode") - return commandsToRecord(commands) + const opencodeCommandDirs = getOpenCodeCommandDirs({ binary: "opencode" }) + const allCommands = await Promise.all( + opencodeCommandDirs.map((commandsDir) => loadCommandsFromDir(commandsDir, "opencode")), + ) + return commandsToRecord(allCommands.flat()) } export async function loadOpencodeProjectCommands(directory?: string): Promise> { - const opencodeProjectDir = join(directory ?? process.cwd(), ".opencode", "command") - const commands = await loadCommandsFromDir(opencodeProjectDir, "opencode-project") - return commandsToRecord(commands) + const opencodeProjectDirs = findProjectOpencodeCommandDirs(directory ?? process.cwd()) + const allCommands = await Promise.all( + opencodeProjectDirs.map((commandsDir) => + loadCommandsFromDir(commandsDir, "opencode-project"), + ), + ) + return commandsToRecord(allCommands.flat()) } export async function loadAllCommands(directory?: string): Promise> {