diff --git a/packages/server/src/routes/commands.ts b/packages/server/src/routes/commands.ts index 743e270..105874b 100644 --- a/packages/server/src/routes/commands.ts +++ b/packages/server/src/routes/commands.ts @@ -275,54 +275,9 @@ commandsRouter.post('/reload', async (c) => { } }); -/** - * GET /commands/:name - 获取命令详情 - * 使用 {.+} 匹配包含 / 的命令名(如 deploy/staging) - */ -commandsRouter.get('/: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 registry = module.getCommandRegistry(); - const command = registry.get(name); - - if (!command) { - return c.json( - { - success: false, - error: `Command not found: ${name}`, - }, - 404 - ); - } - - // Return sanitized command info (exclude template for security) - return c.json({ - success: true, - data: { - name: command.name, - description: command.description, - agent: command.agent, - model: command.model, - subtask: command.subtask, - source: command.source, - hasTemplate: !!command.template, - }, - }); -}); - /** * POST /commands/:name/execute - 执行命令(渲染模板) + * 注意:带后缀的路由必须在 /:name{.+} 之前定义,否则会被 {.+} 匹配 */ commandsRouter.post('/:name{.+}/execute', async (c) => { const name = c.req.param('name'); @@ -381,6 +336,91 @@ commandsRouter.post('/:name{.+}/execute', async (c) => { } }); +/** + * GET /commands/:name/content - 获取命令完整内容(包含 template) + * 注意:带后缀的路由必须在 /:name{.+} 之前定义,否则会被 {.+} 匹配 + */ +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 + ); + } +}); + +/** + * GET /commands/:name - 获取命令详情 + * 使用 {.+} 匹配包含 / 的命令名(如 deploy/staging) + * 注意:这个通用路由必须在所有带后缀的路由之后定义 + */ +commandsRouter.get('/: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 registry = module.getCommandRegistry(); + const command = registry.get(name); + + if (!command) { + return c.json( + { + success: false, + error: `Command not found: ${name}`, + }, + 404 + ); + } + + // Return sanitized command info (exclude template for security) + return c.json({ + success: true, + data: { + name: command.name, + description: command.description, + agent: command.agent, + model: command.model, + subtask: command.subtask, + source: command.source, + hasTemplate: !!command.template, + }, + }); +}); + // ============================================================================ // CRUD 操作 // ============================================================================ @@ -437,43 +477,6 @@ commandsRouter.post('/', async (c) => { } }); -/** - * 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 - 更新命令 */ diff --git a/packages/server/tests/unit/routes/commands.test.ts b/packages/server/tests/unit/routes/commands.test.ts index df7d6a1..33a69e1 100644 --- a/packages/server/tests/unit/routes/commands.test.ts +++ b/packages/server/tests/unit/routes/commands.test.ts @@ -305,18 +305,16 @@ describe('Commands Route', () => { }); describe('GET /commands/:name/content - 获取命令完整内容', () => { - // Note: Due to Hono route matching, /:name{.+} matches before /:name{.+}/content - // So /test/content is interpreted as getting command "test/content" via the detail route - // This is a known issue in the source code - the /content endpoint is shadowed - - it('路由被 /:name{.+} 遮蔽,实际请求 test/content 作为命令名', async () => { - // The request goes to GET /:name{.+} with name="test/content" - // Not to GET /:name{.+}/content - mockCommandRegistry.get.mockReturnValue({ - name: 'test/content', - description: 'A command with /content in name', - template: 'echo test', - source: 'project', + it('获取成功,返回包含 template 的完整内容', async () => { + mockCommandManager.getContent.mockResolvedValue({ + success: true, + data: { + name: 'test', + description: 'Test command', + template: 'echo test', + source: 'project', + sourcePath: '/test/.claude/commands/test.md', + }, }); const res = await app.request('/commands/test/content'); @@ -324,21 +322,40 @@ describe('Commands Route', () => { expect(res.status).toBe(200); expect(json.success).toBe(true); - // The detail route returns hasTemplate, not template - expect(json.data.hasTemplate).toBe(true); - expect(json.data.template).toBeUndefined(); + expect(json.data.template).toBe('echo test'); + expect(json.data.name).toBe('test'); }); - it('当命令不存在时返回 404', async () => { - // This also goes through GET /:name{.+} with name="non-existent/content" - mockCommandRegistry.get.mockReturnValue(undefined); + it('支持包含斜杠的命令名', async () => { + mockCommandManager.getContent.mockResolvedValue({ + success: true, + data: { + name: 'deploy/staging', + template: 'deploy to staging', + source: 'project', + }, + }); + + const res = await app.request('/commands/deploy/staging/content'); + const json = await res.json(); + + expect(res.status).toBe(200); + expect(json.success).toBe(true); + expect(json.data.template).toBe('deploy to staging'); + }); + + it('命令不存在返回 404', async () => { + mockCommandManager.getContent.mockResolvedValue({ + success: false, + error: 'Command not found: non-existent', + }); const res = await app.request('/commands/non-existent/content'); const json = await res.json(); expect(res.status).toBe(404); expect(json.success).toBe(false); - expect(json.error).toContain('non-existent/content'); + expect(json.error).toContain('not found'); }); });