merge: sync upstream mole/main
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
# Application Data Cleanup Module
|
||||
set -euo pipefail
|
||||
|
||||
readonly ORPHAN_AGE_THRESHOLD=${ORPHAN_AGE_THRESHOLD:-${MOLE_ORPHAN_AGE_DAYS:-60}}
|
||||
readonly ORPHAN_AGE_THRESHOLD=${ORPHAN_AGE_THRESHOLD:-${MOLE_ORPHAN_AGE_DAYS:-30}}
|
||||
readonly CLAUDE_VM_ORPHAN_AGE_THRESHOLD=${MOLE_CLAUDE_VM_ORPHAN_AGE_DAYS:-7}
|
||||
# Args: $1=target_dir, $2=label
|
||||
clean_ds_store_tree() {
|
||||
local target="$1"
|
||||
@@ -59,7 +60,7 @@ clean_ds_store_tree() {
|
||||
note_activity
|
||||
fi
|
||||
}
|
||||
# Orphaned app data (60+ days inactive). Env: ORPHAN_AGE_THRESHOLD, DRY_RUN
|
||||
# Orphaned app data (30+ days inactive). Env: ORPHAN_AGE_THRESHOLD, DRY_RUN
|
||||
# Usage: scan_installed_apps "output_file"
|
||||
scan_installed_apps() {
|
||||
local installed_bundles="$1"
|
||||
@@ -201,13 +202,13 @@ is_bundle_orphaned() {
|
||||
;;
|
||||
esac
|
||||
|
||||
# 5. Fast path: 60-day modification check (stat call, fast)
|
||||
# 5. Fast path: 30-day modification check (stat call, fast)
|
||||
if [[ -e "$directory_path" ]]; then
|
||||
local last_modified_epoch=$(get_file_mtime "$directory_path")
|
||||
local current_epoch
|
||||
current_epoch=$(get_epoch_seconds)
|
||||
local days_since_modified=$(((current_epoch - last_modified_epoch) / 86400))
|
||||
if [[ $days_since_modified -lt ${ORPHAN_AGE_THRESHOLD:-60} ]]; then
|
||||
if [[ $days_since_modified -lt ${ORPHAN_AGE_THRESHOLD:-30} ]]; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
@@ -261,6 +262,17 @@ is_claude_vm_bundle_orphaned() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -e "$vm_bundle_path" ]]; then
|
||||
local last_modified_epoch
|
||||
last_modified_epoch=$(get_file_mtime "$vm_bundle_path")
|
||||
local current_epoch
|
||||
current_epoch=$(get_epoch_seconds)
|
||||
local days_since_modified=$(((current_epoch - last_modified_epoch) / 86400))
|
||||
if [[ $days_since_modified -lt ${CLAUDE_VM_ORPHAN_AGE_THRESHOLD:-7} ]]; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$ORPHAN_MDFIND_CACHE_FILE" ]]; then
|
||||
ORPHAN_MDFIND_CACHE_FILE=$(mktemp "${TMPDIR:-/tmp}/mole_mdfind_cache.XXXXXX")
|
||||
register_temp_file "$ORPHAN_MDFIND_CACHE_FILE"
|
||||
|
||||
@@ -212,7 +212,9 @@ clean_project_caches() {
|
||||
[[ -d "$cache_dir/cache" ]] && safe_clean "$cache_dir/cache"/* "Next.js build cache" || true
|
||||
;;
|
||||
"__pycache__")
|
||||
[[ -d "$cache_dir" ]] && safe_clean "$cache_dir"/* "Python bytecode cache" || true
|
||||
# Remove the cache directory itself so we avoid expanding every
|
||||
# .pyc file into a separate safe_clean target.
|
||||
[[ -d "$cache_dir" ]] && safe_clean "$cache_dir" "Python bytecode cache" || true
|
||||
;;
|
||||
".dart_tool")
|
||||
if [[ -d "$cache_dir" ]]; then
|
||||
|
||||
@@ -198,13 +198,18 @@ clean_dev_docker() {
|
||||
fi
|
||||
stop_section_spinner
|
||||
if [[ "$docker_running" == "true" ]]; then
|
||||
clean_tool_cache "Docker build cache" docker builder prune -af
|
||||
# Remove unused images, stopped containers, unused networks, and
|
||||
# anonymous volumes in one pass. This maps better to the large
|
||||
# reclaimable "docker system df" buckets users typically see.
|
||||
clean_tool_cache "Docker unused data" docker system prune -af --volumes
|
||||
else
|
||||
echo -e " ${GRAY}${ICON_WARNING}${NC} Docker unused data · skipped (daemon not running)"
|
||||
note_activity
|
||||
debug_log "Docker daemon not running, skipping Docker cache cleanup"
|
||||
fi
|
||||
else
|
||||
note_activity
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Docker build cache · would clean"
|
||||
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Docker unused data · would clean"
|
||||
fi
|
||||
fi
|
||||
safe_clean ~/.docker/buildx/cache/* "Docker BuildX cache"
|
||||
@@ -359,47 +364,49 @@ clean_xcode_device_support() {
|
||||
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
|
||||
printf '%s %s\n' "$(stat -f%m "$entry" 2> /dev/null || echo 0)" "$entry"
|
||||
done | sort -rn
|
||||
)
|
||||
if [[ ${#version_dirs[@]} -gt 0 ]]; then
|
||||
# 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
|
||||
printf '%s %s\n' "$(stat -f%m "$entry" 2> /dev/null || echo 0)" "$entry"
|
||||
done | sort -rn
|
||||
)
|
||||
|
||||
# Get stale versions (everything after keep_count)
|
||||
local -a stale_dirs=("${sorted_dirs[@]:$keep_count}")
|
||||
# Get stale versions (everything after keep_count)
|
||||
local -a stale_dirs=("${sorted_dirs[@]:$keep_count}")
|
||||
|
||||
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))")
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
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))")
|
||||
|
||||
if [[ $removed_count -gt 0 ]]; then
|
||||
echo -e " ${GREEN}${ICON_SUCCESS}${NC} ${display_name} · removed ${removed_count} old versions, ${stale_size_human}"
|
||||
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
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1310,6 +1310,14 @@ clean_project_artifacts() {
|
||||
if [[ -t 1 ]]; then
|
||||
stop_inline_spinner
|
||||
fi
|
||||
# Exit early if no artifacts were found to avoid unbound variable errors
|
||||
# when expanding empty arrays with set -u active.
|
||||
if [[ ${#menu_options[@]} -eq 0 ]]; then
|
||||
echo ""
|
||||
echo -e "${GRAY}No artifacts found to purge${NC}"
|
||||
printf '\n'
|
||||
return 0
|
||||
fi
|
||||
# Set global vars for selector
|
||||
export PURGE_CATEGORY_SIZES=$(
|
||||
IFS=,
|
||||
|
||||
@@ -752,6 +752,23 @@ clean_virtualization_tools() {
|
||||
|
||||
# Estimate item size for Application Support cleanup.
|
||||
# Files use stat; directories use du with timeout to avoid long blocking scans.
|
||||
app_support_entry_count_capped() {
|
||||
local dir="$1"
|
||||
local maxdepth="${2:-1}"
|
||||
local cap="${3:-101}"
|
||||
local count=0
|
||||
|
||||
while IFS= read -r -d '' _entry; do
|
||||
count=$((count + 1))
|
||||
if ((count >= cap)); then
|
||||
break
|
||||
fi
|
||||
done < <(command find "$dir" -mindepth 1 -maxdepth "$maxdepth" -print0 2> /dev/null)
|
||||
|
||||
[[ "$count" =~ ^[0-9]+$ ]] || count=0
|
||||
printf '%s\n' "$count"
|
||||
}
|
||||
|
||||
app_support_item_size_bytes() {
|
||||
local item="$1"
|
||||
local timeout_seconds="${2:-0.4}"
|
||||
@@ -768,7 +785,7 @@ app_support_item_size_bytes() {
|
||||
# Fast path: if directory has too many items, skip detailed size calculation
|
||||
# to avoid hanging on deep directories (e.g., node_modules, .git)
|
||||
local item_count
|
||||
item_count=$(command find "$item" -maxdepth 2 -print0 2> /dev/null | tr -d '\0' | wc -c)
|
||||
item_count=$(app_support_entry_count_capped "$item" 2 10001)
|
||||
if [[ "$item_count" -gt 10000 ]]; then
|
||||
# Return 1 to signal "too many items, size unknown"
|
||||
return 1
|
||||
@@ -859,7 +876,7 @@ clean_application_support_logs() {
|
||||
if [[ -d "$candidate" ]]; then
|
||||
# Quick count check - skip if too many items to avoid hanging
|
||||
local quick_count
|
||||
quick_count=$(command find "$candidate" -mindepth 1 -maxdepth 1 -printf '1\n' 2> /dev/null | wc -l | tr -d ' ')
|
||||
quick_count=$(app_support_entry_count_capped "$candidate" 1 101)
|
||||
if [[ "$quick_count" -gt 100 ]]; then
|
||||
# Too many items - use bulk removal instead of item-by-item
|
||||
local app_label="$app_name"
|
||||
@@ -935,7 +952,7 @@ clean_application_support_logs() {
|
||||
if [[ -d "$candidate" ]]; then
|
||||
# Quick count check - skip if too many items
|
||||
local quick_count
|
||||
quick_count=$(command find "$candidate" -mindepth 1 -maxdepth 1 -printf '1\n' 2> /dev/null | wc -l | tr -d ' ')
|
||||
quick_count=$(app_support_entry_count_capped "$candidate" 1 101)
|
||||
if [[ "$quick_count" -gt 100 ]]; then
|
||||
local container_label="$container"
|
||||
if [[ ${#container_label} -gt 24 ]]; then
|
||||
|
||||
@@ -68,7 +68,7 @@ get_lsregister_path() {
|
||||
# Global Configuration Constants
|
||||
# ============================================================================
|
||||
readonly MOLE_TEMP_FILE_AGE_DAYS=7 # Temp file retention (days)
|
||||
readonly MOLE_ORPHAN_AGE_DAYS=60 # Orphaned data retention (days)
|
||||
readonly MOLE_ORPHAN_AGE_DAYS=30 # Orphaned data retention (days)
|
||||
readonly MOLE_MAX_PARALLEL_JOBS=15 # Parallel job limit
|
||||
readonly MOLE_MAIL_DOWNLOADS_MIN_KB=5120 # Mail attachment size threshold
|
||||
readonly MOLE_MAIL_AGE_DAYS=30 # Mail attachment retention (days)
|
||||
@@ -191,11 +191,17 @@ is_sip_enabled() {
|
||||
# Detect CPU architecture
|
||||
# Returns: "Apple Silicon" or "Intel"
|
||||
detect_architecture() {
|
||||
if [[ "$(uname -m)" == "arm64" ]]; then
|
||||
echo "Apple Silicon"
|
||||
else
|
||||
echo "Intel"
|
||||
if [[ -n "${MOLE_ARCH_CACHE:-}" ]]; then
|
||||
echo "$MOLE_ARCH_CACHE"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$(uname -m)" == "arm64" ]]; then
|
||||
export MOLE_ARCH_CACHE="Apple Silicon"
|
||||
else
|
||||
export MOLE_ARCH_CACHE="Intel"
|
||||
fi
|
||||
echo "$MOLE_ARCH_CACHE"
|
||||
}
|
||||
|
||||
# Get free disk space on root volume
|
||||
@@ -212,6 +218,11 @@ get_free_space() {
|
||||
# Get Darwin kernel major version (e.g., 24 for 24.2.0)
|
||||
# Returns 999 on failure to adopt conservative behavior (assume modern system)
|
||||
get_darwin_major() {
|
||||
if [[ -n "${MOLE_DARWIN_MAJOR_CACHE:-}" ]]; then
|
||||
echo "$MOLE_DARWIN_MAJOR_CACHE"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local kernel
|
||||
kernel=$(uname -r 2> /dev/null || true)
|
||||
local major="${kernel%%.*}"
|
||||
@@ -219,6 +230,7 @@ get_darwin_major() {
|
||||
# Return high number to skip potentially dangerous operations on unknown systems
|
||||
major=999
|
||||
fi
|
||||
export MOLE_DARWIN_MAJOR_CACHE="$major"
|
||||
echo "$major"
|
||||
}
|
||||
|
||||
@@ -233,8 +245,10 @@ is_darwin_ge() {
|
||||
# Get optimal parallel jobs for operation type (scan|io|compute|default)
|
||||
get_optimal_parallel_jobs() {
|
||||
local operation_type="${1:-default}"
|
||||
local cpu_cores
|
||||
cpu_cores=$(sysctl -n hw.ncpu 2> /dev/null || echo 4)
|
||||
if [[ -z "${MOLE_CPU_CORES_CACHE:-}" ]]; then
|
||||
export MOLE_CPU_CORES_CACHE=$(sysctl -n hw.ncpu 2> /dev/null || echo 4)
|
||||
fi
|
||||
local cpu_cores="$MOLE_CPU_CORES_CACHE"
|
||||
case "$operation_type" in
|
||||
scan | io)
|
||||
echo $((cpu_cores * 2))
|
||||
@@ -318,7 +332,7 @@ get_user_home() {
|
||||
fi
|
||||
|
||||
if [[ -z "$home" ]]; then
|
||||
home=$(eval echo "~$user" 2> /dev/null || true)
|
||||
home=$(id -P "$user" 2> /dev/null | cut -d: -f9 || true)
|
||||
fi
|
||||
|
||||
if [[ "$home" == "~"* ]]; then
|
||||
@@ -586,7 +600,7 @@ mktemp_file() {
|
||||
|
||||
# Cleanup all tracked temp files and directories
|
||||
cleanup_temp_files() {
|
||||
stop_inline_spinner 2> /dev/null || true
|
||||
stop_inline_spinner || true
|
||||
local file
|
||||
if [[ ${#MOLE_TEMP_FILES[@]} -gt 0 ]]; then
|
||||
for file in "${MOLE_TEMP_FILES[@]}"; do
|
||||
@@ -641,7 +655,7 @@ note_activity() {
|
||||
# Usage: start_section_spinner "message"
|
||||
start_section_spinner() {
|
||||
local message="${1:-Scanning...}"
|
||||
stop_inline_spinner 2> /dev/null || true
|
||||
stop_inline_spinner || true
|
||||
if [[ -t 1 ]]; then
|
||||
MOLE_SPINNER_PREFIX=" " start_inline_spinner "$message"
|
||||
fi
|
||||
@@ -651,7 +665,7 @@ start_section_spinner() {
|
||||
# Usage: stop_section_spinner
|
||||
stop_section_spinner() {
|
||||
# Always try to stop spinner (function handles empty PID gracefully)
|
||||
stop_inline_spinner 2> /dev/null || true
|
||||
stop_inline_spinner || true
|
||||
# Always clear line to handle edge cases where spinner output remains
|
||||
# (e.g., spinner was stopped elsewhere but line not cleared)
|
||||
if [[ -t 1 ]]; then
|
||||
@@ -732,18 +746,30 @@ update_progress_if_needed() {
|
||||
# Usage: is_ansi_supported
|
||||
# Returns: 0 if supported, 1 if not
|
||||
is_ansi_supported() {
|
||||
if [[ -n "${MOLE_ANSI_SUPPORTED_CACHE:-}" ]]; then
|
||||
return "$MOLE_ANSI_SUPPORTED_CACHE"
|
||||
fi
|
||||
|
||||
# Check if running in interactive terminal
|
||||
[[ -t 1 ]] || return 1
|
||||
if ! [[ -t 1 ]]; then
|
||||
export MOLE_ANSI_SUPPORTED_CACHE=1
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check TERM variable
|
||||
[[ -n "${TERM:-}" ]] || return 1
|
||||
if [[ -z "${TERM:-}" ]]; then
|
||||
export MOLE_ANSI_SUPPORTED_CACHE=1
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check for known ANSI-compatible terminals
|
||||
case "$TERM" in
|
||||
xterm* | vt100 | vt220 | screen* | tmux* | ansi | linux | rxvt* | konsole*)
|
||||
export MOLE_ANSI_SUPPORTED_CACHE=0
|
||||
return 0
|
||||
;;
|
||||
dumb | unknown)
|
||||
export MOLE_ANSI_SUPPORTED_CACHE=1
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
@@ -751,8 +777,12 @@ is_ansi_supported() {
|
||||
if command -v tput > /dev/null 2>&1; then
|
||||
# Test if terminal supports colors (good proxy for ANSI support)
|
||||
local colors=$(tput colors 2> /dev/null || echo "0")
|
||||
[[ "$colors" -ge 8 ]] && return 0
|
||||
if [[ "$colors" -ge 8 ]]; then
|
||||
export MOLE_ANSI_SUPPORTED_CACHE=0
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
export MOLE_ANSI_SUPPORTED_CACHE=1
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -92,7 +92,10 @@ validate_path_for_deletion() {
|
||||
# Validate resolved target against protected paths
|
||||
if [[ -n "$resolved_target" ]]; then
|
||||
case "$resolved_target" in
|
||||
/System/* | /usr/bin/* | /usr/lib/* | /bin/* | /sbin/* | /private/etc/*)
|
||||
/ | /System | /System/* | /bin | /bin/* | /sbin | /sbin/* | \
|
||||
/usr | /usr/bin | /usr/bin/* | /usr/lib | /usr/lib/* | \
|
||||
/etc | /etc/* | /private/etc | /private/etc/* | \
|
||||
/Library/Extensions | /Library/Extensions/*)
|
||||
log_error "Symlink points to protected system path: $path -> $resolved_target"
|
||||
return 1
|
||||
;;
|
||||
|
||||
@@ -42,9 +42,9 @@ if [[ -z "${MO_TIMEOUT_INITIALIZED:-}" ]]; then
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "$MO_TIMEOUT_BIN" ]] && command -v perl > /dev/null 2>&1; then
|
||||
if command -v perl > /dev/null 2>&1; then
|
||||
MO_TIMEOUT_PERL_BIN="$(command -v perl)"
|
||||
if [[ "${MO_DEBUG:-0}" == "1" ]]; then
|
||||
if [[ -z "$MO_TIMEOUT_BIN" ]] && [[ "${MO_DEBUG:-0}" == "1" ]]; then
|
||||
echo "[TIMEOUT] Using perl fallback: $MO_TIMEOUT_PERL_BIN" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user