diff --git a/lib/clean/brew.sh b/lib/clean/brew.sh index 631f102..dff204b 100644 --- a/lib/clean/brew.sh +++ b/lib/clean/brew.sh @@ -1,101 +1,4 @@ #!/bin/bash -# Homebrew Cleanup Module - -set -euo pipefail - -# Clean orphaned cask records (apps manually deleted but cask record remains) -# Uses 2-day cache to avoid expensive brew info calls -# Cache format: cask_name|AppName.app -clean_orphaned_casks() { - command -v brew > /dev/null 2>&1 || return 0 - - if [[ -t 1 ]]; then - MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning orphaned casks..." - fi - - local cache_dir="$HOME/.cache/mole" - local cask_cache="$cache_dir/cask_apps.cache" - local use_cache=false - - # Check if cache is valid (less than 2 days old) - if [[ -f "$cask_cache" ]]; then - local cache_age=$(($(date +%s) - $(get_file_mtime "$cask_cache"))) - if [[ $cache_age -lt 172800 ]]; then - use_cache=true - fi - fi - - local orphaned_casks=() - if [[ "$use_cache" == "true" ]]; then - # Use cached cask → app mapping to avoid expensive brew info calls - while IFS='|' read -r cask app_name; do - [[ ! -e "/Applications/$app_name" ]] && orphaned_casks+=("$cask") - done < "$cask_cache" - else - # Rebuild cache: query all installed casks and extract app names - mkdir -p "$cache_dir" - # Remove stale cache if it exists but has permission issues - rm -f "$cask_cache" 2> /dev/null || true - true > "$cask_cache" - - while IFS= read -r cask; do - # Get app path from cask info with timeout protection (expensive call, hence caching) - local cask_info - cask_info=$(run_with_timeout 10 brew info --cask "$cask" 2> /dev/null || true) - - # SAFETY: Skip if cask contains non-App artifacts (Screen Savers, Plugins, etc.) - # This prevents accidental deletion of casks that don't primarily install to /Applications - if echo "$cask_info" | grep -qE '\((Screen Saver|Preference Pane|Audio Unit|VST|VST3|Component|QuickLook|Spotlight|Artifact)\)'; then - continue - fi - - # Extract app name from "AppName.app (App)" format in cask info output - local app_name - app_name=$(echo "$cask_info" | grep -E '\.app \(App\)' | head -1 | sed -E 's/^[[:space:]]*//' | sed -E 's/ \(App\).*//' || true) - - # Skip if no app artifact (might be a utility package like fonts or just drivers) - [[ -z "$app_name" ]] && continue - - # Save to cache for future runs - echo "$cask|$app_name" >> "$cask_cache" - - # Check if app exists into common locations - # We must check both /Applications and ~/Applications - if [[ ! -e "/Applications/$app_name" ]] && [[ ! -e "$HOME/Applications/$app_name" ]]; then - orphaned_casks+=("$cask") - fi - done < <(brew list --cask 2> /dev/null || true) - fi - - # Remove orphaned casks if found and sudo session is still valid - if [[ ${#orphaned_casks[@]} -gt 0 ]]; then - # Check if sudo session is still valid (without prompting) - if sudo -n true 2> /dev/null; then - if [[ -t 1 ]]; then - stop_inline_spinner - echo -e " ${BLUE}${ICON_ARROW}${NC} Removing orphaned Homebrew casks (may require password for certain apps)" - MOLE_SPINNER_PREFIX=" " start_inline_spinner "Cleaning..." - fi - - local removed_casks=0 - for cask in "${orphaned_casks[@]}"; do - if brew uninstall --cask "$cask" --force > /dev/null 2>&1; then - ((removed_casks++)) - fi - done - - if [[ -t 1 ]]; then stop_inline_spinner; fi - - [[ $removed_casks -gt 0 ]] && echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed $removed_casks orphaned cask(s)" - else - # Sudo session expired - inform user to run brew manually - if [[ -t 1 ]]; then stop_inline_spinner; fi - echo -e " ${YELLOW}${ICON_WARNING}${NC} Found ${#orphaned_casks[@]} orphaned casks (sudo expired, run ${GRAY}brew list --cask${NC} to check)" - fi - else - if [[ -t 1 ]]; then stop_inline_spinner; fi - fi -} # Clean Homebrew caches and remove orphaned dependencies # Skips if run within 2 days, runs cleanup/autoremove in parallel with 120s timeout diff --git a/lib/clean/user.sh b/lib/clean/user.sh index b60c526..d6a7bdc 100644 --- a/lib/clean/user.sh +++ b/lib/clean/user.sh @@ -137,7 +137,7 @@ clean_sandboxed_app_caches() { # Clean contents safely # We know this is a user cache path, so rm -rf is acceptable here # provided we keep the Cache directory itself - rm -rf "$cache_dir"/* 2> /dev/null || true + rm -rf "${cache_dir:?}"/* 2> /dev/null || true fi fi fi @@ -247,7 +247,9 @@ clean_application_support_logs() { is_protected=true fi - [[ "$is_protected" == "true" ]] && continue + if [[ "$is_protected" == "true" ]]; then + continue + fi if [[ "$app_name" =~ backgroundtaskmanagement || "$app_name" =~ loginitems ]]; then continue diff --git a/mole b/mole index 33ed719..e1d69ec 100755 --- a/mole +++ b/mole @@ -22,7 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/core/common.sh" # Version info -VERSION="1.12.21" +VERSION="1.12.22" MOLE_TAGLINE="can dig deep to clean your Mac." # Check if Touch ID is already configured diff --git a/tests/system_maintenance.bats b/tests/system_maintenance.bats index 37565e0..071d1a5 100644 --- a/tests/system_maintenance.bats +++ b/tests/system_maintenance.bats @@ -100,30 +100,6 @@ EOF [[ "$output" == *"No failed Time Machine backups found"* ]] } -@test "clean_orphaned_casks uses cached mapping when recent" { - cache_dir="$HOME/.cache/mole" - mkdir -p "$cache_dir" - cat > "$cache_dir/cask_apps.cache" <<'EOF' -fake-app|Fake.app -EOF - - run bash --noprofile --norc <<'EOF' -set -euo pipefail -source "$PROJECT_ROOT/lib/core/common.sh" -source "$PROJECT_ROOT/lib/clean/brew.sh" - -touch "$HOME/.cache/mole/cask_apps.cache" - -brew() { return 0; } -start_inline_spinner(){ :; } -stop_inline_spinner(){ :; } -sudo() { return 0; } -MOLE_SPINNER_PREFIX="" -clean_orphaned_casks -EOF - - [ "$status" -eq 0 ] -} @test "clean_homebrew skips when cleaned recently" { run bash --noprofile --norc <<'EOF'