diff --git a/README.md b/README.md index 4d56e98..c541c41 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,14 @@ Prune migrated legacy backups from runtime folders (dry-run by default): ~/.mosaic/bin/mosaic-prune-legacy-runtime --runtime claude --apply ``` +Clean empty legacy runtime directories: + +```bash +~/.mosaic/bin/mosaic-clean-runtime --runtime claude +~/.mosaic/bin/mosaic-clean-runtime --runtime claude --apply +~/.mosaic/bin/mosaic-clean-runtime --runtime claude --all-empty --apply +``` + Audit runtime drift: ```bash diff --git a/bin/mosaic-clean-runtime b/bin/mosaic-clean-runtime new file mode 100755 index 0000000..dbacf48 --- /dev/null +++ b/bin/mosaic-clean-runtime @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +set -euo pipefail + +RUNTIME="claude" +APPLY=0 +ALL_EMPTY=0 + +usage() { + cat < Runtime to clean (default: claude) + --all-empty Scan all runtime directories (except protected paths) + --apply Perform deletions (default: dry-run) + -h, --help Show help +USAGE +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --runtime) + [[ $# -lt 2 ]] && { echo "Missing value for --runtime" >&2; exit 1; } + RUNTIME="$2" + shift 2 + ;; + --all-empty) + ALL_EMPTY=1 + shift + ;; + --apply) + APPLY=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +case "$RUNTIME" in + claude) + TARGET_ROOT="$HOME/.claude" + managed_roots=( + "$HOME/.claude/agent-guides" + "$HOME/.claude/scripts" + "$HOME/.claude/templates" + "$HOME/.claude/presets" + "$HOME/.claude/skills" + "$HOME/.claude/agents" + "$HOME/.claude/agents.bak" + ) + protected_roots=( + "$HOME/.claude/.git" + "$HOME/.claude/debug" + "$HOME/.claude/file-history" + "$HOME/.claude/projects" + "$HOME/.claude/session-env" + "$HOME/.claude/tasks" + "$HOME/.claude/todos" + "$HOME/.claude/plugins" + "$HOME/.claude/statsig" + "$HOME/.claude/logs" + "$HOME/.claude/shell-snapshots" + "$HOME/.claude/paste-cache" + "$HOME/.claude/plans" + "$HOME/.claude/ide" + "$HOME/.claude/cache" + ) + ;; + *) + echo "Unsupported runtime: $RUNTIME" >&2 + exit 1 + ;; +esac + +[[ -d "$TARGET_ROOT" ]] || { echo "[mosaic-clean] Runtime dir missing: $TARGET_ROOT" >&2; exit 1; } + +is_protected() { + local path="$1" + for p in "${protected_roots[@]}"; do + [[ -e "$p" ]] || continue + case "$path" in + "$p"|"$p"/*) + return 0 + ;; + esac + done + return 1 +} + +collect_empty_dirs() { + if [[ $ALL_EMPTY -eq 1 ]]; then + find "$TARGET_ROOT" -depth -type d -empty + else + for r in "${managed_roots[@]}"; do + [[ -d "$r" ]] || continue + find "$r" -depth -type d -empty + done + fi +} + +count_candidates=0 +count_deletable=0 + +while IFS= read -r d; do + [[ -n "$d" ]] || continue + + count_candidates=$((count_candidates + 1)) + + # Never remove runtime root. + [[ "$d" == "$TARGET_ROOT" ]] && continue + + if is_protected "$d"; then + continue + fi + + count_deletable=$((count_deletable + 1)) + + if [[ $APPLY -eq 1 ]]; then + rmdir "$d" 2>/dev/null || true + if [[ ! -d "$d" ]]; then + echo "[mosaic-clean] deleted: $d" + fi + else + echo "[mosaic-clean] would delete: $d" + fi +done < <(collect_empty_dirs | sort -u) + +mode="managed" +[[ $ALL_EMPTY -eq 1 ]] && mode="all-empty" + +if [[ $APPLY -eq 1 ]]; then + echo "[mosaic-clean] complete: mode=$mode deleted_or_attempted=$count_deletable candidates=$count_candidates runtime=$RUNTIME" +else + echo "[mosaic-clean] dry-run: mode=$mode deletable=$count_deletable candidates=$count_candidates runtime=$RUNTIME" + echo "[mosaic-clean] re-run with --apply to delete" +fi