#!/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 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 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}${ICON_WARNING}${NC} Touch ID ${YELLOW}Not configured${NC} for sudo" 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) 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}${ICON_WARNING}${NC} Rosetta 2 ${YELLOW}Not installed${NC}" export ROSETTA_NOT_INSTALLED=true 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 Config Configured" else echo -e " ${YELLOW}${ICON_WARNING}${NC} Git Config ${YELLOW}Not configured${NC}" fi fi } check_all_config() { 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 Enabled" else echo -e " ${RED}✗${NC} FileVault ${RED}Disabled${NC} (Recommend enabling)" export FILEVAULT_DISABLED=true fi fi } check_firewall() { # Check whitelist if command -v is_whitelisted > /dev/null && is_whitelisted "firewall"; then return; fi # Check firewall status unset FIREWALL_DISABLED 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}${ICON_WARNING}${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 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 Active" unset GATEKEEPER_DISABLED else echo -e " ${YELLOW}${ICON_WARNING}${NC} Gatekeeper ${YELLOW}Disabled${NC}" echo -e " ${GRAY}Enable via System Settings → Privacy & Security, or:${NC}" echo -e " ${GRAY}sudo spctl --master-enable${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 Enabled" else echo -e " ${YELLOW}${ICON_WARNING}${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 } # ============================================================================ # Software Update Checks # ============================================================================ # 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) - $(get_file_mtime "$cache_file"))) [[ $cache_age -lt $ttl ]] } check_homebrew_updates() { # Check whitelist if command -v is_whitelisted > /dev/null && is_whitelisted "check_brew_updates"; then return; fi 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}${ICON_WARNING}${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 (querying Apple servers)..." 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 spinner_started=false if [[ -t 1 ]]; then printf " Checking App Store updates...\r" start_inline_spinner "Checking App Store updates (querying Apple servers)..." spinner_started=true export SOFTWAREUPDATE_SPINNER_SHOWN="external" else echo "Checking App Store updates..." fi local update_list="" update_list=$(get_software_updates | grep -v "Software Update Tool" | grep "^\*" | grep -vi "macOS" || echo "") if [[ "$spinner_started" == "true" ]]; then stop_inline_spinner unset SOFTWAREUPDATE_SPINNER_SHOWN fi 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}${ICON_WARNING}${NC} App Store ${YELLOW}${update_count} apps${NC} need update" echo -e " ${GRAY}updates available in final step${NC}" else echo -e " ${GREEN}✓${NC} App Store Up to date" fi } check_macos_update() { # Check whitelist if command -v is_whitelisted > /dev/null && is_whitelisted "check_macos_updates"; then return; fi local spinner_started=false if [[ -t 1 ]]; then printf " Checking macOS updates...\r" start_inline_spinner "Checking macOS updates (querying Apple servers)..." spinner_started=true export SOFTWAREUPDATE_SPINNER_SHOWN="external" else echo "Checking macOS updates..." fi # Check for macOS system update using cached list local macos_update="" macos_update=$(get_software_updates | grep -i "macOS" | head -1 || echo "") if [[ "$spinner_started" == "true" ]]; then stop_inline_spinner unset SOFTWAREUPDATE_SPINNER_SHOWN fi export MACOS_UPDATE_AVAILABLE="false" if [[ -n "$macos_update" ]]; then export MACOS_UPDATE_AVAILABLE="true" local version=$(echo "$macos_update" | grep -o '[0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?' | head -1) if [[ -n "$version" ]]; then echo -e " ${YELLOW}${ICON_WARNING}${NC} macOS ${YELLOW}${version} available${NC}" else echo -e " ${YELLOW}${ICON_WARNING}${NC} macOS ${YELLOW}Update available${NC}" fi echo -e " ${GRAY}update available in final step${NC}" else echo -e " ${GREEN}✓${NC} macOS Up to date" fi } check_mole_update() { # 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 start_inline_spinner "Checking Mole version..." fi # Try to get latest version from GitHub if command -v curl > /dev/null 2>&1; then latest_version=$(curl -fsSL https://api.github.com/repos/tw93/mole/releases/latest 2> /dev/null | grep '"tag_name"' | sed -E 's/.*"v?([^"]+)".*/\1/' || echo "") # Save to cache if [[ -n "$latest_version" ]]; then echo "$latest_version" > "$cache_file" 2> /dev/null || true fi 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" echo -e " ${YELLOW}${ICON_WARNING}${NC} Mole ${YELLOW}${latest_version} available${NC} (current: ${current_version})" echo -e " ${GRAY}Run: ${GREEN}mo update${NC}" else echo -e " ${GREEN}✓${NC} Mole Up to date (${current_version})" fi else echo -e " ${GREEN}✓${NC} Mole Up to date (${current_version})" fi } check_all_updates() { # Reset spinner flag for softwareupdate unset SOFTWAREUPDATE_SPINNER_SHOWN check_homebrew_updates # Preload software update data to avoid delays between subsequent checks # Only redirect stdout, keep stderr for spinner display get_software_updates > /dev/null 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 (index(lower, "macos") == 0) { print label } } ' } get_macos_update_labels() { get_software_updates | awk ' /^\*/ { label=$0 sub(/^[[:space:]]*\* Label: */, "", label) sub(/,.*/, "", label) lower=tolower(label) if (index(lower, "macos") != 0) { print label } } ' } # ============================================================================ # System Health Checks # ============================================================================ check_disk_space() { local free_gb=$(command 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}${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 " ${YELLOW}${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 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}${ICON_WARNING}${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_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 " ${YELLOW}${ICON_WARNING}${NC} Cache Size ${YELLOW}${cache_size_gb}GB${NC} cleanable" elif [[ $cache_size_int -gt 5 ]]; then echo -e " ${YELLOW}${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 '{print $3}' || echo "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 " ${YELLOW}${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 Homebrew status (fast) if command -v brew > /dev/null 2>&1; then # Skip slow 'brew doctor' check by default echo -e " ${GREEN}✓${NC} Homebrew Installed" 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 }