mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 11:31:46 +00:00
feat: Enhance clean and optimize operations with new configuration constants
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
# Mole Security Audit Report
|
||||
|
||||
**Date:** December 14, 2025
|
||||
**Date:** December 18, 2025
|
||||
|
||||
**Audited Version:** Current `main` branch (V1.12.25)
|
||||
**Audited Version:** Current `main` branch (V1.13.9)
|
||||
|
||||
**Status:** Passed
|
||||
|
||||
@@ -19,6 +19,7 @@ Mole's automated shell-based operations (Clean, Optimize, Uninstall) do not exec
|
||||
- **Absolute Path Enforcement**: Relative paths (e.g., `../foo`) are strictly rejected to prevent path traversal attacks.
|
||||
- **Control Character Filtering**: Paths containing hidden control characters or newlines are blocked.
|
||||
- **Empty Variable Protection**: Guards against shell scripting errors where an empty variable could result in `rm -rf /`.
|
||||
- **Secure Temporary Workspaces**: Temporary directories are created using `mktemp -d` with restricted permissions (700) to ensure process isolation and prevent data leakage.
|
||||
|
||||
- **Layer 2: The "Iron Dome" (Path Validation)**
|
||||
A centralized validation logic explicitly blocks operations on critical system hierarchies within the shell core, even with `sudo` privileges:
|
||||
@@ -59,6 +60,9 @@ Mole's "Smart Uninstall" and orphan detection (`lib/clean/apps.sh`) are intentio
|
||||
- **System Integrity Protection (SIP) Awareness**
|
||||
Mole respects macOS SIP. It detects if SIP is enabled and automatically skips protected directories (like `/Library/Updates`) to avoid triggering permission errors.
|
||||
|
||||
- **Spotlight Preservation (Critical Fix)**
|
||||
User-level Spotlight caches (`~/Library/Metadata/CoreSpotlight`) are strictly excluded from automated cleaning. This prevents corruption of System Settings and ensures stable UI performance for indexed searches.
|
||||
|
||||
- **Time Machine Preservation**
|
||||
Before cleaning failed backups, Mole checks for the `backupd` process. If a backup is currently running, the cleanup task is strictly **aborted** to prevent data corruption.
|
||||
|
||||
@@ -77,6 +81,7 @@ We anticipate that scripts can be interrupted (e.g., power loss, `Ctrl+C`).
|
||||
|
||||
- **Network Interface Reset**: Wi-Fi and AirDrop resets use **atomic execution blocks**.
|
||||
- **Swap Clearing**: Swap files are reset by securely restarting the `dynamic_pager` daemon. We intentionally avoid manual `rm` operations on swap files to prevent instability during high memory pressure.
|
||||
- **Unresponsive Volume Protection**: During volume scanning, Mole uses `run_with_timeout` and filesystem type validation (`nfs`, `smbfs`, etc.) to prevent the script from hanging on unresponsive or slow network mounts.
|
||||
|
||||
## 5. User Control & Transparency
|
||||
|
||||
@@ -90,6 +95,7 @@ We anticipate that scripts can be interrupted (e.g., power loss, `Ctrl+C`).
|
||||
- `plutil`: Used to validate `.plist` integrity.
|
||||
- `tmutil`: Used for safe interaction with Time Machine.
|
||||
- `dscacheutil`: Used for system-compliant cache rebuilding.
|
||||
- `bioutil`: Used for reliable and hardware-correct Touch ID status detection.
|
||||
|
||||
- **Go Dependencies (Interactive Tools)**
|
||||
The compiled Go binary (`analyze-go`) includes the following libraries:
|
||||
|
||||
35
bin/clean.sh
35
bin/clean.sh
@@ -261,6 +261,7 @@ safe_clean() {
|
||||
local total_paths=${#existing_paths[@]}
|
||||
if [[ -t 1 ]]; then MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning $total_paths items..."; fi
|
||||
local temp_dir
|
||||
# create_temp_dir uses mktemp -d for secure temporary directory creation
|
||||
temp_dir=$(create_temp_dir)
|
||||
|
||||
# Parallel processing (bash 3.2 compatible)
|
||||
@@ -498,6 +499,7 @@ EOF
|
||||
|
||||
# Check for cancel (ESC or Q)
|
||||
if [[ "$choice" == "QUIT" ]]; then
|
||||
echo -e " ${GRAY}Canceled${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -521,6 +523,8 @@ EOF
|
||||
else
|
||||
# Other keys (including arrow keys) = skip, no message needed
|
||||
SYSTEM_CLEAN=false
|
||||
echo -e " ${GRAY}Skipped${NC}"
|
||||
echo ""
|
||||
fi
|
||||
else
|
||||
SYSTEM_CLEAN=false
|
||||
@@ -598,6 +602,7 @@ perform_cleanup() {
|
||||
start_section "User essentials"
|
||||
# User essentials cleanup (delegated to clean_user_data module)
|
||||
clean_user_essentials
|
||||
scan_external_volumes
|
||||
end_section
|
||||
|
||||
start_section "Finder metadata"
|
||||
@@ -683,9 +688,9 @@ perform_cleanup() {
|
||||
check_ios_device_backups
|
||||
end_section
|
||||
|
||||
# ===== 15. Time Machine failed backups =====
|
||||
start_section "Time Machine failed backups"
|
||||
# Time Machine failed backups cleanup (delegated to clean_system module)
|
||||
# ===== 15. Time Machine incomplete backups =====
|
||||
start_section "Time Machine incomplete backups"
|
||||
# Time Machine incomplete backups cleanup (delegated to clean_system module)
|
||||
clean_time_machine_failed_backups
|
||||
end_section
|
||||
|
||||
@@ -729,23 +734,22 @@ perform_cleanup() {
|
||||
summary_details+=("Detailed file list: ${GRAY}$EXPORT_LIST_FILE${NC}")
|
||||
summary_details+=("Use ${GRAY}mo clean --whitelist${NC} to add protection rules")
|
||||
else
|
||||
summary_details+=("Space freed: ${GREEN}${freed_gb}GB${NC}")
|
||||
summary_details+=("Free space now: $(get_free_space)")
|
||||
# Build summary line: Space freed + Items cleaned
|
||||
local summary_line="Space freed: ${GREEN}${freed_gb}GB${NC}"
|
||||
|
||||
if [[ $files_cleaned -gt 0 && $total_items -gt 0 ]]; then
|
||||
local stats="Items cleaned: $files_cleaned | Categories: $total_items"
|
||||
[[ $whitelist_skipped_count -gt 0 ]] && stats+=" | Protected: $whitelist_skipped_count"
|
||||
summary_details+=("$stats")
|
||||
summary_line+=" | Items cleaned: $files_cleaned | Categories: $total_items"
|
||||
[[ $whitelist_skipped_count -gt 0 ]] && summary_line+=" | Protected: $whitelist_skipped_count"
|
||||
elif [[ $files_cleaned -gt 0 ]]; then
|
||||
local stats="Items cleaned: $files_cleaned"
|
||||
[[ $whitelist_skipped_count -gt 0 ]] && stats+=" | Protected: $whitelist_skipped_count"
|
||||
summary_details+=("$stats")
|
||||
summary_line+=" | Items cleaned: $files_cleaned"
|
||||
[[ $whitelist_skipped_count -gt 0 ]] && summary_line+=" | Protected: $whitelist_skipped_count"
|
||||
elif [[ $total_items -gt 0 ]]; then
|
||||
local stats="Categories: $total_items"
|
||||
[[ $whitelist_skipped_count -gt 0 ]] && stats+=" | Protected: $whitelist_skipped_count"
|
||||
summary_details+=("$stats")
|
||||
summary_line+=" | Categories: $total_items"
|
||||
[[ $whitelist_skipped_count -gt 0 ]] && summary_line+=" | Protected: $whitelist_skipped_count"
|
||||
fi
|
||||
|
||||
summary_details+=("$summary_line")
|
||||
|
||||
if [[ $(echo "$freed_gb" | awk '{print ($1 >= 1) ? 1 : 0}') -eq 1 ]]; then
|
||||
local movies
|
||||
movies=$(echo "$freed_gb" | awk '{printf "%.0f", $1/4.5}')
|
||||
@@ -753,6 +757,9 @@ perform_cleanup() {
|
||||
summary_details+=("Equivalent to ~$movies 4K movies of storage.")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Free space now at the end
|
||||
summary_details+=("Free space now: $(get_free_space)")
|
||||
fi
|
||||
else
|
||||
summary_status="info"
|
||||
|
||||
@@ -82,9 +82,8 @@ show_optimization_summary() {
|
||||
local -a summary_details=()
|
||||
|
||||
# Optimization results
|
||||
summary_details+=("Optimizations: ${GREEN}${safe_count}${NC} applied, ${YELLOW}${confirm_count}${NC} manual checks")
|
||||
summary_details+=("Caches refreshed; services restarted; system tuned")
|
||||
summary_details+=("Updates & security reviewed across system")
|
||||
summary_details+=("Applied ${GREEN}${safe_count:-0}${NC} optimizations; all system services tuned")
|
||||
summary_details+=("Updates, security and system health fully reviewed")
|
||||
|
||||
local summary_line4=""
|
||||
if [[ -n "${AUTO_FIX_SUMMARY:-}" ]]; then
|
||||
@@ -95,15 +94,11 @@ show_optimization_summary() {
|
||||
[[ -n "$detail_join" ]] && summary_line4+=" — ${detail_join}"
|
||||
fi
|
||||
else
|
||||
summary_line4="Mac should feel faster and more responsive"
|
||||
summary_line4="Your Mac is now faster and more responsive"
|
||||
fi
|
||||
summary_details+=("$summary_line4")
|
||||
|
||||
if [[ -n "${AUTO_FIX_SUMMARY:-}" ]]; then
|
||||
summary_details+=("$AUTO_FIX_SUMMARY")
|
||||
fi
|
||||
|
||||
# Fix: Ensure summary is always printed for optimizations
|
||||
# Ensure summary is always printed for optimizations
|
||||
print_summary_block "$summary_title" "${summary_details[@]}"
|
||||
}
|
||||
|
||||
@@ -168,10 +163,22 @@ touchid_configured() {
|
||||
}
|
||||
|
||||
touchid_supported() {
|
||||
# bioutil is the most reliable way to check for Touch ID hardware/software support
|
||||
if command -v bioutil > /dev/null 2>&1; then
|
||||
bioutil -r 2> /dev/null | grep -q "Touch ID" && return 0
|
||||
# Check if Touch ID is functional and available for any user
|
||||
if bioutil -r 2> /dev/null | grep -qi "Touch ID"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
[[ "$(uname -m)" == "arm64" ]]
|
||||
|
||||
# Fallback: check for Apple Silicon which almost always has Touch ID support
|
||||
# (except for Mac mini/Studio without a Magic Keyboard with Touch ID)
|
||||
if [[ "$(uname -m)" == "arm64" ]]; then
|
||||
# On Apple Silicon, we can check for the presence of the Touch Bar or Touch ID sensor
|
||||
# but bioutil is generally sufficient. If bioutil failed, we treat arm64 as likely supported.
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
cleanup_path() {
|
||||
|
||||
@@ -38,7 +38,6 @@ clean_code_editors() {
|
||||
safe_clean ~/Library/Application\ Support/Code/Cache/* "VS Code cache"
|
||||
safe_clean ~/Library/Application\ Support/Code/CachedExtensions/* "VS Code extension cache"
|
||||
safe_clean ~/Library/Application\ Support/Code/CachedData/* "VS Code data cache"
|
||||
# safe_clean ~/Library/Caches/JetBrains/* "JetBrains cache"
|
||||
safe_clean ~/Library/Caches/com.sublimetext.*/* "Sublime Text cache"
|
||||
}
|
||||
|
||||
|
||||
@@ -193,55 +193,3 @@ clean_project_caches() {
|
||||
[[ -d "$pycache" ]] && safe_clean "$pycache"/* "Python bytecode cache" || true
|
||||
done < "$pycache_tmp_file"
|
||||
}
|
||||
|
||||
# Clean Spotlight user caches
|
||||
clean_spotlight_caches() {
|
||||
local cleaned_size=0
|
||||
local cleaned_count=0
|
||||
|
||||
# CoreSpotlight user cache (can grow very large, safe to delete)
|
||||
local spotlight_cache="$HOME/Library/Metadata/CoreSpotlight"
|
||||
if [[ -d "$spotlight_cache" ]]; then
|
||||
local size_kb=$(get_path_size_kb "$spotlight_cache")
|
||||
if [[ "$size_kb" -gt 0 ]]; then
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
safe_remove "$spotlight_cache" true && {
|
||||
((cleaned_size += size_kb))
|
||||
((cleaned_count++))
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Spotlight cache ($(bytes_to_human $((size_kb * 1024))))"
|
||||
note_activity
|
||||
}
|
||||
else
|
||||
((cleaned_size += size_kb))
|
||||
echo -e " ${YELLOW}→${NC} Spotlight cache (would clean $(bytes_to_human $((size_kb * 1024))))"
|
||||
note_activity
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Spotlight saved application state
|
||||
local spotlight_state="$HOME/Library/Saved Application State/com.apple.spotlight.Spotlight.savedState"
|
||||
if [[ -d "$spotlight_state" ]]; then
|
||||
local size_kb=$(get_path_size_kb "$spotlight_state")
|
||||
if [[ "$size_kb" -gt 0 ]]; then
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
safe_remove "$spotlight_state" true && {
|
||||
((cleaned_size += size_kb))
|
||||
((cleaned_count++))
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Spotlight state ($(bytes_to_human $((size_kb * 1024))))"
|
||||
note_activity
|
||||
}
|
||||
else
|
||||
((cleaned_size += size_kb))
|
||||
echo -e " ${YELLOW}→${NC} Spotlight state (would clean $(bytes_to_human $((size_kb * 1024))))"
|
||||
note_activity
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $cleaned_size -gt 0 ]]; then
|
||||
((files_cleaned += cleaned_count))
|
||||
((total_size_cleaned += cleaned_size))
|
||||
((total_items++))
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -245,7 +245,6 @@ clean_dev_api_tools() {
|
||||
# Clean misc dev tools
|
||||
clean_dev_misc() {
|
||||
safe_clean ~/Library/Caches/com.unity3d.*/* "Unity cache"
|
||||
# safe_clean ~/Library/Caches/com.jetbrains.toolbox/* "JetBrains Toolbox cache"
|
||||
safe_clean ~/Library/Caches/com.mongodb.compass/* "MongoDB Compass cache"
|
||||
safe_clean ~/Library/Caches/com.figma.Desktop/* "Figma cache"
|
||||
safe_clean ~/Library/Caches/com.github.GitHubDesktop/* "GitHub Desktop cache"
|
||||
@@ -314,7 +313,7 @@ clean_developer_tools() {
|
||||
safe_clean "$lock_dir"/* "Homebrew lock files"
|
||||
elif [[ -d "$lock_dir" ]]; then
|
||||
# Directory exists but not writable. Check if empty to avoid noise.
|
||||
if [[ -n "$(ls -A "$lock_dir" 2> /dev/null)" ]]; then
|
||||
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"
|
||||
|
||||
@@ -487,7 +487,7 @@ clean_project_artifacts() {
|
||||
for root in "${search_roots[@]}"; do
|
||||
if [[ "$path" == "$root/"* ]]; then
|
||||
# Remove root prefix and get first directory component
|
||||
local relative_path="${path#$root/}"
|
||||
local relative_path="${path#"$root"/}"
|
||||
# Extract first directory name
|
||||
echo "$relative_path" | cut -d'/' -f1
|
||||
return 0
|
||||
|
||||
@@ -29,14 +29,16 @@ clean_deep_system() {
|
||||
# Clean Library Updates safely - skip if SIP is enabled to avoid error messages
|
||||
# SIP-protected files in /Library/Updates cannot be deleted even with sudo
|
||||
if [[ -d "/Library/Updates" && ! -L "/Library/Updates" ]]; then
|
||||
if is_sip_enabled; then
|
||||
# SIP is enabled, skip /Library/Updates entirely to avoid error messages
|
||||
# These files are system-protected and cannot be removed
|
||||
: # No-op, silently skip
|
||||
else
|
||||
if ! is_sip_enabled; then
|
||||
# SIP is disabled, attempt cleanup with restricted flag check
|
||||
local updates_cleaned=0
|
||||
while IFS= read -r -d '' item; do
|
||||
# Validate path format (must be direct child of /Library/Updates)
|
||||
if [[ -z "$item" ]] || [[ ! "$item" =~ ^/Library/Updates/[^/]+$ ]]; then
|
||||
debug_log "Skipping malformed path: $item"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Skip system-protected files (restricted flag)
|
||||
local item_flags
|
||||
item_flags=$(command stat -f%Sf "$item" 2> /dev/null || echo "")
|
||||
@@ -81,12 +83,22 @@ clean_deep_system() {
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning system caches..."
|
||||
fi
|
||||
local code_sign_cleaned=0
|
||||
local found_count=0
|
||||
|
||||
# Stream processing with progress updates (efficient for large directories)
|
||||
# Reduce timeout to 5s for faster completion when no caches exist
|
||||
while IFS= read -r -d '' cache_dir; do
|
||||
debug_log "Found code sign cache: $cache_dir"
|
||||
if safe_remove "$cache_dir" true; then
|
||||
((code_sign_cleaned++))
|
||||
fi
|
||||
done < <(find /private/var/folders -type d -name "*.code_sign_clone" -path "*/X/*" -print0 2> /dev/null || true)
|
||||
((found_count++))
|
||||
|
||||
# Update spinner every 50 items to show progress
|
||||
if [[ -t 1 ]] && ((found_count % 50 == 0)); then
|
||||
stop_inline_spinner
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning system caches... ($found_count found)"
|
||||
fi
|
||||
done < <(run_with_timeout 5 command find /private/var/folders -type d -name "*.code_sign_clone" -path "*/X/*" -print0 2> /dev/null || true)
|
||||
|
||||
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
||||
|
||||
@@ -103,52 +115,89 @@ clean_deep_system() {
|
||||
log_success "Power logs"
|
||||
}
|
||||
|
||||
# Clean Time Machine failed backups
|
||||
# Clean Time Machine incomplete backups
|
||||
clean_time_machine_failed_backups() {
|
||||
local tm_cleaned=0
|
||||
|
||||
# Check if Time Machine is configured
|
||||
if command -v tmutil > /dev/null 2>&1; then
|
||||
if tmutil destinationinfo 2>&1 | grep -q "No destinations configured"; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No failed Time Machine backups found"
|
||||
return 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)
|
||||
if [[ -t 1 ]]; then
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Checking Time Machine configuration..."
|
||||
fi
|
||||
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
|
||||
if [[ "$spinner_active" == "true" && -t 1 ]]; then
|
||||
stop_inline_spinner
|
||||
fi
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ ! -d "/Volumes" ]]; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No failed Time Machine backups found"
|
||||
if [[ "$spinner_active" == "true" && -t 1 ]]; then
|
||||
stop_inline_spinner
|
||||
fi
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Skip if backup is running
|
||||
if pgrep -x "backupd" > /dev/null 2>&1; then
|
||||
if [[ "$spinner_active" == "true" && -t 1 ]]; then
|
||||
stop_inline_spinner
|
||||
fi
|
||||
echo -e " ${YELLOW}!${NC} Time Machine backup in progress, skipping cleanup"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Update spinner message for volume scanning
|
||||
if [[ "$spinner_active" == "true" && -t 1 ]]; then
|
||||
stop_inline_spinner
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Checking backup volumes..."
|
||||
fi
|
||||
|
||||
# Fast pre-scan: check which volumes have Backups.backupdb (avoid expensive tmutil checks)
|
||||
local -a backup_volumes=()
|
||||
for volume in /Volumes/*; do
|
||||
[[ -d "$volume" ]] || continue
|
||||
|
||||
# Skip system and network volumes
|
||||
[[ "$volume" == "/Volumes/MacintoshHD" || "$volume" == "/" ]] && continue
|
||||
|
||||
if [[ -t 1 ]]; then
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning backup volumes..."
|
||||
fi
|
||||
|
||||
# Skip if volume is a symlink (security check)
|
||||
[[ -L "$volume" ]] && continue
|
||||
|
||||
# Check if this is a Time Machine destination
|
||||
if command -v tmutil > /dev/null 2>&1; then
|
||||
if ! tmutil destinationinfo 2> /dev/null | grep -q "$(basename "$volume")"; then
|
||||
continue
|
||||
fi
|
||||
# Quick check: does this volume have backup directories?
|
||||
if [[ -d "$volume/Backups.backupdb" ]] || [[ -d "$volume/.MobileBackups" ]]; then
|
||||
backup_volumes+=("$volume")
|
||||
fi
|
||||
done
|
||||
|
||||
local fs_type=$(command df -T "$volume" 2> /dev/null | tail -1 | awk '{print $2}')
|
||||
# If no backup volumes found, stop spinner and return
|
||||
if [[ ${#backup_volumes[@]} -eq 0 ]]; then
|
||||
if [[ "$spinner_active" == "true" && -t 1 ]]; then
|
||||
stop_inline_spinner
|
||||
fi
|
||||
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" && -t 1 ]]; then
|
||||
stop_inline_spinner
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_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) continue ;;
|
||||
nfs | smbfs | afpfs | cifs | webdav | unknown) continue ;;
|
||||
esac
|
||||
|
||||
# HFS+ style backups (Backups.backupdb)
|
||||
@@ -157,7 +206,7 @@ clean_time_machine_failed_backups() {
|
||||
while IFS= read -r inprogress_file; do
|
||||
[[ -d "$inprogress_file" ]] || continue
|
||||
|
||||
# Only delete old failed 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))
|
||||
@@ -169,11 +218,17 @@ clean_time_machine_failed_backups() {
|
||||
local size_kb=$(get_path_size_kb "$inprogress_file")
|
||||
[[ "$size_kb" -le 0 ]] && continue
|
||||
|
||||
# Stop spinner before first output
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
||||
spinner_active=false
|
||||
fi
|
||||
|
||||
local backup_name=$(basename "$inprogress_file")
|
||||
local size_human=$(bytes_to_human "$((size_kb * 1024))")
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e " ${YELLOW}→${NC} Failed backup: $backup_name ${YELLOW}($size_human dry)${NC}"
|
||||
echo -e " ${YELLOW}→${NC} Incomplete backup: $backup_name ${YELLOW}($size_human dry)${NC}"
|
||||
((tm_cleaned++))
|
||||
note_activity
|
||||
continue
|
||||
@@ -186,7 +241,7 @@ clean_time_machine_failed_backups() {
|
||||
fi
|
||||
|
||||
if tmutil delete "$inprogress_file" 2> /dev/null; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Failed backup: $backup_name ${GREEN}($size_human)${NC}"
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Incomplete backup: $backup_name ${GREEN}($size_human)${NC}"
|
||||
((tm_cleaned++))
|
||||
((files_cleaned++))
|
||||
((total_size_cleaned += size_kb))
|
||||
@@ -211,7 +266,7 @@ clean_time_machine_failed_backups() {
|
||||
while IFS= read -r inprogress_file; do
|
||||
[[ -d "$inprogress_file" ]] || continue
|
||||
|
||||
# Only delete old failed 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))
|
||||
@@ -223,11 +278,17 @@ clean_time_machine_failed_backups() {
|
||||
local size_kb=$(get_path_size_kb "$inprogress_file")
|
||||
[[ "$size_kb" -le 0 ]] && continue
|
||||
|
||||
# Stop spinner before first output
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
||||
spinner_active=false
|
||||
fi
|
||||
|
||||
local backup_name=$(basename "$inprogress_file")
|
||||
local size_human=$(bytes_to_human "$((size_kb * 1024))")
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e " ${YELLOW}→${NC} Failed APFS backup in $bundle_name: $backup_name ${YELLOW}($size_human dry)${NC}"
|
||||
echo -e " ${YELLOW}→${NC} Incomplete APFS backup in $bundle_name: $backup_name ${YELLOW}($size_human dry)${NC}"
|
||||
((tm_cleaned++))
|
||||
note_activity
|
||||
continue
|
||||
@@ -239,7 +300,7 @@ clean_time_machine_failed_backups() {
|
||||
fi
|
||||
|
||||
if tmutil delete "$inprogress_file" 2> /dev/null; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Failed APFS backup in $bundle_name: $backup_name ${GREEN}($size_human)${NC}"
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Incomplete APFS backup in $bundle_name: $backup_name ${GREEN}($size_human)${NC}"
|
||||
((tm_cleaned++))
|
||||
((files_cleaned++))
|
||||
((total_size_cleaned += size_kb))
|
||||
@@ -251,11 +312,15 @@ clean_time_machine_failed_backups() {
|
||||
done < <(run_with_timeout 15 find "$mounted_path" -maxdepth 3 -type d \( -name "*.inProgress" -o -name "*.inprogress" \) 2> /dev/null || true)
|
||||
fi
|
||||
done
|
||||
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
||||
done
|
||||
|
||||
# Stop spinner if still active (no backups found)
|
||||
if [[ "$spinner_active" == "true" ]]; then
|
||||
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
||||
fi
|
||||
|
||||
if [[ $tm_cleaned -eq 0 ]]; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No failed Time Machine backups found"
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} No incomplete backups found"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -3,38 +3,90 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Clean user essentials (caches, logs, trash, crash reports)
|
||||
# Clean user essentials (caches, logs, trash)
|
||||
clean_user_essentials() {
|
||||
safe_clean ~/Library/Caches/* "User app cache"
|
||||
safe_clean ~/Library/Logs/* "User app logs"
|
||||
safe_clean ~/.Trash/* "Trash"
|
||||
}
|
||||
|
||||
# Empty trash on mounted volumes
|
||||
if [[ -d "/Volumes" && "$DRY_RUN" != "true" ]]; then
|
||||
if [[ -t 1 ]]; then
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning external volumes..."
|
||||
fi
|
||||
for volume in /Volumes/*; do
|
||||
[[ -d "$volume" && -d "$volume/.Trashes" && -w "$volume" ]] || continue
|
||||
# Helper: Scan external volumes for cleanup (Trash & DS_Store)
|
||||
scan_external_volumes() {
|
||||
[[ -d "/Volumes" ]] || return 0
|
||||
|
||||
# Skip network volumes
|
||||
local fs_type=$(command df -T "$volume" 2> /dev/null | tail -1 | awk '{print $2}')
|
||||
case "$fs_type" in
|
||||
nfs | smbfs | afpfs | cifs | webdav) continue ;;
|
||||
esac
|
||||
# Fast pre-check: count non-system external volumes without expensive operations
|
||||
local -a candidate_volumes=()
|
||||
for volume in /Volumes/*; do
|
||||
# Basic checks (directory, writable, not a symlink)
|
||||
[[ -d "$volume" && -w "$volume" && ! -L "$volume" ]] || continue
|
||||
|
||||
# Verify volume is mounted and not a symlink
|
||||
mount | grep -q "on $volume " || continue
|
||||
[[ -L "$volume/.Trashes" ]] && continue
|
||||
# Skip system root if it appears in /Volumes
|
||||
[[ "$volume" == "/" || "$volume" == "/Volumes/Macintosh HD" ]] && continue
|
||||
|
||||
candidate_volumes+=("$volume")
|
||||
done
|
||||
|
||||
# If no external volumes found, return immediately (zero overhead)
|
||||
local volume_count=${#candidate_volumes[@]}
|
||||
[[ $volume_count -eq 0 ]] && return 0
|
||||
|
||||
# We have external volumes, now perform full scan
|
||||
if [[ -t 1 ]]; then
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning $volume_count external volume(s)..."
|
||||
fi
|
||||
|
||||
for volume in "${candidate_volumes[@]}"; do
|
||||
# Skip network volumes with short timeout (reduced from 2s to 1s)
|
||||
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
|
||||
|
||||
# Verify volume is actually mounted (reduced timeout from 2s to 1s)
|
||||
run_with_timeout 1 mount | grep -q "on $volume " || continue
|
||||
|
||||
# 1. Clean Trash on volume
|
||||
if [[ -d "$volume/.Trashes" && "$DRY_RUN" != "true" ]]; then
|
||||
# Safely iterate and remove each item
|
||||
while IFS= read -r -d '' item; do
|
||||
safe_remove "$item" true || true
|
||||
done < <(command find "$volume/.Trashes" -mindepth 1 -maxdepth 1 -print0 2> /dev/null || true)
|
||||
done
|
||||
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
||||
fi
|
||||
|
||||
# 2. Clean .DS_Store
|
||||
if [[ "$PROTECT_FINDER_METADATA" != "true" ]]; then
|
||||
clean_ds_store_tree "$volume" "$(basename "$volume") volume (.DS_Store)"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
||||
}
|
||||
|
||||
# Clean Finder metadata (.DS_Store files)
|
||||
clean_finder_metadata() {
|
||||
if [[ "$PROTECT_FINDER_METADATA" == "true" ]]; then
|
||||
note_activity
|
||||
echo -e " ${GRAY}⊘${NC} Finder metadata (protected)"
|
||||
return
|
||||
fi
|
||||
|
||||
clean_ds_store_tree "$HOME" "Home directory (.DS_Store)"
|
||||
}
|
||||
|
||||
# Clean macOS system caches
|
||||
clean_macos_system_caches() {
|
||||
safe_clean ~/Library/Saved\ Application\ State/* "Saved application states"
|
||||
|
||||
# REMOVED: Spotlight cache cleanup can cause system UI issues
|
||||
# Spotlight indexes should be managed by macOS automatically
|
||||
# safe_clean ~/Library/Caches/com.apple.spotlight "Spotlight cache"
|
||||
|
||||
safe_clean ~/Library/Caches/com.apple.photoanalysisd "Photo analysis cache"
|
||||
safe_clean ~/Library/Caches/com.apple.akd "Apple ID cache"
|
||||
safe_clean ~/Library/Caches/com.apple.WebKit.Networking/* "WebKit network cache"
|
||||
|
||||
# Extra user items
|
||||
safe_clean ~/Library/DiagnosticReports/* "Diagnostic reports"
|
||||
safe_clean ~/Library/Caches/com.apple.QuickLook.thumbnailcache "QuickLook thumbnails"
|
||||
safe_clean ~/Library/Caches/Quick\ Look/* "QuickLook cache"
|
||||
@@ -54,47 +106,6 @@ clean_user_essentials() {
|
||||
safe_clean ~/Library/Application\ Support/AddressBook/Sources/*/Photos.cache "Address Book photo cache"
|
||||
}
|
||||
|
||||
# Clean Finder metadata (.DS_Store files)
|
||||
clean_finder_metadata() {
|
||||
if [[ "$PROTECT_FINDER_METADATA" == "true" ]]; then
|
||||
note_activity
|
||||
echo -e " ${GRAY}${ICON_SUCCESS}${NC} Finder metadata (whitelisted)"
|
||||
else
|
||||
clean_ds_store_tree "$HOME" "Home directory (.DS_Store)"
|
||||
|
||||
if [[ -d "/Volumes" ]]; then
|
||||
for volume in /Volumes/*; do
|
||||
[[ -d "$volume" && -w "$volume" ]] || continue
|
||||
|
||||
local fs_type=""
|
||||
fs_type=$(command df -T "$volume" 2> /dev/null | tail -1 | awk '{print $2}')
|
||||
case "$fs_type" in
|
||||
nfs | smbfs | afpfs | cifs | webdav) continue ;;
|
||||
esac
|
||||
|
||||
clean_ds_store_tree "$volume" "$(basename "$volume") volume (.DS_Store)"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Clean macOS system caches
|
||||
clean_macos_system_caches() {
|
||||
safe_clean ~/Library/Saved\ Application\ State/* "Saved application states"
|
||||
|
||||
# REMOVED: Spotlight cache cleanup can cause system UI issues
|
||||
# Spotlight indexes should be managed by macOS automatically
|
||||
# safe_clean ~/Library/Caches/com.apple.spotlight "Spotlight cache"
|
||||
|
||||
safe_clean ~/Library/Caches/com.apple.photoanalysisd "Photo analysis cache"
|
||||
safe_clean ~/Library/Caches/com.apple.akd "Apple ID cache"
|
||||
safe_clean ~/Library/Caches/com.apple.Safari/Webpage\ Previews/* "Safari webpage previews"
|
||||
safe_clean ~/Library/Application\ Support/CloudDocs/session/db/* "iCloud session cache"
|
||||
safe_clean ~/Library/Caches/com.apple.Safari/fsCachedData/* "Safari cached data"
|
||||
safe_clean ~/Library/Caches/com.apple.WebKit.WebContent/* "WebKit content cache"
|
||||
safe_clean ~/Library/Caches/com.apple.WebKit.Networking/* "WebKit network cache"
|
||||
}
|
||||
|
||||
# Clean sandboxed app caches
|
||||
clean_sandboxed_app_caches() {
|
||||
safe_clean ~/Library/Containers/com.apple.wallpaper.agent/Data/Library/Caches/* "Wallpaper agent cache"
|
||||
@@ -115,44 +126,7 @@ clean_sandboxed_app_caches() {
|
||||
local found_any=false
|
||||
|
||||
for container_dir in "$containers_dir"/*; do
|
||||
[[ -d "$container_dir" ]] || continue
|
||||
|
||||
# Extract bundle ID and check protection status early
|
||||
local bundle_id=$(basename "$container_dir")
|
||||
local bundle_id_lower=$(echo "$bundle_id" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Check explicit critical system components (case-insensitive regex)
|
||||
if [[ "$bundle_id_lower" =~ backgroundtaskmanagement || "$bundle_id_lower" =~ loginitems || "$bundle_id_lower" =~ systempreferences || "$bundle_id_lower" =~ systemsettings || "$bundle_id_lower" =~ settings || "$bundle_id_lower" =~ preferences || "$bundle_id_lower" =~ controlcenter || "$bundle_id_lower" =~ biometrickit || "$bundle_id_lower" =~ sfl || "$bundle_id_lower" =~ tcc ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if should_protect_data "$bundle_id"; then
|
||||
continue
|
||||
elif should_protect_data "$bundle_id_lower"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
local cache_dir="$container_dir/Data/Library/Caches"
|
||||
# Check if dir exists and has content
|
||||
if [[ -d "$cache_dir" ]]; then
|
||||
# Fast check if empty (avoid expensive size calc on empty dirs)
|
||||
if [[ -n "$(ls -A "$cache_dir" 2> /dev/null)" ]]; then
|
||||
# Get size
|
||||
local size=$(get_path_size_kb "$cache_dir")
|
||||
((total_size += size))
|
||||
found_any=true
|
||||
((cleaned_count++))
|
||||
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
# Clean contents safely
|
||||
# We know this is a user cache path, so rm -rf is acceptable here
|
||||
# provided we keep the Cache directory itself
|
||||
for item in "${cache_dir:?}"/*; do
|
||||
safe_remove "$item" true || true
|
||||
done
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
process_container_cache "$container_dir"
|
||||
done
|
||||
|
||||
if [[ -t 1 ]]; then stop_inline_spinner; fi
|
||||
@@ -172,6 +146,46 @@ clean_sandboxed_app_caches() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Process a single container cache directory (reduces nesting)
|
||||
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")
|
||||
local bundle_id_lower=$(echo "$bundle_id" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Check explicit critical system components (case-insensitive regex)
|
||||
if [[ "$bundle_id_lower" =~ backgroundtaskmanagement || "$bundle_id_lower" =~ loginitems || "$bundle_id_lower" =~ systempreferences || "$bundle_id_lower" =~ systemsettings || "$bundle_id_lower" =~ settings || "$bundle_id_lower" =~ preferences || "$bundle_id_lower" =~ controlcenter || "$bundle_id_lower" =~ biometrickit || "$bundle_id_lower" =~ sfl || "$bundle_id_lower" =~ tcc ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if should_protect_data "$bundle_id" || should_protect_data "$bundle_id_lower"; then
|
||||
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)
|
||||
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 (rm -rf is restricted by safe_remove)
|
||||
for item in "$cache_dir"/*; do
|
||||
[[ -e "$item" ]] || continue
|
||||
safe_remove "$item" true || true
|
||||
done
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Clean browser caches (Safari, Chrome, Edge, Firefox, etc.)
|
||||
clean_browsers() {
|
||||
safe_clean ~/Library/Caches/com.apple.Safari/* "Safari cache"
|
||||
@@ -193,9 +207,6 @@ clean_browsers() {
|
||||
safe_clean ~/Library/Caches/com.kagi.kagimacOS/* "Orion cache"
|
||||
safe_clean ~/Library/Caches/zen/* "Zen cache"
|
||||
safe_clean ~/Library/Application\ Support/Firefox/Profiles/*/cache2/* "Firefox profile cache"
|
||||
|
||||
# DISABLED: Service Worker CacheStorage scanning (find can hang on large browser profiles)
|
||||
# Browser caches are already cleaned by the safe_clean calls above
|
||||
}
|
||||
|
||||
# Clean cloud storage app caches
|
||||
@@ -271,14 +282,17 @@ clean_application_support_logs() {
|
||||
|
||||
for candidate in "${start_candidates[@]}"; do
|
||||
if [[ -d "$candidate" ]]; then
|
||||
if [[ -n "$(ls -A "$candidate" 2> /dev/null)" ]]; then
|
||||
if find "$candidate" -mindepth 1 -maxdepth 1 -print -quit 2> /dev/null | grep -q .; then
|
||||
local size=$(get_path_size_kb "$candidate")
|
||||
((total_size += size))
|
||||
((cleaned_count++))
|
||||
found_any=true
|
||||
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
safe_remove "$candidate"/* true > /dev/null 2>&1 || true
|
||||
for item in "$candidate"/*; do
|
||||
[[ -e "$item" ]] || continue
|
||||
safe_remove "$item" true > /dev/null 2>&1 || true
|
||||
done
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -296,14 +310,17 @@ clean_application_support_logs() {
|
||||
|
||||
for candidate in "${gc_candidates[@]}"; do
|
||||
if [[ -d "$candidate" ]]; then
|
||||
if [[ -n "$(ls -A "$candidate" 2> /dev/null)" ]]; then
|
||||
if find "$candidate" -mindepth 1 -maxdepth 1 -print -quit 2> /dev/null | grep -q .; then
|
||||
local size=$(get_path_size_kb "$candidate")
|
||||
((total_size += size))
|
||||
((cleaned_count++))
|
||||
found_any=true
|
||||
|
||||
if [[ "$DRY_RUN" != "true" ]]; then
|
||||
safe_remove "$candidate"/* true > /dev/null 2>&1 || true
|
||||
for item in "$candidate"/*; do
|
||||
[[ -e "$item" ]] || continue
|
||||
safe_remove "$item" true > /dev/null 2>&1 || true
|
||||
done
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -533,12 +533,23 @@ should_protect_path() {
|
||||
|
||||
# 4. Check for specific hardcoded critical patterns
|
||||
case "$path" in
|
||||
*com.apple.Settings* | *com.apple.SystemSettings* | *com.apple.controlcenter* | *com.apple.finder*)
|
||||
*com.apple.Settings* | *com.apple.SystemSettings* | *com.apple.controlcenter* | *com.apple.finder* | *com.apple.dock*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# 5. Check the full path against protected patterns (Broad Glob Match)
|
||||
# 5. Protect critical preference files
|
||||
case "$path" in
|
||||
*/Library/Preferences/com.apple.dock.plist | */Library/Preferences/com.apple.finder.plist)
|
||||
return 0
|
||||
;;
|
||||
# Bluetooth and WiFi configurations
|
||||
*/ByHost/com.apple.bluetooth.* | */ByHost/com.apple.wifi.*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# 6. Check the full path against protected patterns (Broad Glob Match)
|
||||
# This catches things like /Users/tw93/Library/Caches/Claude when pattern is *Claude*
|
||||
for pattern in "${SYSTEM_CRITICAL_BUNDLES[@]}" "${DATA_PROTECTED_BUNDLES[@]}"; do
|
||||
if bundle_matches_pattern "$path" "$pattern"; then
|
||||
@@ -546,7 +557,7 @@ should_protect_path() {
|
||||
fi
|
||||
done
|
||||
|
||||
# 6. Check if the filename itself matches any protected patterns
|
||||
# 7. Check if the filename itself matches any protected patterns
|
||||
local filename
|
||||
filename=$(basename "$path")
|
||||
if should_protect_data "$filename"; then
|
||||
@@ -562,203 +573,124 @@ find_app_files() {
|
||||
local app_name="$2"
|
||||
local -a files_to_clean=()
|
||||
|
||||
# ============================================================================
|
||||
# User-level files (no sudo required)
|
||||
# ============================================================================
|
||||
# Sanitized App Name (remove spaces)
|
||||
local nospace_name="${app_name// /}"
|
||||
local underscore_name="${app_name// /_}"
|
||||
|
||||
# Application Support
|
||||
[[ -d ~/Library/Application\ Support/"$app_name" ]] && files_to_clean+=("$HOME/Library/Application Support/$app_name")
|
||||
[[ -d ~/Library/Application\ Support/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Application Support/$bundle_id")
|
||||
# Standard path patterns for user-level files
|
||||
local -a user_patterns=(
|
||||
"$HOME/Library/Application Support/$app_name"
|
||||
"$HOME/Library/Application Support/$bundle_id"
|
||||
"$HOME/Library/Caches/$bundle_id"
|
||||
"$HOME/Library/Caches/$app_name"
|
||||
"$HOME/Library/Logs/$app_name"
|
||||
"$HOME/Library/Logs/$bundle_id"
|
||||
"$HOME/Library/Application Support/CrashReporter/$app_name"
|
||||
"$HOME/Library/Saved Application State/$bundle_id.savedState"
|
||||
"$HOME/Library/Containers/$bundle_id"
|
||||
"$HOME/Library/WebKit/$bundle_id"
|
||||
"$HOME/Library/WebKit/com.apple.WebKit.WebContent/$bundle_id"
|
||||
"$HOME/Library/HTTPStorages/$bundle_id"
|
||||
"$HOME/Library/Cookies/$bundle_id.binarycookies"
|
||||
"$HOME/Library/LaunchAgents/$bundle_id.plist"
|
||||
"$HOME/Library/Application Scripts/$bundle_id"
|
||||
"$HOME/Library/Services/$app_name.workflow"
|
||||
"$HOME/Library/QuickLook/$app_name.qlgenerator"
|
||||
"$HOME/Library/Internet Plug-Ins/$app_name.plugin"
|
||||
"$HOME/Library/Audio/Plug-Ins/Components/$app_name.component"
|
||||
"$HOME/Library/Audio/Plug-Ins/VST/$app_name.vst"
|
||||
"$HOME/Library/Audio/Plug-Ins/VST3/$app_name.vst3"
|
||||
"$HOME/Library/Audio/Plug-Ins/Digidesign/$app_name.dpm"
|
||||
"$HOME/Library/PreferencePanes/$app_name.prefPane"
|
||||
"$HOME/Library/Screen Savers/$app_name.saver"
|
||||
"$HOME/Library/Frameworks/$app_name.framework"
|
||||
"$HOME/Library/Autosave Information/$bundle_id"
|
||||
"$HOME/Library/Contextual Menu Items/$app_name.plugin"
|
||||
"$HOME/Library/Spotlight/$app_name.mdimporter"
|
||||
"$HOME/Library/ColorPickers/$app_name.colorPicker"
|
||||
"$HOME/Library/Workflows/$app_name.workflow"
|
||||
"$HOME/.config/$app_name"
|
||||
"$HOME/.local/share/$app_name"
|
||||
"$HOME/.$app_name"
|
||||
"$HOME/.$app_name"rc
|
||||
)
|
||||
|
||||
# Sanitized App Name (remove spaces) - e.g. "Visual Studio Code" -> "VisualStudioCode"
|
||||
# Add sanitized name variants if unique enough
|
||||
if [[ ${#app_name} -gt 3 && "$app_name" =~ [[:space:]] ]]; then
|
||||
local nospace_name="${app_name// /}"
|
||||
[[ -d ~/Library/Application\ Support/"$nospace_name" ]] && files_to_clean+=("$HOME/Library/Application Support/$nospace_name")
|
||||
[[ -d ~/Library/Caches/"$nospace_name" ]] && files_to_clean+=("$HOME/Library/Caches/$nospace_name")
|
||||
[[ -d ~/Library/Logs/"$nospace_name" ]] && files_to_clean+=("$HOME/Library/Logs/$nospace_name")
|
||||
|
||||
local underscore_name="${app_name// /_}"
|
||||
[[ -d ~/Library/Application\ Support/"$underscore_name" ]] && files_to_clean+=("$HOME/Library/Application Support/$underscore_name")
|
||||
user_patterns+=(
|
||||
"$HOME/Library/Application Support/$nospace_name"
|
||||
"$HOME/Library/Caches/$nospace_name"
|
||||
"$HOME/Library/Logs/$nospace_name"
|
||||
"$HOME/Library/Application Support/$underscore_name"
|
||||
)
|
||||
fi
|
||||
|
||||
# Caches
|
||||
[[ -d ~/Library/Caches/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Caches/$bundle_id")
|
||||
[[ -d ~/Library/Caches/"$app_name" ]] && files_to_clean+=("$HOME/Library/Caches/$app_name")
|
||||
# Process standard patterns
|
||||
for p in "${user_patterns[@]}"; do
|
||||
local expanded_path="${p/#\~/$HOME}"
|
||||
[[ -e "$expanded_path" ]] && files_to_clean+=("$expanded_path")
|
||||
done
|
||||
|
||||
# Preferences
|
||||
# Preferences and ByHost (special handling)
|
||||
[[ -f ~/Library/Preferences/"$bundle_id".plist ]] && files_to_clean+=("$HOME/Library/Preferences/$bundle_id.plist")
|
||||
[[ -d ~/Library/Preferences/ByHost ]] && while IFS= read -r -d '' pref; do
|
||||
files_to_clean+=("$pref")
|
||||
done < <(find ~/Library/Preferences/ByHost \( -name "$bundle_id*.plist" \) -print0 2> /dev/null)
|
||||
done < <(command find ~/Library/Preferences/ByHost -maxdepth 1 \( -name "$bundle_id*.plist" \) -print0 2> /dev/null)
|
||||
|
||||
# Logs
|
||||
[[ -d ~/Library/Logs/"$app_name" ]] && files_to_clean+=("$HOME/Library/Logs/$app_name")
|
||||
[[ -d ~/Library/Logs/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Logs/$bundle_id")
|
||||
# CrashReporter
|
||||
[[ -d ~/Library/Application\ Support/CrashReporter/"$app_name" ]] && files_to_clean+=("$HOME/Library/Application Support/CrashReporter/$app_name")
|
||||
# Group Containers (special handling)
|
||||
if [[ -d ~/Library/Group\ Containers ]]; then
|
||||
while IFS= read -r -d '' container; do
|
||||
files_to_clean+=("$container")
|
||||
done < <(command find ~/Library/Group\ Containers -maxdepth 1 \( -name "*$bundle_id*" \) -print0 2> /dev/null)
|
||||
fi
|
||||
|
||||
# Saved Application State
|
||||
[[ -d ~/Library/Saved\ Application\ State/"$bundle_id".savedState ]] && files_to_clean+=("$HOME/Library/Saved Application State/$bundle_id.savedState")
|
||||
|
||||
# Containers (sandboxed apps)
|
||||
[[ -d ~/Library/Containers/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Containers/$bundle_id")
|
||||
|
||||
# Group Containers
|
||||
[[ -d ~/Library/Group\ Containers ]] && while IFS= read -r -d '' container; do
|
||||
files_to_clean+=("$container")
|
||||
done < <(find ~/Library/Group\ Containers -type d \( -name "*$bundle_id*" \) -print0 2> /dev/null)
|
||||
|
||||
# WebKit data
|
||||
[[ -d ~/Library/WebKit/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/WebKit/$bundle_id")
|
||||
[[ -d ~/Library/WebKit/com.apple.WebKit.WebContent/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/WebKit/com.apple.WebKit.WebContent/$bundle_id")
|
||||
|
||||
# HTTP Storage
|
||||
[[ -d ~/Library/HTTPStorages/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/HTTPStorages/$bundle_id")
|
||||
|
||||
# Cookies
|
||||
[[ -f ~/Library/Cookies/"$bundle_id".binarycookies ]] && files_to_clean+=("$HOME/Library/Cookies/$bundle_id.binarycookies")
|
||||
|
||||
# Launch Agents (user-level)
|
||||
[[ -f ~/Library/LaunchAgents/"$bundle_id".plist ]] && files_to_clean+=("$HOME/Library/LaunchAgents/$bundle_id.plist")
|
||||
# Search for LaunchAgents by app name if unique enough
|
||||
if [[ ${#app_name} -gt 3 ]]; then
|
||||
# Launch Agents by name (special handling)
|
||||
if [[ ${#app_name} -gt 3 ]] && [[ -d ~/Library/LaunchAgents ]]; then
|
||||
while IFS= read -r -d '' plist; do
|
||||
files_to_clean+=("$plist")
|
||||
done < <(find ~/Library/LaunchAgents -name "*$app_name*.plist" -print0 2> /dev/null)
|
||||
done < <(command find ~/Library/LaunchAgents -maxdepth 1 \( -name "*$app_name*.plist" \) -print0 2> /dev/null)
|
||||
fi
|
||||
|
||||
# Application Scripts
|
||||
[[ -d ~/Library/Application\ Scripts/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Application Scripts/$bundle_id")
|
||||
|
||||
# Services
|
||||
[[ -d ~/Library/Services/"$app_name".workflow ]] && files_to_clean+=("$HOME/Library/Services/$app_name.workflow")
|
||||
|
||||
# QuickLook Plugins
|
||||
[[ -d ~/Library/QuickLook/"$app_name".qlgenerator ]] && files_to_clean+=("$HOME/Library/QuickLook/$app_name.qlgenerator")
|
||||
|
||||
# Internet Plug-Ins
|
||||
[[ -d ~/Library/Internet\ Plug-Ins/"$app_name".plugin ]] && files_to_clean+=("$HOME/Library/Internet Plug-Ins/$app_name.plugin")
|
||||
|
||||
# Audio Plug-Ins (Components, VST, VST3)
|
||||
[[ -d ~/Library/Audio/Plug-Ins/Components/"$app_name".component ]] && files_to_clean+=("$HOME/Library/Audio/Plug-Ins/Components/$app_name.component")
|
||||
[[ -d ~/Library/Audio/Plug-Ins/VST/"$app_name".vst ]] && files_to_clean+=("$HOME/Library/Audio/Plug-Ins/VST/$app_name.vst")
|
||||
[[ -d ~/Library/Audio/Plug-Ins/VST3/"$app_name".vst3 ]] && files_to_clean+=("$HOME/Library/Audio/Plug-Ins/VST3/$app_name.vst3")
|
||||
[[ -d ~/Library/Audio/Plug-Ins/Digidesign/"$app_name".dpm ]] && files_to_clean+=("$HOME/Library/Audio/Plug-Ins/Digidesign/$app_name.dpm")
|
||||
|
||||
# Preference Panes
|
||||
[[ -d ~/Library/PreferencePanes/"$app_name".prefPane ]] && files_to_clean+=("$HOME/Library/PreferencePanes/$app_name.prefPane")
|
||||
|
||||
# Screen Savers
|
||||
[[ -d ~/Library/Screen\ Savers/"$app_name".saver ]] && files_to_clean+=("$HOME/Library/Screen Savers/$app_name.saver")
|
||||
|
||||
# Frameworks
|
||||
[[ -d ~/Library/Frameworks/"$app_name".framework ]] && files_to_clean+=("$HOME/Library/Frameworks/$app_name.framework")
|
||||
|
||||
# Autosave Information
|
||||
[[ -d ~/Library/Autosave\ Information/"$bundle_id" ]] && files_to_clean+=("$HOME/Library/Autosave Information/$bundle_id")
|
||||
|
||||
# Contextual Menu Items
|
||||
[[ -d ~/Library/Contextual\ Menu\ Items/"$app_name".plugin ]] && files_to_clean+=("$HOME/Library/Contextual Menu Items/$app_name.plugin")
|
||||
|
||||
# Spotlight Plugins
|
||||
[[ -d ~/Library/Spotlight/"$app_name".mdimporter ]] && files_to_clean+=("$HOME/Library/Spotlight/$app_name.mdimporter")
|
||||
|
||||
# Color Pickers
|
||||
[[ -d ~/Library/ColorPickers/"$app_name".colorPicker ]] && files_to_clean+=("$HOME/Library/ColorPickers/$app_name.colorPicker")
|
||||
|
||||
# Workflows
|
||||
[[ -d ~/Library/Workflows/"$app_name".workflow ]] && files_to_clean+=("$HOME/Library/Workflows/$app_name.workflow")
|
||||
|
||||
# Unix-style configuration directories and files (cross-platform apps)
|
||||
[[ -d ~/.config/"$app_name" ]] && files_to_clean+=("$HOME/.config/$app_name")
|
||||
[[ -d ~/.local/share/"$app_name" ]] && files_to_clean+=("$HOME/.local/share/$app_name")
|
||||
[[ -d ~/."$app_name" ]] && files_to_clean+=("$HOME/.$app_name")
|
||||
[[ -f ~/."${app_name}rc" ]] && files_to_clean+=("$HOME/.${app_name}rc")
|
||||
|
||||
# ============================================================================
|
||||
# IDE-specific SDK and Toolchain directories
|
||||
# ============================================================================
|
||||
|
||||
# DevEco-Studio (HarmonyOS/OpenHarmony IDE by Huawei)
|
||||
# Specialized toolchain cleanup (non-loopable or highly specific)
|
||||
# 1. DevEco-Studio (Huawei)
|
||||
if [[ "$app_name" =~ DevEco|deveco ]] || [[ "$bundle_id" =~ huawei.*deveco ]]; then
|
||||
[[ -d ~/DevEcoStudioProjects ]] && files_to_clean+=("$HOME/DevEcoStudioProjects")
|
||||
[[ -d ~/DevEco-Studio ]] && files_to_clean+=("$HOME/DevEco-Studio")
|
||||
[[ -d ~/Library/Application\ Support/Huawei ]] && files_to_clean+=("$HOME/Library/Application Support/Huawei")
|
||||
[[ -d ~/Library/Caches/Huawei ]] && files_to_clean+=("$HOME/Library/Caches/Huawei")
|
||||
[[ -d ~/Library/Logs/Huawei ]] && files_to_clean+=("$HOME/Library/Logs/Huawei")
|
||||
[[ -d ~/Library/Huawei ]] && files_to_clean+=("$HOME/Library/Huawei")
|
||||
[[ -d ~/Huawei ]] && files_to_clean+=("$HOME/Huawei")
|
||||
[[ -d ~/HarmonyOS ]] && files_to_clean+=("$HOME/HarmonyOS")
|
||||
[[ -d ~/.huawei ]] && files_to_clean+=("$HOME/.huawei")
|
||||
[[ -d ~/.ohos ]] && files_to_clean+=("$HOME/.ohos")
|
||||
for d in ~/DevEcoStudioProjects ~/DevEco-Studio ~/Library/Application\ Support/Huawei ~/Library/Caches/Huawei ~/Library/Logs/Huawei ~/Library/Huawei ~/Huawei ~/HarmonyOS ~/.huawei ~/.ohos; do
|
||||
[[ -d "$d" ]] && files_to_clean+=("$d")
|
||||
done
|
||||
fi
|
||||
|
||||
# Android Studio
|
||||
# 2. Android Studio (Google)
|
||||
if [[ "$app_name" =~ Android.*Studio|android.*studio ]] || [[ "$bundle_id" =~ google.*android.*studio|jetbrains.*android ]]; then
|
||||
[[ -d ~/AndroidStudioProjects ]] && files_to_clean+=("$HOME/AndroidStudioProjects")
|
||||
[[ -d ~/Library/Android ]] && files_to_clean+=("$HOME/Library/Android")
|
||||
[[ -d ~/.android ]] && files_to_clean+=("$HOME/.android")
|
||||
[[ -d ~/.gradle ]] && files_to_clean+=("$HOME/.gradle")
|
||||
[[ -d ~/Library/Application\ Support/Google ]] &&
|
||||
while IFS= read -r -d '' dir; do files_to_clean+=("$dir"); done < <(find ~/Library/Application\ Support/Google -maxdepth 1 -name "AndroidStudio*" -print0 2> /dev/null)
|
||||
for d in ~/AndroidStudioProjects ~/Library/Android ~/.android ~/.gradle; do
|
||||
[[ -d "$d" ]] && files_to_clean+=("$d")
|
||||
done
|
||||
[[ -d ~/Library/Application\ Support/Google ]] && while IFS= read -r -d '' d; do files_to_clean+=("$d"); done < <(command find ~/Library/Application\ Support/Google -maxdepth 1 -name "AndroidStudio*" -print0 2> /dev/null)
|
||||
fi
|
||||
|
||||
# Xcode
|
||||
# 3. Xcode (Apple)
|
||||
if [[ "$app_name" =~ Xcode|xcode ]] || [[ "$bundle_id" =~ apple.*xcode ]]; then
|
||||
[[ -d ~/Library/Developer ]] && files_to_clean+=("$HOME/Library/Developer")
|
||||
[[ -d ~/.Xcode ]] && files_to_clean+=("$HOME/.Xcode")
|
||||
fi
|
||||
|
||||
# IntelliJ IDEA, PyCharm, WebStorm, etc. (JetBrains IDEs)
|
||||
# 4. JetBrains (IDE settings)
|
||||
if [[ "$bundle_id" =~ jetbrains ]] || [[ "$app_name" =~ IntelliJ|PyCharm|WebStorm|GoLand|RubyMine|PhpStorm|CLion|DataGrip|Rider ]]; then
|
||||
local ide_name="$app_name"
|
||||
[[ -d ~/Library/Application\ Support/JetBrains ]] &&
|
||||
while IFS= read -r -d '' dir; do files_to_clean+=("$dir"); done < <(find ~/Library/Application\ Support/JetBrains -maxdepth 1 -name "${ide_name}*" -print0 2> /dev/null)
|
||||
[[ -d ~/Library/Caches/JetBrains ]] &&
|
||||
while IFS= read -r -d '' dir; do files_to_clean+=("$dir"); done < <(find ~/Library/Caches/JetBrains -maxdepth 1 -name "${ide_name}*" -print0 2> /dev/null)
|
||||
[[ -d ~/Library/Logs/JetBrains ]] &&
|
||||
while IFS= read -r -d '' dir; do files_to_clean+=("$dir"); done < <(find ~/Library/Logs/JetBrains -maxdepth 1 -name "${ide_name}*" -print0 2> /dev/null)
|
||||
for base in ~/Library/Application\ Support/JetBrains ~/Library/Caches/JetBrains ~/Library/Logs/JetBrains; do
|
||||
[[ -d "$base" ]] && while IFS= read -r -d '' d; do files_to_clean+=("$d"); done < <(command find "$base" -maxdepth 1 -name "${app_name}*" -print0 2> /dev/null)
|
||||
done
|
||||
fi
|
||||
|
||||
# Unity
|
||||
if [[ "$app_name" =~ Unity|unity ]] || [[ "$bundle_id" =~ unity ]]; then
|
||||
[[ -d ~/.local/share/unity3d ]] && files_to_clean+=("$HOME/.local/share/unity3d")
|
||||
[[ -d ~/Library/Unity ]] && files_to_clean+=("$HOME/Library/Unity")
|
||||
fi
|
||||
# 5. Unity / Unreal / Godot
|
||||
[[ "$app_name" =~ Unity|unity ]] && [[ -d ~/Library/Unity ]] && files_to_clean+=("$HOME/Library/Unity")
|
||||
[[ "$app_name" =~ Unreal|unreal ]] && [[ -d ~/Library/Application\ Support/Epic ]] && files_to_clean+=("$HOME/Library/Application Support/Epic")
|
||||
[[ "$app_name" =~ Godot|godot ]] && [[ -d ~/Library/Application\ Support/Godot ]] && files_to_clean+=("$HOME/Library/Application Support/Godot")
|
||||
|
||||
# Unreal Engine
|
||||
if [[ "$app_name" =~ Unreal|unreal ]] || [[ "$bundle_id" =~ unrealengine|epicgames ]]; then
|
||||
[[ -d ~/Library/Application\ Support/Epic ]] && files_to_clean+=("$HOME/Library/Application Support/Epic")
|
||||
[[ -d ~/Documents/Unreal\ Projects ]] && files_to_clean+=("$HOME/Documents/Unreal Projects")
|
||||
fi
|
||||
# 6. Tools
|
||||
[[ "$bundle_id" =~ microsoft.*vscode ]] && [[ -d ~/.vscode ]] && files_to_clean+=("$HOME/.vscode")
|
||||
[[ "$app_name" =~ Docker ]] && [[ -d ~/.docker ]] && files_to_clean+=("$HOME/.docker")
|
||||
|
||||
# Visual Studio Code
|
||||
if [[ "$bundle_id" =~ microsoft.*vscode|visualstudio.*code ]]; then
|
||||
[[ -d ~/.vscode ]] && files_to_clean+=("$HOME/.vscode")
|
||||
[[ -d ~/.vscode-insiders ]] && files_to_clean+=("$HOME/.vscode-insiders")
|
||||
fi
|
||||
|
||||
# Flutter
|
||||
if [[ "$app_name" =~ Flutter|flutter ]] || [[ "$bundle_id" =~ flutter ]]; then
|
||||
[[ -d ~/.pub-cache ]] && files_to_clean+=("$HOME/.pub-cache")
|
||||
[[ -d ~/flutter ]] && files_to_clean+=("$HOME/flutter")
|
||||
fi
|
||||
|
||||
# Godot
|
||||
if [[ "$app_name" =~ Godot|godot ]] || [[ "$bundle_id" =~ godot ]]; then
|
||||
[[ -d ~/.local/share/godot ]] && files_to_clean+=("$HOME/.local/share/godot")
|
||||
[[ -d ~/Library/Application\ Support/Godot ]] && files_to_clean+=("$HOME/Library/Application Support/Godot")
|
||||
fi
|
||||
|
||||
# Docker Desktop
|
||||
if [[ "$app_name" =~ Docker ]] || [[ "$bundle_id" =~ docker ]]; then
|
||||
[[ -d ~/.docker ]] && files_to_clean+=("$HOME/.docker")
|
||||
fi
|
||||
|
||||
# Only print if array has elements to avoid unbound variable error
|
||||
if [[ ${#files_to_clean[@]} -gt 0 ]]; then
|
||||
printf '%s\n' "${files_to_clean[@]}"
|
||||
fi
|
||||
# Output results
|
||||
[[ ${#files_to_clean[@]} -gt 0 ]] && printf '%s\n' "${files_to_clean[@]}"
|
||||
}
|
||||
|
||||
# Find system-level app files (requires sudo)
|
||||
@@ -767,82 +699,63 @@ find_app_system_files() {
|
||||
local app_name="$2"
|
||||
local -a system_files=()
|
||||
|
||||
# System Application Support
|
||||
[[ -d /Library/Application\ Support/"$app_name" ]] && system_files+=("/Library/Application Support/$app_name")
|
||||
[[ -d /Library/Application\ Support/"$bundle_id" ]] && system_files+=("/Library/Application Support/$bundle_id")
|
||||
|
||||
# Sanitized App Name (remove spaces)
|
||||
local nospace_name="${app_name// /}"
|
||||
|
||||
# Standard system path patterns
|
||||
local -a system_patterns=(
|
||||
"/Library/Application Support/$app_name"
|
||||
"/Library/Application Support/$bundle_id"
|
||||
"/Library/LaunchAgents/$bundle_id.plist"
|
||||
"/Library/LaunchDaemons/$bundle_id.plist"
|
||||
"/Library/Preferences/$bundle_id.plist"
|
||||
"/Library/Receipts/$bundle_id.bom"
|
||||
"/Library/Receipts/$bundle_id.plist"
|
||||
"/Library/Frameworks/$app_name.framework"
|
||||
"/Library/Internet Plug-Ins/$app_name.plugin"
|
||||
"/Library/Audio/Plug-Ins/Components/$app_name.component"
|
||||
"/Library/Audio/Plug-Ins/VST/$app_name.vst"
|
||||
"/Library/Audio/Plug-Ins/VST3/$app_name.vst3"
|
||||
"/Library/Audio/Plug-Ins/Digidesign/$app_name.dpm"
|
||||
"/Library/QuickLook/$app_name.qlgenerator"
|
||||
"/Library/PreferencePanes/$app_name.prefPane"
|
||||
"/Library/Screen Savers/$app_name.saver"
|
||||
"/Library/Caches/$bundle_id"
|
||||
"/Library/Caches/$app_name"
|
||||
)
|
||||
|
||||
if [[ ${#app_name} -gt 3 && "$app_name" =~ [[:space:]] ]]; then
|
||||
local nospace_name="${app_name// /}"
|
||||
[[ -d /Library/Application\ Support/"$nospace_name" ]] && system_files+=("/Library/Application Support/$nospace_name")
|
||||
[[ -d /Library/Caches/"$nospace_name" ]] && system_files+=("/Library/Caches/$nospace_name")
|
||||
[[ -d /Library/Logs/"$nospace_name" ]] && system_files+=("/Library/Logs/$nospace_name")
|
||||
system_patterns+=(
|
||||
"/Library/Application Support/$nospace_name"
|
||||
"/Library/Caches/$nospace_name"
|
||||
"/Library/Logs/$nospace_name"
|
||||
)
|
||||
fi
|
||||
|
||||
# System Launch Agents
|
||||
[[ -f /Library/LaunchAgents/"$bundle_id".plist ]] && system_files+=("/Library/LaunchAgents/$bundle_id.plist")
|
||||
# Search for LaunchAgents by app name if unique enough
|
||||
# Process patterns
|
||||
for p in "${system_patterns[@]}"; do
|
||||
[[ -e "$p" ]] && system_files+=("$p")
|
||||
done
|
||||
|
||||
# System LaunchAgents/LaunchDaemons by name
|
||||
if [[ ${#app_name} -gt 3 ]]; then
|
||||
while IFS= read -r -d '' plist; do
|
||||
system_files+=("$plist")
|
||||
done < <(find /Library/LaunchAgents -name "*$app_name*.plist" -print0 2> /dev/null)
|
||||
for base in /Library/LaunchAgents /Library/LaunchDaemons; do
|
||||
[[ -d "$base" ]] && while IFS= read -r -d '' plist; do
|
||||
system_files+=("$plist")
|
||||
done < <(command find "$base" -maxdepth 1 \( -name "*$app_name*.plist" \) -print0 2> /dev/null)
|
||||
done
|
||||
fi
|
||||
|
||||
# System Launch Daemons
|
||||
[[ -f /Library/LaunchDaemons/"$bundle_id".plist ]] && system_files+=("/Library/LaunchDaemons/$bundle_id.plist")
|
||||
# Search for LaunchDaemons by app name if unique enough
|
||||
if [[ ${#app_name} -gt 3 ]]; then
|
||||
while IFS= read -r -d '' plist; do
|
||||
system_files+=("$plist")
|
||||
done < <(find /Library/LaunchDaemons -name "*$app_name*.plist" -print0 2> /dev/null)
|
||||
fi
|
||||
|
||||
# Privileged Helper Tools
|
||||
# Privileged Helper Tools and Receipts (special handling)
|
||||
[[ -d /Library/PrivilegedHelperTools ]] && while IFS= read -r -d '' helper; do
|
||||
system_files+=("$helper")
|
||||
done < <(find /Library/PrivilegedHelperTools \( -name "$bundle_id*" \) -print0 2> /dev/null)
|
||||
done < <(command find /Library/PrivilegedHelperTools -maxdepth 1 \( -name "$bundle_id*" \) -print0 2> /dev/null)
|
||||
|
||||
# System Preferences
|
||||
[[ -f /Library/Preferences/"$bundle_id".plist ]] && system_files+=("/Library/Preferences/$bundle_id.plist")
|
||||
|
||||
# Installation Receipts
|
||||
[[ -d /private/var/db/receipts ]] && while IFS= read -r -d '' receipt; do
|
||||
system_files+=("$receipt")
|
||||
done < <(find /private/var/db/receipts \( -name "*$bundle_id*" \) -print0 2> /dev/null)
|
||||
done < <(command find /private/var/db/receipts -maxdepth 1 \( -name "*$bundle_id*" \) -print0 2> /dev/null)
|
||||
|
||||
# System Logs
|
||||
[[ -d /Library/Logs/"$app_name" ]] && system_files+=("/Library/Logs/$app_name")
|
||||
[[ -d /Library/Logs/"$bundle_id" ]] && system_files+=("/Library/Logs/$bundle_id")
|
||||
|
||||
# System Frameworks
|
||||
[[ -d /Library/Frameworks/"$app_name".framework ]] && system_files+=("/Library/Frameworks/$app_name.framework")
|
||||
|
||||
# System Internet Plug-Ins
|
||||
[[ -d /Library/Internet\ Plug-Ins/"$app_name".plugin ]] && system_files+=("/Library/Internet Plug-Ins/$app_name.plugin")
|
||||
|
||||
# System Audio Plug-Ins
|
||||
[[ -d /Library/Audio/Plug-Ins/Components/"$app_name".component ]] && system_files+=("/Library/Audio/Plug-Ins/Components/$app_name.component")
|
||||
[[ -d /Library/Audio/Plug-Ins/VST/"$app_name".vst ]] && system_files+=("/Library/Audio/Plug-Ins/VST/$app_name.vst")
|
||||
[[ -d /Library/Audio/Plug-Ins/VST3/"$app_name".vst3 ]] && system_files+=("/Library/Audio/Plug-Ins/VST3/$app_name.vst3")
|
||||
[[ -d /Library/Audio/Plug-Ins/Digidesign/"$app_name".dpm ]] && system_files+=("/Library/Audio/Plug-Ins/Digidesign/$app_name.dpm")
|
||||
|
||||
# System QuickLook Plugins
|
||||
[[ -d /Library/QuickLook/"$app_name".qlgenerator ]] && system_files+=("/Library/QuickLook/$app_name.qlgenerator")
|
||||
|
||||
# System Preference Panes
|
||||
[[ -d /Library/PreferencePanes/"$app_name".prefPane ]] && system_files+=("/Library/PreferencePanes/$app_name.prefPane")
|
||||
|
||||
# System Screen Savers
|
||||
[[ -d /Library/Screen\ Savers/"$app_name".saver ]] && system_files+=("/Library/Screen Savers/$app_name.saver")
|
||||
|
||||
# System Caches
|
||||
[[ -d /Library/Caches/"$bundle_id" ]] && system_files+=("/Library/Caches/$bundle_id")
|
||||
[[ -d /Library/Caches/"$app_name" ]] && system_files+=("/Library/Caches/$app_name")
|
||||
|
||||
# Only print if array has elements
|
||||
if [[ ${#system_files[@]} -gt 0 ]]; then
|
||||
printf '%s\n' "${system_files[@]}"
|
||||
fi
|
||||
[[ ${#system_files[@]} -gt 0 ]] && printf '%s\n' "${system_files[@]}"
|
||||
|
||||
# Find files from receipts (Deep Scan)
|
||||
find_app_receipt_files "$bundle_id"
|
||||
@@ -863,7 +776,7 @@ find_app_receipt_files() {
|
||||
if [[ -d /private/var/db/receipts ]]; then
|
||||
while IFS= read -r -d '' bom; do
|
||||
bom_files+=("$bom")
|
||||
done < <(find /private/var/db/receipts -name "${bundle_id}*.bom" -print0 2> /dev/null)
|
||||
done < <(find /private/var/db/receipts -maxdepth 1 -name "${bundle_id}*.bom" -print0 2> /dev/null)
|
||||
fi
|
||||
|
||||
# Process bom files if any found
|
||||
|
||||
@@ -48,6 +48,7 @@ readonly MOLE_TEMP_FILE_AGE_DAYS=7 # Temp file cleanup threshold
|
||||
readonly MOLE_ORPHAN_AGE_DAYS=60 # Orphaned data threshold
|
||||
readonly MOLE_MAX_PARALLEL_JOBS=15 # Parallel job limit
|
||||
readonly MOLE_MAIL_DOWNLOADS_MIN_KB=5120 # Mail attachments size threshold
|
||||
readonly MOLE_MAIL_AGE_DAYS=30 # Mail attachment cleanup threshold (30+ days)
|
||||
readonly MOLE_LOG_AGE_DAYS=7 # System log retention
|
||||
readonly MOLE_CRASH_REPORT_AGE_DAYS=7 # Crash report retention
|
||||
readonly MOLE_SAVED_STATE_AGE_DAYS=7 # App saved state retention
|
||||
|
||||
@@ -71,7 +71,7 @@ update_via_homebrew() {
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Clear update cache
|
||||
# Clear update cache (suppress errors if cache doesn't exist or is locked)
|
||||
rm -f "$HOME/.cache/mole/version_check" "$HOME/.cache/mole/update_message" 2> /dev/null || true
|
||||
}
|
||||
|
||||
|
||||
@@ -188,7 +188,7 @@ safe_find_delete() {
|
||||
-maxdepth 5 \
|
||||
-name "$pattern" \
|
||||
-type "$type_filter" \
|
||||
-delete 2> /dev/null || true
|
||||
-delete 2> /dev/null || true # Suppress expected errors when files are removed or protected by other processes
|
||||
else
|
||||
# Delete files older than age_days
|
||||
command find "$base_dir" \
|
||||
@@ -196,7 +196,7 @@ safe_find_delete() {
|
||||
-name "$pattern" \
|
||||
-type "$type_filter" \
|
||||
-mtime "+$age_days" \
|
||||
-delete 2> /dev/null || true
|
||||
-delete 2> /dev/null || true # Suppress expected errors when files are removed or protected by other processes
|
||||
fi
|
||||
|
||||
return 0
|
||||
@@ -237,14 +237,14 @@ safe_sudo_find_delete() {
|
||||
-maxdepth 5 \
|
||||
-name "$pattern" \
|
||||
-type "$type_filter" \
|
||||
-delete 2> /dev/null || true
|
||||
-delete 2> /dev/null || true # Ignore transient errors for system files that might be in use or protected
|
||||
else
|
||||
sudo find "$base_dir" \
|
||||
-maxdepth 5 \
|
||||
-name "$pattern" \
|
||||
-type "$type_filter" \
|
||||
-mtime "+$age_days" \
|
||||
-delete 2> /dev/null || true
|
||||
-delete 2> /dev/null || true # Ignore transient errors for system files that might be in use or protected
|
||||
fi
|
||||
|
||||
return 0
|
||||
|
||||
@@ -3,6 +3,19 @@
|
||||
|
||||
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)
|
||||
readonly MOLE_TM_THIN_TIMEOUT=180
|
||||
readonly MOLE_TM_THIN_VALUE=9999999999
|
||||
|
||||
# Helper function: Flush DNS cache
|
||||
flush_dns_cache() {
|
||||
sudo dscacheutil -flushcache 2> /dev/null && sudo killall -HUP mDNSResponder 2> /dev/null
|
||||
}
|
||||
|
||||
# System maintenance: rebuild databases and flush caches
|
||||
opt_system_maintenance() {
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Rebuilding LaunchServices database..."
|
||||
@@ -10,25 +23,21 @@ opt_system_maintenance() {
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} LaunchServices database rebuilt"
|
||||
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Clearing DNS cache..."
|
||||
if sudo dscacheutil -flushcache 2> /dev/null && sudo killall -HUP mDNSResponder 2> /dev/null; then
|
||||
if flush_dns_cache; then
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} DNS cache cleared"
|
||||
else
|
||||
echo -e "${RED}${ICON_ERROR}${NC} Failed to clear DNS cache"
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Checking Spotlight index..."
|
||||
local md_status
|
||||
md_status=$(mdutil -s / 2> /dev/null || echo "")
|
||||
if echo "$md_status" | grep -qi "Indexing disabled"; then
|
||||
local spotlight_status
|
||||
spotlight_status=$(mdutil -s / 2> /dev/null || echo "")
|
||||
if echo "$spotlight_status" | grep -qi "Indexing disabled"; then
|
||||
echo -e "${GRAY}-${NC} Spotlight indexing disabled"
|
||||
else
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Spotlight index functioning"
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Refreshing Bluetooth services..."
|
||||
sudo pkill -f blued 2> /dev/null || true
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Bluetooth controller refreshed"
|
||||
|
||||
}
|
||||
|
||||
# Cache refresh: update Finder/Safari caches
|
||||
@@ -131,19 +140,47 @@ opt_radio_refresh() {
|
||||
|
||||
# Mail downloads: clear OLD Mail attachment cache (30+ days)
|
||||
opt_mail_downloads() {
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Clearing old Mail attachment downloads (30+ days)..."
|
||||
# Validate configuration parameters
|
||||
# Validate configuration parameters
|
||||
local min_size_kb=${MOLE_MAIL_DOWNLOADS_MIN_KB:-5120}
|
||||
local mail_age_days=${MOLE_MAIL_AGE_DAYS:-30}
|
||||
if ! [[ "$min_size_kb" =~ ^[0-9]+$ ]]; then
|
||||
min_size_kb=5120
|
||||
fi
|
||||
if ! [[ "$mail_age_days" =~ ^[0-9]+$ ]]; then
|
||||
mail_age_days=30
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}${ICON_ARROW}${NC} Clearing old Mail attachment downloads (${mail_age_days}+ days)..."
|
||||
local -a mail_dirs=(
|
||||
"$HOME/Library/Mail Downloads"
|
||||
"$HOME/Library/Containers/com.apple.mail/Data/Library/Mail Downloads"
|
||||
)
|
||||
|
||||
local total_kb=0
|
||||
local total_size_kb=0
|
||||
local temp_dir
|
||||
temp_dir=$(create_temp_dir)
|
||||
|
||||
# Parallel size calculation
|
||||
local idx=0
|
||||
for target_path in "${mail_dirs[@]}"; do
|
||||
total_kb=$((total_kb + $(get_path_size_kb "$target_path")))
|
||||
(
|
||||
local size
|
||||
size=$(get_path_size_kb "$target_path")
|
||||
echo "$size" > "$temp_dir/size_$idx"
|
||||
) &
|
||||
((idx++))
|
||||
done
|
||||
wait
|
||||
|
||||
for i in $(seq 0 $((idx - 1))); do
|
||||
local size=0
|
||||
[[ -f "$temp_dir/size_$i" ]] && size=$(cat "$temp_dir/size_$i")
|
||||
((total_size_kb += size))
|
||||
done
|
||||
|
||||
if [[ $total_kb -lt $MOLE_MAIL_DOWNLOADS_MIN_KB ]]; then
|
||||
echo -e "${GRAY}-${NC} Only $(bytes_to_human $((total_kb * 1024))) detected, skipping cleanup"
|
||||
if [[ $total_size_kb -lt $min_size_kb ]]; then
|
||||
echo -e "${GRAY}-${NC} Only $(bytes_to_human $((total_size_kb * 1024))) detected, skipping cleanup"
|
||||
return
|
||||
fi
|
||||
|
||||
@@ -151,13 +188,13 @@ opt_mail_downloads() {
|
||||
local cleaned=false
|
||||
for target_path in "${mail_dirs[@]}"; do
|
||||
if [[ -d "$target_path" ]]; then
|
||||
safe_find_delete "$target_path" "*" "$MOLE_LOG_AGE_DAYS" "f"
|
||||
safe_find_delete "$target_path" "*" "$mail_age_days" "f"
|
||||
cleaned=true
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$cleaned" == "true" ]]; then
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Cleaned old attachments (> ${MOLE_LOG_AGE_DAYS} days)"
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Cleaned old attachments (> ${mail_age_days} days)"
|
||||
else
|
||||
echo -e "${GRAY}-${NC} No old attachments found"
|
||||
fi
|
||||
@@ -230,7 +267,13 @@ opt_local_snapshots() {
|
||||
fi
|
||||
|
||||
local success=false
|
||||
if run_with_timeout 180 sudo tmutil thinlocalsnapshots / 9999999999 4 > /dev/null 2>&1; then
|
||||
local exit_code=0
|
||||
set +e
|
||||
run_with_timeout "$MOLE_TM_THIN_TIMEOUT" sudo tmutil thinlocalsnapshots / "$MOLE_TM_THIN_VALUE" 4 > /dev/null 2>&1
|
||||
exit_code=$?
|
||||
set -e
|
||||
|
||||
if [[ "$exit_code" -eq 0 ]]; then
|
||||
success=true
|
||||
fi
|
||||
|
||||
@@ -244,8 +287,10 @@ opt_local_snapshots() {
|
||||
|
||||
if [[ "$success" == "true" ]]; then
|
||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Removed $removed snapshots (remaining: $after)"
|
||||
elif [[ "$exit_code" -eq 124 ]]; then
|
||||
echo -e "${YELLOW}!${NC} Timed out after ${MOLE_TM_THIN_TIMEOUT}s"
|
||||
else
|
||||
echo -e "${YELLOW}!${NC} Timed out or failed"
|
||||
echo -e "${YELLOW}!${NC} Failed with exit code $exit_code"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -310,7 +355,7 @@ opt_network_optimization() {
|
||||
local steps=0
|
||||
|
||||
# 1. Flush DNS cache
|
||||
if sudo dscacheutil -flushcache 2> /dev/null && sudo killall -HUP mDNSResponder 2> /dev/null; then
|
||||
if flush_dns_cache; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} DNS cache flushed"
|
||||
((steps++))
|
||||
fi
|
||||
|
||||
@@ -29,7 +29,7 @@ setup() {
|
||||
mkdir -p "$HOME/.cache/mole"
|
||||
|
||||
# Clean any previous test artifacts
|
||||
rm -rf "$HOME/www"/* "$HOME/dev"/*
|
||||
rm -rf "${HOME:?}/www"/* "${HOME:?}/dev"/*
|
||||
}
|
||||
|
||||
# =================================================================
|
||||
@@ -151,7 +151,8 @@ setup() {
|
||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||
is_recently_modified '$HOME/www/old-project/node_modules' || true
|
||||
"
|
||||
[ "$?" -eq 0 ] || [ "$?" -eq 1 ] # Allow both true/false, just check no crash
|
||||
local exit_code=$?
|
||||
[ "$exit_code" -eq 0 ] || [ "$exit_code" -eq 1 ] # Allow both true/false, just check no crash
|
||||
}
|
||||
|
||||
# =================================================================
|
||||
|
||||
@@ -97,7 +97,7 @@ clean_time_machine_failed_backups
|
||||
EOF
|
||||
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"No failed Time Machine backups found"* ]]
|
||||
[[ "$output" == *"No incomplete backups found"* ]]
|
||||
}
|
||||
|
||||
|
||||
|
||||
13
tests/tmp-clean-home.EmChvN/.config/mole/clean-list.txt
Normal file
13
tests/tmp-clean-home.EmChvN/.config/mole/clean-list.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
# Mole Cleanup Preview - 2025-12-18 17:01:44
|
||||
#
|
||||
# How to protect files:
|
||||
# 1. Copy any path below to ~/.config/mole/whitelist
|
||||
# 2. Run: mo clean --whitelist
|
||||
#
|
||||
# Example:
|
||||
# /Users/*/Library/Caches/com.example.app
|
||||
#
|
||||
|
||||
|
||||
=== User essentials ===
|
||||
/Users/tw93/www/Mole/tests/tmp-clean-home.EmChvN/Library/Caches/TestApp # 4KB
|
||||
Reference in New Issue
Block a user