UNPKG

termcode

Version:

Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative

278 lines (273 loc) 9.48 kB
import { BuiltinHooks } from "./builtins.js"; import { log } from "../util/logging.js"; import { promises as fs } from "node:fs"; import { spawn } from "node:child_process"; export class HookExecutor { config; constructor(config) { this.config = config; } /** * Execute a hook with the given context */ async execute(hook, context, additionalContext) { const startTime = Date.now(); try { let result; switch (hook.handler.type) { case 'builtin': result = await this.executeBuiltin(hook, context, additionalContext); break; case 'javascript': result = await this.executeJavaScript(hook, context, additionalContext); break; case 'python': result = await this.executePython(hook, context, additionalContext); break; case 'shell': result = await this.executeShell(hook, context, additionalContext); break; default: throw new Error(`Unsupported handler type: ${hook.handler.type}`); } const executionTime = Date.now() - startTime; if (this.config.logging.enabled) { this.logExecution(hook, result, executionTime); } return result; } catch (error) { const executionTime = Date.now() - startTime; const errorResult = { success: false, error: error instanceof Error ? error.message : String(error) }; if (this.config.logging.enabled) { this.logExecution(hook, errorResult, executionTime); } return errorResult; } } /** * Execute built-in hook */ async executeBuiltin(hook, context, additionalContext) { if (!hook.handler.builtin) { throw new Error('Builtin hook type not specified'); } return BuiltinHooks.execute(hook.handler.builtin, context, additionalContext); } /** * Execute JavaScript hook */ async executeJavaScript(hook, context, additionalContext) { const code = hook.handler.script || (hook.handler.file ? await this.loadFile(hook.handler.file) : ''); if (!code) { throw new Error('No JavaScript code provided'); } // Create isolated context for hook execution const hookContext = { context, additionalContext, console: { log: (...args) => log.debug(`[Hook ${hook.id}]`, ...args), warn: (...args) => log.warn(`[Hook ${hook.id}]`, ...args), error: (...args) => log.error(`[Hook ${hook.id}]`, ...args) }, require: (moduleName) => { // Whitelist of allowed modules for security const allowedModules = ['path', 'fs', 'crypto', 'util', 'zod']; if (allowedModules.includes(moduleName)) { return require(moduleName); } throw new Error(`Module ${moduleName} not allowed in hooks`); } }; // Execute with timeout const result = await this.executeWithTimeout(async () => { const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor; const hookFunction = new AsyncFunction('hookContext', code); return await hookFunction(hookContext); }, hook.timeout); return this.normalizeResult(result); } /** * Execute Python hook */ async executePython(hook, context, additionalContext) { const script = hook.handler.script || hook.handler.file; if (!script) { throw new Error('No Python script provided'); } // Prepare input data const inputData = { hook_id: hook.id, context, additional_context: additionalContext, tool_name: additionalContext?.toolName, tool_input: additionalContext?.toolInput }; // Execute Python script const result = await this.executeProcess('python3', ['-c', ` import json import sys # Read input data input_data = json.loads('${JSON.stringify(inputData)}') # User hook code ${script} # Default result if hook doesn't return anything if 'result' not in locals(): result = {"success": True} # Output result print(json.dumps(result)) `], JSON.stringify(inputData)); try { return JSON.parse(result.stdout); } catch (error) { throw new Error(`Invalid JSON result from Python hook: ${result.stdout}`); } } /** * Execute shell hook */ async executeShell(hook, context, additionalContext) { const script = hook.handler.script || (hook.handler.file ? await this.loadFile(hook.handler.file) : ''); if (!script) { throw new Error('No shell script provided'); } // Set environment variables for the hook const env = { ...process.env, HOOK_ID: hook.id, HOOK_CONTEXT: JSON.stringify(context), HOOK_ADDITIONAL_CONTEXT: JSON.stringify(additionalContext || {}), REPO_PATH: context.repoPath, PROVIDER: context.provider, MODEL: context.model }; const result = await this.executeProcess('sh', ['-c', script], '', env); // Try to parse JSON result, fallback to simple success/error try { return JSON.parse(result.stdout); } catch { return { success: result.code === 0, data: result.stdout, error: result.code !== 0 ? result.stderr : undefined }; } } /** * Execute process with timeout */ async executeProcess(command, args, stdin = '', env) { return new Promise((resolve, reject) => { const child = spawn(command, args, { env: env || process.env, stdio: ['pipe', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; child.stdout?.on('data', (data) => { stdout += data.toString(); }); child.stderr?.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { resolve({ stdout, stderr, code: code || 0 }); }); child.on('error', (error) => { reject(error); }); // Send stdin if provided if (stdin && child.stdin) { child.stdin.write(stdin); child.stdin.end(); } }); } /** * Execute function with timeout */ async executeWithTimeout(fn, timeoutMs) { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Hook execution timeout')), timeoutMs); }); return Promise.race([fn(), timeoutPromise]); } /** * Load file content */ async loadFile(filePath) { try { return await fs.readFile(filePath, 'utf8'); } catch (error) { throw new Error(`Failed to load hook file ${filePath}: ${error}`); } } /** * Normalize hook result to standard format */ normalizeResult(result) { if (typeof result === 'boolean') { return { success: result }; } if (typeof result === 'string') { return { success: true, data: result }; } if (result && typeof result === 'object') { return { success: result.success !== false, data: result.data, error: result.error, suggestions: result.suggestions, transformedInput: result.transformedInput, metadata: result.metadata }; } return { success: true, data: result }; } /** * Log hook execution */ logExecution(hook, result, executionTime) { const logLevel = this.config.logging.level; const logData = { hookId: hook.id, hookName: hook.name, success: result.success, executionTime, error: result.error, suggestions: result.suggestions?.length || 0, timestamp: new Date().toISOString() }; if (result.success) { if (logLevel === 'debug' || logLevel === 'info') { log.debug(`Hook executed: ${hook.id} (${executionTime}ms)`, logData); } } else { log.warn(`Hook failed: ${hook.id}`, logData); } // Write to log file if configured if (this.config.logging.logFile) { this.writeToLogFile(logData); } } /** * Write execution log to file */ async writeToLogFile(logData) { try { const logEntry = JSON.stringify(logData) + '\n'; await fs.appendFile(this.config.logging.logFile, logEntry); } catch (error) { log.warn('Failed to write hook log to file:', error); } } }