feat(photos): Add new photo processing backend methods for CLI integration

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Quinn Ftw 2026-03-20 02:38:21 -07:00
parent c2ea41f331
commit 8b0472efd3

View file

@ -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<boolean> {
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<void>((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<CommandResult> {
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<CommandResult>((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();