session-tools/bin/disk-reclaim
Natalie dbcaf999f9 feat(@scripts): add disk reclaim, host probe, power-cycle tools
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-17 19:38:40 -07:00

162 lines
5.2 KiB
Bash
Executable file

#!/bin/sh
# disk-reclaim [path] [--min SIZE] [--all] [--no-summary]
#
# Scan <path> (default $HOME) for generated/cache directories worth deleting.
# Read-only — never deletes. Reports dirs that regenerate from source (build
# outputs, dependency caches, IDE/framework state) sorted by size desc.
#
# Flags:
# --min SIZE only show entries >= SIZE (e.g. 100M, 1G; default 100M)
# --all alias for --min 0
# --no-summary skip the totals-per-category section
#
# Patterns it looks for (project-scoped, found via find):
# JS/TS: node_modules, .next, .nuxt, .turbo, .vite, .parcel-cache,
# .svelte-kit, .astro, .cache, dist, build, out
# Python: __pycache__, .pytest_cache, .mypy_cache, .ruff_cache, .tox, .venv
# Rust: target
# Other: _build, Pods, DerivedData, .gradle, .android
#
# Plus top-level cache roots checked once each:
# ~/Library/Caches, ~/Library/Developer/Xcode/DerivedData
# ~/.cache, ~/.npm, ~/.pnpm-store, ~/.yarn/cache
# ~/.cargo/registry, ~/.cargo/git
#
# Caveats:
# - .venv requires a rebuild from pyproject/requirements after deletion
# - target (Rust) requires a recompile that can take minutes
# - node_modules needs npm/pnpm install
# - vendor/ is intentionally NOT scanned — often committed (Go) or required (PHP)
set -eu
root=$HOME
min_human=100M
show_summary=1
die() { echo "disk-reclaim: $*" >&2; exit 1; }
usage() {
sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'
exit 2
}
to_kb() {
case "$1" in
*[Kk]) echo "${1%[Kk]}" ;;
*[Mm]) echo $(( ${1%[Mm]} * 1024 )) ;;
*[Gg]) echo $(( ${1%[Gg]} * 1024 * 1024 )) ;;
*[Tt]) echo $(( ${1%[Tt]} * 1024 * 1024 * 1024 )) ;;
''|*[!0-9]*) die "bad size: $1 (use K/M/G/T suffix or plain bytes)" ;;
*) echo "$(( $1 / 1024 ))" ;;
esac
}
human() {
awk -v kb="$1" 'BEGIN {
if (kb >= 1048576) printf "%.1fG", kb/1048576
else if (kb >= 1024) printf "%.0fM", kb/1024
else printf "%dK", kb
}'
}
while [ $# -gt 0 ]; do
case "$1" in
-h|--help|help) usage ;;
--min) [ $# -ge 2 ] || die "--min needs a value"; min_human=$2; shift 2 ;;
--min=*) min_human=${1#--min=}; shift ;;
--all) min_human=0; shift ;;
--no-summary) show_summary=0; shift ;;
-*) die "unknown flag: $1" ;;
*) root=$1; shift ;;
esac
done
[ -d "$root" ] || die "not a directory: $root"
min_kb=$(to_kb "$min_human")
scan_root=$(cd "$root" && pwd -P)
patterns="node_modules .next .nuxt .turbo .vite .parcel-cache .svelte-kit .astro .cache dist build out __pycache__ .pytest_cache .mypy_cache .ruff_cache .tox .venv target _build Pods DerivedData .gradle .android"
# Build the find -name OR-chain.
expr=""
for n in $patterns; do
expr="$expr -name $n -o"
done
expr=${expr% -o}
echo "scanning $scan_root (min size: $(human "$min_kb"))..."
echo
# Find each matching dir; once matched, -prune so we don't descend into it
# looking for nested matches (e.g. avoid target/ inside node_modules).
# stderr → /dev/null to silence permission-denied noise on system dirs.
# shellcheck disable=SC2086
results=$(
find "$scan_root" -type d \( $expr \) -prune -print 2>/dev/null \
| while IFS= read -r dir; do
kb=$(du -sk "$dir" 2>/dev/null | awk '{print $1}')
[ -z "$kb" ] && continue
[ "$kb" -lt "$min_kb" ] && continue
printf '%s\t%s\n' "$kb" "$dir"
done \
| sort -rn
)
if [ -z "$results" ]; then
echo " (no project-scoped entries >= $(human "$min_kb"))"
else
printf ' %8s %s\n' "SIZE" "PATH"
printf ' %8s %s\n' "----" "----"
echo "$results" | while IFS="$(printf '\t')" read -r kb path; do
printf ' %8s %s\n' "$(human "$kb")" "$path"
done
fi
echo
echo "top-level cache roots:"
cache_results=$(
for p in \
"$HOME/Library/Caches" \
"$HOME/Library/Developer/Xcode/DerivedData" \
"$HOME/.cache" \
"$HOME/.npm" \
"$HOME/.pnpm-store" \
"$HOME/.yarn/cache" \
"$HOME/.cargo/registry" \
"$HOME/.cargo/git"
do
[ -d "$p" ] || continue
kb=$(du -sk "$p" 2>/dev/null | awk '{print $1}')
[ -z "$kb" ] && continue
[ "$kb" -lt "$min_kb" ] && continue
printf '%s\t%s\n' "$kb" "$p"
done | sort -rn
)
if [ -z "$cache_results" ]; then
echo " (none >= $(human "$min_kb"))"
else
echo "$cache_results" | while IFS="$(printf '\t')" read -r kb path; do
printf ' %8s %s\n' "$(human "$kb")" "$path"
done
fi
if [ "$show_summary" = 1 ] && [ -n "$results" ]; then
echo
echo "totals by category:"
totals=$(
for n in $patterns; do
sum=$(echo "$results" | awk -v n="$n" -F'\t' '
{ i = split($2, a, "/"); if (a[i] == n) total += $1 }
END { print total+0 }
')
[ "$sum" -gt 0 ] && printf '%s\t%s\n' "$sum" "$n"
done | sort -rn
)
echo "$totals" | while IFS="$(printf '\t')" read -r kb name; do
printf ' %8s %s\n' "$(human "$kb")" "$name"
done
fi
echo
echo "review carefully before rm -rf. some dirs (.venv, target, node_modules) need a rebuild after deletion."