/** * Atomic file writes for oh-my-claudecode hooks. * Self-contained module with no external dependencies. * * Mirrors templates/hooks/lib/atomic-write.mjs for use by plugin hook scripts. */ import { openSync, writeSync, fsyncSync, closeSync, renameSync, unlinkSync, mkdirSync, existsSync } from 'fs'; import { dirname, basename, join } from 'path'; import { randomUUID } from 'crypto'; /** * Ensure directory exists */ export function ensureDirSync(dir) { if (existsSync(dir)) { return; } try { mkdirSync(dir, { recursive: true }); } catch (err) { if (err.code === 'EEXIST') { return; } throw err; } } /** * Write string content atomically to a file. * Uses temp file + atomic rename pattern with fsync for durability. * * @param {string} filePath Target file path * @param {string} content String content to write */ export function atomicWriteFileSync(filePath, content) { const dir = dirname(filePath); const base = basename(filePath); const tempPath = join(dir, `.${base}.tmp.${randomUUID()}`); let fd = null; let success = false; try { // Ensure parent directory exists ensureDirSync(dir); // Open temp file with exclusive creation (O_CREAT | O_EXCL | O_WRONLY) fd = openSync(tempPath, 'wx', 0o600); // Write content writeSync(fd, content, 0, 'utf-8'); // Sync file data to disk before rename fsyncSync(fd); // Close before rename closeSync(fd); fd = null; // Atomic rename - replaces target file if it exists renameSync(tempPath, filePath); success = true; // Best-effort directory fsync to ensure rename is durable try { const dirFd = openSync(dir, 'r'); try { fsyncSync(dirFd); } finally { closeSync(dirFd); } } catch { // Some platforms don't support directory fsync - that's okay } } finally { // Close fd if still open if (fd !== null) { try { closeSync(fd); } catch { // Ignore close errors } } // Clean up temp file on error if (!success) { try { unlinkSync(tempPath); } catch { // Ignore cleanup errors } } } }