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:
@@ -26,5 +26,15 @@ export {
|
|||||||
// 执行器
|
// 执行器
|
||||||
export { CommandExecutor, createCommandExecutor } from './executor.js';
|
export { CommandExecutor, createCommandExecutor } from './executor.js';
|
||||||
|
|
||||||
|
// 管理器
|
||||||
|
export {
|
||||||
|
CommandManager,
|
||||||
|
createCommandManager,
|
||||||
|
type CreateCommandInput,
|
||||||
|
type UpdateCommandInput,
|
||||||
|
type CommandContent,
|
||||||
|
type CommandOperationResult,
|
||||||
|
} from './manager.js';
|
||||||
|
|
||||||
// 内置 Commands
|
// 内置 Commands
|
||||||
export { builtinCommands } from './builtin/index.js';
|
export { builtinCommands } from './builtin/index.js';
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -33,8 +33,16 @@ export { SessionStorage } from './session/storage.js';
|
|||||||
export type { SessionData, SessionSummary } from './session/types.js';
|
export type { SessionData, SessionSummary } from './session/types.js';
|
||||||
|
|
||||||
// Commands
|
// Commands
|
||||||
export { getCommandRegistry, createCommandExecutor } from './commands/index.js';
|
export { getCommandRegistry, createCommandExecutor, createCommandManager } from './commands/index.js';
|
||||||
export type { Command, CommandInput, CommandExecutionResult } from './commands/index.js';
|
export type {
|
||||||
|
Command,
|
||||||
|
CommandInput,
|
||||||
|
CommandExecutionResult,
|
||||||
|
CreateCommandInput,
|
||||||
|
UpdateCommandInput,
|
||||||
|
CommandContent,
|
||||||
|
CommandOperationResult,
|
||||||
|
} from './commands/index.js';
|
||||||
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,30 @@ const SearchCommandInputSchema = z.object({
|
|||||||
|
|
||||||
export const commandsRouter = new Hono();
|
export const commandsRouter = new Hono();
|
||||||
|
|
||||||
|
// Zod schemas for CRUD
|
||||||
|
const CreateCommandInputSchema = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
description: z.string().optional(),
|
||||||
|
template: z.string().min(1),
|
||||||
|
agent: z.string().optional(),
|
||||||
|
model: z.string().optional(),
|
||||||
|
subtask: z.boolean().optional(),
|
||||||
|
scope: z.enum(['user', 'project']),
|
||||||
|
});
|
||||||
|
|
||||||
|
const UpdateCommandInputSchema = z.object({
|
||||||
|
description: z.string().optional(),
|
||||||
|
template: z.string().optional(),
|
||||||
|
agent: z.string().optional(),
|
||||||
|
model: z.string().optional(),
|
||||||
|
subtask: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
// Core 模块类型
|
// Core 模块类型
|
||||||
interface CommandModule {
|
interface CommandModule {
|
||||||
getCommandRegistry: () => CommandRegistry;
|
getCommandRegistry: () => CommandRegistry;
|
||||||
createCommandExecutor: (workdir: string) => CommandExecutor;
|
createCommandExecutor: (workdir: string) => CommandExecutor;
|
||||||
|
createCommandManager: (workdir: string) => CommandManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CommandRegistry {
|
interface CommandRegistry {
|
||||||
@@ -67,6 +87,43 @@ interface CommandExecutionResult {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CommandManager {
|
||||||
|
create(input: {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
template: string;
|
||||||
|
agent?: string;
|
||||||
|
model?: string;
|
||||||
|
subtask?: boolean;
|
||||||
|
scope: 'user' | 'project';
|
||||||
|
}): Promise<{ success: boolean; path?: string; error?: string }>;
|
||||||
|
update(
|
||||||
|
name: string,
|
||||||
|
input: {
|
||||||
|
description?: string;
|
||||||
|
template?: string;
|
||||||
|
agent?: string;
|
||||||
|
model?: string;
|
||||||
|
subtask?: boolean;
|
||||||
|
}
|
||||||
|
): Promise<{ success: boolean; path?: string; error?: string }>;
|
||||||
|
delete(name: string): Promise<{ success: boolean; path?: string; error?: string }>;
|
||||||
|
getContent(name: string): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
data?: {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
template: string;
|
||||||
|
agent?: string;
|
||||||
|
model?: string;
|
||||||
|
subtask?: boolean;
|
||||||
|
source: string;
|
||||||
|
sourcePath?: string;
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
// Core 模块缓存
|
// Core 模块缓存
|
||||||
let commandModule: CommandModule | null = null;
|
let commandModule: CommandModule | null = null;
|
||||||
|
|
||||||
@@ -82,7 +139,8 @@ async function initCommandModule(): Promise<CommandModule | null> {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
typeof core.getCommandRegistry !== 'function' ||
|
typeof core.getCommandRegistry !== 'function' ||
|
||||||
typeof core.createCommandExecutor !== 'function'
|
typeof core.createCommandExecutor !== 'function' ||
|
||||||
|
typeof core.createCommandManager !== 'function'
|
||||||
) {
|
) {
|
||||||
console.warn('[Commands] Core module missing command exports');
|
console.warn('[Commands] Core module missing command exports');
|
||||||
return null;
|
return null;
|
||||||
@@ -91,6 +149,7 @@ async function initCommandModule(): Promise<CommandModule | null> {
|
|||||||
commandModule = {
|
commandModule = {
|
||||||
getCommandRegistry: core.getCommandRegistry as () => CommandRegistry,
|
getCommandRegistry: core.getCommandRegistry as () => CommandRegistry,
|
||||||
createCommandExecutor: core.createCommandExecutor as (workdir: string) => CommandExecutor,
|
createCommandExecutor: core.createCommandExecutor as (workdir: string) => CommandExecutor,
|
||||||
|
createCommandManager: core.createCommandManager as (workdir: string) => CommandManager,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize registry with server workdir
|
// Initialize registry with server workdir
|
||||||
@@ -321,3 +380,189 @@ commandsRouter.post('/:name{.+}/execute', async (c) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// CRUD 操作
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /commands - 创建命令
|
||||||
|
*/
|
||||||
|
commandsRouter.post('/', async (c) => {
|
||||||
|
const module = await initCommandModule();
|
||||||
|
|
||||||
|
if (!module) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Command module not available',
|
||||||
|
},
|
||||||
|
503
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await c.req.json();
|
||||||
|
const input = CreateCommandInputSchema.parse(body);
|
||||||
|
|
||||||
|
const config = getConfig();
|
||||||
|
const manager = module.createCommandManager(config.workdir);
|
||||||
|
const result = await manager.create(input);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
name: input.name,
|
||||||
|
path: result.path,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: result.error,
|
||||||
|
},
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Invalid input',
|
||||||
|
},
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /commands/:name/content - 获取命令完整内容(包含 template)
|
||||||
|
*/
|
||||||
|
commandsRouter.get('/:name{.+}/content', async (c) => {
|
||||||
|
const name = c.req.param('name');
|
||||||
|
const module = await initCommandModule();
|
||||||
|
|
||||||
|
if (!module) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Command module not available',
|
||||||
|
},
|
||||||
|
503
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = getConfig();
|
||||||
|
const manager = module.createCommandManager(config.workdir);
|
||||||
|
const result = await manager.getContent(name);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: result.data,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: result.error,
|
||||||
|
},
|
||||||
|
404
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /commands/:name - 更新命令
|
||||||
|
*/
|
||||||
|
commandsRouter.put('/:name{.+}', async (c) => {
|
||||||
|
const name = c.req.param('name');
|
||||||
|
const module = await initCommandModule();
|
||||||
|
|
||||||
|
if (!module) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Command module not available',
|
||||||
|
},
|
||||||
|
503
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await c.req.json();
|
||||||
|
const input = UpdateCommandInputSchema.parse(body);
|
||||||
|
|
||||||
|
const config = getConfig();
|
||||||
|
const manager = module.createCommandManager(config.workdir);
|
||||||
|
const result = await manager.update(name, input);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
path: result.path,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: result.error,
|
||||||
|
},
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Invalid input',
|
||||||
|
},
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /commands/:name - 删除命令
|
||||||
|
*/
|
||||||
|
commandsRouter.delete('/:name{.+}', async (c) => {
|
||||||
|
const name = c.req.param('name');
|
||||||
|
const module = await initCommandModule();
|
||||||
|
|
||||||
|
if (!module) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'Command module not available',
|
||||||
|
},
|
||||||
|
503
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = getConfig();
|
||||||
|
const manager = module.createCommandManager(config.workdir);
|
||||||
|
const result = await manager.delete(name);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
path: result.path,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: result.error,
|
||||||
|
},
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import type {
|
|||||||
CommandSearchResult,
|
CommandSearchResult,
|
||||||
CommandExecuteResult,
|
CommandExecuteResult,
|
||||||
CommandListResponse,
|
CommandListResponse,
|
||||||
|
CreateCommandInput,
|
||||||
|
UpdateCommandInput,
|
||||||
|
CommandContent,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
// Re-export types
|
// Re-export types
|
||||||
@@ -31,6 +34,9 @@ export type {
|
|||||||
CommandSearchResult,
|
CommandSearchResult,
|
||||||
CommandExecuteResult,
|
CommandExecuteResult,
|
||||||
CommandListResponse,
|
CommandListResponse,
|
||||||
|
CreateCommandInput,
|
||||||
|
UpdateCommandInput,
|
||||||
|
CommandContent,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
// API Configuration
|
// API Configuration
|
||||||
@@ -203,3 +209,29 @@ export async function reloadCommands(): Promise<{
|
|||||||
}> {
|
}> {
|
||||||
return request('POST', '/commands/reload');
|
return request('POST', '/commands/reload');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Commands CRUD
|
||||||
|
export async function createCommand(
|
||||||
|
input: CreateCommandInput
|
||||||
|
): Promise<{ success: boolean; data?: { name: string; path: string }; error?: string }> {
|
||||||
|
return request('POST', '/commands', input);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCommandContent(
|
||||||
|
name: string
|
||||||
|
): Promise<{ success: boolean; data?: CommandContent; error?: string }> {
|
||||||
|
return request('GET', `/commands/${encodeURIComponent(name)}/content`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateCommand(
|
||||||
|
name: string,
|
||||||
|
input: UpdateCommandInput
|
||||||
|
): Promise<{ success: boolean; data?: { name: string; path: string }; error?: string }> {
|
||||||
|
return request('PUT', `/commands/${encodeURIComponent(name)}`, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteCommand(
|
||||||
|
name: string
|
||||||
|
): Promise<{ success: boolean; data?: { name: string; path: string }; error?: string }> {
|
||||||
|
return request('DELETE', `/commands/${encodeURIComponent(name)}`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -128,3 +128,46 @@ export interface CommandListResponse {
|
|||||||
bySource: Record<string, number>;
|
bySource: Record<string, number>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ Command CRUD 相关 ============
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandContent {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
template: string;
|
||||||
|
agent?: string;
|
||||||
|
model?: string;
|
||||||
|
subtask?: boolean;
|
||||||
|
source: string;
|
||||||
|
sourcePath?: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ export {
|
|||||||
executeCommand,
|
executeCommand,
|
||||||
searchCommands,
|
searchCommands,
|
||||||
reloadCommands,
|
reloadCommands,
|
||||||
|
// Commands CRUD
|
||||||
|
createCommand,
|
||||||
|
getCommandContent,
|
||||||
|
updateCommand,
|
||||||
|
deleteCommand,
|
||||||
} from './api/client.js';
|
} from './api/client.js';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
@@ -45,6 +50,10 @@ export type {
|
|||||||
CommandSearchResult,
|
CommandSearchResult,
|
||||||
CommandExecuteResult,
|
CommandExecuteResult,
|
||||||
CommandListResponse,
|
CommandListResponse,
|
||||||
|
// Command CRUD types
|
||||||
|
CreateCommandInput,
|
||||||
|
UpdateCommandInput,
|
||||||
|
CommandContent,
|
||||||
} from './api/client.js';
|
} from './api/client.js';
|
||||||
|
|
||||||
// Primitives (shadcn/ui style)
|
// Primitives (shadcn/ui style)
|
||||||
|
|||||||
Reference in New Issue
Block a user