Harden remove.sh: fix fallback data-loss risk, prevent remove.sh clobbering, and ensure macOS compatibility

This commit is contained in:
2026-06-24 12:08:00 +09:00
parent db75b7deb0
commit 5d69ad4f0b
2 changed files with 135 additions and 59 deletions
+14
View File
@@ -106,6 +106,11 @@ if ! check_assets_present "."; then
exit 1 exit 1
fi fi
# Create metadata directory and initialize manifest before copying
mkdir -p .mam
MANIFEST_FILE=".mam/install_manifest.txt"
touch "$MANIFEST_FILE"
# Copy ONLY runtime assets into the target, never overwriting an existing # Copy ONLY runtime assets into the target, never overwriting an existing
# target file. We merge per-file (POSIX find + an explicit "[ ! -e ]" guard) # target file. We merge per-file (POSIX find + an explicit "[ ! -e ]" guard)
# instead of `cp -n`: `cp -n` is non-portable and now prints a deprecation # instead of `cp -n`: `cp -n` is non-portable and now prints a deprecation
@@ -117,6 +122,7 @@ if ! check_assets_present "."; then
if [ ! -e "$dest" ]; then if [ ! -e "$dest" ]; then
mkdir -p "$(dirname "$dest")" mkdir -p "$(dirname "$dest")"
cp "$STAGE_DIR/.agents/$rel" "$dest" || { echo "❌ Error: Failed to copy $rel" >&2; exit 1; } cp "$STAGE_DIR/.agents/$rel" "$dest" || { echo "❌ Error: Failed to copy $rel" >&2; exit 1; }
echo "$dest" >> "$MANIFEST_FILE"
fi fi
done done
@@ -125,16 +131,19 @@ if ! check_assets_present "."; then
for doc in AGENT.md AGENT.ko.md MESSAGING.md BOOTSTRAP.md BOOTSTRAP.ko.md INSTRUCTION.md; do for doc in AGENT.md AGENT.ko.md MESSAGING.md BOOTSTRAP.md BOOTSTRAP.ko.md INSTRUCTION.md; do
if [ -f "$STAGE_DIR/$doc" ] && [ ! -e "$doc" ]; then if [ -f "$STAGE_DIR/$doc" ] && [ ! -e "$doc" ]; then
cp "$STAGE_DIR/$doc" . || { echo "❌ Error: Failed to copy $doc" >&2; exit 1; } cp "$STAGE_DIR/$doc" . || { echo "❌ Error: Failed to copy $doc" >&2; exit 1; }
echo "$doc" >> "$MANIFEST_FILE"
fi fi
done done
if [ -f "$STAGE_DIR/deploy/remove.sh" ] && [ ! -e "remove.sh" ]; then if [ -f "$STAGE_DIR/deploy/remove.sh" ] && [ ! -e "remove.sh" ]; then
cp "$STAGE_DIR/deploy/remove.sh" remove.sh || { echo "❌ Error: Failed to copy remove.sh" >&2; exit 1; } cp "$STAGE_DIR/deploy/remove.sh" remove.sh || { echo "❌ Error: Failed to copy remove.sh" >&2; exit 1; }
chmod +x remove.sh chmod +x remove.sh
echo "remove.sh" >> "$MANIFEST_FILE"
fi fi
if [ -f "$STAGE_DIR/.env.example" ] && [ ! -e ".env.example" ]; then if [ -f "$STAGE_DIR/.env.example" ] && [ ! -e ".env.example" ]; then
cp "$STAGE_DIR/.env.example" . || { echo "❌ Error: Failed to copy .env.example" >&2; exit 1; } cp "$STAGE_DIR/.env.example" . || { echo "❌ Error: Failed to copy .env.example" >&2; exit 1; }
echo ".env.example" >> "$MANIFEST_FILE"
fi fi
rm -rf "$STAGE_DIR" rm -rf "$STAGE_DIR"
@@ -225,6 +234,11 @@ TMUX_SERVER_NAME=default
EOF EOF
chmod 0600 "$ENV_FILE" chmod 0600 "$ENV_FILE"
echo "✅ Config file .env initialized with chmod 0600." echo "✅ Config file .env initialized with chmod 0600."
# Record the newly created .env in the manifest
mkdir -p .mam
touch .mam/install_manifest.txt
echo "$ENV_FILE" >> .mam/install_manifest.txt
else else
echo "$ENV_FILE already exists. Skipping config override." echo "$ENV_FILE already exists. Skipping config override."
fi fi
+121 -59
View File
@@ -3,12 +3,14 @@
# remove.sh — Multi-Agent Mux (MAM) Orchestration Uninstaller # remove.sh — Multi-Agent Mux (MAM) Orchestration Uninstaller
# ============================================================================== # ==============================================================================
# Safely removes MAM orchestration skills, virtual environment, and metadata. # Safely removes MAM orchestration skills, virtual environment, and metadata.
# Leaves pre-existing user configurations and files untouched. # Leaves pre-existing user configurations and files untouched by reading
# the installation manifest (.mam/install_manifest.txt).
# ============================================================================== # ==============================================================================
set -euo pipefail set -euo pipefail
TARGET_DIR="" TARGET_DIR=""
FORCE=0 FORCE=0
PURGE_ENV=0
# Parse arguments # Parse arguments
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
@@ -17,6 +19,10 @@ while [[ $# -gt 0 ]]; do
FORCE=1 FORCE=1
shift shift
;; ;;
--purge-env)
PURGE_ENV=1
shift
;;
*) *)
TARGET_DIR="$1" TARGET_DIR="$1"
shift shift
@@ -40,33 +46,45 @@ fi
cd "$TARGET_DIR" cd "$TARGET_DIR"
# Check if there is anything to remove # 1. Non-interactive input safety guard (set -e read crash prevention)
assets_to_check=( if [ ! -t 0 ] && [ $FORCE -eq 0 ]; then
".agents/skills/lib.sh" echo "❌ Error: Non-interactive terminal detected. Please run with -y/--yes/--force." >&2
".agents/skills/multi-agent-mux-create" exit 1
".agents/skills/multi-agent-mux-delegate-job" fi
".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"
"AGENT.md"
"AGENT.ko.md"
"MESSAGING.md"
"BOOTSTRAP.md"
"BOOTSTRAP.ko.md"
"INSTRUCTION.md"
".env"
)
# Check if there is anything to remove
MANIFEST_FILE=".mam/install_manifest.txt"
any_exist=0 any_exist=0
for asset in "${assets_to_check[@]}"; do manifest_files=()
if [ -e "$asset" ] || [ -h "$asset" ]; then
any_exist=1 # Load the install manifest if it exists
break if [ -f "$MANIFEST_FILE" ]; then
fi any_exist=1
done while IFS= read -r line; do
if [ -n "$line" ]; then
manifest_files+=("$line")
fi
done < "$MANIFEST_FILE"
else
# Fallback to the core MAM directories to check if any exist
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"
)
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 if [ $any_exist -eq 0 ]; then
echo "️ No MAM assets detected in '$TARGET_DIR'. Nothing to do." echo "️ No MAM assets detected in '$TARGET_DIR'. Nothing to do."
@@ -78,7 +96,10 @@ if [ $FORCE -eq 0 ]; then
echo "⚠️ WARNING: This will permanently remove the MAM orchestration skills, " echo "⚠️ WARNING: This will permanently remove the MAM orchestration skills, "
echo " virtual environment (.venv), local metadata (.mam), and docs." echo " virtual environment (.venv), local metadata (.mam), and docs."
echo " (Your own custom files inside .agents/ will NOT be touched)." echo " (Your own custom files inside .agents/ will NOT be touched)."
read -p "❓ Are you sure you want to proceed? [y/N]: " -r response
if ! read -p "❓ Are you sure you want to proceed? [y/N]: " -r response; then
response="n"
fi
if [[ ! "$response" =~ ^[yY](es)?$ ]]; then if [[ ! "$response" =~ ^[yY](es)?$ ]]; then
echo "❌ Uninstallation cancelled by user." echo "❌ Uninstallation cancelled by user."
exit 0 exit 0
@@ -93,54 +114,95 @@ delete_asset() {
fi fi
} }
# 1. Remove MAM skills # 2. Uninstall files using the manifest if present
delete_asset ".agents/skills/lib.sh" if [ ${#manifest_files[@]} -gt 0 ]; then
delete_asset ".agents/skills/multi-agent-mux-create" echo "📜 Manifest found. Reversing installer-created files..."
delete_asset ".agents/skills/multi-agent-mux-delegate-job" for f in ${manifest_files[@]+"${manifest_files[@]}"}; do
delete_asset ".agents/skills/multi-agent-mux-monitor" # Skip .env and remove.sh for now, they are handled separately
delete_asset ".agents/skills/multi-agent-mux-resume" if [ "$f" = ".env" ] || [ "$f" = "remove.sh" ]; then
delete_asset ".agents/skills/multi-agent-mux-status" continue
delete_asset ".agents/skills/multi-agent-mux-stop" fi
delete_asset "$f"
done
else
# Fallback: Delete MAM skills manually (only if manifest is missing)
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
# Remove empty parent dirs under .agents to avoid littering # 3. Clean up empty parent directories under .agents recursively to avoid littering
if [ -d ".agents/skills" ]; then
rmdir ".agents/skills" 2>/dev/null || true
fi
if [ -d ".agents" ]; then if [ -d ".agents" ]; then
rmdir ".agents" 2>/dev/null || true find .agents -depth -type d -exec rmdir {} + 2>/dev/null || true
fi fi
# 2. Remove virtual environment & metadata # 4. Remove virtual environment, monitor cache, and metadata database
delete_asset ".venv" delete_asset ".venv"
delete_asset ".mam" delete_asset ".cache/multi-agent-mux-monitor"
delete_asset ".mam" # Deletes manifest file too
# 5. Clean up .env file (Only if created by installer, or forced with --purge-env)
# If .env is in manifest, it means MAM created it.
env_created_by_mam=0
for f in ${manifest_files[@]+"${manifest_files[@]}"}; do
if [ "$f" = ".env" ]; then
env_created_by_mam=1
break
fi
done
# 3. Remove configuration files (optionally keep or confirm .env)
# Since .env might contain custom keys, we confirm unless forced.
if [ -f ".env" ]; then if [ -f ".env" ]; then
if [ $FORCE -eq 1 ]; then should_delete_env=0
if [ $PURGE_ENV -eq 1 ]; then
should_delete_env=1
elif [ $env_created_by_mam -eq 1 ]; then
# Even if MAM created it, ask or rename to backup to prevent loss of custom secrets
if [ $FORCE -eq 1 ]; then
should_delete_env=1
else
if ! read -p "❓ MAM-created '.env' 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
fi
if [ $should_delete_env -eq 1 ]; then
delete_asset ".env" delete_asset ".env"
else else
read -p "❓ Do you want to remove '.env' configuration file? [y/N]: " -r response if [ $env_created_by_mam -eq 1 ]; then
if [[ "$response" =~ ^[yY](es)?$ ]]; then backup_name=".env.mam-backup"
delete_asset ".env" if [ -e "$backup_name" ]; then
backup_name=".env.mam-backup.$(date +%Y%m%d%H%M%S)"
fi
echo "💾 Backing up .env configuration to $backup_name..."
mv ".env" "$backup_name"
else else
echo "️ Preserving .env configuration." echo "️ Preserving user-owned .env configuration."
fi fi
fi fi
fi fi
delete_asset ".env.example"
# 4. Remove runtime documentation # 6. Remove uninstaller file itself (if we are in the target root)
delete_asset "AGENT.md" # Simple check: only delete remove.sh if it is recorded in the manifest
delete_asset "AGENT.ko.md" remove_in_manifest=0
delete_asset "MESSAGING.md" for f in ${manifest_files[@]+"${manifest_files[@]}"}; do
delete_asset "BOOTSTRAP.md" if [ "$f" = "remove.sh" ]; then
delete_asset "BOOTSTRAP.ko.md" remove_in_manifest=1
delete_asset "INSTRUCTION.md" break
fi
done
# Remove this uninstaller file itself (if we are in the target root) if [ -f "remove.sh" ] && [ $remove_in_manifest -eq 1 ]; then
if [ -f "remove.sh" ] && [ "$(realpath "remove.sh")" = "$(realpath "$0")" ]; then
echo "🗑️ Removing uninstaller: remove.sh" echo "🗑️ Removing uninstaller: remove.sh"
# Self-delete is the final action
rm -f "remove.sh" rm -f "remove.sh"
fi fi