feat(mosaic): unified first-run UX wizard -> gateway install -> verify #418
@@ -188,6 +188,16 @@ export function registerGatewayCommand(program: Command): void {
|
||||
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')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 { homedir, tmpdir } from 'node:os';
|
||||
import { createInterface } from 'node:readline';
|
||||
import type { GatewayMeta } from './daemon.js';
|
||||
import {
|
||||
@@ -21,6 +22,39 @@ import {
|
||||
|
||||
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 {
|
||||
host: string;
|
||||
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> {
|
||||
// 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 envExists = existsSync(ENV_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(` Logs: mosaic gateway logs`);
|
||||
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(
|
||||
|
||||
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 { ConfigService } from './config/config-service.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 { 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 {
|
||||
mosaicHome: string;
|
||||
sourceDir: string;
|
||||
@@ -92,4 +114,8 @@ export async function runWizard(options: WizardOptions): Promise<void> {
|
||||
|
||||
// Stage 9: Finalize
|
||||
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
|
||||
@@ -12,18 +12,21 @@
|
||||
# curl -fsSL https://git.mosaicstack.dev/mosaicstack/mosaic-stack/raw/branch/main/tools/install.sh | bash -s --
|
||||
#
|
||||
# Flags:
|
||||
# --check Version check only, no install
|
||||
# --framework Install/upgrade framework only (skip npm CLI)
|
||||
# --cli Install/upgrade npm CLI only (skip framework)
|
||||
# --ref <branch> Git ref for framework archive (default: main)
|
||||
# --check Version check only, no install
|
||||
# --framework Install/upgrade framework only (skip npm CLI)
|
||||
# --cli Install/upgrade npm CLI only (skip framework)
|
||||
# --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:
|
||||
# MOSAIC_HOME — framework install dir (default: ~/.config/mosaic)
|
||||
# MOSAIC_REGISTRY — npm registry URL (default: Gitea instance)
|
||||
# MOSAIC_SCOPE — npm scope (default: @mosaicstack)
|
||||
# MOSAIC_PREFIX — npm global prefix (default: ~/.npm-global)
|
||||
# MOSAIC_NO_COLOR — disable colour (set to 1)
|
||||
# MOSAIC_REF — git ref for framework (default: main)
|
||||
# MOSAIC_HOME — framework install dir (default: ~/.config/mosaic)
|
||||
# MOSAIC_REGISTRY — npm registry URL (default: Gitea instance)
|
||||
# MOSAIC_SCOPE — npm scope (default: @mosaicstack)
|
||||
# MOSAIC_PREFIX — npm global prefix (default: ~/.npm-global)
|
||||
# MOSAIC_NO_COLOR — disable colour (set to 1)
|
||||
# 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.
|
||||
@@ -36,15 +39,24 @@ main() {
|
||||
FLAG_CHECK=false
|
||||
FLAG_FRAMEWORK=true
|
||||
FLAG_CLI=true
|
||||
FLAG_NO_AUTO_LAUNCH=false
|
||||
FLAG_YES=false
|
||||
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
|
||||
case "$1" in
|
||||
--check) FLAG_CHECK=true; shift ;;
|
||||
--framework) FLAG_CLI=false; shift ;;
|
||||
--cli) FLAG_FRAMEWORK=false; shift ;;
|
||||
--ref) GIT_REF="${2:-main}"; shift 2 ;;
|
||||
*) shift ;;
|
||||
--check) FLAG_CHECK=true; shift ;;
|
||||
--framework) FLAG_CLI=false; shift ;;
|
||||
--cli) FLAG_FRAMEWORK=false; shift ;;
|
||||
--ref) GIT_REF="${2:-main}"; shift 2 ;;
|
||||
--yes|-y) FLAG_YES=true; shift ;;
|
||||
--no-auto-launch) FLAG_NO_AUTO_LAUNCH=true; shift ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
@@ -301,12 +313,49 @@ if [[ "$FLAG_CHECK" == "false" ]]; then
|
||||
dim " Framework data: $MOSAIC_HOME/"
|
||||
echo ""
|
||||
|
||||
# First install guidance
|
||||
# First install guidance / auto-launch
|
||||
if [[ ! -f "$MOSAIC_HOME/SOUL.md" ]]; then
|
||||
echo ""
|
||||
info "First install detected. Set up your agent identity:"
|
||||
echo " ${C}mosaic init${RESET} (interactive SOUL.md / USER.md setup)"
|
||||
echo " ${C}mosaic wizard${RESET} (full guided wizard via Node.js)"
|
||||
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:"
|
||||
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 gateway install${RESET} (install and start the gateway)"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
Reference in New Issue
Block a user