396 lines
12 KiB
TypeScript
396 lines
12 KiB
TypeScript
// ── 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<string, unknown>;
|
|
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, unknown> = {}): 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<string, string> = {
|
|
"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<T extends (...args: unknown[]) => unknown>(
|
|
fn: T,
|
|
delay: number
|
|
): (...args: Parameters<T>) => void {
|
|
let timeoutId: ReturnType<typeof setTimeout>;
|
|
return (...args: Parameters<T>) => {
|
|
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;
|
|
} |