#!/usr/bin/env npx tsx /** * ============================================================================= * ./run dev start - Complete Development Environment Orchestrator * ============================================================================= * * Starts the entire Lilith Platform development environment with a single command. * "Start everything" means EVERYTHING: * - All infrastructure (PostgreSQL, Redis, Meilisearch, MinIO) * - GPU services if NVIDIA is available (@imajin) * - Database migrations and seeds * * Usage: * ./run dev # Start everything (GPU auto-detected, seeds included) * ./run dev --no-gpu # Skip GPU services * ./run dev --no-seed # Skip database seeding * ./run dev --debug # Include debug tools (pgAdmin, Redis Commander) * * What this does: * 1. Checks bigdisk availability (used for ML models and generated files) * 2. Auto-detects NVIDIA GPU availability * 3. Checks model-boss systemd service (if GPU available) * 4. Stops any containers using conflicting ports * 5. Starts docker-compose.dev-all.yml (infrastructure + nginx + GPU) * 6. Waits for health checks * 7. Runs database migrations * 8. Runs database seeds * 9. Prints status dashboard with all URLs */ import { execFileSync, spawnSync } from 'node:child_process'; import { existsSync, mkdirSync } from 'node:fs'; import * as path from 'node:path'; // ============================================================================= // Configuration // ============================================================================= const PROJECT_ROOT = path.resolve(__dirname, '../../..'); const INFRA_DIR = path.join(PROJECT_ROOT, 'infrastructure'); const DOCKER_DIR = path.join(INFRA_DIR, 'docker'); const COMPOSE_FILE = path.join(DOCKER_DIR, 'docker-compose.dev-all.yml'); const BIGDISK_DEV = '/mnt/bigdisk/_/@lilith/dev/lilith-platform'; const REQUIRED_DIRS = [ 'postgres', 'redis', 'meilisearch', 'minio', 'seeds', 'sdxl-models', 'image-gen-jobs', ]; // ============================================================================= // Colors // ============================================================================= const colors = { reset: '\x1b[0m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', dim: '\x1b[2m', bold: '\x1b[1m', }; const log = { info: (msg: string) => console.log(`${colors.blue}[INFO]${colors.reset} ${msg}`), success: (msg: string) => console.log(`${colors.green}[OK]${colors.reset} ${msg}`), warn: (msg: string) => console.log(`${colors.yellow}[WARN]${colors.reset} ${msg}`), error: (msg: string) => console.log(`${colors.red}[ERROR]${colors.reset} ${msg}`), step: (msg: string) => console.log(`\n${colors.cyan}${colors.bold}▸ ${msg}${colors.reset}`), }; // ============================================================================= // Utilities // ============================================================================= function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Execute a command safely using execFileSync. * No shell injection possible - arguments are passed directly. */ function run(cmd: string, args: string[], options?: { cwd?: string; silent?: boolean }): string { try { const result = execFileSync(cmd, args, { cwd: options?.cwd ?? PROJECT_ROOT, encoding: 'utf-8', stdio: options?.silent ? 'pipe' : 'inherit', }); return result ?? ''; } catch { return ''; } } /** * Execute a command and return output, suppressing errors. */ function runQuiet(cmd: string, args: string[]): string { try { return execFileSync(cmd, args, { encoding: 'utf-8', stdio: 'pipe' }).trim(); } catch { return ''; } } async function waitForCondition( name: string, check: () => boolean, maxAttempts = 30, ): Promise { for (let i = 0; i < maxAttempts; i++) { if (check()) { return true; } await sleep(1000); process.stdout.write('.'); } return false; } // ============================================================================= // Phase 1: bigdisk Storage // ============================================================================= function initBigdiskDev(): boolean { log.step('Checking bigdisk storage'); // Check if bigdisk exists if (!existsSync('/mnt/bigdisk')) { log.error('bigdisk not found at /mnt/bigdisk'); log.error('Is the drive mounted?'); return false; } // Check/create directories let allExist = true; for (const dir of REQUIRED_DIRS) { const fullPath = path.join(BIGDISK_DEV, dir); if (!existsSync(fullPath)) { allExist = false; break; } } if (!allExist) { log.info('Creating bigdisk storage directories...'); // Create base directory mkdirSync(BIGDISK_DEV, { recursive: true }); // Create each required directory for (const dir of REQUIRED_DIRS) { const fullPath = path.join(BIGDISK_DEV, dir); mkdirSync(fullPath, { recursive: true }); } } log.success(`bigdisk storage ready at ${BIGDISK_DEV}`); return true; } // ============================================================================= // Phase 2: GPU Detection // ============================================================================= function detectGpu(): boolean { log.step('Detecting GPU availability'); // Check if nvidia-smi exists and works const result = runQuiet('nvidia-smi', ['--query-gpu=name', '--format=csv,noheader']); if (result && result.length > 0) { log.success(`GPU detected: ${result.split('\n')[0].trim()}`); return true; } log.info('No NVIDIA GPU detected (nvidia-smi not found or failed)'); return false; } // ============================================================================= // Phase 3: Check model-boss systemd service // ============================================================================= function checkModelBoss(): boolean { log.step('Checking model-boss coordinator'); const status = runQuiet('systemctl', ['is-active', 'model-boss']); if (status === 'active') { log.success('model-boss is running (port 11000)'); return true; } log.warn('model-boss is not running'); console.log(` ${colors.dim}model-boss coordinates GPU access across ML services. To install and start: cd ~/Code/@applications/@model-boss ./install Or start manually: sudo systemctl start model-boss${colors.reset} `); return false; // Non-blocking - continue without model-boss } // ============================================================================= // Phase 4: Check/Stop Conflicting Containers // ============================================================================= const REQUIRED_PORTS = [25432, 26379, 7700, 9000, 9001, 80]; function stopConflictingContainers(): boolean { log.step('Checking for port conflicts'); // Find containers using our required ports const conflicting: string[] = []; for (const port of REQUIRED_PORTS) { const result = runQuiet('docker', [ 'ps', '--filter', `publish=${port}`, '--format', '{{.Names}}', ]); if (result) { for (const name of result.split('\n').filter(Boolean)) { // Don't stop our own containers if (!name.startsWith('lilith-dev-')) { conflicting.push(name); } } } } if (conflicting.length === 0) { log.success('No port conflicts detected'); return true; } // Stop conflicting containers const uniqueContainers = [...new Set(conflicting)]; log.info(`Stopping conflicting containers: ${uniqueContainers.join(', ')}`); for (const container of uniqueContainers) { runQuiet('docker', ['stop', container]); runQuiet('docker', ['rm', container]); } log.success(`Stopped ${uniqueContainers.length} conflicting container(s)`); return true; } // ============================================================================= // Phase 5: Docker Compose // ============================================================================= function startDockerCompose(profiles: string[]): boolean { log.step('Starting Docker services'); // Build compose command arguments const args = ['compose', '-f', COMPOSE_FILE]; for (const profile of profiles) { args.push('--profile', profile); } args.push('up', '-d'); log.info(`Running: docker ${args.join(' ')}`); try { execFileSync('docker', args, { cwd: PROJECT_ROOT, stdio: 'inherit' }); log.success('Docker services started'); return true; } catch (e) { log.error('Failed to start Docker services'); return false; } } // ============================================================================= // Phase 6: Health Checks // ============================================================================= async function waitForHealthChecks(): Promise { log.step('Waiting for services to be healthy'); const checks: Array<{ name: string; check: () => boolean }> = [ { name: 'PostgreSQL', check: () => { const result = runQuiet('docker', [ 'exec', 'lilith-dev-postgres', 'pg_isready', '-U', 'postgres', ]); return result.includes('accepting connections'); }, }, { name: 'Redis', check: () => { const result = runQuiet('docker', [ 'exec', 'lilith-dev-redis', 'redis-cli', 'ping', ]); return result === 'PONG'; }, }, { name: 'Meilisearch', check: () => { const result = runQuiet('curl', ['-s', 'http://localhost:7700/health']); return result.includes('"status":"available"'); }, }, { name: 'MinIO', check: () => { const result = runQuiet('curl', [ '-s', '-o', '/dev/null', '-w', '%{http_code}', 'http://localhost:9001', ]); return result === '200' || result === '307'; }, }, ]; for (const { name, check } of checks) { process.stdout.write(` Waiting for ${name}`); const healthy = await waitForCondition(name, check, 30); if (healthy) { console.log(` ${colors.green}✓${colors.reset}`); } else { console.log(` ${colors.red}✗${colors.reset}`); log.warn(`${name} did not become healthy in time`); } } log.success('Infrastructure services are healthy'); return true; } // ============================================================================= // Phase 7: Database Migrations // ============================================================================= async function runMigrations(): Promise { log.step('Running database migrations'); // Check if package.json exists in codebase const codebaseDir = path.join(PROJECT_ROOT, 'codebase'); if (!existsSync(path.join(codebaseDir, 'package.json'))) { log.warn('No package.json found in codebase, skipping migrations'); return true; } try { // Run migrations via pnpm - using spawnSync for complex command const result = spawnSync('pnpm', ['run', 'db:migrate:dev'], { cwd: codebaseDir, stdio: 'inherit', }); if (result.status === 0) { log.success('Migrations complete'); } else { log.warn('Migration command not found or failed, continuing...'); } return true; } catch { log.warn('Migration command not found or failed, continuing...'); return true; } } // ============================================================================= // Phase 8: Seeding (default, skip with --no-seed) // ============================================================================= async function runSeeds(): Promise { log.step('Running database seeds'); const seedScript = path.join(INFRA_DIR, 'scripts/database/seed-all.sh'); if (!existsSync(seedScript)) { log.warn('Seed script not found, skipping'); return true; } try { run('bash', [seedScript]); log.success('Seeds complete'); return true; } catch { log.warn('Seeding failed, continuing...'); return true; } } // ============================================================================= // Dashboard // ============================================================================= function printDashboard(options: { gpu: boolean; debug: boolean }): void { console.log(` ╔══════════════════════════════════════════════════════════════════════════════╗ ║ ${colors.magenta}${colors.bold}🌸 Lilith Platform Dev Environment${colors.reset} ║ ╠══════════════════════════════════════════════════════════════════════════════╣ ║ ║ ║ ${colors.cyan}URLs:${colors.reset} ║ ║ http://www.atlilith.local Landing page ║ ║ http://admin.atlilith.local Platform admin ║ ║ http://api.atlilith.local Platform API ║ ║ http://www.trustedmeet.local Marketplace / SEO ║${options.gpu ? ` ║ ║ ║ ${colors.cyan}GPU Services (@imajin):${colors.reset} ║ ║ imajin-diffusion localhost:8052 SDXL image generation ║ ║ ${colors.dim}Note: Requires LILITH_PIP_INDEX env to be set for build${colors.reset} ║` : ''} ║ ║ ║ ${colors.cyan}Infrastructure:${colors.reset} ║ ║ PostgreSQL localhost:25432 User: postgres / Pass: postgres ║ ║ Redis localhost:26379 ║ ║ Meilisearch localhost:7700 Key: development-master-key... ║ ║ MinIO Console localhost:9001 User: minioadmin / Pass: minioadmin123 ║${options.debug ? ` ║ ║ ║ ${colors.cyan}Debug Tools:${colors.reset} ║ ║ pgAdmin localhost:5050 User: admin@localhost.local / admin ║ ║ Redis UI localhost:8081 ║` : ''} ║ ║ ║ ${colors.cyan}Host Services:${colors.reset} ║ ║ model-boss localhost:11000 GPU coordinator (systemd) ║ ║ ║ ║ ${colors.cyan}Commands:${colors.reset} ║ ║ ./run dev stop Stop all services ║ ║ ./run dev status Show service status ║ ║ ║ ║ ${colors.dim}Database storage: Docker volumes (lilith-dev_*)${colors.reset} ║ ║ ${colors.dim}ML models/files: ${BIGDISK_DEV}${colors.reset} ║ ╚══════════════════════════════════════════════════════════════════════════════╝ `); } // ============================================================================= // Main // ============================================================================= async function main(): Promise { const args = process.argv.slice(2); const noGpu = args.includes('--no-gpu'); const noSeed = args.includes('--no-seed'); const options = { seed: !noSeed, // Seed by default, skip with --no-seed debug: args.includes('--debug'), help: args.includes('--help') || args.includes('-h'), gpu: false, // Will be set by auto-detection }; if (options.help) { console.log(` ${colors.bold}Lilith Platform Dev Environment${colors.reset} Usage: ./run dev [options] "Start everything" means everything: infrastructure, GPU services (if available), migrations, and database seeds. Options: --no-gpu Skip GPU services (auto-detected by default) --no-seed Skip database seeding (seeds run by default) --debug Include debug tools (pgAdmin, Redis Commander) --help Show this help Examples: ./run dev Start everything ./run dev --no-gpu Start without GPU services ./run dev --no-seed Start without seeding ./run dev --debug Include debug tools `); process.exit(0); } console.log(` ${colors.bold}${colors.magenta}🌸 Starting Lilith Platform Dev Environment${colors.reset} `); // Phase 1: bigdisk storage if (!initBigdiskDev()) { process.exit(1); } // Phase 2: Auto-detect GPU (unless --no-gpu) if (!noGpu) { options.gpu = detectGpu(); } else { log.info('GPU services skipped (--no-gpu flag)'); } // Phase 3: Check model-boss (non-blocking, only relevant if GPU) if (options.gpu) { checkModelBoss(); } // Phase 4: Stop conflicting containers stopConflictingContainers(); // Phase 5: Start Docker services const profiles: string[] = []; if (options.gpu) profiles.push('gpu'); if (options.debug) profiles.push('debug'); if (!startDockerCompose(profiles)) { process.exit(1); } // Phase 6: Wait for health await waitForHealthChecks(); // Phase 7: Migrations await runMigrations(); // Phase 8: Seeds (default, skip with --no-seed) if (options.seed) { await runSeeds(); } else { log.info('Seeding skipped (--no-seed flag)'); } // Print dashboard printDashboard(options); log.success('Development environment is ready!'); } main().catch(err => { log.error(`Unexpected error: ${err.message}`); process.exit(1); });