149 lines
5.5 KiB
JavaScript
149 lines
5.5 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
/**
|
|||
|
|
* C2 补丁 — 2026-04-25 audit-2026-04-25 CRITICAL
|
|||
|
|
*
|
|||
|
|
* 目标: 规范两个 staging-pipeline 钩子的异常退出码 (exit(1) → exit(2))
|
|||
|
|
* - hooks/rollback-on-fail.js: L72 (bad-args), L97 (crash)
|
|||
|
|
* - hooks/staging-validator.js: L80 (bad-args), L112 (crash)
|
|||
|
|
*
|
|||
|
|
* 修复原理:
|
|||
|
|
* Claude Code hook 规范: exit(0)=通过, exit(2)=阻断并向 Claude 反馈 stderr,
|
|||
|
|
* 其它非零码=非阻断错误。bad-args 与 crash 场景应显式返回 2,
|
|||
|
|
* 当前未注册所以无实际阻断,但注册后会导致反馈异常。
|
|||
|
|
*
|
|||
|
|
* 保留不动:
|
|||
|
|
* - rollback-on-fail.js L93 `process.exit(restored ? 0 : 1)` 是成功/失败三态语义,非阻断
|
|||
|
|
* - staging-validator.js L108 `process.exit(passed ? 0 : 2)` 已正确
|
|||
|
|
*
|
|||
|
|
* 幂等: 通过 SENTINEL 字符串检测,已打过则跳过。
|
|||
|
|
* 安全: .bak 备份 + tmp+rename 原子写 (与 patch-c1 同模板)。
|
|||
|
|
*/
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
const HOOKS_DIR = path.join(__dirname, '..', '..', 'hooks');
|
|||
|
|
const SENTINEL = '/* patch-c2-exit-code-normalize:v1 */';
|
|||
|
|
|
|||
|
|
const TARGETS = [
|
|||
|
|
{
|
|||
|
|
file: path.join(HOOKS_DIR, 'rollback-on-fail.js'),
|
|||
|
|
replacements: [
|
|||
|
|
{
|
|||
|
|
from: "appendManifest({ ts: new Date().toISOString(), event: 'rollback-bad-args' });\n process.exit(1);",
|
|||
|
|
to: "appendManifest({ ts: new Date().toISOString(), event: 'rollback-bad-args' });\n process.exit(2);",
|
|||
|
|
label: 'rollback-bad-args',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
from: "appendManifest({ ts: new Date().toISOString(), event: 'rollback-crash', error: String(e).slice(0, 200) });\n process.exit(1);",
|
|||
|
|
to: "appendManifest({ ts: new Date().toISOString(), event: 'rollback-crash', error: String(e).slice(0, 200) });\n process.exit(2);",
|
|||
|
|
label: 'rollback-crash',
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
file: path.join(HOOKS_DIR, 'staging-validator.js'),
|
|||
|
|
replacements: [
|
|||
|
|
{
|
|||
|
|
from: "appendManifest({ ts: new Date().toISOString(), event: 'validator-bad-args' });\n process.exit(1);",
|
|||
|
|
to: "appendManifest({ ts: new Date().toISOString(), event: 'validator-bad-args' });\n process.exit(2);",
|
|||
|
|
label: 'validator-bad-args',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
from: "appendManifest({ ts: new Date().toISOString(), event: 'validator-crash', error: String(e).slice(0, 200) });\n process.exit(1);",
|
|||
|
|
to: "appendManifest({ ts: new Date().toISOString(), event: 'validator-crash', error: String(e).slice(0, 200) });\n process.exit(2);",
|
|||
|
|
label: 'validator-crash',
|
|||
|
|
},
|
|||
|
|
],
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
function atomicWrite(file, content) {
|
|||
|
|
const tmp = file + '.tmp.' + process.pid + '.' + Date.now();
|
|||
|
|
fs.writeFileSync(tmp, content);
|
|||
|
|
fs.renameSync(tmp, file);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function patchOne(target) {
|
|||
|
|
const { file, replacements } = target;
|
|||
|
|
const rel = path.relative(path.join(__dirname, '..', '..'), file);
|
|||
|
|
|
|||
|
|
if (!fs.existsSync(file)) {
|
|||
|
|
console.error('[patch-c2] 目标文件不存在:', rel);
|
|||
|
|
return { file: rel, status: 'missing', applied: 0 };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const before = fs.readFileSync(file, 'utf8');
|
|||
|
|
|
|||
|
|
if (before.includes(SENTINEL)) {
|
|||
|
|
console.log('[patch-c2] ' + rel + ' 已打过补丁 (sentinel),跳过');
|
|||
|
|
return { file: rel, status: 'skipped', applied: 0 };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let after = before;
|
|||
|
|
const applied = [];
|
|||
|
|
const missing = [];
|
|||
|
|
|
|||
|
|
for (const rep of replacements) {
|
|||
|
|
if (after.includes(rep.from)) {
|
|||
|
|
after = after.replace(rep.from, rep.to);
|
|||
|
|
applied.push(rep.label);
|
|||
|
|
} else if (after.includes(rep.to)) {
|
|||
|
|
// 已是目标状态 (手动改过或半幂等场景)
|
|||
|
|
applied.push(rep.label + ' [already-correct]');
|
|||
|
|
} else {
|
|||
|
|
missing.push(rep.label);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (applied.length === 0) {
|
|||
|
|
console.error('[patch-c2] ' + rel + ' 未找到任何锚点,可能文件已大幅修改。missing=' + missing.join(','));
|
|||
|
|
return { file: rel, status: 'no-anchor', applied: 0 };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 注入 sentinel 到文件顶部注释块(紧跟 'use strict' 或文件开头)
|
|||
|
|
const sentinelComment = SENTINEL + '\n';
|
|||
|
|
if (!after.includes(SENTINEL)) {
|
|||
|
|
if (after.startsWith("'use strict';")) {
|
|||
|
|
after = after.replace("'use strict';", "'use strict';\n" + sentinelComment.trim());
|
|||
|
|
} else {
|
|||
|
|
after = sentinelComment + after;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (after === before) {
|
|||
|
|
console.log('[patch-c2] ' + rel + ' 无需改动');
|
|||
|
|
return { file: rel, status: 'noop', applied: 0 };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// .bak 备份 (同目录) — hooks/ 受保护,退化到 scripts/patches/bak/
|
|||
|
|
const bakDir = path.join(__dirname, 'bak');
|
|||
|
|
if (!fs.existsSync(bakDir)) fs.mkdirSync(bakDir, { recursive: true });
|
|||
|
|
const bakFile = path.join(bakDir, path.basename(file) + '.c2.' + Date.now() + '.bak');
|
|||
|
|
fs.writeFileSync(bakFile, before);
|
|||
|
|
|
|||
|
|
atomicWrite(file, after);
|
|||
|
|
|
|||
|
|
console.log('[patch-c2] ' + rel + ' ✓ applied=[' + applied.join(', ') + '] bak=' + path.relative(path.join(__dirname, '..', '..'), bakFile));
|
|||
|
|
if (missing.length > 0) {
|
|||
|
|
console.warn('[patch-c2] ' + rel + ' ⚠ missing=' + missing.join(','));
|
|||
|
|
}
|
|||
|
|
return { file: rel, status: 'applied', applied: applied.length, missing };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function main() {
|
|||
|
|
console.log('=== patch-c2-exit-code-normalize ===');
|
|||
|
|
const results = TARGETS.map(patchOne);
|
|||
|
|
const summary = {
|
|||
|
|
applied: results.filter(r => r.status === 'applied').length,
|
|||
|
|
skipped: results.filter(r => r.status === 'skipped').length,
|
|||
|
|
failed: results.filter(r => r.status === 'missing' || r.status === 'no-anchor').length,
|
|||
|
|
};
|
|||
|
|
console.log('=== 完成 ===');
|
|||
|
|
console.log(JSON.stringify(summary, null, 2));
|
|||
|
|
process.exit(summary.failed > 0 ? 1 : 0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
main();
|