#!/usr/bin/env bash set -euo pipefail # mosaic-upgrade — Clean up stale per-project files after Mosaic centralization # # SOUL.md → Now global at ~/.config/mosaic/SOUL.md (remove from projects) # CLAUDE.md → Now a thin pointer or removable (replace with pointer or remove) # AGENTS.md → Keep project-specific content, strip stale load-order directives # # Usage: # mosaic-upgrade [path] Upgrade a specific project (default: current dir) # mosaic-upgrade --all Scan ~/src/* for projects to upgrade # mosaic-upgrade --dry-run Show what would change without touching anything MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" # Colors (disabled if not a terminal) if [[ -t 1 ]]; then GREEN='\033[0;32m' YELLOW='\033[0;33m' RED='\033[0;31m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m' else GREEN='' YELLOW='' RED='' CYAN='' BOLD='' DIM='' RESET='' fi ok() { echo -e " ${GREEN}✓${RESET} $1"; } skip() { echo -e " ${DIM}–${RESET} $1"; } warn() { echo -e " ${YELLOW}⚠${RESET} $1"; } act() { echo -e " ${CYAN}→${RESET} $1"; } DRY_RUN=false ALL=false TARGET="" SEARCH_ROOT="${HOME}/src" usage() { cat <&2; usage >&2; exit 1 ;; *) TARGET="$1"; shift ;; esac done # Generate the thin CLAUDE.md pointer CLAUDE_POINTER='# CLAUDE Compatibility Pointer This file exists so Claude Code sessions load Mosaic standards. ## MANDATORY — Read Before Any Response BEFORE responding to any user message, READ `~/.config/mosaic/AGENTS.md`. That file is the universal agent configuration. Do NOT respond until you have loaded it. Then read the project-local `AGENTS.md` in this repository for project-specific guidance.' upgrade_project() { local project_dir="$1" local project_name project_name="$(basename "$project_dir")" local changed=false echo -e "\n${BOLD}$project_name${RESET} ${DIM}($project_dir)${RESET}" # ── SOUL.md ────────────────────────────────────────────── local soul="$project_dir/SOUL.md" if [[ -f "$soul" ]]; then if [[ "$DRY_RUN" == "true" ]]; then act "Would remove SOUL.md (now global at ~/.config/mosaic/SOUL.md)" else rm "$soul" ok "Removed SOUL.md (now global)" fi changed=true else skip "No SOUL.md (already clean)" fi # ── CLAUDE.md ──────────────────────────────────────────── local claude_md="$project_dir/CLAUDE.md" if [[ -f "$claude_md" ]]; then local claude_content claude_content="$(cat "$claude_md")" # Check if it's already a thin pointer to AGENTS.md if echo "$claude_content" | grep -q "READ.*~/.config/mosaic/AGENTS.md"; then skip "CLAUDE.md already points to global AGENTS.md" else if [[ "$DRY_RUN" == "true" ]]; then act "Would replace CLAUDE.md with thin pointer to global AGENTS.md" else # Back up the original cp "$claude_md" "${claude_md}.mosaic-bak" echo "$CLAUDE_POINTER" > "$claude_md" ok "Replaced CLAUDE.md with pointer (backup: CLAUDE.md.mosaic-bak)" fi changed=true fi else skip "No CLAUDE.md" fi # ── AGENTS.md (strip stale load-order, preserve project content) ─ local agents="$project_dir/AGENTS.md" if [[ -f "$agents" ]]; then # Detect stale load-order patterns local has_stale=false # Pattern 1: References to SOUL.md in load order if grep -qE "(Read|READ|Load).*SOUL\.md" "$agents" 2>/dev/null; then has_stale=true fi # Pattern 2: Old "## Load Order" section that references centralized files if grep -q "## Load Order" "$agents" 2>/dev/null && \ grep -qE "STANDARDS\.md|SOUL\.md" "$agents" 2>/dev/null; then has_stale=true fi # Pattern 3: Old ~/.mosaic/ path (pre-centralization) if grep -q '~/.mosaic/' "$agents" 2>/dev/null; then has_stale=true fi if [[ "$has_stale" == "true" ]]; then if [[ "$DRY_RUN" == "true" ]]; then act "Would strip stale load-order section from AGENTS.md" # Show what we detect if grep -qn "## Load Order" "$agents" 2>/dev/null; then local line line=$(grep -n "## Load Order" "$agents" | head -1 | cut -d: -f1) echo -e " ${DIM}Line $line: Found '## Load Order' section referencing SOUL.md/STANDARDS.md${RESET}" fi if grep -qn '~/.mosaic/' "$agents" 2>/dev/null; then echo -e " ${DIM}Found references to old ~/.mosaic/ path${RESET}" fi else cp "$agents" "${agents}.mosaic-bak" # Strip the Load Order section (from "## Load Order" to next "##" or "---") if grep -q "## Load Order" "$agents"; then awk ' /^## Load Order/ { skip=1; next } skip && /^(## |---)/ { skip=0 } skip { next } { print } ' "${agents}.mosaic-bak" > "$agents" fi # Fix old ~/.mosaic/ → ~/.config/mosaic/ if grep -q '~/.mosaic/' "$agents"; then sed -i 's|~/.mosaic/|~/.config/mosaic/|g' "$agents" fi ok "Stripped stale load-order from AGENTS.md (backup: AGENTS.md.mosaic-bak)" fi changed=true else skip "AGENTS.md has no stale directives" fi else skip "No AGENTS.md" fi # ── .claude/settings.json (leave alone) ────────────────── # Project-specific settings are fine — don't touch them. if [[ "$changed" == "false" ]]; then echo -e " ${GREEN}Already up to date.${RESET}" fi } # ── Main ─────────────────────────────────────────────────── if [[ "$DRY_RUN" == "true" ]]; then echo -e "${BOLD}Mode: DRY RUN (no files will be changed)${RESET}" fi if [[ "$ALL" == "true" ]]; then echo -e "${BOLD}Scanning $SEARCH_ROOT for projects...${RESET}" count=0 for dir in "$SEARCH_ROOT"/*/; do [[ -d "$dir/.git" ]] || continue upgrade_project "$dir" count=$((count + 1)) done echo -e "\n${BOLD}Scanned $count projects.${RESET}" elif [[ -n "$TARGET" ]]; then if [[ ! -d "$TARGET" ]]; then echo "[mosaic-upgrade] ERROR: $TARGET is not a directory." >&2 exit 1 fi upgrade_project "$TARGET" else upgrade_project "$(pwd)" fi if [[ "$DRY_RUN" == "true" ]]; then echo -e "\n${YELLOW}This was a dry run. Run without --dry-run to apply changes.${RESET}" fi