1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 15:39:42 +00:00

feat: Enhance clean and optimize operations with new configuration constants

This commit is contained in:
Tw93
2025-12-18 17:02:04 +08:00
parent 456215f2ff
commit af03452f6d
17 changed files with 504 additions and 483 deletions

View File

@@ -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:

View File

@@ -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"

View File

@@ -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() {

View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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"

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
# =================================================================

View File

@@ -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"* ]]
}

View 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