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
JavaScript
/**
* 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