Files
stack/tools/install.sh
Jarvis 45f5b9062e feat: install.sh + auto-update checker for CLI
- tools/install.sh: standalone installer/upgrader, curl-pipe safe
  (main() wrapper, process.argv instead of stdin, mkdir -p prefix)
- packages/mosaic/src/runtime/update-checker.ts: version check module
  with 1h cache at ~/.cache/mosaic/update-check.json
- CLI startup: non-blocking background update check on every invocation
- 'mosaic update' command: explicit check + install (--check for CI)
- session-start.sh: warns agents when CLI is outdated
- Proper semver comparison including pre-release precedence
- eslint: allow __tests__ in packages/mosaic for projectService
2026-04-02 00:11:42 -05:00

219 lines
7.9 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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<pB)process.exit(0);
process.exit(1);
" "$1" "$2" 2>/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 "$@"