import { lstatSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs"; import { basename, dirname, extname, resolve } from "node:path"; import * as yaml from "js-yaml"; import type { DiagramIRV2 } from "../../model/diagram-ir.js"; import { normalizeDiagramIR, validateDiagramIR } from "../../model/diagram-ir.js"; import { importDrawioToIR } from "./import-drawio.js"; export interface LifecycleActionOptions { spec?: string; base?: string; strict?: boolean; prune?: boolean; dryRun?: boolean; fail?: string; views?: string; kind?: string; property?: string[]; from?: string; to?: string } export function loadStructured(path: string): unknown { return yaml.load(readFileSync(path, "utf8"), { schema: yaml.JSON_SCHEMA }); } export function loadIR(path: string): DiagramIRV2 { return /\.(drawio|xml)$/i.test(path) ? importDrawioToIR(path).ir : normalizeDiagramIR(validateDiagramIR(loadStructured(path))); } export function structuredText(value: unknown, path: string): string { return extname(path).toLowerCase() === ".json" ? `${JSON.stringify(value, null, 2)}\n` : yaml.dump(value, { noRefs: true, sortKeys: false, lineWidth: 120 }); } function canonicalExisting(path: string): string { const absolute = resolve(path); try { return realpathSync(absolute); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return absolute; throw error; } } export function assertOutputSafe(path: string, inputs: string[] = []): string { const output = resolve(path); try { if (lstatSync(output).isSymbolicLink()) throw new Error("Refusing to overwrite a symbolic link"); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; } const canonicalOutput = canonicalExisting(output); if (inputs.some((input) => canonicalExisting(input) === canonicalOutput)) throw new Error("Refusing to overwrite an input path (including traversal aliases)"); return output; } export function atomicWrite(path: string, content: string, inputs: string[] = []): string { const output = assertOutputSafe(path, inputs); const temporary = resolve(dirname(output), `.${basename(output)}.${process.pid}.${Date.now()}.tmp`); try { writeFileSync(temporary, content, { encoding: "utf8", flag: "wx" }); renameSync(temporary, output); } finally { rmSync(temporary, { force: true }); } return output; }