#!/usr/bin/env bash # ============================================================================== # remove.sh — Multi-Agent Mux (MAM) Orchestration Uninstaller # ============================================================================== # Safely removes MAM orchestration skills, virtual environment, and metadata. # Leaves pre-existing user configurations and files untouched by reading # the installation manifest (.mam/install_manifest.txt). # ============================================================================== set -euo pipefail TARGET_DIR="" FORCE=0 PURGE_ENV=0 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Parse arguments while [[ $# -gt 0 ]]; do case "$1" in -y|--yes|--force) FORCE=1 shift ;; --purge-env) PURGE_ENV=1 shift ;; *) TARGET_DIR="$1" shift ;; esac done if [ -z "$TARGET_DIR" ]; then if [ "$(basename "$SCRIPT_DIR")" = ".mam_deploy" ]; then TARGET_DIR="$(dirname "$SCRIPT_DIR")" else TARGET_DIR="$(pwd)" fi else if [ "$(basename "$TARGET_DIR")" = ".mam_deploy" ]; then TARGET_DIR="$(dirname "$TARGET_DIR")" fi fi echo "====================================================================" echo "⚡ Starting Multi-Agent Mux (MAM) Uninstallation" echo "📂 Target Workspace: $TARGET_DIR" echo "====================================================================" if [ ! -d "$TARGET_DIR" ]; then echo "❌ Error: Target directory '$TARGET_DIR' does not exist." >&2 exit 1 fi cd "$TARGET_DIR" GI_CREATED=0 if [ -f ".mam/install_state" ]; then GI_CREATED=$(grep '^gitignore_created=' .mam/install_state 2>/dev/null | cut -d= -f2 || echo 0) fi # 1. Non-interactive input safety guard (set -e read crash prevention) if [ ! -t 0 ] && [ $FORCE -eq 0 ]; then echo "❌ Error: Non-interactive terminal detected. Please run with -y/--yes/--force." >&2 exit 1 fi # Check if there is anything to remove MANIFEST_FILE=".mam/install_manifest.txt" any_exist=0 manifest_files=() # Load the install manifest if it exists if [ -f "$MANIFEST_FILE" ]; then any_exist=1 while IFS= read -r line; do if [ -n "$line" ]; then manifest_files+=("$line") fi done < "$MANIFEST_FILE" else fallback_assets=( ".agents/skills/lib.sh" ".agents/skills/multi-agent-mux-create" ".agents/skills/multi-agent-mux-delegate-job" ".agents/skills/multi-agent-mux-loop" ".agents/skills/multi-agent-mux-monitor" ".agents/skills/multi-agent-mux-resume" ".agents/skills/multi-agent-mux-status" ".agents/skills/multi-agent-mux-stop" ".agents/hooks" ".agents/hooks.json" ".agents/MULTI_AGENT_RULES.md" ".agents/MULTI_AGENT_RULES.ko.md" ".agents/INSTALL.md" ".venv" ".mam" ".mam_deploy" ) for asset in "${fallback_assets[@]}"; do if [ -e "$asset" ] || [ -h "$asset" ]; then any_exist=1 break fi done fi if [ $any_exist -eq 0 ]; then echo "â„šī¸ No MAM assets detected in '$TARGET_DIR'. Nothing to do." exit 0 fi # Request confirmation if not forced if [ $FORCE -eq 0 ]; then echo "âš ī¸ WARNING: This will permanently remove the MAM orchestration skills, " echo " virtual environment (.venv), local metadata (.mam), and docs." if ! read -p "❓ Are you sure you want to proceed? [y/N]: " -r response; then response="n" fi if [[ ! "$response" =~ ^[yY](es)?$ ]]; then echo "❌ Uninstallation cancelled by user." exit 0 fi fi delete_asset() { local asset="$1" if [ -e "$asset" ] || [ -h "$asset" ]; then echo "đŸ—‘ī¸ Removing: $asset" rm -rf "$asset" fi } # Check for modified skills before deleting TS=$(date -u +%Y%m%dT%H%M%SZ) python3 - ".mam/asset_hashes.txt" "$TS" <<'PY' 2>/dev/null || true import sys, os, hashlib, shutil hash_db_path = sys.argv[1] ts = sys.argv[2] if not os.path.exists(hash_db_path): sys.exit(0) modified = [] with open(hash_db_path, "r") as f: for line in f: parts = line.strip().split(None, 1) if len(parts) == 2: expected_hash, path = parts[0], parts[1] if os.path.exists(path): h = hashlib.sha256() with open(path, "rb") as pf: while chunk := pf.read(65536): h.update(chunk) if h.hexdigest() != expected_hash: modified.append(path) if modified: backup_dir = f".mam-skill-backup.{ts}" os.makedirs(backup_dir, exist_ok=True) for p in modified: dest = os.path.join(backup_dir, p) os.makedirs(os.path.dirname(dest), exist_ok=True) shutil.copy2(p, dest) print(f"💾 Preserved {len(modified)} modified skill file(s) under {backup_dir}") PY # 2. Uninstall files using the manifest if present if [ ${#manifest_files[@]} -gt 0 ]; then echo "📜 Manifest found. Reversing installer-created files..." for f in ${manifest_files[@]+"${manifest_files[@]}"}; do if [ "$f" = ".env" ] || [ "$f" = ".mam.env" ] || [ "$f" = "remove.sh" ] || [ "$f" = ".mam_deploy/remove.sh" ]; then continue fi delete_asset "$f" done else echo "âš ī¸ No manifest found. Deleting standard MAM assets..." for asset in ${fallback_assets[@]+"${fallback_assets[@]}"}; do case "$asset" in .venv|.mam|.mam_deploy) continue ;; # handled explicitly further down esac delete_asset "$asset" done fi if [ -d ".agents" ]; then find .agents -depth -type d -exec rmdir {} + 2>/dev/null || true fi # Clean up .gitignore managed block (C8) MAM_GI_START="# >>> MAM managed block (managed by install.sh — do not edit) >>>" MAM_GI_END="# <<< MAM managed block <<<" if [ -f .gitignore ]; then python3 - .gitignore "$MAM_GI_START" "$MAM_GI_END" "$GI_CREATED" <<'PY' import sys, os gi_path, start_marker, end_marker, gi_created = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] == "1" if not os.path.exists(gi_path): sys.exit(0) with open(gi_path, "r") as f: lines = f.readlines() new_lines = [] in_block = False block_found = False for line in lines: if line.strip() == start_marker: in_block = True block_found = True continue if line.strip() == end_marker: in_block = False continue if not in_block: new_lines.append(line) if block_found: content = "".join(new_lines).strip() if gi_created and not content: os.remove(gi_path) else: with open(gi_path, "w") as f: f.writelines(new_lines) PY fi delete_asset ".venv" delete_asset ".cache/multi-agent-mux-monitor" rmdir ".cache" 2>/dev/null || true delete_asset ".mam" for env_name in ".mam.env" ".env"; do [ -f "$env_name" ] || continue env_created_by_mam=0 for f in ${manifest_files[@]+"${manifest_files[@]}"}; do if [ "$f" = "$env_name" ]; then env_created_by_mam=1 break fi done should_delete_env=0 if [ $PURGE_ENV -eq 1 ]; then should_delete_env=1 elif [ $env_created_by_mam -eq 1 ] && [ $FORCE -eq 0 ]; then if ! read -p "❓ MAM-created '$env_name' found. Delete it? (Saying No preserves it) [y/N]: " -r env_response; then env_response="n" fi if [[ "$env_response" =~ ^[yY](es)?$ ]]; then should_delete_env=1 fi fi if [ $should_delete_env -eq 1 ]; then delete_asset "$env_name" else if [ $env_created_by_mam -eq 1 ]; then already_preserved=0 for existing in "${env_name}.mam-backup" "${env_name}".mam-backup.*; do [ -f "$existing" ] || continue if cmp -s "$env_name" "$existing" 2>/dev/null; then already_preserved=1 rm -f "$env_name" echo "â„šī¸ '$env_name' is already preserved in $existing (no duplicate created)." break fi done if [ $already_preserved -eq 0 ]; then slot="${env_name}.mam-backup" if [ -e "$slot" ]; then slot="${env_name}.mam-backup.$(date +%Y%m%d%H%M%S)" n=1 while [ -e "$slot" ]; do slot="${env_name}.mam-backup.$(date +%Y%m%d%H%M%S)-$n" n=$((n + 1)) done fi mv "$env_name" "$slot" echo "💾 Backed up $env_name -> $slot" fi else echo "â„šī¸ Preserving user-owned $env_name configuration." fi fi done # Remove uninstaller file(s) for self in ".mam_deploy/remove.sh" "remove.sh"; do [ -f "$self" ] || continue in_manifest=0 for f in ${manifest_files[@]+"${manifest_files[@]}"}; do if [ "$f" = "$self" ]; then in_manifest=1 break fi done if [ $in_manifest -eq 1 ] || [ $FORCE -eq 1 ]; then echo "đŸ—‘ī¸ Removing uninstaller: $self" rm -f "$self" fi done delete_asset ".mam_deploy/update.sh" rmdir .mam_deploy 2>/dev/null || true echo "====================================================================" echo "🎉 Uninstallation complete!" echo "===================================================================="