/* RFC-0362: Atomic write helpers — temp-file + rename for files, staging dir + rename for directories. Do not implement distributed transaction coordination. RFC-0362: initial atomic write helpers (atomicWriteFile, atomicMoveDir). RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. */ import path from "node:path"; import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; export async function atomicWriteFile( targetPath: string, content: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const dir = path.dirname(targetPath); const tmpPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`; if (!(await io.exists(dir))) { await io.mkdir(dir); } await io.writeFile(tmpPath, content); try { await io.rename(tmpPath, targetPath); } catch (err) { try { await io.rm(tmpPath); } catch { // ignore } throw err; } } const RENAME_RETRYABLE_CODES = new Set(["EPERM", "EACCES", "EBUSY", "ENOTEMPTY"]); async function renameWithRetry( src: string, dest: string, maxRetries = 5, io: WorkspaceIO = createDefaultIO().io, ): Promise { let lastErr: unknown; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { await io.rename(src, dest); return; } catch (err) { lastErr = err; const code = (err as NodeJS.ErrnoException).code; if (!code || !RENAME_RETRYABLE_CODES.has(code)) throw err; if (attempt < maxRetries) { const delay = 50 * Math.pow(2, attempt); await new Promise((r) => setTimeout(r, delay)); } } } throw lastErr; } export async function atomicMoveDir( stagingDir: string, targetDir: string, options?: { replace?: boolean }, io: WorkspaceIO = createDefaultIO().io, ): Promise { let trashDir: string | undefined; if (await io.exists(targetDir)) { if (!options?.replace) { throw new Error( `[werkstatt.atomic] target '${targetDir}' already exists — use --replace to overwrite`, ); } // On Windows, fs.rm often fails with EBUSY on directories containing // symlinks/junctions (e.g. pnpm node_modules). Renaming usually succeeds // where deletion fails. Strategy: rename old target to trash, then rename // staging into place, then best-effort delete trash. trashDir = `${targetDir}.trash-${process.pid}-${Date.now()}`; if (await io.exists(trashDir)) { await io.rm(trashDir, { recursive: true }).catch(() => {}); } await renameWithRetry(targetDir, trashDir); } const parentDir = path.dirname(targetDir); if (!(await io.exists(parentDir))) { await io.mkdir(parentDir); } await renameWithRetry(stagingDir, targetDir); // Best-effort cleanup of the old directory if (trashDir && (await io.exists(trashDir))) { await io.rm(trashDir, { recursive: true }).catch(() => { // Ignore — trash will be cleaned up on next run or manually. }); } } export function resolveStagingDir( workspaceRoot: string, targetDir: string, operationId: string, ): string { const relativeTarget = path.relative(workspaceRoot, targetDir); const safeName = relativeTarget.replace(/[^a-zA-Z0-9/_-]/g, "_"); return path.join(workspaceRoot, `${safeName}.staging-${operationId}`); }