diff --git a/AGENT.md b/AGENT.md deleted file mode 100644 index c2c0f64..0000000 --- a/AGENT.md +++ /dev/null @@ -1,114 +0,0 @@ -# Mole AI Agent Guide - -Quick reference for AI assistants working on Mole (Mac system cleaner). -**Last updated**: 2026-01-04 - -## Safety Checklist - -Before any operation: - -- Use `safe_*` helpers (never raw `rm -rf` or `find -delete`) -- Check protection: `is_protected()`, `is_whitelisted()` -- Test first: `MO_DRY_RUN=1 ./mole clean` -- Validate syntax: `bash -n ` -- Run tests: `./scripts/test.sh` - -## Never Do - -- Raw deletions without `safe_*` helpers -- Remove `--prefix`/`--config` flags from install.sh -- Commit code unless explicitly requested -- Mix languages: Python in shell, shell in Go -- Delete without checking protection lists - -## Architecture Quick Map - -``` -mole # CLI entrypoint (menu + routing) -├── bin/ # Commands: clean, uninstall, optimize, analyze, status -├── lib/ # Shell logic: core/, clean/, ui/ -├── cmd/ # Go apps: analyze/, status/ -├── tests/ # BATS integration tests -└── scripts/ # Build and test automation -``` - -**Decision Tree**: - -- User cleanup logic → `lib/clean/.sh` -- Command entry → `bin/.sh` -- Core utils → `lib/core/.sh` -- Performance tool → `cmd//*.go` -- Tests → `tests/.bats` - -## Common Commands - -```bash -# Validation (run before suggesting changes) -bash -n # Syntax check -./scripts/test.sh # Full test suite -MO_DRY_RUN=1 ./mole clean # Safe dry-run test - -# Development -make build # Build Go binaries -go run ./cmd/analyze # Test without building -bats tests/clean.bats -f "name" # Specific test - -# Debugging -MO_DEBUG=1 ./mole clean # Verbose output -``` - -## Code Style Rules - -**Shell** (Bash 3.2 compatible): - -- 2-space indent, quote variables: `"$var"` -- Use `[[` not `[`, prefer `$(cmd)` over backticks -- Comments: English, intent-focused, for safety boundaries only -- Entry scripts: 2-3 line header describing purpose - -**Go**: - -- Standard conventions: `gofmt`, `go vet` -- Never ignore errors - -## Key Helpers - -- `safe_rm ` - Protected deletion -- `safe_find_delete ` - Safe find+delete -- `is_protected ` - Check system protection -- `is_whitelisted ` - Check user whitelist -- `log_info/success/warn/error ` - Logging - -## Workflow - -1. **Read first**: Never propose changes to unread code -2. **Shell work**: Logic in `lib/`, called from `bin/` -3. **Go work**: Edit `cmd//*.go` -4. **Test**: Dry-run → BATS → full test suite -5. **Style**: Match existing file conventions (Bash 3.2) - -## Example: Add New Cleanup - -```bash -# 1. Create lib/clean/my_module.sh -clean_my_cache() { - local dir="$HOME/Library/Caches/MyApp" - [[ -d "$dir" ]] && ! is_whitelisted "my_app" || return - safe_find_delete "$dir" "*" "30" "f" - log_success "Cleaned MyApp cache" -} - -# 2. Call from bin/clean.sh -source "${LIB_DIR}/clean/my_module.sh" -clean_my_cache - -# 3. Test -bats tests/clean.bats -f "my_cache" -``` - -## Troubleshooting - -- Tests fail → `bats tests/.bats -f "test name"` -- Syntax error → `bash -n ` -- Permission denied → `./mole touchid` -- Cleanup not working → Check `is_protected()` or `~/.config/mole/whitelist` diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..490036c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,374 @@ +# AGENTS.md - Development Guide for Mole + +This guide provides AI coding assistants with essential commands, patterns, and conventions for working in the Mole codebase. + +**Quick reference**: Build/test commands • Safety rules • Architecture map • Code style + +--- + +## Safety Checklist + +Before any operation: + +- Use `safe_*` helpers (never raw `rm -rf` or `find -delete`) +- Check protection: `is_protected()`, `is_whitelisted()` +- Test first: `MO_DRY_RUN=1 ./mole clean` +- Validate syntax: `bash -n ` +- Run tests: `./scripts/test.sh` + +## NEVER Do These + +- Run `rm -rf` or any raw deletion commands +- Delete files without checking protection lists +- Modify system-critical paths (e.g., `/System`, `/Library/Apple`) +- Remove installer flags `--prefix`/`--config` from `install.sh` +- Commit code changes unless explicitly requested +- Run destructive operations without dry-run validation +- Use raw `git` commands when `gh` CLI is available + +## ALWAYS Do These + +- Use `safe_*` helper functions for deletions (`safe_rm`, `safe_find_delete`) +- Respect whitelist files (e.g., `~/.config/mole/whitelist`) +- Check protection logic before cleanup operations +- Test with dry-run modes first +- Validate syntax before suggesting changes: `bash -n ` +- Use `gh` CLI for all GitHub operations (issues, PRs, releases, etc.) +- Never commit code unless explicitly requested by user + +--- + +## Quick Reference + +### Build Commands +```bash +# Build Go binaries for current platform +make build + +# Build release binaries (cross-platform) +make release-amd64 # macOS Intel +make release-arm64 # macOS Apple Silicon + +# Clean build artifacts +make clean +``` + +### Test Commands +```bash +# Run full test suite (recommended before commits) +./scripts/test.sh + +# Run specific BATS test file +bats tests/clean.bats + +# Run specific test case by name +bats tests/clean.bats -f "should respect whitelist" + +# Run Go tests only +go test -v ./cmd/... + +# Run Go tests for specific package +go test -v ./cmd/analyze + +# Shell syntax check +bash -n lib/clean/user.sh +bash -n mole + +# Lint shell scripts +shellcheck --rcfile .shellcheckrc lib/**/*.sh bin/**/*.sh +``` + +### Development Commands +```bash +# Test cleanup in dry-run mode +MO_DRY_RUN=1 ./mole clean + +# Enable debug logging +MO_DEBUG=1 ./mole clean + +# Test Go tool directly +go run ./cmd/analyze + +# Test installation locally +./install.sh --prefix /usr/local/bin --config ~/.config/mole +``` + +--- + +## Architecture Quick Map + +``` +mole/ # Main CLI entrypoint (menu + routing) +├── mo # CLI alias wrapper +├── install.sh # Manual installer/updater (preserves --prefix/--config) +├── bin/ # Command entry points (thin wrappers) +│ ├── clean.sh # Deep cleanup orchestrator +│ ├── uninstall.sh # App removal with leftover detection +│ ├── optimize.sh # Cache rebuild + service refresh +│ ├── purge.sh # Aggressive cleanup mode +│ ├── touchid.sh # Touch ID sudo enabler +│ ├── analyze.sh # Disk usage explorer wrapper +│ └── status.sh # System health dashboard wrapper +├── lib/ # Reusable shell logic +│ ├── core/ # base.sh, log.sh, sudo.sh, ui.sh +│ ├── clean/ # Cleanup modules (user, apps, dev, caches, system) +│ └── ui/ # Confirmation dialogs, progress bars +├── cmd/ # Go applications +│ ├── analyze/ # Disk analysis tool +│ └── status/ # Real-time monitoring +├── scripts/ # Build and test automation +│ └── test.sh # Main test runner (shell + go + BATS) +└── tests/ # BATS integration tests +``` + +**Decision Tree**: + +- User cleanup logic → `lib/clean/.sh` +- Command entry → `bin/.sh` +- Core utils → `lib/core/.sh` +- Performance tool → `cmd//*.go` +- Tests → `tests/.bats` + +### Language Stack +- **Shell (Bash 3.2)**: Core cleanup and system operations (`lib/`, `bin/`) +- **Go**: Performance-critical tools (`cmd/analyze/`, `cmd/status/`) +- **BATS**: Integration testing (`tests/`) + +--- + +## Code Style Guidelines + +### Shell Scripts +- **Indentation**: 4 spaces (configured in .editorconfig) +- **Variables**: `lowercase_with_underscores` +- **Functions**: `verb_noun` format (e.g., `clean_caches`, `get_size`) +- **Constants**: `UPPERCASE_WITH_UNDERSCORES` +- **Quoting**: Always quote variables: `"$var"` not `$var` +- **Tests**: Use `[[` instead of `[` +- **Command substitution**: Use `$(command)` not backticks +- **Error handling**: Use `set -euo pipefail` at top of files + +### Go Code +- **Formatting**: Follow standard Go conventions (`gofmt`, `go vet`) +- **Package docs**: Add package-level documentation for exported functions +- **Error handling**: Never ignore errors, always handle them explicitly +- **Build tags**: Use `//go:build darwin` for macOS-specific code + +### Comments +- **Language**: English only +- **Focus**: Explain "why" not "what" (code should be self-documenting) +- **Safety**: Document safety boundaries explicitly +- **Non-obvious logic**: Explain workarounds or complex patterns + +--- + +## Key Helper Functions + +### Safety Helpers (lib/core/base.sh) +- `safe_rm `: Safe deletion with validation +- `safe_find_delete `: Protected find+delete +- `is_protected `: Check if path is system-protected +- `is_whitelisted `: Check user whitelist + +### Logging (lib/core/log.sh) +- `log_info `: Informational messages +- `log_success `: Success notifications +- `log_warn `: Warnings +- `log_error `: Error messages +- `debug `: Debug output (requires MO_DEBUG=1) + +### UI Helpers (lib/core/ui.sh) +- `confirm `: Yes/no confirmation +- `show_progress `: Progress display + +--- + +## Testing Strategy + +### Test Types +1. **Syntax Validation**: `bash -n ` - catches basic errors +2. **Unit Tests**: BATS tests for individual functions +3. **Integration Tests**: Full command execution with BATS +4. **Dry-run Tests**: `MO_DRY_RUN=1` to validate without deletion +5. **Go Tests**: `go test -v ./cmd/...` + +### Test Environment Variables +- `MO_DRY_RUN=1`: Preview changes without execution +- `MO_DEBUG=1`: Enable detailed debug logging +- `BATS_FORMATTER=pretty`: Use pretty output for BATS (default) +- `BATS_FORMATTER=tap`: Use TAP output for CI + +--- + +## Common Development Tasks + +### Adding New Cleanup Module +1. Create `lib/clean/new_module.sh` +2. Implement cleanup logic using `safe_*` helpers +3. Source it in `bin/clean.sh` +4. Add protection checks for critical paths +5. Write BATS test in `tests/clean.bats` +6. Test with `MO_DRY_RUN=1` first + +### Modifying Go Tools +1. Navigate to `cmd//` +2. Make changes to Go files +3. Test with `go run .` or `make build && ./bin/-go` +4. Run `go test -v` for unit tests +5. Check integration: `./mole ` + +### Debugging Issues +1. Enable debug mode: `MO_DEBUG=1 ./mole clean` +2. Check logs for error messages +3. Verify sudo permissions: `sudo -n true` or `./mole touchid` +4. Test individual functions in isolation +5. Use `shellcheck` for shell script issues + +--- + +## Linting and Quality + +### Shell Script Linting +- **Tool**: shellcheck with custom `.shellcheckrc` +- **Disabled rules**: SC2155, SC2034, SC2059, SC1091, SC2038 +- **Command**: `shellcheck --rcfile .shellcheckrc lib/**/*.sh bin/**/*.sh` + +### Go Code Quality +- **Tools**: `go vet`, `go fmt`, `go test` +- **Command**: `go vet ./cmd/... && go test ./cmd/...` + +### CI/CD Pipeline +- **Triggers**: Push/PR to main, dev branches +- **Platforms**: macOS 14, macOS 15 +- **Tools**: bats-core, shellcheck, Go 1.24.6 +- **Security checks**: Unsafe rm usage, app protection, secret scanning + +--- + +## File Organization Patterns + +### Shell Modules +- Entry scripts in `bin/` should be thin wrappers +- Reusable logic goes in `lib/` +- Core utilities in `lib/core/` +- Feature-specific modules in `lib/clean/`, `lib/ui/`, etc. + +### Go Packages +- Each tool in its own `cmd//` directory +- Main entry point in `main.go` +- Use standard Go project layout +- macOS-specific code guarded with build tags + +--- + +## GitHub Operations + +### Use gh CLI for All GitHub Work + +**Preferred Commands**: +```bash +# Issues +gh issue view 123 # View issue details +gh issue list # List issues +gh issue comment 123 "message" # Comment on issue + +# Pull Requests +gh pr view # View current PR +gh pr diff # Show diff +gh pr list # List PRs +gh pr checkout 123 # Checkout PR branch +gh pr merge # Merge current PR + +# Repository operations +gh release create v1.0.0 # Create release +gh repo view # Repository info +gh api repos/owner/repo/issues # Raw API access +``` + +**NEVER use raw git commands for GitHub operations** when `gh` is available: +- ❌ `git log --oneline origin/main..HEAD` → ✅ `gh pr view` +- ❌ `git remote get-url origin` → ✅ `gh repo view` +- ❌ Manual GitHub API curl commands → ✅ `gh api` + +## Error Handling Patterns + +### Shell Scripts +- Use `set -euo pipefail` for strict error handling +- Check command exit codes: `if command; then ...` +- Provide meaningful error messages with `log_error` +- Use cleanup traps for temporary resources + +### Go Code +- Never ignore errors: `if err != nil { return err }` +- Use structured error messages +- Handle context cancellation appropriately +- Log errors with context information + +--- + +## Performance Considerations + +### Shell Optimization +- Use built-in shell operations over external commands +- Prefer `find -delete` over `-exec rm` +- Minimize subprocess creation +- Use appropriate timeout mechanisms + +### Go Optimization +- Use concurrency for I/O-bound operations +- Implement proper caching for expensive operations +- Profile memory usage in scanning operations +- Use efficient data structures for large datasets + +--- + +## Security Best Practices + +### Path Validation +- Always validate user-provided paths +- Check against protection lists before operations +- Use absolute paths to prevent directory traversal +- Implement proper sandboxing for destructive operations + +### Permission Management +- Request sudo only when necessary +- Use `sudo -n true` to check sudo availability +- Implement proper Touch ID integration +- Respect user whitelist configurations + +--- + +## Common Pitfalls to Avoid + +1. **Over-engineering**: Keep solutions simple. Don't add abstractions for one-time operations. +2. **Premature optimization**: Focus on correctness first, performance second. +3. **Assuming paths exist**: Always check before operating on files/directories. +4. **Ignoring protection logic**: User data loss is unacceptable. +5. **Breaking updates**: Keep `--prefix`/`--config` flags in `install.sh`. +6. **Platform assumptions**: Code must work on all supported macOS versions (10.13+). +7. **Silent failures**: Always log errors and provide actionable messages. + +--- + +## Communication Style + +- Be concise and technical +- Explain safety implications upfront +- Show before/after for significant changes +- Provide file:line references for code locations +- Suggest testing steps for validation + +--- + +## Resources + +- Main script: `mole` (menu + routing logic) +- Protection lists: Check `is_protected()` implementations +- User config: `~/.config/mole/` +- Test directory: `tests/` +- Build scripts: `scripts/` +- Documentation: `README.md`, `CONTRIBUTING.md`, `SECURITY_AUDIT.md` + +--- + +**Remember**: When in doubt, err on the side of safety. It's better to clean less than to risk user data. \ No newline at end of file diff --git a/bin/installer.sh b/bin/installer.sh index a6bfab4..73a8bd8 100755 --- a/bin/installer.sh +++ b/bin/installer.sh @@ -43,7 +43,7 @@ readonly INSTALLER_SCAN_PATHS=( "$HOME/Library/Application Support/Telegram Desktop" "$HOME/Downloads/Telegram Desktop" ) -readonly MAX_ZIP_ENTRIES=5 +readonly MAX_ZIP_ENTRIES=50 ZIP_LIST_CMD=() IN_ALT_SCREEN=0 @@ -55,7 +55,7 @@ fi TERMINAL_WIDTH=0 -# Check for installer payloads inside ZIP - single pass, fused size and pattern check +# Check for installer payloads inside ZIP - check first N entries for installer patterns is_installer_zip() { local zip="$1" local cap="$MAX_ZIP_ENTRIES" @@ -63,13 +63,10 @@ is_installer_zip() { [[ ${#ZIP_LIST_CMD[@]} -gt 0 ]] || return 1 if ! "${ZIP_LIST_CMD[@]}" "$zip" 2> /dev/null | - head -n $((cap + 1)) | - awk -v cap="$cap" ' - /\.(app|pkg|dmg|xip)(\/|$)/ { found=1 } - END { - if (NR > cap) exit 1 - exit found ? 0 : 1 - } + head -n "$cap" | + awk ' + /\.(app|pkg|dmg|xip)(\/|$)/ { found=1; exit 0 } + END { exit found ? 0 : 1 } '; then return 1 fi diff --git a/tests/installer_zip.bats b/tests/installer_zip.bats index ae2bc98..743df15 100644 --- a/tests/installer_zip.bats +++ b/tests/installer_zip.bats @@ -71,12 +71,12 @@ require_unzip_support() { # Test ZIP installer detection -@test "is_installer_zip: rejects ZIP with installer content but too many entries" { +@test "is_installer_zip: detects ZIP with installer content even with many entries" { if ! require_zip_support; then return 0 fi - # Create a ZIP with too many files (exceeds MAX_ZIP_ENTRIES=5) + # Create a ZIP with many files (more than old MAX_ZIP_ENTRIES=5) # Include a .app file to have installer content mkdir -p "$HOME/Downloads/large-app" touch "$HOME/Downloads/large-app/MyApp.app" @@ -96,7 +96,7 @@ require_unzip_support() { ' bash "$PROJECT_ROOT/bin/installer.sh" [ "$status" -eq 0 ] - [[ "$output" == "NOT_INSTALLER" ]] + [[ "$output" == "INSTALLER" ]] } @test "is_installer_zip: detects ZIP with app content" { @@ -122,6 +122,71 @@ require_unzip_support() { [[ "$output" == "INSTALLER" ]] } +@test "is_installer_zip: rejects ZIP when installer pattern appears after MAX_ZIP_ENTRIES" { + if ! require_zip_support; then + return 0 + fi + + # Create a ZIP where .app appears after the 50th entry + mkdir -p "$HOME/Downloads/deep-content" + # Create 51 regular files first + for i in {1..51}; do + touch "$HOME/Downloads/deep-content/file$i.txt" + done + # Add .app file at the end (52nd entry) + touch "$HOME/Downloads/deep-content/MyApp.app" + (cd "$HOME/Downloads" && zip -q -r deep.zip deep-content) + + run bash -euo pipefail -c ' + export MOLE_TEST_MODE=1 + source "$1" + if is_installer_zip "'"$HOME/Downloads/deep.zip"'"; then + echo "INSTALLER" + else + echo "NOT_INSTALLER" + fi + ' bash "$PROJECT_ROOT/bin/installer.sh" + + [ "$status" -eq 0 ] + [[ "$output" == "NOT_INSTALLER" ]] +} + +@test "is_installer_zip: detects ZIP with real app bundle structure" { + if ! require_zip_support; then + return 0 + fi + + # Create a realistic .app bundle structure (directory, not just a file) + mkdir -p "$HOME/Downloads/RealApp.app/Contents/MacOS" + mkdir -p "$HOME/Downloads/RealApp.app/Contents/Resources" + echo "#!/bin/bash" > "$HOME/Downloads/RealApp.app/Contents/MacOS/RealApp" + chmod +x "$HOME/Downloads/RealApp.app/Contents/MacOS/RealApp" + cat > "$HOME/Downloads/RealApp.app/Contents/Info.plist" << 'EOF' + + + + + CFBundleExecutable + RealApp + + +EOF + (cd "$HOME/Downloads" && zip -q -r realapp.zip RealApp.app) + + run bash -euo pipefail -c ' + export MOLE_TEST_MODE=1 + source "$1" + if is_installer_zip "'"$HOME/Downloads/realapp.zip"'"; then + echo "INSTALLER" + else + echo "NOT_INSTALLER" + fi + ' bash "$PROJECT_ROOT/bin/installer.sh" + + [ "$status" -eq 0 ] + [[ "$output" == "INSTALLER" ]] +} + @test "is_installer_zip: rejects ZIP with only regular files" { if ! require_zip_support; then return 0