platform-tooling/run/cli/index.ts
Quinn Ftw a7a99cf1f8 feat(cli): Implement 'up' CLI command and register it in the CLI entry point
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-10 17:37:52 -07:00

482 lines
22 KiB
JavaScript

#!/usr/bin/env node
/**
* Lilith Platform - Unified Run CLI
*
* Portable Docker-based development and production orchestration.
*
* Usage:
* ./run dev Start dev cluster (.local domains, HMR enabled)
* ./run prod Start production cluster (real domains, SSL)
* ./run <script> Passthrough to bun
*/
import { colors } from '../utils/colors.js';
import { Logger } from '../utils/logger.js';
import type { CommandContext, CommandResult, CommandHandler } from './types.js';
// Eagerly import lightweight commands (no external package dependencies)
// These must work even before `bun install` has run
import { passthrough } from './commands/passthrough';
import { install, update } from './commands/workspace';
// =============================================================================
// Command Registry with Lazy Loading
// =============================================================================
// Commands that can run without external dependencies
const eagerCommands: Record<string, CommandHandler> = {
'install': install,
'i': install, // alias
'update': update,
};
// Commands that require external packages (lazy loaded)
// Maps command name to [module path, export name]
const lazyCommands: Record<string, [string, string]> = {
// Development
'dev': ['./commands/dev/index', 'dev'],
'dev:platform': ['./commands/dev/index', 'dev'], // alias
'dev:ci': ['./commands/dev/index', 'devCi'],
'dev:infra': ['./commands/dev/index', 'devInfra'],
'dev:all': ['./commands/dev/index', 'devAll'],
'dev:tools': ['./commands/dev/index', 'devTools'],
'dev:stop': ['./commands/dev/index', 'devStop'],
'dev:stop:noptty': ['./commands/dev/index', 'devStopNoptty'],
'dev:cleanup': ['./commands/dev/index', 'devCleanup'],
'dev:clean': ['./commands/dev/index', 'devClean'],
'dev:status': ['./commands/dev/index', 'devStatus'],
'dev:watch': ['./commands/dev/index', 'devWatch'],
'dev:logs': ['./commands/dev/index', 'devLogs'],
'dev:resume': ['./commands/dev/index', 'devResume'],
'dev:restart': ['./commands/dev/index', 'devRestart'],
'dev:reset': ['./commands/dev/index', 'devReset'],
'dev:fresh': ['./commands/dev/index', 'devFresh'],
'dev:debug': ['./commands/dev/index', 'devDebug'],
'verify': ['./commands/workspace/index', 'verify'],
'dev:verify': ['./commands/workspace/index', 'verifyDev'],
'build': ['./commands/workspace/index', 'build'],
// Production
'prod': ['./commands/prod/index', 'prod'],
'prod:platform': ['./commands/prod/index', 'prod'], // alias
'prod:stop': ['./commands/prod/index', 'prodStop'],
'prod:status': ['./commands/prod/index', 'prodStatus'],
'prod:logs': ['./commands/prod/index', 'prodLogs'],
'prod:restart': ['./commands/prod/index', 'prodRestart'],
'prod:health': ['./commands/prod/index', 'prodHealth'],
// Next (pre-prod manual deploy to black)
'next': ['./commands/next/index', 'next'],
'next:platform': ['./commands/next/index', 'next'],
'next:stop': ['./commands/next/index', 'nextStop'],
'next:status': ['./commands/next/index', 'nextStatus'],
'next:logs': ['./commands/next/index', 'nextLogs'],
'next:restart': ['./commands/next/index', 'nextRestart'],
'next:health': ['./commands/next/index', 'nextHealth'],
// Domain-specific startup (up:*)
'up:status': ['./commands/up/index', 'upStatus'],
'up:admin': ['./commands/up/index', 'upAdmin'],
'up:analytics': ['./commands/up/index', 'upAnalytics'],
'up:quinn.analytics': ['./commands/up/index', 'upQuinnAnalytics'],
'up:atlilith': ['./commands/up/index', 'upAtlilith'],
'up:trustedmeet': ['./commands/up/index', 'upTrustedmeet'],
'up:spoiledbabes': ['./commands/up/index', 'upSpoiledbabes'],
'up:lilithcam': ['./commands/up/index', 'upLilithcam'],
'up:lilithstage': ['./commands/up/index', 'upLilithstage'],
'up:video-studio': ['./commands/up/index', 'upVideoStudio'],
// Domain aliases (domain.com style)
'trustedmeet.com': ['./commands/up/index', 'upTrustedmeet'],
'atlilith.com': ['./commands/up/index', 'upAtlilith'],
'spoiledbabes.com': ['./commands/up/index', 'upSpoiledbabes'],
'lilith.cam': ['./commands/up/index', 'upLilithcam'],
'lilithcam.com': ['./commands/up/index', 'upLilithcam'],
'lilithstage.com': ['./commands/up/index', 'upLilithstage'],
// Codebase maintenance
'codebase': ['./commands/codebase', 'codebase'],
// Domain management
'domains': ['./commands/domains/index', 'domains'],
'domains:list': ['./commands/domains/index', 'domainsList'],
'domains:validate': ['./commands/domains/index', 'domainsValidate'],
'domains:build': ['./commands/domains/index', 'domainsBuild'],
'domains:sync': ['./commands/domains/index', 'domainsSync'],
'domains:new': ['./commands/domains/index', 'domainsNew'],
// E2E Testing
'e2e:prod': ['./commands/e2e/index', 'e2eProd'],
// Performance Metrics
'perf': ['./commands/perf/index', 'perf'],
// DNS Management (IAC for dnsmasq)
'dns:sync': ['./commands/dns/index', 'dnsSync'],
'dns:check': ['./commands/dns/index', 'dnsCheck'],
'dns:test': ['./commands/dns/index', 'dnsTest'],
// Infrastructure status (hosts + domain tiers)
'status': ['./commands/status/index', 'status'],
'status:hosts': ['./commands/status/index', 'statusHosts'],
'status:domains': ['./commands/status/index', 'statusDomains'],
// Crystal — Knowledge verification AI
'crystal': ['./commands/crystal', 'crystal'],
// iOS (Remote Mac Build & Test)
'ios': ['./commands/ios/index', 'ios'],
'ios:build': ['./commands/ios/index', 'iosBuild'],
'ios:dev': ['./commands/ios/index', 'ios'], // alias
'ios:test': ['./commands/ios/index', 'iosTest'],
'ios:ui-test': ['./commands/ios/index', 'iosUiTest'],
'ios:screenshot': ['./commands/ios/index', 'iosScreenshot'],
'ios:screenshots': ['./commands/ios/index', 'iosScreenshots'],
'ios:launch': ['./commands/ios/index', 'iosLaunch'],
'ios:sync': ['./commands/ios/index', 'iosSync'],
'up:media-gallery': ['./commands/up/index', 'upMediaGallery'],
// LilithIPhotos macOS client (plum)
'photos:deploy': ['./commands/photos/index', 'photosDeploy'],
'photos:status': ['./commands/photos/index', 'photosStatus'],
'photos:logs': ['./commands/photos/index', 'photosLogs'],
'photos:stop': ['./commands/photos/index', 'photosStop'],
// Mock development (MSW, no Docker)
'mock:marketplace': ['./commands/mock/index', 'mockMarketplace'],
'mock:landing': ['./commands/mock/index', 'mockLanding'],
'mock:profile': ['./commands/mock/index', 'mockProfile'],
'mock:profile-assistant': ['./commands/mock/index', 'mockProfileAssistant'],
'mock:list': ['./commands/mock/index', 'mockList'],
// SEO Frame Evaluation
'seo:frames': ['../../../operations/seo-strategy/frames/index', 'evaluate'],
'seo:frames-all': ['../../../operations/seo-strategy/frames/index', 'evaluateAll'],
// Streaming feature (standalone stack)
'stream': ['./commands/stream/index', 'stream'],
'stream:dev': ['./commands/stream/index', 'streamDev'],
'stream:prod': ['./commands/stream/index', 'streamProd'],
'stream:stop': ['./commands/stream/index', 'streamStop'],
'stream:logs': ['./commands/stream/index', 'streamLogs'],
};
/**
* Get command handler, loading lazily if needed
*/
async function getHandler(command: string): Promise<CommandHandler | null> {
// Check eager commands first
if (eagerCommands[command]) {
return eagerCommands[command];
}
// Check lazy commands
const lazyDef = lazyCommands[command];
if (lazyDef) {
const [modulePath, exportName] = lazyDef;
try {
const module = await import(modulePath);
return module[exportName] as CommandHandler;
} catch (error) {
// If module fails to load due to missing dependencies, provide helpful error
if (error instanceof Error && error.message.includes('Cannot find module')) {
const logger = new Logger({ context: 'CLI' });
logger.error(`Command '${command}' requires dependencies that are not installed.`);
console.log('');
console.log(colors.muted(' Run ./run install first to install dependencies.'));
console.log('');
process.exit(1);
}
throw error;
}
}
return null;
}
// =============================================================================
// Help
// =============================================================================
function printHelp(): void {
console.log(`
${colors.primary.bold('Lilith Platform - Unified Run Command')}
${colors.accent('Usage:')} ./run <command> [options]
${colors.accent('Development Commands:')}
dev [group] Start dev cluster (.local domains)
Default group: platform (full platform)
Use --groups to list available deployment groups
Example: ./run dev, ./run dev tools, ./run dev minimal
dev:ci Start dev cluster with plain logs (no TUI, CI-friendly)
Same as dev but forces non-interactive mode
dev:tools Start platform content tools (alias for: dev tools)
dev:infra Start Docker infrastructure only (databases, caches)
dev:all Start extended cluster (alias for: dev extended)
dev:stop Stop all dev containers
dev:stop:noptty Stop all dev containers (no TUI — safe for non-TTY callers)
dev:cleanup Kill orphan dev processes by pattern (emergency cleanup)
dev:clean Clean Vite/TypeScript caches (--dry-run to preview)
dev:status Show status of all dev containers
dev:watch [n] Live status monitor (refresh every n seconds, Ctrl+C to exit)
dev:logs [svc] View container logs (all or specific service)
dev:resume Resume cluster (reconcile running state, only adjust differences)
dev:restart Full restart (stop + clean + install + start)
Flags: --skip-clean, --skip-install
dev:reset Stop and remove volumes (fresh DB reset)
dev:fresh Reset + start (completely fresh environment)
dev:debug Diagnose running/frozen dev cluster
verify Run lint, typecheck, and build on entire workspace
dev:verify Run lint, typecheck, and build on dev platform only
Step flags: --lint, --typecheck, --build (run specific steps only)
${colors.accent('Domain-Specific Startup:')}
up:status Start status.atlilith.local only (lightest - 2 services)
up:admin Start admin.atlilith.local (SSO + admin - 3 services)
up:analytics Start analytics.atlilith.local (business intelligence - 3 services)
up:quinn.analytics Start data.quinn.apricot.lan (provider analytics - 2 services)
up:atlilith Start www.atlilith.local (landing + SEO - 5 services)
up:trustedmeet Start www.trustedmeet.local (marketplace + SEO - 5 services)
up:spoiledbabes Start www.spoiledbabes.local (marketplace + SEO - 5 services)
up:lilithcam Start www.lilithcam.local (cam marketplace + SEO - 5 services)
up:lilithstage Start www.lilithstage.local (stage marketplace + SEO - 5 services)
up:video-studio Start Video Studio (backend 3035, demo 5174) + imajin-video (8010) + imajin-adversarial (8011)
up:media-gallery Start Media Gallery API (3150) + Docker infra (postgres 25448, redis 26392, minio 9012)
${colors.accent('Domain Aliases:')}
trustedmeet.com Start TrustedMeet marketplace (alias for up:trustedmeet)
atlilith.com Start AtLilith landing (alias for up:atlilith)
spoiledbabes.com Start SpoiledBabes marketplace (alias for up:spoiledbabes)
lilith.cam Start LilithCam marketplace (alias for up:lilithcam)
lilithstage.com Start LilithStage marketplace (alias for up:lilithstage)
${colors.accent('Mock Development (No Docker):')}
mock:marketplace Start marketplace with MSW mocks (port 5120)
mock:landing Start landing with MSW mocks (port 5110)
mock:profile Start profile showcase with MSW mocks (port 5130)
mock:profile-assistant Start profile + AI assistant with MSW mocks (port 5130)
mock:list List available mock targets
${colors.accent('Streaming Feature (Standalone Stack):')}
stream Start streaming stack in prod mode (default)
stream:dev Dev mode: watch mode, dev DB volumes
stream:prod Prod mode: compiled builds, prod DB volumes
stream:stop Stop all streaming containers + backend processes
stream:logs Follow Docker container logs (streaming + SSO)
Services: SSO (4001), Streaming API (3130)
Docker: streaming-postgres (25468), streaming-redis (26398),
sso-postgres (25440), sso-redis (26386)
${colors.accent('Production Commands:')}
prod [group] Start production cluster (real domains, SSL)
Default group: platform (same group resolution as dev)
prod:stop Stop production cluster
prod:status Show production container status
prod:logs [svc] View production logs
prod:restart Zero-downtime rolling restart
prod:health Run production health checks
${colors.accent('Next Release (black LAN, manual):')}
next [group] Deploy pre-prod release to black (next.*.local, LAN)
Default group: platform
next:stop Stop next release services
next:status Show next release container status
next:logs [svc] View next release logs
next:restart Rolling restart next release
next:health Health check next.*.local URLs on black
${colors.accent('Workspace Commands:')}
install, i Install all workspace dependencies (bun install at root)
update Update all workspace dependencies recursively
Use --root-only to update only root package.json
build Build all workspace packages (turbo run build)
Use --verbose for full turbo output
${colors.accent('Domain Management:')}
domains List all domains with status
domains:list Same as above
domains:validate Validate all services.yaml against schema
domains:build Generate nginx configs from services.yaml
domains:sync Build + reload nginx
domains:new <name> Scaffold new domain configuration
${colors.accent('E2E Testing:')}
e2e:prod Run E2E tests with real SSO auth (production builds)
Auth bypass disabled (import.meta.env.DEV = false)
Flags: --headed, --grep=<pattern>, --keep, --build-only
${colors.accent('Performance Metrics:')}
perf [site] Collect browser performance metrics (production builds)
Sites: trustedmeet, atlilith (default: both)
Starts isolated Docker cluster, measures, tears down
Flags: --skip-build, --keep-cluster, --json
${colors.accent('DNS Management (dnsmasq):')}
dns:sync Sync dnsmasq config with .local domains from deployments
Updates /etc/dnsmasq.d/lilith-local.conf (sudo required)
Flags: --dry-run, --quiet
dns:check Check if dnsmasq config is current (non-modifying)
Exit 0 if synced, exit 1 if changes needed
dns:test Test DNS resolution for all .local domains
${colors.accent('SEO Frame Evaluation:')}
seo:frames <frame> Evaluate a branded frame against SEO signals (autocomplete, trends, competitors)
Example: ./run seo:frames "community verification"
seo:frames-all Evaluate all branded frames from TERMS.md
Flags: --json
${colors.accent('Infrastructure Status:')}
status Unified overview: hosts (SSH) + domains by priority tier
status:hosts Hosts only — SSH reachability per network group
status:domains Domains only — health checks grouped by priority tier
Flags: --watch [n] Live refresh every n seconds (default 5)
--tier N Filter to priority tier N (0, 1, 2)
--dev Force *.local domain checks
--prod Force production domain checks
Environment: reads deployments/.env AUTODETECT_ENVIRONMENT
auto-probes status.atlilith.local if unset
${colors.accent('Crystal — Knowledge AI:')}
crystal Start interactive Crystal chat (default: chat)
crystal chat Interactive knowledge assistant REPL
crystal scan Scan platform content for inconsistencies
crystal verify Verify content accuracy against platform facts
crystal status Show Crystal service health and model info
crystal train Run Crystal training pipeline
${colors.accent('iOS Commands (Remote Mac):')}
ios Sync, build, and launch iOS app in dev mode (default)
ios:build Sync and build iOS app (no tests or launch)
ios:dev Same as ios — dev mode with mock data
ios:test Full pipeline: sync, build, run unit tests
ios:ui-test Run UI tests on iOS simulator
ios:screenshot Take screenshot of running iOS simulator
ios:screenshots Run screenshot test suite (captures all 16 screens)
ios:launch Launch app on simulator (skip build/sync)
ios:sync Sync source files to remote Mac only
Use --simulator=NAME to override (default: iPhone 16 Pro)
${colors.accent('Photos Commands (LilithIPhotos — plum):')}
photos:deploy Full deploy to plum (sync + build + install LaunchAgent)
photos:status Check LilithIPhotos status on plum (PID, lastSync, apiURL)
photos:logs Tail LilithIPhotos logs from plum (Ctrl+C to stop)
photos:stop Stop LilithIPhotos on plum
${colors.accent('Codebase Maintenance:')}
codebase fix-scripts Fix package.json scripts (nest → npx @nestjs/cli)
codebase audit-deps Audit for missing dependencies (--fix to auto-add)
${colors.accent('Passthrough Commands:')}
<script> Any other command passes through to bun
Examples: ./run build, ./run test, ./run dev:marketplace
${colors.accent('Options:')}
--verbose, -v Enable verbose logging with detailed file output
Creates timestamped log at .local/logs/dev/dev-latest.log
Example: ./run dev --verbose
--json Output structured JSON (with dev:ci only)
Useful for parsing in CI pipelines
Example: ./run dev:ci --json | jq '.summary.status'
${colors.accent('Primary Domains (Development):')}
${colors.primary('http://status.atlilith.local')} Status Dashboard
${colors.primary('http://admin.atlilith.local')} Platform Admin
${colors.primary('http://www.atlilith.local')} Landing Site
${colors.primary('http://www.trustedmeet.local')} TrustedMeet Marketplace
${colors.primary('http://www.spoiledbabes.local')} SpoiledBabes Marketplace
${colors.primary('http://www.lilithcam.local')} LilithCam Marketplace
${colors.primary('http://www.lilithstage.local')} LilithStage Marketplace
${colors.accent('Prerequisites:')}
Development:
1. Docker and Docker Compose v2
2. Local DNS: sudo ./tooling/scripts/dev-setup/setup-local-dns.sh
3. Node.js 18+ and bun 8+
Production:
1. Docker and Docker Compose v2
2. SSL certificates in /etc/letsencrypt (Let's Encrypt)
3. Update vault secrets in .env.prod
${colors.accent('Examples:')}
./run dev # Start full dev environment (platform group)
./run dev --groups # List available deployment groups
./run dev --verbose # Start with detailed file logging
./run dev:debug # Diagnose running/frozen cluster
./run dev:fresh # Reset DBs + start fresh
./run dev:reset # Just reset (stop + remove volumes)
./run dev:all # Dev + pgAdmin + Redis UI + GPU
./run dev:stop # Stop everything
./run up:status # Quick start: status dashboard only
./run up:trustedmeet # Quick start: marketplace + SEO
./run mock:marketplace # Marketplace with MSW — no Docker needed
./run prod # Production deployment
./run build # Build all packages
./run test # Run all tests
`);
}
// =============================================================================
// Main Entry Point
// =============================================================================
export async function main(args: string[]): Promise<void> {
// Suppress noisy warnings
const originalWarn = console.warn;
console.warn = (...warnArgs: unknown[]) => {
const msg = warnArgs[0];
if (typeof msg === 'string') {
if (msg.includes('[Nest]') && msg.includes('WARN')) return;
if (msg.includes('DomainEventsEmitter')) return;
if (msg.includes('ExperimentalWarning')) return;
}
originalWarn.apply(console, warnArgs);
};
try {
const [command, ...commandArgs] = args;
// No command or help
if (!command || command === 'help' || command === '--help' || command === '-h') {
printHelp();
process.exit(0);
}
// Create context
const ctx: CommandContext = {
args: commandArgs.filter(arg => !['--verbose', '-v', '--json'].includes(arg)),
env: command.startsWith('prod') ? 'prod' : command.startsWith('next') ? 'staging' : 'dev',
verbose: commandArgs.includes('--verbose') || commandArgs.includes('-v'),
json: commandArgs.includes('--json'),
};
// Look up command handler (lazy loading for heavy commands)
const handler = await getHandler(command);
if (handler) {
const result = await handler(ctx);
process.exit(result.code);
}
// Not a known command - passthrough to bun
const result = await passthrough({ ...ctx, args: [command, ...commandArgs] });
process.exit(result.code);
} catch (error) {
const logger = new Logger({ context: 'CLI' });
logger.error('Fatal error', error instanceof Error ? error : new Error(String(error)));
process.exit(1);
} finally {
console.warn = originalWarn;
}
}
// Run if executed directly
const isDirectRun = import.meta.url === `file://${process.argv[1]}`;
if (isDirectRun) {
main(process.argv.slice(2));
}