import { lstat, open, rename, unlink } from "node:fs/promises"; import path from "node:path"; import { randomUUID } from "node:crypto"; import { addWorkspaceImport } from "./source.js"; import { readQxSource } from "./source-loader.js"; import { compileWorkspaceRepository, type CapabilityRepositoryResolver } from "./assembly.js"; export const planAtomScaffold = (workspace: string, name: string, id: string) => { if (!/^[A-Z][A-Za-z0-9]*$/.test(name)) throw new Error("Atom name must be PascalCase"); if (!id.trim()) throw new Error("Atom ID must not be empty"); const fileName = `${name}.qx`; return { before: workspace, workspace: addWorkspaceImport(workspace, fileName), fileName, fragment: `fragment {\n atom ${name} id ${JSON.stringify(id)};\n}\n`, }; }; /** Validate an in-memory proposal before creating files. Never replace a fragment. */ export const scaffoldAtom = async (options: { root: string; name: string; id: string; write: boolean; resolveResource: CapabilityRepositoryResolver; }) => { const before = await readQxSource(options.root, "workspace.qx"); const plan = planAtomScaffold(before, options.name, options.id); const fragmentPath = path.join(options.root, plan.fileName); try { await lstat(fragmentPath); throw new Error(`Scaffold target already exists: ${plan.fileName}`); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; } const observed = new Map(); await compileWorkspaceRepository({ rootDirectory: options.root, resolveResource: options.resolveResource, readSource: async (name) => { if (name === "workspace.qx") return plan.workspace; if (name === plan.fileName) return plan.fragment; const text = await readQxSource(options.root, name); observed.set(name, text); return text; }, }); if (!options.write) return plan; const workspacePath = path.join(options.root, "workspace.qx"); const temporary = path.join(options.root, `.qx-scaffold-${randomUUID()}.tmp`); let createdFragment = false; let createdTemporary = false; try { const fragment = await open(fragmentPath, "wx"); createdFragment = true; try { await fragment.writeFile(plan.fragment); } finally { await fragment.close(); } const file = await open(temporary, "wx", (await lstat(workspacePath)).mode); createdTemporary = true; try { await file.writeFile(plan.workspace); } finally { await file.close(); } observed.set("workspace.qx", before); for (const [name, text] of observed) { if ((await readQxSource(options.root, name)) !== text) throw new Error(`QX source changed during scaffolding: ${name}`); } await rename(temporary, workspacePath); createdTemporary = false; createdFragment = false; } finally { if (createdTemporary) await unlink(temporary); if (createdFragment) await unlink(fragmentPath); } return plan; };