/** * gateway-doctor.ts — `mosaic gateway doctor` implementation. * * Reports current tier and per-service health (PG, Valkey, pgvector) for the * Mosaic gateway. Supports machine-readable JSON output for CI. * * Exit codes: * 0 — overall green or yellow * 1 — overall red (at least one required service failed) */ import { existsSync } from 'node:fs'; import { resolve, join } from 'node:path'; import { homedir } from 'node:os'; import { loadConfig } from '@mosaicstack/config'; import { probeServiceHealth } from '@mosaicstack/storage'; import type { TierHealthReport, ServiceCheck } from '@mosaicstack/storage'; /* ------------------------------------------------------------------ */ /* Config resolution */ /* ------------------------------------------------------------------ */ const CONFIG_CANDIDATES = [ resolve(process.cwd(), 'mosaic.config.json'), join(homedir(), '.mosaic', 'mosaic.config.json'), ]; /** * Resolve the config path to report in output. * * Priority: * 1. Explicit `--config ` flag * 2. `./mosaic.config.json` (cwd) * 3. `~/.mosaic/mosaic.config.json` * 4. undefined — `loadConfig()` falls back to env-var detection * * `loadConfig()` itself already handles priority 1-3 when passed an explicit * path, and falls back to env-detection when none exists. We resolve here * only so we can surface the path in the health report. */ function resolveConfigPath(explicit?: string): string | undefined { if (explicit) return resolve(explicit); for (const candidate of CONFIG_CANDIDATES) { if (existsSync(candidate)) return candidate; } return undefined; } /* ------------------------------------------------------------------ */ /* Output helpers */ /* ------------------------------------------------------------------ */ const TICK = '\u2713'; // ✓ const CROSS = '\u2717'; // ✗ const SKIP = '-'; function padRight(s: string, n: number): string { return s + ' '.repeat(Math.max(0, n - s.length)); } function serviceLabel(svc: ServiceCheck): string { const hostPort = svc.host !== undefined && svc.port !== undefined ? `${svc.host}:${svc.port.toString()}` : ''; const duration = `(${svc.durationMs.toString()}ms)`; switch (svc.status) { case 'ok': return ` ${TICK} ${padRight(svc.name, 10)} ${padRight(hostPort, 22)} ${duration}`; case 'fail': { const errMsg = svc.error?.message ?? 'unknown error'; return ` ${CROSS} ${padRight(svc.name, 10)} ${padRight(hostPort, 22)} ${duration} \u2192 ${errMsg}`; } case 'skipped': return ` ${SKIP} ${padRight(svc.name, 10)} (skipped)`; } } function printReport(report: TierHealthReport): void { const configDisplay = report.configPath ?? '(auto-detected)'; console.log(`Tier: ${report.tier} Config: ${configDisplay}`); console.log(''); for (const svc of report.services) { console.log(serviceLabel(svc)); } console.log(''); // Print remediations for failed services. const failed = report.services.filter((s) => s.status === 'fail' && s.error); if (failed.length > 0) { console.log('Remediations:'); for (const svc of failed) { if (svc.error) { console.log(` ${svc.name}: ${svc.error.remediation}`); } } console.log(''); } console.log(`Overall: ${report.overall.toUpperCase()}`); } /* ------------------------------------------------------------------ */ /* Main runner */ /* ------------------------------------------------------------------ */ export interface GatewayDoctorOptions { json?: boolean; config?: string; } export async function runGatewayDoctor(opts: GatewayDoctorOptions): Promise { const configPath = resolveConfigPath(opts.config); let mosaicConfig; try { mosaicConfig = loadConfig(configPath); } catch (err) { const msg = err instanceof Error ? err.message : String(err); if (opts.json) { process.stdout.write( JSON.stringify({ error: `Failed to load config: ${msg}` }, null, 2) + '\n', ); } else { process.stderr.write(`Error: Failed to load config: ${msg}\n`); } process.exit(1); } const report = await probeServiceHealth(mosaicConfig, configPath); if (opts.json) { process.stdout.write(JSON.stringify(report, null, 2) + '\n'); } else { printReport(report); } // Exit 1 if overall is red. if (report.overall === 'red') { process.exit(1); } }