#!/usr/bin/env bash # ─── Mosaic Stack Installer / Upgrader ──────────────────────────────────────── # # Installs both components: # 1. Mosaic framework → ~/.config/mosaic/ (bash launcher, guides, runtime configs, tools) # 2. @mosaicstack/mosaic (npm) → ~/.npm-global/ (CLI, TUI, gateway client, wizard) # # Remote install (recommended): # bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/mosaic-stack/raw/branch/main/tools/install.sh) # # Remote install (alternative — use -s -- to pass flags): # 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 Git ref for framework archive (default: main) # # 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) # ────────────────────────────────────────────────────────────────────────────── # # Wrapped in main() for safe curl-pipe usage. set -euo pipefail main() { # ─── parse flags ────────────────────────────────────────────────────────────── FLAG_CHECK=false FLAG_FRAMEWORK=true FLAG_CLI=true GIT_REF="${MOSAIC_REF:-main}" 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 ;; esac done # ─── constants ──────────────────────────────────────────────────────────────── MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" REGISTRY="${MOSAIC_REGISTRY:-https://git.mosaicstack.dev/api/packages/mosaicstack/npm/}" SCOPE="${MOSAIC_SCOPE:-@mosaicstack}" PREFIX="${MOSAIC_PREFIX:-$HOME/.npm-global}" CLI_PKG="${SCOPE}/mosaic" REPO_BASE="https://git.mosaicstack.dev/mosaicstack/mosaic-stack" ARCHIVE_URL="${REPO_BASE}/archive/${GIT_REF}.tar.gz" # ─── colours ────────────────────────────────────────────────────────────────── if [[ "${MOSAIC_NO_COLOR:-0}" == "1" ]] || ! [[ -t 1 ]]; then R="" G="" Y="" B="" C="" DIM="" BOLD="" RESET="" else R=$'\033[0;31m' G=$'\033[0;32m' Y=$'\033[0;33m' B=$'\033[0;34m' C=$'\033[0;36m' DIM=$'\033[2m' BOLD=$'\033[1m' RESET=$'\033[0m' fi info() { echo "${B}ℹ${RESET} $*"; } ok() { echo "${G}✔${RESET} $*"; } warn() { echo "${Y}⚠${RESET} $*"; } fail() { echo "${R}✖${RESET} $*" >&2; } dim() { echo "${DIM}$*${RESET}"; } step() { echo ""; echo "${BOLD}$*${RESET}"; } # ─── helpers ────────────────────────────────────────────────────────────────── require_cmd() { if ! command -v "$1" &>/dev/null; then fail "Required command not found: $1" echo " Install it and re-run this script." exit 1 fi } installed_cli_version() { local json json="$(npm ls -g --depth=0 --json --prefix="$PREFIX" 2>/dev/null)" || true if [[ -n "$json" ]]; then node -e " const d = JSON.parse(process.argv[1]); const v = d?.dependencies?.['${CLI_PKG}']?.version ?? ''; process.stdout.write(v); " "$json" 2>/dev/null || true fi } latest_cli_version() { npm view "${CLI_PKG}" version --registry="$REGISTRY" 2>/dev/null || true } version_lt() { node -e " const a=process.argv[1], b=process.argv[2]; const sp = v => { const i=v.indexOf('-'); return i===-1 ? [v,null] : [v.slice(0,i),v.slice(i+1)]; }; const [cA,pA]=sp(a.replace(/^v/,'')), [cB,pB]=sp(b.replace(/^v/,'')); const nA=cA.split('.').map(Number), nB=cB.split('.').map(Number); for(let i=0;i<3;i++){if((nA[i]||0)<(nB[i]||0))process.exit(0);if((nA[i]||0)>(nB[i]||0))process.exit(1);} if(pA!==null&&pB===null)process.exit(0); if(pA===null)process.exit(1); if(pA/dev/null } framework_version() { # Read framework schema version stamp local vf="$MOSAIC_HOME/.framework-version" if [[ -f "$vf" ]]; then cat "$vf" 2>/dev/null || true fi } # ─── preflight ──────────────────────────────────────────────────────────────── require_cmd node require_cmd npm NODE_MAJOR="$(node -e 'process.stdout.write(String(process.versions.node.split(".")[0]))')" if [[ "$NODE_MAJOR" -lt 20 ]]; then fail "Node.js >= 20 required (found v$(node --version))" exit 1 fi echo "" echo "${BOLD}Mosaic Stack Installer${RESET}" echo "" # ═══════════════════════════════════════════════════════════════════════════════ # PART 1: Framework (bash launcher + guides + runtime configs + tools) # ═══════════════════════════════════════════════════════════════════════════════ if [[ "$FLAG_FRAMEWORK" == "true" ]]; then step "Framework (~/.config/mosaic)" FRAMEWORK_CURRENT="$(framework_version)" HAS_FRAMEWORK=false [[ -f "$MOSAIC_HOME/AGENTS.md" ]] || [[ -f "$MOSAIC_HOME/.framework-version" ]] && HAS_FRAMEWORK=true if [[ -n "$FRAMEWORK_CURRENT" ]]; then dim " Installed: framework v${FRAMEWORK_CURRENT}" elif [[ "$HAS_FRAMEWORK" == "true" ]]; then dim " Installed: framework (version unknown)" else dim " Installed: (none)" fi dim " Source: ${REPO_BASE} (ref: ${GIT_REF})" echo "" if [[ "$FLAG_CHECK" == "true" ]]; then if [[ "$HAS_FRAMEWORK" == "true" ]]; then ok "Framework is installed." else warn "Framework not installed." fi else # Download repo archive and extract framework require_cmd tar WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-install-XXXXXX")" cleanup_work() { rm -rf "$WORK_DIR"; } trap cleanup_work EXIT info "Downloading framework from ${GIT_REF}…" if command -v curl &>/dev/null; then curl -fsSL "$ARCHIVE_URL" | tar xz -C "$WORK_DIR" elif command -v wget &>/dev/null; then wget -qO- "$ARCHIVE_URL" | tar xz -C "$WORK_DIR" else fail "curl or wget required to download framework." exit 1 fi # Gitea archives extract to / inside the work dir EXTRACTED_DIR="$(find "$WORK_DIR" -maxdepth 1 -mindepth 1 -type d | head -1)" FRAMEWORK_SRC="$EXTRACTED_DIR/packages/mosaic/framework" if [[ ! -d "$FRAMEWORK_SRC" ]]; then fail "Framework not found in archive at packages/mosaic/framework/" fail "Archive contents:" ls -la "$WORK_DIR" >&2 exit 1 fi # Run the framework's own install.sh (handles keep/overwrite for SOUL.md etc.) info "Installing framework to ${MOSAIC_HOME}…" MOSAIC_INSTALL_MODE="${MOSAIC_INSTALL_MODE:-keep}" \ MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING=1 \ MOSAIC_SKIP_SKILLS_SYNC="${MOSAIC_SKIP_SKILLS_SYNC:-0}" \ bash "$FRAMEWORK_SRC/install.sh" ok "Framework installed" echo "" # Framework bin is no longer needed on PATH — the npm CLI delegates # to mosaic-launch directly via its absolute path. fi fi # ═══════════════════════════════════════════════════════════════════════════════ # PART 2: @mosaicstack/mosaic (npm — TUI, gateway client, wizard, CLI) # ═══════════════════════════════════════════════════════════════════════════════ if [[ "$FLAG_CLI" == "true" ]]; then step "@mosaicstack/mosaic (npm package)" # Ensure prefix dir if [[ ! -d "$PREFIX" ]]; then info "Creating global prefix directory: $PREFIX" mkdir -p "$PREFIX"/{bin,lib} fi # Ensure npmrc scope mapping NPMRC="$HOME/.npmrc" SCOPE_LINE="${SCOPE}:registry=${REGISTRY}" if ! grep -qF "$SCOPE_LINE" "$NPMRC" 2>/dev/null; then info "Adding ${SCOPE} registry to $NPMRC" echo "$SCOPE_LINE" >> "$NPMRC" ok "Registry configured" fi if ! grep -qF "prefix=$PREFIX" "$NPMRC" 2>/dev/null; then if ! grep -q '^prefix=' "$NPMRC" 2>/dev/null; then echo "prefix=$PREFIX" >> "$NPMRC" info "Set npm global prefix to $PREFIX" fi fi CURRENT="$(installed_cli_version)" LATEST="$(latest_cli_version)" if [[ -n "$CURRENT" ]]; then dim " Installed: ${CLI_PKG}@${CURRENT}" else dim " Installed: (none)" fi if [[ -n "$LATEST" ]]; then dim " Latest: ${CLI_PKG}@${LATEST}" else dim " Latest: (registry unreachable)" fi echo "" if [[ "$FLAG_CHECK" == "true" ]]; then if [[ -z "$LATEST" ]]; then warn "Could not reach registry." elif [[ -z "$CURRENT" ]]; then warn "Not installed." elif [[ "$CURRENT" == "$LATEST" ]]; then ok "Up to date." elif version_lt "$CURRENT" "$LATEST"; then warn "Update available: $CURRENT → $LATEST" else ok "Up to date (or ahead of registry)." fi else if [[ -z "$LATEST" ]]; then warn "Could not reach registry at $REGISTRY — skipping npm CLI." elif [[ -z "$CURRENT" ]]; then info "Installing ${CLI_PKG}@${LATEST}…" npm install -g "${CLI_PKG}@${LATEST}" --prefix="$PREFIX" 2>&1 | sed 's/^/ /' ok "CLI installed: $(installed_cli_version)" elif [[ "$CURRENT" == "$LATEST" ]]; then ok "Already at latest version ($LATEST)." elif version_lt "$CURRENT" "$LATEST"; then info "Upgrading ${CLI_PKG}: $CURRENT → $LATEST…" npm install -g "${CLI_PKG}@${LATEST}" --prefix="$PREFIX" 2>&1 | sed 's/^/ /' ok "CLI upgraded: $(installed_cli_version)" else ok "CLI is at or ahead of registry ($CURRENT ≥ $LATEST)." fi # PATH check for npm prefix if [[ ":$PATH:" != *":$PREFIX/bin:"* ]]; then warn "$PREFIX/bin is not on your PATH" dim " Add to your shell rc: export PATH=\"$PREFIX/bin:\$PATH\"" fi fi fi # ═══════════════════════════════════════════════════════════════════════════════ # Summary # ═══════════════════════════════════════════════════════════════════════════════ if [[ "$FLAG_CHECK" == "false" ]]; then step "Summary" echo " ${BOLD}mosaic:${RESET} $PREFIX/bin/mosaic" dim " Framework data: $MOSAIC_HOME/" echo "" # First install guidance 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)" fi echo "" ok "Done." fi } # end main main "$@"