Release v1.21.0: Add macos-cleaner skill
- Add macos-cleaner v1.0.0 - Intelligent macOS disk space recovery - Safety-first philosophy with risk categorization (Safe/Caution/Keep) - Smart analysis: caches, app remnants, large files, dev environments - Interactive cleanup with explicit user confirmation - Bundled scripts: analyze_caches, analyze_dev_env, analyze_large_files, find_app_remnants, safe_delete, cleanup_report - Comprehensive references: cleanup_targets, mole_integration, safety_rules - Update marketplace to v1.21.0 - Update all documentation (README.md, README.zh-CN.md, CHANGELOG.md, CLAUDE.md) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
4
macos-cleaner/.security-scan-passed
Normal file
4
macos-cleaner/.security-scan-passed
Normal file
@@ -0,0 +1,4 @@
|
||||
Security scan passed
|
||||
Scanned at: 2026-01-11T15:33:50.497864
|
||||
Tool: gitleaks + pattern-based validation
|
||||
Content hash: 837b8b7dfa3a728295649b608acaba00d7eb2b1fbbe02a35f5c8e0ffc8c2e361
|
||||
528
macos-cleaner/SKILL.md
Normal file
528
macos-cleaner/SKILL.md
Normal file
@@ -0,0 +1,528 @@
|
||||
---
|
||||
name: macos-cleaner
|
||||
description: Analyze and reclaim macOS disk space through intelligent cleanup recommendations. This skill should be used when users report disk space issues, need to clean up their Mac, or want to understand what's consuming storage. Focus on safe, interactive analysis with user confirmation before any deletions.
|
||||
---
|
||||
|
||||
# macOS Cleaner
|
||||
|
||||
## Overview
|
||||
|
||||
Intelligently analyze macOS disk usage and provide actionable cleanup recommendations to reclaim storage space. This skill follows a **safety-first philosophy**: analyze thoroughly, present clear findings, and require explicit user confirmation before executing any deletions.
|
||||
|
||||
**Target users**: Users with basic technical knowledge who understand file systems but need guidance on what's safe to delete on macOS.
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Analyze First, Act Second**: Never delete files without explicit user confirmation
|
||||
2. **Transparency**: Explain what each file/directory is and why it's safe (or unsafe) to delete
|
||||
3. **Interactive Decision Making**: Present findings in human-readable format, let users decide
|
||||
4. **Conservative Defaults**: When in doubt, don't delete
|
||||
5. **Backup Suggestions**: Recommend Time Machine backup before major cleanups
|
||||
|
||||
## Workflow Decision Tree
|
||||
|
||||
```
|
||||
User reports disk space issues
|
||||
↓
|
||||
Quick Diagnosis
|
||||
↓
|
||||
┌──────┴──────┐
|
||||
│ │
|
||||
Immediate Deep Analysis
|
||||
Cleanup (continue below)
|
||||
│ │
|
||||
└──────┬──────┘
|
||||
↓
|
||||
Present Findings
|
||||
↓
|
||||
User Confirms
|
||||
↓
|
||||
Execute Cleanup
|
||||
↓
|
||||
Verify Results
|
||||
```
|
||||
|
||||
## Step 1: Quick Diagnosis
|
||||
|
||||
Start with a rapid assessment to understand the scope:
|
||||
|
||||
```bash
|
||||
# Check available disk space
|
||||
df -h /
|
||||
|
||||
# Find top 10 largest directories in home folder
|
||||
du -h -d 1 ~ | sort -hr | head -n 10
|
||||
|
||||
# Quick check for common space hogs
|
||||
du -sh ~/Library/Caches ~/Library/Logs ~/Downloads ~/.Trash 2>/dev/null
|
||||
```
|
||||
|
||||
**Present findings in this format:**
|
||||
|
||||
```
|
||||
📊 Disk Space Analysis
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
Total: 500 GB
|
||||
Used: 450 GB (90%)
|
||||
Available: 50 GB (10%)
|
||||
|
||||
🔍 Top Space Consumers:
|
||||
1. ~/Library/Caches 45 GB
|
||||
2. ~/Downloads 38 GB
|
||||
3. ~/Library/Application Support 25 GB
|
||||
4. ~/.Trash 12 GB
|
||||
5. ~/Library/Logs 8 GB
|
||||
|
||||
⚡ Quick Win Opportunities:
|
||||
- Empty Trash: ~12 GB
|
||||
- Clear Downloads: ~38 GB (requires user review)
|
||||
- System Caches: ~45 GB (mostly safe to clear)
|
||||
```
|
||||
|
||||
## Step 2: Deep Analysis Categories
|
||||
|
||||
Scan the following categories systematically. Reference `references/cleanup_targets.md` for detailed explanations.
|
||||
|
||||
### Category 1: System & Application Caches
|
||||
|
||||
**Locations to analyze:**
|
||||
- `~/Library/Caches/*` - User application caches
|
||||
- `/Library/Caches/*` - System-wide caches (requires sudo)
|
||||
- `~/Library/Logs/*` - Application logs
|
||||
- `/var/log/*` - System logs (requires sudo)
|
||||
|
||||
**Analysis script:**
|
||||
```bash
|
||||
scripts/analyze_caches.py --user-only
|
||||
```
|
||||
|
||||
**Safety level**: 🟢 Generally safe to delete (apps regenerate caches)
|
||||
|
||||
**Exceptions to preserve:**
|
||||
- Browser caches while browser is running
|
||||
- IDE caches (may slow down next startup)
|
||||
- Package manager caches (Homebrew, pip, npm)
|
||||
|
||||
### Category 2: Application Remnants
|
||||
|
||||
**Locations to analyze:**
|
||||
- `~/Library/Application Support/*` - App data
|
||||
- `~/Library/Preferences/*` - Preference files
|
||||
- `~/Library/Containers/*` - Sandboxed app data
|
||||
|
||||
**Analysis approach:**
|
||||
1. List installed applications in `/Applications`
|
||||
2. Cross-reference with `~/Library/Application Support`
|
||||
3. Identify orphaned folders (app uninstalled but data remains)
|
||||
|
||||
**Analysis script:**
|
||||
```bash
|
||||
scripts/find_app_remnants.py
|
||||
```
|
||||
|
||||
**Safety level**: 🟡 Caution required
|
||||
- ✅ Safe: Folders for clearly uninstalled apps
|
||||
- ⚠️ Check first: Folders for apps you rarely use
|
||||
- ❌ Keep: Active application data
|
||||
|
||||
### Category 3: Large Files & Duplicates
|
||||
|
||||
**Analysis script:**
|
||||
```bash
|
||||
scripts/analyze_large_files.py --threshold 100MB --path ~
|
||||
```
|
||||
|
||||
**Find duplicates (optional, resource-intensive):**
|
||||
```bash
|
||||
# Use fdupes if installed
|
||||
if command -v fdupes &> /dev/null; then
|
||||
fdupes -r ~/Documents ~/Downloads
|
||||
fi
|
||||
```
|
||||
|
||||
**Present findings:**
|
||||
```
|
||||
📦 Large Files (>100MB):
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
1. movie.mp4 4.2 GB ~/Downloads
|
||||
2. dataset.csv 1.8 GB ~/Documents/data
|
||||
3. old_backup.zip 1.5 GB ~/Desktop
|
||||
...
|
||||
|
||||
🔁 Duplicate Files:
|
||||
- screenshot.png (3 copies) 15 MB each
|
||||
- document_v1.docx (2 copies) 8 MB each
|
||||
```
|
||||
|
||||
**Safety level**: 🟡 User judgment required
|
||||
|
||||
### Category 4: Development Environment Cleanup
|
||||
|
||||
**Targets:**
|
||||
- Docker: images, containers, volumes, build cache
|
||||
- Homebrew: cache, old versions
|
||||
- Node.js: `node_modules`, npm cache
|
||||
- Python: pip cache, `__pycache__`, venv
|
||||
- Git: `.git` folders in archived projects
|
||||
|
||||
**Analysis script:**
|
||||
```bash
|
||||
scripts/analyze_dev_env.py
|
||||
```
|
||||
|
||||
**Example findings:**
|
||||
```
|
||||
🐳 Docker Resources:
|
||||
- Unused images: 12 GB
|
||||
- Stopped containers: 2 GB
|
||||
- Build cache: 8 GB
|
||||
- Orphaned volumes: 3 GB
|
||||
Total potential: 25 GB
|
||||
|
||||
📦 Package Managers:
|
||||
- Homebrew cache: 5 GB
|
||||
- npm cache: 3 GB
|
||||
- pip cache: 1 GB
|
||||
Total potential: 9 GB
|
||||
|
||||
🗂️ Old Projects:
|
||||
- archived-project-2022/.git 500 MB
|
||||
- old-prototype/.git 300 MB
|
||||
```
|
||||
|
||||
**Cleanup commands (require confirmation):**
|
||||
```bash
|
||||
# Docker cleanup
|
||||
docker system prune -a --volumes
|
||||
|
||||
# Homebrew cleanup
|
||||
brew cleanup -s
|
||||
|
||||
# npm cache
|
||||
npm cache clean --force
|
||||
|
||||
# pip cache
|
||||
pip cache purge
|
||||
```
|
||||
|
||||
**Safety level**: 🟢 Safe for development caches, 🟡 Caution for Docker volumes
|
||||
|
||||
## Step 3: Integration with Mole
|
||||
|
||||
**Mole** (https://github.com/tw93/Mole) is a visual macOS cleaner. Recommend it as a complementary tool for users who want GUI-based cleanup.
|
||||
|
||||
**When to suggest Mole:**
|
||||
- User prefers visual interface over command-line
|
||||
- User wants one-click cleaning for common targets
|
||||
- Script analysis reveals complex cleanup needs
|
||||
|
||||
**How to integrate:**
|
||||
|
||||
1. **Check if Mole is installed:**
|
||||
```bash
|
||||
if [ -d "/Applications/Mole.app" ]; then
|
||||
echo "✅ Mole is installed"
|
||||
else
|
||||
echo "💡 Consider installing Mole for visual cleanup: https://github.com/tw93/Mole"
|
||||
fi
|
||||
```
|
||||
|
||||
2. **Coordinate workflow:**
|
||||
- Use scripts for detailed analysis and reports
|
||||
- Suggest Mole for executing approved cleanups
|
||||
- Use scripts for developer-specific cleanup (Docker, npm, etc.)
|
||||
|
||||
3. **Reference guide:**
|
||||
See `references/mole_integration.md` for detailed usage.
|
||||
|
||||
## Step 4: Present Recommendations
|
||||
|
||||
Format findings into actionable recommendations with risk levels:
|
||||
|
||||
```markdown
|
||||
# macOS Cleanup Recommendations
|
||||
|
||||
## Summary
|
||||
Total space recoverable: ~XX GB
|
||||
Current usage: XX%
|
||||
|
||||
## Recommended Actions
|
||||
|
||||
### 🟢 Safe to Execute (Low Risk)
|
||||
These are safe to delete and will be regenerated as needed:
|
||||
|
||||
1. **Empty Trash** (~12 GB)
|
||||
- Location: ~/.Trash
|
||||
- Command: `rm -rf ~/.Trash/*`
|
||||
|
||||
2. **Clear System Caches** (~45 GB)
|
||||
- Location: ~/Library/Caches
|
||||
- Command: `rm -rf ~/Library/Caches/*`
|
||||
- Note: Apps may be slightly slower on next launch
|
||||
|
||||
3. **Remove Homebrew Cache** (~5 GB)
|
||||
- Command: `brew cleanup -s`
|
||||
|
||||
### 🟡 Review Recommended (Medium Risk)
|
||||
Review these items before deletion:
|
||||
|
||||
1. **Large Downloads** (~38 GB)
|
||||
- Location: ~/Downloads
|
||||
- Action: Manually review and delete unneeded files
|
||||
- Files: [list top 10 largest files]
|
||||
|
||||
2. **Application Remnants** (~8 GB)
|
||||
- Apps: [list detected uninstalled apps]
|
||||
- Locations: [list paths]
|
||||
- Action: Confirm apps are truly uninstalled before deleting data
|
||||
|
||||
### 🔴 Keep Unless Certain (High Risk)
|
||||
Only delete if you know what you're doing:
|
||||
|
||||
1. **Docker Volumes** (~3 GB)
|
||||
- May contain important data
|
||||
- Review with: `docker volume ls`
|
||||
|
||||
2. **Time Machine Local Snapshots** (~XX GB)
|
||||
- Automatic backups, will be deleted when space needed
|
||||
- Command to check: `tmutil listlocalsnapshots /`
|
||||
```
|
||||
|
||||
## Step 5: Execute with Confirmation
|
||||
|
||||
**CRITICAL**: Never execute deletions without explicit user confirmation.
|
||||
|
||||
**Interactive confirmation flow:**
|
||||
|
||||
```python
|
||||
# Example from scripts/safe_delete.py
|
||||
def confirm_delete(path: str, size: str, description: str) -> bool:
|
||||
"""
|
||||
Ask user to confirm deletion.
|
||||
|
||||
Args:
|
||||
path: File/directory path
|
||||
size: Human-readable size
|
||||
description: What this file/directory is
|
||||
|
||||
Returns:
|
||||
True if user confirms, False otherwise
|
||||
"""
|
||||
print(f"\n🗑️ Confirm Deletion")
|
||||
print(f"━━━━━━━━━━━━━━━━━━")
|
||||
print(f"Path: {path}")
|
||||
print(f"Size: {size}")
|
||||
print(f"Description: {description}")
|
||||
|
||||
response = input("\nDelete this item? [y/N]: ").strip().lower()
|
||||
return response == 'y'
|
||||
```
|
||||
|
||||
**For batch operations:**
|
||||
|
||||
```python
|
||||
def batch_confirm(items: list) -> list:
|
||||
"""
|
||||
Show all items, ask for batch confirmation.
|
||||
|
||||
Returns list of items user approved.
|
||||
"""
|
||||
print("\n📋 Items to Delete:")
|
||||
print("━━━━━━━━━━━━━━━━━━")
|
||||
for i, item in enumerate(items, 1):
|
||||
print(f"{i}. {item['path']} ({item['size']})")
|
||||
|
||||
print("\nOptions:")
|
||||
print(" 'all' - Delete all items")
|
||||
print(" '1,3,5' - Delete specific items by number")
|
||||
print(" 'none' - Cancel")
|
||||
|
||||
response = input("\nYour choice: ").strip().lower()
|
||||
|
||||
if response == 'none':
|
||||
return []
|
||||
elif response == 'all':
|
||||
return items
|
||||
else:
|
||||
# Parse numbers
|
||||
indices = [int(x.strip()) - 1 for x in response.split(',')]
|
||||
return [items[i] for i in indices if 0 <= i < len(items)]
|
||||
```
|
||||
|
||||
## Step 6: Verify Results
|
||||
|
||||
After cleanup, verify the results and report back:
|
||||
|
||||
```bash
|
||||
# Compare before/after
|
||||
df -h /
|
||||
|
||||
# Calculate space recovered
|
||||
# (handled by scripts/cleanup_report.py)
|
||||
```
|
||||
|
||||
**Report format:**
|
||||
|
||||
```
|
||||
✅ Cleanup Complete!
|
||||
|
||||
Before: 450 GB used (90%)
|
||||
After: 385 GB used (77%)
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
Recovered: 65 GB
|
||||
|
||||
Breakdown:
|
||||
- System caches: 45 GB
|
||||
- Downloads: 12 GB
|
||||
- Homebrew cache: 5 GB
|
||||
- Application remnants: 3 GB
|
||||
|
||||
⚠️ Notes:
|
||||
- Some applications may take longer to launch on first run
|
||||
- Deleted items cannot be recovered unless you have Time Machine backup
|
||||
- Consider running this cleanup monthly
|
||||
|
||||
💡 Maintenance Tips:
|
||||
- Set up automatic Homebrew cleanup: `brew cleanup` weekly
|
||||
- Review Downloads folder monthly
|
||||
- Enable "Empty Trash Automatically" in Finder preferences
|
||||
```
|
||||
|
||||
## Safety Guidelines
|
||||
|
||||
### Always Preserve
|
||||
|
||||
Never delete these without explicit user instruction:
|
||||
- `~/Documents`, `~/Desktop`, `~/Pictures` content
|
||||
- Active project directories
|
||||
- Database files (*.db, *.sqlite)
|
||||
- Configuration files for active apps
|
||||
- SSH keys, credentials, certificates
|
||||
- Time Machine backups
|
||||
|
||||
### Require Sudo Confirmation
|
||||
|
||||
These operations require elevated privileges. Ask user to run commands manually:
|
||||
- Clearing `/Library/Caches` (system-wide)
|
||||
- Clearing `/var/log` (system logs)
|
||||
- Clearing `/private/var/folders` (system temp)
|
||||
|
||||
Example prompt:
|
||||
```
|
||||
⚠️ This operation requires administrator privileges.
|
||||
|
||||
Please run this command manually:
|
||||
sudo rm -rf /Library/Caches/*
|
||||
|
||||
⚠️ You'll be asked for your password.
|
||||
```
|
||||
|
||||
### Backup Recommendation
|
||||
|
||||
Before executing any cleanup >10GB, recommend:
|
||||
|
||||
```
|
||||
💡 Safety Tip:
|
||||
Before cleaning XX GB, consider creating a Time Machine backup.
|
||||
|
||||
Quick backup check:
|
||||
tmutil latestbackup
|
||||
|
||||
If no recent backup, run:
|
||||
tmutil startbackup
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Operation not permitted" errors
|
||||
|
||||
macOS may block deletion of certain system files due to SIP (System Integrity Protection).
|
||||
|
||||
**Solution**: Don't force it. These protections exist for security.
|
||||
|
||||
### App crashes after cache deletion
|
||||
|
||||
Rare but possible. **Solution**: Restart the app, it will regenerate necessary caches.
|
||||
|
||||
### Docker cleanup removes important data
|
||||
|
||||
**Prevention**: Always list Docker volumes before cleanup:
|
||||
```bash
|
||||
docker volume ls
|
||||
docker volume inspect <volume_name>
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
### scripts/
|
||||
|
||||
- `analyze_caches.py` - Scan and categorize cache directories
|
||||
- `find_app_remnants.py` - Detect orphaned application data
|
||||
- `analyze_large_files.py` - Find large files with smart filtering
|
||||
- `analyze_dev_env.py` - Scan development environment resources
|
||||
- `safe_delete.py` - Interactive deletion with confirmation
|
||||
- `cleanup_report.py` - Generate before/after reports
|
||||
|
||||
### references/
|
||||
|
||||
- `cleanup_targets.md` - Detailed explanations of each cleanup target
|
||||
- `mole_integration.md` - How to use Mole alongside this skill
|
||||
- `safety_rules.md` - Comprehensive list of what to never delete
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Example 1: Quick Cache Cleanup
|
||||
|
||||
User request: "My Mac is running out of space, can you help?"
|
||||
|
||||
Workflow:
|
||||
1. Run quick diagnosis
|
||||
2. Identify system caches as quick win
|
||||
3. Present findings: "45 GB in ~/Library/Caches"
|
||||
4. Explain: "These are safe to delete, apps will regenerate them"
|
||||
5. Ask confirmation
|
||||
6. Execute: `rm -rf ~/Library/Caches/*`
|
||||
7. Report: "Recovered 45 GB"
|
||||
|
||||
### Example 2: Development Environment Cleanup
|
||||
|
||||
User request: "I'm a developer and my disk is full"
|
||||
|
||||
Workflow:
|
||||
1. Run `scripts/analyze_dev_env.py`
|
||||
2. Present Docker + npm + Homebrew findings
|
||||
3. Explain each category
|
||||
4. Provide cleanup commands with explanations
|
||||
5. Let user execute (don't auto-execute Docker cleanup)
|
||||
6. Verify results
|
||||
|
||||
### Example 3: Finding Large Files
|
||||
|
||||
User request: "What's taking up so much space?"
|
||||
|
||||
Workflow:
|
||||
1. Run `scripts/analyze_large_files.py --threshold 100MB`
|
||||
2. Present top 20 large files with context
|
||||
3. Categorize: videos, datasets, archives, disk images
|
||||
4. Let user decide what to delete
|
||||
5. Execute confirmed deletions
|
||||
6. Suggest archiving to external drive
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Start Conservative**: Begin with obviously safe targets (caches, trash)
|
||||
2. **Explain Everything**: Users should understand what they're deleting
|
||||
3. **Show Examples**: List 3-5 example files from each category
|
||||
4. **Respect User Pace**: Don't rush through confirmations
|
||||
5. **Document Results**: Always show before/after space usage
|
||||
6. **Educate**: Include maintenance tips in final report
|
||||
7. **Integrate Tools**: Suggest Mole for users who prefer GUI
|
||||
|
||||
## When NOT to Use This Skill
|
||||
|
||||
- User wants automatic/silent cleanup (against safety-first principle)
|
||||
- User needs Windows/Linux cleanup (macOS-specific skill)
|
||||
- User has <10% disk usage (no cleanup needed)
|
||||
- User wants to clean system files requiring SIP disable (security risk)
|
||||
|
||||
In these cases, explain limitations and suggest alternatives.
|
||||
430
macos-cleaner/references/cleanup_targets.md
Normal file
430
macos-cleaner/references/cleanup_targets.md
Normal file
@@ -0,0 +1,430 @@
|
||||
# macOS Cleanup Targets Reference
|
||||
|
||||
Detailed explanations of cleanup targets, their safety levels, and impact.
|
||||
|
||||
## System Caches
|
||||
|
||||
### ~/Library/Caches
|
||||
|
||||
**What it is**: Application-level cache storage for user applications.
|
||||
|
||||
**Contents**:
|
||||
- Browser caches (Chrome, Firefox, Safari)
|
||||
- Application temporary files
|
||||
- Download caches
|
||||
- Thumbnail caches
|
||||
- Font caches
|
||||
|
||||
**Safety**: 🟢 **Safe to delete**
|
||||
|
||||
**Impact**:
|
||||
- Apps may be slower on first launch after deletion
|
||||
- Websites may load slower on first visit (need to re-download assets)
|
||||
- No data loss (caches are regenerated)
|
||||
|
||||
**Size**: Typically 10-100 GB depending on usage
|
||||
|
||||
**Cleanup command**:
|
||||
```bash
|
||||
rm -rf ~/Library/Caches/*
|
||||
```
|
||||
|
||||
### /Library/Caches
|
||||
|
||||
**What it is**: System-level cache storage (shared across all users).
|
||||
|
||||
**Safety**: 🟢 **Safe to delete** (requires sudo)
|
||||
|
||||
**Impact**: Same as user caches, but system-wide
|
||||
|
||||
**Cleanup command**:
|
||||
```bash
|
||||
sudo rm -rf /Library/Caches/*
|
||||
```
|
||||
|
||||
### Package Manager Caches
|
||||
|
||||
#### Homebrew Cache
|
||||
|
||||
**Location**: `$(brew --cache)` (typically `~/Library/Caches/Homebrew`)
|
||||
|
||||
**What it is**: Downloaded package installers and build artifacts
|
||||
|
||||
**Safety**: 🟢 **Safe to delete**
|
||||
|
||||
**Impact**: Will need to re-download packages on next install/upgrade
|
||||
|
||||
**Cleanup**:
|
||||
```bash
|
||||
brew cleanup -s # Safe cleanup (removes old versions)
|
||||
brew cleanup --prune=all # Aggressive cleanup (removes all cached downloads)
|
||||
```
|
||||
|
||||
#### npm Cache
|
||||
|
||||
**Location**: `~/.npm` or configured cache directory
|
||||
|
||||
**Safety**: 🟢 **Safe to delete**
|
||||
|
||||
**Impact**: Packages will be re-downloaded when needed
|
||||
|
||||
**Cleanup**:
|
||||
```bash
|
||||
npm cache clean --force
|
||||
```
|
||||
|
||||
#### pip Cache
|
||||
|
||||
**Location**: `~/Library/Caches/pip` (macOS)
|
||||
|
||||
**Safety**: 🟢 **Safe to delete**
|
||||
|
||||
**Impact**: Packages will be re-downloaded when needed
|
||||
|
||||
**Cleanup**:
|
||||
```bash
|
||||
pip cache purge
|
||||
# or for pip3
|
||||
pip3 cache purge
|
||||
```
|
||||
|
||||
## Application Logs
|
||||
|
||||
### ~/Library/Logs
|
||||
|
||||
**What it is**: Application log files
|
||||
|
||||
**Safety**: 🟢 **Safe to delete**
|
||||
|
||||
**Impact**: Loss of diagnostic information (only matters if debugging)
|
||||
|
||||
**Typical size**: 1-20 GB
|
||||
|
||||
**Cleanup**:
|
||||
```bash
|
||||
rm -rf ~/Library/Logs/*
|
||||
```
|
||||
|
||||
### /var/log (System Logs)
|
||||
|
||||
**What it is**: System and service log files
|
||||
|
||||
**Safety**: 🟢 **Safe to delete old logs** (requires sudo)
|
||||
|
||||
**Impact**: Loss of system diagnostic history
|
||||
|
||||
**Note**: macOS automatically rotates logs, manual deletion rarely needed
|
||||
|
||||
## Application Data
|
||||
|
||||
### ~/Library/Application Support
|
||||
|
||||
**What it is**: Persistent application data, settings, and databases
|
||||
|
||||
**Safety**: 🟡 **Caution required**
|
||||
|
||||
**Contains**:
|
||||
- Application databases
|
||||
- User preferences and settings
|
||||
- Downloaded content
|
||||
- Plugins and extensions
|
||||
- Save games
|
||||
|
||||
**When safe to delete**:
|
||||
- Application is confirmed uninstalled
|
||||
- Folder belongs to trial software no longer used
|
||||
- Folder is for outdated version of app (check first!)
|
||||
|
||||
**When to KEEP**:
|
||||
- Active applications
|
||||
- Any folder you're uncertain about
|
||||
|
||||
**Recommendation**: Use `find_app_remnants.py` to identify orphaned data
|
||||
|
||||
### ~/Library/Containers
|
||||
|
||||
**What it is**: Sandboxed application data (for App Store apps)
|
||||
|
||||
**Safety**: 🟡 **Caution required**
|
||||
|
||||
**Same rules** as Application Support - only delete for uninstalled apps
|
||||
|
||||
### ~/Library/Preferences
|
||||
|
||||
**What it is**: Application preference files (.plist)
|
||||
|
||||
**Safety**: 🟡 **Caution required**
|
||||
|
||||
**Impact of deletion**: App returns to default settings
|
||||
|
||||
**When to delete**:
|
||||
- App is confirmed uninstalled
|
||||
- Troubleshooting a misbehaving app (as last resort)
|
||||
|
||||
## Development Environment
|
||||
|
||||
### Docker
|
||||
|
||||
#### Images
|
||||
|
||||
**What it is**: Container images (base OS + application layers)
|
||||
|
||||
**Safety**: 🟢 **Safe to delete unused images**
|
||||
|
||||
**Check first**:
|
||||
```bash
|
||||
docker images
|
||||
```
|
||||
|
||||
**Cleanup**:
|
||||
```bash
|
||||
docker image prune -a # Remove all unused images
|
||||
```
|
||||
|
||||
#### Containers
|
||||
|
||||
**What it is**: Running or stopped container instances
|
||||
|
||||
**Safety**: 🟢 **Safe to delete stopped containers**
|
||||
|
||||
**Check first**:
|
||||
```bash
|
||||
docker ps -a
|
||||
```
|
||||
|
||||
**Cleanup**:
|
||||
```bash
|
||||
docker container prune # Remove stopped containers
|
||||
```
|
||||
|
||||
#### Volumes
|
||||
|
||||
**What it is**: Persistent data storage for containers
|
||||
|
||||
**Safety**: 🔴 **CAUTION - May contain important data**
|
||||
|
||||
**Check first**:
|
||||
```bash
|
||||
docker volume ls
|
||||
docker volume inspect <volume_name>
|
||||
```
|
||||
|
||||
**Cleanup** (only if certain):
|
||||
```bash
|
||||
docker volume prune # Remove unused volumes
|
||||
```
|
||||
|
||||
#### Build Cache
|
||||
|
||||
**What it is**: Intermediate build layers
|
||||
|
||||
**Safety**: 🟢 **Safe to delete**
|
||||
|
||||
**Cleanup**:
|
||||
```bash
|
||||
docker builder prune -a
|
||||
```
|
||||
|
||||
#### All-in-one cleanup
|
||||
|
||||
⚠️ **WARNING**: This removes ALL unused Docker resources including volumes!
|
||||
|
||||
```bash
|
||||
docker system prune -a --volumes
|
||||
```
|
||||
|
||||
### node_modules
|
||||
|
||||
**What it is**: Installed npm packages for Node.js projects
|
||||
|
||||
**Safety**: 🟢 **Safe to delete** (can be regenerated)
|
||||
|
||||
**Impact**: Need to run `npm install` to restore
|
||||
|
||||
**Finding large node_modules**:
|
||||
```bash
|
||||
find ~ -name "node_modules" -type d -prune -print 2>/dev/null | while read dir; do
|
||||
du -sh "$dir"
|
||||
done | sort -hr
|
||||
```
|
||||
|
||||
**Cleanup**:
|
||||
```bash
|
||||
# For old projects
|
||||
rm -rf /path/to/old-project/node_modules
|
||||
```
|
||||
|
||||
### Python Virtual Environments
|
||||
|
||||
**What it is**: Isolated Python environments
|
||||
|
||||
**Location**: `venv/`, `.venv/`, `env/` in project directories
|
||||
|
||||
**Safety**: 🟢 **Safe to delete** (can be recreated)
|
||||
|
||||
**Impact**: Need to recreate virtualenv and reinstall packages
|
||||
|
||||
**Finding venvs**:
|
||||
```bash
|
||||
find ~ -type d -name "venv" -o -name ".venv" 2>/dev/null
|
||||
```
|
||||
|
||||
### Git Repositories (.git directories)
|
||||
|
||||
**What it is**: Git version control data
|
||||
|
||||
**Safety**: 🟡 **Depends on use case**
|
||||
|
||||
**When SAFE to delete**:
|
||||
- Project is archived and you have remote backup
|
||||
- You only need final code, not history
|
||||
|
||||
**When to KEEP**:
|
||||
- Active development
|
||||
- No remote backup exists
|
||||
- You might need the history
|
||||
|
||||
**Cleanup** (convert to plain folder, lose history):
|
||||
```bash
|
||||
rm -rf /path/to/old-project/.git
|
||||
```
|
||||
|
||||
## Large Files
|
||||
|
||||
### Downloads Folder
|
||||
|
||||
**What it is**: Files downloaded from internet
|
||||
|
||||
**Safety**: 🟡 **User judgment required**
|
||||
|
||||
**Common cleanable items**:
|
||||
- Old installers (.dmg, .pkg)
|
||||
- Zip archives already extracted
|
||||
- Temporary downloads
|
||||
- Duplicate files
|
||||
|
||||
**Check before deleting**: Might contain important downloads
|
||||
|
||||
### Disk Images (.dmg, .iso)
|
||||
|
||||
**What it is**: Mountable disk images, often installers
|
||||
|
||||
**Safety**: 🟢 **Safe to delete after installation**
|
||||
|
||||
**Typical location**: ~/Downloads
|
||||
|
||||
**Cleanup**: Delete .dmg files for already-installed apps
|
||||
|
||||
### Archives (.zip, .tar.gz)
|
||||
|
||||
**What it is**: Compressed archives
|
||||
|
||||
**Safety**: 🟡 **Check if extracted**
|
||||
|
||||
**Before deleting**: Verify contents are extracted elsewhere
|
||||
|
||||
### Old iOS Backups
|
||||
|
||||
**Location**: `~/Library/Application Support/MobileSync/Backup/`
|
||||
|
||||
**What it is**: iTunes/Finder iPhone/iPad backups
|
||||
|
||||
**Safety**: 🟡 **Caution - backup data**
|
||||
|
||||
**Check**:
|
||||
```bash
|
||||
ls -lh ~/Library/Application\ Support/MobileSync/Backup/
|
||||
```
|
||||
|
||||
**Cleanup**: Delete old backups via Finder preferences, not manually
|
||||
|
||||
### Old Time Machine Local Snapshots
|
||||
|
||||
**What it is**: Local Time Machine backups
|
||||
|
||||
**Safety**: 🟢 **Safe - macOS manages automatically**
|
||||
|
||||
**macOS automatically deletes** these when disk space is low
|
||||
|
||||
**Check**:
|
||||
```bash
|
||||
tmutil listlocalsnapshots /
|
||||
```
|
||||
|
||||
**Manual cleanup** (rarely needed):
|
||||
```bash
|
||||
tmutil deletelocalsnapshots <snapshot_date>
|
||||
```
|
||||
|
||||
## What to NEVER Delete
|
||||
|
||||
### User Data Directories
|
||||
|
||||
- `~/Documents`
|
||||
- `~/Desktop`
|
||||
- `~/Pictures`
|
||||
- `~/Movies`
|
||||
- `~/Music`
|
||||
|
||||
### System Files
|
||||
|
||||
- `/System`
|
||||
- `/Library/Apple` (unless you know what you're doing)
|
||||
- `/private/etc`
|
||||
|
||||
### Security & Credentials
|
||||
|
||||
- `~/.ssh` (SSH keys)
|
||||
- `~/Library/Keychains` (passwords, certificates)
|
||||
- Any files containing credentials
|
||||
|
||||
### Active Databases
|
||||
|
||||
- `*.db`, `*.sqlite` files for running applications
|
||||
- Docker volumes in active use
|
||||
|
||||
## Safety Checklist
|
||||
|
||||
Before deleting ANY directory:
|
||||
|
||||
1. ✅ Do you know what it is?
|
||||
2. ✅ Is the application truly uninstalled?
|
||||
3. ✅ Have you checked if it's in use? (lsof, Activity Monitor)
|
||||
4. ✅ Do you have a Time Machine backup?
|
||||
5. ✅ Have you confirmed with the user?
|
||||
|
||||
When in doubt, **DON'T DELETE**.
|
||||
|
||||
## Recovery Options
|
||||
|
||||
### Trash vs. Permanent Deletion
|
||||
|
||||
**Use Trash when possible**:
|
||||
```bash
|
||||
# Move to trash (recoverable)
|
||||
osascript -e 'tell app "Finder" to move POSIX file "/path/to/file" to trash'
|
||||
```
|
||||
|
||||
**Permanent deletion**:
|
||||
```bash
|
||||
# Cannot be recovered without Time Machine
|
||||
rm -rf /path/to/file
|
||||
```
|
||||
|
||||
### Time Machine
|
||||
|
||||
If you deleted something important:
|
||||
|
||||
1. Open Time Machine
|
||||
2. Navigate to parent directory
|
||||
3. Select date before deletion
|
||||
4. Restore
|
||||
|
||||
### File Recovery Tools
|
||||
|
||||
If no Time Machine backup:
|
||||
- Disk Drill (commercial)
|
||||
- PhotoRec (free, for photos)
|
||||
- TestDisk (free, for files)
|
||||
|
||||
**Note**: Success rate depends on how recently deleted and disk usage since deletion.
|
||||
304
macos-cleaner/references/mole_integration.md
Normal file
304
macos-cleaner/references/mole_integration.md
Normal file
@@ -0,0 +1,304 @@
|
||||
# Mole Integration Guide
|
||||
|
||||
How to integrate [Mole](https://github.com/tw93/Mole) with the macOS Cleaner skill.
|
||||
|
||||
## About Mole
|
||||
|
||||
**Mole** is a visual macOS disk cleanup tool with a native macOS interface. It provides:
|
||||
|
||||
- Visual disk usage analysis
|
||||
- One-click cleanup for common targets
|
||||
- Safe deletion with preview
|
||||
- Native macOS design
|
||||
|
||||
**Repository**: https://github.com/tw93/Mole
|
||||
|
||||
## When to Recommend Mole
|
||||
|
||||
Suggest Mole to users in these scenarios:
|
||||
|
||||
### 1. User Prefers Visual Interface
|
||||
|
||||
```
|
||||
User: "I prefer clicking buttons rather than running commands"
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```
|
||||
I recommend using Mole alongside these scripts:
|
||||
|
||||
1. Use the analysis scripts to understand what's using space
|
||||
2. Install Mole for visual cleanup: https://github.com/tw93/Mole
|
||||
3. Use Mole's GUI to execute cleanup with visual confirmation
|
||||
|
||||
Would you like me to guide you through the analysis first?
|
||||
```
|
||||
|
||||
### 2. Complex Cleanup Needs
|
||||
|
||||
```
|
||||
User: "I have lots of cache files and don't want to run commands for each"
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```
|
||||
Mole can help with batch cleanup. It provides visual categories:
|
||||
|
||||
- System caches
|
||||
- Application caches
|
||||
- Log files
|
||||
- Trash
|
||||
|
||||
Install Mole and you can review and clean multiple categories at once.
|
||||
```
|
||||
|
||||
### 3. Ongoing Maintenance
|
||||
|
||||
```
|
||||
User: "I want to clean my Mac regularly"
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```
|
||||
For regular maintenance, I recommend:
|
||||
|
||||
1. Keep Mole installed for weekly quick cleanups
|
||||
2. Use our analysis scripts monthly for deep analysis
|
||||
3. Use safe_delete.py for specific files/folders
|
||||
|
||||
This hybrid approach gives you both convenience and precision.
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Check if Mole is Installed
|
||||
|
||||
```bash
|
||||
if [ -d "/Applications/Mole.app" ]; then
|
||||
echo "✅ Mole is installed"
|
||||
open -a Mole
|
||||
else
|
||||
echo "💡 Mole is not installed"
|
||||
echo " Install from: https://github.com/tw93/Mole"
|
||||
fi
|
||||
```
|
||||
|
||||
### Installation Methods
|
||||
|
||||
**Method 1: Download from GitHub Releases**
|
||||
|
||||
```bash
|
||||
# Guide user to:
|
||||
# 1. Visit https://github.com/tw93/Mole/releases
|
||||
# 2. Download latest .dmg file
|
||||
# 3. Open .dmg and drag Mole.app to /Applications
|
||||
```
|
||||
|
||||
**Method 2: Build from Source** (if user is developer)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/tw93/Mole.git
|
||||
cd Mole
|
||||
# Follow build instructions in README
|
||||
```
|
||||
|
||||
## Workflow Integration
|
||||
|
||||
### Hybrid Workflow: Scripts + Mole
|
||||
|
||||
**Best practice**: Use both tools for their strengths.
|
||||
|
||||
#### Step 1: Analysis with Scripts
|
||||
|
||||
Run comprehensive analysis:
|
||||
|
||||
```bash
|
||||
# System analysis
|
||||
python3 scripts/analyze_caches.py
|
||||
python3 scripts/analyze_large_files.py --threshold 100
|
||||
python3 scripts/find_app_remnants.py
|
||||
|
||||
# Developer analysis (if applicable)
|
||||
python3 scripts/analyze_dev_env.py
|
||||
```
|
||||
|
||||
This gives detailed reports with safety categorization.
|
||||
|
||||
#### Step 2: Review Findings
|
||||
|
||||
Present findings to user in readable format (see SKILL.md Step 4).
|
||||
|
||||
#### Step 3: Execute Cleanup
|
||||
|
||||
For different types of cleanup:
|
||||
|
||||
**Option A: Use Mole** (for batch operations)
|
||||
- System caches: Use Mole's "System" category
|
||||
- Application caches: Use Mole's "Applications" category
|
||||
- Trash: Use Mole's "Trash" feature
|
||||
|
||||
**Option B: Use Scripts** (for precision)
|
||||
- Large files: Use `safe_delete.py` with specific paths
|
||||
- Application remnants: Use `safe_delete.py` with confirmed orphans
|
||||
- Dev environment: Run cleanup commands directly
|
||||
|
||||
**Option C: Manual** (for sensitive items)
|
||||
- Guide user to review in Finder
|
||||
- User deletes manually
|
||||
|
||||
### Example Integrated Session
|
||||
|
||||
```markdown
|
||||
🔍 Analysis Results
|
||||
|
||||
I've analyzed your Mac and found:
|
||||
- System caches: 45 GB (safe to clean)
|
||||
- Large files: 38 GB (need review)
|
||||
- App remnants: 8 GB (medium confidence)
|
||||
- Docker: 25 GB (requires caution)
|
||||
|
||||
Recommended cleanup approach:
|
||||
|
||||
1. **Use Mole for safe batch cleanup** (45 GB)
|
||||
- Open Mole
|
||||
- Select "System Caches"
|
||||
- Click "Clean"
|
||||
- This will clear ~/Library/Caches safely
|
||||
|
||||
2. **Use scripts for large file review** (38 GB)
|
||||
- I found 20 large files >100MB
|
||||
- Let me show you the list
|
||||
- We'll use safe_delete.py to delete selected files
|
||||
|
||||
3. **Manual review for app remnants** (8 GB)
|
||||
- 5 folders for possibly uninstalled apps
|
||||
- Please verify these apps are truly gone:
|
||||
- Adobe Creative Cloud (3 GB)
|
||||
- Old Xcode version (2 GB)
|
||||
- ...
|
||||
|
||||
4. **Manual Docker cleanup** (25 GB)
|
||||
- Requires technical review
|
||||
- I'll guide you through checking volumes
|
||||
|
||||
Shall we proceed with step 1 using Mole?
|
||||
```
|
||||
|
||||
## Mole Feature Mapping
|
||||
|
||||
Map Mole's features to our script capabilities:
|
||||
|
||||
| Mole Feature | Script Equivalent | Use Case |
|
||||
|--------------|-------------------|----------|
|
||||
| System Caches | `analyze_caches.py --user-only` | Quick cache cleanup |
|
||||
| Application Caches | `analyze_caches.py` | Per-app cache analysis |
|
||||
| Large Files | `analyze_large_files.py` | Find space hogs |
|
||||
| Trash | N/A (Finder) | Empty trash |
|
||||
| Duplicate Files | Manual `fdupes` | Find duplicates |
|
||||
|
||||
**Mole's advantages**:
|
||||
- Visual representation
|
||||
- One-click cleanup
|
||||
- Native macOS integration
|
||||
|
||||
**Scripts' advantages**:
|
||||
- Developer-specific tools (Docker, npm, pip)
|
||||
- Application remnant detection
|
||||
- Detailed categorization and safety notes
|
||||
- Batch operations with confirmation
|
||||
|
||||
## Coordinated Cleanup Strategy
|
||||
|
||||
### For Non-Technical Users
|
||||
|
||||
1. **Install Mole** - Primary cleanup tool
|
||||
2. **Keep scripts** - For occasional deep analysis
|
||||
3. **Workflow**:
|
||||
- Monthly: Run `analyze_caches.py` to see what's using space
|
||||
- Use Mole to execute cleanup
|
||||
- Special cases: Use scripts
|
||||
|
||||
### For Technical Users / Developers
|
||||
|
||||
1. **Keep both** - Mole for quick cleanup, scripts for precision
|
||||
2. **Workflow**:
|
||||
- Weekly: Mole for routine cache cleanup
|
||||
- Monthly: Full script analysis for deep cleaning
|
||||
- As needed: Script-based cleanup for dev environment
|
||||
|
||||
### For Power Users
|
||||
|
||||
1. **Scripts only** - Full control and automation
|
||||
2. **Workflow**:
|
||||
- Schedule analysis scripts with cron/launchd
|
||||
- Review reports
|
||||
- Execute cleanup with `safe_delete.py` or direct commands
|
||||
|
||||
## Limitations & Complementary Use
|
||||
|
||||
### What Mole Does Well
|
||||
|
||||
✅ Visual disk usage analysis
|
||||
✅ Safe cache cleanup
|
||||
✅ User-friendly interface
|
||||
✅ Quick routine maintenance
|
||||
|
||||
### What Mole Doesn't Do (Use Scripts For)
|
||||
|
||||
❌ Docker cleanup
|
||||
❌ Homebrew cache (command-line only)
|
||||
❌ npm/pip cache
|
||||
❌ Application remnant detection with confidence levels
|
||||
❌ Large .git directory detection
|
||||
❌ Development environment analysis
|
||||
|
||||
### Recommended Approach
|
||||
|
||||
**Use Mole for**: 80% of routine cleanup needs
|
||||
**Use Scripts for**: 20% of specialized/technical cleanup needs
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Mole Not Opening
|
||||
|
||||
```bash
|
||||
# Check if Mole is installed
|
||||
ls -l /Applications/Mole.app
|
||||
|
||||
# Try opening from command line (see error messages)
|
||||
open -a Mole
|
||||
|
||||
# If not installed
|
||||
echo "Download from: https://github.com/tw93/Mole/releases"
|
||||
```
|
||||
|
||||
### Mole Shows Different Numbers than Scripts
|
||||
|
||||
**Explanation**:
|
||||
- Mole uses different calculation methods
|
||||
- Scripts use `du` command (more accurate for directory sizes)
|
||||
- Both are valid, differences typically <5%
|
||||
|
||||
**Not a problem**: Use Mole's numbers for decisions
|
||||
|
||||
### Mole Can't Delete Some Caches
|
||||
|
||||
**Reason**: Permission issues (some caches are protected)
|
||||
|
||||
**Solution**:
|
||||
1. Use scripts with sudo for system caches
|
||||
2. Or manually delete in Finder with authentication
|
||||
|
||||
## Summary
|
||||
|
||||
**Best Practice**: Use both tools
|
||||
|
||||
- **Mole**: Visual cleanup, routine maintenance, user-friendly
|
||||
- **Scripts**: Deep analysis, developer tools, precise control
|
||||
|
||||
**Workflow**:
|
||||
1. Analyze with scripts (comprehensive report)
|
||||
2. Execute with Mole (safe and visual) OR scripts (precise and technical)
|
||||
3. Maintain with Mole (weekly/monthly routine)
|
||||
|
||||
This combination provides the best user experience for macOS cleanup.
|
||||
474
macos-cleaner/references/safety_rules.md
Normal file
474
macos-cleaner/references/safety_rules.md
Normal file
@@ -0,0 +1,474 @@
|
||||
# Safety Rules for macOS Cleanup
|
||||
|
||||
Critical safety guidelines to prevent data loss and system damage.
|
||||
|
||||
## Golden Rules
|
||||
|
||||
### Rule 1: Never Delete Without Confirmation
|
||||
|
||||
**ALWAYS** ask user before deleting ANY file or directory.
|
||||
|
||||
**Bad**:
|
||||
```python
|
||||
shutil.rmtree(cache_dir) # Immediately deletes
|
||||
```
|
||||
|
||||
**Good**:
|
||||
```python
|
||||
if confirm_delete(cache_dir, size, description):
|
||||
shutil.rmtree(cache_dir)
|
||||
else:
|
||||
print("Skipped")
|
||||
```
|
||||
|
||||
### Rule 2: Explain Before Deleting
|
||||
|
||||
Users should understand:
|
||||
- **What** is being deleted
|
||||
- **Why** it's safe (or not safe)
|
||||
- **Impact** of deletion
|
||||
- **Recoverability** (can it be restored?)
|
||||
|
||||
### Rule 3: When in Doubt, Don't Delete
|
||||
|
||||
If uncertain about safety: **DON'T DELETE**.
|
||||
|
||||
Ask user to verify instead.
|
||||
|
||||
### Rule 4: Suggest Backups for Large Deletions
|
||||
|
||||
Before deleting >10 GB, recommend Time Machine backup.
|
||||
|
||||
### Rule 5: Use Trash When Possible
|
||||
|
||||
Prefer moving to Trash over permanent deletion:
|
||||
|
||||
```bash
|
||||
# Recoverable
|
||||
osascript -e 'tell app "Finder" to move POSIX file "/path/to/file" to trash'
|
||||
|
||||
# Permanent (use only when confirmed safe)
|
||||
rm -rf /path/to/file
|
||||
```
|
||||
|
||||
## Never Delete These
|
||||
|
||||
### System Directories
|
||||
|
||||
| Path | Why | Impact if Deleted |
|
||||
|------|-----|-------------------|
|
||||
| `/System` | macOS core | System unbootable |
|
||||
| `/Library/Apple` | Apple frameworks | Apps won't launch |
|
||||
| `/private/etc` | System config | System unstable |
|
||||
| `/private/var/db` | System databases | System unstable |
|
||||
| `/usr` | Unix utilities | Commands won't work |
|
||||
| `/bin`, `/sbin` | System binaries | System unusable |
|
||||
|
||||
### User Data
|
||||
|
||||
| Path | Why | Impact if Deleted |
|
||||
|------|-----|-------------------|
|
||||
| `~/Documents` | User documents | Data loss |
|
||||
| `~/Desktop` | User files | Data loss |
|
||||
| `~/Pictures` | Photos | Data loss |
|
||||
| `~/Movies` | Videos | Data loss |
|
||||
| `~/Music` | Music library | Data loss |
|
||||
| `~/Downloads` | May contain important files | Potential data loss |
|
||||
|
||||
### Security & Credentials
|
||||
|
||||
| Path | Why | Impact if Deleted |
|
||||
|------|-----|-------------------|
|
||||
| `~/.ssh` | SSH keys | Cannot access servers |
|
||||
| `~/Library/Keychains` | Passwords, certificates | Cannot access accounts/services |
|
||||
| Any file with "credential", "password", "key" in name | Security data | Cannot authenticate |
|
||||
|
||||
### Active Databases
|
||||
|
||||
| Pattern | Why | Impact if Deleted |
|
||||
|---------|-----|-------------------|
|
||||
| `*.db`, `*.sqlite`, `*.sqlite3` | Application databases | App data loss |
|
||||
| Any database file for running app | Active data | Data corruption |
|
||||
|
||||
### Running Applications
|
||||
|
||||
| Path | Why | Impact if Deleted |
|
||||
|------|-----|-------------------|
|
||||
| `/Applications` | Installed apps | Apps won't launch |
|
||||
| `~/Applications` | User-installed apps | Apps won't launch |
|
||||
| Files in use (check with `lsof`) | Currently open | App crash, data corruption |
|
||||
|
||||
## Require Extra Confirmation
|
||||
|
||||
### Large Deletions
|
||||
|
||||
**Threshold**: >10 GB
|
||||
|
||||
**Action**: Warn user and suggest Time Machine backup
|
||||
|
||||
**Example**:
|
||||
```
|
||||
⚠️ This operation will delete 45 GB of data.
|
||||
|
||||
💡 Recommendation:
|
||||
Create a Time Machine backup first.
|
||||
|
||||
Check last backup:
|
||||
tmutil latestbackup
|
||||
|
||||
Create backup now:
|
||||
tmutil startbackup
|
||||
|
||||
Proceed without backup? [y/N]:
|
||||
```
|
||||
|
||||
### System-Wide Caches
|
||||
|
||||
**Paths**: `/Library/Caches`, `/var/log`
|
||||
|
||||
**Action**: Require manual sudo command (don't execute directly)
|
||||
|
||||
**Example**:
|
||||
```
|
||||
⚠️ This operation requires administrator privileges.
|
||||
|
||||
Please run this command manually:
|
||||
sudo rm -rf /Library/Caches/*
|
||||
|
||||
⚠️ You will be asked for your password.
|
||||
```
|
||||
|
||||
**Reason**:
|
||||
- Requires elevated privileges
|
||||
- User should be aware of system-wide impact
|
||||
- Audit trail (user types password)
|
||||
|
||||
### Docker Volumes
|
||||
|
||||
**Action**: Always list volumes before cleanup
|
||||
|
||||
**Example**:
|
||||
```
|
||||
⚠️ Docker cleanup may remove important data.
|
||||
|
||||
Current volumes:
|
||||
postgres_data (1.2 GB) - May contain database
|
||||
redis_data (500 MB) - May contain cache
|
||||
app_uploads (3 GB) - May contain user files
|
||||
|
||||
Review each volume:
|
||||
docker volume inspect <volume_name>
|
||||
|
||||
Proceed with cleanup? [y/N]:
|
||||
```
|
||||
|
||||
### Application Preferences
|
||||
|
||||
**Path**: `~/Library/Preferences/*.plist`
|
||||
|
||||
**Action**: Warn that app will reset to defaults
|
||||
|
||||
**Example**:
|
||||
```
|
||||
⚠️ Deleting preferences will reset the app to defaults.
|
||||
|
||||
Impact:
|
||||
- All settings will be lost
|
||||
- Custom configurations will be reset
|
||||
- May need to re-enter license keys
|
||||
|
||||
Only delete if:
|
||||
- App is misbehaving (troubleshooting)
|
||||
- App is confirmed uninstalled
|
||||
|
||||
Proceed? [y/N]:
|
||||
```
|
||||
|
||||
## Safety Checks Before Deletion
|
||||
|
||||
### Check 1: Path Exists
|
||||
|
||||
```python
|
||||
if not os.path.exists(path):
|
||||
print(f"❌ Path does not exist: {path}")
|
||||
return False
|
||||
```
|
||||
|
||||
### Check 2: Not a System Path
|
||||
|
||||
```python
|
||||
system_paths = [
|
||||
'/System', '/Library/Apple', '/private/etc',
|
||||
'/usr', '/bin', '/sbin', '/private/var/db'
|
||||
]
|
||||
|
||||
for sys_path in system_paths:
|
||||
if path.startswith(sys_path):
|
||||
print(f"❌ Cannot delete system path: {path}")
|
||||
return False
|
||||
```
|
||||
|
||||
### Check 3: Not User Data
|
||||
|
||||
```python
|
||||
user_data_paths = [
|
||||
'~/Documents', '~/Desktop', '~/Pictures',
|
||||
'~/Movies', '~/Music', '~/.ssh'
|
||||
]
|
||||
|
||||
expanded_path = os.path.expanduser(path)
|
||||
for data_path in user_data_paths:
|
||||
if expanded_path.startswith(os.path.expanduser(data_path)):
|
||||
print(f"⚠️ This is a user data directory: {path}")
|
||||
print(" Are you ABSOLUTELY sure? [type 'DELETE' to confirm]:")
|
||||
response = input().strip()
|
||||
if response != 'DELETE':
|
||||
return False
|
||||
```
|
||||
|
||||
### Check 4: Not in Use
|
||||
|
||||
```python
|
||||
def is_in_use(path):
|
||||
"""Check if file/directory is in use."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['lsof', path],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
# If lsof finds processes using the file, returncode is 0
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
return False
|
||||
except:
|
||||
return False # Assume not in use if check fails
|
||||
|
||||
if is_in_use(path):
|
||||
print(f"⚠️ Warning: {path} is currently in use")
|
||||
print(" Close the application first, then try again.")
|
||||
return False
|
||||
```
|
||||
|
||||
### Check 5: Permissions
|
||||
|
||||
```python
|
||||
def can_delete(path):
|
||||
"""Check if we have permission to delete."""
|
||||
try:
|
||||
# Check parent directory write permission
|
||||
parent = os.path.dirname(path)
|
||||
return os.access(parent, os.W_OK)
|
||||
except:
|
||||
return False
|
||||
|
||||
if not can_delete(path):
|
||||
print(f"❌ No permission to delete: {path}")
|
||||
print(" You may need sudo, but be careful!")
|
||||
return False
|
||||
```
|
||||
|
||||
## Safe Deletion Workflow
|
||||
|
||||
```python
|
||||
def safe_delete(path, size, description):
|
||||
"""
|
||||
Safe deletion workflow with all checks.
|
||||
|
||||
Args:
|
||||
path: Path to delete
|
||||
size: Size in bytes
|
||||
description: Human-readable description
|
||||
|
||||
Returns:
|
||||
(success, message)
|
||||
"""
|
||||
# Safety checks
|
||||
if not os.path.exists(path):
|
||||
return (False, "Path does not exist")
|
||||
|
||||
if is_system_path(path):
|
||||
return (False, "Cannot delete system path")
|
||||
|
||||
if is_user_data(path):
|
||||
if not extra_confirm(path):
|
||||
return (False, "User cancelled")
|
||||
|
||||
if is_in_use(path):
|
||||
return (False, "Path is in use")
|
||||
|
||||
if not can_delete(path):
|
||||
return (False, "No permission")
|
||||
|
||||
# Backup warning for large deletions
|
||||
if size > 10 * 1024 * 1024 * 1024: # 10 GB
|
||||
if not confirm_large_deletion(size):
|
||||
return (False, "User cancelled")
|
||||
|
||||
# Final confirmation
|
||||
if not confirm_delete(path, size, description):
|
||||
return (False, "User cancelled")
|
||||
|
||||
# Execute deletion
|
||||
try:
|
||||
if os.path.isfile(path):
|
||||
os.unlink(path)
|
||||
else:
|
||||
shutil.rmtree(path)
|
||||
return (True, f"Deleted successfully ({format_size(size)} freed)")
|
||||
except Exception as e:
|
||||
return (False, f"Deletion failed: {str(e)}")
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Permission Denied
|
||||
|
||||
```python
|
||||
except PermissionError:
|
||||
print(f"❌ Permission denied: {path}")
|
||||
print(" Try running with sudo (use caution!)")
|
||||
```
|
||||
|
||||
### Operation Not Permitted (SIP)
|
||||
|
||||
```python
|
||||
# macOS System Integrity Protection blocks some deletions
|
||||
except OSError as e:
|
||||
if e.errno == 1: # Operation not permitted
|
||||
print(f"❌ System Integrity Protection prevents deletion: {path}")
|
||||
print(" This is a protected system file.")
|
||||
print(" Do NOT attempt to bypass SIP unless you know what you're doing.")
|
||||
```
|
||||
|
||||
### Path Too Long
|
||||
|
||||
```python
|
||||
except OSError as e:
|
||||
if e.errno == 63: # File name too long
|
||||
print(f"⚠️ Path too long, trying alternative method...")
|
||||
# Try using find + rm
|
||||
```
|
||||
|
||||
## Recovery Options
|
||||
|
||||
### If User Accidentally Confirmed
|
||||
|
||||
**Immediate action**: Check Trash first
|
||||
|
||||
```bash
|
||||
# Files may be in Trash
|
||||
ls -lh ~/.Trash
|
||||
```
|
||||
|
||||
**Next**: Time Machine
|
||||
|
||||
```bash
|
||||
# Open Time Machine to date before deletion
|
||||
tmutil browse
|
||||
```
|
||||
|
||||
**Last resort**: File recovery tools
|
||||
|
||||
- Disk Drill (commercial)
|
||||
- PhotoRec (free)
|
||||
- TestDisk (free)
|
||||
|
||||
**Note**: Success rate depends on:
|
||||
- How recently deleted
|
||||
- How much disk activity since deletion
|
||||
- Whether SSD (TRIM) or HDD
|
||||
|
||||
### Preventing Accidents
|
||||
|
||||
1. **Use Trash instead of rm** when possible
|
||||
2. **Require Time Machine backup** for >10 GB deletions
|
||||
3. **Test on small items first** before batch operations
|
||||
4. **Show dry-run results** before actual deletion
|
||||
|
||||
## Red Flags to Watch For
|
||||
|
||||
### User Requests
|
||||
|
||||
If user asks to:
|
||||
- "Delete everything in ~/Library"
|
||||
- "Clear all caches including system"
|
||||
- "Delete all .log files on the entire system"
|
||||
- "Remove all databases"
|
||||
|
||||
**Response**:
|
||||
```
|
||||
⚠️ This request is too broad and risky.
|
||||
|
||||
Let me help you with a safer approach:
|
||||
1. Run analysis to identify specific targets
|
||||
2. Review each category
|
||||
3. Delete selectively with confirmation
|
||||
|
||||
This prevents accidental data loss.
|
||||
```
|
||||
|
||||
### Script Behavior
|
||||
|
||||
If script is about to:
|
||||
- Delete >100 GB at once
|
||||
- Delete entire directory trees without listing contents
|
||||
- Run `rm -rf /` or similar dangerous commands
|
||||
- Delete from system paths
|
||||
|
||||
**Action**: STOP and ask for confirmation
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
### Before Packaging
|
||||
|
||||
Test safety checks:
|
||||
|
||||
1. ✅ Attempt to delete system path → Should reject
|
||||
2. ✅ Attempt to delete user data → Should require extra confirmation
|
||||
3. ✅ Attempt to delete in-use file → Should warn
|
||||
4. ✅ Attempt to delete without permission → Should fail gracefully
|
||||
5. ✅ Large deletion → Should suggest backup
|
||||
|
||||
### In Production
|
||||
|
||||
Always:
|
||||
- Start with smallest items
|
||||
- Confirm results after each deletion
|
||||
- Monitor disk space before/after
|
||||
- Ask user to verify important apps still work
|
||||
|
||||
## Summary
|
||||
|
||||
### Conservative Approach
|
||||
|
||||
When implementing cleanup:
|
||||
|
||||
1. **Assume danger** until proven safe
|
||||
2. **Explain everything** to user
|
||||
3. **Confirm each step**
|
||||
4. **Suggest backups** for large operations
|
||||
5. **Use Trash** when possible
|
||||
6. **Test thoroughly** before packaging
|
||||
|
||||
### Remember
|
||||
|
||||
> "It's better to leave 1 GB of unnecessary files than to delete 1 MB of important data."
|
||||
|
||||
User trust is fragile. One bad deletion loses it forever.
|
||||
|
||||
### Final Checklist
|
||||
|
||||
Before any deletion:
|
||||
|
||||
- [ ] Path is verified to exist
|
||||
- [ ] Path is not a system path
|
||||
- [ ] Path is not user data (or extra confirmed)
|
||||
- [ ] Path is not in use
|
||||
- [ ] User has been informed of impact
|
||||
- [ ] User has explicitly confirmed
|
||||
- [ ] Backup suggested for large deletions
|
||||
- [ ] Error handling in place
|
||||
- [ ] Recovery options documented
|
||||
|
||||
Only then: proceed with deletion.
|
||||
211
macos-cleaner/scripts/analyze_caches.py
Executable file
211
macos-cleaner/scripts/analyze_caches.py
Executable file
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Analyze macOS cache directories and categorize them by size and safety.
|
||||
|
||||
Usage:
|
||||
python3 analyze_caches.py [--user-only] [--min-size SIZE]
|
||||
|
||||
Options:
|
||||
--user-only Only scan user caches (~/Library/Caches), skip system caches
|
||||
--min-size Minimum size in MB to report (default: 10)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_dir_size(path):
|
||||
"""
|
||||
Get directory size using du command.
|
||||
|
||||
Args:
|
||||
path: Directory path
|
||||
|
||||
Returns:
|
||||
Size in bytes, or 0 if error
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['du', '-sk', path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
if result.returncode == 0:
|
||||
# du -sk returns size in KB
|
||||
size_kb = int(result.stdout.split()[0])
|
||||
return size_kb * 1024 # Convert to bytes
|
||||
return 0
|
||||
except (subprocess.TimeoutExpired, ValueError, IndexError):
|
||||
return 0
|
||||
|
||||
|
||||
def format_size(bytes_size):
|
||||
"""Convert bytes to human-readable format."""
|
||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||
if bytes_size < 1024.0:
|
||||
return f"{bytes_size:.1f} {unit}"
|
||||
bytes_size /= 1024.0
|
||||
return f"{bytes_size:.1f} PB"
|
||||
|
||||
|
||||
def analyze_cache_dir(base_path, min_size_bytes):
|
||||
"""
|
||||
Analyze a cache directory and list subdirectories by size.
|
||||
|
||||
Args:
|
||||
base_path: Path to cache directory
|
||||
min_size_bytes: Minimum size to report
|
||||
|
||||
Returns:
|
||||
List of (name, path, size_bytes) tuples
|
||||
"""
|
||||
if not os.path.exists(base_path):
|
||||
return []
|
||||
|
||||
results = []
|
||||
try:
|
||||
for entry in os.scandir(base_path):
|
||||
if entry.is_dir():
|
||||
size = get_dir_size(entry.path)
|
||||
if size >= min_size_bytes:
|
||||
results.append((entry.name, entry.path, size))
|
||||
except PermissionError:
|
||||
print(f"⚠️ Permission denied: {base_path}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
# Sort by size descending
|
||||
results.sort(key=lambda x: x[2], reverse=True)
|
||||
return results
|
||||
|
||||
|
||||
def categorize_safety(name):
|
||||
"""
|
||||
Categorize cache safety based on name patterns.
|
||||
|
||||
Returns:
|
||||
('safe'|'check'|'keep', reason)
|
||||
"""
|
||||
name_lower = name.lower()
|
||||
|
||||
# Known safe to delete
|
||||
safe_patterns = [
|
||||
'chrome', 'firefox', 'safari', 'edge', # Browsers
|
||||
'spotify', 'slack', 'discord', # Communication
|
||||
'pip', 'npm', 'homebrew', # Package managers
|
||||
'temp', 'tmp', 'cache' # Generic temp
|
||||
]
|
||||
if any(pattern in name_lower for pattern in safe_patterns):
|
||||
return ('safe', 'Application regenerates cache automatically')
|
||||
|
||||
# Check before deleting
|
||||
check_patterns = [
|
||||
'xcode', 'android', # IDEs (may slow next launch)
|
||||
'jetbrains', 'vscode',
|
||||
'docker' # May contain important build cache
|
||||
]
|
||||
if any(pattern in name_lower for pattern in check_patterns):
|
||||
return ('check', 'May slow down next application launch')
|
||||
|
||||
# Default: check first
|
||||
return ('check', 'Unknown application, verify before deleting')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Analyze macOS cache directories'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--user-only',
|
||||
action='store_true',
|
||||
help='Only scan user caches (skip system caches)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--min-size',
|
||||
type=int,
|
||||
default=10,
|
||||
help='Minimum size in MB to report (default: 10)'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
min_size_bytes = args.min_size * 1024 * 1024 # Convert MB to bytes
|
||||
|
||||
print("🔍 Analyzing macOS Cache Directories")
|
||||
print("=" * 50)
|
||||
|
||||
# User caches
|
||||
user_cache_path = os.path.expanduser('~/Library/Caches')
|
||||
print(f"\n📂 User Caches: {user_cache_path}")
|
||||
print("-" * 50)
|
||||
|
||||
user_caches = analyze_cache_dir(user_cache_path, min_size_bytes)
|
||||
total_user = 0
|
||||
|
||||
if user_caches:
|
||||
print(f"{'Application':<40} {'Size':<12} {'Safety'}")
|
||||
print("-" * 70)
|
||||
for name, path, size in user_caches:
|
||||
safety, reason = categorize_safety(name)
|
||||
safety_icon = {'safe': '🟢', 'check': '🟡', 'keep': '🔴'}[safety]
|
||||
print(f"{name:<40} {format_size(size):<12} {safety_icon}")
|
||||
total_user += size
|
||||
print("-" * 70)
|
||||
print(f"{'Total':<40} {format_size(total_user):<12}")
|
||||
else:
|
||||
print("No cache directories found above minimum size.")
|
||||
|
||||
# User logs
|
||||
user_log_path = os.path.expanduser('~/Library/Logs')
|
||||
if os.path.exists(user_log_path):
|
||||
log_size = get_dir_size(user_log_path)
|
||||
if log_size >= min_size_bytes:
|
||||
print(f"\n📝 User Logs: {user_log_path}")
|
||||
print(f" Size: {format_size(log_size)} 🟢 Safe to delete")
|
||||
total_user += log_size
|
||||
|
||||
# System caches (if not --user-only)
|
||||
if not args.user_only:
|
||||
print(f"\n\n📂 System Caches: /Library/Caches")
|
||||
print("-" * 50)
|
||||
print("⚠️ Requires administrator privileges to delete")
|
||||
|
||||
system_cache_path = '/Library/Caches'
|
||||
system_caches = analyze_cache_dir(system_cache_path, min_size_bytes)
|
||||
total_system = 0
|
||||
|
||||
if system_caches:
|
||||
print(f"{'Application':<40} {'Size':<12}")
|
||||
print("-" * 70)
|
||||
for name, path, size in system_caches[:10]: # Top 10 only
|
||||
print(f"{name:<40} {format_size(size):<12}")
|
||||
total_system += size
|
||||
if len(system_caches) > 10:
|
||||
print(f"... and {len(system_caches) - 10} more")
|
||||
print("-" * 70)
|
||||
print(f"{'Total':<40} {format_size(total_system):<12}")
|
||||
else:
|
||||
print("No cache directories found above minimum size.")
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 50)
|
||||
print("📊 Summary")
|
||||
print("=" * 50)
|
||||
print(f"Total User Caches: {format_size(total_user)}")
|
||||
if not args.user_only:
|
||||
print(f"Total System Caches: {format_size(total_system)}")
|
||||
print(f"Combined Total: {format_size(total_user + total_system)}")
|
||||
|
||||
print("\n💡 Next Steps:")
|
||||
print(" 1. Review the list above")
|
||||
print(" 2. Identify caches marked 🟢 (safe to delete)")
|
||||
print(" 3. For 🟡 items, verify the application is not running")
|
||||
print(" 4. Use safe_delete.py for interactive cleanup")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
307
macos-cleaner/scripts/analyze_dev_env.py
Executable file
307
macos-cleaner/scripts/analyze_dev_env.py
Executable file
@@ -0,0 +1,307 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Analyze development environment and find cleanable resources.
|
||||
|
||||
Checks:
|
||||
- Docker (images, containers, volumes, build cache)
|
||||
- Homebrew cache
|
||||
- npm cache
|
||||
- pip cache
|
||||
- Old .git directories in archived projects
|
||||
|
||||
Usage:
|
||||
python3 analyze_dev_env.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def format_size(bytes_size):
|
||||
"""Convert bytes to human-readable format."""
|
||||
if bytes_size is None:
|
||||
return "Unknown"
|
||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||
if bytes_size < 1024.0:
|
||||
return f"{bytes_size:.1f} {unit}"
|
||||
bytes_size /= 1024.0
|
||||
return f"{bytes_size:.1f} PB"
|
||||
|
||||
|
||||
def run_command(cmd):
|
||||
"""Run command and return output, or None if error."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()
|
||||
return None
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
return None
|
||||
|
||||
|
||||
def get_dir_size(path):
|
||||
"""Get directory size using du command."""
|
||||
output = run_command(['du', '-sk', path])
|
||||
if output:
|
||||
try:
|
||||
size_kb = int(output.split()[0])
|
||||
return size_kb * 1024 # Convert to bytes
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
def check_docker():
|
||||
"""Check Docker resources."""
|
||||
print("\n🐳 Docker Resources")
|
||||
print("=" * 50)
|
||||
|
||||
# Check if Docker is installed
|
||||
if not run_command(['which', 'docker']):
|
||||
print(" Docker not installed or not in PATH")
|
||||
return 0
|
||||
|
||||
# Check if Docker daemon is running
|
||||
if not run_command(['docker', 'info']):
|
||||
print(" Docker daemon not running")
|
||||
return 0
|
||||
|
||||
total_size = 0
|
||||
|
||||
# Images
|
||||
images_output = run_command(['docker', 'images', '-q'])
|
||||
if images_output:
|
||||
image_count = len(images_output.split('\n'))
|
||||
print(f"\n📦 Images: {image_count}")
|
||||
|
||||
# Get size estimate
|
||||
system_output = run_command(['docker', 'system', 'df', '--format', '{{json .}}'])
|
||||
if system_output:
|
||||
for line in system_output.split('\n'):
|
||||
try:
|
||||
data = json.loads(line)
|
||||
if data.get('Type') == 'Images':
|
||||
size_str = data.get('Size', '')
|
||||
# Parse size (format like "1.2GB")
|
||||
if 'GB' in size_str:
|
||||
size = float(size_str.replace('GB', '')) * 1024 * 1024 * 1024
|
||||
elif 'MB' in size_str:
|
||||
size = float(size_str.replace('MB', '')) * 1024 * 1024
|
||||
else:
|
||||
size = 0
|
||||
print(f" Total size: {format_size(size)}")
|
||||
total_size += size
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
pass
|
||||
|
||||
# Containers
|
||||
containers_output = run_command(['docker', 'ps', '-a', '-q'])
|
||||
if containers_output:
|
||||
container_count = len(containers_output.split('\n'))
|
||||
stopped = run_command(['docker', 'ps', '-a', '-f', 'status=exited', '-q'])
|
||||
stopped_count = len(stopped.split('\n')) if stopped else 0
|
||||
print(f"\n📦 Containers: {container_count} total, {stopped_count} stopped")
|
||||
|
||||
# Volumes
|
||||
volumes_output = run_command(['docker', 'volume', 'ls', '-q'])
|
||||
if volumes_output:
|
||||
volume_count = len(volumes_output.split('\n'))
|
||||
print(f"\n📦 Volumes: {volume_count}")
|
||||
|
||||
# List volumes
|
||||
for volume in volumes_output.split('\n')[:5]: # Show first 5
|
||||
inspect = run_command(['docker', 'volume', 'inspect', volume])
|
||||
print(f" - {volume}")
|
||||
if volume_count > 5:
|
||||
print(f" ... and {volume_count - 5} more")
|
||||
|
||||
# Build cache
|
||||
buildx_output = run_command(['docker', 'buildx', 'du'])
|
||||
if buildx_output and 'Total:' in buildx_output:
|
||||
print(f"\n📦 Build Cache:")
|
||||
for line in buildx_output.split('\n'):
|
||||
if 'Total:' in line:
|
||||
print(f" {line}")
|
||||
|
||||
print(f"\n💡 Cleanup command: docker system prune -a --volumes")
|
||||
print(f" ⚠️ Warning: This will remove ALL unused Docker resources")
|
||||
|
||||
return total_size
|
||||
|
||||
|
||||
def check_homebrew():
|
||||
"""Check Homebrew cache."""
|
||||
print("\n🍺 Homebrew")
|
||||
print("=" * 50)
|
||||
|
||||
if not run_command(['which', 'brew']):
|
||||
print(" Homebrew not installed")
|
||||
return 0
|
||||
|
||||
cache_path = run_command(['brew', '--cache'])
|
||||
if cache_path and os.path.exists(cache_path):
|
||||
size = get_dir_size(cache_path)
|
||||
print(f" Cache location: {cache_path}")
|
||||
print(f" Cache size: {format_size(size)}")
|
||||
print(f"\n💡 Cleanup command: brew cleanup -s")
|
||||
return size
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def check_npm():
|
||||
"""Check npm cache."""
|
||||
print("\n📦 npm")
|
||||
print("=" * 50)
|
||||
|
||||
if not run_command(['which', 'npm']):
|
||||
print(" npm not installed")
|
||||
return 0
|
||||
|
||||
cache_path = run_command(['npm', 'config', 'get', 'cache'])
|
||||
if cache_path and cache_path != 'undefined' and os.path.exists(cache_path):
|
||||
size = get_dir_size(cache_path)
|
||||
print(f" Cache location: {cache_path}")
|
||||
print(f" Cache size: {format_size(size)}")
|
||||
print(f"\n💡 Cleanup command: npm cache clean --force")
|
||||
return size
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def check_pip():
|
||||
"""Check pip cache."""
|
||||
print("\n🐍 pip")
|
||||
print("=" * 50)
|
||||
|
||||
# Try pip3 first
|
||||
pip_cmd = 'pip3' if run_command(['which', 'pip3']) else 'pip'
|
||||
|
||||
if not run_command(['which', pip_cmd]):
|
||||
print(" pip not installed")
|
||||
return 0
|
||||
|
||||
cache_dir = run_command([pip_cmd, 'cache', 'dir'])
|
||||
if cache_dir and os.path.exists(cache_dir):
|
||||
size = get_dir_size(cache_dir)
|
||||
print(f" Cache location: {cache_dir}")
|
||||
print(f" Cache size: {format_size(size)}")
|
||||
print(f"\n💡 Cleanup command: {pip_cmd} cache purge")
|
||||
return size
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def check_old_git_repos():
|
||||
"""Find large .git directories in archived projects."""
|
||||
print("\n📁 Old Git Repositories")
|
||||
print("=" * 50)
|
||||
|
||||
home = Path.home()
|
||||
common_project_dirs = [
|
||||
home / 'Projects',
|
||||
home / 'workspace',
|
||||
home / 'dev',
|
||||
home / 'src',
|
||||
home / 'code'
|
||||
]
|
||||
|
||||
git_repos = []
|
||||
total_size = 0
|
||||
|
||||
for project_dir in common_project_dirs:
|
||||
if not project_dir.exists():
|
||||
continue
|
||||
|
||||
# Find .git directories
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['find', str(project_dir), '-name', '.git', '-type', 'd', '-maxdepth', 3],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
if result.returncode == 0:
|
||||
for git_path in result.stdout.strip().split('\n'):
|
||||
if git_path:
|
||||
size = get_dir_size(git_path)
|
||||
if size > 10 * 1024 * 1024: # > 10 MB
|
||||
git_repos.append((git_path, size))
|
||||
total_size += size
|
||||
except subprocess.TimeoutExpired:
|
||||
continue
|
||||
|
||||
if git_repos:
|
||||
# Sort by size
|
||||
git_repos.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
print(f" Found {len(git_repos)} .git directories > 10 MB")
|
||||
print(f"\n Top 10 largest:")
|
||||
for path, size in git_repos[:10]:
|
||||
# Get parent directory name (project name)
|
||||
project_name = Path(path).parent.name
|
||||
print(f" - {project_name:<30} {format_size(size)}")
|
||||
|
||||
print(f"\n Total: {format_size(total_size)}")
|
||||
print(f"\n💡 If these are archived projects, consider:")
|
||||
print(f" 1. Delete .git history: rm -rf <project>/.git")
|
||||
print(f" 2. Or compress entire project: tar -czf archive.tar.gz <project>")
|
||||
else:
|
||||
print(" No large .git directories found in common project locations")
|
||||
|
||||
return total_size
|
||||
|
||||
|
||||
def main():
|
||||
print("🔍 Development Environment Analysis")
|
||||
print("=" * 50)
|
||||
|
||||
total_savings = 0
|
||||
|
||||
# Check each component
|
||||
docker_size = check_docker()
|
||||
brew_size = check_homebrew()
|
||||
npm_size = check_npm()
|
||||
pip_size = check_pip()
|
||||
git_size = check_old_git_repos()
|
||||
|
||||
# Summary
|
||||
print("\n\n📊 Summary")
|
||||
print("=" * 50)
|
||||
if docker_size:
|
||||
print(f"Docker: {format_size(docker_size)}")
|
||||
total_savings += docker_size
|
||||
if brew_size:
|
||||
print(f"Homebrew cache: {format_size(brew_size)}")
|
||||
total_savings += brew_size
|
||||
if npm_size:
|
||||
print(f"npm cache: {format_size(npm_size)}")
|
||||
total_savings += npm_size
|
||||
if pip_size:
|
||||
print(f"pip cache: {format_size(pip_size)}")
|
||||
total_savings += pip_size
|
||||
if git_size:
|
||||
print(f"Old .git repos: {format_size(git_size)}")
|
||||
total_savings += git_size
|
||||
|
||||
print("-" * 50)
|
||||
print(f"Potential savings: {format_size(total_savings)}")
|
||||
|
||||
print("\n💡 Next Steps:")
|
||||
print(" 1. Review Docker volumes before cleanup (may contain data)")
|
||||
print(" 2. Package manager caches are safe to delete")
|
||||
print(" 3. For .git directories, ensure project is truly archived")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
241
macos-cleaner/scripts/analyze_large_files.py
Executable file
241
macos-cleaner/scripts/analyze_large_files.py
Executable file
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Find large files on macOS and categorize them.
|
||||
|
||||
Usage:
|
||||
python3 analyze_large_files.py [--threshold SIZE] [--path PATH] [--limit N]
|
||||
|
||||
Options:
|
||||
--threshold Minimum file size in MB (default: 100)
|
||||
--path Path to search (default: ~)
|
||||
--limit Maximum number of results (default: 50)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def format_size(bytes_size):
|
||||
"""Convert bytes to human-readable format."""
|
||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||
if bytes_size < 1024.0:
|
||||
return f"{bytes_size:.1f} {unit}"
|
||||
bytes_size /= 1024.0
|
||||
return f"{bytes_size:.1f} PB"
|
||||
|
||||
|
||||
def categorize_file(path):
|
||||
"""
|
||||
Categorize file by type and suggest safety.
|
||||
|
||||
Returns:
|
||||
(category, icon, safety_note)
|
||||
"""
|
||||
suffix = path.suffix.lower()
|
||||
|
||||
# Video files
|
||||
video_exts = {'.mp4', '.mov', '.avi', '.mkv', '.m4v', '.flv', '.wmv'}
|
||||
if suffix in video_exts:
|
||||
return ('Video', '🎬', 'Review and archive to external storage')
|
||||
|
||||
# Archive files
|
||||
archive_exts = {'.zip', '.tar', '.gz', '.bz2', '.7z', '.rar', '.dmg'}
|
||||
if suffix in archive_exts:
|
||||
return ('Archive', '📦', 'Extract if needed, then delete archive')
|
||||
|
||||
# Disk images
|
||||
disk_exts = {'.iso', '.img', '.toast'}
|
||||
if suffix in disk_exts:
|
||||
return ('Disk Image', '💿', 'Delete after installation/use')
|
||||
|
||||
# Database files
|
||||
db_exts = {'.db', '.sqlite', '.sqlite3', '.sql'}
|
||||
if suffix in db_exts:
|
||||
return ('Database', '🗄️', '⚠️ Verify not in use before deleting')
|
||||
|
||||
# Data files
|
||||
data_exts = {'.csv', '.json', '.xml', '.parquet', '.arrow'}
|
||||
if suffix in data_exts:
|
||||
return ('Data File', '📊', 'Archive or compress if historical data')
|
||||
|
||||
# Log files
|
||||
if suffix == '.log' or 'log' in path.name.lower():
|
||||
return ('Log File', '📝', 'Safe to delete old logs')
|
||||
|
||||
# Build artifacts
|
||||
build_patterns = ['.o', '.a', '.so', '.dylib', '.framework']
|
||||
if suffix in build_patterns:
|
||||
return ('Build Artifact', '🔨', 'Safe to delete, rebuild will regenerate')
|
||||
|
||||
# Virtual machine images
|
||||
vm_exts = {'.vmdk', '.vdi', '.qcow2', '.vhd'}
|
||||
if suffix in vm_exts:
|
||||
return ('VM Image', '💻', '⚠️ Contains VM data, verify before deleting')
|
||||
|
||||
# Other
|
||||
return ('Other', '📄', 'Review before deleting')
|
||||
|
||||
|
||||
def find_large_files(search_path, threshold_bytes, limit):
|
||||
"""
|
||||
Find files larger than threshold using find command.
|
||||
|
||||
Args:
|
||||
search_path: Path to search
|
||||
threshold_bytes: Minimum size in bytes
|
||||
limit: Maximum results
|
||||
|
||||
Returns:
|
||||
List of (path, size_bytes) tuples
|
||||
"""
|
||||
# Convert bytes to 512-byte blocks (find -size uses 512-byte blocks)
|
||||
threshold_blocks = threshold_bytes // 512
|
||||
|
||||
# Exclude common directories to avoid
|
||||
exclude_dirs = [
|
||||
'.Trash',
|
||||
'Library/Caches',
|
||||
'Library/Application Support/MobileSync', # iOS backups
|
||||
'.git',
|
||||
'node_modules',
|
||||
'__pycache__'
|
||||
]
|
||||
|
||||
# Build find command
|
||||
cmd = ['find', search_path, '-type', 'f', '-size', f'+{threshold_blocks}']
|
||||
|
||||
# Add exclusions
|
||||
for exclude in exclude_dirs:
|
||||
cmd.extend(['-not', '-path', f'*/{exclude}/*'])
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"⚠️ Warning: find command had errors", file=sys.stderr)
|
||||
|
||||
files = []
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
path = Path(line)
|
||||
if path.exists():
|
||||
size = path.stat().st_size
|
||||
files.append((path, size))
|
||||
except (OSError, PermissionError):
|
||||
continue
|
||||
|
||||
# Sort by size descending
|
||||
files.sort(key=lambda x: x[1], reverse=True)
|
||||
return files[:limit]
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
print("⚠️ Search timed out, showing partial results", file=sys.stderr)
|
||||
return []
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Find large files on macOS'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--threshold',
|
||||
type=int,
|
||||
default=100,
|
||||
help='Minimum file size in MB (default: 100)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--path',
|
||||
default=os.path.expanduser('~'),
|
||||
help='Path to search (default: ~)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--limit',
|
||||
type=int,
|
||||
default=50,
|
||||
help='Maximum number of results (default: 50)'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
threshold_bytes = args.threshold * 1024 * 1024
|
||||
search_path = os.path.expanduser(args.path)
|
||||
|
||||
print(f"🔍 Searching for files larger than {args.threshold} MB")
|
||||
print(f"📂 Search path: {search_path}")
|
||||
print("=" * 80)
|
||||
print("This may take a few minutes...\n")
|
||||
|
||||
large_files = find_large_files(search_path, threshold_bytes, args.limit)
|
||||
|
||||
if not large_files:
|
||||
print("✅ No large files found above the threshold.")
|
||||
return 0
|
||||
|
||||
print(f"\n📦 Found {len(large_files)} large files")
|
||||
print("=" * 80)
|
||||
print(f"{'#':<4} {'Size':<12} {'Type':<12} {'Location'}")
|
||||
print("-" * 80)
|
||||
|
||||
# Group by category
|
||||
by_category = {}
|
||||
total_size = 0
|
||||
|
||||
for i, (path, size) in enumerate(large_files, 1):
|
||||
category, icon, note = categorize_file(path)
|
||||
|
||||
# Shorten path for display
|
||||
try:
|
||||
rel_path = path.relative_to(Path.home())
|
||||
display_path = f"~/{rel_path}"
|
||||
except ValueError:
|
||||
display_path = str(path)
|
||||
|
||||
# Truncate long paths
|
||||
if len(display_path) > 45:
|
||||
display_path = display_path[:42] + "..."
|
||||
|
||||
print(f"{i:<4} {format_size(size):<12} {icon} {category:<10} {display_path}")
|
||||
|
||||
# Track by category
|
||||
if category not in by_category:
|
||||
by_category[category] = {'count': 0, 'size': 0, 'note': note}
|
||||
by_category[category]['count'] += 1
|
||||
by_category[category]['size'] += size
|
||||
total_size += size
|
||||
|
||||
print("-" * 80)
|
||||
print(f"{'Total':<4} {format_size(total_size):<12}")
|
||||
|
||||
# Category summary
|
||||
print("\n\n📊 Breakdown by Category")
|
||||
print("=" * 80)
|
||||
for category, data in sorted(
|
||||
by_category.items(),
|
||||
key=lambda x: x[1]['size'],
|
||||
reverse=True
|
||||
):
|
||||
print(f"\n{category}")
|
||||
print(f" Files: {data['count']}")
|
||||
print(f" Total: {format_size(data['size'])}")
|
||||
print(f" 💡 {data['note']}")
|
||||
|
||||
print("\n\n💡 Next Steps:")
|
||||
print(" 1. Review the list and identify files you no longer need")
|
||||
print(" 2. For videos/archives: consider moving to external storage")
|
||||
print(" 3. For databases/VMs: verify they're not in use")
|
||||
print(" 4. Use safe_delete.py for interactive cleanup")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
234
macos-cleaner/scripts/cleanup_report.py
Executable file
234
macos-cleaner/scripts/cleanup_report.py
Executable file
@@ -0,0 +1,234 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate before/after cleanup reports.
|
||||
|
||||
Usage:
|
||||
# Capture before snapshot
|
||||
python3 cleanup_report.py --snapshot before
|
||||
|
||||
# Capture after snapshot and generate report
|
||||
python3 cleanup_report.py --snapshot after --compare
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def format_size(bytes_size):
|
||||
"""Convert bytes to human-readable format."""
|
||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||
if bytes_size < 1024.0:
|
||||
return f"{bytes_size:.1f} {unit}"
|
||||
bytes_size /= 1024.0
|
||||
return f"{bytes_size:.1f} PB"
|
||||
|
||||
|
||||
def get_disk_usage():
|
||||
"""
|
||||
Get current disk usage.
|
||||
|
||||
Returns:
|
||||
dict with total, used, available, percent
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['df', '-k', '/'],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
lines = result.stdout.strip().split('\n')
|
||||
if len(lines) >= 2:
|
||||
# Parse df output
|
||||
parts = lines[1].split()
|
||||
total_kb = int(parts[1])
|
||||
used_kb = int(parts[2])
|
||||
available_kb = int(parts[3])
|
||||
percent = int(parts[4].rstrip('%'))
|
||||
|
||||
return {
|
||||
'total': total_kb * 1024,
|
||||
'used': used_kb * 1024,
|
||||
'available': available_kb * 1024,
|
||||
'percent': percent,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def save_snapshot(name):
|
||||
"""Save disk usage snapshot to file."""
|
||||
snapshot_dir = Path.home() / '.macos-cleaner'
|
||||
snapshot_dir.mkdir(exist_ok=True)
|
||||
|
||||
snapshot_file = snapshot_dir / f'{name}.json'
|
||||
|
||||
usage = get_disk_usage()
|
||||
if usage:
|
||||
with snapshot_file.open('w') as f:
|
||||
json.dump(usage, f, indent=2)
|
||||
print(f"✅ Snapshot saved: {snapshot_file}")
|
||||
return True
|
||||
else:
|
||||
print("❌ Failed to get disk usage")
|
||||
return False
|
||||
|
||||
|
||||
def load_snapshot(name):
|
||||
"""Load disk usage snapshot from file."""
|
||||
snapshot_dir = Path.home() / '.macos-cleaner'
|
||||
snapshot_file = snapshot_dir / f'{name}.json'
|
||||
|
||||
if not snapshot_file.exists():
|
||||
print(f"❌ Snapshot not found: {snapshot_file}")
|
||||
return None
|
||||
|
||||
with snapshot_file.open('r') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def generate_report(before, after):
|
||||
"""Generate comparison report."""
|
||||
print("\n" + "=" * 60)
|
||||
print("📊 Cleanup Report")
|
||||
print("=" * 60)
|
||||
|
||||
# Time
|
||||
before_time = datetime.fromisoformat(before['timestamp'])
|
||||
after_time = datetime.fromisoformat(after['timestamp'])
|
||||
duration = after_time - before_time
|
||||
|
||||
print(f"\nCleanup Duration: {duration}")
|
||||
print(f"Before: {before_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print(f"After: {after_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
# Disk usage comparison
|
||||
print("\n" + "-" * 60)
|
||||
print("Disk Usage")
|
||||
print("-" * 60)
|
||||
|
||||
before_used = before['used']
|
||||
after_used = after['used']
|
||||
recovered = before_used - after_used
|
||||
|
||||
print(f"Before: {format_size(before_used):>12} ({before['percent']}%)")
|
||||
print(f"After: {format_size(after_used):>12} ({after['percent']}%)")
|
||||
print("-" * 60)
|
||||
|
||||
if recovered > 0:
|
||||
print(f"✅ Recovered: {format_size(recovered):>12}")
|
||||
percent_recovered = (recovered / before_used) * 100
|
||||
print(f" ({percent_recovered:.1f}% of used space)")
|
||||
elif recovered < 0:
|
||||
print(f"⚠️ Space increased: {format_size(abs(recovered)):>12}")
|
||||
print(" (This may be due to system activity during cleanup)")
|
||||
else:
|
||||
print("No change in disk usage")
|
||||
|
||||
# Available space
|
||||
print("\n" + "-" * 60)
|
||||
print("Available Space")
|
||||
print("-" * 60)
|
||||
|
||||
before_avail = before['available']
|
||||
after_avail = after['available']
|
||||
gained = after_avail - before_avail
|
||||
|
||||
print(f"Before: {format_size(before_avail):>12}")
|
||||
print(f"After: {format_size(after_avail):>12}")
|
||||
print("-" * 60)
|
||||
|
||||
if gained > 0:
|
||||
print(f"✅ Gained: {format_size(gained):>12}")
|
||||
elif gained < 0:
|
||||
print(f"⚠️ Lost: {format_size(abs(gained)):>12}")
|
||||
else:
|
||||
print("No change")
|
||||
|
||||
# Recommendations
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
if after['percent'] > 90:
|
||||
print("⚠️ Warning: Disk is still >90% full")
|
||||
print("\n💡 Recommendations:")
|
||||
print(" - Consider moving large files to external storage")
|
||||
print(" - Review and delete old projects")
|
||||
print(" - Check for large application data")
|
||||
elif after['percent'] > 80:
|
||||
print("⚠️ Disk usage is still high (>80%)")
|
||||
print("\n💡 Recommendations:")
|
||||
print(" - Run cleanup again in 1-2 weeks")
|
||||
print(" - Monitor large file creation")
|
||||
else:
|
||||
print("✅ Disk usage is healthy!")
|
||||
print("\n💡 Maintenance Tips:")
|
||||
print(" - Run cleanup monthly")
|
||||
print(" - Empty Trash regularly")
|
||||
print(" - Clear browser caches weekly")
|
||||
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Generate cleanup reports'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--snapshot',
|
||||
choices=['before', 'after'],
|
||||
required=True,
|
||||
help='Snapshot type (before or after cleanup)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--compare',
|
||||
action='store_true',
|
||||
help='Compare with before snapshot (use with --snapshot after)'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.snapshot == 'before':
|
||||
# Save before snapshot
|
||||
print("📸 Capturing disk usage before cleanup...")
|
||||
if save_snapshot('before'):
|
||||
usage = get_disk_usage()
|
||||
print(f"\nCurrent Usage: {format_size(usage['used'])} ({usage['percent']}%)")
|
||||
print(f"Available: {format_size(usage['available'])}")
|
||||
print("\n💡 Run cleanup operations, then:")
|
||||
print(" python3 cleanup_report.py --snapshot after --compare")
|
||||
return 0
|
||||
|
||||
elif args.snapshot == 'after':
|
||||
# Save after snapshot
|
||||
print("📸 Capturing disk usage after cleanup...")
|
||||
if not save_snapshot('after'):
|
||||
return 1
|
||||
|
||||
if args.compare:
|
||||
# Load before snapshot and compare
|
||||
before = load_snapshot('before')
|
||||
after = load_snapshot('after')
|
||||
|
||||
if before and after:
|
||||
generate_report(before, after)
|
||||
else:
|
||||
print("❌ Cannot compare: missing snapshots")
|
||||
return 1
|
||||
else:
|
||||
usage = get_disk_usage()
|
||||
print(f"\nCurrent Usage: {format_size(usage['used'])} ({usage['percent']}%)")
|
||||
print(f"Available: {format_size(usage['available'])}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
246
macos-cleaner/scripts/find_app_remnants.py
Executable file
246
macos-cleaner/scripts/find_app_remnants.py
Executable file
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Find orphaned application support files and preferences.
|
||||
|
||||
This script identifies directories in ~/Library that may belong to
|
||||
uninstalled applications.
|
||||
|
||||
Usage:
|
||||
python3 find_app_remnants.py [--min-size SIZE]
|
||||
|
||||
Options:
|
||||
--min-size Minimum size in MB to report (default: 10)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def format_size(bytes_size):
|
||||
"""Convert bytes to human-readable format."""
|
||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||
if bytes_size < 1024.0:
|
||||
return f"{bytes_size:.1f} {unit}"
|
||||
bytes_size /= 1024.0
|
||||
return f"{bytes_size:.1f} PB"
|
||||
|
||||
|
||||
def get_dir_size(path):
|
||||
"""Get directory size using du command."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['du', '-sk', path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
if result.returncode == 0:
|
||||
size_kb = int(result.stdout.split()[0])
|
||||
return size_kb * 1024
|
||||
return 0
|
||||
except (subprocess.TimeoutExpired, ValueError, IndexError):
|
||||
return 0
|
||||
|
||||
|
||||
def get_installed_apps():
|
||||
"""Get list of installed application names."""
|
||||
apps = set()
|
||||
|
||||
# System applications
|
||||
system_app_dir = Path('/Applications')
|
||||
if system_app_dir.exists():
|
||||
for app in system_app_dir.iterdir():
|
||||
if app.suffix == '.app':
|
||||
# Remove .app suffix
|
||||
apps.add(app.stem)
|
||||
|
||||
# User applications
|
||||
user_app_dir = Path.home() / 'Applications'
|
||||
if user_app_dir.exists():
|
||||
for app in user_app_dir.iterdir():
|
||||
if app.suffix == '.app':
|
||||
apps.add(app.stem)
|
||||
|
||||
return apps
|
||||
|
||||
|
||||
def normalize_name(name):
|
||||
"""
|
||||
Normalize app name for matching.
|
||||
|
||||
Examples:
|
||||
'Google Chrome' -> 'googlechrome'
|
||||
'com.apple.Safari' -> 'safari'
|
||||
"""
|
||||
# Remove common prefixes
|
||||
for prefix in ['com.', 'org.', 'net.', 'io.']:
|
||||
if name.startswith(prefix):
|
||||
name = name[len(prefix):]
|
||||
|
||||
# Remove non-alphanumeric
|
||||
name = ''.join(c for c in name if c.isalnum())
|
||||
|
||||
return name.lower()
|
||||
|
||||
|
||||
def is_likely_orphaned(dir_name, installed_apps):
|
||||
"""
|
||||
Check if directory is likely orphaned.
|
||||
|
||||
Returns:
|
||||
(is_orphaned, confidence, reason)
|
||||
confidence: 'high' | 'medium' | 'low'
|
||||
"""
|
||||
norm_dir = normalize_name(dir_name)
|
||||
|
||||
# Check exact matches
|
||||
for app in installed_apps:
|
||||
norm_app = normalize_name(app)
|
||||
if norm_app in norm_dir or norm_dir in norm_app:
|
||||
return (False, None, f"Matches installed app: {app}")
|
||||
|
||||
# System/common directories to always keep
|
||||
system_dirs = {
|
||||
'apple', 'safari', 'finder', 'mail', 'messages', 'notes',
|
||||
'photos', 'music', 'calendar', 'contacts', 'reminders',
|
||||
'preferences', 'cookies', 'webkit', 'coredata',
|
||||
'cloudkit', 'icloud', 'appstore', 'systemmigration'
|
||||
}
|
||||
|
||||
if any(sys_dir in norm_dir for sys_dir in system_dirs):
|
||||
return (False, None, "System/built-in application")
|
||||
|
||||
# If we get here, likely orphaned
|
||||
return (True, 'medium', "No matching application found")
|
||||
|
||||
|
||||
def analyze_library_dir(library_path, min_size_bytes, installed_apps):
|
||||
"""
|
||||
Analyze a Library subdirectory for orphaned data.
|
||||
|
||||
Args:
|
||||
library_path: Path to scan (e.g., ~/Library/Application Support)
|
||||
min_size_bytes: Minimum size to report
|
||||
installed_apps: Set of installed app names
|
||||
|
||||
Returns:
|
||||
List of (name, path, size, confidence, reason) tuples
|
||||
"""
|
||||
if not os.path.exists(library_path):
|
||||
return []
|
||||
|
||||
results = []
|
||||
|
||||
try:
|
||||
for entry in os.scandir(library_path):
|
||||
if entry.is_dir():
|
||||
size = get_dir_size(entry.path)
|
||||
if size >= min_size_bytes:
|
||||
is_orphaned, confidence, reason = is_likely_orphaned(
|
||||
entry.name,
|
||||
installed_apps
|
||||
)
|
||||
if is_orphaned:
|
||||
results.append((
|
||||
entry.name,
|
||||
entry.path,
|
||||
size,
|
||||
confidence,
|
||||
reason
|
||||
))
|
||||
except PermissionError:
|
||||
print(f"⚠️ Permission denied: {library_path}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
# Sort by size descending
|
||||
results.sort(key=lambda x: x[2], reverse=True)
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Find orphaned application data'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--min-size',
|
||||
type=int,
|
||||
default=10,
|
||||
help='Minimum size in MB to report (default: 10)'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
min_size_bytes = args.min_size * 1024 * 1024
|
||||
|
||||
print("🔍 Searching for Orphaned Application Data")
|
||||
print("=" * 70)
|
||||
|
||||
# Get installed apps
|
||||
print("Scanning installed applications...")
|
||||
installed_apps = get_installed_apps()
|
||||
print(f"Found {len(installed_apps)} installed applications\n")
|
||||
|
||||
# Directories to check
|
||||
library_dirs = {
|
||||
'Application Support': Path.home() / 'Library' / 'Application Support',
|
||||
'Containers': Path.home() / 'Library' / 'Containers',
|
||||
'Preferences': Path.home() / 'Library' / 'Preferences',
|
||||
'Saved Application State': Path.home() / 'Library' / 'Saved Application State'
|
||||
}
|
||||
|
||||
all_orphans = []
|
||||
total_size = 0
|
||||
|
||||
for category, path in library_dirs.items():
|
||||
print(f"\n📂 {category}")
|
||||
print("-" * 70)
|
||||
|
||||
orphans = analyze_library_dir(path, min_size_bytes, installed_apps)
|
||||
|
||||
if orphans:
|
||||
print(f"{'Name':<40} {'Size':<12} {'Confidence'}")
|
||||
print("-" * 70)
|
||||
|
||||
for name, full_path, size, confidence, reason in orphans:
|
||||
conf_icon = {'high': '🔴', 'medium': '🟡', 'low': '🟢'}[confidence]
|
||||
# Truncate long names
|
||||
display_name = name if len(name) <= 37 else name[:34] + "..."
|
||||
print(f"{display_name:<40} {format_size(size):<12} {conf_icon} {confidence}")
|
||||
|
||||
all_orphans.append((category, name, full_path, size, confidence, reason))
|
||||
total_size += size
|
||||
else:
|
||||
print("No orphaned data found above minimum size")
|
||||
|
||||
# Summary
|
||||
print("\n\n📊 Summary")
|
||||
print("=" * 70)
|
||||
print(f"Total orphaned data found: {len(all_orphans)} items")
|
||||
print(f"Total size: {format_size(total_size)}")
|
||||
|
||||
if all_orphans:
|
||||
print("\n\n🗑️ Recommended Deletions (Medium/High Confidence)")
|
||||
print("=" * 70)
|
||||
|
||||
for category, name, path, size, confidence, reason in all_orphans:
|
||||
if confidence in ['medium', 'high']:
|
||||
print(f"\n{name}")
|
||||
print(f" Location: {path}")
|
||||
print(f" Size: {format_size(size)}")
|
||||
print(f" Reason: {reason}")
|
||||
print(f" ⚠️ Verify this app is truly uninstalled before deleting")
|
||||
|
||||
print("\n\n💡 Next Steps:")
|
||||
print(" 1. Double-check each item in /Applications and ~/Applications")
|
||||
print(" 2. Search Spotlight for the application name")
|
||||
print(" 3. If truly uninstalled, safe to delete with:")
|
||||
print(" rm -rf '<path>'")
|
||||
print(" 4. Or use safe_delete.py for interactive cleanup")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
305
macos-cleaner/scripts/safe_delete.py
Executable file
305
macos-cleaner/scripts/safe_delete.py
Executable file
@@ -0,0 +1,305 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Interactive safe file/directory deletion with confirmation.
|
||||
|
||||
Usage:
|
||||
python3 safe_delete.py <path1> [path2] [path3] ...
|
||||
python3 safe_delete.py --batch <file_with_paths>
|
||||
|
||||
Options:
|
||||
--batch FILE Read paths from a file (one per line)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import argparse
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def format_size(bytes_size):
|
||||
"""Convert bytes to human-readable format."""
|
||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
||||
if bytes_size < 1024.0:
|
||||
return f"{bytes_size:.1f} {unit}"
|
||||
bytes_size /= 1024.0
|
||||
return f"{bytes_size:.1f} PB"
|
||||
|
||||
|
||||
def get_size(path):
|
||||
"""Get size of file or directory."""
|
||||
path_obj = Path(path)
|
||||
|
||||
if not path_obj.exists():
|
||||
return 0
|
||||
|
||||
if path_obj.is_file():
|
||||
return path_obj.stat().st_size
|
||||
elif path_obj.is_dir():
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['du', '-sk', path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
if result.returncode == 0:
|
||||
size_kb = int(result.stdout.split()[0])
|
||||
return size_kb * 1024
|
||||
except (subprocess.TimeoutExpired, ValueError, IndexError):
|
||||
pass
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def get_description(path):
|
||||
"""Get human-readable description of path."""
|
||||
path_obj = Path(path)
|
||||
|
||||
if not path_obj.exists():
|
||||
return "Path does not exist"
|
||||
|
||||
if path_obj.is_file():
|
||||
suffix = path_obj.suffix or "file"
|
||||
return f"File ({suffix})"
|
||||
elif path_obj.is_dir():
|
||||
try:
|
||||
# Count items
|
||||
items = list(path_obj.iterdir())
|
||||
return f"Directory ({len(items)} items)"
|
||||
except PermissionError:
|
||||
return "Directory (permission denied to list)"
|
||||
|
||||
return "Unknown"
|
||||
|
||||
|
||||
def confirm_delete(path, size, description):
|
||||
"""
|
||||
Ask user to confirm deletion.
|
||||
|
||||
Args:
|
||||
path: File/directory path
|
||||
size: Size in bytes
|
||||
description: What this file/directory is
|
||||
|
||||
Returns:
|
||||
True if user confirms, False otherwise
|
||||
"""
|
||||
print(f"\n🗑️ Confirm Deletion")
|
||||
print("━" * 50)
|
||||
print(f"Path: {path}")
|
||||
print(f"Size: {format_size(size)}")
|
||||
print(f"Description: {description}")
|
||||
|
||||
# Additional safety check for important paths
|
||||
path_str = str(path).lower()
|
||||
danger_patterns = [
|
||||
'documents', 'desktop', 'pictures', 'movies',
|
||||
'downloads', 'music', '.ssh', 'credentials'
|
||||
]
|
||||
|
||||
if any(pattern in path_str for pattern in danger_patterns):
|
||||
print("\n⚠️ WARNING: This path may contain important personal data!")
|
||||
print(" Consider backing up before deletion.")
|
||||
|
||||
response = input("\nDelete this item? [y/N]: ").strip().lower()
|
||||
return response == 'y'
|
||||
|
||||
|
||||
def batch_confirm(items):
|
||||
"""
|
||||
Show all items, ask for batch confirmation.
|
||||
|
||||
Args:
|
||||
items: List of (path, size, description) tuples
|
||||
|
||||
Returns:
|
||||
List of items user approved
|
||||
"""
|
||||
print("\n📋 Items to Delete:")
|
||||
print("━" * 70)
|
||||
print(f"{'#':<4} {'Size':<12} {'Path'}")
|
||||
print("-" * 70)
|
||||
|
||||
for i, (path, size, description) in enumerate(items, 1):
|
||||
# Truncate long paths
|
||||
display_path = str(path)
|
||||
if len(display_path) > 48:
|
||||
display_path = display_path[:45] + "..."
|
||||
print(f"{i:<4} {format_size(size):<12} {display_path}")
|
||||
|
||||
total_size = sum(item[1] for item in items)
|
||||
print("-" * 70)
|
||||
print(f"{'Total':<4} {format_size(total_size):<12}")
|
||||
|
||||
print("\nOptions:")
|
||||
print(" 'all' - Delete all items")
|
||||
print(" '1,3,5' - Delete specific items by number")
|
||||
print(" '1-5' - Delete range of items")
|
||||
print(" 'none' - Cancel (default)")
|
||||
|
||||
response = input("\nYour choice: ").strip().lower()
|
||||
|
||||
if response == '' or response == 'none':
|
||||
return []
|
||||
elif response == 'all':
|
||||
return items
|
||||
else:
|
||||
selected = []
|
||||
# Parse response
|
||||
parts = response.replace(' ', '').split(',')
|
||||
|
||||
for part in parts:
|
||||
try:
|
||||
if '-' in part:
|
||||
# Range: 1-5
|
||||
start, end = part.split('-')
|
||||
start_idx = int(start) - 1
|
||||
end_idx = int(end) - 1
|
||||
for i in range(start_idx, end_idx + 1):
|
||||
if 0 <= i < len(items):
|
||||
selected.append(items[i])
|
||||
else:
|
||||
# Single number
|
||||
idx = int(part) - 1
|
||||
if 0 <= idx < len(items):
|
||||
selected.append(items[idx])
|
||||
except ValueError:
|
||||
print(f"⚠️ Ignoring invalid selection: {part}")
|
||||
continue
|
||||
|
||||
return selected
|
||||
|
||||
|
||||
def delete_path(path):
|
||||
"""
|
||||
Delete a file or directory.
|
||||
|
||||
Returns:
|
||||
(success, message)
|
||||
"""
|
||||
try:
|
||||
path_obj = Path(path)
|
||||
|
||||
if not path_obj.exists():
|
||||
return (False, "Path does not exist")
|
||||
|
||||
if path_obj.is_file():
|
||||
path_obj.unlink()
|
||||
elif path_obj.is_dir():
|
||||
shutil.rmtree(path)
|
||||
else:
|
||||
return (False, "Unknown path type")
|
||||
|
||||
return (True, "Deleted successfully")
|
||||
|
||||
except PermissionError:
|
||||
return (False, "Permission denied")
|
||||
except Exception as e:
|
||||
return (False, f"Error: {str(e)}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Interactive safe deletion'
|
||||
)
|
||||
parser.add_argument(
|
||||
'paths',
|
||||
nargs='*',
|
||||
help='Paths to delete'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--batch',
|
||||
metavar='FILE',
|
||||
help='Read paths from file (one per line)'
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Collect paths
|
||||
paths = []
|
||||
|
||||
if args.batch:
|
||||
# Read from file
|
||||
batch_file = Path(args.batch)
|
||||
if not batch_file.exists():
|
||||
print(f"❌ Batch file not found: {args.batch}")
|
||||
return 1
|
||||
|
||||
with batch_file.open('r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#'):
|
||||
paths.append(line)
|
||||
else:
|
||||
paths = args.paths
|
||||
|
||||
if not paths:
|
||||
parser.print_help()
|
||||
return 1
|
||||
|
||||
# Prepare items
|
||||
items = []
|
||||
for path in paths:
|
||||
size = get_size(path)
|
||||
description = get_description(path)
|
||||
items.append((path, size, description))
|
||||
|
||||
# Remove non-existent paths
|
||||
items = [(p, s, d) for p, s, d in items if Path(p).exists()]
|
||||
|
||||
if not items:
|
||||
print("❌ No valid paths to delete")
|
||||
return 1
|
||||
|
||||
# Interactive mode
|
||||
if len(items) == 1:
|
||||
# Single item - simple confirmation
|
||||
path, size, description = items[0]
|
||||
if not confirm_delete(path, size, description):
|
||||
print("\n✅ Deletion cancelled")
|
||||
return 0
|
||||
|
||||
success, message = delete_path(path)
|
||||
if success:
|
||||
print(f"\n✅ {message}")
|
||||
print(f" Freed: {format_size(size)}")
|
||||
return 0
|
||||
else:
|
||||
print(f"\n❌ {message}")
|
||||
return 1
|
||||
|
||||
else:
|
||||
# Multiple items - batch confirmation
|
||||
selected = batch_confirm(items)
|
||||
|
||||
if not selected:
|
||||
print("\n✅ Deletion cancelled")
|
||||
return 0
|
||||
|
||||
# Delete selected items
|
||||
print(f"\n🗑️ Deleting {len(selected)} items...")
|
||||
print("━" * 50)
|
||||
|
||||
success_count = 0
|
||||
total_freed = 0
|
||||
|
||||
for path, size, description in selected:
|
||||
success, message = delete_path(path)
|
||||
status_icon = "✅" if success else "❌"
|
||||
print(f"{status_icon} {path}: {message}")
|
||||
|
||||
if success:
|
||||
success_count += 1
|
||||
total_freed += size
|
||||
|
||||
print("━" * 50)
|
||||
print(f"\n📊 Results:")
|
||||
print(f" Successfully deleted: {success_count}/{len(selected)}")
|
||||
print(f" Total freed: {format_size(total_freed)}")
|
||||
|
||||
return 0 if success_count == len(selected) else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user