mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 15:39:42 +00:00
Whitelist mode is more powerful
This commit is contained in:
10
README.md
10
README.md
@@ -57,9 +57,7 @@ mo --help # Show help
|
||||
mo --version # Show installed version
|
||||
```
|
||||
|
||||
> If the command is not found, run `mole update` once to upgrade to the latest version.
|
||||
>
|
||||
> Recommended: Start with `mo clean --dry-run` to preview what will be deleted, use `mo clean --whitelist` to protect important caches
|
||||
> **Recommended:** Start with `mo clean --dry-run` to preview what will be deleted. Use `mo clean --whitelist` to protect important caches that are slow to re-download, like Gradle, npm, Homebrew, PyTorch models, Docker images. Interactive menu with 42 cache types, config saved to `~/.config/mole/whitelist`.
|
||||
|
||||
## Features in Detail
|
||||
|
||||
@@ -142,16 +140,14 @@ Total: 156.8GB
|
||||
|
||||
## FAQ
|
||||
|
||||
- **Is Mole safe?** Mole only cleans caches and logs, it doesn't touch app settings, user documents, or system files. Run `mo clean --dry-run` first to preview what will be removed.
|
||||
- **Is Mole safe?** Mole only cleans caches and logs, it doesn't touch app settings, user documents, or system files. Start with `mo clean --dry-run` to preview what will be removed.
|
||||
|
||||
- **How often should I clean?** Once a month, or when disk space is running low.
|
||||
|
||||
- **Can I protect specific caches?** Yes. Run `mo clean --whitelist` to choose which caches to keep. Common ones like Playwright browsers and HuggingFace models are already protected.
|
||||
- **Can I protect specific caches?** Yes. Run `mo clean --whitelist` to select caches to protect.
|
||||
|
||||
- **Touch ID support?** Mole uses `sudo` for privileges, so you'll get a password prompt unless you've configured Touch ID for sudo.
|
||||
|
||||
- **Enable Touch ID for sudo:**
|
||||
|
||||
```bash
|
||||
sudo nano /etc/pam.d/sudo
|
||||
|
||||
|
||||
75
bin/clean.sh
75
bin/clean.sh
@@ -23,7 +23,7 @@ readonly TEMP_FILE_AGE_DAYS=7 # Age threshold for temp file cleanup
|
||||
readonly ORPHAN_AGE_DAYS=60 # Age threshold for orphaned data
|
||||
readonly SIZE_1GB_KB=1048576 # 1GB in kilobytes
|
||||
readonly SIZE_1MB_KB=1024 # 1MB in kilobytes
|
||||
# Default whitelist patterns to avoid removing critical caches (can be extended by user)
|
||||
# Core whitelist patterns - always protected
|
||||
WHITELIST_PATTERNS=(
|
||||
"$HOME/Library/Caches/ms-playwright*"
|
||||
"$HOME/.cache/huggingface*"
|
||||
@@ -68,6 +68,7 @@ SECTION_ACTIVITY=0
|
||||
LAST_CLEAN_RESULT=0
|
||||
files_cleaned=0
|
||||
total_size_cleaned=0
|
||||
whitelist_skipped_count=0
|
||||
SUDO_KEEPALIVE_PID=""
|
||||
|
||||
note_activity() {
|
||||
@@ -204,20 +205,31 @@ safe_clean() {
|
||||
local removed_any=0
|
||||
local total_size_bytes=0
|
||||
local total_count=0
|
||||
local skipped_count=0
|
||||
|
||||
# Optimized parallel processing for better performance
|
||||
local -a existing_paths=()
|
||||
for path in "${targets[@]}"; do
|
||||
local skip=false
|
||||
if [[ ${#WHITELIST_PATTERNS[@]} -gt 0 ]]; then
|
||||
for w in "${WHITELIST_PATTERNS[@]}"; do
|
||||
if [[ "$path" == $w ]]; then
|
||||
skip=true; break
|
||||
# Match both exact path and glob pattern
|
||||
if [[ "$path" == "$w" ]] || [[ "$path" == $w ]]; then
|
||||
skip=true
|
||||
((skipped_count++))
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
[[ "$skip" == "true" ]] && continue
|
||||
[[ -e "$path" ]] && existing_paths+=("$path")
|
||||
done
|
||||
|
||||
# Update global whitelist skip counter
|
||||
if [[ $skipped_count -gt 0 ]]; then
|
||||
((whitelist_skipped_count += skipped_count))
|
||||
fi
|
||||
|
||||
if [[ ${#existing_paths[@]} -eq 0 ]]; then
|
||||
LAST_CLEAN_RESULT=0
|
||||
return 0
|
||||
@@ -322,14 +334,15 @@ start_cleanup() {
|
||||
clear
|
||||
printf '\n'
|
||||
echo -e "${PURPLE}Clean Your Mac${NC}"
|
||||
|
||||
if [[ "$DRY_RUN" != "true" && -t 0 ]]; then
|
||||
printf '\n'
|
||||
echo ""
|
||||
echo -e "${YELLOW}Tip:${NC} Safety first—run 'mo clean --dry-run'. Important Macs should stop."
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo ""
|
||||
echo -e "${YELLOW}Dry Run mode:${NC} showing what would be removed (no deletions)."
|
||||
echo -e "${YELLOW}Dry Run Mode${NC} - Preview only, no deletions"
|
||||
echo ""
|
||||
SYSTEM_CLEAN=false
|
||||
return
|
||||
@@ -337,7 +350,7 @@ start_cleanup() {
|
||||
|
||||
if [[ -t 0 ]]; then
|
||||
echo ""
|
||||
echo -ne "${BLUE}System cleanup? ${GRAY}Enter to continue, any key to skip${NC} "
|
||||
echo -ne "${ICON_SETTINGS} ${BLUE}System cleanup?${NC} ${GRAY}Enter to continue, any key to skip${NC} "
|
||||
|
||||
# Use IFS= and read without -n to allow Ctrl+C to work properly
|
||||
IFS= read -r -s -n 1 choice
|
||||
@@ -351,10 +364,9 @@ start_cleanup() {
|
||||
|
||||
# Enter or y = yes, do system cleanup
|
||||
if [[ -z "$choice" ]] || [[ "$choice" == $'\n' ]] || [[ "$choice" =~ ^[Yy]$ ]]; then
|
||||
echo ""
|
||||
if request_sudo_access "System cleanup requires admin access"; then
|
||||
SYSTEM_CLEAN=true
|
||||
echo -e "${GREEN}✓ Admin access granted${NC}"
|
||||
echo -e "${GREEN}✓${NC} Admin access granted"
|
||||
# Start sudo keepalive with error handling
|
||||
(
|
||||
local retry_count=0
|
||||
@@ -376,18 +388,17 @@ start_cleanup() {
|
||||
else
|
||||
SYSTEM_CLEAN=false
|
||||
echo ""
|
||||
echo -e "${YELLOW}Authentication failed, continuing with user-level cleanup${NC}"
|
||||
echo -e "${YELLOW}Authentication failed${NC}, continuing with user-level cleanup"
|
||||
fi
|
||||
else
|
||||
# Any other key = no system cleanup
|
||||
SYSTEM_CLEAN=false
|
||||
echo ""
|
||||
echo -e "Skipped system cleanup, user-level only"
|
||||
fi
|
||||
else
|
||||
SYSTEM_CLEAN=false
|
||||
echo ""
|
||||
echo -e " Running in non-interactive mode"
|
||||
echo "Running in non-interactive mode"
|
||||
echo " • System-level cleanup skipped (requires interaction)"
|
||||
echo " • User-level cleanup will proceed automatically"
|
||||
echo ""
|
||||
@@ -396,7 +407,16 @@ start_cleanup() {
|
||||
|
||||
perform_cleanup() {
|
||||
echo ""
|
||||
echo "$(detect_architecture) | Free space: $(get_free_space)"
|
||||
echo "${ICON_SYSTEM} $(detect_architecture) | Free space: $(get_free_space)"
|
||||
|
||||
# Show whitelist info if patterns are active
|
||||
local active_count=${#WHITELIST_PATTERNS[@]}
|
||||
if [[ $active_count -gt 2 ]]; then
|
||||
local custom_count=$((active_count - 2))
|
||||
echo -e "${BLUE}✓${NC} Whitelist: $custom_count custom + 2 core patterns active"
|
||||
elif [[ $active_count -eq 2 ]]; then
|
||||
echo -e "${BLUE}✓${NC} Whitelist: 2 core patterns active"
|
||||
fi
|
||||
|
||||
# Get initial space
|
||||
space_before=$(df / | tail -1 | awk '{print $4}')
|
||||
@@ -406,9 +426,9 @@ perform_cleanup() {
|
||||
files_cleaned=0
|
||||
total_size_cleaned=0
|
||||
|
||||
# ===== 1. System cleanup (if admin) - Do this first while sudo is fresh =====
|
||||
# ===== 1. Deep system cleanup (if admin) - Do this first while sudo is fresh =====
|
||||
if [[ "$SYSTEM_CLEAN" == "true" ]]; then
|
||||
start_section "System-level cleanup"
|
||||
start_section "Deep system-level cleanup"
|
||||
|
||||
# Clean system caches more safely
|
||||
sudo find /Library/Caches -name "*.cache" -delete 2>/dev/null || true
|
||||
@@ -1273,8 +1293,7 @@ perform_cleanup() {
|
||||
# Summary
|
||||
if [[ $orphaned_count -gt 0 ]]; then
|
||||
local orphaned_mb=$(echo "$total_orphaned_kb" | awk '{printf "%.1f", $1/1024}')
|
||||
echo ""
|
||||
echo " ${GREEN}☺︎${NC} ${GREEN}Cleaned $orphaned_count orphaned items (~${orphaned_mb}MB)${NC}"
|
||||
echo " ${BLUE}●${NC} Cleaned $orphaned_count orphaned items (~${orphaned_mb}MB)"
|
||||
note_activity
|
||||
else
|
||||
echo " ${BLUE}○${NC} No old orphaned app data found"
|
||||
@@ -1328,11 +1347,17 @@ perform_cleanup() {
|
||||
|
||||
# Show file/category stats for dry run
|
||||
if [[ $files_cleaned -gt 0 && $total_items -gt 0 ]]; then
|
||||
printf "Files to clean: %s | Categories: %s\n" "$files_cleaned" "$total_items"
|
||||
printf "Files to clean: %s | Categories: %s" "$files_cleaned" "$total_items"
|
||||
[[ $whitelist_skipped_count -gt 0 ]] && printf " | Protected: %s" "$whitelist_skipped_count"
|
||||
printf "\n"
|
||||
elif [[ $files_cleaned -gt 0 ]]; then
|
||||
printf "Files to clean: %s\n" "$files_cleaned"
|
||||
printf "Files to clean: %s" "$files_cleaned"
|
||||
[[ $whitelist_skipped_count -gt 0 ]] && printf " | Protected: %s" "$whitelist_skipped_count"
|
||||
printf "\n"
|
||||
elif [[ $total_items -gt 0 ]]; then
|
||||
printf "Categories: %s\n" "$total_items"
|
||||
printf "Categories: %s" "$total_items"
|
||||
[[ $whitelist_skipped_count -gt 0 ]] && printf " | Protected: %s" "$whitelist_skipped_count"
|
||||
printf "\n"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
@@ -1342,11 +1367,17 @@ perform_cleanup() {
|
||||
|
||||
# Show file/category stats for actual cleanup
|
||||
if [[ $files_cleaned -gt 0 && $total_items -gt 0 ]]; then
|
||||
printf "Files cleaned: %s | Categories: %s\n" "$files_cleaned" "$total_items"
|
||||
printf "Files cleaned: %s | Categories: %s" "$files_cleaned" "$total_items"
|
||||
[[ $whitelist_skipped_count -gt 0 ]] && printf " | Protected: %s" "$whitelist_skipped_count"
|
||||
printf "\n"
|
||||
elif [[ $files_cleaned -gt 0 ]]; then
|
||||
printf "Files cleaned: %s\n" "$files_cleaned"
|
||||
printf "Files cleaned: %s" "$files_cleaned"
|
||||
[[ $whitelist_skipped_count -gt 0 ]] && printf " | Protected: %s" "$whitelist_skipped_count"
|
||||
printf "\n"
|
||||
elif [[ $total_items -gt 0 ]]; then
|
||||
printf "Categories: %s\n" "$total_items"
|
||||
printf "Categories: %s" "$total_items"
|
||||
[[ $whitelist_skipped_count -gt 0 ]] && printf " | Protected: %s" "$whitelist_skipped_count"
|
||||
printf "\n"
|
||||
fi
|
||||
|
||||
if [[ $(echo "$freed_gb" | awk '{print ($1 >= 1) ? 1 : 0}') -eq 1 ]]; then
|
||||
|
||||
@@ -28,6 +28,8 @@ readonly ICON_ERROR="✗" # Error
|
||||
readonly ICON_EMPTY="○" # Empty state
|
||||
readonly ICON_LIST="-" # List item
|
||||
readonly ICON_MENU="▸" # Menu item
|
||||
readonly ICON_SYSTEM="☯︎" # System/Architecture info
|
||||
readonly ICON_SETTINGS="⚙" # Settings/Configuration
|
||||
|
||||
# Spinner character helpers (ASCII by default, overridable via env)
|
||||
mo_spinner_chars() {
|
||||
|
||||
@@ -75,6 +75,8 @@ paginated_multi_select() {
|
||||
# Setup terminal - preserve interrupt character
|
||||
stty -echo -icanon intr ^C 2>/dev/null || true
|
||||
enter_alt_screen
|
||||
# Clear screen once on entry to alt screen
|
||||
printf "\033[2J\033[H" >&2
|
||||
hide_cursor
|
||||
|
||||
# Helper functions
|
||||
@@ -94,7 +96,11 @@ paginated_multi_select() {
|
||||
|
||||
# Draw the complete menu
|
||||
draw_menu() {
|
||||
printf "\033[H\033[J" >&2 # Clear screen and move to top
|
||||
# Move to home position without clearing (reduces flicker)
|
||||
printf "\033[H" >&2
|
||||
|
||||
# Clear each line as we go instead of clearing entire screen
|
||||
local clear_line="\r\033[2K"
|
||||
|
||||
# Header - compute underline length without external seq dependency
|
||||
local title_clean="${title//[^[:print:]]/}"
|
||||
@@ -103,14 +109,15 @@ paginated_multi_select() {
|
||||
# Build underline robustly (no seq); printf width then translate spaces to '='
|
||||
local underline
|
||||
underline=$(printf '%*s' "$underline_len" '' | tr ' ' '=')
|
||||
printf "${PURPLE}%s${NC}\n%s\n" "$title" "$underline" >&2
|
||||
printf "${clear_line}${PURPLE}%s${NC}\n" "$title" >&2
|
||||
printf "${clear_line}%s\n" "$underline" >&2
|
||||
|
||||
# Status
|
||||
local selected_count=0
|
||||
for ((i = 0; i < total_items; i++)); do
|
||||
[[ ${selected[i]} == true ]] && ((selected_count++))
|
||||
done
|
||||
printf "Page %d/%d │ Total: %d │ Selected: %d\n\n" \
|
||||
printf "${clear_line}Page %d/%d │ Total: %d │ Selected: %d\n\n" \
|
||||
$((current_page + 1)) $total_pages $total_items $selected_count >&2
|
||||
|
||||
# Items for current page
|
||||
@@ -124,14 +131,18 @@ paginated_multi_select() {
|
||||
render_item $i $is_current
|
||||
done
|
||||
|
||||
# Fill empty slots
|
||||
# Fill empty slots to clear previous content
|
||||
local items_shown=$((end_idx - start_idx + 1))
|
||||
for ((i = items_shown; i < items_per_page; i++)); do
|
||||
print_line ""
|
||||
printf "${clear_line}\n" >&2
|
||||
done
|
||||
|
||||
print_line ""
|
||||
print_line "${GRAY}↑/↓${NC} Navigate ${GRAY}|${NC} ${GRAY}Space${NC} Select ${GRAY}|${NC} ${GRAY}Enter${NC} Confirm ${GRAY}|${NC} ${GRAY}Q/ESC${NC} Quit"
|
||||
# Clear any remaining lines at bottom
|
||||
printf "${clear_line}\n" >&2
|
||||
printf "${clear_line}${GRAY}↑/↓${NC} Navigate ${GRAY}|${NC} ${GRAY}Space${NC} Select ${GRAY}|${NC} ${GRAY}Enter${NC} Confirm ${GRAY}|${NC} ${GRAY}Q/ESC${NC} Quit\n" >&2
|
||||
|
||||
# Clear one more line to ensure no artifacts
|
||||
printf "${clear_line}" >&2
|
||||
}
|
||||
|
||||
# Show help screen
|
||||
|
||||
@@ -12,11 +12,113 @@ source "$SCRIPT_DIR/paginated_menu.sh"
|
||||
# Config file path
|
||||
WHITELIST_CONFIG="$HOME/.config/mole/whitelist"
|
||||
|
||||
# Core whitelist patterns that are always protected
|
||||
declare -a DEFAULT_WHITELIST_PATTERNS=(
|
||||
"$HOME/Library/Caches/ms-playwright*"
|
||||
"$HOME/.cache/huggingface*"
|
||||
)
|
||||
|
||||
# Determine if a pattern matches one of the defaults
|
||||
is_default_pattern() {
|
||||
local candidate="$1"
|
||||
for default_pat in "${DEFAULT_WHITELIST_PATTERNS[@]}"; do
|
||||
if patterns_equivalent "$candidate" "$default_pat"; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Save whitelist patterns to config
|
||||
save_whitelist_patterns() {
|
||||
local -a patterns
|
||||
patterns=("$@")
|
||||
local -a custom_patterns
|
||||
custom_patterns=()
|
||||
mkdir -p "$(dirname "$WHITELIST_CONFIG")"
|
||||
|
||||
cat > "$WHITELIST_CONFIG" << 'EOF'
|
||||
# Mole Whitelist - Protected paths won't be deleted
|
||||
# Default protections: Playwright browsers, HuggingFace models
|
||||
# You can add custom paths here
|
||||
EOF
|
||||
|
||||
if [[ ${#patterns[@]} -gt 0 ]]; then
|
||||
for pattern in "${patterns[@]}"; do
|
||||
if is_default_pattern "$pattern"; then
|
||||
continue
|
||||
fi
|
||||
local duplicate="false"
|
||||
if [[ ${#custom_patterns[@]} -gt 0 ]]; then
|
||||
for existing in "${custom_patterns[@]}"; do
|
||||
if patterns_equivalent "$pattern" "$existing"; then
|
||||
duplicate="true"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
[[ "$duplicate" == "true" ]] && continue
|
||||
custom_patterns+=("$pattern")
|
||||
done
|
||||
|
||||
if [[ ${#custom_patterns[@]} -gt 0 ]]; then
|
||||
printf '\n' >> "$WHITELIST_CONFIG"
|
||||
for pattern in "${custom_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'
|
||||
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
|
||||
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
|
||||
Rust Cargo registry cache|$HOME/.cargo/registry/cache/*|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
|
||||
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
|
||||
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
|
||||
Go module cache|$HOME/go/pkg/mod/cache/*|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
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
patterns_equivalent() {
|
||||
local first="${1/#~/$HOME}"
|
||||
local second="${2/#~/$HOME}"
|
||||
@@ -26,136 +128,11 @@ patterns_equivalent() {
|
||||
return 1
|
||||
}
|
||||
|
||||
is_default_pattern() {
|
||||
local candidate="$1"
|
||||
local default_pat
|
||||
for default_pat in "${DEFAULT_WHITELIST_PATTERNS[@]}"; do
|
||||
if patterns_equivalent "$candidate" "$default_pat"; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Run dry-run cleanup and collect what would be deleted
|
||||
collect_files_to_be_cleaned() {
|
||||
local clean_sh="$SCRIPT_DIR/../bin/clean.sh"
|
||||
local -a items=()
|
||||
|
||||
if [[ -t 1 ]]; then
|
||||
start_inline_spinner "Scanning cache files..."
|
||||
else
|
||||
echo "Scanning cache files..."
|
||||
fi
|
||||
|
||||
# Run clean.sh in dry-run mode
|
||||
local temp_output=$(create_temp_file)
|
||||
echo "" | bash "$clean_sh" --dry-run 2>&1 > "$temp_output" || true
|
||||
|
||||
if [[ -t 1 ]]; then
|
||||
stop_inline_spinner
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Strip ANSI color codes for parsing
|
||||
local temp_plain=$(create_temp_file)
|
||||
sed $'s/\033\[[0-9;]*m//g' "$temp_output" > "$temp_plain"
|
||||
|
||||
# Parse output: " → Description (size, dry)"
|
||||
local pattern='^[[:space:]]*→[[:space:]]+([^(]+)(\([^)]+\))?[[:space:]]*\(([^,)]+),.*dry\)$'
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ $pattern ]]; then
|
||||
local description="${BASH_REMATCH[1]}"
|
||||
local size="${BASH_REMATCH[3]}"
|
||||
|
||||
description="${description#${description%%[![:space:]]*}}"
|
||||
description="${description%${description##*[![:space:]]}}"
|
||||
|
||||
[[ "$description" =~ ^Orphaned ]] && continue
|
||||
|
||||
# Find corresponding path from clean.sh
|
||||
local path=""
|
||||
while IFS= read -r src_line; do
|
||||
# Match: safe_clean <path> "<description>"
|
||||
# Path may contain escaped spaces (\ )
|
||||
if [[ "$src_line" =~ safe_clean[[:space:]]+(.+)[[:space:]]+\"$description\" ]]; then
|
||||
path="${BASH_REMATCH[1]}"
|
||||
break
|
||||
fi
|
||||
done < "$clean_sh"
|
||||
|
||||
path="${path/#\~/$HOME}"
|
||||
[[ -z "$path" || "$path" =~ \$ ]] && continue
|
||||
|
||||
items+=("$path|$description|$size")
|
||||
fi
|
||||
done < "$temp_plain"
|
||||
|
||||
# Temp files will be auto-cleaned by cleanup_temp_files
|
||||
|
||||
# Return early if no items found
|
||||
if [[ ${#items[@]} -eq 0 ]]; then
|
||||
AVAILABLE_CACHE_ITEMS=()
|
||||
return
|
||||
fi
|
||||
|
||||
# Remove duplicates
|
||||
local -a unique_items=()
|
||||
local -a seen_descriptions=()
|
||||
|
||||
for item in "${items[@]}"; do
|
||||
IFS='|' read -r path desc size <<< "$item"
|
||||
local is_duplicate=false
|
||||
if [[ ${#seen_descriptions[@]} -gt 0 ]]; then
|
||||
for seen in "${seen_descriptions[@]}"; do
|
||||
[[ "$desc" == "$seen" ]] && is_duplicate=true && break
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ "$is_duplicate" == "false" ]]; then
|
||||
unique_items+=("$item")
|
||||
seen_descriptions+=("$desc")
|
||||
fi
|
||||
done
|
||||
|
||||
# Sort by size (largest first)
|
||||
local -a sorted_items=()
|
||||
if [[ ${#unique_items[@]} -gt 0 ]]; then
|
||||
while IFS= read -r item; do
|
||||
sorted_items+=("$item")
|
||||
done < <(
|
||||
for item in "${unique_items[@]}"; do
|
||||
IFS='|' read -r path desc size <<< "$item"
|
||||
local size_kb=0
|
||||
if [[ "$size" =~ ([0-9.]+)GB ]]; then
|
||||
size_kb=$(echo "${BASH_REMATCH[1]}" | awk '{printf "%d", $1 * 1024 * 1024}')
|
||||
elif [[ "$size" =~ ([0-9.]+)MB ]]; then
|
||||
size_kb=$(echo "${BASH_REMATCH[1]}" | awk '{printf "%d", $1 * 1024}')
|
||||
elif [[ "$size" =~ ([0-9.]+)KB ]]; then
|
||||
size_kb=$(echo "${BASH_REMATCH[1]}" | awk '{printf "%d", $1}')
|
||||
fi
|
||||
printf "%010d|%s\n" "$size_kb" "$item"
|
||||
done | sort -rn | cut -d'|' -f2-
|
||||
)
|
||||
fi
|
||||
|
||||
# Safe assignment for empty array
|
||||
if [[ ${#sorted_items[@]} -gt 0 ]]; then
|
||||
AVAILABLE_CACHE_ITEMS=("${sorted_items[@]}")
|
||||
else
|
||||
AVAILABLE_CACHE_ITEMS=()
|
||||
fi
|
||||
}
|
||||
|
||||
declare -a AVAILABLE_CACHE_ITEMS=()
|
||||
|
||||
load_whitelist() {
|
||||
local -a patterns=()
|
||||
local -a patterns=("${DEFAULT_WHITELIST_PATTERNS[@]}")
|
||||
|
||||
# Always include default patterns
|
||||
patterns=("${DEFAULT_WHITELIST_PATTERNS[@]}")
|
||||
|
||||
# Add user-defined patterns from config file
|
||||
# Load user-defined patterns from config file
|
||||
if [[ -f "$WHITELIST_CONFIG" ]]; then
|
||||
while IFS= read -r line; do
|
||||
line="${line#${line%%[![:space:]]*}}"
|
||||
@@ -206,135 +183,44 @@ is_whitelisted() {
|
||||
return 1
|
||||
}
|
||||
|
||||
format_whitelist_item() {
|
||||
local description="$1" size="$2" is_protected="$3"
|
||||
local desc_display="$description"
|
||||
[[ ${#description} -gt 40 ]] && desc_display="${description:0:37}..."
|
||||
local size_display=$(printf "%-15s" "$size")
|
||||
local status=""
|
||||
[[ "$is_protected" == "true" ]] && status=" ${GREEN}[Protected]${NC}"
|
||||
printf "%-40s %s%s" "$desc_display" "$size_display" "$status"
|
||||
}
|
||||
|
||||
# Get friendly description for a path pattern
|
||||
get_description_for_pattern() {
|
||||
local pattern="$1"
|
||||
local desc=""
|
||||
|
||||
# Hardcoded descriptions for common patterns
|
||||
case "$pattern" in
|
||||
*"ms-playwright"*)
|
||||
echo "Playwright Browser"
|
||||
return
|
||||
;;
|
||||
*"huggingface"*)
|
||||
echo "HuggingFace Model"
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
# Try to match with safe_clean in clean.sh
|
||||
# Use fuzzy matching by removing trailing /* or *
|
||||
local pattern_base="${pattern%/\*}"
|
||||
pattern_base="${pattern_base%\*}"
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" =~ safe_clean[[:space:]]+(.+)[[:space:]]+\"([^\"]+)\" ]]; then
|
||||
local clean_path="${BASH_REMATCH[1]}"
|
||||
local clean_desc="${BASH_REMATCH[2]}"
|
||||
clean_path="${clean_path/#\~/$HOME}"
|
||||
|
||||
# Remove trailing /* or * for comparison
|
||||
local clean_base="${clean_path%/\*}"
|
||||
clean_base="${clean_base%\*}"
|
||||
|
||||
# Check if base paths match
|
||||
if [[ "$pattern_base" == "$clean_base" || "$clean_path" == "$pattern" || "$pattern" == "$clean_path" ]]; then
|
||||
echo "$clean_desc"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
done < "$SCRIPT_DIR/../bin/clean.sh"
|
||||
|
||||
# If no match found, return short path
|
||||
echo "${pattern/#$HOME/~}"
|
||||
}
|
||||
|
||||
manage_whitelist() {
|
||||
manage_whitelist_categories
|
||||
}
|
||||
|
||||
manage_whitelist_categories() {
|
||||
clear
|
||||
echo ""
|
||||
echo -e "${PURPLE}Whitelist Manager${NC}"
|
||||
echo ""
|
||||
echo -e "${GRAY}Select caches to protect from cleanup.${NC}"
|
||||
echo ""
|
||||
|
||||
# Load user-defined whitelist
|
||||
CURRENT_WHITELIST_PATTERNS=()
|
||||
# Load currently enabled patterns from both sources
|
||||
load_whitelist
|
||||
|
||||
echo "Select the cache files that need to be protected"
|
||||
echo -e "${GRAY}Protected items are pre-selected. You can also edit ${WHITELIST_CONFIG} directly.${NC}"
|
||||
echo ""
|
||||
|
||||
collect_files_to_be_cleaned
|
||||
|
||||
# Add items from config that are not in the scan results
|
||||
local -a all_items=()
|
||||
if [[ ${#AVAILABLE_CACHE_ITEMS[@]} -gt 0 ]]; then
|
||||
all_items=("${AVAILABLE_CACHE_ITEMS[@]}")
|
||||
fi
|
||||
|
||||
# Add saved patterns that are not in scan results
|
||||
if [[ ${#CURRENT_WHITELIST_PATTERNS[@]} -gt 0 ]]; then
|
||||
for pattern in "${CURRENT_WHITELIST_PATTERNS[@]}"; do
|
||||
local pattern_expanded="${pattern/#\~/$HOME}"
|
||||
local found="false"
|
||||
|
||||
if [[ ${#all_items[@]} -gt 0 ]]; then
|
||||
for item in "${all_items[@]}"; do
|
||||
IFS='|' read -r path _ _ <<< "$item"
|
||||
if patterns_equivalent "$path" "$pattern_expanded"; then
|
||||
found="true"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ "$found" == "false" ]]; then
|
||||
local desc=$(get_description_for_pattern "$pattern_expanded")
|
||||
all_items+=("$pattern_expanded|$desc|0B")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ ${#all_items[@]} -eq 0 ]]; then
|
||||
echo -e "${GREEN}✓${NC} No cache files found - system is clean!"
|
||||
echo ""
|
||||
echo "Press any key to exit..."
|
||||
read -n 1 -s
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Update global array with all items
|
||||
AVAILABLE_CACHE_ITEMS=("${all_items[@]}")
|
||||
|
||||
echo -e "${GREEN}✓${NC} Found ${#AVAILABLE_CACHE_ITEMS[@]} items"
|
||||
echo ""
|
||||
|
||||
# Build cache items list
|
||||
local -a cache_items=()
|
||||
local -a cache_patterns=()
|
||||
local -a menu_options=()
|
||||
local -a preselected_indices=()
|
||||
local index=0
|
||||
|
||||
for item in "${AVAILABLE_CACHE_ITEMS[@]}"; do
|
||||
IFS='|' read -r path description size <<< "$item"
|
||||
local is_protected="false"
|
||||
if is_whitelisted "$path"; then
|
||||
is_protected="true"
|
||||
while IFS='|' read -r display_name pattern category; do
|
||||
# Expand $HOME in pattern
|
||||
pattern="${pattern/\$HOME/$HOME}"
|
||||
|
||||
cache_items+=("$display_name")
|
||||
cache_patterns+=("$pattern")
|
||||
menu_options+=("$display_name")
|
||||
|
||||
# Check if this pattern is currently whitelisted
|
||||
if is_whitelisted "$pattern"; then
|
||||
preselected_indices+=("$index")
|
||||
fi
|
||||
menu_options+=("$(format_whitelist_item "$description" "$size" "$is_protected")")
|
||||
((index++))
|
||||
done
|
||||
|
||||
echo -e "${GRAY}↑↓ Navigate | Space Toggle | Enter Save | Q Quit${NC}"
|
||||
((index++))
|
||||
done < <(get_all_cache_items)
|
||||
|
||||
if [[ ${#preselected_indices[@]} -gt 0 ]]; then
|
||||
local IFS=','
|
||||
@@ -344,106 +230,40 @@ manage_whitelist() {
|
||||
fi
|
||||
|
||||
MOLE_SELECTION_RESULT=""
|
||||
paginated_multi_select "Select items to protect" "${menu_options[@]}"
|
||||
paginated_multi_select "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} - No changes made"
|
||||
echo -e "${YELLOW}Cancelled${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local -a selected_indices=()
|
||||
if [[ -n "$MOLE_SELECTION_RESULT" ]]; then
|
||||
IFS=',' read -ra selected_indices <<< "$MOLE_SELECTION_RESULT"
|
||||
fi
|
||||
|
||||
save_whitelist "${selected_indices[@]}"
|
||||
}
|
||||
|
||||
save_whitelist() {
|
||||
local -a selected_indices=("$@")
|
||||
mkdir -p "$(dirname "$WHITELIST_CONFIG")"
|
||||
|
||||
# Convert selected indices to patterns
|
||||
local -a selected_patterns=()
|
||||
local selected_default_count=0
|
||||
local selected_custom_count=0
|
||||
|
||||
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 ${#AVAILABLE_CACHE_ITEMS[@]} ]]; then
|
||||
local item="${AVAILABLE_CACHE_ITEMS[$idx]}"
|
||||
IFS='|' read -r path description size <<< "$item"
|
||||
local portable_path="${path/#$HOME/~}"
|
||||
|
||||
local duplicate="false"
|
||||
if [[ ${#selected_patterns[@]} -gt 0 ]]; then
|
||||
for existing in "${selected_patterns[@]}"; do
|
||||
if patterns_equivalent "$portable_path" "$existing"; then
|
||||
duplicate="true"
|
||||
break
|
||||
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
|
||||
[[ "$duplicate" == "true" ]] && continue
|
||||
|
||||
if is_default_pattern "$portable_path"; then
|
||||
((selected_default_count++))
|
||||
else
|
||||
((selected_custom_count++))
|
||||
fi
|
||||
|
||||
selected_patterns+=("$portable_path")
|
||||
fi
|
||||
done
|
||||
|
||||
cat > "$WHITELIST_CONFIG" << 'EOF'
|
||||
# Mole Whitelist - Protected paths won't be deleted
|
||||
# Default: Playwright browsers, HuggingFace models
|
||||
EOF
|
||||
|
||||
# Only save custom (non-default) patterns
|
||||
local -a custom_patterns=()
|
||||
for pattern in "${selected_patterns[@]}"; do
|
||||
if ! is_default_pattern "$pattern"; then
|
||||
custom_patterns+=("$pattern")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#custom_patterns[@]} -gt 0 ]]; then
|
||||
printf '\n' >> "$WHITELIST_CONFIG"
|
||||
for pattern in "${custom_patterns[@]}"; do
|
||||
echo "$pattern" >> "$WHITELIST_CONFIG"
|
||||
done
|
||||
fi
|
||||
|
||||
local total_count=${#selected_patterns[@]}
|
||||
local -a summary_parts=()
|
||||
if [[ $selected_default_count -gt 0 ]]; then
|
||||
local default_label="default"
|
||||
[[ $selected_default_count -ne 1 ]] && default_label+="s"
|
||||
summary_parts+=("$selected_default_count $default_label")
|
||||
fi
|
||||
if [[ $selected_custom_count -gt 0 ]]; then
|
||||
local custom_label="custom"
|
||||
[[ $selected_custom_count -ne 1 ]] && custom_label+="s"
|
||||
summary_parts+=("$selected_custom_count $custom_label")
|
||||
fi
|
||||
|
||||
local summary=""
|
||||
if [[ ${#summary_parts[@]} -gt 0 ]]; then
|
||||
summary=" (${summary_parts[0]}"
|
||||
for ((i = 1; i < ${#summary_parts[@]}; i++)); do
|
||||
summary+=", ${summary_parts[$i]}"
|
||||
done
|
||||
summary+=")"
|
||||
fi
|
||||
# Save to whitelist config
|
||||
save_whitelist_patterns "${selected_patterns[@]}"
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}✓${NC} Protected $total_count items${summary}"
|
||||
echo -e "${GREEN}✓${NC} Protected ${#selected_patterns[@]} cache(s)"
|
||||
echo -e "${GRAY}Config: ${WHITELIST_CONFIG}${NC}"
|
||||
}
|
||||
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
manage_whitelist
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user