a476a4240c
实现类似 OpenCode 的 Commands 功能: - 支持 Markdown 格式定义命令,带 YAML frontmatter - 变量替换:$ARGUMENTS, $1/$2, @filepath, !`shell` - 三级加载:builtin < user < project - 7 个内置命令:init, review, test, fix, explain, commit, help - 集成终端 UI 支持 /commands 列表和命令执行 - 完整单元测试覆盖 (46 tests)
200 lines
4.5 KiB
TypeScript
200 lines
4.5 KiB
TypeScript
/**
|
|
* Command 注册表
|
|
*
|
|
* 管理所有可用的 Commands,支持:
|
|
* - 注册/注销 Commands
|
|
* - 按名称查询
|
|
* - 搜索 Commands
|
|
*/
|
|
|
|
import type { Command, CommandSearchResult } from './types.js';
|
|
import { commandLoader } from './loader.js';
|
|
import { builtinCommands } from './builtin/index.js';
|
|
|
|
/**
|
|
* Command 注册表
|
|
*/
|
|
export class CommandRegistry {
|
|
private commands = new Map<string, Command>();
|
|
private initialized = false;
|
|
|
|
/**
|
|
* 初始化注册表
|
|
*/
|
|
async initialize(workdir: string = process.cwd()): Promise<void> {
|
|
if (this.initialized) {
|
|
return;
|
|
}
|
|
|
|
// 1. 注册内置 Commands
|
|
for (const command of builtinCommands) {
|
|
this.register(command);
|
|
}
|
|
|
|
// 2. 加载用户 Commands
|
|
const userDir = commandLoader.getUserCommandsDir();
|
|
const userCommands = await commandLoader.loadFromDirectory(userDir, 'user');
|
|
for (const command of userCommands) {
|
|
this.register(command);
|
|
}
|
|
|
|
// 3. 加载项目 Commands
|
|
const projectDir = commandLoader.getProjectCommandsDir(workdir);
|
|
const projectCommands = await commandLoader.loadFromDirectory(
|
|
projectDir,
|
|
'project'
|
|
);
|
|
for (const command of projectCommands) {
|
|
this.register(command);
|
|
}
|
|
|
|
this.initialized = true;
|
|
}
|
|
|
|
/**
|
|
* 注册 Command
|
|
*/
|
|
register(command: Command): void {
|
|
// 项目 Commands 优先级最高,可以覆盖同名的内置/用户 Commands
|
|
const existing = this.commands.get(command.name);
|
|
if (existing) {
|
|
// 优先级: project > user > builtin
|
|
const priority = { project: 3, user: 2, builtin: 1 };
|
|
if (priority[command.source] < priority[existing.source]) {
|
|
return; // 不覆盖更高优先级的 Command
|
|
}
|
|
}
|
|
this.commands.set(command.name, command);
|
|
}
|
|
|
|
/**
|
|
* 注销 Command
|
|
*/
|
|
unregister(name: string): boolean {
|
|
return this.commands.delete(name);
|
|
}
|
|
|
|
/**
|
|
* 获取 Command
|
|
*/
|
|
get(name: string): Command | undefined {
|
|
return this.commands.get(name);
|
|
}
|
|
|
|
/**
|
|
* 检查 Command 是否存在
|
|
*/
|
|
has(name: string): boolean {
|
|
return this.commands.has(name);
|
|
}
|
|
|
|
/**
|
|
* 获取所有 Commands
|
|
*/
|
|
getAll(): Command[] {
|
|
return Array.from(this.commands.values());
|
|
}
|
|
|
|
/**
|
|
* 搜索 Commands
|
|
*/
|
|
search(query: string, limit: number = 10): CommandSearchResult[] {
|
|
const queryLower = query.toLowerCase();
|
|
const results: CommandSearchResult[] = [];
|
|
|
|
for (const command of this.commands.values()) {
|
|
let score = 0;
|
|
|
|
// 精确名称匹配
|
|
if (command.name.toLowerCase() === queryLower) {
|
|
score = 100;
|
|
}
|
|
// 名称前缀匹配
|
|
else if (command.name.toLowerCase().startsWith(queryLower)) {
|
|
score = 80;
|
|
}
|
|
// 名称包含匹配
|
|
else if (command.name.toLowerCase().includes(queryLower)) {
|
|
score = 60;
|
|
}
|
|
// 描述匹配
|
|
else if (command.description?.toLowerCase().includes(queryLower)) {
|
|
score = 40;
|
|
}
|
|
|
|
if (score > 0) {
|
|
results.push({ command, score });
|
|
}
|
|
}
|
|
|
|
// 按分数降序排序
|
|
results.sort((a, b) => b.score - a.score);
|
|
|
|
return results.slice(0, limit);
|
|
}
|
|
|
|
/**
|
|
* 列出所有 Commands(用于帮助显示)
|
|
*/
|
|
list(): Array<{ name: string; description?: string; source: string }> {
|
|
return this.getAll()
|
|
.map((cmd) => ({
|
|
name: cmd.name,
|
|
description: cmd.description,
|
|
source: cmd.source,
|
|
}))
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
}
|
|
|
|
/**
|
|
* 重新加载 Commands
|
|
*/
|
|
async reload(workdir: string = process.cwd()): Promise<void> {
|
|
this.commands.clear();
|
|
this.initialized = false;
|
|
await this.initialize(workdir);
|
|
}
|
|
|
|
/**
|
|
* 获取统计信息
|
|
*/
|
|
getStats(): {
|
|
total: number;
|
|
bySource: Record<string, number>;
|
|
} {
|
|
const commands = this.getAll();
|
|
const bySource: Record<string, number> = {};
|
|
|
|
for (const command of commands) {
|
|
bySource[command.source] = (bySource[command.source] || 0) + 1;
|
|
}
|
|
|
|
return {
|
|
total: commands.length,
|
|
bySource,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 全局 Command 注册表实例
|
|
*/
|
|
let commandRegistryInstance: CommandRegistry | null = null;
|
|
|
|
/**
|
|
* 获取全局 Command 注册表
|
|
*/
|
|
export function getCommandRegistry(): CommandRegistry {
|
|
if (!commandRegistryInstance) {
|
|
commandRegistryInstance = new CommandRegistry();
|
|
}
|
|
return commandRegistryInstance;
|
|
}
|
|
|
|
/**
|
|
* 重置全局 Command 注册表(用于测试)
|
|
*/
|
|
export function resetCommandRegistry(): void {
|
|
commandRegistryInstance = null;
|
|
}
|