// ── Module Generator ───────────────────────────────────────────────── // Generates missing internal modules from templates. // Supports built-in templates and custom .setup-templates/ directory. import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, "..", ".."); const SETUP_TEMPLATES_DIR = path.join(ROOT, ".setup-templates"); const REGISTRY_PATH = path.join(SETUP_TEMPLATES_DIR, "registry.json"); const TEMPLATES_DIR = path.join(SETUP_TEMPLATES_DIR, "templates"); export interface TemplateRegistry { [importPath: string]: { template: string; params?: Record; generator?: string; // optional custom generator function name }; } export interface GenerationResult { success: boolean; filePath: string; internalPath: string; message?: string; error?: string; } export interface FallbackRegistry { [pattern: string]: string; } /** Load the template registry from .setup-templates/registry.json */ export function loadRegistry(): { templates: TemplateRegistry; fallbacks: FallbackRegistry } { const builtInTemplates: TemplateRegistry = { "data/stickers": { template: "stickers.ts" }, "data/constants": { template: "constants.ts" }, "lib/utils": { template: "utils.ts" }, "types/index": { template: "types-index.ts", params: { inferFromUsage: true } }, }; const builtInFallbacks: FallbackRegistry = { "@/data/*": "data-generic.ts", "@/lib/*": "lib-generic.ts", "@/hooks/*": "hook-generic.ts", "@/types/*": "types-generic.ts", "@/components/*": "component-generic.tsx", "@/utils/*": "utils-generic.ts", }; try { if (fs.existsSync(REGISTRY_PATH)) { const content = fs.readFileSync(REGISTRY_PATH, "utf-8"); const custom = JSON.parse(content); return { templates: { ...builtInTemplates, ...(custom.templates || {}) }, fallbacks: { ...builtInFallbacks, ...(custom.fallbackTemplates || {}) }, }; } } catch { // Use built-in } return { templates: builtInTemplates, fallbacks: builtInFallbacks }; } /** Match an internal path against fallback patterns */ export function matchFallback(internalPath: string, fallbacks: FallbackRegistry): string | undefined { for (const [pattern, template] of Object.entries(fallbacks)) { // Convert glob pattern to regex const regexStr = "^" + pattern .replace(/\//g, "\\/") .replace(/\./g, "\\.") .replace(/\*/g, ".*") .replace(/[$^+(){}[\]|]/g, "\\$&") + "$"; const regex = new RegExp(regexStr); if (regex.test(internalPath)) { return template; } } return undefined; } /** Render a template with parameters */ function renderTemplate(templateContent: string, params: Record = {}): string { let result = templateContent; for (const [key, value] of Object.entries(params)) { const regex = new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, "g"); result = result.replace(regex, String(value)); } return result; } /** Get template content - first from custom templates dir, then built-in */ function getTemplateContent(templateName: string): string { // Check custom templates first const customPath = path.join(TEMPLATES_DIR, templateName); if (fs.existsSync(customPath)) { return fs.readFileSync(customPath, "utf-8"); } // Built-in templates (embedded) const builtInTemplates: Record = { "stickers.ts": `// ── Sticker Packs ───────────────────────────────────────────────── // Auto-generated by self-healing setup. Edit .setup-templates/templates/stickers.ts to customize. export interface Sticker { id: string; name: string; emoji?: string; svg?: string; } export interface StickerPack { id: string; name: string; stickers: Sticker[]; } export function getStickerPacks(): StickerPack[] { return [ { id: "reactions", name: "Reactions", stickers: [ { id: "thumbs-up", name: "Thumbs Up", emoji: "👍" }, { id: "thumbs-down", name: "Thumbs Down", emoji: "👎" }, { id: "heart", name: "Heart", emoji: "❤️" }, { id: "laugh", name: "Laugh", emoji: "😂" }, { id: "wow", name: "Wow", emoji: "😮" }, { id: "sad", name: "Sad", emoji: "😢" }, { id: "angry", name: "Angry", emoji: "😡" }, { id: "clap", name: "Clap", emoji: "👏" }, { id: "fire", name: "Fire", emoji: "🔥" }, { id: "100", name: "100", emoji: "💯" }, { id: "eyes", name: "Eyes", emoji: "👀" }, { id: "pray", name: "Pray", emoji: "🙏" }, ], }, { id: "animals", name: "Animals", stickers: [ { id: "cat", name: "Cat", emoji: "🐱" }, { id: "dog", name: "Dog", emoji: "🐶" }, { id: "panda", name: "Panda", emoji: "🐼" }, { id: "fox", name: "Fox", emoji: "🦊" }, { id: "bear", name: "Bear", emoji: "🐻" }, { id: "koala", name: "Koala", emoji: "🐨" }, { id: "tiger", name: "Tiger", emoji: "🐯" }, { id: "lion", name: "Lion", emoji: "🦁" }, { id: "monkey", name: "Monkey", emoji: "🐵" }, { id: "frog", name: "Frog", emoji: "🐸" }, { id: "penguin", name: "Penguin", emoji: "🐧" }, { id: "bird", name: "Bird", emoji: "🐦" }, ], }, { id: "food", name: "Food & Drink", stickers: [ { id: "pizza", name: "Pizza", emoji: "🍕" }, { id: "burger", name: "Burger", emoji: "🍔" }, { id: "taco", name: "Taco", emoji: "🌮" }, { id: "sushi", name: "Sushi", emoji: "🍣" }, { id: "ramen", name: "Ramen", emoji: "🍜" }, { id: "ice-cream", name: "Ice Cream", emoji: "🍦" }, { id: "cake", name: "Cake", emoji: "🎂" }, { id: "coffee", name: "Coffee", emoji: "☕" }, { id: "beer", name: "Beer", emoji: "🍺" }, { id: "wine", name: "Wine", emoji: "🍷" }, { id: "donut", name: "Donut", emoji: "🍩" }, { id: "cookie", name: "Cookie", emoji: "🍪" }, ], }, ]; }; export function getStickerPacks(): StickerPack[] { return [ // reactions, animals, food packs defined above // (inline for brevity - full packs in template file) ]; } `, "constants.ts": `// ── App Constants ──────────────────────────────────────────────────── // Auto-generated by self-healing setup. export const APP_NAME = "CoastIT CRM"; export const APP_VERSION = "1.0.0"; export const LEAD_STATUSES = { new: { label: "New", color: "bg-blue-500" }, contacted: { label: "Contacted", color: "bg-yellow-500" }, qualified: { label: "Qualified", color: "bg-purple-500" }, proposal: { label: "Proposal", color: "bg-orange-500" }, won: { label: "Won", color: "bg-green-500" }, lost: { label: "Lost", color: "bg-red-500" }, } as const; export const USER_ROLES = { super_admin: { label: "Super Admin", level: 1 }, admin: { label: "Admin", level: 2 }, sales: { label: "Sales", level: 3 }, dev: { label: "Developer", level: 4 }, } as const; export const EVENT_TYPES = [ "meeting", "call", "email", "task", "note", ] as const; `, "utils.ts": `// ── Utility Functions ──────────────────────────────────────────────── // Auto-generated by self-healing setup. import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } export function formatDate(date: Date | string, options?: Intl.DateTimeFormatOptions): string { const d = typeof date === "string" ? new Date(date) : date; return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", ...options, }); } export function formatTime(date: Date | string): string { const d = typeof date === "string" ? new Date(date) : date; return d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true }); } export function formatCurrency(amount: number, currency = "USD"): string { return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount); } export function truncate(str: string, length: number): string { if (str.length <= length) return str; return str.slice(0, length - 3) + "..."; } export function generateId(): string { return crypto.randomUUID(); } export function debounce unknown>( fn: T, delay: number ): (...args: Parameters) => void { let timeoutId: ReturnType; return (...args: Parameters) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; } `, "types-index.ts": `// ── Type Definitions ────────────────────────────────────────────────── // Auto-generated by self-healing setup. export type UserRole = "super_admin" | "admin" | "sales" | "dev"; export interface User { id: string; name: string; email: string; phone?: string; role: UserRole; active: boolean; avatar: string; createdAt: string; } export interface Lead { id: string; companyName: string; contactName: string; email: string; phone: string; source: string; description: string; status: LeadStatus; assignedUserId: string | null; assignedUser: User | null; createdAt: string; updatedAt: string; } export type LeadStatus = "new" | "contacted" | "qualified" | "proposal" | "won" | "lost"; export interface DashboardStats { totalLeads: number; newLeads: number; contactedLeads: number; qualifiedLeads: number; proposalLeads: number; wonLeads: number; lostLeads: number; conversionRate: number; } `, }; return builtInTemplates[templateName] || ""; } /** Generate a single missing module */ export function generateModule( internalPath: string, templates: TemplateRegistry, fallbacks: FallbackRegistry ): GenerationResult { let entry = templates[internalPath]; let templateFile = entry?.template; // Try fallback patterns if (!entry) { const fallbackTemplate = matchFallback(internalPath, fallbacks); if (fallbackTemplate) { templateFile = fallbackTemplate; entry = { template: fallbackTemplate }; } } if (!templateFile) { return { success: false, filePath: "", internalPath, error: `No template registered for @/${internalPath}`, }; } const templateContent = getTemplateContent(templateFile); if (!templateContent) { return { success: false, filePath: "", internalPath, error: `Template not found: ${entry.template}`, }; } const rendered = renderTemplate(templateContent, entry.params || {}); const filePath = path.join(ROOT, "src", internalPath + (internalPath.endsWith(".ts") ? "" : ".ts")); // Ensure directory exists const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } // Write file try { fs.writeFileSync(filePath, rendered, "utf-8"); return { success: true, filePath, internalPath, message: `Generated @/${internalPath}`, }; } catch (e) { return { success: false, filePath, internalPath, error: `Failed to write file: ${e instanceof Error ? e.message : String(e)}`, }; } } /** Generate all missing modules for a list of internal paths */ export function generateAll(internalPaths: string[]): GenerationResult[] { const { templates, fallbacks } = loadRegistry(); const results: GenerationResult[] = []; for (const internalPath of internalPaths) { const result = generateModule(internalPath, templates, fallbacks); results.push(result); if (result.success) { console.log(` ✓ ${result.message}`); } else { console.error(` ✗ Failed @/${internalPath}: ${result.error}`); } } return results; }