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

System optimization and massive updates

This commit is contained in:
Tw93
2025-11-25 11:19:13 +08:00
parent 4aa50ac715
commit a19d525072
11 changed files with 440 additions and 101 deletions

View File

@@ -27,6 +27,7 @@ print_header() {
# System check functions (real-time display)
run_system_checks() {
unset AUTO_FIX_SUMMARY AUTO_FIX_DETAILS
echo ""
echo -e "${PURPLE}System Check${NC}"
echo ""
@@ -44,6 +45,9 @@ run_system_checks() {
# Check security - real-time display
echo -e "${BLUE}${ICON_ARROW}${NC} Security posture"
check_all_security
if ask_for_security_fixes; then
perform_security_fixes
fi
echo ""
# Check configuration - real-time display
@@ -67,30 +71,32 @@ run_system_checks() {
}
show_optimization_summary() {
if [[ -z "${OPTIMIZE_SAFE_COUNT:-}" ]]; then
local safe_count="${OPTIMIZE_SAFE_COUNT:-0}"
local confirm_count="${OPTIMIZE_CONFIRM_COUNT:-0}"
if (( safe_count == 0 && confirm_count == 0 )) && [[ -z "${AUTO_FIX_SUMMARY:-}" ]]; then
return
fi
echo ""
local summary_title="Optimization and Check Complete"
local -a summary_details=()
# Optimization results
if ((OPTIMIZE_SAFE_COUNT > 0)); then
summary_details+=("Applied ${GREEN}${OPTIMIZE_SAFE_COUNT}${NC} optimizations")
summary_details+=("Optimizations: ${GREEN}${safe_count}${NC} applied, ${YELLOW}${confirm_count}${NC} manual checks")
summary_details+=("Caches refreshed; services restarted; system tuned")
summary_details+=("Updates & security reviewed across system")
local summary_line4=""
if [[ -n "${AUTO_FIX_SUMMARY:-}" ]]; then
summary_line4="${AUTO_FIX_SUMMARY}"
if [[ -n "${AUTO_FIX_DETAILS:-}" ]]; then
local detail_join
detail_join=$(echo "${AUTO_FIX_DETAILS}" | paste -sd ", " -)
[[ -n "$detail_join" ]] && summary_line4+="${detail_join}"
fi
else
summary_details+=("System already optimized")
summary_line4="Mac should feel faster and more responsive"
fi
if ((OPTIMIZE_CONFIRM_COUNT > 0)); then
summary_details+=("${YELLOW}${OPTIMIZE_CONFIRM_COUNT}${NC} manual checks suggested")
fi
summary_details+=("Caches cleared, databases rebuilt, services refreshed")
# System check results
summary_details+=("System updates, health, security, and config reviewed")
summary_details+=("System should feel faster and more responsive")
summary_details+=("$summary_line4")
if [[ "${OPTIMIZE_SHOW_TOUCHID_TIP:-false}" == "true" ]]; then
echo -e "${YELLOW}${NC} Run ${GRAY}mo touchid${NC} to approve sudo via Touch ID"
@@ -218,6 +224,97 @@ count_local_snapshots() {
echo "$output" | grep -c "com.apple.TimeMachine." | tr -d ' '
}
declare -a SECURITY_FIXES=()
collect_security_fix_actions() {
SECURITY_FIXES=()
if [[ "${FIREWALL_DISABLED:-}" == "true" ]]; then
SECURITY_FIXES+=("firewall|Enable macOS firewall")
fi
if [[ "${GATEKEEPER_DISABLED:-}" == "true" ]]; then
SECURITY_FIXES+=("gatekeeper|Enable Gatekeeper (App download protection)")
fi
((${#SECURITY_FIXES[@]} > 0))
}
ask_for_security_fixes() {
if ! collect_security_fix_actions; then
return 1
fi
echo -e "${BLUE}SECURITY FIXES${NC}"
for entry in "${SECURITY_FIXES[@]}"; do
IFS='|' read -r _ label <<< "$entry"
echo -e " ${ICON_LIST} $label"
done
echo ""
echo -ne "${YELLOW}Apply now?${NC} ${GRAY}Enter confirm / ESC cancel${NC}: "
local key
if ! key=$(read_key); then
echo "skip"
echo ""
return 1
fi
if [[ "$key" == "ENTER" ]]; then
echo "apply"
echo ""
return 0
else
echo "skip"
echo ""
return 1
fi
}
apply_firewall_fix() {
if sudo defaults write /Library/Preferences/com.apple.alf globalstate -int 1; then
sudo pkill -HUP socketfilterfw 2> /dev/null || true
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Firewall enabled"
FIREWALL_DISABLED=false
return 0
fi
echo -e " ${YELLOW}${ICON_WARNING}${NC} Failed to enable firewall (check permissions)"
return 1
}
apply_gatekeeper_fix() {
if sudo spctl --master-enable 2> /dev/null; then
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Gatekeeper enabled"
GATEKEEPER_DISABLED=false
return 0
fi
echo -e " ${YELLOW}${ICON_WARNING}${NC} Failed to enable Gatekeeper"
return 1
}
perform_security_fixes() {
if ! ensure_sudo_session "Security changes require admin access"; then
echo -e "${YELLOW}${ICON_WARNING}${NC} Skipped security fixes (sudo denied)"
return 1
fi
local applied=0
for entry in "${SECURITY_FIXES[@]}"; do
IFS='|' read -r action _ <<< "$entry"
case "$action" in
firewall)
apply_firewall_fix && ((applied++))
;;
gatekeeper)
apply_gatekeeper_fix && ((applied++))
;;
esac
done
if ((applied > 0)); then
log_success "Security settings updated"
fi
SECURITY_FIXES=()
}
cleanup_all() {
stop_sudo_session
@@ -364,6 +461,9 @@ main() {
local safe_count=${#safe_items[@]}
local confirm_count=${#confirm_items[@]}
# Run system checks first
run_system_checks
export OPTIMIZE_SAFE_COUNT=$safe_count
export OPTIMIZE_CONFIRM_COUNT=$confirm_count
export OPTIMIZE_SHOW_TOUCHID_TIP="false"
@@ -371,9 +471,6 @@ main() {
export OPTIMIZE_SHOW_TOUCHID_TIP="true"
fi
# Run system checks first
run_system_checks
# Show optimization summary at the end
show_optimization_summary

View File

@@ -69,7 +69,7 @@ show_suggestions() {
# 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}"
echo -e " ${YELLOW}${ICON_WARNING}${NC} ${item} ${GREEN}[auto]${NC}"
done
fi
@@ -78,7 +78,7 @@ show_suggestions() {
for item in "${manual_items[@]}"; do
local title="${item%%|*}"
local hint="${item#*|}"
echo -e " ${YELLOW}${NC} ${title}"
echo -e " ${YELLOW}${ICON_WARNING}${NC} ${title}"
echo -e " ${GRAY}${hint}${NC}"
done
fi
@@ -94,7 +94,7 @@ ask_for_auto_fix() {
return 1
fi
echo -ne "Fix issues marked ${GREEN}[auto]${NC}? ${GRAY}Enter yes / ESC no${NC}: "
echo -ne "${PURPLE}${ICON_ARROW}${NC} Auto-fix issues now? ${GRAY}Enter confirm / ESC cancel${NC}: "
local key
if ! key=$(read_key); then
@@ -118,6 +118,7 @@ ask_for_auto_fix() {
# Returns: number of fixes applied
perform_auto_fix() {
local fixed_count=0
local -a fixed_items=()
# Ensure sudo access
if ! has_sudo_session; then
@@ -134,6 +135,7 @@ perform_auto_fix() {
if sudo defaults write /Library/Preferences/com.apple.alf globalstate -int 1 2>/dev/null; then
echo -e "${GREEN}${NC} Firewall enabled"
((fixed_count++))
fixed_items+=("Firewall enabled")
else
echo -e "${RED}${NC} Failed to enable Firewall"
fi
@@ -142,13 +144,14 @@ perform_auto_fix() {
# Fix Touch ID
if [[ -n "${TOUCHID_NOT_CONFIGURED:-}" && "${TOUCHID_NOT_CONFIGURED}" == "true" ]]; then
echo -e "${BLUE}Configuring Touch ID for sudo...${NC}"
echo -e "${BLUE}${ICON_ARROW}${NC} Configuring Touch ID for sudo..."
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++))
fixed_items+=("Touch ID configured for sudo")
else
echo -e "${RED}${NC} Failed to configure Touch ID"
fi
@@ -161,6 +164,7 @@ auth sufficient pam_tid.so
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++))
fixed_items+=("Rosetta 2 installed")
else
echo -e "${RED}${NC} Failed to install Rosetta 2"
fi
@@ -168,11 +172,16 @@ auth sufficient pam_tid.so
fi
if [[ $fixed_count -gt 0 ]]; then
echo -e "${GREEN}Fixed ${fixed_count} issue(s)${NC}"
AUTO_FIX_SUMMARY="Auto fixes applied: ${fixed_count} issue(s)"
if [[ ${#fixed_items[@]} -gt 0 ]]; then
AUTO_FIX_DETAILS=$(printf '%s\n' "${fixed_items[@]}")
else
AUTO_FIX_DETAILS=""
fi
else
echo -e "${YELLOW}No issues were fixed${NC}"
AUTO_FIX_SUMMARY="Auto fixes skipped: No changes were required"
AUTO_FIX_DETAILS=""
fi
echo ""
return $fixed_count
export AUTO_FIX_SUMMARY AUTO_FIX_DETAILS
return 0
}

View File

@@ -19,7 +19,7 @@ check_touchid_sudo() {
fi
if [[ "$is_supported" == "true" ]]; then
echo -e " ${YELLOW}${NC} Touch ID ${YELLOW}Not configured${NC} for sudo"
echo -e " ${YELLOW}${ICON_WARNING}${NC} Touch ID ${YELLOW}Not configured${NC} for sudo"
export TOUCHID_NOT_CONFIGURED=true
fi
fi
@@ -31,7 +31,7 @@ check_rosetta() {
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}"
echo -e " ${YELLOW}${ICON_WARNING}${NC} Rosetta 2 ${YELLOW}Not installed${NC}"
export ROSETTA_NOT_INSTALLED=true
fi
fi
@@ -46,7 +46,7 @@ check_git_config() {
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}"
echo -e " ${YELLOW}${ICON_WARNING}${NC} Git Config ${YELLOW}Not configured${NC}"
fi
fi
}

View File

@@ -12,7 +12,7 @@ check_disk_space() {
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)"
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
@@ -58,7 +58,7 @@ check_memory_usage() {
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)"
echo -e " ${YELLOW}${ICON_WARNING}${NC} Memory ${YELLOW}${used_percent}% used${NC} (High)"
else
echo -e " ${GREEN}${NC} Memory ${used_percent}% used"
fi
@@ -86,7 +86,7 @@ check_login_items() {
fi
if [[ $login_items_count -gt 15 ]]; then
echo -e " ${YELLOW}${NC} Login Items ${YELLOW}${login_items_count} apps${NC} auto-start (High)"
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
@@ -151,9 +151,9 @@ check_cache_size() {
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"
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}${NC} Cache Size ${YELLOW}${cache_size_gb}GB${NC} cleanable"
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
@@ -170,7 +170,7 @@ check_swap_usage() {
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)"
echo -e " ${YELLOW}${ICON_WARNING}${NC} Swap Usage ${YELLOW}${swap_used}${NC} (High)"
else
echo -e " ${GREEN}${NC} Swap Usage ${swap_used}"
fi
@@ -186,7 +186,7 @@ check_timemachine() {
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 " ${YELLOW}${ICON_WARNING}${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
@@ -194,7 +194,7 @@ check_timemachine() {
if [[ -n "$backup_date" ]]; then
echo -e " ${GREEN}${NC} Time Machine Backup active"
else
echo -e " ${YELLOW}${NC} Time Machine Not configured"
echo -e " ${YELLOW}${ICON_WARNING}${NC} Time Machine Not configured"
fi
fi
fi
@@ -220,7 +220,7 @@ check_brew_health() {
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 " ${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

View File

@@ -17,11 +17,12 @@ check_filevault() {
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}${NC} Firewall ${YELLOW}Disabled${NC} (Consider enabling)"
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
@@ -34,10 +35,12 @@ check_gatekeeper() {
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}${NC} Gatekeeper ${YELLOW}Disabled${NC}"
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
}
@@ -49,7 +52,7 @@ check_sip() {
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 " ${YELLOW}${ICON_WARNING}${NC} SIP ${YELLOW}Disabled${NC}"
echo -e " ${GRAY}Restart into Recovery → Utilities → Terminal → run: csrutil enable${NC}"
fi
fi

View File

@@ -94,7 +94,7 @@ check_homebrew_updates() {
elif [[ $cask_count -gt 0 ]]; then
breakdown=" (${cask_count} cask)"
fi
echo -e " ${YELLOW}${NC} Homebrew ${YELLOW}${total_count} updates${NC}${breakdown}"
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"
@@ -134,9 +134,24 @@ get_software_updates() {
}
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 ' ')
@@ -145,7 +160,7 @@ check_appstore_updates() {
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 " ${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"
@@ -153,19 +168,34 @@ check_appstore_updates() {
}
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}${NC} macOS ${YELLOW}${version} available${NC}"
echo -e " ${YELLOW}${ICON_WARNING}${NC} macOS ${YELLOW}${version} available${NC}"
else
echo -e " ${YELLOW}${NC} macOS ${YELLOW}Update available${NC}"
echo -e " ${YELLOW}${ICON_WARNING}${NC} macOS ${YELLOW}Update available${NC}"
fi
echo -e " ${GRAY}Run: ${GREEN}softwareupdate -i <label>${NC}"
else
@@ -220,7 +250,7 @@ check_mole_update() {
# 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 " ${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})"

View File

@@ -356,6 +356,53 @@ drain_pending_input() {
done
}
# Shared timeout helper -------------------------------------------------------
if [[ -z "${MO_TIMEOUT_INITIALIZED:-}" ]]; then
MO_TIMEOUT_BIN=""
for candidate in gtimeout timeout; do
if command -v "$candidate" > /dev/null 2>&1; then
MO_TIMEOUT_BIN="$candidate"
break
fi
done
export MO_TIMEOUT_INITIALIZED=1
fi
run_with_timeout() {
local duration="${1:-0}"
shift || true
if [[ ! "$duration" =~ ^[0-9]+$ ]]; then
duration=0
fi
if [[ "$duration" -le 0 ]]; then
"$@"
return $?
fi
if [[ -n "${MO_TIMEOUT_BIN:-}" ]]; then
"$MO_TIMEOUT_BIN" "$duration" "$@"
return $?
fi
"$@" &
local cmd_pid=$!
local elapsed=0
while kill -0 "$cmd_pid" 2> /dev/null; do
if [[ $elapsed -ge $duration ]]; then
kill -TERM "$cmd_pid" 2> /dev/null || true
sleep 1
kill -KILL "$cmd_pid" 2> /dev/null || true
wait "$cmd_pid" 2> /dev/null || true
return 124
fi
sleep 1
((elapsed++))
done
wait "$cmd_pid"
}
# Menu display helper
show_menu_option() {
local number="$1"
@@ -594,35 +641,41 @@ request_sudo_access() {
[[ -z "$tty_path" ]] && return 1
local password=""
local saved_stty=""
if command -v stty > /dev/null 2>&1; then
saved_stty=$(stty -g < "$tty_path" 2> /dev/null || echo "")
stty -echo < "$tty_path" 2> /dev/null || true
fi
local attempts=0
while ((attempts < 3)); do
local password=""
local saved_stty=""
if command -v stty > /dev/null 2>&1; then
saved_stty=$(stty -g < "$tty_path" 2> /dev/null || echo "")
stty -echo < "$tty_path" 2> /dev/null || true
fi
printf "${PURPLE}${ICON_ARROW}${NC} Enter admin password: " > "$tty_path" 2> /dev/null || true
IFS= read -r password < "$tty_path" || password=""
printf "\n" > "$tty_path" 2> /dev/null || true
printf "${PURPLE}${ICON_ARROW}${NC} Enter admin password: " > "$tty_path" 2> /dev/null || true
IFS= read -r password < "$tty_path" || password=""
printf "\n" > "$tty_path" 2> /dev/null || true
if [[ -n "$saved_stty" ]]; then
stty "$saved_stty" < "$tty_path" 2> /dev/null || stty echo < "$tty_path" 2> /dev/null || true
else
stty echo < "$tty_path" 2> /dev/null || true
fi
if [[ -n "$saved_stty" ]]; then
stty "$saved_stty" < "$tty_path" 2> /dev/null || stty echo < "$tty_path" 2> /dev/null || true
else
stty echo < "$tty_path" 2> /dev/null || true
fi
if [[ -z "$password" ]]; then
unset password
((attempts++))
continue
fi
if printf '%s\n' "$password" | "${sudo_stdin_cmd[@]}" > /dev/null 2>&1; then
unset password
sudo -n true 2> /dev/null || true
return 0
fi
if [[ -z "$password" ]]; then
unset password
return 1
fi
if printf '%s\n' "$password" | "${sudo_stdin_cmd[@]}" > /dev/null 2>&1; then
unset password
sudo -n true 2> /dev/null || true
return 0
fi
unset password
((attempts++))
echo -e "${YELLOW}${ICON_WARNING}${NC} Incorrect password, try again" > "$tty_path" 2> /dev/null || true
done
return 1
}

View File

@@ -4,10 +4,18 @@
set -euo pipefail
readonly MAIL_DOWNLOADS_MIN_KB=5120 # ~5MB threshold
_opt_get_dir_size_kb() {
local path="$1"
[[ -e "$path" ]] || { echo 0; return; }
du -sk "$path" 2> /dev/null | awk '{print $1}' || echo 0
}
# 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
run_with_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..."
@@ -29,9 +37,30 @@ opt_system_maintenance() {
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"
local md_status
md_status=$(mdutil -s / 2> /dev/null || echo "")
if echo "$md_status" | grep -qi "Indexing disabled"; then
echo -e "${GRAY}-${NC} Spotlight indexing disabled, skipping rebuild"
else
# mdutil triggers background indexing - don't wait
run_with_timeout 10 sudo mdutil -E / > /dev/null 2>&1 || true
echo -e "${GREEN}${ICON_SUCCESS}${NC} Spotlight rebuild initiated"
fi
echo -e "${BLUE}${ICON_ARROW}${NC} Refreshing Bluetooth services..."
sudo pkill -f blued 2> /dev/null || true
echo -e "${GREEN}${ICON_SUCCESS}${NC} Bluetooth controller refreshed"
if command -v log > /dev/null 2>&1 && [[ "${MO_ENABLE_LOG_CLEANUP:-0}" == "1" ]]; then
echo -e "${BLUE}${ICON_ARROW}${NC} Compressing system logs..."
if command -v has_sudo_session > /dev/null 2>&1 && ! has_sudo_session; then
echo -e "${YELLOW}!${NC} Skipped log compression ${GRAY}(admin session inactive)${NC}"
elif run_with_timeout 15 sudo -n log erase --all --force > /dev/null 2>&1; then
echo -e "${GREEN}${ICON_SUCCESS}${NC} logarchive trimmed"
else
echo -e "${YELLOW}!${NC} Skipped log compression ${GRAY}(requires Full Disk Access)${NC}"
fi
fi
}
# Cache refresh: update Finder/Safari caches
@@ -68,19 +97,19 @@ opt_maintenance_scripts() {
# 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
if ! run_with_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
if ! run_with_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
if ! run_with_timeout 180 sudo /usr/libexec/repair_packages --repair --standard-pkgs --volume / > /dev/null 2>&1; then
success=false
fi
fi
@@ -162,6 +191,18 @@ opt_mail_downloads() {
"$HOME/Library/Mail Downloads|Mail Downloads"
"$HOME/Library/Containers/com.apple.mail/Data/Library/Mail Downloads|Mail Container Downloads"
)
local total_kb=0
for target in "${mail_dirs[@]}"; do
IFS='|' read -r target_path _ <<< "$target"
total_kb=$((total_kb + $(_opt_get_dir_size_kb "$target_path")))
done
if [[ $total_kb -lt $MAIL_DOWNLOADS_MIN_KB ]]; then
echo -e "${GRAY}-${NC} Only $(bytes_to_human $((total_kb * 1024))) detected, skipping cleanup"
return
fi
for target in "${mail_dirs[@]}"; do
IFS='|' read -r target_path label <<< "$target"
cleanup_path "$target_path" "$label"
@@ -220,16 +261,16 @@ opt_startup_cache() {
fi
if [[ "$macos_version" -ge 11 ]] || [[ "$(uname -m)" == "arm64" ]]; then
if ! timeout 120 sudo kextcache -i / > /dev/null 2>&1; then
if ! run_with_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
if ! run_with_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
run_with_timeout 120 sudo kextcache -system-prelinked-kernel > /dev/null 2>&1 || true
fi
if [[ -t 1 ]]; then
@@ -262,7 +303,7 @@ opt_local_snapshots() {
fi
local success=false
if timeout 180 sudo tmutil thinlocalsnapshots / 9999999999 4 > /dev/null 2>&1; then
if run_with_timeout 180 sudo tmutil thinlocalsnapshots / 9999999999 4 > /dev/null 2>&1; then
success=true
fi

View File

@@ -25,6 +25,17 @@ format_brew_update_label() {
printf " • Homebrew %s" "$detail_str"
}
brew_has_outdated() {
local kind="${1:-formula}"
command -v brew > /dev/null 2>&1 || return 1
if [[ "$kind" == "cask" ]]; then
brew outdated --cask --quiet 2> /dev/null | grep -q .
else
brew outdated --quiet 2> /dev/null | grep -q .
fi
}
# Ask user if they want to update
# Returns: 0 if yes, 1 if no
ask_for_updates() {
@@ -62,7 +73,7 @@ ask_for_updates() {
echo -e "$item"
done
echo ""
echo -ne "${YELLOW}Update all now?${NC} ${GRAY}Enter yes / ESC skip${NC}: "
echo -ne "${YELLOW}Update all now?${NC} ${GRAY}Enter confirm / ESC cancel${NC}: "
local key
if ! key=$(read_key); then
@@ -124,28 +135,80 @@ perform_updates() {
# 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++))
if ! brew_has_outdated "formula"; then
echo -e "${GRAY}-${NC} Homebrew formulae already up to date"
((total_count--))
echo ""
else
echo -e "${RED}${NC} Homebrew formula update failed"
echo -e "${BLUE}Updating Homebrew formulae...${NC}"
local spinner_started=false
if [[ -t 1 ]]; then
start_inline_spinner "Running brew upgrade"
spinner_started=true
fi
local brew_output=""
local brew_status=0
if ! brew_output=$(brew upgrade 2>&1); then
brew_status=$?
fi
if [[ "$spinner_started" == "true" ]]; then
stop_inline_spinner
fi
local filtered_output
filtered_output=$(echo "$brew_output" | grep -Ev "^(==>|Warning:)" || true)
[[ -n "$filtered_output" ]] && echo "$filtered_output"
if [[ ${brew_status:-0} -eq 0 ]]; 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
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++))
if ! brew_has_outdated "cask"; then
echo -e "${GRAY}-${NC} Homebrew casks already up to date"
((total_count--))
echo ""
else
echo -e "${RED}${NC} Homebrew cask update failed"
echo -e "${BLUE}Updating Homebrew casks...${NC}"
local spinner_started=false
if [[ -t 1 ]]; then
start_inline_spinner "Running brew upgrade --cask"
spinner_started=true
fi
local brew_output=""
local brew_status=0
if ! brew_output=$(brew upgrade --cask 2>&1); then
brew_status=$?
fi
if [[ "$spinner_started" == "true" ]]; then
stop_inline_spinner
fi
local filtered_output
filtered_output=$(echo "$brew_output" | grep -Ev "^(==>|Warning:)" || true)
[[ -n "$filtered_output" ]] && echo "$filtered_output"
if [[ ${brew_status:-0} -eq 0 ]]; 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
echo ""
fi
# Update App Store apps
@@ -154,7 +217,7 @@ perform_updates() {
# 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 -e "${YELLOW}${ICON_WARNING}${NC} Skipping App Store updates (admin authentication required)"
echo ""
((total_count--))
if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" ]]; then
@@ -171,7 +234,7 @@ perform_updates() {
# 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 -e "${YELLOW}${ICON_WARNING}${NC} Skipping macOS updates (admin authentication required)"
echo ""
else
_perform_macos_update
@@ -260,7 +323,7 @@ _perform_macos_update() {
fi
if grep -qi "restart" "$macos_log" 2>/dev/null; then
echo -e "${YELLOW}${NC} Restart required to complete update"
echo -e "${YELLOW}${ICON_WARNING}${NC} Restart required to complete update"
fi
rm -f "$macos_log" 2>/dev/null || true

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bats
setup_file() {
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
export PROJECT_ROOT
}
@test "run_with_timeout succeeds without GNU timeout" {
run bash --noprofile --norc -c '
set -euo pipefail
PATH="/usr/bin:/bin"
source "'"$PROJECT_ROOT"'/lib/common.sh"
run_with_timeout 1 sleep 0.1
'
[ "$status" -eq 0 ]
}
@test "run_with_timeout enforces timeout and returns 124" {
run bash --noprofile --norc -c '
set -euo pipefail
PATH="/usr/bin:/bin"
source "'"$PROJECT_ROOT"'/lib/common.sh"
run_with_timeout 1 sleep 5
'
[ "$status" -eq 124 ]
}

View File

@@ -97,3 +97,20 @@ setup() {
fi
[ "$status" -ne 0 ]
}
@test "mo clean --whitelist persists selections" {
whitelist_file="$HOME/.config/mole/whitelist"
mkdir -p "$(dirname "$whitelist_file")"
run bash --noprofile --norc -c "cd '$PROJECT_ROOT'; printf \$'\\n' | HOME='$HOME' ./mo clean --whitelist"
[ "$status" -eq 0 ]
grep -q "\\.m2/repository" "$whitelist_file"
run bash --noprofile --norc -c "cd '$PROJECT_ROOT'; printf \$' \\n' | HOME='$HOME' ./mo clean --whitelist"
[ "$status" -eq 0 ]
! grep -q "\\.m2/repository" "$whitelist_file"
run bash --noprofile --norc -c "cd '$PROJECT_ROOT'; printf \$'\\n' | HOME='$HOME' ./mo clean --whitelist"
[ "$status" -eq 0 ]
! grep -q "\\.m2/repository" "$whitelist_file"
}