mirror of
https://github.com/tw93/Mole.git
synced 2026-02-08 14:24:21 +00:00
feat: Enhance clean, optimize, analyze, and status commands, and update security audit documentation.
This commit is contained in:
@@ -1,22 +1,19 @@
|
||||
#!/bin/bash
|
||||
# User GUI Applications Cleanup Module
|
||||
# Desktop applications, communication tools, media players, games, utilities
|
||||
# User GUI Applications Cleanup Module (desktop apps, media, utilities).
|
||||
set -euo pipefail
|
||||
# Clean Xcode and iOS development tools
|
||||
# Xcode and iOS tooling.
|
||||
clean_xcode_tools() {
|
||||
# Check if Xcode is running for safer cleanup of critical resources
|
||||
# Skip DerivedData/Archives while Xcode is running.
|
||||
local xcode_running=false
|
||||
if pgrep -x "Xcode" > /dev/null 2>&1; then
|
||||
xcode_running=true
|
||||
fi
|
||||
# Safe to clean regardless of Xcode state
|
||||
safe_clean ~/Library/Developer/CoreSimulator/Caches/* "Simulator cache"
|
||||
safe_clean ~/Library/Developer/CoreSimulator/Devices/*/data/tmp/* "Simulator temp files"
|
||||
safe_clean ~/Library/Caches/com.apple.dt.Xcode/* "Xcode cache"
|
||||
safe_clean ~/Library/Developer/Xcode/iOS\ Device\ Logs/* "iOS device logs"
|
||||
safe_clean ~/Library/Developer/Xcode/watchOS\ Device\ Logs/* "watchOS device logs"
|
||||
safe_clean ~/Library/Developer/Xcode/Products/* "Xcode build products"
|
||||
# Clean build artifacts only if Xcode is not running
|
||||
if [[ "$xcode_running" == "false" ]]; then
|
||||
safe_clean ~/Library/Developer/Xcode/DerivedData/* "Xcode derived data"
|
||||
safe_clean ~/Library/Developer/Xcode/Archives/* "Xcode archives"
|
||||
@@ -24,7 +21,7 @@ clean_xcode_tools() {
|
||||
echo -e " ${YELLOW}${ICON_WARNING}${NC} Xcode is running, skipping DerivedData and Archives cleanup"
|
||||
fi
|
||||
}
|
||||
# Clean code editors (VS Code, Sublime, etc.)
|
||||
# Code editors.
|
||||
clean_code_editors() {
|
||||
safe_clean ~/Library/Application\ Support/Code/logs/* "VS Code logs"
|
||||
safe_clean ~/Library/Application\ Support/Code/Cache/* "VS Code cache"
|
||||
@@ -32,7 +29,7 @@ clean_code_editors() {
|
||||
safe_clean ~/Library/Application\ Support/Code/CachedData/* "VS Code data cache"
|
||||
safe_clean ~/Library/Caches/com.sublimetext.*/* "Sublime Text cache"
|
||||
}
|
||||
# Clean communication apps (Slack, Discord, Zoom, etc.)
|
||||
# Communication apps.
|
||||
clean_communication_apps() {
|
||||
safe_clean ~/Library/Application\ Support/discord/Cache/* "Discord cache"
|
||||
safe_clean ~/Library/Application\ Support/legcord/Cache/* "Legcord cache"
|
||||
@@ -47,43 +44,43 @@ clean_communication_apps() {
|
||||
safe_clean ~/Library/Caches/com.tencent.WeWorkMac/* "WeCom cache"
|
||||
safe_clean ~/Library/Caches/com.feishu.*/* "Feishu cache"
|
||||
}
|
||||
# Clean DingTalk
|
||||
# DingTalk.
|
||||
clean_dingtalk() {
|
||||
safe_clean ~/Library/Caches/dd.work.exclusive4aliding/* "DingTalk iDingTalk cache"
|
||||
safe_clean ~/Library/Caches/com.alibaba.AliLang.osx/* "AliLang security component"
|
||||
safe_clean ~/Library/Application\ Support/iDingTalk/log/* "DingTalk logs"
|
||||
safe_clean ~/Library/Application\ Support/iDingTalk/holmeslogs/* "DingTalk holmes logs"
|
||||
}
|
||||
# Clean AI assistants
|
||||
# AI assistants.
|
||||
clean_ai_apps() {
|
||||
safe_clean ~/Library/Caches/com.openai.chat/* "ChatGPT cache"
|
||||
safe_clean ~/Library/Caches/com.anthropic.claudefordesktop/* "Claude desktop cache"
|
||||
safe_clean ~/Library/Logs/Claude/* "Claude logs"
|
||||
}
|
||||
# Clean design and creative tools
|
||||
# Design and creative tools.
|
||||
clean_design_tools() {
|
||||
safe_clean ~/Library/Caches/com.bohemiancoding.sketch3/* "Sketch cache"
|
||||
safe_clean ~/Library/Application\ Support/com.bohemiancoding.sketch3/cache/* "Sketch app cache"
|
||||
safe_clean ~/Library/Caches/Adobe/* "Adobe cache"
|
||||
safe_clean ~/Library/Caches/com.adobe.*/* "Adobe app caches"
|
||||
safe_clean ~/Library/Caches/com.figma.Desktop/* "Figma cache"
|
||||
# Note: Raycast cache is protected - contains clipboard history (including images)
|
||||
# Raycast cache is protected (clipboard history, images).
|
||||
}
|
||||
# Clean video editing tools
|
||||
# Video editing tools.
|
||||
clean_video_tools() {
|
||||
safe_clean ~/Library/Caches/net.telestream.screenflow10/* "ScreenFlow cache"
|
||||
safe_clean ~/Library/Caches/com.apple.FinalCut/* "Final Cut Pro cache"
|
||||
safe_clean ~/Library/Caches/com.blackmagic-design.DaVinciResolve/* "DaVinci Resolve cache"
|
||||
safe_clean ~/Library/Caches/com.adobe.PremierePro.*/* "Premiere Pro cache"
|
||||
}
|
||||
# Clean 3D and CAD tools
|
||||
# 3D and CAD tools.
|
||||
clean_3d_tools() {
|
||||
safe_clean ~/Library/Caches/org.blenderfoundation.blender/* "Blender cache"
|
||||
safe_clean ~/Library/Caches/com.maxon.cinema4d/* "Cinema 4D cache"
|
||||
safe_clean ~/Library/Caches/com.autodesk.*/* "Autodesk cache"
|
||||
safe_clean ~/Library/Caches/com.sketchup.*/* "SketchUp cache"
|
||||
}
|
||||
# Clean productivity apps
|
||||
# Productivity apps.
|
||||
clean_productivity_apps() {
|
||||
safe_clean ~/Library/Caches/com.tw93.MiaoYan/* "MiaoYan cache"
|
||||
safe_clean ~/Library/Caches/com.klee.desktop/* "Klee cache"
|
||||
@@ -92,20 +89,18 @@ clean_productivity_apps() {
|
||||
safe_clean ~/Library/Caches/com.filo.client/* "Filo cache"
|
||||
safe_clean ~/Library/Caches/com.flomoapp.mac/* "Flomo cache"
|
||||
}
|
||||
# Clean music and media players (protects Spotify offline music)
|
||||
# Music/media players (protect Spotify offline music).
|
||||
clean_media_players() {
|
||||
# Spotify cache protection: check for offline music indicators
|
||||
local spotify_cache="$HOME/Library/Caches/com.spotify.client"
|
||||
local spotify_data="$HOME/Library/Application Support/Spotify"
|
||||
local has_offline_music=false
|
||||
# Check for offline music database or large cache (>500MB)
|
||||
# Heuristics: offline DB or large cache.
|
||||
if [[ -f "$spotify_data/PersistentCache/Storage/offline.bnk" ]] ||
|
||||
[[ -d "$spotify_data/PersistentCache/Storage" && -n "$(find "$spotify_data/PersistentCache/Storage" -type f -name "*.file" 2> /dev/null | head -1)" ]]; then
|
||||
has_offline_music=true
|
||||
elif [[ -d "$spotify_cache" ]]; then
|
||||
local cache_size_kb
|
||||
cache_size_kb=$(get_path_size_kb "$spotify_cache")
|
||||
# Large cache (>500MB) likely contains offline music
|
||||
if [[ $cache_size_kb -ge 512000 ]]; then
|
||||
has_offline_music=true
|
||||
fi
|
||||
@@ -125,7 +120,7 @@ clean_media_players() {
|
||||
safe_clean ~/Library/Caches/com.kugou.mac/* "Kugou Music cache"
|
||||
safe_clean ~/Library/Caches/com.kuwo.mac/* "Kuwo Music cache"
|
||||
}
|
||||
# Clean video players
|
||||
# Video players.
|
||||
clean_video_players() {
|
||||
safe_clean ~/Library/Caches/com.colliderli.iina "IINA cache"
|
||||
safe_clean ~/Library/Caches/org.videolan.vlc "VLC cache"
|
||||
@@ -136,7 +131,7 @@ clean_video_players() {
|
||||
safe_clean ~/Library/Caches/com.douyu.*/* "Douyu cache"
|
||||
safe_clean ~/Library/Caches/com.huya.*/* "Huya cache"
|
||||
}
|
||||
# Clean download managers
|
||||
# Download managers.
|
||||
clean_download_managers() {
|
||||
safe_clean ~/Library/Caches/net.xmac.aria2gui "Aria2 cache"
|
||||
safe_clean ~/Library/Caches/org.m0k.transmission "Transmission cache"
|
||||
@@ -145,7 +140,7 @@ clean_download_managers() {
|
||||
safe_clean ~/Library/Caches/com.folx.*/* "Folx cache"
|
||||
safe_clean ~/Library/Caches/com.charlessoft.pacifist/* "Pacifist cache"
|
||||
}
|
||||
# Clean gaming platforms
|
||||
# Gaming platforms.
|
||||
clean_gaming_platforms() {
|
||||
safe_clean ~/Library/Caches/com.valvesoftware.steam/* "Steam cache"
|
||||
safe_clean ~/Library/Application\ Support/Steam/htmlcache/* "Steam web cache"
|
||||
@@ -156,41 +151,41 @@ clean_gaming_platforms() {
|
||||
safe_clean ~/Library/Caches/com.gog.galaxy/* "GOG Galaxy cache"
|
||||
safe_clean ~/Library/Caches/com.riotgames.*/* "Riot Games cache"
|
||||
}
|
||||
# Clean translation and dictionary apps
|
||||
# Translation/dictionary apps.
|
||||
clean_translation_apps() {
|
||||
safe_clean ~/Library/Caches/com.youdao.YoudaoDict "Youdao Dictionary cache"
|
||||
safe_clean ~/Library/Caches/com.eudic.* "Eudict cache"
|
||||
safe_clean ~/Library/Caches/com.bob-build.Bob "Bob Translation cache"
|
||||
}
|
||||
# Clean screenshot and screen recording tools
|
||||
# Screenshot/recording tools.
|
||||
clean_screenshot_tools() {
|
||||
safe_clean ~/Library/Caches/com.cleanshot.* "CleanShot cache"
|
||||
safe_clean ~/Library/Caches/com.reincubate.camo "Camo cache"
|
||||
safe_clean ~/Library/Caches/com.xnipapp.xnip "Xnip cache"
|
||||
}
|
||||
# Clean email clients
|
||||
# Email clients.
|
||||
clean_email_clients() {
|
||||
safe_clean ~/Library/Caches/com.readdle.smartemail-Mac "Spark cache"
|
||||
safe_clean ~/Library/Caches/com.airmail.* "Airmail cache"
|
||||
}
|
||||
# Clean task management apps
|
||||
# Task management apps.
|
||||
clean_task_apps() {
|
||||
safe_clean ~/Library/Caches/com.todoist.mac.Todoist "Todoist cache"
|
||||
safe_clean ~/Library/Caches/com.any.do.* "Any.do cache"
|
||||
}
|
||||
# Clean shell and terminal utilities
|
||||
# Shell/terminal utilities.
|
||||
clean_shell_utils() {
|
||||
safe_clean ~/.zcompdump* "Zsh completion cache"
|
||||
safe_clean ~/.lesshst "less history"
|
||||
safe_clean ~/.viminfo.tmp "Vim temporary files"
|
||||
safe_clean ~/.wget-hsts "wget HSTS cache"
|
||||
}
|
||||
# Clean input method and system utilities
|
||||
# Input methods and system utilities.
|
||||
clean_system_utils() {
|
||||
safe_clean ~/Library/Caches/com.runjuu.Input-Source-Pro/* "Input Source Pro cache"
|
||||
safe_clean ~/Library/Caches/macos-wakatime.WakaTime/* "WakaTime cache"
|
||||
}
|
||||
# Clean note-taking apps
|
||||
# Note-taking apps.
|
||||
clean_note_apps() {
|
||||
safe_clean ~/Library/Caches/notion.id/* "Notion cache"
|
||||
safe_clean ~/Library/Caches/md.obsidian/* "Obsidian cache"
|
||||
@@ -199,19 +194,19 @@ clean_note_apps() {
|
||||
safe_clean ~/Library/Caches/com.evernote.*/* "Evernote cache"
|
||||
safe_clean ~/Library/Caches/com.yinxiang.*/* "Yinxiang Note cache"
|
||||
}
|
||||
# Clean launcher and automation tools
|
||||
# Launchers and automation tools.
|
||||
clean_launcher_apps() {
|
||||
safe_clean ~/Library/Caches/com.runningwithcrayons.Alfred/* "Alfred cache"
|
||||
safe_clean ~/Library/Caches/cx.c3.theunarchiver/* "The Unarchiver cache"
|
||||
}
|
||||
# Clean remote desktop tools
|
||||
# Remote desktop tools.
|
||||
clean_remote_desktop() {
|
||||
safe_clean ~/Library/Caches/com.teamviewer.*/* "TeamViewer cache"
|
||||
safe_clean ~/Library/Caches/com.anydesk.*/* "AnyDesk cache"
|
||||
safe_clean ~/Library/Caches/com.todesk.*/* "ToDesk cache"
|
||||
safe_clean ~/Library/Caches/com.sunlogin.*/* "Sunlogin cache"
|
||||
}
|
||||
# Main function to clean all user GUI applications
|
||||
# Main entry for GUI app cleanup.
|
||||
clean_user_gui_applications() {
|
||||
stop_section_spinner
|
||||
clean_xcode_tools
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# Application Data Cleanup Module
|
||||
set -euo pipefail
|
||||
# Args: $1=target_dir, $2=label
|
||||
# Clean .DS_Store (Finder metadata), home uses maxdepth 5, excludes slow paths, max 500 files
|
||||
clean_ds_store_tree() {
|
||||
local target="$1"
|
||||
local label="$2"
|
||||
@@ -15,7 +14,6 @@ clean_ds_store_tree() {
|
||||
start_inline_spinner "Cleaning Finder metadata..."
|
||||
spinner_active="true"
|
||||
fi
|
||||
# Build exclusion paths for find (skip common slow/large directories)
|
||||
local -a exclude_paths=(
|
||||
-path "*/Library/Application Support/MobileSync" -prune -o
|
||||
-path "*/Library/Developer" -prune -o
|
||||
@@ -24,13 +22,11 @@ clean_ds_store_tree() {
|
||||
-path "*/.git" -prune -o
|
||||
-path "*/Library/Caches" -prune -o
|
||||
)
|
||||
# Build find command to avoid unbound array expansion with set -u
|
||||
local -a find_cmd=("command" "find" "$target")
|
||||
if [[ "$target" == "$HOME" ]]; then
|
||||
find_cmd+=("-maxdepth" "5")
|
||||
fi
|
||||
find_cmd+=("${exclude_paths[@]}" "-type" "f" "-name" ".DS_Store" "-print0")
|
||||
# Find .DS_Store files with exclusions and depth limit
|
||||
while IFS= read -r -d '' ds_file; do
|
||||
local size
|
||||
size=$(get_file_size "$ds_file")
|
||||
@@ -61,14 +57,11 @@ clean_ds_store_tree() {
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
# Clean data for uninstalled apps (caches/logs/states older than 60 days)
|
||||
# Protects system apps, major vendors, scans /Applications+running processes
|
||||
# Max 100 items/pattern, 2s du timeout. Env: ORPHAN_AGE_THRESHOLD, DRY_RUN
|
||||
# Orphaned app data (60+ days inactive). Env: ORPHAN_AGE_THRESHOLD, DRY_RUN
|
||||
# Usage: scan_installed_apps "output_file"
|
||||
# Scan system for installed application bundle IDs
|
||||
scan_installed_apps() {
|
||||
local installed_bundles="$1"
|
||||
# Performance optimization: cache results for 5 minutes
|
||||
# Cache installed app scan briefly to speed repeated runs.
|
||||
local cache_file="$HOME/.cache/mole/installed_apps_cache"
|
||||
local cache_age_seconds=300 # 5 minutes
|
||||
if [[ -f "$cache_file" ]]; then
|
||||
@@ -77,7 +70,6 @@ scan_installed_apps() {
|
||||
local age=$((current_time - cache_mtime))
|
||||
if [[ $age -lt $cache_age_seconds ]]; then
|
||||
debug_log "Using cached app list (age: ${age}s)"
|
||||
# Verify cache file is readable and not empty
|
||||
if [[ -r "$cache_file" ]] && [[ -s "$cache_file" ]]; then
|
||||
if cat "$cache_file" > "$installed_bundles" 2> /dev/null; then
|
||||
return 0
|
||||
@@ -90,26 +82,22 @@ scan_installed_apps() {
|
||||
fi
|
||||
fi
|
||||
debug_log "Scanning installed applications (cache expired or missing)"
|
||||
# Scan all Applications directories
|
||||
local -a app_dirs=(
|
||||
"/Applications"
|
||||
"/System/Applications"
|
||||
"$HOME/Applications"
|
||||
)
|
||||
# Create a temp dir for parallel results to avoid write contention
|
||||
# Temp dir avoids write contention across parallel scans.
|
||||
local scan_tmp_dir=$(create_temp_dir)
|
||||
# Parallel scan for applications
|
||||
local pids=()
|
||||
local dir_idx=0
|
||||
for app_dir in "${app_dirs[@]}"; do
|
||||
[[ -d "$app_dir" ]] || continue
|
||||
(
|
||||
# Quickly find all .app bundles first
|
||||
local -a app_paths=()
|
||||
while IFS= read -r app_path; do
|
||||
[[ -n "$app_path" ]] && app_paths+=("$app_path")
|
||||
done < <(find "$app_dir" -name '*.app' -maxdepth 3 -type d 2> /dev/null)
|
||||
# Read bundle IDs with PlistBuddy
|
||||
local count=0
|
||||
for app_path in "${app_paths[@]:-}"; do
|
||||
local plist_path="$app_path/Contents/Info.plist"
|
||||
@@ -124,7 +112,7 @@ scan_installed_apps() {
|
||||
pids+=($!)
|
||||
((dir_idx++))
|
||||
done
|
||||
# Get running applications and LaunchAgents in parallel
|
||||
# Collect running apps and LaunchAgents to avoid false orphan cleanup.
|
||||
(
|
||||
local running_apps=$(run_with_timeout 5 osascript -e 'tell application "System Events" to get bundle identifier of every application process' 2> /dev/null || echo "")
|
||||
echo "$running_apps" | tr ',' '\n' | sed -e 's/^ *//;s/ *$//' -e '/^$/d' > "$scan_tmp_dir/running.txt"
|
||||
@@ -136,7 +124,6 @@ scan_installed_apps() {
|
||||
xargs -I {} basename {} .plist > "$scan_tmp_dir/agents.txt" 2> /dev/null || true
|
||||
) &
|
||||
pids+=($!)
|
||||
# Wait for all background scans to complete
|
||||
debug_log "Waiting for ${#pids[@]} background processes: ${pids[*]}"
|
||||
for pid in "${pids[@]}"; do
|
||||
wait "$pid" 2> /dev/null || true
|
||||
@@ -145,37 +132,30 @@ scan_installed_apps() {
|
||||
cat "$scan_tmp_dir"/*.txt >> "$installed_bundles" 2> /dev/null || true
|
||||
safe_remove "$scan_tmp_dir" true
|
||||
sort -u "$installed_bundles" -o "$installed_bundles"
|
||||
# Cache the results
|
||||
ensure_user_dir "$(dirname "$cache_file")"
|
||||
cp "$installed_bundles" "$cache_file" 2> /dev/null || true
|
||||
local app_count=$(wc -l < "$installed_bundles" 2> /dev/null | tr -d ' ')
|
||||
debug_log "Scanned $app_count unique applications"
|
||||
}
|
||||
# Usage: is_bundle_orphaned "bundle_id" "directory_path" "installed_bundles_file"
|
||||
# Check if bundle is orphaned
|
||||
is_bundle_orphaned() {
|
||||
local bundle_id="$1"
|
||||
local directory_path="$2"
|
||||
local installed_bundles="$3"
|
||||
# Skip system-critical and protected apps
|
||||
if should_protect_data "$bundle_id"; then
|
||||
return 1
|
||||
fi
|
||||
# Check if app exists in our scan
|
||||
if grep -Fxq "$bundle_id" "$installed_bundles" 2> /dev/null; then
|
||||
return 1
|
||||
fi
|
||||
# Check against centralized protected patterns (app_protection.sh)
|
||||
if should_protect_data "$bundle_id"; then
|
||||
return 1
|
||||
fi
|
||||
# Extra check for specific system bundles not covered by patterns
|
||||
case "$bundle_id" in
|
||||
loginwindow | dock | systempreferences | systemsettings | settings | controlcenter | finder | safari)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
# Check file age - only clean if 60+ days inactive
|
||||
if [[ -e "$directory_path" ]]; then
|
||||
local last_modified_epoch=$(get_file_mtime "$directory_path")
|
||||
local current_epoch=$(date +%s)
|
||||
@@ -186,31 +166,23 @@ is_bundle_orphaned() {
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
# Clean data for uninstalled apps (caches/logs/states older than 60 days)
|
||||
# Max 100 items/pattern, 2s du timeout. Env: ORPHAN_AGE_THRESHOLD, DRY_RUN
|
||||
# Protects system apps, major vendors, scans /Applications+running processes
|
||||
# Orphaned app data sweep.
|
||||
clean_orphaned_app_data() {
|
||||
# Quick permission check - if we can't access Library folders, skip
|
||||
if ! ls "$HOME/Library/Caches" > /dev/null 2>&1; then
|
||||
stop_section_spinner
|
||||
echo -e " ${YELLOW}${ICON_WARNING}${NC} Skipped: No permission to access Library folders"
|
||||
return 0
|
||||
fi
|
||||
# Build list of installed/active apps
|
||||
start_section_spinner "Scanning installed apps..."
|
||||
local installed_bundles=$(create_temp_file)
|
||||
scan_installed_apps "$installed_bundles"
|
||||
stop_section_spinner
|
||||
# Display scan results
|
||||
local app_count=$(wc -l < "$installed_bundles" 2> /dev/null | tr -d ' ')
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Found $app_count active/installed apps"
|
||||
# Track statistics
|
||||
local orphaned_count=0
|
||||
local total_orphaned_kb=0
|
||||
# Unified orphaned resource scanner (caches, logs, states, webkit, HTTP, cookies)
|
||||
start_section_spinner "Scanning orphaned app resources..."
|
||||
# Define resource types to scan
|
||||
# CRITICAL: NEVER add LaunchAgents or LaunchDaemons (breaks login items/startup apps)
|
||||
# CRITICAL: NEVER add LaunchAgents or LaunchDaemons (breaks login items/startup apps).
|
||||
local -a resource_types=(
|
||||
"$HOME/Library/Caches|Caches|com.*:org.*:net.*:io.*"
|
||||
"$HOME/Library/Logs|Logs|com.*:org.*:net.*:io.*"
|
||||
@@ -222,38 +194,29 @@ clean_orphaned_app_data() {
|
||||
orphaned_count=0
|
||||
for resource_type in "${resource_types[@]}"; do
|
||||
IFS='|' read -r base_path label patterns <<< "$resource_type"
|
||||
# Check both existence and permission to avoid hanging
|
||||
if [[ ! -d "$base_path" ]]; then
|
||||
continue
|
||||
fi
|
||||
# Quick permission check - if we can't ls the directory, skip it
|
||||
if ! ls "$base_path" > /dev/null 2>&1; then
|
||||
continue
|
||||
fi
|
||||
# Build file pattern array
|
||||
local -a file_patterns=()
|
||||
IFS=':' read -ra pattern_arr <<< "$patterns"
|
||||
for pat in "${pattern_arr[@]}"; do
|
||||
file_patterns+=("$base_path/$pat")
|
||||
done
|
||||
# Scan and clean orphaned items
|
||||
for item_path in "${file_patterns[@]}"; do
|
||||
# Use shell glob (no ls needed)
|
||||
# Limit iterations to prevent hanging on directories with too many files
|
||||
local iteration_count=0
|
||||
for match in $item_path; do
|
||||
[[ -e "$match" ]] || continue
|
||||
# Safety: limit iterations to prevent infinite loops on massive directories
|
||||
((iteration_count++))
|
||||
if [[ $iteration_count -gt $MOLE_MAX_ORPHAN_ITERATIONS ]]; then
|
||||
break
|
||||
fi
|
||||
# Extract bundle ID from filename
|
||||
local bundle_id=$(basename "$match")
|
||||
bundle_id="${bundle_id%.savedState}"
|
||||
bundle_id="${bundle_id%.binarycookies}"
|
||||
if is_bundle_orphaned "$bundle_id" "$match" "$installed_bundles"; then
|
||||
# Use timeout to prevent du from hanging on network mounts or problematic paths
|
||||
local size_kb
|
||||
size_kb=$(get_path_size_kb "$match")
|
||||
if [[ -z "$size_kb" || "$size_kb" == "0" ]]; then
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
# Skips if run within 7 days, runs cleanup/autoremove in parallel with 120s timeout
|
||||
clean_homebrew() {
|
||||
command -v brew > /dev/null 2>&1 || return 0
|
||||
# Dry run mode - just indicate what would happen
|
||||
if [[ "${DRY_RUN:-false}" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Homebrew · would cleanup and autoremove"
|
||||
return 0
|
||||
fi
|
||||
# Smart caching: check if brew cleanup was run recently (within 7 days)
|
||||
# Extended from 2 days to 7 days to reduce cleanup frequency
|
||||
# Skip if cleaned recently to avoid repeated heavy operations.
|
||||
local brew_cache_file="${HOME}/.cache/mole/brew_last_cleanup"
|
||||
local cache_valid_days=7
|
||||
local should_skip=false
|
||||
@@ -27,20 +25,17 @@ clean_homebrew() {
|
||||
fi
|
||||
fi
|
||||
[[ "$should_skip" == "true" ]] && return 0
|
||||
# Quick pre-check: determine if cleanup is needed based on cache size (<50MB)
|
||||
# Use timeout to prevent slow du on very large caches
|
||||
# If timeout occurs, assume cache is large and run cleanup
|
||||
# Skip cleanup if cache is small; still run autoremove.
|
||||
local skip_cleanup=false
|
||||
local brew_cache_size=0
|
||||
if [[ -d ~/Library/Caches/Homebrew ]]; then
|
||||
brew_cache_size=$(run_with_timeout 3 du -sk ~/Library/Caches/Homebrew 2> /dev/null | awk '{print $1}')
|
||||
local du_exit=$?
|
||||
# Skip cleanup (but still run autoremove) if cache is small
|
||||
if [[ $du_exit -eq 0 && -n "$brew_cache_size" && "$brew_cache_size" -lt 51200 ]]; then
|
||||
skip_cleanup=true
|
||||
fi
|
||||
fi
|
||||
# Display appropriate spinner message
|
||||
# Spinner reflects whether cleanup is skipped.
|
||||
if [[ -t 1 ]]; then
|
||||
if [[ "$skip_cleanup" == "true" ]]; then
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Homebrew autoremove (cleanup skipped)..."
|
||||
@@ -48,8 +43,8 @@ clean_homebrew() {
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Homebrew cleanup and autoremove..."
|
||||
fi
|
||||
fi
|
||||
# Run cleanup/autoremove in parallel with a timeout guard.
|
||||
local timeout_seconds=${MO_BREW_TIMEOUT:-120}
|
||||
# Run brew cleanup and/or autoremove based on cache size
|
||||
local brew_tmp_file autoremove_tmp_file
|
||||
local brew_pid autoremove_pid
|
||||
if [[ "$skip_cleanup" == "false" ]]; then
|
||||
@@ -63,9 +58,7 @@ clean_homebrew() {
|
||||
local elapsed=0
|
||||
local brew_done=false
|
||||
local autoremove_done=false
|
||||
# Mark cleanup as done if it was skipped
|
||||
[[ "$skip_cleanup" == "true" ]] && brew_done=true
|
||||
# Wait for both to complete or timeout
|
||||
while [[ "$brew_done" == "false" ]] || [[ "$autoremove_done" == "false" ]]; do
|
||||
if [[ $elapsed -ge $timeout_seconds ]]; then
|
||||
[[ -n "$brew_pid" ]] && kill -TERM $brew_pid 2> /dev/null || true
|
||||
@@ -77,7 +70,6 @@ clean_homebrew() {
|
||||
sleep 1
|
||||
((elapsed++))
|
||||
done
|
||||
# Wait for processes to finish
|
||||
local brew_success=false
|
||||
if [[ "$skip_cleanup" == "false" && -n "$brew_pid" ]]; then
|
||||
if wait $brew_pid 2> /dev/null; then
|
||||
@@ -90,6 +82,7 @@ clean_homebrew() {
|
||||
fi
|
||||
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
||||
# Process cleanup output and extract metrics
|
||||
# Summarize cleanup results.
|
||||
if [[ "$skip_cleanup" == "true" ]]; then
|
||||
# Cleanup was skipped due to small cache size
|
||||
local size_mb=$((brew_cache_size / 1024))
|
||||
@@ -111,6 +104,7 @@ clean_homebrew() {
|
||||
echo -e " ${YELLOW}${ICON_WARNING}${NC} Homebrew cleanup timed out · run ${GRAY}brew cleanup${NC} manually"
|
||||
fi
|
||||
# Process autoremove output - only show if packages were removed
|
||||
# Only surface autoremove output when packages were removed.
|
||||
if [[ "$autoremove_success" == "true" && -f "$autoremove_tmp_file" ]]; then
|
||||
local autoremove_output
|
||||
autoremove_output=$(cat "$autoremove_tmp_file" 2> /dev/null || echo "")
|
||||
@@ -124,6 +118,7 @@ clean_homebrew() {
|
||||
fi
|
||||
# Update cache timestamp on successful completion or when cleanup was intelligently skipped
|
||||
# This prevents repeated cache size checks within the 7-day window
|
||||
# Update cache timestamp when any work succeeded or was intentionally skipped.
|
||||
if [[ "$skip_cleanup" == "true" ]] || [[ "$brew_success" == "true" ]] || [[ "$autoremove_success" == "true" ]]; then
|
||||
ensure_user_file "$brew_cache_file"
|
||||
date +%s > "$brew_cache_file"
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
#!/bin/bash
|
||||
# Cache Cleanup Module
|
||||
set -euo pipefail
|
||||
# Only runs once (uses ~/.cache/mole/permissions_granted flag)
|
||||
# Trigger all TCC permission dialogs upfront to avoid random interruptions
|
||||
# Preflight TCC prompts once to avoid mid-run interruptions.
|
||||
check_tcc_permissions() {
|
||||
# Only check in interactive mode
|
||||
[[ -t 1 ]] || return 0
|
||||
local permission_flag="$HOME/.cache/mole/permissions_granted"
|
||||
# Skip if permissions were already granted
|
||||
[[ -f "$permission_flag" ]] && return 0
|
||||
# Key protected directories that require TCC approval
|
||||
local -a tcc_dirs=(
|
||||
"$HOME/Library/Caches"
|
||||
"$HOME/Library/Logs"
|
||||
@@ -17,8 +13,7 @@ check_tcc_permissions() {
|
||||
"$HOME/Library/Containers"
|
||||
"$HOME/.cache"
|
||||
)
|
||||
# Quick permission test - if first directory is accessible, likely others are too
|
||||
# Use simple ls test instead of find to avoid triggering permission dialogs prematurely
|
||||
# Quick permission probe (avoid deep scans).
|
||||
local needs_permission_check=false
|
||||
if ! ls "$HOME/Library/Caches" > /dev/null 2>&1; then
|
||||
needs_permission_check=true
|
||||
@@ -32,35 +27,30 @@ check_tcc_permissions() {
|
||||
echo -ne "${PURPLE}${ICON_ARROW}${NC} Press ${GREEN}Enter${NC} to continue: "
|
||||
read -r
|
||||
MOLE_SPINNER_PREFIX="" start_inline_spinner "Requesting permissions..."
|
||||
# Trigger all TCC prompts upfront by accessing each directory
|
||||
# Using find -maxdepth 1 ensures we touch the directory without deep scanning
|
||||
# Touch each directory to trigger prompts without deep scanning.
|
||||
for dir in "${tcc_dirs[@]}"; do
|
||||
[[ -d "$dir" ]] && command find "$dir" -maxdepth 1 -type d > /dev/null 2>&1
|
||||
done
|
||||
stop_inline_spinner
|
||||
echo ""
|
||||
fi
|
||||
# Mark permissions as granted (won't prompt again)
|
||||
# Mark as granted to avoid repeat prompts.
|
||||
ensure_user_file "$permission_flag"
|
||||
return 0
|
||||
}
|
||||
# Args: $1=browser_name, $2=cache_path
|
||||
# Clean browser Service Worker cache, protecting web editing tools (capcut, photopea, pixlr)
|
||||
# Clean Service Worker cache while protecting critical web editors.
|
||||
clean_service_worker_cache() {
|
||||
local browser_name="$1"
|
||||
local cache_path="$2"
|
||||
[[ ! -d "$cache_path" ]] && return 0
|
||||
local cleaned_size=0
|
||||
local protected_count=0
|
||||
# Find all cache directories and calculate sizes with timeout protection
|
||||
while IFS= read -r cache_dir; do
|
||||
[[ ! -d "$cache_dir" ]] && continue
|
||||
# Extract domain from path using regex
|
||||
# Pattern matches: letters/numbers, hyphens, then dot, then TLD
|
||||
# Example: "abc123_https_example.com_0" → "example.com"
|
||||
# Extract a best-effort domain name from cache folder.
|
||||
local domain=$(basename "$cache_dir" | grep -oE '[a-zA-Z0-9][-a-zA-Z0-9]*\.[a-zA-Z]{2,}' | head -1 || echo "")
|
||||
local size=$(run_with_timeout 5 get_path_size_kb "$cache_dir")
|
||||
# Check if domain is protected
|
||||
local is_protected=false
|
||||
for protected_domain in "${PROTECTED_SW_DOMAINS[@]}"; do
|
||||
if [[ "$domain" == *"$protected_domain"* ]]; then
|
||||
@@ -69,7 +59,6 @@ clean_service_worker_cache() {
|
||||
break
|
||||
fi
|
||||
done
|
||||
# Clean if not protected
|
||||
if [[ "$is_protected" == "false" ]]; then
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
safe_remove "$cache_dir" true || true
|
||||
@@ -78,7 +67,6 @@ clean_service_worker_cache() {
|
||||
fi
|
||||
done < <(run_with_timeout 10 sh -c "find '$cache_path' -type d -depth 2 2> /dev/null || true")
|
||||
if [[ $cleaned_size -gt 0 ]]; then
|
||||
# Temporarily stop spinner for clean output
|
||||
local spinner_was_running=false
|
||||
if [[ -t 1 && -n "${INLINE_SPINNER_PID:-}" ]]; then
|
||||
stop_inline_spinner
|
||||
@@ -95,17 +83,15 @@ clean_service_worker_cache() {
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} $browser_name Service Worker (would clean ${cleaned_mb}MB, ${protected_count} protected)"
|
||||
fi
|
||||
note_activity
|
||||
# Restart spinner if it was running
|
||||
if [[ "$spinner_was_running" == "true" ]]; then
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning browser Service Worker caches..."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
# Uses maxdepth 3, excludes Library/.Trash/node_modules, 10s timeout per scan
|
||||
# Clean Next.js (.next/cache) and Python (__pycache__) build caches
|
||||
# Next.js/Python project caches with tight scan bounds and timeouts.
|
||||
clean_project_caches() {
|
||||
stop_inline_spinner 2> /dev/null || true
|
||||
# Quick check: skip if user likely doesn't have development projects
|
||||
# Fast pre-check before scanning the whole home dir.
|
||||
local has_dev_projects=false
|
||||
local -a common_dev_dirs=(
|
||||
"$HOME/Code"
|
||||
@@ -133,8 +119,7 @@ clean_project_caches() {
|
||||
break
|
||||
fi
|
||||
done
|
||||
# If no common dev directories found, perform feature-based detection
|
||||
# Check for project markers in $HOME (node_modules, .git, target, etc.)
|
||||
# Fallback: look for project markers near $HOME.
|
||||
if [[ "$has_dev_projects" == "false" ]]; then
|
||||
local -a project_markers=(
|
||||
"node_modules"
|
||||
@@ -153,7 +138,6 @@ clean_project_caches() {
|
||||
spinner_active=true
|
||||
fi
|
||||
for marker in "${project_markers[@]}"; do
|
||||
# Quick check with maxdepth 2 and 3s timeout to avoid slow scans
|
||||
if run_with_timeout 3 sh -c "find '$HOME' -maxdepth 2 -name '$marker' -not -path '*/Library/*' -not -path '*/.Trash/*' 2>/dev/null | head -1" | grep -q .; then
|
||||
has_dev_projects=true
|
||||
break
|
||||
@@ -162,7 +146,6 @@ clean_project_caches() {
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_inline_spinner 2> /dev/null || true
|
||||
fi
|
||||
# If still no dev projects found, skip scanning
|
||||
[[ "$has_dev_projects" == "false" ]] && return 0
|
||||
fi
|
||||
if [[ -t 1 ]]; then
|
||||
@@ -174,7 +157,7 @@ clean_project_caches() {
|
||||
local pycache_tmp_file
|
||||
pycache_tmp_file=$(create_temp_file)
|
||||
local find_timeout=10
|
||||
# 1. Start Next.js search
|
||||
# Parallel scans (Next.js and __pycache__).
|
||||
(
|
||||
command find "$HOME" -P -mount -type d -name ".next" -maxdepth 3 \
|
||||
-not -path "*/Library/*" \
|
||||
@@ -184,7 +167,6 @@ clean_project_caches() {
|
||||
2> /dev/null || true
|
||||
) > "$nextjs_tmp_file" 2>&1 &
|
||||
local next_pid=$!
|
||||
# 2. Start Python search
|
||||
(
|
||||
command find "$HOME" -P -mount -type d -name "__pycache__" -maxdepth 3 \
|
||||
-not -path "*/Library/*" \
|
||||
@@ -194,7 +176,6 @@ clean_project_caches() {
|
||||
2> /dev/null || true
|
||||
) > "$pycache_tmp_file" 2>&1 &
|
||||
local py_pid=$!
|
||||
# 3. Wait for both with timeout (using smaller intervals for better responsiveness)
|
||||
local elapsed=0
|
||||
local check_interval=0.2 # Check every 200ms instead of 1s for smoother experience
|
||||
while [[ $(echo "$elapsed < $find_timeout" | awk '{print ($1 < $2)}') -eq 1 ]]; do
|
||||
@@ -204,12 +185,10 @@ clean_project_caches() {
|
||||
sleep $check_interval
|
||||
elapsed=$(echo "$elapsed + $check_interval" | awk '{print $1 + $2}')
|
||||
done
|
||||
# 4. Clean up any stuck processes
|
||||
# Kill stuck scans after timeout.
|
||||
for pid in $next_pid $py_pid; do
|
||||
if kill -0 "$pid" 2> /dev/null; then
|
||||
# Send TERM signal first
|
||||
kill -TERM "$pid" 2> /dev/null || true
|
||||
# Wait up to 2 seconds for graceful termination
|
||||
local grace_period=0
|
||||
while [[ $grace_period -lt 20 ]]; do
|
||||
if ! kill -0 "$pid" 2> /dev/null; then
|
||||
@@ -218,11 +197,9 @@ clean_project_caches() {
|
||||
sleep 0.1
|
||||
((grace_period++))
|
||||
done
|
||||
# Force kill if still running
|
||||
if kill -0 "$pid" 2> /dev/null; then
|
||||
kill -KILL "$pid" 2> /dev/null || true
|
||||
fi
|
||||
# Final wait (should be instant now)
|
||||
wait "$pid" 2> /dev/null || true
|
||||
else
|
||||
wait "$pid" 2> /dev/null || true
|
||||
@@ -231,11 +208,9 @@ clean_project_caches() {
|
||||
if [[ -t 1 ]]; then
|
||||
stop_inline_spinner
|
||||
fi
|
||||
# 5. Process Next.js results
|
||||
while IFS= read -r next_dir; do
|
||||
[[ -d "$next_dir/cache" ]] && safe_clean "$next_dir/cache"/* "Next.js build cache" || true
|
||||
done < "$nextjs_tmp_file"
|
||||
# 6. Process Python results
|
||||
while IFS= read -r pycache; do
|
||||
[[ -d "$pycache" ]] && safe_clean "$pycache"/* "Python bytecode cache" || true
|
||||
done < "$pycache_tmp_file"
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
#!/bin/bash
|
||||
# Developer Tools Cleanup Module
|
||||
set -euo pipefail
|
||||
# Helper function to clean tool caches using their built-in commands
|
||||
# Args: $1 - description, $@ - command to execute
|
||||
# Env: DRY_RUN
|
||||
# so we just report the action if we can't easily find a path)
|
||||
# Note: Try to estimate potential savings (many tool caches don't have a direct path,
|
||||
# Tool cache helper (respects DRY_RUN).
|
||||
clean_tool_cache() {
|
||||
local description="$1"
|
||||
shift
|
||||
@@ -18,50 +14,38 @@ clean_tool_cache() {
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
# Clean npm cache (command + directories)
|
||||
# Env: DRY_RUN
|
||||
# npm cache clean clears official npm cache, safe_clean handles alternative package managers
|
||||
# npm/pnpm/yarn/bun caches.
|
||||
clean_dev_npm() {
|
||||
if command -v npm > /dev/null 2>&1; then
|
||||
# clean_tool_cache now calculates size before cleanup for better statistics
|
||||
clean_tool_cache "npm cache" npm cache clean --force
|
||||
note_activity
|
||||
fi
|
||||
# Clean pnpm store cache
|
||||
local pnpm_default_store=~/Library/pnpm/store
|
||||
if command -v pnpm > /dev/null 2>&1; then
|
||||
# Use pnpm's built-in prune command
|
||||
clean_tool_cache "pnpm cache" pnpm store prune
|
||||
# Get the actual store path to check if default is orphaned
|
||||
local pnpm_store_path
|
||||
start_section_spinner "Checking store path..."
|
||||
pnpm_store_path=$(run_with_timeout 2 pnpm store path 2> /dev/null) || pnpm_store_path=""
|
||||
stop_section_spinner
|
||||
# If store path is different from default, clean the orphaned default
|
||||
if [[ -n "$pnpm_store_path" && "$pnpm_store_path" != "$pnpm_default_store" ]]; then
|
||||
safe_clean "$pnpm_default_store"/* "Orphaned pnpm store"
|
||||
fi
|
||||
else
|
||||
# pnpm not installed, clean default location
|
||||
safe_clean "$pnpm_default_store"/* "pnpm store"
|
||||
fi
|
||||
note_activity
|
||||
# Clean alternative package manager caches
|
||||
safe_clean ~/.tnpm/_cacache/* "tnpm cache directory"
|
||||
safe_clean ~/.tnpm/_logs/* "tnpm logs"
|
||||
safe_clean ~/.yarn/cache/* "Yarn cache"
|
||||
safe_clean ~/.bun/install/cache/* "Bun cache"
|
||||
}
|
||||
# Clean Python/pip cache (command + directories)
|
||||
# Env: DRY_RUN
|
||||
# pip cache purge clears official pip cache, safe_clean handles other Python tools
|
||||
# Python/pip ecosystem caches.
|
||||
clean_dev_python() {
|
||||
if command -v pip3 > /dev/null 2>&1; then
|
||||
# clean_tool_cache now calculates size before cleanup for better statistics
|
||||
clean_tool_cache "pip cache" bash -c 'pip3 cache purge >/dev/null 2>&1 || true'
|
||||
note_activity
|
||||
fi
|
||||
# Clean Python ecosystem caches
|
||||
safe_clean ~/.pyenv/cache/* "pyenv cache"
|
||||
safe_clean ~/.cache/poetry/* "Poetry cache"
|
||||
safe_clean ~/.cache/uv/* "uv cache"
|
||||
@@ -76,28 +60,23 @@ clean_dev_python() {
|
||||
safe_clean ~/anaconda3/pkgs/* "Anaconda packages cache"
|
||||
safe_clean ~/.cache/wandb/* "Weights & Biases cache"
|
||||
}
|
||||
# Clean Go cache (command + directories)
|
||||
# Env: DRY_RUN
|
||||
# go clean handles build and module caches comprehensively
|
||||
# Go build/module caches.
|
||||
clean_dev_go() {
|
||||
if command -v go > /dev/null 2>&1; then
|
||||
# clean_tool_cache now calculates size before cleanup for better statistics
|
||||
clean_tool_cache "Go cache" bash -c 'go clean -modcache >/dev/null 2>&1 || true; go clean -cache >/dev/null 2>&1 || true'
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
# Clean Rust/cargo cache directories
|
||||
# Rust/cargo caches.
|
||||
clean_dev_rust() {
|
||||
safe_clean ~/.cargo/registry/cache/* "Rust cargo cache"
|
||||
safe_clean ~/.cargo/git/* "Cargo git cache"
|
||||
safe_clean ~/.rustup/downloads/* "Rust downloads cache"
|
||||
}
|
||||
# Env: DRY_RUN
|
||||
# Clean Docker cache (command + directories)
|
||||
# Docker caches (guarded by daemon check).
|
||||
clean_dev_docker() {
|
||||
if command -v docker > /dev/null 2>&1; then
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
# Check if Docker daemon is running (with timeout to prevent hanging)
|
||||
start_section_spinner "Checking Docker daemon..."
|
||||
local docker_running=false
|
||||
if run_with_timeout 3 docker info > /dev/null 2>&1; then
|
||||
@@ -107,7 +86,6 @@ clean_dev_docker() {
|
||||
if [[ "$docker_running" == "true" ]]; then
|
||||
clean_tool_cache "Docker build cache" docker builder prune -af
|
||||
else
|
||||
# Docker not running - silently skip without user interaction
|
||||
debug_log "Docker daemon not running, skipping Docker cache cleanup"
|
||||
fi
|
||||
else
|
||||
@@ -117,8 +95,7 @@ clean_dev_docker() {
|
||||
fi
|
||||
safe_clean ~/.docker/buildx/cache/* "Docker BuildX cache"
|
||||
}
|
||||
# Env: DRY_RUN
|
||||
# Clean Nix package manager
|
||||
# Nix garbage collection.
|
||||
clean_dev_nix() {
|
||||
if command -v nix-collect-garbage > /dev/null 2>&1; then
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
@@ -129,7 +106,7 @@ clean_dev_nix() {
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
# Clean cloud CLI tools cache
|
||||
# Cloud CLI caches.
|
||||
clean_dev_cloud() {
|
||||
safe_clean ~/.kube/cache/* "Kubernetes cache"
|
||||
safe_clean ~/.local/share/containers/storage/tmp/* "Container storage temp"
|
||||
@@ -137,7 +114,7 @@ clean_dev_cloud() {
|
||||
safe_clean ~/.config/gcloud/logs/* "Google Cloud logs"
|
||||
safe_clean ~/.azure/logs/* "Azure CLI logs"
|
||||
}
|
||||
# Clean frontend build tool caches
|
||||
# Frontend build caches.
|
||||
clean_dev_frontend() {
|
||||
safe_clean ~/.cache/typescript/* "TypeScript cache"
|
||||
safe_clean ~/.cache/electron/* "Electron cache"
|
||||
@@ -151,40 +128,29 @@ clean_dev_frontend() {
|
||||
safe_clean ~/.cache/eslint/* "ESLint cache"
|
||||
safe_clean ~/.cache/prettier/* "Prettier cache"
|
||||
}
|
||||
# Clean mobile development tools
|
||||
# iOS simulator cleanup can free significant space (70GB+ in some cases)
|
||||
# Simulator runtime caches can grow large over time
|
||||
# DeviceSupport files accumulate for each iOS version connected
|
||||
# Mobile dev caches (can be large).
|
||||
clean_dev_mobile() {
|
||||
# Clean Xcode unavailable simulators
|
||||
# Removes old and unused local iOS simulator data from old unused runtimes
|
||||
# Can free up significant space (70GB+ in some cases)
|
||||
if command -v xcrun > /dev/null 2>&1; then
|
||||
debug_log "Checking for unavailable Xcode simulators"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
clean_tool_cache "Xcode unavailable simulators" xcrun simctl delete unavailable
|
||||
else
|
||||
start_section_spinner "Checking unavailable simulators..."
|
||||
# Run command manually to control UI output order
|
||||
if xcrun simctl delete unavailable > /dev/null 2>&1; then
|
||||
stop_section_spinner
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Xcode unavailable simulators"
|
||||
else
|
||||
stop_section_spinner
|
||||
# Silently fail or log error if needed, matching clean_tool_cache behavior
|
||||
fi
|
||||
fi
|
||||
note_activity
|
||||
fi
|
||||
# Clean iOS DeviceSupport - more comprehensive cleanup
|
||||
# DeviceSupport directories store debug symbols for each iOS version
|
||||
# Safe to clean caches and logs, but preserve device support files themselves
|
||||
# DeviceSupport caches/logs (preserve core support files).
|
||||
safe_clean ~/Library/Developer/Xcode/iOS\ DeviceSupport/*/Symbols/System/Library/Caches/* "iOS device symbol cache"
|
||||
safe_clean ~/Library/Developer/Xcode/iOS\ DeviceSupport/*.log "iOS device support logs"
|
||||
safe_clean ~/Library/Developer/Xcode/watchOS\ DeviceSupport/*/Symbols/System/Library/Caches/* "watchOS device symbol cache"
|
||||
safe_clean ~/Library/Developer/Xcode/tvOS\ DeviceSupport/*/Symbols/System/Library/Caches/* "tvOS device symbol cache"
|
||||
# Clean simulator runtime caches
|
||||
# RuntimeRoot caches can accumulate system library caches
|
||||
# Simulator runtime caches.
|
||||
safe_clean ~/Library/Developer/CoreSimulator/Profiles/Runtimes/*/Contents/Resources/RuntimeRoot/System/Library/Caches/* "Simulator runtime cache"
|
||||
safe_clean ~/Library/Caches/Google/AndroidStudio*/* "Android Studio cache"
|
||||
safe_clean ~/Library/Caches/CocoaPods/* "CocoaPods cache"
|
||||
@@ -194,14 +160,14 @@ clean_dev_mobile() {
|
||||
safe_clean ~/Library/Developer/Xcode/UserData/IB\ Support/* "Xcode Interface Builder cache"
|
||||
safe_clean ~/.cache/swift-package-manager/* "Swift package manager cache"
|
||||
}
|
||||
# Clean JVM ecosystem tools
|
||||
# JVM ecosystem caches.
|
||||
clean_dev_jvm() {
|
||||
safe_clean ~/.gradle/caches/* "Gradle caches"
|
||||
safe_clean ~/.gradle/daemon/* "Gradle daemon logs"
|
||||
safe_clean ~/.sbt/* "SBT cache"
|
||||
safe_clean ~/.ivy2/cache/* "Ivy cache"
|
||||
}
|
||||
# Clean other language tools
|
||||
# Other language tool caches.
|
||||
clean_dev_other_langs() {
|
||||
safe_clean ~/.bundle/cache/* "Ruby Bundler cache"
|
||||
safe_clean ~/.composer/cache/* "PHP Composer cache"
|
||||
@@ -211,7 +177,7 @@ clean_dev_other_langs() {
|
||||
safe_clean ~/.cache/zig/* "Zig cache"
|
||||
safe_clean ~/Library/Caches/deno/* "Deno cache"
|
||||
}
|
||||
# Clean CI/CD and DevOps tools
|
||||
# CI/CD and DevOps caches.
|
||||
clean_dev_cicd() {
|
||||
safe_clean ~/.cache/terraform/* "Terraform cache"
|
||||
safe_clean ~/.grafana/cache/* "Grafana cache"
|
||||
@@ -222,7 +188,7 @@ clean_dev_cicd() {
|
||||
safe_clean ~/.circleci/cache/* "CircleCI cache"
|
||||
safe_clean ~/.sonar/* "SonarQube cache"
|
||||
}
|
||||
# Clean database tools
|
||||
# Database tool caches.
|
||||
clean_dev_database() {
|
||||
safe_clean ~/Library/Caches/com.sequel-ace.sequel-ace/* "Sequel Ace cache"
|
||||
safe_clean ~/Library/Caches/com.eggerapps.Sequel-Pro/* "Sequel Pro cache"
|
||||
@@ -231,7 +197,7 @@ clean_dev_database() {
|
||||
safe_clean ~/Library/Caches/com.dbeaver.* "DBeaver cache"
|
||||
safe_clean ~/Library/Caches/com.redis.RedisInsight "Redis Insight cache"
|
||||
}
|
||||
# Clean API/network debugging tools
|
||||
# API/debugging tool caches.
|
||||
clean_dev_api_tools() {
|
||||
safe_clean ~/Library/Caches/com.postmanlabs.mac/* "Postman cache"
|
||||
safe_clean ~/Library/Caches/com.konghq.insomnia/* "Insomnia cache"
|
||||
@@ -240,7 +206,7 @@ clean_dev_api_tools() {
|
||||
safe_clean ~/Library/Caches/com.charlesproxy.charles/* "Charles Proxy cache"
|
||||
safe_clean ~/Library/Caches/com.proxyman.NSProxy/* "Proxyman cache"
|
||||
}
|
||||
# Clean misc dev tools
|
||||
# Misc dev tool caches.
|
||||
clean_dev_misc() {
|
||||
safe_clean ~/Library/Caches/com.unity3d.*/* "Unity cache"
|
||||
safe_clean ~/Library/Caches/com.mongodb.compass/* "MongoDB Compass cache"
|
||||
@@ -250,7 +216,7 @@ clean_dev_misc() {
|
||||
safe_clean ~/Library/Caches/KSCrash/* "KSCrash reports"
|
||||
safe_clean ~/Library/Caches/com.crashlytics.data/* "Crashlytics data"
|
||||
}
|
||||
# Clean shell and version control
|
||||
# Shell and VCS leftovers.
|
||||
clean_dev_shell() {
|
||||
safe_clean ~/.gitconfig.lock "Git config lock"
|
||||
safe_clean ~/.gitconfig.bak* "Git config backup"
|
||||
@@ -260,28 +226,20 @@ clean_dev_shell() {
|
||||
safe_clean ~/.zsh_history.bak* "Zsh history backup"
|
||||
safe_clean ~/.cache/pre-commit/* "pre-commit cache"
|
||||
}
|
||||
# Clean network utilities
|
||||
# Network tool caches.
|
||||
clean_dev_network() {
|
||||
safe_clean ~/.cache/curl/* "curl cache"
|
||||
safe_clean ~/.cache/wget/* "wget cache"
|
||||
safe_clean ~/Library/Caches/curl/* "macOS curl cache"
|
||||
safe_clean ~/Library/Caches/wget/* "macOS wget cache"
|
||||
}
|
||||
# Clean orphaned SQLite temporary files (-shm and -wal files)
|
||||
# Strategy: Only clean truly orphaned temp files where base database is missing
|
||||
# Env: DRY_RUN
|
||||
# This is fast and safe - skip complex checks for files with existing base DB
|
||||
# Orphaned SQLite temp files (-shm/-wal). Disabled due to low ROI.
|
||||
clean_sqlite_temp_files() {
|
||||
# Skip this cleanup due to low ROI (收益比低,经常没东西可清理)
|
||||
# Find scan is still slow even optimized, and orphaned files are rare
|
||||
return 0
|
||||
}
|
||||
# Main developer tools cleanup function
|
||||
# Env: DRY_RUN
|
||||
# Calls all specialized cleanup functions
|
||||
# Main developer tools cleanup sequence.
|
||||
clean_developer_tools() {
|
||||
stop_section_spinner
|
||||
# Clean SQLite temporary files first
|
||||
clean_sqlite_temp_files
|
||||
clean_dev_npm
|
||||
clean_dev_python
|
||||
@@ -292,7 +250,6 @@ clean_developer_tools() {
|
||||
clean_dev_nix
|
||||
clean_dev_shell
|
||||
clean_dev_frontend
|
||||
# Project build caches (delegated to clean_caches module)
|
||||
clean_project_caches
|
||||
clean_dev_mobile
|
||||
clean_dev_jvm
|
||||
@@ -302,22 +259,17 @@ clean_developer_tools() {
|
||||
clean_dev_api_tools
|
||||
clean_dev_network
|
||||
clean_dev_misc
|
||||
# Homebrew caches and cleanup (delegated to clean_brew module)
|
||||
safe_clean ~/Library/Caches/Homebrew/* "Homebrew cache"
|
||||
# Clean Homebrew locks intelligently (avoid repeated sudo prompts)
|
||||
# Clean Homebrew locks without repeated sudo prompts.
|
||||
local brew_lock_dirs=(
|
||||
"/opt/homebrew/var/homebrew/locks"
|
||||
"/usr/local/var/homebrew/locks"
|
||||
)
|
||||
for lock_dir in "${brew_lock_dirs[@]}"; do
|
||||
if [[ -d "$lock_dir" && -w "$lock_dir" ]]; then
|
||||
# User can write, safe to clean
|
||||
safe_clean "$lock_dir"/* "Homebrew lock files"
|
||||
elif [[ -d "$lock_dir" ]]; then
|
||||
# Directory exists but not writable. Check if empty to avoid noise.
|
||||
if find "$lock_dir" -mindepth 1 -maxdepth 1 -print -quit 2> /dev/null | grep -q .; then
|
||||
# Only try sudo ONCE if we really need to, or just skip to avoid spam
|
||||
# Decision: Skip strict system/root owned locks to avoid nag.
|
||||
debug_log "Skipping read-only Homebrew locks in $lock_dir"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# Project Purge Module (mo purge)
|
||||
# Removes heavy project build artifacts and dependencies
|
||||
# Project Purge Module (mo purge).
|
||||
# Removes heavy project build artifacts and dependencies.
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
@@ -10,7 +10,7 @@ if ! command -v ensure_user_dir > /dev/null 2>&1; then
|
||||
source "$CORE_LIB_DIR/common.sh"
|
||||
fi
|
||||
|
||||
# Targets to look for (heavy build artifacts)
|
||||
# Targets to look for (heavy build artifacts).
|
||||
readonly PURGE_TARGETS=(
|
||||
"node_modules"
|
||||
"target" # Rust, Maven
|
||||
@@ -29,12 +29,12 @@ readonly PURGE_TARGETS=(
|
||||
".parcel-cache" # Parcel bundler
|
||||
".dart_tool" # Flutter/Dart build cache
|
||||
)
|
||||
# Minimum age in days before considering for cleanup
|
||||
# Minimum age in days before considering for cleanup.
|
||||
readonly MIN_AGE_DAYS=7
|
||||
# Scan depth defaults (relative to search root)
|
||||
# Scan depth defaults (relative to search root).
|
||||
readonly PURGE_MIN_DEPTH_DEFAULT=2
|
||||
readonly PURGE_MAX_DEPTH_DEFAULT=8
|
||||
# Search paths (default, can be overridden via config file)
|
||||
# Search paths (default, can be overridden via config file).
|
||||
readonly DEFAULT_PURGE_SEARCH_PATHS=(
|
||||
"$HOME/www"
|
||||
"$HOME/dev"
|
||||
@@ -46,13 +46,13 @@ readonly DEFAULT_PURGE_SEARCH_PATHS=(
|
||||
"$HOME/Development"
|
||||
)
|
||||
|
||||
# Config file for custom purge paths
|
||||
# Config file for custom purge paths.
|
||||
readonly PURGE_CONFIG_FILE="$HOME/.config/mole/purge_paths"
|
||||
|
||||
# Global array to hold actual search paths
|
||||
# Resolved search paths.
|
||||
PURGE_SEARCH_PATHS=()
|
||||
|
||||
# Project indicator files (if a directory contains these, it's likely a project)
|
||||
# Project indicators for container detection.
|
||||
readonly PROJECT_INDICATORS=(
|
||||
"package.json"
|
||||
"Cargo.toml"
|
||||
@@ -68,12 +68,12 @@ readonly PROJECT_INDICATORS=(
|
||||
".git"
|
||||
)
|
||||
|
||||
# Check if a directory contains projects (directly or in subdirectories)
|
||||
# Check if a directory contains projects (directly or in subdirectories).
|
||||
is_project_container() {
|
||||
local dir="$1"
|
||||
local max_depth="${2:-2}"
|
||||
|
||||
# Skip hidden directories and system directories
|
||||
# Skip hidden/system directories.
|
||||
local basename
|
||||
basename=$(basename "$dir")
|
||||
[[ "$basename" == .* ]] && return 1
|
||||
@@ -84,7 +84,7 @@ is_project_container() {
|
||||
[[ "$basename" == "Pictures" ]] && return 1
|
||||
[[ "$basename" == "Public" ]] && return 1
|
||||
|
||||
# Build find expression with all indicators (single find call for efficiency)
|
||||
# Single find expression for indicators.
|
||||
local -a find_args=("$dir" "-maxdepth" "$max_depth" "(")
|
||||
local first=true
|
||||
for indicator in "${PROJECT_INDICATORS[@]}"; do
|
||||
@@ -97,7 +97,6 @@ is_project_container() {
|
||||
done
|
||||
find_args+=(")" "-print" "-quit")
|
||||
|
||||
# Single find call to check all indicators at once
|
||||
if find "${find_args[@]}" 2> /dev/null | grep -q .; then
|
||||
return 0
|
||||
fi
|
||||
@@ -105,24 +104,22 @@ is_project_container() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Discover project directories in $HOME
|
||||
# Discover project directories in $HOME.
|
||||
discover_project_dirs() {
|
||||
local -a discovered=()
|
||||
|
||||
# First check default paths that exist
|
||||
for path in "${DEFAULT_PURGE_SEARCH_PATHS[@]}"; do
|
||||
if [[ -d "$path" ]]; then
|
||||
discovered+=("$path")
|
||||
fi
|
||||
done
|
||||
|
||||
# Then scan $HOME for other project containers (depth 1)
|
||||
# Scan $HOME for other containers (depth 1).
|
||||
local dir
|
||||
for dir in "$HOME"/*/; do
|
||||
[[ ! -d "$dir" ]] && continue
|
||||
dir="${dir%/}" # Remove trailing slash
|
||||
|
||||
# Skip if already in defaults
|
||||
local already_found=false
|
||||
for existing in "${DEFAULT_PURGE_SEARCH_PATHS[@]}"; do
|
||||
if [[ "$dir" == "$existing" ]]; then
|
||||
@@ -132,17 +129,15 @@ discover_project_dirs() {
|
||||
done
|
||||
[[ "$already_found" == "true" ]] && continue
|
||||
|
||||
# Check if this directory contains projects
|
||||
if is_project_container "$dir" 2; then
|
||||
discovered+=("$dir")
|
||||
fi
|
||||
done
|
||||
|
||||
# Return unique paths
|
||||
printf '%s\n' "${discovered[@]}" | sort -u
|
||||
}
|
||||
|
||||
# Save discovered paths to config
|
||||
# Save discovered paths to config.
|
||||
save_discovered_paths() {
|
||||
local -a paths=("$@")
|
||||
|
||||
@@ -166,26 +161,20 @@ EOF
|
||||
load_purge_config() {
|
||||
PURGE_SEARCH_PATHS=()
|
||||
|
||||
# Try loading from config file
|
||||
if [[ -f "$PURGE_CONFIG_FILE" ]]; then
|
||||
while IFS= read -r line; do
|
||||
# Remove leading/trailing whitespace
|
||||
line="${line#"${line%%[![:space:]]*}"}"
|
||||
line="${line%"${line##*[![:space:]]}"}"
|
||||
|
||||
# Skip empty lines and comments
|
||||
[[ -z "$line" || "$line" =~ ^# ]] && continue
|
||||
|
||||
# Expand tilde to HOME
|
||||
line="${line/#\~/$HOME}"
|
||||
|
||||
PURGE_SEARCH_PATHS+=("$line")
|
||||
done < "$PURGE_CONFIG_FILE"
|
||||
fi
|
||||
|
||||
# If no paths loaded, auto-discover and save
|
||||
if [[ ${#PURGE_SEARCH_PATHS[@]} -eq 0 ]]; then
|
||||
# Show discovery message if running interactively
|
||||
if [[ -t 1 ]] && [[ -z "${_PURGE_DISCOVERY_SILENT:-}" ]]; then
|
||||
echo -e "${GRAY}First run: discovering project directories...${NC}" >&2
|
||||
fi
|
||||
@@ -197,47 +186,37 @@ load_purge_config() {
|
||||
|
||||
if [[ ${#discovered[@]} -gt 0 ]]; then
|
||||
PURGE_SEARCH_PATHS=("${discovered[@]}")
|
||||
# Save for next time
|
||||
save_discovered_paths "${discovered[@]}"
|
||||
|
||||
if [[ -t 1 ]] && [[ -z "${_PURGE_DISCOVERY_SILENT:-}" ]]; then
|
||||
echo -e "${GRAY}Found ${#discovered[@]} project directories, saved to config${NC}" >&2
|
||||
fi
|
||||
else
|
||||
# Fallback to defaults if nothing found
|
||||
PURGE_SEARCH_PATHS=("${DEFAULT_PURGE_SEARCH_PATHS[@]}")
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Initialize paths on script load
|
||||
# Initialize paths on script load.
|
||||
load_purge_config
|
||||
|
||||
# Args: $1 - path to check
|
||||
# Check if path is safe to clean (must be inside a project directory)
|
||||
# Safe cleanup requires the path be inside a project directory.
|
||||
is_safe_project_artifact() {
|
||||
local path="$1"
|
||||
local search_path="$2"
|
||||
# Path must be absolute
|
||||
if [[ "$path" != /* ]]; then
|
||||
return 1
|
||||
fi
|
||||
# Must not be a direct child of HOME directory
|
||||
# e.g., ~/.gradle is NOT safe, but ~/Projects/foo/.gradle IS safe
|
||||
# Must not be a direct child of the search root.
|
||||
local relative_path="${path#"$search_path"/}"
|
||||
local depth=$(echo "$relative_path" | tr -cd '/' | wc -c)
|
||||
# Require at least 1 level deep (inside a project folder)
|
||||
# e.g., ~/www/weekly/node_modules is OK (depth >= 1)
|
||||
# but ~/www/node_modules is NOT OK (depth < 1)
|
||||
if [[ $depth -lt 1 ]]; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
# Fast scan using fd or optimized find
|
||||
# Args: $1 - search path, $2 - output file
|
||||
# Args: $1 - search path, $2 - output file
|
||||
# Scan for purge targets using strict project boundary checks
|
||||
# Scan purge targets using fd (fast) or pruned find.
|
||||
scan_purge_targets() {
|
||||
local search_path="$1"
|
||||
local output_file="$2"
|
||||
@@ -255,7 +234,6 @@ scan_purge_targets() {
|
||||
if [[ ! -d "$search_path" ]]; then
|
||||
return
|
||||
fi
|
||||
# Use fd for fast parallel search if available
|
||||
if command -v fd > /dev/null 2>&1; then
|
||||
local fd_args=(
|
||||
"--absolute-path"
|
||||
@@ -273,47 +251,28 @@ scan_purge_targets() {
|
||||
for target in "${PURGE_TARGETS[@]}"; do
|
||||
fd_args+=("-g" "$target")
|
||||
done
|
||||
# Run fd command
|
||||
fd "${fd_args[@]}" . "$search_path" 2> /dev/null | while IFS= read -r item; do
|
||||
if is_safe_project_artifact "$item" "$search_path"; then
|
||||
echo "$item"
|
||||
fi
|
||||
done | filter_nested_artifacts > "$output_file"
|
||||
else
|
||||
# Fallback to optimized find with pruning
|
||||
# This prevents descending into heavily nested dirs like node_modules once found,
|
||||
# providing a massive speedup (O(project_dirs) vs O(files)).
|
||||
# Pruned find avoids descending into heavy directories.
|
||||
local prune_args=()
|
||||
# 1. Directories to prune (ignore completely)
|
||||
local prune_dirs=(".git" "Library" ".Trash" "Applications")
|
||||
for dir in "${prune_dirs[@]}"; do
|
||||
# -name "DIR" -prune -o
|
||||
prune_args+=("-name" "$dir" "-prune" "-o")
|
||||
done
|
||||
# 2. Targets to find (print AND prune)
|
||||
# If we find node_modules, we print it and STOP looking inside it
|
||||
for target in "${PURGE_TARGETS[@]}"; do
|
||||
# -name "TARGET" -print -prune -o
|
||||
prune_args+=("-name" "$target" "-print" "-prune" "-o")
|
||||
done
|
||||
# Run find command
|
||||
# Logic: ( prune_pattern -prune -o target_pattern -print -prune )
|
||||
# Note: We rely on implicit recursion for directories that don't match any pattern.
|
||||
# -print is only called explicitly on targets.
|
||||
# Removing the trailing -o from loop construction if necessary?
|
||||
# Actually my loop adds -o at the end. I need to handle that.
|
||||
# Let's verify the array construction.
|
||||
# Re-building args cleanly:
|
||||
local find_expr=()
|
||||
# Excludes
|
||||
for dir in "${prune_dirs[@]}"; do
|
||||
find_expr+=("-name" "$dir" "-prune" "-o")
|
||||
done
|
||||
# Targets
|
||||
local i=0
|
||||
for target in "${PURGE_TARGETS[@]}"; do
|
||||
find_expr+=("-name" "$target" "-print" "-prune")
|
||||
# Add -o unless it's the very last item of targets
|
||||
if [[ $i -lt $((${#PURGE_TARGETS[@]} - 1)) ]]; then
|
||||
find_expr+=("-o")
|
||||
fi
|
||||
@@ -327,15 +286,12 @@ scan_purge_targets() {
|
||||
done | filter_nested_artifacts > "$output_file"
|
||||
fi
|
||||
}
|
||||
# Filter out nested artifacts (e.g. node_modules inside node_modules)
|
||||
# Filter out nested artifacts (e.g. node_modules inside node_modules).
|
||||
filter_nested_artifacts() {
|
||||
while IFS= read -r item; do
|
||||
local parent_dir=$(dirname "$item")
|
||||
local is_nested=false
|
||||
for target in "${PURGE_TARGETS[@]}"; do
|
||||
# Check if parent directory IS a target or IS INSIDE a target
|
||||
# e.g. .../node_modules/foo/node_modules -> parent has node_modules
|
||||
# Use more strict matching to avoid false positives like "my_node_modules_backup"
|
||||
if [[ "$parent_dir" == *"/$target/"* || "$parent_dir" == *"/$target" ]]; then
|
||||
is_nested=true
|
||||
break
|
||||
@@ -347,14 +303,13 @@ filter_nested_artifacts() {
|
||||
done
|
||||
}
|
||||
# Args: $1 - path
|
||||
# Check if a path was modified recently (safety check)
|
||||
# Check if a path was modified recently (safety check).
|
||||
is_recently_modified() {
|
||||
local path="$1"
|
||||
local age_days=$MIN_AGE_DAYS
|
||||
if [[ ! -e "$path" ]]; then
|
||||
return 1
|
||||
fi
|
||||
# Get modification time using base.sh helper (handles GNU vs BSD stat)
|
||||
local mod_time
|
||||
mod_time=$(get_file_mtime "$path")
|
||||
local current_time=$(date +%s)
|
||||
@@ -367,7 +322,7 @@ is_recently_modified() {
|
||||
fi
|
||||
}
|
||||
# Args: $1 - path
|
||||
# Get human-readable size of directory
|
||||
# Get directory size in KB.
|
||||
get_dir_size_kb() {
|
||||
local path="$1"
|
||||
if [[ -d "$path" ]]; then
|
||||
@@ -376,10 +331,7 @@ get_dir_size_kb() {
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
# Simple category selector (for purge only)
|
||||
# Args: category names and metadata as arrays (passed via global vars)
|
||||
# Uses PURGE_RECENT_CATEGORIES to mark categories with recent items (default unselected)
|
||||
# Returns: selected indices in PURGE_SELECTION_RESULT (comma-separated)
|
||||
# Purge category selector.
|
||||
select_purge_categories() {
|
||||
local -a categories=("$@")
|
||||
local total_items=${#categories[@]}
|
||||
@@ -388,8 +340,7 @@ select_purge_categories() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Calculate items per page based on terminal height
|
||||
# Reserved: header(2) + blank(2) + footer(1) = 5 rows
|
||||
# Calculate items per page based on terminal height.
|
||||
_get_items_per_page() {
|
||||
local term_height=24
|
||||
if [[ -t 0 ]] || [[ -t 2 ]]; then
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
#!/bin/bash
|
||||
# System-Level Cleanup Module
|
||||
# Deep system cleanup (requires sudo) and Time Machine failed backups
|
||||
# System-Level Cleanup Module (requires sudo).
|
||||
set -euo pipefail
|
||||
# Deep system cleanup (requires sudo)
|
||||
# System caches, logs, and temp files.
|
||||
clean_deep_system() {
|
||||
stop_section_spinner
|
||||
# Clean old system caches
|
||||
local cache_cleaned=0
|
||||
safe_sudo_find_delete "/Library/Caches" "*.cache" "$MOLE_TEMP_FILE_AGE_DAYS" "f" && cache_cleaned=1 || true
|
||||
safe_sudo_find_delete "/Library/Caches" "*.tmp" "$MOLE_TEMP_FILE_AGE_DAYS" "f" && cache_cleaned=1 || true
|
||||
@@ -15,7 +13,6 @@ clean_deep_system() {
|
||||
safe_sudo_find_delete "/private/tmp" "*" "${MOLE_TEMP_FILE_AGE_DAYS}" "f" && tmp_cleaned=1 || true
|
||||
safe_sudo_find_delete "/private/var/tmp" "*" "${MOLE_TEMP_FILE_AGE_DAYS}" "f" && tmp_cleaned=1 || true
|
||||
[[ $tmp_cleaned -eq 1 ]] && log_success "System temp files"
|
||||
# Clean crash reports
|
||||
safe_sudo_find_delete "/Library/Logs/DiagnosticReports" "*" "$MOLE_CRASH_REPORT_AGE_DAYS" "f" || true
|
||||
log_success "System crash reports"
|
||||
safe_sudo_find_delete "/private/var/log" "*.log" "$MOLE_LOG_AGE_DAYS" "f" || true
|
||||
@@ -91,18 +88,15 @@ clean_deep_system() {
|
||||
stop_section_spinner
|
||||
[[ $diag_logs_cleaned -eq 1 ]] && log_success "System diagnostic trace logs"
|
||||
}
|
||||
# Clean incomplete Time Machine backups
|
||||
# Incomplete Time Machine backups.
|
||||
clean_time_machine_failed_backups() {
|
||||
local tm_cleaned=0
|
||||
# Check if tmutil is available
|
||||
if ! command -v tmutil > /dev/null 2>&1; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
|
||||
return 0
|
||||
fi
|
||||
# Start spinner early (before potentially slow tmutil command)
|
||||
start_section_spinner "Checking Time Machine configuration..."
|
||||
local spinner_active=true
|
||||
# Check if Time Machine is configured (with short timeout for faster response)
|
||||
local tm_info
|
||||
tm_info=$(run_with_timeout 2 tmutil destinationinfo 2>&1 || echo "failed")
|
||||
if [[ "$tm_info" == *"No destinations configured"* || "$tm_info" == "failed" ]]; then
|
||||
@@ -119,7 +113,6 @@ clean_time_machine_failed_backups() {
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
|
||||
return 0
|
||||
fi
|
||||
# Skip if backup is running (check actual Running status, not just daemon existence)
|
||||
if tmutil status 2> /dev/null | grep -q "Running = 1"; then
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_section_spinner
|
||||
@@ -127,22 +120,19 @@ clean_time_machine_failed_backups() {
|
||||
echo -e " ${YELLOW}!${NC} Time Machine backup in progress, skipping cleanup"
|
||||
return 0
|
||||
fi
|
||||
# Update spinner message for volume scanning
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
start_section_spinner "Checking backup volumes..."
|
||||
fi
|
||||
# Fast pre-scan: check which volumes have Backups.backupdb (avoid expensive tmutil checks)
|
||||
# Fast pre-scan for backup volumes to avoid slow tmutil checks.
|
||||
local -a backup_volumes=()
|
||||
for volume in /Volumes/*; do
|
||||
[[ -d "$volume" ]] || continue
|
||||
[[ "$volume" == "/Volumes/MacintoshHD" || "$volume" == "/" ]] && continue
|
||||
[[ -L "$volume" ]] && continue
|
||||
# Quick check: does this volume have backup directories?
|
||||
if [[ -d "$volume/Backups.backupdb" ]] || [[ -d "$volume/.MobileBackups" ]]; then
|
||||
backup_volumes+=("$volume")
|
||||
fi
|
||||
done
|
||||
# If no backup volumes found, stop spinner and return
|
||||
if [[ ${#backup_volumes[@]} -eq 0 ]]; then
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_section_spinner
|
||||
@@ -150,23 +140,20 @@ clean_time_machine_failed_backups() {
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
|
||||
return 0
|
||||
fi
|
||||
# Update spinner message: we have potential backup volumes, now scan them
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
start_section_spinner "Scanning backup volumes..."
|
||||
fi
|
||||
for volume in "${backup_volumes[@]}"; do
|
||||
# Skip network volumes (quick check)
|
||||
local fs_type
|
||||
fs_type=$(run_with_timeout 1 command df -T "$volume" 2> /dev/null | tail -1 | awk '{print $2}' || echo "unknown")
|
||||
case "$fs_type" in
|
||||
nfs | smbfs | afpfs | cifs | webdav | unknown) continue ;;
|
||||
esac
|
||||
# HFS+ style backups (Backups.backupdb)
|
||||
local backupdb_dir="$volume/Backups.backupdb"
|
||||
if [[ -d "$backupdb_dir" ]]; then
|
||||
while IFS= read -r inprogress_file; do
|
||||
[[ -d "$inprogress_file" ]] || continue
|
||||
# Only delete old incomplete backups (safety window)
|
||||
# Only delete old incomplete backups (safety window).
|
||||
local file_mtime=$(get_file_mtime "$inprogress_file")
|
||||
local current_time=$(date +%s)
|
||||
local hours_old=$(((current_time - file_mtime) / 3600))
|
||||
@@ -175,7 +162,6 @@ clean_time_machine_failed_backups() {
|
||||
fi
|
||||
local size_kb=$(get_path_size_kb "$inprogress_file")
|
||||
[[ "$size_kb" -le 0 ]] && continue
|
||||
# Stop spinner before first output
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_section_spinner
|
||||
spinner_active=false
|
||||
@@ -188,7 +174,6 @@ clean_time_machine_failed_backups() {
|
||||
note_activity
|
||||
continue
|
||||
fi
|
||||
# Real deletion
|
||||
if ! command -v tmutil > /dev/null 2>&1; then
|
||||
echo -e " ${YELLOW}!${NC} tmutil not available, skipping: $backup_name"
|
||||
continue
|
||||
@@ -205,17 +190,15 @@ clean_time_machine_failed_backups() {
|
||||
fi
|
||||
done < <(run_with_timeout 15 find "$backupdb_dir" -maxdepth 3 -type d \( -name "*.inProgress" -o -name "*.inprogress" \) 2> /dev/null || true)
|
||||
fi
|
||||
# APFS style backups (.backupbundle or .sparsebundle)
|
||||
# APFS bundles.
|
||||
for bundle in "$volume"/*.backupbundle "$volume"/*.sparsebundle; do
|
||||
[[ -e "$bundle" ]] || continue
|
||||
[[ -d "$bundle" ]] || continue
|
||||
# Check if bundle is mounted
|
||||
local bundle_name=$(basename "$bundle")
|
||||
local mounted_path=$(hdiutil info 2> /dev/null | grep -A 5 "image-path.*$bundle_name" | grep "/Volumes/" | awk '{print $1}' | head -1 || echo "")
|
||||
if [[ -n "$mounted_path" && -d "$mounted_path" ]]; then
|
||||
while IFS= read -r inprogress_file; do
|
||||
[[ -d "$inprogress_file" ]] || continue
|
||||
# Only delete old incomplete backups (safety window)
|
||||
local file_mtime=$(get_file_mtime "$inprogress_file")
|
||||
local current_time=$(date +%s)
|
||||
local hours_old=$(((current_time - file_mtime) / 3600))
|
||||
@@ -224,7 +207,6 @@ clean_time_machine_failed_backups() {
|
||||
fi
|
||||
local size_kb=$(get_path_size_kb "$inprogress_file")
|
||||
[[ "$size_kb" -le 0 ]] && continue
|
||||
# Stop spinner before first output
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_section_spinner
|
||||
spinner_active=false
|
||||
@@ -237,7 +219,6 @@ clean_time_machine_failed_backups() {
|
||||
note_activity
|
||||
continue
|
||||
fi
|
||||
# Real deletion
|
||||
if ! command -v tmutil > /dev/null 2>&1; then
|
||||
continue
|
||||
fi
|
||||
@@ -255,7 +236,6 @@ clean_time_machine_failed_backups() {
|
||||
fi
|
||||
done
|
||||
done
|
||||
# Stop spinner if still active (no backups found)
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
stop_section_spinner
|
||||
fi
|
||||
@@ -263,33 +243,27 @@ clean_time_machine_failed_backups() {
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
|
||||
fi
|
||||
}
|
||||
# Clean local APFS snapshots (keep the most recent snapshot)
|
||||
# Local APFS snapshots (keep the most recent).
|
||||
clean_local_snapshots() {
|
||||
# Check if tmutil is available
|
||||
if ! command -v tmutil > /dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
start_section_spinner "Checking local snapshots..."
|
||||
# Check for local snapshots
|
||||
local snapshot_list
|
||||
snapshot_list=$(tmutil listlocalsnapshots / 2> /dev/null)
|
||||
stop_section_spinner
|
||||
[[ -z "$snapshot_list" ]] && return 0
|
||||
# Parse and clean snapshots
|
||||
local cleaned_count=0
|
||||
local total_cleaned_size=0 # Estimation not possible without thin
|
||||
local newest_ts=0
|
||||
local newest_name=""
|
||||
local -a snapshots=()
|
||||
# Find the most recent snapshot to keep at least one version
|
||||
while IFS= read -r line; do
|
||||
# Format: com.apple.TimeMachine.2023-10-25-120000
|
||||
if [[ "$line" =~ com\.apple\.TimeMachine\.([0-9]{4})-([0-9]{2})-([0-9]{2})-([0-9]{6}) ]]; then
|
||||
local snap_name="${BASH_REMATCH[0]}"
|
||||
snapshots+=("$snap_name")
|
||||
local date_str="${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]} ${BASH_REMATCH[4]:0:2}:${BASH_REMATCH[4]:2:2}:${BASH_REMATCH[4]:4:2}"
|
||||
local snap_ts=$(date -j -f "%Y-%m-%d %H:%M:%S" "$date_str" "+%s" 2> /dev/null || echo "0")
|
||||
# Skip if parsing failed
|
||||
[[ "$snap_ts" == "0" ]] && continue
|
||||
if [[ "$snap_ts" -gt "$newest_ts" ]]; then
|
||||
newest_ts="$snap_ts"
|
||||
@@ -332,16 +306,13 @@ clean_local_snapshots() {
|
||||
|
||||
local snap_name
|
||||
for snap_name in "${snapshots[@]}"; do
|
||||
# Format: com.apple.TimeMachine.2023-10-25-120000
|
||||
if [[ "$snap_name" =~ com\.apple\.TimeMachine\.([0-9]{4})-([0-9]{2})-([0-9]{2})-([0-9]{6}) ]]; then
|
||||
# Remove all but the most recent snapshot
|
||||
if [[ "${BASH_REMATCH[0]}" != "$newest_name" ]]; then
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Local snapshot: $snap_name ${YELLOW}dry-run${NC}"
|
||||
((cleaned_count++))
|
||||
note_activity
|
||||
else
|
||||
# Secure removal
|
||||
if sudo tmutil deletelocalsnapshots "${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]}-${BASH_REMATCH[4]}" > /dev/null 2>&1; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed snapshot: $snap_name"
|
||||
((cleaned_count++))
|
||||
|
||||
@@ -62,7 +62,7 @@ scan_external_volumes() {
|
||||
done
|
||||
stop_section_spinner
|
||||
}
|
||||
# Clean Finder metadata (.DS_Store files)
|
||||
# Finder metadata (.DS_Store).
|
||||
clean_finder_metadata() {
|
||||
stop_section_spinner
|
||||
if [[ "$PROTECT_FINDER_METADATA" == "true" ]]; then
|
||||
@@ -72,16 +72,14 @@ clean_finder_metadata() {
|
||||
fi
|
||||
clean_ds_store_tree "$HOME" "Home directory (.DS_Store)"
|
||||
}
|
||||
# Clean macOS system caches
|
||||
# macOS system caches and user-level leftovers.
|
||||
clean_macos_system_caches() {
|
||||
stop_section_spinner
|
||||
# Clean saved application states with protection for System Settings
|
||||
# Note: safe_clean already calls should_protect_path for each file
|
||||
# safe_clean already checks protected paths.
|
||||
safe_clean ~/Library/Saved\ Application\ State/* "Saved application states" || true
|
||||
safe_clean ~/Library/Caches/com.apple.photoanalysisd "Photo analysis cache" || true
|
||||
safe_clean ~/Library/Caches/com.apple.akd "Apple ID cache" || true
|
||||
safe_clean ~/Library/Caches/com.apple.WebKit.Networking/* "WebKit network cache" || true
|
||||
# Extra user items
|
||||
safe_clean ~/Library/DiagnosticReports/* "Diagnostic reports" || true
|
||||
safe_clean ~/Library/Caches/com.apple.QuickLook.thumbnailcache "QuickLook thumbnails" || true
|
||||
safe_clean ~/Library/Caches/Quick\ Look/* "QuickLook cache" || true
|
||||
@@ -158,28 +156,26 @@ clean_mail_downloads() {
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
# Clean sandboxed app caches
|
||||
# Sandboxed app caches.
|
||||
clean_sandboxed_app_caches() {
|
||||
stop_section_spinner
|
||||
safe_clean ~/Library/Containers/com.apple.wallpaper.agent/Data/Library/Caches/* "Wallpaper agent cache"
|
||||
safe_clean ~/Library/Containers/com.apple.mediaanalysisd/Data/Library/Caches/* "Media analysis cache"
|
||||
safe_clean ~/Library/Containers/com.apple.AppStore/Data/Library/Caches/* "App Store cache"
|
||||
safe_clean ~/Library/Containers/com.apple.configurator.xpc.InternetService/Data/tmp/* "Apple Configurator temp files"
|
||||
# Clean sandboxed app caches - iterate quietly to avoid UI flashing
|
||||
local containers_dir="$HOME/Library/Containers"
|
||||
[[ ! -d "$containers_dir" ]] && return 0
|
||||
start_section_spinner "Scanning sandboxed apps..."
|
||||
local total_size=0
|
||||
local cleaned_count=0
|
||||
local found_any=false
|
||||
# Enable nullglob for safe globbing; restore afterwards
|
||||
# Use nullglob to avoid literal globs.
|
||||
local _ng_state
|
||||
_ng_state=$(shopt -p nullglob || true)
|
||||
shopt -s nullglob
|
||||
for container_dir in "$containers_dir"/*; do
|
||||
process_container_cache "$container_dir"
|
||||
done
|
||||
# Restore nullglob to previous state
|
||||
eval "$_ng_state"
|
||||
stop_section_spinner
|
||||
if [[ "$found_any" == "true" ]]; then
|
||||
@@ -195,11 +191,10 @@ clean_sandboxed_app_caches() {
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
# Process a single container cache directory (reduces nesting)
|
||||
# Process a single container cache directory.
|
||||
process_container_cache() {
|
||||
local container_dir="$1"
|
||||
[[ -d "$container_dir" ]] || return 0
|
||||
# Extract bundle ID and check protection status early
|
||||
local bundle_id=$(basename "$container_dir")
|
||||
if is_critical_system_component "$bundle_id"; then
|
||||
return 0
|
||||
@@ -208,17 +203,15 @@ process_container_cache() {
|
||||
return 0
|
||||
fi
|
||||
local cache_dir="$container_dir/Data/Library/Caches"
|
||||
# Check if dir exists and has content
|
||||
[[ -d "$cache_dir" ]] || return 0
|
||||
# Fast check if empty using find (more efficient than ls)
|
||||
# Fast non-empty check.
|
||||
if find "$cache_dir" -mindepth 1 -maxdepth 1 -print -quit 2> /dev/null | grep -q .; then
|
||||
# Use global variables from caller for tracking
|
||||
local size=$(get_path_size_kb "$cache_dir")
|
||||
((total_size += size))
|
||||
found_any=true
|
||||
((cleaned_count++))
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
# Clean contents safely with local nullglob management
|
||||
# Clean contents safely with local nullglob.
|
||||
local _ng_state
|
||||
_ng_state=$(shopt -p nullglob || true)
|
||||
shopt -s nullglob
|
||||
@@ -230,11 +223,11 @@ process_container_cache() {
|
||||
fi
|
||||
fi
|
||||
}
|
||||
# Clean browser caches (Safari, Chrome, Edge, Firefox, etc.)
|
||||
# Browser caches (Safari/Chrome/Edge/Firefox).
|
||||
clean_browsers() {
|
||||
stop_section_spinner
|
||||
safe_clean ~/Library/Caches/com.apple.Safari/* "Safari cache"
|
||||
# Chrome/Chromium
|
||||
# Chrome/Chromium.
|
||||
safe_clean ~/Library/Caches/Google/Chrome/* "Chrome cache"
|
||||
safe_clean ~/Library/Application\ Support/Google/Chrome/*/Application\ Cache/* "Chrome app cache"
|
||||
safe_clean ~/Library/Application\ Support/Google/Chrome/*/GPUCache/* "Chrome GPU cache"
|
||||
@@ -251,7 +244,7 @@ clean_browsers() {
|
||||
safe_clean ~/Library/Caches/zen/* "Zen cache"
|
||||
safe_clean ~/Library/Application\ Support/Firefox/Profiles/*/cache2/* "Firefox profile cache"
|
||||
}
|
||||
# Clean cloud storage app caches
|
||||
# Cloud storage caches.
|
||||
clean_cloud_storage() {
|
||||
stop_section_spinner
|
||||
safe_clean ~/Library/Caches/com.dropbox.* "Dropbox cache"
|
||||
@@ -262,7 +255,7 @@ clean_cloud_storage() {
|
||||
safe_clean ~/Library/Caches/com.box.desktop "Box cache"
|
||||
safe_clean ~/Library/Caches/com.microsoft.OneDrive "OneDrive cache"
|
||||
}
|
||||
# Clean office application caches
|
||||
# Office app caches.
|
||||
clean_office_applications() {
|
||||
stop_section_spinner
|
||||
safe_clean ~/Library/Caches/com.microsoft.Word "Microsoft Word cache"
|
||||
@@ -274,7 +267,7 @@ clean_office_applications() {
|
||||
safe_clean ~/Library/Caches/org.mozilla.thunderbird/* "Thunderbird cache"
|
||||
safe_clean ~/Library/Caches/com.apple.mail/* "Apple Mail cache"
|
||||
}
|
||||
# Clean virtualization tools
|
||||
# Virtualization caches.
|
||||
clean_virtualization_tools() {
|
||||
stop_section_spinner
|
||||
safe_clean ~/Library/Caches/com.vmware.fusion "VMware Fusion cache"
|
||||
@@ -282,7 +275,7 @@ clean_virtualization_tools() {
|
||||
safe_clean ~/VirtualBox\ VMs/.cache "VirtualBox cache"
|
||||
safe_clean ~/.vagrant.d/tmp/* "Vagrant temporary files"
|
||||
}
|
||||
# Clean Application Support logs and caches
|
||||
# Application Support logs/caches.
|
||||
clean_application_support_logs() {
|
||||
stop_section_spinner
|
||||
if [[ ! -d "$HOME/Library/Application Support" ]] || ! ls "$HOME/Library/Application Support" > /dev/null 2>&1; then
|
||||
@@ -294,11 +287,10 @@ clean_application_support_logs() {
|
||||
local total_size=0
|
||||
local cleaned_count=0
|
||||
local found_any=false
|
||||
# Enable nullglob for safe globbing
|
||||
# Enable nullglob for safe globbing.
|
||||
local _ng_state
|
||||
_ng_state=$(shopt -p nullglob || true)
|
||||
shopt -s nullglob
|
||||
# Clean log directories and cache patterns
|
||||
for app_dir in ~/Library/Application\ Support/*; do
|
||||
[[ -d "$app_dir" ]] || continue
|
||||
local app_name=$(basename "$app_dir")
|
||||
@@ -333,7 +325,7 @@ clean_application_support_logs() {
|
||||
fi
|
||||
done
|
||||
done
|
||||
# Clean Group Containers logs
|
||||
# Group Containers logs (explicit allowlist).
|
||||
local known_group_containers=(
|
||||
"group.com.apple.contentdelivery"
|
||||
)
|
||||
@@ -357,7 +349,6 @@ clean_application_support_logs() {
|
||||
fi
|
||||
done
|
||||
done
|
||||
# Restore nullglob to previous state
|
||||
eval "$_ng_state"
|
||||
stop_section_spinner
|
||||
if [[ "$found_any" == "true" ]]; then
|
||||
@@ -373,10 +364,10 @@ clean_application_support_logs() {
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
# Check and show iOS device backup info
|
||||
# iOS device backup info.
|
||||
check_ios_device_backups() {
|
||||
local backup_dir="$HOME/Library/Application Support/MobileSync/Backup"
|
||||
# Simplified check without find to avoid hanging
|
||||
# Simplified check without find to avoid hanging.
|
||||
if [[ -d "$backup_dir" ]]; then
|
||||
local backup_kb=$(get_path_size_kb "$backup_dir")
|
||||
if [[ -n "${backup_kb:-}" && "$backup_kb" -gt 102400 ]]; then
|
||||
@@ -390,8 +381,7 @@ check_ios_device_backups() {
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
# Env: IS_M_SERIES
|
||||
# Clean Apple Silicon specific caches
|
||||
# Apple Silicon specific caches (IS_M_SERIES).
|
||||
clean_apple_silicon_caches() {
|
||||
if [[ "${IS_M_SERIES:-false}" != "true" ]]; then
|
||||
return 0
|
||||
|
||||
@@ -12,9 +12,7 @@ readonly MOLE_APP_PROTECTION_LOADED=1
|
||||
_MOLE_CORE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
[[ -z "${MOLE_BASE_LOADED:-}" ]] && source "$_MOLE_CORE_DIR/base.sh"
|
||||
|
||||
# ============================================================================
|
||||
# Application Management
|
||||
# ============================================================================
|
||||
|
||||
# Critical system components protected from uninstallation
|
||||
readonly SYSTEM_CRITICAL_BUNDLES=(
|
||||
@@ -70,9 +68,7 @@ readonly SYSTEM_CRITICAL_BUNDLES=(
|
||||
|
||||
# Applications with sensitive data; protected during cleanup but removable
|
||||
readonly DATA_PROTECTED_BUNDLES=(
|
||||
# ============================================================================
|
||||
# System Utilities & Cleanup Tools
|
||||
# ============================================================================
|
||||
"com.nektony.*" # App Cleaner & Uninstaller
|
||||
"com.macpaw.*" # CleanMyMac, CleanMaster
|
||||
"com.freemacsoft.AppCleaner" # AppCleaner
|
||||
@@ -82,9 +78,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"com.grandperspectiv.*" # GrandPerspective
|
||||
"com.binaryfruit.*" # FusionCast
|
||||
|
||||
# ============================================================================
|
||||
# Password Managers & Security
|
||||
# ============================================================================
|
||||
"com.1password.*" # 1Password
|
||||
"com.agilebits.*" # 1Password legacy
|
||||
"com.lastpass.*" # LastPass
|
||||
@@ -95,9 +89,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"com.authy.*" # Authy
|
||||
"com.yubico.*" # YubiKey Manager
|
||||
|
||||
# ============================================================================
|
||||
# Development Tools - IDEs & Editors
|
||||
# ============================================================================
|
||||
"com.jetbrains.*" # JetBrains IDEs (IntelliJ, DataGrip, etc.)
|
||||
"JetBrains*" # JetBrains Application Support folders
|
||||
"com.microsoft.VSCode" # Visual Studio Code
|
||||
@@ -112,9 +104,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"abnerworks.Typora" # Typora (Markdown editor)
|
||||
"com.uranusjr.macdown" # MacDown
|
||||
|
||||
# ============================================================================
|
||||
# AI & LLM Tools
|
||||
# ============================================================================
|
||||
"com.todesktop.*" # Cursor (often uses generic todesktop ID)
|
||||
"Cursor" # Cursor App Support
|
||||
"com.anthropic.claude*" # Claude
|
||||
@@ -136,9 +126,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"com.quora.poe.electron" # Poe
|
||||
"chat.openai.com.*" # OpenAI web wrappers
|
||||
|
||||
# ============================================================================
|
||||
# Development Tools - Database Clients
|
||||
# ============================================================================
|
||||
"com.sequelpro.*" # Sequel Pro
|
||||
"com.sequel-ace.*" # Sequel Ace
|
||||
"com.tinyapp.*" # TablePlus
|
||||
@@ -151,9 +139,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"com.valentina-db.Valentina-Studio" # Valentina Studio
|
||||
"com.dbvis.DbVisualizer" # DbVisualizer
|
||||
|
||||
# ============================================================================
|
||||
# Development Tools - API & Network
|
||||
# ============================================================================
|
||||
"com.postmanlabs.mac" # Postman
|
||||
"com.konghq.insomnia" # Insomnia
|
||||
"com.CharlesProxy.*" # Charles Proxy
|
||||
@@ -164,9 +150,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"com.telerik.Fiddler" # Fiddler
|
||||
"com.usebruno.app" # Bruno (API client)
|
||||
|
||||
# ============================================================================
|
||||
# Network Proxy & VPN Tools (pattern-based protection)
|
||||
# ============================================================================
|
||||
# Clash variants
|
||||
"*clash*" # All Clash variants (ClashX, ClashX Pro, Clash Verge, etc)
|
||||
"*Clash*" # Capitalized variants
|
||||
@@ -217,9 +201,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"*Fliqlo*" # Fliqlo screensaver (all case variants)
|
||||
"*fliqlo*" # Fliqlo lowercase
|
||||
|
||||
# ============================================================================
|
||||
# Development Tools - Git & Version Control
|
||||
# ============================================================================
|
||||
"com.github.GitHubDesktop" # GitHub Desktop
|
||||
"com.sublimemerge" # Sublime Merge
|
||||
"com.torusknot.SourceTreeNotMAS" # SourceTree
|
||||
@@ -229,9 +211,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"com.fork.Fork" # Fork
|
||||
"com.axosoft.gitkraken" # GitKraken
|
||||
|
||||
# ============================================================================
|
||||
# Development Tools - Terminal & Shell
|
||||
# ============================================================================
|
||||
"com.googlecode.iterm2" # iTerm2
|
||||
"net.kovidgoyal.kitty" # Kitty
|
||||
"io.alacritty" # Alacritty
|
||||
@@ -242,9 +222,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"dev.warp.Warp-Stable" # Warp
|
||||
"com.termius-dmg" # Termius (SSH client)
|
||||
|
||||
# ============================================================================
|
||||
# Development Tools - Docker & Virtualization
|
||||
# ============================================================================
|
||||
"com.docker.docker" # Docker Desktop
|
||||
"com.getutm.UTM" # UTM
|
||||
"com.vmware.fusion" # VMware Fusion
|
||||
@@ -253,9 +231,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"com.vagrant.*" # Vagrant
|
||||
"com.orbstack.OrbStack" # OrbStack
|
||||
|
||||
# ============================================================================
|
||||
# System Monitoring & Performance
|
||||
# ============================================================================
|
||||
"com.bjango.istatmenus*" # iStat Menus
|
||||
"eu.exelban.Stats" # Stats
|
||||
"com.monitorcontrol.*" # MonitorControl
|
||||
@@ -264,9 +240,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"com.activity-indicator.app" # Activity Indicator
|
||||
"net.cindori.sensei" # Sensei
|
||||
|
||||
# ============================================================================
|
||||
# Window Management & Productivity
|
||||
# ============================================================================
|
||||
"com.macitbetter.*" # BetterTouchTool, BetterSnapTool
|
||||
"com.hegenberg.*" # BetterTouchTool legacy
|
||||
"com.manytricks.*" # Moom, Witch, Name Mangler, Resolutionator
|
||||
@@ -284,9 +258,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"com.gaosun.eul" # eul (system monitor)
|
||||
"com.pointum.hazeover" # HazeOver
|
||||
|
||||
# ============================================================================
|
||||
# Launcher & Automation
|
||||
# ============================================================================
|
||||
"com.runningwithcrayons.Alfred" # Alfred
|
||||
"com.raycast.macos" # Raycast
|
||||
"com.blacktree.Quicksilver" # Quicksilver
|
||||
@@ -297,9 +269,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"org.pqrs.Karabiner-Elements" # Karabiner-Elements
|
||||
"com.apple.Automator" # Automator (system, but keep user workflows)
|
||||
|
||||
# ============================================================================
|
||||
# Note-Taking & Documentation
|
||||
# ============================================================================
|
||||
"com.bear-writer.*" # Bear
|
||||
"com.typora.*" # Typora
|
||||
"com.ulyssesapp.*" # Ulysses
|
||||
@@ -318,9 +288,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"com.reflect.ReflectApp" # Reflect
|
||||
"com.inkdrop.*" # Inkdrop
|
||||
|
||||
# ============================================================================
|
||||
# Design & Creative Tools
|
||||
# ============================================================================
|
||||
"com.adobe.*" # Adobe Creative Suite
|
||||
"com.bohemiancoding.*" # Sketch
|
||||
"com.figma.*" # Figma
|
||||
@@ -338,9 +306,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"com.autodesk.*" # Autodesk products
|
||||
"com.sketchup.*" # SketchUp
|
||||
|
||||
# ============================================================================
|
||||
# Communication & Collaboration
|
||||
# ============================================================================
|
||||
"com.tencent.xinWeChat" # WeChat (Chinese users)
|
||||
"com.tencent.qq" # QQ
|
||||
"com.alibaba.DingTalkMac" # DingTalk
|
||||
@@ -363,9 +329,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"com.postbox-inc.postbox" # Postbox
|
||||
"com.tinyspeck.slackmacgap" # Slack legacy
|
||||
|
||||
# ============================================================================
|
||||
# Task Management & Productivity
|
||||
# ============================================================================
|
||||
"com.omnigroup.OmniFocus*" # OmniFocus
|
||||
"com.culturedcode.*" # Things
|
||||
"com.todoist.*" # Todoist
|
||||
@@ -380,9 +344,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"com.notion.id" # Notion (also note-taking)
|
||||
"com.linear.linear" # Linear
|
||||
|
||||
# ============================================================================
|
||||
# File Transfer & Sync
|
||||
# ============================================================================
|
||||
"com.panic.transmit*" # Transmit (FTP/SFTP)
|
||||
"com.binarynights.ForkLift*" # ForkLift
|
||||
"com.noodlesoft.Hazel" # Hazel
|
||||
@@ -391,9 +353,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"com.apple.Xcode.CloudDocuments" # Xcode Cloud Documents
|
||||
"com.synology.*" # Synology apps
|
||||
|
||||
# ============================================================================
|
||||
# Cloud Storage & Backup (Issue #204)
|
||||
# ============================================================================
|
||||
"com.dropbox.*" # Dropbox
|
||||
"com.getdropbox.*" # Dropbox legacy
|
||||
"*dropbox*" # Dropbox helpers/updaters
|
||||
@@ -420,9 +380,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"com.shirtpocket.*" # SuperDuper backup
|
||||
"homebrew.mxcl.*" # Homebrew services
|
||||
|
||||
# ============================================================================
|
||||
# Screenshot & Recording
|
||||
# ============================================================================
|
||||
"com.cleanshot.*" # CleanShot X
|
||||
"com.xnipapp.xnip" # Xnip
|
||||
"com.reincubate.camo" # Camo
|
||||
@@ -436,9 +394,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"com.linebreak.CloudApp" # CloudApp
|
||||
"com.droplr.droplr-mac" # Droplr
|
||||
|
||||
# ============================================================================
|
||||
# Media & Entertainment
|
||||
# ============================================================================
|
||||
"com.spotify.client" # Spotify
|
||||
"com.apple.Music" # Apple Music
|
||||
"com.apple.podcasts" # Apple Podcasts
|
||||
@@ -456,9 +412,7 @@ readonly DATA_PROTECTED_BUNDLES=(
|
||||
"tv.plex.player.desktop" # Plex
|
||||
"com.netease.163music" # NetEase Music
|
||||
|
||||
# ============================================================================
|
||||
# License Management & App Stores
|
||||
# ============================================================================
|
||||
"com.paddle.Paddle*" # Paddle (license management)
|
||||
"com.setapp.DesktopClient" # Setapp
|
||||
"com.devmate.*" # DevMate (license framework)
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
#!/bin/bash
|
||||
# System Configuration Maintenance Module
|
||||
# Fix broken preferences and broken login items
|
||||
# System Configuration Maintenance Module.
|
||||
# Fix broken preferences and login items.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ============================================================================
|
||||
# Broken Preferences Detection and Cleanup
|
||||
# Find and remove corrupted .plist files
|
||||
# ============================================================================
|
||||
|
||||
# Clean corrupted preference files
|
||||
# Remove corrupted preference files.
|
||||
fix_broken_preferences() {
|
||||
local prefs_dir="$HOME/Library/Preferences"
|
||||
[[ -d "$prefs_dir" ]] || return 0
|
||||
|
||||
local broken_count=0
|
||||
|
||||
# Check main preferences directory
|
||||
while IFS= read -r plist_file; do
|
||||
[[ -f "$plist_file" ]] || continue
|
||||
|
||||
# Skip system preferences
|
||||
local filename
|
||||
filename=$(basename "$plist_file")
|
||||
case "$filename" in
|
||||
@@ -29,15 +22,13 @@ fix_broken_preferences() {
|
||||
;;
|
||||
esac
|
||||
|
||||
# Validate plist using plutil
|
||||
plutil -lint "$plist_file" > /dev/null 2>&1 && continue
|
||||
|
||||
# Remove broken plist
|
||||
safe_remove "$plist_file" true > /dev/null 2>&1 || true
|
||||
((broken_count++))
|
||||
done < <(command find "$prefs_dir" -maxdepth 1 -name "*.plist" -type f 2> /dev/null || true)
|
||||
|
||||
# Check ByHost preferences with timeout protection
|
||||
# Check ByHost preferences.
|
||||
local byhost_dir="$prefs_dir/ByHost"
|
||||
if [[ -d "$byhost_dir" ]]; then
|
||||
while IFS= read -r plist_file; do
|
||||
|
||||
@@ -3,16 +3,12 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration constants
|
||||
# MOLE_TM_THIN_TIMEOUT: Max seconds to wait for tmutil thinning (default: 180)
|
||||
# MOLE_TM_THIN_VALUE: Bytes to thin for local snapshots (default: 9999999999)
|
||||
# MOLE_MAIL_DOWNLOADS_MIN_KB: Minimum size in KB before cleaning Mail attachments (default: 5120)
|
||||
# MOLE_MAIL_AGE_DAYS: Minimum age in days for Mail attachments to be cleaned (default: 30)
|
||||
# Config constants (override via env).
|
||||
readonly MOLE_TM_THIN_TIMEOUT=180
|
||||
readonly MOLE_TM_THIN_VALUE=9999999999
|
||||
readonly MOLE_SQLITE_MAX_SIZE=104857600 # 100MB
|
||||
|
||||
# Helper function to get appropriate icon and color for dry-run mode
|
||||
# Dry-run aware output.
|
||||
opt_msg() {
|
||||
local message="$1"
|
||||
if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
|
||||
@@ -92,7 +88,6 @@ is_memory_pressure_high() {
|
||||
}
|
||||
|
||||
flush_dns_cache() {
|
||||
# Skip actual flush in dry-run mode
|
||||
if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
|
||||
MOLE_DNS_FLUSHED=1
|
||||
return 0
|
||||
@@ -105,7 +100,7 @@ flush_dns_cache() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Rebuild databases and flush caches
|
||||
# Basic system maintenance.
|
||||
opt_system_maintenance() {
|
||||
if flush_dns_cache; then
|
||||
opt_msg "DNS cache flushed"
|
||||
@@ -120,10 +115,8 @@ opt_system_maintenance() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Refresh Finder caches (QuickLook and icon services)
|
||||
# Note: Safari caches are cleaned separately in clean/user.sh, so excluded here
|
||||
# Refresh Finder caches (QuickLook/icon services).
|
||||
opt_cache_refresh() {
|
||||
# Skip qlmanage commands in dry-run mode
|
||||
if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then
|
||||
qlmanage -r cache > /dev/null 2>&1 || true
|
||||
qlmanage -r > /dev/null 2>&1 || true
|
||||
@@ -151,7 +144,7 @@ opt_cache_refresh() {
|
||||
|
||||
# Removed: opt_radio_refresh - Interrupts active user connections (WiFi, Bluetooth), degrading UX
|
||||
|
||||
# Saved state: remove OLD app saved states (7+ days)
|
||||
# Old saved states cleanup.
|
||||
opt_saved_state_cleanup() {
|
||||
local state_dir="$HOME/Library/Saved Application State"
|
||||
|
||||
@@ -193,7 +186,7 @@ opt_fix_broken_configs() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Network cache optimization
|
||||
# DNS cache refresh.
|
||||
opt_network_optimization() {
|
||||
if [[ "${MOLE_DNS_FLUSHED:-0}" == "1" ]]; then
|
||||
opt_msg "DNS cache already refreshed"
|
||||
@@ -209,8 +202,7 @@ opt_network_optimization() {
|
||||
fi
|
||||
}
|
||||
|
||||
# SQLite database vacuum optimization
|
||||
# Compresses and optimizes SQLite databases for Mail, Messages, Safari
|
||||
# SQLite vacuum for Mail/Messages/Safari (safety checks applied).
|
||||
opt_sqlite_vacuum() {
|
||||
if ! command -v sqlite3 > /dev/null 2>&1; then
|
||||
echo -e " ${GRAY}-${NC} Database optimization already optimal (sqlite3 unavailable)"
|
||||
@@ -254,15 +246,13 @@ opt_sqlite_vacuum() {
|
||||
[[ ! -f "$db_file" ]] && continue
|
||||
[[ "$db_file" == *"-wal" || "$db_file" == *"-shm" ]] && continue
|
||||
|
||||
# Skip if protected
|
||||
should_protect_path "$db_file" && continue
|
||||
|
||||
# Verify it's a SQLite database
|
||||
if ! file "$db_file" 2> /dev/null | grep -q "SQLite"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Safety check 1: Skip large databases (>100MB) to avoid timeouts
|
||||
# Skip large DBs (>100MB).
|
||||
local file_size
|
||||
file_size=$(get_file_size "$db_file")
|
||||
if [[ "$file_size" -gt "$MOLE_SQLITE_MAX_SIZE" ]]; then
|
||||
@@ -270,7 +260,7 @@ opt_sqlite_vacuum() {
|
||||
continue
|
||||
fi
|
||||
|
||||
# Safety check 2: Skip if freelist is tiny (already compact)
|
||||
# Skip if freelist is tiny (already compact).
|
||||
local page_info=""
|
||||
page_info=$(run_with_timeout 5 sqlite3 "$db_file" "PRAGMA page_count; PRAGMA freelist_count;" 2> /dev/null || echo "")
|
||||
local page_count=""
|
||||
@@ -284,7 +274,7 @@ opt_sqlite_vacuum() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Safety check 3: Verify database integrity before VACUUM
|
||||
# Verify integrity before VACUUM.
|
||||
if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then
|
||||
local integrity_check=""
|
||||
set +e
|
||||
@@ -292,14 +282,12 @@ opt_sqlite_vacuum() {
|
||||
local integrity_status=$?
|
||||
set -e
|
||||
|
||||
# Skip if integrity check failed or database is corrupted
|
||||
if [[ $integrity_status -ne 0 ]] || ! echo "$integrity_check" | grep -q "ok"; then
|
||||
((skipped++))
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
# Try to vacuum (skip in dry-run mode)
|
||||
local exit_code=0
|
||||
if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then
|
||||
set +e
|
||||
@@ -315,7 +303,6 @@ opt_sqlite_vacuum() {
|
||||
((failed++))
|
||||
fi
|
||||
else
|
||||
# In dry-run mode, just count the database
|
||||
((vacuumed++))
|
||||
fi
|
||||
done < <(compgen -G "$pattern" || true)
|
||||
@@ -346,8 +333,7 @@ opt_sqlite_vacuum() {
|
||||
fi
|
||||
}
|
||||
|
||||
# LaunchServices database rebuild
|
||||
# Fixes "Open with" menu issues, duplicate apps, broken file associations
|
||||
# LaunchServices rebuild ("Open with" issues).
|
||||
opt_launch_services_rebuild() {
|
||||
if [[ -t 1 ]]; then
|
||||
start_inline_spinner ""
|
||||
@@ -358,7 +344,6 @@ opt_launch_services_rebuild() {
|
||||
if [[ -f "$lsregister" ]]; then
|
||||
local success=0
|
||||
|
||||
# Skip actual rebuild in dry-run mode
|
||||
if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then
|
||||
set +e
|
||||
"$lsregister" -r -domain local -domain user -domain system > /dev/null 2>&1
|
||||
@@ -369,7 +354,7 @@ opt_launch_services_rebuild() {
|
||||
fi
|
||||
set -e
|
||||
else
|
||||
success=0 # Assume success in dry-run mode
|
||||
success=0
|
||||
fi
|
||||
|
||||
if [[ -t 1 ]]; then
|
||||
@@ -390,18 +375,16 @@ opt_launch_services_rebuild() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Font cache rebuild
|
||||
# Fixes font rendering issues, missing fonts, and character display problems
|
||||
# Font cache rebuild.
|
||||
opt_font_cache_rebuild() {
|
||||
local success=false
|
||||
|
||||
# Skip actual font cache removal in dry-run mode
|
||||
if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then
|
||||
if sudo atsutil databases -remove > /dev/null 2>&1; then
|
||||
success=true
|
||||
fi
|
||||
else
|
||||
success=true # Assume success in dry-run mode
|
||||
success=true
|
||||
fi
|
||||
|
||||
if [[ "$success" == "true" ]]; then
|
||||
@@ -417,8 +400,7 @@ opt_font_cache_rebuild() {
|
||||
# - opt_dyld_cache_update: Low benefit, time-consuming, auto-managed by macOS
|
||||
# - opt_system_services_refresh: Risk of data loss when killing system services
|
||||
|
||||
# Memory pressure relief
|
||||
# Clears inactive memory and disk cache to improve system responsiveness
|
||||
# Memory pressure relief.
|
||||
opt_memory_pressure_relief() {
|
||||
if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then
|
||||
if ! is_memory_pressure_high; then
|
||||
@@ -438,8 +420,7 @@ opt_memory_pressure_relief() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Network stack optimization
|
||||
# Flushes routing table and ARP cache to resolve network issues
|
||||
# Network stack reset (route + ARP).
|
||||
opt_network_stack_optimize() {
|
||||
local route_flushed="false"
|
||||
local arp_flushed="false"
|
||||
@@ -460,12 +441,10 @@ opt_network_stack_optimize() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Flush routing table
|
||||
if sudo route -n flush > /dev/null 2>&1; then
|
||||
route_flushed="true"
|
||||
fi
|
||||
|
||||
# Clear ARP cache
|
||||
if sudo arp -a -d > /dev/null 2>&1; then
|
||||
arp_flushed="true"
|
||||
fi
|
||||
@@ -487,8 +466,7 @@ opt_network_stack_optimize() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Disk permissions repair
|
||||
# Fixes user home directory permission issues
|
||||
# User directory permissions repair.
|
||||
opt_disk_permissions_repair() {
|
||||
local user_id
|
||||
user_id=$(id -u)
|
||||
@@ -524,11 +502,7 @@ opt_disk_permissions_repair() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Bluetooth module reset
|
||||
# Resets Bluetooth daemon to fix connectivity issues
|
||||
# Intelligently detects Bluetooth audio usage:
|
||||
# 1. Checks if default audio output is Bluetooth (precise)
|
||||
# 2. Falls back to Bluetooth + media app detection (compatibility)
|
||||
# Bluetooth reset (skip if HID/audio active).
|
||||
opt_bluetooth_reset() {
|
||||
local spinner_started="false"
|
||||
if [[ -t 1 ]]; then
|
||||
@@ -545,26 +519,20 @@ opt_bluetooth_reset() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check if any audio is playing through Bluetooth
|
||||
local bt_audio_active=false
|
||||
|
||||
# Method 1: Check if default audio output is Bluetooth (precise)
|
||||
local audio_info
|
||||
audio_info=$(system_profiler SPAudioDataType 2> /dev/null || echo "")
|
||||
|
||||
# Extract default output device information
|
||||
local default_output
|
||||
default_output=$(echo "$audio_info" | awk '/Default Output Device: Yes/,/^$/' 2> /dev/null || echo "")
|
||||
|
||||
# Check if transport type is Bluetooth
|
||||
if echo "$default_output" | grep -qi "Transport:.*Bluetooth"; then
|
||||
bt_audio_active=true
|
||||
fi
|
||||
|
||||
# Method 2: Fallback - Bluetooth connected + media apps running (compatibility)
|
||||
if [[ "$bt_audio_active" == "false" ]]; then
|
||||
if system_profiler SPBluetoothDataType 2> /dev/null | grep -q "Connected: Yes"; then
|
||||
# Extended media apps list for broader coverage
|
||||
local -a media_apps=("Music" "Spotify" "VLC" "QuickTime Player" "TV" "Podcasts" "Safari" "Google Chrome" "Chrome" "Firefox" "Arc" "IINA" "mpv")
|
||||
for app in "${media_apps[@]}"; do
|
||||
if pgrep -x "$app" > /dev/null 2>&1; then
|
||||
@@ -583,7 +551,6 @@ opt_bluetooth_reset() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Safe to reset Bluetooth
|
||||
if sudo pkill -TERM bluetoothd > /dev/null 2>&1; then
|
||||
sleep 1
|
||||
if pgrep -x bluetoothd > /dev/null 2>&1; then
|
||||
@@ -609,11 +576,8 @@ opt_bluetooth_reset() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Spotlight index optimization
|
||||
# Rebuilds Spotlight index if search is slow or results are inaccurate
|
||||
# Only runs if index is actually problematic
|
||||
# Spotlight index check/rebuild (only if slow).
|
||||
opt_spotlight_index_optimize() {
|
||||
# Check if Spotlight indexing is disabled
|
||||
local spotlight_status
|
||||
spotlight_status=$(mdutil -s / 2> /dev/null || echo "")
|
||||
|
||||
@@ -622,9 +586,7 @@ opt_spotlight_index_optimize() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check if indexing is currently running
|
||||
if echo "$spotlight_status" | grep -qi "Indexing enabled" && ! echo "$spotlight_status" | grep -qi "Indexing and searching disabled"; then
|
||||
# Check index health by testing search speed twice
|
||||
local slow_count=0
|
||||
local test_start test_end test_duration
|
||||
for _ in 1 2; do
|
||||
@@ -663,13 +625,11 @@ opt_spotlight_index_optimize() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Dock cache refresh
|
||||
# Fixes broken icons, duplicate items, and visual glitches in the Dock
|
||||
# Dock cache refresh.
|
||||
opt_dock_refresh() {
|
||||
local dock_support="$HOME/Library/Application Support/Dock"
|
||||
local refreshed=false
|
||||
|
||||
# Remove Dock database files (icons, positions, etc.)
|
||||
if [[ -d "$dock_support" ]]; then
|
||||
while IFS= read -r db_file; do
|
||||
if [[ -f "$db_file" ]]; then
|
||||
@@ -678,14 +638,11 @@ opt_dock_refresh() {
|
||||
done < <(find "$dock_support" -name "*.db" -type f 2> /dev/null || true)
|
||||
fi
|
||||
|
||||
# Also clear Dock plist cache
|
||||
local dock_plist="$HOME/Library/Preferences/com.apple.dock.plist"
|
||||
if [[ -f "$dock_plist" ]]; then
|
||||
# Just touch to invalidate cache, don't delete (preserves user settings)
|
||||
touch "$dock_plist" 2> /dev/null || true
|
||||
fi
|
||||
|
||||
# Restart Dock to apply changes (skip in dry-run mode)
|
||||
if [[ "${MOLE_DRY_RUN:-0}" != "1" ]]; then
|
||||
killall Dock 2> /dev/null || true
|
||||
fi
|
||||
@@ -696,7 +653,7 @@ opt_dock_refresh() {
|
||||
opt_msg "Dock refreshed"
|
||||
}
|
||||
|
||||
# Execute optimization by action name
|
||||
# Dispatch optimization by action name.
|
||||
execute_optimization() {
|
||||
local action="$1"
|
||||
local path="${2:-}"
|
||||
|
||||
@@ -2,18 +2,13 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Ensure common.sh is loaded
|
||||
# Ensure common.sh is loaded.
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
[[ -z "${MOLE_COMMON_LOADED:-}" ]] && source "$SCRIPT_DIR/lib/core/common.sh"
|
||||
|
||||
# Batch uninstall functionality with minimal confirmations
|
||||
# Replaces the overly verbose individual confirmation approach
|
||||
# Batch uninstall with a single confirmation.
|
||||
|
||||
# ============================================================================
|
||||
# Configuration: User Data Detection Patterns
|
||||
# ============================================================================
|
||||
# Directories that typically contain user-customized configurations, themes,
|
||||
# or personal data that users might want to backup before uninstalling
|
||||
# User data detection patterns (prompt user to backup if found).
|
||||
readonly SENSITIVE_DATA_PATTERNS=(
|
||||
"\.warp" # Warp terminal configs/themes
|
||||
"/\.config/" # Standard Unix config directory
|
||||
@@ -26,24 +21,20 @@ readonly SENSITIVE_DATA_PATTERNS=(
|
||||
"/\.gnupg/" # GPG keys (critical)
|
||||
)
|
||||
|
||||
# Join patterns into a single regex for grep
|
||||
# Join patterns into a single regex for grep.
|
||||
SENSITIVE_DATA_REGEX=$(
|
||||
IFS='|'
|
||||
echo "${SENSITIVE_DATA_PATTERNS[*]}"
|
||||
)
|
||||
|
||||
# Decode and validate base64 encoded file list
|
||||
# Returns decoded string if valid, empty string otherwise
|
||||
# Decode and validate base64 file list (safe for set -e).
|
||||
decode_file_list() {
|
||||
local encoded="$1"
|
||||
local app_name="$2"
|
||||
local decoded
|
||||
|
||||
# Decode base64 data (macOS uses -D, GNU uses -d)
|
||||
# Try macOS format first, then GNU format for compatibility
|
||||
# IMPORTANT: Always return 0 to prevent set -e from terminating the script
|
||||
# macOS uses -D, GNU uses -d. Always return 0 for set -e safety.
|
||||
if ! decoded=$(printf '%s' "$encoded" | base64 -D 2> /dev/null); then
|
||||
# Fallback to GNU base64 format
|
||||
if ! decoded=$(printf '%s' "$encoded" | base64 -d 2> /dev/null); then
|
||||
log_error "Failed to decode file list for $app_name" >&2
|
||||
echo ""
|
||||
@@ -51,14 +42,12 @@ decode_file_list() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate decoded data doesn't contain null bytes
|
||||
if [[ "$decoded" =~ $'\0' ]]; then
|
||||
log_warning "File list for $app_name contains null bytes, rejecting" >&2
|
||||
echo ""
|
||||
return 0 # Return success with empty string
|
||||
fi
|
||||
|
||||
# Validate paths look reasonable (each line should be a path or empty)
|
||||
while IFS= read -r line; do
|
||||
if [[ -n "$line" && ! "$line" =~ ^/ ]]; then
|
||||
log_warning "Invalid path in file list for $app_name: $line" >&2
|
||||
@@ -70,24 +59,21 @@ decode_file_list() {
|
||||
echo "$decoded"
|
||||
return 0
|
||||
}
|
||||
# Note: find_app_files() and calculate_total_size() functions now in lib/core/common.sh
|
||||
# Note: find_app_files() and calculate_total_size() are in lib/core/common.sh.
|
||||
|
||||
# Stop Launch Agents and Daemons for an app
|
||||
# Args: $1 = bundle_id, $2 = has_system_files (true/false)
|
||||
# Stop Launch Agents/Daemons for an app.
|
||||
stop_launch_services() {
|
||||
local bundle_id="$1"
|
||||
local has_system_files="${2:-false}"
|
||||
|
||||
[[ -z "$bundle_id" || "$bundle_id" == "unknown" ]] && return 0
|
||||
|
||||
# User-level Launch Agents
|
||||
if [[ -d ~/Library/LaunchAgents ]]; then
|
||||
while IFS= read -r -d '' plist; do
|
||||
launchctl unload "$plist" 2> /dev/null || true
|
||||
done < <(find ~/Library/LaunchAgents -maxdepth 1 -name "${bundle_id}*.plist" -print0 2> /dev/null)
|
||||
fi
|
||||
|
||||
# System-level services (requires sudo)
|
||||
if [[ "$has_system_files" == "true" ]]; then
|
||||
if [[ -d /Library/LaunchAgents ]]; then
|
||||
while IFS= read -r -d '' plist; do
|
||||
@@ -102,9 +88,7 @@ stop_launch_services() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Remove a list of files (handles both regular files and symlinks)
|
||||
# Args: $1 = file_list (newline-separated), $2 = use_sudo (true/false)
|
||||
# Returns: number of files removed
|
||||
# Remove files (handles symlinks, optional sudo).
|
||||
remove_file_list() {
|
||||
local file_list="$1"
|
||||
local use_sudo="${2:-false}"
|
||||
@@ -114,14 +98,12 @@ remove_file_list() {
|
||||
[[ -n "$file" && -e "$file" ]] || continue
|
||||
|
||||
if [[ -L "$file" ]]; then
|
||||
# Symlink: use direct rm
|
||||
if [[ "$use_sudo" == "true" ]]; then
|
||||
sudo rm "$file" 2> /dev/null && ((count++)) || true
|
||||
else
|
||||
rm "$file" 2> /dev/null && ((count++)) || true
|
||||
fi
|
||||
else
|
||||
# Regular file/directory: use safe_remove
|
||||
if [[ "$use_sudo" == "true" ]]; then
|
||||
safe_sudo_remove "$file" && ((count++)) || true
|
||||
else
|
||||
@@ -133,8 +115,7 @@ remove_file_list() {
|
||||
echo "$count"
|
||||
}
|
||||
|
||||
# Batch uninstall with single confirmation
|
||||
# Globals: selected_apps (read) - array of selected applications
|
||||
# Batch uninstall with single confirmation.
|
||||
batch_uninstall_applications() {
|
||||
local total_size_freed=0
|
||||
|
||||
@@ -144,19 +125,18 @@ batch_uninstall_applications() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Pre-process: Check for running apps and calculate total impact
|
||||
# Pre-scan: running apps, sudo needs, size.
|
||||
local -a running_apps=()
|
||||
local -a sudo_apps=()
|
||||
local total_estimated_size=0
|
||||
local -a app_details=()
|
||||
|
||||
# Analyze selected apps with progress indicator
|
||||
if [[ -t 1 ]]; then start_inline_spinner "Scanning files..."; fi
|
||||
for selected_app in "${selected_apps[@]}"; do
|
||||
[[ -z "$selected_app" ]] && continue
|
||||
IFS='|' read -r _ app_path app_name bundle_id _ _ <<< "$selected_app"
|
||||
|
||||
# Check if app is running using executable name from bundle
|
||||
# Check running app by bundle executable if available.
|
||||
local exec_name=""
|
||||
if [[ -e "$app_path/Contents/Info.plist" ]]; then
|
||||
exec_name=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2> /dev/null || echo "")
|
||||
@@ -166,11 +146,7 @@ batch_uninstall_applications() {
|
||||
running_apps+=("$app_name")
|
||||
fi
|
||||
|
||||
# Check if app requires sudo to delete (either app bundle or system files)
|
||||
# Need sudo if:
|
||||
# 1. Parent directory is not writable (may be owned by another user or root)
|
||||
# 2. App owner is root
|
||||
# 3. App owner is different from current user
|
||||
# Sudo needed if bundle owner/dir is not writable or system files exist.
|
||||
local needs_sudo=false
|
||||
local app_owner=$(get_file_owner "$app_path")
|
||||
local current_user=$(whoami)
|
||||
@@ -180,11 +156,11 @@ batch_uninstall_applications() {
|
||||
needs_sudo=true
|
||||
fi
|
||||
|
||||
# Calculate size for summary (including system files)
|
||||
# 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
|
||||
# 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
|
||||
@@ -192,7 +168,6 @@ batch_uninstall_applications() {
|
||||
local total_kb=$((app_size_kb + related_size_kb + system_size_kb))
|
||||
((total_estimated_size += total_kb))
|
||||
|
||||
# Check if system files require sudo
|
||||
# shellcheck disable=SC2128
|
||||
if [[ -n "$system_files" ]]; then
|
||||
needs_sudo=true
|
||||
@@ -202,33 +177,28 @@ batch_uninstall_applications() {
|
||||
sudo_apps+=("$app_name")
|
||||
fi
|
||||
|
||||
# Check for sensitive user data (performance optimization: do this once)
|
||||
# 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 encode file lists to handle multi-line data safely (single line)
|
||||
# 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')
|
||||
# Store needs_sudo to avoid recalculating during deletion phase
|
||||
app_details+=("$app_name|$app_path|$bundle_id|$total_kb|$encoded_files|$encoded_system_files|$has_sensitive_data|$needs_sudo")
|
||||
done
|
||||
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
||||
|
||||
# Format size display (convert KB to bytes for bytes_to_human())
|
||||
local size_display=$(bytes_to_human "$((total_estimated_size * 1024))")
|
||||
|
||||
# Display detailed file list for each app before confirmation
|
||||
echo ""
|
||||
echo -e "${PURPLE_BOLD}Files to be removed:${NC}"
|
||||
echo ""
|
||||
|
||||
# Check for apps with user data that might need backup
|
||||
# Performance optimization: use pre-calculated flags from app_details
|
||||
# Warn if user data is detected.
|
||||
local has_user_data=false
|
||||
for detail in "${app_details[@]}"; do
|
||||
IFS='|' read -r _ _ _ _ _ _ has_sensitive_data <<< "$detail"
|
||||
@@ -252,7 +222,7 @@ batch_uninstall_applications() {
|
||||
echo -e "${BLUE}${ICON_CONFIRM}${NC} ${app_name} ${GRAY}(${app_size_display})${NC}"
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} ${app_path/$HOME/~}"
|
||||
|
||||
# Show related files (limit to 5 most important ones for brevity)
|
||||
# Show related files (limit to 5).
|
||||
local file_count=0
|
||||
local max_files=5
|
||||
while IFS= read -r file; do
|
||||
@@ -264,7 +234,7 @@ batch_uninstall_applications() {
|
||||
fi
|
||||
done <<< "$related_files"
|
||||
|
||||
# Show system files
|
||||
# Show system files (limit to 5).
|
||||
local sys_file_count=0
|
||||
while IFS= read -r file; do
|
||||
if [[ -n "$file" && -e "$file" ]]; then
|
||||
@@ -275,7 +245,6 @@ batch_uninstall_applications() {
|
||||
fi
|
||||
done <<< "$system_files"
|
||||
|
||||
# Show count of remaining files if truncated
|
||||
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
|
||||
@@ -283,7 +252,7 @@ batch_uninstall_applications() {
|
||||
fi
|
||||
done
|
||||
|
||||
# Show summary and get batch confirmation first (before asking for password)
|
||||
# Confirmation before requesting sudo.
|
||||
local app_total=${#selected_apps[@]}
|
||||
local app_text="app"
|
||||
[[ $app_total -gt 1 ]] && app_text="apps"
|
||||
@@ -315,9 +284,8 @@ batch_uninstall_applications() {
|
||||
;;
|
||||
esac
|
||||
|
||||
# User confirmed, now request sudo access if needed
|
||||
# Request sudo if needed.
|
||||
if [[ ${#sudo_apps[@]} -gt 0 ]]; then
|
||||
# Check if sudo is already cached
|
||||
if ! sudo -n true 2> /dev/null; then
|
||||
if ! request_sudo_access "Admin required for system apps: ${sudo_apps[*]}"; then
|
||||
echo ""
|
||||
@@ -325,10 +293,9 @@ batch_uninstall_applications() {
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
# Start sudo keepalive with robust parent checking
|
||||
# Keep sudo alive during uninstall.
|
||||
parent_pid=$$
|
||||
(while true; do
|
||||
# Check if parent process still exists first
|
||||
if ! kill -0 "$parent_pid" 2> /dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
@@ -340,10 +307,7 @@ batch_uninstall_applications() {
|
||||
|
||||
if [[ -t 1 ]]; then start_inline_spinner "Uninstalling apps..."; fi
|
||||
|
||||
# Force quit running apps first (batch)
|
||||
# Note: Apps are already killed in the individual uninstall loop below with app_path for precise matching
|
||||
|
||||
# Perform uninstallations (silent mode, show results at end)
|
||||
# Perform uninstallations (silent mode, show results at end).
|
||||
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
||||
local success_count=0 failed_count=0
|
||||
local -a failed_items=()
|
||||
@@ -354,23 +318,19 @@ batch_uninstall_applications() {
|
||||
local system_files=$(decode_file_list "$encoded_system_files" "$app_name")
|
||||
local reason=""
|
||||
|
||||
# Note: needs_sudo is already calculated during scanning phase (performance optimization)
|
||||
|
||||
# Stop Launch Agents and Daemons before removal
|
||||
# Stop Launch Agents/Daemons before removal.
|
||||
local has_system_files="false"
|
||||
[[ -n "$system_files" ]] && has_system_files="true"
|
||||
stop_launch_services "$bundle_id" "$has_system_files"
|
||||
|
||||
# Force quit app if still running
|
||||
if ! force_kill_app "$app_name" "$app_path"; then
|
||||
reason="still running"
|
||||
fi
|
||||
|
||||
# Remove the application only if not running
|
||||
# Remove the application only if not running.
|
||||
if [[ -z "$reason" ]]; then
|
||||
if [[ "$needs_sudo" == true ]]; then
|
||||
if ! safe_sudo_remove "$app_path"; then
|
||||
# Determine specific failure reason (only fetch owner info when needed)
|
||||
local app_owner=$(get_file_owner "$app_path")
|
||||
local current_user=$(whoami)
|
||||
if [[ -n "$app_owner" && "$app_owner" != "$current_user" && "$app_owner" != "root" ]]; then
|
||||
@@ -384,25 +344,18 @@ batch_uninstall_applications() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove related files if app removal succeeded
|
||||
# Remove related files if app removal succeeded.
|
||||
if [[ -z "$reason" ]]; then
|
||||
# Remove user-level files
|
||||
remove_file_list "$related_files" "false" > /dev/null
|
||||
# Remove system-level files (requires sudo)
|
||||
remove_file_list "$system_files" "true" > /dev/null
|
||||
|
||||
# Clean up macOS defaults (preference domain)
|
||||
# This removes configuration data stored in the macOS defaults system
|
||||
# Note: This complements plist file deletion by clearing cached preferences
|
||||
# Clean up macOS defaults (preference domains).
|
||||
if [[ -n "$bundle_id" && "$bundle_id" != "unknown" ]]; then
|
||||
# 1. Standard defaults domain cleanup
|
||||
if defaults read "$bundle_id" &> /dev/null; then
|
||||
defaults delete "$bundle_id" 2> /dev/null || true
|
||||
fi
|
||||
|
||||
# 2. Clean up ByHost preferences (machine-specific configs)
|
||||
# These are often missed by standard cleanup tools
|
||||
# Format: ~/Library/Preferences/ByHost/com.app.id.XXXX.plist
|
||||
# ByHost preferences (machine-specific).
|
||||
if [[ -d ~/Library/Preferences/ByHost ]]; then
|
||||
find ~/Library/Preferences/ByHost -maxdepth 1 -name "${bundle_id}.*.plist" -delete 2> /dev/null || true
|
||||
fi
|
||||
@@ -435,7 +388,7 @@ batch_uninstall_applications() {
|
||||
success_line+=", freed ${GREEN}${freed_display}${NC}"
|
||||
fi
|
||||
|
||||
# Format app list with max 3 per line
|
||||
# Format app list with max 3 per line.
|
||||
if [[ -n "$success_list" ]]; then
|
||||
local idx=0
|
||||
local is_first_line=true
|
||||
@@ -445,25 +398,20 @@ batch_uninstall_applications() {
|
||||
local display_item="${GREEN}${app_name}${NC}"
|
||||
|
||||
if ((idx % 3 == 0)); then
|
||||
# Start new line
|
||||
if [[ -n "$current_line" ]]; then
|
||||
summary_details+=("$current_line")
|
||||
fi
|
||||
if [[ "$is_first_line" == true ]]; then
|
||||
# First line: append to success_line
|
||||
current_line="${success_line}: $display_item"
|
||||
is_first_line=false
|
||||
else
|
||||
# Subsequent lines: just the apps
|
||||
current_line="$display_item"
|
||||
fi
|
||||
else
|
||||
# Add to current line
|
||||
current_line="$current_line, $display_item"
|
||||
fi
|
||||
((idx++))
|
||||
done
|
||||
# Add the last line
|
||||
if [[ -n "$current_line" ]]; then
|
||||
summary_details+=("$current_line")
|
||||
fi
|
||||
@@ -509,12 +457,11 @@ batch_uninstall_applications() {
|
||||
print_summary_block "$title" "${summary_details[@]}"
|
||||
printf '\n'
|
||||
|
||||
# Clean up Dock entries for uninstalled apps
|
||||
# Clean up Dock entries for uninstalled apps.
|
||||
if [[ $success_count -gt 0 ]]; then
|
||||
local -a removed_paths=()
|
||||
for detail in "${app_details[@]}"; do
|
||||
IFS='|' read -r app_name app_path _ _ _ _ <<< "$detail"
|
||||
# Check if this app was successfully removed
|
||||
for success_name in "${success_items[@]}"; do
|
||||
if [[ "$success_name" == "$app_name" ]]; then
|
||||
removed_paths+=("$app_path")
|
||||
@@ -527,14 +474,14 @@ batch_uninstall_applications() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean up sudo keepalive if it was started
|
||||
# Clean up sudo keepalive if it was started.
|
||||
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
|
||||
|
||||
# Invalidate cache if any apps were successfully uninstalled
|
||||
# Invalidate cache if any apps were successfully uninstalled.
|
||||
if [[ $success_count -gt 0 ]]; then
|
||||
local cache_file="$HOME/.cache/mole/app_scan_cache"
|
||||
rm -f "$cache_file" 2> /dev/null || true
|
||||
|
||||
Reference in New Issue
Block a user