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:
daymade
2026-01-11 15:59:13 +08:00
parent 3f2f8f02ca
commit 4d6ed53c1e
16 changed files with 3546 additions and 10 deletions

View 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
View 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.

View 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.

View 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.

View 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.

View 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())

View 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())

View 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())

View 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())

View 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())

View 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())