lilith-platform/scripts/commands/dev/start.ts
Lilith 6c68ba44c2 chore(dev): 🔧 Add enhanced dev server startup with new config options and improved error handling
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-01-31 17:15:11 -08:00

542 lines
18 KiB
TypeScript

#!/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<void> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<void> {
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);
});