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
This commit is contained in:
Jarvis
2026-04-01 21:03:23 -05:00
parent 147f5f1bec
commit 45f5b9062e
7 changed files with 584 additions and 0 deletions

218
tools/install.sh Executable file
View File

@@ -0,0 +1,218 @@
#!/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 "$@"