diff --git a/.gitea/workflows/build-and-repair.yml b/.gitea/workflows/build-and-repair.yml
new file mode 100644
index 0000000..005935d
--- /dev/null
+++ b/.gitea/workflows/build-and-repair.yml
@@ -0,0 +1,58 @@
+name: Build & Auto-Repair
+on:
+ push:
+ branches: [main, develop]
+ pull_request:
+ branches: [main]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+
+ - name: Install dependencies
+ run: npm ci --no-audit --no-fund
+
+ - name: Install Playwright
+ run: npx playwright install chromium firefox
+
+ - name: Build check
+ id: build
+ continue-on-error: true
+ run: npx tsc --noEmit
+
+ - name: Run self-healing setup
+ if: steps.build.outcome == 'failure'
+ run: node scripts/setup.mjs --self-heal
+
+ - name: Run code repair agent
+ if: steps.build.outcome == 'failure'
+ env:
+ OLLAMA_HOST: ${{ vars.OLLAMA_HOST || 'http://localhost:11434' }}
+ run: node scripts/code-repair-agent.mjs --ci
+
+ - name: Re-check build after repair
+ id: rebuild
+ continue-on-error: true
+ run: npx tsc --noEmit
+
+ - name: Commit fixes
+ if: steps.build.outcome == 'failure' && steps.rebuild.outcome == 'success'
+ run: |
+ git config user.name "CRM Repair Bot"
+ git config user.email "bot@coastit.co.za"
+ git add -A
+ git diff --cached --quiet || git commit -m "[bot] Auto-repair build errors"
+ git push
+
+ - name: Build failed — report
+ if: steps.rebuild.outcome == 'failure'
+ run: |
+ echo "::error::Build still failing after auto-repair. Manual intervention required."
+ exit 1
diff --git a/.setup-templates/registry.json b/.setup-templates/registry.json
new file mode 100644
index 0000000..de7ef0f
--- /dev/null
+++ b/.setup-templates/registry.json
@@ -0,0 +1,42 @@
+{
+ "version": 2,
+ "description": "Template registry for self-healing setup. Maps internal import paths to templates.",
+ "templates": {
+ "data/stickers": {
+ "template": "stickers.ts",
+ "description": "Emoji-based sticker packs for media picker"
+ },
+ "data/constants": {
+ "template": "constants.ts",
+ "description": "App-wide constants (lead statuses, roles, event types)"
+ },
+ "lib/utils": {
+ "template": "utils.ts",
+ "description": "Utility functions (cn, formatters, helpers)"
+ },
+ "types/index": {
+ "template": "types-index.ts",
+ "description": "Core type definitions (User, Lead, DashboardStats)"
+ },
+ "hooks/use-media": {
+ "template": "hooks/use-media.ts",
+ "description": "Media query hook"
+ },
+ "hooks/use-debounce": {
+ "template": "hooks/use-debounce.ts",
+ "description": "Debounce hook"
+ },
+ "hooks/use-local-storage": {
+ "template": "hooks/use-local-storage.ts",
+ "description": "LocalStorage hook with SSR safety"
+ }
+ },
+ "fallbackTemplates": {
+ "@/data/*": "data-generic.ts",
+ "@/lib/*": "lib-generic.ts",
+ "@/hooks/*": "hook-generic.ts",
+ "@/types/*": "types-generic.ts",
+ "@/components/*": "component-generic.tsx",
+ "@/utils/*": "utils-generic.ts"
+ }
+}
\ No newline at end of file
diff --git a/.setup-templates/templates/component-generic.tsx b/.setup-templates/templates/component-generic.tsx
new file mode 100644
index 0000000..d200bb1
--- /dev/null
+++ b/.setup-templates/templates/component-generic.tsx
@@ -0,0 +1,7 @@
+// ── Generic Component Template ────────────────────────────────────────
+// Auto-generated by self-healing setup.
+// Placeholder for @/components/* imports. Customize as needed.
+
+export default function GenericComponent() {
+ return
;
+}
diff --git a/.setup-templates/templates/constants.ts b/.setup-templates/templates/constants.ts
new file mode 100644
index 0000000..17c4217
--- /dev/null
+++ b/.setup-templates/templates/constants.ts
@@ -0,0 +1,54 @@
+// ── App Constants ────────────────────────────────────────────────────
+// Auto-generated by self-healing setup. Edit .setup-templates/templates/constants.ts to customize.
+
+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;
+
+export const EVENT_STATUSES = [
+ "scheduled",
+ "completed",
+ "cancelled",
+ "rescheduled",
+] as const;
+
+export const AI_MODELS = {
+ chat: "llama3.2:3b",
+ scraper: "dolphin-llama3:8b",
+ coder: "qwen2.5-coder:1.5b-base",
+} as const;
+
+export const PAGINATION = {
+ defaultLimit: 50,
+ maxLimit: 200,
+} as const;
+
+export const DATE_FORMATS = {
+ short: "MMM d, yyyy",
+ long: "MMMM d, yyyy",
+ time: "h:mm a",
+ datetime: "MMM d, yyyy h:mm a",
+} as const;
\ No newline at end of file
diff --git a/.setup-templates/templates/data-generic.ts b/.setup-templates/templates/data-generic.ts
new file mode 100644
index 0000000..290dc41
--- /dev/null
+++ b/.setup-templates/templates/data-generic.ts
@@ -0,0 +1,17 @@
+// ── Generic Data Module Template ─────────────────────────────────────
+// Auto-generated by self-healing setup.
+// Placeholder for @/data/* imports. Customize as needed.
+
+export interface DataItem {
+ id: string;
+ name: string;
+ [key: string]: unknown;
+}
+
+export function getDataItems(): DataItem[] {
+ return [];
+}
+
+export function getDataItem(id: string): DataItem | undefined {
+ return undefined;
+}
\ No newline at end of file
diff --git a/.setup-templates/templates/hook-generic.ts b/.setup-templates/templates/hook-generic.ts
new file mode 100644
index 0000000..3ba9a41
--- /dev/null
+++ b/.setup-templates/templates/hook-generic.ts
@@ -0,0 +1,10 @@
+// ── Generic Hook Template ────────────────────────────────────────────
+// Auto-generated by self-healing setup.
+// Placeholder for @/hooks/* imports. Customize as needed.
+
+import { useState, useEffect } from "react";
+
+export function useHook(initialValue: T): [T, (value: T) => void] {
+ const [value, setValue] = useState(initialValue);
+ return [value, setValue];
+}
\ No newline at end of file
diff --git a/.setup-templates/templates/hooks/use-debounce.ts b/.setup-templates/templates/hooks/use-debounce.ts
new file mode 100644
index 0000000..de9d434
--- /dev/null
+++ b/.setup-templates/templates/hooks/use-debounce.ts
@@ -0,0 +1,15 @@
+// ── useDebounce Hook ──────────────────────────────────────────────────
+// Auto-generated by self-healing setup. Edit to customize.
+
+import { useState, useEffect } from "react";
+
+export function useDebounce(value: T, delay: number): T {
+ const [debouncedValue, setDebouncedValue] = useState(value);
+
+ useEffect(() => {
+ const timer = setTimeout(() => setDebouncedValue(value), delay);
+ return () => clearTimeout(timer);
+ }, [value, delay]);
+
+ return debouncedValue;
+}
diff --git a/.setup-templates/templates/hooks/use-local-storage.ts b/.setup-templates/templates/hooks/use-local-storage.ts
new file mode 100644
index 0000000..5a011cd
--- /dev/null
+++ b/.setup-templates/templates/hooks/use-local-storage.ts
@@ -0,0 +1,29 @@
+// ── useLocalStorage Hook ──────────────────────────────────────────────
+// Auto-generated by self-healing setup. Edit to customize.
+
+import { useState, useEffect } from "react";
+
+export function useLocalStorage(key: string, initialValue: T): [T, (value: T | ((prev: T) => T)) => void] {
+ const [storedValue, setStoredValue] = useState(initialValue);
+
+ useEffect(() => {
+ try {
+ const item = window.localStorage.getItem(key);
+ if (item) setStoredValue(JSON.parse(item));
+ } catch {
+ // corrupted data — use initial value
+ }
+ }, [key]);
+
+ const setValue = (value: T | ((prev: T) => T)) => {
+ try {
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
+ setStoredValue(valueToStore);
+ window.localStorage.setItem(key, JSON.stringify(valueToStore));
+ } catch {
+ // storage full or disabled
+ }
+ };
+
+ return [storedValue, setValue];
+}
diff --git a/.setup-templates/templates/hooks/use-media.ts b/.setup-templates/templates/hooks/use-media.ts
new file mode 100644
index 0000000..4b4acfc
--- /dev/null
+++ b/.setup-templates/templates/hooks/use-media.ts
@@ -0,0 +1,18 @@
+// ── useMedia Hook ─────────────────────────────────────────────────────
+// Auto-generated by self-healing setup. Edit to customize.
+
+import { useState, useEffect } from "react";
+
+export function useMedia(query: string): boolean {
+ const [matches, setMatches] = useState(false);
+
+ useEffect(() => {
+ const mql = window.matchMedia(query);
+ setMatches(mql.matches);
+ const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
+ mql.addEventListener("change", handler);
+ return () => mql.removeEventListener("change", handler);
+ }, [query]);
+
+ return matches;
+}
diff --git a/.setup-templates/templates/lib-generic.ts b/.setup-templates/templates/lib-generic.ts
new file mode 100644
index 0000000..df1fca1
--- /dev/null
+++ b/.setup-templates/templates/lib-generic.ts
@@ -0,0 +1,7 @@
+// ── Generic Lib Module Template ──────────────────────────────────────
+// Auto-generated by self-healing setup.
+// Placeholder for @/lib/* imports. Customize as needed.
+
+export function libFunction(): void {
+ // Placeholder
+}
\ No newline at end of file
diff --git a/.setup-templates/templates/stickers.ts b/.setup-templates/templates/stickers.ts
new file mode 100644
index 0000000..6452718
--- /dev/null
+++ b/.setup-templates/templates/stickers.ts
@@ -0,0 +1,95 @@
+// ── 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[];
+}
+
+const reactionStickers: Sticker[] = [
+ { 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: "🙏" },
+];
+
+const animalStickers: Sticker[] = [
+ { 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: "🐦" },
+];
+
+const foodStickers: Sticker[] = [
+ { 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: "🍪" },
+];
+
+const activityStickers: Sticker[] = [
+ { id: "football", name: "Football", emoji: "⚽" },
+ { id: "basketball", name: "Basketball", emoji: "🏀" },
+ { id: "tennis", name: "Tennis", emoji: "🎾" },
+ { id: "running", name: "Running", emoji: "🏃" },
+ { id: "swimming", name: "Swimming", emoji: "🏊" },
+ { id: "cycling", name: "Cycling", emoji: "🚴" },
+ { id: "gym", name: "Gym", emoji: "🏋️" },
+ { id: "yoga", name: "Yoga", emoji: "🧘" },
+ { id: "music", name: "Music", emoji: "🎵" },
+ { id: "party", name: "Party", emoji: "🎉" },
+ { id: "gift", name: "Gift", emoji: "🎁" },
+ { id: "trophy", name: "Trophy", emoji: "🏆" },
+];
+
+const stickerPacks: StickerPack[] = [
+ { id: "reactions", name: "Reactions", stickers: reactionStickers },
+ { id: "animals", name: "Animals", stickers: animalStickers },
+ { id: "food", name: "Food & Drink", stickers: foodStickers },
+ { id: "activities", name: "Activities", stickers: activityStickers },
+];
+
+export function getStickerPacks(): StickerPack[] {
+ return stickerPacks;
+}
+
+export function getStickerPack(id: string): StickerPack | undefined {
+ return stickerPacks.find((p) => p.id === id);
+}
+
+export function getSticker(packId: string, stickerId: string): Sticker | undefined {
+ const pack = getStickerPack(packId);
+ return pack?.stickers.find((s) => s.id === stickerId);
+}
\ No newline at end of file
diff --git a/.setup-templates/templates/types-generic.ts b/.setup-templates/templates/types-generic.ts
new file mode 100644
index 0000000..33d0b1c
--- /dev/null
+++ b/.setup-templates/templates/types-generic.ts
@@ -0,0 +1,10 @@
+// ── Generic Types Template ────────────────────────────────────────────
+// Auto-generated by self-healing setup.
+// Placeholder for @/types/* imports. Customize as needed.
+
+export type GenericType = Record;
+
+export interface GenericInterface {
+ id: string;
+ [key: string]: unknown;
+}
diff --git a/.setup-templates/templates/types-index.ts b/.setup-templates/templates/types-index.ts
new file mode 100644
index 0000000..bf9e45f
--- /dev/null
+++ b/.setup-templates/templates/types-index.ts
@@ -0,0 +1,141 @@
+// ── Type Definitions ──────────────────────────────────────────────────
+// Auto-generated by self-healing setup. Edit .setup-templates/templates/types-index.ts to customize.
+
+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;
+ monthlyBreakdown: {
+ label: string;
+ total: number;
+ new: number;
+ contacted: number;
+ qualified: number;
+ proposal: number;
+ won: number;
+ lost: number;
+ }[];
+ leadsPerMonth: { label: string; leads: number; won: number }[];
+ recentLeads: Lead[];
+ statusDistribution: { name: string; value: number; color: string }[];
+ periodLabel: string;
+ trends: Record;
+}
+
+export interface Note {
+ id: string;
+ leadId: string;
+ userId: string;
+ authorName: string;
+ authorAvatar: string;
+ authorRole: UserRole;
+ note: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export type NotificationType =
+ | "lead_created"
+ | "lead_status_changed"
+ | "lead_assigned"
+ | "chat_message"
+ | "note_added"
+ | "event_scheduled";
+
+export interface Notification {
+ id: string;
+ type: NotificationType;
+ title: string;
+ description: string;
+ timestamp: string;
+ read: boolean;
+ link?: string;
+}
+
+export interface ChatMessage {
+ id: string;
+ conversationId: string;
+ senderId: string;
+ senderName: string;
+ senderAvatar: string;
+ content: string;
+ timestamp: string;
+}
+
+export interface Conversation {
+ id: string;
+ participants: { id: string; name: string; avatar: string; role: string }[];
+ lastMessage: string;
+ lastMessageTime: string;
+ unread: number;
+ messages: ChatMessage[];
+}
+
+export type EventType = "meeting" | "call" | "email" | "task" | "note";
+export type EventStatus = "scheduled" | "completed" | "cancelled" | "rescheduled";
+
+export interface CalendarEvent {
+ id: string;
+ userId: string;
+ participantId: string | null;
+ developerId: string | null;
+ leadId: string | null;
+ conversationId: string | null;
+ title: string;
+ description: string | null;
+ participantNotes: string | null;
+ eventType: EventType;
+ startTime: string;
+ endTime: string | null;
+ durationMinutes: number | null;
+ status: EventStatus;
+ creator: { id: string; name: string; email?: string; role?: string; avatar?: string } | null;
+ participant: { id: string; name: string; email?: string; role?: string; avatar?: string } | null;
+ developer: { id: string; name: string; email?: string; role?: string; avatar?: string } | null;
+ lead: { id: string; companyName: string; contactName: string } | null;
+ clientName: string | null;
+ clientEmail: string | null;
+ clientPhone: string | null;
+ createdAt: string;
+}
\ No newline at end of file
diff --git a/.setup-templates/templates/utils-generic.ts b/.setup-templates/templates/utils-generic.ts
new file mode 100644
index 0000000..490b49b
--- /dev/null
+++ b/.setup-templates/templates/utils-generic.ts
@@ -0,0 +1,7 @@
+// ── Generic Utils Template ────────────────────────────────────────────
+// Auto-generated by self-healing setup.
+// Placeholder for @/utils/* imports. Customize as needed.
+
+export function utilsFunction(): void {
+ // Placeholder
+}
diff --git a/.setup-templates/templates/utils.ts b/.setup-templates/templates/utils.ts
new file mode 100644
index 0000000..0ad46ae
--- /dev/null
+++ b/.setup-templates/templates/utils.ts
@@ -0,0 +1,76 @@
+// ── Utility Functions ────────────────────────────────────────────────
+// Auto-generated by self-healing setup. Edit .setup-templates/templates/utils.ts to customize.
+
+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);
+ };
+}
+
+export function throttle unknown>(
+ fn: T,
+ limit: number
+): (...args: Parameters) => void {
+ let inThrottle = false;
+ return (...args: Parameters) => {
+ if (!inThrottle) {
+ fn(...args);
+ inThrottle = true;
+ setTimeout(() => (inThrottle = false), limit);
+ }
+ };
+}
+
+export function classNames(...classes: (string | boolean | undefined | null)[]): string {
+ return classes.filter(Boolean).join(" ");
+}
+
+export function sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export function retry(fn: () => Promise, retries = 3, delay = 1000): Promise {
+ return fn().catch((err) =>
+ retries > 0 ? sleep(delay).then(() => retry(fn, retries - 1, delay * 2)) : Promise.reject(err)
+ );
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 616ca7d..411cb26 100644
--- a/package.json
+++ b/package.json
@@ -6,13 +6,19 @@
"dev": "npm run dev:precheck & npm run dev:ollama & npm run dev:start",
"dev:signaling": "node signaling-server.mjs",
"dev:open": "node scripts/open-browser.mjs",
- "dev:start": "concurrently -n AI,BROWSE,SIGNAL,NEXT,OPEN -c cyan,magenta,yellow,green,white \"npm run dev:rust\" \"npm run dev:browser-use\" \"npm run dev:signaling\" \"npm run dev:next\" \"npm run dev:open\"",
+ "dev:repair": "node scripts/code-repair-agent.mjs --watch",
+ "dev:start": "concurrently -n REPAIR,AI,BROWSE,SIGNAL,NEXT,OPEN -c red,cyan,magenta,yellow,green,white \"npm run dev:repair\" \"npm run dev:rust\" \"npm run dev:browser-use\" \"npm run dev:signaling\" \"npm run dev:next\" \"npm run dev:open\"",
"dev:next": "next dev -p 3006",
"dev:precheck": "node scripts/precheck.mjs",
"dev:ollama": "node scripts/ensure-ollama.mjs",
"dev:rust": "node ai-server/index.mjs",
"dev:browser-use": "cd browser-use-service && node ../scripts/run-python.mjs main.py",
"setup": "node scripts/setup.mjs",
+ "setup:self-heal": "node scripts/setup.mjs --self-heal",
+ "repair": "node scripts/code-repair-agent.mjs",
+ "repair:watch": "node scripts/code-repair-agent.mjs --watch",
+ "repair:ci": "node scripts/code-repair-agent.mjs --ci",
+ "build:fix": "npm run build 2>&1 || node scripts/code-repair-agent.mjs --ci",
"build": "next build",
"start": "next start -p 3006",
"lint": "eslint"
diff --git a/scripts/code-repair-agent.mjs b/scripts/code-repair-agent.mjs
new file mode 100644
index 0000000..c9534b5
--- /dev/null
+++ b/scripts/code-repair-agent.mjs
@@ -0,0 +1,346 @@
+// ── Code Repair Agent ─────────────────────────────────────────────
+// Phase 2: Auto-fix TypeScript build errors using qwen2.5-coder:1.5b-base
+//
+// Usage:
+// node scripts/code-repair-agent.mjs # one-shot: fix build errors
+// node scripts/code-repair-agent.mjs --watch # watch mode: re-fix on file changes
+// node scripts/code-repair-agent.mjs --ci # CI mode: fix + commit
+//
+// Requires: Ollama running at OLLAMA_HOST (default http://localhost:11434)
+// Model: qwen2.5-coder:1.5b-base
+//
+// ── Architecture ──────────────────────────────────────────────────
+// 1. Capture build output from `npx tsc --noEmit`
+// 2. Parse error locations (file, line, column, message)
+// 3. Read the failing source file + surrounding context (~20 lines)
+// 4. Send to qwen2.5-coder via Ollama API for fix suggestion
+// 5. Apply fix if confidence > threshold (0.7)
+// 6. Re-run build to verify fix
+// 7. If breaking change detected (wide-reaching import changes), escalate
+// 8. In --ci mode: auto-commit with [bot] prefix
+
+import { execSync } from "node:child_process"
+import { readFileSync, writeFileSync, existsSync, appendFileSync } from "node:fs"
+import { resolve, dirname } from "node:path"
+import { fileURLToPath } from "node:url"
+import { createRequire } from "node:module"
+
+const SELF = dirname(fileURLToPath(import.meta.url))
+const _require = createRequire(import.meta.url)
+const ROOT = resolve(SELF, "..")
+const OLLAMA_HOST = process.env.OLLAMA_HOST || "http://localhost:11434"
+const MODEL = process.env.REPAIR_MODEL || "qwen2.5-coder:1.5b-base"
+const MAX_ITERATIONS = 5
+const CONFIDENCE_THRESHOLD = 0.7
+const ESCALATION_TIMEOUT_MS = 30 * 60 * 1000 // 30 min
+
+// ── Helpers ────────────────────────────────────────────────────────
+
+function log(msg, level = "info") {
+ const prefix = level === "error" ? "✗" : level === "warn" ? "⚠" : "✓"
+ console.log(` ${prefix} [repair] ${msg}`)
+}
+
+function runTsc() {
+ try {
+ const out = execSync("npx tsc --noEmit", { stdio: "pipe", timeout: 60000, cwd: ROOT })
+ return { ok: true, output: out.toString() }
+ } catch (e) {
+ return { ok: false, output: e.stdout?.toString() || e.message || "" }
+ }
+}
+
+function parseErrors(output) {
+ const errors = []
+ // Match: src/file.ts(line,col): error TSxxxx: message
+ const lineRegex = /(src\/[^:]+)\((\d+),(\d+)\):\s+error\s+TS\d+:\s+(.+)/g
+ let m
+ while ((m = lineRegex.exec(output)) !== null) {
+ errors.push({
+ file: resolve(ROOT, m[1]),
+ line: parseInt(m[2], 10),
+ column: parseInt(m[3], 10),
+ message: m[4].trim(),
+ })
+ }
+ return errors
+}
+
+function readContext(filePath, errorLine, contextLines = 20) {
+ try {
+ const lines = readFileSync(filePath, "utf-8").split("\n")
+ const start = Math.max(0, errorLine - 1 - contextLines)
+ const end = Math.min(lines.length, errorLine - 1 + contextLines)
+ return {
+ content: lines.slice(start, end).join("\n"),
+ contextLine: errorLine - 1 - start, // 0-indexed line of the error in the snippet
+ totalLines: lines.length,
+ }
+ } catch {
+ return null
+ }
+}
+
+// ── Ollama Integration ─────────────────────────────────────────────
+
+async function queryOllama(prompt) {
+ const response = await fetch(`${OLLAMA_HOST}/api/generate`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ model: MODEL,
+ prompt,
+ stream: false,
+ options: { temperature: 0.3, num_predict: 2048 },
+ }),
+ })
+ if (!response.ok) throw new Error(`Ollama returned ${response.status}`)
+ const data = await response.json()
+ return data.response || ""
+}
+
+function buildRepairPrompt(filePath, errorMessage, codeContext) {
+ return `You are a TypeScript repair agent. Fix the following error.
+
+FILE: ${filePath}
+ERROR: ${errorMessage}
+
+CODE CONTEXT (line ${codeContext.contextLine + 1} is the error line):
+\`\`\`typescript
+${codeContext.content}
+\`\`\`
+
+Respond with ONLY the fix JSON in this exact format:
+{"fix": "the exact replacement code for the error line(s)", "confidence": 0.0-1.0, "explanation": "brief explanation"}
+
+Rules:
+- Keep the fix minimal and precise
+- Only change what's necessary to fix the error
+- If the error is a missing import, suggest the import statement
+- If the error is a type mismatch, suggest the corrected code
+- confidence < 0.7 will not be auto-applied
+- If you cannot determine a fix, set confidence to 0`
+}
+
+function parseFixResponse(text) {
+ try {
+ // Extract JSON from response (it might have markdown fences)
+ const jsonMatch = text.match(/\{[\s\S]*"fix"[\s\S]*"confidence"[\s\S]*\}/)
+ if (!jsonMatch) return null
+ return JSON.parse(jsonMatch[0])
+ } catch {
+ return null
+ }
+}
+
+function applyFix(filePath, fixSuggestion) {
+ if (!fixSuggestion || !fixSuggestion.fix) return { applied: false, reason: "No fix suggestion" }
+
+ try {
+ const original = readFileSync(filePath, "utf-8")
+ writeFileSync(filePath, fixSuggestion.fix, "utf-8")
+ return { applied: true, backup: original }
+ } catch (e) {
+ return { applied: false, reason: e.message }
+ }
+}
+
+// ── Breaking Change Escalation ─────────────────────────────────────
+
+function isBreakingChange(errors, fixes) {
+ // Heuristic: if fix touches import structure or exports, it's breaking
+ for (const fix of fixes) {
+ if (!fix || !fix.fix) continue
+ if (fix.fix.includes("export ") || fix.fix.includes("import ") || fix.fix.includes("delete ")) {
+ return true
+ }
+ }
+ return false
+}
+
+async function escalateBreakingChange(errors, fixes) {
+ const logFile = resolve(ROOT, "repair-escalation.log")
+ const timestamp = new Date().toISOString()
+ const entry = [
+ `=== Breaking Change Escalation @ ${timestamp} ===`,
+ `Errors: ${errors.map(e => `${e.file}:${e.line}: ${e.message}`).join("\n ")}`,
+ `Fixes: ${fixes.map(f => f?.fix?.slice(0, 200)).join("\n ")}`,
+ `Auto-apply in ${ESCALATION_TIMEOUT_MS / 60000} minutes or cancel with Ctrl+C`,
+ "========================================\n",
+ ].join("\n")
+
+ appendFileSync(logFile, entry)
+ log(`Breaking change escalated — see ${logFile}`, "warn")
+ log(`Auto-applying in ${ESCALATION_TIMEOUT_MS / 60000} min (Ctrl+C to cancel)...`, "warn")
+
+ // Wait for timeout or user intervention
+ await new Promise(resolve => setTimeout(resolve, ESCALATION_TIMEOUT_MS))
+ log("Escalation timeout reached — applying fixes")
+ return true
+}
+
+// ── Git Auto-Commit ────────────────────────────────────────────────
+
+function gitCommit(message) {
+ try {
+ execSync("git add -A", { stdio: "pipe", cwd: ROOT })
+ execSync(`git commit -m "[bot] ${message}"`, { stdio: "pipe", cwd: ROOT })
+ log(`Committed: [bot] ${message}`)
+ return true
+ } catch (e) {
+ log(`Git commit failed: ${e.message}`, "warn")
+ return false
+ }
+}
+
+// ── Main Repair Loop ───────────────────────────────────────────────
+
+async function repairOnce(doCommit = false) {
+ log("Running build check...")
+ const build = runTsc()
+ if (build.ok) {
+ log("No build errors found")
+ return { fixed: false, errors: [] }
+ }
+
+ const errors = parseErrors(build.output)
+ if (errors.length === 0) {
+ log("Could not parse build errors from output", "warn")
+ log(build.output.slice(0, 1000))
+ return { fixed: false, errors: [] }
+ }
+
+ log(`Found ${errors.length} error(s):`)
+ for (const e of errors.slice(0, 10)) {
+ const shortFile = e.file.replace(ROOT, "").replace(/^\//, "")
+ log(`${shortFile}:${e.line}:${e.column} — ${e.message}`)
+ }
+
+ const fixes = []
+ for (const error of errors) {
+ const context = readContext(error.file, error.line)
+ if (!context) {
+ log(`Cannot read ${error.file}`, "error")
+ continue
+ }
+
+ log(`Asking ${MODEL} to fix ${error.file}:${error.line}...`)
+ try {
+ const prompt = buildRepairPrompt(error.file, error.message, context)
+ const response = await queryOllama(prompt)
+ const fix = parseFixResponse(response)
+
+ if (fix && fix.confidence >= CONFIDENCE_THRESHOLD) {
+ const result = applyFix(error.file, fix)
+ if (result.applied) {
+ log(`${fix.explanation || "Applied fix"} (confidence: ${fix.confidence})`)
+ fixes.push(fix)
+ } else {
+ log(`Failed to apply fix: ${result.reason}`, "error")
+ }
+ } else {
+ log(`Low confidence (${fix?.confidence || 0}) or invalid response — skipping`, "warn")
+ }
+ } catch (e) {
+ log(`Ollama error: ${e.message}`, "error")
+ }
+ }
+
+ if (fixes.length === 0) {
+ log("No fixes could be applied", "error")
+ return { fixed: false, errors }
+ }
+
+ // Verify by re-running build
+ log("Verifying fixes...")
+ const verify = runTsc()
+ if (verify.ok) {
+ log("All fixes verified — build passes!")
+ if (doCommit) gitCommit(`Auto-fix: ${fixes.length} error(s) resolved`)
+ return { fixed: true, errors: [], fixes }
+ }
+
+ const remainingErrors = parseErrors(verify.output)
+ log(`${remainingErrors.length} error(s) remaining after fix`, "warn")
+
+ // Check for breaking changes
+ if (isBreakingChange(remainingErrors, fixes)) {
+ log("Breaking change detected — escalating", "warn")
+ const approved = await escalateBreakingChange(remainingErrors, fixes)
+ if (approved) {
+ if (doCommit) gitCommit(`Breaking change applied after escalation`)
+ return { fixed: true, errors: remainingErrors, fixes, escalated: true }
+ }
+ }
+
+ return { fixed: remainingErrors.length === 0, errors: remainingErrors, fixes }
+}
+
+async function repairLoop(maxIterations = MAX_ITERATIONS, doCommit = false) {
+ for (let i = 0; i < maxIterations; i++) {
+ log(`=== Repair iteration ${i + 1}/${maxIterations} ===`)
+ const result = await repairOnce(doCommit)
+ if (!result.fixed) {
+ if (result.errors?.length > 0) {
+ log(`${result.errors.length} error(s) remaining — run again for deeper fix`, "warn")
+ }
+ break
+ }
+ if (result.errors?.length === 0) break
+ }
+ log("Repair agent finished")
+}
+
+// ── Watch Mode ─────────────────────────────────────────────────────
+
+function watchMode() {
+ log("Watch mode active — monitoring src/ for changes...", "info")
+ const chokidar = (() => {
+ try {
+ return _require("chokidar")
+ } catch {
+ return null
+ }
+ })()
+
+ if (!chokidar) {
+ log("chokidar not available — using simple polling (5s interval)", "warn")
+ setInterval(async () => {
+ const result = await repairOnce(false)
+ if (result.fixed) log("Auto-fix applied")
+ }, 5000)
+ return
+ }
+
+ const watcher = chokidar.watch("src/**/*.{ts,tsx}", { cwd: ROOT, ignoreInitial: true })
+ let debounceTimer = null
+
+ watcher.on("change", async (filePath) => {
+ clearTimeout(debounceTimer)
+ debounceTimer = setTimeout(async () => {
+ log(`Change detected: ${filePath}`)
+ await repairLoop(3, false)
+ }, 1000)
+ })
+
+ log("Watching for changes...")
+}
+
+// ── Entry ──────────────────────────────────────────────────────────
+
+const args = process.argv.slice(2)
+const isWatch = args.includes("--watch") || args.includes("-w")
+const isCI = args.includes("--ci") || args.includes("-c")
+
+async function main() {
+ if (isWatch) {
+ watchMode()
+ } else {
+ await repairLoop(MAX_ITERATIONS, isCI)
+ }
+}
+
+main().catch((e) => {
+ console.error("Repair agent crashed:", e)
+ process.exit(1)
+})
diff --git a/scripts/lib/module-generator.ts b/scripts/lib/module-generator.ts
new file mode 100644
index 0000000..7170e05
--- /dev/null
+++ b/scripts/lib/module-generator.ts
@@ -0,0 +1,396 @@
+// ── 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;
+}
\ No newline at end of file
diff --git a/scripts/lib/ts-error-parser.ts b/scripts/lib/ts-error-parser.ts
new file mode 100644
index 0000000..2261930
--- /dev/null
+++ b/scripts/lib/ts-error-parser.ts
@@ -0,0 +1,92 @@
+// ── TypeScript Error Parser ──────────────────────────────────────────
+// Parses `tsc --noEmit` or `npm run build` output to extract
+// "Module not found: Can't resolve '@/path/to/module'" errors.
+// Returns actionable missing module info for the generator.
+
+export interface MissingModule {
+ /** The import path that failed, e.g. "@/data/stickers" */
+ importPath: string;
+ /** The file that contains the failing import */
+ importerFile: string;
+ /** Line number in importer file */
+ line: number;
+ /** Column number */
+ column: number;
+ /** Full error message for context */
+ message: string;
+ /** Whether this is an internal (@/...) module we can generate */
+ isInternal: boolean;
+ /** The normalized internal path, e.g. "data/stickers" */
+ internalPath?: string;
+}
+
+/** Parse TypeScript compiler output for missing module errors */
+export function parseMissingModules(tscOutput: string): MissingModule[] {
+ const results: MissingModule[] = [];
+
+ // Pattern: error TS2307: Cannot find module '@/data/stickers' or its corresponding type declarations.
+ // File: src/components/chats/media-picker.tsx:10:1
+ const errorRegex = /error\s+TS\d+:\s+Cannot find module\s+'([^']+)'\s+or its corresponding type declarations\.\s*(?:\n\s*(.*?):(\d+):(\d+))?/g;
+
+ let match;
+ while ((match = errorRegex.exec(tscOutput)) !== null) {
+ const importPath = match[1];
+ const importerFile = match[2] || "";
+ const line = parseInt(match[3] || "0", 10);
+ const column = parseInt(match[4] || "0", 10);
+
+ // Also handle: Module not found: Error: Can't resolve '@/data/stickers' in 'C:\...\src\components\chats'
+ const altRegex = /Module not found: Error: Can't resolve\s+'([^']+)'\s+in\s+'([^']+)'/g;
+ let altMatch;
+ let altImporter = importerFile;
+ while ((altMatch = altRegex.exec(tscOutput)) !== null) {
+ if (altMatch[1] === importPath) {
+ altImporter = altMatch[2];
+ break;
+ }
+ }
+
+ const isInternal = importPath.startsWith("@/") || importPath.startsWith("~/");
+
+ let internalPath: string | undefined;
+ if (isInternal) {
+ internalPath = importPath.replace(/^[@~]\//, "");
+ }
+
+ results.push({
+ importPath,
+ importerFile: altImporter,
+ line,
+ column,
+ message: `Cannot find module '${importPath}'`,
+ isInternal,
+ internalPath,
+ });
+ }
+
+ // Deduplicate by importPath + importerFile
+ const seen = new Set();
+ return results.filter((m) => {
+ const key = `${m.importPath}|${m.importerFile}`;
+ if (seen.has(key)) return false;
+ seen.add(key);
+ return true;
+ });
+}
+
+/** Filter to only internal (@/...) modules we can auto-generate */
+export function filterGeneratable(modules: MissingModule[]): MissingModule[] {
+ return modules.filter((m) => m.isInternal && m.internalPath);
+}
+
+/** Group missing modules by internal path */
+export function groupByInternalPath(modules: MissingModule[]): Map {
+ const groups = new Map();
+ for (const m of modules) {
+ if (!m.internalPath) continue;
+ const existing = groups.get(m.internalPath) || [];
+ existing.push(m);
+ groups.set(m.internalPath, existing);
+ }
+ return groups;
+}
\ No newline at end of file
diff --git a/scripts/setup.mjs b/scripts/setup.mjs
index 1cbdf0c..c8cec1f 100644
--- a/scripts/setup.mjs
+++ b/scripts/setup.mjs
@@ -5,17 +5,22 @@
// 2. pip install -r requirements.txt (Python dependencies)
// 3. playwright install firefox chromium (Playwright browsers)
// 4. Copies .env.example to .env.local if not exists
+// 5. --self-heal flag: runs build check + auto-generates missing modules
//
// All steps are cross-platform (Windows, Mac, Linux).
-// Uses execSync for simplicity since each step blocks the next.
import { execSync } from "node:child_process"
-import { existsSync, copyFileSync } from "node:fs"
+import { existsSync, copyFileSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"
import { platform } from "node:os"
+import { resolve, dirname } from "node:path"
+import { fileURLToPath } from "node:url"
const SEP = platform() === "win32" ? "&" : ";"
+const SELF = dirname(fileURLToPath(import.meta.url))
+const ROOT = resolve(SELF, "..")
+
+// ── Helpers ────────────────────────────────────────────────────────
-// Auto-detect Python executable (python vs python3)
function detectPython() {
const candidates = platform() === "win32" ? ["python", "python3"] : ["python3", "python"]
for (const cmd of candidates) {
@@ -28,7 +33,6 @@ function detectPython() {
process.exit(1)
}
-// Auto-detect pip (pip vs pip3), fall back to python -m pip
function detectPip(python) {
const candidates = platform() === "win32" ? ["pip", "pip3"] : ["pip3", "pip"]
for (const cmd of candidates) {
@@ -40,41 +44,216 @@ function detectPip(python) {
return `${python} -m pip`
}
-const PY = detectPython()
-const PIP = detectPip(PY)
-
-function run(cmd, label) {
+function run(cmd, label, opts = {}) {
console.log(`\n── ${label} ──`)
try {
- execSync(cmd, { stdio: "inherit", timeout: 120000 })
+ execSync(cmd, { stdio: "inherit", timeout: opts.timeout || 120000, cwd: opts.cwd || ROOT })
} catch (e) {
+ if (opts.optional) {
+ console.log(` ~ Skipped (${e.message})`)
+ return false
+ }
console.error(` ✗ Failed: ${e.message}`)
process.exit(1)
}
+ return true
}
+// ── Self-Healing Module Registry ───────────────────────────────────
+
+function loadRegistry() {
+ const registryPath = resolve(ROOT, ".setup-templates", "registry.json")
+ const templatesDir = resolve(ROOT, ".setup-templates", "templates")
+
+ const builtInTemplates = {
+ "data/stickers": { template: "stickers.ts" },
+ "data/constants": { template: "constants.ts" },
+ "lib/utils": { template: "utils.ts" },
+ "types/index": { template: "types-index.ts" },
+ "hooks/use-media": { template: "hooks/use-media.ts" },
+ "hooks/use-debounce": { template: "hooks/use-debounce.ts" },
+ "hooks/use-local-storage": { template: "hooks/use-local-storage.ts" },
+ }
+
+ const builtInFallbacks = {
+ "@/data/*": "data-generic.ts",
+ "@/lib/*": "lib-generic.ts",
+ "@/hooks/*": "hook-generic.ts",
+ "@/types/*": "types-generic.ts",
+ "@/components/*": "component-generic.tsx",
+ "@/utils/*": "utils-generic.ts",
+ }
+
+ let templates = { ...builtInTemplates }
+ let fallbacks = { ...builtInFallbacks }
+
+ try {
+ if (existsSync(registryPath)) {
+ const custom = JSON.parse(readFileSync(registryPath, "utf-8"))
+ templates = { ...templates, ...(custom.templates || {}) }
+ fallbacks = { ...fallbacks, ...(custom.fallbackTemplates || {}) }
+ }
+ } catch {}
+
+ const templateCache = {}
+
+ function getTemplate(templateName) {
+ if (templateCache[templateName]) return templateCache[templateName]
+ const candidatePaths = [
+ resolve(templatesDir, templateName),
+ resolve(templatesDir, "hooks", templateName.replace("hooks/", "")),
+ ]
+ for (const p of candidatePaths) {
+ if (existsSync(p)) {
+ const content = readFileSync(p, "utf-8")
+ templateCache[templateName] = content
+ return content
+ }
+ }
+ return ""
+ }
+
+ function matchFallback(internalPath) {
+ for (const [pattern, templateFile] of Object.entries(fallbacks)) {
+ const regexStr = "^" + pattern
+ .replace(/\//g, "\\/").replace(/\./g, "\\.")
+ .replace(/\*/g, ".*")
+ .replace(/[$^+(){}[\]|]/g, "\\$&") + "$"
+ const regex = new RegExp(regexStr)
+ if (regex.test(internalPath)) return templateFile
+ }
+ return null
+ }
+
+ function generateModule(internalPath) {
+ let templateFile = templates[internalPath]?.template
+ if (!templateFile) templateFile = matchFallback(internalPath)
+ if (!templateFile) return { success: false, error: `No template for @/${internalPath}` }
+
+ const content = getTemplate(templateFile)
+ if (!content) return { success: false, error: `Template file not found: ${templateFile}` }
+
+ const ext = templateFile.endsWith(".tsx") ? ".tsx" : ".ts"
+ const filePath = resolve(ROOT, "src", internalPath + ext)
+ mkdirSync(dirname(filePath), { recursive: true })
+ writeFileSync(filePath, content, "utf-8")
+ return { success: true, filePath, message: `Generated @/${internalPath}` }
+ }
+
+ return { generateModule }
+}
+
+function parseMissingModules(buildOutput) {
+ const results = []
+ const seen = new Set()
+
+ // Pattern: error TS2307: Cannot find module '@/path' or its corresponding type declarations.
+ const errorRegex = /error\s+TS\d+:\s+Cannot find module\s+'([^']+)'/g
+ let match
+ while ((match = errorRegex.exec(buildOutput)) !== null) {
+ const importPath = match[1]
+ if (!seen.has(importPath)) {
+ seen.add(importPath)
+ const isInternal = importPath.startsWith("@/") || importPath.startsWith("~/")
+ const internalPath = isInternal ? importPath.replace(/^[@~]\//, "") : undefined
+ results.push({ importPath, isInternal, internalPath })
+ }
+ }
+
+ // Also catch webpack-style: Module not found: Error: Can't resolve '@/path'
+ const webpackRegex = /Module not found: Error: Can't resolve\s+'([^']+)'/g
+ while ((match = webpackRegex.exec(buildOutput)) !== null) {
+ const importPath = match[1]
+ if (!seen.has(importPath)) {
+ seen.add(importPath)
+ const isInternal = importPath.startsWith("@/") || importPath.startsWith("~/")
+ const internalPath = isInternal ? importPath.replace(/^[@~]\//, "") : undefined
+ results.push({ importPath, isInternal, internalPath })
+ }
+ }
+
+ return results
+}
+
+// ── Main ───────────────────────────────────────────────────────────
+
+const args = process.argv.slice(2)
+const doSelfHeal = args.includes("--self-heal") || args.includes("-s")
+const PY = detectPython()
+const PIP = detectPip(PY)
+
console.log("=== CoastIT CRM Setup ===\n")
// 1. Node dependencies
run("npm install", "Installing Node.js dependencies")
-// 2. Python dependencies (run from browser-use-service directory)
+// 2. Python dependencies
run(`cd browser-use-service ${SEP} ${PIP} install -r requirements.txt`, "Installing Python dependencies")
-// 3. Playwright browsers (Firefox for primary scraping, Chromium for Chrome/Edge/Opera + Agent fallback)
+// 3. Playwright browsers
run(`${PY} -m playwright install firefox chromium`, "Installing Playwright browsers")
-// 4. .env file — create from template if it doesn't exist
-if (!existsSync(".env.local")) {
+// 4. .env file
+if (!existsSync(resolve(ROOT, ".env.local"))) {
console.log("\n── Creating .env.local ──")
- copyFileSync(".env.example", ".env.local")
+ copyFileSync(resolve(ROOT, ".env.example"), resolve(ROOT, ".env.local"))
console.log(" ✓ Created .env.local from .env.example — edit it with your settings")
} else {
console.log("\n── .env.local already exists, skipping ──")
}
-// 5. Remaining manual steps
-console.log("\n── Next steps ──")
+// 5. Self-healing build check (--self-heal flag)
+if (doSelfHeal) {
+ console.log("\n── Self-Healing Build Check ──")
+ const { generateModule } = loadRegistry()
+ let lastGeneratedCount = -1
+ let iteration = 0
+ const MAX_ITERATIONS = 5
+
+ while (iteration < MAX_ITERATIONS) {
+ iteration++
+ console.log(` Build check pass ${iteration}/${MAX_ITERATIONS}...`)
+
+ let buildOutput = ""
+ try {
+ buildOutput = execSync("npx tsc --noEmit", { stdio: "pipe", timeout: 60000, cwd: ROOT }).toString()
+ } catch (e) {
+ buildOutput = e.stdout?.toString() || e.message || ""
+ }
+
+ const missing = parseMissingModules(buildOutput)
+ const internalMissing = missing
+ .filter(m => m.isInternal && m.internalPath)
+ .filter((m, i, arr) => arr.findIndex(x => x.internalPath === m.internalPath) === i) // dedup
+
+ if (internalMissing.length === 0) {
+ console.log(" ✓ No missing internal modules found!")
+ break
+ }
+
+ console.log(` Found ${internalMissing.length} missing internal module(s)`)
+
+ let generated = 0
+ for (const mod of internalMissing) {
+ const result = generateModule(mod.internalPath)
+ if (result.success) {
+ console.log(` ✓ ${result.message}`)
+ generated++
+ } else {
+ console.log(` ✗ ${result.error}`)
+ }
+ }
+
+ if (generated === 0 || generated === lastGeneratedCount) {
+ console.log(" No new modules could be generated. Stopping.")
+ break
+ }
+ lastGeneratedCount = generated
+ }
+}
+
+// 6. Final summary
+console.log("\n── Final Steps ──")
console.log(" 1. Make sure PostgreSQL is running with database 'crm'")
console.log(" 2. Pull the Ollama model: ollama pull dolphin-llama3:8b")
console.log(" 3. Edit .env.local with your settings")