import { stat } from "node:fs/promises"; async function statSafe(path: string): Promise { try { return await stat(path); } catch (error) { if (error && typeof error === "object" && "code" in error && (error as { code?: string }).code === "ENOENT") { return null; } throw error; } } export async function pathExists(path: string): Promise { return (await statSafe(path)) !== null; } export async function fileExists(path: string): Promise { const st = await statSafe(path); return st?.isFile() ?? false; } export async function dirExists(path: string): Promise { const st = await statSafe(path); return st?.isDirectory() ?? false; } export async function readText(path: string): Promise { return await Bun.file(path).text(); } export async function writeText(path: string, data: string, mode?: number): Promise { await Bun.write(path, data); if (mode !== undefined) { const result = Bun.spawnSync(["chmod", mode.toString(8), path], { stdout: "pipe", stderr: "pipe" }); if (!result.success) { throw new Error(`chmod failed for ${path}: ${result.stderr.toString()}`); } } } export async function writeTextAtomic( path: string, data: string, options: { mode: number; owner: string; group: string; } ): Promise { const dir = path.replace(/\/[^/]+$/, "") || "."; const base = path.split("/").pop() || "tmp"; const tmp = `${dir}/.${base}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`; await Bun.write(tmp, data); const chmodResult = Bun.spawnSync(["chmod", options.mode.toString(8), tmp], { stdout: "pipe", stderr: "pipe" }); if (!chmodResult.success) { throw new Error(`chmod failed for ${tmp}: ${chmodResult.stderr.toString()}`); } const chownResult = Bun.spawnSync(["chown", `${options.owner}:${options.group}`, tmp], { stdout: "pipe", stderr: "pipe" }); if (!chownResult.success) { throw new Error(`chown failed for ${tmp}: ${chownResult.stderr.toString()}`); } const mvResult = Bun.spawnSync(["mv", "-f", tmp, path], { stdout: "pipe", stderr: "pipe" }); if (!mvResult.success) { throw new Error(`atomic rename failed for ${path}: ${mvResult.stderr.toString()}`); } } export function renderTemplate(template: string, values: Record): string { let rendered = template; for (const [key, value] of Object.entries(values)) { rendered = rendered.replaceAll(`{{${key}}}`, String(value)); } const unresolved = rendered.match(/\{\{\s*[A-Z0-9_]+\s*\}\}/g) ?? []; if (unresolved.length > 0) { const unique = [...new Set(unresolved.map((item) => item.replace(/\s+/g, "")))].sort(); throw new Error(`template render failed: unresolved placeholders: ${unique.join(", ")}`); } return rendered; }