#!/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();