1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 18:34:46 +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

187
lib/manage/autofix.sh Normal file
View File

@@ -0,0 +1,187 @@
#!/bin/bash
# Auto-fix Manager
# Unified auto-fix suggestions and execution
set -euo pipefail
# Show system suggestions with auto-fix markers
show_suggestions() {
local has_suggestions=false
local can_auto_fix=false
local -a auto_fix_items=()
local -a manual_items=()
# Security suggestions
if [[ -n "${FIREWALL_DISABLED:-}" && "${FIREWALL_DISABLED}" == "true" ]]; then
auto_fix_items+=("Enable Firewall for better security")
has_suggestions=true
can_auto_fix=true
fi
if [[ -n "${FILEVAULT_DISABLED:-}" && "${FILEVAULT_DISABLED}" == "true" ]]; then
manual_items+=("Enable FileVault|System Settings → Privacy & Security → FileVault")
has_suggestions=true
fi
# Configuration suggestions
if [[ -n "${TOUCHID_NOT_CONFIGURED:-}" && "${TOUCHID_NOT_CONFIGURED}" == "true" ]]; then
auto_fix_items+=("Enable Touch ID for sudo")
has_suggestions=true
can_auto_fix=true
fi
if [[ -n "${ROSETTA_NOT_INSTALLED:-}" && "${ROSETTA_NOT_INSTALLED}" == "true" ]]; then
auto_fix_items+=("Install Rosetta 2 for Intel app support")
has_suggestions=true
can_auto_fix=true
fi
# Health suggestions
if [[ -n "${CACHE_SIZE_GB:-}" ]]; then
local cache_gb="${CACHE_SIZE_GB:-0}"
if (($(echo "$cache_gb > 5" | bc -l 2> /dev/null || echo 0))); then
manual_items+=("Free up ${cache_gb}GB by cleaning caches|Run: mo clean")
has_suggestions=true
fi
fi
if [[ -n "${BREW_HAS_WARNINGS:-}" && "${BREW_HAS_WARNINGS}" == "true" ]]; then
manual_items+=("Fix Homebrew warnings|Run: brew doctor to see details")
has_suggestions=true
fi
if [[ -n "${DISK_FREE_GB:-}" && "${DISK_FREE_GB:-0}" -lt 50 ]]; then
if [[ -z "${CACHE_SIZE_GB:-}" ]] || (($(echo "${CACHE_SIZE_GB:-0} <= 5" | bc -l 2> /dev/null || echo 1))); then
manual_items+=("Low disk space (${DISK_FREE_GB}GB free)|Run: mo analyze to find large files")
has_suggestions=true
fi
fi
# Display suggestions
echo -e "${BLUE}${ICON_ARROW}${NC} Suggestions"
if [[ "$has_suggestions" == "false" ]]; then
echo -e " ${GREEN}${NC} All looks good"
export HAS_AUTO_FIX_SUGGESTIONS="false"
return
fi
# Show auto-fix items
if [[ ${#auto_fix_items[@]} -gt 0 ]]; then
for item in "${auto_fix_items[@]}"; do
echo -e " ${YELLOW}${ICON_WARNING}${NC} ${item} ${GREEN}[auto]${NC}"
done
fi
# Show manual items
if [[ ${#manual_items[@]} -gt 0 ]]; then
for item in "${manual_items[@]}"; do
local title="${item%%|*}"
local hint="${item#*|}"
echo -e " ${YELLOW}${ICON_WARNING}${NC} ${title}"
echo -e " ${GRAY}${hint}${NC}"
done
fi
# Export for use in auto-fix
export HAS_AUTO_FIX_SUGGESTIONS="$can_auto_fix"
}
# Ask user if they want to auto-fix
# Returns: 0 if yes, 1 if no
ask_for_auto_fix() {
if [[ "${HAS_AUTO_FIX_SUGGESTIONS:-false}" != "true" ]]; then
return 1
fi
echo -ne "${PURPLE}${ICON_ARROW}${NC} Auto-fix issues now? ${GRAY}Enter confirm / ESC cancel${NC}: "
local key
if ! key=$(read_key); then
echo "no"
echo ""
return 1
fi
if [[ "$key" == "ENTER" ]]; then
echo "yes"
echo ""
return 0
else
echo "no"
echo ""
return 1
fi
}
# Perform auto-fixes
# Returns: number of fixes applied
perform_auto_fix() {
local fixed_count=0
local -a fixed_items=()
# Ensure sudo access
if ! has_sudo_session; then
if ! ensure_sudo_session "System fixes require admin access"; then
echo -e "${YELLOW}Skipping auto fixes (admin authentication required)${NC}"
echo ""
return 0
fi
fi
# Fix Firewall
if [[ -n "${FIREWALL_DISABLED:-}" && "${FIREWALL_DISABLED}" == "true" ]]; then
echo -e "${BLUE}Enabling Firewall...${NC}"
if sudo defaults write /Library/Preferences/com.apple.alf globalstate -int 1 2> /dev/null; then
echo -e "${GREEN}${NC} Firewall enabled"
((fixed_count++))
fixed_items+=("Firewall enabled")
else
echo -e "${RED}${NC} Failed to enable Firewall"
fi
echo ""
fi
# Fix Touch ID
if [[ -n "${TOUCHID_NOT_CONFIGURED:-}" && "${TOUCHID_NOT_CONFIGURED}" == "true" ]]; then
echo -e "${BLUE}${ICON_ARROW}${NC} Configuring Touch ID for sudo..."
local pam_file="/etc/pam.d/sudo"
if sudo bash -c "grep -q 'pam_tid.so' '$pam_file' 2>/dev/null || sed -i '' '2i\\
auth sufficient pam_tid.so
' '$pam_file'" 2> /dev/null; then
echo -e "${GREEN}${NC} Touch ID configured"
((fixed_count++))
fixed_items+=("Touch ID configured for sudo")
else
echo -e "${RED}${NC} Failed to configure Touch ID"
fi
echo ""
fi
# Install Rosetta 2
if [[ -n "${ROSETTA_NOT_INSTALLED:-}" && "${ROSETTA_NOT_INSTALLED}" == "true" ]]; then
echo -e "${BLUE}Installing Rosetta 2...${NC}"
if sudo softwareupdate --install-rosetta --agree-to-license 2>&1 | grep -qE "(Installing|Installed|already installed)"; then
echo -e "${GREEN}${NC} Rosetta 2 installed"
((fixed_count++))
fixed_items+=("Rosetta 2 installed")
else
echo -e "${RED}${NC} Failed to install Rosetta 2"
fi
echo ""
fi
if [[ $fixed_count -gt 0 ]]; then
AUTO_FIX_SUMMARY="Auto fixes applied: ${fixed_count} issue(s)"
if [[ ${#fixed_items[@]} -gt 0 ]]; then
AUTO_FIX_DETAILS=$(printf '%s\n' "${fixed_items[@]}")
else
AUTO_FIX_DETAILS=""
fi
else
AUTO_FIX_SUMMARY="Auto fixes skipped: No changes were required"
AUTO_FIX_DETAILS=""
fi
export AUTO_FIX_SUMMARY AUTO_FIX_DETAILS
return 0
}

335
lib/manage/update.sh Normal file
View File

@@ -0,0 +1,335 @@
#!/bin/bash
# Update Manager
# Unified update execution for all update types
set -euo pipefail
# Format Homebrew update label for display
format_brew_update_label() {
local total="${BREW_OUTDATED_COUNT:-0}"
if [[ -z "$total" || "$total" -le 0 ]]; then
return
fi
local -a details=()
local formulas="${BREW_FORMULA_OUTDATED_COUNT:-0}"
local casks="${BREW_CASK_OUTDATED_COUNT:-0}"
((formulas > 0)) && details+=("${formulas} formula")
((casks > 0)) && details+=("${casks} cask")
local detail_str="(${total} updates)"
if ((${#details[@]} > 0)); then
detail_str="($(
IFS=', '
printf '%s' "${details[*]}"
))"
fi
printf " • Homebrew %s" "$detail_str"
}
brew_has_outdated() {
local kind="${1:-formula}"
command -v brew > /dev/null 2>&1 || return 1
if [[ "$kind" == "cask" ]]; then
brew outdated --cask --quiet 2> /dev/null | grep -q .
else
brew outdated --quiet 2> /dev/null | grep -q .
fi
}
# Ask user if they want to update
# Returns: 0 if yes, 1 if no
ask_for_updates() {
local has_updates=false
local -a update_list=()
local brew_entry
brew_entry=$(format_brew_update_label || true)
if [[ -n "$brew_entry" ]]; then
has_updates=true
update_list+=("$brew_entry")
fi
if [[ -n "${APPSTORE_UPDATE_COUNT:-}" && "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 ]]; then
has_updates=true
update_list+=(" • App Store (${APPSTORE_UPDATE_COUNT} apps)")
fi
if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" ]]; then
has_updates=true
update_list+=(" • macOS system")
fi
if [[ -n "${MOLE_UPDATE_AVAILABLE:-}" && "${MOLE_UPDATE_AVAILABLE}" == "true" ]]; then
has_updates=true
update_list+=(" • Mole")
fi
if [[ "$has_updates" == "false" ]]; then
return 1
fi
echo -e "${BLUE}AVAILABLE UPDATES${NC}"
for item in "${update_list[@]}"; do
echo -e "$item"
done
echo ""
echo -ne "${YELLOW}Update all now?${NC} ${GRAY}Enter confirm / ESC cancel${NC}: "
local key
if ! key=$(read_key); then
echo "skip"
echo ""
return 1
fi
if [[ "$key" == "ENTER" ]]; then
echo "yes"
echo ""
return 0
else
echo "skip"
echo ""
return 1
fi
}
# Perform all pending updates
# Returns: 0 if all succeeded, 1 if some failed
perform_updates() {
local updated_count=0
local total_count=0
local brew_formula="${BREW_FORMULA_OUTDATED_COUNT:-0}"
local brew_cask="${BREW_CASK_OUTDATED_COUNT:-0}"
# Get update labels
local -a appstore_labels=()
if [[ -n "${APPSTORE_UPDATE_COUNT:-}" && "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 ]]; then
while IFS= read -r label; do
[[ -n "$label" ]] && appstore_labels+=("$label")
done < <(get_appstore_update_labels || true)
fi
local -a macos_labels=()
if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" ]]; then
while IFS= read -r label; do
[[ -n "$label" ]] && macos_labels+=("$label")
done < <(get_macos_update_labels || true)
fi
# Check fallback needed
local appstore_needs_fallback=false
local macos_needs_fallback=false
if [[ -n "${APPSTORE_UPDATE_COUNT:-}" && "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 && ${#appstore_labels[@]} -eq 0 ]]; then
appstore_needs_fallback=true
fi
if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" && ${#macos_labels[@]} -eq 0 ]]; then
macos_needs_fallback=true
fi
# Count total updates
((brew_formula > 0)) && ((total_count++))
((brew_cask > 0)) && ((total_count++))
[[ -n "${APPSTORE_UPDATE_COUNT:-}" && "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 ]] && ((total_count++))
[[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" ]] && ((total_count++))
[[ -n "${MOLE_UPDATE_AVAILABLE:-}" && "${MOLE_UPDATE_AVAILABLE}" == "true" ]] && ((total_count++))
# Update Homebrew formulae
if ((brew_formula > 0)); then
if ! brew_has_outdated "formula"; then
echo -e "${GRAY}-${NC} Homebrew formulae already up to date"
((total_count--))
echo ""
else
echo -e "${BLUE}Updating Homebrew formulae...${NC}"
local spinner_started=false
if [[ -t 1 ]]; then
start_inline_spinner "Running brew upgrade"
spinner_started=true
fi
local brew_output=""
local brew_status=0
if ! brew_output=$(brew upgrade 2>&1); then
brew_status=$?
fi
if [[ "$spinner_started" == "true" ]]; then
stop_inline_spinner
fi
local filtered_output
filtered_output=$(echo "$brew_output" | grep -Ev "^(==>|Warning:)" || true)
[[ -n "$filtered_output" ]] && echo "$filtered_output"
if [[ ${brew_status:-0} -eq 0 ]]; then
echo -e "${GREEN}${NC} Homebrew formulae updated"
reset_brew_cache
((updated_count++))
else
echo -e "${RED}${NC} Homebrew formula update failed"
fi
echo ""
fi
fi
# Update Homebrew casks
if ((brew_cask > 0)); then
if ! brew_has_outdated "cask"; then
echo -e "${GRAY}-${NC} Homebrew casks already up to date"
((total_count--))
echo ""
else
echo -e "${BLUE}Updating Homebrew casks...${NC}"
local spinner_started=false
if [[ -t 1 ]]; then
start_inline_spinner "Running brew upgrade --cask"
spinner_started=true
fi
local brew_output=""
local brew_status=0
if ! brew_output=$(brew upgrade --cask 2>&1); then
brew_status=$?
fi
if [[ "$spinner_started" == "true" ]]; then
stop_inline_spinner
fi
local filtered_output
filtered_output=$(echo "$brew_output" | grep -Ev "^(==>|Warning:)" || true)
[[ -n "$filtered_output" ]] && echo "$filtered_output"
if [[ ${brew_status:-0} -eq 0 ]]; then
echo -e "${GREEN}${NC} Homebrew casks updated"
reset_brew_cache
((updated_count++))
else
echo -e "${RED}${NC} Homebrew cask update failed"
fi
echo ""
fi
fi
# Update App Store apps
local macos_handled_via_appstore=false
if [[ -n "${APPSTORE_UPDATE_COUNT:-}" && "${APPSTORE_UPDATE_COUNT:-0}" -gt 0 ]]; then
# Check sudo access
if ! has_sudo_session; then
if ! ensure_sudo_session "Software updates require admin access"; then
echo -e "${YELLOW}${ICON_WARNING}${NC} Skipping App Store updates (admin authentication required)"
echo ""
((total_count--))
if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" ]]; then
((total_count--))
fi
else
_perform_appstore_update
fi
else
_perform_appstore_update
fi
fi
# Update macOS
if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" && "$macos_handled_via_appstore" != "true" ]]; then
if ! has_sudo_session; then
echo -e "${YELLOW}${ICON_WARNING}${NC} Skipping macOS updates (admin authentication required)"
echo ""
else
_perform_macos_update
fi
fi
# Update Mole
if [[ -n "${MOLE_UPDATE_AVAILABLE:-}" && "${MOLE_UPDATE_AVAILABLE}" == "true" ]]; then
echo -e "${BLUE}Updating Mole...${NC}"
if "${SCRIPT_DIR}/mole" update 2>&1 | grep -qE "(Updated|latest version)"; then
echo -e "${GREEN}${NC} Mole updated"
reset_mole_cache
((updated_count++))
else
echo -e "${RED}${NC} Mole update failed"
fi
echo ""
fi
# Summary
if [[ $updated_count -eq $total_count && $total_count -gt 0 ]]; then
echo -e "${GREEN}All updates completed (${updated_count}/${total_count})${NC}"
return 0
elif [[ $updated_count -gt 0 ]]; then
echo -e "${YELLOW}Partial updates completed (${updated_count}/${total_count})${NC}"
return 1
else
echo -e "${RED}No updates were completed${NC}"
return 1
fi
}
# Internal: Perform App Store update
_perform_appstore_update() {
echo -e "${BLUE}Updating App Store apps...${NC}"
local appstore_log
appstore_log=$(mktemp -t mole-appstore 2> /dev/null || echo "/tmp/mole-appstore.log")
if [[ "$appstore_needs_fallback" == "true" ]]; then
echo -e " ${GRAY}Installing all available updates${NC}"
if sudo softwareupdate -i -a 2>&1 | tee "$appstore_log" | grep -v "^$"; then
echo -e "${GREEN}${NC} Software updates completed"
((updated_count++))
if [[ -n "${MACOS_UPDATE_AVAILABLE:-}" && "${MACOS_UPDATE_AVAILABLE}" == "true" ]]; then
macos_handled_via_appstore=true
((updated_count++))
fi
else
echo -e "${RED}${NC} Software update failed"
fi
else
if sudo softwareupdate -i "${appstore_labels[@]}" 2>&1 | tee "$appstore_log" | grep -v "^$"; then
echo -e "${GREEN}${NC} App Store apps updated"
((updated_count++))
else
echo -e "${RED}${NC} App Store update failed"
fi
fi
rm -f "$appstore_log" 2> /dev/null || true
reset_softwareupdate_cache
echo ""
}
# Internal: Perform macOS update
_perform_macos_update() {
echo -e "${BLUE}Updating macOS...${NC}"
echo -e "${YELLOW}Note:${NC} System update may require restart"
local macos_log
macos_log=$(mktemp -t mole-macos 2> /dev/null || echo "/tmp/mole-macos.log")
if [[ "$macos_needs_fallback" == "true" ]]; then
if sudo softwareupdate -i -r 2>&1 | tee "$macos_log" | grep -v "^$"; then
echo -e "${GREEN}${NC} macOS updated"
((updated_count++))
else
echo -e "${RED}${NC} macOS update failed"
fi
else
if sudo softwareupdate -i "${macos_labels[@]}" 2>&1 | tee "$macos_log" | grep -v "^$"; then
echo -e "${GREEN}${NC} macOS updated"
((updated_count++))
else
echo -e "${RED}${NC} macOS update failed"
fi
fi
if grep -qi "restart" "$macos_log" 2> /dev/null; then
echo -e "${YELLOW}${ICON_WARNING}${NC} Restart required to complete update"
fi
rm -f "$macos_log" 2> /dev/null || true
reset_softwareupdate_cache
echo ""
}

339
lib/manage/whitelist.sh Executable file
View File

@@ -0,0 +1,339 @@
#!/bin/bash
# Whitelist management functionality
# Shows actual files that would be deleted by dry-run
set -euo pipefail
# Get script directory and source dependencies
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
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/core/common.sh:
# - DEFAULT_WHITELIST_PATTERNS
# - FINDER_METADATA_SENTINEL
# Save whitelist patterns to config
save_whitelist_patterns() {
local -a patterns
patterns=("$@")
mkdir -p "$(dirname "$WHITELIST_CONFIG")"
cat > "$WHITELIST_CONFIG" << 'EOF'
# Mole Whitelist - Protected paths won't be deleted
# Default protections: Playwright browsers, HuggingFace models, Maven repo, Ollama models, Surge Mac, R renv, Finder metadata
# Add one pattern per line to keep items safe.
EOF
if [[ ${#patterns[@]} -gt 0 ]]; then
local -a unique_patterns=()
for pattern in "${patterns[@]}"; do
local duplicate="false"
if [[ ${#unique_patterns[@]} -gt 0 ]]; then
for existing in "${unique_patterns[@]}"; do
if patterns_equivalent "$pattern" "$existing"; then
duplicate="true"
break
fi
done
fi
[[ "$duplicate" == "true" ]] && continue
unique_patterns+=("$pattern")
done
if [[ ${#unique_patterns[@]} -gt 0 ]]; then
printf '\n' >> "$WHITELIST_CONFIG"
for pattern in "${unique_patterns[@]}"; do
echo "$pattern" >> "$WHITELIST_CONFIG"
done
fi
fi
}
# Get all cache items with their patterns
get_all_cache_items() {
# Format: "display_name|pattern|category"
cat << 'EOF'
Apple Mail cache|$HOME/Library/Caches/com.apple.mail/*|system_cache
Gradle build cache (Android Studio, Gradle projects)|$HOME/.gradle/caches/*|ide_cache
Gradle daemon processes cache|$HOME/.gradle/daemon/*|ide_cache
Xcode DerivedData (build outputs, indexes)|$HOME/Library/Developer/Xcode/DerivedData/*|ide_cache
Xcode internal cache files|$HOME/Library/Caches/com.apple.dt.Xcode/*|ide_cache
Xcode iOS device support symbols|$HOME/Library/Developer/Xcode/iOS DeviceSupport/*/Symbols/System/Library/Caches/*|ide_cache
Maven local repository (Java dependencies)|$HOME/.m2/repository/*|ide_cache
JetBrains IDEs cache (IntelliJ, PyCharm, WebStorm)|$HOME/Library/Caches/JetBrains/*|ide_cache
Android Studio cache and indexes|$HOME/Library/Caches/Google/AndroidStudio*/*|ide_cache
Android build cache|$HOME/.android/build-cache/*|ide_cache
VS Code runtime cache|$HOME/Library/Application Support/Code/Cache/*|ide_cache
VS Code extension and update cache|$HOME/Library/Application Support/Code/CachedData/*|ide_cache
VS Code system cache (Cursor, VSCodium)|$HOME/Library/Caches/com.microsoft.VSCode/*|ide_cache
Cursor editor cache|$HOME/Library/Caches/com.todesktop.230313mzl4w4u92/*|ide_cache
Bazel build cache|$HOME/.cache/bazel/*|compiler_cache
Go build cache and module cache|$HOME/Library/Caches/go-build/*|compiler_cache
Go module cache|$HOME/go/pkg/mod/cache/*|compiler_cache
Rust Cargo registry cache|$HOME/.cargo/registry/cache/*|compiler_cache
Rust documentation cache|$HOME/.rustup/toolchains/*/share/doc/*|compiler_cache
Rustup toolchain downloads|$HOME/.rustup/downloads/*|compiler_cache
ccache compiler cache|$HOME/.ccache/*|compiler_cache
sccache distributed compiler cache|$HOME/.cache/sccache/*|compiler_cache
SBT Scala build cache|$HOME/.sbt/*|compiler_cache
Ivy dependency cache|$HOME/.ivy2/cache/*|compiler_cache
Turbo monorepo build cache|$HOME/.turbo/*|compiler_cache
Next.js build cache|$HOME/.next/*|compiler_cache
Vite build cache|$HOME/.vite/*|compiler_cache
Parcel bundler cache|$HOME/.parcel-cache/*|compiler_cache
pre-commit hooks cache|$HOME/.cache/pre-commit/*|compiler_cache
Ruff Python linter cache|$HOME/.cache/ruff/*|compiler_cache
MyPy type checker cache|$HOME/.cache/mypy/*|compiler_cache
Pytest test cache|$HOME/.pytest_cache/*|compiler_cache
Flutter SDK cache|$HOME/.cache/flutter/*|compiler_cache
Swift Package Manager cache|$HOME/.cache/swift-package-manager/*|compiler_cache
Zig compiler cache|$HOME/.cache/zig/*|compiler_cache
Deno cache|$HOME/Library/Caches/deno/*|compiler_cache
CocoaPods cache (iOS dependencies)|$HOME/Library/Caches/CocoaPods/*|package_manager
npm package cache|$HOME/.npm/_cacache/*|package_manager
pip Python package cache|$HOME/.cache/pip/*|package_manager
uv Python package cache|$HOME/.cache/uv/*|package_manager
R renv global cache (virtual environments)|$HOME/Library/Caches/org.R-project.R/R/renv/*|package_manager
Homebrew downloaded packages|$HOME/Library/Caches/Homebrew/*|package_manager
Yarn package manager cache|$HOME/.cache/yarn/*|package_manager
pnpm package store|$HOME/.pnpm-store/*|package_manager
Composer PHP dependencies cache|$HOME/.composer/cache/*|package_manager
RubyGems cache|$HOME/.gem/cache/*|package_manager
Conda packages cache|$HOME/.conda/pkgs/*|package_manager
Anaconda packages cache|$HOME/anaconda3/pkgs/*|package_manager
PyTorch model cache|$HOME/.cache/torch/*|ai_ml_cache
TensorFlow model and dataset cache|$HOME/.cache/tensorflow/*|ai_ml_cache
HuggingFace models and datasets|$HOME/.cache/huggingface/*|ai_ml_cache
Playwright browser binaries|$HOME/Library/Caches/ms-playwright*|ai_ml_cache
Selenium WebDriver binaries|$HOME/.cache/selenium/*|ai_ml_cache
Ollama local AI models|$HOME/.ollama/models/*|ai_ml_cache
Weights & Biases ML experiments cache|$HOME/.cache/wandb/*|ai_ml_cache
Safari web browser cache|$HOME/Library/Caches/com.apple.Safari/*|browser_cache
Chrome browser cache|$HOME/Library/Caches/Google/Chrome/*|browser_cache
Firefox browser cache|$HOME/Library/Caches/Firefox/*|browser_cache
Brave browser cache|$HOME/Library/Caches/BraveSoftware/Brave-Browser/*|browser_cache
Surge proxy cache|$HOME/Library/Caches/com.nssurge.surge-mac/*|network_tools
Surge configuration and data|$HOME/Library/Application Support/com.nssurge.surge-mac/*|network_tools
Docker Desktop image cache|$HOME/Library/Containers/com.docker.docker/Data/*|container_cache
Podman container cache|$HOME/.local/share/containers/cache/*|container_cache
Font cache|$HOME/Library/Caches/com.apple.FontRegistry/*|system_cache
Spotlight metadata cache|$HOME/Library/Caches/com.apple.spotlight/*|system_cache
CloudKit cache|$HOME/Library/Caches/CloudKit/*|system_cache
EOF
# Add FINDER_METADATA with constant reference
echo "Finder metadata (.DS_Store)|$FINDER_METADATA_SENTINEL|system_cache"
}
patterns_equivalent() {
local first="${1/#~/$HOME}"
local second="${2/#~/$HOME}"
# Only exact string match, no glob expansion
[[ "$first" == "$second" ]] && return 0
return 1
}
load_whitelist() {
local -a patterns=()
if [[ -f "$WHITELIST_CONFIG" ]]; then
while IFS= read -r line; do
line="${line#${line%%[![:space:]]*}}"
line="${line%${line##*[![:space:]]}}"
[[ -z "$line" || "$line" =~ ^# ]] && continue
patterns+=("$line")
done < "$WHITELIST_CONFIG"
else
patterns=("${DEFAULT_WHITELIST_PATTERNS[@]}")
fi
if [[ ${#patterns[@]} -gt 0 ]]; then
local -a unique_patterns=()
for pattern in "${patterns[@]}"; do
local duplicate="false"
if [[ ${#unique_patterns[@]} -gt 0 ]]; then
for existing in "${unique_patterns[@]}"; do
if patterns_equivalent "$pattern" "$existing"; then
duplicate="true"
break
fi
done
fi
[[ "$duplicate" == "true" ]] && continue
unique_patterns+=("$pattern")
done
CURRENT_WHITELIST_PATTERNS=("${unique_patterns[@]}")
else
CURRENT_WHITELIST_PATTERNS=()
fi
}
is_whitelisted() {
local pattern="$1"
local check_pattern="${pattern/#\~/$HOME}"
if [[ ${#CURRENT_WHITELIST_PATTERNS[@]} -eq 0 ]]; then
return 1
fi
for existing in "${CURRENT_WHITELIST_PATTERNS[@]}"; do
local existing_expanded="${existing/#\~/$HOME}"
# Only use exact string match to prevent glob expansion security issues
if [[ "$check_pattern" == "$existing_expanded" ]]; then
return 0
fi
done
return 1
}
manage_whitelist() {
manage_whitelist_categories
}
manage_whitelist_categories() {
# Load currently enabled patterns from both sources
load_whitelist
# Build cache items list
local -a cache_items=()
local -a cache_patterns=()
local -a menu_options=()
local index=0
while IFS='|' read -r display_name pattern _; do
# Expand $HOME in pattern
pattern="${pattern/\$HOME/$HOME}"
cache_items+=("$display_name")
cache_patterns+=("$pattern")
menu_options+=("$display_name")
((index++))
done < <(get_all_cache_items)
# Identify custom patterns (not in predefined list)
local -a custom_patterns=()
if [[ ${#CURRENT_WHITELIST_PATTERNS[@]} -gt 0 ]]; then
for current_pattern in "${CURRENT_WHITELIST_PATTERNS[@]}"; do
local is_predefined=false
for predefined_pattern in "${cache_patterns[@]}"; do
if patterns_equivalent "$current_pattern" "$predefined_pattern"; then
is_predefined=true
break
fi
done
if [[ "$is_predefined" == "false" ]]; then
custom_patterns+=("$current_pattern")
fi
done
fi
# Prioritize already-selected items to appear first
local -a selected_cache_items=()
local -a selected_cache_patterns=()
local -a selected_menu_options=()
local -a remaining_cache_items=()
local -a remaining_cache_patterns=()
local -a remaining_menu_options=()
for ((i = 0; i < ${#cache_patterns[@]}; i++)); do
if is_whitelisted "${cache_patterns[i]}"; then
selected_cache_items+=("${cache_items[i]}")
selected_cache_patterns+=("${cache_patterns[i]}")
selected_menu_options+=("${menu_options[i]}")
else
remaining_cache_items+=("${cache_items[i]}")
remaining_cache_patterns+=("${cache_patterns[i]}")
remaining_menu_options+=("${menu_options[i]}")
fi
done
cache_items=()
cache_patterns=()
menu_options=()
if [[ ${#selected_cache_items[@]} -gt 0 ]]; then
cache_items=("${selected_cache_items[@]}")
cache_patterns=("${selected_cache_patterns[@]}")
menu_options=("${selected_menu_options[@]}")
fi
if [[ ${#remaining_cache_items[@]} -gt 0 ]]; then
cache_items+=("${remaining_cache_items[@]}")
cache_patterns+=("${remaining_cache_patterns[@]}")
menu_options+=("${remaining_menu_options[@]}")
fi
if [[ ${#selected_cache_patterns[@]} -gt 0 ]]; then
local -a preselected_indices=()
for ((i = 0; i < ${#selected_cache_patterns[@]}; i++)); do
preselected_indices+=("$i")
done
local IFS=','
export MOLE_PRESELECTED_INDICES="${preselected_indices[*]}"
else
unset MOLE_PRESELECTED_INDICES
fi
MOLE_SELECTION_RESULT=""
paginated_multi_select "Whitelist Manager Select caches to protect" "${menu_options[@]}"
unset MOLE_PRESELECTED_INDICES
local exit_code=$?
if [[ $exit_code -ne 0 ]]; then
echo ""
echo -e "${YELLOW}Cancelled${NC}"
return 1
fi
# Convert selected indices to patterns
local -a selected_patterns=()
if [[ -n "$MOLE_SELECTION_RESULT" ]]; then
local -a selected_indices
IFS=',' read -ra selected_indices <<< "$MOLE_SELECTION_RESULT"
for idx in "${selected_indices[@]}"; do
if [[ $idx -ge 0 && $idx -lt ${#cache_patterns[@]} ]]; then
local pattern="${cache_patterns[$idx]}"
# Convert back to portable format with ~
pattern="${pattern/#$HOME/~}"
selected_patterns+=("$pattern")
fi
done
fi
# Merge custom patterns with selected patterns
local -a all_patterns=()
if [[ ${#selected_patterns[@]} -gt 0 ]]; then
all_patterns=("${selected_patterns[@]}")
fi
if [[ ${#custom_patterns[@]} -gt 0 ]]; then
for custom_pattern in "${custom_patterns[@]}"; do
all_patterns+=("$custom_pattern")
done
fi
# Save to whitelist config (bash 3.2 + set -u safe)
if [[ ${#all_patterns[@]} -gt 0 ]]; then
save_whitelist_patterns "${all_patterns[@]}"
else
save_whitelist_patterns
fi
local total_protected=$((${#selected_patterns[@]} + ${#custom_patterns[@]}))
local -a summary_lines=()
if [[ ${#custom_patterns[@]} -gt 0 ]]; then
summary_lines+=("Protected ${#selected_patterns[@]} predefined + ${#custom_patterns[@]} custom patterns")
else
summary_lines+=("Protected ${total_protected} cache(s)")
fi
summary_lines+=("Saved to ${WHITELIST_CONFIG}")
print_summary_block "success" "${summary_lines[@]}"
printf '\n'
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
manage_whitelist
fi