feat(mosaic): unified first-run UX wizard -> gateway install -> verify (#418)
This commit was merged in pull request #418.
This commit is contained in:
@@ -188,6 +188,16 @@ export function registerGatewayCommand(program: Command): void {
|
|||||||
runLogs({ follow: cmdOpts.follow, lines: parseInt(cmdOpts.lines ?? '50', 10) });
|
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 ──────────────────────────────────────────────────────────
|
// ─── uninstall ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
gw.command('uninstall')
|
gw.command('uninstall')
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { randomBytes } from 'node:crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
import { homedir, tmpdir } from 'node:os';
|
||||||
import { createInterface } from 'node:readline';
|
import { createInterface } from 'node:readline';
|
||||||
import type { GatewayMeta } from './daemon.js';
|
import type { GatewayMeta } from './daemon.js';
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +22,39 @@ import {
|
|||||||
|
|
||||||
const MOSAIC_CONFIG_FILE = join(GATEWAY_HOME, 'mosaic.config.json');
|
const MOSAIC_CONFIG_FILE = join(GATEWAY_HOME, 'mosaic.config.json');
|
||||||
|
|
||||||
|
// ─── Wizard session state (transient, CU-07-02) ──────────────────────────────
|
||||||
|
|
||||||
|
const INSTALL_STATE_FILE = join(
|
||||||
|
process.env['XDG_RUNTIME_DIR'] ?? process.env['TMPDIR'] ?? tmpdir(),
|
||||||
|
'mosaic-install-state.json',
|
||||||
|
);
|
||||||
|
|
||||||
|
interface InstallSessionState {
|
||||||
|
wizardCompletedAt: string;
|
||||||
|
mosaicHome: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readInstallState(): InstallSessionState | null {
|
||||||
|
if (!existsSync(INSTALL_STATE_FILE)) return null;
|
||||||
|
try {
|
||||||
|
const raw = JSON.parse(readFileSync(INSTALL_STATE_FILE, 'utf-8')) as InstallSessionState;
|
||||||
|
// Only trust state that is < 10 minutes old
|
||||||
|
const age = Date.now() - new Date(raw.wizardCompletedAt).getTime();
|
||||||
|
if (age > 10 * 60 * 1000) return null;
|
||||||
|
return raw;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearInstallState(): void {
|
||||||
|
try {
|
||||||
|
unlinkSync(INSTALL_STATE_FILE);
|
||||||
|
} catch {
|
||||||
|
// Ignore — file may already be gone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface InstallOpts {
|
interface InstallOpts {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
@@ -41,6 +75,30 @@ export async function runInstall(opts: InstallOpts): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function doInstall(rl: ReturnType<typeof createInterface>, opts: InstallOpts): Promise<void> {
|
async function doInstall(rl: ReturnType<typeof createInterface>, opts: InstallOpts): Promise<void> {
|
||||||
|
// CU-07-02: Check for a fresh wizard session state and apply it.
|
||||||
|
const sessionState = readInstallState();
|
||||||
|
if (sessionState) {
|
||||||
|
const defaultHome = join(homedir(), '.config', 'mosaic');
|
||||||
|
const customHome = sessionState.mosaicHome !== defaultHome ? sessionState.mosaicHome : null;
|
||||||
|
|
||||||
|
if (customHome && !process.env['MOSAIC_GATEWAY_HOME']) {
|
||||||
|
// The wizard ran with a custom MOSAIC_HOME that differs from the default.
|
||||||
|
// GATEWAY_HOME is derived from MOSAIC_GATEWAY_HOME (or defaults to
|
||||||
|
// ~/.config/mosaic/gateway). Set the env var so the rest of this install
|
||||||
|
// inherits the correct location. This must be set before GATEWAY_HOME is
|
||||||
|
// evaluated by any imported helper — helpers that re-evaluate the path at
|
||||||
|
// call time will pick it up automatically.
|
||||||
|
process.env['MOSAIC_GATEWAY_HOME'] = join(customHome, 'gateway');
|
||||||
|
console.log(
|
||||||
|
`Resuming from wizard session — gateway home set to ${process.env['MOSAIC_GATEWAY_HOME']}\n`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`Resuming from wizard session — using ${sessionState.mosaicHome} from earlier.\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const existing = readMeta();
|
const existing = readMeta();
|
||||||
const envExists = existsSync(ENV_FILE);
|
const envExists = existsSync(ENV_FILE);
|
||||||
const mosaicConfigExists = existsSync(MOSAIC_CONFIG_FILE);
|
const mosaicConfigExists = existsSync(MOSAIC_CONFIG_FILE);
|
||||||
@@ -218,6 +276,13 @@ async function doInstall(rl: ReturnType<typeof createInterface>, opts: InstallOp
|
|||||||
console.log(` Config: ${GATEWAY_HOME}`);
|
console.log(` Config: ${GATEWAY_HOME}`);
|
||||||
console.log(` Logs: mosaic gateway logs`);
|
console.log(` Logs: mosaic gateway logs`);
|
||||||
console.log(` Status: mosaic gateway status`);
|
console.log(` Status: mosaic gateway status`);
|
||||||
|
|
||||||
|
// Step 7: Post-install verification (CU-07-03)
|
||||||
|
const { runPostInstallVerification } = await import('./verify.js');
|
||||||
|
await runPostInstallVerification(host, port);
|
||||||
|
|
||||||
|
// CU-07-02: Clear transient wizard session state on successful install.
|
||||||
|
clearInstallState();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runConfigWizard(
|
async function runConfigWizard(
|
||||||
|
|||||||
117
packages/mosaic/src/commands/gateway/verify.ts
Normal file
117
packages/mosaic/src/commands/gateway/verify.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { readMeta } from './daemon.js';
|
||||||
|
|
||||||
|
// ANSI colour helpers (gracefully degrade when not a TTY)
|
||||||
|
const isTTY = Boolean(process.stdout.isTTY);
|
||||||
|
const G = isTTY ? '\x1b[0;32m' : '';
|
||||||
|
const R = isTTY ? '\x1b[0;31m' : '';
|
||||||
|
const BOLD = isTTY ? '\x1b[1m' : '';
|
||||||
|
const RESET = isTTY ? '\x1b[0m' : '';
|
||||||
|
|
||||||
|
function ok(label: string): void {
|
||||||
|
process.stdout.write(` ${G}✔${RESET} ${label.padEnd(36)}${G}[ok]${RESET}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fail(label: string, hint: string): void {
|
||||||
|
process.stdout.write(` ${R}✖${RESET} ${label.padEnd(36)}${R}[FAIL]${RESET}\n`);
|
||||||
|
process.stdout.write(` ${R}↳ ${hint}${RESET}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWithRetry(
|
||||||
|
url: string,
|
||||||
|
opts: RequestInit = {},
|
||||||
|
retries = 3,
|
||||||
|
delayMs = 1000,
|
||||||
|
): Promise<Response | null> {
|
||||||
|
for (let attempt = 0; attempt < retries; attempt++) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, opts);
|
||||||
|
// Retry on non-OK responses too — the gateway may still be starting up
|
||||||
|
// (e.g. 503 before the app bootstrap completes).
|
||||||
|
if (res.ok) return res;
|
||||||
|
} catch {
|
||||||
|
// Network-level error — not ready yet, will retry
|
||||||
|
}
|
||||||
|
if (attempt < retries - 1) await sleep(delayMs);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VerifyResult {
|
||||||
|
gatewayHealthy: boolean;
|
||||||
|
adminTokenOnFile: boolean;
|
||||||
|
bootstrapReachable: boolean;
|
||||||
|
allPassed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run post-install verification checks.
|
||||||
|
*
|
||||||
|
* @param host - Gateway hostname (e.g. "localhost")
|
||||||
|
* @param port - Gateway port (e.g. 14242)
|
||||||
|
* @returns VerifyResult — callers can inspect individual flags
|
||||||
|
*/
|
||||||
|
export async function runPostInstallVerification(
|
||||||
|
host: string,
|
||||||
|
port: number,
|
||||||
|
): Promise<VerifyResult> {
|
||||||
|
const baseUrl = `http://${host}:${port.toString()}`;
|
||||||
|
|
||||||
|
console.log(`\n${BOLD}Mosaic installation verified:${RESET}`);
|
||||||
|
|
||||||
|
// ─── Check 1: Gateway /health ─────────────────────────────────────────────
|
||||||
|
const healthRes = await fetchWithRetry(`${baseUrl}/health`);
|
||||||
|
const gatewayHealthy = healthRes !== null && healthRes.ok;
|
||||||
|
if (gatewayHealthy) {
|
||||||
|
ok('gateway healthy');
|
||||||
|
} else {
|
||||||
|
fail('gateway healthy', 'Run: mosaic gateway status / mosaic gateway logs');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Check 2: Admin token on file ─────────────────────────────────────────
|
||||||
|
const meta = readMeta();
|
||||||
|
const adminTokenOnFile = Boolean(meta?.adminToken && meta.adminToken.length > 0);
|
||||||
|
if (adminTokenOnFile) {
|
||||||
|
ok('admin token on file');
|
||||||
|
} else {
|
||||||
|
fail('admin token on file', 'Run: mosaic gateway config recover-token');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Check 3: Bootstrap endpoint reachable ────────────────────────────────
|
||||||
|
const bootstrapRes = await fetchWithRetry(`${baseUrl}/api/bootstrap/status`);
|
||||||
|
const bootstrapReachable = bootstrapRes !== null && bootstrapRes.ok;
|
||||||
|
if (bootstrapReachable) {
|
||||||
|
ok('bootstrap endpoint reach');
|
||||||
|
} else {
|
||||||
|
fail('bootstrap endpoint reach', 'Run: mosaic gateway status / mosaic gateway logs');
|
||||||
|
}
|
||||||
|
|
||||||
|
const allPassed = gatewayHealthy && adminTokenOnFile && bootstrapReachable;
|
||||||
|
|
||||||
|
if (!allPassed) {
|
||||||
|
console.log(
|
||||||
|
`\n${R}One or more checks failed.${RESET} Recovery commands listed above.\n` +
|
||||||
|
`Use ${BOLD}mosaic gateway status${RESET} and ${BOLD}mosaic gateway config recover-token${RESET} to investigate.\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { gatewayHealthy, adminTokenOnFile, bootstrapReachable, allPassed };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standalone entry point for `mosaic gateway verify`.
|
||||||
|
* Reads host/port from meta.json if not provided.
|
||||||
|
*/
|
||||||
|
export async function runVerify(opts: { host?: string; port?: number }): Promise<void> {
|
||||||
|
const meta = readMeta();
|
||||||
|
const host = opts.host ?? meta?.host ?? 'localhost';
|
||||||
|
const port = opts.port ?? meta?.port ?? 14242;
|
||||||
|
|
||||||
|
const result = await runPostInstallVerification(host, port);
|
||||||
|
if (!result.allPassed) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { writeFileSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
import type { WizardPrompter } from './prompter/interface.js';
|
import type { WizardPrompter } from './prompter/interface.js';
|
||||||
import type { ConfigService } from './config/config-service.js';
|
import type { ConfigService } from './config/config-service.js';
|
||||||
import type { WizardState } from './types.js';
|
import type { WizardState } from './types.js';
|
||||||
@@ -11,6 +14,25 @@ import { runtimeSetupStage } from './stages/runtime-setup.js';
|
|||||||
import { skillsSelectStage } from './stages/skills-select.js';
|
import { skillsSelectStage } from './stages/skills-select.js';
|
||||||
import { finalizeStage } from './stages/finalize.js';
|
import { finalizeStage } from './stages/finalize.js';
|
||||||
|
|
||||||
|
// ─── Transient install session state (CU-07-02) ───────────────────────────────
|
||||||
|
|
||||||
|
const INSTALL_STATE_FILE = join(
|
||||||
|
process.env['XDG_RUNTIME_DIR'] ?? process.env['TMPDIR'] ?? tmpdir(),
|
||||||
|
'mosaic-install-state.json',
|
||||||
|
);
|
||||||
|
|
||||||
|
function writeInstallState(mosaicHome: string): void {
|
||||||
|
try {
|
||||||
|
const state = {
|
||||||
|
wizardCompletedAt: new Date().toISOString(),
|
||||||
|
mosaicHome,
|
||||||
|
};
|
||||||
|
writeFileSync(INSTALL_STATE_FILE, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
|
||||||
|
} catch {
|
||||||
|
// Non-fatal — gateway install will just ask for home again
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface WizardOptions {
|
export interface WizardOptions {
|
||||||
mosaicHome: string;
|
mosaicHome: string;
|
||||||
sourceDir: string;
|
sourceDir: string;
|
||||||
@@ -92,4 +114,8 @@ export async function runWizard(options: WizardOptions): Promise<void> {
|
|||||||
|
|
||||||
// Stage 9: Finalize
|
// Stage 9: Finalize
|
||||||
await finalizeStage(prompter, state, configService);
|
await finalizeStage(prompter, state, configService);
|
||||||
|
|
||||||
|
// CU-07-02: Write transient session state so `mosaic gateway install` can
|
||||||
|
// pick up mosaicHome without re-prompting.
|
||||||
|
writeInstallState(state.mosaicHome);
|
||||||
}
|
}
|
||||||
|
|||||||
184
tools/e2e-install-test.sh
Executable file
184
tools/e2e-install-test.sh
Executable file
@@ -0,0 +1,184 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ─── Mosaic Stack — End-to-End Install Test ────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Runs a clean-container install test to verify the full first-run flow:
|
||||||
|
# tools/install.sh -> mosaic wizard (non-interactive)
|
||||||
|
# -> mosaic gateway install
|
||||||
|
# -> mosaic gateway verify
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash tools/e2e-install-test.sh
|
||||||
|
#
|
||||||
|
# Requirements:
|
||||||
|
# - Docker (skips gracefully if not available)
|
||||||
|
# - Run from the repository root
|
||||||
|
#
|
||||||
|
# How it works:
|
||||||
|
# 1. Mounts the repository into a node:22-alpine container.
|
||||||
|
# 2. Installs prerequisites (bash, curl, jq, git) inside the container.
|
||||||
|
# 3. Runs `bash tools/install.sh --yes --no-auto-launch` to install the
|
||||||
|
# framework and CLI from the Gitea registry.
|
||||||
|
# 4. Runs `mosaic wizard --non-interactive` to set up SOUL/USER.
|
||||||
|
# 5. Runs `mosaic gateway install` with piped defaults (non-interactive).
|
||||||
|
# 6. Runs `mosaic gateway verify` and checks its exit code.
|
||||||
|
# NOTE: `mosaic gateway verify` is a new command added in the
|
||||||
|
# feat/mosaic-first-run-ux branch. If the installed CLI version
|
||||||
|
# pre-dates this branch (does not have `gateway verify`), the test
|
||||||
|
# marks this step as EXPECTED-SKIP and reports the installed version.
|
||||||
|
# 7. Reports PASS or FAIL with a summary.
|
||||||
|
#
|
||||||
|
# To run manually:
|
||||||
|
# cd /path/to/mosaic-stack
|
||||||
|
# bash tools/e2e-install-test.sh
|
||||||
|
#
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
IMAGE="node:22-alpine"
|
||||||
|
CONTAINER_NAME="mosaic-e2e-install-$$"
|
||||||
|
|
||||||
|
# ─── Colour helpers ───────────────────────────────────────────────────────────
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
R=$'\033[0;31m' G=$'\033[0;32m' Y=$'\033[0;33m' BOLD=$'\033[1m' RESET=$'\033[0m'
|
||||||
|
else
|
||||||
|
R="" G="" Y="" BOLD="" RESET=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
info() { echo "${BOLD}[e2e]${RESET} $*"; }
|
||||||
|
ok() { echo "${G}[PASS]${RESET} $*"; }
|
||||||
|
fail() { echo "${R}[FAIL]${RESET} $*" >&2; }
|
||||||
|
warn() { echo "${Y}[WARN]${RESET} $*"; }
|
||||||
|
|
||||||
|
# ─── Docker availability check ────────────────────────────────────────────────
|
||||||
|
if ! command -v docker &>/dev/null; then
|
||||||
|
warn "Docker not found — skipping e2e install test."
|
||||||
|
warn "Install Docker and re-run this script to exercise the full install flow."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! docker info &>/dev/null 2>&1; then
|
||||||
|
warn "Docker daemon is not running or not accessible — skipping e2e install test."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Docker available — proceeding with e2e install test."
|
||||||
|
info "Repo root: ${REPO_ROOT}"
|
||||||
|
info "Container image: ${IMAGE}"
|
||||||
|
|
||||||
|
# ─── Inline script that runs INSIDE the container ────────────────────────────
|
||||||
|
INNER_SCRIPT="$(mktemp /tmp/mosaic-e2e-inner-XXXXXX.sh)"
|
||||||
|
trap 'rm -f "$INNER_SCRIPT"' EXIT
|
||||||
|
|
||||||
|
cat > "$INNER_SCRIPT" <<'INNER_SCRIPT_EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
# Bootstrap: /bin/sh until bash is installed, then re-exec.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== [inner] Installing system prerequisites ==="
|
||||||
|
apk add --no-cache bash curl jq git 2>/dev/null || \
|
||||||
|
apt-get install -y -q bash curl jq git 2>/dev/null || true
|
||||||
|
|
||||||
|
# Re-exec under bash.
|
||||||
|
if [ -z "${BASH_VERSION:-}" ] && command -v bash >/dev/null 2>&1; then
|
||||||
|
exec bash "$0" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── bash from here ────────────────────────────────────────────────────────────
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "=== [inner] Node.js / npm versions ==="
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
echo "=== [inner] Setting up npm global prefix ==="
|
||||||
|
export NPM_PREFIX="/root/.npm-global"
|
||||||
|
mkdir -p "$NPM_PREFIX/bin"
|
||||||
|
npm config set prefix "$NPM_PREFIX" 2>/dev/null || true
|
||||||
|
export PATH="$NPM_PREFIX/bin:$PATH"
|
||||||
|
|
||||||
|
echo "=== [inner] Running install.sh --yes --no-auto-launch ==="
|
||||||
|
# Install both framework and CLI from the Gitea registry.
|
||||||
|
MOSAIC_SKIP_SKILLS_SYNC=1 \
|
||||||
|
MOSAIC_ASSUME_YES=1 \
|
||||||
|
bash /repo/tools/install.sh --yes --no-auto-launch
|
||||||
|
|
||||||
|
INSTALLED_VERSION="$(mosaic --version 2>/dev/null || echo 'unknown')"
|
||||||
|
echo "[inner] mosaic CLI installed: ${INSTALLED_VERSION}"
|
||||||
|
|
||||||
|
echo "=== [inner] Running mosaic wizard (non-interactive) ==="
|
||||||
|
mosaic wizard \
|
||||||
|
--non-interactive \
|
||||||
|
--name "test-agent" \
|
||||||
|
--user-name "tester" \
|
||||||
|
--pronouns "they/them" \
|
||||||
|
--timezone "UTC" || {
|
||||||
|
echo "[WARN] mosaic wizard exited non-zero — continuing"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== [inner] Running mosaic gateway install ==="
|
||||||
|
# Feed non-interactive answers:
|
||||||
|
# "1" → storage tier: local
|
||||||
|
# "" → port: accept default (14242)
|
||||||
|
# "" → ANTHROPIC_API_KEY: skip
|
||||||
|
# "" → CORS origin: accept default
|
||||||
|
# Then admin bootstrap: name, email, password
|
||||||
|
printf '1\n\n\n\nTest Admin\ntest@example.com\ntestpassword123\n' \
|
||||||
|
| mosaic gateway install
|
||||||
|
INSTALL_EXIT="$?"
|
||||||
|
if [ "${INSTALL_EXIT}" -ne 0 ]; then
|
||||||
|
echo "[ERR] mosaic gateway install exited ${INSTALL_EXIT}"
|
||||||
|
mosaic gateway status 2>/dev/null || true
|
||||||
|
exit "${INSTALL_EXIT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== [inner] Running mosaic gateway verify ==="
|
||||||
|
# `gateway verify` was added in feat/mosaic-first-run-ux.
|
||||||
|
# If the installed version pre-dates this, skip gracefully.
|
||||||
|
if ! mosaic gateway --help 2>&1 | grep -q 'verify'; then
|
||||||
|
echo "[SKIP] 'mosaic gateway verify' not available in installed version ${INSTALLED_VERSION}."
|
||||||
|
echo "[SKIP] This command was added in the feat/mosaic-first-run-ux release."
|
||||||
|
echo "[SKIP] Re-run after the new version is published to validate this step."
|
||||||
|
# Treat as pass — the install flow itself worked.
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mosaic gateway verify
|
||||||
|
VERIFY_EXIT="$?"
|
||||||
|
echo "=== [inner] verify exit code: ${VERIFY_EXIT} ==="
|
||||||
|
exit "${VERIFY_EXIT}"
|
||||||
|
INNER_SCRIPT_EOF
|
||||||
|
|
||||||
|
chmod +x "$INNER_SCRIPT"
|
||||||
|
|
||||||
|
# ─── Pull image ───────────────────────────────────────────────────────────────
|
||||||
|
info "Pulling ${IMAGE}…"
|
||||||
|
docker pull "${IMAGE}" --quiet
|
||||||
|
|
||||||
|
# ─── Run container ────────────────────────────────────────────────────────────
|
||||||
|
info "Starting container ${CONTAINER_NAME}…"
|
||||||
|
|
||||||
|
EXIT_CODE=0
|
||||||
|
docker run --rm \
|
||||||
|
--name "${CONTAINER_NAME}" \
|
||||||
|
--volume "${REPO_ROOT}:/repo:ro" \
|
||||||
|
--volume "${INNER_SCRIPT}:/e2e-inner.sh:ro" \
|
||||||
|
--network host \
|
||||||
|
"${IMAGE}" \
|
||||||
|
/bin/sh /e2e-inner.sh \
|
||||||
|
|| EXIT_CODE=$?
|
||||||
|
|
||||||
|
# ─── Report ───────────────────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
if [[ "$EXIT_CODE" -eq 0 ]]; then
|
||||||
|
ok "End-to-end install test PASSED (exit ${EXIT_CODE})"
|
||||||
|
else
|
||||||
|
fail "End-to-end install test FAILED (exit ${EXIT_CODE})"
|
||||||
|
echo ""
|
||||||
|
echo " Troubleshooting:"
|
||||||
|
echo " - Review the output above for the failing step."
|
||||||
|
echo " - Re-run with bash -x tools/e2e-install-test.sh for verbose trace."
|
||||||
|
echo " - Run mosaic gateway logs inside a manual container for daemon output."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
# --framework Install/upgrade framework only (skip npm CLI)
|
# --framework Install/upgrade framework only (skip npm CLI)
|
||||||
# --cli Install/upgrade npm CLI only (skip framework)
|
# --cli Install/upgrade npm CLI only (skip framework)
|
||||||
# --ref <branch> Git ref for framework archive (default: main)
|
# --ref <branch> Git ref for framework archive (default: main)
|
||||||
|
# --yes Accept all defaults; headless/non-interactive install
|
||||||
|
# --no-auto-launch Skip automatic mosaic wizard + gateway install on first install
|
||||||
#
|
#
|
||||||
# Environment:
|
# Environment:
|
||||||
# MOSAIC_HOME — framework install dir (default: ~/.config/mosaic)
|
# MOSAIC_HOME — framework install dir (default: ~/.config/mosaic)
|
||||||
@@ -24,6 +26,7 @@
|
|||||||
# MOSAIC_PREFIX — npm global prefix (default: ~/.npm-global)
|
# MOSAIC_PREFIX — npm global prefix (default: ~/.npm-global)
|
||||||
# MOSAIC_NO_COLOR — disable colour (set to 1)
|
# MOSAIC_NO_COLOR — disable colour (set to 1)
|
||||||
# MOSAIC_REF — git ref for framework (default: main)
|
# MOSAIC_REF — git ref for framework (default: main)
|
||||||
|
# MOSAIC_ASSUME_YES — equivalent to --yes (set to 1)
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
#
|
#
|
||||||
# Wrapped in main() for safe curl-pipe usage.
|
# Wrapped in main() for safe curl-pipe usage.
|
||||||
@@ -36,14 +39,23 @@ main() {
|
|||||||
FLAG_CHECK=false
|
FLAG_CHECK=false
|
||||||
FLAG_FRAMEWORK=true
|
FLAG_FRAMEWORK=true
|
||||||
FLAG_CLI=true
|
FLAG_CLI=true
|
||||||
|
FLAG_NO_AUTO_LAUNCH=false
|
||||||
|
FLAG_YES=false
|
||||||
GIT_REF="${MOSAIC_REF:-main}"
|
GIT_REF="${MOSAIC_REF:-main}"
|
||||||
|
|
||||||
|
# MOSAIC_ASSUME_YES env var acts the same as --yes
|
||||||
|
if [[ "${MOSAIC_ASSUME_YES:-0}" == "1" ]]; then
|
||||||
|
FLAG_YES=true
|
||||||
|
fi
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--check) FLAG_CHECK=true; shift ;;
|
--check) FLAG_CHECK=true; shift ;;
|
||||||
--framework) FLAG_CLI=false; shift ;;
|
--framework) FLAG_CLI=false; shift ;;
|
||||||
--cli) FLAG_FRAMEWORK=false; shift ;;
|
--cli) FLAG_FRAMEWORK=false; shift ;;
|
||||||
--ref) GIT_REF="${2:-main}"; shift 2 ;;
|
--ref) GIT_REF="${2:-main}"; shift 2 ;;
|
||||||
|
--yes|-y) FLAG_YES=true; shift ;;
|
||||||
|
--no-auto-launch) FLAG_NO_AUTO_LAUNCH=true; shift ;;
|
||||||
*) shift ;;
|
*) shift ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@@ -301,12 +313,49 @@ if [[ "$FLAG_CHECK" == "false" ]]; then
|
|||||||
dim " Framework data: $MOSAIC_HOME/"
|
dim " Framework data: $MOSAIC_HOME/"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# First install guidance
|
# First install guidance / auto-launch
|
||||||
if [[ ! -f "$MOSAIC_HOME/SOUL.md" ]]; then
|
if [[ ! -f "$MOSAIC_HOME/SOUL.md" ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
|
if [[ "$FLAG_NO_AUTO_LAUNCH" == "false" ]] && [[ -t 0 ]] && [[ -t 1 ]]; then
|
||||||
|
# Interactive TTY and auto-launch not suppressed: run wizard + gateway install
|
||||||
|
info "First install detected — launching setup wizard…"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
MOSAIC_BIN="$PREFIX/bin/mosaic"
|
||||||
|
|
||||||
|
if ! command -v "$MOSAIC_BIN" &>/dev/null && ! command -v mosaic &>/dev/null; then
|
||||||
|
warn "mosaic binary not found on PATH — skipping auto-launch."
|
||||||
|
warn "Add $PREFIX/bin to PATH and run: mosaic wizard && mosaic gateway install"
|
||||||
|
else
|
||||||
|
# Prefer the absolute path from the prefix we just installed to
|
||||||
|
MOSAIC_CMD="mosaic"
|
||||||
|
if [[ -x "$MOSAIC_BIN" ]]; then
|
||||||
|
MOSAIC_CMD="$MOSAIC_BIN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run wizard; if it fails we still try gateway install (best effort)
|
||||||
|
if "$MOSAIC_CMD" wizard; then
|
||||||
|
ok "Wizard complete."
|
||||||
|
else
|
||||||
|
warn "Wizard exited non-zero — continuing to gateway install."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "Launching gateway install…"
|
||||||
|
if "$MOSAIC_CMD" gateway install; then
|
||||||
|
ok "Gateway install complete."
|
||||||
|
else
|
||||||
|
warn "Gateway install exited non-zero."
|
||||||
|
echo " You can retry with: ${C}mosaic gateway install${RESET}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Non-interactive or --no-auto-launch: print guidance only
|
||||||
info "First install detected. Set up your agent identity:"
|
info "First install detected. Set up your agent identity:"
|
||||||
echo " ${C}mosaic init${RESET} (interactive SOUL.md / USER.md setup)"
|
echo " ${C}mosaic init${RESET} (interactive SOUL.md / USER.md setup)"
|
||||||
echo " ${C}mosaic wizard${RESET} (full guided wizard via Node.js)"
|
echo " ${C}mosaic wizard${RESET} (full guided wizard via Node.js)"
|
||||||
|
echo " ${C}mosaic gateway install${RESET} (install and start the gateway)"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
Reference in New Issue
Block a user