- 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)
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();
|