Files
stack/tools/install.sh
Jarvis 0a76afb7f2
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
feat: unify mosaic CLI — single binary, no PATH conflict
- Rename framework bash launcher: mosaic → mosaic-launch
- npm CLI (@mosaic/cli) is now the single 'mosaic' binary on PATH
- Add launcher delegation: mosaic {claude,codex,opencode,pi,yolo}
  delegates to mosaic-launch via execFileSync with inherited stdio
- Add delegated management commands: init, doctor, sync, seq,
  bootstrap, coord, upgrade
- install.sh: remove framework bin PATH requirement, clean up old
  mosaic binary on upgrade, simplified summary output
- framework install.sh: preserve sources/ dir (fixes rsync noise
  about agent-skills)
- session-run.sh: update references to mosaic-launch

Users now only need ~/.npm-global/bin on PATH. The npm CLI finds
mosaic-launch at its absolute path (~/.config/mosaic/bin/mosaic-launch).
2026-04-02 19:23:44 -05:00

331 lines
13 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 ────────────────────────────────────────
#
# 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 --
#
# 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_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)
# ──────────────────────────────────────────────────────────────────────────────
#
# 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 ──────────────────────────────────────────────────────────────────
if [[ "${MOSAIC_NO_COLOR:-0}" == "1" ]] || ! [[ -t 1 ]]; then
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'
BOLD=$'\033[1m' 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}"; }
step() { echo ""; echo "${BOLD}$*${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
}
installed_cli_version() {
local json
json="$(npm ls -g --depth=0 --json --prefix="$PREFIX" 2>/dev/null)" || true
if [[ -n "$json" ]]; then
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
}
latest_cli_version() {
npm view "${CLI_PKG}" version --registry="$REGISTRY" 2>/dev/null || true
}
version_lt() {
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
}
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
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
echo ""
echo "${BOLD}Mosaic Stack Installer${RESET}"
echo ""
# ═══════════════════════════════════════════════════════════════════════════════
# 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
dim " Installed: (none)"
fi
dim " Source: ${REPO_BASE} (ref: ${GIT_REF})"
echo ""
if [[ "$FLAG_CHECK" == "true" ]]; then
if [[ "$HAS_FRAMEWORK" == "true" ]]; then
ok "Framework is installed."
else
warn "Framework not installed."
fi
else
# 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 ""
# Framework bin is no longer needed on PATH — the npm CLI delegates
# to mosaic-launch directly via its absolute path.
fi
fi
# ═══════════════════════════════════════════════════════════════════════════════
# 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 " Add to your shell rc: export PATH=\"$PREFIX/bin:\$PATH\""
fi
fi
fi
# ═══════════════════════════════════════════════════════════════════════════════
# Summary
# ═══════════════════════════════════════════════════════════════════════════════
if [[ "$FLAG_CHECK" == "false" ]]; then
step "Summary"
echo " ${BOLD}mosaic CLI:${RESET} $PREFIX/bin/mosaic"
dim " All commands: mosaic claude, mosaic yolo pi, mosaic tui, mosaic doctor, …"
echo ""
dim " Framework data: $MOSAIC_HOME/"
dim " Launcher backend: $MOSAIC_HOME/bin/mosaic-launch"
# Clean up old mosaic binary from PATH if framework bin is still there
FRAMEWORK_BIN="$MOSAIC_HOME/bin"
if [[ ":$PATH:" == *":$FRAMEWORK_BIN:"* ]]; then
OLD_MOSAIC="$FRAMEWORK_BIN/mosaic"
if [[ -f "$OLD_MOSAIC" ]] && [[ ! -L "$OLD_MOSAIC" ]]; then
info "Removing old framework 'mosaic' binary (replaced by npm CLI)"
rm -f "$OLD_MOSAIC"
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
main "$@"