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

Reconstruct clean lib code

This commit is contained in:
Tw93
2025-12-01 16:58:35 +08:00
parent 1578988ede
commit 4bd4ffc7be
43 changed files with 1105 additions and 1098 deletions

View File

@@ -4,15 +4,12 @@ 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/core/common.sh"
source "$SCRIPT_DIR/lib/core/sudo.sh"
source "$SCRIPT_DIR/lib/manage/update.sh"
source "$SCRIPT_DIR/lib/manage/autofix.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"
source "$SCRIPT_DIR/lib/check/all.sh"
cleanup_all() {
stop_sudo_session

View File

@@ -10,16 +10,16 @@ export LANG=C
# Get script directory and source 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/clean_brew.sh"
source "$SCRIPT_DIR/../lib/clean_caches.sh"
source "$SCRIPT_DIR/../lib/clean_apps.sh"
source "$SCRIPT_DIR/../lib/clean_dev.sh"
source "$SCRIPT_DIR/../lib/clean_user_apps.sh"
source "$SCRIPT_DIR/../lib/clean_system.sh"
source "$SCRIPT_DIR/../lib/clean_user_data.sh"
source "$SCRIPT_DIR/../lib/clean_maintenance.sh"
source "$SCRIPT_DIR/../lib/core/common.sh"
source "$SCRIPT_DIR/../lib/core/sudo.sh"
source "$SCRIPT_DIR/../lib/clean/brew.sh"
source "$SCRIPT_DIR/../lib/clean/caches.sh"
source "$SCRIPT_DIR/../lib/clean/apps.sh"
source "$SCRIPT_DIR/../lib/clean/dev.sh"
source "$SCRIPT_DIR/../lib/clean/app_caches.sh"
source "$SCRIPT_DIR/../lib/clean/system.sh"
source "$SCRIPT_DIR/../lib/clean/user.sh"
source "$SCRIPT_DIR/../lib/clean/maintenance.sh"
# Configuration
SYSTEM_CLEAN=false
@@ -35,7 +35,7 @@ readonly PROTECTED_SW_DOMAINS=(
)
# Whitelist patterns (loaded from common.sh)
# FINDER_METADATA_SENTINEL and DEFAULT_WHITELIST_PATTERNS defined in lib/common.sh
# FINDER_METADATA_SENTINEL and DEFAULT_WHITELIST_PATTERNS defined in lib/core/common.sh
declare -a WHITELIST_PATTERNS=()
WHITELIST_WARNINGS=()
@@ -846,7 +846,7 @@ main() {
DRY_RUN=true
;;
"--whitelist")
source "$SCRIPT_DIR/../lib/whitelist_manager.sh"
source "$SCRIPT_DIR/../lib/manage/whitelist.sh"
manage_whitelist
exit 0
;;

View File

@@ -4,18 +4,14 @@ set -euo pipefail
# Load common functions
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"
source "$SCRIPT_DIR/lib/core/common.sh"
source "$SCRIPT_DIR/lib/core/sudo.sh"
source "$SCRIPT_DIR/lib/manage/update.sh"
source "$SCRIPT_DIR/lib/manage/autofix.sh"
source "$SCRIPT_DIR/lib/optimize/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"
source "$SCRIPT_DIR/lib/check/all.sh"
# Colors and icons from common.sh

View File

@@ -9,8 +9,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LIB_DIR="$(cd "$SCRIPT_DIR/../lib" && pwd)"
# Source common functions
# shellcheck source=../lib/common.sh
source "$LIB_DIR/common.sh"
# shellcheck source=../lib/core/common.sh
source "$LIB_DIR/core/common.sh"
readonly PAM_SUDO_FILE="/etc/pam.d/sudo"
readonly PAM_TID_LINE="auth sufficient pam_tid.so"

View File

@@ -14,12 +14,12 @@ export LANG=C
# Get script directory and source common functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../lib/common.sh"
source "$SCRIPT_DIR/../lib/menu_paginated.sh"
source "$SCRIPT_DIR/../lib/ui_app_selector.sh"
source "$SCRIPT_DIR/../lib/uninstall_batch.sh"
source "$SCRIPT_DIR/../lib/core/common.sh"
source "$SCRIPT_DIR/../lib/ui/menu_paginated.sh"
source "$SCRIPT_DIR/../lib/ui/app_selector.sh"
source "$SCRIPT_DIR/../lib/uninstall.sh"
# Note: Bundle preservation logic is now in lib/common.sh
# Note: Bundle preservation logic is now in lib/core/common.sh
# Initialize global variables
selected_apps=() # Global array for app selection
@@ -404,7 +404,7 @@ load_applications() {
# Read a single key with proper escape sequence handling
# This function has been replaced by the menu.sh library
# Note: App file discovery and size calculation functions moved to lib/common.sh
# Note: App file discovery and size calculation functions moved to lib/core/common.sh
# Use find_app_files() and calculate_total_size() from common.sh
# Uninstall selected applications

View File

@@ -39,7 +39,7 @@ fi; }
# Verbosity (0 = quiet, 1 = verbose)
VERBOSE=1
# Icons (duplicated from lib/common.sh - necessary as install.sh runs standalone)
# Icons (duplicated from lib/core/common.sh - necessary as install.sh runs standalone)
readonly ICON_SUCCESS="✓"
readonly ICON_ADMIN="●"
readonly ICON_CONFIRM="◎"
@@ -349,7 +349,7 @@ install_files() {
# Verify installation
verify_installation() {
if [[ -x "$INSTALL_DIR/mole" ]] && [[ -f "$CONFIG_DIR/lib/common.sh" ]]; then
if [[ -x "$INSTALL_DIR/mole" ]] && [[ -f "$CONFIG_DIR/lib/core/common.sh" ]]; then
# Test if mole command works
if "$INSTALL_DIR/mole" --help > /dev/null 2>&1; then
@@ -522,9 +522,9 @@ perform_update() {
if command -v brew > /dev/null 2>&1 && brew list mole > /dev/null 2>&1; then
# Try to use shared function if available (when running from installed Mole)
resolve_source_dir 2> /dev/null || true
if [[ -f "$SOURCE_DIR/lib/common.sh" ]]; then
if [[ -f "$SOURCE_DIR/lib/core/common.sh" ]]; then
# shellcheck disable=SC1090,SC1091
source "$SOURCE_DIR/lib/common.sh"
source "$SOURCE_DIR/lib/core/common.sh"
update_via_homebrew "$VERSION"
else
# Fallback: inline implementation

656
lib/check/all.sh Normal file
View File

@@ -0,0 +1,656 @@
#!/bin/bash
# System Checks Module
# Combines configuration, security, updates, and health checks
set -euo pipefail
# ============================================================================
# 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}${ICON_WARNING}${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}${ICON_WARNING}${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}${ICON_WARNING}${NC} Git Config ${YELLOW}Not configured${NC}"
fi
fi
}
check_all_config() {
check_touchid_sudo
check_rosetta
check_git_config
}
# ============================================================================
# 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
unset FIREWALL_DISABLED
local firewall_status=$(defaults read /Library/Preferences/com.apple.alf globalstate 2> /dev/null || echo "0")
if [[ "$firewall_status" == "1" || "$firewall_status" == "2" ]]; then
echo -e " ${GREEN}${NC} Firewall Enabled"
else
echo -e " ${YELLOW}${ICON_WARNING}${NC} Firewall ${YELLOW}Disabled${NC} (Consider enabling)"
echo -e " ${GRAY}System Settings → Network → Firewall, or run:${NC}"
echo -e " ${GRAY}sudo defaults write /Library/Preferences/com.apple.alf globalstate -int 1${NC}"
export FIREWALL_DISABLED=true
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"
unset GATEKEEPER_DISABLED
else
echo -e " ${YELLOW}${ICON_WARNING}${NC} Gatekeeper ${YELLOW}Disabled${NC}"
echo -e " ${GRAY}Enable via System Settings → Privacy & Security, or:${NC}"
echo -e " ${GRAY}sudo spctl --master-enable${NC}"
export GATEKEEPER_DISABLED=true
fi
fi
}
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}${ICON_WARNING}${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
}
# ============================================================================
# Software Update Checks
# ============================================================================
# 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) - $(get_file_mtime "$cache_file")))
[[ $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}${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=""
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 spinner_started=false
if [[ -t 1 ]]; then
printf " Checking App Store updates...\r"
start_inline_spinner "Checking App Store updates..."
spinner_started=true
export SOFTWAREUPDATE_SPINNER_SHOWN="external"
else
echo "Checking App Store updates..."
fi
local update_list=""
update_list=$(get_software_updates | grep -v "Software Update Tool" | grep "^\*" | grep -vi "macOS" || echo "")
if [[ "$spinner_started" == "true" ]]; then
stop_inline_spinner
unset SOFTWAREUPDATE_SPINNER_SHOWN
fi
local update_count=0
if [[ -n "$update_list" ]]; then
update_count=$(echo "$update_list" | wc -l | tr -d ' ')
fi
export APPSTORE_UPDATE_COUNT=$update_count
if [[ $update_count -gt 0 ]]; then
echo -e " ${YELLOW}${ICON_WARNING}${NC} App Store ${YELLOW}${update_count} apps${NC} need update"
echo -e " ${GRAY}Run: ${GREEN}softwareupdate -i <label>${NC}"
else
echo -e " ${GREEN}${NC} App Store Up to date"
fi
}
check_macos_update() {
local spinner_started=false
if [[ -t 1 ]]; then
printf " Checking macOS updates...\r"
start_inline_spinner "Checking macOS updates..."
spinner_started=true
export SOFTWAREUPDATE_SPINNER_SHOWN="external"
else
echo "Checking macOS updates..."
fi
# Check for macOS system update using cached list
local macos_update=""
macos_update=$(get_software_updates | grep -i "macOS" | head -1 || echo "")
if [[ "$spinner_started" == "true" ]]; then
stop_inline_spinner
unset SOFTWAREUPDATE_SPINNER_SHOWN
fi
export MACOS_UPDATE_AVAILABLE="false"
if [[ -n "$macos_update" ]]; then
export MACOS_UPDATE_AVAILABLE="true"
local version=$(echo "$macos_update" | grep -o '[0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?' | head -1)
if [[ -n "$version" ]]; then
echo -e " ${YELLOW}${ICON_WARNING}${NC} macOS ${YELLOW}${version} available${NC}"
else
echo -e " ${YELLOW}${ICON_WARNING}${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}${ICON_WARNING}${NC} Mole ${YELLOW}${latest_version} available${NC} (current: ${current_version})"
echo -e " ${GRAY}Run: ${GREEN}mo update${NC}"
else
echo -e " ${GREEN}${NC} Mole Up to date (${current_version})"
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
}
}
'
}
# ============================================================================
# System Health Checks
# ============================================================================
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}${ICON_WARNING}${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}${ICON_WARNING}${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}${ICON_WARNING}${NC} Login Items ${YELLOW}${login_items_count} apps${NC} auto-start (High)"
elif [[ $login_items_count -gt 0 ]]; then
echo -e " ${GREEN}${NC} Login Items ${login_items_count} apps auto-start"
else
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_output
size_output=$(du -sk "$cache_path" 2> /dev/null | awk 'NR==1 {print $1}' | tr -d '[:space:]' || echo "")
[[ "$size_output" =~ ^[0-9]+$ ]] || size_output=0
cache_size_kb=$((cache_size_kb + size_output))
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}${ICON_WARNING}${NC} Cache Size ${YELLOW}${cache_size_gb}GB${NC} cleanable"
elif [[ $cache_size_int -gt 5 ]]; then
echo -e " ${YELLOW}${ICON_WARNING}${NC} Cache Size ${YELLOW}${cache_size_gb}GB${NC} cleanable"
else
echo -e " ${GREEN}${NC} Cache Size ${cache_size_gb}GB"
fi
}
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}${ICON_WARNING}${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_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}${ICON_WARNING}${NC} Homebrew ${YELLOW}${warning_count} warnings${NC}"
echo -e " ${GRAY}Run: ${GREEN}brew doctor${NC} to see fixes, then rerun until clean${NC}"
export BREW_HAS_WARNINGS=true
else
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
}

View File

@@ -1,58 +0,0 @@
#!/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}${ICON_WARNING}${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}${ICON_WARNING}${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}${ICON_WARNING}${NC} Git Config ${YELLOW}Not configured${NC}"
fi
fi
}
check_all_config() {
check_touchid_sudo
check_rosetta
check_git_config
}

View File

@@ -1,222 +0,0 @@
#!/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}${ICON_WARNING}${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}${ICON_WARNING}${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}${ICON_WARNING}${NC} Login Items ${YELLOW}${login_items_count} apps${NC} auto-start (High)"
elif [[ $login_items_count -gt 0 ]]; then
echo -e " ${GREEN}${NC} Login Items ${login_items_count} apps auto-start"
else
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_output
size_output=$(du -sk "$cache_path" 2> /dev/null | awk 'NR==1 {print $1}' | tr -d '[:space:]' || echo "")
[[ "$size_output" =~ ^[0-9]+$ ]] || size_output=0
cache_size_kb=$((cache_size_kb + size_output))
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}${ICON_WARNING}${NC} Cache Size ${YELLOW}${cache_size_gb}GB${NC} cleanable"
elif [[ $cache_size_int -gt 5 ]]; then
echo -e " ${YELLOW}${ICON_WARNING}${NC} Cache Size ${YELLOW}${cache_size_gb}GB${NC} cleanable"
else
echo -e " ${GREEN}${NC} Cache Size ${cache_size_gb}GB"
fi
}
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}${ICON_WARNING}${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_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}${ICON_WARNING}${NC} Homebrew ${YELLOW}${warning_count} warnings${NC}"
echo -e " ${GRAY}Run: ${GREEN}brew doctor${NC} to see fixes, then rerun until clean${NC}"
export BREW_HAS_WARNINGS=true
else
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
}

View File

@@ -1,66 +0,0 @@
#!/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
unset FIREWALL_DISABLED
local firewall_status=$(defaults read /Library/Preferences/com.apple.alf globalstate 2> /dev/null || echo "0")
if [[ "$firewall_status" == "1" || "$firewall_status" == "2" ]]; then
echo -e " ${GREEN}${NC} Firewall Enabled"
else
echo -e " ${YELLOW}${ICON_WARNING}${NC} Firewall ${YELLOW}Disabled${NC} (Consider enabling)"
echo -e " ${GRAY}System Settings → Network → Firewall, or run:${NC}"
echo -e " ${GRAY}sudo defaults write /Library/Preferences/com.apple.alf globalstate -int 1${NC}"
export FIREWALL_DISABLED=true
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"
unset GATEKEEPER_DISABLED
else
echo -e " ${YELLOW}${ICON_WARNING}${NC} Gatekeeper ${YELLOW}Disabled${NC}"
echo -e " ${GRAY}Enable via System Settings → Privacy & Security, or:${NC}"
echo -e " ${GRAY}sudo spctl --master-enable${NC}"
export GATEKEEPER_DISABLED=true
fi
fi
}
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}${ICON_WARNING}${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
}

View File

@@ -1,303 +0,0 @@
#!/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) - $(get_file_mtime "$cache_file")))
[[ $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}${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=""
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 spinner_started=false
if [[ -t 1 ]]; then
printf " Checking App Store updates...\r"
start_inline_spinner "Checking App Store updates..."
spinner_started=true
export SOFTWAREUPDATE_SPINNER_SHOWN="external"
else
echo "Checking App Store updates..."
fi
local update_list=""
update_list=$(get_software_updates | grep -v "Software Update Tool" | grep "^\*" | grep -vi "macOS" || echo "")
if [[ "$spinner_started" == "true" ]]; then
stop_inline_spinner
unset SOFTWAREUPDATE_SPINNER_SHOWN
fi
local update_count=0
if [[ -n "$update_list" ]]; then
update_count=$(echo "$update_list" | wc -l | tr -d ' ')
fi
export APPSTORE_UPDATE_COUNT=$update_count
if [[ $update_count -gt 0 ]]; then
echo -e " ${YELLOW}${ICON_WARNING}${NC} App Store ${YELLOW}${update_count} apps${NC} need update"
echo -e " ${GRAY}Run: ${GREEN}softwareupdate -i <label>${NC}"
else
echo -e " ${GREEN}${NC} App Store Up to date"
fi
}
check_macos_update() {
local spinner_started=false
if [[ -t 1 ]]; then
printf " Checking macOS updates...\r"
start_inline_spinner "Checking macOS updates..."
spinner_started=true
export SOFTWAREUPDATE_SPINNER_SHOWN="external"
else
echo "Checking macOS updates..."
fi
# Check for macOS system update using cached list
local macos_update=""
macos_update=$(get_software_updates | grep -i "macOS" | head -1 || echo "")
if [[ "$spinner_started" == "true" ]]; then
stop_inline_spinner
unset SOFTWAREUPDATE_SPINNER_SHOWN
fi
export MACOS_UPDATE_AVAILABLE="false"
if [[ -n "$macos_update" ]]; then
export MACOS_UPDATE_AVAILABLE="true"
local version=$(echo "$macos_update" | grep -o '[0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?' | head -1)
if [[ -n "$version" ]]; then
echo -e " ${YELLOW}${ICON_WARNING}${NC} macOS ${YELLOW}${version} available${NC}"
else
echo -e " ${YELLOW}${ICON_WARNING}${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}${ICON_WARNING}${NC} Mole ${YELLOW}${latest_version} available${NC} (current: ${current_version})"
echo -e " ${GRAY}Run: ${GREEN}mo update${NC}"
else
echo -e " ${GREEN}${NC} Mole Up to date (${current_version})"
fi
else
echo -e " ${GREEN}${NC} Mole Up to date (${current_version})"
fi
}
check_all_updates() {
# Reset spinner flag for softwareupdate
unset SOFTWAREUPDATE_SPINNER_SHOWN
check_homebrew_updates
# Preload software update data to avoid delays between subsequent checks
get_software_updates > /dev/null 2>&1
check_appstore_updates
check_macos_update
check_mole_update
}
get_appstore_update_labels() {
get_software_updates | awk '
/^\*/ {
label=$0
sub(/^[[:space:]]*\* Label: */, "", label)
sub(/,.*/, "", label)
lower=tolower(label)
if (index(lower, "macos") == 0) {
print label
}
}
'
}
get_macos_update_labels() {
get_software_updates | awk '
/^\*/ {
label=$0
sub(/^[[:space:]]*\* Label: */, "", label)
sub(/,.*/, "", label)
lower=tolower(label)
if (index(lower, "macos") != 0) {
print label
}
}
'
}

View File

@@ -6,13 +6,13 @@ set -euo pipefail
# Get script directory and source dependencies
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
source "$SCRIPT_DIR/menu_simple.sh"
source "$SCRIPT_DIR/../core/common.sh"
source "$SCRIPT_DIR/../ui/menu_simple.sh"
# Config file path
WHITELIST_CONFIG="$HOME/.config/mole/whitelist"
# Default whitelist patterns defined in lib/common.sh:
# Default whitelist patterns defined in lib/core/common.sh:
# - DEFAULT_WHITELIST_PATTERNS
# - FINDER_METADATA_SENTINEL

View File

@@ -417,3 +417,282 @@ execute_optimization() {
;;
esac
}
#!/bin/bash
# System Health Check - Pure Bash Implementation
# Replaces optimize-go
set -euo pipefail
# Get memory info in GB
get_memory_info() {
local total_bytes used_gb total_gb
# Total memory
total_bytes=$(sysctl -n hw.memsize 2> /dev/null || echo "0")
total_gb=$(awk "BEGIN {printf \"%.2f\", $total_bytes / (1024*1024*1024)}" 2> /dev/null || echo "0")
[[ -z "$total_gb" || "$total_gb" == "" ]] && total_gb="0"
# Used memory from vm_stat
local vm_output active wired compressed page_size
vm_output=$(vm_stat 2> /dev/null || echo "")
page_size=4096
active=$(echo "$vm_output" | awk '/Pages active:/ {print $NF}' | tr -d '.' 2> /dev/null || echo "0")
wired=$(echo "$vm_output" | awk '/Pages wired down:/ {print $NF}' | tr -d '.' 2> /dev/null || echo "0")
compressed=$(echo "$vm_output" | awk '/Pages occupied by compressor:/ {print $NF}' | tr -d '.' 2> /dev/null || echo "0")
active=${active:-0}
wired=${wired:-0}
compressed=${compressed:-0}
local used_bytes=$(((active + wired + compressed) * page_size))
used_gb=$(awk "BEGIN {printf \"%.2f\", $used_bytes / (1024*1024*1024)}" 2> /dev/null || echo "0")
[[ -z "$used_gb" || "$used_gb" == "" ]] && used_gb="0"
echo "$used_gb $total_gb"
}
# Get disk info
get_disk_info() {
local home="${HOME:-/}"
local df_output total_gb used_gb used_percent
df_output=$(df -k "$home" 2> /dev/null | tail -1)
local total_kb used_kb
total_kb=$(echo "$df_output" | awk '{print $2}' 2> /dev/null || echo "0")
used_kb=$(echo "$df_output" | awk '{print $3}' 2> /dev/null || echo "0")
total_kb=${total_kb:-0}
used_kb=${used_kb:-0}
[[ "$total_kb" == "0" ]] && total_kb=1 # Avoid division by zero
total_gb=$(awk "BEGIN {printf \"%.2f\", $total_kb / (1024*1024)}" 2> /dev/null || echo "0")
used_gb=$(awk "BEGIN {printf \"%.2f\", $used_kb / (1024*1024)}" 2> /dev/null || echo "0")
used_percent=$(awk "BEGIN {printf \"%.1f\", ($used_kb / $total_kb) * 100}" 2> /dev/null || echo "0")
[[ -z "$total_gb" || "$total_gb" == "" ]] && total_gb="0"
[[ -z "$used_gb" || "$used_gb" == "" ]] && used_gb="0"
[[ -z "$used_percent" || "$used_percent" == "" ]] && used_percent="0"
echo "$used_gb $total_gb $used_percent"
}
# Get uptime in days
get_uptime_days() {
local boot_output boot_time uptime_days
boot_output=$(sysctl -n kern.boottime 2> /dev/null || echo "")
boot_time=$(echo "$boot_output" | sed -n 's/.*sec = \([0-9]*\).*/\1/p' 2> /dev/null || echo "")
if [[ -n "$boot_time" && "$boot_time" =~ ^[0-9]+$ ]]; then
local now=$(date +%s 2> /dev/null || echo "0")
local uptime_sec=$((now - boot_time))
uptime_days=$(awk "BEGIN {printf \"%.1f\", $uptime_sec / 86400}" 2> /dev/null || echo "0")
else
uptime_days="0"
fi
[[ -z "$uptime_days" || "$uptime_days" == "" ]] && uptime_days="0"
echo "$uptime_days"
}
# Get directory size in KB
dir_size_kb() {
local path="$1"
[[ ! -e "$path" ]] && echo "0" && return
du -sk "$path" 2> /dev/null | awk '{print $1}' || echo "0"
}
# Format size from KB
format_size_kb() {
local kb="$1"
[[ "$kb" -le 0 ]] && echo "0B" && return
local mb gb
mb=$(awk "BEGIN {printf \"%.1f\", $kb / 1024}")
gb=$(awk "BEGIN {printf \"%.2f\", $mb / 1024}")
if awk "BEGIN {exit !($gb >= 1)}"; then
echo "${gb}GB"
elif awk "BEGIN {exit !($mb >= 1)}"; then
printf "%.0fMB\n" "$mb"
else
echo "${kb}KB"
fi
}
# Check cache size
check_cache_refresh() {
local cache_dir="$HOME/Library/Caches"
local size_kb=$(dir_size_kb "$cache_dir")
local desc="Refresh Finder previews, Quick Look, and Safari caches"
if [[ $size_kb -gt 0 ]]; then
local size_str=$(format_size_kb "$size_kb")
desc="Refresh ${size_str} of Finder/Safari caches"
fi
echo "cache_refresh|User Cache Refresh|${desc}|true"
}
# Check Mail downloads
check_mail_downloads() {
local dirs=(
"$HOME/Library/Mail Downloads"
"$HOME/Library/Containers/com.apple.mail/Data/Library/Mail Downloads"
)
local total_kb=0
for dir in "${dirs[@]}"; do
total_kb=$((total_kb + $(dir_size_kb "$dir")))
done
if [[ $total_kb -gt 0 ]]; then
local size_str=$(format_size_kb "$total_kb")
echo "mail_downloads|Mail Downloads|Recover ${size_str} of Mail attachments|true"
fi
}
# Check saved state
check_saved_state() {
local state_dir="$HOME/Library/Saved Application State"
local size_kb=$(dir_size_kb "$state_dir")
if [[ $size_kb -gt 0 ]]; then
local size_str=$(format_size_kb "$size_kb")
echo "saved_state_cleanup|Saved State|Clear ${size_str} of stale saved states|true"
fi
}
# Check swap files
check_swap_cleanup() {
local total_kb=0
local file
for file in /private/var/vm/swapfile*; do
[[ -f "$file" ]] && total_kb=$((total_kb + $(get_file_size "$file") / 1024))
done
if [[ $total_kb -gt 0 ]]; then
local size_str=$(format_size_kb "$total_kb")
echo "swap_cleanup|Memory & Swap|Purge swap (${size_str}) & inactive memory|false"
fi
}
# Check local snapshots
check_local_snapshots() {
command -v tmutil > /dev/null 2>&1 || return
local snapshots
snapshots=$(tmutil listlocalsnapshots / 2> /dev/null || echo "")
local count
count=$(echo "$snapshots" | grep -c "com.apple.TimeMachine" 2> /dev/null)
count=$(echo "$count" | tr -d ' \n')
count=${count:-0}
[[ "$count" =~ ^[0-9]+$ ]] && [[ $count -gt 0 ]] && echo "local_snapshots|Local Snapshots|${count} APFS local snapshots detected|true"
}
# Check developer cleanup
check_developer_cleanup() {
local dirs=(
"$HOME/Library/Developer/Xcode/DerivedData"
"$HOME/Library/Developer/Xcode/Archives"
"$HOME/Library/Developer/Xcode/iOS DeviceSupport"
"$HOME/Library/Developer/CoreSimulator/Caches"
)
local total_kb=0
for dir in "${dirs[@]}"; do
total_kb=$((total_kb + $(dir_size_kb "$dir")))
done
if [[ $total_kb -gt 0 ]]; then
local size_str=$(format_size_kb "$total_kb")
echo "developer_cleanup|Developer Cleanup|Recover ${size_str} of Xcode/simulator data|false"
fi
}
# Generate JSON output
generate_health_json() {
# System info
read -r mem_used mem_total <<< "$(get_memory_info)"
read -r disk_used disk_total disk_percent <<< "$(get_disk_info)"
local uptime=$(get_uptime_days)
# Ensure all values are valid numbers (fallback to 0)
mem_used=${mem_used:-0}
mem_total=${mem_total:-0}
disk_used=${disk_used:-0}
disk_total=${disk_total:-0}
disk_percent=${disk_percent:-0}
uptime=${uptime:-0}
# Start JSON
cat << EOF
{
"memory_used_gb": $mem_used,
"memory_total_gb": $mem_total,
"disk_used_gb": $disk_used,
"disk_total_gb": $disk_total,
"disk_used_percent": $disk_percent,
"uptime_days": $uptime,
"optimizations": [
EOF
# Collect all optimization items
local -a items=()
# Always-on items
items+=('system_maintenance|System Maintenance|Rebuild system databases & flush caches|true')
items+=('maintenance_scripts|Maintenance Scripts|Run daily/weekly/monthly scripts & rotate logs|true')
items+=('radio_refresh|Bluetooth & Wi-Fi Refresh|Reset wireless preference caches|true')
items+=('recent_items|Recent Items|Clear recent apps/documents/servers lists|true')
items+=('log_cleanup|Diagnostics Cleanup|Purge old diagnostic & crash logs|true')
items+=('startup_cache|Startup Cache Rebuild|Rebuild kext caches & prelinked kernel|true')
# Conditional items
local item
item=$(check_cache_refresh || true)
[[ -n "$item" ]] && items+=("$item")
item=$(check_mail_downloads || true)
[[ -n "$item" ]] && items+=("$item")
item=$(check_saved_state || true)
[[ -n "$item" ]] && items+=("$item")
item=$(check_swap_cleanup || true)
[[ -n "$item" ]] && items+=("$item")
item=$(check_local_snapshots || true)
[[ -n "$item" ]] && items+=("$item")
item=$(check_developer_cleanup || true)
[[ -n "$item" ]] && items+=("$item")
# Output items as JSON
local first=true
for item in "${items[@]}"; do
IFS='|' read -r action name desc safe <<< "$item"
[[ "$first" == "true" ]] && first=false || echo ","
cat << EOF
{
"category": "system",
"name": "$name",
"description": "$desc",
"action": "$action",
"safe": $safe
}
EOF
done
# Close JSON
cat << 'EOF'
]
}
EOF
}
# Main execution
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
generate_health_json
fi

View File

@@ -1,279 +0,0 @@
#!/bin/bash
# System Health Check - Pure Bash Implementation
# Replaces optimize-go
set -euo pipefail
# Get memory info in GB
get_memory_info() {
local total_bytes used_gb total_gb
# Total memory
total_bytes=$(sysctl -n hw.memsize 2> /dev/null || echo "0")
total_gb=$(awk "BEGIN {printf \"%.2f\", $total_bytes / (1024*1024*1024)}" 2> /dev/null || echo "0")
[[ -z "$total_gb" || "$total_gb" == "" ]] && total_gb="0"
# Used memory from vm_stat
local vm_output active wired compressed page_size
vm_output=$(vm_stat 2> /dev/null || echo "")
page_size=4096
active=$(echo "$vm_output" | awk '/Pages active:/ {print $NF}' | tr -d '.' 2> /dev/null || echo "0")
wired=$(echo "$vm_output" | awk '/Pages wired down:/ {print $NF}' | tr -d '.' 2> /dev/null || echo "0")
compressed=$(echo "$vm_output" | awk '/Pages occupied by compressor:/ {print $NF}' | tr -d '.' 2> /dev/null || echo "0")
active=${active:-0}
wired=${wired:-0}
compressed=${compressed:-0}
local used_bytes=$(((active + wired + compressed) * page_size))
used_gb=$(awk "BEGIN {printf \"%.2f\", $used_bytes / (1024*1024*1024)}" 2> /dev/null || echo "0")
[[ -z "$used_gb" || "$used_gb" == "" ]] && used_gb="0"
echo "$used_gb $total_gb"
}
# Get disk info
get_disk_info() {
local home="${HOME:-/}"
local df_output total_gb used_gb used_percent
df_output=$(df -k "$home" 2> /dev/null | tail -1)
local total_kb used_kb
total_kb=$(echo "$df_output" | awk '{print $2}' 2> /dev/null || echo "0")
used_kb=$(echo "$df_output" | awk '{print $3}' 2> /dev/null || echo "0")
total_kb=${total_kb:-0}
used_kb=${used_kb:-0}
[[ "$total_kb" == "0" ]] && total_kb=1 # Avoid division by zero
total_gb=$(awk "BEGIN {printf \"%.2f\", $total_kb / (1024*1024)}" 2> /dev/null || echo "0")
used_gb=$(awk "BEGIN {printf \"%.2f\", $used_kb / (1024*1024)}" 2> /dev/null || echo "0")
used_percent=$(awk "BEGIN {printf \"%.1f\", ($used_kb / $total_kb) * 100}" 2> /dev/null || echo "0")
[[ -z "$total_gb" || "$total_gb" == "" ]] && total_gb="0"
[[ -z "$used_gb" || "$used_gb" == "" ]] && used_gb="0"
[[ -z "$used_percent" || "$used_percent" == "" ]] && used_percent="0"
echo "$used_gb $total_gb $used_percent"
}
# Get uptime in days
get_uptime_days() {
local boot_output boot_time uptime_days
boot_output=$(sysctl -n kern.boottime 2> /dev/null || echo "")
boot_time=$(echo "$boot_output" | sed -n 's/.*sec = \([0-9]*\).*/\1/p' 2> /dev/null || echo "")
if [[ -n "$boot_time" && "$boot_time" =~ ^[0-9]+$ ]]; then
local now=$(date +%s 2> /dev/null || echo "0")
local uptime_sec=$((now - boot_time))
uptime_days=$(awk "BEGIN {printf \"%.1f\", $uptime_sec / 86400}" 2> /dev/null || echo "0")
else
uptime_days="0"
fi
[[ -z "$uptime_days" || "$uptime_days" == "" ]] && uptime_days="0"
echo "$uptime_days"
}
# Get directory size in KB
dir_size_kb() {
local path="$1"
[[ ! -e "$path" ]] && echo "0" && return
du -sk "$path" 2> /dev/null | awk '{print $1}' || echo "0"
}
# Format size from KB
format_size_kb() {
local kb="$1"
[[ "$kb" -le 0 ]] && echo "0B" && return
local mb gb
mb=$(awk "BEGIN {printf \"%.1f\", $kb / 1024}")
gb=$(awk "BEGIN {printf \"%.2f\", $mb / 1024}")
if awk "BEGIN {exit !($gb >= 1)}"; then
echo "${gb}GB"
elif awk "BEGIN {exit !($mb >= 1)}"; then
printf "%.0fMB\n" "$mb"
else
echo "${kb}KB"
fi
}
# Check cache size
check_cache_refresh() {
local cache_dir="$HOME/Library/Caches"
local size_kb=$(dir_size_kb "$cache_dir")
local desc="Refresh Finder previews, Quick Look, and Safari caches"
if [[ $size_kb -gt 0 ]]; then
local size_str=$(format_size_kb "$size_kb")
desc="Refresh ${size_str} of Finder/Safari caches"
fi
echo "cache_refresh|User Cache Refresh|${desc}|true"
}
# Check Mail downloads
check_mail_downloads() {
local dirs=(
"$HOME/Library/Mail Downloads"
"$HOME/Library/Containers/com.apple.mail/Data/Library/Mail Downloads"
)
local total_kb=0
for dir in "${dirs[@]}"; do
total_kb=$((total_kb + $(dir_size_kb "$dir")))
done
if [[ $total_kb -gt 0 ]]; then
local size_str=$(format_size_kb "$total_kb")
echo "mail_downloads|Mail Downloads|Recover ${size_str} of Mail attachments|true"
fi
}
# Check saved state
check_saved_state() {
local state_dir="$HOME/Library/Saved Application State"
local size_kb=$(dir_size_kb "$state_dir")
if [[ $size_kb -gt 0 ]]; then
local size_str=$(format_size_kb "$size_kb")
echo "saved_state_cleanup|Saved State|Clear ${size_str} of stale saved states|true"
fi
}
# Check swap files
check_swap_cleanup() {
local total_kb=0
local file
for file in /private/var/vm/swapfile*; do
[[ -f "$file" ]] && total_kb=$((total_kb + $(get_file_size "$file") / 1024))
done
if [[ $total_kb -gt 0 ]]; then
local size_str=$(format_size_kb "$total_kb")
echo "swap_cleanup|Memory & Swap|Purge swap (${size_str}) & inactive memory|false"
fi
}
# Check local snapshots
check_local_snapshots() {
command -v tmutil > /dev/null 2>&1 || return
local snapshots
snapshots=$(tmutil listlocalsnapshots / 2> /dev/null || echo "")
local count
count=$(echo "$snapshots" | grep -c "com.apple.TimeMachine" 2> /dev/null)
count=$(echo "$count" | tr -d ' \n')
count=${count:-0}
[[ "$count" =~ ^[0-9]+$ ]] && [[ $count -gt 0 ]] && echo "local_snapshots|Local Snapshots|${count} APFS local snapshots detected|true"
}
# Check developer cleanup
check_developer_cleanup() {
local dirs=(
"$HOME/Library/Developer/Xcode/DerivedData"
"$HOME/Library/Developer/Xcode/Archives"
"$HOME/Library/Developer/Xcode/iOS DeviceSupport"
"$HOME/Library/Developer/CoreSimulator/Caches"
)
local total_kb=0
for dir in "${dirs[@]}"; do
total_kb=$((total_kb + $(dir_size_kb "$dir")))
done
if [[ $total_kb -gt 0 ]]; then
local size_str=$(format_size_kb "$total_kb")
echo "developer_cleanup|Developer Cleanup|Recover ${size_str} of Xcode/simulator data|false"
fi
}
# Generate JSON output
generate_health_json() {
# System info
read -r mem_used mem_total <<< "$(get_memory_info)"
read -r disk_used disk_total disk_percent <<< "$(get_disk_info)"
local uptime=$(get_uptime_days)
# Ensure all values are valid numbers (fallback to 0)
mem_used=${mem_used:-0}
mem_total=${mem_total:-0}
disk_used=${disk_used:-0}
disk_total=${disk_total:-0}
disk_percent=${disk_percent:-0}
uptime=${uptime:-0}
# Start JSON
cat << EOF
{
"memory_used_gb": $mem_used,
"memory_total_gb": $mem_total,
"disk_used_gb": $disk_used,
"disk_total_gb": $disk_total,
"disk_used_percent": $disk_percent,
"uptime_days": $uptime,
"optimizations": [
EOF
# Collect all optimization items
local -a items=()
# Always-on items
items+=('system_maintenance|System Maintenance|Rebuild system databases & flush caches|true')
items+=('maintenance_scripts|Maintenance Scripts|Run daily/weekly/monthly scripts & rotate logs|true')
items+=('radio_refresh|Bluetooth & Wi-Fi Refresh|Reset wireless preference caches|true')
items+=('recent_items|Recent Items|Clear recent apps/documents/servers lists|true')
items+=('log_cleanup|Diagnostics Cleanup|Purge old diagnostic & crash logs|true')
items+=('startup_cache|Startup Cache Rebuild|Rebuild kext caches & prelinked kernel|true')
# Conditional items
local item
item=$(check_cache_refresh || true)
[[ -n "$item" ]] && items+=("$item")
item=$(check_mail_downloads || true)
[[ -n "$item" ]] && items+=("$item")
item=$(check_saved_state || true)
[[ -n "$item" ]] && items+=("$item")
item=$(check_swap_cleanup || true)
[[ -n "$item" ]] && items+=("$item")
item=$(check_local_snapshots || true)
[[ -n "$item" ]] && items+=("$item")
item=$(check_developer_cleanup || true)
[[ -n "$item" ]] && items+=("$item")
# Output items as JSON
local first=true
for item in "${items[@]}"; do
IFS='|' read -r action name desc safe <<< "$item"
[[ "$first" == "true" ]] && first=false || echo ","
cat << EOF
{
"category": "system",
"name": "$name",
"description": "$desc",
"action": "$action",
"safe": $safe
}
EOF
done
# Close JSON
cat << 'EOF'
]
}
EOF
}
# Main execution
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
generate_health_json
fi

View File

@@ -4,7 +4,7 @@ set -euo pipefail
# Ensure common.sh is loaded
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
[[ -z "${MOLE_COMMON_LOADED:-}" ]] && source "$SCRIPT_DIR/lib/common.sh"
[[ -z "${MOLE_COMMON_LOADED:-}" ]] && source "$SCRIPT_DIR/lib/core/common.sh"
# Batch uninstall functionality with minimal confirmations
# Replaces the overly verbose individual confirmation approach
@@ -42,7 +42,7 @@ decode_file_list() {
echo "$decoded"
return 0
}
# Note: find_app_files() and calculate_total_size() functions now in lib/common.sh
# Note: find_app_files() and calculate_total_size() functions now in lib/core/common.sh
# Batch uninstall with single confirmation
batch_uninstall_applications() {

4
mole
View File

@@ -19,10 +19,10 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Source common functions
source "$SCRIPT_DIR/lib/common.sh"
source "$SCRIPT_DIR/lib/core/common.sh"
# Version info
VERSION="1.11.6"
VERSION="1.11.7"
MOLE_TAGLINE="can dig deep to clean your Mac."
# Check if Touch ID is already configured

View File

@@ -69,7 +69,7 @@ TOTAL_CHECKS=0
# Check 1: Keyboard input handling (restored to 1s for reliability)
((TOTAL_CHECKS++))
if grep -q "read -r -s -n 1 -t 1" lib/common.sh; then
if grep -q "read -r -s -n 1 -t 1" lib/core/common.sh; then
echo -e "${GREEN} ✓ Keyboard timeout properly configured (1s)${NC}"
((OPTIMIZATION_SCORE++))
else
@@ -78,7 +78,7 @@ fi
# Check 2: Single-pass drain_pending_input
((TOTAL_CHECKS++))
DRAIN_PASSES=$(grep -c "while IFS= read -r -s -n 1" lib/common.sh || echo 0)
DRAIN_PASSES=$(grep -c "while IFS= read -r -s -n 1" lib/core/common.sh || echo 0)
if [[ $DRAIN_PASSES -eq 1 ]]; then
echo -e "${GREEN} ✓ drain_pending_input optimized (single-pass)${NC}"
((OPTIMIZATION_SCORE++))
@@ -88,7 +88,7 @@ fi
# Check 3: Log rotation once per session
((TOTAL_CHECKS++))
if grep -q "rotate_log_once" lib/common.sh && ! grep "rotate_log()" lib/common.sh | grep -v "rotate_log_once" > /dev/null 2>&1; then
if grep -q "rotate_log_once" lib/core/common.sh && ! grep "rotate_log()" lib/core/common.sh | grep -v "rotate_log_once" > /dev/null 2>&1; then
echo -e "${GREEN} ✓ Log rotation optimized (once per session)${NC}"
((OPTIMIZATION_SCORE++))
else

View File

@@ -24,8 +24,8 @@ teardown_file() {
}
setup() {
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/clean_caches.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
source "$PROJECT_ROOT/lib/clean/caches.sh"
# Clean permission flag for each test
rm -f "$HOME/.cache/mole/permissions_granted"
@@ -34,7 +34,7 @@ setup() {
# Test check_tcc_permissions in non-interactive mode
@test "check_tcc_permissions skips in non-interactive mode" {
# Redirect stdin to simulate non-TTY
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/clean_caches.sh'; check_tcc_permissions" < /dev/null
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/clean/caches.sh'; check_tcc_permissions" < /dev/null
[ "$status" -eq 0 ]
# Should not create permission flag in non-interactive mode
[[ ! -f "$HOME/.cache/mole/permissions_granted" ]]
@@ -47,7 +47,7 @@ setup() {
touch "$HOME/.cache/mole/permissions_granted"
# Even in TTY mode, should skip if flag exists
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/clean_caches.sh'; [[ -t 1 ]] || true; check_tcc_permissions"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/clean/caches.sh'; [[ -t 1 ]] || true; check_tcc_permissions"
[ "$status" -eq 0 ]
}
@@ -66,13 +66,13 @@ setup() {
[[ -d "$HOME/.cache/mole" ]]
# Function should handle missing directories gracefully
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/clean_caches.sh'; check_tcc_permissions < /dev/null"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/clean/caches.sh'; check_tcc_permissions < /dev/null"
[ "$status" -eq 0 ]
}
# Test clean_service_worker_cache with non-existent path
@test "clean_service_worker_cache returns early when path doesn't exist" {
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/clean_caches.sh'; clean_service_worker_cache 'TestBrowser' '/nonexistent/path'"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/clean/caches.sh'; clean_service_worker_cache 'TestBrowser' '/nonexistent/path'"
[ "$status" -eq 0 ]
}
@@ -81,7 +81,7 @@ setup() {
local test_cache="$HOME/test_sw_cache"
mkdir -p "$test_cache"
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/clean_caches.sh'; clean_service_worker_cache 'TestBrowser' '$test_cache'"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/clean/caches.sh'; clean_service_worker_cache 'TestBrowser' '$test_cache'"
[ "$status" -eq 0 ]
rm -rf "$test_cache"
@@ -100,8 +100,8 @@ setup() {
run bash -c "
export DRY_RUN=true
export PROTECTED_SW_DOMAINS=(capcut.com photopea.com)
source '$PROJECT_ROOT/lib/common.sh'
source '$PROJECT_ROOT/lib/clean_caches.sh'
source '$PROJECT_ROOT/lib/core/common.sh'
source '$PROJECT_ROOT/lib/clean/caches.sh'
clean_service_worker_cache 'TestBrowser' '$test_cache'
"
[ "$status" -eq 0 ]
@@ -124,8 +124,8 @@ setup() {
run bash -c "
export DRY_RUN=true
source '$PROJECT_ROOT/lib/common.sh'
source '$PROJECT_ROOT/lib/clean_caches.sh'
source '$PROJECT_ROOT/lib/core/common.sh'
source '$PROJECT_ROOT/lib/clean/caches.sh'
clean_project_caches
"
[ "$status" -eq 0 ]
@@ -147,8 +147,8 @@ setup() {
# Should complete within reasonable time even with slow find
run timeout 15 bash -c "
source '$PROJECT_ROOT/lib/common.sh'
source '$PROJECT_ROOT/lib/clean_caches.sh'
source '$PROJECT_ROOT/lib/core/common.sh'
source '$PROJECT_ROOT/lib/clean/caches.sh'
clean_project_caches
"
# Either succeeds or times out gracefully (both acceptable)
@@ -168,8 +168,8 @@ setup() {
# We can't easily test this without mocking, but we can verify no crashes
run bash -c "
export DRY_RUN=true
source '$PROJECT_ROOT/lib/common.sh'
source '$PROJECT_ROOT/lib/clean_caches.sh'
source '$PROJECT_ROOT/lib/core/common.sh'
source '$PROJECT_ROOT/lib/clean/caches.sh'
clean_project_caches
"
[ "$status" -eq 0 ]

View File

@@ -46,8 +46,11 @@ setup() {
}
@test "touchid status reports current configuration" {
# Don't test actual Touch ID config (system-dependent, may trigger prompts)
# Just verify the command exists and can run
run env HOME="$HOME" "$PROJECT_ROOT/mole" touchid status
[ "$status" -eq 0 ]
# Should output either "enabled" or "not configured" message
[[ "$output" == *"Touch ID"* ]]
}

View File

@@ -30,13 +30,13 @@ teardown() {
}
@test "mo_spinner_chars returns default sequence when unset" {
result="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; mo_spinner_chars")"
result="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/core/common.sh'; mo_spinner_chars")"
[ "$result" = "|/-\\" ]
}
@test "mo_spinner_chars respects MO_SPINNER_CHARS override" {
export MO_SPINNER_CHARS="abcd"
result="$(HOME="$HOME" MO_SPINNER_CHARS="$MO_SPINNER_CHARS" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; mo_spinner_chars")"
result="$(HOME="$HOME" MO_SPINNER_CHARS="$MO_SPINNER_CHARS" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/core/common.sh'; mo_spinner_chars")"
[ "$result" = "abcd" ]
}
@@ -45,19 +45,19 @@ teardown() {
if [[ "$(uname -m)" == "arm64" ]]; then
expected="Apple Silicon"
fi
result="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; detect_architecture")"
result="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/core/common.sh'; detect_architecture")"
[ "$result" = "$expected" ]
}
@test "get_free_space returns a non-empty value" {
result="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; get_free_space")"
result="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/core/common.sh'; get_free_space")"
[[ -n "$result" ]]
}
@test "log_info prints message and appends to log file" {
local message="Informational message from test"
local stdout_output
stdout_output="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; log_info '$message'")"
stdout_output="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/core/common.sh'; log_info '$message'")"
[[ "$stdout_output" == *"$message"* ]]
local log_file="$HOME/.config/mole/mole.log"
@@ -69,7 +69,7 @@ teardown() {
local message="Something went wrong"
local stderr_file="$HOME/log_error_stderr.txt"
HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; log_error '$message' 1>/dev/null 2>'$stderr_file'"
HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/core/common.sh'; log_error '$message' 1>/dev/null 2>'$stderr_file'"
[[ -s "$stderr_file" ]]
grep -q "$message" "$stderr_file"
@@ -86,18 +86,18 @@ teardown() {
dd if=/dev/zero of="$log_file" bs=1024 count=1100 2> /dev/null
# First call should rotate
HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'"
HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/core/common.sh'"
[[ -f "${log_file}.old" ]]
# Verify MOLE_LOG_ROTATED was set (rotation happened)
result=$(HOME="$HOME" MOLE_LOG_ROTATED=1 bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; echo \$MOLE_LOG_ROTATED")
result=$(HOME="$HOME" MOLE_LOG_ROTATED=1 bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/core/common.sh'; echo \$MOLE_LOG_ROTATED")
[[ "$result" == "1" ]]
}
@test "drain_pending_input clears stdin buffer" {
# Test that drain_pending_input doesn't hang (using background job with timeout)
result=$(
(echo -e "test\ninput" | HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; drain_pending_input; echo done") &
(echo -e "test\ninput" | HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/core/common.sh'; drain_pending_input; echo done") &
pid=$!
sleep 2
if kill -0 "$pid" 2> /dev/null; then
@@ -114,7 +114,7 @@ teardown() {
@test "bytes_to_human converts byte counts into readable units" {
output="$(
HOME="$HOME" bash --noprofile --norc << 'EOF'
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
bytes_to_human 512
bytes_to_human 2048
bytes_to_human $((5 * 1024 * 1024))
@@ -135,7 +135,7 @@ EOF
@test "create_temp_file and create_temp_dir are tracked and cleaned" {
HOME="$HOME" bash --noprofile --norc << 'EOF'
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
create_temp_file > "$HOME/temp_file_path.txt"
create_temp_dir > "$HOME/temp_dir_path.txt"
cleanup_temp_files
@@ -151,20 +151,20 @@ EOF
@test "should_protect_data protects system and critical apps" {
# System apps should be protected
result=$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; should_protect_data 'com.apple.Safari' && echo 'protected' || echo 'not-protected'")
result=$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/core/common.sh'; should_protect_data 'com.apple.Safari' && echo 'protected' || echo 'not-protected'")
[ "$result" = "protected" ]
# Critical network apps should be protected
result=$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; should_protect_data 'com.clash.app' && echo 'protected' || echo 'not-protected'")
result=$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/core/common.sh'; should_protect_data 'com.clash.app' && echo 'protected' || echo 'not-protected'")
[ "$result" = "protected" ]
# Regular apps should not be protected
result=$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; should_protect_data 'com.example.RegularApp' && echo 'protected' || echo 'not-protected'")
result=$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/core/common.sh'; should_protect_data 'com.example.RegularApp' && echo 'protected' || echo 'not-protected'")
[ "$result" = "not-protected" ]
}
@test "print_summary_block formats output correctly" {
result=$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; print_summary_block 'success' 'Test Summary' 'Detail 1' 'Detail 2'")
result=$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/core/common.sh'; print_summary_block 'success' 'Test Summary' 'Detail 1' 'Detail 2'")
[[ "$result" == *"Test Summary"* ]]
[[ "$result" == *"Detail 1"* ]]
[[ "$result" == *"Detail 2"* ]]
@@ -173,7 +173,7 @@ EOF
@test "start_inline_spinner and stop_inline_spinner work in non-TTY" {
# Should not hang in non-interactive mode
result=$(HOME="$HOME" bash --noprofile --norc << 'EOF'
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Testing..."
sleep 0.1
stop_inline_spinner

View File

@@ -32,7 +32,7 @@ setup() {
run bash --noprofile --norc -c '
set -euo pipefail
PATH="/usr/bin:/bin"
source "'"$PROJECT_ROOT"'/lib/common.sh"
source "'"$PROJECT_ROOT"'/lib/core/common.sh"
run_with_timeout 1 sleep 0.1
'
[ "$status" -eq 0 ]
@@ -42,7 +42,7 @@ setup() {
run bash --noprofile --norc -c '
set -euo pipefail
PATH="/usr/bin:/bin"
source "'"$PROJECT_ROOT"'/lib/common.sh"
source "'"$PROJECT_ROOT"'/lib/core/common.sh"
run_with_timeout 1 sleep 5
'
[ "$status" -eq 124 ]
@@ -56,8 +56,8 @@ setup() {
run env HOME="$HOME" bash --noprofile --norc << 'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/optimization_tasks.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
source "$PROJECT_ROOT/lib/optimize/tasks.sh"
# Mock sudo and defaults to avoid system changes
sudo() { return 0; }
defaults() { return 0; }
@@ -74,8 +74,8 @@ EOF
run env HOME="$HOME" bash --noprofile --norc << 'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/optimization_tasks.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
source "$PROJECT_ROOT/lib/optimize/tasks.sh"
sudo() { return 0; }
defaults() { return 0; }
export -f sudo defaults
@@ -96,8 +96,8 @@ EOF
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/optimization_tasks.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
source "$PROJECT_ROOT/lib/optimize/tasks.sh"
opt_saved_state_cleanup
EOF
@@ -109,8 +109,8 @@ EOF
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/optimization_tasks.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
source "$PROJECT_ROOT/lib/optimize/tasks.sh"
opt_saved_state_cleanup
EOF
@@ -124,8 +124,8 @@ EOF
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/optimization_tasks.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
source "$PROJECT_ROOT/lib/optimize/tasks.sh"
# Mock qlmanage and cleanup_path to avoid system calls
qlmanage() { return 0; }
cleanup_path() {
@@ -148,8 +148,8 @@ EOF
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/optimization_tasks.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
source "$PROJECT_ROOT/lib/optimize/tasks.sh"
# MOLE_MAIL_DOWNLOADS_MIN_KB is readonly, defaults to 5120 KB (~5MB)
opt_mail_downloads
EOF
@@ -170,8 +170,8 @@ EOF
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/optimization_tasks.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
source "$PROJECT_ROOT/lib/optimize/tasks.sh"
# MOLE_MAIL_DOWNLOADS_MIN_KB and MOLE_LOG_AGE_DAYS are readonly constants
opt_mail_downloads
EOF
@@ -182,8 +182,8 @@ EOF
@test "_opt_get_dir_size_kb returns zero for missing directory" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/optimization_tasks.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
source "$PROJECT_ROOT/lib/optimize/tasks.sh"
size=$(_opt_get_dir_size_kb "/nonexistent/path")
echo "$size"
EOF
@@ -198,8 +198,8 @@ EOF
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/optimization_tasks.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
source "$PROJECT_ROOT/lib/optimize/tasks.sh"
size=$(_opt_get_dir_size_kb "$HOME/test_size")
echo "$size"
EOF

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bats
# Tests for safe_* functions in lib/common.sh
# Tests for safe_* functions in lib/core/common.sh
setup_file() {
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
@@ -22,7 +22,7 @@ teardown_file() {
}
setup() {
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
TEST_DIR="$HOME/test_safe_functions"
mkdir -p "$TEST_DIR"
}
@@ -33,39 +33,39 @@ teardown() {
# Test validate_path_for_deletion
@test "validate_path_for_deletion rejects empty path" {
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; validate_path_for_deletion ''"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; validate_path_for_deletion ''"
[ "$status" -eq 1 ]
}
@test "validate_path_for_deletion rejects relative path" {
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; validate_path_for_deletion 'relative/path'"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; validate_path_for_deletion 'relative/path'"
[ "$status" -eq 1 ]
}
@test "validate_path_for_deletion rejects path traversal" {
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; validate_path_for_deletion '/tmp/../etc'"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; validate_path_for_deletion '/tmp/../etc'"
[ "$status" -eq 1 ]
}
@test "validate_path_for_deletion rejects system directories" {
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; validate_path_for_deletion '/System'"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; validate_path_for_deletion '/System'"
[ "$status" -eq 1 ]
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; validate_path_for_deletion '/usr/bin'"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; validate_path_for_deletion '/usr/bin'"
[ "$status" -eq 1 ]
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; validate_path_for_deletion '/etc'"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; validate_path_for_deletion '/etc'"
[ "$status" -eq 1 ]
}
@test "validate_path_for_deletion accepts valid path" {
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; validate_path_for_deletion '$TEST_DIR/valid'"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; validate_path_for_deletion '$TEST_DIR/valid'"
[ "$status" -eq 0 ]
}
# Test safe_remove
@test "safe_remove validates path before deletion" {
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; safe_remove '/System/test' 2>&1"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; safe_remove '/System/test' 2>&1"
[ "$status" -eq 1 ]
}
@@ -73,7 +73,7 @@ teardown() {
local test_file="$TEST_DIR/test_file.txt"
echo "test" > "$test_file"
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; safe_remove '$test_file' true"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; safe_remove '$test_file' true"
[ "$status" -eq 0 ]
[ ! -f "$test_file" ]
}
@@ -83,19 +83,19 @@ teardown() {
mkdir -p "$test_subdir"
touch "$test_subdir/file.txt"
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; safe_remove '$test_subdir' true"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; safe_remove '$test_subdir' true"
[ "$status" -eq 0 ]
[ ! -d "$test_subdir" ]
}
@test "safe_remove handles non-existent path gracefully" {
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; safe_remove '$TEST_DIR/nonexistent' true"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; safe_remove '$TEST_DIR/nonexistent' true"
[ "$status" -eq 0 ]
}
@test "safe_remove in silent mode suppresses error output" {
# Try to remove system directory in silent mode
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; safe_remove '/System/test' true 2>&1"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; safe_remove '/System/test' true 2>&1"
[ "$status" -eq 1 ]
# Should not output error in silent mode
}
@@ -103,7 +103,7 @@ teardown() {
# Test safe_find_delete
@test "safe_find_delete validates base directory" {
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; safe_find_delete '/nonexistent' '*.tmp' 7 'f' 2>&1"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; safe_find_delete '/nonexistent' '*.tmp' 7 'f' 2>&1"
[ "$status" -eq 1 ]
}
@@ -113,7 +113,7 @@ teardown() {
mkdir -p "$real_dir"
ln -s "$real_dir" "$link_dir"
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; safe_find_delete '$link_dir' '*.tmp' 7 'f' 2>&1"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; safe_find_delete '$link_dir' '*.tmp' 7 'f' 2>&1"
[ "$status" -eq 1 ]
[[ "$output" == *"symlink"* ]]
@@ -121,7 +121,7 @@ teardown() {
}
@test "safe_find_delete validates type filter" {
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; safe_find_delete '$TEST_DIR' '*.tmp' 7 'x' 2>&1"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; safe_find_delete '$TEST_DIR' '*.tmp' 7 'x' 2>&1"
[ "$status" -eq 1 ]
[[ "$output" == *"Invalid type filter"* ]]
}
@@ -137,21 +137,21 @@ teardown() {
# Make old_file 8 days old (requires touch -t)
touch -t "$(date -v-8d '+%Y%m%d%H%M.%S' 2>/dev/null || date -d '8 days ago' '+%Y%m%d%H%M.%S')" "$old_file" 2>/dev/null || true
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; safe_find_delete '$TEST_DIR' '*.tmp' 7 'f'"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; safe_find_delete '$TEST_DIR' '*.tmp' 7 'f'"
[ "$status" -eq 0 ]
}
# Test MOLE constants are defined
@test "MOLE_* constants are defined" {
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; echo \$MOLE_TEMP_FILE_AGE_DAYS"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; echo \$MOLE_TEMP_FILE_AGE_DAYS"
[ "$status" -eq 0 ]
[ "$output" = "7" ]
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; echo \$MOLE_MAX_PARALLEL_JOBS"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; echo \$MOLE_MAX_PARALLEL_JOBS"
[ "$status" -eq 0 ]
[ "$output" = "15" ]
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; echo \$MOLE_TM_BACKUP_SAFE_HOURS"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; echo \$MOLE_TM_BACKUP_SAFE_HOURS"
[ "$status" -eq 0 ]
[ "$output" = "48" ]
}

View File

@@ -55,12 +55,16 @@ setup() {
@test "build-analyze.sh detects missing Go toolchain" {
if command -v go > /dev/null 2>&1; then
skip "Go is installed, cannot test missing toolchain"
# Go is installed, verify script doesn't error out
# (Don't actually build - too slow)
run bash -c "grep -q 'go build' '$PROJECT_ROOT/scripts/build-analyze.sh'"
[ "$status" -eq 0 ]
else
# Go is missing, verify proper error handling
run "$PROJECT_ROOT/scripts/build-analyze.sh"
[ "$status" -ne 0 ]
[[ "$output" == *"Go not installed"* ]]
fi
run "$PROJECT_ROOT/scripts/build-analyze.sh"
[ "$status" -ne 0 ]
[[ "$output" == *"Go not installed"* ]]
}
@test "build-analyze.sh has version info support" {

View File

@@ -7,8 +7,8 @@ setup_file() {
setup() {
# Source common.sh first (required by sudo_manager)
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/sudo_manager.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
source "$PROJECT_ROOT/lib/core/sudo.sh"
}
# Test sudo session detection
@@ -33,7 +33,7 @@ setup() {
export -f sudo
# These should not crash even without real sudo
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/sudo_manager.sh'; has_sudo_session"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/core/sudo.sh'; has_sudo_session"
[ "$status" -eq 1 ] # Expected: no sudo session
}
@@ -51,7 +51,7 @@ setup() {
# Start keepalive (will run in background)
local pid
pid=$(bash -c "source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/sudo_manager.sh'; _start_sudo_keepalive")
pid=$(bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/core/sudo.sh'; _start_sudo_keepalive")
# Should return a PID (number)
[[ "$pid" =~ ^[0-9]+$ ]]
@@ -63,10 +63,10 @@ setup() {
# Test _stop_sudo_keepalive
@test "_stop_sudo_keepalive handles invalid PID gracefully" {
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/sudo_manager.sh'; _stop_sudo_keepalive ''"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/core/sudo.sh'; _stop_sudo_keepalive ''"
[ "$status" -eq 0 ]
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/sudo_manager.sh'; _stop_sudo_keepalive '99999'"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/core/sudo.sh'; _stop_sudo_keepalive '99999'"
[ "$status" -eq 0 ]
}
@@ -77,12 +77,12 @@ setup() {
# Set a fake PID
export MOLE_SUDO_KEEPALIVE_PID="99999"
run bash -c "export MOLE_SUDO_KEEPALIVE_PID=99999; source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/sudo_manager.sh'; stop_sudo_session"
run bash -c "export MOLE_SUDO_KEEPALIVE_PID=99999; source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/core/sudo.sh'; stop_sudo_session"
[ "$status" -eq 0 ]
}
# Test global state management
@test "sudo manager initializes global state correctly" {
result=$(bash -c "source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/sudo_manager.sh'; echo \$MOLE_SUDO_ESTABLISHED")
result=$(bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/core/sudo.sh'; echo \$MOLE_SUDO_ESTABLISHED")
[[ "$result" == "false" ]] || [[ -z "$result" ]]
}

View File

@@ -42,7 +42,7 @@ create_app_artifacts() {
result="$(
HOME="$HOME" bash --noprofile --norc << 'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
find_app_files "com.example.TestApp" "TestApp"
EOF
)"
@@ -62,7 +62,7 @@ EOF
result="$(
HOME="$HOME" bash --noprofile --norc << 'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
files="$(printf '%s\n%s\n' "$HOME/sized/file1" "$HOME/sized/file2")"
calculate_total_size "$files"
EOF
@@ -77,8 +77,8 @@ EOF
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/uninstall_batch.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
source "$PROJECT_ROOT/lib/uninstall.sh"
# Test stubs
request_sudo_access() { return 0; }
@@ -119,8 +119,8 @@ EOF
@test "decode_file_list validates base64 encoding" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/uninstall_batch.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
source "$PROJECT_ROOT/lib/uninstall.sh"
# Valid base64 encoded path list
valid_data=$(printf '/path/one\n/path/two' | base64)
@@ -134,8 +134,8 @@ EOF
@test "decode_file_list rejects invalid base64" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/uninstall_batch.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
source "$PROJECT_ROOT/lib/uninstall.sh"
# Invalid base64 - function should return empty and fail
if result=$(decode_file_list "not-valid-base64!!!" "TestApp" 2>/dev/null); then
@@ -153,8 +153,8 @@ EOF
@test "decode_file_list handles empty input" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/uninstall_batch.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
source "$PROJECT_ROOT/lib/uninstall.sh"
# Empty base64
empty_data=$(printf '' | base64)
@@ -169,8 +169,8 @@ EOF
@test "decode_file_list rejects non-absolute paths" {
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
set -euo pipefail
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/uninstall_batch.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
source "$PROJECT_ROOT/lib/uninstall.sh"
# Relative path - function should reject it
bad_data=$(printf 'relative/path' | base64)

View File

@@ -22,8 +22,8 @@ teardown_file() {
}
setup() {
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/update_manager.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
source "$PROJECT_ROOT/lib/manage/update.sh"
}
# Test brew_has_outdated function
@@ -33,7 +33,7 @@ setup() {
}
export -f brew
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/update_manager.sh'; brew_has_outdated"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/manage/update.sh'; brew_has_outdated"
[ "$status" -eq 1 ]
}
@@ -49,7 +49,7 @@ setup() {
}
export -f brew
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/update_manager.sh'; brew_has_outdated"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/manage/update.sh'; brew_has_outdated"
[ "$status" -eq 0 ]
}
@@ -64,30 +64,30 @@ setup() {
}
export -f brew
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/update_manager.sh'; brew_has_outdated cask"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/manage/update.sh'; brew_has_outdated cask"
[ "$status" -eq 0 ]
}
# Test format_brew_update_label function
@test "format_brew_update_label returns empty when no updates" {
result=$(BREW_OUTDATED_COUNT=0 bash -c "source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/update_manager.sh'; format_brew_update_label")
result=$(BREW_OUTDATED_COUNT=0 bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/manage/update.sh'; format_brew_update_label")
[[ -z "$result" ]]
}
@test "format_brew_update_label formats with formula and cask counts" {
result=$(BREW_OUTDATED_COUNT=5 BREW_FORMULA_OUTDATED_COUNT=3 BREW_CASK_OUTDATED_COUNT=2 bash -c "source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/update_manager.sh'; format_brew_update_label")
result=$(BREW_OUTDATED_COUNT=5 BREW_FORMULA_OUTDATED_COUNT=3 BREW_CASK_OUTDATED_COUNT=2 bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/manage/update.sh'; format_brew_update_label")
[[ "$result" =~ "3 formula" ]]
[[ "$result" =~ "2 cask" ]]
}
@test "format_brew_update_label shows total when breakdown unavailable" {
result=$(BREW_OUTDATED_COUNT=5 bash -c "source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/update_manager.sh'; format_brew_update_label")
result=$(BREW_OUTDATED_COUNT=5 bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/manage/update.sh'; format_brew_update_label")
[[ "$result" =~ "5 updates" ]]
}
# Test ask_for_updates function
@test "ask_for_updates returns 1 when no updates available" {
run bash -c "source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/update_manager.sh'; ask_for_updates < /dev/null"
run bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/manage/update.sh'; ask_for_updates < /dev/null"
[ "$status" -eq 1 ]
}
@@ -98,7 +98,7 @@ setup() {
export BREW_CASK_OUTDATED_COUNT=2
# Use input redirection to simulate ESC (cancel)
run bash -c "printf '\x1b' | source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/update_manager.sh'; ask_for_updates"
run bash -c "printf '\x1b' | source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/manage/update.sh'; ask_for_updates"
# Should show updates and ask for confirmation
[ "$status" -eq 1 ] # ESC cancels
}
@@ -106,21 +106,21 @@ setup() {
@test "ask_for_updates detects App Store updates" {
export APPSTORE_UPDATE_COUNT=3
run bash -c "printf '\x1b' | source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/update_manager.sh'; ask_for_updates"
run bash -c "printf '\x1b' | source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/manage/update.sh'; ask_for_updates"
[ "$status" -eq 1 ] # ESC cancels
}
@test "ask_for_updates detects macOS updates" {
export MACOS_UPDATE_AVAILABLE=true
run bash -c "printf '\x1b' | source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/update_manager.sh'; ask_for_updates"
run bash -c "printf '\x1b' | source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/manage/update.sh'; ask_for_updates"
[ "$status" -eq 1 ] # ESC cancels
}
@test "ask_for_updates detects Mole updates" {
export MOLE_UPDATE_AVAILABLE=true
run bash -c "printf '\x1b' | source '$PROJECT_ROOT/lib/common.sh'; source '$PROJECT_ROOT/lib/update_manager.sh'; ask_for_updates"
run bash -c "printf '\x1b' | source '$PROJECT_ROOT/lib/core/common.sh'; source '$PROJECT_ROOT/lib/manage/update.sh'; ask_for_updates"
[ "$status" -eq 1 ] # ESC cancels
}

View File

@@ -44,7 +44,7 @@ brew() {
esac
}
export -f brew start_inline_spinner stop_inline_spinner
source "$PROJECT_ROOT/lib/common.sh"
source "$PROJECT_ROOT/lib/core/common.sh"
update_via_homebrew "1.7.9"
EOF

View File

@@ -28,7 +28,7 @@ setup() {
@test "patterns_equivalent treats paths with tilde expansion as equal" {
local status
if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; patterns_equivalent '~/.cache/test' \"\$HOME/.cache/test\""; then
if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/manage/whitelist.sh'; patterns_equivalent '~/.cache/test' \"\$HOME/.cache/test\""; then
status=0
else
status=$?
@@ -38,7 +38,7 @@ setup() {
@test "patterns_equivalent distinguishes different paths" {
local status
if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; patterns_equivalent '~/.cache/test' \"\$HOME/.cache/other\""; then
if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/manage/whitelist.sh'; patterns_equivalent '~/.cache/test' \"\$HOME/.cache/other\""; then
status=0
else
status=$?
@@ -47,7 +47,7 @@ setup() {
}
@test "save_whitelist_patterns keeps unique entries and preserves header" {
HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; save_whitelist_patterns \"\$HOME/.cache/foo\" \"\$HOME/.cache/foo\" \"\$HOME/.cache/bar\""
HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/manage/whitelist.sh'; save_whitelist_patterns \"\$HOME/.cache/foo\" \"\$HOME/.cache/foo\" \"\$HOME/.cache/bar\""
[[ -f "$WHITELIST_PATH" ]]
@@ -64,8 +64,8 @@ setup() {
@test "load_whitelist falls back to defaults when config missing" {
rm -f "$WHITELIST_PATH"
HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; rm -f \"\$HOME/.config/mole/whitelist\"; load_whitelist; printf '%s\n' \"\${CURRENT_WHITELIST_PATTERNS[@]}\"" > "$HOME/current_whitelist.txt"
HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; printf '%s\n' \"\${DEFAULT_WHITELIST_PATTERNS[@]}\"" > "$HOME/default_whitelist.txt"
HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/manage/whitelist.sh'; rm -f \"\$HOME/.config/mole/whitelist\"; load_whitelist; printf '%s\n' \"\${CURRENT_WHITELIST_PATTERNS[@]}\"" > "$HOME/current_whitelist.txt"
HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/manage/whitelist.sh'; printf '%s\n' \"\${DEFAULT_WHITELIST_PATTERNS[@]}\"" > "$HOME/default_whitelist.txt"
current=()
while IFS= read -r line; do
@@ -83,14 +83,14 @@ setup() {
@test "is_whitelisted matches saved patterns exactly" {
local status
if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; save_whitelist_patterns \"\$HOME/.cache/unique-pattern\"; load_whitelist; is_whitelisted \"\$HOME/.cache/unique-pattern\""; then
if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/manage/whitelist.sh'; save_whitelist_patterns \"\$HOME/.cache/unique-pattern\"; load_whitelist; is_whitelisted \"\$HOME/.cache/unique-pattern\""; then
status=0
else
status=$?
fi
[ "$status" -eq 0 ]
if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; save_whitelist_patterns \"\$HOME/.cache/unique-pattern\"; load_whitelist; is_whitelisted \"\$HOME/.cache/other-pattern\""; then
if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/manage/whitelist.sh'; save_whitelist_patterns \"\$HOME/.cache/unique-pattern\"; load_whitelist; is_whitelisted \"\$HOME/.cache/other-pattern\""; then
status=0
else
status=$?