542 lines
18 KiB
TypeScript
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);
|
|
});
|