- 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
219 lines
7.9 KiB
Bash
Executable File
219 lines
7.9 KiB
Bash
Executable File
#!/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 "$@"
|