UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

121 lines 4.8 kB
/** * Script Runner * * Utility for running framework scripts from handlers. * Script execution utility for CLI handlers. * * @implements @.aiwg/architecture/decisions/ADR-001-unified-extension-system.md * @source @src/cli/router.ts * @issue #33 */ import { spawn } from 'child_process'; import path from 'path'; /** * Default script runner implementation * * Delegates command execution to existing Node.js scripts. */ export class DefaultScriptRunner { frameworkRoot; constructor(frameworkRoot) { this.frameworkRoot = frameworkRoot; } /** * Run a Node.js script from the framework * * @param scriptPath - Relative path to script from framework root * @param args - Arguments to pass to script * @param options - Execution options */ async run(scriptPath, args = [], options = {}) { const fullPath = path.join(this.frameworkRoot, scriptPath); return new Promise((resolve) => { // When capture mode is enabled, stdout/stderr are piped back to the // caller so that quiet-mode callers can suppress output. When not // capturing, stdio: 'inherit' lets subprocess output flow directly to // the terminal (verbose mode). const stdio = options.capture ? 'pipe' : 'inherit'; const child = spawn('node', [fullPath, ...args], { stdio, cwd: options.cwd || process.cwd(), env: { ...process.env, ...options.env }, }); let capturedStderr = ''; if (options.capture && child.stderr) { child.stderr.on('data', (data) => { capturedStderr += data.toString(); }); } const timeout = options.timeout; let timeoutId; let killEscalationId; // Hard deadline for a well-behaved child to respond to SIGTERM before we // escalate to SIGKILL. Prevents a stuck child from outliving the CLI and // masking a timeout as a hang. Overridable via AIWG_SIGKILL_DEADLINE_MS. const killDeadlineMs = (() => { const raw = process.env['AIWG_SIGKILL_DEADLINE_MS']; const n = raw ? parseInt(raw, 10) : NaN; return Number.isFinite(n) && n > 0 ? n : 10_000; })(); if (timeout) { timeoutId = setTimeout(() => { // Request graceful termination first, then escalate if the child // does not exit within the deadline. Both kill() calls are wrapped // because they can throw ESRCH on an already-exited child. try { child.kill('SIGTERM'); } catch { /* already exited */ } killEscalationId = setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* already exited */ } }, killDeadlineMs); killEscalationId.unref?.(); resolve({ exitCode: 124, message: `Script timed out after ${timeout}ms`, error: new Error(`Timeout after ${timeout}ms (child sent SIGTERM, SIGKILL after ${killDeadlineMs}ms)`), }); }, timeout); } child.on('close', (code) => { if (timeoutId) clearTimeout(timeoutId); if (killEscalationId) clearTimeout(killEscalationId); // On failure in capture mode, surface the captured stderr if (options.capture && code !== 0 && capturedStderr) { resolve({ exitCode: code ?? 1, message: capturedStderr.trim(), }); } else { resolve({ exitCode: code ?? 0, }); } }); child.on('error', (err) => { if (timeoutId) clearTimeout(timeoutId); if (killEscalationId) clearTimeout(killEscalationId); resolve({ exitCode: 1, message: `Script error: ${err.message}`, error: err, }); }); }); } } /** * Create a script runner for the given framework root */ export function createScriptRunner(frameworkRoot) { return new DefaultScriptRunner(frameworkRoot); } //# sourceMappingURL=script-runner.js.map