From 7e69a4eb71166dbbc291b2b3899c1356ec8d6809 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Fri, 6 Mar 2026 07:34:24 +0800 Subject: [PATCH] clean orphaned Claude VM bundle Fixes #537 --- lib/clean/apps.sh | 52 ++++++++++++++++++++++++++- tests/clean_apps.bats | 82 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) diff --git a/lib/clean/apps.sh b/lib/clean/apps.sh index b96f355..f1e454d 100644 --- a/lib/clean/apps.sh +++ b/lib/clean/apps.sh @@ -244,6 +244,44 @@ is_bundle_orphaned() { # All checks passed - this is an orphan return 0 } + +is_claude_vm_bundle_orphaned() { + local vm_bundle_path="$1" + local installed_bundles="$2" + local claude_bundle_id="com.anthropic.claudefordesktop" + + [[ -d "$vm_bundle_path" ]] || return 1 + + # Extra guard in case the running-app scan missed Claude Desktop. + if pgrep -x "Claude" > /dev/null 2>&1; then + return 1 + fi + + if grep -Fxq "$claude_bundle_id" "$installed_bundles" 2> /dev/null; then + return 1 + fi + + if [[ -z "$ORPHAN_MDFIND_CACHE_FILE" ]]; then + ORPHAN_MDFIND_CACHE_FILE=$(mktemp "${TMPDIR:-/tmp}/mole_mdfind_cache.XXXXXX") + register_temp_file "$ORPHAN_MDFIND_CACHE_FILE" + fi + + if grep -Fxq "FOUND:$claude_bundle_id" "$ORPHAN_MDFIND_CACHE_FILE" 2> /dev/null; then + return 1 + fi + if ! grep -Fxq "NOTFOUND:$claude_bundle_id" "$ORPHAN_MDFIND_CACHE_FILE" 2> /dev/null; then + local app_exists + app_exists=$(run_with_timeout 2 mdfind "kMDItemCFBundleIdentifier == '$claude_bundle_id'" 2> /dev/null | head -1 || echo "") + if [[ -n "$app_exists" ]]; then + echo "FOUND:$claude_bundle_id" >> "$ORPHAN_MDFIND_CACHE_FILE" + return 1 + fi + echo "NOTFOUND:$claude_bundle_id" >> "$ORPHAN_MDFIND_CACHE_FILE" + fi + + return 0 +} + # Orphaned app data sweep. clean_orphaned_app_data() { if ! ls "$HOME/Library/Caches" > /dev/null 2>&1; then @@ -260,6 +298,19 @@ clean_orphaned_app_data() { local orphaned_count=0 local total_orphaned_kb=0 start_section_spinner "Scanning orphaned app resources..." + + local claude_vm_bundle="$HOME/Library/Application Support/Claude/vm_bundles/claudevm.bundle" + if is_claude_vm_bundle_orphaned "$claude_vm_bundle" "$installed_bundles"; then + local claude_vm_size_kb + claude_vm_size_kb=$(get_path_size_kb "$claude_vm_bundle") + if [[ -n "$claude_vm_size_kb" && "$claude_vm_size_kb" != "0" ]]; then + if safe_clean "$claude_vm_bundle" "Orphaned Claude workspace VM"; then + orphaned_count=$((orphaned_count + 1)) + total_orphaned_kb=$((total_orphaned_kb + claude_vm_size_kb)) + fi + fi + fi + # CRITICAL: NEVER add LaunchAgents or LaunchDaemons (breaks login items/startup apps). local -a resource_types=( "$HOME/Library/Caches|Caches|com.*:org.*:net.*:io.*" @@ -269,7 +320,6 @@ clean_orphaned_app_data() { "$HOME/Library/HTTPStorages|HTTP|com.*:org.*:net.*:io.*" "$HOME/Library/Cookies|Cookies|*.binarycookies" ) - orphaned_count=0 for resource_type in "${resource_types[@]}"; do IFS='|' read -r base_path label patterns <<< "$resource_type" if [[ ! -d "$base_path" ]]; then diff --git a/tests/clean_apps.bats b/tests/clean_apps.bats index 6308f0e..a974716 100644 --- a/tests/clean_apps.bats +++ b/tests/clean_apps.bats @@ -210,6 +210,88 @@ EOF [[ "$output" == *"PASS: Successful deletion removed"* ]] } +@test "clean_orphaned_app_data removes orphaned Claude VM bundle" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/clean/apps.sh" + +scan_installed_apps() { + : > "$1" +} + +mdfind() { + return 0 +} + +pgrep() { + return 1 +} + +run_with_timeout() { shift; "$@"; } + +safe_clean() { + echo "$2" + rm -rf "$1" +} + +start_section_spinner() { :; } +stop_section_spinner() { :; } + +mkdir -p "$HOME/Library/Caches" +mkdir -p "$HOME/Library/Application Support/Claude/vm_bundles/claudevm.bundle" +echo "vm data" > "$HOME/Library/Application Support/Claude/vm_bundles/claudevm.bundle/rootfs.img" + +clean_orphaned_app_data + +if [[ ! -d "$HOME/Library/Application Support/Claude/vm_bundles/claudevm.bundle" ]]; then + echo "PASS: Claude VM removed" +fi +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"Orphaned Claude workspace VM"* ]] + [[ "$output" == *"PASS: Claude VM removed"* ]] +} + +@test "clean_orphaned_app_data keeps Claude VM bundle when Claude is installed" { + run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF' +set -euo pipefail +source "$PROJECT_ROOT/lib/core/common.sh" +source "$PROJECT_ROOT/lib/clean/apps.sh" + +scan_installed_apps() { + echo "com.anthropic.claudefordesktop" > "$1" +} + +pgrep() { + return 1 +} + +safe_clean() { + echo "UNEXPECTED:$2" + return 1 +} + +start_section_spinner() { :; } +stop_section_spinner() { :; } + +mkdir -p "$HOME/Library/Caches" +mkdir -p "$HOME/Library/Application Support/Claude/vm_bundles/claudevm.bundle" +echo "vm data" > "$HOME/Library/Application Support/Claude/vm_bundles/claudevm.bundle/rootfs.img" + +clean_orphaned_app_data + +if [[ -d "$HOME/Library/Application Support/Claude/vm_bundles/claudevm.bundle" ]]; then + echo "PASS: Claude VM kept" +fi +EOF + + [ "$status" -eq 0 ] + [[ "$output" != *"UNEXPECTED:Orphaned Claude workspace VM"* ]] + [[ "$output" == *"PASS: Claude VM kept"* ]] +} + @test "is_critical_system_component matches known system services" { run bash --noprofile --norc <<'EOF'