#!/bin/bash # System Checks Module # Combines configuration, security, updates, and health checks set -euo pipefail # ============================================================================ # Helper Functions # ============================================================================ list_login_items() { if ! command -v osascript > /dev/null 2>&1; then return fi # Skip AppleScript during tests to avoid permission dialogs if [[ "${MOLE_TEST_MODE:-0}" == "1" || "${MOLE_TEST_NO_AUTH:-0}" == "1" ]]; then return fi 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 } # ============================================================================ # Configuration Checks # ============================================================================ check_touchid_sudo() { # Check whitelist if command -v is_whitelisted > /dev/null && is_whitelisted "check_touchid"; then return; fi # 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 Biometric authentication enabled" 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 " ${GRAY}${ICON_WARNING}${NC} Touch ID ${YELLOW}Not configured for sudo${NC}" export TOUCHID_NOT_CONFIGURED=true fi fi } check_rosetta() { # Check whitelist if command -v is_whitelisted > /dev/null && is_whitelisted "check_rosetta"; then return; fi # Check Rosetta 2 (for Apple Silicon Macs) - informational only, not auto-fixed if [[ "$(uname -m)" == "arm64" ]]; then if [[ -f "/Library/Apple/usr/share/rosetta/rosetta" ]]; then echo -e " ${GREEN}✓${NC} Rosetta 2 Intel app translation ready" else echo -e " ${GRAY}${ICON_EMPTY}${NC} Rosetta 2 ${GRAY}Not installed${NC}" fi fi } check_git_config() { # Check whitelist if command -v is_whitelisted > /dev/null && is_whitelisted "check_git_config"; then return; fi # 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 Global identity configured" else echo -e " ${GRAY}${ICON_WARNING}${NC} Git ${YELLOW}User identity not set${NC}" fi fi } check_all_config() { echo -e "${BLUE}${ICON_ARROW}${NC} System Configuration" check_touchid_sudo check_rosetta check_git_config } # ============================================================================ # Security Checks # ============================================================================ check_filevault() { # Check whitelist if command -v is_whitelisted > /dev/null && is_whitelisted "check_filevault"; then return; fi # 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 Disk encryption active" else echo -e " ${RED}✗${NC} FileVault ${RED}Disk encryption disabled${NC}" export FILEVAULT_DISABLED=true fi fi } check_firewall() { # Check whitelist if command -v is_whitelisted > /dev/null && is_whitelisted "firewall"; then return; fi unset FIREWALL_DISABLED # Check third-party firewalls first (lightweight path-based detection, no sudo required) local third_party_firewall="" if [[ -d "/Applications/Little Snitch.app" ]] || [[ -d "/Library/Little Snitch" ]]; then third_party_firewall="Little Snitch" elif [[ -d "/Applications/LuLu.app" ]]; then third_party_firewall="LuLu" elif [[ -d "/Applications/Radio Silence.app" ]]; then third_party_firewall="Radio Silence" elif [[ -d "/Applications/Hands Off!.app" ]]; then third_party_firewall="Hands Off!" elif [[ -d "/Applications/Murus.app" ]]; then third_party_firewall="Murus" elif [[ -d "/Applications/Vallum.app" ]]; then third_party_firewall="Vallum" fi if [[ -n "$third_party_firewall" ]]; then echo -e " ${GREEN}✓${NC} Firewall ${third_party_firewall} active" return fi # Fall back to macOS built-in firewall check local firewall_output=$(sudo /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2> /dev/null || echo "") if [[ "$firewall_output" == *"State = 1"* ]] || [[ "$firewall_output" == *"State = 2"* ]]; then echo -e " ${GREEN}✓${NC} Firewall Network protection enabled" else echo -e " ${GRAY}${ICON_WARNING}${NC} Firewall ${YELLOW}Network protection disabled${NC}" export FIREWALL_DISABLED=true fi } check_gatekeeper() { # Check whitelist if command -v is_whitelisted > /dev/null && is_whitelisted "gatekeeper"; then return; fi # 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 App download protection active" unset GATEKEEPER_DISABLED else echo -e " ${GRAY}${ICON_WARNING}${NC} Gatekeeper ${YELLOW}App security disabled${NC}" export GATEKEEPER_DISABLED=true fi fi } check_sip() { # Check whitelist if command -v is_whitelisted > /dev/null && is_whitelisted "check_sip"; then return; fi # 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 System integrity protected" else echo -e " ${GRAY}${ICON_WARNING}${NC} SIP ${YELLOW}System protection disabled${NC}" fi fi } check_all_security() { echo -e "${BLUE}${ICON_ARROW}${NC} Security Status" check_filevault check_firewall check_gatekeeper check_sip } # ============================================================================ # Software Update Checks # ============================================================================ # Cache configuration CACHE_DIR="${HOME}/.cache/mole" CACHE_TTL=600 # 10 minutes in seconds # Ensure cache directory exists ensure_user_dir "$CACHE_DIR" 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="" SOFTWARE_UPDATE_LIST_LOADED="false" } 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=$(($(get_epoch_seconds) - $(get_file_mtime "$cache_file"))) [[ $cache_age -lt $ttl ]] } # Cache software update list to avoid calling softwareupdate twice SOFTWARE_UPDATE_LIST="" SOFTWARE_UPDATE_LIST_LOADED="false" software_update_has_entries() { printf '%s\n' "$1" | grep -qE '^[[:space:]]*\* Label:' } is_macos_software_update_text() { local text text=$(printf '%s' "$1" | LC_ALL=C tr '[:upper:]' '[:lower:]') case "$text" in *macos* | *background\ security\ improvement* | *rapid\ security\ response* | *security\ response*) return 0 ;; esac return 1 } get_first_macos_software_update_summary() { printf '%s\n' "$1" | awk ' /^\* Label:/ { label=$0 sub(/^[[:space:]]*\* Label: */, "", label) next } /^[[:space:]]*Title:/ { title=$0 sub(/^[[:space:]]*Title: */, "", title) sub(/, Size:.*/, "", title) combined=tolower(label " " title) if (combined ~ /macos|background security improvement|rapid security response|security response/) { print title exit } } ' } get_software_updates() { local cache_file="$CACHE_DIR/softwareupdate_list" if [[ "${SOFTWARE_UPDATE_LIST_LOADED:-false}" == "true" ]]; then printf '%s\n' "$SOFTWARE_UPDATE_LIST" return 0 fi if is_cache_valid "$cache_file"; then SOFTWARE_UPDATE_LIST=$(cat "$cache_file" 2> /dev/null || true) SOFTWARE_UPDATE_LIST_LOADED="true" printf '%s\n' "$SOFTWARE_UPDATE_LIST" return 0 fi local spinner_started=false if [[ -t 1 && -z "${SOFTWAREUPDATE_SPINNER_SHOWN:-}" ]]; then MOLE_SPINNER_PREFIX=" " start_inline_spinner "Checking system updates..." spinner_started=true export SOFTWAREUPDATE_SPINNER_SHOWN=1 fi local output="" local sw_status=0 if output=$(run_with_timeout 10 softwareupdate -l --no-scan 2> /dev/null); then SOFTWARE_UPDATE_LIST="$output" ensure_user_file "$cache_file" printf '%s' "$SOFTWARE_UPDATE_LIST" > "$cache_file" 2> /dev/null || true else sw_status=$? SOFTWARE_UPDATE_LIST="" if [[ -f "$cache_file" ]]; then SOFTWARE_UPDATE_LIST=$(cat "$cache_file" 2> /dev/null || true) fi if [[ -n "${MO_DEBUG:-}" ]]; then echo "[DEBUG] softwareupdate preload exit status: $sw_status" >&2 fi fi if [[ "$spinner_started" == "true" ]]; then stop_inline_spinner fi SOFTWARE_UPDATE_LIST_LOADED="true" printf '%s\n' "$SOFTWARE_UPDATE_LIST" } check_homebrew_updates() { # Check whitelist if command -v is_whitelisted > /dev/null && is_whitelisted "check_homebrew_updates"; then return; fi export BREW_OUTDATED_COUNT=0 export BREW_FORMULA_OUTDATED_COUNT=0 export BREW_CASK_OUTDATED_COUNT=0 if ! command -v brew > /dev/null 2>&1; then printf " ${GRAY}${ICON_EMPTY}${NC} %-12s %s\n" "Homebrew" "Not installed" return fi local cache_file="$CACHE_DIR/brew_updates" local formula_count=0 local cask_count=0 local total_count=0 local use_cache=false if is_cache_valid "$cache_file"; then local cached_formula="" local cached_cask="" IFS=' ' read -r cached_formula cached_cask < "$cache_file" || true if [[ "$cached_formula" =~ ^[0-9]+$ && "$cached_cask" =~ ^[0-9]+$ ]]; then formula_count="$cached_formula" cask_count="$cached_cask" use_cache=true fi fi if [[ "$use_cache" == "false" ]]; then local formula_outdated="" local cask_outdated="" local formula_status=0 local cask_status=0 local spinner_started=false if [[ -t 1 ]]; then MOLE_SPINNER_PREFIX=" " start_inline_spinner "Checking Homebrew updates..." spinner_started=true fi local _brew_formula_tmp _brew_cask_tmp _brew_formula_tmp=$(mktemp_file "brew_formula") _brew_cask_tmp=$(mktemp_file "brew_cask") ( run_with_timeout 8 brew outdated --formula --quiet > "$_brew_formula_tmp" 2> /dev/null echo $? > "${_brew_formula_tmp}.status" ) & local _formula_pid=$! ( run_with_timeout 8 brew outdated --cask --quiet > "$_brew_cask_tmp" 2> /dev/null echo $? > "${_brew_cask_tmp}.status" ) & local _cask_pid=$! wait "$_formula_pid" 2> /dev/null || true wait "$_cask_pid" 2> /dev/null || true formula_outdated=$(cat "$_brew_formula_tmp" 2> /dev/null || true) cask_outdated=$(cat "$_brew_cask_tmp" 2> /dev/null || true) formula_status=$(cat "${_brew_formula_tmp}.status" 2> /dev/null || echo "1") cask_status=$(cat "${_brew_cask_tmp}.status" 2> /dev/null || echo "1") rm -f "$_brew_formula_tmp" "$_brew_cask_tmp" "${_brew_formula_tmp}.status" "${_brew_cask_tmp}.status" 2> /dev/null || true if [[ "$spinner_started" == "true" ]]; then stop_inline_spinner fi if [[ $formula_status -eq 0 || $cask_status -eq 0 ]]; then formula_count=$(printf '%s\n' "$formula_outdated" | awk 'NF {count++} END {print count + 0}') cask_count=$(printf '%s\n' "$cask_outdated" | awk 'NF {count++} END {print count + 0}') # Only cache when both calls succeeded; partial results (one side failed) # must not be written as zeros — next run should retry the failed side. if [[ $formula_status -eq 0 && $cask_status -eq 0 ]]; then ensure_user_file "$cache_file" printf '%s %s\n' "$formula_count" "$cask_count" > "$cache_file" 2> /dev/null || true fi elif [[ $formula_status -eq 124 || $cask_status -eq 124 ]]; then printf " ${GRAY}${ICON_WARNING}${NC} %-12s ${YELLOW}%s${NC}\n" "Homebrew" "Check timed out" return else printf " ${GRAY}${ICON_WARNING}${NC} %-12s ${YELLOW}%s${NC}\n" "Homebrew" "Check failed" return fi fi 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 detail="" if [[ $formula_count -gt 0 ]]; then detail="${formula_count} formula" fi if [[ $cask_count -gt 0 ]]; then [[ -n "$detail" ]] && detail="${detail}, " detail="${detail}${cask_count} cask" fi [[ -z "$detail" ]] && detail="${total_count} updates" printf " ${GRAY}%s${NC} %-12s ${YELLOW}%s${NC}\n" "$ICON_WARNING" "Homebrew" "${detail} available" else printf " ${GREEN}✓${NC} %-12s %s\n" "Homebrew" "Up to date" fi } check_appstore_updates() { # Skipped for speed optimization - consolidated into check_macos_update # We can't easily distinguish app store vs macos updates without the slow softwareupdate -l call export APPSTORE_UPDATE_COUNT=0 } check_macos_update() { # Check whitelist if command -v is_whitelisted > /dev/null && is_whitelisted "check_macos_updates"; then return; fi local updates_available="false" local macos_update_summary="" local sw_output="" sw_output=$(get_software_updates) if [[ -n "${MO_DEBUG:-}" ]]; then echo "[DEBUG] softwareupdate cached output lines: $(printf '%s\n' "$sw_output" | wc -l | tr -d ' ')" >&2 fi if software_update_has_entries "$sw_output"; then macos_update_summary=$(get_first_macos_software_update_summary "$sw_output") if [[ -n "$macos_update_summary" ]] || is_macos_software_update_text "$sw_output"; then updates_available="true" fi fi export MACOS_UPDATE_AVAILABLE="$updates_available" if [[ "$updates_available" == "true" ]]; then if [[ -n "$macos_update_summary" ]]; then printf " ${GRAY}%s${NC} %-12s ${YELLOW}%s${NC}\n" "$ICON_WARNING" "macOS" "$macos_update_summary" else printf " ${GRAY}%s${NC} %-12s ${YELLOW}%s${NC}\n" "$ICON_WARNING" "macOS" "Update available" fi else printf " ${GREEN}✓${NC} %-12s %s\n" "macOS" "System up to date" fi } check_mole_update() { if command -v is_whitelisted > /dev/null && is_whitelisted "check_mole_update"; then return; fi # Check if Mole has updates # Auto-detect version from mole main script local current_version if [[ -f "${SCRIPT_DIR:-/usr/local/bin}/mole" ]]; then current_version=$(grep '^VERSION=' "${SCRIPT_DIR:-/usr/local/bin}/mole" 2> /dev/null | head -1 | sed 's/VERSION="\(.*\)"/\1/' || echo "unknown") else current_version="${VERSION:-unknown}" fi local latest_version="" local cache_file="$CACHE_DIR/mole_version" export MOLE_UPDATE_AVAILABLE="false" # Check cache first if is_cache_valid "$cache_file"; then latest_version=$(cat "$cache_file" 2> /dev/null || echo "") else # Show spinner while checking if [[ -t 1 ]]; then MOLE_SPINNER_PREFIX=" " start_inline_spinner "Checking Mole version..." fi # Try to get latest version from GitHub if command -v curl > /dev/null 2>&1; then # Run in background to allow Ctrl+C to interrupt local temp_version temp_version=$(mktemp_file "mole_version_check") curl -fsSL --connect-timeout 3 --max-time 5 https://api.github.com/repos/tw93/mole/releases/latest 2> /dev/null | grep '"tag_name"' | sed -E 's/.*"v?([^"]+)".*/\1/' > "$temp_version" & local curl_pid=$! # Wait for curl to complete (allows Ctrl+C to interrupt) if wait "$curl_pid" 2> /dev/null; then latest_version=$(cat "$temp_version" 2> /dev/null || echo "") # Save to cache if [[ -n "$latest_version" ]]; then ensure_user_file "$cache_file" echo "$latest_version" > "$cache_file" 2> /dev/null || true fi fi rm -f "$temp_version" 2> /dev/null || true fi # Stop spinner if [[ -t 1 ]]; then stop_inline_spinner fi fi # Normalize version strings (remove leading 'v' or 'V') current_version="${current_version#v}" current_version="${current_version#V}" latest_version="${latest_version#v}" latest_version="${latest_version#V}" if [[ -n "$latest_version" && "$current_version" != "$latest_version" ]]; then # Compare versions if [[ "$(printf '%s\n' "$current_version" "$latest_version" | sort -V | head -1)" == "$current_version" ]]; then export MOLE_UPDATE_AVAILABLE="true" printf " ${GRAY}%s${NC} %-12s ${YELLOW}%s${NC}, running %s\n" "$ICON_WARNING" "Mole" "${latest_version} available" "${current_version}" else printf " ${GREEN}✓${NC} %-12s %s\n" "Mole" "Latest version ${current_version}" fi else printf " ${GREEN}✓${NC} %-12s %s\n" "Mole" "Latest version ${current_version}" fi } check_all_updates() { # Reset spinner flag for softwareupdate unset SOFTWAREUPDATE_SPINNER_SHOWN # Preload software update data to avoid delays between subsequent checks # Only redirect stdout, keep stderr for spinner display get_software_updates > /dev/null echo -e "${BLUE}${ICON_ARROW}${NC} System Updates" check_homebrew_updates check_appstore_updates check_macos_update check_mole_update } get_appstore_update_labels() { get_software_updates | awk ' /^\*/ { label=$0 sub(/^[[:space:]]*\* Label: */, "", label) sub(/,.*/, "", label) lower=tolower(label) if (lower !~ /macos|background security improvement|rapid security response|security response/) { print label } } ' } get_macos_update_labels() { get_software_updates | awk ' /^\*/ { label=$0 sub(/^[[:space:]]*\* Label: */, "", label) sub(/,.*/, "", label) lower=tolower(label) if (lower ~ /macos|background security improvement|rapid security response|security response/) { print label } } ' } # ============================================================================ # System Health Checks # ============================================================================ check_disk_space() { # Use df -k to get KB values (always numeric), then calculate GB via math # This avoids unit suffix parsing issues (df -H can return MB or GB) local free_kb=$(command df -k / | awk 'NR==2 {print $4}') local free_gb=$(awk "BEGIN {printf \"%.1f\", $free_kb / 1048576}") local free_num=$(awk "BEGIN {printf \"%d\", $free_kb / 1048576}") 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 " ${GRAY}${ICON_WARNING}${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 " ${GRAY}${ICON_WARNING}${NC} Memory ${YELLOW}${used_percent}% used${NC}, High" else echo -e " ${GREEN}✓${NC} Memory ${used_percent}% used" fi } check_login_items() { # Check whitelist if command -v is_whitelisted > /dev/null && is_whitelisted "check_login_items"; then return; fi local login_items_count=0 local -a login_items_list=() if [[ -t 0 ]]; then # Show spinner while getting login items if [[ -t 1 ]]; then MOLE_SPINNER_PREFIX=" " 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 " ${GRAY}${ICON_WARNING}${NC} Login Items ${YELLOW}${login_items_count} apps${NC}" elif [[ $login_items_count -gt 0 ]]; then echo -e " ${GREEN}✓${NC} Login Items ${login_items_count} apps" else echo -e " ${GREEN}✓${NC} Login Items None" return fi # Show items in a single line (compact) local preview_limit=3 ((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} +${remaining}" fi echo -e " ${GRAY}${items_display}${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 MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning cache..." fi for cache_path in "${cache_paths[@]}"; do if [[ -d "$cache_path" ]]; then local size_output size_output=$(get_path_size_kb "$cache_path") [[ "$size_output" =~ ^[0-9]+$ ]] || size_output=0 cache_size_kb=$((cache_size_kb + size_output)) 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 " ${GRAY}${ICON_WARNING}${NC} Cache Size ${YELLOW}${cache_size_gb}GB${NC} cleanable" elif [[ $cache_size_int -gt 5 ]]; then echo -e " ${GRAY}${ICON_WARNING}${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 'NR==1{print $3}') swap_used=${swap_used:-0M} local swap_num="${swap_used//[GM]/}" if [[ "$swap_used" == *"G"* ]]; then local swap_gb=${swap_num%.*} if [[ $swap_gb -gt 2 ]]; then echo -e " ${GRAY}${ICON_WARNING}${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_brew_health() { # Check whitelist if command -v is_whitelisted > /dev/null && is_whitelisted "check_brew_health"; then return; fi } check_system_health() { echo -e "${BLUE}${ICON_ARROW}${NC} 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 }