/** * Hooks API Routes * * 提供 Hooks 配置管理的 REST API */ import { Hono } from 'hono'; import * as fs from 'fs/promises'; import * as path from 'path'; import { spawn } from 'child_process'; import { getConfig } from './config.js'; import type { HookConfig, ProjectConfig, ShellCommandConfig, FileHookConfig, } from '@ai-assistant/core'; import { loadProjectConfig, loadHookConfig, getConfigFilePath, createDefaultConfig, } from '@ai-assistant/core'; interface HookTestResult { success: boolean; stdout: string; stderr: string; exitCode: number; duration: number; } export const hooksRouter = new Hono(); /** * 移除 JSON 中的注释(支持 JSONC 格式) */ function stripJsonComments(jsonString: string): string { let result = jsonString.replace(/\/\/.*$/gm, ''); result = result.replace(/\/\*[\s\S]*?\*\//g, ''); return result; } /** * 读取配置文件 */ async function readConfigFile(configPath: string): Promise { try { const content = await fs.readFile(configPath, 'utf-8'); const cleanContent = stripJsonComments(content); return JSON.parse(cleanContent); } catch { return null; } } /** * 写入配置文件 */ async function writeConfigFile(configPath: string, config: ProjectConfig): Promise { await fs.writeFile(configPath, JSON.stringify(config, null, 2)); } /** * 获取或创建配置文件路径 */ async function getOrCreateConfigPath(workdir: string): Promise { const existingPath = await getConfigFilePath(workdir); if (existingPath) return existingPath; return path.join(workdir, '.ai-assistant.json'); } /** * GET /hooks/config - 获取完整钩子配置 */ hooksRouter.get('/config', async (c) => { const config = getConfig(); const hookConfig = await loadHookConfig(config.workdir); return c.json({ success: true, data: hookConfig || { file_edited: {}, file_created: {}, file_deleted: {}, session_completed: [], }, }); }); /** * PUT /hooks/config - 更新完整钩子配置 */ hooksRouter.put('/config', async (c) => { try { const newHookConfig = await c.req.json(); const config = getConfig(); const configPath = await getOrCreateConfigPath(config.workdir); // 读取现有配置 let projectConfig = await readConfigFile(configPath); if (!projectConfig) { projectConfig = {}; } // 更新 hooks 部分 projectConfig.hooks = newHookConfig; // 写入配置文件 await writeConfigFile(configPath, projectConfig); return c.json({ success: true, data: newHookConfig, }); } catch (error) { return c.json( { success: false, error: error instanceof Error ? error.message : 'Failed to update config', }, 500 ); } }); /** * GET /hooks/file-edited - 获取 file_edited 钩子 */ hooksRouter.get('/file-edited', async (c) => { const config = getConfig(); const hookConfig = await loadHookConfig(config.workdir); return c.json({ success: true, data: hookConfig?.file_edited || {}, }); }); /** * PUT /hooks/file-edited - 更新 file_edited 钩子 */ hooksRouter.put('/file-edited', async (c) => { try { const newFileEditedHooks = await c.req.json(); const config = getConfig(); const configPath = await getOrCreateConfigPath(config.workdir); let projectConfig = await readConfigFile(configPath); if (!projectConfig) { projectConfig = {}; } if (!projectConfig.hooks) { projectConfig.hooks = {}; } projectConfig.hooks.file_edited = newFileEditedHooks; await writeConfigFile(configPath, projectConfig); return c.json({ success: true, data: newFileEditedHooks, }); } catch (error) { return c.json( { success: false, error: error instanceof Error ? error.message : 'Failed to update file_edited hooks', }, 500 ); } }); /** * GET /hooks/file-created - 获取 file_created 钩子 */ hooksRouter.get('/file-created', async (c) => { const config = getConfig(); const hookConfig = await loadHookConfig(config.workdir); return c.json({ success: true, data: hookConfig?.file_created || {}, }); }); /** * PUT /hooks/file-created - 更新 file_created 钩子 */ hooksRouter.put('/file-created', async (c) => { try { const newFileCreatedHooks = await c.req.json(); const config = getConfig(); const configPath = await getOrCreateConfigPath(config.workdir); let projectConfig = await readConfigFile(configPath); if (!projectConfig) { projectConfig = {}; } if (!projectConfig.hooks) { projectConfig.hooks = {}; } projectConfig.hooks.file_created = newFileCreatedHooks; await writeConfigFile(configPath, projectConfig); return c.json({ success: true, data: newFileCreatedHooks, }); } catch (error) { return c.json( { success: false, error: error instanceof Error ? error.message : 'Failed to update file_created hooks', }, 500 ); } }); /** * GET /hooks/file-deleted - 获取 file_deleted 钩子 */ hooksRouter.get('/file-deleted', async (c) => { const config = getConfig(); const hookConfig = await loadHookConfig(config.workdir); return c.json({ success: true, data: hookConfig?.file_deleted || {}, }); }); /** * PUT /hooks/file-deleted - 更新 file_deleted 钩子 */ hooksRouter.put('/file-deleted', async (c) => { try { const newFileDeletedHooks = await c.req.json(); const config = getConfig(); const configPath = await getOrCreateConfigPath(config.workdir); let projectConfig = await readConfigFile(configPath); if (!projectConfig) { projectConfig = {}; } if (!projectConfig.hooks) { projectConfig.hooks = {}; } projectConfig.hooks.file_deleted = newFileDeletedHooks; await writeConfigFile(configPath, projectConfig); return c.json({ success: true, data: newFileDeletedHooks, }); } catch (error) { return c.json( { success: false, error: error instanceof Error ? error.message : 'Failed to update file_deleted hooks', }, 500 ); } }); /** * GET /hooks/session-completed - 获取 session_completed 钩子 */ hooksRouter.get('/session-completed', async (c) => { const config = getConfig(); const hookConfig = await loadHookConfig(config.workdir); return c.json({ success: true, data: hookConfig?.session_completed || [], }); }); /** * PUT /hooks/session-completed - 更新 session_completed 钩子 */ hooksRouter.put('/session-completed', async (c) => { try { const newSessionCompletedHooks = await c.req.json(); const config = getConfig(); const configPath = await getOrCreateConfigPath(config.workdir); let projectConfig = await readConfigFile(configPath); if (!projectConfig) { projectConfig = {}; } if (!projectConfig.hooks) { projectConfig.hooks = {}; } projectConfig.hooks.session_completed = newSessionCompletedHooks; await writeConfigFile(configPath, projectConfig); return c.json({ success: true, data: newSessionCompletedHooks, }); } catch (error) { return c.json( { success: false, error: error instanceof Error ? error.message : 'Failed to update session_completed hooks', }, 500 ); } }); /** * POST /hooks/test - 测试执行钩子命令 */ hooksRouter.post('/test', async (c) => { try { const commandConfig = await c.req.json(); const config = getConfig(); // 验证命令配置 if (!commandConfig.command || !Array.isArray(commandConfig.command) || commandConfig.command.length === 0) { return c.json( { success: false, error: 'Invalid command configuration: command must be a non-empty array', }, 400 ); } const startTime = Date.now(); const timeout = commandConfig.timeout || 30000; const cwd = commandConfig.cwd || config.workdir; // 执行命令 const result = await new Promise((resolve) => { const [cmd, ...args] = commandConfig.command; const proc = spawn(cmd, args, { cwd, env: { ...process.env, ...commandConfig.environment, }, shell: true, timeout, }); let stdout = ''; let stderr = ''; proc.stdout?.on('data', (data) => { stdout += data.toString(); }); proc.stderr?.on('data', (data) => { stderr += data.toString(); }); proc.on('close', (code) => { resolve({ success: code === 0, stdout, stderr, exitCode: code ?? -1, duration: Date.now() - startTime, }); }); proc.on('error', (error) => { resolve({ success: false, stdout, stderr: error.message, exitCode: -1, duration: Date.now() - startTime, }); }); }); return c.json({ success: true, data: result, }); } catch (error) { return c.json( { success: false, error: error instanceof Error ? error.message : 'Failed to test command', }, 500 ); } });