diff --git a/src/features/claude-code-command-loader/loader.ts b/src/features/claude-code-command-loader/loader.ts index 31677db..763bef8 100644 --- a/src/features/claude-code-command-loader/loader.ts +++ b/src/features/claude-code-command-loader/loader.ts @@ -1,4 +1,5 @@ import { existsSync, readdirSync, readFileSync, realpathSync, type Dirent } from "fs" +import { promises as fsPromises } from "fs" import { join, basename } from "path" import { parseFrontmatter } from "../../shared/frontmatter" import { sanitizeModelField } from "../../shared/model-sanitizer" @@ -129,3 +130,130 @@ export function loadOpencodeProjectCommands(): Record const commands = loadCommandsFromDir(opencodeProjectDir, "opencode-project") return commandsToRecord(commands) } + +async function loadCommandsFromDirAsync( + commandsDir: string, + scope: CommandScope, + visited: Set = new Set(), + prefix: string = "" +): Promise { + try { + await fsPromises.access(commandsDir) + } catch { + return [] + } + + let realPath: string + try { + realPath = await fsPromises.realpath(commandsDir) + } catch (error) { + log(`Failed to resolve command directory: ${commandsDir}`, error) + return [] + } + + if (visited.has(realPath)) { + return [] + } + visited.add(realPath) + + let entries: Dirent[] + try { + entries = await fsPromises.readdir(commandsDir, { withFileTypes: true }) + } catch (error) { + log(`Failed to read command directory: ${commandsDir}`, error) + return [] + } + + const commands: LoadedCommand[] = [] + + for (const entry of entries) { + if (entry.isDirectory()) { + if (entry.name.startsWith(".")) continue + const subDirPath = join(commandsDir, entry.name) + const subPrefix = prefix ? `${prefix}:${entry.name}` : entry.name + const subCommands = await loadCommandsFromDirAsync(subDirPath, scope, visited, subPrefix) + commands.push(...subCommands) + continue + } + + if (!isMarkdownFile(entry)) continue + + const commandPath = join(commandsDir, entry.name) + const baseCommandName = basename(entry.name, ".md") + const commandName = prefix ? `${prefix}:${baseCommandName}` : baseCommandName + + try { + const content = await fsPromises.readFile(commandPath, "utf-8") + const { data, body } = parseFrontmatter(content) + + const wrappedTemplate = ` + ${body.trim()} + + + + $ARGUMENTS + ` + + const formattedDescription = `(${scope}) ${data.description || ""}` + + const isOpencodeSource = scope === "opencode" || scope === "opencode-project" + const definition: CommandDefinition = { + name: commandName, + description: formattedDescription, + template: wrappedTemplate, + agent: data.agent, + model: sanitizeModelField(data.model, isOpencodeSource ? "opencode" : "claude-code"), + subtask: data.subtask, + argumentHint: data["argument-hint"], + handoffs: data.handoffs, + } + + commands.push({ + name: commandName, + path: commandPath, + definition, + scope, + }) + } catch (error) { + log(`Failed to parse command: ${commandPath}`, error) + continue + } + } + + return commands +} + +export async function loadUserCommandsAsync(): Promise> { + const userCommandsDir = join(getClaudeConfigDir(), "commands") + const commands = await loadCommandsFromDirAsync(userCommandsDir, "user") + return commandsToRecord(commands) +} + +export async function loadProjectCommandsAsync(): Promise> { + const projectCommandsDir = join(process.cwd(), ".claude", "commands") + const commands = await loadCommandsFromDirAsync(projectCommandsDir, "project") + return commandsToRecord(commands) +} + +export async function loadOpencodeGlobalCommandsAsync(): Promise> { + const { homedir } = require("os") + const opencodeCommandsDir = join(homedir(), ".config", "opencode", "command") + const commands = await loadCommandsFromDirAsync(opencodeCommandsDir, "opencode") + return commandsToRecord(commands) +} + +export async function loadOpencodeProjectCommandsAsync(): Promise> { + const opencodeProjectDir = join(process.cwd(), ".opencode", "command") + const commands = await loadCommandsFromDirAsync(opencodeProjectDir, "opencode-project") + return commandsToRecord(commands) +} + +export async function loadAllCommandsAsync(): Promise> { + const [user, project, global, projectOpencode] = await Promise.all([ + loadUserCommandsAsync(), + loadProjectCommandsAsync(), + loadOpencodeGlobalCommandsAsync(), + loadOpencodeProjectCommandsAsync(), + ]) + return { ...projectOpencode, ...global, ...project, ...user } +}