diff --git a/lib/check/all.sh b/lib/check/all.sh index c419a68..1d6b4ee 100644 --- a/lib/check/all.sh +++ b/lib/check/all.sh @@ -207,68 +207,6 @@ is_cache_valid() { [[ $cache_age -lt $ttl ]] } -check_homebrew_updates() { - # Check whitelist - if command -v is_whitelisted > /dev/null && is_whitelisted "check_brew_updates"; then return; fi - if ! command -v brew > /dev/null 2>&1; then - return - fi - - local cache_file="$CACHE_DIR/brew_updates" - local formula_count=0 - local cask_count=0 - - if is_cache_valid "$cache_file"; then - read -r formula_count cask_count < "$cache_file" 2> /dev/null || true - formula_count=${formula_count:-0} - cask_count=${cask_count:-0} - else - # Show spinner while checking - if [[ -t 1 ]]; then - start_inline_spinner "Checking Homebrew..." - fi - - local outdated_list="" - outdated_list=$(brew outdated --quiet 2> /dev/null || echo "") - if [[ -n "$outdated_list" ]]; then - formula_count=$(echo "$outdated_list" | wc -l | tr -d ' ') - fi - - local cask_list="" - cask_list=$(brew outdated --cask --quiet 2> /dev/null || echo "") - if [[ -n "$cask_list" ]]; then - cask_count=$(echo "$cask_list" | wc -l | tr -d ' ') - fi - - echo "$formula_count $cask_count" > "$cache_file" 2> /dev/null || true - - # Stop spinner before output - if [[ -t 1 ]]; then - stop_inline_spinner - fi - fi - - local total_count=$((formula_count + cask_count)) - export BREW_FORMULA_OUTDATED_COUNT=$formula_count - export BREW_CASK_OUTDATED_COUNT=$cask_count - export BREW_OUTDATED_COUNT=$total_count - - if [[ $total_count -gt 0 ]]; then - local breakdown="" - if [[ $formula_count -gt 0 && $cask_count -gt 0 ]]; then - breakdown=" (${formula_count} formula, ${cask_count} cask)" - elif [[ $formula_count -gt 0 ]]; then - breakdown=" (${formula_count} formula)" - elif [[ $cask_count -gt 0 ]]; then - breakdown=" (${cask_count} cask)" - fi - echo -e " ${YELLOW}${ICON_WARNING}${NC} Homebrew ${YELLOW}${total_count} updates${NC}${breakdown}" - echo -e " ${GRAY}Run: ${GREEN}brew upgrade${NC} ${GRAY}and/or${NC} ${GREEN}brew upgrade --cask${NC}" - else - echo -e " ${GREEN}✓${NC} Homebrew Up to date" - fi -} - # Cache software update list to avoid calling softwareupdate twice SOFTWARE_UPDATE_LIST="" @@ -300,13 +238,36 @@ check_macos_update() { local updates_available="false" if [[ $(get_software_updates) == "Updates Available" ]]; then updates_available="true" + + # Verify with softwareupdate -l (short timeout) to reduce false positives + local sw_output="" + local sw_status=0 + local spinner_started=false + if [[ -t 1 ]]; then + start_inline_spinner "Checking macOS updates..." + spinner_started=true + fi + + if ! sw_output=$(run_with_timeout 5 softwareupdate -l 2> /dev/null); then + sw_status=$? + fi + + if [[ "$spinner_started" == "true" ]]; then + stop_inline_spinner + fi + + # If command failed, timed out, or returned empty, treat as no updates to avoid false positives + if [[ $sw_status -ne 0 || -z "$sw_output" ]]; then + updates_available="false" + elif echo "$sw_output" | grep -q "No new software available"; then + updates_available="false" + fi fi export MACOS_UPDATE_AVAILABLE="$updates_available" if [[ "$updates_available" == "true" ]]; then echo -e " ${YELLOW}${ICON_WARNING}${NC} macOS ${YELLOW}Update available${NC}" - echo -e " ${GRAY}update available in final step${NC}" else echo -e " ${GREEN}✓${NC} macOS Up to date" fi @@ -375,8 +336,6 @@ 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 # Only redirect stdout, keep stderr for spinner display get_software_updates > /dev/null @@ -601,11 +560,6 @@ check_swap_usage() { check_brew_health() { # Check whitelist if command -v is_whitelisted > /dev/null && is_whitelisted "check_brew_health"; then return; fi - # Check Homebrew status (fast) - if command -v brew > /dev/null 2>&1; then - # Skip slow 'brew doctor' check by default - echo -e " ${GREEN}✓${NC} Homebrew Installed" - fi } check_system_health() { @@ -615,5 +569,4 @@ check_system_health() { check_login_items check_cache_size # Time Machine check is optional; skip by default to avoid noise on systems without backups - check_brew_health } diff --git a/lib/core/file_ops.sh b/lib/core/file_ops.sh index 23519a9..328d3dc 100644 --- a/lib/core/file_ops.sh +++ b/lib/core/file_ops.sh @@ -160,24 +160,21 @@ safe_find_delete() { debug_log "Finding in $base_dir: $pattern (age: ${age_days}d, type: $type_filter)" - # Execute find with safety limits (maxdepth 5 covers most app cache structures) - if [[ "$age_days" -eq 0 ]]; then - # Delete all matching files without time restriction - command find "$base_dir" \ - -maxdepth 5 \ - -name "$pattern" \ - -type "$type_filter" \ - -delete 2> /dev/null || true # Suppress expected errors when files are removed or protected by other processes - else - # Delete files older than age_days - command find "$base_dir" \ - -maxdepth 5 \ - -name "$pattern" \ - -type "$type_filter" \ - -mtime "+$age_days" \ - -delete 2> /dev/null || true # Suppress expected errors when files are removed or protected by other processes + local find_args=("-maxdepth" "5" "-name" "$pattern" "-type" "$type_filter") + if [[ "$age_days" -gt 0 ]]; then + find_args+=("-mtime" "+$age_days") fi + # Iterate results to respect should_protect_path when available + while IFS= read -r -d '' match; do + if command -v should_protect_path > /dev/null 2>&1; then + if should_protect_path "$match"; then + continue + fi + fi + safe_remove "$match" true || true + done < <(command find "$base_dir" "${find_args[@]}" -print0 2> /dev/null || true) + return 0 } @@ -207,22 +204,21 @@ safe_sudo_find_delete() { debug_log "Finding (sudo) in $base_dir: $pattern (age: ${age_days}d, type: $type_filter)" - # Execute find with sudo - if [[ "$age_days" -eq 0 ]]; then - sudo find "$base_dir" \ - -maxdepth 5 \ - -name "$pattern" \ - -type "$type_filter" \ - -delete 2> /dev/null || true # Ignore transient errors for system files that might be in use or protected - else - sudo find "$base_dir" \ - -maxdepth 5 \ - -name "$pattern" \ - -type "$type_filter" \ - -mtime "+$age_days" \ - -delete 2> /dev/null || true # Ignore transient errors for system files that might be in use or protected + local find_args=("-maxdepth" "5" "-name" "$pattern" "-type" "$type_filter") + if [[ "$age_days" -gt 0 ]]; then + find_args+=("-mtime" "+$age_days") fi + # Iterate results to respect should_protect_path when available + while IFS= read -r -d '' match; do + if command -v should_protect_path > /dev/null 2>&1; then + if should_protect_path "$match"; then + continue + fi + fi + safe_sudo_remove "$match" || true + done < <(sudo find "$base_dir" "${find_args[@]}" -print0 2> /dev/null || true) + return 0 } diff --git a/lib/manage/update.sh b/lib/manage/update.sh index 8337cfd..996a16e 100644 --- a/lib/manage/update.sh +++ b/lib/manage/update.sh @@ -76,9 +76,9 @@ ask_for_updates() { echo -e "$item" done echo "" - - # If Mole has updates, offer to update it + # If only Mole is relevant for automation, prompt just for Mole if [[ "${MOLE_UPDATE_AVAILABLE:-}" == "true" ]]; then + echo "" echo -ne "${YELLOW}Update Mole now?${NC} ${GRAY}Enter confirm / ESC cancel${NC}: " local key @@ -92,55 +92,33 @@ ask_for_updates() { echo "yes" echo "" return 0 - else - echo "skip" - echo "" - return 1 fi fi - # For other updates, just show instructions - # (Mole update check above handles the return 0 case, so we only get here if no Mole update) - if [[ "${BREW_OUTDATED_COUNT:-0}" -gt 0 ]]; then - echo -e "${YELLOW}Tip:${NC} Run ${GREEN}brew upgrade${NC} to update Homebrew packages" - fi - if [[ "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 ]]; then - echo -e "${YELLOW}Tip:${NC} Open ${BLUE}App Store${NC} to update apps" - fi - if [[ "${MACOS_UPDATE_AVAILABLE:-}" == "true" ]]; then - echo -e "${YELLOW}Tip:${NC} Open ${BLUE}System Settings${NC} to update macOS" - fi echo "" + echo -e "${YELLOW}Tip:${NC} Homebrew: brew upgrade / brew upgrade --cask" + echo -e "${YELLOW}Tip:${NC} App Store: open App Store → Updates" + echo -e "${YELLOW}Tip:${NC} macOS: System Settings → General → Software Update" return 1 } # Perform all pending updates # Returns: 0 if all succeeded, 1 if some failed perform_updates() { - # Only handle Mole updates here - # Other updates are now informational-only in ask_for_updates - + # Only handle Mole updates here; Homebrew/App Store/macOS are manual (tips shown in ask_for_updates) local updated_count=0 + local total_count=0 - # Update Mole if [[ -n "${MOLE_UPDATE_AVAILABLE:-}" && "${MOLE_UPDATE_AVAILABLE}" == "true" ]]; then echo -e "${BLUE}Updating Mole...${NC}" - # Try to find mole executable local mole_bin="${SCRIPT_DIR}/../../mole" [[ ! -f "$mole_bin" ]] && mole_bin=$(command -v mole 2> /dev/null || echo "") if [[ -x "$mole_bin" ]]; then - # We use exec here or just run it? - # If we run 'mole update', it replaces the script. - # Since this function is part of a sourced script, replacing the file on disk is risky while running. - # However, 'mole update' script usually handles this by downloading to a temp file and moving it. - # But the shell might not like the file changing under it. - # The original code ran it this way, so we assume it's safe enough or handled by mole update implementation. - if "$mole_bin" update 2>&1 | grep -qE "(Updated|latest version)"; then echo -e "${GREEN}✓${NC} Mole updated" reset_mole_cache - updated_count=1 + ((updated_count++)) else echo -e "${RED}✗${NC} Mole update failed" fi @@ -148,11 +126,17 @@ perform_updates() { echo -e "${RED}✗${NC} Mole executable not found" fi echo "" + total_count=1 fi - if [[ $updated_count -gt 0 ]]; then + if [[ $total_count -eq 0 ]]; then + echo -e "${GRAY}No updates to perform${NC}" + return 0 + elif [[ $updated_count -eq $total_count ]]; then + echo -e "${GREEN}All updates completed (${updated_count}/${total_count})${NC}" return 0 else + echo -e "${RED}Update failed (${updated_count}/${total_count})${NC}" return 1 fi } diff --git a/mole b/mole index 2b0340c..7ffa22b 100755 --- a/mole +++ b/mole @@ -22,7 +22,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/lib/core/common.sh" # Version info -VERSION="1.13.12" +VERSION="1.13.13" MOLE_TAGLINE="Deep clean and optimize your Mac." # Check TouchID configuration diff --git a/tests/system_maintenance.bats b/tests/system_maintenance.bats index d3e7e36..8728e26 100644 --- a/tests/system_maintenance.bats +++ b/tests/system_maintenance.bats @@ -156,39 +156,6 @@ EOF [[ "$output" == *"Homebrew cleanup"* ]] } -@test "check_homebrew_updates reports counts and uses cache" { - run bash --noprofile --norc <<'EOF' -set -euo pipefail -source "$PROJECT_ROOT/lib/core/common.sh" -source "$PROJECT_ROOT/lib/check/all.sh" - -brew() { - if [[ "$1" == "outdated" && "$2" == "--quiet" ]]; then - echo "pkg1" - echo "pkg2" - return 0 - fi - if [[ "$1" == "outdated" && "$2" == "--cask" ]]; then - echo "cask1" - return 0 - fi - return 0 -} - -start_inline_spinner(){ :; } -stop_inline_spinner(){ :; } - -check_homebrew_updates - -# second call should read cache (no spinner) -check_homebrew_updates -EOF - - [ "$status" -eq 0 ] - [[ "$output" == *"Homebrew"* ]] - [[ "$output" == *"2 formula"* ]] -} - @test "check_appstore_updates is skipped for performance" { run bash --noprofile --norc <<'EOF' set -euo pipefail