2025-09-25 20:22:51 +08:00
|
|
|
#!/bin/bash
|
|
|
|
|
|
2025-10-08 18:01:46 +08:00
|
|
|
set -euo pipefail
|
|
|
|
|
|
2025-12-31 16:23:31 +08:00
|
|
|
# Ensure common.sh is loaded.
|
2026-01-14 08:55:41 -05:00
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
2025-12-01 16:58:35 +08:00
|
|
|
[[ -z "${MOLE_COMMON_LOADED:-}" ]] && source "$SCRIPT_DIR/lib/core/common.sh"
|
2025-10-08 18:01:46 +08:00
|
|
|
|
2026-01-14 08:55:41 -05:00
|
|
|
# Load Homebrew cask support (provides get_brew_cask_name, brew_uninstall_cask)
|
|
|
|
|
[[ -f "$SCRIPT_DIR/lib/uninstall/brew.sh" ]] && source "$SCRIPT_DIR/lib/uninstall/brew.sh"
|
|
|
|
|
|
2025-12-31 16:23:31 +08:00
|
|
|
# Batch uninstall with a single confirmation.
|
2025-11-14 11:38:25 +08:00
|
|
|
|
2026-01-17 09:09:11 +08:00
|
|
|
# High-performance sensitive data detection (pure Bash, no subprocess)
|
|
|
|
|
# Faster than grep for batch operations, especially when processing many apps
|
|
|
|
|
has_sensitive_data() {
|
|
|
|
|
local files="$1"
|
|
|
|
|
[[ -z "$files" ]] && return 1
|
|
|
|
|
|
|
|
|
|
while IFS= read -r file; do
|
|
|
|
|
[[ -z "$file" ]] && continue
|
|
|
|
|
|
|
|
|
|
# Use Bash native pattern matching (faster than spawning grep)
|
|
|
|
|
case "$file" in
|
2026-01-17 01:53:40 +00:00
|
|
|
*/.warp* | */.config/* | */themes/* | */settings/* | */User\ Data/* | \
|
|
|
|
|
*/.ssh/* | */.gnupg/* | */Documents/* | */Preferences/*.plist | \
|
|
|
|
|
*/Desktop/* | */Downloads/* | */Movies/* | */Music/* | */Pictures/* | \
|
|
|
|
|
*/.password* | */.token* | */.auth* | */keychain* | \
|
|
|
|
|
*/Passwords/* | */Accounts/* | */Cookies/* | \
|
|
|
|
|
*/.aws/* | */.docker/config.json | */.kube/* | \
|
|
|
|
|
*/credentials/* | */secrets/*)
|
|
|
|
|
return 0 # Found sensitive data
|
|
|
|
|
;;
|
2026-01-17 09:09:11 +08:00
|
|
|
esac
|
2026-01-17 01:53:40 +00:00
|
|
|
done <<< "$files"
|
2026-01-17 09:09:11 +08:00
|
|
|
|
|
|
|
|
return 1 # Not found
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-31 16:23:31 +08:00
|
|
|
# Decode and validate base64 file list (safe for set -e).
|
2025-11-14 11:38:25 +08:00
|
|
|
decode_file_list() {
|
|
|
|
|
local encoded="$1"
|
|
|
|
|
local app_name="$2"
|
|
|
|
|
local decoded
|
|
|
|
|
|
2025-12-31 16:23:31 +08:00
|
|
|
# macOS uses -D, GNU uses -d. Always return 0 for set -e safety.
|
2026-01-17 01:53:40 +00:00
|
|
|
if ! decoded=$(printf '%s' "$encoded" | base64 -D 2> /dev/null); then
|
|
|
|
|
if ! decoded=$(printf '%s' "$encoded" | base64 -d 2> /dev/null); then
|
2025-12-25 11:24:12 +08:00
|
|
|
log_error "Failed to decode file list for $app_name" >&2
|
|
|
|
|
echo ""
|
2025-12-25 03:27:51 +00:00
|
|
|
return 0 # Return success with empty string
|
2025-12-25 11:24:12 +08:00
|
|
|
fi
|
2025-11-14 11:38:25 +08:00
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [[ "$decoded" =~ $'\0' ]]; then
|
2025-12-25 11:24:12 +08:00
|
|
|
log_warning "File list for $app_name contains null bytes, rejecting" >&2
|
2025-11-14 11:38:25 +08:00
|
|
|
echo ""
|
2025-12-25 03:27:51 +00:00
|
|
|
return 0 # Return success with empty string
|
2025-11-14 11:38:25 +08:00
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
while IFS= read -r line; do
|
|
|
|
|
if [[ -n "$line" && ! "$line" =~ ^/ ]]; then
|
2025-12-25 11:24:12 +08:00
|
|
|
log_warning "Invalid path in file list for $app_name: $line" >&2
|
2025-11-14 11:38:25 +08:00
|
|
|
echo ""
|
2025-12-25 03:27:51 +00:00
|
|
|
return 0 # Return success with empty string
|
2025-11-14 11:38:25 +08:00
|
|
|
fi
|
2026-01-17 01:53:40 +00:00
|
|
|
done <<< "$decoded"
|
2025-11-14 11:38:25 +08:00
|
|
|
|
|
|
|
|
echo "$decoded"
|
|
|
|
|
return 0
|
|
|
|
|
}
|
2025-12-31 16:23:31 +08:00
|
|
|
# Note: find_app_files() and calculate_total_size() are in lib/core/common.sh.
|
2025-09-25 20:22:51 +08:00
|
|
|
|
2025-12-31 16:23:31 +08:00
|
|
|
# Stop Launch Agents/Daemons for an app.
|
2026-01-26 20:27:46 +08:00
|
|
|
# Security: bundle_id is validated to be reverse-DNS format before use in find patterns
|
2025-12-02 10:58:40 +08:00
|
|
|
stop_launch_services() {
|
|
|
|
|
local bundle_id="$1"
|
|
|
|
|
local has_system_files="${2:-false}"
|
|
|
|
|
|
2025-12-25 11:24:12 +08:00
|
|
|
[[ -z "$bundle_id" || "$bundle_id" == "unknown" ]] && return 0
|
|
|
|
|
|
2026-01-26 20:27:46 +08:00
|
|
|
# Validate bundle_id format: must be reverse-DNS style (e.g., com.example.app)
|
|
|
|
|
# This prevents glob injection attacks if bundle_id contains special characters
|
|
|
|
|
if [[ ! "$bundle_id" =~ ^[a-zA-Z0-9][-a-zA-Z0-9]*(\.[a-zA-Z0-9][-a-zA-Z0-9]*)+$ ]]; then
|
|
|
|
|
debug_log "Invalid bundle_id format for LaunchAgent search: $bundle_id"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
2025-12-25 11:24:12 +08:00
|
|
|
if [[ -d ~/Library/LaunchAgents ]]; then
|
|
|
|
|
while IFS= read -r -d '' plist; do
|
2026-01-17 01:53:40 +00:00
|
|
|
launchctl unload "$plist" 2> /dev/null || true
|
|
|
|
|
done < <(find ~/Library/LaunchAgents -maxdepth 1 -name "${bundle_id}*.plist" -print0 2> /dev/null)
|
2025-12-25 11:24:12 +08:00
|
|
|
fi
|
2025-12-02 10:58:40 +08:00
|
|
|
|
|
|
|
|
if [[ "$has_system_files" == "true" ]]; then
|
2025-12-25 11:24:12 +08:00
|
|
|
if [[ -d /Library/LaunchAgents ]]; then
|
|
|
|
|
while IFS= read -r -d '' plist; do
|
2026-01-17 01:53:40 +00:00
|
|
|
sudo launchctl unload "$plist" 2> /dev/null || true
|
|
|
|
|
done < <(find /Library/LaunchAgents -maxdepth 1 -name "${bundle_id}*.plist" -print0 2> /dev/null)
|
2025-12-25 11:24:12 +08:00
|
|
|
fi
|
|
|
|
|
if [[ -d /Library/LaunchDaemons ]]; then
|
|
|
|
|
while IFS= read -r -d '' plist; do
|
2026-01-17 01:53:40 +00:00
|
|
|
sudo launchctl unload "$plist" 2> /dev/null || true
|
|
|
|
|
done < <(find /Library/LaunchDaemons -maxdepth 1 -name "${bundle_id}*.plist" -print0 2> /dev/null)
|
2025-12-25 11:24:12 +08:00
|
|
|
fi
|
2025-12-02 10:58:40 +08:00
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 11:39:33 +08:00
|
|
|
# Remove macOS Login Items for an app
|
|
|
|
|
remove_login_item() {
|
|
|
|
|
local app_name="$1"
|
|
|
|
|
local bundle_id="$2"
|
|
|
|
|
|
|
|
|
|
# Skip if no identifiers provided
|
|
|
|
|
[[ -z "$app_name" && -z "$bundle_id" ]] && return 0
|
|
|
|
|
|
|
|
|
|
# Strip .app suffix if present (login items don't include it)
|
|
|
|
|
local clean_name="${app_name%.app}"
|
|
|
|
|
|
|
|
|
|
# Remove from Login Items using index-based deletion (handles broken items)
|
|
|
|
|
if [[ -n "$clean_name" ]]; then
|
2026-01-15 15:13:51 +08:00
|
|
|
# Escape double quotes and backslashes for AppleScript
|
|
|
|
|
local escaped_name="${clean_name//\\/\\\\}"
|
|
|
|
|
escaped_name="${escaped_name//\"/\\\"}"
|
|
|
|
|
|
2026-01-17 01:53:40 +00:00
|
|
|
osascript <<- EOF > /dev/null 2>&1 || true
|
2026-01-15 11:39:33 +08:00
|
|
|
tell application "System Events"
|
|
|
|
|
try
|
|
|
|
|
set itemCount to count of login items
|
|
|
|
|
-- Delete in reverse order to avoid index shifting
|
|
|
|
|
repeat with i from itemCount to 1 by -1
|
|
|
|
|
try
|
|
|
|
|
set itemName to name of login item i
|
2026-01-15 15:13:51 +08:00
|
|
|
if itemName is "$escaped_name" then
|
2026-01-15 11:39:33 +08:00
|
|
|
delete login item i
|
|
|
|
|
end if
|
|
|
|
|
end try
|
|
|
|
|
end repeat
|
|
|
|
|
end try
|
|
|
|
|
end tell
|
|
|
|
|
EOF
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-31 16:23:31 +08:00
|
|
|
# Remove files (handles symlinks, optional sudo).
|
2026-01-26 20:27:46 +08:00
|
|
|
# Security: All paths pass validate_path_for_deletion() before any deletion.
|
2025-12-02 10:58:40 +08:00
|
|
|
remove_file_list() {
|
|
|
|
|
local file_list="$1"
|
|
|
|
|
local use_sudo="${2:-false}"
|
|
|
|
|
local count=0
|
|
|
|
|
|
|
|
|
|
while IFS= read -r file; do
|
|
|
|
|
[[ -n "$file" && -e "$file" ]] || continue
|
|
|
|
|
|
fix(uninstall): enhance receipt file processing safety and prevent system file deletion
CRITICAL SECURITY FIX
Enhanced the receipt file parsing in uninstall operations to prevent
accidental deletion of critical system files while maintaining deep
cleanup capabilities.
Changes:
- Tightened whitelist in find_app_receipt_files() to exclude /Users/*,
/usr/*, and /opt/* broad patterns
- Added explicit blacklist for /private/* with safe exceptions for
logs, temp files, and diagnostic data
- Integrated should_protect_path() check for additional protection
- Added file deduplication with sort -u to prevent duplicate deletions
- Removed dry-run feature from batch uninstall (unused entry point)
Path Protection:
✅ Blocked: /etc/passwd, /var/db/*, /private/etc/*, all system binaries
✅ Allowed: /Applications/*, specific /Library/* subdirs, safe /private/* paths
✅ Additional: Keychain files, system preferences via should_protect_path()
This fixes a critical security issue where parsing .bom receipt files
could result in deletion of system files like /etc/passwd and /var/db/*,
leading to system corruption and data loss.
Affects: V1.12.14 and later versions
Testing: Validated against critical system paths, all blocked correctly
2026-01-15 21:01:11 +08:00
|
|
|
if ! validate_path_for_deletion "$file"; then
|
|
|
|
|
continue
|
|
|
|
|
fi
|
|
|
|
|
|
2025-12-02 10:58:40 +08:00
|
|
|
if [[ -L "$file" ]]; then
|
2026-02-02 17:05:42 +08:00
|
|
|
safe_remove_symlink "$file" "$use_sudo" && ((++count)) || true
|
2025-12-02 10:58:40 +08:00
|
|
|
else
|
|
|
|
|
if [[ "$use_sudo" == "true" ]]; then
|
2026-01-14 08:55:41 -05:00
|
|
|
safe_sudo_remove "$file" && ((++count)) || true
|
2025-12-02 10:58:40 +08:00
|
|
|
else
|
2026-01-14 08:55:41 -05:00
|
|
|
safe_remove "$file" true && ((++count)) || true
|
2025-12-02 10:58:40 +08:00
|
|
|
fi
|
|
|
|
|
fi
|
2026-01-17 01:53:40 +00:00
|
|
|
done <<< "$file_list"
|
2025-12-02 10:58:40 +08:00
|
|
|
|
|
|
|
|
echo "$count"
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-31 16:23:31 +08:00
|
|
|
# Batch uninstall with single confirmation.
|
2025-09-25 20:22:51 +08:00
|
|
|
batch_uninstall_applications() {
|
|
|
|
|
local total_size_freed=0
|
|
|
|
|
|
2025-12-02 17:02:14 +08:00
|
|
|
# shellcheck disable=SC2154
|
2025-09-25 20:22:51 +08:00
|
|
|
if [[ ${#selected_apps[@]} -eq 0 ]]; then
|
|
|
|
|
log_warning "No applications selected for uninstallation"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
|
2026-01-22 17:45:53 +08:00
|
|
|
local old_trap_int old_trap_term
|
|
|
|
|
old_trap_int=$(trap -p INT)
|
|
|
|
|
old_trap_term=$(trap -p TERM)
|
|
|
|
|
|
2026-02-02 17:05:42 +08:00
|
|
|
_cleanup_sudo_keepalive() {
|
|
|
|
|
if [[ -n "${sudo_keepalive_pid:-}" ]]; then
|
|
|
|
|
kill "$sudo_keepalive_pid" 2> /dev/null || true
|
|
|
|
|
wait "$sudo_keepalive_pid" 2> /dev/null || true
|
|
|
|
|
sudo_keepalive_pid=""
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 17:45:53 +08:00
|
|
|
_restore_uninstall_traps() {
|
2026-02-02 17:05:42 +08:00
|
|
|
_cleanup_sudo_keepalive
|
2026-01-22 17:45:53 +08:00
|
|
|
if [[ -n "$old_trap_int" ]]; then
|
|
|
|
|
eval "$old_trap_int"
|
|
|
|
|
else
|
|
|
|
|
trap - INT
|
|
|
|
|
fi
|
|
|
|
|
if [[ -n "$old_trap_term" ]]; then
|
|
|
|
|
eval "$old_trap_term"
|
|
|
|
|
else
|
|
|
|
|
trap - TERM
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 17:05:42 +08:00
|
|
|
# Trap to clean up spinner, sudo keepalive, and uninstall mode on interrupt
|
|
|
|
|
trap 'stop_inline_spinner 2>/dev/null; _cleanup_sudo_keepalive; unset MOLE_UNINSTALL_MODE; echo ""; _restore_uninstall_traps; return 130' INT TERM
|
2026-01-22 17:45:53 +08:00
|
|
|
|
2025-12-31 16:23:31 +08:00
|
|
|
# Pre-scan: running apps, sudo needs, size.
|
2025-09-25 20:22:51 +08:00
|
|
|
local -a running_apps=()
|
2025-10-03 11:38:54 +08:00
|
|
|
local -a sudo_apps=()
|
2025-09-25 20:22:51 +08:00
|
|
|
local total_estimated_size=0
|
|
|
|
|
local -a app_details=()
|
|
|
|
|
|
2026-01-19 16:53:51 +08:00
|
|
|
# Cache current user outside loop
|
|
|
|
|
local current_user=$(whoami)
|
|
|
|
|
|
2025-12-05 14:21:18 +08:00
|
|
|
if [[ -t 1 ]]; then start_inline_spinner "Scanning files..."; fi
|
2025-09-25 20:22:51 +08:00
|
|
|
for selected_app in "${selected_apps[@]}"; do
|
2025-10-08 18:01:46 +08:00
|
|
|
[[ -z "$selected_app" ]] && continue
|
2026-01-17 01:53:40 +00:00
|
|
|
IFS='|' read -r _ app_path app_name bundle_id _ _ <<< "$selected_app"
|
2025-09-25 20:22:51 +08:00
|
|
|
|
2026-01-19 16:53:51 +08:00
|
|
|
# Check running app by bundle executable if available
|
2025-12-04 15:06:45 +08:00
|
|
|
local exec_name=""
|
2026-01-19 16:53:51 +08:00
|
|
|
local info_plist="$app_path/Contents/Info.plist"
|
|
|
|
|
if [[ -e "$info_plist" ]]; then
|
2026-01-19 08:54:45 +00:00
|
|
|
exec_name=$(defaults read "$info_plist" CFBundleExecutable 2> /dev/null || echo "")
|
2025-12-04 15:06:45 +08:00
|
|
|
fi
|
2026-01-19 08:54:45 +00:00
|
|
|
if pgrep -qx "${exec_name:-$app_name}" 2> /dev/null; then
|
2025-09-25 20:22:51 +08:00
|
|
|
running_apps+=("$app_name")
|
|
|
|
|
fi
|
|
|
|
|
|
2026-01-19 16:53:51 +08:00
|
|
|
local cask_name="" is_brew_cask="false"
|
2026-01-19 08:54:45 +00:00
|
|
|
local resolved_path=$(readlink "$app_path" 2> /dev/null || echo "")
|
2026-01-19 16:53:51 +08:00
|
|
|
if [[ "$resolved_path" == */Caskroom/* ]]; then
|
|
|
|
|
# Extract cask name using bash parameter expansion (faster than sed)
|
|
|
|
|
local tmp="${resolved_path#*/Caskroom/}"
|
|
|
|
|
cask_name="${tmp%%/*}"
|
|
|
|
|
[[ -n "$cask_name" ]] && is_brew_cask="true"
|
2026-01-20 10:26:13 +08:00
|
|
|
elif command -v get_brew_cask_name > /dev/null 2>&1; then
|
|
|
|
|
local detected_cask
|
|
|
|
|
detected_cask=$(get_brew_cask_name "$app_path" 2> /dev/null || true)
|
|
|
|
|
if [[ -n "$detected_cask" ]]; then
|
|
|
|
|
cask_name="$detected_cask"
|
|
|
|
|
is_brew_cask="true"
|
|
|
|
|
fi
|
2026-01-19 16:53:51 +08:00
|
|
|
fi
|
2026-01-13 10:44:48 +08:00
|
|
|
|
2026-01-19 16:53:51 +08:00
|
|
|
# Check if sudo is needed
|
2026-01-14 08:55:41 -05:00
|
|
|
local needs_sudo=false
|
|
|
|
|
local app_owner=$(get_file_owner "$app_path")
|
|
|
|
|
if [[ ! -w "$(dirname "$app_path")" ]] ||
|
|
|
|
|
[[ "$app_owner" == "root" ]] ||
|
|
|
|
|
[[ -n "$app_owner" && "$app_owner" != "$current_user" ]]; then
|
|
|
|
|
needs_sudo=true
|
|
|
|
|
fi
|
2025-12-02 10:58:40 +08:00
|
|
|
|
2026-01-14 08:55:41 -05:00
|
|
|
# Size estimate includes related and system files.
|
2026-02-02 17:05:42 +08:00
|
|
|
local app_size_kb=$(get_path_size_kb "$app_path" || echo "0")
|
|
|
|
|
local related_files=$(find_app_files "$bundle_id" "$app_name" || true)
|
|
|
|
|
local related_size_kb=$(calculate_total_size "$related_files" || echo "0")
|
2026-01-14 08:55:41 -05:00
|
|
|
# system_files is a newline-separated string, not an array.
|
|
|
|
|
# shellcheck disable=SC2178,SC2128
|
2026-02-02 17:05:42 +08:00
|
|
|
local system_files=$(find_app_system_files "$bundle_id" "$app_name" || true)
|
2026-01-14 08:55:41 -05:00
|
|
|
# shellcheck disable=SC2128
|
2026-02-02 17:05:42 +08:00
|
|
|
local system_size_kb=$(calculate_total_size "$system_files" || echo "0")
|
2026-01-14 08:55:41 -05:00
|
|
|
local total_kb=$((app_size_kb + related_size_kb + system_size_kb))
|
2026-02-02 17:05:42 +08:00
|
|
|
((total_estimated_size += total_kb)) || true
|
2026-01-14 08:55:41 -05:00
|
|
|
|
|
|
|
|
# shellcheck disable=SC2128
|
|
|
|
|
if [[ -n "$system_files" ]]; then
|
|
|
|
|
needs_sudo=true
|
|
|
|
|
fi
|
2025-12-25 11:24:12 +08:00
|
|
|
|
2026-01-14 08:55:41 -05:00
|
|
|
if [[ "$needs_sudo" == "true" ]]; then
|
|
|
|
|
sudo_apps+=("$app_name")
|
|
|
|
|
fi
|
2026-01-13 10:44:48 +08:00
|
|
|
|
2026-01-14 08:55:41 -05:00
|
|
|
# Check for sensitive user data once.
|
|
|
|
|
local has_sensitive_data="false"
|
2026-02-02 17:05:42 +08:00
|
|
|
if has_sensitive_data "$related_files" 2> /dev/null; then
|
2026-01-14 08:55:41 -05:00
|
|
|
has_sensitive_data="true"
|
2026-01-13 10:44:48 +08:00
|
|
|
fi
|
2026-01-14 08:55:41 -05:00
|
|
|
|
|
|
|
|
# Store details for later use (base64 keeps lists on one line).
|
|
|
|
|
local encoded_files
|
2026-02-02 17:05:42 +08:00
|
|
|
encoded_files=$(printf '%s' "$related_files" | base64 | tr -d '\n' || echo "")
|
2026-01-14 08:55:41 -05:00
|
|
|
local encoded_system_files
|
2026-02-02 17:05:42 +08:00
|
|
|
encoded_system_files=$(printf '%s' "$system_files" | base64 | tr -d '\n' || echo "")
|
2026-01-14 08:55:41 -05:00
|
|
|
app_details+=("$app_name|$app_path|$bundle_id|$total_kb|$encoded_files|$encoded_system_files|$has_sensitive_data|$needs_sudo|$is_brew_cask|$cask_name")
|
2025-09-25 20:22:51 +08:00
|
|
|
done
|
2025-12-05 14:21:18 +08:00
|
|
|
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
2025-09-25 20:22:51 +08:00
|
|
|
|
2025-10-08 18:01:46 +08:00
|
|
|
local size_display=$(bytes_to_human "$((total_estimated_size * 1024))")
|
2025-09-25 20:22:51 +08:00
|
|
|
|
2025-11-21 10:44:09 +08:00
|
|
|
echo ""
|
2025-12-02 14:45:48 +08:00
|
|
|
echo -e "${PURPLE_BOLD}Files to be removed:${NC}"
|
2025-10-11 11:40:01 +08:00
|
|
|
echo ""
|
2025-12-25 11:24:12 +08:00
|
|
|
|
2026-02-03 17:36:15 +08:00
|
|
|
# Warn if brew cask apps are present.
|
|
|
|
|
local has_brew_cask=false
|
2025-10-11 11:40:01 +08:00
|
|
|
for detail in "${app_details[@]}"; do
|
2026-02-03 17:36:15 +08:00
|
|
|
IFS='|' read -r _ _ _ _ _ _ _ _ is_brew_cask_flag _ <<< "$detail"
|
|
|
|
|
[[ "$is_brew_cask_flag" == "true" ]] && has_brew_cask=true
|
2025-12-25 11:24:12 +08:00
|
|
|
done
|
|
|
|
|
|
2026-02-03 17:36:15 +08:00
|
|
|
if [[ "$has_brew_cask" == "true" ]]; then
|
|
|
|
|
echo -e "${GRAY}${ICON_WARNING}${NC} ${YELLOW}Homebrew apps will be fully cleaned (--zap: removes configs & data)${NC}"
|
2025-12-25 11:24:12 +08:00
|
|
|
echo ""
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
for detail in "${app_details[@]}"; do
|
2026-01-17 01:53:40 +00:00
|
|
|
IFS='|' read -r app_name app_path bundle_id total_kb encoded_files encoded_system_files has_sensitive_data needs_sudo_flag is_brew_cask cask_name <<< "$detail"
|
2025-10-11 11:40:01 +08:00
|
|
|
local app_size_display=$(bytes_to_human "$((total_kb * 1024))")
|
|
|
|
|
|
2026-01-13 10:44:48 +08:00
|
|
|
local brew_tag=""
|
|
|
|
|
[[ "$is_brew_cask" == "true" ]] && brew_tag=" ${CYAN}[Brew]${NC}"
|
2026-01-26 14:36:06 +08:00
|
|
|
echo -e "${BLUE}${ICON_CONFIRM}${NC} ${app_name}${brew_tag} ${GRAY}, ${app_size_display}${NC}"
|
2026-01-13 10:44:48 +08:00
|
|
|
|
2026-01-14 08:55:41 -05:00
|
|
|
# Show detailed file list for ALL apps (brew casks leave user data behind)
|
|
|
|
|
local related_files=$(decode_file_list "$encoded_files" "$app_name")
|
|
|
|
|
local system_files=$(decode_file_list "$encoded_system_files" "$app_name")
|
|
|
|
|
|
|
|
|
|
echo -e " ${GREEN}${ICON_SUCCESS}${NC} ${app_path/$HOME/~}"
|
|
|
|
|
|
|
|
|
|
# Show related files (limit to 5).
|
|
|
|
|
local file_count=0
|
|
|
|
|
local max_files=5
|
|
|
|
|
while IFS= read -r file; do
|
|
|
|
|
if [[ -n "$file" && -e "$file" ]]; then
|
|
|
|
|
if [[ $file_count -lt $max_files ]]; then
|
|
|
|
|
echo -e " ${GREEN}${ICON_SUCCESS}${NC} ${file/$HOME/~}"
|
2025-10-11 11:40:01 +08:00
|
|
|
fi
|
2026-01-14 08:55:41 -05:00
|
|
|
((file_count++))
|
|
|
|
|
fi
|
2026-01-17 01:53:40 +00:00
|
|
|
done <<< "$related_files"
|
2026-01-14 08:55:41 -05:00
|
|
|
|
|
|
|
|
# Show system files (limit to 5).
|
|
|
|
|
local sys_file_count=0
|
|
|
|
|
while IFS= read -r file; do
|
|
|
|
|
if [[ -n "$file" && -e "$file" ]]; then
|
|
|
|
|
if [[ $sys_file_count -lt $max_files ]]; then
|
2026-01-22 20:15:13 +08:00
|
|
|
echo -e " ${BLUE}${ICON_WARNING}${NC} System: $file"
|
2025-12-02 10:58:40 +08:00
|
|
|
fi
|
2026-01-14 08:55:41 -05:00
|
|
|
((sys_file_count++))
|
2026-01-13 10:44:48 +08:00
|
|
|
fi
|
2026-01-17 01:53:40 +00:00
|
|
|
done <<< "$system_files"
|
2026-01-14 08:55:41 -05:00
|
|
|
|
|
|
|
|
local total_hidden=$((file_count > max_files ? file_count - max_files : 0))
|
|
|
|
|
((total_hidden += sys_file_count > max_files ? sys_file_count - max_files : 0))
|
|
|
|
|
if [[ $total_hidden -gt 0 ]]; then
|
|
|
|
|
echo -e " ${GRAY} ... and ${total_hidden} more files${NC}"
|
2025-10-11 11:40:01 +08:00
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
2025-12-31 16:23:31 +08:00
|
|
|
# Confirmation before requesting sudo.
|
2025-10-09 14:24:00 +08:00
|
|
|
local app_total=${#selected_apps[@]}
|
|
|
|
|
local app_text="app"
|
|
|
|
|
[[ $app_total -gt 1 ]] && app_text="apps"
|
2025-10-11 22:43:18 +08:00
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
local removal_note="Remove ${app_total} ${app_text}"
|
2026-01-26 14:36:06 +08:00
|
|
|
[[ -n "$size_display" ]] && removal_note+=", ${size_display}"
|
2025-10-09 14:24:00 +08:00
|
|
|
if [[ ${#running_apps[@]} -gt 0 ]]; then
|
2025-11-19 11:33:15 +08:00
|
|
|
removal_note+=" ${YELLOW}[Running]${NC}"
|
2025-10-09 14:24:00 +08:00
|
|
|
fi
|
2025-11-19 11:33:15 +08:00
|
|
|
echo -ne "${PURPLE}${ICON_ARROW}${NC} ${removal_note} ${GREEN}Enter${NC} confirm, ${GRAY}ESC${NC} cancel: "
|
2025-10-11 22:43:18 +08:00
|
|
|
|
2025-11-20 15:15:33 +08:00
|
|
|
drain_pending_input # Clean up any pending input before confirmation
|
2025-10-09 14:24:00 +08:00
|
|
|
IFS= read -r -s -n1 key || key=""
|
2025-11-15 13:18:57 +08:00
|
|
|
drain_pending_input # Clean up any escape sequence remnants
|
2025-10-09 14:24:00 +08:00
|
|
|
case "$key" in
|
2026-01-17 01:53:40 +00:00
|
|
|
$'\e' | q | Q)
|
|
|
|
|
echo ""
|
|
|
|
|
echo ""
|
2026-01-22 17:45:53 +08:00
|
|
|
_restore_uninstall_traps
|
2026-01-17 01:53:40 +00:00
|
|
|
return 0
|
|
|
|
|
;;
|
|
|
|
|
"" | $'\n' | $'\r' | y | Y)
|
|
|
|
|
echo "" # Move to next line
|
|
|
|
|
;;
|
|
|
|
|
*)
|
|
|
|
|
echo ""
|
|
|
|
|
echo ""
|
2026-01-22 17:45:53 +08:00
|
|
|
_restore_uninstall_traps
|
2026-01-17 01:53:40 +00:00
|
|
|
return 0
|
|
|
|
|
;;
|
2025-10-09 14:24:00 +08:00
|
|
|
esac
|
|
|
|
|
|
2026-01-20 11:53:45 +08:00
|
|
|
# Enable uninstall mode - allows deletion of data-protected apps (VPNs, dev tools, etc.)
|
|
|
|
|
# that user explicitly chose to uninstall. System-critical components remain protected.
|
|
|
|
|
export MOLE_UNINSTALL_MODE=1
|
|
|
|
|
|
2025-12-31 16:23:31 +08:00
|
|
|
# Request sudo if needed.
|
2025-10-03 11:38:54 +08:00
|
|
|
if [[ ${#sudo_apps[@]} -gt 0 ]]; then
|
2026-01-17 01:53:40 +00:00
|
|
|
if ! sudo -n true 2> /dev/null; then
|
2025-10-09 14:24:00 +08:00
|
|
|
if ! request_sudo_access "Admin required for system apps: ${sudo_apps[*]}"; then
|
2025-10-08 18:01:46 +08:00
|
|
|
echo ""
|
|
|
|
|
log_error "Admin access denied"
|
2026-01-22 17:45:53 +08:00
|
|
|
_restore_uninstall_traps
|
2025-10-08 18:01:46 +08:00
|
|
|
return 1
|
|
|
|
|
fi
|
2025-10-03 11:38:54 +08:00
|
|
|
fi
|
2025-12-31 16:23:31 +08:00
|
|
|
# Keep sudo alive during uninstall.
|
2025-11-14 11:38:25 +08:00
|
|
|
parent_pid=$$
|
2025-10-12 20:49:10 +08:00
|
|
|
(while true; do
|
2026-01-17 01:53:40 +00:00
|
|
|
if ! kill -0 "$parent_pid" 2> /dev/null; then
|
2025-11-14 11:38:25 +08:00
|
|
|
exit 0
|
|
|
|
|
fi
|
2025-10-12 20:49:10 +08:00
|
|
|
sudo -n true
|
|
|
|
|
sleep 60
|
2026-01-17 01:53:40 +00:00
|
|
|
done 2> /dev/null) &
|
2025-10-11 22:43:18 +08:00
|
|
|
sudo_keepalive_pid=$!
|
2025-10-03 11:38:54 +08:00
|
|
|
fi
|
|
|
|
|
|
2026-01-13 10:44:48 +08:00
|
|
|
# Perform uninstallations with per-app progress feedback
|
2025-10-08 18:01:46 +08:00
|
|
|
local success_count=0 failed_count=0
|
2026-01-15 21:02:13 +08:00
|
|
|
local brew_apps_removed=0 # Track successful brew uninstalls for autoremove tip
|
2025-10-08 18:01:46 +08:00
|
|
|
local -a failed_items=()
|
2025-10-09 14:24:00 +08:00
|
|
|
local -a success_items=()
|
2026-01-13 10:44:48 +08:00
|
|
|
local current_index=0
|
2025-09-25 20:22:51 +08:00
|
|
|
for detail in "${app_details[@]}"; do
|
2026-01-13 10:44:48 +08:00
|
|
|
((current_index++))
|
2026-01-17 01:53:40 +00:00
|
|
|
IFS='|' read -r app_name app_path bundle_id total_kb encoded_files encoded_system_files has_sensitive_data needs_sudo is_brew_cask cask_name <<< "$detail"
|
2025-11-14 11:38:25 +08:00
|
|
|
local related_files=$(decode_file_list "$encoded_files" "$app_name")
|
2025-12-02 10:58:40 +08:00
|
|
|
local system_files=$(decode_file_list "$encoded_system_files" "$app_name")
|
2025-10-08 18:01:46 +08:00
|
|
|
local reason=""
|
2026-02-03 17:36:15 +08:00
|
|
|
local suggestion=""
|
2025-12-25 11:24:12 +08:00
|
|
|
|
2026-01-13 10:44:48 +08:00
|
|
|
# Show progress for current app
|
|
|
|
|
local brew_tag=""
|
|
|
|
|
[[ "$is_brew_cask" == "true" ]] && brew_tag=" ${CYAN}[Brew]${NC}"
|
|
|
|
|
if [[ -t 1 ]]; then
|
|
|
|
|
if [[ ${#app_details[@]} -gt 1 ]]; then
|
|
|
|
|
start_inline_spinner "[$current_index/${#app_details[@]}] Uninstalling ${app_name}${brew_tag}..."
|
|
|
|
|
else
|
|
|
|
|
start_inline_spinner "Uninstalling ${app_name}${brew_tag}..."
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
2025-12-31 16:23:31 +08:00
|
|
|
# Stop Launch Agents/Daemons before removal.
|
2025-12-02 10:58:40 +08:00
|
|
|
local has_system_files="false"
|
|
|
|
|
[[ -n "$system_files" ]] && has_system_files="true"
|
2026-01-16 12:54:21 +08:00
|
|
|
|
2025-12-02 10:58:40 +08:00
|
|
|
stop_launch_services "$bundle_id" "$has_system_files"
|
|
|
|
|
|
2026-01-15 11:39:33 +08:00
|
|
|
# Remove from Login Items
|
|
|
|
|
remove_login_item "$app_name" "$bundle_id"
|
|
|
|
|
|
2025-10-11 11:40:01 +08:00
|
|
|
if ! force_kill_app "$app_name" "$app_path"; then
|
2025-10-08 18:01:46 +08:00
|
|
|
reason="still running"
|
2025-10-03 11:38:54 +08:00
|
|
|
fi
|
2025-12-02 10:58:40 +08:00
|
|
|
|
2025-12-31 16:23:31 +08:00
|
|
|
# Remove the application only if not running.
|
2026-01-19 16:53:51 +08:00
|
|
|
# Stop spinner before any removal attempt (avoids mixed output on errors)
|
|
|
|
|
[[ -t 1 ]] && stop_inline_spinner
|
|
|
|
|
|
2026-01-14 08:55:41 -05:00
|
|
|
local used_brew_successfully=false
|
2025-10-08 18:01:46 +08:00
|
|
|
if [[ -z "$reason" ]]; then
|
2026-01-13 10:44:48 +08:00
|
|
|
if [[ "$is_brew_cask" == "true" && -n "$cask_name" ]]; then
|
2026-01-14 08:55:41 -05:00
|
|
|
# Use brew_uninstall_cask helper (handles env vars, timeout, verification)
|
|
|
|
|
if brew_uninstall_cask "$cask_name" "$app_path"; then
|
|
|
|
|
used_brew_successfully=true
|
|
|
|
|
else
|
2026-01-13 10:44:48 +08:00
|
|
|
# Fallback to manual removal if brew fails
|
|
|
|
|
if [[ "$needs_sudo" == true ]]; then
|
2026-01-26 15:22:16 +08:00
|
|
|
if ! safe_sudo_remove "$app_path"; then
|
|
|
|
|
reason="brew failed, manual removal failed"
|
|
|
|
|
fi
|
2026-01-13 10:44:48 +08:00
|
|
|
else
|
2026-01-26 15:22:16 +08:00
|
|
|
if ! safe_remove "$app_path" true; then
|
|
|
|
|
reason="brew failed, manual removal failed"
|
|
|
|
|
fi
|
2026-01-13 10:44:48 +08:00
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
elif [[ "$needs_sudo" == true ]]; then
|
2026-02-02 17:05:42 +08:00
|
|
|
if [[ -L "$app_path" ]]; then
|
|
|
|
|
local link_target
|
|
|
|
|
link_target=$(readlink "$app_path" 2> /dev/null)
|
|
|
|
|
if [[ -n "$link_target" ]]; then
|
|
|
|
|
local resolved_target="$link_target"
|
|
|
|
|
if [[ "$link_target" != /* ]]; then
|
|
|
|
|
local link_dir
|
|
|
|
|
link_dir=$(dirname "$app_path")
|
|
|
|
|
resolved_target=$(cd "$link_dir" 2> /dev/null && cd "$(dirname "$link_target")" 2> /dev/null && pwd)/$(basename "$link_target") 2> /dev/null || echo ""
|
|
|
|
|
fi
|
|
|
|
|
case "$resolved_target" in
|
|
|
|
|
/System/* | /usr/bin/* | /usr/lib/* | /bin/* | /sbin/* | /private/etc/*)
|
|
|
|
|
reason="protected system symlink, cannot remove"
|
|
|
|
|
;;
|
|
|
|
|
*)
|
|
|
|
|
if ! safe_remove_symlink "$app_path" "true"; then
|
|
|
|
|
reason="failed to remove symlink"
|
|
|
|
|
fi
|
|
|
|
|
;;
|
|
|
|
|
esac
|
2025-12-25 11:24:12 +08:00
|
|
|
else
|
2026-02-02 17:05:42 +08:00
|
|
|
if ! safe_remove_symlink "$app_path" "true"; then
|
|
|
|
|
reason="failed to remove symlink"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
local ret=0
|
|
|
|
|
safe_sudo_remove "$app_path" || ret=$?
|
|
|
|
|
if [[ $ret -ne 0 ]]; then
|
|
|
|
|
local diagnosis
|
|
|
|
|
diagnosis=$(diagnose_removal_failure "$ret" "$app_name")
|
|
|
|
|
IFS='|' read -r reason suggestion <<< "$diagnosis"
|
2025-12-25 11:24:12 +08:00
|
|
|
fi
|
|
|
|
|
fi
|
2025-10-03 11:38:54 +08:00
|
|
|
else
|
2026-01-26 15:22:16 +08:00
|
|
|
if ! safe_remove "$app_path" true; then
|
|
|
|
|
if [[ ! -w "$(dirname "$app_path")" ]]; then
|
|
|
|
|
reason="parent directory not writable"
|
|
|
|
|
else
|
|
|
|
|
reason="remove failed, check permissions"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
2025-10-03 11:38:54 +08:00
|
|
|
fi
|
|
|
|
|
fi
|
2025-12-02 10:58:40 +08:00
|
|
|
|
2025-12-31 16:23:31 +08:00
|
|
|
# Remove related files if app removal succeeded.
|
2025-10-08 18:01:46 +08:00
|
|
|
if [[ -z "$reason" ]]; then
|
2026-01-17 01:53:40 +00:00
|
|
|
remove_file_list "$related_files" "false" > /dev/null
|
2026-01-14 08:55:41 -05:00
|
|
|
|
|
|
|
|
# If brew successfully uninstalled the cask, avoid deleting
|
|
|
|
|
# system-level files Mole discovered. Brew manages its own
|
|
|
|
|
# receipts/symlinks and we don't want to fight it.
|
|
|
|
|
if [[ "$used_brew_successfully" != "true" ]]; then
|
2026-01-17 01:53:40 +00:00
|
|
|
remove_file_list "$system_files" "true" > /dev/null
|
2026-01-14 08:55:41 -05:00
|
|
|
fi
|
2025-12-02 10:58:40 +08:00
|
|
|
|
2025-12-31 16:23:31 +08:00
|
|
|
# Clean up macOS defaults (preference domains).
|
2025-12-25 11:24:12 +08:00
|
|
|
if [[ -n "$bundle_id" && "$bundle_id" != "unknown" ]]; then
|
2026-01-17 01:53:40 +00:00
|
|
|
if defaults read "$bundle_id" &> /dev/null; then
|
|
|
|
|
defaults delete "$bundle_id" 2> /dev/null || true
|
2025-12-25 11:24:12 +08:00
|
|
|
fi
|
|
|
|
|
|
2025-12-31 16:23:31 +08:00
|
|
|
# ByHost preferences (machine-specific).
|
2026-01-26 15:43:11 +08:00
|
|
|
if [[ -d "$HOME/Library/Preferences/ByHost" ]]; then
|
|
|
|
|
if [[ "$bundle_id" =~ ^[A-Za-z0-9._-]+$ ]]; then
|
|
|
|
|
while IFS= read -r -d '' plist_file; do
|
|
|
|
|
safe_remove "$plist_file" true > /dev/null || true
|
|
|
|
|
done < <(command find "$HOME/Library/Preferences/ByHost" -maxdepth 1 -type f -name "${bundle_id}.*.plist" -print0 2> /dev/null || true)
|
|
|
|
|
else
|
|
|
|
|
debug_log "Skipping ByHost cleanup, invalid bundle id: $bundle_id"
|
|
|
|
|
fi
|
2025-12-25 11:24:12 +08:00
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
2026-01-19 16:53:51 +08:00
|
|
|
# Show success
|
2026-01-13 10:44:48 +08:00
|
|
|
if [[ -t 1 ]]; then
|
|
|
|
|
if [[ ${#app_details[@]} -gt 1 ]]; then
|
2026-01-19 16:53:51 +08:00
|
|
|
echo -e "${GREEN}✓${NC} [$current_index/${#app_details[@]}] ${app_name}"
|
2026-01-13 10:44:48 +08:00
|
|
|
else
|
2026-01-19 16:53:51 +08:00
|
|
|
echo -e "${GREEN}✓${NC} ${app_name}"
|
2026-01-13 10:44:48 +08:00
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
2025-09-25 20:22:51 +08:00
|
|
|
((total_size_freed += total_kb))
|
|
|
|
|
((success_count++))
|
2026-01-14 08:55:41 -05:00
|
|
|
[[ "$used_brew_successfully" == "true" ]] && ((brew_apps_removed++))
|
2025-09-25 20:22:51 +08:00
|
|
|
((files_cleaned++))
|
|
|
|
|
((total_items++))
|
2026-02-03 17:36:15 +08:00
|
|
|
success_items+=("$app_path")
|
2025-09-25 20:22:51 +08:00
|
|
|
else
|
2026-01-13 10:44:48 +08:00
|
|
|
if [[ -t 1 ]]; then
|
|
|
|
|
if [[ ${#app_details[@]} -gt 1 ]]; then
|
2026-01-26 14:36:06 +08:00
|
|
|
echo -e "${ICON_ERROR} [$current_index/${#app_details[@]}] ${app_name} ${GRAY}, $reason${NC}"
|
2026-01-13 10:44:48 +08:00
|
|
|
else
|
2026-01-19 16:53:51 +08:00
|
|
|
echo -e "${ICON_ERROR} ${app_name} failed: $reason"
|
2026-01-13 10:44:48 +08:00
|
|
|
fi
|
2026-02-02 17:05:42 +08:00
|
|
|
if [[ -n "${suggestion:-}" ]]; then
|
|
|
|
|
echo -e "${GRAY} → ${suggestion}${NC}"
|
|
|
|
|
fi
|
2026-01-13 10:44:48 +08:00
|
|
|
fi
|
|
|
|
|
|
2025-09-25 20:22:51 +08:00
|
|
|
((failed_count++))
|
2026-02-02 17:05:42 +08:00
|
|
|
failed_items+=("$app_name:$reason:${suggestion:-}")
|
2025-09-25 20:22:51 +08:00
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
|
2025-10-08 18:01:46 +08:00
|
|
|
# Summary
|
2025-10-11 15:02:15 +08:00
|
|
|
local freed_display
|
|
|
|
|
freed_display=$(bytes_to_human "$((total_size_freed * 1024))")
|
|
|
|
|
|
|
|
|
|
local summary_status="success"
|
|
|
|
|
local -a summary_details=()
|
|
|
|
|
|
2025-10-09 14:24:00 +08:00
|
|
|
if [[ $success_count -gt 0 ]]; then
|
2025-10-11 22:43:18 +08:00
|
|
|
local success_text="app"
|
|
|
|
|
[[ $success_count -gt 1 ]] && success_text="apps"
|
|
|
|
|
local success_line="Removed ${success_count} ${success_text}"
|
|
|
|
|
if [[ -n "$freed_display" ]]; then
|
|
|
|
|
success_line+=", freed ${GREEN}${freed_display}${NC}"
|
|
|
|
|
fi
|
2025-10-12 15:43:45 +08:00
|
|
|
|
2025-12-31 16:23:31 +08:00
|
|
|
# Format app list with max 3 per line.
|
2026-02-03 17:36:15 +08:00
|
|
|
if [[ ${#success_items[@]} -gt 0 ]]; then
|
2025-10-12 15:43:45 +08:00
|
|
|
local idx=0
|
|
|
|
|
local is_first_line=true
|
|
|
|
|
local current_line=""
|
|
|
|
|
|
2026-02-03 17:36:15 +08:00
|
|
|
for success_path in "${success_items[@]}"; do
|
|
|
|
|
local display_name
|
|
|
|
|
display_name=$(basename "$success_path" .app)
|
|
|
|
|
local display_item="${GREEN}${display_name}${NC}"
|
2025-10-12 15:43:45 +08:00
|
|
|
|
2025-10-12 20:49:10 +08:00
|
|
|
if ((idx % 3 == 0)); then
|
2025-10-12 15:43:45 +08:00
|
|
|
if [[ -n "$current_line" ]]; then
|
|
|
|
|
summary_details+=("$current_line")
|
|
|
|
|
fi
|
|
|
|
|
if [[ "$is_first_line" == true ]]; then
|
|
|
|
|
current_line="${success_line}: $display_item"
|
|
|
|
|
is_first_line=false
|
|
|
|
|
else
|
|
|
|
|
current_line="$display_item"
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
current_line="$current_line, $display_item"
|
|
|
|
|
fi
|
|
|
|
|
((idx++))
|
2025-10-11 22:43:18 +08:00
|
|
|
done
|
2025-10-12 15:43:45 +08:00
|
|
|
if [[ -n "$current_line" ]]; then
|
|
|
|
|
summary_details+=("$current_line")
|
2025-10-11 22:43:18 +08:00
|
|
|
fi
|
2025-10-12 15:43:45 +08:00
|
|
|
else
|
|
|
|
|
summary_details+=("$success_line")
|
2025-10-11 22:43:18 +08:00
|
|
|
fi
|
2025-10-09 14:24:00 +08:00
|
|
|
fi
|
2025-10-11 15:02:15 +08:00
|
|
|
|
2025-09-25 20:22:51 +08:00
|
|
|
if [[ $failed_count -gt 0 ]]; then
|
2025-10-11 15:02:15 +08:00
|
|
|
summary_status="warn"
|
|
|
|
|
|
2025-10-09 14:24:00 +08:00
|
|
|
local failed_names=()
|
|
|
|
|
for item in "${failed_items[@]}"; do
|
|
|
|
|
local name=${item%%:*}
|
|
|
|
|
failed_names+=("$name")
|
|
|
|
|
done
|
|
|
|
|
local failed_list="${failed_names[*]}"
|
|
|
|
|
|
2025-10-11 15:02:15 +08:00
|
|
|
local reason_summary="could not be removed"
|
2026-02-02 17:05:42 +08:00
|
|
|
local suggestion_text=""
|
2025-10-08 18:01:46 +08:00
|
|
|
if [[ $failed_count -eq 1 ]]; then
|
2026-02-02 17:05:42 +08:00
|
|
|
# Extract reason and suggestion from format: app:reason:suggestion
|
|
|
|
|
local item="${failed_items[0]}"
|
|
|
|
|
local without_app="${item#*:}"
|
|
|
|
|
local first_reason="${without_app%%:*}"
|
|
|
|
|
local first_suggestion="${without_app#*:}"
|
|
|
|
|
|
|
|
|
|
# If suggestion is same as reason, there was no suggestion part
|
|
|
|
|
# Also check if suggestion is empty
|
|
|
|
|
if [[ "$first_suggestion" != "$first_reason" && -n "$first_suggestion" ]]; then
|
|
|
|
|
suggestion_text="${GRAY} → ${first_suggestion}${NC}"
|
|
|
|
|
fi
|
|
|
|
|
|
2025-10-09 14:24:00 +08:00
|
|
|
case "$first_reason" in
|
2026-01-17 01:53:40 +00:00
|
|
|
still*running*) reason_summary="is still running" ;;
|
|
|
|
|
remove*failed*) reason_summary="could not be removed" ;;
|
|
|
|
|
permission*denied*) reason_summary="permission denied" ;;
|
2026-01-26 14:36:06 +08:00
|
|
|
owned*by*) reason_summary="$first_reason, try with sudo" ;;
|
2026-01-17 01:53:40 +00:00
|
|
|
*) reason_summary="$first_reason" ;;
|
2025-10-09 14:24:00 +08:00
|
|
|
esac
|
2025-10-08 18:01:46 +08:00
|
|
|
fi
|
2025-10-11 15:02:15 +08:00
|
|
|
summary_details+=("Failed: ${RED}${failed_list}${NC} ${reason_summary}")
|
2026-02-02 17:05:42 +08:00
|
|
|
if [[ -n "$suggestion_text" ]]; then
|
|
|
|
|
summary_details+=("$suggestion_text")
|
|
|
|
|
fi
|
2025-10-11 15:02:15 +08:00
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [[ $success_count -eq 0 && $failed_count -eq 0 ]]; then
|
|
|
|
|
summary_status="info"
|
|
|
|
|
summary_details+=("No applications were uninstalled.")
|
2025-09-25 20:22:51 +08:00
|
|
|
fi
|
2025-10-11 15:02:15 +08:00
|
|
|
|
2025-12-17 11:56:39 +08:00
|
|
|
local title="Uninstall complete"
|
|
|
|
|
if [[ "$summary_status" == "warn" ]]; then
|
|
|
|
|
title="Uninstall incomplete"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-01-13 10:44:48 +08:00
|
|
|
echo ""
|
2025-12-17 11:56:39 +08:00
|
|
|
print_summary_block "$title" "${summary_details[@]}"
|
2025-10-11 22:43:18 +08:00
|
|
|
printf '\n'
|
2025-09-25 20:22:51 +08:00
|
|
|
|
2026-01-15 14:31:36 +08:00
|
|
|
# Auto-run brew autoremove if Homebrew casks were uninstalled
|
2026-01-14 08:55:41 -05:00
|
|
|
if [[ $brew_apps_removed -gt 0 ]]; then
|
2026-01-16 10:26:46 +08:00
|
|
|
# Show spinner while checking for orphaned dependencies
|
|
|
|
|
if [[ -t 1 ]]; then
|
|
|
|
|
start_inline_spinner "Checking brew dependencies..."
|
|
|
|
|
fi
|
|
|
|
|
|
2026-01-15 14:31:36 +08:00
|
|
|
local autoremove_output removed_count
|
2026-01-17 01:53:40 +00:00
|
|
|
autoremove_output=$(HOMEBREW_NO_ENV_HINTS=1 brew autoremove 2> /dev/null) || true
|
2026-01-15 14:31:36 +08:00
|
|
|
removed_count=$(printf '%s\n' "$autoremove_output" | grep -c "^Uninstalling" || true)
|
|
|
|
|
removed_count=${removed_count:-0}
|
2026-01-16 10:26:46 +08:00
|
|
|
|
|
|
|
|
if [[ -t 1 ]]; then
|
|
|
|
|
stop_inline_spinner
|
|
|
|
|
fi
|
|
|
|
|
|
2026-01-15 14:31:36 +08:00
|
|
|
if [[ $removed_count -gt 0 ]]; then
|
|
|
|
|
echo -e "${GREEN}${ICON_SUCCESS}${NC} Cleaned $removed_count orphaned brew dependencies"
|
|
|
|
|
echo ""
|
|
|
|
|
fi
|
2026-01-14 08:55:41 -05:00
|
|
|
fi
|
|
|
|
|
|
2025-12-31 16:23:31 +08:00
|
|
|
# Clean up Dock entries for uninstalled apps.
|
2026-02-03 17:36:15 +08:00
|
|
|
if [[ $success_count -gt 0 && ${#success_items[@]} -gt 0 ]]; then
|
|
|
|
|
remove_apps_from_dock "${success_items[@]}" 2> /dev/null || true
|
2025-10-11 11:40:01 +08:00
|
|
|
fi
|
|
|
|
|
|
2026-02-02 17:05:42 +08:00
|
|
|
_cleanup_sudo_keepalive
|
2025-10-03 11:38:54 +08:00
|
|
|
|
2026-01-20 11:53:45 +08:00
|
|
|
# Disable uninstall mode
|
|
|
|
|
unset MOLE_UNINSTALL_MODE
|
|
|
|
|
|
2026-01-22 17:45:53 +08:00
|
|
|
_restore_uninstall_traps
|
|
|
|
|
unset -f _restore_uninstall_traps
|
|
|
|
|
|
2025-09-25 20:22:51 +08:00
|
|
|
((total_size_cleaned += total_size_freed))
|
2025-10-08 18:01:46 +08:00
|
|
|
unset failed_items
|
2025-10-03 13:56:53 +08:00
|
|
|
}
|