108 lines
3.8 KiB
TypeScript
108 lines
3.8 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 { 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) — non-fatal.
|
|
try {
|
|
const { runPostInstallVerification } = await import('./verify.js');
|
|
await runPostInstallVerification(configResult.host, configResult.port);
|
|
} catch {
|
|
// Non-fatal — verification is a courtesy
|
|
}
|
|
} 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;
|
|
}
|
|
}
|