import { createHash } from "node:crypto"; import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; type JsonRecord = Record; const isErrnoException = (error: unknown): error is NodeJS.ErrnoException => error instanceof Error && "code" in error; export const ensureDirectory = async (path: string) => { await mkdir(path, { recursive: true }); }; export const atomicWriteFile = async (path: string, content: string) => { await ensureDirectory(dirname(path)); const tempPath = `${path}.${process.pid}.${Date.now().toString(36)}.tmp`; await writeFile(tempPath, content, "utf8"); await rename(tempPath, path); }; export const atomicWriteJson = async (path: string, value: JsonRecord | unknown[]) => { await atomicWriteFile(path, JSON.stringify(value, null, 2)); }; export const readJsonFile = async (path: string): Promise => { try { const content = await readFile(path, "utf8"); return JSON.parse(content) as T; } catch (error) { if (isErrnoException(error) && error.code === "ENOENT") { return null; } throw error; } }; export const readTextFile = async (path: string): Promise => { try { return await readFile(path, "utf8"); } catch (error) { if (isErrnoException(error) && error.code === "ENOENT") { return null; } throw error; } }; export const listJsonFiles = async (path: string) => { try { const names = await readdir(path); return names.filter((name) => name.endsWith(".json")).map((name) => join(path, name)); } catch (error) { if (isErrnoException(error) && error.code === "ENOENT") { return []; } throw error; } }; export const listFiles = async (path: string) => { try { const names = await readdir(path); return names.map((name) => join(path, name)); } catch (error) { if (isErrnoException(error) && error.code === "ENOENT") { return []; } throw error; } }; export const removeFileIfExists = async (path: string) => { try { await rm(path, { force: true }); } catch (error) { if (isErrnoException(error) && error.code === "ENOENT") { return; } throw error; } }; export const getFileStat = async (path: string) => { try { return await stat(path); } catch (error) { if (isErrnoException(error) && error.code === "ENOENT") { return null; } throw error; } }; export const toScopedKey = (prefix: string, value?: string) => { const normalized = value?.trim() || `${prefix}-default`; return `${prefix}-${createHash("sha256").update(normalized).digest("hex").slice(0, 16)}`; }; export const toActorKey = (userId?: string) => toScopedKey("actor", userId); export const toProjectKey = (projectId?: string) => toScopedKey("project", projectId); export const toStableId = (...parts: string[]) => createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 24); export const slugify = (value: string) => value .toLowerCase() .replace(/[^a-z0-9._-]+/g, "-") .replace(/^-+|-+$/g, "") .slice(0, 64) || "entry";