feat: unify install.sh — single installer for framework + npm CLI
- tools/install.sh now installs both components: 1. Framework (bash launcher, guides, runtime configs) → ~/.config/mosaic/ 2. @mosaic/cli (TUI, gateway client, wizard) → ~/.npm-global/ - Downloads framework from monorepo archive (no bootstrap repo dependency) - Supports --framework, --cli, --check, --ref flags - Delete remote-install.sh and remote-install.ps1 (redundant redirectors) - Update all stale mosaic/bootstrap references → mosaic/mosaic-stack - Update README.md with monorepo install instructions Deprecates: mosaic/bootstrap repo
This commit is contained in:
365
tools/install.sh
365
tools/install.sh
@@ -1,45 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
# ─── Mosaic Stack Installer / Upgrader ────────────────────────────────────────
|
||||
#
|
||||
# Installs both components:
|
||||
# 1. Mosaic framework → ~/.config/mosaic/ (bash launcher, guides, runtime configs, tools)
|
||||
# 2. @mosaic/cli (npm) → ~/.npm-global/ (TUI, gateway client, wizard)
|
||||
#
|
||||
# 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
|
||||
# Flags:
|
||||
# --check Version check only, no install
|
||||
# --framework Install/upgrade framework only (skip npm CLI)
|
||||
# --cli Install/upgrade npm CLI only (skip framework)
|
||||
# --ref <branch> Git ref for framework archive (default: main)
|
||||
#
|
||||
# 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)
|
||||
# MOSAIC_HOME — framework install dir (default: ~/.config/mosaic)
|
||||
# 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)
|
||||
# MOSAIC_REF — git ref for framework (default: main)
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# The entire script is wrapped in main() so that bash reads it fully into
|
||||
# memory before execution — required for safe curl-pipe usage.
|
||||
# Wrapped in main() for safe curl-pipe usage.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
|
||||
# ─── parse flags ──────────────────────────────────────────────────────────────
|
||||
FLAG_CHECK=false
|
||||
FLAG_FRAMEWORK=true
|
||||
FLAG_CLI=true
|
||||
GIT_REF="${MOSAIC_REF:-main}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--check) FLAG_CHECK=true; shift ;;
|
||||
--framework) FLAG_CLI=false; shift ;;
|
||||
--cli) FLAG_FRAMEWORK=false; shift ;;
|
||||
--ref) GIT_REF="${2:-main}"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ─── constants ────────────────────────────────────────────────────────────────
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
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"
|
||||
REPO_BASE="https://git.mosaicstack.dev/mosaic/mosaic-stack"
|
||||
ARCHIVE_URL="${REPO_BASE}/archive/${GIT_REF}.tar.gz"
|
||||
|
||||
# ─── 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=""
|
||||
R="" G="" Y="" B="" C="" DIM="" BOLD="" 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'
|
||||
B=$'\033[0;34m' C=$'\033[0;36m' DIM=$'\033[2m'
|
||||
BOLD=$'\033[1m' RESET=$'\033[0m'
|
||||
fi
|
||||
|
||||
info() { echo "${B}ℹ${RESET} $*"; }
|
||||
@@ -47,6 +71,7 @@ ok() { echo "${G}✔${RESET} $*"; }
|
||||
warn() { echo "${Y}⚠${RESET} $*"; }
|
||||
fail() { echo "${R}✖${RESET} $*" >&2; }
|
||||
dim() { echo "${DIM}$*${RESET}"; }
|
||||
step() { echo ""; echo "${BOLD}$*${RESET}"; }
|
||||
|
||||
# ─── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -58,12 +83,10 @@ require_cmd() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Get the installed version of @mosaic/cli (empty string if not installed)
|
||||
installed_version() {
|
||||
installed_cli_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 ?? '';
|
||||
@@ -72,14 +95,11 @@ installed_version() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Get the latest published version from the registry
|
||||
latest_version() {
|
||||
latest_cli_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)]; };
|
||||
@@ -93,6 +113,14 @@ version_lt() {
|
||||
" "$1" "$2" 2>/dev/null
|
||||
}
|
||||
|
||||
framework_version() {
|
||||
# Read VERSION from the installed mosaic launcher
|
||||
local mosaic_bin="$MOSAIC_HOME/bin/mosaic"
|
||||
if [[ -f "$mosaic_bin" ]]; then
|
||||
grep -m1 '^VERSION=' "$mosaic_bin" 2>/dev/null | cut -d'"' -f2 || true
|
||||
fi
|
||||
}
|
||||
|
||||
# ─── preflight ────────────────────────────────────────────────────────────────
|
||||
|
||||
require_cmd node
|
||||
@@ -104,116 +132,211 @@ if [[ "$NODE_MAJOR" -lt 20 ]]; then
|
||||
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 "${BOLD}Mosaic Stack Installer${RESET}"
|
||||
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
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# PART 1: Framework (bash launcher + guides + runtime configs + tools)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if [[ "$FLAG_FRAMEWORK" == "true" ]]; then
|
||||
step "Framework (~/.config/mosaic)"
|
||||
|
||||
FRAMEWORK_CURRENT="$(framework_version)"
|
||||
HAS_FRAMEWORK=false
|
||||
[[ -d "$MOSAIC_HOME/bin" ]] && [[ -f "$MOSAIC_HOME/bin/mosaic" ]] && HAS_FRAMEWORK=true
|
||||
|
||||
if [[ -n "$FRAMEWORK_CURRENT" ]]; then
|
||||
dim " Installed: framework v${FRAMEWORK_CURRENT}"
|
||||
elif [[ "$HAS_FRAMEWORK" == "true" ]]; then
|
||||
dim " Installed: framework (version unknown)"
|
||||
else
|
||||
ok "Up to date (or ahead of registry)."
|
||||
exit 0
|
||||
dim " Installed: (none)"
|
||||
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
|
||||
|
||||
# NOTE: Do NOT pass --registry here. The @mosaic scope is already mapped
|
||||
# in ~/.npmrc. Passing --registry globally would redirect ALL deps (including
|
||||
# @clack/prompts, commander, etc.) to the Gitea registry, causing 404s.
|
||||
npm install -g "${CLI_PKG}@${LATEST}" \
|
||||
--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
|
||||
dim " Source: ${REPO_BASE} (ref: ${GIT_REF})"
|
||||
echo ""
|
||||
info "First install detected."
|
||||
if [[ -t 0 ]] && [[ -t 1 ]]; then
|
||||
echo " Run ${C}mosaic wizard${RESET} to set up your configuration."
|
||||
|
||||
if [[ "$FLAG_CHECK" == "true" ]]; then
|
||||
if [[ "$HAS_FRAMEWORK" == "true" ]]; then
|
||||
ok "Framework is installed."
|
||||
else
|
||||
warn "Framework not installed."
|
||||
fi
|
||||
else
|
||||
dim " Run 'mosaic wizard' to set up your configuration."
|
||||
# Download repo archive and extract framework
|
||||
require_cmd tar
|
||||
|
||||
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-install-XXXXXX")"
|
||||
cleanup_work() { rm -rf "$WORK_DIR"; }
|
||||
trap cleanup_work EXIT
|
||||
|
||||
info "Downloading framework from ${GIT_REF}…"
|
||||
if command -v curl &>/dev/null; then
|
||||
curl -fsSL "$ARCHIVE_URL" | tar xz -C "$WORK_DIR"
|
||||
elif command -v wget &>/dev/null; then
|
||||
wget -qO- "$ARCHIVE_URL" | tar xz -C "$WORK_DIR"
|
||||
else
|
||||
fail "curl or wget required to download framework."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Gitea archives extract to <repo-name>/ inside the work dir
|
||||
EXTRACTED_DIR="$(find "$WORK_DIR" -maxdepth 1 -mindepth 1 -type d | head -1)"
|
||||
FRAMEWORK_SRC="$EXTRACTED_DIR/packages/mosaic/framework"
|
||||
|
||||
if [[ ! -d "$FRAMEWORK_SRC" ]]; then
|
||||
fail "Framework not found in archive at packages/mosaic/framework/"
|
||||
fail "Archive contents:"
|
||||
ls -la "$WORK_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run the framework's own install.sh (handles keep/overwrite for SOUL.md etc.)
|
||||
info "Installing framework to ${MOSAIC_HOME}…"
|
||||
MOSAIC_INSTALL_MODE="${MOSAIC_INSTALL_MODE:-keep}" \
|
||||
MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING=1 \
|
||||
MOSAIC_SKIP_SKILLS_SYNC="${MOSAIC_SKIP_SKILLS_SYNC:-0}" \
|
||||
bash "$FRAMEWORK_SRC/install.sh"
|
||||
|
||||
ok "Framework installed"
|
||||
echo ""
|
||||
|
||||
# Ensure framework bin is on PATH
|
||||
FRAMEWORK_BIN="$MOSAIC_HOME/bin"
|
||||
if [[ ":$PATH:" != *":$FRAMEWORK_BIN:"* ]]; then
|
||||
warn "$FRAMEWORK_BIN is not on your PATH"
|
||||
dim " The 'mosaic' launcher lives here. Add to your shell rc:"
|
||||
dim " export PATH=\"$FRAMEWORK_BIN:\$PATH\""
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
ok "Done."
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# PART 2: @mosaic/cli (npm — TUI, gateway client, wizard)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if [[ "$FLAG_CLI" == "true" ]]; then
|
||||
step "@mosaic/cli (npm package)"
|
||||
|
||||
# Ensure prefix dir
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
CURRENT="$(installed_cli_version)"
|
||||
LATEST="$(latest_cli_version)"
|
||||
|
||||
if [[ -n "$CURRENT" ]]; then
|
||||
dim " Installed: ${CLI_PKG}@${CURRENT}"
|
||||
else
|
||||
dim " Installed: (none)"
|
||||
fi
|
||||
|
||||
if [[ -n "$LATEST" ]]; then
|
||||
dim " Latest: ${CLI_PKG}@${LATEST}"
|
||||
else
|
||||
dim " Latest: (registry unreachable)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
if [[ "$FLAG_CHECK" == "true" ]]; then
|
||||
if [[ -z "$LATEST" ]]; then
|
||||
warn "Could not reach registry."
|
||||
elif [[ -z "$CURRENT" ]]; then
|
||||
warn "Not installed."
|
||||
elif [[ "$CURRENT" == "$LATEST" ]]; then
|
||||
ok "Up to date."
|
||||
elif version_lt "$CURRENT" "$LATEST"; then
|
||||
warn "Update available: $CURRENT → $LATEST"
|
||||
else
|
||||
ok "Up to date (or ahead of registry)."
|
||||
fi
|
||||
else
|
||||
if [[ -z "$LATEST" ]]; then
|
||||
warn "Could not reach registry at $REGISTRY — skipping npm CLI."
|
||||
elif [[ -z "$CURRENT" ]]; then
|
||||
info "Installing ${CLI_PKG}@${LATEST}…"
|
||||
npm install -g "${CLI_PKG}@${LATEST}" --prefix="$PREFIX" 2>&1 | sed 's/^/ /'
|
||||
ok "CLI installed: $(installed_cli_version)"
|
||||
elif [[ "$CURRENT" == "$LATEST" ]]; then
|
||||
ok "Already at latest version ($LATEST)."
|
||||
elif version_lt "$CURRENT" "$LATEST"; then
|
||||
info "Upgrading ${CLI_PKG}: $CURRENT → $LATEST…"
|
||||
npm install -g "${CLI_PKG}@${LATEST}" --prefix="$PREFIX" 2>&1 | sed 's/^/ /'
|
||||
ok "CLI upgraded: $(installed_cli_version)"
|
||||
else
|
||||
ok "CLI is at or ahead of registry ($CURRENT ≥ $LATEST)."
|
||||
fi
|
||||
|
||||
# PATH check for npm prefix
|
||||
if [[ ":$PATH:" != *":$PREFIX/bin:"* ]]; then
|
||||
warn "$PREFIX/bin is not on your PATH"
|
||||
dim " The 'mosaic' TUI/gateway CLI lives here (separate from the launcher)."
|
||||
dim " Add to your shell rc: export PATH=\"$PREFIX/bin:\$PATH\""
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Summary
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if [[ "$FLAG_CHECK" == "false" ]]; then
|
||||
step "Summary"
|
||||
|
||||
echo " ${BOLD}Framework launcher:${RESET} $MOSAIC_HOME/bin/mosaic"
|
||||
echo " ${DIM}mosaic claude, mosaic yolo claude, mosaic pi, mosaic doctor, …${RESET}"
|
||||
echo ""
|
||||
echo " ${BOLD}npm CLI (TUI):${RESET} $PREFIX/bin/mosaic"
|
||||
echo " ${DIM}mosaic tui, mosaic login, mosaic wizard, mosaic update, …${RESET}"
|
||||
echo ""
|
||||
|
||||
# Warn if there's a naming collision (both on PATH)
|
||||
FRAMEWORK_BIN="$MOSAIC_HOME/bin"
|
||||
if [[ ":$PATH:" == *":$FRAMEWORK_BIN:"* ]] && [[ ":$PATH:" == *":$PREFIX/bin:"* ]]; then
|
||||
# Check which one wins
|
||||
WHICH_MOSAIC="$(command -v mosaic 2>/dev/null || true)"
|
||||
if [[ -n "$WHICH_MOSAIC" ]]; then
|
||||
dim " Active 'mosaic' binary: $WHICH_MOSAIC"
|
||||
if [[ "$WHICH_MOSAIC" == "$FRAMEWORK_BIN/mosaic" ]]; then
|
||||
dim " (Framework launcher takes priority — this is correct)"
|
||||
else
|
||||
warn "npm CLI shadows the framework launcher!"
|
||||
dim " Ensure $FRAMEWORK_BIN appears BEFORE $PREFIX/bin in your PATH."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# First install guidance
|
||||
if [[ ! -f "$MOSAIC_HOME/SOUL.md" ]]; then
|
||||
echo ""
|
||||
info "First install detected. Set up your agent identity:"
|
||||
echo " ${C}mosaic init${RESET} (interactive SOUL.md / USER.md setup)"
|
||||
echo " ${C}mosaic wizard${RESET} (full guided wizard via Node.js)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
ok "Done."
|
||||
fi
|
||||
|
||||
} # end main
|
||||
|
||||
|
||||
Reference in New Issue
Block a user