From 178176500c47bd4fbe36974f568144431c322c38 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Sun, 23 Nov 2025 14:03:14 +0800 Subject: [PATCH] Support more detection and update --- bin/check.sh | 94 +++++++ bin/optimize.sh | 554 ++++++++++++-------------------------- lib/autofix_manager.sh | 178 ++++++++++++ lib/check_config.sh | 58 ++++ lib/check_health.sh | 239 ++++++++++++++++ lib/check_security.sh | 63 +++++ lib/check_updates.sh | 273 +++++++++++++++++++ lib/common.sh | 104 +++++-- lib/optimization_tasks.sh | 332 +++++++++++++++++++++++ lib/sudo_manager.sh | 147 ++++++++++ lib/update_manager.sh | 269 ++++++++++++++++++ mole | 4 +- 12 files changed, 1905 insertions(+), 410 deletions(-) create mode 100755 bin/check.sh create mode 100644 lib/autofix_manager.sh create mode 100644 lib/check_config.sh create mode 100644 lib/check_health.sh create mode 100644 lib/check_security.sh create mode 100644 lib/check_updates.sh create mode 100644 lib/optimization_tasks.sh create mode 100644 lib/sudo_manager.sh create mode 100644 lib/update_manager.sh diff --git a/bin/check.sh b/bin/check.sh new file mode 100755 index 0000000..361337d --- /dev/null +++ b/bin/check.sh @@ -0,0 +1,94 @@ +#!/bin/bash + +set -euo pipefail + +# Load common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "$SCRIPT_DIR/lib/common.sh" +source "$SCRIPT_DIR/lib/sudo_manager.sh" +source "$SCRIPT_DIR/lib/update_manager.sh" +source "$SCRIPT_DIR/lib/autofix_manager.sh" + +source "$SCRIPT_DIR/lib/check_updates.sh" +source "$SCRIPT_DIR/lib/check_health.sh" +source "$SCRIPT_DIR/lib/check_security.sh" +source "$SCRIPT_DIR/lib/check_config.sh" + +cleanup_all() { + stop_sudo_session + cleanup_temp_files +} + +main() { + # Register unified cleanup handler + trap cleanup_all EXIT INT TERM + + if [[ -t 1 ]]; then + clear + fi + + printf '\n' + + # Create temp files for parallel execution + local updates_file=$(mktemp_file) + local health_file=$(mktemp_file) + local security_file=$(mktemp_file) + local config_file=$(mktemp_file) + + # Run all checks in parallel with spinner + if [[ -t 1 ]]; then + echo -ne "${PURPLE}System Check${NC} " + start_inline_spinner "Running checks..." + else + echo -e "${PURPLE}System Check${NC}" + echo "" + fi + + # Parallel execution + { + check_all_updates > "$updates_file" 2>&1 & + check_system_health > "$health_file" 2>&1 & + check_all_security > "$security_file" 2>&1 & + check_all_config > "$config_file" 2>&1 & + wait + } + + if [[ -t 1 ]]; then + stop_inline_spinner + printf '\n' + fi + + # Display results + echo -e "${BLUE}${ICON_ARROW}${NC} System updates" + cat "$updates_file" + + printf '\n' + echo -e "${BLUE}${ICON_ARROW}${NC} System health" + cat "$health_file" + + printf '\n' + echo -e "${BLUE}${ICON_ARROW}${NC} Security posture" + cat "$security_file" + + printf '\n' + echo -e "${BLUE}${ICON_ARROW}${NC} Configuration" + cat "$config_file" + + # Show suggestions + show_suggestions + + # Ask about auto-fix + if ask_for_auto_fix; then + perform_auto_fix + fi + + # Ask about updates + if ask_for_updates; then + perform_updates + fi + + printf '\n' +} + + +main "$@" diff --git a/bin/optimize.sh b/bin/optimize.sh index 7f1fdf2..3fb9379 100755 --- a/bin/optimize.sh +++ b/bin/optimize.sh @@ -6,15 +6,99 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" source "$SCRIPT_DIR/lib/common.sh" source "$SCRIPT_DIR/lib/optimize_health.sh" +source "$SCRIPT_DIR/lib/sudo_manager.sh" +source "$SCRIPT_DIR/lib/update_manager.sh" +source "$SCRIPT_DIR/lib/autofix_manager.sh" +source "$SCRIPT_DIR/lib/optimization_tasks.sh" + +# Load check modules +source "$SCRIPT_DIR/lib/check_updates.sh" +source "$SCRIPT_DIR/lib/check_health.sh" +source "$SCRIPT_DIR/lib/check_security.sh" +source "$SCRIPT_DIR/lib/check_config.sh" # Colors and icons from common.sh print_header() { printf '\n' - echo -e "${PURPLE}Optimize Your Mac${NC}" + echo -e "${PURPLE}Optimize and Check${NC}" echo "" } +# System check functions (real-time display) +run_system_checks() { + echo "" + echo -e "${PURPLE}System Check${NC}" + echo "" + + # Check updates - real-time display + echo -e "${BLUE}${ICON_ARROW}${NC} System updates" + check_all_updates + echo "" + + # Check health - real-time display + echo -e "${BLUE}${ICON_ARROW}${NC} System health" + check_system_health + echo "" + + # Check security - real-time display + echo -e "${BLUE}${ICON_ARROW}${NC} Security posture" + check_all_security + echo "" + + # Check configuration - real-time display + echo -e "${BLUE}${ICON_ARROW}${NC} Configuration" + check_all_config + echo "" + + # Show suggestions + show_suggestions + echo "" + + # Ask about updates first + if ask_for_updates; then + perform_updates + fi + + # Ask about auto-fix + if ask_for_auto_fix; then + perform_auto_fix + fi +} + +show_optimization_summary() { + if [[ -z "${OPTIMIZE_SAFE_COUNT:-}" ]]; then + return + fi + + echo "" + local summary_title="Optimization and Check Complete" + local -a summary_details=() + + # Optimization results + if ((OPTIMIZE_SAFE_COUNT > 0)); then + summary_details+=("Applied ${GREEN}${OPTIMIZE_SAFE_COUNT}${NC} optimizations") + else + summary_details+=("System already optimized") + fi + + if ((OPTIMIZE_CONFIRM_COUNT > 0)); then + summary_details+=("${YELLOW}${OPTIMIZE_CONFIRM_COUNT}${NC} manual checks suggested") + fi + + summary_details+=("Caches cleared, databases rebuilt, services refreshed") + + # System check results + summary_details+=("System updates, health, security, and config reviewed") + summary_details+=("System should feel faster and more responsive") + + if [[ "${OPTIMIZE_SHOW_TOUCHID_TIP:-false}" == "true" ]]; then + echo -e "${YELLOW}☻${NC} Run ${GRAY}mo touchid${NC} to approve sudo via Touch ID" + fi + print_summary_block "success" "$summary_title" "${summary_details[@]}" +} + + show_system_health() { local health_json="$1" @@ -81,7 +165,7 @@ cleanup_path() { local expanded_path="${raw_path/#\~/$HOME}" if [[ ! -e "$expanded_path" ]]; then - echo -e " ${GREEN}${ICON_SUCCESS}${NC} $label" + echo -e "${GREEN}${ICON_SUCCESS}${NC} $label" return fi @@ -94,12 +178,12 @@ cleanup_path() { if rm -rf "$expanded_path"; then if [[ -n "$size_display" ]]; then - echo -e " ${GREEN}${ICON_SUCCESS}${NC} $label ${GREEN}(${size_display})${NC}" + echo -e "${GREEN}${ICON_SUCCESS}${NC} $label ${GREEN}(${size_display})${NC}" else - echo -e " ${GREEN}${ICON_SUCCESS}${NC} $label" + echo -e "${GREEN}${ICON_SUCCESS}${NC} $label" fi else - echo -e " ${RED}${ICON_ERROR}${NC} Failed to remove $label" + echo -e "${RED}${ICON_ERROR}${NC} Failed to remove $label" fi } @@ -109,45 +193,6 @@ ensure_directory() { mkdir -p "$expanded_path" > /dev/null 2>&1 || true } -list_login_items() { - local raw_items - raw_items=$(osascript -e 'tell application "System Events" to get the name of every login item' 2> /dev/null || echo "") - [[ -z "$raw_items" || "$raw_items" == "missing value" ]] && return - - IFS=',' read -ra login_items_array <<< "$raw_items" - for entry in "${login_items_array[@]}"; do - local trimmed - trimmed=$(echo "$entry" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') - [[ -n "$trimmed" ]] && printf "%s\n" "$trimmed" - done -} - -SUDO_KEEPALIVE_PID="" - -start_sudo_keepalive() { - [[ -n "$SUDO_KEEPALIVE_PID" ]] && return - - ( - while true; do - if ! sudo -n true 2> /dev/null; then - exit 0 - fi - sleep 30 - done - ) & - SUDO_KEEPALIVE_PID=$! -} - -stop_sudo_keepalive() { - if [[ -n "$SUDO_KEEPALIVE_PID" ]]; then - kill "$SUDO_KEEPALIVE_PID" 2> /dev/null || true - wait "$SUDO_KEEPALIVE_PID" 2> /dev/null || true - SUDO_KEEPALIVE_PID="" - fi -} - -trap stop_sudo_keepalive EXIT - count_local_snapshots() { if ! command -v tmutil > /dev/null 2>&1; then echo 0 @@ -164,281 +209,16 @@ count_local_snapshots() { 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} Clearing 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 cleared" - else - echo -e "${RED}${ICON_ERROR}${NC} Failed to clear DNS cache" - fi - - echo -e "${BLUE}${ICON_ARROW}${NC} Clearing memory cache..." - if sudo purge 2> /dev/null; then - echo -e "${GREEN}${ICON_SUCCESS}${NC} Memory cache cleared" - else - echo -e "${RED}${ICON_ERROR}${NC} Failed to clear 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" - ;; - - 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 updated" - ;; - - 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} Moving old system logs..." - if sudo newsyslog > /dev/null 2>&1; then - echo -e "${GREEN}${ICON_SUCCESS}${NC} Log move 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 - # Delete shared file lists - find "$shared_dir" -name "*.sfl2" -type f -delete 2> /dev/null || true - echo -e " ${GREEN}${ICON_SUCCESS}${NC} Shared file lists cleared" - fi - - # Clear recent items preferences - 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} 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 settings..." - 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 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} Removing 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} Removing swapfiles and resetting dynamic pager..." - 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 - ;; - - startup_cache) - local macos_version - macos_version=$(sw_vers -productVersion | cut -d '.' -f 1) - - # macOS 11+ has read-only system volume, skip system file operations - if [[ "$macos_version" -ge 11 ]] || [[ "$(uname -m)" == "arm64" ]]; then - echo -e "${BLUE}${ICON_ARROW}${NC} Rebuilding kext caches..." - if sudo kextcache -i / > /dev/null 2>&1; then - echo -e "${GREEN}${ICON_SUCCESS}${NC} Startup caches refreshed" - else - echo -e "${GREEN}${ICON_SUCCESS}${NC} Startup caches refreshed (sealed system volume)" - fi - else - 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" - fi - ;; - - 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/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 +cleanup_all() { + stop_sudo_session + cleanup_temp_files } main() { + # Register unified cleanup handler + trap cleanup_all EXIT INT TERM + if [[ -t 1 ]]; then clear fi @@ -457,52 +237,66 @@ main() { exit 1 fi - # Simple confirmation first - echo -ne "${PURPLE}${ICON_ARROW}${NC} System optimization needs admin access ${GREEN}Enter${NC} continue / ${GRAY}ESC${NC} cancel: " + # Simple confirmation + echo -ne "${PURPLE}${ICON_ARROW}${NC} Optimization needs sudo — ${GREEN}Enter${NC} continue, ${GRAY}ESC${NC} cancel: " - IFS= read -r -s -n1 key || key="" - drain_pending_input # Clean up any escape sequence remnants - case "$key" in - $'\e' | q | Q) - echo "" - echo "" - echo -e "${GRAY}Cancelled${NC}" - echo "" - exit 0 - ;; - "" | $'\n' | $'\r') - printf "\r\033[K" - if ! request_sudo_access "System optimizations require admin access"; then - echo "" - echo -e "${YELLOW}Authentication failed${NC}" - exit 1 - fi - start_sudo_keepalive - ;; - *) - echo "" - echo "" - echo -e "${GRAY}Cancelled${NC}" - echo "" - exit 0 - ;; - esac + local key + if ! key=$(read_key); then + echo -e " ${GRAY}Cancelled${NC}" + exit 0 + fi + + if [[ "$key" == "ENTER" ]]; then + printf "\r\033[K" + else + echo -e " ${GRAY}Cancelled${NC}" + exit 0 + fi # Collect system health data after confirmation + if [[ -t 1 ]]; then + start_inline_spinner "Collecting system info..." + fi + local health_json if ! health_json=$(generate_health_json 2> /dev/null); then + if [[ -t 1 ]]; then + stop_inline_spinner + fi echo "" log_error "Failed to collect system health data" exit 1 fi + if [[ -t 1 ]]; then + stop_inline_spinner + fi + # Show system health show_system_health "$health_json" + if [[ "${MO_DEBUG:-}" == "1" ]]; then + echo "DEBUG: System health displayed" + fi + # Parse and display optimizations local -a safe_items=() local -a confirm_items=() + if [[ "${MO_DEBUG:-}" == "1" ]]; then + echo "DEBUG: Parsing optimizations..." + fi + + # Use temp file instead of process substitution to avoid hanging + local opts_file + opts_file=$(mktemp_file) + parse_optimizations "$health_json" > "$opts_file" + + if [[ "${MO_DEBUG:-}" == "1" ]]; then + local opt_count=$(wc -l < "$opts_file" | tr -d ' ') + echo "DEBUG: Found $opt_count optimizations" + fi + while IFS= read -r opt_json; do [[ -z "$opt_json" ]] && continue @@ -519,11 +313,26 @@ main() { else confirm_items+=("$item") fi - done < <(parse_optimizations "$health_json") + done < "$opts_file" + + if [[ "${MO_DEBUG:-}" == "1" ]]; then + echo "DEBUG: Parsing complete. Safe: ${#safe_items[@]}, Confirm: ${#confirm_items[@]}" + fi # Execute all optimizations local first_heading=true + # Debug: show what we're about to do + if [[ "${MO_DEBUG:-}" == "1" ]]; then + echo "DEBUG: About to request sudo. Safe items: ${#safe_items[@]}, Confirm items: ${#confirm_items[@]}" + fi + + ensure_sudo_session "System optimization requires admin access" || true + + if [[ "${MO_DEBUG:-}" == "1" ]]; then + echo "DEBUG: Sudo session established or skipped" + fi + # Run safe optimizations if [[ ${#safe_items[@]} -gt 0 ]]; then for item in "${safe_items[@]}"; do @@ -542,56 +351,23 @@ main() { done fi - # Show login item reminder at the end of optimization log - local -a login_items_list=() - while IFS= read -r login_item; do - [[ -n "$login_item" ]] && login_items_list+=("$login_item") - done < <(list_login_items || true) - - if ((${#login_items_list[@]} > 0)); then - local display_count=${#login_items_list[@]} - echo "" - echo -e "${BLUE}${ICON_ARROW}${NC} Found ${display_count} items that auto-start at login:" - local preview_limit=5 - ((preview_limit > display_count)) && preview_limit=$display_count - for ((i = 0; i < preview_limit; i++)); do - printf " • %s\n" "${login_items_list[$i]}" - done - if ((display_count > preview_limit)); then - local remaining=$((display_count - preview_limit)) - echo " • …and $remaining more" - fi - echo -e "${GRAY}Review in System Settings → Login Items to remove unnecessary ones${NC}" - fi - - echo "" - local summary_title="System optimization complete" - local -a summary_details=() - + # Prepare optimization summary data (to show at the end) local safe_count=${#safe_items[@]} local confirm_count=${#confirm_items[@]} - if ((safe_count > 0)); then - summary_details+=("Applied ${GREEN}${safe_count}${NC} optimizations") - else - summary_details+=("System already optimized") - fi - if ((confirm_count > 0)); then - summary_details+=("${YELLOW}${confirm_count}${NC} manual checks suggested") - fi - - summary_details+=("Caches cleared, databases rebuilt, services refreshed") - summary_details+=("System should feel faster and more responsive") - - local show_touchid_tip="false" + export OPTIMIZE_SAFE_COUNT=$safe_count + export OPTIMIZE_CONFIRM_COUNT=$confirm_count + export OPTIMIZE_SHOW_TOUCHID_TIP="false" if touchid_supported && ! touchid_configured; then - show_touchid_tip="true" + export OPTIMIZE_SHOW_TOUCHID_TIP="true" fi - if [[ "$show_touchid_tip" == "true" ]]; then - echo -e "${YELLOW}☻${NC} Run ${GRAY}mo touchid${NC} to approve sudo via Touch ID" - fi - print_summary_block "success" "$summary_title" "${summary_details[@]}" + # Run system checks first + run_system_checks + + # Show optimization summary at the end + show_optimization_summary + printf '\n' } diff --git a/lib/autofix_manager.sh b/lib/autofix_manager.sh new file mode 100644 index 0000000..c68056c --- /dev/null +++ b/lib/autofix_manager.sh @@ -0,0 +1,178 @@ +#!/bin/bash +# Auto-fix Manager +# Unified auto-fix suggestions and execution + +set -euo pipefail + +# Show system suggestions with auto-fix markers +show_suggestions() { + local has_suggestions=false + local can_auto_fix=false + local -a auto_fix_items=() + local -a manual_items=() + + # Security suggestions + if [[ -n "${FIREWALL_DISABLED:-}" && "${FIREWALL_DISABLED}" == "true" ]]; then + auto_fix_items+=("Enable Firewall for better security") + has_suggestions=true + can_auto_fix=true + fi + + if [[ -n "${FILEVAULT_DISABLED:-}" && "${FILEVAULT_DISABLED}" == "true" ]]; then + manual_items+=("Enable FileVault|System Settings → Privacy & Security → FileVault") + has_suggestions=true + fi + + # Configuration suggestions + if [[ -n "${TOUCHID_NOT_CONFIGURED:-}" && "${TOUCHID_NOT_CONFIGURED}" == "true" ]]; then + auto_fix_items+=("Enable Touch ID for sudo") + has_suggestions=true + can_auto_fix=true + fi + + if [[ -n "${ROSETTA_NOT_INSTALLED:-}" && "${ROSETTA_NOT_INSTALLED}" == "true" ]]; then + auto_fix_items+=("Install Rosetta 2 for Intel app support") + has_suggestions=true + can_auto_fix=true + fi + + # Health suggestions + if [[ -n "${CACHE_SIZE_GB:-}" ]]; then + local cache_gb="${CACHE_SIZE_GB:-0}" + if (( $(echo "$cache_gb > 5" | bc -l 2>/dev/null || echo 0) )); then + manual_items+=("Free up ${cache_gb}GB by cleaning caches|Run: mo clean") + has_suggestions=true + fi + fi + + if [[ -n "${BREW_HAS_WARNINGS:-}" && "${BREW_HAS_WARNINGS}" == "true" ]]; then + manual_items+=("Fix Homebrew warnings|Run: brew doctor to see details") + has_suggestions=true + fi + + if [[ -n "${DISK_FREE_GB:-}" && "${DISK_FREE_GB:-0}" -lt 50 ]]; then + if [[ -z "${CACHE_SIZE_GB:-}" ]] || (( $(echo "${CACHE_SIZE_GB:-0} <= 5" | bc -l 2>/dev/null || echo 1) )); then + manual_items+=("Low disk space (${DISK_FREE_GB}GB free)|Run: mo analyze to find large files") + has_suggestions=true + fi + fi + + # Display suggestions + echo -e "${BLUE}${ICON_ARROW}${NC} Suggestions" + + if [[ "$has_suggestions" == "false" ]]; then + echo -e " ${GREEN}✓${NC} All looks good" + export HAS_AUTO_FIX_SUGGESTIONS="false" + return + fi + + # Show auto-fix items + if [[ ${#auto_fix_items[@]} -gt 0 ]]; then + for item in "${auto_fix_items[@]}"; do + echo -e " ${YELLOW}⚠${NC} ${item} ${GREEN}[auto]${NC}" + done + fi + + # Show manual items + if [[ ${#manual_items[@]} -gt 0 ]]; then + for item in "${manual_items[@]}"; do + local title="${item%%|*}" + local hint="${item#*|}" + echo -e " ${YELLOW}⚠${NC} ${title}" + echo -e " ${GRAY}${hint}${NC}" + done + fi + + # Export for use in auto-fix + export HAS_AUTO_FIX_SUGGESTIONS="$can_auto_fix" +} + +# Ask user if they want to auto-fix +# Returns: 0 if yes, 1 if no +ask_for_auto_fix() { + if [[ "${HAS_AUTO_FIX_SUGGESTIONS:-false}" != "true" ]]; then + return 1 + fi + + echo -ne "Fix issues marked ${GREEN}[auto]${NC}? ${GRAY}Enter yes / ESC no${NC}: " + + local key + if ! key=$(read_key); then + echo "no" + echo "" + return 1 + fi + + if [[ "$key" == "ENTER" ]]; then + echo "yes" + echo "" + return 0 + else + echo "no" + echo "" + return 1 + fi +} + +# Perform auto-fixes +# Returns: number of fixes applied +perform_auto_fix() { + local fixed_count=0 + + # Ensure sudo access + if ! has_sudo_session; then + if ! ensure_sudo_session "System fixes require admin access"; then + echo -e "${YELLOW}Skipping auto fixes (admin authentication required)${NC}" + echo "" + return 0 + fi + fi + + # Fix Firewall + if [[ -n "${FIREWALL_DISABLED:-}" && "${FIREWALL_DISABLED}" == "true" ]]; then + echo -e "${BLUE}Enabling Firewall...${NC}" + if sudo defaults write /Library/Preferences/com.apple.alf globalstate -int 1 2>/dev/null; then + echo -e "${GREEN}✓${NC} Firewall enabled" + ((fixed_count++)) + else + echo -e "${RED}✗${NC} Failed to enable Firewall" + fi + echo "" + fi + + # Fix Touch ID + if [[ -n "${TOUCHID_NOT_CONFIGURED:-}" && "${TOUCHID_NOT_CONFIGURED}" == "true" ]]; then + echo -e "${BLUE}Configuring Touch ID for sudo...${NC}" + local pam_file="/etc/pam.d/sudo" + if sudo bash -c "grep -q 'pam_tid.so' '$pam_file' 2>/dev/null || sed -i '' '2i\\ +auth sufficient pam_tid.so +' '$pam_file'" 2>/dev/null; then + echo -e "${GREEN}✓${NC} Touch ID configured" + ((fixed_count++)) + else + echo -e "${RED}✗${NC} Failed to configure Touch ID" + fi + echo "" + fi + + # Install Rosetta 2 + if [[ -n "${ROSETTA_NOT_INSTALLED:-}" && "${ROSETTA_NOT_INSTALLED}" == "true" ]]; then + echo -e "${BLUE}Installing Rosetta 2...${NC}" + if sudo softwareupdate --install-rosetta --agree-to-license 2>&1 | grep -qE "(Installing|Installed|already installed)"; then + echo -e "${GREEN}✓${NC} Rosetta 2 installed" + ((fixed_count++)) + else + echo -e "${RED}✗${NC} Failed to install Rosetta 2" + fi + echo "" + fi + + if [[ $fixed_count -gt 0 ]]; then + echo -e "${GREEN}Fixed ${fixed_count} issue(s)${NC}" + else + echo -e "${YELLOW}No issues were fixed${NC}" + fi + echo "" + + return $fixed_count +} diff --git a/lib/check_config.sh b/lib/check_config.sh new file mode 100644 index 0000000..9140efd --- /dev/null +++ b/lib/check_config.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# Configuration checks + +check_touchid_sudo() { + # Check if Touch ID is configured for sudo + local pam_file="/etc/pam.d/sudo" + if [[ -f "$pam_file" ]] && grep -q "pam_tid.so" "$pam_file" 2>/dev/null; then + echo -e " ${GREEN}✓${NC} Touch ID Enabled for sudo" + else + # Check if Touch ID is supported + local is_supported=false + if command -v bioutil > /dev/null 2>&1; then + if bioutil -r 2>/dev/null | grep -q "Touch ID"; then + is_supported=true + fi + elif [[ "$(uname -m)" == "arm64" ]]; then + is_supported=true + fi + + if [[ "$is_supported" == "true" ]]; then + echo -e " ${YELLOW}⚠${NC} Touch ID ${YELLOW}Not configured${NC} for sudo" + export TOUCHID_NOT_CONFIGURED=true + fi + fi +} + +check_rosetta() { + # Check Rosetta 2 (for Apple Silicon Macs) + if [[ "$(uname -m)" == "arm64" ]]; then + if [[ -f "/Library/Apple/usr/share/rosetta/rosetta" ]]; then + echo -e " ${GREEN}✓${NC} Rosetta 2 Installed" + else + echo -e " ${YELLOW}⚠${NC} Rosetta 2 ${YELLOW}Not installed${NC}" + export ROSETTA_NOT_INSTALLED=true + fi + fi +} + +check_git_config() { + # Check basic Git configuration + if command -v git > /dev/null 2>&1; then + local git_name=$(git config --global user.name 2>/dev/null || echo "") + local git_email=$(git config --global user.email 2>/dev/null || echo "") + + if [[ -n "$git_name" && -n "$git_email" ]]; then + echo -e " ${GREEN}✓${NC} Git Config Configured" + else + echo -e " ${YELLOW}⚠${NC} Git Config ${YELLOW}Not configured${NC}" + fi + fi +} + +check_all_config() { + check_touchid_sudo + check_rosetta + check_git_config +} diff --git a/lib/check_health.sh b/lib/check_health.sh new file mode 100644 index 0000000..9f853b9 --- /dev/null +++ b/lib/check_health.sh @@ -0,0 +1,239 @@ +#!/bin/bash + +# System health checks +# Sets global variables for use in suggestions + +check_disk_space() { + local free_gb=$(df -H / | awk 'NR==2 {print $4}' | sed 's/G//') + local free_num=$(echo "$free_gb" | tr -d 'G' | cut -d'.' -f1) + + export DISK_FREE_GB=$free_num + + if [[ $free_num -lt 20 ]]; then + echo -e " ${RED}✗${NC} Disk Space ${RED}${free_gb}GB free${NC} (Critical)" + elif [[ $free_num -lt 50 ]]; then + echo -e " ${YELLOW}⚠${NC} Disk Space ${YELLOW}${free_gb}GB free${NC} (Low)" + else + echo -e " ${GREEN}✓${NC} Disk Space ${free_gb}GB free" + fi +} + +check_memory_usage() { + local mem_total + mem_total=$(sysctl -n hw.memsize 2>/dev/null || echo "0") + if [[ -z "$mem_total" || "$mem_total" -le 0 ]]; then + echo -e " ${GRAY}-${NC} Memory Unable to determine" + return + fi + + local vm_output + vm_output=$(vm_stat 2>/dev/null || echo "") + + local page_size + page_size=$(echo "$vm_output" | awk '/page size of/ {print $8}') + [[ -z "$page_size" ]] && page_size=4096 + + local free_pages inactive_pages spec_pages + free_pages=$(echo "$vm_output" | awk '/Pages free/ {gsub(/\./,"",$3); print $3}') + inactive_pages=$(echo "$vm_output" | awk '/Pages inactive/ {gsub(/\./,"",$3); print $3}') + spec_pages=$(echo "$vm_output" | awk '/Pages speculative/ {gsub(/\./,"",$3); print $3}') + + free_pages=${free_pages:-0} + inactive_pages=${inactive_pages:-0} + spec_pages=${spec_pages:-0} + + # Estimate used percent: (total - free - inactive - speculative) / total + local total_pages=$((mem_total / page_size)) + local free_total=$((free_pages + inactive_pages + spec_pages)) + local used_pages=$((total_pages - free_total)) + if ((used_pages < 0)); then + used_pages=0 + fi + + local used_percent + used_percent=$(awk "BEGIN {printf \"%.0f\", ($used_pages / $total_pages) * 100}") + ((used_percent > 100)) && used_percent=100 + ((used_percent < 0)) && used_percent=0 + + if [[ $used_percent -gt 90 ]]; then + echo -e " ${RED}✗${NC} Memory ${RED}${used_percent}% used${NC} (Critical)" + elif [[ $used_percent -gt 80 ]]; then + echo -e " ${YELLOW}⚠${NC} Memory ${YELLOW}${used_percent}% used${NC} (High)" + else + echo -e " ${GREEN}✓${NC} Memory ${used_percent}% used" + fi +} + +check_login_items() { + local login_items_count=0 + local -a login_items_list=() + + if [[ -t 0 ]]; then + # Show spinner while getting login items + if [[ -t 1 ]]; then + start_inline_spinner "Checking login items..." + fi + + while IFS= read -r login_item; do + [[ -n "$login_item" ]] && login_items_list+=("$login_item") + done < <(list_login_items || true) + login_items_count=${#login_items_list[@]} + + # Stop spinner before output + if [[ -t 1 ]]; then + stop_inline_spinner + fi + fi + + if [[ $login_items_count -gt 15 ]]; then + echo -e " ${YELLOW}⚠${NC} Login Items ${YELLOW}${login_items_count} apps${NC} auto-start (High)" + elif [[ $login_items_count -gt 0 ]]; then + echo -e " ${GREEN}✓${NC} Login Items ${login_items_count} apps auto-start" + else + echo -e " ${GREEN}✓${NC} Login Items None" + return + fi + + # Show items in a single line + local preview_limit=5 + ((preview_limit > login_items_count)) && preview_limit=$login_items_count + + local items_display="" + for ((i = 0; i < preview_limit; i++)); do + if [[ $i -eq 0 ]]; then + items_display="${login_items_list[$i]}" + else + items_display="${items_display}, ${login_items_list[$i]}" + fi + done + + if ((login_items_count > preview_limit)); then + local remaining=$((login_items_count - preview_limit)) + items_display="${items_display}, and ${remaining} more" + fi + + echo -e " ${GRAY}${items_display}${NC}" + echo -e " ${GRAY}Manage in System Settings → Login Items${NC}" +} + +check_cache_size() { + local cache_size_kb=0 + + # Check common cache locations + local -a cache_paths=( + "$HOME/Library/Caches" + "$HOME/Library/Logs" + ) + + # Show spinner while calculating cache size + if [[ -t 1 ]]; then + start_inline_spinner "Scanning cache..." + fi + + for cache_path in "${cache_paths[@]}"; do + if [[ -d "$cache_path" ]]; then + local size=$(du -sk "$cache_path" 2>/dev/null | awk '{print $1}' || echo "0") + cache_size_kb=$((cache_size_kb + size)) + fi + done + + local cache_size_gb=$(echo "scale=1; $cache_size_kb / 1024 / 1024" | bc) + export CACHE_SIZE_GB=$cache_size_gb + + # Stop spinner before output + if [[ -t 1 ]]; then + stop_inline_spinner + fi + + # Convert to integer for comparison + local cache_size_int=$(echo "$cache_size_gb" | cut -d'.' -f1) + + if [[ $cache_size_int -gt 10 ]]; then + echo -e " ${YELLOW}⚠${NC} Cache Size ${YELLOW}${cache_size_gb}GB${NC} cleanable" + elif [[ $cache_size_int -gt 5 ]]; then + echo -e " ${YELLOW}⚠${NC} Cache Size ${YELLOW}${cache_size_gb}GB${NC} cleanable" + else + echo -e " ${GREEN}✓${NC} Cache Size ${cache_size_gb}GB" + fi +} + +check_swap_usage() { + # Check swap usage + if command -v sysctl > /dev/null 2>&1; then + local swap_info=$(sysctl vm.swapusage 2>/dev/null || echo "") + if [[ -n "$swap_info" ]]; then + local swap_used=$(echo "$swap_info" | grep -o "used = [0-9.]*[GM]" | awk '{print $3}' || echo "0M") + local swap_num=$(echo "$swap_used" | sed 's/[GM]//') + + if [[ "$swap_used" == *"G"* ]]; then + local swap_gb=${swap_num%.*} + if [[ $swap_gb -gt 2 ]]; then + echo -e " ${YELLOW}⚠${NC} Swap Usage ${YELLOW}${swap_used}${NC} (High)" + else + echo -e " ${GREEN}✓${NC} Swap Usage ${swap_used}" + fi + else + echo -e " ${GREEN}✓${NC} Swap Usage ${swap_used}" + fi + fi + fi +} + +check_timemachine() { + # Check Time Machine backup status + if command -v tmutil > /dev/null 2>&1; then + local tm_status=$(tmutil latestbackup 2>/dev/null || echo "") + if [[ -z "$tm_status" ]]; then + echo -e " ${YELLOW}⚠${NC} Time Machine No backups found" + echo -e " ${GRAY}Set up in System Settings → General → Time Machine (optional but recommended)${NC}" + else + # Get last backup time + local backup_date=$(tmutil latestbackup 2>/dev/null | xargs basename 2>/dev/null || echo "") + if [[ -n "$backup_date" ]]; then + echo -e " ${GREEN}✓${NC} Time Machine Backup active" + else + echo -e " ${YELLOW}⚠${NC} Time Machine Not configured" + fi + fi + fi +} + +check_brew_health() { + # Check Homebrew doctor + if command -v brew > /dev/null 2>&1; then + # Show spinner while running brew doctor + if [[ -t 1 ]]; then + start_inline_spinner "Running brew doctor..." + fi + + local brew_doctor=$(brew doctor 2>&1 || echo "") + + # Stop spinner before output + if [[ -t 1 ]]; then + stop_inline_spinner + fi + + if echo "$brew_doctor" | grep -q "ready to brew"; then + echo -e " ${GREEN}✓${NC} Homebrew Healthy" + else + local warning_count=$(echo "$brew_doctor" | grep -c "Warning:" || echo "0") + if [[ $warning_count -gt 0 ]]; then + echo -e " ${YELLOW}⚠${NC} Homebrew ${YELLOW}${warning_count} warnings${NC}" + echo -e " ${GRAY}Run: ${GREEN}brew doctor${NC} to see fixes, then rerun until clean${NC}" + export BREW_HAS_WARNINGS=true + else + echo -e " ${GREEN}✓${NC} Homebrew Healthy" + fi + fi + fi +} + +check_system_health() { + check_disk_space + check_memory_usage + check_swap_usage + check_login_items + check_cache_size + # Time Machine check is optional; skip by default to avoid noise on systems without backups + check_brew_health +} diff --git a/lib/check_security.sh b/lib/check_security.sh new file mode 100644 index 0000000..f767593 --- /dev/null +++ b/lib/check_security.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +# Security checks + +check_filevault() { + # Check FileVault encryption status + if command -v fdesetup > /dev/null 2>&1; then + local fv_status=$(fdesetup status 2>/dev/null || echo "") + if echo "$fv_status" | grep -q "FileVault is On"; then + echo -e " ${GREEN}✓${NC} FileVault Enabled" + else + echo -e " ${RED}✗${NC} FileVault ${RED}Disabled${NC} (Recommend enabling)" + export FILEVAULT_DISABLED=true + fi + fi +} + +check_firewall() { + # Check firewall status + local firewall_status=$(defaults read /Library/Preferences/com.apple.alf globalstate 2>/dev/null || echo "0") + if [[ "$firewall_status" == "1" || "$firewall_status" == "2" ]]; then + echo -e " ${GREEN}✓${NC} Firewall Enabled" + else + echo -e " ${YELLOW}⚠${NC} Firewall ${YELLOW}Disabled${NC} (Consider enabling)" + echo -e " ${GRAY}System Settings → Network → Firewall, or run:${NC}" + echo -e " ${GRAY}sudo defaults write /Library/Preferences/com.apple.alf globalstate -int 1${NC}" + export FIREWALL_DISABLED=true + fi +} + +check_gatekeeper() { + # Check Gatekeeper status + if command -v spctl > /dev/null 2>&1; then + local gk_status=$(spctl --status 2>/dev/null || echo "") + if echo "$gk_status" | grep -q "enabled"; then + echo -e " ${GREEN}✓${NC} Gatekeeper Active" + else + echo -e " ${YELLOW}⚠${NC} Gatekeeper ${YELLOW}Disabled${NC}" + echo -e " ${GRAY}Enable via System Settings → Privacy & Security, or:${NC}" + echo -e " ${GRAY}sudo spctl --master-enable${NC}" + fi + fi +} + +check_sip() { + # Check System Integrity Protection + if command -v csrutil > /dev/null 2>&1; then + local sip_status=$(csrutil status 2>/dev/null || echo "") + if echo "$sip_status" | grep -q "enabled"; then + echo -e " ${GREEN}✓${NC} SIP Enabled" + else + echo -e " ${YELLOW}⚠${NC} SIP ${YELLOW}Disabled${NC}" + echo -e " ${GRAY}Restart into Recovery → Utilities → Terminal → run: csrutil enable${NC}" + fi + fi +} + +check_all_security() { + check_filevault + check_firewall + check_gatekeeper + check_sip +} diff --git a/lib/check_updates.sh b/lib/check_updates.sh new file mode 100644 index 0000000..aa07ddc --- /dev/null +++ b/lib/check_updates.sh @@ -0,0 +1,273 @@ +#!/bin/bash + +# Check for software updates +# Sets global variables for use in suggestions + +# Cache configuration +CACHE_DIR="${HOME}/.cache/mole" +CACHE_TTL=600 # 10 minutes in seconds + +# Ensure cache directory exists +mkdir -p "$CACHE_DIR" 2>/dev/null || true + +clear_cache_file() { + local file="$1" + rm -f "$file" 2>/dev/null || true +} + +reset_brew_cache() { + clear_cache_file "$CACHE_DIR/brew_updates" +} + +reset_softwareupdate_cache() { + clear_cache_file "$CACHE_DIR/softwareupdate_list" + SOFTWARE_UPDATE_LIST="" +} + +reset_mole_cache() { + clear_cache_file "$CACHE_DIR/mole_version" +} + +# Check if cache is still valid +is_cache_valid() { + local cache_file="$1" + local ttl="${2:-$CACHE_TTL}" + + if [[ ! -f "$cache_file" ]]; then + return 1 + fi + + local cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || echo 0))) + [[ $cache_age -lt $ttl ]] +} + +check_homebrew_updates() { + if ! command -v brew > /dev/null 2>&1; then + return + fi + + local cache_file="$CACHE_DIR/brew_updates" + local formula_count=0 + local cask_count=0 + + if is_cache_valid "$cache_file"; then + read -r formula_count cask_count < "$cache_file" 2>/dev/null || true + formula_count=${formula_count:-0} + cask_count=${cask_count:-0} + else + # Show spinner while checking + if [[ -t 1 ]]; then + start_inline_spinner "Checking Homebrew..." + fi + + local outdated_list="" + outdated_list=$(brew outdated --quiet 2>/dev/null || echo "") + if [[ -n "$outdated_list" ]]; then + formula_count=$(echo "$outdated_list" | wc -l | tr -d ' ') + fi + + local cask_list="" + cask_list=$(brew outdated --cask --quiet 2>/dev/null || echo "") + if [[ -n "$cask_list" ]]; then + cask_count=$(echo "$cask_list" | wc -l | tr -d ' ') + fi + + echo "$formula_count $cask_count" > "$cache_file" 2>/dev/null || true + + # Stop spinner before output + if [[ -t 1 ]]; then + stop_inline_spinner + fi + fi + + local total_count=$((formula_count + cask_count)) + export BREW_FORMULA_OUTDATED_COUNT=$formula_count + export BREW_CASK_OUTDATED_COUNT=$cask_count + export BREW_OUTDATED_COUNT=$total_count + + if [[ $total_count -gt 0 ]]; then + local breakdown="" + if [[ $formula_count -gt 0 && $cask_count -gt 0 ]]; then + breakdown=" (${formula_count} formula, ${cask_count} cask)" + elif [[ $formula_count -gt 0 ]]; then + breakdown=" (${formula_count} formula)" + elif [[ $cask_count -gt 0 ]]; then + breakdown=" (${cask_count} cask)" + fi + echo -e " ${YELLOW}⚠${NC} Homebrew ${YELLOW}${total_count} updates${NC}${breakdown}" + echo -e " ${GRAY}Run: ${GREEN}brew upgrade${NC} ${GRAY}and/or${NC} ${GREEN}brew upgrade --cask${NC}" + else + echo -e " ${GREEN}✓${NC} Homebrew Up to date" + fi +} + +# Cache software update list to avoid calling softwareupdate twice +SOFTWARE_UPDATE_LIST="" + +get_software_updates() { + local cache_file="$CACHE_DIR/softwareupdate_list" + + if [[ -z "$SOFTWARE_UPDATE_LIST" ]]; then + # Check cache first + if is_cache_valid "$cache_file"; then + SOFTWARE_UPDATE_LIST=$(cat "$cache_file" 2>/dev/null || echo "") + else + # Show spinner while checking (only on first call) + local show_spinner=false + if [[ -t 1 && -z "${SOFTWAREUPDATE_SPINNER_SHOWN:-}" ]]; then + start_inline_spinner "Checking system updates..." + show_spinner=true + export SOFTWAREUPDATE_SPINNER_SHOWN="true" + fi + + SOFTWARE_UPDATE_LIST=$(softwareupdate -l 2>/dev/null || echo "") + # Save to cache + echo "$SOFTWARE_UPDATE_LIST" > "$cache_file" 2>/dev/null || true + + # Stop spinner + if [[ "$show_spinner" == "true" ]]; then + stop_inline_spinner + fi + fi + fi + echo "$SOFTWARE_UPDATE_LIST" +} + +check_appstore_updates() { + local update_list="" + update_list=$(get_software_updates | grep -v "Software Update Tool" | grep "^\*" | grep -vi "macOS" || echo "") + + local update_count=0 + if [[ -n "$update_list" ]]; then + update_count=$(echo "$update_list" | wc -l | tr -d ' ') + fi + + export APPSTORE_UPDATE_COUNT=$update_count + + if [[ $update_count -gt 0 ]]; then + echo -e " ${YELLOW}⚠${NC} App Store ${YELLOW}${update_count} apps${NC} need update" + echo -e " ${GRAY}Run: ${GREEN}softwareupdate -i