macOS system health toolkit with 5 audit modules (packages, environment, security, cleanup, resources) and cleanup executor. Fixed bugs from review: - Replace GNU timeout with perl alarm (macOS compatible) - Remove Linux-only ps --sort flag, use portable sort - Add JSON escaping to all audit scripts - Remove redundant classify_size branch Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
161 lines
5.7 KiB
Bash
Executable File
161 lines
5.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# audit_cleanup.sh — Scan caches, logs, and clutter on macOS, report sizes
|
|
# Output: JSON lines to stdout, one per finding
|
|
set -euo pipefail
|
|
|
|
json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' '; }
|
|
|
|
json_finding() {
|
|
local severity="$1" finding action details
|
|
finding=$(json_escape "$2")
|
|
action=$(json_escape "$3")
|
|
details=$(json_escape "$4")
|
|
printf '{"module":"cleanup","severity":"%s","finding":"%s","action":"%s","details":"%s"}\n' \
|
|
"$severity" "$finding" "$action" "$details"
|
|
}
|
|
|
|
# Get directory size in human-readable and bytes
|
|
dir_size() {
|
|
local path="$1"
|
|
if [ -d "$path" ]; then
|
|
local bytes=$(du -sk "$path" 2>/dev/null | awk '{print $1}')
|
|
local human=$(du -sh "$path" 2>/dev/null | awk '{print $1}')
|
|
echo "${bytes:-0}|${human:-0B}"
|
|
else
|
|
echo "0|0B"
|
|
fi
|
|
}
|
|
|
|
classify_size() {
|
|
local kb="$1"
|
|
if [ "$kb" -gt 1048576 ] 2>/dev/null; then # > 1GB
|
|
echo "warning"
|
|
else
|
|
echo "info"
|
|
fi
|
|
}
|
|
|
|
total_reclaimable_kb=0
|
|
|
|
# --- User logs ---
|
|
result=$(dir_size "$HOME/Library/Logs")
|
|
IFS='|' read -r kb human <<< "$result"
|
|
total_reclaimable_kb=$((total_reclaimable_kb + kb))
|
|
severity=$(classify_size "$kb")
|
|
json_finding "$severity" "User logs: ${human}" "clean" "~/Library/Logs/ [safe, no sudo]"
|
|
|
|
# --- System logs ---
|
|
result=$(dir_size "/var/log" 2>/dev/null || echo "0|0B")
|
|
IFS='|' read -r kb human <<< "$result"
|
|
if [ "$kb" -gt 0 ] 2>/dev/null; then
|
|
severity=$(classify_size "$kb")
|
|
json_finding "$severity" "System logs: ${human}" "clean" "/var/log/ [moderate, requires sudo]"
|
|
fi
|
|
|
|
# --- User caches ---
|
|
result=$(dir_size "$HOME/Library/Caches")
|
|
IFS='|' read -r kb human <<< "$result"
|
|
total_reclaimable_kb=$((total_reclaimable_kb + kb))
|
|
severity=$(classify_size "$kb")
|
|
json_finding "$severity" "User caches: ${human}" "clean" "~/Library/Caches/ [safe, per-app]"
|
|
|
|
# --- Homebrew cache ---
|
|
if command -v brew &>/dev/null; then
|
|
brew_cache=$(brew --cache 2>/dev/null || echo "")
|
|
if [ -n "$brew_cache" ] && [ -d "$brew_cache" ]; then
|
|
result=$(dir_size "$brew_cache")
|
|
IFS='|' read -r kb human <<< "$result"
|
|
total_reclaimable_kb=$((total_reclaimable_kb + kb))
|
|
severity=$(classify_size "$kb")
|
|
json_finding "$severity" "Homebrew cache: ${human}" "brew cleanup" "$brew_cache [safe]"
|
|
fi
|
|
fi
|
|
|
|
# --- npm cache ---
|
|
npm_cache="$HOME/.npm/_cacache"
|
|
if [ -d "$npm_cache" ]; then
|
|
result=$(dir_size "$npm_cache")
|
|
IFS='|' read -r kb human <<< "$result"
|
|
total_reclaimable_kb=$((total_reclaimable_kb + kb))
|
|
severity=$(classify_size "$kb")
|
|
json_finding "$severity" "npm cache: ${human}" "npm cache clean --force" "$npm_cache [safe]"
|
|
fi
|
|
|
|
# --- pip cache ---
|
|
pip_cache="$HOME/Library/Caches/pip"
|
|
if [ -d "$pip_cache" ]; then
|
|
result=$(dir_size "$pip_cache")
|
|
IFS='|' read -r kb human <<< "$result"
|
|
total_reclaimable_kb=$((total_reclaimable_kb + kb))
|
|
severity=$(classify_size "$kb")
|
|
json_finding "$severity" "pip cache: ${human}" "pip cache purge" "$pip_cache [safe]"
|
|
fi
|
|
|
|
# --- Xcode derived data ---
|
|
xcode_derived="$HOME/Library/Developer/Xcode/DerivedData"
|
|
if [ -d "$xcode_derived" ]; then
|
|
result=$(dir_size "$xcode_derived")
|
|
IFS='|' read -r kb human <<< "$result"
|
|
total_reclaimable_kb=$((total_reclaimable_kb + kb))
|
|
severity=$(classify_size "$kb")
|
|
json_finding "$severity" "Xcode DerivedData: ${human}" "clean" "$xcode_derived [safe]"
|
|
fi
|
|
|
|
# --- Xcode archives ---
|
|
xcode_archives="$HOME/Library/Developer/Xcode/Archives"
|
|
if [ -d "$xcode_archives" ]; then
|
|
result=$(dir_size "$xcode_archives")
|
|
IFS='|' read -r kb human <<< "$result"
|
|
total_reclaimable_kb=$((total_reclaimable_kb + kb))
|
|
severity=$(classify_size "$kb")
|
|
json_finding "$severity" "Xcode Archives: ${human}" "clean" "$xcode_archives [moderate - check before deleting]"
|
|
fi
|
|
|
|
# --- iOS Device Support ---
|
|
ios_support="$HOME/Library/Developer/Xcode/iOS DeviceSupport"
|
|
if [ -d "$ios_support" ]; then
|
|
result=$(dir_size "$ios_support")
|
|
IFS='|' read -r kb human <<< "$result"
|
|
total_reclaimable_kb=$((total_reclaimable_kb + kb))
|
|
severity=$(classify_size "$kb")
|
|
json_finding "$severity" "iOS DeviceSupport: ${human}" "clean" "$ios_support [safe]"
|
|
fi
|
|
|
|
# --- Docker ---
|
|
if command -v docker &>/dev/null; then
|
|
docker_df=$(docker system df 2>/dev/null || echo "")
|
|
if [ -n "$docker_df" ]; then
|
|
reclaimable=$(echo "$docker_df" | grep -i "reclaimable" | head -1 || echo "")
|
|
json_finding "info" "Docker space usage detected" "docker system prune" "Run docker system df for details [use docker system prune to clean]"
|
|
fi
|
|
else
|
|
# Skip silently if docker not installed
|
|
true
|
|
fi
|
|
|
|
# --- Trash ---
|
|
trash_dir="$HOME/.Trash"
|
|
if [ -d "$trash_dir" ]; then
|
|
result=$(dir_size "$trash_dir")
|
|
IFS='|' read -r kb human <<< "$result"
|
|
total_reclaimable_kb=$((total_reclaimable_kb + kb))
|
|
if [ "$kb" -gt 0 ] 2>/dev/null; then
|
|
severity=$(classify_size "$kb")
|
|
json_finding "$severity" "Trash: ${human}" "empty trash" "$trash_dir [safe]"
|
|
fi
|
|
fi
|
|
|
|
# --- Old downloads (report only) ---
|
|
if [ -d "$HOME/Downloads" ]; then
|
|
old_files=$(find "$HOME/Downloads" -maxdepth 1 -mtime +90 -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
if [ "$old_files" -gt 0 ] 2>/dev/null; then
|
|
old_size=$(find "$HOME/Downloads" -maxdepth 1 -mtime +90 -type f -exec du -ck {} + 2>/dev/null | tail -1 | awk '{print $1}')
|
|
old_human=$(echo "$old_size" | awk '{printf "%.1fM", $1/1024}')
|
|
json_finding "info" "${old_files} files in Downloads older than 90 days (${old_human})" "review" "Info only - review manually"
|
|
fi
|
|
fi
|
|
|
|
# --- Total summary ---
|
|
total_human=$(echo "$total_reclaimable_kb" | awk '{if($1>1048576) printf "%.1fG",$1/1048576; else if($1>1024) printf "%.1fM",$1/1024; else printf "%dK",$1}')
|
|
json_finding "info" "Total scannable space: ${total_human}" "review" "Approx reclaimable across all categories"
|