87 lines
3.4 KiB
JavaScript
87 lines
3.4 KiB
JavaScript
|
|
#!/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); }
|