#!/usr/bin/env bash # ─── Mosaic Stack Installer / Upgrader ──────────────────────────────────────── # # Remote install (recommended): # bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh) # # Remote install (alternative — use -s -- to pass flags): # curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh | bash -s -- # curl -fsSL ... | bash -s -- --check # # Local (from repo checkout): # bash tools/install.sh # bash tools/install.sh --check # version check only # # Environment: # MOSAIC_REGISTRY — npm registry URL (default: Gitea instance) # MOSAIC_SCOPE — npm scope (default: @mosaic) # MOSAIC_PREFIX — npm global prefix (default: ~/.npm-global) # MOSAIC_NO_COLOR — disable colour (set to 1) # ────────────────────────────────────────────────────────────────────────────── # # The entire script is wrapped in main() so that bash reads it fully into # memory before execution — required for safe curl-pipe usage. set -euo pipefail main() { # ─── constants ──────────────────────────────────────────────────────────────── REGISTRY="${MOSAIC_REGISTRY:-https://git.mosaicstack.dev/api/packages/mosaic/npm/}" SCOPE="${MOSAIC_SCOPE:-@mosaic}" PREFIX="${MOSAIC_PREFIX:-$HOME/.npm-global}" CLI_PKG="${SCOPE}/cli" # ─── colours ────────────────────────────────────────────────────────────────── # Detect colour support: works in terminals and bash <(curl …), but correctly # disables when piped through a non-tty (curl … | bash). if [[ "${MOSAIC_NO_COLOR:-0}" == "1" ]] || ! [[ -t 1 ]]; then R="" G="" Y="" B="" C="" DIM="" 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' 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}"; } # ─── 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 } # Get the installed version of @mosaic/cli (empty string if not installed) installed_version() { local json json="$(npm ls -g --depth=0 --json --prefix="$PREFIX" 2>/dev/null)" || true if [[ -n "$json" ]]; then # Feed json via heredoc, not stdin pipe — safe in curl-pipe context 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 } # Get the latest published version from the registry latest_version() { npm view "${CLI_PKG}" version --registry="$REGISTRY" 2>/dev/null || true } # Compare two semver strings: returns 0 (true) if $1 < $2 version_lt() { # Semver-aware comparison via node (handles pre-release correctly) 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 } # ─── 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 # ─── ensure prefix directory exists ────────────────────────────────────────── 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 # Ensure prefix is set (only if no prefix= line exists yet) 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 # Ensure PREFIX/bin is on PATH 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 # ─── version check ─────────────────────────────────────────────────────────── info "Checking versions…" CURRENT="$(installed_version)" LATEST="$(latest_version)" if [[ -z "$LATEST" ]]; then fail "Could not reach registry at $REGISTRY" fail "Check network connectivity and registry URL." exit 1 fi echo "" if [[ -n "$CURRENT" ]]; then dim " Installed: ${CLI_PKG}@${CURRENT}" else dim " Installed: (none)" fi dim " Latest: ${CLI_PKG}@${LATEST}" echo "" # --check flag: just report, don't install if [[ "${1:-}" == "--check" ]]; then if [[ -z "$CURRENT" ]]; then warn "Not installed. Run without --check to install." exit 1 elif [[ "$CURRENT" == "$LATEST" ]]; then ok "Up to date." exit 0 elif version_lt "$CURRENT" "$LATEST"; then warn "Update available: $CURRENT → $LATEST" exit 2 else ok "Up to date (or ahead of registry)." exit 0 fi fi # ─── install / upgrade ────────────────────────────────────────────────────── if [[ -z "$CURRENT" ]]; then info "Installing ${CLI_PKG}@${LATEST}…" elif [[ "$CURRENT" == "$LATEST" ]]; then ok "Already at latest version ($LATEST). Nothing to do." exit 0 elif version_lt "$CURRENT" "$LATEST"; then info "Upgrading ${CLI_PKG}: $CURRENT → $LATEST…" else ok "Installed version ($CURRENT) is at or ahead of registry ($LATEST)." exit 0 fi npm install -g "${CLI_PKG}@${LATEST}" \ --registry="$REGISTRY" \ --prefix="$PREFIX" \ 2>&1 | sed 's/^/ /' echo "" ok "Mosaic CLI installed: $(installed_version)" dim " Binary: $PREFIX/bin/mosaic" # ─── post-install: run wizard if first install ─────────────────────────────── if [[ -z "$CURRENT" ]]; then echo "" info "First install detected." if [[ -t 0 ]] && [[ -t 1 ]]; then echo " Run ${C}mosaic wizard${RESET} to set up your configuration." else dim " Run 'mosaic wizard' to set up your configuration." fi fi echo "" ok "Done." } # end main main "$@"