feat(commands): 实现命令 CRUD 完整功能

Core:
- 新增 CommandManager 类,支持创建、更新、删除命令
- 验证命令名称防止路径遍历攻击
- 自动生成 Markdown 文件(含 YAML frontmatter)
- 内置命令保护(不可修改/删除)

Server:
- POST /api/commands - 创建命令
- GET /api/commands/:name/content - 获取命令完整内容
- PUT /api/commands/:name - 更新命令
- DELETE /api/commands/:name - 删除命令

UI:
- 新增 createCommand、updateCommand、deleteCommand、getCommandContent 函数
- 新增 CreateCommandInput、UpdateCommandInput、CommandContent 类型
This commit is contained in:
2025-12-12 18:51:38 +08:00
parent db711648e0
commit f0385ef221
7 changed files with 780 additions and 3 deletions
+10
View File
@@ -26,5 +26,15 @@ export {
// 执行器
export { CommandExecutor, createCommandExecutor } from './executor.js';
// 管理器
export {
CommandManager,
createCommandManager,
type CreateCommandInput,
type UpdateCommandInput,
type CommandContent,
type CommandOperationResult,
} from './manager.js';
// 内置 Commands
export { builtinCommands } from './builtin/index.js';
+430
View File
@@ -0,0 +1,430 @@
/**
* Command 管理器
*
* 负责命令的 CRUD 操作(创建、更新、删除)
* 命令存储在文件系统的 Markdown 文件中
*/
import * as fs from 'fs/promises';
import * as path from 'path';
import * as yaml from 'yaml';
import type { Command, CommandFrontmatter } from './types.js';
import { commandLoader } from './loader.js';
import { getCommandRegistry } from './registry.js';
/**
* 创建命令的输入参数
*/
export interface CreateCommandInput {
/** 命令名称(支持嵌套如 deploy/staging */
name: string;
/** 命令描述 */
description?: string;
/** 提示词模板 */
template: string;
/** 指定 Agent */
agent?: string;
/** 指定模型 */
model?: string;
/** 是否作为子任务执行 */
subtask?: boolean;
/** 存储位置 */
scope: 'user' | 'project';
}
/**
* 更新命令的输入参数
*/
export interface UpdateCommandInput {
/** 命令描述 */
description?: string;
/** 提示词模板 */
template?: string;
/** 指定 Agent */
agent?: string;
/** 指定模型 */
model?: string;
/** 是否作为子任务执行 */
subtask?: boolean;
}
/**
* 命令完整内容(包含 template)
*/
export interface CommandContent {
name: string;
description?: string;
template: string;
agent?: string;
model?: string;
subtask?: boolean;
source: string;
sourcePath?: string;
}
/**
* 操作结果
*/
export interface CommandOperationResult {
success: boolean;
path?: string;
error?: string;
}
/**
* 命令管理器
*/
export class CommandManager {
private workdir: string;
constructor(workdir: string = process.cwd()) {
this.workdir = workdir;
}
/**
* 验证命令名称是否合法
* 防止路径遍历攻击
*/
private validateCommandName(name: string): boolean {
// 不能为空
if (!name || name.trim() === '') {
return false;
}
// 不能包含 .. 或绝对路径
if (name.includes('..') || path.isAbsolute(name)) {
return false;
}
// 只允许字母、数字、连字符、下划线和斜杠
if (!/^[a-zA-Z0-9_\-/]+$/.test(name)) {
return false;
}
// 不能以斜杠开头或结尾
if (name.startsWith('/') || name.endsWith('/')) {
return false;
}
// 不能有连续的斜杠
if (name.includes('//')) {
return false;
}
return true;
}
/**
* 生成 Markdown 内容
*/
private generateMarkdownContent(input: {
description?: string;
template: string;
agent?: string;
model?: string;
subtask?: boolean;
}): string {
const frontmatter: CommandFrontmatter = {};
if (input.description) {
frontmatter.description = input.description;
}
if (input.agent) {
frontmatter.agent = input.agent;
}
if (input.model) {
frontmatter.model = input.model;
}
if (input.subtask !== undefined) {
frontmatter.subtask = input.subtask;
}
// 如果有 frontmatter,生成 YAML
if (Object.keys(frontmatter).length > 0) {
const yamlStr = yaml.stringify(frontmatter).trim();
return `---\n${yamlStr}\n---\n\n${input.template}`;
}
// 没有 frontmatter,直接返回模板
return input.template;
}
/**
* 获取命令文件路径
*/
getCommandFilePath(name: string, scope: 'user' | 'project'): string {
const baseDir =
scope === 'user'
? commandLoader.getUserCommandsDir()
: commandLoader.getProjectCommandsDir(this.workdir);
return path.join(baseDir, `${name}.md`);
}
/**
* 创建命令
*/
async create(input: CreateCommandInput): Promise<CommandOperationResult> {
// 验证命令名称
if (!this.validateCommandName(input.name)) {
return {
success: false,
error: `Invalid command name: ${input.name}. Name can only contain letters, numbers, hyphens, underscores, and forward slashes.`,
};
}
// 验证模板不为空
if (!input.template || input.template.trim() === '') {
return {
success: false,
error: 'Template cannot be empty',
};
}
// 检查是否已存在同名命令
const registry = getCommandRegistry();
const existing = registry.get(input.name);
if (existing) {
return {
success: false,
error: `Command already exists: ${input.name} (source: ${existing.source})`,
};
}
// 生成文件路径
const filePath = this.getCommandFilePath(input.name, input.scope);
// 确保目录存在
const dir = path.dirname(filePath);
try {
await fs.mkdir(dir, { recursive: true });
} catch (error) {
return {
success: false,
error: `Failed to create directory: ${dir}`,
};
}
// 生成 Markdown 内容
const content = this.generateMarkdownContent({
description: input.description,
template: input.template,
agent: input.agent,
model: input.model,
subtask: input.subtask,
});
// 写入文件
try {
await fs.writeFile(filePath, content, 'utf-8');
} catch (error) {
return {
success: false,
error: `Failed to write file: ${filePath}`,
};
}
// 重新加载命令注册表
await registry.reload(this.workdir);
return {
success: true,
path: filePath,
};
}
/**
* 更新命令
*/
async update(
name: string,
input: UpdateCommandInput
): Promise<CommandOperationResult> {
// 验证命令名称
if (!this.validateCommandName(name)) {
return {
success: false,
error: `Invalid command name: ${name}`,
};
}
// 获取现有命令
const registry = getCommandRegistry();
const existing = registry.get(name);
if (!existing) {
return {
success: false,
error: `Command not found: ${name}`,
};
}
// 内置命令不可修改
if (existing.source === 'builtin') {
return {
success: false,
error: `Cannot modify builtin command: ${name}`,
};
}
// 获取文件路径
const filePath = existing.sourcePath;
if (!filePath) {
return {
success: false,
error: `Command source path not found: ${name}`,
};
}
// 合并更新
const updatedCommand = {
description: input.description ?? existing.description,
template: input.template ?? existing.template,
agent: input.agent ?? existing.agent,
model: input.model ?? existing.model,
subtask: input.subtask ?? existing.subtask,
};
// 生成新的 Markdown 内容
const content = this.generateMarkdownContent(updatedCommand);
// 写入文件
try {
await fs.writeFile(filePath, content, 'utf-8');
} catch (error) {
return {
success: false,
error: `Failed to write file: ${filePath}`,
};
}
// 重新加载命令注册表
await registry.reload(this.workdir);
return {
success: true,
path: filePath,
};
}
/**
* 删除命令
*/
async delete(name: string): Promise<CommandOperationResult> {
// 验证命令名称
if (!this.validateCommandName(name)) {
return {
success: false,
error: `Invalid command name: ${name}`,
};
}
// 获取现有命令
const registry = getCommandRegistry();
const existing = registry.get(name);
if (!existing) {
return {
success: false,
error: `Command not found: ${name}`,
};
}
// 内置命令不可删除
if (existing.source === 'builtin') {
return {
success: false,
error: `Cannot delete builtin command: ${name}`,
};
}
// 获取文件路径
const filePath = existing.sourcePath;
if (!filePath) {
return {
success: false,
error: `Command source path not found: ${name}`,
};
}
// 删除文件
try {
await fs.unlink(filePath);
} catch (error) {
return {
success: false,
error: `Failed to delete file: ${filePath}`,
};
}
// 尝试删除空的父目录
try {
const dir = path.dirname(filePath);
const files = await fs.readdir(dir);
if (files.length === 0) {
await fs.rmdir(dir);
}
} catch {
// 忽略目录删除失败
}
// 重新加载命令注册表
await registry.reload(this.workdir);
return {
success: true,
path: filePath,
};
}
/**
* 获取命令完整内容(包含 template)
*/
async getContent(name: string): Promise<{
success: boolean;
data?: CommandContent;
error?: string;
}> {
// 验证命令名称
if (!this.validateCommandName(name)) {
return {
success: false,
error: `Invalid command name: ${name}`,
};
}
// 获取命令
const registry = getCommandRegistry();
const command = registry.get(name);
if (!command) {
return {
success: false,
error: `Command not found: ${name}`,
};
}
return {
success: true,
data: {
name: command.name,
description: command.description,
template: command.template,
agent: command.agent,
model: command.model,
subtask: command.subtask,
source: command.source,
sourcePath: command.sourcePath,
},
};
}
}
/**
* 创建命令管理器实例
*/
export function createCommandManager(
workdir: string = process.cwd()
): CommandManager {
return new CommandManager(workdir);
}
+10 -2
View File
@@ -33,8 +33,16 @@ export { SessionStorage } from './session/storage.js';
export type { SessionData, SessionSummary } from './session/types.js';
// Commands
export { getCommandRegistry, createCommandExecutor } from './commands/index.js';
export type { Command, CommandInput, CommandExecutionResult } from './commands/index.js';
export { getCommandRegistry, createCommandExecutor, createCommandManager } from './commands/index.js';
export type {
Command,
CommandInput,
CommandExecutionResult,
CreateCommandInput,
UpdateCommandInput,
CommandContent,
CommandOperationResult,
} from './commands/index.js';
const program = new Command();