feat(photos): ✨ Add new photo processing backend methods for CLI integration
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
c2ea41f331
commit
8b0472efd3
1 changed files with 0 additions and 245 deletions
|
|
@ -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();
|
||||
Loading…
Add table
Reference in a new issue