feat: Enhance provider components with detailed documentation and comments
- Added comprehensive comments and JSDoc-style documentation to NotificationProvider, ThemeProvider, UserProvider, and WebsiteThemeProvider for better clarity and maintainability. - Improved type definitions in index.ts for better code understanding and usage. - Introduced Docker support with Dockerfiles for various services including AI server, signaling server, and browser-use service. - Created a docker-compose.yml file to orchestrate multiple services including PostgreSQL, AI, scraper, and frontend. - Added a startup guide (startup.txt) for setting up the CRM environment on Ubuntu with Docker. - Included a .dockerignore file to exclude unnecessary files from Docker builds.
This commit is contained in:
+28
-9
@@ -1,17 +1,30 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
PostgreSQL database backup with retention management and logging.
|
||||
.DESCRIPTION
|
||||
Uses pg_dump (custom format) to back up the CRM database. Tracks the
|
||||
backup in the backup_logs table and auto-prunes files older than
|
||||
$RetentionDays. Reads DATABASE_URL from the environment or .env.local.
|
||||
.PARAMETER BackupDir
|
||||
Directory where backup files are stored (default: ../backups).
|
||||
.PARAMETER RetentionDays
|
||||
Number of days to retain backups (default: 30).
|
||||
#>
|
||||
param(
|
||||
[string]$BackupDir = "$PSScriptRoot\..\backups",
|
||||
[int]$RetentionDays = 30
|
||||
)
|
||||
|
||||
# Fail fast on any error
|
||||
$ErrorActionPreference = "Stop"
|
||||
$startTime = Get-Date
|
||||
|
||||
# Ensure backup directory exists
|
||||
# ── Ensure backup directory exists ────────────────────────────────
|
||||
if (-not (Test-Path $BackupDir)) {
|
||||
New-Item -ItemType Directory -Path $BackupDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# Determine database URL from env or .env.local
|
||||
# ── Determine database URL from env or .env.local ─────────────────
|
||||
$dbUrl = $env:DATABASE_URL
|
||||
if (-not $dbUrl -and (Test-Path "$PSScriptRoot\..\.env.local")) {
|
||||
$envContent = Get-Content "$PSScriptRoot\..\.env.local" -Raw
|
||||
@@ -25,7 +38,7 @@ if (-not $dbUrl) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse the database URL
|
||||
# ── Parse the database URL into connection components ─────────────
|
||||
$uri = [System.Uri]$dbUrl
|
||||
$pgUser = $uri.UserInfo.Split(':')[0]
|
||||
$pgPass = $uri.UserInfo.Split(':')[1]
|
||||
@@ -33,7 +46,7 @@ $pgHost = $uri.Host
|
||||
$pgPort = $uri.Port
|
||||
$pgDb = $uri.AbsolutePath.TrimStart('/')
|
||||
|
||||
# Set PGPASSWORD for pg_dump
|
||||
# pg_dump reads password from this env var (avoids interactive prompt)
|
||||
$env:PGPASSWORD = $pgPass
|
||||
|
||||
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||
@@ -44,13 +57,15 @@ Write-Output "Starting backup of database '$pgDb' to $filepath"
|
||||
Write-Output "Database: $pgHost:$pgPort/$pgDb"
|
||||
|
||||
try {
|
||||
# Run pg_dump
|
||||
# ── Run pg_dump (custom format) ──────────────────────────────
|
||||
# Check PATH first, then fall back to the bundled psql location
|
||||
$pgDumpPath = if (Get-Command pg_dump -ErrorAction SilentlyContinue) {
|
||||
"pg_dump"
|
||||
} else {
|
||||
"$env:TEMP\pg\pgsql\bin\pg_dump.exe"
|
||||
}
|
||||
|
||||
# Custom format (-Fc) for compressed, restorable output
|
||||
$output = & $pgDumpPath -h $pgHost -p $pgPort -U $pgUser -d $pgDb --format=custom --verbose --file $filename 2>&1
|
||||
$exitCode = $LASTEXITCODE
|
||||
|
||||
@@ -58,11 +73,13 @@ try {
|
||||
throw "pg_dump failed with exit code $exitCode"
|
||||
}
|
||||
|
||||
# Move to final backup location (dg_dump writes to CWD first)
|
||||
Move-Item -Path $filename -Destination $filepath -Force
|
||||
$fileInfo = Get-Item $filepath
|
||||
$fileSize = $fileInfo.Length
|
||||
|
||||
# Verify backup by checking if file is valid custom format
|
||||
# ── Verify backup integrity ──────────────────────────────────
|
||||
# Run a schema-only dump against the same file to check it's valid custom format
|
||||
$verifyOutput = & $pgDumpPath -h $pgHost -p $pgPort -U $pgUser -d $pgDb --format=custom --schema-only --file "$filename.verify" 2>&1
|
||||
$verifyExitCode = $LASTEXITCODE
|
||||
if (Test-Path "$filename.verify") { Remove-Item "$filename.verify" -Force }
|
||||
@@ -73,7 +90,8 @@ try {
|
||||
$endTime = Get-Date
|
||||
$durationSeconds = [math]::Round(($endTime - $startTime).TotalSeconds)
|
||||
|
||||
# Log the backup
|
||||
# ── Log backup to database ───────────────────────────────────
|
||||
# Records metadata in backup_logs table for audit trail
|
||||
$logQuery = @"
|
||||
INSERT INTO backup_logs (backup_type, status, file_name, file_size_bytes, pg_dump_exit_code, verification_status, started_at, completed_at, duration_seconds, retention_days)
|
||||
VALUES ('full', 'completed', '$filename', $fileSize, $exitCode, '$verificationStatus', '$($startTime.ToString("yyyy-MM-dd HH:mm:ss"))', '$($endTime.ToString("yyyy-MM-dd HH:mm:ss"))', $durationSeconds, $RetentionDays);
|
||||
@@ -93,7 +111,7 @@ VALUES ('full', 'completed', '$filename', $fileSize, $exitCode, '$verificationSt
|
||||
Write-Output " Duration: ${durationSeconds}s"
|
||||
Write-Output " Status: $verificationStatus"
|
||||
|
||||
# Cleanup old backups (beyond retention period)
|
||||
# ── Cleanup old backups beyond retention period ──────────────
|
||||
$cutoff = (Get-Date).AddDays(-$RetentionDays)
|
||||
$oldBackups = Get-ChildItem $BackupDir -Filter "crm_backup_*.sql" | Where-Object {
|
||||
$_.CreationTime -lt $cutoff
|
||||
@@ -106,7 +124,7 @@ VALUES ('full', 'completed', '$filename', $fileSize, $exitCode, '$verificationSt
|
||||
} catch {
|
||||
Write-Error "Backup failed: $_"
|
||||
|
||||
# Log failure
|
||||
# Log failure to database for monitoring
|
||||
$endTime = Get-Date
|
||||
$durationSeconds = [math]::Round(($endTime - $startTime).TotalSeconds)
|
||||
$errorMsg = $_.ToString().Replace("'", "''")
|
||||
@@ -121,5 +139,6 @@ VALUES ('full', 'failed', '$filename', '$errorMsg', $exitCode, 'failed', '$($sta
|
||||
|
||||
exit 1
|
||||
} finally {
|
||||
# Clean up the password environment variable for security
|
||||
Remove-Item "Env:PGPASSWORD" -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
@@ -28,19 +28,23 @@ import { createRequire } from "node:module"
|
||||
const SELF = dirname(fileURLToPath(import.meta.url))
|
||||
const _require = createRequire(import.meta.url)
|
||||
const ROOT = resolve(SELF, "..")
|
||||
// Ollama endpoint and model can be overridden via environment variables
|
||||
const OLLAMA_HOST = process.env.OLLAMA_HOST || "http://localhost:11434"
|
||||
const MODEL = process.env.REPAIR_MODEL || "qwen2.5-coder:1.5b-base"
|
||||
// Safety limits: max repair iterations, minimum AI confidence to auto-apply, escalation timeout
|
||||
const MAX_ITERATIONS = 5
|
||||
const CONFIDENCE_THRESHOLD = 0.7
|
||||
const ESCALATION_TIMEOUT_MS = 30 * 60 * 1000 // 30 min
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/** Log a message with a level-based prefix for visual scanning of output. */
|
||||
function log(msg, level = "info") {
|
||||
const prefix = level === "error" ? "✗" : level === "warn" ? "⚠" : "✓"
|
||||
console.log(` ${prefix} [repair] ${msg}`)
|
||||
}
|
||||
|
||||
/** Run `npx tsc --noEmit` and return the output (success or failure). */
|
||||
function runTsc() {
|
||||
try {
|
||||
const out = execSync("npx tsc --noEmit", { stdio: "pipe", timeout: 60000, cwd: ROOT })
|
||||
@@ -50,6 +54,7 @@ function runTsc() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Parse TypeScript compiler output into structured error objects with file, line, column, and message. */
|
||||
function parseErrors(output) {
|
||||
const errors = []
|
||||
// Match: src/file.ts(line,col): error TSxxxx: message
|
||||
@@ -66,6 +71,7 @@ function parseErrors(output) {
|
||||
return errors
|
||||
}
|
||||
|
||||
/** Read source file lines surrounding the error, returning a snippet with context line offset. */
|
||||
function readContext(filePath, errorLine, contextLines = 20) {
|
||||
try {
|
||||
const lines = readFileSync(filePath, "utf-8").split("\n")
|
||||
@@ -83,6 +89,7 @@ function readContext(filePath, errorLine, contextLines = 20) {
|
||||
|
||||
// ── Ollama Integration ─────────────────────────────────────────────
|
||||
|
||||
/** Send a repair prompt to the Ollama model and return the raw response text. */
|
||||
async function queryOllama(prompt) {
|
||||
const response = await fetch(`${OLLAMA_HOST}/api/generate`, {
|
||||
method: "POST",
|
||||
@@ -99,6 +106,7 @@ async function queryOllama(prompt) {
|
||||
return data.response || ""
|
||||
}
|
||||
|
||||
/** Build a structured prompt for the AI model, including file path, error message, and surrounding code context. */
|
||||
function buildRepairPrompt(filePath, errorMessage, codeContext) {
|
||||
return `You are a TypeScript repair agent. Fix the following error.
|
||||
|
||||
@@ -122,6 +130,7 @@ Rules:
|
||||
- If you cannot determine a fix, set confidence to 0`
|
||||
}
|
||||
|
||||
/** Parse the AI's JSON response (handles optional markdown fences around the JSON block). */
|
||||
function parseFixResponse(text) {
|
||||
try {
|
||||
// Extract JSON from response (it might have markdown fences)
|
||||
@@ -133,6 +142,7 @@ function parseFixResponse(text) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Apply a fix suggestion to disk, returning success/failure and a backup of the original content. */
|
||||
function applyFix(filePath, fixSuggestion) {
|
||||
if (!fixSuggestion || !fixSuggestion.fix) return { applied: false, reason: "No fix suggestion" }
|
||||
|
||||
@@ -147,8 +157,8 @@ function applyFix(filePath, fixSuggestion) {
|
||||
|
||||
// ── Breaking Change Escalation ─────────────────────────────────────
|
||||
|
||||
/** Heuristic check: if any fix touches imports, exports, or deletions, consider it a breaking change. */
|
||||
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 ")) {
|
||||
@@ -158,6 +168,7 @@ function isBreakingChange(errors, fixes) {
|
||||
return false
|
||||
}
|
||||
|
||||
/** Log the breaking change, wait for the escalation timeout, then auto-apply. */
|
||||
async function escalateBreakingChange(errors, fixes) {
|
||||
const logFile = resolve(ROOT, "repair-escalation.log")
|
||||
const timestamp = new Date().toISOString()
|
||||
@@ -181,6 +192,7 @@ async function escalateBreakingChange(errors, fixes) {
|
||||
|
||||
// ── Git Auto-Commit ────────────────────────────────────────────────
|
||||
|
||||
/** Git commit all changes with a [bot]-prefixed message. Returns false on failure (non-blocking). */
|
||||
function gitCommit(message) {
|
||||
try {
|
||||
execSync("git add -A", { stdio: "pipe", cwd: ROOT })
|
||||
@@ -195,6 +207,7 @@ function gitCommit(message) {
|
||||
|
||||
// ── Main Repair Loop ───────────────────────────────────────────────
|
||||
|
||||
/** Single pass: run tsc, parse errors, ask AI for fixes, apply them, and verify. Returns result summary. */
|
||||
async function repairOnce(doCommit = false) {
|
||||
log("Running build check...")
|
||||
const build = runTsc()
|
||||
@@ -276,6 +289,7 @@ async function repairOnce(doCommit = false) {
|
||||
return { fixed: remainingErrors.length === 0, errors: remainingErrors, fixes }
|
||||
}
|
||||
|
||||
/** Run up to maxIterations repair passes, stopping once all errors are resolved or no progress is made. */
|
||||
async function repairLoop(maxIterations = MAX_ITERATIONS, doCommit = false) {
|
||||
for (let i = 0; i < maxIterations; i++) {
|
||||
log(`=== Repair iteration ${i + 1}/${maxIterations} ===`)
|
||||
@@ -293,8 +307,10 @@ async function repairLoop(maxIterations = MAX_ITERATIONS, doCommit = false) {
|
||||
|
||||
// ── Watch Mode ─────────────────────────────────────────────────────
|
||||
|
||||
/** Watch mode: monitor src/ for file changes and auto-repair errors. Falls back to polling if chokidar is unavailable. */
|
||||
function watchMode() {
|
||||
log("Watch mode active — monitoring src/ for changes...", "info")
|
||||
// Try loading chokidar; if not installed, fall back to simple interval polling
|
||||
const chokidar = (() => {
|
||||
try {
|
||||
return _require("chokidar")
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { execSync, spawn } from "node:child_process"
|
||||
import { platform } from "node:os"
|
||||
|
||||
/** Check if Ollama is already running by querying the OS process list. */
|
||||
function isRunning() {
|
||||
try {
|
||||
if (platform() === "win32") {
|
||||
@@ -20,6 +21,7 @@ function isRunning() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Locate the Ollama binary via PATH, then fall back to common install directories. */
|
||||
function findOllama() {
|
||||
if (platform() === "win32") {
|
||||
// Try PATH first, then common install locations
|
||||
@@ -49,7 +51,7 @@ if (!isRunning()) {
|
||||
} else {
|
||||
spawn(bin, ["serve"], { stdio: "ignore", detached: true }).unref()
|
||||
}
|
||||
// Give it a moment to start listening
|
||||
// Give it a moment to start listening before we return control
|
||||
execSync("sleep 3", { stdio: "ignore", timeout: 5000 })
|
||||
} else {
|
||||
console.log("Ollama already running")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// ── Module Generator ─────────────────────────────────────────────────
|
||||
// Generates missing internal modules from templates.
|
||||
// Supports built-in templates and custom .setup-templates/ directory.
|
||||
// Part of the self-healing setup pipeline — invoked when tsc reports
|
||||
// TS2307 errors for @/ imports that don't exist yet.
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
@@ -12,6 +14,7 @@ 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");
|
||||
|
||||
/** Maps internal import paths (e.g. "data/stickers") to their template file and optional params. */
|
||||
export interface TemplateRegistry {
|
||||
[importPath: string]: {
|
||||
template: string;
|
||||
@@ -20,6 +23,7 @@ export interface TemplateRegistry {
|
||||
};
|
||||
}
|
||||
|
||||
/** Result of a single module generation attempt. */
|
||||
export interface GenerationResult {
|
||||
success: boolean;
|
||||
filePath: string;
|
||||
@@ -28,18 +32,21 @@ export interface GenerationResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Maps glob patterns (e.g. "@/hooks/*") to fallback template file names. */
|
||||
export interface FallbackRegistry {
|
||||
[pattern: string]: string;
|
||||
}
|
||||
|
||||
/** Load the template registry from .setup-templates/registry.json */
|
||||
/** Load the template registry from .setup-templates/registry.json, merging with built-in templates. */
|
||||
export function loadRegistry(): { templates: TemplateRegistry; fallbacks: FallbackRegistry } {
|
||||
// Built-in mappings for the most commonly imported modules
|
||||
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 } },
|
||||
};
|
||||
// Directory-level catch-all fallbacks for unknown imports
|
||||
const builtInFallbacks: FallbackRegistry = {
|
||||
"@/data/*": "data-generic.ts",
|
||||
"@/lib/*": "lib-generic.ts",
|
||||
@@ -49,6 +56,7 @@ export function loadRegistry(): { templates: TemplateRegistry; fallbacks: Fallba
|
||||
"@/utils/*": "utils-generic.ts",
|
||||
};
|
||||
|
||||
// Merge in any custom templates from the .setup-templates directory
|
||||
try {
|
||||
if (fs.existsSync(REGISTRY_PATH)) {
|
||||
const content = fs.readFileSync(REGISTRY_PATH, "utf-8");
|
||||
@@ -64,10 +72,10 @@ export function loadRegistry(): { templates: TemplateRegistry; fallbacks: Fallba
|
||||
return { templates: builtInTemplates, fallbacks: builtInFallbacks };
|
||||
}
|
||||
|
||||
/** Match an internal path against fallback patterns */
|
||||
/** Match an internal path (e.g. "data/stickers") against glob fallback patterns. Converts glob to regex internally. */
|
||||
export function matchFallback(internalPath: string, fallbacks: FallbackRegistry): string | undefined {
|
||||
for (const [pattern, template] of Object.entries(fallbacks)) {
|
||||
// Convert glob pattern to regex
|
||||
// Convert glob pattern (e.g. "@/hooks/*") to a regular expression
|
||||
const regexStr = "^" + pattern
|
||||
.replace(/\//g, "\\/")
|
||||
.replace(/\./g, "\\.")
|
||||
@@ -82,7 +90,7 @@ export function matchFallback(internalPath: string, fallbacks: FallbackRegistry)
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Render a template with parameters */
|
||||
/** Render a template string by replacing {{ paramName }} placeholders with actual values. */
|
||||
function renderTemplate(templateContent: string, params: Record<string, unknown> = {}): string {
|
||||
let result = templateContent;
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
@@ -92,7 +100,7 @@ function renderTemplate(templateContent: string, params: Record<string, unknown>
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Get template content - first from custom templates dir, then built-in */
|
||||
/** Get template content — first tries the custom .setup-templates/templates dir, then falls back to built-in embedded templates. */
|
||||
function getTemplateContent(templateName: string): string {
|
||||
// Check custom templates first
|
||||
const customPath = path.join(TEMPLATES_DIR, templateName);
|
||||
@@ -100,7 +108,7 @@ function getTemplateContent(templateName: string): string {
|
||||
return fs.readFileSync(customPath, "utf-8");
|
||||
}
|
||||
|
||||
// Built-in templates (embedded)
|
||||
// Built-in templates (embedded in the script — no external file needed)
|
||||
const builtInTemplates: Record<string, string> = {
|
||||
"stickers.ts": `// ── Sticker Packs ─────────────────────────────────────────────────
|
||||
// Auto-generated by self-healing setup. Edit .setup-templates/templates/stickers.ts to customize.
|
||||
@@ -312,16 +320,16 @@ export interface DashboardStats {
|
||||
return builtInTemplates[templateName] || "";
|
||||
}
|
||||
|
||||
/** Generate a single missing module */
|
||||
/** Generate a single missing module: resolve template, render parameters, write to src/. */
|
||||
export function generateModule(
|
||||
internalPath: string,
|
||||
templates: TemplateRegistry,
|
||||
fallbacks: FallbackRegistry
|
||||
): GenerationResult {
|
||||
// Look up exact template first, then fall back to directory-level glob patterns
|
||||
let entry = templates[internalPath];
|
||||
let templateFile = entry?.template;
|
||||
|
||||
// Try fallback patterns
|
||||
if (!entry) {
|
||||
const fallbackTemplate = matchFallback(internalPath, fallbacks);
|
||||
if (fallbackTemplate) {
|
||||
@@ -349,10 +357,11 @@ export function generateModule(
|
||||
};
|
||||
}
|
||||
|
||||
// Replace template parameter placeholders with actual values
|
||||
const rendered = renderTemplate(templateContent, entry.params || {});
|
||||
const filePath = path.join(ROOT, "src", internalPath + (internalPath.endsWith(".ts") ? "" : ".ts"));
|
||||
|
||||
// Ensure directory exists
|
||||
// Ensure directory exists before writing
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
@@ -377,7 +386,7 @@ export function generateModule(
|
||||
}
|
||||
}
|
||||
|
||||
/** Generate all missing modules for a list of internal paths */
|
||||
/** Generate all missing modules for a list of internal paths, logging each result. */
|
||||
export function generateAll(internalPaths: string[]): GenerationResult[] {
|
||||
const { templates, fallbacks } = loadRegistry();
|
||||
const results: GenerationResult[] = [];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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.
|
||||
// Used by both setup.mjs (self-heal) and code-repair-agent.mjs.
|
||||
|
||||
export interface MissingModule {
|
||||
/** The import path that failed, e.g. "@/data/stickers" */
|
||||
@@ -20,7 +21,7 @@ export interface MissingModule {
|
||||
internalPath?: string;
|
||||
}
|
||||
|
||||
/** Parse TypeScript compiler output for missing module errors */
|
||||
/** Parse TypeScript compiler output for missing module errors (TS2307 + webpack-style "Can't resolve"). */
|
||||
export function parseMissingModules(tscOutput: string): MissingModule[] {
|
||||
const results: MissingModule[] = [];
|
||||
|
||||
@@ -74,12 +75,12 @@ export function parseMissingModules(tscOutput: string): MissingModule[] {
|
||||
});
|
||||
}
|
||||
|
||||
/** Filter to only internal (@/...) modules we can auto-generate */
|
||||
/** Filter to only internal (@/... or ~/...) modules we can auto-generate from templates. */
|
||||
export function filterGeneratable(modules: MissingModule[]): MissingModule[] {
|
||||
return modules.filter((m) => m.isInternal && m.internalPath);
|
||||
}
|
||||
|
||||
/** Group missing modules by internal path */
|
||||
/** Group missing modules by their normalized internal path (deduplicates same module imported from multiple files). */
|
||||
export function groupByInternalPath(modules: MissingModule[]): Map<string, MissingModule[]> {
|
||||
const groups = new Map<string, MissingModule[]>();
|
||||
for (const m of modules) {
|
||||
|
||||
@@ -9,11 +9,14 @@ import { platform } from "node:os"
|
||||
|
||||
const url = process.argv[2] || "http://localhost:3001/splash"
|
||||
|
||||
/** Promise-based sleep helper for the startup delay. */
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
async function main() {
|
||||
// Wait for the AI server, scraper, and frontend to finish booting
|
||||
await sleep(8000)
|
||||
try {
|
||||
// Platform-specific command to open the default browser
|
||||
if (platform() === "win32") {
|
||||
execSync(`start "" "${url}"`, { stdio: "ignore", timeout: 5000 })
|
||||
} else if (platform() === "darwin") {
|
||||
|
||||
@@ -12,6 +12,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const root = resolve(__dirname, "..")
|
||||
|
||||
try {
|
||||
// Read package.json and check each dependency's package.json in node_modules
|
||||
const pkg = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"))
|
||||
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
||||
|
||||
@@ -45,6 +46,7 @@ if (platform() === "win32") {
|
||||
const out = execSync(`netstat -ano | findstr "LISTENING" | findstr ":${port} "`, { encoding: "utf8", timeout: 5000 })
|
||||
const lines = out.trim().split("\n").filter(Boolean)
|
||||
for (const line of lines) {
|
||||
// Parse the last column of netstat output — the PID
|
||||
const parts = line.trim().split(/\s+/)
|
||||
const pid = parts[parts.length - 1]
|
||||
if (pid) {
|
||||
|
||||
@@ -14,6 +14,8 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = resolve(__dirname, "..")
|
||||
|
||||
// ── Load .env.local into process.env ─────────────────────────────────
|
||||
// Simple key=value parser (no dotenv dependency needed).
|
||||
// Handles quoted values and skips comments / blank lines.
|
||||
|
||||
const envPath = resolve(ROOT, ".env.local")
|
||||
if (existsSync(envPath)) {
|
||||
@@ -24,9 +26,11 @@ if (existsSync(envPath)) {
|
||||
if (eq === -1) continue
|
||||
const key = trimmed.slice(0, eq).trim()
|
||||
let value = trimmed.slice(eq + 1).trim()
|
||||
// Strip matching surrounding quotes (single or double)
|
||||
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
||||
value = value.slice(1, -1)
|
||||
}
|
||||
// Don't override already-set env vars
|
||||
if (!process.env[key]) process.env[key] = value
|
||||
}
|
||||
}
|
||||
@@ -38,6 +42,7 @@ if (!DATABASE_URL) {
|
||||
}
|
||||
|
||||
// ── Find psql ────────────────────────────────────────────────────────
|
||||
// Check PATH first, then probe common PostgreSQL v14–17 install paths.
|
||||
|
||||
function findPsql() {
|
||||
try {
|
||||
@@ -84,8 +89,9 @@ try {
|
||||
|
||||
// ── Migration logic ──────────────────────────────────────────────────
|
||||
|
||||
/** Run all unapplied database migrations in the order defined by run_all.sql. */
|
||||
async function runMigrations() {
|
||||
// 1. Create tracking table if not exists
|
||||
// 1. Ensure the _migrations tracking table exists (idempotent)
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS _migrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
@@ -94,11 +100,12 @@ async function runMigrations() {
|
||||
)
|
||||
`)
|
||||
|
||||
// 2. Determine file order from run_all.sql (authoritative)
|
||||
// 2. Determine file order from run_all.sql (authoritative source of truth)
|
||||
const runAllPath = resolve(ROOT, "database", "migrations", "run_all.sql")
|
||||
const runAllContent = readFileSync(runAllPath, "utf-8")
|
||||
const fileOrder = []
|
||||
for (const line of runAllContent.split("\n")) {
|
||||
// `\i filename.sql` is the psql include directive
|
||||
const match = line.match(/\\i\s+(\S+\.sql)/)
|
||||
if (match) fileOrder.push(match[1])
|
||||
}
|
||||
@@ -117,7 +124,7 @@ async function runMigrations() {
|
||||
appliedSet = new Set()
|
||||
}
|
||||
|
||||
// 4. Build psql command base
|
||||
// 4. Build psql command base with ON_ERROR_STOP=1 so psql aborts on first error
|
||||
const psqlCmd = `${PSQL} -d "${DATABASE_URL}" -v ON_ERROR_STOP=1`
|
||||
|
||||
// 5. Run unapplied files in order.
|
||||
@@ -133,6 +140,7 @@ async function runMigrations() {
|
||||
|
||||
let count = 0
|
||||
for (const file of fileOrder) {
|
||||
// Skip files already recorded in the _migrations table
|
||||
if (appliedSet.has(file)) continue
|
||||
|
||||
const filePath = resolve(ROOT, "database", "migrations", file)
|
||||
@@ -149,12 +157,14 @@ async function runMigrations() {
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
})
|
||||
// psql outputs notices/warnings to stderr even on success
|
||||
// psql outputs notices/warnings to stderr even on success — capture for diagnostics
|
||||
stderr = result.stderr || ""
|
||||
} catch (err) {
|
||||
// Distinguish between "already applied" (harmless) and genuine failures
|
||||
const msg = err.stderr || err.stdout || err.message
|
||||
const isAlreadyApplied = ALREADY_APPLIED_PATTERNS.some((p) => msg.includes(p))
|
||||
if (isAlreadyApplied) {
|
||||
// File was applied before tracking existed — record it and continue
|
||||
await pool.query("INSERT INTO _migrations (filename) VALUES ($1) ON CONFLICT DO NOTHING", [file])
|
||||
console.log(` ~ Already applied: ${file}`)
|
||||
count++
|
||||
@@ -165,6 +175,7 @@ async function runMigrations() {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Record successful migration
|
||||
await pool.query("INSERT INTO _migrations (filename) VALUES ($1)", [file])
|
||||
console.log(` ✓ Applied: ${file}`)
|
||||
count++
|
||||
|
||||
@@ -8,7 +8,9 @@ import { execSync, spawn } from "node:child_process"
|
||||
import { platform } from "node:os"
|
||||
import { statSync } from "node:fs"
|
||||
|
||||
/** Locate the Python 3 executable. Checks common install paths first, then the system PATH. */
|
||||
function detectPython() {
|
||||
// Pre-check common Windows install directories to avoid PATH lookup
|
||||
const commonPaths = [
|
||||
`${process.env.LOCALAPPDATA}\\Programs\\Python\\Python313\\python.exe`,
|
||||
`${process.env.LOCALAPPDATA}\\Programs\\Python\\Python312\\python.exe`,
|
||||
@@ -21,6 +23,7 @@ function detectPython() {
|
||||
return p
|
||||
} catch {}
|
||||
}
|
||||
// Fall back to PATH resolution (prefer python3 on Unix)
|
||||
const candidates = platform() === "win32" ? ["python", "python3"] : ["python3", "python"]
|
||||
for (const cmd of candidates) {
|
||||
try {
|
||||
@@ -42,6 +45,6 @@ if (!script) {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Spawn Python with inherited stdio so the script's output is visible
|
||||
// Spawn Python with inherited stdio so the script's output is visible in real-time
|
||||
const proc = spawn(PYTHON, [script, ...args], { stdio: "inherit" })
|
||||
proc.on("exit", (code) => process.exit(code ?? 1))
|
||||
|
||||
+20
-5
@@ -15,12 +15,14 @@ import { platform } from "node:os"
|
||||
import { resolve, dirname } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
// Shell command separator differs per platform (Windows uses &, POSIX uses ;)
|
||||
const SEP = platform() === "win32" ? "&" : ";"
|
||||
const SELF = dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = resolve(SELF, "..")
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/** Try known Python command names until one succeeds, then exit if none found. */
|
||||
function detectPython() {
|
||||
const candidates = platform() === "win32" ? ["python", "python3"] : ["python3", "python"]
|
||||
for (const cmd of candidates) {
|
||||
@@ -33,6 +35,7 @@ function detectPython() {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
/** Detect pip; fall back to `python -m pip` if no standalone pip binary exists. */
|
||||
function detectPip(python) {
|
||||
const candidates = platform() === "win32" ? ["pip", "pip3"] : ["pip3", "pip"]
|
||||
for (const cmd of candidates) {
|
||||
@@ -44,6 +47,7 @@ function detectPip(python) {
|
||||
return `${python} -m pip`
|
||||
}
|
||||
|
||||
/** Run a shell command with label output. Exits on failure unless opts.optional is set. */
|
||||
function run(cmd, label, opts = {}) {
|
||||
console.log(`\n── ${label} ──`)
|
||||
try {
|
||||
@@ -60,11 +64,15 @@ function run(cmd, label, opts = {}) {
|
||||
}
|
||||
|
||||
// ── Self-Healing Module Registry ───────────────────────────────────
|
||||
// Maps internal import paths (like @/data/stickers) to template files.
|
||||
// Supports custom templates in .setup-templates/ that override built-ins.
|
||||
// Fallback patterns catch whole directories (e.g. @/hooks/* → hook-generic.ts).
|
||||
|
||||
function loadRegistry() {
|
||||
const registryPath = resolve(ROOT, ".setup-templates", "registry.json")
|
||||
const templatesDir = resolve(ROOT, ".setup-templates", "templates")
|
||||
|
||||
// Hard-coded templates for commonly imported modules
|
||||
const builtInTemplates = {
|
||||
"data/stickers": { template: "stickers.ts" },
|
||||
"data/constants": { template: "constants.ts" },
|
||||
@@ -75,6 +83,7 @@ function loadRegistry() {
|
||||
"hooks/use-local-storage": { template: "hooks/use-local-storage.ts" },
|
||||
}
|
||||
|
||||
// Catch-all fallbacks for entire directory prefixes
|
||||
const builtInFallbacks = {
|
||||
"@/data/*": "data-generic.ts",
|
||||
"@/lib/*": "lib-generic.ts",
|
||||
@@ -87,6 +96,7 @@ function loadRegistry() {
|
||||
let templates = { ...builtInTemplates }
|
||||
let fallbacks = { ...builtInFallbacks }
|
||||
|
||||
// Merge custom templates from .setup-templates/registry.json (if it exists)
|
||||
try {
|
||||
if (existsSync(registryPath)) {
|
||||
const custom = JSON.parse(readFileSync(registryPath, "utf-8"))
|
||||
@@ -97,6 +107,7 @@ function loadRegistry() {
|
||||
|
||||
const templateCache = {}
|
||||
|
||||
/** Read a template file, checking the custom templates dir first, then hooks subdirectory. */
|
||||
function getTemplate(templateName) {
|
||||
if (templateCache[templateName]) return templateCache[templateName]
|
||||
const candidatePaths = [
|
||||
@@ -113,6 +124,7 @@ function loadRegistry() {
|
||||
return ""
|
||||
}
|
||||
|
||||
/** Convert a glob pattern (e.g. @/hooks/*) to a regex and test against the internal path. */
|
||||
function matchFallback(internalPath) {
|
||||
for (const [pattern, templateFile] of Object.entries(fallbacks)) {
|
||||
const regexStr = "^" + pattern
|
||||
@@ -125,6 +137,7 @@ function loadRegistry() {
|
||||
return null
|
||||
}
|
||||
|
||||
/** Generate (or regenerate) a single module file from its template. */
|
||||
function generateModule(internalPath) {
|
||||
let templateFile = templates[internalPath]?.template
|
||||
if (!templateFile) templateFile = matchFallback(internalPath)
|
||||
@@ -143,6 +156,7 @@ function loadRegistry() {
|
||||
return { generateModule }
|
||||
}
|
||||
|
||||
/** Parse `tsc --noEmit` or `npm run build` output for missing-module errors, returning unique import paths. */
|
||||
function parseMissingModules(buildOutput) {
|
||||
const results = []
|
||||
const seen = new Set()
|
||||
@@ -177,21 +191,22 @@ function parseMissingModules(buildOutput) {
|
||||
|
||||
// ── Main ───────────────────────────────────────────────────────────
|
||||
|
||||
// Detect Python and pip executables before running any steps
|
||||
const PY = detectPython()
|
||||
const PIP = detectPip(PY)
|
||||
|
||||
console.log("=== CoastIT CRM Setup ===\n")
|
||||
|
||||
// 1. Node dependencies
|
||||
// Step 1 — Install all Node.js dependencies (includes next, react, etc.)
|
||||
run("npm install", "Installing Node.js dependencies")
|
||||
|
||||
// 2. Python dependencies
|
||||
// Step 2 — Install Python deps for the browser-use service (Playwright-based scraper)
|
||||
run(`cd browser-use-service ${SEP} ${PIP} install -r requirements.txt`, "Installing Python dependencies")
|
||||
|
||||
// 3. Playwright browsers
|
||||
// Step 3 — Download Playwright browser binaries for scraper automation
|
||||
run(`${PY} -m playwright install firefox chromium`, "Installing Playwright browsers")
|
||||
|
||||
// 4. .env file
|
||||
// Step 4 — Bootstrap .env.local from the example template (won't overwrite existing)
|
||||
if (!existsSync(resolve(ROOT, ".env.local"))) {
|
||||
console.log("\n── Creating .env.local ──")
|
||||
copyFileSync(resolve(ROOT, ".env.example"), resolve(ROOT, ".env.local"))
|
||||
@@ -200,7 +215,7 @@ if (!existsSync(resolve(ROOT, ".env.local"))) {
|
||||
console.log("\n── .env.local already exists, skipping ──")
|
||||
}
|
||||
|
||||
// 5. Final summary
|
||||
// Step 5 — Print post-setup instructions
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user