Added qwen to serve as an repair agent.
This commit is contained in:
@@ -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)
|
||||
})
|
||||
@@ -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<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;
|
||||
}
|
||||
@@ -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<string>();
|
||||
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<string, MissingModule[]> {
|
||||
const groups = new Map<string, MissingModule[]>();
|
||||
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;
|
||||
}
|
||||
+195
-16
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user