Files
stack/packages/mosaic/src/commands/gateway.ts
jason.woltje 872c124581
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
feat(mosaic): unified first-run UX wizard -> gateway install -> verify (#418)
2026-04-05 07:29:17 +00:00

210 lines
8.7 KiB
TypeScript

import type { Command } from 'commander';
import {
getDaemonPid,
readMeta,
startDaemon,
stopDaemon,
waitForHealth,
} from './gateway/daemon.js';
import { getGatewayUrl } from './gateway/login.js';
interface GatewayParentOpts {
host: string;
port: string;
token?: string;
}
function resolveOpts(raw: GatewayParentOpts): { host: string; port: number; token?: string } {
const meta = readMeta();
return {
host: raw.host ?? meta?.host ?? 'localhost',
port: parseInt(raw.port, 10) || meta?.port || 14242,
token: raw.token ?? meta?.adminToken,
};
}
export function registerGatewayCommand(program: Command): void {
const gw = program
.command('gateway')
.description('Manage the Mosaic gateway daemon')
.helpOption('--help', 'Display help')
.option('-h, --host <host>', 'Gateway host', 'localhost')
.option('-p, --port <port>', 'Gateway port', '14242')
.option('-t, --token <token>', 'Admin API token')
.configureHelp({ sortSubcommands: true })
.action(() => {
gw.outputHelp();
});
// ─── install ────────────────────────────────────────────────────────────
gw.command('install')
.description('Install and configure the gateway daemon')
.option('--skip-install', 'Skip npm package installation (use local build)')
.action(async (cmdOpts: { skipInstall?: boolean }) => {
const opts = resolveOpts(gw.opts() as GatewayParentOpts);
const { runInstall } = await import('./gateway/install.js');
await runInstall({ ...opts, skipInstall: cmdOpts.skipInstall });
});
// ─── start ──────────────────────────────────────────────────────────────
gw.command('start')
.description('Start the gateway daemon')
.action(async () => {
const opts = resolveOpts(gw.opts() as GatewayParentOpts);
try {
const pid = startDaemon();
console.log(`Gateway started (PID ${pid.toString()})`);
console.log('Waiting for health...');
const healthy = await waitForHealth(opts.host, opts.port);
if (healthy) {
console.log(`Gateway ready at http://${opts.host}:${opts.port.toString()}`);
} else {
console.warn('Gateway started but health check timed out. Check logs.');
}
} catch (err) {
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
}
});
// ─── stop ───────────────────────────────────────────────────────────────
gw.command('stop')
.description('Stop the gateway daemon')
.action(async () => {
try {
await stopDaemon();
console.log('Gateway stopped.');
} catch (err) {
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
}
});
// ─── restart ────────────────────────────────────────────────────────────
gw.command('restart')
.description('Restart the gateway daemon')
.action(async () => {
const opts = resolveOpts(gw.opts() as GatewayParentOpts);
const pid = getDaemonPid();
if (pid !== null) {
console.log('Stopping gateway...');
await stopDaemon();
}
console.log('Starting gateway...');
try {
const newPid = startDaemon();
console.log(`Gateway started (PID ${newPid.toString()})`);
const healthy = await waitForHealth(opts.host, opts.port);
if (healthy) {
console.log(`Gateway ready at http://${opts.host}:${opts.port.toString()}`);
} else {
console.warn('Gateway started but health check timed out. Check logs.');
}
} catch (err) {
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
}
});
// ─── status ─────────────────────────────────────────────────────────────
gw.command('status')
.description('Show gateway daemon status and health')
.action(async () => {
const opts = resolveOpts(gw.opts() as GatewayParentOpts);
const { runStatus } = await import('./gateway/status.js');
await runStatus(opts);
});
// ─── login ──────────────────────────────────────────────────────────────
gw.command('login')
.description('Sign in to the gateway (defaults to URL from meta.json)')
.option('-g, --gateway <url>', 'Gateway URL (overrides meta.json)')
.option('-e, --email <email>', 'Email address')
.option(
'-p, --password <password>',
'[UNSAFE] Avoid — exposes credentials in shell history and process listings',
)
.action(async (cmdOpts: { gateway?: string; email?: string; password?: string }) => {
const { runLogin } = await import('./gateway/login.js');
const url = getGatewayUrl(cmdOpts.gateway);
if (cmdOpts.password) {
console.warn(
'Warning: --password flag exposes credentials in shell history and process listings.',
);
}
try {
await runLogin({ gatewayUrl: url, email: cmdOpts.email, password: cmdOpts.password });
} catch (err) {
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
}
});
// ─── config ─────────────────────────────────────────────────────────────
const configCmd = gw
.command('config')
.description('View or modify gateway configuration')
.option('--set <KEY=VALUE>', 'Set a configuration value')
.option('--unset <KEY>', 'Remove a configuration key')
.option('--edit', 'Open config in $EDITOR')
.action(async (cmdOpts: { set?: string; unset?: string; edit?: boolean }) => {
const { runConfig } = await import('./gateway/config.js');
await runConfig(cmdOpts);
});
configCmd
.command('rotate-token')
.description('Mint a new admin token using the stored BetterAuth session')
.option('-g, --gateway <url>', 'Gateway URL (overrides meta.json)')
.action(async (cmdOpts: { gateway?: string }) => {
const { runRotateToken } = await import('./gateway/token-ops.js');
await runRotateToken(cmdOpts.gateway);
});
configCmd
.command('recover-token')
.description('Recover an admin token — prompts for login if no valid session exists')
.option('-g, --gateway <url>', 'Gateway URL (overrides meta.json)')
.action(async (cmdOpts: { gateway?: string }) => {
const { runRecoverToken } = await import('./gateway/token-ops.js');
await runRecoverToken(cmdOpts.gateway);
});
// ─── logs ───────────────────────────────────────────────────────────────
gw.command('logs')
.description('View gateway daemon logs')
.option('-f, --follow', 'Follow log output')
.option('-n, --lines <count>', 'Number of lines to show', '50')
.action(async (cmdOpts: { follow?: boolean; lines?: string }) => {
const { runLogs } = await import('./gateway/logs.js');
runLogs({ follow: cmdOpts.follow, lines: parseInt(cmdOpts.lines ?? '50', 10) });
});
// ─── verify ─────────────────────────────────────────────────────────────
gw.command('verify')
.description('Verify the gateway installation (health, token, bootstrap endpoint)')
.action(async () => {
const opts = resolveOpts(gw.opts() as GatewayParentOpts);
const { runVerify } = await import('./gateway/verify.js');
await runVerify(opts);
});
// ─── uninstall ──────────────────────────────────────────────────────────
gw.command('uninstall')
.description('Uninstall the gateway daemon and optionally remove data')
.action(async () => {
const { runUninstall } = await import('./gateway/uninstall.js');
await runUninstall();
});
}