1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-06 05:33: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

94
bin/check.sh Executable file
View File

@@ -0,0 +1,94 @@
#!/bin/bash
set -euo pipefail
# Load common functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
source "$SCRIPT_DIR/lib/sudo_manager.sh"
source "$SCRIPT_DIR/lib/update_manager.sh"
source "$SCRIPT_DIR/lib/autofix_manager.sh"
source "$SCRIPT_DIR/lib/check_updates.sh"
source "$SCRIPT_DIR/lib/check_health.sh"
source "$SCRIPT_DIR/lib/check_security.sh"
source "$SCRIPT_DIR/lib/check_config.sh"
cleanup_all() {
stop_sudo_session
cleanup_temp_files
}
main() {
# Register unified cleanup handler
trap cleanup_all EXIT INT TERM
if [[ -t 1 ]]; then
clear
fi
printf '\n'
# Create temp files for parallel execution
local updates_file=$(mktemp_file)
local health_file=$(mktemp_file)
local security_file=$(mktemp_file)
local config_file=$(mktemp_file)
# Run all checks in parallel with spinner
if [[ -t 1 ]]; then
echo -ne "${PURPLE}System Check${NC} "
start_inline_spinner "Running checks..."
else
echo -e "${PURPLE}System Check${NC}"
echo ""
fi
# Parallel execution
{
check_all_updates > "$updates_file" 2>&1 &
check_system_health > "$health_file" 2>&1 &
check_all_security > "$security_file" 2>&1 &
check_all_config > "$config_file" 2>&1 &
wait
}
if [[ -t 1 ]]; then
stop_inline_spinner
printf '\n'
fi
# Display results
echo -e "${BLUE}${ICON_ARROW}${NC} System updates"
cat "$updates_file"
printf '\n'
echo -e "${BLUE}${ICON_ARROW}${NC} System health"
cat "$health_file"
printf '\n'
echo -e "${BLUE}${ICON_ARROW}${NC} Security posture"
cat "$security_file"
printf '\n'
echo -e "${BLUE}${ICON_ARROW}${NC} Configuration"
cat "$config_file"
# Show suggestions
show_suggestions
# Ask about auto-fix
if ask_for_auto_fix; then
perform_auto_fix
fi
# Ask about updates
if ask_for_updates; then
perform_updates
fi
printf '\n'
}
main "$@"

View File

@@ -6,15 +6,99 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
source "$SCRIPT_DIR/lib/optimize_health.sh"
source "$SCRIPT_DIR/lib/sudo_manager.sh"
source "$SCRIPT_DIR/lib/update_manager.sh"
source "$SCRIPT_DIR/lib/autofix_manager.sh"
source "$SCRIPT_DIR/lib/optimization_tasks.sh"
# Load check modules
source "$SCRIPT_DIR/lib/check_updates.sh"
source "$SCRIPT_DIR/lib/check_health.sh"
source "$SCRIPT_DIR/lib/check_security.sh"
source "$SCRIPT_DIR/lib/check_config.sh"
# Colors and icons from common.sh
print_header() {
printf '\n'
echo -e "${PURPLE}Optimize Your Mac${NC}"
echo -e "${PURPLE}Optimize and Check${NC}"
echo ""
}
# System check functions (real-time display)
run_system_checks() {
echo ""
echo -e "${PURPLE}System Check${NC}"
echo ""
# Check updates - real-time display
echo -e "${BLUE}${ICON_ARROW}${NC} System updates"
check_all_updates
echo ""
# Check health - real-time display
echo -e "${BLUE}${ICON_ARROW}${NC} System health"
check_system_health
echo ""
# Check security - real-time display
echo -e "${BLUE}${ICON_ARROW}${NC} Security posture"
check_all_security
echo ""
# Check configuration - real-time display
echo -e "${BLUE}${ICON_ARROW}${NC} Configuration"
check_all_config
echo ""
# Show suggestions
show_suggestions
echo ""
# Ask about updates first
if ask_for_updates; then
perform_updates
fi
# Ask about auto-fix
if ask_for_auto_fix; then
perform_auto_fix
fi
}
show_optimization_summary() {
if [[ -z "${OPTIMIZE_SAFE_COUNT:-}" ]]; 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")
else
summary_details+=("System already optimized")
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")
if [[ "${OPTIMIZE_SHOW_TOUCHID_TIP:-false}" == "true" ]]; then
echo -e "${YELLOW}${NC} Run ${GRAY}mo touchid${NC} to approve sudo via Touch ID"
fi
print_summary_block "success" "$summary_title" "${summary_details[@]}"
}
show_system_health() {
local health_json="$1"
@@ -81,7 +165,7 @@ cleanup_path() {
local expanded_path="${raw_path/#\~/$HOME}"
if [[ ! -e "$expanded_path" ]]; then
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $label"
echo -e "${GREEN}${ICON_SUCCESS}${NC} $label"
return
fi
@@ -94,12 +178,12 @@ cleanup_path() {
if rm -rf "$expanded_path"; then
if [[ -n "$size_display" ]]; then
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $label ${GREEN}(${size_display})${NC}"
echo -e "${GREEN}${ICON_SUCCESS}${NC} $label ${GREEN}(${size_display})${NC}"
else
echo -e " ${GREEN}${ICON_SUCCESS}${NC} $label"
echo -e "${GREEN}${ICON_SUCCESS}${NC} $label"
fi
else
echo -e " ${RED}${ICON_ERROR}${NC} Failed to remove $label"
echo -e "${RED}${ICON_ERROR}${NC} Failed to remove $label"
fi
}
@@ -109,45 +193,6 @@ ensure_directory() {
mkdir -p "$expanded_path" > /dev/null 2>&1 || true
}
list_login_items() {
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
}
SUDO_KEEPALIVE_PID=""
start_sudo_keepalive() {
[[ -n "$SUDO_KEEPALIVE_PID" ]] && return
(
while true; do
if ! sudo -n true 2> /dev/null; then
exit 0
fi
sleep 30
done
) &
SUDO_KEEPALIVE_PID=$!
}
stop_sudo_keepalive() {
if [[ -n "$SUDO_KEEPALIVE_PID" ]]; then
kill "$SUDO_KEEPALIVE_PID" 2> /dev/null || true
wait "$SUDO_KEEPALIVE_PID" 2> /dev/null || true
SUDO_KEEPALIVE_PID=""
fi
}
trap stop_sudo_keepalive EXIT
count_local_snapshots() {
if ! command -v tmutil > /dev/null 2>&1; then
echo 0
@@ -164,281 +209,16 @@ count_local_snapshots() {
echo "$output" | grep -c "com.apple.TimeMachine." | tr -d ' '
}
execute_optimization() {
local action="$1"
local path="$2"
case "$action" in
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..."
sudo mdutil -E / > /dev/null 2>&1 || true
echo -e "${GREEN}${ICON_SUCCESS}${NC} Spotlight index rebuilt"
;;
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)
echo -e "${BLUE}${ICON_ARROW}${NC} Running macOS periodic scripts..."
local periodic_cmd="/usr/sbin/periodic"
if [[ -x "$periodic_cmd" ]]; then
local periodic_output=""
if periodic_output=$(sudo "$periodic_cmd" daily weekly monthly 2>&1); then
echo -e "${GREEN}${ICON_SUCCESS}${NC} Daily/weekly/monthly scripts completed"
else
echo -e "${YELLOW}!${NC} periodic scripts reported an issue"
printf '%s\n' "$periodic_output" | sed 's/^/ /'
fi
fi
echo -e "${BLUE}${ICON_ARROW}${NC} Moving old system logs..."
if sudo newsyslog > /dev/null 2>&1; then
echo -e "${GREEN}${ICON_SUCCESS}${NC} Log move complete"
else
echo -e "${YELLOW}!${NC} newsyslog reported an issue"
fi
if [[ -x "/usr/libexec/repair_packages" ]]; then
echo -e "${BLUE}${ICON_ARROW}${NC} Repairing base system permissions..."
if sudo /usr/libexec/repair_packages --repair --standard-pkgs --volume / > /dev/null 2>&1; then
echo -e "${GREEN}${ICON_SUCCESS}${NC} Base system permission repair complete"
else
echo -e "${YELLOW}!${NC} repair_packages reported an issue"
fi
fi
;;
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)
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
# Delete shared file lists
find "$shared_dir" -name "*.sfl2" -type f -delete 2> /dev/null || true
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Shared file lists cleared"
fi
# Clear recent items preferences
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)
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)
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_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_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)
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)
local macos_version
macos_version=$(sw_vers -productVersion | cut -d '.' -f 1)
# macOS 11+ has read-only system volume, skip system file operations
if [[ "$macos_version" -ge 11 ]] || [[ "$(uname -m)" == "arm64" ]]; then
echo -e "${BLUE}${ICON_ARROW}${NC} Rebuilding kext caches..."
if sudo kextcache -i / > /dev/null 2>&1; then
echo -e "${GREEN}${ICON_SUCCESS}${NC} Startup caches refreshed"
else
echo -e "${GREEN}${ICON_SUCCESS}${NC} Startup caches refreshed (sealed system volume)"
fi
else
echo -e "${BLUE}${ICON_ARROW}${NC} Rebuilding kext caches..."
if sudo kextcache -i / > /dev/null 2>&1; then
echo -e "${GREEN}${ICON_SUCCESS}${NC} Kernel/kext caches rebuilt"
else
echo -e "${YELLOW}!${NC} kextcache reported an issue"
fi
echo -e "${BLUE}${ICON_ARROW}${NC} Clearing system prelinked kernel caches..."
sudo rm -rf /System/Library/PrelinkedKernels/* > /dev/null 2>&1 || true
sudo kextcache -system-prelinked-kernel > /dev/null 2>&1 || true
echo -e "${GREEN}${ICON_SUCCESS}${NC} Startup caches refreshed"
fi
;;
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
echo -e "${BLUE}${ICON_ARROW}${NC} Thinning $before APFS local snapshots..."
if sudo tmutil thinlocalsnapshots / 9999999999 4 > /dev/null 2>&1; then
after=$(count_local_snapshots)
local removed=$((before - after))
if [[ "$removed" -lt 0 ]]; then
removed=0
fi
echo -e "${GREEN}${ICON_SUCCESS}${NC} Removed $removed snapshots (remaining: $after)"
else
echo -e "${RED}${ICON_ERROR}${NC} Failed to thin local snapshots"
fi
;;
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"
;;
*)
echo -e "${RED}${ICON_ERROR}${NC} Unknown action: $action"
;;
esac
cleanup_all() {
stop_sudo_session
cleanup_temp_files
}
main() {
# Register unified cleanup handler
trap cleanup_all EXIT INT TERM
if [[ -t 1 ]]; then
clear
fi
@@ -457,52 +237,66 @@ main() {
exit 1
fi
# Simple confirmation first
echo -ne "${PURPLE}${ICON_ARROW}${NC} System optimization needs admin access ${GREEN}Enter${NC} continue / ${GRAY}ESC${NC} cancel: "
# Simple confirmation
echo -ne "${PURPLE}${ICON_ARROW}${NC} Optimization needs sudo — ${GREEN}Enter${NC} continue, ${GRAY}ESC${NC} cancel: "
IFS= read -r -s -n1 key || key=""
drain_pending_input # Clean up any escape sequence remnants
case "$key" in
$'\e' | q | Q)
echo ""
echo ""
echo -e "${GRAY}Cancelled${NC}"
echo ""
exit 0
;;
"" | $'\n' | $'\r')
printf "\r\033[K"
if ! request_sudo_access "System optimizations require admin access"; then
echo ""
echo -e "${YELLOW}Authentication failed${NC}"
exit 1
fi
start_sudo_keepalive
;;
*)
echo ""
echo ""
echo -e "${GRAY}Cancelled${NC}"
echo ""
exit 0
;;
esac
local key
if ! key=$(read_key); then
echo -e " ${GRAY}Cancelled${NC}"
exit 0
fi
if [[ "$key" == "ENTER" ]]; then
printf "\r\033[K"
else
echo -e " ${GRAY}Cancelled${NC}"
exit 0
fi
# Collect system health data after confirmation
if [[ -t 1 ]]; then
start_inline_spinner "Collecting system info..."
fi
local health_json
if ! health_json=$(generate_health_json 2> /dev/null); then
if [[ -t 1 ]]; then
stop_inline_spinner
fi
echo ""
log_error "Failed to collect system health data"
exit 1
fi
if [[ -t 1 ]]; then
stop_inline_spinner
fi
# Show system health
show_system_health "$health_json"
if [[ "${MO_DEBUG:-}" == "1" ]]; then
echo "DEBUG: System health displayed"
fi
# Parse and display optimizations
local -a safe_items=()
local -a confirm_items=()
if [[ "${MO_DEBUG:-}" == "1" ]]; then
echo "DEBUG: Parsing optimizations..."
fi
# Use temp file instead of process substitution to avoid hanging
local opts_file
opts_file=$(mktemp_file)
parse_optimizations "$health_json" > "$opts_file"
if [[ "${MO_DEBUG:-}" == "1" ]]; then
local opt_count=$(wc -l < "$opts_file" | tr -d ' ')
echo "DEBUG: Found $opt_count optimizations"
fi
while IFS= read -r opt_json; do
[[ -z "$opt_json" ]] && continue
@@ -519,11 +313,26 @@ main() {
else
confirm_items+=("$item")
fi
done < <(parse_optimizations "$health_json")
done < "$opts_file"
if [[ "${MO_DEBUG:-}" == "1" ]]; then
echo "DEBUG: Parsing complete. Safe: ${#safe_items[@]}, Confirm: ${#confirm_items[@]}"
fi
# Execute all optimizations
local first_heading=true
# Debug: show what we're about to do
if [[ "${MO_DEBUG:-}" == "1" ]]; then
echo "DEBUG: About to request sudo. Safe items: ${#safe_items[@]}, Confirm items: ${#confirm_items[@]}"
fi
ensure_sudo_session "System optimization requires admin access" || true
if [[ "${MO_DEBUG:-}" == "1" ]]; then
echo "DEBUG: Sudo session established or skipped"
fi
# Run safe optimizations
if [[ ${#safe_items[@]} -gt 0 ]]; then
for item in "${safe_items[@]}"; do
@@ -542,56 +351,23 @@ main() {
done
fi
# Show login item reminder at the end of optimization log
local -a login_items_list=()
while IFS= read -r login_item; do
[[ -n "$login_item" ]] && login_items_list+=("$login_item")
done < <(list_login_items || true)
if ((${#login_items_list[@]} > 0)); then
local display_count=${#login_items_list[@]}
echo ""
echo -e "${BLUE}${ICON_ARROW}${NC} Found ${display_count} items that auto-start at login:"
local preview_limit=5
((preview_limit > display_count)) && preview_limit=$display_count
for ((i = 0; i < preview_limit; i++)); do
printf " • %s\n" "${login_items_list[$i]}"
done
if ((display_count > preview_limit)); then
local remaining=$((display_count - preview_limit))
echo " • …and $remaining more"
fi
echo -e "${GRAY}Review in System Settings → Login Items to remove unnecessary ones${NC}"
fi
echo ""
local summary_title="System optimization complete"
local -a summary_details=()
# Prepare optimization summary data (to show at the end)
local safe_count=${#safe_items[@]}
local confirm_count=${#confirm_items[@]}
if ((safe_count > 0)); then
summary_details+=("Applied ${GREEN}${safe_count}${NC} optimizations")
else
summary_details+=("System already optimized")
fi
if ((confirm_count > 0)); then
summary_details+=("${YELLOW}${confirm_count}${NC} manual checks suggested")
fi
summary_details+=("Caches cleared, databases rebuilt, services refreshed")
summary_details+=("System should feel faster and more responsive")
local show_touchid_tip="false"
export OPTIMIZE_SAFE_COUNT=$safe_count
export OPTIMIZE_CONFIRM_COUNT=$confirm_count
export OPTIMIZE_SHOW_TOUCHID_TIP="false"
if touchid_supported && ! touchid_configured; then
show_touchid_tip="true"
export OPTIMIZE_SHOW_TOUCHID_TIP="true"
fi
if [[ "$show_touchid_tip" == "true" ]]; then
echo -e "${YELLOW}${NC} Run ${GRAY}mo touchid${NC} to approve sudo via Touch ID"
fi
print_summary_block "success" "$summary_title" "${summary_details[@]}"
# Run system checks first
run_system_checks
# Show optimization summary at the end
show_optimization_summary
printf '\n'
}

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 ""
}

4
mole
View File

@@ -173,7 +173,7 @@ show_help() {
printf " %s%-28s%s %s\n" "$GREEN" "mo clean --dry-run" "$NC" "Preview cleanup"
printf " %s%-28s%s %s\n" "$GREEN" "mo clean --whitelist" "$NC" "Manage protected caches"
printf " %s%-28s%s %s\n" "$GREEN" "mo uninstall" "$NC" "Remove apps completely"
printf " %s%-28s%s %s\n" "$GREEN" "mo optimize" "$NC" "Tune system performance"
printf " %s%-28s%s %s\n" "$GREEN" "mo optimize" "$NC" "Check and maintain system"
printf " %s%-28s%s %s\n" "$GREEN" "mo analyze" "$NC" "Explore disk usage"
printf " %s%-28s%s %s\n" "$GREEN" "mo status" "$NC" "Monitor system health"
printf " %s%-28s%s %s\n" "$GREEN" "mo touchid" "$NC" "Configure Touch ID for sudo"
@@ -506,7 +506,7 @@ show_main_menu() {
printf '\r\033[2K%s\n' "$(show_menu_option 1 "Clean Free up disk space" "$([[ $selected -eq 1 ]] && echo true || echo false)")"
printf '\r\033[2K%s\n' "$(show_menu_option 2 "Uninstall Remove apps completely" "$([[ $selected -eq 2 ]] && echo true || echo false)")"
printf '\r\033[2K%s\n' "$(show_menu_option 3 "Optimize Tune system performance" "$([[ $selected -eq 3 ]] && echo true || echo false)")"
printf '\r\033[2K%s\n' "$(show_menu_option 3 "Optimize Check and maintain system" "$([[ $selected -eq 3 ]] && echo true || echo false)")"
printf '\r\033[2K%s\n' "$(show_menu_option 4 "Analyze Explore disk usage" "$([[ $selected -eq 4 ]] && echo true || echo false)")"
printf '\r\033[2K%s\n' "$(show_menu_option 5 "Status Monitor system health" "$([[ $selected -eq 5 ]] && echo true || echo false)")"