diff --git a/stack/stack-poster.tex b/stack/stack-poster.tex index b4a61a8bc..8d7015e4b 100644 --- a/stack/stack-poster.tex +++ b/stack/stack-poster.tex @@ -131,8 +131,8 @@ neo & macOS & primary dev; builds menubar/electron; deskflow KVM server (8GB) \\ blueberry & macOS & Xcode 26.5 + Metal; Menu Band App Store builds \\ panda & macOS & Iris/Fuser tooling; deskflow client \\ chicken & macOS & fleet client (Ableton, menubar) \\ -macbook-pro-clam & macOS (M1) & viewer; kidlispkids hourly art loop \\ -jas-nzxt & \chip{acblue}{Fedora} & RTX 3070 BTX miner \\ +poorslice & macOS (M1) & viewer; kidlispkids hourly art loop \\ +jastow & \chip{acblue}{Fedora} & RTX 3070 BTX miner \\ jasellite & Linux (\chip{cDO}{DO}) & remote-claude box, 24.144.92.66 \\ \bottomrule \end{tabular} diff --git a/toolchain/btx/README.md b/toolchain/btx/README.md index f4ca549c2..27edfa00f 100644 --- a/toolchain/btx/README.md +++ b/toolchain/btx/README.md @@ -1,7 +1,7 @@ # BTX mining ops scripts Operational helpers for the BTX (btxchain) mining setup. See agent memory -`jas-nzxt-btx-mining` for the full architecture (GPU miner on `jas-nzxt`, +`jas-nzxt-btx-mining` for the full architecture (GPU miner on `jastow`, wallet + full node on `jasellite`). **No secrets live here.** Credentials are read at runtime from separate @@ -19,17 +19,26 @@ wallet + full node on `jasellite`). first shows a nonzero balance. Uses sentinel files (`~/.btx-synced-notified`, `~/.btx-paid-notified`) so it fires once each. -- **`jas-nzxt-disk-guard.sh`** — one-shot, idempotent disk self-maintenance - for the `jas-nzxt` Fedora miner (44G root partition, runs hot on docker + +- **`jas-nzxt-disk-guard.sh`** — legacy-named, one-shot, idempotent disk + self-maintenance for the `jastow` Fedora miner (44G root partition, runs hot on docker + osbuild artifacts). Caps the systemd journal at 500M, installs a weekly docker/package-cache prune timer, and an hourly disk-guard that prunes when `/` crosses 90%. +- **`ac-disk-clean.sh`** — reusable `jastow` disk inventory and safe-clean + command. It reports by default, requires `--apply` before deleting known + regenerable caches, protects the active ChromiumOS/VM build tree, and can + install itself with a weekly systemd timer via `--install`. + +- **`btx-miner-guard.sh`** — pauses `btx-miner.service` while Will is logged + in or native/ChromeOS compilation is active, then resumes after ten quiet + minutes. The accompanying systemd timer checks every 30 seconds. + ## Deploy These run on the remote boxes, not in this repo. Copy to the target and run: ```bash scp toolchain/btx/btx-notify.py toolchain/btx/btx-sync-watch.py jas@jasellite:~/ -scp toolchain/btx/jas-nzxt-disk-guard.sh me@jas-nzxt:/tmp/ && ssh me@jas-nzxt bash /tmp/jas-nzxt-disk-guard.sh +scp toolchain/btx/jas-nzxt-disk-guard.sh me@jastow:/tmp/ && ssh me@jastow bash /tmp/jas-nzxt-disk-guard.sh ``` diff --git a/toolchain/btx/ac-disk-clean.sh b/toolchain/btx/ac-disk-clean.sh new file mode 100755 index 000000000..31786eae4 --- /dev/null +++ b/toolchain/btx/ac-disk-clean.sh @@ -0,0 +1,234 @@ +#!/usr/bin/env bash +# Safe disk inventory/cleanup for jastow. +# +# Default: report only. Nothing is removed without --apply. +set -euo pipefail + +APPLY=0 +INSTALL=0 +PRUNE_VOLUMES=0 + +usage() { + cat <<'EOF' +Usage: ac-disk-clean [--apply] [--prune-volumes] [--install] + + (no args) Report disk use and cleanup candidates; change nothing. + --apply Clear only known regenerable caches. + --prune-volumes With --apply, also remove unused Docker volumes. + --install Install as /usr/local/bin/ac-disk-clean and enable its + weekly systemd timer. + +Protected on every run: fleet-os-builds, VM images, ChromiumOS source/build +state, repositories, node_modules, Downloads, wallet/key material, and active +application caches. +EOF +} + +while (($#)); do + case "$1" in + --apply) APPLY=1 ;; + --install) INSTALL=1 ;; + --prune-volumes) PRUNE_VOLUMES=1 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; + esac + shift +done + +if ((EUID != 0 && (APPLY || INSTALL))); then + elevated_args=() + ((APPLY)) && elevated_args+=(--apply) + ((INSTALL)) && elevated_args+=(--install) + ((PRUNE_VOLUMES)) && elevated_args+=(--prune-volumes) + exec sudo -n "$(readlink -f "$0")" "${elevated_args[@]}" +fi + +TARGET_USER="${SUDO_USER:-${USER:-me}}" +if [[ "$TARGET_USER" == root ]]; then + TARGET_USER=me +fi +TARGET_HOME=$(getent passwd "$TARGET_USER" | cut -d: -f6) +TARGET_HOME=${TARGET_HOME:-/home/me} + +as_root() { + if ((EUID == 0)); then + "$@" + else + sudo -n "$@" + fi +} + +human_size() { + local path="$1" + if [[ -e "$path" ]]; then + as_root du -sh -- "$path" 2>/dev/null | awk '{print $1}' || echo "?" + else + echo "0" + fi +} + +disk_used_bytes() { + df -B1 --output=used / | tail -1 | tr -d ' ' +} + +disk_percent() { + df --output=pcent / | tail -1 | tr -dc '0-9' +} + +active_build() { + pgrep -af '(^|/)([c]c|[c]make|[g]cc|[g]\+\+|[c]lang|[c]lang\+\+|[r]ustc|[n]inja|[m]ake|[o]sbuild)( |$)|[d]ocker([^ ]* )+build' >/dev/null || + pgrep -f 'qemu-system.*fedora38-cros-builder\.qcow2' >/dev/null +} + +report() { + local build_state="idle" + active_build && build_state="ACTIVE — protected" + + echo "jastow disk report — $(date -Is)" + echo + df -h / | awk 'NR == 1 || NR == 2' + echo "Build workload: $build_state" + echo + printf '%-34s %9s %s\n' "Path/category" "Size" "Policy" + printf '%-34s %9s %s\n' "----------------------------------" "---------" "------" + printf '%-34s %9s %s\n' "fleet-os-builds" "not scanned" "PROTECTED; deep scans are intentionally manual" + printf '%-34s %9s %s\n' " ChromiumOS tree" "not scanned" "PROTECTED" + printf '%-34s %9s %s\n' " builder VM image" "$(human_size "$TARGET_HOME/fleet-os-builds/vm-build/fedora38-cros-builder.qcow2")" "PROTECTED" + printf '%-34s %9s %s\n' "aesthetic-computer/.git" "$(human_size "$TARGET_HOME/aesthetic-computer/.git")" "PROTECTED" + printf '%-34s %9s %s\n' "PackageKit cache" "$(human_size /var/cache/PackageKit)" "safe when package manager idle" + printf '%-34s %9s %s\n' "DNF cache" "$(human_size /var/cache/dnf)" "safe when package manager idle" + printf '%-34s %9s %s\n' "osbuild caches" "$(human_size /var/cache/osbuild-worker) + $(human_size /var/cache/osbuild-composer)" "report only" + printf '%-34s %9s %s\n' "Chrome cache" "$(human_size "$TARGET_HOME/.cache/google-chrome")" "safe when Chrome idle" + printf '%-34s %9s %s\n' "Puppeteer cache" "$(human_size "$TARGET_HOME/.cache/puppeteer")" "safe when browser tooling idle" + printf '%-34s %9s %s\n' "GNOME Software cache" "$(human_size "$TARGET_HOME/.cache/gnome-software")" "safe when app idle" + printf '%-34s %9s %s\n' "npm content cache" "$(human_size "$TARGET_HOME/.npm/_cacache")" "safe when npm idle" + printf '%-34s %9s %s\n' "Electron/node-gyp caches" "$(human_size "$TARGET_HOME/.cache/electron") + $(human_size "$TARGET_HOME/.cache/node-gyp")" "safe when build idle" + printf '%-34s %9s %s\n' "system journal" "$(as_root journalctl --disk-usage 2>/dev/null | sed -n 's/.*take up \([^ ]*\).*/\1/p')" "capped at 500M" + printf '%-34s %9s %s\n' "Trash" "$(human_size "$TARGET_HOME/.local/share/Trash")" "safe" + echo + docker system df 2>/dev/null || true + echo + if ((APPLY == 0)); then + echo "Report only. Run 'ac-disk-clean --apply' to clear safe caches." + fi +} + +clean_contents() { + local path="$1" + [[ -d "$path" ]] || return 0 + find "$path" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + +} + +skip() { + echo " skip: $1" +} + +apply_cleanup() { + local before after reclaimed + before=$(disk_used_bytes) + echo "Applying safe cleanup on jastow..." + + journalctl --vacuum-size=500M >/dev/null + systemd-tmpfiles --clean + + if pgrep -x packagekitd >/dev/null || pgrep -x dnf >/dev/null || pgrep -x rpm >/dev/null; then + skip "package manager is active; PackageKit/DNF caches kept" + else + clean_contents /var/cache/PackageKit + dnf clean all >/dev/null 2>&1 || true + fi + + if pgrep -f '[g]oogle-chrome|/chrome ' >/dev/null; then + skip "Chrome is active; Chrome cache kept" + else + clean_contents "$TARGET_HOME/.cache/google-chrome" + fi + + if pgrep -f '[p]uppeteer|chrome-headless' >/dev/null; then + skip "Puppeteer/browser tooling is active; Puppeteer cache kept" + else + clean_contents "$TARGET_HOME/.cache/puppeteer" + fi + + if pgrep -x gnome-software >/dev/null; then + skip "GNOME Software is active; its cache kept" + else + clean_contents "$TARGET_HOME/.cache/gnome-software" + fi + + if active_build; then + skip "build workload is active; npm/Electron/node-gyp caches kept" + else + clean_contents "$TARGET_HOME/.npm/_cacache" + clean_contents "$TARGET_HOME/.cache/electron" + clean_contents "$TARGET_HOME/.cache/node-gyp" + clean_contents "$TARGET_HOME/.cache/ffmpeg-static-nodejs" + fi + + clean_contents "$TARGET_HOME/.cache/Homebrew" + clean_contents "$TARGET_HOME/.local/share/Trash/files" + clean_contents "$TARGET_HOME/.local/share/Trash/info" + + docker image prune -af >/dev/null 2>&1 || true + docker builder prune -af >/dev/null 2>&1 || true + if ((PRUNE_VOLUMES)); then + docker volume prune -f >/dev/null 2>&1 || true + fi + + after=$(disk_used_bytes) + reclaimed=$((before - after)) + ((reclaimed < 0)) && reclaimed=0 + echo "Cleanup complete: $(numfmt --to=iec-i --suffix=B "$reclaimed") reclaimed." + df -h / | awk 'NR == 1 || NR == 2' +} + +install_utility() { + local source + source=$(readlink -f "$0") + if [[ "$source" != /usr/local/bin/ac-disk-clean ]]; then + install -m 0755 "$source" /usr/local/bin/ac-disk-clean + else + chmod 0755 /usr/local/bin/ac-disk-clean + fi + + cat >/etc/systemd/system/ac-disk-clean.service <<'EOF' +[Unit] +Description=jastow safe weekly disk cleanup +After=network.target + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/ac-disk-clean --apply +EOF + + cat >/etc/systemd/system/ac-disk-clean.timer <<'EOF' +[Unit] +Description=Run jastow safe disk cleanup weekly + +[Timer] +OnCalendar=weekly +RandomizedDelaySec=30m +Persistent=true + +[Install] +WantedBy=timers.target +EOF + + systemctl daemon-reload + # Supersede the older unconditional weekly cache pruner. Keep the separate + # 90%-full emergency disk guard installed by jas-nzxt-disk-guard.sh. + systemctl disable --now disk-tidy.timer >/dev/null 2>&1 || true + systemctl enable --now ac-disk-clean.timer + echo "Installed /usr/local/bin/ac-disk-clean and enabled ac-disk-clean.timer." +} + +if ((INSTALL)); then + install_utility + exit 0 +fi + +report +if ((APPLY)); then + echo + apply_cleanup +fi diff --git a/toolchain/btx/btx-miner-guard.service b/toolchain/btx/btx-miner-guard.service new file mode 100644 index 000000000..e5723164f --- /dev/null +++ b/toolchain/btx/btx-miner-guard.service @@ -0,0 +1,7 @@ +[Unit] +Description=Pause BTX mining during interactive/build work +After=multi-user.target + +[Service] +Type=oneshot +ExecStart=/usr/local/sbin/btx-miner-guard diff --git a/toolchain/btx/btx-miner-guard.sh b/toolchain/btx/btx-miner-guard.sh new file mode 100755 index 000000000..40116171f --- /dev/null +++ b/toolchain/btx/btx-miner-guard.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Keep BTX mining out of the way of interactive users and build workloads. +set -u + +MINER_UNIT="btx-miner.service" +STATE_DIR="/var/lib/btx-miner-guard" +LAST_BUSY="$STATE_DIR/last-busy" +QUIET_SECONDS=600 +QEMU_BUSY_PERCENT=1 +QEMU_BUSY_IO_BYTES=$((1024 * 1024)) + +mkdir -p "$STATE_DIR" + +stop_miner() { + local reason="$1" + date +%s >"$LAST_BUSY" + if systemctl is-active --quiet "$MINER_UNIT"; then + logger -t btx-miner-guard "pausing $MINER_UNIT: $reason" + systemctl stop "$MINER_UNIT" + fi + exit 0 +} + +# Will may arrive through a named local account or a Tailscale identity shown by +# `who` in parentheses. Keep this deliberately narrow so ordinary maintenance +# sessions do not suppress mining. +if who | grep -Eiq '(^will([^[:alnum:]_]|$)|\([^)]*will[^)]*\))'; then + stop_miner "Will is logged in" +fi + +# Native build tools on the host. The bracketed first character keeps pgrep +# from matching its own command line. +if pgrep -af '(^|/)([c]c|[c]make|[g]cc|[g]\+\+|[c]lang|[c]lang\+\+|[r]ustc|[n]inja|[m]ake|[o]sbuild)( |$)|[d]ocker([^ ]* )+build' >/dev/null; then + stop_miner "a compiler or build tool is running" +fi + +# ChromeOS builds run inside the long-lived Fedora QEMU VM. Sample its CPU use +# instead of treating an idle VM as a build. +mapfile -t qemu_pids < <(pgrep -f 'qemu-system.*fedora38-cros-builder\.qcow2' || true) +if ((${#qemu_pids[@]})); then + clk_tck=$(getconf CLK_TCK) + declare -A before_cpu before_io + for pid in "${qemu_pids[@]}"; do + if [[ -r "/proc/$pid/stat" ]]; then + read -r utime stime < <(awk '{print $14, $15}' "/proc/$pid/stat") + before_cpu[$pid]=$((utime + stime)) + before_io[$pid]=$(awk '/^(read_bytes|write_bytes):/ {total += $2} END {print total + 0}' "/proc/$pid/io" 2>/dev/null) + fi + done + sleep 5 + for pid in "${qemu_pids[@]}"; do + if [[ -n "${before_cpu[$pid]:-}" && -r "/proc/$pid/stat" ]]; then + read -r utime stime < <(awk '{print $14, $15}' "/proc/$pid/stat") + cpu_delta=$((utime + stime - before_cpu[$pid])) + cpu_percent=$((cpu_delta * 100 / (clk_tck * 5))) + io_now=$(awk '/^(read_bytes|write_bytes):/ {total += $2} END {print total + 0}' "/proc/$pid/io" 2>/dev/null) + io_delta=$((io_now - before_io[$pid])) + if ((cpu_percent >= QEMU_BUSY_PERCENT || io_delta >= QEMU_BUSY_IO_BYTES)); then + stop_miner "ChromeOS build VM is active (${cpu_percent}% CPU, ${io_delta} I/O bytes)" + fi + fi + done +fi + +# Do not bounce the GPU miner back on between short compiler phases. +now=$(date +%s) +if [[ -f "$LAST_BUSY" ]]; then + last_busy=$(cat "$LAST_BUSY" 2>/dev/null || echo "$now") + if ((now - last_busy < QUIET_SECONDS)); then + exit 0 + fi +fi + +if ! systemctl is-active --quiet "$MINER_UNIT"; then + logger -t btx-miner-guard "starting $MINER_UNIT after ${QUIET_SECONDS}s quiet" + systemctl start "$MINER_UNIT" +fi diff --git a/toolchain/btx/btx-miner-guard.timer b/toolchain/btx/btx-miner-guard.timer new file mode 100644 index 000000000..e6cca4edf --- /dev/null +++ b/toolchain/btx/btx-miner-guard.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Check whether BTX mining should run + +[Timer] +OnBootSec=30s +OnUnitActiveSec=30s +AccuracySec=5s +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/toolchain/btx/jas-nzxt-disk-guard.sh b/toolchain/btx/jas-nzxt-disk-guard.sh index 16670cf7c..92f4a4591 100644 --- a/toolchain/btx/jas-nzxt-disk-guard.sh +++ b/toolchain/btx/jas-nzxt-disk-guard.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# jas-nzxt disk self-maintenance — idempotent, safe to re-run. -# Run on the box: bash jas-nzxt-disk-guard.sh (uses sudo internally) +# jastow disk self-maintenance — idempotent, safe to re-run. +# Legacy filename retained for compatibility. Uses sudo internally. set -euo pipefail echo "== 1. Cap systemd journal at 500M ==" @@ -13,7 +13,7 @@ sudo journalctl --vacuum-size=500M echo "== 2. Weekly docker + package-cache prune (systemd timer) ==" sudo tee /etc/systemd/system/disk-tidy.service >/dev/null <<'EOF' [Unit] -Description=jas-nzxt weekly disk tidy (docker + pkg caches) +Description=jastow weekly disk tidy (docker + pkg caches) [Service] Type=oneshot ExecStart=/usr/bin/docker image prune -af diff --git a/toolchain/fleet/README.md b/toolchain/fleet/README.md index 69f8aa62b..6b3ee7f30 100644 --- a/toolchain/fleet/README.md +++ b/toolchain/fleet/README.md @@ -51,7 +51,7 @@ node toolchain/fleet/normalize-machines.mjs # CLI smoke test (same code the MCP runs) node toolchain/fleet/fleet-mcp.mjs list node toolchain/fleet/fleet-mcp.mjs find gpu -node toolchain/fleet/fleet-mcp.mjs machine macbook-pro-clam +node toolchain/fleet/fleet-mcp.mjs machine poorslice ``` Data path is resolved in order: `$FLEET_MACHINES` → vault diff --git a/toolchain/fleet/fleet-mcp.mjs b/toolchain/fleet/fleet-mcp.mjs index 76f28ac1c..65e5c6f98 100755 --- a/toolchain/fleet/fleet-mcp.mjs +++ b/toolchain/fleet/fleet-mcp.mjs @@ -171,7 +171,7 @@ const TOOLS = [ { name: "fleet_machine", description: "Full detail for ONE machine (hardware, os, ssh, repoPath, designation, capabilities, notes) plus its live tailnet status. Accepts the registry key or the tailnet short name. Names come from fleet_list.", - inputSchema: { type: "object", properties: { name: { type: "string", description: "Machine name, e.g. macbook-pro-clam, jasellite, neo." } }, required: ["name"] }, + inputSchema: { type: "object", properties: { name: { type: "string", description: "Machine name, e.g. poorslice, jastow, neo." } }, required: ["name"] }, }, { name: "fleet_find", diff --git a/toolchain/fleet/normalize-machines.mjs b/toolchain/fleet/normalize-machines.mjs index eb5247c53..7fe6ae885 100755 --- a/toolchain/fleet/normalize-machines.mjs +++ b/toolchain/fleet/normalize-machines.mjs @@ -136,11 +136,11 @@ const ENRICH = { status: { source: "tailscale", key: "blueberry" }, fleetRole: "Lightweight (8GB) control / macpal box. Too small to host hermes — author + drive only.", }, - "macbook-pro-clam": { + poorslice: { designation: "compute-node", capabilities: ["mlx", "macos-automation", "screen-capture", "chromium-pool", "ffmpeg-render", "always-on"], - tailscale: { name: "macbook-pro-clam", ip: "100.86.206.3" }, - status: { source: "tailscale", key: "macbook-pro-clam" }, + tailscale: { name: "poorslice", ip: "100.86.206.3" }, + status: { source: "tailscale", key: "poorslice" }, fleetRole: "Always-on macOS media-gen compute node (M1 Pro/16GB): Chromium pool, MLX local models, screen + native-app automation. Also the strongest agent-endpoint CANDIDATE among the Macs.", }, }; @@ -206,14 +206,14 @@ const ADD = { fleetRole: "Always-on Linux services appliance + PRIMARY hermes agent-endpoint host. Runs the mail appliance and authed tailnet APIs.", _review: "Added by fleet normalization 2026-07-06. This is the intended home for the hermes gateway + fleet identity.", }, - "jas-nzxt": { - label: "🖥️ jas-nzxt (GPU tower)", + jastow: { + label: "🖥️ jastow (GPU tower)", emoji: "🖥️", os: "Linux", designation: "compute-node", capabilities: ["gpu", "cuda", "docker", "ffmpeg-render"], - tailscale: { name: "jas-nzxt", ip: "100.103.42.46" }, - status: { source: "tailscale", key: "jas-nzxt" }, + tailscale: { name: "jastow", ip: "100.103.42.46" }, + status: { source: "tailscale", key: "jastow" }, fleetRole: "Linux GPU tower for heavy compute / render.", _review: "Added by fleet normalization 2026-07-06 (on the tailnet, absent from vault). Fill in hardware/ssh/user.", }, diff --git a/toolchain/macos/SCORE.md b/toolchain/macos/SCORE.md index eeaffe322..9082f2f0b 100644 --- a/toolchain/macos/SCORE.md +++ b/toolchain/macos/SCORE.md @@ -126,6 +126,21 @@ full Bash can't even capture output, redirect to a file and Read it: buckets below — and check `/System/Volumes/Data` (not `/`, the sealed system snapshot) for the real usage. +### Reusable cleanup command + +Fleet Macs can install [`ac-disk-clean.sh`](ac-disk-clean.sh) as a report-first +cleanup utility: + +```bash +ac-disk-clean # inventory only +ac-disk-clean --apply # known regenerable caches +ac-disk-clean --apply --thin-snapshots # opt-in APFS snapshot thinning +``` + +`--install` places it in `~/.local/bin` and enables a weekly user LaunchAgent. +Running applications keep ownership of their caches; repositories, downloads, +models, agent state, and CoreSimulator runtimes are always report-only. + ### Safe regenerable buckets Always clear first — fully recover with no judgment call: diff --git a/toolchain/macos/ac-disk-clean.sh b/toolchain/macos/ac-disk-clean.sh new file mode 100755 index 000000000..b5e477ddc --- /dev/null +++ b/toolchain/macos/ac-disk-clean.sh @@ -0,0 +1,202 @@ +#!/bin/bash +# Safe, user-level macOS disk inventory/cleanup for the AC fleet Macs. +# Default is report-only; known regenerable caches require --apply. +set -euo pipefail + +APPLY=0 +INSTALL=0 +THIN_SNAPSHOTS=0 + +usage() { + cat <<'EOF' +Usage: ac-disk-clean [--apply] [--thin-snapshots] [--install] + + (no args) Report disk use and cleanup candidates; change nothing. + --apply Clear known regenerable user caches when their app is idle. + --thin-snapshots With --apply, ask tmutil to reclaim local snapshots. + --install Install in ~/.local/bin and enable a weekly LaunchAgent. + +Protected: repositories, node_modules, Downloads, Documents/Shelf, model +weights, Codex/Claude state, mail archives, application data, and caches owned +by a currently running application. +EOF +} + +while (($#)); do + case "$1" in + --apply) APPLY=1 ;; + --install) INSTALL=1 ;; + --thin-snapshots) THIN_SNAPSHOTS=1 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; + esac + shift +done + +human_size() { + if [[ -e "$1" ]]; then + du -sh -- "$1" 2>/dev/null | awk '{print $1}' || echo "?" + else + echo "0" + fi +} + +data_volume() { + [[ -d /System/Volumes/Data ]] && echo /System/Volumes/Data || echo / +} + +disk_used_kb() { + df -k "$(data_volume)" | awk 'NR == 2 {print $3}' +} + +report() { + local volume snapshots + volume=$(data_volume) + snapshots=$(tmutil listlocalsnapshots / 2>/dev/null | grep -c '^com\.apple\.' || true) + + echo "$(scutil --get LocalHostName 2>/dev/null || hostname) disk report — $(date '+%Y-%m-%dT%H:%M:%S%z')" + echo + df -h "$volume" | awk 'NR == 1 || NR == 2' + echo "Local snapshots: $snapshots" + echo + printf '%-35s %9s %s\n' "Path/category" "Size" "Policy" + printf '%-35s %9s %s\n' "-----------------------------------" "---------" "------" + printf '%-35s %9s %s\n' "aesthetic-computer/.git" "$(human_size "$HOME/aesthetic-computer/.git")" "PROTECTED" + printf '%-35s %9s %s\n' "aesthetic-computer/node_modules" "$(human_size "$HOME/aesthetic-computer/node_modules")" "PROTECTED" + printf '%-35s %9s %s\n' "Documents/Shelf" "$(human_size "$HOME/Documents/Shelf")" "PROTECTED; curate manually" + printf '%-35s %9s %s\n' "Chrome cache" "$(human_size "$HOME/Library/Caches/Google")" "safe when Chrome idle" + printf '%-35s %9s %s\n' "Puppeteer cache" "$(human_size "$HOME/.cache/puppeteer")" "safe when browser tooling idle" + printf '%-35s %9s %s\n' "Spotify cache" "$(human_size "$HOME/Library/Caches/com.spotify.client")" "safe when Spotify idle" + printf '%-35s %9s %s\n' "pip cache" "$(human_size "$HOME/Library/Caches/pip")" "safe when pip idle" + printf '%-35s %9s %s\n' "Messages cache" "$(human_size "$HOME/Library/Messages/Caches")" "safe when Messages idle" + printf '%-35s %9s %s\n' "npm content cache" "$(human_size "$HOME/.npm/_cacache")" "safe when node/npm idle" + printf '%-35s %9s %s\n' "Slab recordings" "$(human_size "$HOME/.local/share/slab/sessions")" "keeps last 7 days" + printf '%-35s %9s %s\n' "Trash" "$(human_size "$HOME/.Trash")" "safe with --apply" + printf '%-35s %9s %s\n' "CoreSimulator volumes" "$(human_size /Library/Developer/CoreSimulator/Volumes)" "report only" + echo + if ((APPLY == 0)); then + echo "Report only. Run 'ac-disk-clean --apply' to clear safe caches." + fi +} + +clean_contents() { + [[ -d "$1" ]] || return 0 + find "$1" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + +} + +skip() { + echo " skip: $1" +} + +apply_cleanup() { + local before after reclaimed + before=$(disk_used_kb) + echo "Applying safe user-cache cleanup..." + + if pgrep -f '/Google Chrome( |$)|Google Chrome Helper' >/dev/null; then + skip "Chrome is active; Chrome and Puppeteer caches kept" + else + clean_contents "$HOME/Library/Caches/Google" + clean_contents "$HOME/.cache/puppeteer" + fi + + if pgrep -x Spotify >/dev/null; then + skip "Spotify is active; its cache kept" + else + clean_contents "$HOME/Library/Caches/com.spotify.client" + fi + + if pgrep -x Messages >/dev/null; then + skip "Messages is active; its cache kept" + else + clean_contents "$HOME/Library/Messages/Caches" + fi + + if pgrep -f '(^|/)(pip|pip3)( |$)' >/dev/null; then + skip "pip is active; its cache kept" + else + clean_contents "$HOME/Library/Caches/pip" + fi + + if pgrep -f '(^|/)(node|npm|pnpm|yarn)( |$)' >/dev/null; then + skip "node/npm workload is active; npm cache kept" + else + clean_contents "$HOME/.npm/_cacache" + fi + + if pgrep -f '(^|/)(swift|swiftc|xcodebuild)( |$)' >/dev/null; then + skip "Swift/Xcode build is active; .build directories kept" + elif [[ -d "$HOME/aesthetic-computer" ]]; then + find "$HOME/aesthetic-computer" -name .build -type d -prune -exec rm -rf -- {} + 2>/dev/null || true + fi + + if [[ -d "$HOME/.local/share/slab/sessions" ]]; then + find "$HOME/.local/share/slab/sessions" -maxdepth 1 -type f -mtime +7 -delete + fi + clean_contents "$HOME/.Trash" + + if ! pgrep -x brew >/dev/null; then + if [[ -x /opt/homebrew/bin/brew ]]; then + /opt/homebrew/bin/brew cleanup --prune=all >/dev/null 2>&1 || true + elif [[ -x /usr/local/bin/brew ]]; then + /usr/local/bin/brew cleanup --prune=all >/dev/null 2>&1 || true + fi + fi + + if ((THIN_SNAPSHOTS)); then + tmutil thinlocalsnapshots / 10000000000 4 >/dev/null 2>&1 || skip "tmutil could not thin snapshots without administrator approval" + fi + + after=$(disk_used_kb) + reclaimed=$((before - after)) + ((reclaimed < 0)) && reclaimed=0 + echo "Cleanup complete: $(awk -v kb="$reclaimed" 'BEGIN {printf "%.1f MiB", kb / 1024}') reclaimed." + df -h "$(data_volume)" | awk 'NR == 1 || NR == 2' +} + +install_utility() { + local bin_dir agent_dir plist source uid + bin_dir="$HOME/.local/bin" + agent_dir="$HOME/Library/LaunchAgents" + plist="$agent_dir/computer.aesthetic.disk-clean.plist" + source=$(cd "$(dirname "$0")" && pwd)/$(basename "$0") + uid=$(id -u) + mkdir -p "$bin_dir" "$agent_dir" "$HOME/Library/Logs" + if [[ "$source" != "$bin_dir/ac-disk-clean" ]]; then + install -m 0755 "$source" "$bin_dir/ac-disk-clean" + else + chmod 0755 "$bin_dir/ac-disk-clean" + fi + + cat >"$plist" < + + + Labelcomputer.aesthetic.disk-clean + ProgramArguments + $bin_dir/ac-disk-clean--apply + + StartCalendarInterval + Weekday1Hour3Minute15 + + StandardOutPath$HOME/Library/Logs/ac-disk-clean.log + StandardErrorPath$HOME/Library/Logs/ac-disk-clean.log + +EOF + + plutil -lint "$plist" + launchctl bootout "gui/$uid/computer.aesthetic.disk-clean" >/dev/null 2>&1 || true + launchctl bootstrap "gui/$uid" "$plist" + echo "Installed $bin_dir/ac-disk-clean and weekly LaunchAgent." +} + +if ((INSTALL)); then + install_utility + exit 0 +fi + +report +if ((APPLY)); then + echo + apply_cleanup +fi