mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 22:39:42 +00:00
Reconstruct clean lib code
This commit is contained in:
656
lib/check/all.sh
Normal file
656
lib/check/all.sh
Normal file
@@ -0,0 +1,656 @@
|
||||
#!/bin/bash
|
||||
# System Checks Module
|
||||
# Combines configuration, security, updates, and health checks
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ============================================================================
|
||||
# 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}${ICON_WARNING}${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}${ICON_WARNING}${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}${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 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
|
||||
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 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 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() {
|
||||
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..."
|
||||
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..."
|
||||
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}Run: ${GREEN}softwareupdate -i <label>${NC}"
|
||||
else
|
||||
echo -e " ${GREEN}✓${NC} App Store Up to date"
|
||||
fi
|
||||
}
|
||||
|
||||
check_macos_update() {
|
||||
local spinner_started=false
|
||||
if [[ -t 1 ]]; then
|
||||
printf " Checking macOS updates...\r"
|
||||
start_inline_spinner "Checking macOS updates..."
|
||||
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}Run: ${GREEN}softwareupdate -i <label>${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=$(echo "$current_version" | sed 's/^[vV]//')
|
||||
latest_version=$(echo "$latest_version" | sed 's/^[vV]//')
|
||||
|
||||
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
|
||||
get_software_updates > /dev/null 2>&1
|
||||
|
||||
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=$(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() {
|
||||
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=$(du -sk "$cache_path" 2> /dev/null | awk 'NR==1 {print $1}' | tr -d '[:space:]' || echo "")
|
||||
[[ "$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=$(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}${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 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}${ICON_WARNING}${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
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/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}${ICON_WARNING}${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}${ICON_WARNING}${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}${ICON_WARNING}${NC} Git Config ${YELLOW}Not configured${NC}"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_all_config() {
|
||||
check_touchid_sudo
|
||||
check_rosetta
|
||||
check_git_config
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
#!/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}${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() {
|
||||
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=$(du -sk "$cache_path" 2> /dev/null | awk 'NR==1 {print $1}' | tr -d '[:space:]' || echo "")
|
||||
[[ "$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=$(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}${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 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}${ICON_WARNING}${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
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
#!/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
|
||||
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 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 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
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
#!/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) - $(get_file_mtime "$cache_file")))
|
||||
[[ $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}${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..."
|
||||
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..."
|
||||
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}Run: ${GREEN}softwareupdate -i <label>${NC}"
|
||||
else
|
||||
echo -e " ${GREEN}✓${NC} App Store Up to date"
|
||||
fi
|
||||
}
|
||||
|
||||
check_macos_update() {
|
||||
local spinner_started=false
|
||||
if [[ -t 1 ]]; then
|
||||
printf " Checking macOS updates...\r"
|
||||
start_inline_spinner "Checking macOS updates..."
|
||||
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}Run: ${GREEN}softwareupdate -i <label>${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=$(echo "$current_version" | sed 's/^[vV]//')
|
||||
latest_version=$(echo "$latest_version" | sed 's/^[vV]//')
|
||||
|
||||
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
|
||||
get_software_updates > /dev/null 2>&1
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
'
|
||||
}
|
||||
@@ -6,13 +6,13 @@ set -euo pipefail
|
||||
|
||||
# Get script directory and source dependencies
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
source "$SCRIPT_DIR/menu_simple.sh"
|
||||
source "$SCRIPT_DIR/../core/common.sh"
|
||||
source "$SCRIPT_DIR/../ui/menu_simple.sh"
|
||||
|
||||
# Config file path
|
||||
WHITELIST_CONFIG="$HOME/.config/mole/whitelist"
|
||||
|
||||
# Default whitelist patterns defined in lib/common.sh:
|
||||
# Default whitelist patterns defined in lib/core/common.sh:
|
||||
# - DEFAULT_WHITELIST_PATTERNS
|
||||
# - FINDER_METADATA_SENTINEL
|
||||
|
||||
@@ -417,3 +417,282 @@ execute_optimization() {
|
||||
;;
|
||||
esac
|
||||
}
|
||||
#!/bin/bash
|
||||
# System Health Check - Pure Bash Implementation
|
||||
# Replaces optimize-go
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Get memory info in GB
|
||||
get_memory_info() {
|
||||
local total_bytes used_gb total_gb
|
||||
|
||||
# Total memory
|
||||
total_bytes=$(sysctl -n hw.memsize 2> /dev/null || echo "0")
|
||||
total_gb=$(awk "BEGIN {printf \"%.2f\", $total_bytes / (1024*1024*1024)}" 2> /dev/null || echo "0")
|
||||
[[ -z "$total_gb" || "$total_gb" == "" ]] && total_gb="0"
|
||||
|
||||
# Used memory from vm_stat
|
||||
local vm_output active wired compressed page_size
|
||||
vm_output=$(vm_stat 2> /dev/null || echo "")
|
||||
page_size=4096
|
||||
|
||||
active=$(echo "$vm_output" | awk '/Pages active:/ {print $NF}' | tr -d '.' 2> /dev/null || echo "0")
|
||||
wired=$(echo "$vm_output" | awk '/Pages wired down:/ {print $NF}' | tr -d '.' 2> /dev/null || echo "0")
|
||||
compressed=$(echo "$vm_output" | awk '/Pages occupied by compressor:/ {print $NF}' | tr -d '.' 2> /dev/null || echo "0")
|
||||
|
||||
active=${active:-0}
|
||||
wired=${wired:-0}
|
||||
compressed=${compressed:-0}
|
||||
|
||||
local used_bytes=$(((active + wired + compressed) * page_size))
|
||||
used_gb=$(awk "BEGIN {printf \"%.2f\", $used_bytes / (1024*1024*1024)}" 2> /dev/null || echo "0")
|
||||
[[ -z "$used_gb" || "$used_gb" == "" ]] && used_gb="0"
|
||||
|
||||
echo "$used_gb $total_gb"
|
||||
}
|
||||
|
||||
# Get disk info
|
||||
get_disk_info() {
|
||||
local home="${HOME:-/}"
|
||||
local df_output total_gb used_gb used_percent
|
||||
|
||||
df_output=$(df -k "$home" 2> /dev/null | tail -1)
|
||||
|
||||
local total_kb used_kb
|
||||
total_kb=$(echo "$df_output" | awk '{print $2}' 2> /dev/null || echo "0")
|
||||
used_kb=$(echo "$df_output" | awk '{print $3}' 2> /dev/null || echo "0")
|
||||
|
||||
total_kb=${total_kb:-0}
|
||||
used_kb=${used_kb:-0}
|
||||
[[ "$total_kb" == "0" ]] && total_kb=1 # Avoid division by zero
|
||||
|
||||
total_gb=$(awk "BEGIN {printf \"%.2f\", $total_kb / (1024*1024)}" 2> /dev/null || echo "0")
|
||||
used_gb=$(awk "BEGIN {printf \"%.2f\", $used_kb / (1024*1024)}" 2> /dev/null || echo "0")
|
||||
used_percent=$(awk "BEGIN {printf \"%.1f\", ($used_kb / $total_kb) * 100}" 2> /dev/null || echo "0")
|
||||
|
||||
[[ -z "$total_gb" || "$total_gb" == "" ]] && total_gb="0"
|
||||
[[ -z "$used_gb" || "$used_gb" == "" ]] && used_gb="0"
|
||||
[[ -z "$used_percent" || "$used_percent" == "" ]] && used_percent="0"
|
||||
|
||||
echo "$used_gb $total_gb $used_percent"
|
||||
}
|
||||
|
||||
# Get uptime in days
|
||||
get_uptime_days() {
|
||||
local boot_output boot_time uptime_days
|
||||
|
||||
boot_output=$(sysctl -n kern.boottime 2> /dev/null || echo "")
|
||||
boot_time=$(echo "$boot_output" | sed -n 's/.*sec = \([0-9]*\).*/\1/p' 2> /dev/null || echo "")
|
||||
|
||||
if [[ -n "$boot_time" && "$boot_time" =~ ^[0-9]+$ ]]; then
|
||||
local now=$(date +%s 2> /dev/null || echo "0")
|
||||
local uptime_sec=$((now - boot_time))
|
||||
uptime_days=$(awk "BEGIN {printf \"%.1f\", $uptime_sec / 86400}" 2> /dev/null || echo "0")
|
||||
else
|
||||
uptime_days="0"
|
||||
fi
|
||||
|
||||
[[ -z "$uptime_days" || "$uptime_days" == "" ]] && uptime_days="0"
|
||||
echo "$uptime_days"
|
||||
}
|
||||
|
||||
# Get directory size in KB
|
||||
dir_size_kb() {
|
||||
local path="$1"
|
||||
[[ ! -e "$path" ]] && echo "0" && return
|
||||
du -sk "$path" 2> /dev/null | awk '{print $1}' || echo "0"
|
||||
}
|
||||
|
||||
# Format size from KB
|
||||
format_size_kb() {
|
||||
local kb="$1"
|
||||
[[ "$kb" -le 0 ]] && echo "0B" && return
|
||||
|
||||
local mb gb
|
||||
mb=$(awk "BEGIN {printf \"%.1f\", $kb / 1024}")
|
||||
gb=$(awk "BEGIN {printf \"%.2f\", $mb / 1024}")
|
||||
|
||||
if awk "BEGIN {exit !($gb >= 1)}"; then
|
||||
echo "${gb}GB"
|
||||
elif awk "BEGIN {exit !($mb >= 1)}"; then
|
||||
printf "%.0fMB\n" "$mb"
|
||||
else
|
||||
echo "${kb}KB"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check cache size
|
||||
check_cache_refresh() {
|
||||
local cache_dir="$HOME/Library/Caches"
|
||||
local size_kb=$(dir_size_kb "$cache_dir")
|
||||
local desc="Refresh Finder previews, Quick Look, and Safari caches"
|
||||
|
||||
if [[ $size_kb -gt 0 ]]; then
|
||||
local size_str=$(format_size_kb "$size_kb")
|
||||
desc="Refresh ${size_str} of Finder/Safari caches"
|
||||
fi
|
||||
|
||||
echo "cache_refresh|User Cache Refresh|${desc}|true"
|
||||
}
|
||||
|
||||
# Check Mail downloads
|
||||
check_mail_downloads() {
|
||||
local dirs=(
|
||||
"$HOME/Library/Mail Downloads"
|
||||
"$HOME/Library/Containers/com.apple.mail/Data/Library/Mail Downloads"
|
||||
)
|
||||
|
||||
local total_kb=0
|
||||
for dir in "${dirs[@]}"; do
|
||||
total_kb=$((total_kb + $(dir_size_kb "$dir")))
|
||||
done
|
||||
|
||||
if [[ $total_kb -gt 0 ]]; then
|
||||
local size_str=$(format_size_kb "$total_kb")
|
||||
echo "mail_downloads|Mail Downloads|Recover ${size_str} of Mail attachments|true"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check saved state
|
||||
check_saved_state() {
|
||||
local state_dir="$HOME/Library/Saved Application State"
|
||||
local size_kb=$(dir_size_kb "$state_dir")
|
||||
|
||||
if [[ $size_kb -gt 0 ]]; then
|
||||
local size_str=$(format_size_kb "$size_kb")
|
||||
echo "saved_state_cleanup|Saved State|Clear ${size_str} of stale saved states|true"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check swap files
|
||||
check_swap_cleanup() {
|
||||
local total_kb=0
|
||||
local file
|
||||
|
||||
for file in /private/var/vm/swapfile*; do
|
||||
[[ -f "$file" ]] && total_kb=$((total_kb + $(get_file_size "$file") / 1024))
|
||||
done
|
||||
|
||||
if [[ $total_kb -gt 0 ]]; then
|
||||
local size_str=$(format_size_kb "$total_kb")
|
||||
echo "swap_cleanup|Memory & Swap|Purge swap (${size_str}) & inactive memory|false"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check local snapshots
|
||||
check_local_snapshots() {
|
||||
command -v tmutil > /dev/null 2>&1 || return
|
||||
|
||||
local snapshots
|
||||
snapshots=$(tmutil listlocalsnapshots / 2> /dev/null || echo "")
|
||||
|
||||
local count
|
||||
count=$(echo "$snapshots" | grep -c "com.apple.TimeMachine" 2> /dev/null)
|
||||
count=$(echo "$count" | tr -d ' \n')
|
||||
count=${count:-0}
|
||||
[[ "$count" =~ ^[0-9]+$ ]] && [[ $count -gt 0 ]] && echo "local_snapshots|Local Snapshots|${count} APFS local snapshots detected|true"
|
||||
}
|
||||
|
||||
# Check developer cleanup
|
||||
check_developer_cleanup() {
|
||||
local dirs=(
|
||||
"$HOME/Library/Developer/Xcode/DerivedData"
|
||||
"$HOME/Library/Developer/Xcode/Archives"
|
||||
"$HOME/Library/Developer/Xcode/iOS DeviceSupport"
|
||||
"$HOME/Library/Developer/CoreSimulator/Caches"
|
||||
)
|
||||
|
||||
local total_kb=0
|
||||
for dir in "${dirs[@]}"; do
|
||||
total_kb=$((total_kb + $(dir_size_kb "$dir")))
|
||||
done
|
||||
|
||||
if [[ $total_kb -gt 0 ]]; then
|
||||
local size_str=$(format_size_kb "$total_kb")
|
||||
echo "developer_cleanup|Developer Cleanup|Recover ${size_str} of Xcode/simulator data|false"
|
||||
fi
|
||||
}
|
||||
|
||||
# Generate JSON output
|
||||
generate_health_json() {
|
||||
# System info
|
||||
read -r mem_used mem_total <<< "$(get_memory_info)"
|
||||
read -r disk_used disk_total disk_percent <<< "$(get_disk_info)"
|
||||
local uptime=$(get_uptime_days)
|
||||
|
||||
# Ensure all values are valid numbers (fallback to 0)
|
||||
mem_used=${mem_used:-0}
|
||||
mem_total=${mem_total:-0}
|
||||
disk_used=${disk_used:-0}
|
||||
disk_total=${disk_total:-0}
|
||||
disk_percent=${disk_percent:-0}
|
||||
uptime=${uptime:-0}
|
||||
|
||||
# Start JSON
|
||||
cat << EOF
|
||||
{
|
||||
"memory_used_gb": $mem_used,
|
||||
"memory_total_gb": $mem_total,
|
||||
"disk_used_gb": $disk_used,
|
||||
"disk_total_gb": $disk_total,
|
||||
"disk_used_percent": $disk_percent,
|
||||
"uptime_days": $uptime,
|
||||
"optimizations": [
|
||||
EOF
|
||||
|
||||
# Collect all optimization items
|
||||
local -a items=()
|
||||
|
||||
# Always-on items
|
||||
items+=('system_maintenance|System Maintenance|Rebuild system databases & flush caches|true')
|
||||
items+=('maintenance_scripts|Maintenance Scripts|Run daily/weekly/monthly scripts & rotate logs|true')
|
||||
items+=('radio_refresh|Bluetooth & Wi-Fi Refresh|Reset wireless preference caches|true')
|
||||
items+=('recent_items|Recent Items|Clear recent apps/documents/servers lists|true')
|
||||
items+=('log_cleanup|Diagnostics Cleanup|Purge old diagnostic & crash logs|true')
|
||||
items+=('startup_cache|Startup Cache Rebuild|Rebuild kext caches & prelinked kernel|true')
|
||||
|
||||
# Conditional items
|
||||
local item
|
||||
item=$(check_cache_refresh || true)
|
||||
[[ -n "$item" ]] && items+=("$item")
|
||||
item=$(check_mail_downloads || true)
|
||||
[[ -n "$item" ]] && items+=("$item")
|
||||
item=$(check_saved_state || true)
|
||||
[[ -n "$item" ]] && items+=("$item")
|
||||
item=$(check_swap_cleanup || true)
|
||||
[[ -n "$item" ]] && items+=("$item")
|
||||
item=$(check_local_snapshots || true)
|
||||
[[ -n "$item" ]] && items+=("$item")
|
||||
item=$(check_developer_cleanup || true)
|
||||
[[ -n "$item" ]] && items+=("$item")
|
||||
|
||||
# Output items as JSON
|
||||
local first=true
|
||||
for item in "${items[@]}"; do
|
||||
IFS='|' read -r action name desc safe <<< "$item"
|
||||
|
||||
[[ "$first" == "true" ]] && first=false || echo ","
|
||||
|
||||
cat << EOF
|
||||
{
|
||||
"category": "system",
|
||||
"name": "$name",
|
||||
"description": "$desc",
|
||||
"action": "$action",
|
||||
"safe": $safe
|
||||
}
|
||||
EOF
|
||||
done
|
||||
|
||||
# Close JSON
|
||||
cat << 'EOF'
|
||||
]
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Main execution
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
generate_health_json
|
||||
fi
|
||||
@@ -1,279 +0,0 @@
|
||||
#!/bin/bash
|
||||
# System Health Check - Pure Bash Implementation
|
||||
# Replaces optimize-go
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Get memory info in GB
|
||||
get_memory_info() {
|
||||
local total_bytes used_gb total_gb
|
||||
|
||||
# Total memory
|
||||
total_bytes=$(sysctl -n hw.memsize 2> /dev/null || echo "0")
|
||||
total_gb=$(awk "BEGIN {printf \"%.2f\", $total_bytes / (1024*1024*1024)}" 2> /dev/null || echo "0")
|
||||
[[ -z "$total_gb" || "$total_gb" == "" ]] && total_gb="0"
|
||||
|
||||
# Used memory from vm_stat
|
||||
local vm_output active wired compressed page_size
|
||||
vm_output=$(vm_stat 2> /dev/null || echo "")
|
||||
page_size=4096
|
||||
|
||||
active=$(echo "$vm_output" | awk '/Pages active:/ {print $NF}' | tr -d '.' 2> /dev/null || echo "0")
|
||||
wired=$(echo "$vm_output" | awk '/Pages wired down:/ {print $NF}' | tr -d '.' 2> /dev/null || echo "0")
|
||||
compressed=$(echo "$vm_output" | awk '/Pages occupied by compressor:/ {print $NF}' | tr -d '.' 2> /dev/null || echo "0")
|
||||
|
||||
active=${active:-0}
|
||||
wired=${wired:-0}
|
||||
compressed=${compressed:-0}
|
||||
|
||||
local used_bytes=$(((active + wired + compressed) * page_size))
|
||||
used_gb=$(awk "BEGIN {printf \"%.2f\", $used_bytes / (1024*1024*1024)}" 2> /dev/null || echo "0")
|
||||
[[ -z "$used_gb" || "$used_gb" == "" ]] && used_gb="0"
|
||||
|
||||
echo "$used_gb $total_gb"
|
||||
}
|
||||
|
||||
# Get disk info
|
||||
get_disk_info() {
|
||||
local home="${HOME:-/}"
|
||||
local df_output total_gb used_gb used_percent
|
||||
|
||||
df_output=$(df -k "$home" 2> /dev/null | tail -1)
|
||||
|
||||
local total_kb used_kb
|
||||
total_kb=$(echo "$df_output" | awk '{print $2}' 2> /dev/null || echo "0")
|
||||
used_kb=$(echo "$df_output" | awk '{print $3}' 2> /dev/null || echo "0")
|
||||
|
||||
total_kb=${total_kb:-0}
|
||||
used_kb=${used_kb:-0}
|
||||
[[ "$total_kb" == "0" ]] && total_kb=1 # Avoid division by zero
|
||||
|
||||
total_gb=$(awk "BEGIN {printf \"%.2f\", $total_kb / (1024*1024)}" 2> /dev/null || echo "0")
|
||||
used_gb=$(awk "BEGIN {printf \"%.2f\", $used_kb / (1024*1024)}" 2> /dev/null || echo "0")
|
||||
used_percent=$(awk "BEGIN {printf \"%.1f\", ($used_kb / $total_kb) * 100}" 2> /dev/null || echo "0")
|
||||
|
||||
[[ -z "$total_gb" || "$total_gb" == "" ]] && total_gb="0"
|
||||
[[ -z "$used_gb" || "$used_gb" == "" ]] && used_gb="0"
|
||||
[[ -z "$used_percent" || "$used_percent" == "" ]] && used_percent="0"
|
||||
|
||||
echo "$used_gb $total_gb $used_percent"
|
||||
}
|
||||
|
||||
# Get uptime in days
|
||||
get_uptime_days() {
|
||||
local boot_output boot_time uptime_days
|
||||
|
||||
boot_output=$(sysctl -n kern.boottime 2> /dev/null || echo "")
|
||||
boot_time=$(echo "$boot_output" | sed -n 's/.*sec = \([0-9]*\).*/\1/p' 2> /dev/null || echo "")
|
||||
|
||||
if [[ -n "$boot_time" && "$boot_time" =~ ^[0-9]+$ ]]; then
|
||||
local now=$(date +%s 2> /dev/null || echo "0")
|
||||
local uptime_sec=$((now - boot_time))
|
||||
uptime_days=$(awk "BEGIN {printf \"%.1f\", $uptime_sec / 86400}" 2> /dev/null || echo "0")
|
||||
else
|
||||
uptime_days="0"
|
||||
fi
|
||||
|
||||
[[ -z "$uptime_days" || "$uptime_days" == "" ]] && uptime_days="0"
|
||||
echo "$uptime_days"
|
||||
}
|
||||
|
||||
# Get directory size in KB
|
||||
dir_size_kb() {
|
||||
local path="$1"
|
||||
[[ ! -e "$path" ]] && echo "0" && return
|
||||
du -sk "$path" 2> /dev/null | awk '{print $1}' || echo "0"
|
||||
}
|
||||
|
||||
# Format size from KB
|
||||
format_size_kb() {
|
||||
local kb="$1"
|
||||
[[ "$kb" -le 0 ]] && echo "0B" && return
|
||||
|
||||
local mb gb
|
||||
mb=$(awk "BEGIN {printf \"%.1f\", $kb / 1024}")
|
||||
gb=$(awk "BEGIN {printf \"%.2f\", $mb / 1024}")
|
||||
|
||||
if awk "BEGIN {exit !($gb >= 1)}"; then
|
||||
echo "${gb}GB"
|
||||
elif awk "BEGIN {exit !($mb >= 1)}"; then
|
||||
printf "%.0fMB\n" "$mb"
|
||||
else
|
||||
echo "${kb}KB"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check cache size
|
||||
check_cache_refresh() {
|
||||
local cache_dir="$HOME/Library/Caches"
|
||||
local size_kb=$(dir_size_kb "$cache_dir")
|
||||
local desc="Refresh Finder previews, Quick Look, and Safari caches"
|
||||
|
||||
if [[ $size_kb -gt 0 ]]; then
|
||||
local size_str=$(format_size_kb "$size_kb")
|
||||
desc="Refresh ${size_str} of Finder/Safari caches"
|
||||
fi
|
||||
|
||||
echo "cache_refresh|User Cache Refresh|${desc}|true"
|
||||
}
|
||||
|
||||
# Check Mail downloads
|
||||
check_mail_downloads() {
|
||||
local dirs=(
|
||||
"$HOME/Library/Mail Downloads"
|
||||
"$HOME/Library/Containers/com.apple.mail/Data/Library/Mail Downloads"
|
||||
)
|
||||
|
||||
local total_kb=0
|
||||
for dir in "${dirs[@]}"; do
|
||||
total_kb=$((total_kb + $(dir_size_kb "$dir")))
|
||||
done
|
||||
|
||||
if [[ $total_kb -gt 0 ]]; then
|
||||
local size_str=$(format_size_kb "$total_kb")
|
||||
echo "mail_downloads|Mail Downloads|Recover ${size_str} of Mail attachments|true"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check saved state
|
||||
check_saved_state() {
|
||||
local state_dir="$HOME/Library/Saved Application State"
|
||||
local size_kb=$(dir_size_kb "$state_dir")
|
||||
|
||||
if [[ $size_kb -gt 0 ]]; then
|
||||
local size_str=$(format_size_kb "$size_kb")
|
||||
echo "saved_state_cleanup|Saved State|Clear ${size_str} of stale saved states|true"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check swap files
|
||||
check_swap_cleanup() {
|
||||
local total_kb=0
|
||||
local file
|
||||
|
||||
for file in /private/var/vm/swapfile*; do
|
||||
[[ -f "$file" ]] && total_kb=$((total_kb + $(get_file_size "$file") / 1024))
|
||||
done
|
||||
|
||||
if [[ $total_kb -gt 0 ]]; then
|
||||
local size_str=$(format_size_kb "$total_kb")
|
||||
echo "swap_cleanup|Memory & Swap|Purge swap (${size_str}) & inactive memory|false"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check local snapshots
|
||||
check_local_snapshots() {
|
||||
command -v tmutil > /dev/null 2>&1 || return
|
||||
|
||||
local snapshots
|
||||
snapshots=$(tmutil listlocalsnapshots / 2> /dev/null || echo "")
|
||||
|
||||
local count
|
||||
count=$(echo "$snapshots" | grep -c "com.apple.TimeMachine" 2> /dev/null)
|
||||
count=$(echo "$count" | tr -d ' \n')
|
||||
count=${count:-0}
|
||||
[[ "$count" =~ ^[0-9]+$ ]] && [[ $count -gt 0 ]] && echo "local_snapshots|Local Snapshots|${count} APFS local snapshots detected|true"
|
||||
}
|
||||
|
||||
# Check developer cleanup
|
||||
check_developer_cleanup() {
|
||||
local dirs=(
|
||||
"$HOME/Library/Developer/Xcode/DerivedData"
|
||||
"$HOME/Library/Developer/Xcode/Archives"
|
||||
"$HOME/Library/Developer/Xcode/iOS DeviceSupport"
|
||||
"$HOME/Library/Developer/CoreSimulator/Caches"
|
||||
)
|
||||
|
||||
local total_kb=0
|
||||
for dir in "${dirs[@]}"; do
|
||||
total_kb=$((total_kb + $(dir_size_kb "$dir")))
|
||||
done
|
||||
|
||||
if [[ $total_kb -gt 0 ]]; then
|
||||
local size_str=$(format_size_kb "$total_kb")
|
||||
echo "developer_cleanup|Developer Cleanup|Recover ${size_str} of Xcode/simulator data|false"
|
||||
fi
|
||||
}
|
||||
|
||||
# Generate JSON output
|
||||
generate_health_json() {
|
||||
# System info
|
||||
read -r mem_used mem_total <<< "$(get_memory_info)"
|
||||
read -r disk_used disk_total disk_percent <<< "$(get_disk_info)"
|
||||
local uptime=$(get_uptime_days)
|
||||
|
||||
# Ensure all values are valid numbers (fallback to 0)
|
||||
mem_used=${mem_used:-0}
|
||||
mem_total=${mem_total:-0}
|
||||
disk_used=${disk_used:-0}
|
||||
disk_total=${disk_total:-0}
|
||||
disk_percent=${disk_percent:-0}
|
||||
uptime=${uptime:-0}
|
||||
|
||||
# Start JSON
|
||||
cat << EOF
|
||||
{
|
||||
"memory_used_gb": $mem_used,
|
||||
"memory_total_gb": $mem_total,
|
||||
"disk_used_gb": $disk_used,
|
||||
"disk_total_gb": $disk_total,
|
||||
"disk_used_percent": $disk_percent,
|
||||
"uptime_days": $uptime,
|
||||
"optimizations": [
|
||||
EOF
|
||||
|
||||
# Collect all optimization items
|
||||
local -a items=()
|
||||
|
||||
# Always-on items
|
||||
items+=('system_maintenance|System Maintenance|Rebuild system databases & flush caches|true')
|
||||
items+=('maintenance_scripts|Maintenance Scripts|Run daily/weekly/monthly scripts & rotate logs|true')
|
||||
items+=('radio_refresh|Bluetooth & Wi-Fi Refresh|Reset wireless preference caches|true')
|
||||
items+=('recent_items|Recent Items|Clear recent apps/documents/servers lists|true')
|
||||
items+=('log_cleanup|Diagnostics Cleanup|Purge old diagnostic & crash logs|true')
|
||||
items+=('startup_cache|Startup Cache Rebuild|Rebuild kext caches & prelinked kernel|true')
|
||||
|
||||
# Conditional items
|
||||
local item
|
||||
item=$(check_cache_refresh || true)
|
||||
[[ -n "$item" ]] && items+=("$item")
|
||||
item=$(check_mail_downloads || true)
|
||||
[[ -n "$item" ]] && items+=("$item")
|
||||
item=$(check_saved_state || true)
|
||||
[[ -n "$item" ]] && items+=("$item")
|
||||
item=$(check_swap_cleanup || true)
|
||||
[[ -n "$item" ]] && items+=("$item")
|
||||
item=$(check_local_snapshots || true)
|
||||
[[ -n "$item" ]] && items+=("$item")
|
||||
item=$(check_developer_cleanup || true)
|
||||
[[ -n "$item" ]] && items+=("$item")
|
||||
|
||||
# Output items as JSON
|
||||
local first=true
|
||||
for item in "${items[@]}"; do
|
||||
IFS='|' read -r action name desc safe <<< "$item"
|
||||
|
||||
[[ "$first" == "true" ]] && first=false || echo ","
|
||||
|
||||
cat << EOF
|
||||
{
|
||||
"category": "system",
|
||||
"name": "$name",
|
||||
"description": "$desc",
|
||||
"action": "$action",
|
||||
"safe": $safe
|
||||
}
|
||||
EOF
|
||||
done
|
||||
|
||||
# Close JSON
|
||||
cat << 'EOF'
|
||||
]
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Main execution
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
generate_health_json
|
||||
fi
|
||||
@@ -4,7 +4,7 @@ set -euo pipefail
|
||||
|
||||
# Ensure common.sh is loaded
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
[[ -z "${MOLE_COMMON_LOADED:-}" ]] && source "$SCRIPT_DIR/lib/common.sh"
|
||||
[[ -z "${MOLE_COMMON_LOADED:-}" ]] && source "$SCRIPT_DIR/lib/core/common.sh"
|
||||
|
||||
# Batch uninstall functionality with minimal confirmations
|
||||
# Replaces the overly verbose individual confirmation approach
|
||||
@@ -42,7 +42,7 @@ decode_file_list() {
|
||||
echo "$decoded"
|
||||
return 0
|
||||
}
|
||||
# Note: find_app_files() and calculate_total_size() functions now in lib/common.sh
|
||||
# Note: find_app_files() and calculate_total_size() functions now in lib/core/common.sh
|
||||
|
||||
# Batch uninstall with single confirmation
|
||||
batch_uninstall_applications() {
|
||||
Reference in New Issue
Block a user