2025-11-28 22:39:11 +09:00
#!/bin/bash
# Application Data Cleanup Module
set -euo pipefail
# Args: $1=target_dir, $2=label
clean_ds_store_tree( ) {
local target = " $1 "
local label = " $2 "
[ [ -d " $target " ] ] || return 0
local file_count = 0
local total_bytes = 0
local spinner_active = "false"
if [ [ -t 1 ] ] ; then
MOLE_SPINNER_PREFIX = " "
start_inline_spinner "Cleaning Finder metadata..."
spinner_active = "true"
fi
local -a exclude_paths = (
-path "*/Library/Application Support/MobileSync" -prune -o
-path "*/Library/Developer" -prune -o
-path "*/.Trash" -prune -o
-path "*/node_modules" -prune -o
-path "*/.git" -prune -o
-path "*/Library/Caches" -prune -o
)
2025-12-04 15:06:45 +08:00
local -a find_cmd = ( "command" "find" " $target " )
2025-11-28 22:39:11 +09:00
if [ [ " $target " = = " $HOME " ] ] ; then
find_cmd += ( "-maxdepth" "5" )
fi
find_cmd += ( " ${ exclude_paths [@] } " "-type" "f" "-name" ".DS_Store" "-print0" )
while IFS = read -r -d '' ds_file; do
local size
size = $( get_file_size " $ds_file " )
total_bytes = $(( total_bytes + size))
( ( file_count++) )
if [ [ " $DRY_RUN " != "true" ] ] ; then
rm -f " $ds_file " 2> /dev/null || true
fi
2025-12-27 10:16:42 +08:00
if [ [ $file_count -ge $MOLE_MAX_DS_STORE_FILES ] ] ; then
2025-11-28 22:39:11 +09:00
break
fi
2025-12-05 20:09:15 +08:00
done < <( " ${ find_cmd [@] } " 2> /dev/null || true )
2025-11-28 22:39:11 +09:00
if [ [ " $spinner_active " = = "true" ] ] ; then
2025-12-27 10:16:42 +08:00
stop_section_spinner
2025-11-28 22:39:11 +09:00
fi
if [ [ $file_count -gt 0 ] ] ; then
local size_human
size_human = $( bytes_to_human " $total_bytes " )
if [ [ " $DRY_RUN " = = "true" ] ] ; then
2025-12-27 10:16:42 +08:00
echo -e " ${ YELLOW } ${ ICON_DRY_RUN } ${ NC } $label ${ YELLOW } ( $file_count files, $size_human dry) ${ NC } "
2025-11-28 22:39:11 +09:00
else
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } $label ${ GREEN } ( $file_count files, $size_human ) ${ NC } "
fi
local size_kb = $(( ( total_bytes + 1023 ) / 1024 ))
( ( files_cleaned += file_count) )
( ( total_size_cleaned += size_kb) )
( ( total_items++) )
note_activity
fi
}
2025-12-31 16:23:31 +08:00
# Orphaned app data (60+ days inactive). Env: ORPHAN_AGE_THRESHOLD, DRY_RUN
2025-12-12 14:10:36 +08:00
# Usage: scan_installed_apps "output_file"
scan_installed_apps( ) {
local installed_bundles = " $1 "
2025-12-31 16:23:31 +08:00
# Cache installed app scan briefly to speed repeated runs.
2025-12-27 10:16:42 +08:00
local cache_file = " $HOME /.cache/mole/installed_apps_cache "
2025-12-27 02:19:22 +00:00
local cache_age_seconds = 300 # 5 minutes
2025-12-27 10:16:42 +08:00
if [ [ -f " $cache_file " ] ] ; then
local cache_mtime = $( get_file_mtime " $cache_file " )
2026-01-03 18:07:47 +08:00
local current_time
current_time = $( get_epoch_seconds)
2025-12-27 10:16:42 +08:00
local age = $(( current_time - cache_mtime))
if [ [ $age -lt $cache_age_seconds ] ] ; then
debug_log " Using cached app list (age: ${ age } s) "
if [ [ -r " $cache_file " ] ] && [ [ -s " $cache_file " ] ] ; then
2025-12-27 02:19:22 +00:00
if cat " $cache_file " > " $installed_bundles " 2> /dev/null; then
2025-12-27 10:16:42 +08:00
return 0
else
debug_log "Warning: Failed to read cache, rebuilding"
fi
else
debug_log "Warning: Cache file empty or unreadable, rebuilding"
fi
fi
fi
debug_log "Scanning installed applications (cache expired or missing)"
2025-11-28 22:39:11 +09:00
local -a app_dirs = (
"/Applications"
"/System/Applications"
" $HOME /Applications "
)
2025-12-31 16:23:31 +08:00
# Temp dir avoids write contention across parallel scans.
2025-12-08 15:47:04 +08:00
local scan_tmp_dir = $( create_temp_dir)
local pids = ( )
local dir_idx = 0
2025-11-28 22:39:11 +09:00
for app_dir in " ${ app_dirs [@] } " ; do
[ [ -d " $app_dir " ] ] || continue
2025-12-08 15:47:04 +08:00
(
2025-12-10 14:40:14 +08:00
local -a app_paths = ( )
while IFS = read -r app_path; do
[ [ -n " $app_path " ] ] && app_paths += ( " $app_path " )
done < <( find " $app_dir " -name '*.app' -maxdepth 3 -type d 2> /dev/null)
local count = 0
2025-12-11 16:18:49 +08:00
for app_path in " ${ app_paths [@] :- } " ; do
2025-12-10 14:40:14 +08:00
local plist_path = " $app_path /Contents/Info.plist "
[ [ ! -f " $plist_path " ] ] && continue
local bundle_id = $( /usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" " $plist_path " 2> /dev/null || echo "" )
2025-12-09 18:40:21 +08:00
if [ [ -n " $bundle_id " ] ] ; then
echo " $bundle_id "
2025-12-10 14:40:14 +08:00
( ( count++) )
2025-12-09 18:40:21 +08:00
fi
2025-12-10 14:40:14 +08:00
done
) > " $scan_tmp_dir /apps_ ${ dir_idx } .txt " &
2025-12-08 15:47:04 +08:00
pids += ( $! )
( ( dir_idx++) )
2025-11-28 22:39:11 +09:00
done
2025-12-31 16:23:31 +08:00
# Collect running apps and LaunchAgents to avoid false orphan cleanup.
2025-12-08 15:47:04 +08:00
(
local running_apps = $( run_with_timeout 5 osascript -e 'tell application "System Events" to get bundle identifier of every application process' 2> /dev/null || echo "" )
echo " $running_apps " | tr ',' '\n' | sed -e 's/^ *//;s/ *$//' -e '/^$/d' > " $scan_tmp_dir /running.txt "
) &
pids += ( $! )
(
run_with_timeout 5 find ~/Library/LaunchAgents /Library/LaunchAgents \
2025-12-12 06:12:13 +00:00
-name "*.plist" -type f 2> /dev/null |
xargs -I { } basename { } .plist > " $scan_tmp_dir /agents.txt " 2> /dev/null || true
2025-12-08 15:47:04 +08:00
) &
pids += ( $! )
2025-12-27 10:16:42 +08:00
debug_log " Waiting for ${# pids [@] } background processes: ${ pids [*] } "
2025-12-08 15:47:04 +08:00
for pid in " ${ pids [@] } " ; do
wait " $pid " 2> /dev/null || true
done
2025-12-27 10:16:42 +08:00
debug_log "All background processes completed"
2025-12-08 15:47:04 +08:00
cat " $scan_tmp_dir " /*.txt >> " $installed_bundles " 2> /dev/null || true
2025-12-08 17:50:46 +08:00
safe_remove " $scan_tmp_dir " true
2025-11-28 22:39:11 +09:00
sort -u " $installed_bundles " -o " $installed_bundles "
2025-12-27 10:16:42 +08:00
ensure_user_dir " $( dirname " $cache_file " ) "
2025-12-27 02:19:22 +00:00
cp " $installed_bundles " " $cache_file " 2> /dev/null || true
local app_count = $( wc -l < " $installed_bundles " 2> /dev/null | tr -d ' ' )
2025-12-27 10:16:42 +08:00
debug_log " Scanned $app_count unique applications "
2025-12-12 14:10:36 +08:00
}
# Usage: is_bundle_orphaned "bundle_id" "directory_path" "installed_bundles_file"
is_bundle_orphaned( ) {
local bundle_id = " $1 "
local directory_path = " $2 "
local installed_bundles = " $3 "
if should_protect_data " $bundle_id " ; then
return 1
fi
if grep -Fxq " $bundle_id " " $installed_bundles " 2> /dev/null; then
return 1
fi
if should_protect_data " $bundle_id " ; then
return 1
fi
case " $bundle_id " in
2025-12-18 09:34:38 +08:00
loginwindow | dock | systempreferences | systemsettings | settings | controlcenter | finder | safari)
2025-11-28 22:39:11 +09:00
return 1
2025-12-12 14:10:36 +08:00
; ;
esac
if [ [ -e " $directory_path " ] ] ; then
local last_modified_epoch = $( get_file_mtime " $directory_path " )
2026-01-03 18:07:47 +08:00
local current_epoch
current_epoch = $( get_epoch_seconds)
2025-12-12 14:10:36 +08:00
local days_since_modified = $(( ( current_epoch - last_modified_epoch) / 86400 ))
if [ [ $days_since_modified -lt ${ ORPHAN_AGE_THRESHOLD :- 60 } ] ] ; then
2025-11-28 22:39:11 +09:00
return 1
fi
2025-12-12 14:10:36 +08:00
fi
return 0
}
2025-12-31 16:23:31 +08:00
# Orphaned app data sweep.
2025-12-12 14:10:36 +08:00
clean_orphaned_app_data( ) {
if ! ls " $HOME /Library/Caches " > /dev/null 2>& 1; then
2025-12-27 10:16:42 +08:00
stop_section_spinner
2025-12-12 14:10:36 +08:00
echo -e " ${ YELLOW } ${ ICON_WARNING } ${ NC } Skipped: No permission to access Library folders "
2025-11-28 22:39:11 +09:00
return 0
2025-12-12 14:10:36 +08:00
fi
2025-12-27 10:16:42 +08:00
start_section_spinner "Scanning installed apps..."
2025-12-12 14:10:36 +08:00
local installed_bundles = $( create_temp_file)
scan_installed_apps " $installed_bundles "
2025-12-27 10:16:42 +08:00
stop_section_spinner
local app_count = $( wc -l < " $installed_bundles " 2> /dev/null | tr -d ' ' )
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Found $app_count active/installed apps "
2025-12-12 14:10:36 +08:00
local orphaned_count = 0
local total_orphaned_kb = 0
2025-12-27 10:16:42 +08:00
start_section_spinner "Scanning orphaned app resources..."
2025-12-31 16:23:31 +08:00
# CRITICAL: NEVER add LaunchAgents or LaunchDaemons (breaks login items/startup apps).
2025-11-28 22:39:11 +09: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 "
)
orphaned_count = 0
for resource_type in " ${ resource_types [@] } " ; do
IFS = '|' read -r base_path label patterns <<< " $resource_type "
if [ [ ! -d " $base_path " ] ] ; then
continue
fi
if ! ls " $base_path " > /dev/null 2>& 1; then
continue
fi
local -a file_patterns = ( )
IFS = ':' read -ra pattern_arr <<< " $patterns "
for pat in " ${ pattern_arr [@] } " ; do
file_patterns += ( " $base_path / $pat " )
done
for item_path in " ${ file_patterns [@] } " ; do
local iteration_count = 0
for match in $item_path ; do
[ [ -e " $match " ] ] || continue
( ( iteration_count++) )
2025-12-27 10:16:42 +08:00
if [ [ $iteration_count -gt $MOLE_MAX_ORPHAN_ITERATIONS ] ] ; then
2025-11-28 22:39:11 +09:00
break
fi
local bundle_id = $( basename " $match " )
bundle_id = " ${ bundle_id %.savedState } "
bundle_id = " ${ bundle_id %.binarycookies } "
2025-12-12 14:10:36 +08:00
if is_bundle_orphaned " $bundle_id " " $match " " $installed_bundles " ; then
2025-11-28 22:39:11 +09:00
local size_kb
2025-12-08 15:34:51 +08:00
size_kb = $( get_path_size_kb " $match " )
2025-11-28 22:39:11 +09:00
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
done
done
2025-12-27 10:16:42 +08:00
stop_section_spinner
2025-11-28 22:39:11 +09: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
rm -f " $installed_bundles "
}