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