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:
94
bin/check.sh
Executable file
94
bin/check.sh
Executable 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 "$@"
|
||||
554
bin/optimize.sh
554
bin/optimize.sh
@@ -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
178
lib/autofix_manager.sh
Normal 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
58
lib/check_config.sh
Normal 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
239
lib/check_health.sh
Normal 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
63
lib/check_security.sh
Normal 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
273
lib/check_updates.sh
Normal 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
|
||||
}
|
||||
}
|
||||
'
|
||||
}
|
||||
104
lib/common.sh
104
lib/common.sh
@@ -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
332
lib/optimization_tasks.sh
Normal 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
147
lib/sudo_manager.sh
Normal 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
269
lib/update_manager.sh
Normal 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
4
mole
@@ -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)")"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user