bookworm-smart-assistant/hooks/post-edit-snapshot.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

87 lines
3.4 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
/**
* post-edit-snapshot.js · Phase α 冲刺 3 · 2026-04-25
* PostToolUse:Edit|Write hook - snapshot edited file to staging/
* Dormant until feature-flag staging-pipeline.mode != 'off'
*/
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { spawn } = require('child_process');
const ROOT = path.resolve(__dirname, '..');
const FLAG_FILE = path.join(ROOT, 'feature-flags.json');
const PIPELINE_DIR = path.join(ROOT, 'ai-delivery-pipeline');
const STAGING_DIR = path.join(PIPELINE_DIR, 'staging');
const MANIFEST = path.join(PIPELINE_DIR, 'manifest.jsonl');
const VALIDATOR = path.join(__dirname, 'staging-validator.js');
const MAX_FILE_BYTES = 5 * 1024 * 1024;
const HASH_LEN = 12;
function readFlag() {
try {
const f = JSON.parse(fs.readFileSync(FLAG_FILE, 'utf8'));
return f.features && f.features['staging-pipeline'] || { mode: 'off', enabled: false };
} catch (_) { return { mode: 'off', enabled: false }; }
}
function readStdin() {
try { const d = fs.readFileSync(0, 'utf8'); return d ? JSON.parse(d) : {}; }
catch (_) { return {}; }
}
function ensureDir(d) { try { fs.mkdirSync(d, { recursive: true }); } catch (_) {} }
function appendManifest(entry) {
try { fs.appendFileSync(MANIFEST, JSON.stringify(entry) + '\n', 'utf8'); } catch (_) {}
}
function getSessionId(input) {
return input.session_id || process.env.CLAUDE_SESSION_ID || 'no-session';
}
function main() {
const t0 = Date.now();
const flag = readFlag();
if (flag.mode === 'off') process.exit(0);
const input = readStdin();
const tool = input.tool_name || '';
if (!/^(Edit|Write|NotebookEdit)$/.test(tool)) process.exit(0);
const filePath = (input.tool_input && input.tool_input.file_path) || '';
if (!filePath) process.exit(0);
if (filePath.includes('ai-delivery-pipeline')) process.exit(0);
let stat;
try { stat = fs.statSync(filePath); } catch (_) { process.exit(0); }
if (!stat.isFile()) process.exit(0);
if (stat.size > MAX_FILE_BYTES) {
appendManifest({ ts: new Date().toISOString(), event: 'skip-oversize', sessionId: getSessionId(input), originalPath: filePath, size: stat.size, cap: MAX_FILE_BYTES });
process.exit(0);
}
let content;
try { content = fs.readFileSync(filePath); } catch (_) { process.exit(0); }
const hash = crypto.createHash('sha256').update(content).digest('hex').slice(0, HASH_LEN);
const sessionId = getSessionId(input);
const stagingAbsDir = path.join(STAGING_DIR, sessionId, hash);
ensureDir(stagingAbsDir);
const stagingPath = path.join(stagingAbsDir, path.basename(filePath));
try { fs.writeFileSync(stagingPath, content); }
catch (e) {
appendManifest({ ts: new Date().toISOString(), event: 'snapshot-write-fail', originalPath: filePath, error: e.code || e.message });
process.exit(0);
}
appendManifest({
ts: new Date().toISOString(), event: 'staged', sessionId, hash, tool,
originalPath: filePath, stagingPath, status: 'pending', size: content.length,
elapsedMs: Date.now() - t0,
});
if (flag.mode === 'warn' || flag.mode === 'enforce') {
if (fs.existsSync(VALIDATOR)) {
try {
const child = spawn(process.execPath, [VALIDATOR, stagingPath, filePath, hash, flag.mode], {
detached: true, stdio: 'ignore', windowsHide: true,
});
child.unref();
} catch (_) {}
}
}
process.exit(0);
}
try { main(); } catch (_) { process.exit(0); }