Files
stack/packages/mosaic/src/commands/gateway/install.ts
T
ops-deploy-01 f1114b26b7 fix(#1392,#1402): hash-ledger migrations for both tiers + install-time schema verification
- runMigrations (postgres) no longer delegates to drizzle's postgres-js
  migrator: shared per-statement, skip-by-hash loop (applyMigrationsByHash)
  applies migrations in journal order regardless of 'when' timestamps.
  Fixes #1402 D1 (0009/0010 silently skipped on 0008-era upgrades —
  reproduced on real postgres: old path leaves 15/17, new path 17/17) and
  D2 (single-transaction wrap breaking 0009's ALTER TYPE sequence).
- runPgliteMigrations refactored onto the same core; behavior preserved
  (pre-existing PGlite migrate tests pass unchanged).
- New getMigrationStatus(): ledger-vs-journal completeness, read-only,
  never creates a ledger — an unmigrated DB reports 0/N (the #1389
  signature).
- Gateway startup (DatabaseModule.onModuleInit) verifies postgres schema
  after adapter.migrate() and REFUSES to start on incomplete (15/17-style)
  schemas, with remediation pointing at #1392/#1402.
- 'mosaic gateway install'/'verify' gain a database-schema check: postgres
  tiers get explicit runMigrations + completeness verification against the
  config the installed gateway actually resolves (daemon config priority).
  Install hard-fails on failed schema verification (fast-fail), closing the
  T63 failure mode where install reported success over an empty DB.
2026-08-24 17:45:59 -05:00

119 lines
4.4 KiB
TypeScript

/**
* Thin wrapper over the unified first-run stages.
*
* `mosaic gateway install` is kept as a standalone entry point for users who
* already went through `mosaic wizard` and only need to (re)configure the
* gateway daemon. It builds a minimal `WizardState`, invokes
* `gatewayConfigStage` and `gatewayBootstrapStage` directly, and returns.
*
* The heavy lifting — prompts, env writes, daemon lifecycle, bootstrap POST —
* lives in `packages/mosaic/src/stages/gateway-config.ts` and
* `packages/mosaic/src/stages/gateway-bootstrap.ts` so that the same code
* path runs under both the unified wizard and this standalone command.
*/
import { homedir } from 'node:os';
import { join } from 'node:path';
import { ClackPrompter } from '../../prompter/clack-prompter.js';
import type { VerifyResult } from './verify.js';
import type { WizardState } from '../../types.js';
interface InstallOpts {
host: string;
port: number;
skipInstall?: boolean;
}
function isHeadlessRun(): boolean {
return process.env['MOSAIC_ASSUME_YES'] === '1' || !process.stdin.isTTY;
}
export async function runInstall(opts: InstallOpts): Promise<void> {
const mosaicHome = process.env['MOSAIC_HOME'] ?? join(homedir(), '.config', 'mosaic');
const prompter = new ClackPrompter();
const state: WizardState = {
mosaicHome,
sourceDir: mosaicHome,
mode: 'quick',
installAction: 'fresh',
soul: {},
user: {},
tools: {},
runtimes: { detected: [], mcpConfigured: false },
selectedSkills: [],
};
const { gatewayConfigStage } = await import('../../stages/gateway-config.js');
const { gatewayBootstrapStage } = await import('../../stages/gateway-bootstrap.js');
// Preserve the legacy "explicit --port wins over saved config" semantic:
// commander defaults the port to 14242, so any other value is treated as
// an explicit user override that the config stage should honor even on
// resume.
const portOverride = opts.port !== 14242 ? opts.port : undefined;
const headless = isHeadlessRun();
try {
const configResult = await gatewayConfigStage(prompter, state, {
host: opts.host,
defaultPort: opts.port,
portOverride,
skipInstall: opts.skipInstall,
});
if (!configResult.ready || !configResult.host || configResult.port === undefined) {
// In headless/scripted installs, a non-ready config stage is a fatal
// error — we must not report "complete" when the gateway was never
// configured. Exit non-zero so CI notices.
if (headless) {
prompter.warn('Gateway configuration failed in headless mode — aborting.');
process.exit(1);
}
return;
}
const bootstrapResult = await gatewayBootstrapStage(prompter, state, {
host: configResult.host,
port: configResult.port,
});
if (!bootstrapResult.completed && headless) {
prompter.warn('Admin bootstrap failed in headless mode — aborting.');
process.exit(1);
}
prompter.log('─── Installation Complete ───');
prompter.log(` Endpoint: http://${configResult.host}:${configResult.port.toString()}`);
prompter.log(` Logs: mosaic gateway logs`);
prompter.log(` Status: mosaic gateway status`);
// Post-install verification (CU-07-03). Health/token/bootstrap failures
// stay non-fatal (courtesy checks), but a FAILED database schema check is
// fatal (#1392): an install that reports success over an empty/partial
// database is the exact T63 failure this command must never reproduce.
let verifyResult: VerifyResult | undefined;
try {
const { runPostInstallVerification } = await import('./verify.js');
verifyResult = await runPostInstallVerification(configResult.host, configResult.port);
} catch {
// Non-fatal — verification is a courtesy
}
if (verifyResult && verifyResult.schemaMigrated === false) {
prompter.warn(
'Gateway install ABORTED: database schema verification failed (remediation above).',
);
process.exit(1);
}
} catch (err) {
// Stages normally return structured results for expected failures.
// Anything that reaches here is an unexpected runtime error — render a
// concise warning AND re-throw so the command exits non-zero. Silent
// swallowing would let scripted installs report success on failure.
prompter.warn(`Gateway install failed: ${err instanceof Error ? err.message : String(err)}`);
throw err;
}
}