Last active
March 3, 2026 14:52
-
-
Save coccoinomane/05366181517fee2b48a1913f9c36ce70 to your computer and use it in GitHub Desktop.
Interactive disk cleanup script for macOS/Linux — caches, Docker, node_modules, Python venvs, nvm versions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ============================================================================= | |
| # disk-cleanup.sh | |
| # Interactive disk cleanup for macOS and Linux. | |
| # Removes caches, Docker waste, old Node versions, stale node_modules, | |
| # and Python virtual environments. Safe to run periodically. | |
| # | |
| # Usage: | |
| # ./disk-cleanup.sh # interactive mode (approve each removal) | |
| # ./disk-cleanup.sh --force # no confirmations, remove everything | |
| # ./disk-cleanup.sh -t 500 # only prompt for items >= 500 MB | |
| # ./disk-cleanup.sh -t 0 # show everything regardless of size | |
| # ./disk-cleanup.sh --force -t 200 # auto-remove everything >= 200 MB | |
| # | |
| # Options: | |
| # -f, --force Skip all confirmations and remove everything | |
| # -t, --threshold MB Minimum size in MB to prompt for removal (default: 100) | |
| # -h, --help Show help message | |
| # | |
| # What it cleans: | |
| # 1. npm cache (~/.npm/_cacache) | |
| # 2. Docker: unused images, volumes, stopped containers, build cache | |
| # 3. ~/.cache: whisper, huggingface, puppeteer, prisma, and more | |
| # 4. ~/Library/Caches (macOS only): Chrome, Spotify, Brave, Cypress, etc. | |
| # 5. Homebrew downloads (brew cleanup --prune=all) | |
| # 6. Old Node.js versions installed via nvm (keeps the currently active one) | |
| # 7. Stale node_modules not accessed in 30+ days | |
| # 8. Stale Python venvs (.venv/.env with pyvenv.cfg, not accessed in 30+ days) | |
| # 9. uv cache (Python package manager) | |
| # 10. pre-commit cache | |
| # | |
| # Exceptions ("never delete"): | |
| # When prompted for removal, you can type "never" instead of y/N to | |
| # permanently exclude that item from future cleanups. The exception is | |
| # saved to: | |
| # | |
| # ~/.config/disk-cleanup/exceptions.txt | |
| # | |
| # Each line in the file is a path or identifier that will be silently | |
| # skipped on every subsequent run. To undo an exception, simply remove | |
| # the corresponding line from the file. | |
| # | |
| # Examples of what gets saved when you type "never": | |
| # /Users/you/.cache/huggingface # never clean huggingface models | |
| # /Users/you/myproject/node_modules # never clean this specific node_modules | |
| # /Users/you/.npm/_cacache # never clean npm cache | |
| # docker # never prune Docker | |
| # brew # never run brew cleanup | |
| # | |
| # ============================================================================= | |
| set -uo pipefail | |
| # ── Constants ──────────────────────────────────────────────────────────────── | |
| HOME_DIR="$HOME" | |
| NODE_MODULES_SEARCH_DEPTH=5 | |
| NODE_MODULES_MIN_AGE_DAYS=30 # only remove node_modules not accessed in this many days | |
| NVM_DIR="${NVM_DIR:-$HOME_DIR/.nvm}" | |
| EXCLUDE_PATHS=("*/Library/*" "*/CloudStorage/*" "*/.npm/*" "*/.nvm/*" "*/.pnpm/*" "*/.vscode/*" "*/.cursor/*") | |
| BYTES_PER_KB=1024 | |
| BYTES_PER_MB=1048576 | |
| BYTES_PER_GB=1073741824 | |
| DEFAULT_THRESHOLD_MB=100 | |
| THRESHOLD_BYTES=$((DEFAULT_THRESHOLD_MB * BYTES_PER_MB)) | |
| EXCEPTIONS_FILE="$HOME_DIR/.config/disk-cleanup/exceptions.txt" | |
| # Cache dirs inside ~/.cache that are safe to wipe | |
| # Note: "uv" and "pre-commit" are handled separately via their CLI commands | |
| CACHE_DIRS_TO_CLEAN=( | |
| "whisper" | |
| "huggingface" | |
| "puppeteer" | |
| "act" | |
| "openai-python" | |
| "prisma" | |
| "winetricks" | |
| "actcache" | |
| ) | |
| # Library/Caches entries safe to wipe | |
| # Note: "Homebrew" is handled separately via brew cleanup | |
| LIBRARY_CACHE_DIRS_TO_CLEAN=( | |
| "Google" | |
| "com.spotify.client" | |
| "Cypress" | |
| "ms-playwright" | |
| "BraveSoftware" | |
| "Mozilla" | |
| ) | |
| # ── Parse flags ────────────────────────────────────────────────────────────── | |
| FORCE=false | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --force|-f) FORCE=true ;; | |
| --threshold|-t) | |
| shift | |
| if [[ -z "${1:-}" || ! "$1" =~ ^[0-9]+$ ]]; then | |
| echo "Error: --threshold requires a numeric value in MB" | |
| exit 1 | |
| fi | |
| THRESHOLD_BYTES=$(( $1 * BYTES_PER_MB )) | |
| ;; | |
| --help|-h) | |
| echo "Usage: $0 [--force] [--threshold MB]" | |
| echo " --force, -f Skip confirmations, remove everything" | |
| echo " --threshold, -t Min size in MB to prompt for removal (default: ${DEFAULT_THRESHOLD_MB})" | |
| exit 0 | |
| ;; | |
| *) | |
| echo "Unknown option: $1" | |
| echo "Usage: $0 [--force] [--threshold MB]" | |
| exit 1 | |
| ;; | |
| esac | |
| shift | |
| done | |
| # ── Helpers ────────────────────────────────────────────────────────────────── | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| CYAN='\033[0;36m' | |
| BOLD='\033[1m' | |
| NC='\033[0m' | |
| total_freed=0 | |
| total_skipped=0 | |
| human_size() { | |
| local bytes=$1 | |
| if (( bytes >= BYTES_PER_GB )); then | |
| printf "%.1f GB" "$(echo "scale=1; $bytes / $BYTES_PER_GB" | bc)" | |
| elif (( bytes >= BYTES_PER_MB )); then | |
| printf "%.0f MB" "$(echo "scale=0; $bytes / $BYTES_PER_MB" | bc)" | |
| else | |
| printf "%.0f KB" "$(echo "scale=0; $bytes / $BYTES_PER_KB" | bc)" | |
| fi | |
| } | |
| dir_size_bytes() { | |
| du -sk "$1" 2>/dev/null | awk '{print $1 * 1024}' || echo 0 | |
| } | |
| # Check if a path is in the exceptions list. | |
| is_exception() { | |
| local path="$1" | |
| [[ -f "$EXCEPTIONS_FILE" ]] || return 1 | |
| grep -qxF "$path" "$EXCEPTIONS_FILE" 2>/dev/null | |
| } | |
| # Add a path to the exceptions list. | |
| add_exception() { | |
| local path="$1" | |
| mkdir -p "$(dirname "$EXCEPTIONS_FILE")" | |
| echo "$path" >> "$EXCEPTIONS_FILE" | |
| echo -e " ${CYAN}⊘${NC} Added to exceptions (${EXCEPTIONS_FILE})" | |
| } | |
| # Prompt the user for confirmation. Returns 0 (yes) or 1 (no). | |
| # In --force mode, always returns 0. | |
| # Accepts an optional second arg: the path to allow "never" exceptions. | |
| confirm() { | |
| local prompt="$1" | |
| local path="${2:-}" | |
| if $FORCE; then | |
| return 0 | |
| fi | |
| if [[ -n "$path" ]]; then | |
| echo -ne " ${YELLOW}?${NC} ${prompt} [y/N/never] " | |
| else | |
| echo -ne " ${YELLOW}?${NC} ${prompt} [y/N] " | |
| fi | |
| read -r answer </dev/tty | |
| case "$answer" in | |
| [yY]|[yY][eE][sS]) return 0 ;; | |
| [nN][eE][vV][eE][rR]) | |
| if [[ -n "$path" ]]; then | |
| add_exception "$path" | |
| fi | |
| return 1 | |
| ;; | |
| *) return 1 ;; | |
| esac | |
| } | |
| # Tracks whether the last remove_and_tally[_bulk] call showed a prompt. | |
| ITEM_SHOWN=0 | |
| # Remove a path after confirmation; tally freed space. | |
| # Shows the folder path and size before asking. | |
| remove_and_tally() { | |
| local path="$1" | |
| local label="$2" | |
| ITEM_SHOWN=0 | |
| if [[ -e "$path" ]]; then | |
| if is_exception "$path"; then | |
| return | |
| fi | |
| local size | |
| size=$(dir_size_bytes "$path") | |
| if (( size < THRESHOLD_BYTES )); then | |
| return | |
| fi | |
| ITEM_SHOWN=1 | |
| local hsize | |
| hsize=$(human_size "$size") | |
| echo -e " ${BOLD}${label}${NC} — ${hsize}" | |
| if confirm "Remove?" "$path"; then | |
| command -v chflags &>/dev/null && chflags -R nouchg "$path" 2>/dev/null | |
| chmod -R u+rwX "$path" 2>/dev/null | |
| rm -rf "$path" 2>/dev/null || rm -rf "$path" | |
| total_freed=$((total_freed + size)) | |
| echo -e " ${GREEN}✓${NC} Removed" | |
| else | |
| total_skipped=$((total_skipped + size)) | |
| echo -e " ${CYAN}↷${NC} Skipped" | |
| fi | |
| echo "" | |
| fi | |
| } | |
| # Associative array of auto-approved parent directories. | |
| # When a user says "remove all in parent/grandparent", we store the path here. | |
| declare -A AUTO_APPROVED_DIRS | |
| # Prompt with 4 options for project-based removals (node_modules, venvs). | |
| # Sets BULK_REPLY to: "y", "n", "parent", or "grandparent". | |
| # In --force mode, always sets "y". | |
| confirm_bulk() { | |
| local prompt="$1" | |
| local path="$2" | |
| local parent | |
| parent=$(dirname "$path") | |
| local grandparent | |
| grandparent=$(dirname "$parent") | |
| BULK_REPLY="n" | |
| if $FORCE; then | |
| BULK_REPLY="y" | |
| return | |
| fi | |
| # Check if already auto-approved by a previous "all in parent/grandparent" choice | |
| for approved in "${!AUTO_APPROVED_DIRS[@]}"; do | |
| if [[ "$path" == "$approved"/* ]]; then | |
| BULK_REPLY="y" | |
| return | |
| fi | |
| done | |
| local parent_label="${parent/#$HOME_DIR/~}" | |
| local grandparent_label="${grandparent/#$HOME_DIR/~}" | |
| echo -e " ${YELLOW}?${NC} ${prompt}" | |
| echo -e " ${BOLD}y${NC}) Remove" | |
| echo -e " ${BOLD}n${NC}) Skip" | |
| echo -e " ${BOLD}p${NC}) Remove all in ${BOLD}${parent_label}/${NC}" | |
| echo -e " ${BOLD}g${NC}) Remove all in ${BOLD}${grandparent_label}/${NC}" | |
| echo -e " ${BOLD}never${NC}) Never delete this" | |
| echo -ne " Choice [y/n/p/g/never]: " | |
| read -r answer </dev/tty | |
| case "$answer" in | |
| [yY]) BULK_REPLY="y" ;; | |
| [pP]) | |
| AUTO_APPROVED_DIRS["$parent"]=1 | |
| BULK_REPLY="y" | |
| ;; | |
| [gG]) | |
| AUTO_APPROVED_DIRS["$grandparent"]=1 | |
| BULK_REPLY="y" | |
| ;; | |
| [nN][eE][vV][eE][rR]) | |
| add_exception "$path" | |
| BULK_REPLY="n" | |
| ;; | |
| *) BULK_REPLY="n" ;; | |
| esac | |
| } | |
| # Remove a path with bulk options (y/n/parent/grandparent). | |
| # Shows the folder path and size before asking. | |
| remove_and_tally_bulk() { | |
| local path="$1" | |
| local label="$2" | |
| ITEM_SHOWN=0 | |
| if [[ -e "$path" ]]; then | |
| if is_exception "$path"; then | |
| return | |
| fi | |
| local size | |
| size=$(dir_size_bytes "$path") | |
| if (( size < THRESHOLD_BYTES )); then | |
| return | |
| fi | |
| ITEM_SHOWN=1 | |
| local hsize | |
| hsize=$(human_size "$size") | |
| echo -e " ${BOLD}${label}${NC} — ${hsize}" | |
| confirm_bulk "Remove?" "$path" | |
| if [[ "$BULK_REPLY" == "y" ]]; then | |
| command -v chflags &>/dev/null && chflags -R nouchg "$path" 2>/dev/null | |
| chmod -R u+rwX "$path" 2>/dev/null | |
| rm -rf "$path" 2>/dev/null || rm -rf "$path" | |
| total_freed=$((total_freed + size)) | |
| echo -e " ${GREEN}✓${NC} Removed" | |
| else | |
| total_skipped=$((total_skipped + size)) | |
| echo -e " ${CYAN}↷${NC} Skipped" | |
| fi | |
| echo "" | |
| fi | |
| } | |
| section() { | |
| echo "" | |
| echo -e "${CYAN}${BOLD}── $1 ──${NC}" | |
| } | |
| # ── Header ─────────────────────────────────────────────────────────────────── | |
| echo "" | |
| threshold_human=$(human_size "$THRESHOLD_BYTES") | |
| if $FORCE; then | |
| echo -e "${BOLD}Disk Safe Cleanup${NC} (${RED}force mode${NC} — no confirmations, threshold: ${threshold_human})" | |
| else | |
| echo -e "${BOLD}Disk Safe Cleanup${NC} (interactive, threshold: ${threshold_human})" | |
| fi | |
| # ── 1. npm cache ───────────────────────────────────────────────────────────── | |
| section "npm cache" | |
| if command -v npm &>/dev/null; then | |
| npm_cache_path="$HOME_DIR/.npm/_cacache" | |
| size_before=$(dir_size_bytes "$npm_cache_path" 2>/dev/null || echo 0) | |
| if is_exception "$npm_cache_path"; then | |
| : # silently skip | |
| elif (( size_before >= THRESHOLD_BYTES )); then | |
| echo -e " ${BOLD}~/.npm/_cacache${NC} — $(human_size "$size_before")" | |
| if confirm "Clean npm cache?" "$npm_cache_path"; then | |
| npm cache clean --force 2>/dev/null | |
| total_freed=$((total_freed + size_before)) | |
| echo -e " ${GREEN}✓${NC} Cleaned npm cache" | |
| else | |
| total_skipped=$((total_skipped + size_before)) | |
| echo -e " ${CYAN}↷${NC} Skipped npm cache" | |
| fi | |
| else | |
| echo -e " ${YELLOW}⊘${NC} npm cache is already empty" | |
| fi | |
| else | |
| echo -e " ${YELLOW}⊘${NC} npm not found, skipping" | |
| fi | |
| # ── 2. Docker cleanup ─────────────────────────────────────────────────────── | |
| section "Docker" | |
| if is_exception "docker"; then | |
| : # silently skip | |
| elif command -v docker &>/dev/null && docker info &>/dev/null 2>&1; then | |
| # All images not used by running containers | |
| running_images=$(docker ps --format "{{.Image}}" 2>/dev/null || true) | |
| unused_images="" | |
| while IFS= read -r line; do | |
| [[ -z "$line" ]] && continue | |
| repo_tag=$(echo "$line" | awk '{print $1}') | |
| # Check if this image is used by a running container | |
| is_used=false | |
| while IFS= read -r running; do | |
| [[ -z "$running" ]] && continue | |
| if [[ "$repo_tag" == "$running" || "$repo_tag" == "$running:latest" ]]; then | |
| is_used=true | |
| break | |
| fi | |
| done <<< "$running_images" | |
| if ! $is_used; then | |
| unused_images+=" $line"$'\n' | |
| fi | |
| done < <(docker images --format "{{.Repository}}:{{.Tag}} {{.Size}}" 2>/dev/null) | |
| # List volumes not used by running containers | |
| unused_volumes="" | |
| while IFS= read -r vol; do | |
| [[ -z "$vol" ]] && continue | |
| # Check if volume is in use by a running container | |
| in_use=$(docker ps -q --filter "volume=$vol" 2>/dev/null) | |
| if [[ -z "$in_use" ]]; then | |
| vol_size=$(docker system df -v 2>/dev/null | grep "^$vol" | awk '{print $NF}' || echo "?") | |
| unused_volumes+=" $vol $vol_size"$'\n' | |
| fi | |
| done < <(docker volume ls -q 2>/dev/null) | |
| if [[ -n "$unused_images" ]]; then | |
| echo -e " ${BOLD}Images to remove:${NC}" | |
| echo -n "$unused_images" | |
| echo "" | |
| fi | |
| if [[ -n "$unused_volumes" ]]; then | |
| echo -e " ${BOLD}Volumes to remove:${NC}" | |
| echo -n "$unused_volumes" | |
| echo "" | |
| fi | |
| if [[ -z "$unused_images" && -z "$unused_volumes" ]]; then | |
| echo -e " ${YELLOW}⊘${NC} Nothing to prune" | |
| else | |
| if confirm "Prune Docker (unused images + volumes + build cache)?" "docker"; then | |
| # Remove stopped containers first, so their volumes become unused | |
| docker container prune -f 2>/dev/null | tail -1 | |
| # Remove all unused volumes (including named ones) | |
| docker volume prune -a -f 2>/dev/null | tail -1 | |
| # Remove unused images | |
| docker image prune -a -f 2>/dev/null | tail -1 | |
| # Remove build cache | |
| docker builder prune -f 2>/dev/null | tail -1 | |
| echo -e " ${GREEN}✓${NC} Docker pruned" | |
| else | |
| echo -e " ${CYAN}↷${NC} Skipped Docker prune" | |
| fi | |
| fi | |
| else | |
| echo -e " ${YELLOW}⊘${NC} Docker not running or not installed, skipping" | |
| fi | |
| # ── 3. ~/.cache directories ───────────────────────────────────────────────── | |
| section "$HOME_DIR/.cache" | |
| any_shown=0 | |
| for dir in "${CACHE_DIRS_TO_CLEAN[@]}"; do | |
| remove_and_tally "$HOME_DIR/.cache/$dir" "$HOME_DIR/.cache/$dir" | |
| (( ITEM_SHOWN )) && any_shown=1 | |
| done | |
| if [[ $any_shown -eq 0 ]]; then | |
| echo -e " ${YELLOW}⊘${NC} Nothing to delete" | |
| fi | |
| # ── 4. Library/Caches (macOS) ────────────────────────────────────────────── | |
| if [[ -d "$HOME_DIR/Library/Caches" ]]; then | |
| section "Library/Caches" | |
| any_shown=0 | |
| for dir in "${LIBRARY_CACHE_DIRS_TO_CLEAN[@]}"; do | |
| remove_and_tally "$HOME_DIR/Library/Caches/$dir" "Library/Caches/$dir" | |
| (( ITEM_SHOWN )) && any_shown=1 | |
| done | |
| if [[ $any_shown -eq 0 ]]; then | |
| echo -e " ${YELLOW}⊘${NC} Nothing to delete" | |
| fi | |
| # Also run brew cleanup if available | |
| if command -v brew &>/dev/null && ! is_exception "brew"; then | |
| brew_cache_dir="$(brew --cache 2>/dev/null || echo "$HOME_DIR/Library/Caches/Homebrew")" | |
| if [[ -d "$brew_cache_dir" ]]; then | |
| brew_size=$(dir_size_bytes "$brew_cache_dir") | |
| if (( brew_size >= THRESHOLD_BYTES )); then | |
| echo -e " ${BOLD}${brew_cache_dir}${NC} — $(human_size "$brew_size")" | |
| if confirm "Run brew cleanup --prune=all?" "brew"; then | |
| # Go module cache files are read-only by default; fix permissions first | |
| if [[ -d "$brew_cache_dir/go_mod_cache" ]]; then | |
| chmod -R u+w "$brew_cache_dir/go_mod_cache" 2>/dev/null | |
| fi | |
| brew cleanup --prune=all 2>/dev/null | |
| echo -e " ${GREEN}✓${NC} brew cleanup --prune=all" | |
| else | |
| echo -e " ${CYAN}↷${NC} Skipped brew cleanup" | |
| fi | |
| fi | |
| fi | |
| fi | |
| fi | |
| # ── 5. Old Node.js versions (nvm) ─────────────────────────────────────────── | |
| section "Old Node.js versions (nvm)" | |
| if [[ -d "$NVM_DIR/versions/node" ]]; then | |
| # Source nvm so we can use it | |
| export NVM_DIR | |
| # shellcheck disable=SC1091 | |
| [[ -s "$NVM_DIR/nvm.sh" ]] && source "$NVM_DIR/nvm.sh" | |
| current_version=$(node -v 2>/dev/null || echo "") | |
| echo -e " Current Node version: ${BOLD}${current_version}${NC}" | |
| for version_dir in "$NVM_DIR/versions/node"/v*; do | |
| [[ -d "$version_dir" ]] || continue | |
| version=$(basename "$version_dir") | |
| if [[ "$version" == "$current_version" ]]; then | |
| echo -e " ${CYAN}↳${NC} Keeping $version (active)" | |
| else | |
| remove_and_tally "$version_dir" "Node $version" | |
| fi | |
| done | |
| # Also clean nvm cache | |
| remove_and_tally "$NVM_DIR/.cache" "nvm download cache" | |
| else | |
| echo -e " ${YELLOW}⊘${NC} nvm not found, skipping" | |
| fi | |
| # ── 6. Stale node_modules ─────────────────────────────────────────────────── | |
| section "Stale node_modules (not accessed in ${NODE_MODULES_MIN_AGE_DAYS}+ days)" | |
| # Build the -not -path exclusions | |
| exclude_args=() | |
| for pattern in "${EXCLUDE_PATHS[@]}"; do | |
| exclude_args+=(-not -path "$pattern") | |
| done | |
| AUTO_APPROVED_DIRS=() | |
| any_shown=0 | |
| while IFS= read -r nm_dir; do | |
| [[ -z "$nm_dir" ]] && continue | |
| remove_and_tally_bulk "$nm_dir" "$nm_dir" | |
| (( ITEM_SHOWN )) && any_shown=1 | |
| done < <(find "$HOME_DIR" \ | |
| -maxdepth "$NODE_MODULES_SEARCH_DEPTH" \ | |
| -name "node_modules" \ | |
| -type d \ | |
| -atime +"$NODE_MODULES_MIN_AGE_DAYS" \ | |
| "${exclude_args[@]}" \ | |
| 2>/dev/null || true) | |
| if [[ $any_shown -eq 0 ]]; then | |
| echo -e " ${YELLOW}⊘${NC} Nothing to delete" | |
| fi | |
| # ── 7. Stale Python virtual environments ───────────────────────────────────── | |
| section "Stale Python venvs (not accessed in ${NODE_MODULES_MIN_AGE_DAYS}+ days)" | |
| VENV_NAMES=(".venv" ".env") | |
| AUTO_APPROVED_DIRS=() | |
| any_shown=0 | |
| for venv_name in "${VENV_NAMES[@]}"; do | |
| while IFS= read -r venv_dir; do | |
| [[ -z "$venv_dir" ]] && continue | |
| # Only match actual Python venvs (must contain pyvenv.cfg) | |
| if [[ -f "$venv_dir/pyvenv.cfg" ]]; then | |
| remove_and_tally_bulk "$venv_dir" "$venv_dir" | |
| (( ITEM_SHOWN )) && any_shown=1 | |
| fi | |
| done < <(find "$HOME_DIR" \ | |
| -maxdepth "$NODE_MODULES_SEARCH_DEPTH" \ | |
| -name "$venv_name" \ | |
| -type d \ | |
| -atime +"$NODE_MODULES_MIN_AGE_DAYS" \ | |
| "${exclude_args[@]}" \ | |
| 2>/dev/null || true) | |
| done | |
| if [[ $any_shown -eq 0 ]]; then | |
| echo -e " ${YELLOW}⊘${NC} Nothing to delete" | |
| fi | |
| # ── 8. uv cache (Python) ───────────────────────────────────────────────────── | |
| section "uv cache (Python)" | |
| uv_shown=false | |
| if command -v uv &>/dev/null && ! is_exception "$HOME_DIR/.cache/uv"; then | |
| uv_cache_dir="$HOME_DIR/.cache/uv" | |
| if [[ -d "$uv_cache_dir" ]]; then | |
| uv_size=$(dir_size_bytes "$uv_cache_dir") | |
| if (( uv_size >= THRESHOLD_BYTES )); then | |
| uv_shown=true | |
| echo -e " ${BOLD}${uv_cache_dir}${NC} — $(human_size "$uv_size")" | |
| if confirm "Run uv cache clean?" "$uv_cache_dir"; then | |
| # Check for running uv processes that hold the cache lock | |
| uv_procs=$(pgrep -fl 'uv run' || true) | |
| if [[ -n "$uv_procs" ]]; then | |
| echo -e " ${YELLOW}!${NC} uv processes are using the cache:" | |
| while IFS= read -r proc; do | |
| [[ -z "$proc" ]] && continue | |
| uv_pid=$(echo "$proc" | awk '{print $1}') | |
| uv_cmd=$(echo "$proc" | awk '{$1=""; print substr($0,2)}') | |
| echo -e " PID ${BOLD}${uv_pid}${NC} ${uv_cmd}" | |
| done <<< "$uv_procs" | |
| if confirm "Kill these processes and proceed?"; then | |
| while IFS= read -r proc; do | |
| [[ -z "$proc" ]] && continue | |
| kill "$(echo "$proc" | awk '{print $1}')" 2>/dev/null | |
| done <<< "$uv_procs" | |
| sleep 1 | |
| else | |
| total_skipped=$((total_skipped + uv_size)) | |
| echo -e " ${CYAN}↷${NC} Skipped uv cache clean" | |
| uv_size=0 # prevent double tally below | |
| fi | |
| fi | |
| if (( uv_size > 0 )); then | |
| uv cache clean 2>/dev/null | |
| total_freed=$((total_freed + uv_size)) | |
| echo -e " ${GREEN}✓${NC} uv cache clean" | |
| fi | |
| else | |
| total_skipped=$((total_skipped + uv_size)) | |
| echo -e " ${CYAN}↷${NC} Skipped uv cache clean" | |
| fi | |
| fi | |
| fi | |
| fi | |
| if ! $uv_shown; then | |
| echo -e " ${YELLOW}⊘${NC} Nothing to delete" | |
| fi | |
| # ── 9. pre-commit cache ───────────────────────────────────────────────────── | |
| section "pre-commit cache" | |
| pc_shown=false | |
| if command -v pre-commit &>/dev/null && ! is_exception "$HOME_DIR/.cache/pre-commit"; then | |
| precommit_cache_dir="$HOME_DIR/.cache/pre-commit" | |
| if [[ -d "$precommit_cache_dir" ]]; then | |
| pc_size=$(dir_size_bytes "$precommit_cache_dir") | |
| if (( pc_size >= THRESHOLD_BYTES )); then | |
| pc_shown=true | |
| echo -e " ${BOLD}${precommit_cache_dir}${NC} — $(human_size "$pc_size")" | |
| if confirm "Run pre-commit clean?" "$precommit_cache_dir"; then | |
| pre-commit clean 2>/dev/null | |
| total_freed=$((total_freed + pc_size)) | |
| echo -e " ${GREEN}✓${NC} pre-commit clean" | |
| else | |
| total_skipped=$((total_skipped + pc_size)) | |
| echo -e " ${CYAN}↷${NC} Skipped pre-commit clean" | |
| fi | |
| fi | |
| fi | |
| fi | |
| if ! $pc_shown; then | |
| echo -e " ${YELLOW}⊘${NC} Nothing to delete" | |
| fi | |
| # ── Summary ────────────────────────────────────────────────────────────────── | |
| echo "" | |
| echo -e "${BOLD}════════════════════════════════════════${NC}" | |
| echo -e "${GREEN}${BOLD} Space freed: $(human_size "$total_freed")${NC}" | |
| if (( total_skipped > 0 )); then | |
| echo -e "${YELLOW}${BOLD} Space skipped: $(human_size "$total_skipped")${NC}" | |
| fi | |
| echo -e "${BOLD}════════════════════════════════════════${NC}" | |
| echo "" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment