diff --git a/bin/clean.sh b/bin/clean.sh index 3f50147..89a9abe 100755 --- a/bin/clean.sh +++ b/bin/clean.sh @@ -18,6 +18,7 @@ source "$SCRIPT_DIR/../lib/clean_dev.sh" source "$SCRIPT_DIR/../lib/clean_user_apps.sh" source "$SCRIPT_DIR/../lib/clean_system.sh" source "$SCRIPT_DIR/../lib/clean_user_data.sh" +source "$SCRIPT_DIR/../lib/clean_maintenance.sh" # Configuration SYSTEM_CLEAN=false @@ -832,6 +833,12 @@ perform_cleanup() { clean_time_machine_failed_backups end_section + # ===== 16. System maintenance ===== + start_section "System maintenance" + # Broken preferences and login items cleanup (delegated to clean_maintenance module) + clean_maintenance + end_section + # ===== Final summary ===== echo "" diff --git a/lib/clean_maintenance.sh b/lib/clean_maintenance.sh new file mode 100644 index 0000000..dc8785a --- /dev/null +++ b/lib/clean_maintenance.sh @@ -0,0 +1,302 @@ +#!/bin/bash +# Maintenance Cleanup Module +# Universal binary slimming, broken preferences, broken login items + +set -euo pipefail + +# ============================================================================ +# Universal Binary Slimming +# Remove unused architecture code from universal binaries +# ============================================================================ + +# Slim universal binaries to current architecture only +# Only processes apps in /Applications, skips signed/notarized apps +# Env: DRY_RUN +# Globals: files_cleaned, total_size_cleaned, total_items (modified) +clean_universal_binaries() { + # Only run on Apple Silicon (most benefit) + if [[ "$(uname -m)" != "arm64" ]]; then + return 0 + fi + + # Check if lipo is available + if ! command -v lipo > /dev/null 2>&1; then + return 0 + fi + + local current_arch="arm64" + local remove_arch="x86_64" + local total_saved_kb=0 + local apps_slimmed=0 + local max_apps=50 # Limit to prevent long runs + + if [[ -t 1 ]]; then + MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning universal binaries..." + fi + + local app_count=0 + while IFS= read -r app_path; do + [[ -d "$app_path" ]] || continue + + ((app_count++)) + if [[ $app_count -gt $max_apps ]]; then + break + fi + + local binary_path="$app_path/Contents/MacOS" + [[ -d "$binary_path" ]] || continue + + # Get the main executable + local info_plist="$app_path/Contents/Info.plist" + [[ -f "$info_plist" ]] || continue + + local exec_name + exec_name=$(defaults read "$info_plist" CFBundleExecutable 2> /dev/null || echo "") + [[ -z "$exec_name" ]] && continue + + local exec_path="$binary_path/$exec_name" + [[ -f "$exec_path" ]] || continue + + # Check if it's a universal binary with both architectures + local archs + archs=$(lipo -archs "$exec_path" 2> /dev/null || echo "") + if [[ "$archs" != *"$current_arch"* ]] || [[ "$archs" != *"$remove_arch"* ]]; then + continue + fi + + # Skip if app is code signed (removing arch breaks signature) + if codesign -v "$app_path" 2> /dev/null; then + continue + fi + + # Calculate size before + local size_before + size_before=$(du -sk "$exec_path" 2> /dev/null | awk '{print $1}' || echo "0") + + if [[ "$DRY_RUN" != "true" ]]; then + # Create backup and slim + local backup_path="${exec_path}.universal.bak" + if cp "$exec_path" "$backup_path" 2> /dev/null; then + if lipo "$backup_path" -remove "$remove_arch" -output "$exec_path" 2> /dev/null; then + rm -f "$backup_path" + local size_after + size_after=$(du -sk "$exec_path" 2> /dev/null | awk '{print $1}' || echo "0") + local saved=$((size_before - size_after)) + if [[ $saved -gt 0 ]]; then + ((total_saved_kb += saved)) + ((apps_slimmed++)) + fi + else + # Restore backup on failure + mv "$backup_path" "$exec_path" 2> /dev/null || true + fi + fi + else + # Dry run: estimate savings (roughly 40-50% of binary size) + local estimated_save=$((size_before / 2)) + ((total_saved_kb += estimated_save)) + ((apps_slimmed++)) + fi + done < <(find /Applications -maxdepth 2 -type d -name "*.app" 2> /dev/null || true) + + if [[ -t 1 ]]; then + stop_inline_spinner + fi + + if [[ $apps_slimmed -gt 0 && $total_saved_kb -gt 1024 ]]; then + local saved_human + saved_human=$(bytes_to_human "$((total_saved_kb * 1024))") + if [[ "$DRY_RUN" == "true" ]]; then + echo -e " ${YELLOW}→${NC} Universal binaries: $apps_slimmed apps ${YELLOW}(~$saved_human dry)${NC}" + else + echo -e " ${GREEN}${ICON_SUCCESS}${NC} Slimmed $apps_slimmed apps ${GREEN}($saved_human)${NC}" + fi + # Update global statistics + ((files_cleaned += apps_slimmed)) + ((total_size_cleaned += total_saved_kb)) + ((total_items++)) + note_activity + fi +} + +# ============================================================================ +# Broken Preferences Detection and Cleanup +# Find and remove corrupted .plist files +# ============================================================================ + +# Clean broken preference files +# Uses plutil -lint to validate plist files +# Env: DRY_RUN +# Globals: files_cleaned, total_size_cleaned, total_items (modified) +clean_broken_preferences() { + local prefs_dir="$HOME/Library/Preferences" + [[ -d "$prefs_dir" ]] || return 0 + + local broken_count=0 + local total_size_kb=0 + + if [[ -t 1 ]]; then + MOLE_SPINNER_PREFIX=" " start_inline_spinner "Checking preference files..." + fi + + # Check main preferences directory + while IFS= read -r plist_file; do + [[ -f "$plist_file" ]] || continue + + # Skip system preferences + local filename + filename=$(basename "$plist_file") + case "$filename" in + com.apple.* | .GlobalPreferences* | loginwindow.plist) + continue + ;; + esac + + # Validate plist using plutil + if ! plutil -lint "$plist_file" > /dev/null 2>&1; then + local size_kb + size_kb=$(du -sk "$plist_file" 2> /dev/null | awk '{print $1}' || echo "0") + + if [[ "$DRY_RUN" != "true" ]]; then + rm -f "$plist_file" 2> /dev/null || true + fi + + ((broken_count++)) + ((total_size_kb += size_kb)) + fi + done < <(find "$prefs_dir" -maxdepth 1 -name "*.plist" -type f 2> /dev/null || true) + + # Check ByHost preferences + local byhost_dir="$prefs_dir/ByHost" + if [[ -d "$byhost_dir" ]]; then + while IFS= read -r plist_file; do + [[ -f "$plist_file" ]] || continue + + local filename + filename=$(basename "$plist_file") + case "$filename" in + com.apple.* | .GlobalPreferences*) + continue + ;; + esac + + if ! plutil -lint "$plist_file" > /dev/null 2>&1; then + local size_kb + size_kb=$(du -sk "$plist_file" 2> /dev/null | awk '{print $1}' || echo "0") + + if [[ "$DRY_RUN" != "true" ]]; then + rm -f "$plist_file" 2> /dev/null || true + fi + + ((broken_count++)) + ((total_size_kb += size_kb)) + fi + done < <(find "$byhost_dir" -name "*.plist" -type f 2> /dev/null || true) + fi + + if [[ -t 1 ]]; then + stop_inline_spinner + fi + + if [[ $broken_count -gt 0 ]]; then + if [[ "$DRY_RUN" == "true" ]]; then + echo -e " ${YELLOW}→${NC} Broken preferences: $broken_count files ${YELLOW}(dry)${NC}" + else + echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed $broken_count broken preference files" + fi + # Update global statistics + ((files_cleaned += broken_count)) + ((total_size_cleaned += total_size_kb)) + ((total_items++)) + note_activity + fi +} + +# ============================================================================ +# Broken Login Items Cleanup +# Find and remove login items pointing to non-existent files +# ============================================================================ + +# Clean broken login items (LaunchAgents pointing to missing executables) +# Env: DRY_RUN +# Globals: files_cleaned, total_items (modified) +clean_broken_login_items() { + local launch_agents_dir="$HOME/Library/LaunchAgents" + [[ -d "$launch_agents_dir" ]] || return 0 + + local broken_count=0 + local total_size_kb=0 + + if [[ -t 1 ]]; then + MOLE_SPINNER_PREFIX=" " start_inline_spinner "Checking login items..." + fi + + while IFS= read -r plist_file; do + [[ -f "$plist_file" ]] || continue + + # Skip system items + local filename + filename=$(basename "$plist_file") + case "$filename" in + com.apple.*) + continue + ;; + esac + + # Extract Program or ProgramArguments[0] from plist + local program="" + program=$(defaults read "$plist_file" Program 2> /dev/null || echo "") + + if [[ -z "$program" ]]; then + # Try ProgramArguments array + program=$(defaults read "$plist_file" ProgramArguments 2> /dev/null | head -2 | tail -1 | sed 's/^[[:space:]]*"//' | sed 's/".*$//' || echo "") + fi + + # Skip if no program found or program exists + [[ -z "$program" ]] && continue + [[ -e "$program" ]] && continue + + # Program doesn't exist - this is a broken login item + local size_kb + size_kb=$(du -sk "$plist_file" 2> /dev/null | awk '{print $1}' || echo "0") + + if [[ "$DRY_RUN" != "true" ]]; then + # Unload first if loaded + launchctl unload "$plist_file" 2> /dev/null || true + rm -f "$plist_file" 2> /dev/null || true + fi + + ((broken_count++)) + ((total_size_kb += size_kb)) + done < <(find "$launch_agents_dir" -name "*.plist" -type f 2> /dev/null || true) + + if [[ -t 1 ]]; then + stop_inline_spinner + fi + + if [[ $broken_count -gt 0 ]]; then + if [[ "$DRY_RUN" == "true" ]]; then + echo -e " ${YELLOW}→${NC} Broken login items: $broken_count ${YELLOW}(dry)${NC}" + else + echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed $broken_count broken login items" + fi + # Update global statistics + ((files_cleaned += broken_count)) + ((total_size_cleaned += total_size_kb)) + ((total_items++)) + note_activity + fi +} + +# ============================================================================ +# Main maintenance cleanup function +# ============================================================================ + +clean_maintenance() { + clean_broken_preferences + clean_broken_login_items + # Universal binary slimming is risky, only run if explicitly enabled + if [[ "${MOLE_SLIM_BINARIES:-false}" == "true" ]]; then + clean_universal_binaries + fi +} diff --git a/lib/clean_user_apps.sh b/lib/clean_user_apps.sh old mode 100644 new mode 100755 index da2d934..1bef79b --- a/lib/clean_user_apps.sh +++ b/lib/clean_user_apps.sh @@ -92,8 +92,24 @@ clean_productivity_apps() { } # Clean music and media players +# Note: Spotify cache is protected by default (may contain offline music) +# Users can override via whitelist settings clean_media_players() { - safe_clean ~/Library/Caches/com.spotify.client/* "Spotify cache" + # Spotify cache protection: skip if has offline music (>500MB cache) + local spotify_cache="$HOME/Library/Caches/com.spotify.client" + if [[ -d "$spotify_cache" ]]; then + local cache_size_kb + cache_size_kb=$(du -sk "$spotify_cache" 2> /dev/null | awk '{print $1}' || echo "0") + # Only clean if cache is small (<500MB, unlikely to have offline music) + if [[ $cache_size_kb -lt 512000 ]]; then + safe_clean ~/Library/Caches/com.spotify.client/* "Spotify cache" + else + local cache_human + cache_human=$(bytes_to_human "$((cache_size_kb * 1024))") + echo -e " ${YELLOW}${ICON_WARNING}${NC} Spotify cache protected ($cache_human, may contain offline music)" + note_activity + fi + fi safe_clean ~/Library/Caches/com.apple.Music "Apple Music cache" safe_clean ~/Library/Caches/com.apple.podcasts "Apple Podcasts cache" safe_clean ~/Library/Caches/com.apple.TV/* "Apple TV cache" diff --git a/lib/clean_user_data.sh b/lib/clean_user_data.sh old mode 100644 new mode 100755 index a018e06..a2ea839 --- a/lib/clean_user_data.sh +++ b/lib/clean_user_data.sh @@ -99,7 +99,36 @@ clean_sandboxed_app_caches() { } # Clean browser caches (Safari, Chrome, Edge, Firefox, etc.) +# Warns if browsers are running (some cache files may be locked) clean_browsers() { + # Check for running browsers and warn user + local running_browsers="" + local -a browser_checks=( + "Safari" + "Google Chrome" + "Firefox" + "Microsoft Edge" + "Brave Browser" + "Arc" + "Opera" + "Vivaldi" + ) + + for browser in "${browser_checks[@]}"; do + if pgrep -x "$browser" > /dev/null 2>&1; then + if [[ -z "$running_browsers" ]]; then + running_browsers="$browser" + else + running_browsers="$running_browsers, $browser" + fi + fi + done + + if [[ -n "$running_browsers" ]]; then + echo -e " ${YELLOW}${ICON_WARNING}${NC} Running: $running_browsers (some files may be locked)" + note_activity + fi + safe_clean ~/Library/Caches/com.apple.Safari/* "Safari cache" # Chrome/Chromium