import { readFile, writeFile, mkdir } from "node:fs/promises"; import { existsSync } from "node:fs"; import path from "node:path"; export interface PlaceholderEntry { fileId: string; filename: string; description: string; replacedByFileId?: string; } const MANIFEST_FILENAME = "PLACEHOLDERS.md"; function serialize(active: PlaceholderEntry[], replaced: PlaceholderEntry[]): string { const activeRows = active.length === 0 ? "_None_" : active.map((e) => `| ${e.filename} | ${e.description} | ${e.fileId} |`).join("\n"); const replacedRows = replaced.length === 0 ? "_None_" : replaced .map((e) => `| ${e.filename} | ${e.description} | ${e.replacedByFileId ?? ""} |`) .join("\n"); return `# Placeholder Assets Auto-maintained by Nexus. Do not edit manually. ## Active Placeholders | File | Description | File ID | |------|-------------|---------| ${activeRows} ## Replaced | Original | Description | Replaced By | |----------|-------------|-------------| ${replacedRows} `; } function parse(content: string): { active: PlaceholderEntry[]; replaced: PlaceholderEntry[] } { const active: PlaceholderEntry[] = []; const replaced: PlaceholderEntry[] = []; let section: "active" | "replaced" | null = null; const rowRe = /^\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|$/; for (const line of content.split("\n")) { if (line.includes("## Active Placeholders")) { section = "active"; continue; } if (line.includes("## Replaced")) { section = "replaced"; continue; } if (!section) continue; const match = rowRe.exec(line); if (!match) continue; const [, col1, col2, col3] = match; // Skip header row and separator row if ( col1 === "File" || col1 === "Original" || col1 === "---" || col1?.startsWith("-") ) { continue; } // Skip "_None_" placeholder rows if (col1?.trim() === "_None_") continue; if (section === "active") { active.push({ filename: col1 ?? "", description: col2 ?? "", fileId: col3 ?? "", }); } else { replaced.push({ filename: col1 ?? "", description: col2 ?? "", replacedByFileId: col3 ?? undefined, fileId: col3 ?? "", }); } } return { active, replaced }; } async function readManifest(projectDir: string): Promise<{ active: PlaceholderEntry[]; replaced: PlaceholderEntry[] }> { const manifestPath = path.join(projectDir, MANIFEST_FILENAME); if (!existsSync(manifestPath)) { return { active: [], replaced: [] }; } const content = await readFile(manifestPath, "utf-8"); return parse(content); } async function writeManifest( projectDir: string, active: PlaceholderEntry[], replaced: PlaceholderEntry[], ): Promise { await mkdir(projectDir, { recursive: true }); const manifestPath = path.join(projectDir, MANIFEST_FILENAME); await writeFile(manifestPath, serialize(active, replaced), "utf-8"); } export function placeholderService() { return { async addEntry( projectDir: string, entry: { fileId: string; filename: string; description: string }, ): Promise { const { active, replaced } = await readManifest(projectDir); // Avoid duplicates if (!active.some((e) => e.fileId === entry.fileId)) { active.push({ fileId: entry.fileId, filename: entry.filename, description: entry.description, }); } await writeManifest(projectDir, active, replaced); }, async replaceEntry( projectDir: string, oldFileId: string, newFileId: string, ): Promise { const { active, replaced } = await readManifest(projectDir); const idx = active.findIndex((e) => e.fileId === oldFileId); if (idx === -1) return; const [entry] = active.splice(idx, 1); replaced.push({ fileId: entry!.fileId, filename: entry!.filename, description: entry!.description, replacedByFileId: newFileId, }); await writeManifest(projectDir, active, replaced); }, async listEntries( projectDir: string, ): Promise<{ active: PlaceholderEntry[]; replaced: PlaceholderEntry[] }> { return readManifest(projectDir); }, }; }