1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-12 00:04:00 +00:00

Support more detection and update

This commit is contained in:
Tw93
2025-11-23 14:03:14 +08:00
parent 9624366838
commit 178176500c
12 changed files with 1905 additions and 410 deletions

178
lib/autofix_manager.sh Normal file
View File

@@ -0,0 +1,178 @@
#!/bin/bash
# Auto-fix Manager
# Unified auto-fix suggestions and execution
set -euo pipefail
# Show system suggestions with auto-fix markers
show_suggestions() {
local has_suggestions=false
local can_auto_fix=false
local -a auto_fix_items=()
local -a manual_items=()
# Security suggestions
if [[ -n "${FIREWALL_DISABLED:-}" && "${FIREWALL_DISABLED}" == "true" ]]; then
auto_fix_items+=("Enable Firewall for better security")
has_suggestions=true
can_auto_fix=true
fi
if [[ -n "${FILEVAULT_DISABLED:-}" && "${FILEVAULT_DISABLED}" == "true" ]]; then
manual_items+=("Enable FileVault|System Settings → Privacy & Security → FileVault")
has_suggestions=true
fi
# Configuration suggestions
if [[ -n "${TOUCHID_NOT_CONFIGURED:-}" && "${TOUCHID_NOT_CONFIGURED}" == "true" ]]; then
auto_fix_items+=("Enable Touch ID for sudo")
has_suggestions=true
can_auto_fix=true
fi
if [[ -n "${ROSETTA_NOT_INSTALLED:-}" && "${ROSETTA_NOT_INSTALLED}" == "true" ]]; then
auto_fix_items+=("Install Rosetta 2 for Intel app support")
has_suggestions=true
can_auto_fix=true
fi
# Health suggestions
if [[ -n "${CACHE_SIZE_GB:-}" ]]; then
local cache_gb="${CACHE_SIZE_GB:-0}"
if (( $(echo "$cache_gb > 5" | bc -l 2>/dev/null || echo 0) )); then
manual_items+=("Free up ${cache_gb}GB by cleaning caches|Run: mo clean")
has_suggestions=true
fi
fi
if [[ -n "${BREW_HAS_WARNINGS:-}" && "${BREW_HAS_WARNINGS}" == "true" ]]; then
manual_items+=("Fix Homebrew warnings|Run: brew doctor to see details")
has_suggestions=true
fi
if [[ -n "${DISK_FREE_GB:-}" && "${DISK_FREE_GB:-0}" -lt 50 ]]; then
if [[ -z "${CACHE_SIZE_GB:-}" ]] || (( $(echo "${CACHE_SIZE_GB:-0} <= 5" | bc -l 2>/dev/null || echo 1) )); then
manual_items+=("Low disk space (${DISK_FREE_GB}GB free)|Run: mo analyze to find large files")
has_suggestions=true
fi
fi
# Display suggestions
echo -e "${BLUE}${ICON_ARROW}${NC} Suggestions"
if [[ "$has_suggestions" == "false" ]]; then
echo -e " ${GREEN}${NC} All looks good"
export HAS_AUTO_FIX_SUGGESTIONS="false"
return
fi
# Show auto-fix items
if [[ ${#auto_fix_items[@]} -gt 0 ]]; then
for item in "${auto_fix_items[@]}"; do
echo -e " ${YELLOW}${NC} ${item} ${GREEN}[auto]${NC}"
done
fi
# Show manual items
if [[ ${#manual_items[@]} -gt 0 ]]; then
for item in "${manual_items[@]}"; do
local title="${item%%|*}"
local hint="${item#*|}"
echo -e " ${YELLOW}${NC} ${title}"
echo -e " ${GRAY}${hint}${NC}"
done
fi
# Export for use in auto-fix
export HAS_AUTO_FIX_SUGGESTIONS="$can_auto_fix"
}
# Ask user if they want to auto-fix
# Returns: 0 if yes, 1 if no
ask_for_auto_fix() {
if [[ "${HAS_AUTO_FIX_SUGGESTIONS:-false}" != "true" ]]; then
return 1
fi
echo -ne "Fix issues marked ${GREEN}[auto]${NC}? ${GRAY}Enter yes / ESC no${NC}: "
local key
if ! key=$(read_key); then
echo "no"
echo ""
return 1
fi
if [[ "$key" == "ENTER" ]]; then
echo "yes"
echo ""
return 0
else
echo "no"
echo ""
return 1
fi
}
# Perform auto-fixes
# Returns: number of fixes applied
perform_auto_fix() {
local fixed_count=0
# Ensure sudo access
if ! has_sudo_session; then
if ! ensure_sudo_session "System fixes require admin access"; then
echo -e "${YELLOW}Skipping auto fixes (admin authentication required)${NC}"
echo ""
return 0
fi
fi
# Fix Firewall
if [[ -n "${FIREWALL_DISABLED:-}" && "${FIREWALL_DISABLED}" == "true" ]]; then
echo -e "${BLUE}Enabling Firewall...${NC}"
if sudo defaults write /Library/Preferences/com.apple.alf globalstate -int 1 2>/dev/null; then
echo -e "${GREEN}${NC} Firewall enabled"
((fixed_count++))
else
echo -e "${RED}${NC} Failed to enable Firewall"
fi
echo ""
fi
# Fix Touch ID
if [[ -n "${TOUCHID_NOT_CONFIGURED:-}" && "${TOUCHID_NOT_CONFIGURED}" == "true" ]]; then
echo -e "${BLUE}Configuring Touch ID for sudo...${NC}"
local pam_file="/etc/pam.d/sudo"
if sudo bash -c "grep -q 'pam_tid.so' '$pam_file' 2>/dev/null || sed -i '' '2i\\
auth sufficient pam_tid.so
' '$pam_file'" 2>/dev/null; then
echo -e "${GREEN}${NC} Touch ID configured"
((fixed_count++))
else
echo -e "${RED}${NC} Failed to configure Touch ID"
fi
echo ""
fi
# Install Rosetta 2
if [[ -n "${ROSETTA_NOT_INSTALLED:-}" && "${ROSETTA_NOT_INSTALLED}" == "true" ]]; then
echo -e "${BLUE}Installing Rosetta 2...${NC}"
if sudo softwareupdate --install-rosetta --agree-to-license 2>&1 | grep -qE "(Installing|Installed|already installed)"; then
echo -e "${GREEN}${NC} Rosetta 2 installed"
((fixed_count++))
else
echo -e "${RED}${NC} Failed to install Rosetta 2"
fi
echo ""
fi
if [[ $fixed_count -gt 0 ]]; then
echo -e "${GREEN}Fixed ${fixed_count} issue(s)${NC}"
else
echo -e "${YELLOW}No issues were fixed${NC}"
fi
echo ""
return $fixed_count
}

58
lib/check_config.sh Normal file
View File

@@ -0,0 +1,58 @@
#!/bin/bash
# Configuration checks
check_touchid_sudo() {
# Check if Touch ID is configured for sudo
local pam_file="/etc/pam.d/sudo"
if [[ -f "$pam_file" ]] && grep -q "pam_tid.so" "$pam_file" 2>/dev/null; then
echo -e " ${GREEN}${NC} Touch ID Enabled for sudo"
else
# Check if Touch ID is supported
local is_supported=false
if command -v bioutil > /dev/null 2>&1; then
if bioutil -r 2>/dev/null | grep -q "Touch ID"; then
is_supported=true
fi
elif [[ "$(uname -m)" == "arm64" ]]; then
is_supported=true
fi
if [[ "$is_supported" == "true" ]]; then
echo -e " ${YELLOW}${NC} Touch ID ${YELLOW}Not configured${NC} for sudo"
export TOUCHID_NOT_CONFIGURED=true
fi
fi
}
check_rosetta() {
# Check Rosetta 2 (for Apple Silicon Macs)
if [[ "$(uname -m)" == "arm64" ]]; then
if [[ -f "/Library/Apple/usr/share/rosetta/rosetta" ]]; then
echo -e " ${GREEN}${NC} Rosetta 2 Installed"
else
echo -e " ${YELLOW}${NC} Rosetta 2 ${YELLOW}Not installed${NC}"
export ROSETTA_NOT_INSTALLED=true
fi
fi
}
check_git_config() {
# Check basic Git configuration
if command -v git > /dev/null 2>&1; then
local git_name=$(git config --global user.name 2>/dev/null || echo "")
local git_email=$(git config --global user.email 2>/dev/null || echo "")
if [[ -n "$git_name" && -n "$git_email" ]]; then
echo -e " ${GREEN}${NC} Git Config Configured"
else
echo -e " ${YELLOW}${NC} Git Config ${YELLOW}Not configured${NC}"
fi
fi
}
check_all_config() {
check_touchid_sudo
check_rosetta
check_git_config
}

239
lib/check_health.sh Normal file
View File

@@ -0,0 +1,239 @@
#!/bin/bash
# System health checks
# Sets global variables for use in suggestions
check_disk_space() {
local free_gb=$(df -H / | awk 'NR==2 {print $4}' | sed 's/G//')
local free_num=$(echo "$free_gb" | tr -d 'G' | cut -d'.' -f1)
export DISK_FREE_GB=$free_num
if [[ $free_num -lt 20 ]]; then
echo -e " ${RED}${NC} Disk Space ${RED}${free_gb}GB free${NC} (Critical)"
elif [[ $free_num -lt 50 ]]; then
echo -e " ${YELLOW}${NC} Disk Space ${YELLOW}${free_gb}GB free${NC} (Low)"
else
echo -e " ${GREEN}${NC} Disk Space ${free_gb}GB free"
fi
}
check_memory_usage() {
local mem_total
mem_total=$(sysctl -n hw.memsize 2>/dev/null || echo "0")
if [[ -z "$mem_total" || "$mem_total" -le 0 ]]; then
echo -e " ${GRAY}-${NC} Memory Unable to determine"
return
fi
local vm_output
vm_output=$(vm_stat 2>/dev/null || echo "")
local page_size
page_size=$(echo "$vm_output" | awk '/page size of/ {print $8}')
[[ -z "$page_size" ]] && page_size=4096
local free_pages inactive_pages spec_pages
free_pages=$(echo "$vm_output" | awk '/Pages free/ {gsub(/\./,"",$3); print $3}')
inactive_pages=$(echo "$vm_output" | awk '/Pages inactive/ {gsub(/\./,"",$3); print $3}')
spec_pages=$(echo "$vm_output" | awk '/Pages speculative/ {gsub(/\./,"",$3); print $3}')
free_pages=${free_pages:-0}
inactive_pages=${inactive_pages:-0}
spec_pages=${spec_pages:-0}
# Estimate used percent: (total - free - inactive - speculative) / total
local total_pages=$((mem_total / page_size))
local free_total=$((free_pages + inactive_pages + spec_pages))
local used_pages=$((total_pages - free_total))
if ((used_pages < 0)); then
used_pages=0
fi
local used_percent
used_percent=$(awk "BEGIN {printf \"%.0f\", ($used_pages / $total_pages) * 100}")
((used_percent > 100)) && used_percent=100
((used_percent < 0)) && used_percent=0
if [[ $used_percent -gt 90 ]]; then
echo -e " ${RED}${NC} Memory ${RED}${used_percent}% used${NC} (Critical)"
elif [[ $used_percent -gt 80 ]]; then
echo -e " ${YELLOW}${NC} Memory ${YELLOW}${used_percent}% used${NC} (High)"
else
echo -e " ${GREEN}${NC} Memory ${used_percent}% used"
fi
}
check_login_items() {
local login_items_count=0
local -a login_items_list=()
if [[ -t 0 ]]; then
# Show spinner while getting login items
if [[ -t 1 ]]; then
start_inline_spinner "Checking login items..."
fi
while IFS= read -r login_item; do
[[ -n "$login_item" ]] && login_items_list+=("$login_item")
done < <(list_login_items || true)
login_items_count=${#login_items_list[@]}
# Stop spinner before output
if [[ -t 1 ]]; then
stop_inline_spinner
fi
fi
if [[ $login_items_count -gt 15 ]]; then
echo -e " ${YELLOW}${NC} Login Items ${YELLOW}${login_items_count} apps${NC} auto-start (High)"
elif [[ $login_items_count -gt 0 ]]; then
echo -e " ${GREEN}${NC} Login Items ${login_items_count} apps auto-start"
else
echo -e " ${GREEN}${NC} Login Items None"
return
fi
# Show items in a single line
local preview_limit=5
((preview_limit > login_items_count)) && preview_limit=$login_items_count
local items_display=""
for ((i = 0; i < preview_limit; i++)); do
if [[ $i -eq 0 ]]; then
items_display="${login_items_list[$i]}"
else
items_display="${items_display}, ${login_items_list[$i]}"
fi
done
if ((login_items_count > preview_limit)); then
local remaining=$((login_items_count - preview_limit))
items_display="${items_display}, and ${remaining} more"
fi
echo -e " ${GRAY}${items_display}${NC}"
echo -e " ${GRAY}Manage in System Settings → Login Items${NC}"
}
check_cache_size() {
local cache_size_kb=0
# Check common cache locations
local -a cache_paths=(
"$HOME/Library/Caches"
"$HOME/Library/Logs"
)
# Show spinner while calculating cache size
if [[ -t 1 ]]; then
start_inline_spinner "Scanning cache..."
fi
for cache_path in "${cache_paths[@]}"; do
if [[ -d "$cache_path" ]]; then
local size=$(du -sk "$cache_path" 2>/dev/null | awk '{print $1}' || echo "0")
cache_size_kb=$((cache_size_kb + size))
fi
done
local cache_size_gb=$(echo "scale=1; $cache_size_kb / 1024 / 1024" | bc)
export CACHE_SIZE_GB=$cache_size_gb
# Stop spinner before output
if [[ -t 1 ]]; then
stop_inline_spinner
fi
# Convert to integer for comparison
local cache_size_int=$(echo "$cache_size_gb" | cut -d'.' -f1)
if [[ $cache_size_int -gt 10 ]]; then
echo -e " ${YELLOW}${NC} Cache Size ${YELLOW}${cache_size_gb}GB${NC} cleanable"
elif [[ $cache_size_int -gt 5 ]]; then
echo -e " ${YELLOW}${NC} Cache Size ${YELLOW}${cache_size_gb}GB${NC} cleanable"
else
echo -e " ${GREEN}${NC} Cache Size ${cache_size_gb}GB"
fi
}
check_swap_usage() {
# Check swap usage
if command -v sysctl > /dev/null 2>&1; then
local swap_info=$(sysctl vm.swapusage 2>/dev/null || echo "")
if [[ -n "$swap_info" ]]; then
local swap_used=$(echo "$swap_info" | grep -o "used = [0-9.]*[GM]" | awk '{print $3}' || echo "0M")
local swap_num=$(echo "$swap_used" | sed 's/[GM]//')
if [[ "$swap_used" == *"G"* ]]; then
local swap_gb=${swap_num%.*}
if [[ $swap_gb -gt 2 ]]; then
echo -e " ${YELLOW}${NC} Swap Usage ${YELLOW}${swap_used}${NC} (High)"
else
echo -e " ${GREEN}${NC} Swap Usage ${swap_used}"
fi
else
echo -e " ${GREEN}${NC} Swap Usage ${swap_used}"
fi
fi
fi
}
check_timemachine() {
# Check Time Machine backup status
if command -v tmutil > /dev/null 2>&1; then
local tm_status=$(tmutil latestbackup 2>/dev/null || echo "")
if [[ -z "$tm_status" ]]; then
echo -e " ${YELLOW}${NC} Time Machine No backups found"
echo -e " ${GRAY}Set up in System Settings → General → Time Machine (optional but recommended)${NC}"
else
# Get last backup time
local backup_date=$(tmutil latestbackup 2>/dev/null | xargs basename 2>/dev/null || echo "")
if [[ -n "$backup_date" ]]; then
echo -e " ${GREEN}${NC} Time Machine Backup active"
else
echo -e " ${YELLOW}${NC} Time Machine Not configured"
fi
fi
fi
}
check_brew_health() {
# Check Homebrew doctor
if command -v brew > /dev/null 2>&1; then
# Show spinner while running brew doctor
if [[ -t 1 ]]; then
start_inline_spinner "Running brew doctor..."
fi
local brew_doctor=$(brew doctor 2>&1 || echo "")
# Stop spinner before output
if [[ -t 1 ]]; then
stop_inline_spinner
fi
if echo "$brew_doctor" | grep -q "ready to brew"; then
echo -e " ${GREEN}${NC} Homebrew Healthy"
else
local warning_count=$(echo "$brew_doctor" | grep -c "Warning:" || echo "0")
if [[ $warning_count -gt 0 ]]; then
echo -e " ${YELLOW}${NC} Homebrew ${YELLOW}${warning_count} warnings${NC}"
echo -e " ${GRAY}Run: ${GREEN}brew doctor${NC} to see fixes, then rerun until clean${NC}"
export BREW_HAS_WARNINGS=true
else
echo -e " ${GREEN}${NC} Homebrew Healthy"
fi
fi
fi
}
check_system_health() {
check_disk_space
check_memory_usage
check_swap_usage
check_login_items
check_cache_size
# Time Machine check is optional; skip by default to avoid noise on systems without backups
check_brew_health
}

63
lib/check_security.sh Normal file
View File

@@ -0,0 +1,63 @@
#!/bin/bash
# Security checks
check_filevault() {
# Check FileVault encryption status
if command -v fdesetup > /dev/null 2>&1; then
local fv_status=$(fdesetup status 2>/dev/null || echo "")
if echo "$fv_status" | grep -q "FileVault is On"; then
echo -e " ${GREEN}${NC} FileVault Enabled"
else
echo -e " ${RED}${NC} FileVault ${RED}Disabled${NC} (Recommend enabling)"
export FILEVAULT_DISABLED=true
fi
fi
}
check_firewall() {
# Check firewall status
local firewall_status=$(defaults read /Library/Preferences/com.apple.alf globalstate 2>/dev/null || echo "0")
if [[ "$firewall_status" == "1" || "$firewall_status" == "2" ]]; then
echo -e " ${GREEN}${NC} Firewall Enabled"
else
echo -e " ${YELLOW}${NC} Firewall ${YELLOW}Disabled${NC} (Consider enabling)"
echo -e " ${GRAY}System Settings → Network → Firewall, or run:${NC}"
echo -e " ${GRAY}sudo defaults write /Library/Preferences/com.apple.alf globalstate -int 1${NC}"
export FIREWALL_DISABLED=true
fi
}
check_gatekeeper() {
# Check Gatekeeper status
if command -v spctl > /dev/null 2>&1; then
local gk_status=$(spctl --status 2>/dev/null || echo "")
if echo "$gk_status" | grep -q "enabled"; then
echo -e " ${GREEN}${NC} Gatekeeper Active"
else
echo -e " ${YELLOW}${NC} Gatekeeper ${YELLOW}Disabled${NC}"
echo -e " ${GRAY}Enable via System Settings → Privacy & Security, or:${NC}"
echo -e " ${GRAY}sudo spctl --master-enable${NC}"
fi
fi
}
check_sip() {
# Check System Integrity Protection
if command -v csrutil > /dev/null 2>&1; then
local sip_status=$(csrutil status 2>/dev/null || echo "")
if echo "$sip_status" | grep -q "enabled"; then
echo -e " ${GREEN}${NC} SIP Enabled"
else
echo -e " ${YELLOW}${NC} SIP ${YELLOW}Disabled${NC}"
echo -e " ${GRAY}Restart into Recovery → Utilities → Terminal → run: csrutil enable${NC}"
fi
fi
}
check_all_security() {
check_filevault
check_firewall
check_gatekeeper
check_sip
}

273
lib/check_updates.sh Normal file
View File

@@ -0,0 +1,273 @@
#!/bin/bash
# Check for software updates
# Sets global variables for use in suggestions
# Cache configuration
CACHE_DIR="${HOME}/.cache/mole"
CACHE_TTL=600 # 10 minutes in seconds
# Ensure cache directory exists
mkdir -p "$CACHE_DIR" 2>/dev/null || true
clear_cache_file() {
local file="$1"
rm -f "$file" 2>/dev/null || true
}
reset_brew_cache() {
clear_cache_file "$CACHE_DIR/brew_updates"
}
reset_softwareupdate_cache() {
clear_cache_file "$CACHE_DIR/softwareupdate_list"
SOFTWARE_UPDATE_LIST=""
}
reset_mole_cache() {
clear_cache_file "$CACHE_DIR/mole_version"
}
# Check if cache is still valid
is_cache_valid() {
local cache_file="$1"
local ttl="${2:-$CACHE_TTL}"
if [[ ! -f "$cache_file" ]]; then
return 1
fi
local cache_age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || echo 0)))
[[ $cache_age -lt $ttl ]]
}
check_homebrew_updates() {
if ! command -v brew > /dev/null 2>&1; then
return
fi
local cache_file="$CACHE_DIR/brew_updates"
local formula_count=0
local cask_count=0
if is_cache_valid "$cache_file"; then
read -r formula_count cask_count < "$cache_file" 2>/dev/null || true
formula_count=${formula_count:-0}
cask_count=${cask_count:-0}
else
# Show spinner while checking
if [[ -t 1 ]]; then
start_inline_spinner "Checking Homebrew..."
fi
local outdated_list=""
outdated_list=$(brew outdated --quiet 2>/dev/null || echo "")
if [[ -n "$outdated_list" ]]; then
formula_count=$(echo "$outdated_list" | wc -l | tr -d ' ')
fi
local cask_list=""
cask_list=$(brew outdated --cask --quiet 2>/dev/null || echo "")
if [[ -n "$cask_list" ]]; then
cask_count=$(echo "$cask_list" | wc -l | tr -d ' ')
fi
echo "$formula_count $cask_count" > "$cache_file" 2>/dev/null || true
# Stop spinner before output
if [[ -t 1 ]]; then
stop_inline_spinner
fi
fi
local total_count=$((formula_count + cask_count))
export BREW_FORMULA_OUTDATED_COUNT=$formula_count
export BREW_CASK_OUTDATED_COUNT=$cask_count
export BREW_OUTDATED_COUNT=$total_count
if [[ $total_count -gt 0 ]]; then
local breakdown=""
if [[ $formula_count -gt 0 && $cask_count -gt 0 ]]; then
breakdown=" (${formula_count} formula, ${cask_count} cask)"
elif [[ $formula_count -gt 0 ]]; then
breakdown=" (${formula_count} formula)"
elif [[ $cask_count -gt 0 ]]; then
breakdown=" (${cask_count} cask)"
fi
echo -e " ${YELLOW}${NC} Homebrew ${YELLOW}${total_count} updates${NC}${breakdown}"
echo -e " ${GRAY}Run: ${GREEN}brew upgrade${NC} ${GRAY}and/or${NC} ${GREEN}brew upgrade --cask${NC}"
else
echo -e " ${GREEN}${NC} Homebrew Up to date"
fi
}
# Cache software update list to avoid calling softwareupdate twice
SOFTWARE_UPDATE_LIST=""
get_software_updates() {
local cache_file="$CACHE_DIR/softwareupdate_list"
if [[ -z "$SOFTWARE_UPDATE_LIST" ]]; then
# Check cache first
if is_cache_valid "$cache_file"; then
SOFTWARE_UPDATE_LIST=$(cat "$cache_file" 2>/dev/null || echo "")
else
# Show spinner while checking (only on first call)
local show_spinner=false
if [[ -t 1 && -z "${SOFTWAREUPDATE_SPINNER_SHOWN:-}" ]]; then
start_inline_spinner "Checking system updates..."
show_spinner=true
export SOFTWAREUPDATE_SPINNER_SHOWN="true"
fi
SOFTWARE_UPDATE_LIST=$(softwareupdate -l 2>/dev/null || echo "")
# Save to cache
echo "$SOFTWARE_UPDATE_LIST" > "$cache_file" 2>/dev/null || true
# Stop spinner
if [[ "$show_spinner" == "true" ]]; then
stop_inline_spinner
fi
fi
fi
echo "$SOFTWARE_UPDATE_LIST"
}
check_appstore_updates() {
local update_list=""
update_list=$(get_software_updates | grep -v "Software Update Tool" | grep "^\*" | grep -vi "macOS" || echo "")
local update_count=0
if [[ -n "$update_list" ]]; then
update_count=$(echo "$update_list" | wc -l | tr -d ' ')
fi
export APPSTORE_UPDATE_COUNT=$update_count
if [[ $update_count -gt 0 ]]; then
echo -e " ${YELLOW}${NC} App Store ${YELLOW}${update_count} apps${NC} need update"
echo -e " ${GRAY}Run: ${GREEN}softwareupdate -i <label>${NC}"
else
echo -e " ${GREEN}${NC} App Store Up to date"
fi
}
check_macos_update() {
# Check for macOS system update using cached list
local macos_update=""
macos_update=$(get_software_updates | grep -i "macOS" | head -1 || echo "")
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}${NC} macOS ${YELLOW}${version} available${NC}"
else
echo -e " ${YELLOW}${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}${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
}
}
'
}

View File

@@ -437,6 +437,24 @@ get_directory_size_bytes() {
du -sk "$path" 2> /dev/null | cut -f1 | awk '{print $1 * 1024}' || echo "0"
}
# List login items (one per line)
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
}
# Permission checks
check_sudo() {
if ! sudo -n true 2> /dev/null; then
@@ -458,33 +476,81 @@ check_touchid_support() {
# Usage: request_sudo_access "prompt message" [optional: force_password]
request_sudo_access() {
local prompt_msg="${1:-Admin access required}"
local force_password="${2:-false}"
# Check if already has sudo access
if sudo -n true 2> /dev/null; then
return 0
fi
# If Touch ID is supported and not forced to use password
if [[ "$force_password" != "true" ]] && check_touchid_support; then
echo -e "${PURPLE}${ICON_ARROW}${NC} ${prompt_msg} ${GRAY}(Touch ID or password)${NC}"
if sudo -v 2> /dev/null; then
return 0
else
return 1
echo -e "${PURPLE}${ICON_ARROW}${NC} ${prompt_msg} ${GRAY}(Touch ID or password)${NC}"
sudo -k
# Use default sudo prompt (Touch ID behaves more reliably without a custom prompt)
local sudo_cmd=(sudo -v)
# Optional timeout command to prevent hangs
local timeout_cmd=""
for t in gtimeout timeout; do
if command -v "$t" > /dev/null 2>&1; then
timeout_cmd="$t"
break
fi
else
# Traditional password method
echo -e "${PURPLE}${ICON_ARROW}${NC} ${prompt_msg}"
echo -ne "${PURPLE}${ICON_ARROW}${NC} Password: "
IFS= read -r -s password
echo ""
if [[ -n "$password" ]] && echo "$password" | sudo -S true 2> /dev/null; then
return 0
done
# Helper to attempt sudo with optional IO redirection and timeout
_run_sudo_attempt() {
local in="$1"
local out="$2"
local err="${3:-$2}"
local cmd=("${sudo_cmd[@]}")
if [[ -n "$timeout_cmd" ]]; then
cmd=("$timeout_cmd" 20 "${cmd[@]}")
fi
if [[ -n "$in" ]]; then
if [[ -n "$out" ]]; then
"${cmd[@]}" < "$in" > "$out" 2> "${err:-$out}"
else
"${cmd[@]}" < "$in"
fi
else
return 1
"${cmd[@]}"
fi
}
# Try current TTY first
if _run_sudo_attempt "" ""; then
sudo -n true 2> /dev/null || true
return 0
fi
# Always talk to the real terminal so Touch ID/password prompts show up
local sudo_tty="/dev/tty"
if [[ -r "$sudo_tty" ]]; then
if _run_sudo_attempt "$sudo_tty" "$sudo_tty" "$sudo_tty"; then
sudo -n true 2> /dev/null || true
return 0
fi
fi
# Last resort: spawn a fresh pty (helps when stdin/out were redirected)
if command -v script > /dev/null 2>&1; then
local script_cmd=(script -q /dev/null)
if [[ -n "$timeout_cmd" ]]; then
script_cmd=("$timeout_cmd" 20 "${script_cmd[@]}")
fi
if "${script_cmd[@]}" "${sudo_cmd[@]}"; then
sudo -n true 2> /dev/null || true
return 0
fi
fi
# Fallback for environments without /dev/tty or script
if _run_sudo_attempt "" ""; then
sudo -n true 2> /dev/null || true
return 0
fi
return 1
}
request_sudo() {
@@ -1095,7 +1161,7 @@ start_sudo_keepalive() {
(
local retry_count=0
while true; do
if ! sudo -n true 2> /dev/null; then
if ! sudo -n -v 2> /dev/null; then
((retry_count++))
if [[ $retry_count -ge 3 ]]; then
exit 1

332
lib/optimization_tasks.sh Normal file
View File

@@ -0,0 +1,332 @@
#!/bin/bash
# Optimization Tasks
# Individual optimization operations extracted from execute_optimization
set -euo pipefail
# System maintenance: rebuild databases and flush caches
opt_system_maintenance() {
echo -e "${BLUE}${ICON_ARROW}${NC} Rebuilding LaunchServices database..."
timeout 10 /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user > /dev/null 2>&1 || true
echo -e "${GREEN}${ICON_SUCCESS}${NC} LaunchServices database rebuilt"
echo -e "${BLUE}${ICON_ARROW}${NC} Clearing DNS cache..."
if sudo dscacheutil -flushcache 2> /dev/null && sudo killall -HUP mDNSResponder 2> /dev/null; then
echo -e "${GREEN}${ICON_SUCCESS}${NC} DNS cache cleared"
else
echo -e "${RED}${ICON_ERROR}${NC} Failed to clear DNS cache"
fi
echo -e "${BLUE}${ICON_ARROW}${NC} Clearing memory cache..."
if sudo purge 2> /dev/null; then
echo -e "${GREEN}${ICON_SUCCESS}${NC} Memory cache cleared"
else
echo -e "${RED}${ICON_ERROR}${NC} Failed to clear memory"
fi
echo -e "${BLUE}${ICON_ARROW}${NC} Rebuilding font cache..."
sudo atsutil databases -remove > /dev/null 2>&1
echo -e "${GREEN}${ICON_SUCCESS}${NC} Font cache rebuilt"
echo -e "${BLUE}${ICON_ARROW}${NC} Rebuilding Spotlight index (runs in background)..."
# mdutil triggers background indexing - don't wait
timeout 10 sudo mdutil -E / > /dev/null 2>&1 || true
echo -e "${GREEN}${ICON_SUCCESS}${NC} Spotlight rebuild initiated"
}
# Cache refresh: update Finder/Safari caches
opt_cache_refresh() {
echo -e "${BLUE}${ICON_ARROW}${NC} Resetting Quick Look cache..."
qlmanage -r cache > /dev/null 2>&1 || true
qlmanage -r > /dev/null 2>&1 || true
local -a cache_targets=(
"$HOME/Library/Caches/com.apple.QuickLook.thumbnailcache|Quick Look thumbnails"
"$HOME/Library/Caches/com.apple.iconservices.store|Icon Services store"
"$HOME/Library/Caches/com.apple.iconservices|Icon Services cache"
"$HOME/Library/Caches/com.apple.Safari/WebKitCache|Safari WebKit cache"
"$HOME/Library/Caches/com.apple.Safari/Favicon|Safari favicon cache"
)
for target in "${cache_targets[@]}"; do
IFS='|' read -r target_path label <<< "$target"
cleanup_path "$target_path" "$label"
done
echo -e "${GREEN}${ICON_SUCCESS}${NC} Finder and Safari caches updated"
}
# Maintenance scripts: run periodic tasks
opt_maintenance_scripts() {
local success=true
local periodic_cmd="/usr/sbin/periodic"
# Show spinner while running all tasks
if [[ -t 1 ]]; then
start_inline_spinner ""
fi
# Run periodic scripts silently with timeout
if [[ -x "$periodic_cmd" ]]; then
if ! timeout 180 sudo "$periodic_cmd" daily weekly monthly > /dev/null 2>&1; then
success=false
fi
fi
# Run newsyslog silently with timeout
if ! timeout 120 sudo newsyslog > /dev/null 2>&1; then
success=false
fi
# Run repair_packages silently with timeout
if [[ -x "/usr/libexec/repair_packages" ]]; then
if ! timeout 180 sudo /usr/libexec/repair_packages --repair --standard-pkgs --volume / > /dev/null 2>&1; then
success=false
fi
fi
if [[ -t 1 ]]; then
stop_inline_spinner
fi
# Show final status
if [[ "$success" == "true" ]]; then
echo -e "${GREEN}${ICON_SUCCESS}${NC} Complete"
else
echo -e "${YELLOW}!${NC} Some tasks timed out or failed"
fi
}
# Log cleanup: remove diagnostic and crash logs
opt_log_cleanup() {
echo -e "${BLUE}${ICON_ARROW}${NC} Clearing diagnostic & crash logs..."
local -a user_logs=(
"$HOME/Library/Logs/DiagnosticReports"
"$HOME/Library/Logs/CrashReporter"
"$HOME/Library/Logs/corecaptured"
)
for target in "${user_logs[@]}"; do
cleanup_path "$target" "$(basename "$target")"
done
if [[ -d "/Library/Logs/DiagnosticReports" ]]; then
sudo find /Library/Logs/DiagnosticReports -type f -name "*.crash" -delete 2> /dev/null || true
sudo find /Library/Logs/DiagnosticReports -type f -name "*.panic" -delete 2> /dev/null || true
echo -e "${GREEN}${ICON_SUCCESS}${NC} System diagnostic logs cleared"
else
echo -e "${GRAY}-${NC} No system diagnostic logs found"
fi
}
# Recent items: clear recent file lists
opt_recent_items() {
echo -e "${BLUE}${ICON_ARROW}${NC} Clearing recent items lists..."
local shared_dir="$HOME/Library/Application Support/com.apple.sharedfilelist"
if [[ -d "$shared_dir" ]]; then
find "$shared_dir" -name "*.sfl2" -type f -delete 2> /dev/null || true
echo -e "${GREEN}${ICON_SUCCESS}${NC} Shared file lists cleared"
fi
rm -f "$HOME/Library/Preferences/com.apple.recentitems.plist" 2> /dev/null || true
defaults delete NSGlobalDomain NSRecentDocumentsLimit 2> /dev/null || true
echo -e "${GREEN}${ICON_SUCCESS}${NC} Recent items cleared"
}
# Radio refresh: reset Bluetooth and Wi-Fi
opt_radio_refresh() {
echo -e "${BLUE}${ICON_ARROW}${NC} Resetting Bluetooth preferences..."
rm -f "$HOME/Library/Preferences/com.apple.Bluetooth.plist" 2> /dev/null || true
sudo rm -f /Library/Preferences/com.apple.Bluetooth.plist 2> /dev/null || true
echo -e "${GREEN}${ICON_SUCCESS}${NC} Bluetooth caches refreshed"
echo -e "${BLUE}${ICON_ARROW}${NC} Resetting Wi-Fi settings..."
local sysconfig="/Library/Preferences/SystemConfiguration"
if [[ -d "$sysconfig" ]]; then
sudo cp "$sysconfig"/com.apple.airport.preferences.plist "$sysconfig"/com.apple.airport.preferences.plist.bak 2> /dev/null || true
sudo rm -f "$sysconfig"/com.apple.airport.preferences.plist 2> /dev/null || true
echo -e "${GREEN}${ICON_SUCCESS}${NC} Wi-Fi preferences reset"
else
echo -e "${GRAY}-${NC} SystemConfiguration directory missing"
fi
sudo ifconfig awdl0 down 2> /dev/null || true
sudo ifconfig awdl0 up 2> /dev/null || true
echo -e "${GREEN}${ICON_SUCCESS}${NC} Wireless services refreshed"
}
# Mail downloads: clear Mail attachment cache
opt_mail_downloads() {
echo -e "${BLUE}${ICON_ARROW}${NC} Clearing Mail attachment downloads..."
local -a mail_dirs=(
"$HOME/Library/Mail Downloads|Mail Downloads"
"$HOME/Library/Containers/com.apple.mail/Data/Library/Mail Downloads|Mail Container Downloads"
)
for target in "${mail_dirs[@]}"; do
IFS='|' read -r target_path label <<< "$target"
cleanup_path "$target_path" "$label"
ensure_directory "$target_path"
done
echo -e "${GREEN}${ICON_SUCCESS}${NC} Mail downloads cleared"
}
# Saved state: remove app saved states
opt_saved_state_cleanup() {
echo -e "${BLUE}${ICON_ARROW}${NC} Removing saved application states..."
local state_dir="$HOME/Library/Saved Application State"
cleanup_path "$state_dir" "Saved Application State"
ensure_directory "$state_dir"
echo -e "${GREEN}${ICON_SUCCESS}${NC} Saved states cleared"
}
# Finder and Dock: refresh interface caches
opt_finder_dock_refresh() {
echo -e "${BLUE}${ICON_ARROW}${NC} Resetting Finder & Dock caches..."
local -a interface_targets=(
"$HOME/Library/Caches/com.apple.finder|Finder cache"
"$HOME/Library/Caches/com.apple.dock.iconcache|Dock icon cache"
)
for target in "${interface_targets[@]}"; do
IFS='|' read -r target_path label <<< "$target"
cleanup_path "$target_path" "$label"
done
killall Finder > /dev/null 2>&1 || true
killall Dock > /dev/null 2>&1 || true
echo -e "${GREEN}${ICON_SUCCESS}${NC} Finder & Dock relaunched"
}
# Swap cleanup: reset swap files
opt_swap_cleanup() {
echo -e "${BLUE}${ICON_ARROW}${NC} Removing swapfiles and resetting dynamic pager..."
if sudo launchctl unload /System/Library/LaunchDaemons/com.apple.dynamic_pager.plist > /dev/null 2>&1; then
sudo rm -f /private/var/vm/swapfile* > /dev/null 2>&1 || true
sudo touch /private/var/vm/swapfile0 > /dev/null 2>&1 || true
sudo chmod 600 /private/var/vm/swapfile0 > /dev/null 2>&1 || true
sudo launchctl load /System/Library/LaunchDaemons/com.apple.dynamic_pager.plist > /dev/null 2>&1 || true
echo -e "${GREEN}${ICON_SUCCESS}${NC} Swap cache rebuilt"
else
echo -e "${YELLOW}!${NC} Could not unload dynamic_pager"
fi
}
# Startup cache: rebuild kernel caches
opt_startup_cache() {
local macos_version
macos_version=$(sw_vers -productVersion | cut -d '.' -f 1)
local success=true
if [[ -t 1 ]]; then
start_inline_spinner ""
fi
if [[ "$macos_version" -ge 11 ]] || [[ "$(uname -m)" == "arm64" ]]; then
if ! timeout 120 sudo kextcache -i / > /dev/null 2>&1; then
success=false
fi
else
if ! timeout 180 sudo kextcache -i / > /dev/null 2>&1; then
success=false
fi
sudo rm -rf /System/Library/PrelinkedKernels/* > /dev/null 2>&1 || true
timeout 120 sudo kextcache -system-prelinked-kernel > /dev/null 2>&1 || true
fi
if [[ -t 1 ]]; then
stop_inline_spinner
fi
if [[ "$success" == "true" ]]; then
echo -e "${GREEN}${ICON_SUCCESS}${NC} Complete"
else
echo -e "${YELLOW}!${NC} Timed out or failed"
fi
}
# Local snapshots: thin Time Machine snapshots
opt_local_snapshots() {
if ! command -v tmutil > /dev/null 2>&1; then
echo -e "${YELLOW}!${NC} tmutil not available on this system"
return
fi
local before after
before=$(count_local_snapshots)
if [[ "$before" -eq 0 ]]; then
echo -e "${GREEN}${ICON_SUCCESS}${NC} No local snapshots to thin"
return
fi
if [[ -t 1 ]]; then
start_inline_spinner ""
fi
local success=false
if timeout 180 sudo tmutil thinlocalsnapshots / 9999999999 4 > /dev/null 2>&1; then
success=true
fi
if [[ -t 1 ]]; then
stop_inline_spinner
fi
if [[ "$success" == "true" ]]; then
after=$(count_local_snapshots)
local removed=$((before - after))
[[ "$removed" -lt 0 ]] && removed=0
echo -e "${GREEN}${ICON_SUCCESS}${NC} Removed $removed snapshots (remaining: $after)"
else
echo -e "${YELLOW}!${NC} Timed out or failed"
fi
}
# Developer cleanup: remove Xcode/simulator cruft
opt_developer_cleanup() {
local -a dev_targets=(
"$HOME/Library/Developer/Xcode/DerivedData|Xcode DerivedData"
"$HOME/Library/Developer/Xcode/iOS DeviceSupport|iOS Device support files"
"$HOME/Library/Developer/CoreSimulator/Caches|CoreSimulator caches"
)
for target in "${dev_targets[@]}"; do
IFS='|' read -r target_path label <<< "$target"
cleanup_path "$target_path" "$label"
done
if command -v xcrun > /dev/null 2>&1; then
echo -e "${BLUE}${ICON_ARROW}${NC} Removing unavailable simulator runtimes..."
if xcrun simctl delete unavailable > /dev/null 2>&1; then
echo -e "${GREEN}${ICON_SUCCESS}${NC} Unavailable simulators removed"
else
echo -e "${YELLOW}!${NC} Could not prune simulator runtimes"
fi
fi
echo -e "${GREEN}${ICON_SUCCESS}${NC} Developer caches cleaned"
}
# Execute optimization by action name
execute_optimization() {
local action="$1"
local path="${2:-}"
case "$action" in
system_maintenance) opt_system_maintenance ;;
cache_refresh) opt_cache_refresh ;;
maintenance_scripts) opt_maintenance_scripts ;;
log_cleanup) opt_log_cleanup ;;
recent_items) opt_recent_items ;;
radio_refresh) opt_radio_refresh ;;
mail_downloads) opt_mail_downloads ;;
saved_state_cleanup) opt_saved_state_cleanup ;;
finder_dock_refresh) opt_finder_dock_refresh ;;
swap_cleanup) opt_swap_cleanup ;;
startup_cache) opt_startup_cache ;;
local_snapshots) opt_local_snapshots ;;
developer_cleanup) opt_developer_cleanup ;;
*)
echo -e "${RED}${ICON_ERROR}${NC} Unknown action: $action"
return 1
;;
esac
}

147
lib/sudo_manager.sh Normal file
View File

@@ -0,0 +1,147 @@
#!/bin/bash
# Sudo Session Manager
# Unified sudo authentication and keepalive management
set -euo pipefail
# Global state
MOLE_SUDO_KEEPALIVE_PID=""
MOLE_SUDO_ESTABLISHED="false"
# Start sudo keepalive background process
# Returns: PID of keepalive process
_start_sudo_keepalive() {
[[ "${MO_DEBUG:-}" == "1" ]] && echo "DEBUG: _start_sudo_keepalive: starting background process..." >&2
# Start background keepalive process with all outputs redirected
# This is critical: command substitution waits for all file descriptors to close
(
local retry_count=0
while true; do
if ! sudo -n -v 2> /dev/null; then
((retry_count++))
if [[ $retry_count -ge 3 ]]; then
exit 1
fi
sleep 5
continue
fi
retry_count=0
sleep 30
kill -0 "$$" 2> /dev/null || exit
done
) >/dev/null 2>&1 &
local pid=$!
[[ "${MO_DEBUG:-}" == "1" ]] && echo "DEBUG: _start_sudo_keepalive: background PID = $pid" >&2
echo $pid
}
# Stop sudo keepalive process
# Args: $1 - PID of keepalive process
_stop_sudo_keepalive() {
local pid="${1:-}"
if [[ -n "$pid" ]]; then
kill "$pid" 2> /dev/null || true
wait "$pid" 2> /dev/null || true
fi
}
# Check if sudo session is active
has_sudo_session() {
sudo -n true 2> /dev/null
}
# Request sudo access (wrapper for common.sh function)
# Args: $1 - prompt message
request_sudo() {
local prompt_msg="${1:-Admin access required}"
[[ "${MO_DEBUG:-}" == "1" ]] && echo "DEBUG: request_sudo: checking existing session..."
if has_sudo_session; then
[[ "${MO_DEBUG:-}" == "1" ]] && echo "DEBUG: request_sudo: session already exists"
return 0
fi
[[ "${MO_DEBUG:-}" == "1" ]] && echo "DEBUG: request_sudo: calling request_sudo_access from common.sh..."
# Use the robust implementation from common.sh
if request_sudo_access "$prompt_msg"; then
[[ "${MO_DEBUG:-}" == "1" ]] && echo "DEBUG: request_sudo: request_sudo_access succeeded"
return 0
else
[[ "${MO_DEBUG:-}" == "1" ]] && echo "DEBUG: request_sudo: request_sudo_access failed"
return 1
fi
}
# Ensure sudo session is established with keepalive
# Args: $1 - prompt message
ensure_sudo_session() {
local prompt="${1:-Admin access required}"
[[ "${MO_DEBUG:-}" == "1" ]] && echo "DEBUG: ensure_sudo_session called"
# Check if already established
if has_sudo_session && [[ "$MOLE_SUDO_ESTABLISHED" == "true" ]]; then
[[ "${MO_DEBUG:-}" == "1" ]] && echo "DEBUG: Sudo session already active"
return 0
fi
[[ "${MO_DEBUG:-}" == "1" ]] && echo "DEBUG: Checking for old keepalive..."
# Stop old keepalive if exists
if [[ -n "$MOLE_SUDO_KEEPALIVE_PID" ]]; then
[[ "${MO_DEBUG:-}" == "1" ]] && echo "DEBUG: Stopping old keepalive PID $MOLE_SUDO_KEEPALIVE_PID"
_stop_sudo_keepalive "$MOLE_SUDO_KEEPALIVE_PID"
MOLE_SUDO_KEEPALIVE_PID=""
fi
[[ "${MO_DEBUG:-}" == "1" ]] && echo "DEBUG: Calling request_sudo..."
# Request sudo access
if ! request_sudo "$prompt"; then
[[ "${MO_DEBUG:-}" == "1" ]] && echo "DEBUG: request_sudo failed"
MOLE_SUDO_ESTABLISHED="false"
return 1
fi
[[ "${MO_DEBUG:-}" == "1" ]] && echo "DEBUG: request_sudo succeeded, starting keepalive..."
# Start keepalive
MOLE_SUDO_KEEPALIVE_PID=$(_start_sudo_keepalive)
[[ "${MO_DEBUG:-}" == "1" ]] && echo "DEBUG: Keepalive started with PID $MOLE_SUDO_KEEPALIVE_PID"
MOLE_SUDO_ESTABLISHED="true"
return 0
}
# Stop sudo session and cleanup
stop_sudo_session() {
if [[ -n "$MOLE_SUDO_KEEPALIVE_PID" ]]; then
_stop_sudo_keepalive "$MOLE_SUDO_KEEPALIVE_PID"
MOLE_SUDO_KEEPALIVE_PID=""
fi
MOLE_SUDO_ESTABLISHED="false"
}
# Register cleanup on script exit
register_sudo_cleanup() {
trap stop_sudo_session EXIT INT TERM
}
# Check if sudo is likely needed for given operations
# Args: $@ - list of operations to check
will_need_sudo() {
local -a operations=("$@")
for op in "${operations[@]}"; do
case "$op" in
system_update | appstore_update | macos_update | firewall | touchid | rosetta | system_fix)
return 0
;;
esac
done
return 1
}

269
lib/update_manager.sh Normal file
View File

@@ -0,0 +1,269 @@
#!/bin/bash
# Update Manager
# Unified update execution for all update types
set -euo pipefail
# Format Homebrew update label for display
format_brew_update_label() {
local total="${BREW_OUTDATED_COUNT:-0}"
if [[ -z "$total" || "$total" -le 0 ]]; then
return
fi
local -a details=()
local formulas="${BREW_FORMULA_OUTDATED_COUNT:-0}"
local casks="${BREW_CASK_OUTDATED_COUNT:-0}"
((formulas > 0)) && details+=("${formulas} formula")
((casks > 0)) && details+=("${casks} cask")
local detail_str="(${total} updates)"
if ((${#details[@]} > 0)); then
detail_str="($(IFS=', '; printf '%s' "${details[*]}"))"
fi
printf " • Homebrew %s" "$detail_str"
}
# Ask user if they want to update
# Returns: 0 if yes, 1 if no
ask_for_updates() {
local has_updates=false
local -a update_list=()
local brew_entry
brew_entry=$(format_brew_update_label || true)
if [[ -n "$brew_entry" ]]; then
has_updates=true
update_list+=("$brew_entry")
fi
if [[ -n "${APPSTORE_UPDATE_COUNT:-}" && "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 ]]; then
has_updates=true
update_list+=(" • App Store (${APPSTORE_UPDATE_COUNT} apps)")
fi
if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" ]]; then
has_updates=true
update_list+=(" • macOS system")
fi
if [[ -n "${MOLE_UPDATE_AVAILABLE:-}" && "${MOLE_UPDATE_AVAILABLE}" == "true" ]]; then
has_updates=true
update_list+=(" • Mole")
fi
if [[ "$has_updates" == "false" ]]; then
return 1
fi
echo -e "${BLUE}AVAILABLE UPDATES${NC}"
for item in "${update_list[@]}"; do
echo -e "$item"
done
echo ""
echo -ne "${YELLOW}Update all now?${NC} ${GRAY}Enter yes / ESC skip${NC}: "
local key
if ! key=$(read_key); then
echo "skip"
echo ""
return 1
fi
if [[ "$key" == "ENTER" ]]; then
echo "yes"
echo ""
return 0
else
echo "skip"
echo ""
return 1
fi
}
# Perform all pending updates
# Returns: 0 if all succeeded, 1 if some failed
perform_updates() {
local updated_count=0
local total_count=0
local brew_formula="${BREW_FORMULA_OUTDATED_COUNT:-0}"
local brew_cask="${BREW_CASK_OUTDATED_COUNT:-0}"
# Get update labels
local -a appstore_labels=()
if [[ -n "${APPSTORE_UPDATE_COUNT:-}" && "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 ]]; then
while IFS= read -r label; do
[[ -n "$label" ]] && appstore_labels+=("$label")
done < <(get_appstore_update_labels || true)
fi
local -a macos_labels=()
if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" ]]; then
while IFS= read -r label; do
[[ -n "$label" ]] && macos_labels+=("$label")
done < <(get_macos_update_labels || true)
fi
# Check fallback needed
local appstore_needs_fallback=false
local macos_needs_fallback=false
if [[ -n "${APPSTORE_UPDATE_COUNT:-}" && "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 && ${#appstore_labels[@]} -eq 0 ]]; then
appstore_needs_fallback=true
fi
if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" && ${#macos_labels[@]} -eq 0 ]]; then
macos_needs_fallback=true
fi
# Count total updates
((brew_formula > 0)) && ((total_count++))
((brew_cask > 0)) && ((total_count++))
[[ -n "${APPSTORE_UPDATE_COUNT:-}" && "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 ]] && ((total_count++))
[[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" ]] && ((total_count++))
[[ -n "${MOLE_UPDATE_AVAILABLE:-}" && "${MOLE_UPDATE_AVAILABLE}" == "true" ]] && ((total_count++))
# Update Homebrew formulae
if ((brew_formula > 0)); then
echo -e "${BLUE}Updating Homebrew formulae...${NC}"
if brew upgrade 2>&1 | grep -v "^==>" | grep -v "^Warning:" || true; then
echo -e "${GREEN}${NC} Homebrew formulae updated"
reset_brew_cache
((updated_count++))
else
echo -e "${RED}${NC} Homebrew formula update failed"
fi
echo ""
fi
# Update Homebrew casks
if ((brew_cask > 0)); then
echo -e "${BLUE}Updating Homebrew casks...${NC}"
if brew upgrade --cask 2>&1 | grep -v "^==>" | grep -v "^Warning:" || true; then
echo -e "${GREEN}${NC} Homebrew casks updated"
reset_brew_cache
((updated_count++))
else
echo -e "${RED}${NC} Homebrew cask update failed"
fi
echo ""
fi
# Update App Store apps
local macos_handled_via_appstore=false
if [[ -n "${APPSTORE_UPDATE_COUNT:-}" && "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 ]]; then
# Check sudo access
if ! has_sudo_session; then
if ! ensure_sudo_session "Software updates require admin access"; then
echo -e "${YELLOW}${NC} Skipping App Store updates (admin authentication required)"
echo ""
((total_count--))
if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" ]]; then
((total_count--))
fi
else
_perform_appstore_update
fi
else
_perform_appstore_update
fi
fi
# Update macOS
if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" && "$macos_handled_via_appstore" != "true" ]]; then
if ! has_sudo_session; then
echo -e "${YELLOW}${NC} Skipping macOS updates (admin authentication required)"
echo ""
else
_perform_macos_update
fi
fi
# Update Mole
if [[ -n "${MOLE_UPDATE_AVAILABLE:-}" && "${MOLE_UPDATE_AVAILABLE}" == "true" ]]; then
echo -e "${BLUE}Updating Mole...${NC}"
if "${SCRIPT_DIR}/mole" update 2>&1 | grep -qE "(Updated|latest version)"; then
echo -e "${GREEN}${NC} Mole updated"
reset_mole_cache
((updated_count++))
else
echo -e "${RED}${NC} Mole update failed"
fi
echo ""
fi
# Summary
if [[ $updated_count -eq $total_count && $total_count -gt 0 ]]; then
echo -e "${GREEN}All updates completed (${updated_count}/${total_count})${NC}"
return 0
elif [[ $updated_count -gt 0 ]]; then
echo -e "${YELLOW}Partial updates completed (${updated_count}/${total_count})${NC}"
return 1
else
echo -e "${RED}No updates were completed${NC}"
return 1
fi
}
# Internal: Perform App Store update
_perform_appstore_update() {
echo -e "${BLUE}Updating App Store apps...${NC}"
local appstore_log
appstore_log=$(mktemp -t mole-appstore 2>/dev/null || echo "/tmp/mole-appstore.log")
if [[ "$appstore_needs_fallback" == "true" ]]; then
echo -e " ${GRAY}Installing all available updates${NC}"
if sudo softwareupdate -i -a 2>&1 | tee "$appstore_log" | grep -v "^$"; then
echo -e "${GREEN}${NC} Software updates completed"
((updated_count++))
if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" ]]; then
macos_handled_via_appstore=true
((updated_count++))
fi
else
echo -e "${RED}${NC} Software update failed"
fi
else
if sudo softwareupdate -i "${appstore_labels[@]}" 2>&1 | tee "$appstore_log" | grep -v "^$"; then
echo -e "${GREEN}${NC} App Store apps updated"
((updated_count++))
else
echo -e "${RED}${NC} App Store update failed"
fi
fi
rm -f "$appstore_log" 2>/dev/null || true
reset_softwareupdate_cache
echo ""
}
# Internal: Perform macOS update
_perform_macos_update() {
echo -e "${BLUE}Updating macOS...${NC}"
echo -e "${YELLOW}Note:${NC} System update may require restart"
local macos_log
macos_log=$(mktemp -t mole-macos 2>/dev/null || echo "/tmp/mole-macos.log")
if [[ "$macos_needs_fallback" == "true" ]]; then
if sudo softwareupdate -i -r 2>&1 | tee "$macos_log" | grep -v "^$"; then
echo -e "${GREEN}${NC} macOS updated"
((updated_count++))
else
echo -e "${RED}${NC} macOS update failed"
fi
else
if sudo softwareupdate -i "${macos_labels[@]}" 2>&1 | tee "$macos_log" | grep -v "^$"; then
echo -e "${GREEN}${NC} macOS updated"
((updated_count++))
else
echo -e "${RED}${NC} macOS update failed"
fi
fi
if grep -qi "restart" "$macos_log" 2>/dev/null; then
echo -e "${YELLOW}${NC} Restart required to complete update"
fi
rm -f "$macos_log" 2>/dev/null || true
reset_softwareupdate_cache
echo ""
}