mirror of
https://git.coastit.co.za/caitlin/CRM_ENVR.git
synced 2026-07-10 19:17:16 +02:00
262 lines
9.2 KiB
JavaScript
262 lines
9.2 KiB
JavaScript
// ── One-command Setup ──────────────────────────────────────────────
|
|
// Run via: npm run setup
|
|
// Does the following in order:
|
|
// 1. npm install (Node.js dependencies)
|
|
// 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).
|
|
|
|
import { execSync } from "node:child_process"
|
|
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 ────────────────────────────────────────────────────────
|
|
|
|
function detectPython() {
|
|
const candidates = platform() === "win32" ? ["python", "python3"] : ["python3", "python"]
|
|
for (const cmd of candidates) {
|
|
try {
|
|
execSync(`${cmd} --version`, { stdio: "pipe", timeout: 5000 })
|
|
return cmd
|
|
} catch {}
|
|
}
|
|
console.error("Python not found. Install Python 3 from https://python.org")
|
|
process.exit(1)
|
|
}
|
|
|
|
function detectPip(python) {
|
|
const candidates = platform() === "win32" ? ["pip", "pip3"] : ["pip3", "pip"]
|
|
for (const cmd of candidates) {
|
|
try {
|
|
execSync(`${cmd} --version`, { stdio: "pipe", timeout: 5000 })
|
|
return cmd
|
|
} catch {}
|
|
}
|
|
return `${python} -m pip`
|
|
}
|
|
|
|
function run(cmd, label, opts = {}) {
|
|
console.log(`\n── ${label} ──`)
|
|
try {
|
|
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(`cd browser-use-service ${SEP} ${PIP} install -r requirements.txt`, "Installing Python dependencies")
|
|
|
|
// 3. Playwright browsers
|
|
run(`${PY} -m playwright install firefox chromium`, "Installing Playwright browsers")
|
|
|
|
// 4. .env file
|
|
if (!existsSync(resolve(ROOT, ".env.local"))) {
|
|
console.log("\n── Creating .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. 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")
|
|
console.log(" 4. Run: npm run dev")
|
|
console.log("\n=== Setup complete! ===")
|