2025-11-28 22:39:11 +09:00
#!/bin/bash
# Developer Tools Cleanup Module
set -euo pipefail
2026-02-13 10:41:28 +08:00
2025-12-31 16:23:31 +08:00
# Tool cache helper (respects DRY_RUN).
2025-12-08 15:34:51 +08:00
clean_tool_cache( ) {
local description = " $1 "
shift
if [ [ " $DRY_RUN " != "true" ] ] ; then
2026-02-26 16:36:06 +08:00
local command_succeeded = false
if [ [ -t 1 ] ] ; then
start_section_spinner " Cleaning $description ... "
fi
2026-02-07 11:21:37 +08:00
if " $@ " > /dev/null 2>& 1; then
2026-02-26 16:36:06 +08:00
command_succeeded = true
fi
if [ [ -t 1 ] ] ; then
stop_section_spinner
fi
if [ [ " $command_succeeded " = = "true" ] ] ; then
2025-12-08 15:34:51 +08:00
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } $description "
fi
else
2025-12-27 10:17:05 +08:00
echo -e " ${ YELLOW } ${ ICON_DRY_RUN } ${ NC } $description · would clean "
2025-12-08 15:34:51 +08:00
fi
2025-12-29 14:27:47 +08:00
return 0
2025-12-08 15:34:51 +08:00
}
2025-12-31 16:23:31 +08:00
# npm/pnpm/yarn/bun caches.
2025-11-28 22:39:11 +09:00
clean_dev_npm( ) {
2026-02-16 08:40:13 +01:00
local npm_default_cache = " $HOME /.npm "
local npm_cache_path = " $npm_default_cache "
2026-02-07 11:21:37 +08:00
if command -v npm > /dev/null 2>& 1; then
2025-12-06 20:28:49 +08:00
clean_tool_cache "npm cache" npm cache clean --force
2026-02-16 08:40:13 +01:00
start_section_spinner "Checking npm cache path..."
npm_cache_path = $( run_with_timeout 2 npm config get cache 2> /dev/null) || npm_cache_path = ""
stop_section_spinner
if [ [ -z " $npm_cache_path " || " $npm_cache_path " != /* ] ] ; then
npm_cache_path = " $npm_default_cache "
fi
2025-11-28 22:39:11 +09:00
note_activity
fi
2026-02-16 08:40:13 +01:00
2026-02-16 15:53:32 +08:00
# These residual directories are not removed by `npm cache clean --force`
local -a npm_residual_dirs = ( "_cacache" "_npx" "_logs" "_prebuilds" )
local -a npm_descriptions = ( "npm cache directory" "npm npx cache" "npm logs" "npm prebuilds" )
# Clean default npm cache path
local i
for i in " ${ !npm_residual_dirs[@] } " ; do
safe_clean " $npm_default_cache / ${ npm_residual_dirs [ $i ] } " /* " ${ npm_descriptions [ $i ] } "
done
2026-02-16 08:40:13 +01:00
2026-02-21 20:22:01 +08:00
# Normalize paths for comparison (remove trailing slash + resolve symlinked dirs)
local npm_cache_path_normalized = " ${ npm_cache_path %/ } "
local npm_default_cache_normalized = " ${ npm_default_cache %/ } "
if [ [ -d " $npm_cache_path_normalized " ] ] ; then
npm_cache_path_normalized = $( cd " $npm_cache_path_normalized " 2> /dev/null && pwd -P) || npm_cache_path_normalized = " ${ npm_cache_path %/ } "
fi
if [ [ -d " $npm_default_cache_normalized " ] ] ; then
npm_default_cache_normalized = $( cd " $npm_default_cache_normalized " 2> /dev/null && pwd -P) || npm_default_cache_normalized = " ${ npm_default_cache %/ } "
fi
2026-02-16 15:53:32 +08:00
# Clean custom npm cache path (if different from default)
2026-02-21 20:22:01 +08:00
if [ [ " $npm_cache_path_normalized " != " $npm_default_cache_normalized " ] ] ; then
2026-02-16 15:53:32 +08:00
for i in " ${ !npm_residual_dirs[@] } " ; do
safe_clean " $npm_cache_path / ${ npm_residual_dirs [ $i ] } " /* " ${ npm_descriptions [ $i ] } (custom path) "
done
2026-02-16 08:40:13 +01:00
fi
2025-12-26 03:46:00 +00:00
# Clean pnpm store cache
local pnpm_default_store = ~/Library/pnpm/store
2026-01-02 18:50:42 +08:00
# Check if pnpm is actually usable (not just Corepack shim)
2026-02-07 11:21:37 +08:00
if command -v pnpm > /dev/null 2>& 1 && COREPACK_ENABLE_DOWNLOAD_PROMPT = 0 pnpm --version > /dev/null 2>& 1; then
2026-01-02 19:10:20 +08:00
COREPACK_ENABLE_DOWNLOAD_PROMPT = 0 clean_tool_cache "pnpm cache" pnpm store prune
2025-12-26 03:46:00 +00:00
local pnpm_store_path
2025-12-27 10:17:05 +08:00
start_section_spinner "Checking store path..."
2026-02-07 11:21:37 +08:00
pnpm_store_path = $( COREPACK_ENABLE_DOWNLOAD_PROMPT = 0 run_with_timeout 2 pnpm store path 2> /dev/null) || pnpm_store_path = ""
2025-12-27 10:17:05 +08:00
stop_section_spinner
2025-12-26 03:46:00 +00:00
if [ [ -n " $pnpm_store_path " && " $pnpm_store_path " != " $pnpm_default_store " ] ] ; then
2025-12-27 10:17:05 +08:00
safe_clean " $pnpm_default_store " /* "Orphaned pnpm store"
2025-12-26 03:46:00 +00:00
fi
else
2026-01-02 18:50:42 +08:00
# pnpm not installed or not usable, just clean the default store directory
2025-12-26 03:46:00 +00:00
safe_clean " $pnpm_default_store " /* "pnpm store"
fi
2025-12-26 12:49:56 +08:00
note_activity
2025-11-28 22:39:11 +09:00
safe_clean ~/.tnpm/_cacache/* "tnpm cache directory"
safe_clean ~/.tnpm/_logs/* "tnpm logs"
safe_clean ~/.yarn/cache/* "Yarn cache"
safe_clean ~/.bun/install/cache/* "Bun cache"
}
2025-12-31 16:23:31 +08:00
# Python/pip ecosystem caches.
2025-11-28 22:39:11 +09:00
clean_dev_python( ) {
2026-02-28 20:19:30 +08:00
# Check pip3 is functional (not just macOS stub that triggers CLT install dialog)
if command -v pip3 > /dev/null 2>& 1 && pip3 --version > /dev/null 2>& 1; then
2026-01-19 17:08:35 +08:00
clean_tool_cache "pip cache" bash -c 'pip3 cache purge > /dev/null 2>&1 || true'
2025-11-28 22:39:11 +09:00
note_activity
fi
safe_clean ~/.pyenv/cache/* "pyenv cache"
safe_clean ~/.cache/poetry/* "Poetry cache"
safe_clean ~/.cache/uv/* "uv cache"
safe_clean ~/.cache/ruff/* "Ruff cache"
safe_clean ~/.cache/mypy/* "MyPy cache"
safe_clean ~/.pytest_cache/* "Pytest cache"
safe_clean ~/.jupyter/runtime/* "Jupyter runtime cache"
safe_clean ~/.cache/huggingface/* "Hugging Face cache"
safe_clean ~/.cache/torch/* "PyTorch cache"
safe_clean ~/.cache/tensorflow/* "TensorFlow cache"
safe_clean ~/.conda/pkgs/* "Conda packages cache"
safe_clean ~/anaconda3/pkgs/* "Anaconda packages cache"
safe_clean ~/.cache/wandb/* "Weights & Biases cache"
}
2025-12-31 16:23:31 +08:00
# Go build/module caches.
2025-11-28 22:39:11 +09:00
clean_dev_go( ) {
2026-03-03 16:02:23 +08:00
command -v go > /dev/null 2>& 1 || return 0
2026-03-03 15:57:45 +08:00
2026-03-03 16:02:23 +08:00
local go_build_cache go_mod_cache
go_build_cache = $( go env GOCACHE 2> /dev/null || echo " $HOME /Library/Caches/go-build " )
go_mod_cache = $( go env GOMODCACHE 2> /dev/null || echo " $HOME /go/pkg/mod " )
2026-03-03 15:57:45 +08:00
2026-03-03 16:02:23 +08:00
local build_protected = false mod_protected = false
is_path_whitelisted " $go_build_cache " && build_protected = true
is_path_whitelisted " $go_mod_cache " && mod_protected = true
if [ [ " $build_protected " = = "true" && " $mod_protected " = = "true" ] ] ; then
if [ [ " $DRY_RUN " = = "true" ] ] ; then
echo -e " ${ YELLOW } ${ ICON_DRY_RUN } ${ NC } Go cache · would skip (whitelist) "
2026-03-03 15:57:45 +08:00
else
2026-03-03 16:02:23 +08:00
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Go cache · skipped (whitelist) "
2026-03-03 15:57:45 +08:00
fi
2026-03-03 16:02:23 +08:00
return 0
2025-11-28 22:39:11 +09:00
fi
2026-03-03 16:02:23 +08:00
if [ [ " $build_protected " != "true" && " $mod_protected " != "true" ] ] ; then
clean_tool_cache "Go cache" bash -c 'go clean -modcache > /dev/null 2>&1 || true; go clean -cache > /dev/null 2>&1 || true'
elif [ [ " $build_protected " = = "true" ] ] ; then
clean_tool_cache "Go module cache" bash -c 'go clean -modcache > /dev/null 2>&1 || true'
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Go build cache · skipped (whitelist) "
else
clean_tool_cache "Go build cache" bash -c 'go clean -cache > /dev/null 2>&1 || true'
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Go module cache · skipped (whitelist) "
fi
note_activity
2025-11-28 22:39:11 +09:00
}
2025-12-31 16:23:31 +08:00
# Rust/cargo caches.
2025-11-28 22:39:11 +09:00
clean_dev_rust( ) {
safe_clean ~/.cargo/registry/cache/* "Rust cargo cache"
safe_clean ~/.cargo/git/* "Cargo git cache"
safe_clean ~/.rustup/downloads/* "Rust downloads cache"
}
2026-01-13 15:59:43 +08:00
# Helper: Check for multiple versions in a directory.
2026-01-20 14:44:11 +08:00
# Args: $1=directory, $2=tool_name, $3=list_command, $4=remove_command
2026-01-13 15:59:43 +08:00
check_multiple_versions( ) {
local dir = " $1 "
local tool_name = " $2 "
2026-01-20 14:44:11 +08:00
local list_cmd = " ${ 3 :- } "
local remove_cmd = " ${ 4 :- } "
2026-01-13 15:59:43 +08:00
if [ [ ! -d " $dir " ] ] ; then
return 0
fi
local count
2026-02-07 11:21:37 +08:00
count = $( find " $dir " -mindepth 1 -maxdepth 1 -type d 2> /dev/null | wc -l | tr -d ' ' )
2026-01-13 15:59:43 +08:00
if [ [ " $count " -gt 1 ] ] ; then
note_activity
2026-01-20 14:44:11 +08:00
local hint = ""
if [ [ -n " $list_cmd " ] ] ; then
hint = " · ${ GRAY } ${ list_cmd } ${ NC } "
fi
2026-01-28 20:15:26 +08:00
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } ${ tool_name } : ${ count } found ${ hint } "
2026-01-13 15:59:43 +08:00
fi
}
# Check for multiple Rust toolchains.
check_rust_toolchains( ) {
2026-02-07 11:21:37 +08:00
command -v rustup > /dev/null 2>& 1 || return 0
2026-01-13 15:59:43 +08:00
check_multiple_versions \
" $HOME /.rustup/toolchains " \
"Rust toolchains" \
2026-01-20 14:44:11 +08:00
"rustup toolchain list"
2026-01-13 15:59:43 +08:00
}
2025-12-31 16:23:31 +08:00
# Docker caches (guarded by daemon check).
2025-11-28 22:39:11 +09:00
clean_dev_docker( ) {
2026-02-07 11:21:37 +08:00
if command -v docker > /dev/null 2>& 1; then
2025-11-28 22:39:11 +09:00
if [ [ " $DRY_RUN " != "true" ] ] ; then
2025-12-27 10:17:05 +08:00
start_section_spinner "Checking Docker daemon..."
local docker_running = false
2026-02-07 11:21:37 +08:00
if run_with_timeout 3 docker info > /dev/null 2>& 1; then
2025-12-27 10:17:05 +08:00
docker_running = true
fi
stop_section_spinner
if [ [ " $docker_running " = = "true" ] ] ; then
2025-12-06 09:17:58 +08:00
clean_tool_cache "Docker build cache" docker builder prune -af
else
2025-12-30 17:13:43 +08:00
debug_log "Docker daemon not running, skipping Docker cache cleanup"
2025-12-06 09:17:58 +08:00
fi
2025-11-28 22:39:11 +09:00
else
2025-12-06 09:17:58 +08:00
note_activity
2025-12-27 10:17:05 +08:00
echo -e " ${ YELLOW } ${ ICON_DRY_RUN } ${ NC } Docker build cache · would clean "
2025-11-28 22:39:11 +09:00
fi
fi
safe_clean ~/.docker/buildx/cache/* "Docker BuildX cache"
}
2025-12-31 16:23:31 +08:00
# Nix garbage collection.
2025-11-28 22:39:11 +09:00
clean_dev_nix( ) {
2026-02-07 11:21:37 +08:00
if command -v nix-collect-garbage > /dev/null 2>& 1; then
2025-11-28 22:39:11 +09:00
if [ [ " $DRY_RUN " != "true" ] ] ; then
clean_tool_cache "Nix garbage collection" nix-collect-garbage --delete-older-than 30d
else
2025-12-27 10:17:05 +08:00
echo -e " ${ YELLOW } ${ ICON_DRY_RUN } ${ NC } Nix garbage collection · would clean "
2025-11-28 22:39:11 +09:00
fi
note_activity
fi
}
2025-12-31 16:23:31 +08:00
# Cloud CLI caches.
2025-11-28 22:39:11 +09:00
clean_dev_cloud( ) {
safe_clean ~/.kube/cache/* "Kubernetes cache"
safe_clean ~/.local/share/containers/storage/tmp/* "Container storage temp"
safe_clean ~/.aws/cli/cache/* "AWS CLI cache"
safe_clean ~/.config/gcloud/logs/* "Google Cloud logs"
safe_clean ~/.azure/logs/* "Azure CLI logs"
}
2025-12-31 16:23:31 +08:00
# Frontend build caches.
2025-11-28 22:39:11 +09:00
clean_dev_frontend( ) {
safe_clean ~/.cache/typescript/* "TypeScript cache"
safe_clean ~/.cache/electron/* "Electron cache"
safe_clean ~/.cache/node-gyp/* "node-gyp cache"
safe_clean ~/.node-gyp/* "node-gyp build cache"
safe_clean ~/.turbo/cache/* "Turbo cache"
safe_clean ~/.vite/cache/* "Vite cache"
safe_clean ~/.cache/vite/* "Vite global cache"
safe_clean ~/.cache/webpack/* "Webpack cache"
safe_clean ~/.parcel-cache/* "Parcel cache"
safe_clean ~/.cache/eslint/* "ESLint cache"
safe_clean ~/.cache/prettier/* "Prettier cache"
}
2026-01-03 09:41:05 +08:00
# Check for multiple Android NDK versions.
check_android_ndk( ) {
2026-01-13 15:59:43 +08:00
check_multiple_versions \
" $HOME /Library/Android/sdk/ndk " \
"Android NDK versions" \
2026-01-20 14:44:11 +08:00
"Android Studio → SDK Manager"
2026-01-03 09:41:05 +08:00
}
2026-02-10 15:51:28 +08:00
clean_xcode_documentation_cache( ) {
local doc_cache_root = " ${ MOLE_XCODE_DOCUMENTATION_CACHE_DIR :- /Library/Developer/Xcode/DocumentationCache } "
[ [ -d " $doc_cache_root " ] ] || return 0
if pgrep -x "Xcode" > /dev/null 2>& 1; then
echo -e " ${ GRAY } ${ ICON_WARNING } ${ NC } Xcode is running, skipping documentation cache cleanup "
note_activity
return 0
fi
local -a index_entries = ( )
while IFS = read -r -d '' entry; do
index_entries += ( " $entry " )
done < <( command find " $doc_cache_root " -mindepth 1 -maxdepth 1 \( -name "DeveloperDocumentation.index" -o -name "DeveloperDocumentation*.index" \) -print0 2> /dev/null)
if [ [ ${# index_entries [@] } -le 1 ] ] ; then
return 0
fi
local -a sorted_entries = ( )
while IFS = read -r line; do
sorted_entries += ( " ${ line #* } " )
done < <(
for entry in " ${ index_entries [@] } " ; do
2026-02-10 16:33:17 +08:00
local mtime
2026-02-10 15:51:28 +08:00
mtime = $( stat -f%m " $entry " 2> /dev/null || echo "0" )
printf '%s %s\n' " $mtime " " $entry "
done | sort -rn
)
local -a stale_entries = ( )
local idx = 0
local entry
for entry in " ${ sorted_entries [@] } " ; do
if [ [ $idx -eq 0 ] ] ; then
2026-02-28 11:10:18 +08:00
idx = $(( idx + 1 ))
2026-02-10 15:51:28 +08:00
continue
fi
stale_entries += ( " $entry " )
2026-02-28 11:10:18 +08:00
idx = $(( idx + 1 ))
2026-02-10 15:51:28 +08:00
done
2026-02-15 07:52:43 +08:00
if [ [ ${# stale_entries [@] } -eq 0 ] ] ; then
return 0
fi
if [ [ " ${ DRY_RUN :- false } " = = "true" ] ] ; then
2026-02-10 15:51:28 +08:00
safe_clean " ${ stale_entries [@] } " "Xcode documentation cache (old indexes)"
note_activity
2026-02-15 07:52:43 +08:00
return 0
fi
if ! has_sudo_session; then
if ! ensure_sudo_session "Cleaning Xcode documentation cache requires admin access" ; then
echo -e " ${ GRAY } ${ ICON_WARNING } ${ NC } Xcode documentation cache cleanup skipped (sudo denied) "
note_activity
return 0
fi
fi
local removed_count = 0
local skipped_count = 0
local stale_entry
for stale_entry in " ${ stale_entries [@] } " ; do
if should_protect_path " $stale_entry " || is_path_whitelisted " $stale_entry " ; then
skipped_count = $(( skipped_count + 1 ))
continue
fi
if safe_sudo_remove " $stale_entry " ; then
removed_count = $(( removed_count + 1 ))
fi
done
if [ [ $removed_count -gt 0 ] ] ; then
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Xcode documentation cache · removed ${ removed_count } old indexes "
if [ [ $skipped_count -gt 0 ] ] ; then
echo -e " ${ GRAY } ${ ICON_WARNING } ${ NC } Xcode documentation cache · skipped ${ skipped_count } protected items "
fi
note_activity
elif [ [ $skipped_count -gt 0 ] ] ; then
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Xcode documentation cache · nothing to clean "
echo -e " ${ GRAY } ${ ICON_WARNING } ${ NC } Xcode documentation cache · skipped ${ skipped_count } protected items "
note_activity
else
echo -e " ${ GRAY } ${ ICON_WARNING } ${ NC } Xcode documentation cache · no items removed "
note_activity
2026-02-10 15:51:28 +08:00
fi
}
2026-03-03 11:05:29 +03:00
# Clean old Xcode DeviceSupport versions, keeping the most recent ones.
# Each version holds debug symbols (1-3 GB) for a specific iOS/watchOS/tvOS version.
# Symbols regenerate automatically when a device running that version is connected.
# Args: $1=directory path, $2=display name (e.g. "iOS DeviceSupport")
clean_xcode_device_support( ) {
local ds_dir = " $1 "
local display_name = " $2 "
local keep_count = " ${ MOLE_XCODE_DEVICE_SUPPORT_KEEP :- 2 } "
[ [ " $keep_count " = ~ ^[ 0-9] +$ ] ] || keep_count = 2
[ [ -d " $ds_dir " ] ] || return 0
# Collect version directories (each is a platform version like "17.5 (21F79)")
local -a version_dirs = ( )
while IFS = read -r -d '' entry; do
# Skip non-directories (e.g. .log files at the top level)
[ [ -d " $entry " ] ] || continue
version_dirs += ( " $entry " )
done < <( command find " $ds_dir " -mindepth 1 -maxdepth 1 -print0 2> /dev/null)
# Sort by modification time (most recent first)
local -a sorted_dirs = ( )
while IFS = read -r line; do
sorted_dirs += ( " ${ line #* } " )
done < <(
for entry in " ${ version_dirs [@] } " ; do
2026-03-03 16:16:52 +08:00
printf '%s %s\n' " $( stat -f%m " $entry " 2> /dev/null || echo 0) " " $entry "
2026-03-03 11:05:29 +03:00
done | sort -rn
)
2026-03-03 16:16:52 +08:00
# Get stale versions (everything after keep_count)
local -a stale_dirs = ( " ${ sorted_dirs [@] : $keep_count } " )
2026-03-03 11:05:29 +03:00
2026-03-03 16:16:52 +08:00
if [ [ ${# stale_dirs [@] } -gt 0 ] ] ; then
# Calculate total size of stale versions
local stale_size_kb = 0 entry_size_kb
for stale_entry in " ${ stale_dirs [@] } " ; do
entry_size_kb = $( get_path_size_kb " $stale_entry " 2> /dev/null || echo 0)
stale_size_kb = $(( stale_size_kb + entry_size_kb))
done
local stale_size_human
stale_size_human = $( bytes_to_human " $(( stale_size_kb * 1024 )) " )
2026-03-03 11:05:29 +03:00
2026-03-03 16:16:52 +08:00
if [ [ " $DRY_RUN " = = "true" ] ] ; then
echo -e " ${ YELLOW } ${ ICON_DRY_RUN } ${ NC } ${ display_name } · would remove ${# stale_dirs [@] } old versions ( ${ stale_size_human } ), keeping ${ keep_count } most recent "
note_activity
else
# Remove old versions
local removed_count = 0
for stale_entry in " ${ stale_dirs [@] } " ; do
if should_protect_path " $stale_entry " || is_path_whitelisted " $stale_entry " ; then
continue
fi
if safe_remove " $stale_entry " ; then
removed_count = $(( removed_count + 1 ))
fi
done
if [ [ $removed_count -gt 0 ] ] ; then
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } ${ display_name } · removed ${ removed_count } old versions, ${ stale_size_human } "
note_activity
fi
2026-03-03 11:05:29 +03:00
fi
fi
2026-03-03 16:16:52 +08:00
# Clean caches/logs inside kept versions
2026-03-03 11:05:29 +03:00
safe_clean " $ds_dir " /*/Symbols/System/Library/Caches/* " $display_name symbol cache "
safe_clean " $ds_dir " /*.log " $display_name logs "
}
2026-02-13 10:41:28 +08:00
_sim_runtime_mount_points( ) {
if [ [ -n " ${ MOLE_XCODE_SIM_RUNTIME_MOUNT_POINTS :- } " ] ] ; then
printf '%s\n' " $MOLE_XCODE_SIM_RUNTIME_MOUNT_POINTS "
return 0
fi
mount 2> /dev/null | command awk '{print $3}' || true
}
_sim_runtime_is_path_in_use( ) {
local target_path = " $1 "
shift || true
local mount_path
for mount_path in " $@ " ; do
[ [ -z " $mount_path " ] ] && continue
if [ [ " $mount_path " = = " $target_path " || " $mount_path " = = " $target_path " /* ] ] ; then
return 0
fi
done
return 1
}
_sim_runtime_size_kb( ) {
local target_path = " $1 "
local size_kb = 0
if has_sudo_session; then
size_kb = $( sudo du -skP " $target_path " 2> /dev/null | command awk 'NR==1 {print $1; exit}' || echo "0" )
else
size_kb = $( du -skP " $target_path " 2> /dev/null | command awk 'NR==1 {print $1; exit}' || echo "0" )
fi
[ [ " $size_kb " = ~ ^[ 0-9] +$ ] ] || size_kb = 0
echo " $size_kb "
}
clean_xcode_simulator_runtime_volumes( ) {
local volumes_root = " ${ MOLE_XCODE_SIM_RUNTIME_VOLUMES_ROOT :- /Library/Developer/CoreSimulator/Volumes } "
local cryptex_root = " ${ MOLE_XCODE_SIM_RUNTIME_CRYPTEX_ROOT :- /Library/Developer/CoreSimulator/Cryptex } "
local -a candidates = ( )
local candidate
for candidate in " $volumes_root " " $cryptex_root " ; do
[ [ -d " $candidate " ] ] || continue
while IFS = read -r -d '' entry; do
candidates += ( " $entry " )
done < <( command find " $candidate " -mindepth 1 -maxdepth 1 -type d -print0 2> /dev/null)
done
if [ [ ${# candidates [@] } -eq 0 ] ] ; then
return 0
fi
local -a mount_points = ( )
while IFS = read -r line; do
[ [ -n " $line " ] ] && mount_points += ( " $line " )
done < <( _sim_runtime_mount_points)
local -a entry_statuses = ( )
local -a sorted_candidates = ( )
local sorted
while IFS = read -r sorted; do
[ [ -n " $sorted " ] ] && sorted_candidates += ( " $sorted " )
done < <( printf '%s\n' " ${ candidates [@] } " | LC_ALL = C sort)
2026-02-21 23:36:05 +08:00
# Only show scanning message in debug mode; spinner provides visual feedback otherwise
if [ [ " ${ MO_DEBUG :- 0 } " = = "1" ] ] ; then
echo -e " ${ GRAY } ${ ICON_LIST } ${ NC } Xcode runtime volumes · scanning ${# sorted_candidates [@] } entries "
fi
2026-02-15 08:45:47 +08:00
local runtime_scan_spinner = false
if [ [ -t 1 ] ] ; then
start_section_spinner "Scanning Xcode runtime volumes..."
runtime_scan_spinner = true
fi
local in_use_count = 0
local unused_count = 0
2026-02-13 10:41:28 +08:00
for candidate in " ${ sorted_candidates [@] } " ; do
local status = "UNUSED"
2026-02-28 09:53:07 +08:00
if [ [ ${# mount_points [@] } -gt 0 ] ] && _sim_runtime_is_path_in_use " $candidate " " ${ mount_points [@] } " ; then
2026-02-13 10:41:28 +08:00
status = "IN_USE"
in_use_count = $(( in_use_count + 1 ))
else
unused_count = $(( unused_count + 1 ))
fi
2026-02-15 08:45:47 +08:00
entry_statuses += ( " $status " )
2026-02-13 10:41:28 +08:00
done
if [ [ " $DRY_RUN " = = "true" ] ] ; then
2026-02-15 08:45:47 +08:00
local -a size_values = ( )
local in_use_kb = 0
local unused_kb = 0
local i = 0
for candidate in " ${ sorted_candidates [@] } " ; do
local size_kb
size_kb = $( _sim_runtime_size_kb " $candidate " )
size_values += ( " $size_kb " )
local status = " ${ entry_statuses [ $i ] :- UNUSED } "
if [ [ " $status " = = "IN_USE" ] ] ; then
in_use_kb = $(( in_use_kb + size_kb))
else
unused_kb = $(( unused_kb + size_kb))
fi
i = $(( i + 1 ))
done
if [ [ " $runtime_scan_spinner " = = "true" ] ] ; then
stop_section_spinner
runtime_scan_spinner = false
fi
2026-02-13 10:41:28 +08:00
echo -e " ${ YELLOW } ${ ICON_DRY_RUN } ${ NC } Xcode runtime volumes · ${ unused_count } unused, ${ in_use_count } in use "
local dryrun_total_kb = $(( unused_kb + in_use_kb))
local dryrun_total_human
dryrun_total_human = $( bytes_to_human " $(( dryrun_total_kb * 1024 )) " )
local dryrun_unused_human
dryrun_unused_human = $( bytes_to_human " $(( unused_kb * 1024 )) " )
local dryrun_in_use_human
dryrun_in_use_human = $( bytes_to_human " $(( in_use_kb * 1024 )) " )
echo -e " ${ GRAY } ${ ICON_LIST } ${ NC } Runtime volumes total: ${ dryrun_total_human } (unused ${ dryrun_unused_human } , in-use ${ dryrun_in_use_human } ) "
local dryrun_max_items = " ${ MOLE_SIM_RUNTIME_DRYRUN_MAX_ITEMS :- 20 } "
[ [ " $dryrun_max_items " = ~ ^[ 0-9] +$ ] ] || dryrun_max_items = 20
if [ [ " $dryrun_max_items " -le 0 ] ] ; then
dryrun_max_items = 20
fi
local shown = 0
local line_size_kb line_status line_path
while IFS = $'\t' read -r line_size_kb line_status line_path; do
[ [ -z " ${ line_path :- } " ] ] && continue
local line_human
line_human = $( bytes_to_human " $(( line_size_kb * 1024 )) " )
echo -e " ${ GRAY } ${ line_status } ${ NC } ${ line_human } · ${ line_path } "
shown = $(( shown + 1 ))
if [ [ " $shown " -ge " $dryrun_max_items " ] ] ; then
break
fi
done < <(
local j = 0
while [ [ $j -lt ${# sorted_candidates [@] } ] ] ; do
printf '%s\t%s\t%s\n' " ${ size_values [ $j ] :- 0 } " " ${ entry_statuses [ $j ] :- UNUSED } " " ${ sorted_candidates [ $j ] } "
j = $(( j + 1 ))
done | LC_ALL = C sort -nr -k1,1
)
local total_entries = " ${# sorted_candidates [@] } "
if [ [ " $total_entries " -gt " $shown " ] ] ; then
local remaining = $(( total_entries - shown))
echo -e " ${ GRAY } ${ ICON_LIST } ${ NC } ... and ${ remaining } more runtime volume entries "
fi
note_activity
return 0
fi
# Auto-clean all UNUSED runtime volumes (no user selection)
local -a selected_paths = ( )
local skipped_protected = 0
local i = 0
for ( ( i = 0; i < ${# sorted_candidates [@] } ; i++) ) ; do
local status = " ${ entry_statuses [ $i ] :- UNUSED } "
[ [ " $status " = = "IN_USE" ] ] && continue
local candidate_path = " ${ sorted_candidates [ $i ] } "
if should_protect_path " $candidate_path " || is_path_whitelisted " $candidate_path " ; then
skipped_protected = $(( skipped_protected + 1 ))
continue
fi
selected_paths += ( " $candidate_path " )
done
2026-02-15 08:45:47 +08:00
if [ [ " $runtime_scan_spinner " = = "true" ] ] ; then
stop_section_spinner
runtime_scan_spinner = false
fi
2026-02-13 10:41:28 +08:00
if [ [ ${# selected_paths [@] } -eq 0 ] ] ; then
2026-02-21 23:36:05 +08:00
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Xcode runtime volumes · already clean "
2026-02-13 10:41:28 +08:00
note_activity
return 0
fi
if ! has_sudo_session; then
if ! ensure_sudo_session "Cleaning Xcode runtime volumes requires admin access" ; then
2026-02-21 23:36:05 +08:00
echo -e " ${ YELLOW } ${ ICON_WARNING } ${ NC } Xcode runtime volumes · skipped (sudo denied) "
2026-02-13 10:41:28 +08:00
note_activity
return 0
fi
fi
2026-02-21 23:36:05 +08:00
# Perform cleanup and report final result in one line
2026-02-13 10:41:28 +08:00
local removed_count = 0
local removed_size_kb = 0
local selected_path
for selected_path in " ${ selected_paths [@] } " ; do
2026-02-15 08:45:47 +08:00
local selected_size_kb = 0
selected_size_kb = $( _sim_runtime_size_kb " $selected_path " )
2026-02-13 10:41:28 +08:00
if safe_sudo_remove " $selected_path " ; then
removed_count = $(( removed_count + 1 ))
removed_size_kb = $(( removed_size_kb + selected_size_kb))
fi
done
2026-02-21 23:36:05 +08:00
# Unified output: report result, not intermediate steps
2026-02-13 10:41:28 +08:00
if [ [ $removed_count -gt 0 ] ] ; then
local removed_human
removed_human = $( bytes_to_human " $(( removed_size_kb * 1024 )) " )
2026-02-21 23:36:05 +08:00
if [ [ $skipped_protected -gt 0 ] ] ; then
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Xcode runtime volumes · removed ${ removed_count } ( ${ removed_human } ), skipped ${ skipped_protected } protected "
else
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Xcode runtime volumes · removed ${ removed_count } ( ${ removed_human } ) "
fi
2026-02-13 10:41:28 +08:00
note_activity
else
2026-02-21 23:36:05 +08:00
if [ [ $skipped_protected -gt 0 ] ] ; then
echo -e " ${ YELLOW } ${ ICON_WARNING } ${ NC } Xcode runtime volumes · skipped ${ skipped_protected } protected, none removed "
else
2026-02-21 23:48:07 +08:00
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Xcode runtime volumes · already clean "
2026-02-21 23:36:05 +08:00
fi
2026-02-13 10:41:28 +08:00
note_activity
fi
}
2025-11-28 22:39:11 +09:00
clean_dev_mobile( ) {
2026-01-03 09:41:05 +08:00
check_android_ndk
2026-02-10 15:51:28 +08:00
clean_xcode_documentation_cache
2026-02-13 10:41:28 +08:00
clean_xcode_simulator_runtime_volumes
2026-01-03 09:41:05 +08:00
2026-02-07 11:21:37 +08:00
if command -v xcrun > /dev/null 2>& 1; then
2025-12-06 12:53:16 +08:00
debug_log "Checking for unavailable Xcode simulators"
2026-02-07 11:17:07 +08:00
local unavailable_before = 0
local unavailable_after = 0
local removed_unavailable = 0
2026-02-13 10:41:28 +08:00
local unavailable_size_kb = 0
local unavailable_size_human = "0B"
local -a unavailable_udids = ( )
local unavailable_udid = ""
2026-02-07 11:17:07 +08:00
2026-03-03 15:38:01 +08:00
# Check if simctl is accessible and working
2026-03-05 12:00:07 +08:00
local simctl_available = true
2026-03-03 15:38:01 +08:00
if ! xcrun simctl list devices > /dev/null 2>& 1; then
debug_log "simctl not accessible or CoreSimulator service not running"
2026-03-03 15:39:45 +08:00
echo -e " ${ GRAY } ${ ICON_WARNING } ${ NC } Xcode unavailable simulators · simctl not available "
2026-03-03 15:38:01 +08:00
note_activity
2026-03-05 12:00:07 +08:00
simctl_available = false
2026-03-03 15:38:01 +08:00
fi
2026-03-05 12:00:07 +08:00
if [ [ " $simctl_available " = = "true" ] ] ; then
unavailable_before = $( xcrun simctl list devices unavailable 2> /dev/null | command awk '/\(unavailable/ { count++ } END { print count+0 }' || echo "0" )
[ [ " $unavailable_before " = ~ ^[ 0-9] +$ ] ] || unavailable_before = 0
while IFS = read -r unavailable_udid; do
[ [ -n " $unavailable_udid " ] ] && unavailable_udids += ( " $unavailable_udid " )
done < <(
xcrun simctl list devices unavailable 2> /dev/null |
command sed -nE 's/.*\(([0-9A-Fa-f-]{36})\).*\(unavailable.*/\1/p' || true
)
if [ [ ${# unavailable_udids [@] } -gt 0 ] ] ; then
local udid
for udid in " ${ unavailable_udids [@] } " ; do
local simulator_device_path = " $HOME /Library/Developer/CoreSimulator/Devices/ $udid "
if [ [ -d " $simulator_device_path " ] ] ; then
unavailable_size_kb = $(( unavailable_size_kb + $( get_path_size_kb " $simulator_device_path " ) ))
fi
done
2026-03-03 15:38:01 +08:00
fi
2026-03-05 12:00:07 +08:00
unavailable_size_human = $( bytes_to_human " $(( unavailable_size_kb * 1024 )) " )
2026-03-03 15:38:01 +08:00
2026-03-05 12:00:07 +08:00
if [ [ " $DRY_RUN " = = "true" ] ] ; then
if ( ( unavailable_before > 0) ) ; then
echo -e " ${ YELLOW } ${ ICON_DRY_RUN } ${ NC } Xcode unavailable simulators · would clean ${ unavailable_before } , ${ unavailable_size_human } "
2026-02-07 11:17:07 +08:00
else
2026-03-05 12:00:07 +08:00
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Xcode unavailable simulators · already clean "
2026-02-07 11:17:07 +08:00
fi
2025-12-13 13:37:41 +08:00
else
2026-03-05 12:00:07 +08:00
# Skip if no unavailable simulators
if ( ( unavailable_before = = 0) ) ; then
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Xcode unavailable simulators · already clean "
note_activity
else
start_section_spinner "Checking unavailable simulators..."
2026-03-03 15:38:01 +08:00
2026-03-05 12:00:07 +08:00
# Capture error output for diagnostics
local delete_output
local delete_exit_code = 0
delete_output = $( xcrun simctl delete unavailable 2>& 1) || delete_exit_code = $?
2026-03-03 15:38:01 +08:00
2026-03-05 12:00:07 +08:00
if [ [ $delete_exit_code -eq 0 ] ] ; then
stop_section_spinner
unavailable_after = $( xcrun simctl list devices unavailable 2> /dev/null | command awk '/\(unavailable/ { count++ } END { print count+0 }' || echo "0" )
[ [ " $unavailable_after " = ~ ^[ 0-9] +$ ] ] || unavailable_after = 0
removed_unavailable = $(( unavailable_before - unavailable_after))
if ( ( removed_unavailable < 0) ) ; then
removed_unavailable = 0
2026-03-03 15:38:01 +08:00
fi
2026-03-05 12:00:07 +08:00
if ( ( removed_unavailable > 0) ) ; then
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Xcode unavailable simulators · removed ${ removed_unavailable } , ${ unavailable_size_human } "
2026-03-03 15:38:01 +08:00
else
2026-03-05 12:00:07 +08:00
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Xcode unavailable simulators · cleanup completed, ${ unavailable_size_human } "
2026-03-03 15:38:01 +08:00
fi
else
2026-03-05 12:00:07 +08:00
stop_section_spinner
# Analyze error and provide helpful message
local error_hint = ""
if echo " $delete_output " | grep -qi "permission denied" ; then
error_hint = " (permission denied)"
elif echo " $delete_output " | grep -qi "in use\|busy" ; then
error_hint = " (device in use)"
elif echo " $delete_output " | grep -qi "unable to boot\|failed to boot" ; then
error_hint = " (boot failure)"
elif echo " $delete_output " | grep -qi "service" ; then
error_hint = " (CoreSimulator service issue)"
fi
# Try fallback: manual deletion of unavailable device directories
if [ [ ${# unavailable_udids [@] } -gt 0 ] ] ; then
debug_log "Attempting fallback: manual deletion of unavailable simulators"
local manually_removed = 0
local manual_failed = 0
for udid in " ${ unavailable_udids [@] } " ; do
# Validate UUID format (36 chars: 8-4-4-4-12 hex pattern)
if [ [ ! " $udid " = ~ ^[ 0-9A-Fa-f] { 8} -[ 0-9A-Fa-f] { 4} -[ 0-9A-Fa-f] { 4} -[ 0-9A-Fa-f] { 4} -[ 0-9A-Fa-f] { 12} $ ] ] ; then
debug_log " Invalid UUID format, skipping: $udid "
( ( manual_failed++) ) || true
continue
fi
local device_path = " $HOME /Library/Developer/CoreSimulator/Devices/ $udid "
if [ [ -d " $device_path " ] ] ; then
# Use safe_remove for validated simulator device directory
if safe_remove " $device_path " true; then
( ( manually_removed++) ) || true
debug_log " Manually removed simulator: $udid "
else
( ( manual_failed++) ) || true
debug_log " Failed to manually remove simulator: $udid "
fi
fi
done
if ( ( manually_removed > 0) ) ; then
if ( ( manual_failed = = 0) ) ; then
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Xcode unavailable simulators · removed ${ manually_removed } (fallback), ${ unavailable_size_human } "
else
echo -e " ${ YELLOW } ${ ICON_WARNING } ${ NC } Xcode unavailable simulators · partially cleaned ${ manually_removed } / ${# unavailable_udids [@] } , ${ unavailable_size_human } "
fi
else
echo -e " ${ GRAY } ${ ICON_WARNING } ${ NC } Xcode unavailable simulators cleanup failed ${ error_hint } "
debug_log " simctl delete error: $delete_output "
fi
else
echo -e " ${ GRAY } ${ ICON_WARNING } ${ NC } Xcode unavailable simulators cleanup failed ${ error_hint } "
debug_log " simctl delete error: $delete_output "
fi
2026-03-03 15:38:01 +08:00
fi
fi
2026-03-05 12:00:07 +08:00
fi # Close if ((unavailable_before == 0))
note_activity
fi # End of simctl_available check
2025-12-05 23:58:55 +08:00
fi
2026-03-03 11:05:29 +03:00
# Old iOS/watchOS/tvOS DeviceSupport versions (debug symbols for connected devices).
# Each iOS version creates a 1-3 GB folder of debug symbols. Only the versions
# matching currently used devices are needed; older ones regenerate on device connect.
clean_xcode_device_support ~/Library/Developer/Xcode/iOS\ DeviceSupport "iOS DeviceSupport"
clean_xcode_device_support ~/Library/Developer/Xcode/watchOS\ DeviceSupport "watchOS DeviceSupport"
clean_xcode_device_support ~/Library/Developer/Xcode/tvOS\ DeviceSupport "tvOS DeviceSupport"
2025-12-31 16:23:31 +08:00
# Simulator runtime caches.
2025-12-06 20:28:49 +08:00
safe_clean ~/Library/Developer/CoreSimulator/Profiles/Runtimes/*/Contents/Resources/RuntimeRoot/System/Library/Caches/* "Simulator runtime cache"
2025-11-28 22:39:11 +09:00
safe_clean ~/Library/Caches/Google/AndroidStudio*/* "Android Studio cache"
2026-01-19 17:08:35 +08:00
# safe_clean ~/Library/Caches/CocoaPods/* "CocoaPods cache"
# safe_clean ~/.cache/flutter/* "Flutter cache"
2025-11-28 22:39:11 +09:00
safe_clean ~/.android/build-cache/* "Android build cache"
safe_clean ~/.android/cache/* "Android SDK cache"
safe_clean ~/Library/Developer/Xcode/UserData/IB\ Support/* "Xcode Interface Builder cache"
safe_clean ~/.cache/swift-package-manager/* "Swift package manager cache"
2026-02-16 15:45:21 +08:00
# Expo/React Native caches (preserve state.json which contains auth tokens).
safe_clean ~/.expo/expo-go/* "Expo Go cache"
safe_clean ~/.expo/android-apk-cache/* "Expo Android APK cache"
safe_clean ~/.expo/ios-simulator-app-cache/* "Expo iOS simulator app cache"
safe_clean ~/.expo/native-modules-cache/* "Expo native modules cache"
safe_clean ~/.expo/schema-cache/* "Expo schema cache"
safe_clean ~/.expo/template-cache/* "Expo template cache"
safe_clean ~/.expo/versions-cache/* "Expo versions cache"
2025-11-28 22:39:11 +09:00
}
2025-12-31 16:23:31 +08:00
# JVM ecosystem caches.
2026-02-16 15:53:32 +08:00
# Gradle: Respects whitelist, cleaned when not protected via: mo clean --whitelist
2025-11-28 22:39:11 +09:00
clean_dev_jvm( ) {
2026-02-16 15:53:32 +08:00
# Source Maven cleanup module (requires bash for BASH_SOURCE)
# shellcheck disable=SC1091
source " $( dirname " ${ BASH_SOURCE [0] } " ) /maven.sh " 2> /dev/null || true
2026-02-21 20:22:01 +08:00
if declare -f clean_maven_repository > /dev/null 2>& 1; then
clean_maven_repository
fi
2025-11-28 22:39:11 +09:00
safe_clean ~/.sbt/* "SBT cache"
safe_clean ~/.ivy2/cache/* "Ivy cache"
2026-02-16 15:53:32 +08:00
safe_clean ~/.gradle/caches/* "Gradle cache"
safe_clean ~/.gradle/daemon/* "Gradle daemon"
2025-11-28 22:39:11 +09:00
}
2026-01-23 17:37:37 +08:00
# JetBrains Toolbox old IDE versions (keep current + recent backup).
clean_dev_jetbrains_toolbox( ) {
local toolbox_root = " $HOME /Library/Application Support/JetBrains/Toolbox/apps "
[ [ -d " $toolbox_root " ] ] || return 0
local keep_previous = " ${ MOLE_JETBRAINS_TOOLBOX_KEEP :- 1 } "
2026-01-26 11:09:38 +08:00
[ [ " $keep_previous " = ~ ^[ 0-9] +$ ] ] || keep_previous = 1
2026-01-23 17:37:37 +08:00
2026-01-26 11:09:38 +08:00
# Save and filter whitelist patterns for toolbox path
2026-01-23 17:37:37 +08:00
local whitelist_overridden = "false"
local -a original_whitelist = ( )
if [ [ ${# WHITELIST_PATTERNS [@] } -gt 0 ] ] ; then
original_whitelist = ( " ${ WHITELIST_PATTERNS [@] } " )
local -a filtered_whitelist = ( )
local pattern
for pattern in " ${ WHITELIST_PATTERNS [@] } " ; do
2026-01-26 11:09:38 +08:00
[ [ " $toolbox_root " = = " $pattern " || " $pattern " = = " $toolbox_root " * ] ] && continue
2026-01-23 17:37:37 +08:00
filtered_whitelist += ( " $pattern " )
done
WHITELIST_PATTERNS = ( " ${ filtered_whitelist [@]+ ${ filtered_whitelist [@] } } " )
whitelist_overridden = "true"
fi
2026-01-26 11:09:38 +08:00
# Helper to restore whitelist on exit
_restore_whitelist( ) {
[ [ " $whitelist_overridden " = = "true" ] ] && WHITELIST_PATTERNS = ( " ${ original_whitelist [@] } " )
return 0
}
2026-01-23 17:37:37 +08:00
local -a product_dirs = ( )
while IFS = read -r -d '' product_dir; do
product_dirs += ( " $product_dir " )
2026-02-07 11:21:37 +08:00
done < <( command find " $toolbox_root " -mindepth 1 -maxdepth 1 -type d -print0 2> /dev/null)
2026-01-23 17:37:37 +08:00
2026-01-26 02:47:08 +00:00
if [ [ ${# product_dirs [@] } -eq 0 ] ] ; then
2026-01-26 11:09:38 +08:00
_restore_whitelist
2026-01-26 02:47:08 +00:00
return 0
fi
2026-01-23 17:37:37 +08:00
local product_dir
for product_dir in " ${ product_dirs [@] } " ; do
while IFS = read -r -d '' channel_dir; do
local current_link = ""
local current_real = ""
if [ [ -L " $channel_dir /current " ] ] ; then
2026-02-07 11:21:37 +08:00
current_link = $( readlink " $channel_dir /current " 2> /dev/null || true )
2026-01-23 17:37:37 +08:00
if [ [ -n " $current_link " ] ] ; then
if [ [ " $current_link " = = /* ] ] ; then
current_real = " $current_link "
else
current_real = " $channel_dir / $current_link "
fi
fi
elif [ [ -d " $channel_dir /current " ] ] ; then
current_real = " $channel_dir /current "
fi
local -a version_dirs = ( )
while IFS = read -r -d '' version_dir; do
local name
name = $( basename " $version_dir " )
[ [ " $name " = = "current" ] ] && continue
[ [ " $name " = = .* ] ] && continue
[ [ " $name " = = "plugins" || " $name " = = "plugins-lib" || " $name " = = "plugins-libs" ] ] && continue
[ [ -n " $current_real " && " $version_dir " = = " $current_real " ] ] && continue
[ [ ! " $name " = ~ ^[ 0-9] ] ] && continue
version_dirs += ( " $version_dir " )
2026-02-07 11:21:37 +08:00
done < <( command find " $channel_dir " -mindepth 1 -maxdepth 1 -type d -print0 2> /dev/null)
2026-01-23 17:37:37 +08:00
2026-01-26 02:47:08 +00:00
[ [ ${# version_dirs [@] } -eq 0 ] ] && continue
2026-01-23 17:37:37 +08:00
local -a sorted_dirs = ( )
while IFS = read -r line; do
local dir_path = " ${ line #* } "
sorted_dirs += ( " $dir_path " )
done < <(
for version_dir in " ${ version_dirs [@] } " ; do
local mtime
2026-02-07 11:21:37 +08:00
mtime = $( stat -f%m " $version_dir " 2> /dev/null || echo "0" )
2026-01-23 17:37:37 +08:00
printf '%s %s\n' " $mtime " " $version_dir "
done | sort -rn
)
if [ [ ${# sorted_dirs [@] } -le " $keep_previous " ] ] ; then
continue
fi
local idx = 0
local dir_path
for dir_path in " ${ sorted_dirs [@] } " ; do
if [ [ $idx -lt $keep_previous ] ] ; then
2026-02-28 11:10:18 +08:00
idx = $(( idx + 1 ))
2026-01-23 17:37:37 +08:00
continue
fi
safe_clean " $dir_path " "JetBrains Toolbox old IDE version"
note_activity
2026-02-28 11:10:18 +08:00
idx = $(( idx + 1 ))
2026-01-23 17:37:37 +08:00
done
2026-02-07 11:21:37 +08:00
done < <( command find " $product_dir " -mindepth 1 -maxdepth 1 -type d -name "ch-*" -print0 2> /dev/null)
2026-01-23 17:37:37 +08:00
done
2026-01-26 11:09:38 +08:00
_restore_whitelist
2026-01-23 17:37:37 +08:00
}
2025-12-31 16:23:31 +08:00
# Other language tool caches.
2025-11-28 22:39:11 +09:00
clean_dev_other_langs( ) {
safe_clean ~/.bundle/cache/* "Ruby Bundler cache"
safe_clean ~/.composer/cache/* "PHP Composer cache"
safe_clean ~/.nuget/packages/* "NuGet packages cache"
2026-01-19 17:08:35 +08:00
# safe_clean ~/.pub-cache/* "Dart Pub cache"
2025-11-28 22:39:11 +09:00
safe_clean ~/.cache/bazel/* "Bazel cache"
safe_clean ~/.cache/zig/* "Zig cache"
safe_clean ~/Library/Caches/deno/* "Deno cache"
}
2025-12-31 16:23:31 +08:00
# CI/CD and DevOps caches.
2025-11-28 22:39:11 +09:00
clean_dev_cicd( ) {
safe_clean ~/.cache/terraform/* "Terraform cache"
safe_clean ~/.grafana/cache/* "Grafana cache"
safe_clean ~/.prometheus/data/wal/* "Prometheus WAL cache"
safe_clean ~/.jenkins/workspace/*/target/* "Jenkins workspace cache"
safe_clean ~/.cache/gitlab-runner/* "GitLab Runner cache"
safe_clean ~/.github/cache/* "GitHub Actions cache"
safe_clean ~/.circleci/cache/* "CircleCI cache"
safe_clean ~/.sonar/* "SonarQube cache"
}
2025-12-31 16:23:31 +08:00
# Database tool caches.
2025-11-28 22:39:11 +09:00
clean_dev_database( ) {
safe_clean ~/Library/Caches/com.sequel-ace.sequel-ace/* "Sequel Ace cache"
safe_clean ~/Library/Caches/com.eggerapps.Sequel-Pro/* "Sequel Pro cache"
safe_clean ~/Library/Caches/redis-desktop-manager/* "Redis Desktop Manager cache"
safe_clean ~/Library/Caches/com.navicat.* "Navicat cache"
safe_clean ~/Library/Caches/com.dbeaver.* "DBeaver cache"
safe_clean ~/Library/Caches/com.redis.RedisInsight "Redis Insight cache"
}
2025-12-31 16:23:31 +08:00
# API/debugging tool caches.
2025-11-28 22:39:11 +09:00
clean_dev_api_tools( ) {
safe_clean ~/Library/Caches/com.postmanlabs.mac/* "Postman cache"
safe_clean ~/Library/Caches/com.konghq.insomnia/* "Insomnia cache"
safe_clean ~/Library/Caches/com.tinyapp.TablePlus/* "TablePlus cache"
safe_clean ~/Library/Caches/com.getpaw.Paw/* "Paw API cache"
safe_clean ~/Library/Caches/com.charlesproxy.charles/* "Charles Proxy cache"
safe_clean ~/Library/Caches/com.proxyman.NSProxy/* "Proxyman cache"
}
2025-12-31 16:23:31 +08:00
# Misc dev tool caches.
2025-11-28 22:39:11 +09:00
clean_dev_misc( ) {
safe_clean ~/Library/Caches/com.unity3d.*/* "Unity cache"
safe_clean ~/Library/Caches/com.mongodb.compass/* "MongoDB Compass cache"
safe_clean ~/Library/Caches/com.figma.Desktop/* "Figma cache"
safe_clean ~/Library/Caches/com.github.GitHubDesktop/* "GitHub Desktop cache"
safe_clean ~/Library/Caches/SentryCrash/* "Sentry crash reports"
safe_clean ~/Library/Caches/KSCrash/* "KSCrash reports"
safe_clean ~/Library/Caches/com.crashlytics.data/* "Crashlytics data"
2026-01-13 19:04:54 +08:00
safe_clean ~/Library/Application\ Support/Antigravity/Cache/* "Antigravity cache"
safe_clean ~/Library/Application\ Support/Antigravity/Code\ Cache/* "Antigravity code cache"
safe_clean ~/Library/Application\ Support/Antigravity/GPUCache/* "Antigravity GPU cache"
safe_clean ~/Library/Application\ Support/Antigravity/DawnGraphiteCache/* "Antigravity Dawn cache"
safe_clean ~/Library/Application\ Support/Antigravity/DawnWebGPUCache/* "Antigravity WebGPU cache"
# Filo (Electron)
safe_clean ~/Library/Application\ Support/Filo/production/Cache/* "Filo cache"
safe_clean ~/Library/Application\ Support/Filo/production/Code\ Cache/* "Filo code cache"
safe_clean ~/Library/Application\ Support/Filo/production/GPUCache/* "Filo GPU cache"
safe_clean ~/Library/Application\ Support/Filo/production/DawnGraphiteCache/* "Filo Dawn cache"
safe_clean ~/Library/Application\ Support/Filo/production/DawnWebGPUCache/* "Filo WebGPU cache"
# Claude (Electron)
safe_clean ~/Library/Application\ Support/Claude/Cache/* "Claude cache"
safe_clean ~/Library/Application\ Support/Claude/Code\ Cache/* "Claude code cache"
safe_clean ~/Library/Application\ Support/Claude/GPUCache/* "Claude GPU cache"
safe_clean ~/Library/Application\ Support/Claude/DawnGraphiteCache/* "Claude Dawn cache"
safe_clean ~/Library/Application\ Support/Claude/DawnWebGPUCache/* "Claude WebGPU cache"
2025-11-28 22:39:11 +09:00
}
2025-12-31 16:23:31 +08:00
# Shell and VCS leftovers.
2025-11-28 22:39:11 +09:00
clean_dev_shell( ) {
safe_clean ~/.gitconfig.lock "Git config lock"
safe_clean ~/.gitconfig.bak* "Git config backup"
safe_clean ~/.oh-my-zsh/cache/* "Oh My Zsh cache"
safe_clean ~/.config/fish/fish_history.bak* "Fish shell backup"
safe_clean ~/.bash_history.bak* "Bash history backup"
safe_clean ~/.zsh_history.bak* "Zsh history backup"
safe_clean ~/.cache/pre-commit/* "pre-commit cache"
}
2025-12-31 16:23:31 +08:00
# Network tool caches.
2025-11-28 22:39:11 +09:00
clean_dev_network( ) {
safe_clean ~/.cache/curl/* "curl cache"
safe_clean ~/.cache/wget/* "wget cache"
2025-12-27 10:17:05 +08:00
safe_clean ~/Library/Caches/curl/* "macOS curl cache"
safe_clean ~/Library/Caches/wget/* "macOS wget cache"
2025-11-28 22:39:11 +09:00
}
2025-12-31 16:23:31 +08:00
# Orphaned SQLite temp files (-shm/-wal). Disabled due to low ROI.
2025-12-28 19:37:42 +08:00
clean_sqlite_temp_files( ) {
return 0
}
2026-01-10 01:22:43 +05:30
# Elixir/Erlang ecosystem.
2026-01-12 10:07:42 +08:00
# Note: ~/.mix/archives contains installed Mix tools - excluded from cleanup
2026-01-10 01:22:43 +05:30
clean_dev_elixir( ) {
2026-01-11 09:55:18 +08:00
safe_clean ~/.hex/cache/* "Hex cache"
2026-01-10 01:22:43 +05:30
}
# Haskell ecosystem.
2026-01-12 10:07:42 +08:00
# Note: ~/.stack/programs contains Stack-installed GHC compilers - excluded from cleanup
2026-01-10 01:22:43 +05:30
clean_dev_haskell( ) {
2026-01-11 09:55:18 +08:00
safe_clean ~/.cabal/packages/* "Cabal install cache"
2026-01-10 01:22:43 +05:30
}
# OCaml ecosystem.
clean_dev_ocaml( ) {
2026-01-11 09:55:18 +08:00
safe_clean ~/.opam/download-cache/* "Opam cache"
2026-01-10 01:22:43 +05:30
}
# Editor caches.
2026-01-12 10:07:42 +08:00
# Note: ~/Library/Application Support/Code/User/workspaceStorage contains workspace settings - excluded from cleanup
2026-01-10 01:22:43 +05:30
clean_dev_editors( ) {
2026-01-11 09:55:18 +08:00
safe_clean ~/Library/Caches/com.microsoft.VSCode/Cache/* "VS Code cached data"
safe_clean ~/Library/Application\ Support/Code/CachedData/* "VS Code cached data"
2026-01-13 19:04:54 +08:00
safe_clean ~/Library/Application\ Support/Code/DawnGraphiteCache/* "VS Code Dawn cache"
safe_clean ~/Library/Application\ Support/Code/DawnWebGPUCache/* "VS Code WebGPU cache"
safe_clean ~/Library/Application\ Support/Code/GPUCache/* "VS Code GPU cache"
safe_clean ~/Library/Application\ Support/Code/CachedExtensionVSIXs/* "VS Code extension cache"
2026-01-11 09:55:18 +08:00
safe_clean ~/Library/Caches/Zed/* "Zed cache"
2026-01-10 01:22:43 +05:30
}
2025-12-31 16:23:31 +08:00
# Main developer tools cleanup sequence.
2025-11-28 22:39:11 +09:00
clean_developer_tools( ) {
2025-12-27 10:17:05 +08:00
stop_section_spinner
2026-02-21 23:36:05 +08:00
# CLI tools and languages
2025-12-28 19:37:42 +08:00
clean_sqlite_temp_files
2025-11-28 22:39:11 +09:00
clean_dev_npm
clean_dev_python
clean_dev_go
clean_dev_rust
2026-01-13 15:59:43 +08:00
check_rust_toolchains
2025-11-28 22:39:11 +09:00
clean_dev_docker
clean_dev_cloud
clean_dev_nix
clean_dev_shell
clean_dev_frontend
clean_project_caches
clean_dev_mobile
clean_dev_jvm
2026-01-23 17:37:37 +08:00
clean_dev_jetbrains_toolbox
2025-11-28 22:39:11 +09:00
clean_dev_other_langs
clean_dev_cicd
clean_dev_database
clean_dev_api_tools
clean_dev_network
clean_dev_misc
2026-01-10 01:22:43 +05:30
clean_dev_elixir
clean_dev_haskell
clean_dev_ocaml
2026-02-21 23:36:05 +08:00
# GUI developer applications
clean_xcode_tools
clean_code_editors
# Homebrew
2025-11-28 22:39:11 +09:00
safe_clean ~/Library/Caches/Homebrew/* "Homebrew cache"
2025-12-13 13:37:41 +08:00
local brew_lock_dirs = (
"/opt/homebrew/var/homebrew/locks"
"/usr/local/var/homebrew/locks"
)
for lock_dir in " ${ brew_lock_dirs [@] } " ; do
if [ [ -d " $lock_dir " && -w " $lock_dir " ] ] ; then
safe_clean " $lock_dir " /* "Homebrew lock files"
elif [ [ -d " $lock_dir " ] ] ; then
2026-02-07 11:21:37 +08:00
if find " $lock_dir " -mindepth 1 -maxdepth 1 -print -quit 2> /dev/null | grep -q .; then
2025-12-13 05:38:36 +00:00
debug_log " Skipping read-only Homebrew locks in $lock_dir "
2025-12-13 13:37:41 +08:00
fi
fi
done
2025-11-28 22:39:11 +09:00
clean_homebrew
}