feat(command-loader): add recursive subdirectory scanning for commands (#378)
Support organizing commands in subdirectories with colon-separated naming (e.g., myproject/deploy.md becomes myproject:deploy). - Recursively traverse subdirectories and load all .md command files - Prefix nested command names with directory path (colon-separated) - Protect against circular symlinks via visited path tracking - Skip hidden directories (consistent with other loaders) - Graceful error handling with logging for debugging
This commit is contained in:
@@ -1,24 +1,59 @@
|
|||||||
import { existsSync, readdirSync, readFileSync } from "fs"
|
import { existsSync, readdirSync, readFileSync, realpathSync, type Dirent } from "fs"
|
||||||
import { join, basename } from "path"
|
import { join, basename } from "path"
|
||||||
import { parseFrontmatter } from "../../shared/frontmatter"
|
import { parseFrontmatter } from "../../shared/frontmatter"
|
||||||
import { sanitizeModelField } from "../../shared/model-sanitizer"
|
import { sanitizeModelField } from "../../shared/model-sanitizer"
|
||||||
import { isMarkdownFile } from "../../shared/file-utils"
|
import { isMarkdownFile } from "../../shared/file-utils"
|
||||||
import { getClaudeConfigDir } from "../../shared"
|
import { getClaudeConfigDir } from "../../shared"
|
||||||
|
import { log } from "../../shared/logger"
|
||||||
import type { CommandScope, CommandDefinition, CommandFrontmatter, LoadedCommand } from "./types"
|
import type { CommandScope, CommandDefinition, CommandFrontmatter, LoadedCommand } from "./types"
|
||||||
|
|
||||||
function loadCommandsFromDir(commandsDir: string, scope: CommandScope): LoadedCommand[] {
|
function loadCommandsFromDir(
|
||||||
|
commandsDir: string,
|
||||||
|
scope: CommandScope,
|
||||||
|
visited: Set<string> = new Set(),
|
||||||
|
prefix: string = ""
|
||||||
|
): LoadedCommand[] {
|
||||||
if (!existsSync(commandsDir)) {
|
if (!existsSync(commandsDir)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = readdirSync(commandsDir, { withFileTypes: true })
|
let realPath: string
|
||||||
|
try {
|
||||||
|
realPath = realpathSync(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 = readdirSync(commandsDir, { withFileTypes: true })
|
||||||
|
} catch (error) {
|
||||||
|
log(`Failed to read command directory: ${commandsDir}`, error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
const commands: LoadedCommand[] = []
|
const commands: LoadedCommand[] = []
|
||||||
|
|
||||||
for (const entry of entries) {
|
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
|
||||||
|
commands.push(...loadCommandsFromDir(subDirPath, scope, visited, subPrefix))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if (!isMarkdownFile(entry)) continue
|
if (!isMarkdownFile(entry)) continue
|
||||||
|
|
||||||
const commandPath = join(commandsDir, entry.name)
|
const commandPath = join(commandsDir, entry.name)
|
||||||
const commandName = basename(entry.name, ".md")
|
const baseCommandName = basename(entry.name, ".md")
|
||||||
|
const commandName = prefix ? `${prefix}:${baseCommandName}` : baseCommandName
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = readFileSync(commandPath, "utf-8")
|
const content = readFileSync(commandPath, "utf-8")
|
||||||
@@ -51,7 +86,8 @@ $ARGUMENTS
|
|||||||
definition,
|
definition,
|
||||||
scope,
|
scope,
|
||||||
})
|
})
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
log(`Failed to parse command: ${commandPath}`, error)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user