1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 20:19:45 +00:00

Merge branch 'dev'

This commit is contained in:
Tw93
2026-01-12 17:50:07 +08:00
13 changed files with 353 additions and 134 deletions

View File

@@ -45,7 +45,7 @@ readonly PURGE_TARGETS=(
readonly MIN_AGE_DAYS=7
# Scan depth defaults (relative to search root).
readonly PURGE_MIN_DEPTH_DEFAULT=2
readonly PURGE_MAX_DEPTH_DEFAULT=8
readonly PURGE_MAX_DEPTH_DEFAULT=4
# Search paths (default, can be overridden via config file).
readonly DEFAULT_PURGE_SEARCH_PATHS=(
"$HOME/www"
@@ -339,6 +339,11 @@ scan_purge_targets() {
if [[ ! -d "$search_path" ]]; then
return
fi
# Update current scanning path
local stats_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mole"
echo "$search_path" > "$stats_dir/purge_scanning" 2> /dev/null || true
if command -v fd > /dev/null 2>&1; then
# Escape regex special characters in target names for fd patterns
local escaped_targets=()
@@ -356,28 +361,39 @@ scan_purge_targets() {
"--type" "d"
"--min-depth" "$min_depth"
"--max-depth" "$max_depth"
"--threads" "4"
"--threads" "8"
"--exclude" ".git"
"--exclude" "Library"
"--exclude" ".Trash"
"--exclude" "Applications"
)
fd "${fd_args[@]}" "$pattern" "$search_path" 2> /dev/null | while IFS= read -r item; do
if is_safe_project_artifact "$item" "$search_path"; then
echo "$item"
fi
done | filter_nested_artifacts | filter_protected_artifacts > "$output_file"
# Write to temp file first, then filter - more efficient than piping
fd "${fd_args[@]}" "$pattern" "$search_path" 2> /dev/null > "$output_file.raw" || true
# Single pass: safe + nested + protected
if [[ -f "$output_file.raw" ]]; then
while IFS= read -r item; do
# Check if we should abort (scanning file removed by Ctrl+C)
if [[ ! -f "$stats_dir/purge_scanning" ]]; then
rm -f "$output_file.raw"
return
fi
if [[ -n "$item" ]] && is_safe_project_artifact "$item" "$search_path"; then
echo "$item"
# Update scanning path to show current project directory
local project_dir=$(dirname "$item")
echo "$project_dir" > "$stats_dir/purge_scanning" 2> /dev/null || true
fi
done < "$output_file.raw" | filter_nested_artifacts | filter_protected_artifacts > "$output_file"
rm -f "$output_file.raw"
else
touch "$output_file"
fi
else
# Pruned find avoids descending into heavy directories.
local prune_args=()
local prune_dirs=(".git" "Library" ".Trash" "Applications")
for dir in "${prune_dirs[@]}"; do
prune_args+=("-name" "$dir" "-prune" "-o")
done
for target in "${PURGE_TARGETS[@]}"; do
prune_args+=("-name" "$target" "-print" "-prune" "-o")
done
local find_expr=()
local prune_dirs=(".git" "Library" ".Trash" "Applications")
for dir in "${prune_dirs[@]}"; do
find_expr+=("-name" "$dir" "-prune" "-o")
done
@@ -390,28 +406,49 @@ scan_purge_targets() {
((i++))
done
command find "$search_path" -mindepth "$min_depth" -maxdepth "$max_depth" -type d \
\( "${find_expr[@]}" \) 2> /dev/null | while IFS= read -r item; do
if is_safe_project_artifact "$item" "$search_path"; then
echo "$item"
fi
done | filter_nested_artifacts | filter_protected_artifacts > "$output_file"
\( "${find_expr[@]}" \) 2> /dev/null > "$output_file.raw" || true
# Single pass: safe + nested + protected
if [[ -f "$output_file.raw" ]]; then
while IFS= read -r item; do
# Check if we should abort (scanning file removed by Ctrl+C)
if [[ ! -f "$stats_dir/purge_scanning" ]]; then
rm -f "$output_file.raw"
return
fi
if [[ -n "$item" ]] && is_safe_project_artifact "$item" "$search_path"; then
echo "$item"
# Update scanning path to show current project directory
local project_dir=$(dirname "$item")
echo "$project_dir" > "$stats_dir/purge_scanning" 2> /dev/null || true
fi
done < "$output_file.raw" | filter_nested_artifacts | filter_protected_artifacts > "$output_file"
rm -f "$output_file.raw"
else
touch "$output_file"
fi
fi
}
# Filter out nested artifacts (e.g. node_modules inside node_modules).
# Filter out nested artifacts (e.g. node_modules inside node_modules, .build inside build).
# Optimized: Sort paths to put parents before children, then filter in single pass.
filter_nested_artifacts() {
while IFS= read -r item; do
local parent_dir=$(dirname "$item")
local is_nested=false
for target in "${PURGE_TARGETS[@]}"; do
if [[ "$parent_dir" == *"/$target/"* || "$parent_dir" == *"/$target" ]]; then
is_nested=true
break
fi
done
if [[ "$is_nested" == "false" ]]; then
echo "$item"
fi
done
# 1. Append trailing slash to each path (to ensure /foo/bar starts with /foo/)
# 2. Sort to group parents and children (LC_COLLATE=C ensures standard sorting)
# 3. Use awk to filter out paths that start with the previous kept path
# 4. Remove trailing slash
sed 's|[^/]$|&/|' | LC_COLLATE=C sort | awk '
BEGIN { last_kept = "" }
{
current = $0
# If current path starts with last_kept, it is nested
# Only check if last_kept is not empty
if (last_kept == "" || index(current, last_kept) != 1) {
print current
last_kept = current
}
}
' | sed 's|/$||'
}
filter_protected_artifacts() {
@@ -703,17 +740,14 @@ clean_project_artifacts() {
for temp in "${scan_temps[@]+"${scan_temps[@]}"}"; do
rm -f "$temp" 2> /dev/null || true
done
if [[ -t 1 ]]; then
stop_inline_spinner
fi
# Clean up purge scanning file
local stats_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mole"
rm -f "$stats_dir/purge_scanning" 2> /dev/null || true
echo ""
exit 130
}
trap cleanup_scan INT TERM
# Start parallel scanning of all paths at once
if [[ -t 1 ]]; then
start_inline_spinner "Scanning projects..."
fi
# Scanning is started from purge.sh with start_inline_spinner
# Launch all scans in parallel
for path in "${PURGE_SEARCH_PATHS[@]}"; do
if [[ -d "$path" ]]; then
@@ -730,9 +764,6 @@ clean_project_artifacts() {
for pid in "${scan_pids[@]+"${scan_pids[@]}"}"; do
wait "$pid" 2> /dev/null || true
done
if [[ -t 1 ]]; then
stop_inline_spinner
fi
# Collect all results
for scan_output in "${scan_temps[@]+"${scan_temps[@]}"}"; do
if [[ -f "$scan_output" ]]; then

View File

@@ -267,40 +267,21 @@ tm_is_running() {
grep -qE '(^|[[:space:]])("Running"|Running)[[:space:]]*=[[:space:]]*1([[:space:]]*;|$)' <<< "$st"
}
# Returns 0 if snapshot mounts exist under local snapshot paths
# Returns 1 if none found
# Returns 2 if mount state cannot be determined
tm_snapshots_mounted() {
local m
if ! m="$(run_with_timeout 3 mount 2> /dev/null)"; then
return 2
fi
# Match modern and legacy local-snapshot browse mounts:
# - /Volumes/com.apple.TimeMachine.localsnapshots/... (APFS)
# - /.TimeMachine (APFS)
# - /Volumes/MobileBackups (HFS+, legacy)
grep -qE '[[:space:]]on[[:space:]](/\.TimeMachine(/|[[:space:]])|/Volumes/com\.apple\.TimeMachine\.localsnapshots(/|[[:space:]])|/Volumes/MobileBackups(/|[[:space:]]))' <<< "$m"
}
# Local APFS snapshots (keep the most recent).
clean_local_snapshots() {
if ! command -v tmutil > /dev/null 2>&1; then
return 0
fi
local rc_running rc_mounted
rc_running=0
local rc_running=0
tm_is_running || rc_running=$?
rc_mounted=0
tm_snapshots_mounted || rc_mounted=$?
if [[ $rc_running -eq 2 || $rc_mounted -eq 2 ]]; then
if [[ $rc_running -eq 2 ]]; then
echo -e " ${YELLOW}!${NC} Could not determine Time Machine status; skipping snapshot cleanup"
return 0
fi
if [[ $rc_running -eq 0 || $rc_mounted -eq 0 ]]; then
if [[ $rc_running -eq 0 ]]; then
echo -e " ${YELLOW}!${NC} Time Machine is active; skipping snapshot cleanup"
return 0
fi

View File

@@ -512,7 +512,7 @@ declare -a MOLE_TEMP_DIRS=()
create_temp_file() {
local temp
temp=$(mktemp) || return 1
MOLE_TEMP_FILES+=("$temp")
register_temp_file "$temp"
echo "$temp"
}
@@ -520,7 +520,7 @@ create_temp_file() {
create_temp_dir() {
local temp
temp=$(mktemp -d) || return 1
MOLE_TEMP_DIRS+=("$temp")
register_temp_dir "$temp"
echo "$temp"
}
@@ -538,9 +538,17 @@ register_temp_dir() {
# Compatible with both BSD mktemp (macOS default) and GNU mktemp (coreutils)
mktemp_file() {
local prefix="${1:-mole}"
local temp
local error_msg
# Use TMPDIR if set, otherwise /tmp
# Add .XXXXXX suffix to work with both BSD and GNU mktemp
mktemp "${TMPDIR:-/tmp}/${prefix}.XXXXXX"
if ! error_msg=$(mktemp "${TMPDIR:-/tmp}/${prefix}.XXXXXX" 2>&1); then
echo "Error: Failed to create temporary file: $error_msg" >&2
return 1
fi
temp="$error_msg"
register_temp_file "$temp"
echo "$temp"
}
# Cleanup all tracked temp files and directories

View File

@@ -34,7 +34,7 @@ update_via_homebrew() {
temp_upgrade=$(mktemp_file "brew_upgrade")
# Set up trap for interruption (Ctrl+C) with inline cleanup
trap 'stop_inline_spinner 2>/dev/null; rm -f "$temp_update" "$temp_upgrade" 2>/dev/null; echo ""; exit 130' INT TERM
trap 'stop_inline_spinner 2>/dev/null; safe_remove "$temp_update" true; safe_remove "$temp_upgrade" true; echo ""; exit 130' INT TERM
# Update Homebrew
if [[ -t 1 ]]; then
@@ -73,7 +73,8 @@ update_via_homebrew() {
trap - INT TERM
# Cleanup temp files
rm -f "$temp_update" "$temp_upgrade"
safe_remove "$temp_update" true
safe_remove "$temp_upgrade" true
if echo "$upgrade_output" | grep -q "already installed"; then
local installed_version