diff --git a/bin/mosaic b/bin/mosaic index d4bea29..e98109a 100755 --- a/bin/mosaic +++ b/bin/mosaic @@ -34,6 +34,7 @@ Management: doctor [args...] Audit runtime state and detect drift sync [args...] Sync skills from canonical source bootstrap Bootstrap a repo with Mosaic standards + upgrade [path] Clean up stale SOUL.md/CLAUDE.md in a project Options: -h, --help Show this help @@ -146,6 +147,11 @@ run_bootstrap() { exec "$MOSAIC_HOME/bin/mosaic-bootstrap-repo" "$@" } +run_upgrade() { + check_mosaic_home + exec "$MOSAIC_HOME/bin/mosaic-upgrade" "$@" +} + # Main router if [[ $# -eq 0 ]]; then usage @@ -163,6 +169,7 @@ case "$command" in doctor) run_doctor "$@" ;; sync) run_sync "$@" ;; bootstrap) run_bootstrap "$@" ;; + upgrade) run_upgrade "$@" ;; help|-h|--help) usage ;; version|-v|--version) echo "mosaic $VERSION" ;; *) diff --git a/bin/mosaic-upgrade b/bin/mosaic-upgrade new file mode 100755 index 0000000..29492c9 --- /dev/null +++ b/bin/mosaic-upgrade @@ -0,0 +1,218 @@ +#!/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 diff --git a/bin/mosaic.ps1 b/bin/mosaic.ps1 index 5a4d14f..47175c7 100644 --- a/bin/mosaic.ps1 +++ b/bin/mosaic.ps1 @@ -31,6 +31,7 @@ Management: doctor [args...] Audit runtime state and detect drift sync [args...] Sync skills from canonical source bootstrap Bootstrap a repo with Mosaic standards + upgrade [path] Clean up stale SOUL.md/CLAUDE.md in a project Options: -h, --help Show this help @@ -140,6 +141,11 @@ switch ($command) { Write-Host "[mosaic] NOTE: mosaic-bootstrap-repo requires bash. Use Git Bash or WSL." -ForegroundColor Yellow & (Join-Path $MosaicHome "bin\mosaic-bootstrap-repo") @remaining } + "upgrade" { + Assert-MosaicHome + Write-Host "[mosaic] NOTE: mosaic-upgrade requires bash. Use Git Bash or WSL." -ForegroundColor Yellow + & (Join-Path $MosaicHome "bin\mosaic-upgrade") @remaining + } { $_ -in "help", "-h", "--help" } { Show-Usage } { $_ -in "version", "-v", "--version" } { Write-Host "mosaic $Version" } default {