- Create server/src/services/placeholder-service.ts with addEntry, replaceEntry, listEntries - Generates PLACEHOLDERS.md with Active Placeholders and Replaced markdown tables - Add ChatPlaceholderEntry interface to packages/shared/src/types/chat.ts - Export ChatPlaceholderEntry from packages/shared/src/index.ts - Add markAsPlaceholder method to chatFileService in chat-files.ts
161 lines
4.2 KiB
TypeScript
161 lines
4.2 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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);
|
|
},
|
|
};
|
|
}
|