2025-09-25 20:22:51 +08:00
#!/bin/bash
2025-09-25 22:10:30 +08:00
# Mole - Deeper system cleanup
2025-09-25 20:22:51 +08:00
# Complete cleanup with smart password handling
set -euo pipefail
2025-10-08 11:32:35 +08:00
# Fix locale issues (avoid Perl warnings on non-English systems)
export LC_ALL = C
export LANG = C
2025-09-25 20:22:51 +08:00
# Get script directory and source common functions
SCRIPT_DIR = " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) " && pwd ) "
2025-12-01 16:58:35 +08:00
source " $SCRIPT_DIR /../lib/core/common.sh "
source " $SCRIPT_DIR /../lib/core/sudo.sh "
source " $SCRIPT_DIR /../lib/clean/brew.sh "
source " $SCRIPT_DIR /../lib/clean/caches.sh "
source " $SCRIPT_DIR /../lib/clean/apps.sh "
source " $SCRIPT_DIR /../lib/clean/dev.sh "
source " $SCRIPT_DIR /../lib/clean/app_caches.sh "
source " $SCRIPT_DIR /../lib/clean/system.sh "
source " $SCRIPT_DIR /../lib/clean/user.sh "
source " $SCRIPT_DIR /../lib/clean/maintenance.sh "
2025-09-25 20:22:51 +08:00
# Configuration
SYSTEM_CLEAN = false
2025-10-03 10:51:59 +08:00
DRY_RUN = false
2025-11-17 14:37:12 +08:00
PROTECT_FINDER_METADATA = false
2025-11-15 13:40:43 +08:00
IS_M_SERIES = $( [ [ " $( uname -m) " = = "arm64" ] ] && echo "true" || echo "false" )
2025-10-08 11:32:35 +08:00
2025-11-09 09:30:35 +08:00
# Protected Service Worker domains (web-based editing tools)
readonly PROTECTED_SW_DOMAINS = (
"capcut.com"
"photopea.com"
"pixlr.com"
)
2025-12-01 14:45:18 +08:00
# Whitelist patterns (loaded from common.sh)
2025-12-01 16:58:35 +08:00
# FINDER_METADATA_SENTINEL and DEFAULT_WHITELIST_PATTERNS defined in lib/core/common.sh
2025-10-11 14:12:00 +08:00
declare -a WHITELIST_PATTERNS = ( )
2025-10-08 11:32:35 +08:00
WHITELIST_WARNINGS = ( )
2025-10-04 22:15:57 +08:00
# Load user-defined whitelist
2025-10-03 11:02:05 +08:00
if [ [ -f " $HOME /.config/mole/whitelist " ] ] ; then
while IFS = read -r line; do
2025-10-08 11:32:35 +08:00
# Trim whitespace
2025-10-03 13:56:34 +08:00
line = " ${ line # ${ line %%[![ : space : ]]* } } "
line = " ${ line % ${ line ##*[![ : space : ]] } } "
2025-10-08 11:32:35 +08:00
# Skip empty lines and comments
2025-10-03 11:02:05 +08:00
[ [ -z " $line " || " $line " = ~ ^# ] ] && continue
2025-10-08 11:32:35 +08:00
# Expand tilde to home directory
2025-10-03 13:56:34 +08:00
[ [ " $line " = = ~* ] ] && line = " ${ line /#~/ $HOME } "
2025-10-08 11:32:35 +08:00
2025-11-29 22:43:57 +09:00
# Security: reject path traversal attempts
if [ [ " $line " = ~ \. \. ] ] ; then
WHITELIST_WARNINGS += ( " Path traversal not allowed: $line " )
continue
fi
2025-12-01 14:45:18 +08:00
# Skip validation for special sentinel values
if [ [ " $line " != " $FINDER_METADATA_SENTINEL " ] ] ; then
# Path validation with support for spaces and wildcards
# Allow: letters, numbers, /, _, ., -, @, spaces, and * anywhere in path
if [ [ ! " $line " = ~ ^[ a-zA-Z0-9/_.@\ *-] +$ ] ] ; then
WHITELIST_WARNINGS += ( " Invalid path format: $line " )
continue
fi
2025-10-08 11:32:35 +08:00
2025-12-01 14:45:18 +08:00
# Require absolute paths (must start with /)
if [ [ " $line " != /* ] ] ; then
WHITELIST_WARNINGS += ( " Must be absolute path: $line " )
continue
fi
2025-11-29 22:43:57 +09:00
fi
2025-11-15 13:19:50 +08:00
# Reject paths with consecutive slashes (e.g., //)
if [ [ " $line " = ~ // ] ] ; then
WHITELIST_WARNINGS += ( " Consecutive slashes: $line " )
continue
fi
# Prevent critical system directories
2025-10-08 11:32:35 +08:00
case " $line " in
2025-11-29 22:43:57 +09:00
/ | /System | /System/* | /bin | /bin/* | /sbin | /sbin/* | /usr/bin | /usr/bin/* | /usr/sbin | /usr/sbin/* | /etc | /etc/* | /var/db | /var/db/*)
2025-11-15 13:19:50 +08:00
WHITELIST_WARNINGS += ( " Protected system path: $line " )
2025-10-08 11:32:35 +08:00
continue
; ;
esac
2025-10-11 14:12:00 +08:00
duplicate = "false"
if [ [ ${# WHITELIST_PATTERNS [@] } -gt 0 ] ] ; then
for existing in " ${ WHITELIST_PATTERNS [@] } " ; do
if [ [ " $line " = = " $existing " ] ] ; then
duplicate = "true"
break
fi
done
fi
[ [ " $duplicate " = = "true" ] ] && continue
2025-10-03 11:02:05 +08:00
WHITELIST_PATTERNS += ( " $line " )
done < " $HOME /.config/mole/whitelist "
2025-10-11 14:12:00 +08:00
else
WHITELIST_PATTERNS = ( " ${ DEFAULT_WHITELIST_PATTERNS [@] } " )
2025-10-03 11:02:05 +08:00
fi
2025-11-17 14:37:12 +08:00
if [ [ ${# WHITELIST_PATTERNS [@] } -gt 0 ] ] ; then
for entry in " ${ WHITELIST_PATTERNS [@] } " ; do
if [ [ " $entry " = = " $FINDER_METADATA_SENTINEL " ] ] ; then
PROTECT_FINDER_METADATA = true
break
fi
done
fi
2025-09-25 20:22:51 +08:00
total_items = 0
# Tracking variables
TRACK_SECTION = 0
SECTION_ACTIVITY = 0
files_cleaned = 0
total_size_cleaned = 0
2025-10-10 23:05:21 +08:00
whitelist_skipped_count = 0
2025-09-25 20:22:51 +08:00
note_activity( ) {
if [ [ $TRACK_SECTION -eq 1 ] ] ; then
SECTION_ACTIVITY = 1
fi
}
# Cleanup background processes
2025-10-08 23:23:07 +08:00
CLEANUP_DONE = false
2025-09-25 20:22:51 +08:00
cleanup( ) {
2025-10-08 23:23:07 +08:00
local signal = " ${ 1 :- EXIT } "
local exit_code = " ${ 2 :- $? } "
# Prevent multiple executions
if [ [ " $CLEANUP_DONE " = = "true" ] ] ; then
return 0
2025-09-25 20:22:51 +08:00
fi
2025-10-08 23:23:07 +08:00
CLEANUP_DONE = true
# Stop all spinners and clear the line
if [ [ -n " $INLINE_SPINNER_PID " ] ] ; then
2025-10-12 20:49:10 +08:00
kill " $INLINE_SPINNER_PID " 2> /dev/null || true
wait " $INLINE_SPINNER_PID " 2> /dev/null || true
2025-10-08 23:23:07 +08:00
INLINE_SPINNER_PID = ""
fi
# Clear any spinner output
if [ [ -t 1 ] ] ; then
printf "\r\033[K"
fi
2025-12-01 16:27:32 +08:00
# Stop sudo session
stop_sudo_session
2025-10-08 23:23:07 +08:00
show_cursor
# If interrupted, show message
if [ [ " $signal " = = "INT" ] ] || [ [ $exit_code -eq 130 ] ] ; then
printf "\r\033[K"
echo -e " ${ YELLOW } Interrupted by user ${ NC } "
fi
2025-09-25 20:22:51 +08:00
}
2025-10-08 23:23:07 +08:00
trap 'cleanup EXIT $?' EXIT
trap 'cleanup INT 130; exit 130' INT
trap 'cleanup TERM 143; exit 143' TERM
2025-09-25 20:22:51 +08:00
start_section( ) {
TRACK_SECTION = 1
SECTION_ACTIVITY = 0
2025-09-30 00:43:52 +08:00
echo ""
2025-10-12 12:42:21 +08:00
echo -e " ${ PURPLE } ${ ICON_ARROW } $1 ${ NC } "
2025-09-25 20:22:51 +08:00
}
end_section( ) {
if [ [ $TRACK_SECTION -eq 1 && $SECTION_ACTIVITY -eq 0 ] ] ; then
2025-11-22 14:00:27 +08:00
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Nothing to clean "
2025-09-25 20:22:51 +08:00
fi
TRACK_SECTION = 0
}
safe_clean( ) {
if [ [ $# -eq 0 ] ] ; then
return 0
fi
local description
local -a targets
if [ [ $# -eq 1 ] ] ; then
description = " $1 "
targets = ( " $1 " )
else
2025-10-12 15:43:45 +08:00
# Get last argument as description
description = " ${ * : -1 } "
# Get all arguments except last as targets array
2025-09-25 20:22:51 +08:00
targets = ( " ${ @ : 1 : $# -1 } " )
fi
local removed_any = 0
2025-09-30 00:43:52 +08:00
local total_size_bytes = 0
local total_count = 0
2025-10-10 23:05:21 +08:00
local skipped_count = 0
2025-09-25 20:22:51 +08:00
2025-10-04 22:15:57 +08:00
# Optimized parallel processing for better performance
2025-09-30 13:48:28 +08:00
local -a existing_paths = ( )
2025-09-25 20:22:51 +08:00
for path in " ${ targets [@] } " ; do
2025-10-03 11:02:05 +08:00
local skip = false
2025-11-16 13:38:16 +08:00
# Hard-coded protection for critical apps (cannot be disabled by user)
case " $path " in
2025-11-24 15:24:06 +08:00
*clash* | *Clash* | *surge* | *Surge* | *mihomo* | *openvpn* | *OpenVPN*)
2025-11-16 13:38:16 +08:00
skip = true
( ( skipped_count++) )
; ;
esac
[ [ " $skip " = = "true" ] ] && continue
# Check user-defined whitelist
2025-10-10 23:05:21 +08:00
if [ [ ${# WHITELIST_PATTERNS [@] } -gt 0 ] ] ; then
for w in " ${ WHITELIST_PATTERNS [@] } " ; do
# Match both exact path and glob pattern
2025-10-13 11:29:45 +08:00
# shellcheck disable=SC2053
if [ [ " $path " = = " $w " ] ] || [ [ $path = = $w ] ] ; then
2025-10-10 23:05:21 +08:00
skip = true
( ( skipped_count++) )
break
fi
done
fi
2025-10-03 11:02:05 +08:00
[ [ " $skip " = = "true" ] ] && continue
2025-09-30 13:48:28 +08:00
[ [ -e " $path " ] ] && existing_paths += ( " $path " )
done
2025-10-12 20:49:10 +08:00
2025-10-10 23:05:21 +08:00
# Update global whitelist skip counter
if [ [ $skipped_count -gt 0 ] ] ; then
( ( whitelist_skipped_count += skipped_count) )
fi
2025-09-25 20:22:51 +08:00
2025-09-30 13:48:28 +08:00
if [ [ ${# existing_paths [@] } -eq 0 ] ] ; then
return 0
fi
2025-09-25 20:22:51 +08:00
2025-10-05 16:41:34 +08:00
# Show progress indicator for potentially slow operations
2025-09-30 13:48:28 +08:00
if [ [ ${# existing_paths [@] } -gt 3 ] ] ; then
2025-10-21 20:06:26 +08:00
local total_paths = ${# existing_paths [@] }
if [ [ -t 1 ] ] ; then MOLE_SPINNER_PREFIX = " " start_inline_spinner " Scanning $total_paths items... " ; fi
2025-10-12 20:49:10 +08:00
local temp_dir
temp_dir = $( create_temp_dir)
2025-09-30 13:48:28 +08:00
2025-10-04 22:15:57 +08:00
# Parallel processing (bash 3.2 compatible)
2025-09-30 13:48:28 +08:00
local -a pids = ( )
2025-10-08 11:32:35 +08:00
local idx = 0
2025-10-21 20:06:26 +08:00
local completed = 0
2025-09-30 13:48:28 +08:00
for path in " ${ existing_paths [@] } " ; do
(
2025-10-12 20:49:10 +08:00
local size
size = $( du -sk " $path " 2> /dev/null | awk '{print $1}' || echo "0" )
local count
count = $( find " $path " -type f 2> /dev/null | wc -l | tr -d ' ' )
2025-10-08 11:32:35 +08:00
# Use index + PID for unique filename
local tmp_file = " $temp_dir /result_ ${ idx } . $$ "
echo " $size $count " > " $tmp_file "
2025-10-12 20:49:10 +08:00
mv " $tmp_file " " $temp_dir /result_ ${ idx } " 2> /dev/null || true
2025-09-30 13:48:28 +08:00
) &
pids += ( $! )
2025-10-08 11:32:35 +08:00
( ( idx++) )
2025-09-30 13:48:28 +08:00
2025-11-29 22:43:57 +09:00
if ( ( ${# pids [@] } >= MOLE_MAX_PARALLEL_JOBS) ) ; then
2025-10-12 20:49:10 +08:00
wait " ${ pids [0] } " 2> /dev/null || true
2025-09-30 13:48:28 +08:00
pids = ( " ${ pids [@] : 1 } " )
2025-10-21 20:06:26 +08:00
( ( completed++) )
# Update progress every 10 items for smoother display
if [ [ -t 1 ] ] && ( ( completed % 10 = = 0) ) ; then
stop_inline_spinner
MOLE_SPINNER_PREFIX = " " start_inline_spinner " Scanning items ( $completed / $total_paths )... "
fi
2025-09-25 20:22:51 +08:00
fi
2025-09-30 13:48:28 +08:00
done
2025-09-25 20:22:51 +08:00
2025-09-30 13:48:28 +08:00
for pid in " ${ pids [@] } " ; do
2025-10-12 20:49:10 +08:00
wait " $pid " 2> /dev/null || true
2025-10-21 20:06:26 +08:00
( ( completed++) )
2025-09-30 13:48:28 +08:00
done
2025-10-05 16:41:34 +08:00
2025-10-08 11:32:35 +08:00
# Read results using same index
idx = 0
2025-09-30 13:48:28 +08:00
for path in " ${ existing_paths [@] } " ; do
2025-10-08 11:32:35 +08:00
local result_file = " $temp_dir /result_ ${ idx } "
if [ [ -f " $result_file " ] ] ; then
2025-10-12 20:49:10 +08:00
read -r size count < " $result_file " 2> /dev/null || true
2025-09-30 13:48:28 +08:00
if [ [ " $count " -gt 0 && " $size " -gt 0 ] ] ; then
2025-10-03 10:51:59 +08:00
if [ [ " $DRY_RUN " != "true" ] ] ; then
2025-11-18 23:07:48 +08:00
# Handle symbolic links separately (only remove the link, not the target)
if [ [ -L " $path " ] ] ; then
rm " $path " 2> /dev/null || true
else
2025-11-29 22:43:57 +09:00
safe_remove " $path " true || true
2025-11-18 23:07:48 +08:00
fi
2025-10-03 10:51:59 +08:00
fi
2025-09-30 13:48:28 +08:00
( ( total_size_bytes += size) )
( ( total_count += count) )
removed_any = 1
fi
fi
2025-10-08 11:32:35 +08:00
( ( idx++) )
2025-09-30 13:48:28 +08:00
done
2025-10-08 18:01:46 +08:00
# Temp dir will be auto-cleaned by cleanup_temp_files
2025-09-30 13:48:28 +08:00
else
2025-10-05 16:41:34 +08:00
# Show progress for small batches too (simpler jobs)
2025-10-21 20:06:26 +08:00
local total_paths = ${# existing_paths [@] }
if [ [ -t 1 ] ] ; then MOLE_SPINNER_PREFIX = " " start_inline_spinner " Scanning $total_paths items... " ; fi
2025-10-05 16:41:34 +08:00
2025-09-30 13:48:28 +08:00
for path in " ${ existing_paths [@] } " ; do
2025-10-12 20:49:10 +08:00
local size_bytes
size_bytes = $( du -sk " $path " 2> /dev/null | awk '{print $1}' || echo "0" )
local count
count = $( find " $path " -type f 2> /dev/null | wc -l | tr -d ' ' )
2025-09-30 13:48:28 +08:00
if [ [ " $count " -gt 0 && " $size_bytes " -gt 0 ] ] ; then
2025-10-03 10:51:59 +08:00
if [ [ " $DRY_RUN " != "true" ] ] ; then
2025-11-18 23:07:48 +08:00
# Handle symbolic links separately (only remove the link, not the target)
if [ [ -L " $path " ] ] ; then
rm " $path " 2> /dev/null || true
else
2025-11-29 22:43:57 +09:00
safe_remove " $path " true || true
2025-11-18 23:07:48 +08:00
fi
2025-10-03 10:51:59 +08:00
fi
2025-09-30 13:48:28 +08:00
( ( total_size_bytes += size_bytes) )
( ( total_count += count) )
removed_any = 1
fi
done
fi
2025-09-30 00:43:52 +08:00
2025-10-08 18:01:46 +08:00
# Clear progress / stop spinner before showing result
2025-10-12 20:49:10 +08:00
if [ [ -t 1 ] ] ; then
stop_inline_spinner
echo -ne "\r\033[K"
fi
2025-10-05 16:41:34 +08:00
2025-09-30 00:43:52 +08:00
if [ [ $removed_any -eq 1 ] ] ; then
2025-10-08 18:01:46 +08:00
# Convert KB to bytes for bytes_to_human()
local size_human = $( bytes_to_human " $(( total_size_bytes * 1024 )) " )
2025-09-25 20:22:51 +08:00
local label = " $description "
if [ [ ${# targets [@] } -gt 1 ] ] ; then
2025-10-06 10:45:51 +08:00
label += " ${# targets [@] } items "
2025-09-25 20:22:51 +08:00
fi
2025-10-03 10:51:59 +08:00
if [ [ " $DRY_RUN " = = "true" ] ] ; then
2025-10-06 10:45:51 +08:00
echo -e " ${ YELLOW } → ${ NC } $label ${ YELLOW } ( $size_human dry) ${ NC } "
2025-10-03 10:51:59 +08:00
else
2025-10-12 12:42:21 +08:00
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } $label ${ GREEN } ( $size_human ) ${ NC } "
2025-10-03 10:51:59 +08:00
fi
2025-10-12 20:49:10 +08:00
( ( files_cleaned += total_count) )
( ( total_size_cleaned += total_size_bytes) )
2025-09-25 20:22:51 +08:00
( ( total_items++) )
note_activity
2025-09-30 00:43:52 +08:00
fi
2025-09-25 20:22:51 +08:00
return 0
}
start_cleanup( ) {
2025-10-04 17:59:12 +08:00
clear
2025-10-06 10:45:51 +08:00
printf '\n'
2025-10-08 23:23:07 +08:00
echo -e " ${ PURPLE } Clean Your Mac ${ NC } "
2025-11-21 10:44:36 +08:00
echo ""
2025-10-12 20:49:10 +08:00
2025-10-03 13:56:34 +08:00
if [ [ " $DRY_RUN " != "true" && -t 0 ] ] ; then
2025-11-19 11:33:15 +08:00
echo -e " ${ YELLOW } ☻ ${ NC } First time? Run ${ GRAY } mo clean --dry-run ${ NC } first to preview changes "
2025-10-03 13:56:34 +08:00
fi
2025-10-03 10:51:59 +08:00
if [ [ " $DRY_RUN " = = "true" ] ] ; then
2025-10-10 23:05:21 +08:00
echo -e " ${ YELLOW } Dry Run Mode ${ NC } - Preview only, no deletions "
2025-10-03 10:51:59 +08:00
echo ""
SYSTEM_CLEAN = false
return
fi
2025-09-25 20:22:51 +08:00
if [ [ -t 0 ] ] ; then
2025-11-15 12:28:34 +08:00
echo -ne " ${ PURPLE } ${ ICON_ARROW } ${ NC } System caches need sudo — ${ GREEN } Enter ${ NC } continue, ${ GRAY } Space ${ NC } skip: "
2025-10-04 22:15:57 +08:00
2025-11-15 12:28:34 +08:00
# Use read_key to properly handle all key inputs
local choice
choice = $( read_key)
2025-10-08 23:23:07 +08:00
2025-11-15 12:28:34 +08:00
# Check for cancel (ESC or Q)
if [ [ " $choice " = = "QUIT" ] ] ; then
2025-10-11 22:43:18 +08:00
echo -e " ${ GRAY } Cancelled ${ NC } "
exit 0
fi
2025-11-15 12:28:34 +08:00
# Space = skip
if [ [ " $choice " = = "SPACE" ] ] ; then
echo -e " ${ GRAY } Skipped ${ NC } "
echo ""
SYSTEM_CLEAN = false
2025-10-11 22:43:18 +08:00
# Enter = yes, do system cleanup
2025-11-15 12:28:34 +08:00
elif [ [ " $choice " = = "ENTER" ] ] ; then
2025-10-12 20:49:10 +08:00
printf "\r\033[K" # Clear the prompt line
2025-12-01 16:27:32 +08:00
if ensure_sudo_session "System cleanup requires admin access" ; then
2025-10-08 23:23:07 +08:00
SYSTEM_CLEAN = true
2025-10-12 12:42:21 +08:00
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Admin access granted "
2025-10-11 22:43:18 +08:00
echo ""
2025-10-08 23:23:07 +08:00
else
SYSTEM_CLEAN = false
2025-10-04 17:59:12 +08:00
echo ""
2025-10-10 23:05:21 +08:00
echo -e " ${ YELLOW } Authentication failed ${ NC } , continuing with user-level cleanup "
2025-10-04 17:59:12 +08:00
fi
2025-10-08 23:23:07 +08:00
else
2025-11-15 12:28:34 +08:00
# Other keys (including arrow keys) = skip, no message needed
2025-10-08 23:23:07 +08:00
SYSTEM_CLEAN = false
2025-10-04 17:59:12 +08:00
fi
2025-09-25 20:22:51 +08:00
else
2025-10-04 17:59:12 +08:00
SYSTEM_CLEAN = false
2025-09-30 13:48:28 +08:00
echo ""
2025-10-10 23:05:21 +08:00
echo "Running in non-interactive mode"
echo " • System-level cleanup skipped (requires interaction)"
echo " • User-level cleanup will proceed automatically"
2025-09-30 13:48:28 +08:00
echo ""
2025-09-25 20:22:51 +08:00
fi
}
2025-11-09 09:30:35 +08:00
# Clean Service Worker CacheStorage with domain protection
2025-09-25 20:22:51 +08:00
perform_cleanup( ) {
2025-10-12 12:42:21 +08:00
echo -e " ${ BLUE } ${ ICON_ADMIN } ${ NC } $( detect_architecture) | Free space: $( get_free_space) "
2025-10-12 20:49:10 +08:00
2025-11-28 22:39:11 +09:00
# Pre-check TCC permissions upfront (delegated to clean_caches module)
check_tcc_permissions
2025-11-23 20:19:42 +08:00
2025-10-10 23:05:21 +08:00
# Show whitelist info if patterns are active
2025-12-01 14:45:18 +08:00
if [ [ ${# WHITELIST_PATTERNS [@] } -gt 0 ] ] ; then
# Count predefined vs custom patterns
local predefined_count = 0
local custom_count = 0
for pattern in " ${ WHITELIST_PATTERNS [@] } " ; do
local is_predefined = false
for default in " ${ DEFAULT_WHITELIST_PATTERNS [@] } " ; do
if [ [ " $pattern " = = " $default " ] ] ; then
is_predefined = true
break
fi
done
if [ [ " $is_predefined " = = "true" ] ] ; then
( ( predefined_count++) )
else
( ( custom_count++) )
fi
done
# Display whitelist status
if [ [ $custom_count -gt 0 && $predefined_count -gt 0 ] ] ; then
echo -e " ${ BLUE } ${ ICON_SUCCESS } ${ NC } Whitelist: $predefined_count core + $custom_count custom patterns active "
elif [ [ $custom_count -gt 0 ] ] ; then
echo -e " ${ BLUE } ${ ICON_SUCCESS } ${ NC } Whitelist: $custom_count custom patterns active "
elif [ [ $predefined_count -gt 0 ] ] ; then
echo -e " ${ BLUE } ${ ICON_SUCCESS } ${ NC } Whitelist: $predefined_count core patterns active "
fi
2025-10-10 23:05:21 +08:00
fi
2025-09-25 20:22:51 +08:00
# Initialize counters
total_items = 0
files_cleaned = 0
total_size_cleaned = 0
2025-10-10 23:05:21 +08:00
# ===== 1. Deep system cleanup (if admin) - Do this first while sudo is fresh =====
2025-09-25 20:22:51 +08:00
if [ [ " $SYSTEM_CLEAN " = = "true" ] ] ; then
2025-11-16 00:39:48 +08:00
start_section "Deep system"
2025-11-28 22:39:11 +09:00
# Deep system cleanup (delegated to clean_system module)
clean_deep_system
2025-09-25 20:22:51 +08:00
end_section
fi
2025-10-08 11:32:35 +08:00
# Show whitelist warnings if any
if [ [ ${# WHITELIST_WARNINGS [@] } -gt 0 ] ] ; then
echo ""
for warning in " ${ WHITELIST_WARNINGS [@] } " ; do
2025-10-12 12:42:21 +08:00
echo -e " ${ YELLOW } ${ ICON_WARNING } ${ NC } Whitelist: $warning "
2025-10-08 11:32:35 +08:00
done
fi
2025-09-30 00:43:52 +08:00
2025-09-25 20:22:51 +08:00
# ===== 2. User essentials =====
2025-11-16 00:39:48 +08:00
start_section "User essentials"
2025-11-28 22:39:11 +09:00
# User essentials cleanup (delegated to clean_user_data module)
clean_user_essentials
2025-09-25 20:22:51 +08:00
end_section
2025-11-16 00:39:48 +08:00
start_section "Finder metadata"
2025-11-28 22:39:11 +09:00
# Finder metadata cleanup (delegated to clean_user_data module)
clean_finder_metadata
2025-11-12 17:09:04 +08:00
end_section
2025-11-16 00:39:48 +08:00
# ===== 3. macOS system caches =====
2025-10-04 08:50:01 +08:00
start_section "macOS system caches"
2025-11-28 22:39:11 +09:00
# macOS system caches cleanup (delegated to clean_user_data module)
clean_macos_system_caches
2025-10-04 08:50:01 +08:00
end_section
2025-11-16 00:39:48 +08:00
# ===== 4. Sandboxed app caches =====
2025-10-04 08:50:01 +08:00
start_section "Sandboxed app caches"
2025-11-28 22:39:11 +09:00
# Sandboxed app caches cleanup (delegated to clean_user_data module)
clean_sandboxed_app_caches
2025-10-04 08:50:01 +08:00
end_section
# ===== 5. Browsers =====
2025-11-16 00:39:48 +08:00
start_section "Browsers"
2025-11-28 22:39:11 +09:00
# Browser caches cleanup (delegated to clean_user_data module)
clean_browsers
2025-09-25 20:22:51 +08:00
end_section
2025-11-16 00:39:48 +08:00
# ===== 6. Cloud storage =====
start_section "Cloud storage"
2025-11-28 22:39:11 +09:00
# Cloud storage caches cleanup (delegated to clean_user_data module)
clean_cloud_storage
2025-10-04 08:50:01 +08:00
end_section
2025-11-16 00:39:48 +08:00
# ===== 7. Office applications =====
2025-10-04 08:50:01 +08:00
start_section "Office applications"
2025-11-28 22:39:11 +09:00
# Office applications cleanup (delegated to clean_user_data module)
clean_office_applications
2025-10-04 08:50:01 +08:00
end_section
# ===== 8. Developer tools =====
2025-09-25 20:22:51 +08:00
start_section "Developer tools"
2025-11-28 22:39:11 +09:00
# Developer tools cleanup (delegated to clean_dev module)
clean_developer_tools
2025-09-25 20:22:51 +08:00
end_section
2025-11-16 00:39:48 +08:00
# ===== 9. Development applications =====
start_section "Development applications"
2025-11-28 22:39:11 +09:00
# User GUI applications cleanup (delegated to clean_user_apps module)
clean_user_gui_applications
2025-09-25 20:22:51 +08:00
end_section
2025-11-16 00:39:48 +08:00
# ===== 10. Virtualization tools =====
2025-11-22 14:00:27 +08:00
start_section "Virtual machine tools"
2025-11-28 22:39:11 +09:00
# Virtualization tools cleanup (delegated to clean_user_data module)
clean_virtualization_tools
2025-10-04 08:50:01 +08:00
end_section
2025-11-29 22:43:57 +09:00
# ===== 11. Application Support logs and caches cleanup =====
start_section "Application Support"
# Clean logs, Service Worker caches, Code Cache, Crashpad, stale updates, Group Containers
2025-11-28 22:39:11 +09:00
clean_application_support_logs
2025-10-06 10:45:51 +08:00
end_section
2025-11-16 00:39:48 +08:00
# ===== 12. Orphaned app data cleanup =====
2025-11-15 13:19:50 +08:00
# Only touch apps missing from scan + 60+ days inactive
2025-10-13 11:29:45 +08:00
# Skip protected vendors, keep Preferences/Application Support
2025-11-24 14:19:59 +08:00
start_section "Uninstalled app data"
2025-09-25 20:22:51 +08:00
2025-11-23 20:19:42 +08:00
# Check if we have permission to access Library folders
# Use simple ls test instead of find to avoid hanging
local has_library_access = true
if ! ls " $HOME /Library/Caches " > /dev/null 2>& 1; then
has_library_access = false
fi
if [ [ " $has_library_access " = = "false" ] ] ; then
note_activity
echo -e " ${ YELLOW } ${ ICON_WARNING } ${ NC } Skipped: No permission to access Library folders "
echo -e " ${ GRAY } Tip: Grant 'Full Disk Access' to iTerm2/Terminal in System Settings ${ NC } "
else
2025-11-25 17:25:13 +08:00
local -r ORPHAN_AGE_THRESHOLD = 60 # 60 days - good balance between safety and cleanup
2025-10-06 10:45:51 +08:00
2025-11-25 17:25:13 +08:00
# Build list of installed application bundle identifiers
MOLE_SPINNER_PREFIX = " " start_inline_spinner "Scanning installed applications..."
local installed_bundles = $( create_temp_file)
2025-10-08 11:32:35 +08:00
2025-11-25 17:25:13 +08:00
# Simplified: only scan primary locations (reduces scan time by ~70%)
local -a search_paths = (
"/Applications"
" $HOME /Applications "
)
2025-10-06 10:45:51 +08:00
2025-11-25 17:25:13 +08:00
# Scan for .app bundles with timeout protection
for search_path in " ${ search_paths [@] } " ; do
[ [ -d " $search_path " ] ] || continue
while IFS = read -r app; do
[ [ -f " $app /Contents/Info.plist " ] ] || continue
bundle_id = $( defaults read " $app /Contents/Info.plist " CFBundleIdentifier 2> /dev/null || echo "" )
[ [ -n " $bundle_id " ] ] && echo " $bundle_id " >> " $installed_bundles "
done < <( run_with_timeout 10 find " $search_path " -maxdepth 2 -type d -name "*.app" 2> /dev/null || true )
done
2025-10-06 10:45:51 +08:00
2025-11-25 17:25:13 +08:00
# Get running applications and LaunchAgents with timeout protection
local running_apps = $( run_with_timeout 5 osascript -e 'tell application "System Events" to get bundle identifier of every application process' 2> /dev/null || echo "" )
echo " $running_apps " | tr ',' '\n' | sed -e 's/^ *//;s/ *$//' -e '/^$/d' >> " $installed_bundles "
2025-10-06 10:45:51 +08:00
2025-11-25 17:25:13 +08:00
run_with_timeout 5 find ~/Library/LaunchAgents /Library/LaunchAgents \
-name "*.plist" -type f 2> /dev/null | while IFS = read -r plist; do
basename " $plist " .plist
done >> " $installed_bundles " 2> /dev/null || true
2025-10-06 10:45:51 +08:00
2025-11-25 17:25:13 +08:00
# Deduplicate
sort -u " $installed_bundles " -o " $installed_bundles "
2025-10-06 10:45:51 +08:00
2025-11-25 17:25:13 +08:00
local app_count = $( wc -l < " $installed_bundles " 2> /dev/null | tr -d ' ' )
stop_inline_spinner
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Found $app_count active/installed apps "
2025-10-06 10:45:51 +08:00
2025-11-25 17:25:13 +08:00
# Track statistics
local orphaned_count = 0
local total_orphaned_kb = 0
2025-10-06 10:45:51 +08:00
2025-11-25 17:25:13 +08:00
# Check if bundle is orphaned - conservative approach
is_orphaned( ) {
local bundle_id = " $1 "
local directory_path = " $2 "
2025-09-25 20:22:51 +08:00
2025-11-25 17:25:13 +08:00
# Skip system-critical and protected apps
if should_protect_data " $bundle_id " ; then
2025-10-06 10:45:51 +08:00
return 1
2025-11-25 17:25:13 +08:00
fi
2025-10-06 10:45:51 +08:00
2025-11-25 17:25:13 +08:00
# Check if app exists in our scan
if grep -q " ^ $bundle_id $" " $installed_bundles " 2> /dev/null; then
2025-10-06 10:45:51 +08:00
return 1
2025-11-25 17:25:13 +08:00
fi
2025-10-06 10:45:51 +08:00
2025-11-25 17:25:13 +08:00
# Extra check for system bundles
case " $bundle_id " in
com.apple.* | loginwindow | dock | systempreferences | finder | safari)
return 1
; ;
esac
2025-10-06 10:45:51 +08:00
2025-11-25 17:25:13 +08:00
# Skip major vendors
case " $bundle_id " in
com.adobe.* | com.microsoft.* | com.google.* | org.mozilla.* | com.jetbrains.* | com.docker.*)
return 1
; ;
esac
# Check file age - only clean if 60+ days inactive
# Use modification time (mtime) instead of access time (atime)
# because macOS disables atime updates by default for performance
if [ [ -e " $directory_path " ] ] ; then
2025-11-27 07:27:48 +09:00
local last_modified_epoch = $( get_file_mtime " $directory_path " )
2025-11-25 17:25:13 +08:00
local current_epoch = $( date +%s)
local days_since_modified = $(( ( current_epoch - last_modified_epoch) / 86400 ))
if [ [ $days_since_modified -lt $ORPHAN_AGE_THRESHOLD ] ] ; then
return 1
fi
2025-10-06 10:45:51 +08:00
fi
2025-11-25 17:25:13 +08:00
return 0
}
2025-10-06 10:45:51 +08:00
2025-11-25 17:25:13 +08:00
# Unified orphaned resource scanner (caches, logs, states, webkit, HTTP, cookies)
MOLE_SPINNER_PREFIX = " " start_inline_spinner "Scanning orphaned app resources..."
2025-11-15 13:19:50 +08:00
2025-11-25 17:25:13 +08:00
# Define resource types to scan
2025-11-27 07:27:48 +09:00
# CRITICAL: NEVER add LaunchAgents or LaunchDaemons (breaks login items/startup apps)
2025-11-25 17:25:13 +08:00
local -a resource_types = (
" $HOME /Library/Caches|Caches|com.*:org.*:net.*:io.* "
" $HOME /Library/Logs|Logs|com.*:org.*:net.*:io.* "
" $HOME /Library/Saved Application State|States|*.savedState "
" $HOME /Library/WebKit|WebKit|com.*:org.*:net.*:io.* "
" $HOME /Library/HTTPStorages|HTTP|com.*:org.*:net.*:io.* "
" $HOME /Library/Cookies|Cookies|*.binarycookies "
)
2025-11-15 13:19:50 +08:00
2025-11-25 17:25:13 +08:00
orphaned_count = 0
2025-11-23 20:19:42 +08:00
2025-11-25 17:25:13 +08:00
for resource_type in " ${ resource_types [@] } " ; do
IFS = '|' read -r base_path label patterns <<< " $resource_type "
2025-11-15 13:19:50 +08:00
2025-11-25 17:25:13 +08:00
# Check both existence and permission to avoid hanging
if [ [ ! -d " $base_path " ] ] ; then
continue
fi
2025-11-15 13:19:50 +08:00
2025-11-25 17:25:13 +08:00
# Quick permission check - if we can't ls the directory, skip it
if ! ls " $base_path " > /dev/null 2>& 1; then
continue
fi
2025-11-23 20:19:42 +08:00
2025-11-25 17:25:13 +08:00
# Build file pattern array
local -a file_patterns = ( )
IFS = ':' read -ra pattern_arr <<< " $patterns "
for pat in " ${ pattern_arr [@] } " ; do
file_patterns += ( " $base_path / $pat " )
done
2025-11-15 13:19:50 +08:00
2025-11-25 17:25:13 +08:00
# Scan and clean orphaned items
for item_path in " ${ file_patterns [@] } " ; do
# Use shell glob (no ls needed)
# Limit iterations to prevent hanging on directories with too many files
local iteration_count = 0
local max_iterations = 100
for match in $item_path ; do
[ [ -e " $match " ] ] || continue
2025-11-23 20:19:42 +08:00
2025-11-25 17:25:13 +08:00
# Safety: limit iterations to prevent infinite loops on massive directories
( ( iteration_count++) )
if [ [ $iteration_count -gt $max_iterations ] ] ; then
break
2025-11-15 13:19:50 +08:00
fi
2025-11-25 17:25:13 +08:00
# Extract bundle ID from filename
local bundle_id = $( basename " $match " )
bundle_id = " ${ bundle_id %.savedState } "
bundle_id = " ${ bundle_id %.binarycookies } "
if is_orphaned " $bundle_id " " $match " ; then
# Use timeout to prevent du from hanging on large/problematic directories
local size_kb
size_kb = $( run_with_timeout 2 du -sk " $match " 2> /dev/null | awk '{print $1}' || echo "0" )
if [ [ -z " $size_kb " || " $size_kb " = = "0" ] ] ; then
continue
fi
safe_clean " $match " " Orphaned $label : $bundle_id "
( ( orphaned_count++) )
( ( total_orphaned_kb += size_kb) )
fi
done
2025-11-15 13:19:50 +08:00
done
2025-09-25 20:22:51 +08:00
done
2025-10-06 10:45:51 +08:00
2025-11-25 17:25:13 +08:00
stop_inline_spinner
2025-10-06 10:45:51 +08:00
2025-11-25 17:25:13 +08:00
if [ [ $orphaned_count -gt 0 ] ] ; then
local orphaned_mb = $( echo " $total_orphaned_kb " | awk '{printf "%.1f", $1/1024}' )
echo " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Cleaned $orphaned_count items (~ ${ orphaned_mb } MB) "
note_activity
fi
2025-09-25 20:22:51 +08:00
2025-11-25 17:25:13 +08:00
rm -f " $installed_bundles "
2025-09-25 20:22:51 +08:00
2025-11-23 20:19:42 +08:00
fi # end of has_library_access check
2025-09-30 00:43:52 +08:00
end_section
2025-11-16 00:39:48 +08:00
# ===== 13. Apple Silicon optimizations =====
2025-09-25 20:22:51 +08:00
if [ [ " $IS_M_SERIES " = = "true" ] ] ; then
2025-11-22 14:00:27 +08:00
start_section "Apple Silicon updates"
2025-09-25 20:22:51 +08:00
safe_clean /Library/Apple/usr/share/rosetta/rosetta_update_bundle "Rosetta 2 cache"
safe_clean ~/Library/Caches/com.apple.rosetta.update "Rosetta 2 user cache"
safe_clean ~/Library/Caches/com.apple.amp.mediasevicesd "Apple Silicon media service cache"
2025-10-13 11:29:45 +08:00
# Skip: iCloud sync cache, may affect device pairing
# safe_clean ~/Library/Caches/com.apple.bird.lsuseractivity "User activity cache"
2025-09-25 20:22:51 +08:00
end_section
fi
2025-11-16 00:39:48 +08:00
# ===== 14. iOS device backups =====
2025-09-28 17:01:25 +08:00
start_section "iOS device backups"
2025-11-28 22:39:11 +09:00
# iOS device backups check (delegated to clean_user_data module)
check_ios_device_backups
2025-09-28 17:01:25 +08:00
end_section
2025-09-25 20:22:51 +08:00
2025-11-16 00:39:48 +08:00
# ===== 15. Time Machine failed backups =====
2025-10-12 12:42:21 +08:00
start_section "Time Machine failed backups"
2025-11-28 22:39:11 +09:00
# Time Machine failed backups cleanup (delegated to clean_system module)
clean_time_machine_failed_backups
2025-10-12 12:42:21 +08:00
end_section
2025-11-30 10:30:22 +08:00
# ===== 16. System maintenance =====
start_section "System maintenance"
# Broken preferences and login items cleanup (delegated to clean_maintenance module)
clean_maintenance
end_section
2025-09-30 00:43:52 +08:00
# ===== Final summary =====
echo ""
2025-10-11 15:02:15 +08:00
local summary_heading = ""
local summary_status = "success"
2025-10-03 10:51:59 +08:00
if [ [ " $DRY_RUN " = = "true" ] ] ; then
2025-10-11 22:43:18 +08:00
summary_heading = "Dry run complete - no changes made"
2025-10-03 10:51:59 +08:00
else
2025-10-11 15:02:15 +08:00
summary_heading = "Cleanup complete"
2025-10-03 10:51:59 +08:00
fi
2025-09-30 00:43:52 +08:00
2025-10-11 15:02:15 +08:00
local -a summary_details = ( )
2025-09-30 00:43:52 +08:00
if [ [ $total_size_cleaned -gt 0 ] ] ; then
2025-10-11 15:02:15 +08:00
local freed_gb
freed_gb = $( echo " $total_size_cleaned " | awk '{printf "%.2f", $1/1024/1024}' )
2025-10-09 14:24:00 +08:00
2025-10-11 15:02:15 +08:00
if [ [ " $DRY_RUN " = = "true" ] ] ; then
2025-10-11 22:43:18 +08:00
# Build compact stats line for dry run
local stats = " Potential space: ${ GREEN } ${ freed_gb } GB ${ NC } "
[ [ $files_cleaned -gt 0 ] ] && stats += " | Files: $files_cleaned "
[ [ $total_items -gt 0 ] ] && stats += " | Categories: $total_items "
2025-10-11 15:02:15 +08:00
[ [ $whitelist_skipped_count -gt 0 ] ] && stats += " | Protected: $whitelist_skipped_count "
summary_details += ( " $stats " )
2025-10-11 22:43:18 +08:00
summary_details += ( " Use ${ GRAY } mo clean --whitelist ${ NC } to protect caches " )
else
summary_details += ( " Space freed: ${ GREEN } ${ freed_gb } GB ${ NC } " )
summary_details += ( " Free space now: $( get_free_space) " )
if [ [ $files_cleaned -gt 0 && $total_items -gt 0 ] ] ; then
local stats = " Files cleaned: $files_cleaned | Categories: $total_items "
[ [ $whitelist_skipped_count -gt 0 ] ] && stats += " | Protected: $whitelist_skipped_count "
summary_details += ( " $stats " )
elif [ [ $files_cleaned -gt 0 ] ] ; then
local stats = " Files cleaned: $files_cleaned "
[ [ $whitelist_skipped_count -gt 0 ] ] && stats += " | Protected: $whitelist_skipped_count "
summary_details += ( " $stats " )
elif [ [ $total_items -gt 0 ] ] ; then
local stats = " Categories: $total_items "
[ [ $whitelist_skipped_count -gt 0 ] ] && stats += " | Protected: $whitelist_skipped_count "
summary_details += ( " $stats " )
2025-10-11 15:02:15 +08:00
fi
2025-10-09 14:24:00 +08:00
2025-10-03 10:51:59 +08:00
if [ [ $( echo " $freed_gb " | awk '{print ($1 >= 1) ? 1 : 0}' ) -eq 1 ] ] ; then
2025-10-11 15:02:15 +08:00
local movies
movies = $( echo " $freed_gb " | awk '{printf "%.0f", $1/4.5}' )
2025-10-03 10:51:59 +08:00
if [ [ $movies -gt 0 ] ] ; then
2025-10-11 15:02:15 +08:00
summary_details += ( " Equivalent to ~ $movies 4K movies of storage. " )
2025-10-03 10:51:59 +08:00
fi
2025-09-30 00:43:52 +08:00
fi
fi
2025-09-25 20:22:51 +08:00
else
2025-10-11 15:02:15 +08:00
summary_status = "info"
2025-10-03 10:51:59 +08:00
if [ [ " $DRY_RUN " = = "true" ] ] ; then
2025-10-11 15:02:15 +08:00
summary_details += ( "No significant reclaimable space detected (system already clean)." )
2025-10-03 10:51:59 +08:00
else
2025-10-11 15:02:15 +08:00
summary_details += ( "System was already clean; no additional space freed." )
2025-10-03 10:51:59 +08:00
fi
2025-10-11 15:02:15 +08:00
summary_details += ( " Free space now: $( get_free_space) " )
2025-09-25 20:22:51 +08:00
fi
2025-10-11 15:02:15 +08:00
print_summary_block " $summary_status " " $summary_heading " " ${ summary_details [@] } "
2025-10-11 22:43:18 +08:00
printf '\n'
2025-09-30 00:43:52 +08:00
}
2025-09-25 20:22:51 +08:00
main( ) {
2025-10-14 19:43:59 +08:00
# Parse args (only dry-run and whitelist)
2025-10-03 10:51:59 +08:00
for arg in " $@ " ; do
case " $arg " in
2025-10-12 20:49:10 +08:00
"--dry-run" | "-n" )
2025-10-03 10:51:59 +08:00
DRY_RUN = true
; ;
2025-10-03 11:02:05 +08:00
"--whitelist" )
2025-12-01 16:58:35 +08:00
source " $SCRIPT_DIR /../lib/manage/whitelist.sh "
2025-10-04 22:15:57 +08:00
manage_whitelist
exit 0
2025-10-03 11:02:05 +08:00
; ;
2025-10-03 10:51:59 +08:00
esac
done
start_cleanup
2025-10-08 18:01:46 +08:00
hide_cursor
2025-10-03 10:51:59 +08:00
perform_cleanup
show_cursor
2025-09-25 20:22:51 +08:00
}
2025-09-25 22:10:30 +08:00
main " $@ "