#!/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"