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:
187
lib/manage/autofix.sh
Normal file
187
lib/manage/autofix.sh
Normal 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
335
lib/manage/update.sh
Normal 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
339
lib/manage/whitelist.sh
Executable 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
|
||||
Reference in New Issue
Block a user