bookworm-smart-assistant/scripts/patches/patch-c2-exit-code-normalize.js
Bookworm Admin b7a8e29d21 release: v6.7.0 - OTA E2E test release
- VERSION file as authoritative version source
- export.mjs reads VERSION with package.json fallback
- bw-ota.ps1 DryRun mode for safe testing
- auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
2026-04-27 17:59:44 +08:00

149 lines
5.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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