mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-21 00:58:07 +02:00
* fix(hooks): keep silent hook paths silent * fix(hooks): harden stream failure handling * fix(hooks): settle interrupted input streams * test(hooks): name stream input limits * test(hooks): isolate PostToolUse dispatcher fixtures
121 lines
3.8 KiB
JavaScript
121 lines
3.8 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const { spawnSync } = require('child_process');
|
|
const { normalizePluginRootForPlatform } = require('../lib/resolve-ecc-root');
|
|
const { readStdinRaw, resolveMaxStdin } = require('./hook-input');
|
|
|
|
const DEFAULT_TIMEOUT_MS = 30000;
|
|
const MAX_TIMEOUT_MS = 300000;
|
|
|
|
function writeStderr(text) {
|
|
if (typeof text !== 'string' || text.length === 0) return;
|
|
process.stderr.write(text.endsWith('\n') ? text : `${text}\n`);
|
|
}
|
|
|
|
function resolveTimeout(value) {
|
|
const parsed = Number(value);
|
|
if (!Number.isSafeInteger(parsed) || parsed <= 0) return DEFAULT_TIMEOUT_MS;
|
|
return Math.min(parsed, MAX_TIMEOUT_MS);
|
|
}
|
|
|
|
function exitAfterFlush(stdout, stderr, exitCode) {
|
|
process.exitCode = exitCode;
|
|
let pendingWrites = 2;
|
|
const finish = () => {
|
|
pendingWrites -= 1;
|
|
if (pendingWrites === 0) process.exit(exitCode);
|
|
};
|
|
|
|
// Empty writes still queue callbacks behind any earlier diagnostics on the
|
|
// same stream, so both streams are drained before the explicit exit.
|
|
process.stdout.write(stdout || '', finish);
|
|
process.stderr.write(stderr || '', finish);
|
|
}
|
|
|
|
async function main() {
|
|
const [, , hookId, relScriptPath, profilesCsv, timeoutValue] = process.argv;
|
|
const maxStdin = resolveMaxStdin(process.env.ECC_HOOK_INPUT_MAX_BYTES, {
|
|
writeDiagnostic: message => process.stderr.write(message)
|
|
});
|
|
const { raw, truncated } = await readStdinRaw(process.stdin, { maxStdin });
|
|
|
|
if (!hookId || !relScriptPath) {
|
|
writeStderr('[Hook] lifecycle bootstrap missing hook ID or script path; skipping hook');
|
|
process.exitCode = 0;
|
|
return;
|
|
}
|
|
|
|
const pluginRoot = normalizePluginRootForPlatform(
|
|
process.env.CLAUDE_PLUGIN_ROOT || process.env.ECC_PLUGIN_ROOT
|
|
);
|
|
if (!pluginRoot) {
|
|
writeStderr('[Hook] lifecycle bootstrap could not resolve ECC plugin root; skipping hook');
|
|
process.exitCode = 0;
|
|
return;
|
|
}
|
|
const resolvedRoot = path.resolve(pluginRoot);
|
|
const runner = path.resolve(resolvedRoot, 'scripts', 'hooks', 'run-with-flags.js');
|
|
if (!runner.startsWith(resolvedRoot + path.sep) || !fs.existsSync(runner)) {
|
|
writeStderr('[Hook] lifecycle bootstrap could not resolve ECC plugin root; skipping hook');
|
|
process.exitCode = 0;
|
|
return;
|
|
}
|
|
|
|
if (truncated) {
|
|
writeStderr(`[Hook] lifecycle stdin exceeded ${maxStdin} bytes; forwarded a bounded prefix`);
|
|
}
|
|
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[runner, hookId, relScriptPath, profilesCsv || 'minimal,standard,strict'],
|
|
{
|
|
input: raw,
|
|
encoding: 'utf8',
|
|
env: {
|
|
...process.env,
|
|
CLAUDE_PLUGIN_ROOT: resolvedRoot,
|
|
ECC_PLUGIN_ROOT: resolvedRoot,
|
|
ECC_HOOK_INPUT_MAX_BYTES: String(maxStdin),
|
|
ECC_HOOK_INPUT_TRUNCATED_UPSTREAM: truncated ? '1' : '0'
|
|
},
|
|
cwd: process.cwd(),
|
|
timeout: resolveTimeout(timeoutValue),
|
|
maxBuffer: 16 * 1024 * 1024,
|
|
windowsHide: true
|
|
}
|
|
);
|
|
|
|
const failed = result.error || result.status === null || result.signal;
|
|
const stdout = !failed && typeof result.stdout === 'string' && result.stdout !== raw
|
|
? result.stdout
|
|
: '';
|
|
let stderr = typeof result.stderr === 'string' ? result.stderr : '';
|
|
let exitCode = Number.isInteger(result.status) ? result.status : 0;
|
|
|
|
if (failed) {
|
|
const reason = result.error
|
|
? result.error.message
|
|
: result.signal
|
|
? `signal ${result.signal}`
|
|
: 'missing exit status';
|
|
stderr += `[Hook] lifecycle runner failed for ${hookId}: ${reason}\n`;
|
|
exitCode = 1;
|
|
}
|
|
|
|
exitAfterFlush(stdout, stderr, exitCode);
|
|
}
|
|
|
|
function cli() {
|
|
main().catch(error => {
|
|
writeStderr(`[Hook] lifecycle bootstrap failed: ${error.message}`);
|
|
process.exitCode = 0;
|
|
});
|
|
}
|
|
|
|
if (require.main === module) cli();
|
|
|
|
module.exports = { cli, exitAfterFlush, main, resolveTimeout };
|