314 lines
8.8 KiB
Bash
314 lines
8.8 KiB
Bash
#!/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-monitor"
|
||
".agents/skills/multi-agent-mux-resume"
|
||
".agents/skills/multi-agent-mux-status"
|
||
".agents/skills/multi-agent-mux-stop"
|
||
".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 skills..."
|
||
delete_asset ".agents/skills/lib.sh"
|
||
delete_asset ".agents/skills/multi-agent-mux-create"
|
||
delete_asset ".agents/skills/multi-agent-mux-delegate-job"
|
||
delete_asset ".agents/skills/multi-agent-mux-monitor"
|
||
delete_asset ".agents/skills/multi-agent-mux-resume"
|
||
delete_asset ".agents/skills/multi-agent-mux-status"
|
||
delete_asset ".agents/skills/multi-agent-mux-stop"
|
||
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 "===================================================================="
|