diff --git a/.github/workflows/shell-quality-checks.yml b/.github/workflows/shell-quality-checks.yml index ecef367..7d1fb98 100644 --- a/.github/workflows/shell-quality-checks.yml +++ b/.github/workflows/shell-quality-checks.yml @@ -29,3 +29,6 @@ jobs: - name: Build Go disk analyzer run: mkdir -p bin && go build -o bin/analyze-go ./cmd/analyze + + - name: Build Go optimizer + run: mkdir -p bin && go build -o bin/optimize-go ./cmd/optimize diff --git a/README.md b/README.md index aeec550..8f6b010 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ - **Thorough Uninstall** - Scans 22+ locations to remove app leftovers, not just the .app file - **Interactive Disk Analyzer** - Navigate folders with arrow keys, find and delete large files quickly - **Fast & Lightweight** - Terminal-based with arrow-key navigation, pagination, and Touch ID support +- **System Optimization** - Rebuilds caches, resets services, and trims swap/network cruft with one run ## Quick Start @@ -46,6 +47,7 @@ mo clean # System cleanup mo clean --dry-run # Preview mode mo clean --whitelist # Manage protected caches mo uninstall # Uninstall apps +mo optimize # System optimization mo analyze # Disk analyzer mo touchid # Configure Touch ID for sudo @@ -104,6 +106,27 @@ Space freed: 95.50GB | Free space now: 223.5GB ==================================================================== ``` +### System Optimization + +```bash +$ mo optimize + +System: 5/32 GB RAM | 333/460 GB Disk (72%) | Uptime 6d + +▶ System Maintenance - Rebuild system databases & flush caches +▶ Network Services - Reset network services +▶ Finder & Dock Refresh - Clear Finder/Dock caches and restart +▶ Diagnostics Cleanup - Purge old diagnostic & crash logs +▶ Mail Downloads - Recover Mail attachment space +▶ Memory & Swap - Purge swapfiles, restart dynamic pager + +==================================================================== +System optimization completed +Automations: 8 sections optimized end-to-end. +Highlights: caches refreshed, services restarted, startup assets rebuilt. +==================================================================== +``` + ### Smart App Uninstaller ```bash diff --git a/bin/clean.sh b/bin/clean.sh index 4783288..4843157 100755 --- a/bin/clean.sh +++ b/bin/clean.sh @@ -378,7 +378,31 @@ clean_ds_store_tree() { 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 + + # Build exclusion paths for find (skip common slow/large directories) + 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 + ) + + # Limit depth for HOME to avoid slow scans + local max_depth="" + if [[ "$target" == "$HOME" ]]; then + max_depth="-maxdepth 5" + fi + + # Find .DS_Store files with exclusions and depth limit while IFS= read -r -d '' ds_file; do local size size=$(stat -f%z "$ds_file" 2> /dev/null || echo 0) @@ -387,7 +411,17 @@ clean_ds_store_tree() { if [[ "$DRY_RUN" != "true" ]]; then rm -f "$ds_file" 2> /dev/null || true fi - done < <(find "$target" -type f -name '.DS_Store' -print0 2> /dev/null) + + # Stop after 500 files to avoid hanging + if [[ $file_count -ge 500 ]]; then + break + fi + done < <(find "$target" $max_depth "${exclude_paths[@]}" -type f -name '.DS_Store' -print0 2> /dev/null) + + if [[ "$spinner_active" == "true" ]]; then + stop_inline_spinner + echo -ne "\r\033[K" + fi if [[ $file_count -gt 0 ]]; then local size_human diff --git a/bin/optimize-go b/bin/optimize-go new file mode 100755 index 0000000..7d10726 Binary files /dev/null and b/bin/optimize-go differ diff --git a/bin/optimize.sh b/bin/optimize.sh new file mode 100755 index 0000000..3e1d16d --- /dev/null +++ b/bin/optimize.sh @@ -0,0 +1,550 @@ +#!/bin/bash + +set -euo pipefail + +# Load common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "$SCRIPT_DIR/lib/common.sh" + +# Path to optimize-go binary +OPTIMIZE_GO="$SCRIPT_DIR/bin/optimize-go" + +# Colors and icons from common.sh + +print_header() { + echo "" + echo -e "${PURPLE}Optimize Your Mac${NC}" + echo "" +} + +show_system_health() { + local health_json="$1" + + # Parse system health using jq + local mem_used=$(echo "$health_json" | jq -r '.memory_used_gb') + local mem_total=$(echo "$health_json" | jq -r '.memory_total_gb') + local disk_used=$(echo "$health_json" | jq -r '.disk_used_gb') + local disk_total=$(echo "$health_json" | jq -r '.disk_total_gb') + local disk_percent=$(echo "$health_json" | jq -r '.disk_used_percent') + local uptime=$(echo "$health_json" | jq -r '.uptime_days') + + # Compact one-line format + printf "System: %.0f/%.0f GB RAM | %.0f/%.0f GB Disk (%.0f%%) | Uptime %.0fd\n" \ + "$mem_used" "$mem_total" "$disk_used" "$disk_total" "$disk_percent" "$uptime" + echo "" +} + +parse_optimizations() { + local health_json="$1" + + # Extract optimizations array + echo "$health_json" | jq -c '.optimizations[]' 2> /dev/null +} + +announce_action() { + local name="$1" + local desc="$2" + local kind="$3" + + local badge="" + if [[ "$kind" == "confirm" ]]; then + badge="${YELLOW}[Confirm]${NC} " + fi + + local line="${BLUE}${ICON_ARROW}${NC} ${badge}${name}" + if [[ -n "$desc" ]]; then + line+=" ${GRAY}- ${desc}${NC}" + fi + + if ${first_heading:-true}; then + first_heading=false + else + echo "" + fi + + echo -e "$line" +} + +touchid_configured() { + local pam_file="/etc/pam.d/sudo" + [[ -f "$pam_file" ]] && grep -q "pam_tid.so" "$pam_file" 2> /dev/null +} + +touchid_supported() { + if command -v bioutil > /dev/null 2>&1; then + bioutil -r 2> /dev/null | grep -q "Touch ID" && return 0 + fi + [[ "$(uname -m)" == "arm64" ]] +} + +cleanup_path() { + local raw_path="$1" + local label="$2" + + local expanded_path="${raw_path/#\~/$HOME}" + if [[ ! -e "$expanded_path" ]]; then + echo -e " ${GREEN}${ICON_SUCCESS}${NC} $label" + return + fi + + local size_kb + size_kb=$(du -sk "$expanded_path" 2> /dev/null | awk '{print $1}' || echo "0") + local size_display="" + if [[ "$size_kb" =~ ^[0-9]+$ && "$size_kb" -gt 0 ]]; then + size_display=$(bytes_to_human "$((size_kb * 1024))") + fi + + if rm -rf "$expanded_path"; then + if [[ -n "$size_display" ]]; then + echo -e " ${GREEN}${ICON_SUCCESS}${NC} $label ${GREEN}(${size_display})${NC}" + else + echo -e " ${GREEN}${ICON_SUCCESS}${NC} $label" + fi + else + echo -e " ${RED}${ICON_ERROR}${NC} Failed to remove $label" + fi +} + +ensure_directory() { + local raw_path="$1" + local expanded_path="${raw_path/#\~/$HOME}" + mkdir -p "$expanded_path" > /dev/null 2>&1 || true +} + +count_local_snapshots() { + if ! command -v tmutil > /dev/null 2>&1; then + echo 0 + return + fi + + local output + output=$(tmutil listlocalsnapshots / 2> /dev/null || true) + if [[ -z "$output" ]]; then + echo 0 + return + fi + + echo "$output" | grep -c "com.apple.TimeMachine." | tr -d ' ' +} + +execute_optimization() { + local action="$1" + local path="$2" + + case "$action" in + system_maintenance) + echo -e "${BLUE}${ICON_ARROW}${NC} Rebuilding LaunchServices database..." + timeout 10 /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user > /dev/null 2>&1 || true + echo -e "${GREEN}${ICON_SUCCESS}${NC} LaunchServices database rebuilt" + + echo -e "${BLUE}${ICON_ARROW}${NC} Flushing DNS cache..." + if sudo dscacheutil -flushcache 2> /dev/null && sudo killall -HUP mDNSResponder 2> /dev/null; then + echo -e "${GREEN}${ICON_SUCCESS}${NC} DNS cache flushed" + else + echo -e "${RED}${ICON_ERROR}${NC} Failed to flush DNS cache" + fi + + echo -e "${BLUE}${ICON_ARROW}${NC} Purging memory cache..." + if sudo purge 2> /dev/null; then + echo -e "${GREEN}${ICON_SUCCESS}${NC} Memory cache purged" + else + echo -e "${RED}${ICON_ERROR}${NC} Failed to purge memory" + fi + + echo -e "${BLUE}${ICON_ARROW}${NC} Rebuilding font cache..." + sudo atsutil databases -remove > /dev/null 2>&1 + echo -e "${GREEN}${ICON_SUCCESS}${NC} Font cache rebuilt" + + echo -e "${BLUE}${ICON_ARROW}${NC} Rebuilding Spotlight index..." + sudo mdutil -E / > /dev/null 2>&1 || true + echo -e "${GREEN}${ICON_SUCCESS}${NC} Spotlight index rebuilt" + ;; + + startup_items) + echo -e "${BLUE}${ICON_ARROW}${NC} Opening Launch Agents directory..." + open ~/Library/LaunchAgents + open /Library/LaunchAgents + echo -e "${GREEN}${ICON_SUCCESS}${NC} Please review and disable unnecessary startup items" + echo -e "${GRAY} Tip: Move unwanted .plist files to trash${NC}" + ;; + + network_services) + echo -e "${BLUE}${ICON_ARROW}${NC} Resetting network services..." + if sudo dscacheutil -flushcache 2> /dev/null && sudo killall -HUP mDNSResponder 2> /dev/null; then + echo -e "${GREEN}${ICON_SUCCESS}${NC} Network services reset" + else + echo -e "${RED}${ICON_ERROR}${NC} Failed to reset network services" + fi + ;; + + cache_refresh) + echo -e "${BLUE}${ICON_ARROW}${NC} Resetting Quick Look cache..." + qlmanage -r cache > /dev/null 2>&1 || true + qlmanage -r > /dev/null 2>&1 || true + + local -a cache_targets=( + "$HOME/Library/Caches/com.apple.QuickLook.thumbnailcache|Quick Look thumbnails" + "$HOME/Library/Caches/com.apple.iconservices.store|Icon Services store" + "$HOME/Library/Caches/com.apple.iconservices|Icon Services cache" + "$HOME/Library/Caches/com.apple.Safari/WebKitCache|Safari WebKit cache" + "$HOME/Library/Caches/com.apple.Safari/Favicon|Safari favicon cache" + ) + + for target in "${cache_targets[@]}"; do + IFS='|' read -r target_path label <<< "$target" + cleanup_path "$target_path" "$label" + done + + echo -e "${GREEN}${ICON_SUCCESS}${NC} Finder and Safari caches refreshed" + ;; + + maintenance_scripts) + echo -e "${BLUE}${ICON_ARROW}${NC} Running macOS periodic scripts..." + local periodic_cmd="/usr/sbin/periodic" + if [[ -x "$periodic_cmd" ]]; then + local periodic_output="" + if periodic_output=$(sudo "$periodic_cmd" daily weekly monthly 2>&1); then + echo -e "${GREEN}${ICON_SUCCESS}${NC} Daily/weekly/monthly scripts completed" + else + echo -e "${YELLOW}!${NC} periodic scripts reported an issue" + printf '%s\n' "$periodic_output" | sed 's/^/ /' + fi + fi + + echo -e "${BLUE}${ICON_ARROW}${NC} Rotating system logs..." + if sudo newsyslog > /dev/null 2>&1; then + echo -e "${GREEN}${ICON_SUCCESS}${NC} Log rotation complete" + else + echo -e "${YELLOW}!${NC} newsyslog reported an issue" + fi + + if [[ -x "/usr/libexec/repair_packages" ]]; then + echo -e "${BLUE}${ICON_ARROW}${NC} Repairing base system permissions..." + if sudo /usr/libexec/repair_packages --repair --standard-pkgs --volume / > /dev/null 2>&1; then + echo -e "${GREEN}${ICON_SUCCESS}${NC} Base system permission repair complete" + else + echo -e "${YELLOW}!${NC} repair_packages reported an issue" + fi + fi + ;; + + log_cleanup) + echo -e "${BLUE}${ICON_ARROW}${NC} Clearing diagnostic & crash logs..." + local -a user_logs=( + "$HOME/Library/Logs/DiagnosticReports" + "$HOME/Library/Logs/CrashReporter" + "$HOME/Library/Logs/corecaptured" + ) + for target in "${user_logs[@]}"; do + cleanup_path "$target" "$(basename "$target")" + done + + if [[ -d "/Library/Logs/DiagnosticReports" ]]; then + sudo find /Library/Logs/DiagnosticReports -type f -name "*.crash" -delete 2> /dev/null || true + sudo find /Library/Logs/DiagnosticReports -type f -name "*.panic" -delete 2> /dev/null || true + echo -e " ${GREEN}${ICON_SUCCESS}${NC} System diagnostic logs cleared" + else + echo -e " ${GRAY}-${NC} No system diagnostic logs found" + fi + ;; + + recent_items) + echo -e "${BLUE}${ICON_ARROW}${NC} Clearing recent items lists..." + local shared_dir="$HOME/Library/Application Support/com.apple.sharedfilelist" + if [[ -d "$shared_dir" ]]; then + local removed + removed=$(find "$shared_dir" -name "*.sfl2" -type f -print -delete 2> /dev/null | wc -l | tr -d ' ') + echo -e " ${GREEN}${ICON_SUCCESS}${NC} Reset $removed shared file lists" + else + echo -e " ${GRAY}-${NC} Recent item caches already clean" + fi + + rm -f "$HOME/Library/Preferences/com.apple.recentitems.plist" 2> /dev/null || true + defaults delete NSGlobalDomain NSRecentDocumentsLimit 2> /dev/null || true + echo -e " ${GREEN}${ICON_SUCCESS}${NC} Finder/Apple menu recent items cleared" + ;; + + radio_refresh) + echo -e "${BLUE}${ICON_ARROW}${NC} Resetting Bluetooth preferences..." + rm -f "$HOME/Library/Preferences/com.apple.Bluetooth.plist" 2> /dev/null || true + sudo rm -f /Library/Preferences/com.apple.Bluetooth.plist 2> /dev/null || true + echo -e " ${GREEN}${ICON_SUCCESS}${NC} Bluetooth caches refreshed" + + echo -e "${BLUE}${ICON_ARROW}${NC} Resetting Wi-Fi configuration..." + local sysconfig="/Library/Preferences/SystemConfiguration" + if [[ -d "$sysconfig" ]]; then + sudo cp "$sysconfig"/com.apple.airport.preferences.plist "$sysconfig"/com.apple.airport.preferences.plist.bak 2> /dev/null || true + sudo rm -f "$sysconfig"/com.apple.airport.preferences.plist "$sysconfig"/NetworkInterfaces.plist "$sysconfig"/preferences.plist 2> /dev/null || true + echo -e " ${GREEN}${ICON_SUCCESS}${NC} Wi-Fi preferences reset" + else + echo -e " ${GRAY}-${NC} SystemConfiguration directory missing" + fi + + sudo ifconfig awdl0 down 2> /dev/null || true + sudo ifconfig awdl0 up 2> /dev/null || true + echo -e " ${GREEN}${ICON_SUCCESS}${NC} Wireless services refreshed" + ;; + + mail_downloads) + echo -e "${BLUE}${ICON_ARROW}${NC} Clearing Mail attachment downloads..." + local -a mail_dirs=( + "$HOME/Library/Mail Downloads|Mail Downloads" + "$HOME/Library/Containers/com.apple.mail/Data/Library/Mail Downloads|Mail Container Downloads" + ) + for target in "${mail_dirs[@]}"; do + IFS='|' read -r target_path label <<< "$target" + cleanup_path "$target_path" "$label" + ensure_directory "$target_path" + done + echo -e " ${GREEN}${ICON_SUCCESS}${NC} Mail downloads cleared" + ;; + + saved_state_cleanup) + echo -e "${BLUE}${ICON_ARROW}${NC} Purging saved application states..." + local state_dir="$HOME/Library/Saved Application State" + cleanup_path "$state_dir" "Saved Application State" + ensure_directory "$state_dir" + echo -e " ${GREEN}${ICON_SUCCESS}${NC} Saved states cleared" + ;; + + finder_dock_refresh) + echo -e "${BLUE}${ICON_ARROW}${NC} Resetting Finder & Dock caches..." + local -a interface_targets=( + "$HOME/Library/Caches/com.apple.finder|Finder cache" + "$HOME/Library/Caches/com.apple.dock.iconcache|Dock icon cache" + ) + for target in "${interface_targets[@]}"; do + IFS='|' read -r target_path label <<< "$target" + cleanup_path "$target_path" "$label" + done + killall Finder > /dev/null 2>&1 || true + killall Dock > /dev/null 2>&1 || true + echo -e " ${GREEN}${ICON_SUCCESS}${NC} Finder & Dock relaunched" + ;; + + swap_cleanup) + echo -e "${BLUE}${ICON_ARROW}${NC} Flushing memory caches..." + if sudo purge > /dev/null 2>&1; then + echo -e "${GREEN}${ICON_SUCCESS}${NC} Inactive memory purged" + else + echo -e "${YELLOW}!${NC} purge command failed" + fi + + echo -e "${BLUE}${ICON_ARROW}${NC} Stopping dynamic pager and removing swapfiles..." + if sudo launchctl unload /System/Library/LaunchDaemons/com.apple.dynamic_pager.plist > /dev/null 2>&1; then + sudo rm -f /private/var/vm/swapfile* > /dev/null 2>&1 || true + sudo touch /private/var/vm/swapfile0 > /dev/null 2>&1 || true + sudo chmod 600 /private/var/vm/swapfile0 > /dev/null 2>&1 || true + sudo launchctl load /System/Library/LaunchDaemons/com.apple.dynamic_pager.plist > /dev/null 2>&1 || true + echo -e "${GREEN}${ICON_SUCCESS}${NC} Swap cache rebuilt" + else + echo -e "${YELLOW}!${NC} Could not unload dynamic_pager" + fi + ;; + + login_items) + echo -e "${BLUE}${ICON_ARROW}${NC} Listing login items..." + osascript -e 'tell application "System Events" to get the name of every login item' 2> /dev/null | sed 's/, /\n • /g; s/^/ • /' + echo -e "${GRAY}Use System Settings → General → Login Items to disable entries you don't need.${NC}" + ;; + + startup_cache) + echo -e "${BLUE}${ICON_ARROW}${NC} Rebuilding kext caches..." + if sudo kextcache -i / > /dev/null 2>&1; then + echo -e "${GREEN}${ICON_SUCCESS}${NC} Kernel/kext caches rebuilt" + else + echo -e "${YELLOW}!${NC} kextcache reported an issue" + fi + + echo -e "${BLUE}${ICON_ARROW}${NC} Clearing system prelinked kernel caches..." + sudo rm -rf /System/Library/PrelinkedKernels/* > /dev/null 2>&1 || true + sudo kextcache -system-prelinked-kernel > /dev/null 2>&1 || true + echo -e "${GREEN}${ICON_SUCCESS}${NC} Startup caches refreshed" + ;; + + local_snapshots) + if ! command -v tmutil > /dev/null 2>&1; then + echo -e "${YELLOW}!${NC} tmutil not available on this system" + return + fi + + local before after + before=$(count_local_snapshots) + if [[ "$before" -eq 0 ]]; then + echo -e "${GREEN}${ICON_SUCCESS}${NC} No local snapshots to thin" + return + fi + + echo -e "${BLUE}${ICON_ARROW}${NC} Thinning $before APFS local snapshots..." + if sudo tmutil thinlocalsnapshots / 9999999999 4 > /dev/null 2>&1; then + after=$(count_local_snapshots) + local removed=$((before - after)) + if [[ "$removed" -lt 0 ]]; then + removed=0 + fi + echo -e "${GREEN}${ICON_SUCCESS}${NC} Removed $removed snapshots (remaining: $after)" + else + echo -e "${RED}${ICON_ERROR}${NC} Failed to thin local snapshots" + fi + ;; + + developer_cleanup) + local -a dev_targets=( + "$HOME/Library/Developer/Xcode/DerivedData|Xcode DerivedData" + "$HOME/Library/Developer/Xcode/Archives|Build archives" + "$HOME/Library/Developer/Xcode/iOS DeviceSupport|iOS Device support files" + "$HOME/Library/Developer/CoreSimulator/Caches|CoreSimulator caches" + ) + + for target in "${dev_targets[@]}"; do + IFS='|' read -r target_path label <<< "$target" + cleanup_path "$target_path" "$label" + done + + if command -v xcrun > /dev/null 2>&1; then + echo -e "${BLUE}${ICON_ARROW}${NC} Removing unavailable simulator runtimes..." + if xcrun simctl delete unavailable > /dev/null 2>&1; then + echo -e "${GREEN}${ICON_SUCCESS}${NC} Unavailable simulators removed" + else + echo -e "${YELLOW}!${NC} Could not prune simulator runtimes" + fi + fi + + echo -e "${GREEN}${ICON_SUCCESS}${NC} Developer caches cleaned" + ;; + + *) + echo -e "${RED}${ICON_ERROR}${NC} Unknown action: $action" + ;; + esac +} + +main() { + print_header + + # Check dependencies + if ! command -v jq > /dev/null 2>&1; then + log_error "jq is required but not installed. Install with: brew install jq" + exit 1 + fi + + if ! command -v bc > /dev/null 2>&1; then + log_error "bc is required but not installed. Install with: brew install bc" + exit 1 + fi + + # Check if optimize-go exists + if [[ ! -x "$OPTIMIZE_GO" ]]; then + log_error "optimize-go binary not found. Please run: go build -o bin/optimize-go cmd/optimize/main.go" + exit 1 + fi + + # Collect system health data (silent) + local health_json + if ! health_json=$("$OPTIMIZE_GO" 2> /dev/null); then + log_error "Failed to collect system health data" + exit 1 + fi + + # Show system health + show_system_health "$health_json" + + # Parse and display optimizations + local -a safe_items=() + local -a confirm_items=() + + while IFS= read -r opt_json; do + [[ -z "$opt_json" ]] && continue + + local name=$(echo "$opt_json" | jq -r '.name') + local desc=$(echo "$opt_json" | jq -r '.description') + local action=$(echo "$opt_json" | jq -r '.action') + local path=$(echo "$opt_json" | jq -r '.path // ""') + local safe=$(echo "$opt_json" | jq -r '.safe') + + local item="${name}|${desc}|${action}|${path}" + + if [[ "$safe" == "true" ]]; then + safe_items+=("$item") + else + confirm_items+=("$item") + fi + done < <(parse_optimizations "$health_json") + + # Simple confirmation + echo -ne "${PURPLE}${ICON_ARROW}${NC} Press ${GREEN}Enter${NC} to optimize, ${GRAY}ESC${NC} to cancel: " + + IFS= read -r -s -n1 key || key="" + case "$key" in + $'\e' | q | Q) + echo "" + echo "" + echo -e "${GRAY}Cancelled${NC}" + echo "" + exit 0 + ;; + "" | $'\n' | $'\r') + printf "\r\033[K" + ;; + *) + echo "" + echo "" + echo -e "${GRAY}Cancelled${NC}" + echo "" + exit 0 + ;; + esac + + # Execute all optimizations + local first_heading=true + + # Run safe optimizations + if [[ ${#safe_items[@]} -gt 0 ]]; then + for item in "${safe_items[@]}"; do + IFS='|' read -r name desc action path <<< "$item" + announce_action "$name" "$desc" "safe" + execute_optimization "$action" "$path" + done + fi + + # Run confirm items + if [[ ${#confirm_items[@]} -gt 0 ]]; then + for item in "${confirm_items[@]}"; do + IFS='|' read -r name desc action path <<< "$item" + announce_action "$name" "$desc" "confirm" + execute_optimization "$action" "$path" + done + fi + + echo "" + local summary_title="System optimization completed" + local -a summary_details=() + + local safe_count=${#safe_items[@]} + local confirm_count=${#confirm_items[@]} + if (( safe_count > 0 )); then + summary_details+=("Automations: ${GREEN}${safe_count}${NC} sections optimized end-to-end.") + else + summary_details+=("Automations: No automated changes were necessary.") + fi + + if (( confirm_count > 0 )); then + summary_details+=("Follow-ups: ${YELLOW}${confirm_count}${NC} manual checks suggested (see log).") + fi + + summary_details+=("Highlights: caches refreshed, services restarted, startup assets rebuilt.") + summary_details+=("Result: system responsiveness should feel lighter.") + + local show_touchid_tip="false" + if touchid_supported && ! touchid_configured; then + show_touchid_tip="true" + fi + + if [[ "$show_touchid_tip" == "true" ]]; then + echo -e "Tip: run 'mo touchid' to approve sudo via Touch ID." + fi + print_summary_block "success" "$summary_title" "${summary_details[@]}" + printf '\n' +} + +main "$@" diff --git a/cmd/optimize/main.go b/cmd/optimize/main.go new file mode 100644 index 0000000..99a05c1 --- /dev/null +++ b/cmd/optimize/main.go @@ -0,0 +1,532 @@ +// Mole System Optimizer +// System optimization and maintenance + +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "syscall" + "time" +) + +type OptimizationItem struct { + Category string `json:"category"` + Name string `json:"name"` + Description string `json:"description"` + Action string `json:"action"` + Safe bool `json:"safe"` +} + +type SystemHealth struct { + MemoryUsedGB float64 `json:"memory_used_gb"` + MemoryTotalGB float64 `json:"memory_total_gb"` + DiskUsedGB float64 `json:"disk_used_gb"` + DiskTotalGB float64 `json:"disk_total_gb"` + DiskUsedPercent float64 `json:"disk_used_percent"` + UptimeDays float64 `json:"uptime_days"` + Optimizations []OptimizationItem `json:"optimizations"` +} + +func main() { + health := collectSystemHealth() + + encoder := json.NewEncoder(os.Stdout) + encoder.SetIndent("", " ") + if err := encoder.Encode(health); err != nil { + fmt.Fprintf(os.Stderr, "Error encoding JSON: %v\n", err) + os.Exit(1) + } +} + +func collectSystemHealth() SystemHealth { + health := SystemHealth{ + Optimizations: []OptimizationItem{}, + } + + // Collect system info + health.MemoryUsedGB, health.MemoryTotalGB = getMemoryInfo() + health.DiskUsedGB, health.DiskTotalGB, health.DiskUsedPercent = getDiskInfo() + health.UptimeDays = getUptimeDays() + + // System optimizations (always show) + health.Optimizations = append(health.Optimizations, OptimizationItem{ + Category: "system", + Name: "System Maintenance", + Description: "Rebuild system databases & flush caches", + Action: "system_maintenance", + Safe: true, + }) + + // Startup items (conditional) + if item := checkStartupItems(); item != nil { + health.Optimizations = append(health.Optimizations, *item) + } + + // Network services (always show) + health.Optimizations = append(health.Optimizations, OptimizationItem{ + Category: "network", + Name: "Network Services", + Description: "Reset network services", + Action: "network_services", + Safe: true, + }) + + // Cache refresh (always available) + if item := buildCacheRefreshItem(); item != nil { + health.Optimizations = append(health.Optimizations, *item) + } + + // macOS maintenance scripts (always available) + health.Optimizations = append(health.Optimizations, OptimizationItem{ + Category: "maintenance", + Name: "Maintenance Scripts", + Description: "Run daily/weekly/monthly scripts & rotate logs", + Action: "maintenance_scripts", + Safe: true, + }) + + // Wireless preferences refresh (always available) + health.Optimizations = append(health.Optimizations, OptimizationItem{ + Category: "network", + Name: "Bluetooth & Wi-Fi Refresh", + Description: "Reset wireless preference caches", + Action: "radio_refresh", + Safe: true, + }) + + // Recent items cleanup (always available) + health.Optimizations = append(health.Optimizations, OptimizationItem{ + Category: "privacy", + Name: "Recent Items", + Description: "Clear recent apps/documents/servers lists", + Action: "recent_items", + Safe: true, + }) + + // Diagnostic log cleanup (always available) + health.Optimizations = append(health.Optimizations, OptimizationItem{ + Category: "system", + Name: "Diagnostics Cleanup", + Description: "Purge old diagnostic & crash logs", + Action: "log_cleanup", + Safe: true, + }) + + if item := buildMailDownloadsItem(); item != nil { + health.Optimizations = append(health.Optimizations, *item) + } + + if item := buildSavedStateItem(); item != nil { + health.Optimizations = append(health.Optimizations, *item) + } + + health.Optimizations = append(health.Optimizations, OptimizationItem{ + Category: "interface", + Name: "Finder & Dock Refresh", + Description: "Clear Finder/Dock caches and restart", + Action: "finder_dock_refresh", + Safe: true, + }) + + if item := buildSwapCleanupItem(); item != nil { + health.Optimizations = append(health.Optimizations, *item) + } + + if item := buildLoginItemsItem(); item != nil { + health.Optimizations = append(health.Optimizations, *item) + } + + health.Optimizations = append(health.Optimizations, OptimizationItem{ + Category: "system", + Name: "Startup Cache Rebuild", + Description: "Rebuild kext caches & prelinked kernel", + Action: "startup_cache", + Safe: true, + }) + + // Local snapshot thinning (conditional) + if item := checkLocalSnapshots(); item != nil { + health.Optimizations = append(health.Optimizations, *item) + } + + // Developer-focused cleanup (conditional) + if item := checkDeveloperCleanup(); item != nil { + health.Optimizations = append(health.Optimizations, *item) + } + + return health +} + +func getMemoryInfo() (float64, float64) { + cmd := exec.Command("sysctl", "-n", "hw.memsize") + output, err := cmd.Output() + if err != nil { + return 0, 0 + } + + totalBytes, err := strconv.ParseInt(strings.TrimSpace(string(output)), 10, 64) + if err != nil { + return 0, 0 + } + totalGB := float64(totalBytes) / (1024 * 1024 * 1024) + + // Get used memory via vm_stat + cmd = exec.Command("vm_stat") + output, err = cmd.Output() + if err != nil { + return 0, totalGB + } + + var pageSize int64 = 4096 + var active, wired, compressed int64 + + lines := strings.Split(string(output), "\n") + for _, line := range lines { + if strings.Contains(line, "Pages active:") { + active = parseVMStatLine(line) + } else if strings.Contains(line, "Pages wired down:") { + wired = parseVMStatLine(line) + } else if strings.Contains(line, "Pages occupied by compressor:") { + compressed = parseVMStatLine(line) + } + } + + usedBytes := (active + wired + compressed) * pageSize + usedGB := float64(usedBytes) / (1024 * 1024 * 1024) + + return usedGB, totalGB +} + +func parseVMStatLine(line string) int64 { + fields := strings.Fields(line) + if len(fields) < 2 { + return 0 + } + numStr := strings.TrimSuffix(fields[len(fields)-1], ".") + num, _ := strconv.ParseInt(numStr, 10, 64) + return num +} + +func getUptimeDays() float64 { + cmd := exec.Command("sysctl", "-n", "kern.boottime") + output, err := cmd.Output() + if err != nil { + return 0 + } + + line := string(output) + if idx := strings.Index(line, "sec = "); idx != -1 { + secStr := line[idx+6:] + if endIdx := strings.Index(secStr, ","); endIdx != -1 { + secStr = secStr[:endIdx] + if bootTime, err := strconv.ParseInt(strings.TrimSpace(secStr), 10, 64); err == nil { + uptime := time.Now().Unix() - bootTime + return float64(uptime) / (24 * 3600) + } + } + } + return 0 +} + +func getDiskInfo() (float64, float64, float64) { + var stat syscall.Statfs_t + home, err := os.UserHomeDir() + if err != nil { + home = "/" + } + + if err := syscall.Statfs(home, &stat); err != nil { + return 0, 0, 0 + } + + totalBytes := stat.Blocks * uint64(stat.Bsize) + freeBytes := stat.Bfree * uint64(stat.Bsize) + usedBytes := totalBytes - freeBytes + + totalGB := float64(totalBytes) / (1024 * 1024 * 1024) + usedGB := float64(usedBytes) / (1024 * 1024 * 1024) + usedPercent := (float64(usedBytes) / float64(totalBytes)) * 100 + + return usedGB, totalGB, usedPercent +} + +func checkStartupItems() *OptimizationItem { + launchAgentsCount := 0 + agentsDirs := []string{ + filepath.Join(os.Getenv("HOME"), "Library/LaunchAgents"), + "/Library/LaunchAgents", + } + + for _, dir := range agentsDirs { + if entries, err := os.ReadDir(dir); err == nil { + launchAgentsCount += len(entries) + } + } + + if launchAgentsCount > 5 { + suggested := launchAgentsCount / 2 + if suggested < 1 { + suggested = 1 + } + return &OptimizationItem{ + Category: "startup", + Name: "Startup Items", + Description: fmt.Sprintf("%d items (suggest disable %d)", launchAgentsCount, suggested), + Action: "startup_items", + Safe: false, + } + } + return nil +} + +func buildCacheRefreshItem() *OptimizationItem { + desc := "Refresh Finder previews, Quick Look, and Safari caches" + if home, err := os.UserHomeDir(); err == nil { + cacheDir := filepath.Join(home, "Library", "Caches") + if sizeKB := dirSizeKB(cacheDir); sizeKB > 0 { + desc = fmt.Sprintf("Refresh %s of Finder/Safari caches", formatSizeFromKB(sizeKB)) + } + } + + return &OptimizationItem{ + Category: "cache", + Name: "User Cache Refresh", + Description: desc, + Action: "cache_refresh", + Safe: true, + } +} + +func buildMailDownloadsItem() *OptimizationItem { + home, err := os.UserHomeDir() + if err != nil { + return nil + } + + dirs := []string{ + filepath.Join(home, "Library", "Mail Downloads"), + filepath.Join(home, "Library", "Containers", "com.apple.mail", "Data", "Library", "Mail Downloads"), + } + + var totalKB int64 + for _, dir := range dirs { + totalKB += dirSizeKB(dir) + } + + if totalKB == 0 { + return nil + } + + return &OptimizationItem{ + Category: "applications", + Name: "Mail Downloads", + Description: fmt.Sprintf("Recover %s of Mail attachments", formatSizeFromKB(totalKB)), + Action: "mail_downloads", + Safe: true, + } +} + +func buildSavedStateItem() *OptimizationItem { + home, err := os.UserHomeDir() + if err != nil { + return nil + } + + stateDir := filepath.Join(home, "Library", "Saved Application State") + sizeKB := dirSizeKB(stateDir) + if sizeKB == 0 { + return nil + } + + return &OptimizationItem{ + Category: "system", + Name: "Saved State", + Description: fmt.Sprintf("Clear %s of stale saved states", formatSizeFromKB(sizeKB)), + Action: "saved_state_cleanup", + Safe: true, + } +} + +func buildSwapCleanupItem() *OptimizationItem { + swapGlob := "/private/var/vm/swapfile*" + matches, err := filepath.Glob(swapGlob) + if err != nil { + return nil + } + + var totalKB int64 + for _, file := range matches { + info, err := os.Stat(file) + if err != nil { + continue + } + totalKB += info.Size() / 1024 + } + + if totalKB == 0 { + return nil + } + + return &OptimizationItem{ + Category: "memory", + Name: "Memory & Swap", + Description: fmt.Sprintf("Purge swap (%s) & inactive memory", formatSizeFromKB(totalKB)), + Action: "swap_cleanup", + Safe: false, + } +} + +func buildLoginItemsItem() *OptimizationItem { + items := listLoginItems() + if len(items) == 0 { + return nil + } + + return &OptimizationItem{ + Category: "startup", + Name: "Login Items", + Description: fmt.Sprintf("Review %d login items", len(items)), + Action: "login_items", + Safe: true, + } +} + +func listLoginItems() []string { + cmd := exec.Command("osascript", "-e", "tell application \"System Events\" to get the name of every login item") + output, err := cmd.Output() + if err != nil { + return nil + } + + line := strings.TrimSpace(string(output)) + if line == "" || line == "missing value" { + return nil + } + + parts := strings.Split(line, ", ") + var items []string + for _, part := range parts { + name := strings.TrimSpace(part) + name = strings.Trim(name, "\"") + if name != "" { + items = append(items, name) + } + } + return items +} + +func checkLocalSnapshots() *OptimizationItem { + if _, err := exec.LookPath("tmutil"); err != nil { + return nil + } + + cmd := exec.Command("tmutil", "listlocalsnapshots", "/") + output, err := cmd.Output() + if err != nil { + return nil + } + + lines := strings.Split(strings.TrimSpace(string(output)), "\n") + count := 0 + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "com.apple.TimeMachine.") { + count++ + } + } + + if count == 0 { + return nil + } + + return &OptimizationItem{ + Category: "storage", + Name: "Local Snapshots", + Description: fmt.Sprintf("%d APFS local snapshots detected", count), + Action: "local_snapshots", + Safe: true, + } +} + +func checkDeveloperCleanup() *OptimizationItem { + home, err := os.UserHomeDir() + if err != nil { + return nil + } + + dirs := []string{ + filepath.Join(home, "Library", "Developer", "Xcode", "DerivedData"), + filepath.Join(home, "Library", "Developer", "Xcode", "Archives"), + filepath.Join(home, "Library", "Developer", "Xcode", "iOS DeviceSupport"), + filepath.Join(home, "Library", "Developer", "CoreSimulator", "Caches"), + } + + var totalKB int64 + for _, dir := range dirs { + totalKB += dirSizeKB(dir) + } + + if totalKB == 0 { + return nil + } + + return &OptimizationItem{ + Category: "developer", + Name: "Developer Cleanup", + Description: fmt.Sprintf("Recover %s of Xcode/simulator data", formatSizeFromKB(totalKB)), + Action: "developer_cleanup", + Safe: false, + } +} + +func dirSizeKB(path string) int64 { + if path == "" { + return 0 + } + + if _, err := os.Stat(path); err != nil { + return 0 + } + + cmd := exec.Command("du", "-sk", path) + output, err := cmd.Output() + if err != nil { + return 0 + } + + fields := strings.Fields(string(output)) + if len(fields) == 0 { + return 0 + } + + size, err := strconv.ParseInt(fields[0], 10, 64) + if err != nil { + return 0 + } + + return size +} + +func formatSizeFromKB(kb int64) string { + if kb <= 0 { + return "0B" + } + + mb := float64(kb) / 1024 + gb := mb / 1024 + + switch { + case gb >= 1: + return fmt.Sprintf("%.1fGB", gb) + case mb >= 1: + return fmt.Sprintf("%.0fMB", mb) + default: + return fmt.Sprintf("%dKB", kb) + } +} diff --git a/mole b/mole index eb430ac..5512178 100755 --- a/mole +++ b/mole @@ -132,6 +132,7 @@ show_help() { echo printf "%s%s%s\n" "$BLUE" "COMMANDS" "$NC" printf " %s%-28s%s %s\n" "$GREEN" "mo" "$NC" "Interactive main menu" + printf " %s%-28s%s %s\n" "$GREEN" "mo optimize" "$NC" "System health check & optimization" printf " %s%-28s%s %s\n" "$GREEN" "mo clean" "$NC" "Deeper system cleanup" printf " %s%-28s%s %s\n" "$GREEN" "mo clean --dry-run" "$NC" "Preview cleanup (no deletions)" printf " %s%-28s%s %s\n" "$GREEN" "mo clean --whitelist" "$NC" "Manage protected caches" @@ -454,9 +455,10 @@ show_main_menu() { printf '\r\033[2K%s\n' "$(show_menu_option 1 "Clean Mac - Remove junk files and optimize" "$([[ $selected -eq 1 ]] && echo true || echo false)")" printf '\r\033[2K%s\n' "$(show_menu_option 2 "Uninstall Apps - Remove applications completely" "$([[ $selected -eq 2 ]] && echo true || echo false)")" - printf '\r\033[2K%s\n' "$(show_menu_option 3 "Analyze Disk - Interactive space explorer" "$([[ $selected -eq 3 ]] && echo true || echo false)")" - printf '\r\033[2K%s\n' "$(show_menu_option 4 "Help & Information - Usage guide and tips" "$([[ $selected -eq 4 ]] && echo true || echo false)")" - printf '\r\033[2K%s\n' "$(show_menu_option 5 "Exit - Close Mole" "$([[ $selected -eq 5 ]] && echo true || echo false)")" + printf '\r\033[2K%s\n' "$(show_menu_option 3 "Optimize Mac - System health & tuning" "$([[ $selected -eq 3 ]] && echo true || echo false)")" + printf '\r\033[2K%s\n' "$(show_menu_option 4 "Analyze Disk - Interactive space explorer" "$([[ $selected -eq 4 ]] && echo true || echo false)")" + printf '\r\033[2K%s\n' "$(show_menu_option 5 "Help & Information - Usage guide and tips" "$([[ $selected -eq 5 ]] && echo true || echo false)")" + printf '\r\033[2K%s\n' "$(show_menu_option 6 "Exit - Close Mole" "$([[ $selected -eq 6 ]] && echo true || echo false)")" if [[ -t 0 ]]; then printf '\r\033[2K\n' @@ -521,7 +523,7 @@ interactive_main_menu() { case "$key" in "UP") ((current_option > 1)) && ((current_option--)) ;; - "DOWN") ((current_option < 5)) && ((current_option++)) ;; + "DOWN") ((current_option < 6)) && ((current_option++)) ;; "ENTER" | "$current_option") show_cursor case $current_option in @@ -529,30 +531,32 @@ interactive_main_menu() { exec "$SCRIPT_DIR/bin/clean.sh" ;; 2) exec "$SCRIPT_DIR/bin/uninstall.sh" ;; - 3) exec "$SCRIPT_DIR/bin/analyze.sh" ;; - 4) + 3) exec "$SCRIPT_DIR/bin/optimize.sh" ;; + 4) exec "$SCRIPT_DIR/bin/analyze.sh" ;; + 5) clear show_help exit 0 ;; - 5) cleanup_and_exit ;; + 6) cleanup_and_exit ;; esac ;; "QUIT") cleanup_and_exit ;; - [1-5]) + [1-6]) show_cursor case $key in 1) exec "$SCRIPT_DIR/bin/clean.sh" ;; 2) exec "$SCRIPT_DIR/bin/uninstall.sh" ;; - 3) exec "$SCRIPT_DIR/bin/analyze.sh" ;; - 4) + 3) exec "$SCRIPT_DIR/bin/optimize.sh" ;; + 4) exec "$SCRIPT_DIR/bin/analyze.sh" ;; + 5) clear show_help exit 0 ;; - 5) cleanup_and_exit ;; + 6) cleanup_and_exit ;; esac ;; esac @@ -561,6 +565,9 @@ interactive_main_menu() { main() { case "${1:-""}" in + "optimize") + exec "$SCRIPT_DIR/bin/optimize.sh" + ;; "clean") exec "$SCRIPT_DIR/bin/clean.sh" "${@:2}" ;; diff --git a/optimize b/optimize new file mode 100755 index 0000000..a8a84fe Binary files /dev/null and b/optimize differ