chore: consolidate new foundation and archive v1 (#1495)
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
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<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function fetchWithRetry(
|
||||
url: string,
|
||||
opts: RequestInit = {},
|
||||
retries = 3,
|
||||
delayMs = 1000,
|
||||
): Promise<Response | null> {
|
||||
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;
|
||||
/** False only on a FAILED postgres schema check; true when ok or skipped. */
|
||||
schemaMigrated: 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<VerifyResult> {
|
||||
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');
|
||||
}
|
||||
|
||||
// ─── Check 4: Database schema migrated (#1392) ────────────────────────────
|
||||
// Fatal-on-failure for install: the #1389 failure mode was an install that
|
||||
// reported success over an empty database. Local tiers skip (the gateway
|
||||
// migrates its own PGlite at startup and would fail health if it couldn't).
|
||||
let schemaMigrated = true;
|
||||
try {
|
||||
const { checkDatabaseSchema } = await import('./schema-check.js');
|
||||
const { runMigrations, getMigrationStatus } = await import('@mosaicstack/db');
|
||||
const result = await checkDatabaseSchema(
|
||||
{ runMigrations, getMigrationStatus },
|
||||
undefined, // resolver mirrors daemon file priorities; env has no config authority (N1)
|
||||
);
|
||||
if (result.status === 'ok') {
|
||||
ok(result.detail);
|
||||
} else if (result.status === 'skipped') {
|
||||
ok(result.detail);
|
||||
} else {
|
||||
fail(result.detail, result.remediation);
|
||||
schemaMigrated = false;
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
fail(
|
||||
`database schema check errored: ${msg}`,
|
||||
'See #1392/#1389; re-run: mosaic gateway install',
|
||||
);
|
||||
schemaMigrated = false;
|
||||
}
|
||||
|
||||
const allPassed = gatewayHealthy && adminTokenOnFile && bootstrapReachable && schemaMigrated;
|
||||
|
||||
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, schemaMigrated, 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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user