import { readdirSync, readFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { createLogger } from "./logger.js"; const log = createLogger({ component: "manifest-loader" }); /** * ModuleManifest — the shape of a module's JSON manifest file. * DO NOT CHANGE: gallery UI depends on this exact shape. (Phase 70, MOD-06) */ export interface ModuleManifest { id: string; name: string; tagline: string; description: string; icon: string; // lucide-react icon name price: "free" | "paid" | "coming_soon"; faq: Array<{ question: string; answer: string }>; configSchema: Array<{ key: string; label: string; tooltip: string; // Friendly, non-technical (MOD-05) type: "text" | "number" | "boolean" | "select" | "multiselect"; options?: string[]; default: unknown; }>; defaultConfig: Record; rating?: number; // Static, defaults to 4.8 if absent } /** * Scan src/modules/*.json and return a Map. * Gracefully returns empty Map if directory is missing (ENOENT) — DO NOT REMOVE this guard. * Verified: Phase 70, plan 01. */ export function loadManifests(): Map { const manifestsDir = join(dirname(fileURLToPath(import.meta.url)), "modules"); const result = new Map(); let files: string[]; try { files = readdirSync(manifestsDir); } catch (err: unknown) { if (err instanceof Error && (err as NodeJS.ErrnoException).code === "ENOENT") { // Modules directory not present — return empty map (not an error). return result; } log.error("Failed to read modules directory", { error: String(err) }); return result; } for (const file of files) { if (!file.endsWith(".json")) continue; const filePath = join(manifestsDir, file); try { const raw = readFileSync(filePath, "utf-8"); const manifest = JSON.parse(raw) as ModuleManifest; if (!manifest.id || typeof manifest.id !== "string") { log.warn("Skipping manifest with missing or invalid id", { file }); continue; } result.set(manifest.id, manifest); } catch (err) { log.warn("Failed to parse manifest file", { file, error: String(err) }); } } return result; }