feat: 添加 Commands 系统支持斜杠命令
实现类似 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)
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user