Compare commits

..
Author SHA1 Message Date
Jason Woltje dc67590a96 fix(installer): propagate wizard gateway failures (#1120)
ci/woodpecker/pr/ci Pipeline was successful
2026-08-09 05:01:09 -05:00
6 changed files with 56 additions and 83 deletions
+2 -2
View File
@@ -48,7 +48,7 @@ mosaic wizard # Full guided setup (gateway install → verify)
### Requirements
- Node.js ≥ 22
- Node.js ≥ 20
- npm (for global @mosaicstack/mosaic install)
- One or more runtimes: [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex](https://github.com/openai/codex), [OpenCode](https://opencode.ai), or [Pi](https://github.com/mariozechner/pi-coding-agent)
@@ -200,7 +200,7 @@ Consent state is persisted in config. Remote upload is a no-op until you run `mo
### Prerequisites
- Node.js ≥ 22
- Node.js ≥ 20
- pnpm 10.6+
- Docker & Docker Compose
@@ -193,16 +193,19 @@ describe('Unified wizard (runWizard with default skipGateway)', () => {
'Your timezone': 'UTC',
});
await runWizard({
mosaicHome: tmpDir,
sourceDir: tmpDir,
prompter,
configService: createConfigService(tmpDir, tmpDir),
skipGatewayNpmInstall: true,
});
await expect(
runWizard({
mosaicHome: tmpDir,
sourceDir: tmpDir,
prompter,
configService: createConfigService(tmpDir, tmpDir),
skipGatewayNpmInstall: true,
}),
).rejects.toThrow('Gateway configuration failed');
const logs = prompter.getLogs();
expect(logs.some((line) => line.includes('Gateway did not become healthy'))).toBe(true);
expect(logs.some((line) => line.includes('Gateway configuration failed'))).toBe(true);
expect(logs.some((line) => line.includes('Installation Summary'))).toBe(false);
expect(logs.some((line) => line.includes('Mosaic is ready.'))).toBe(false);
expect(gatewayConfigMock).toHaveBeenCalledTimes(1);
+6 -10
View File
@@ -68,8 +68,6 @@ export async function quickStartPath(
// Gateway config + bootstrap
if (!options.skipGateway) {
const headlessRun = process.env['MOSAIC_ASSUME_YES'] === '1' || !process.stdin.isTTY;
try {
const configResult = await gatewayConfigStage(prompter, state, {
host: options.gatewayHost ?? 'localhost',
@@ -81,11 +79,9 @@ export async function quickStartPath(
});
if (!configResult.ready || !configResult.host || !configResult.port) {
if (headlessRun) {
prompter.warn('Gateway configuration failed in headless mode — aborting wizard.');
process.exit(1);
}
return;
const message = 'Gateway configuration failed — aborting wizard.';
prompter.warn(message);
throw new Error(message);
}
const bootstrapResult = await gatewayBootstrapStage(prompter, state, {
@@ -93,9 +89,9 @@ export async function quickStartPath(
port: configResult.port,
});
if (!bootstrapResult.completed) {
prompter.warn('Admin bootstrap failed — aborting wizard.');
process.exit(1);
return;
const message = 'Admin bootstrap failed — aborting wizard.';
prompter.warn(message);
throw new Error(message);
}
finalizeResult.showSummary();
} catch (err) {
+34 -28
View File
@@ -348,18 +348,21 @@ async function runFinishPath(
providerType: state.providerType,
});
if (configResult.ready && configResult.host && configResult.port) {
const bootstrapResult = await gatewayBootstrapStage(prompter, state, {
host: configResult.host,
port: configResult.port,
});
if (!bootstrapResult.completed) {
prompter.warn('Admin bootstrap failed — aborting wizard.');
process.exit(1);
return;
}
finalizeResult.showSummary();
if (!configResult.ready || !configResult.host || !configResult.port) {
const message = 'Gateway configuration failed — aborting wizard.';
prompter.warn(message);
throw new Error(message);
}
const bootstrapResult = await gatewayBootstrapStage(prompter, state, {
host: configResult.host,
port: configResult.port,
});
if (!bootstrapResult.completed) {
const message = 'Admin bootstrap failed — aborting wizard.';
prompter.warn(message);
throw new Error(message);
}
finalizeResult.showSummary();
} catch (err) {
prompter.warn(`Gateway setup failed: ${err instanceof Error ? err.message : String(err)}`);
throw err;
@@ -420,9 +423,9 @@ async function runHeadlessPath(
});
if (!configResult.ready || !configResult.host || !configResult.port) {
prompter.warn('Gateway configuration failed in headless mode — aborting wizard.');
process.exit(1);
return;
const message = 'Gateway configuration failed in headless mode — aborting wizard.';
prompter.warn(message);
throw new Error(message);
}
const bootstrapResult = await gatewayBootstrapStage(prompter, state, {
@@ -430,9 +433,9 @@ async function runHeadlessPath(
port: configResult.port,
});
if (!bootstrapResult.completed) {
prompter.warn('Admin bootstrap failed — aborting wizard.');
process.exit(1);
return;
const message = 'Admin bootstrap failed — aborting wizard.';
prompter.warn(message);
throw new Error(message);
}
finalizeResult.showSummary();
} catch (err) {
@@ -477,18 +480,21 @@ async function runKeepPath(
skipInstall: options.skipGatewayNpmInstall,
});
if (configResult.ready && configResult.host && configResult.port) {
const bootstrapResult = await gatewayBootstrapStage(prompter, state, {
host: configResult.host,
port: configResult.port,
});
if (!bootstrapResult.completed) {
prompter.warn('Admin bootstrap failed — aborting wizard.');
process.exit(1);
return;
}
finalizeResult.showSummary();
if (!configResult.ready || !configResult.host || !configResult.port) {
const message = 'Gateway configuration failed — aborting wizard.';
prompter.warn(message);
throw new Error(message);
}
const bootstrapResult = await gatewayBootstrapStage(prompter, state, {
host: configResult.host,
port: configResult.port,
});
if (!bootstrapResult.completed) {
const message = 'Admin bootstrap failed — aborting wizard.';
prompter.warn(message);
throw new Error(message);
}
finalizeResult.showSummary();
} catch (err) {
prompter.warn(`Gateway setup failed: ${err instanceof Error ? err.message : String(err)}`);
throw err;
-31
View File
@@ -12,23 +12,6 @@ MOSAIC_HOME="$TMP/mosaic"
STATE="$TMP/state"
LOG="$TMP/npm.log"
mkdir -p "$FAKE_BIN" "$HOME_DIR" "$STATE"
REAL_NODE="$(command -v node)"
export MOSAIC_TEST_REAL_NODE="$REAL_NODE"
cat > "$FAKE_BIN/node" <<'FAKE_NODE'
#!/usr/bin/env bash
set -euo pipefail
if [[ "$*" == *'process.versions.node.split'* ]]; then
printf '%s' "${MOSAIC_TEST_NODE_MAJOR:-22}"
exit 0
fi
if [[ "${1:-}" == "--version" ]]; then
printf 'v%s.0.0\n' "${MOSAIC_TEST_NODE_MAJOR:-22}"
exit 0
fi
exec "${MOSAIC_TEST_REAL_NODE:?}" "$@"
FAKE_NODE
chmod +x "$FAKE_BIN/node"
cat > "$FAKE_BIN/npm" <<'FAKE_NPM'
#!/usr/bin/env bash
@@ -152,20 +135,6 @@ reset_state() {
rm -f "$STATE"/*
}
reset_state
echo "[test] --next rejects Node 20 before any install action"
if OUTPUT="$(
HOME="$HOME_DIR" MOSAIC_HOME="$MOSAIC_HOME" MOSAIC_PREFIX="$PREFIX" MOSAIC_NO_COLOR=1 \
MOSAIC_TEST_NPM_LOG="$LOG" MOSAIC_TEST_STATE="$STATE" MOSAIC_TEST_REAL_NODE="$REAL_NODE" \
MOSAIC_TEST_NODE_MAJOR=20 PATH="$FAKE_BIN:$PATH" \
bash "$ROOT/tools/install.sh" --cli --next --yes --no-auto-launch 2>&1
)"; then
echo "expected Node 20 next-lane install to fail" >&2
exit 1
fi
grep -qF 'Node.js >= 22 required for the --next lane' <<<"$OUTPUT"
[[ ! -s "$LOG" ]] || { echo "Node 20 gate ran npm actions" >&2; exit 1; }
reset_state
echo "[test] --next fast path pins resolved package versions"
OUTPUT="$(
+4 -5
View File
@@ -526,10 +526,6 @@ if [[ "$NODE_MAJOR" -lt 20 ]]; then
fail "Node.js >= 20 required (found v$(node --version))"
exit 1
fi
if [[ "$FLAG_NEXT" == "true" && "$NODE_MAJOR" -lt 22 ]]; then
fail "Node.js >= 22 required for the --next lane (found v$(node --version))"
exit 1
fi
echo ""
echo "${BOLD}Mosaic Stack Installer${RESET}"
@@ -766,9 +762,12 @@ if [[ "$FLAG_CHECK" == "false" ]]; then
if "$MOSAIC_CMD" wizard; then
ok "Wizard complete."
else
warn "Wizard exited non-zero."
fail "Wizard failed; installation is incomplete."
echo " Completed: framework and CLI installation"
echo " Failed: gateway configuration or admin bootstrap"
echo " You can retry with: ${C}mosaic wizard${RESET}"
echo " Or run gateway install alone: ${C}mosaic gateway install${RESET}"
exit 1
fi
fi
else