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:
218
tools/install.sh
Executable file
218
tools/install.sh
Executable 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 "$@"
|
||||
Reference in New Issue
Block a user