- 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.
119 lines
4.4 KiB
TypeScript
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;
|
|
}
|
|
}
|