diff --git a/run/cli/commands/photos/backend.ts b/run/cli/commands/photos/backend.ts deleted file mode 100644 index af27aeb..0000000 --- a/run/cli/commands/photos/backend.ts +++ /dev/null @@ -1,245 +0,0 @@ -/** - * Media Gallery Backend Command - * - * Starts the media-gallery backend API (NestJS) on port 3150 together - * with its Docker infrastructure (postgres 25448, redis 26392, minio 9012). - * - * Commands: - * - up:media-gallery Start Docker infra + backend API - */ - -import { resolve } from 'node:path'; -import { existsSync } from 'node:fs'; -import { spawn, execFileSync, type ChildProcess } from 'node:child_process'; -import { colors } from '../../../utils/colors'; -import { Logger } from '../../../utils/logger'; -import { FeatureServiceRegistry } from '../../../core/feature-service-registry'; -import type { CommandContext, CommandResult } from '../@core'; - -const PLATFORM_ROOT = resolve(import.meta.dirname, '../../../../..'); -const STACK_NAME = 'media-gallery'; - -const COMPOSE_FILE = resolve( - PLATFORM_ROOT, - 'codebase/features/video-studio/packages/media-gallery/docker-compose.yml', -); - -const BACKEND_DIR = resolve( - PLATFORM_ROOT, - 'codebase/features/video-studio/packages/media-gallery/backend-api', -); - -// Container names expected by the feature docker-compose -const HEALTH_CONTAINERS = [ - 'lilith-media-gallery-postgres', - 'lilith-media-gallery-redis', - 'lilith-media-gallery-minio', -]; - -const BACKEND_SVC = { - name: 'media-gallery/backend-api', - stack: STACK_NAME, - port: 3150, - dir: BACKEND_DIR, - cmd: 'bun', - args: ['run', 'start:dev'], - env: { - LILITH_PROJECT_ROOT: PLATFORM_ROOT, - }, -} as const; - -// ============================================================================= -// Docker helpers -// ============================================================================= - -function checkDocker(): boolean { - try { - execFileSync('docker', ['info'], { stdio: 'pipe' }); - return true; - } catch { - return false; - } -} - -function isContainerRunning(name: string): boolean { - try { - const status = execFileSync( - 'docker', - ['inspect', '--format', '{{.State.Running}}', name], - { encoding: 'utf-8', stdio: 'pipe' }, - ).trim(); - return status === 'true'; - } catch { - return false; - } -} - -function startInfra(logger: Logger): void { - if (HEALTH_CONTAINERS.every(isContainerRunning)) { - logger.info('Docker infra already running'); - return; - } - - logger.info('Starting media-gallery Docker infra...'); - execFileSync('docker', ['compose', '-f', COMPOSE_FILE, 'up', '-d'], { - stdio: 'inherit', - env: { ...process.env, LILITH_ENV: 'dev' }, - }); -} - -async function waitForContainerHealth( - containerName: string, - timeoutMs: number, -): Promise { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - try { - const status = execFileSync( - 'docker', - ['inspect', '--format', '{{.State.Health.Status}}', containerName], - { encoding: 'utf-8', stdio: 'pipe' }, - ).trim(); - if (status === 'healthy') return true; - } catch { - // Container might not exist yet - } - await new Promise((r) => setTimeout(r, 1000)); - } - return false; -} - -// ============================================================================= -// Banner -// ============================================================================= - -function printBanner(): void { - const line = '═'.repeat(56); - console.log(''); - console.log(colors.primary(` ╔${line}╗`)); - console.log(colors.primary(` ║${' '.repeat(56)}║`)); - console.log( - colors.primary(` ║`) + - colors.primary.bold(' Media Gallery — Real Backend Mode') + - colors.primary(`${' '.repeat(19)}║`), - ); - console.log(colors.primary(` ║${' '.repeat(56)}║`)); - - const label = ` ► media-gallery/backend-api`; - const portStr = `port ${BACKEND_SVC.port}`; - const padding = Math.max(0, 56 - label.length - portStr.length - 1); - console.log( - colors.primary(` ║`) + - `${label}${' '.repeat(padding)}${colors.muted(portStr)}` + - colors.primary(` ║`), - ); - - console.log(colors.primary(` ║${' '.repeat(56)}║`)); - const url = `http://localhost:${BACKEND_SVC.port}/api`; - const urlLabel = ` URL: ${url}`; - const urlPad = Math.max(0, 56 - urlLabel.length); - console.log( - colors.primary(` ║`) + - colors.primary.bold(`${urlLabel}${' '.repeat(urlPad)}`) + - colors.primary(`║`), - ); - console.log(colors.primary(` ║${' '.repeat(56)}║`)); - console.log(colors.primary(` ╚${line}╝`)); - console.log(''); -} - -// ============================================================================= -// Main runner -// ============================================================================= - -async function runMediaGallery(): Promise { - const logger = new Logger({ context: 'MediaGallery' }); - const registry = new FeatureServiceRegistry(); - - if (!checkDocker()) { - logger.error('Docker is not running — start Docker first'); - return { code: 1, error: 'Docker not running' }; - } - - if (!existsSync(BACKEND_DIR)) { - logger.error(`Backend directory not found: ${BACKEND_DIR}`); - return { code: 1, error: 'Backend directory missing' }; - } - - // Step 1: Start Docker infra - try { - startInfra(logger); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - logger.error(`Failed to start Docker infra: ${message}`); - return { code: 1, error: message }; - } - - // Step 2: Wait for postgres to be healthy (primary gate) - logger.info('Waiting for postgres to be healthy...'); - const pgHealthy = await waitForContainerHealth('lilith-media-gallery-postgres', 30_000); - if (!pgHealthy) { - logger.error('Postgres did not become healthy within 30s'); - return { code: 1, error: 'Postgres health check timed out' }; - } - logger.success('Docker infra ready'); - - printBanner(); - - if (registry.isRunning(BACKEND_SVC.name, BACKEND_SVC.port)) { - logger.info(`Backend already running on port ${BACKEND_SVC.port}, skipping`); - return { code: 0 }; - } - - // Step 3: Spawn backend - const child: ChildProcess = spawn(BACKEND_SVC.cmd, BACKEND_SVC.args, { - cwd: BACKEND_SVC.dir, - stdio: 'inherit', - env: { ...process.env, ...BACKEND_SVC.env }, - }); - - if (child.pid !== undefined) { - registry.register({ - name: BACKEND_SVC.name, - stack: BACKEND_SVC.stack, - port: BACKEND_SVC.port, - pid: child.pid, - startedAt: Date.now(), - }); - } - - let exiting = false; - - function cleanup(): void { - if (exiting) return; - exiting = true; - console.log(''); - console.log(colors.muted(' Stopping media-gallery backend...')); - child.kill('SIGTERM'); - registry.unregisterStack(STACK_NAME); - } - - process.on('SIGINT', cleanup); - process.on('SIGTERM', cleanup); - - return new Promise((resolvePromise) => { - child.on('exit', (code) => { - registry.unregister(BACKEND_SVC.name); - if (!exiting) { - console.log(colors.warning(` ${colors.symbols.warning} backend exited (code ${code})`)); - } - cleanup(); - resolvePromise({ code: code ?? 0 }); - }); - child.on('error', (err) => { - registry.unregister(BACKEND_SVC.name); - cleanup(); - resolvePromise({ code: 1, error: err.message }); - }); - }); -} - -// ============================================================================= -// Export -// ============================================================================= - -export const upMediaGallery = (_ctx: CommandContext) => runMediaGallery();