#!/usr/bin/env bash set -euo pipefail # ─── Mosaic Framework Installer ────────────────────────────────────────────── # # Installs/upgrades the framework DATA to ~/.config/mosaic/. # No executables are placed on PATH — the mosaic npm CLI is the only binary. # # Called by tools/install.sh (the unified installer). Can also be run directly. # # Environment: # MOSAIC_HOME — target directory (default: ~/.config/mosaic) # MOSAIC_INSTALL_MODE — prompt|keep|overwrite (default: prompt) # MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING — 1 to bypass MCP check # MOSAIC_SKIP_SKILLS_SYNC — 1 to skip skill sync # ────────────────────────────────────────────────────────────────────────────── SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TARGET_DIR="${MOSAIC_HOME:-$HOME/.config/mosaic}" INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}" # Files preserved across upgrades (never overwritten) PRESERVE_PATHS=("SOUL.md" "USER.md" "TOOLS.md" "memory" "sources") # Current framework schema version — bump this when the layout changes. # The migration system uses this to run upgrade steps. FRAMEWORK_VERSION=2 # ─── colours ────────────────────────────────────────────────────────────────── if [[ -t 1 ]]; then GREEN='\033[0;32m' YELLOW='\033[0;33m' RED='\033[0;31m' CYAN='\033[0;36m' BOLD='\033[1m' RESET='\033[0m' else GREEN='' YELLOW='' RED='' CYAN='' BOLD='' RESET='' fi ok() { echo -e " ${GREEN}✓${RESET} $1"; } warn() { echo -e " ${YELLOW}⚠${RESET} $1" >&2; } fail() { echo -e " ${RED}✗${RESET} $1" >&2; } step() { echo -e "\n${BOLD}$1${RESET}"; } # ─── helpers ────────────────────────────────────────────────────────────────── is_existing_install() { [[ -d "$TARGET_DIR" ]] || return 1 [[ -f "$TARGET_DIR/AGENTS.md" || -f "$TARGET_DIR/SOUL.md" ]] } installed_framework_version() { local vf="$TARGET_DIR/.framework-version" if [[ -f "$vf" ]]; then cat "$vf" 2>/dev/null || echo "0" else # No version file = legacy install (version 0 or 1) if [[ -d "$TARGET_DIR/bin" ]]; then echo "1" # Has bin/ → pre-migration legacy else echo "0" # Fresh or unknown fi fi } write_framework_version() { echo "$FRAMEWORK_VERSION" > "$TARGET_DIR/.framework-version" } select_install_mode() { case "$INSTALL_MODE" in keep|overwrite|prompt) ;; *) fail "Invalid MOSAIC_INSTALL_MODE='$INSTALL_MODE'. Use: prompt, keep, overwrite." exit 1 ;; esac if ! is_existing_install; then INSTALL_MODE="overwrite" return fi case "$INSTALL_MODE" in keep|overwrite) ;; prompt) if [[ -t 0 ]]; then echo "" echo "Existing Mosaic install detected at: $TARGET_DIR" echo " 1) keep Update framework, preserve local files (SOUL.md, USER.md, etc.)" echo " 2) overwrite Replace everything" echo " 3) cancel Abort" printf "Selection [1/2/3] (default: 1): " read -r selection case "${selection:-1}" in 1|k|K|keep) INSTALL_MODE="keep" ;; 2|o|O|overwrite) INSTALL_MODE="overwrite" ;; *) fail "Install cancelled."; exit 1 ;; esac else INSTALL_MODE="keep" fi ;; esac } sync_framework() { local source_real target_real source_real="$(cd "$SOURCE_DIR" && pwd -P)" target_real="$(mkdir -p "$TARGET_DIR" && cd "$TARGET_DIR" && pwd -P)" if [[ "$source_real" == "$target_real" ]]; then warn "Source and target are the same directory; skipping file sync." return fi if command -v rsync >/dev/null 2>&1; then local rsync_args=(-a --delete --exclude ".git" --exclude ".framework-version") if [[ "$INSTALL_MODE" == "keep" ]]; then for path in "${PRESERVE_PATHS[@]}"; do rsync_args+=(--exclude "$path") done fi rsync "${rsync_args[@]}" "$SOURCE_DIR/" "$TARGET_DIR/" return fi # Fallback: cp-based sync local preserve_tmp="" if [[ "$INSTALL_MODE" == "keep" ]]; then preserve_tmp="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-preserve-XXXXXX")" for path in "${PRESERVE_PATHS[@]}"; do if [[ -e "$TARGET_DIR/$path" ]]; then mkdir -p "$preserve_tmp/$(dirname "$path")" cp -R "$TARGET_DIR/$path" "$preserve_tmp/$path" fi done fi find "$TARGET_DIR" -mindepth 1 -maxdepth 1 ! -name ".git" ! -name ".framework-version" -exec rm -rf {} + cp -R "$SOURCE_DIR"/. "$TARGET_DIR"/ rm -rf "$TARGET_DIR/.git" if [[ -n "$preserve_tmp" ]]; then for path in "${PRESERVE_PATHS[@]}"; do if [[ -e "$preserve_tmp/$path" ]]; then rm -rf "$TARGET_DIR/$path" mkdir -p "$TARGET_DIR/$(dirname "$path")" cp -R "$preserve_tmp/$path" "$TARGET_DIR/$path" fi done rm -rf "$preserve_tmp" fi } # ═══════════════════════════════════════════════════════════════════════════════ # Migrations — run sequentially from the installed version to FRAMEWORK_VERSION # ═══════════════════════════════════════════════════════════════════════════════ run_migrations() { local from_version from_version="$(installed_framework_version)" if [[ "$from_version" -ge "$FRAMEWORK_VERSION" ]]; then return # Already current fi step "Running migrations (v${from_version} → v${FRAMEWORK_VERSION})" # ── Migration: v0/v1 → v2 ───────────────────────────────────────────────── # Remove bin/ directory — all executables now live in the npm CLI. # Scripts that were in bin/ are now in tools/_scripts/. if [[ "$from_version" -lt 2 ]]; then if [[ -d "$TARGET_DIR/bin" ]]; then ok "Removing legacy bin/ directory (executables now in npm CLI)" rm -rf "$TARGET_DIR/bin" fi # Remove old mosaic PATH entry from shell profiles for profile in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do if [[ -f "$profile" ]] && grep -qF "$TARGET_DIR/bin" "$profile"; then # Remove the PATH line and the comment above it sed -i.mosaic-migration-bak \ -e "\|# Mosaic agent framework|d" \ -e "\|$TARGET_DIR/bin|d" \ "$profile" ok "Cleaned up old PATH entry from $(basename "$profile")" rm -f "${profile}.mosaic-migration-bak" fi done # Remove stale rails/ symlink if [[ -L "$TARGET_DIR/rails" ]]; then rm -f "$TARGET_DIR/rails" fi fi # ── Future migrations go here ────────────────────────────────────────────── # if [[ "$from_version" -lt 3 ]]; then # ... # fi } # ═══════════════════════════════════════════════════════════════════════════════ # Main # ═══════════════════════════════════════════════════════════════════════════════ step "Installing Mosaic framework" mkdir -p "$TARGET_DIR" select_install_mode if [[ "$INSTALL_MODE" == "keep" ]]; then ok "Install mode: keep local files (SOUL.md, USER.md, TOOLS.md, memory/)" else ok "Install mode: overwrite" fi sync_framework # Ensure memory directory exists mkdir -p "$TARGET_DIR/memory" # Ensure tool scripts are executable find "$TARGET_DIR/tools" -name "*.sh" -exec chmod +x {} + 2>/dev/null || true find "$TARGET_DIR/tools/_scripts" -type f -exec chmod +x {} + 2>/dev/null || true ok "Framework synced to $TARGET_DIR" # Run migrations before post-install (migrations may remove old bin/ etc.) run_migrations step "Post-install tasks" SCRIPTS="$TARGET_DIR/tools/_scripts" if [[ -x "$SCRIPTS/mosaic-link-runtime-assets" ]]; then if "$SCRIPTS/mosaic-link-runtime-assets" >/dev/null 2>&1; then ok "Runtime assets linked" else warn "Runtime asset linking failed (non-fatal)" fi fi if [[ -x "$SCRIPTS/mosaic-ensure-sequential-thinking" ]]; then if "$SCRIPTS/mosaic-ensure-sequential-thinking" >/dev/null 2>&1; then ok "sequential-thinking MCP configured" else if [[ "${MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING:-0}" == "1" ]]; then warn "sequential-thinking MCP setup bypassed (MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING=1)" else fail "sequential-thinking MCP setup failed (hard requirement)." exit 1 fi fi fi if [[ -x "$SCRIPTS/mosaic-ensure-excalidraw" ]]; then "$SCRIPTS/mosaic-ensure-excalidraw" >/dev/null 2>&1 && ok "excalidraw MCP configured" || warn "excalidraw MCP setup failed (non-fatal)" fi if [[ "${MOSAIC_SKIP_SKILLS_SYNC:-0}" != "1" ]] && [[ -x "$SCRIPTS/mosaic-sync-skills" ]]; then "$SCRIPTS/mosaic-sync-skills" >/dev/null 2>&1 && ok "Skills synced" || warn "Skills sync failed (non-fatal)" fi if [[ -x "$SCRIPTS/mosaic-migrate-local-skills" ]]; then "$SCRIPTS/mosaic-migrate-local-skills" --apply >/dev/null 2>&1 && ok "Local skills migrated" || warn "Local skill migration failed (non-fatal)" fi if [[ -x "$SCRIPTS/mosaic-doctor" ]]; then "$SCRIPTS/mosaic-doctor" >/dev/null 2>&1 && ok "Health audit passed" || warn "Health audit reported issues — run 'mosaic doctor' for details" fi # Write version stamp AFTER everything succeeds write_framework_version # ── Summary ────────────────────────────────────────────────── echo "" echo -e "${GREEN}${BOLD} Mosaic framework installed.${RESET}" echo "" if [[ ! -f "$TARGET_DIR/SOUL.md" ]]; then echo -e " Run ${CYAN}mosaic init${RESET} to set up your agent identity." echo "" fi