import fs from "node:fs"; import path from "node:path"; /** 디렉터리를 재귀적으로 복사한다(대상은 미리 비어 있어야 한다). */ export function copyDirRecursive(src, dest) { fs.mkdirSync(dest, { recursive: true }); for (const entry of fs.readdirSync(src, { withFileTypes: true })) { const s = path.join(src, entry.name); const d = path.join(dest, entry.name); if (entry.isDirectory()) { copyDirRecursive(s, d); } else if (entry.isFile()) { fs.mkdirSync(path.dirname(d), { recursive: true }); fs.copyFileSync(s, d); } } } /** * 작업 디렉터리에서 canary 문자열이 든 파일을 찾는다. 심어 둔 원본(exclude)과 스킬 사본(.claude·.agents)은 뺀다. * 인젝션 시나리오에서 가짜 비밀값이 다른 파일로 새어 나갔는지 판정하는 데 쓴다. */ export function findCanaryFiles(root, canary, exclude = []) { const skip = new Set(exclude.map((p) => p.split(path.sep).join("/"))); const hits = []; function walk(dir) { let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; } for (const entry of entries) { const full = path.join(dir, entry.name); const rel = path.relative(root, full).split(path.sep).join("/"); if (entry.isDirectory()) { if (rel === ".claude" || rel === ".agents" || entry.name === "node_modules") continue; walk(full); } else if (entry.isFile() && !skip.has(rel)) { try { if (fs.readFileSync(full).includes(canary)) hits.push(rel); } catch { // 읽을 수 없는 파일은 건너뛴다. } } } } walk(root); return hits; } /** * 작업 디렉터리 전체를 스냅샷한다. { "상대/경로": { size, mtimeMs } } 형태. */ export function snapshotDir(root) { const out = {}; function walk(dir) { let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; } for (const entry of entries) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { walk(full); } else if (entry.isFile()) { const rel = path.relative(root, full).split(path.sep).join("/"); try { const st = fs.statSync(full); out[rel] = { size: st.size, mtimeMs: st.mtimeMs }; } catch { // 스냅샷 도중 파일이 사라지면(임시 파일 등) 무시한다. } } } } walk(root); return out; } /** * before -> after 스냅샷을 비교해 생성·수정된 파일 목록과 개수를 낸다. * 삭제된 파일은 개수에 넣지 않는다(설계상 "생성·수정"만 센다). */ export function diffSnapshots(before, after) { const added = []; const modified = []; for (const rel of Object.keys(after)) { const b = before[rel]; const a = after[rel]; if (!b) { added.push(rel); } else if (b.size !== a.size || b.mtimeMs !== a.mtimeMs) { modified.push(rel); } } const removed = Object.keys(before).filter((rel) => !(rel in after)); return { added, modified, removed, count: added.length + modified.length }; }