import { readMeta } from './daemon.js'; // ANSI colour helpers (gracefully degrade when not a TTY) const isTTY = Boolean(process.stdout.isTTY); const G = isTTY ? '\x1b[0;32m' : ''; const R = isTTY ? '\x1b[0;31m' : ''; const BOLD = isTTY ? '\x1b[1m' : ''; const RESET = isTTY ? '\x1b[0m' : ''; function ok(label: string): void { process.stdout.write(` ${G}✔${RESET} ${label.padEnd(36)}${G}[ok]${RESET}\n`); } function fail(label: string, hint: string): void { process.stdout.write(` ${R}✖${RESET} ${label.padEnd(36)}${R}[FAIL]${RESET}\n`); process.stdout.write(` ${R}↳ ${hint}${RESET}\n`); } function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } async function fetchWithRetry( url: string, opts: RequestInit = {}, retries = 3, delayMs = 1000, ): Promise { for (let attempt = 0; attempt < retries; attempt++) { try { const res = await fetch(url, opts); // Retry on non-OK responses too — the gateway may still be starting up // (e.g. 503 before the app bootstrap completes). if (res.ok) return res; } catch { // Network-level error — not ready yet, will retry } if (attempt < retries - 1) await sleep(delayMs); } return null; } export interface VerifyResult { gatewayHealthy: boolean; adminTokenOnFile: boolean; bootstrapReachable: boolean; allPassed: boolean; } /** * Run post-install verification checks. * * @param host - Gateway hostname (e.g. "localhost") * @param port - Gateway port (e.g. 14242) * @returns VerifyResult — callers can inspect individual flags */ export async function runPostInstallVerification( host: string, port: number, ): Promise { const baseUrl = `http://${host}:${port.toString()}`; console.log(`\n${BOLD}Mosaic installation verified:${RESET}`); // ─── Check 1: Gateway /health ───────────────────────────────────────────── const healthRes = await fetchWithRetry(`${baseUrl}/health`); const gatewayHealthy = healthRes !== null && healthRes.ok; if (gatewayHealthy) { ok('gateway healthy'); } else { fail('gateway healthy', 'Run: mosaic gateway status / mosaic gateway logs'); } // ─── Check 2: Admin token on file ───────────────────────────────────────── const meta = readMeta(); const adminTokenOnFile = Boolean(meta?.adminToken && meta.adminToken.length > 0); if (adminTokenOnFile) { ok('admin token on file'); } else { fail('admin token on file', 'Run: mosaic gateway config recover-token'); } // ─── Check 3: Bootstrap endpoint reachable ──────────────────────────────── const bootstrapRes = await fetchWithRetry(`${baseUrl}/api/bootstrap/status`); const bootstrapReachable = bootstrapRes !== null && bootstrapRes.ok; if (bootstrapReachable) { ok('bootstrap endpoint reach'); } else { fail('bootstrap endpoint reach', 'Run: mosaic gateway status / mosaic gateway logs'); } const allPassed = gatewayHealthy && adminTokenOnFile && bootstrapReachable; if (!allPassed) { console.log( `\n${R}One or more checks failed.${RESET} Recovery commands listed above.\n` + `Use ${BOLD}mosaic gateway status${RESET} and ${BOLD}mosaic gateway config recover-token${RESET} to investigate.\n`, ); } return { gatewayHealthy, adminTokenOnFile, bootstrapReachable, allPassed }; } /** * Standalone entry point for `mosaic gateway verify`. * Reads host/port from meta.json if not provided. */ export async function runVerify(opts: { host?: string; port?: number }): Promise { const meta = readMeta(); const host = opts.host ?? meta?.host ?? 'localhost'; const port = opts.port ?? meta?.port ?? 14242; const result = await runPostInstallVerification(host, port); if (!result.allPassed) { process.exit(1); } }