1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-08 23:09:20 +00:00

fix(uninstall): Harden brew uninstall

This commit is contained in:
Jack Phallen
2026-01-14 08:55:41 -05:00
parent 7089ce69d1
commit d884a268e8
3 changed files with 383 additions and 155 deletions

View File

@@ -3,9 +3,12 @@
set -euo pipefail
# Ensure common.sh is loaded.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
[[ -z "${MOLE_COMMON_LOADED:-}" ]] && source "$SCRIPT_DIR/lib/core/common.sh"
# 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"
# Batch uninstall with a single confirmation.
# User data detection patterns (prompt user to backup if found).
@@ -99,15 +102,15 @@ remove_file_list() {
if [[ -L "$file" ]]; then
if [[ "$use_sudo" == "true" ]]; then
sudo rm "$file" 2> /dev/null && ((count++)) || true
sudo rm "$file" 2> /dev/null && ((++count)) || true
else
rm "$file" 2> /dev/null && ((count++)) || true
rm "$file" 2> /dev/null && ((++count)) || true
fi
else
if [[ "$use_sudo" == "true" ]]; then
safe_sudo_remove "$file" && ((count++)) || true
safe_sudo_remove "$file" && ((++count)) || true
else
safe_remove "$file" true && ((count++)) || true
safe_remove "$file" true && ((++count)) || true
fi
fi
done <<< "$file_list"
@@ -146,72 +149,57 @@ batch_uninstall_applications() {
running_apps+=("$app_name")
fi
# Check if it's a Homebrew cask
# Check if it's a Homebrew cask (deterministic: resolved path in Caskroom)
local cask_name=""
cask_name=$(get_brew_cask_name "$app_path" || echo "")
local is_brew_cask="false"
[[ -n "$cask_name" ]] && is_brew_cask="true"
# For Homebrew casks, skip detailed file scanning since brew handles it
if [[ "$is_brew_cask" == "true" ]]; then
local app_size_kb=$(get_path_size_kb "$app_path")
local total_kb=$app_size_kb
((total_estimated_size += total_kb))
# Homebrew may need sudo for system-wide installations
local needs_sudo=false
if [[ "$app_path" == "/Applications/"* ]]; then
needs_sudo=true
sudo_apps+=("$app_name")
fi
# Store minimal details for Homebrew apps
app_details+=("$app_name|$app_path|$bundle_id|$total_kb|||false|$needs_sudo|$is_brew_cask|$cask_name")
else
# For non-Homebrew apps, do full file scanning
local needs_sudo=false
local app_owner=$(get_file_owner "$app_path")
local current_user=$(whoami)
if [[ ! -w "$(dirname "$app_path")" ]] ||
[[ "$app_owner" == "root" ]] ||
[[ -n "$app_owner" && "$app_owner" != "$current_user" ]]; then
needs_sudo=true
fi
# Size estimate includes related and system files.
local app_size_kb=$(get_path_size_kb "$app_path")
local related_files=$(find_app_files "$bundle_id" "$app_name")
local related_size_kb=$(calculate_total_size "$related_files")
# system_files is a newline-separated string, not an array.
# shellcheck disable=SC2178,SC2128
local system_files=$(find_app_system_files "$bundle_id" "$app_name")
# shellcheck disable=SC2128
local system_size_kb=$(calculate_total_size "$system_files")
local total_kb=$((app_size_kb + related_size_kb + system_size_kb))
((total_estimated_size += total_kb))
# shellcheck disable=SC2128
if [[ -n "$system_files" ]]; then
needs_sudo=true
fi
if [[ "$needs_sudo" == "true" ]]; then
sudo_apps+=("$app_name")
fi
# Check for sensitive user data once.
local has_sensitive_data="false"
if [[ -n "$related_files" ]] && echo "$related_files" | grep -qE "$SENSITIVE_DATA_REGEX"; then
has_sensitive_data="true"
fi
# Store details for later use (base64 keeps lists on one line).
local encoded_files
encoded_files=$(printf '%s' "$related_files" | base64 | tr -d '\n')
local encoded_system_files
encoded_system_files=$(printf '%s' "$system_files" | base64 | tr -d '\n')
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")
# Full file scanning for ALL apps (including Homebrew casks)
# brew uninstall --cask does NOT remove user data (caches, prefs, app support)
# Mole's value is cleaning those up, so we must scan for them
local needs_sudo=false
local app_owner=$(get_file_owner "$app_path")
local current_user=$(whoami)
if [[ ! -w "$(dirname "$app_path")" ]] ||
[[ "$app_owner" == "root" ]] ||
[[ -n "$app_owner" && "$app_owner" != "$current_user" ]]; then
needs_sudo=true
fi
# Size estimate includes related and system files.
local app_size_kb=$(get_path_size_kb "$app_path")
local related_files=$(find_app_files "$bundle_id" "$app_name")
local related_size_kb=$(calculate_total_size "$related_files")
# system_files is a newline-separated string, not an array.
# shellcheck disable=SC2178,SC2128
local system_files=$(find_app_system_files "$bundle_id" "$app_name")
# shellcheck disable=SC2128
local system_size_kb=$(calculate_total_size "$system_files")
local total_kb=$((app_size_kb + related_size_kb + system_size_kb))
((total_estimated_size += total_kb))
# shellcheck disable=SC2128
if [[ -n "$system_files" ]]; then
needs_sudo=true
fi
if [[ "$needs_sudo" == "true" ]]; then
sudo_apps+=("$app_name")
fi
# Check for sensitive user data once.
local has_sensitive_data="false"
if [[ -n "$related_files" ]] && echo "$related_files" | grep -qE "$SENSITIVE_DATA_REGEX"; then
has_sensitive_data="true"
fi
# Store details for later use (base64 keeps lists on one line).
local encoded_files
encoded_files=$(printf '%s' "$related_files" | base64 | tr -d '\n')
local encoded_system_files
encoded_system_files=$(printf '%s' "$system_files" | base64 | tr -d '\n')
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")
done
if [[ -t 1 ]]; then stop_inline_spinner; fi
@@ -244,42 +232,39 @@ batch_uninstall_applications() {
[[ "$is_brew_cask" == "true" ]] && brew_tag=" ${CYAN}[Brew]${NC}"
echo -e "${BLUE}${ICON_CONFIRM}${NC} ${app_name}${brew_tag} ${GRAY}(${app_size_display})${NC}"
# For Homebrew apps, [Brew] tag is enough indication
# For non-Homebrew apps, show detailed file list
if [[ "$is_brew_cask" != "true" ]]; then
local related_files=$(decode_file_list "$encoded_files" "$app_name")
local system_files=$(decode_file_list "$encoded_system_files" "$app_name")
# 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/~}"
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/~}"
fi
((file_count++))
# 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/~}"
fi
done <<< "$related_files"
# 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
echo -e " ${BLUE}${ICON_SOLID}${NC} System: $file"
fi
((sys_file_count++))
fi
done <<< "$system_files"
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}"
((file_count++))
fi
done <<< "$related_files"
# 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
echo -e " ${BLUE}${ICON_SOLID}${NC} System: $file"
fi
((sys_file_count++))
fi
done <<< "$system_files"
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}"
fi
done
@@ -338,6 +323,7 @@ batch_uninstall_applications() {
# Perform uninstallations with per-app progress feedback
local success_count=0 failed_count=0
local brew_apps_removed=0 # Track successful brew uninstalls for autoremove tip
local -a failed_items=()
local -a success_items=()
local current_index=0
@@ -369,11 +355,13 @@ batch_uninstall_applications() {
fi
# Remove the application only if not running.
local used_brew_successfully=false
if [[ -z "$reason" ]]; then
if [[ "$is_brew_cask" == "true" && -n "$cask_name" ]]; then
# Use brew uninstall --cask with progress indicator
local brew_output_file=$(mktemp)
if ! run_with_timeout 120 brew uninstall --cask "$cask_name" > "$brew_output_file" 2>&1; then
# Use brew_uninstall_cask helper (handles env vars, timeout, verification)
if brew_uninstall_cask "$cask_name" "$app_path"; then
used_brew_successfully=true
else
# Fallback to manual removal if brew fails
if [[ "$needs_sudo" == true ]]; then
safe_sudo_remove "$app_path" || reason="remove failed"
@@ -381,7 +369,6 @@ batch_uninstall_applications() {
safe_remove "$app_path" true || reason="remove failed"
fi
fi
rm -f "$brew_output_file"
elif [[ "$needs_sudo" == true ]]; then
if ! safe_sudo_remove "$app_path"; then
local app_owner=$(get_file_owner "$app_path")
@@ -400,7 +387,13 @@ batch_uninstall_applications() {
# Remove related files if app removal succeeded.
if [[ -z "$reason" ]]; then
remove_file_list "$related_files" "false" > /dev/null
remove_file_list "$system_files" "true" > /dev/null
# 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
remove_file_list "$system_files" "true" > /dev/null
fi
# Clean up macOS defaults (preference domains).
if [[ -n "$bundle_id" && "$bundle_id" != "unknown" ]]; then
@@ -426,6 +419,7 @@ batch_uninstall_applications() {
((total_size_freed += total_kb))
((success_count++))
[[ "$used_brew_successfully" == "true" ]] && ((brew_apps_removed++))
((files_cleaned++))
((total_items++))
success_items+=("$app_name")
@@ -531,6 +525,12 @@ batch_uninstall_applications() {
print_summary_block "$title" "${summary_details[@]}"
printf '\n'
# Suggest brew autoremove if Homebrew casks were successfully uninstalled
if [[ $brew_apps_removed -gt 0 ]]; then
echo -e " ${GRAY}Tip: Run ${NC}brew autoremove${GRAY} to clean up orphaned dependencies${NC}"
echo ""
fi
# Clean up Dock entries for uninstalled apps.
if [[ $success_count -gt 0 ]]; then
local -a removed_paths=()