From f1a973c42d3d72ac80c867ab46bc18926aac0274 Mon Sep 17 00:00:00 2001 From: Andrew Yim Date: Wed, 11 Mar 2026 15:59:24 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20add=20mac-optimizer=20skill=20(92)=20?= =?UTF-8?q?=E2=80=94=20Claude=20Code=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS system health toolkit with 5 audit modules (packages, environment, security, cleanup, resources) and cleanup executor. Fixed bugs from review: - Replace GNU timeout with perl alarm (macOS compatible) - Remove Linux-only ps --sort flag, use portable sort - Add JSON escaping to all audit scripts - Remove redundant classify_size branch Co-Authored-By: Claude Opus 4.6 --- AGENTS.md | 10 ++ CLAUDE.md | 12 +- README.md | 11 +- custom-skills/92-mac-optimizer/CLAUDE.md | 98 ++++++++++ .../references/cleanup-targets.md | 74 ++++++++ .../92-mac-optimizer/references/packages.md | 52 ++++++ .../references/security-checks.md | 53 ++++++ .../92-mac-optimizer/scripts/audit_cleanup.sh | 160 +++++++++++++++++ .../scripts/audit_environment.sh | 127 +++++++++++++ .../scripts/audit_packages.sh | 118 ++++++++++++ .../scripts/audit_resources.sh | 143 +++++++++++++++ .../scripts/audit_security.sh | 122 +++++++++++++ .../scripts/cleanup_execute.sh | 169 ++++++++++++++++++ 13 files changed, 1145 insertions(+), 4 deletions(-) create mode 100644 custom-skills/92-mac-optimizer/CLAUDE.md create mode 100644 custom-skills/92-mac-optimizer/references/cleanup-targets.md create mode 100644 custom-skills/92-mac-optimizer/references/packages.md create mode 100644 custom-skills/92-mac-optimizer/references/security-checks.md create mode 100755 custom-skills/92-mac-optimizer/scripts/audit_cleanup.sh create mode 100755 custom-skills/92-mac-optimizer/scripts/audit_environment.sh create mode 100755 custom-skills/92-mac-optimizer/scripts/audit_packages.sh create mode 100755 custom-skills/92-mac-optimizer/scripts/audit_resources.sh create mode 100755 custom-skills/92-mac-optimizer/scripts/audit_security.sh create mode 100755 custom-skills/92-mac-optimizer/scripts/cleanup_execute.sh diff --git a/AGENTS.md b/AGENTS.md index 730d14f..a74a6e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -172,6 +172,16 @@ Task( **Parallel workflows:** Set `NOTEBOOKLM_HOME` per agent to avoid context conflicts. +### Mac Optimizer (92) + +- **Claude Code only** — no Desktop version +- Runs read-only audit scripts that output JSON lines, then presents findings as severity-ranked tables +- 5 audit modules: packages, environment, security, cleanup, resources +- `doctor` mode runs all 5 sequentially and presents unified report +- **Always ask user consent** before executing any cleanup or system changes +- Scripts are in `custom-skills/92-mac-optimizer/scripts/` +- Reference docs in `custom-skills/92-mac-optimizer/references/` + ### Reference Curator Skills (90-99) - Use **reference-curator-pipeline** for full automated curation workflows diff --git a/CLAUDE.md b/CLAUDE.md index ab6ba4e..99a6cbd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co **GitHub**: https://github.com/ourdigital/our-claude-skills This is a Claude Skills collection repository containing: -- **custom-skills/**: 61 custom skills for OurDigital workflows, SEO, GTM, Jamie Brand, NotebookLM, Notion, D.intelligence Agent Corps, Reference Curation, and Multi-Agent Collaboration +- **custom-skills/**: 62 custom skills for OurDigital workflows, SEO, GTM, Jamie Brand, NotebookLM, Notion, D.intelligence Agent Corps, Reference Curation, Mac Optimizer, and Multi-Agent Collaboration - **example-skills/**: Reference examples from Anthropic's official skills repository - **official-skills/**: Notion integration skills (3rd party) - **reference/**: Skill format requirements documentation @@ -121,7 +121,13 @@ This is a Claude Skills collection repository containing: **User Guide:** `custom-skills/dintel-shared/USER-GUIDE.md` -### Reference Curator & Multi-Agent (90-99) +### System Tools (92) + +| # | Skill | Purpose | Trigger | +|---|-------|---------|---------| +| 92 | mac-optimizer | macOS system health audit & optimization (Claude Code only) | "audit my mac", "system health", "clean up caches", "check security", "update packages" | + +### Reference Curator & Multi-Agent (90-91) | # | Skill | Purpose | Trigger | |---|-------|---------|---------| @@ -290,6 +296,8 @@ our-claude-skills/ │ │ │ ├── 91-multi-agent-guide/ │ │ +│ ├── 92-mac-optimizer/ # macOS system health toolkit (Claude Code only) +│ │ │ └── 99_archive/ │ ├── example-skills/skills-main/ diff --git a/README.md b/README.md index 4f83968..4657f35 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > **Internal R&D Repository** - This repository is restricted for internal use only. -A collection of **61 custom Claude Skills** for OurDigital workflows, D.intelligence Agent Corps (9-agent business operations suite), Jamie Plastic Surgery Clinic brand management, SEO/GTM tools, NotebookLM automation, Notion integrations, reference documentation curation, and multi-agent collaboration. +A collection of **62 custom Claude Skills** for OurDigital workflows, D.intelligence Agent Corps (9-agent business operations suite), Jamie Plastic Surgery Clinic brand management, SEO/GTM tools, NotebookLM automation, Notion integrations, reference documentation curation, macOS system optimization, and multi-agent collaboration. ## Quick Install @@ -121,7 +121,13 @@ cd our-claude-skills/custom-skills/_ourdigital-shared **Prerequisites:** `pip install notebooklm-py && playwright install chromium && notebooklm login` -### Reference Curator & Multi-Agent (90-99) +### System Tools (92) + +| # | Skill | Purpose | +|---|-------|---------| +| 92 | `mac-optimizer` | macOS system health audit & optimization (Claude Code only) | + +### Reference Curator & Multi-Agent (90-91) | # | Skill | Purpose | |---|-------|---------| @@ -177,6 +183,7 @@ our-claude-skills/ │ ├── 50-53 (NotebookLM tools) │ ├── 90-reference-curator/ │ ├── 91-multi-agent-guide/ +│ ├── 92-mac-optimizer/ │ └── 99_archive/ │ ├── example-skills/ # Anthropic reference examples diff --git a/custom-skills/92-mac-optimizer/CLAUDE.md b/custom-skills/92-mac-optimizer/CLAUDE.md new file mode 100644 index 0000000..9195c3f --- /dev/null +++ b/custom-skills/92-mac-optimizer/CLAUDE.md @@ -0,0 +1,98 @@ +# Mac Optimizer + +Modular macOS system health toolkit. Runs read-only audits first, then recommends actions with user consent. + +## Module Routing + +| Keywords | Module | +|---|---| +| brew, homebrew, npm, nvm, pip, pyenv, packages, update, outdated | packages | +| path, shell, zshrc, environment, env, config, symlink | environment | +| security, firewall, sip, gatekeeper, filevault, ports, ssh | security | +| cache, cleanup, clean, logs, clutter, disk space, free space, trash | cleanup | +| cpu, memory, ram, disk, battery, processes, resources, slow | resources | +| doctor, audit, health, full check, everything, system check | doctor (all) | + +Default to **doctor** when the request is ambiguous. + +## Execution Model + +Every module follows this flow: + +1. **Audit** — run the module's script (read-only) +2. **Report** — parse JSON output, present findings as a severity-ranked table +3. **Recommend** — list available actions grouped by risk +4. **Consent** — ask user which actions to approve (per-category, per-item for risky) +5. **Act** — execute only approved actions + +## Running Audit Scripts + +All scripts are in `scripts/` and output JSON lines to stdout: + +```bash +bash scripts/audit_packages.sh +bash scripts/audit_environment.sh +bash scripts/audit_security.sh +bash scripts/audit_cleanup.sh +bash scripts/audit_resources.sh +``` + +Each JSON line has: `{"module":"...","severity":"...","finding":"...","action":"...","details":"..."}` + +Severity levels: `critical` > `warning` > `info` + +If a script exits non-zero, report the error and continue with other modules. + +## Running Cleanup + +```bash +bash scripts/cleanup_execute.sh --help # See all targets +bash scripts/cleanup_execute.sh --dry-run # Preview (default) +bash scripts/cleanup_execute.sh --execute # Actually clean +``` + +**Always run --dry-run first and show the user what will happen before --execute.** + +## Doctor Mode Workflow + +1. Run all 5 audit scripts sequentially, collect all JSON findings +2. Parse and group findings by severity (critical first, then warning, then info) +3. Present unified report table to the user +4. **STOP and ask**: "Which actions would you like me to perform? You can approve by category (e.g., 'update packages and clean caches') or review each item." +5. Execute only approved actions +6. Present final summary of actions taken + +## Safety Rules + +- **Never execute cleanup without explicit user approval** +- **Always show sizes before deleting anything** +- **Security module is read-only** — present findings and remediation guidance only +- **Back up shell configs** before modifying: `cp ~/.zshrc ~/.config/mac-optimizer-backups/.zshrc.$(date +%s)` +- **Process deny-list**: never suggest killing `kernel_task`, `launchd`, `WindowServer`, `loginwindow`, `mds`, `mds_stores`, `opendirectoryd`, `coreaudiod`, `SystemUIServer`, `Finder`, `Dock` +- **No sudo by default** — if an action needs sudo, state why and ask first +- Docker cleanup uses `docker system prune`, never direct file deletion + +## Report Format + +Present findings as a markdown table: + +``` +### Critical +| Module | Finding | Recommended Action | +|---|---|---| +| security | Firewall disabled | Enable in System Settings | + +### Warning +| Module | Finding | Recommended Action | +|---|---|---| + +### Info +| Module | Finding | Recommended Action | +|---|---|---| +``` + +## Reference Files + +- **references/packages.md** — package manager update strategies and commands +- **references/security-checks.md** — security benchmarks and remediation guidance +- **references/cleanup-targets.md** — full list of cleanup paths with risk ratings diff --git a/custom-skills/92-mac-optimizer/references/cleanup-targets.md b/custom-skills/92-mac-optimizer/references/cleanup-targets.md new file mode 100644 index 0000000..a9836b3 --- /dev/null +++ b/custom-skills/92-mac-optimizer/references/cleanup-targets.md @@ -0,0 +1,74 @@ +# Cleanup Targets Reference + +## Risk Ratings + +| Risk | Meaning | +|---|---| +| Safe | Can be deleted without consequence; regenerated automatically | +| Moderate | Review before deleting; may lose useful data | +| Risky | May break applications; requires careful inspection | +| Info only | Report size but do not offer deletion | + +## Target Details + +### User Logs (Safe) +- **Path**: `~/Library/Logs/` +- **Cleanup target**: `user-logs` +- **Notes**: Application crash reports and debug logs. Regenerated as needed. + +### System Logs (Moderate, requires sudo) +- **Path**: `/var/log/` +- **Cleanup target**: not available via cleanup_execute.sh (requires sudo) +- **Notes**: System-level logs. Only clean old/rotated files. Active logs should not be deleted. + +### User Caches (Safe, per-app) +- **Path**: `~/Library/Caches/` +- **Cleanup target**: `user-caches` +- **Notes**: App caches regenerated on next use. Some apps may need to re-download data. Browser caches can be large. + +### Homebrew Cache (Safe) +- **Path**: `$(brew --cache)` (typically `~/Library/Caches/Homebrew/`) +- **Cleanup target**: `brew-cache` +- **Notes**: Downloaded bottles and source archives. `brew cleanup` handles this properly. + +### npm Cache (Safe) +- **Path**: `~/.npm/_cacache/` +- **Cleanup target**: `npm-cache` +- **Notes**: Package download cache. `npm cache clean --force` is the proper way to clear. + +### pip Cache (Safe) +- **Path**: `~/Library/Caches/pip/` +- **Cleanup target**: `pip-cache` +- **Notes**: Downloaded wheel/sdist cache. `pip cache purge` is the proper way to clear. + +### Xcode DerivedData (Safe) +- **Path**: `~/Library/Developer/Xcode/DerivedData/` +- **Cleanup target**: `xcode-derived` +- **Notes**: Build artifacts and indexes. Rebuilt on next Xcode build. Can grow very large. + +### Xcode Archives (Moderate) +- **Path**: `~/Library/Developer/Xcode/Archives/` +- **Cleanup target**: `xcode-archives` +- **Notes**: App Store submission archives. May be needed for symbolication of crash reports. Review before deleting. + +### iOS DeviceSupport (Safe) +- **Path**: `~/Library/Developer/Xcode/iOS DeviceSupport/` +- **Cleanup target**: `ios-support` +- **Notes**: Debug symbols for connected iOS devices. Re-downloaded when device connects again. + +### Docker (Moderate) +- **Cleanup target**: `docker` +- **Notes**: Uses `docker system prune -f`. Removes stopped containers, unused networks, dangling images. Does NOT remove named volumes. Never delete Docker's filesystem directly. + +### Trash (Safe) +- **Path**: `~/.Trash/` +- **Cleanup target**: `trash` +- **Notes**: Items in Trash. User has already "deleted" these. + +### Old Downloads (Info only) +- **Path**: `~/Downloads/` (files >90 days old) +- **Notes**: Report only. User should review manually. Never auto-delete downloads. + +### Application Support Remnants (Risky) +- **Path**: `~/Library/Application Support/` orphaned directories +- **Notes**: Report only. Directories from uninstalled apps. Difficult to determine automatically which are truly orphaned. Manual review required. diff --git a/custom-skills/92-mac-optimizer/references/packages.md b/custom-skills/92-mac-optimizer/references/packages.md new file mode 100644 index 0000000..6394b33 --- /dev/null +++ b/custom-skills/92-mac-optimizer/references/packages.md @@ -0,0 +1,52 @@ +# Package Manager Reference + +## Homebrew + +### Update workflow +```bash +brew update # Update Homebrew itself and formulae list +brew outdated # List outdated formulae +brew outdated --cask # List outdated casks +brew upgrade # Upgrade all outdated formulae +brew upgrade # Upgrade specific formula +brew upgrade --cask # Upgrade all casks +brew cleanup --prune=0 # Remove all cached downloads +brew autoremove # Remove orphaned dependencies +brew doctor # Diagnose issues +``` + +### Safety notes +- `brew upgrade` is generally safe but can break projects pinned to specific versions +- Always check `brew doctor` warnings — some are cosmetic, some indicate real issues +- `brew autoremove` only removes packages not depended on by anything + +## npm / nvm + +### Update workflow +```bash +nvm ls # List installed Node versions +nvm install --lts # Install latest LTS +npm outdated -g # Check global packages +npm update -g # Update all global packages +npm cache clean --force # Clear npm cache +``` + +### Safety notes +- Global npm packages are version-independent from project dependencies +- Updating global packages won't affect project-level node_modules + +## Python / pyenv + +### Update workflow +```bash +pyenv versions # List installed versions +pyenv install --list # Available versions +pip list --outdated # Outdated packages in current env +pip install --upgrade # Upgrade specific package +pip cache purge # Clear pip cache +``` + +### Safety notes +- Only audit pyenv-managed environments (not system Python) +- pip upgrades can break dependency chains — suggest `--dry-run` first +- Virtual environments in project directories are not scanned (out of scope v1) diff --git a/custom-skills/92-mac-optimizer/references/security-checks.md b/custom-skills/92-mac-optimizer/references/security-checks.md new file mode 100644 index 0000000..adea0a2 --- /dev/null +++ b/custom-skills/92-mac-optimizer/references/security-checks.md @@ -0,0 +1,53 @@ +# Security Checks Reference + +## SIP (System Integrity Protection) +- **Check**: `csrutil status` +- **Expected**: "System Integrity Protection status: enabled." +- **Remediation**: Reboot to Recovery Mode (Cmd+R), open Terminal, run `csrutil enable` +- **Severity**: Critical if disabled + +## Gatekeeper +- **Check**: `spctl --status` +- **Expected**: "assessments enabled" +- **Remediation**: `sudo spctl --master-enable` +- **Severity**: Critical if disabled + +## Firewall +- **Check**: `/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate` +- **Fallback**: `defaults read /Library/Preferences/com.apple.alf globalstate` (0=off, 1=on, 2=essential) +- **Remediation**: System Settings > Network > Firewall > Turn On +- **Severity**: Critical if disabled + +## FileVault +- **Check**: `fdesetup status` +- **Expected**: "FileVault is On." +- **Remediation**: System Settings > Privacy & Security > FileVault > Turn On +- **Severity**: Critical if disabled (disk not encrypted) + +## Open Ports +- **Check**: `lsof -iTCP -sTCP:LISTEN -nP` +- **Review**: unexpected services listening on all interfaces (0.0.0.0 or *) +- **Severity**: Info (review needed, not automatically bad) + +## SSH Configuration +- **Check**: `/etc/ssh/sshd_config` +- **Flags**: `PasswordAuthentication yes`, `PermitRootLogin yes` +- **Remediation**: Set to `no` and restart sshd +- **Severity**: Warning + +## Remote Services +- **Remote Login**: `systemsetup -getremotelogin` +- **Remote Management**: `defaults read /Library/Preferences/com.apple.RemoteManagement ARD_AllLocalUsers` +- **Remediation**: Disable in System Settings > General > Sharing +- **Severity**: Info/Warning depending on context + +## Software Updates +- **Check**: `softwareupdate -l` (can be slow, 30s timeout) +- **Remediation**: System Settings > General > Software Update +- **Severity**: Warning if updates available + +## Screen Lock +- **Check**: `defaults -currentHost read com.apple.screensaver idleTime` +- **Check**: `defaults read com.apple.screensaver askForPassword` +- **Remediation**: System Settings > Lock Screen +- **Severity**: Warning if password not required diff --git a/custom-skills/92-mac-optimizer/scripts/audit_cleanup.sh b/custom-skills/92-mac-optimizer/scripts/audit_cleanup.sh new file mode 100755 index 0000000..c8f9a5e --- /dev/null +++ b/custom-skills/92-mac-optimizer/scripts/audit_cleanup.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash +# audit_cleanup.sh — Scan caches, logs, and clutter on macOS, report sizes +# Output: JSON lines to stdout, one per finding +set -euo pipefail + +json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' '; } + +json_finding() { + local severity="$1" finding action details + finding=$(json_escape "$2") + action=$(json_escape "$3") + details=$(json_escape "$4") + printf '{"module":"cleanup","severity":"%s","finding":"%s","action":"%s","details":"%s"}\n' \ + "$severity" "$finding" "$action" "$details" +} + +# Get directory size in human-readable and bytes +dir_size() { + local path="$1" + if [ -d "$path" ]; then + local bytes=$(du -sk "$path" 2>/dev/null | awk '{print $1}') + local human=$(du -sh "$path" 2>/dev/null | awk '{print $1}') + echo "${bytes:-0}|${human:-0B}" + else + echo "0|0B" + fi +} + +classify_size() { + local kb="$1" + if [ "$kb" -gt 1048576 ] 2>/dev/null; then # > 1GB + echo "warning" + else + echo "info" + fi +} + +total_reclaimable_kb=0 + +# --- User logs --- +result=$(dir_size "$HOME/Library/Logs") +IFS='|' read -r kb human <<< "$result" +total_reclaimable_kb=$((total_reclaimable_kb + kb)) +severity=$(classify_size "$kb") +json_finding "$severity" "User logs: ${human}" "clean" "~/Library/Logs/ [safe, no sudo]" + +# --- System logs --- +result=$(dir_size "/var/log" 2>/dev/null || echo "0|0B") +IFS='|' read -r kb human <<< "$result" +if [ "$kb" -gt 0 ] 2>/dev/null; then + severity=$(classify_size "$kb") + json_finding "$severity" "System logs: ${human}" "clean" "/var/log/ [moderate, requires sudo]" +fi + +# --- User caches --- +result=$(dir_size "$HOME/Library/Caches") +IFS='|' read -r kb human <<< "$result" +total_reclaimable_kb=$((total_reclaimable_kb + kb)) +severity=$(classify_size "$kb") +json_finding "$severity" "User caches: ${human}" "clean" "~/Library/Caches/ [safe, per-app]" + +# --- Homebrew cache --- +if command -v brew &>/dev/null; then + brew_cache=$(brew --cache 2>/dev/null || echo "") + if [ -n "$brew_cache" ] && [ -d "$brew_cache" ]; then + result=$(dir_size "$brew_cache") + IFS='|' read -r kb human <<< "$result" + total_reclaimable_kb=$((total_reclaimable_kb + kb)) + severity=$(classify_size "$kb") + json_finding "$severity" "Homebrew cache: ${human}" "brew cleanup" "$brew_cache [safe]" + fi +fi + +# --- npm cache --- +npm_cache="$HOME/.npm/_cacache" +if [ -d "$npm_cache" ]; then + result=$(dir_size "$npm_cache") + IFS='|' read -r kb human <<< "$result" + total_reclaimable_kb=$((total_reclaimable_kb + kb)) + severity=$(classify_size "$kb") + json_finding "$severity" "npm cache: ${human}" "npm cache clean --force" "$npm_cache [safe]" +fi + +# --- pip cache --- +pip_cache="$HOME/Library/Caches/pip" +if [ -d "$pip_cache" ]; then + result=$(dir_size "$pip_cache") + IFS='|' read -r kb human <<< "$result" + total_reclaimable_kb=$((total_reclaimable_kb + kb)) + severity=$(classify_size "$kb") + json_finding "$severity" "pip cache: ${human}" "pip cache purge" "$pip_cache [safe]" +fi + +# --- Xcode derived data --- +xcode_derived="$HOME/Library/Developer/Xcode/DerivedData" +if [ -d "$xcode_derived" ]; then + result=$(dir_size "$xcode_derived") + IFS='|' read -r kb human <<< "$result" + total_reclaimable_kb=$((total_reclaimable_kb + kb)) + severity=$(classify_size "$kb") + json_finding "$severity" "Xcode DerivedData: ${human}" "clean" "$xcode_derived [safe]" +fi + +# --- Xcode archives --- +xcode_archives="$HOME/Library/Developer/Xcode/Archives" +if [ -d "$xcode_archives" ]; then + result=$(dir_size "$xcode_archives") + IFS='|' read -r kb human <<< "$result" + total_reclaimable_kb=$((total_reclaimable_kb + kb)) + severity=$(classify_size "$kb") + json_finding "$severity" "Xcode Archives: ${human}" "clean" "$xcode_archives [moderate - check before deleting]" +fi + +# --- iOS Device Support --- +ios_support="$HOME/Library/Developer/Xcode/iOS DeviceSupport" +if [ -d "$ios_support" ]; then + result=$(dir_size "$ios_support") + IFS='|' read -r kb human <<< "$result" + total_reclaimable_kb=$((total_reclaimable_kb + kb)) + severity=$(classify_size "$kb") + json_finding "$severity" "iOS DeviceSupport: ${human}" "clean" "$ios_support [safe]" +fi + +# --- Docker --- +if command -v docker &>/dev/null; then + docker_df=$(docker system df 2>/dev/null || echo "") + if [ -n "$docker_df" ]; then + reclaimable=$(echo "$docker_df" | grep -i "reclaimable" | head -1 || echo "") + json_finding "info" "Docker space usage detected" "docker system prune" "Run docker system df for details [use docker system prune to clean]" + fi +else + # Skip silently if docker not installed + true +fi + +# --- Trash --- +trash_dir="$HOME/.Trash" +if [ -d "$trash_dir" ]; then + result=$(dir_size "$trash_dir") + IFS='|' read -r kb human <<< "$result" + total_reclaimable_kb=$((total_reclaimable_kb + kb)) + if [ "$kb" -gt 0 ] 2>/dev/null; then + severity=$(classify_size "$kb") + json_finding "$severity" "Trash: ${human}" "empty trash" "$trash_dir [safe]" + fi +fi + +# --- Old downloads (report only) --- +if [ -d "$HOME/Downloads" ]; then + old_files=$(find "$HOME/Downloads" -maxdepth 1 -mtime +90 -type f 2>/dev/null | wc -l | tr -d ' ') + if [ "$old_files" -gt 0 ] 2>/dev/null; then + old_size=$(find "$HOME/Downloads" -maxdepth 1 -mtime +90 -type f -exec du -ck {} + 2>/dev/null | tail -1 | awk '{print $1}') + old_human=$(echo "$old_size" | awk '{printf "%.1fM", $1/1024}') + json_finding "info" "${old_files} files in Downloads older than 90 days (${old_human})" "review" "Info only - review manually" + fi +fi + +# --- Total summary --- +total_human=$(echo "$total_reclaimable_kb" | awk '{if($1>1048576) printf "%.1fG",$1/1048576; else if($1>1024) printf "%.1fM",$1/1024; else printf "%dK",$1}') +json_finding "info" "Total scannable space: ${total_human}" "review" "Approx reclaimable across all categories" diff --git a/custom-skills/92-mac-optimizer/scripts/audit_environment.sh b/custom-skills/92-mac-optimizer/scripts/audit_environment.sh new file mode 100755 index 0000000..eeb48e6 --- /dev/null +++ b/custom-skills/92-mac-optimizer/scripts/audit_environment.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +# audit_environment.sh — Audit shell environment and paths on macOS +# Output: JSON lines to stdout, one per finding +set -euo pipefail + +json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' '; } + +json_finding() { + local severity="$1" finding action details + finding=$(json_escape "$2") + action=$(json_escape "$3") + details=$(json_escape "$4") + printf '{"module":"environment","severity":"%s","finding":"%s","action":"%s","details":"%s"}\n' \ + "$severity" "$finding" "$action" "$details" +} + +# --- PATH analysis --- +IFS=':' read -ra path_dirs <<< "$PATH" +total_dirs=${#path_dirs[@]} + +# Duplicate detection (compatible with bash 3) +duplicates=() +for i in "${!path_dirs[@]}"; do + dir="${path_dirs[$i]}" + is_dup=false + for j in "${!path_dirs[@]}"; do + if [ "$j" -lt "$i" ] && [ "${path_dirs[$j]}" = "$dir" ]; then + is_dup=true + break + fi + done + if $is_dup; then + duplicates+=("$dir") + fi +done + +if [ ${#duplicates[@]} -gt 0 ]; then + dup_list=$(printf '%s, ' "${duplicates[@]}" | sed 's/, $//') + json_finding "warning" "${#duplicates[@]} duplicate PATH entries" "deduplicate" "$dup_list" +else + json_finding "info" "No duplicate PATH entries" "none" "${total_dirs} directories in PATH" +fi + +# Non-existent directories +missing=() +for dir in "${path_dirs[@]}"; do + if [ ! -d "$dir" ]; then + missing+=("$dir") + fi +done + +if [ ${#missing[@]} -gt 0 ]; then + missing_list=$(printf '%s, ' "${missing[@]}" | sed 's/, $//') + json_finding "warning" "${#missing[@]} non-existent directories in PATH" "remove" "$missing_list" +fi + +# --- Broken symlinks --- +broken_symlinks=() +for check_dir in /usr/local/bin "$HOME/.local/bin"; do + if [ -d "$check_dir" ]; then + while IFS= read -r link; do + broken_symlinks+=("$link") + done < <(find "$check_dir" -maxdepth 1 -type l ! -exec test -e {} \; -print 2>/dev/null || true) + fi +done + +if [ ${#broken_symlinks[@]} -gt 0 ]; then + broken_list=$(printf '%s, ' "${broken_symlinks[@]}" | sed 's/, $//') + json_finding "warning" "${#broken_symlinks[@]} broken symlinks found" "remove" "$broken_list" +else + json_finding "info" "No broken symlinks in bin directories" "none" "" +fi + +# --- Shell config analysis --- +shell_configs=() +for cfg in "$HOME/.zshrc" "$HOME/.zprofile" "$HOME/.zshenv" "$HOME/.bash_profile" "$HOME/.bashrc"; do + if [ -f "$cfg" ]; then + shell_configs+=("$(basename "$cfg")") + fi +done +json_finding "info" "Shell configs found: ${shell_configs[*]}" "none" "" + +# Check for PATH modifications across multiple files +path_mods=0 +for cfg in "$HOME/.zshrc" "$HOME/.zprofile" "$HOME/.zshenv" "$HOME/.bash_profile" "$HOME/.bashrc"; do + if [ -f "$cfg" ]; then + count=$(grep -c -E 'export PATH|PATH=' "$cfg" 2>/dev/null || echo 0) + count=$(echo "$count" | tr -d '[:space:]') + path_mods=$((path_mods + count)) + fi +done + +if [ "$path_mods" -gt 5 ]; then + json_finding "warning" "PATH modified ${path_mods} times across shell configs" "consolidate" "Consider consolidating PATH modifications" +fi + +# --- Environment variable checks --- +if [ -z "${ANTHROPIC_API_KEY:-}" ]; then + json_finding "info" "ANTHROPIC_API_KEY not set" "none" "Set if using Claude API" +fi + +if [ -n "${JAVA_HOME:-}" ] && [ ! -d "$JAVA_HOME" ]; then + json_finding "warning" "JAVA_HOME points to non-existent directory" "fix" "$JAVA_HOME" +fi + +# --- Shell startup time --- +if command -v zsh &>/dev/null; then + startup_time=$( { time zsh -i -c exit; } 2>&1 | grep real | awk '{print $2}' || echo "unknown") + # Parse the time - handle both formats like "0m0.123s" and "0.123" + if echo "$startup_time" | grep -q 'm'; then + seconds=$(echo "$startup_time" | sed 's/.*m//' | sed 's/s//') + else + seconds="$startup_time" + fi + + if command -v python3 &>/dev/null; then + is_slow=$(python3 -c "print('slow' if float('${seconds}') > 0.5 else 'ok')" 2>/dev/null || echo "ok") + else + is_slow="ok" + fi + + if [ "$is_slow" = "slow" ]; then + json_finding "warning" "Shell startup is slow (${startup_time})" "optimize" "Consider profiling with zsh -xv" + else + json_finding "info" "Shell startup time: ${startup_time}" "none" "" + fi +fi diff --git a/custom-skills/92-mac-optimizer/scripts/audit_packages.sh b/custom-skills/92-mac-optimizer/scripts/audit_packages.sh new file mode 100755 index 0000000..1f7ebee --- /dev/null +++ b/custom-skills/92-mac-optimizer/scripts/audit_packages.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# audit_packages.sh — Audit package managers on macOS +# Output: JSON lines to stdout, one per finding +set -euo pipefail + +json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' '; } + +json_finding() { + local severity="$1" finding action details + finding=$(json_escape "$2") + action=$(json_escape "$3") + details=$(json_escape "$4") + printf '{"module":"packages","severity":"%s","finding":"%s","action":"%s","details":"%s"}\n' \ + "$severity" "$finding" "$action" "$details" +} + +# --- Homebrew --- +if command -v brew &>/dev/null; then + echo '{"module":"packages","severity":"info","finding":"Homebrew detected","action":"none","details":"'$(brew --version | head -1)'"}' + + # brew update + brew update --quiet 2>/dev/null || true + + # outdated formulae + outdated=$(brew outdated --json=v2 2>/dev/null || echo '{}') + formula_count=$(echo "$outdated" | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d.get('formulae',[])))" 2>/dev/null || echo 0) + cask_count=$(echo "$outdated" | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d.get('casks',[])))" 2>/dev/null || echo 0) + + if [ "$formula_count" -gt 0 ] 2>/dev/null; then + formula_list=$(echo "$outdated" | python3 -c " +import sys,json +d=json.load(sys.stdin) +names=[f['name'] for f in d.get('formulae',[])] +print(', '.join(names[:10])) +if len(names)>10: print(f'... and {len(names)-10} more') +" 2>/dev/null || echo "") + json_finding "warning" "${formula_count} Homebrew formulae outdated" "brew upgrade" "$formula_list" + fi + + if [ "$cask_count" -gt 0 ] 2>/dev/null; then + json_finding "warning" "${cask_count} Homebrew casks outdated" "brew upgrade --cask" "" + fi + + if [ "$formula_count" -eq 0 ] && [ "$cask_count" -eq 0 ] 2>/dev/null; then + json_finding "info" "All Homebrew packages up to date" "none" "" + fi + + # brew doctor + doctor_output=$(brew doctor 2>&1 || true) + doctor_warnings=$(echo "$doctor_output" | grep -c "Warning:" 2>/dev/null || echo 0) + if [ "$doctor_warnings" -gt 0 ] 2>/dev/null; then + json_finding "warning" "brew doctor found ${doctor_warnings} warning(s)" "brew doctor" "Run brew doctor for details" + fi + + # autoremove candidates + autoremove_output=$(brew autoremove --dry-run 2>/dev/null || echo "") + orphan_count=$(echo "$autoremove_output" | grep -c "^=" 2>/dev/null || echo 0) + if echo "$autoremove_output" | grep -q "would remove" 2>/dev/null; then + json_finding "info" "Orphaned Homebrew dependencies found" "brew autoremove" "$autoremove_output" + fi +else + json_finding "info" "Homebrew not installed" "none" "Consider installing from https://brew.sh" +fi + +# --- npm / nvm --- +if command -v nvm &>/dev/null || [ -d "$HOME/.nvm" ]; then + # Source nvm if available + export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" + [ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh" 2>/dev/null + + if command -v nvm &>/dev/null; then + node_versions=$(nvm ls --no-colors 2>/dev/null | grep -c "v[0-9]" || echo 0) + current_node=$(nvm current 2>/dev/null || echo "none") + json_finding "info" "nvm detected: ${node_versions} version(s), current: ${current_node}" "none" "" + fi +fi + +if command -v npm &>/dev/null; then + json_finding "info" "npm detected" "none" "$(npm --version 2>/dev/null)" + + npm_outdated=$(npm outdated -g --json 2>/dev/null || echo '{}') + npm_outdated_count=$(echo "$npm_outdated" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0) + + if [ "$npm_outdated_count" -gt 0 ] 2>/dev/null; then + json_finding "warning" "${npm_outdated_count} global npm packages outdated" "npm update -g" "" + fi +else + json_finding "info" "npm not installed" "none" "" +fi + +# --- Python / pyenv --- +if command -v pyenv &>/dev/null; then + pyenv_versions=$(pyenv versions --bare 2>/dev/null | wc -l | tr -d ' ') + current_python=$(pyenv version-name 2>/dev/null || echo "system") + json_finding "info" "pyenv detected: ${pyenv_versions} version(s), current: ${current_python}" "none" "" + + # Check pip outdated for current environment + if command -v pip &>/dev/null || command -v pip3 &>/dev/null; then + pip_cmd=$(command -v pip3 || command -v pip) + pip_outdated=$($pip_cmd list --outdated --format=json 2>/dev/null || echo '[]') + pip_count=$(echo "$pip_outdated" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0) + + if [ "$pip_count" -gt 0 ] 2>/dev/null; then + json_finding "warning" "${pip_count} pip packages outdated in current env" "pip install --upgrade" "" + fi + fi +elif command -v python3 &>/dev/null; then + py_version=$(python3 --version 2>/dev/null || echo "unknown") + json_finding "info" "System Python: ${py_version} (no pyenv)" "none" "" +fi + +# --- Other package managers (presence detection only) --- +for mgr in yarn pnpm cargo go gem composer; do + if command -v "$mgr" &>/dev/null; then + version=$($mgr --version 2>/dev/null | head -1 || echo "unknown") + json_finding "info" "${mgr} detected: ${version}" "none" "Presence check only" + fi +done diff --git a/custom-skills/92-mac-optimizer/scripts/audit_resources.sh b/custom-skills/92-mac-optimizer/scripts/audit_resources.sh new file mode 100755 index 0000000..5758b24 --- /dev/null +++ b/custom-skills/92-mac-optimizer/scripts/audit_resources.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# audit_resources.sh — Monitor CPU, memory, disk, battery on macOS +# Output: JSON lines to stdout, one per finding +set -euo pipefail + +json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' '; } + +json_finding() { + local severity="$1" finding action details + finding=$(json_escape "$2") + action=$(json_escape "$3") + details=$(json_escape "$4") + printf '{"module":"resources","severity":"%s","finding":"%s","action":"%s","details":"%s"}\n' \ + "$severity" "$finding" "$action" "$details" +} + +# Process deny-list (never suggest termination) +DENY_LIST="kernel_task|launchd|WindowServer|loginwindow|mds|mds_stores|opendirectoryd|coreaudiod|SystemUIServer|Finder|Dock" + +# --- CPU --- +load_avg=$(sysctl -n vm.loadavg 2>/dev/null | tr -d '{}' | awk '{print $1}') +cpu_count=$(sysctl -n hw.ncpu 2>/dev/null || echo 1) + +if command -v python3 &>/dev/null; then + cpu_severity=$(python3 -c " +load=$load_avg; cores=$cpu_count +if load > cores * 2: print('critical') +elif load > cores: print('warning') +else: print('info') +" 2>/dev/null || echo "info") +else + cpu_severity="info" +fi + +json_finding "$cpu_severity" "Load average: ${load_avg} (${cpu_count} cores)" "review" "" + +# Top CPU-consuming processes (excluding deny-list from action suggestions) +top_cpu=$(ps aux | sort -nrk 3 | head -5 || echo "") +if [ -n "$top_cpu" ]; then + while IFS= read -r line; do + proc_name=$(echo "$line" | awk '{print $11}' | xargs basename 2>/dev/null || echo "unknown") + cpu_pct=$(echo "$line" | awk '{print $3}') + mem_pct=$(echo "$line" | awk '{print $4}') + pid=$(echo "$line" | awk '{print $2}') + + # Skip low-usage processes + is_high=$(python3 -c "print('yes' if float('${cpu_pct}') > 10 else 'no')" 2>/dev/null || echo "no") + if [ "$is_high" = "yes" ]; then + if echo "$proc_name" | grep -qE "$DENY_LIST"; then + json_finding "info" "High CPU: ${proc_name} (${cpu_pct}% CPU, PID ${pid})" "none" "System process - do not terminate" + else + json_finding "warning" "High CPU: ${proc_name} (${cpu_pct}% CPU, PID ${pid})" "quit" "User process - can be terminated" + fi + fi + done <<< "$top_cpu" +fi + +# --- Memory --- +vm_stat_output=$(vm_stat 2>/dev/null || echo "") +if [ -n "$vm_stat_output" ]; then + page_size=$(vm_stat | head -1 | grep -o '[0-9]*' || echo 16384) + + free_pages=$(echo "$vm_stat_output" | grep "Pages free" | awk '{print $3}' | tr -d '.') + active_pages=$(echo "$vm_stat_output" | grep "Pages active" | awk '{print $3}' | tr -d '.') + inactive_pages=$(echo "$vm_stat_output" | grep "Pages inactive" | awk '{print $3}' | tr -d '.') + wired_pages=$(echo "$vm_stat_output" | grep "Pages wired" | awk '{print $4}' | tr -d '.') + compressed_pages=$(echo "$vm_stat_output" | grep "Pages stored in compressor" | awk '{print $5}' | tr -d '.' || echo 0) + + if command -v python3 &>/dev/null; then + mem_summary=$(python3 -c " +ps=${page_size} +free_mb = int(${free_pages:-0}) * ps / 1048576 +active_mb = int(${active_pages:-0}) * ps / 1048576 +inactive_mb = int(${inactive_pages:-0}) * ps / 1048576 +wired_mb = int(${wired_pages:-0}) * ps / 1048576 +compressed_mb = int(${compressed_pages:-0}) * ps / 1048576 +used_mb = active_mb + wired_mb + compressed_mb +total_mb = free_mb + active_mb + inactive_mb + wired_mb + compressed_mb +pct = used_mb / total_mb * 100 if total_mb > 0 else 0 +print(f'{used_mb:.0f}MB used / {total_mb:.0f}MB total ({pct:.0f}%)') +if pct > 90: print('SEVERITY:critical') +elif pct > 75: print('SEVERITY:warning') +else: print('SEVERITY:info') +" 2>/dev/null || echo "unknown") + mem_severity=$(echo "$mem_summary" | grep "SEVERITY:" | cut -d: -f2) + mem_info=$(echo "$mem_summary" | head -1) + json_finding "${mem_severity:-info}" "Memory: ${mem_info}" "review" "" + fi +fi + +# --- Swap --- +swap_info=$(sysctl vm.swapusage 2>/dev/null || echo "") +if [ -n "$swap_info" ]; then + swap_used=$(echo "$swap_info" | grep -o "used = [0-9.]*M" | awk '{print $3}' | tr -d 'M') + if [ -n "$swap_used" ] && command -v python3 &>/dev/null; then + swap_severity=$(python3 -c " +s=float('${swap_used}') +if s > 4096: print('warning') +elif s > 1024: print('info') +else: print('info') +" 2>/dev/null || echo "info") + json_finding "$swap_severity" "Swap: ${swap_used}MB used" "review" "$swap_info" + fi +fi + +# --- Disk usage --- +disk_info=$(df -h / 2>/dev/null | tail -1) +if [ -n "$disk_info" ]; then + disk_used_pct=$(echo "$disk_info" | awk '{print $5}' | tr -d '%') + disk_avail=$(echo "$disk_info" | awk '{print $4}') + disk_total=$(echo "$disk_info" | awk '{print $2}') + + if [ "$disk_used_pct" -gt 90 ] 2>/dev/null; then + json_finding "critical" "Disk ${disk_used_pct}% full (${disk_avail} free of ${disk_total})" "cleanup" "Consider running cleanup module" + elif [ "$disk_used_pct" -gt 75 ] 2>/dev/null; then + json_finding "warning" "Disk ${disk_used_pct}% full (${disk_avail} free of ${disk_total})" "monitor" "" + else + json_finding "info" "Disk ${disk_used_pct}% full (${disk_avail} free of ${disk_total})" "none" "" + fi +fi + +# --- Large directories (top 5 in home) --- +if command -v du &>/dev/null; then + large_dirs=$(du -sh "$HOME"/*/ "$HOME"/.[!.]* 2>/dev/null | sort -hr | head -5 || echo "") + if [ -n "$large_dirs" ]; then + dir_summary=$(echo "$large_dirs" | awk '{printf "%s (%s), ", $2, $1}' | sed 's/, $//') + json_finding "info" "Largest directories in home" "review" "$dir_summary" + fi +fi + +# --- Battery (laptops only) --- +battery_info=$(system_profiler SPPowerDataType 2>/dev/null || echo "") +if echo "$battery_info" | grep -q "Cycle Count"; then + cycle_count=$(echo "$battery_info" | grep "Cycle Count" | awk '{print $NF}') + condition=$(echo "$battery_info" | grep "Condition" | awk '{print $NF}') + max_capacity=$(echo "$battery_info" | grep "Maximum Capacity" | awk '{print $NF}' || echo "unknown") + + if [ "$condition" = "Normal" ]; then + json_finding "info" "Battery: ${condition}, ${cycle_count} cycles, ${max_capacity} capacity" "none" "" + else + json_finding "warning" "Battery: ${condition}, ${cycle_count} cycles, ${max_capacity} capacity" "review" "Battery may need service" + fi +fi diff --git a/custom-skills/92-mac-optimizer/scripts/audit_security.sh b/custom-skills/92-mac-optimizer/scripts/audit_security.sh new file mode 100755 index 0000000..fb4d588 --- /dev/null +++ b/custom-skills/92-mac-optimizer/scripts/audit_security.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# audit_security.sh — Security posture assessment for macOS (read-only) +# Output: JSON lines to stdout, one per finding +set -euo pipefail + +json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' '; } + +json_finding() { + local severity="$1" finding action details + finding=$(json_escape "$2") + action=$(json_escape "$3") + details=$(json_escape "$4") + printf '{"module":"security","severity":"%s","finding":"%s","action":"%s","details":"%s"}\n' \ + "$severity" "$finding" "$action" "$details" +} + +# --- SIP (System Integrity Protection) --- +sip_status=$(csrutil status 2>/dev/null || echo "unknown") +if echo "$sip_status" | grep -q "enabled"; then + json_finding "info" "SIP enabled" "none" "" +elif echo "$sip_status" | grep -q "disabled"; then + json_finding "critical" "SIP is disabled" "report" "System Integrity Protection should be enabled" +else + json_finding "warning" "SIP status unknown" "report" "$sip_status" +fi + +# --- Gatekeeper --- +gatekeeper_status=$(spctl --status 2>/dev/null || echo "unknown") +if echo "$gatekeeper_status" | grep -q "assessments enabled"; then + json_finding "info" "Gatekeeper enabled" "none" "" +elif echo "$gatekeeper_status" | grep -q "assessments disabled"; then + json_finding "critical" "Gatekeeper is disabled" "report" "Enable via: sudo spctl --master-enable" +else + json_finding "warning" "Gatekeeper status unknown" "report" "$gatekeeper_status" +fi + +# --- Firewall --- +fw_tool="/usr/libexec/ApplicationFirewall/socketfilterfw" +if [ -x "$fw_tool" ]; then + fw_status=$("$fw_tool" --getglobalstate 2>/dev/null || echo "unknown") + if echo "$fw_status" | grep -qi "enabled"; then + json_finding "info" "Firewall enabled" "none" "" + else + json_finding "critical" "Firewall is disabled" "report" "Enable in System Settings > Network > Firewall" + fi +else + # Fallback to defaults + fw_default=$(defaults read /Library/Preferences/com.apple.alf globalstate 2>/dev/null || echo "unknown") + case "$fw_default" in + 0) json_finding "critical" "Firewall is disabled" "report" "Enable in System Settings > Network > Firewall" ;; + 1|2) json_finding "info" "Firewall enabled" "none" "" ;; + *) json_finding "warning" "Firewall status unknown" "report" "" ;; + esac +fi + +# --- FileVault --- +fv_status=$(fdesetup status 2>/dev/null || echo "unknown") +if echo "$fv_status" | grep -q "FileVault is On"; then + json_finding "info" "FileVault enabled (disk encryption)" "none" "" +elif echo "$fv_status" | grep -q "FileVault is Off"; then + json_finding "critical" "FileVault is off (disk not encrypted)" "report" "Enable in System Settings > Privacy and Security > FileVault" +else + json_finding "warning" "FileVault status unknown" "report" "$fv_status" +fi + +# --- Open listening ports --- +listening_ports=$(lsof -iTCP -sTCP:LISTEN -nP 2>/dev/null | tail -n +2 || echo "") +port_count=$(echo "$listening_ports" | grep -c . 2>/dev/null || echo 0) + +if [ "$port_count" -gt 0 ]; then + # Extract unique port/process combos + port_summary=$(echo "$listening_ports" | awk '{print $1":"$9}' | sort -u | head -10 | tr '\n' ', ' | sed 's/,$//') + json_finding "info" "${port_count} listening TCP connections" "review" "$port_summary" +fi + +# --- SSH config --- +ssh_config="/etc/ssh/sshd_config" +if [ -f "$ssh_config" ]; then + # Check password auth + if grep -qi "^PasswordAuthentication yes" "$ssh_config" 2>/dev/null; then + json_finding "warning" "SSH password authentication enabled" "report" "Consider key-based auth only" + fi + # Check root login + if grep -qi "^PermitRootLogin yes" "$ssh_config" 2>/dev/null; then + json_finding "warning" "SSH root login permitted" "report" "Consider disabling root SSH login" + fi +fi + +# --- Remote login --- +remote_login=$(systemsetup -getremotelogin 2>/dev/null || echo "unknown") +if echo "$remote_login" | grep -qi "on"; then + json_finding "info" "Remote Login (SSH) is enabled" "review" "Disable if not needed: System Settings > General > Sharing" +fi + +# --- Remote management --- +remote_mgmt=$(defaults read /Library/Preferences/com.apple.RemoteManagement ARD_AllLocalUsers 2>/dev/null || echo "not configured") +if [ "$remote_mgmt" = "1" ]; then + json_finding "warning" "Remote Management enabled for all users" "review" "Check System Settings > General > Sharing" +fi + +# --- Software updates --- +# Just check if updates are available (this can be slow, so we time-limit it) +# macOS lacks GNU timeout — use perl alarm as a portable alternative +update_check=$(perl -e 'alarm 30; exec @ARGV' -- softwareupdate -l 2>&1 || echo "timeout or error") +if echo "$update_check" | grep -q "No new software available"; then + json_finding "info" "macOS software is up to date" "none" "" +elif echo "$update_check" | grep -q "timeout or error"; then + json_finding "info" "Software update check timed out" "none" "Run softwareupdate -l manually" +else + update_count=$(echo "$update_check" | grep -c "^\*" 2>/dev/null || echo 0) + json_finding "warning" "${update_count} software update(s) available" "softwareupdate" "Run: softwareupdate -l for details" +fi + +# --- Screen lock --- +screen_lock_delay=$(defaults -currentHost read com.apple.screensaver idleTime 2>/dev/null || echo "not set") +ask_password=$(defaults read com.apple.screensaver askForPassword 2>/dev/null || echo "unknown") + +if [ "$ask_password" = "0" ]; then + json_finding "warning" "Screen lock password not required" "report" "Enable in System Settings > Lock Screen" +elif [ "$screen_lock_delay" != "not set" ] && [ "$screen_lock_delay" -gt 300 ] 2>/dev/null; then + json_finding "info" "Screen lock delay: ${screen_lock_delay}s (consider reducing)" "report" "" +fi diff --git a/custom-skills/92-mac-optimizer/scripts/cleanup_execute.sh b/custom-skills/92-mac-optimizer/scripts/cleanup_execute.sh new file mode 100755 index 0000000..92613b1 --- /dev/null +++ b/custom-skills/92-mac-optimizer/scripts/cleanup_execute.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +# cleanup_execute.sh — Execute cleanup actions on macOS +# Usage: cleanup_execute.sh [--dry-run|--execute] [...] +# Targets: user-logs, user-caches, brew-cache, npm-cache, pip-cache, +# xcode-derived, xcode-archives, ios-support, docker, trash +set -euo pipefail + +MODE="dry-run" +TARGETS=() +BACKUP_DIR="${BACKUP_DIR:-$HOME/.config/mac-optimizer-backups}" + +usage() { + echo "Usage: $0 [--dry-run|--execute] [...]" + echo "" + echo "Options:" + echo " --dry-run Show what would be deleted (default)" + echo " --execute Actually perform cleanup" + echo "" + echo "Targets:" + echo " user-logs ~/Library/Logs/" + echo " user-caches ~/Library/Caches/" + echo " brew-cache Homebrew download cache" + echo " npm-cache npm cache" + echo " pip-cache pip cache" + echo " xcode-derived Xcode DerivedData" + echo " xcode-archives Xcode Archives" + echo " ios-support iOS DeviceSupport files" + echo " docker Docker system prune" + echo " trash Empty Trash" + exit 1 +} + +# Parse args +while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) MODE="dry-run"; shift ;; + --execute) MODE="execute"; shift ;; + --help|-h) usage ;; + *) TARGETS+=("$1"); shift ;; + esac +done + +if [ ${#TARGETS[@]} -eq 0 ]; then + echo "Error: No targets specified" + usage +fi + +log_action() { + local target="$1" action="$2" size="$3" + echo "[${MODE}] ${target}: ${action} (${size})" +} + +clean_dir() { + local name="$1" path="$2" + if [ ! -d "$path" ]; then + echo "[skip] ${name}: directory not found (${path})" + return + fi + local size=$(du -sh "$path" 2>/dev/null | awk '{print $1}') + if [ "$MODE" = "dry-run" ]; then + log_action "$name" "would remove contents of ${path}" "$size" + else + log_action "$name" "removing contents of ${path}" "$size" + find "$path" -mindepth 1 -delete 2>/dev/null || { + # Fallback: remove what we can + find "$path" -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true + } + echo "[done] ${name}: cleaned" + fi +} + +for target in "${TARGETS[@]}"; do + case "$target" in + user-logs) + clean_dir "User Logs" "$HOME/Library/Logs" + ;; + user-caches) + clean_dir "User Caches" "$HOME/Library/Caches" + ;; + brew-cache) + if command -v brew &>/dev/null; then + brew_cache=$(brew --cache 2>/dev/null || echo "") + if [ -n "$brew_cache" ]; then + size=$(du -sh "$brew_cache" 2>/dev/null | awk '{print $1}') + if [ "$MODE" = "dry-run" ]; then + log_action "Homebrew Cache" "would run brew cleanup" "$size" + else + log_action "Homebrew Cache" "running brew cleanup" "$size" + brew cleanup --prune=0 2>/dev/null || true + echo "[done] Homebrew Cache: cleaned" + fi + fi + else + echo "[skip] brew-cache: Homebrew not installed" + fi + ;; + npm-cache) + if command -v npm &>/dev/null; then + npm_cache="$HOME/.npm/_cacache" + if [ -d "$npm_cache" ]; then + size=$(du -sh "$npm_cache" 2>/dev/null | awk '{print $1}') + if [ "$MODE" = "dry-run" ]; then + log_action "npm Cache" "would run npm cache clean --force" "$size" + else + log_action "npm Cache" "running npm cache clean" "$size" + npm cache clean --force 2>/dev/null || true + echo "[done] npm Cache: cleaned" + fi + fi + else + echo "[skip] npm-cache: npm not installed" + fi + ;; + pip-cache) + pip_cache="$HOME/Library/Caches/pip" + if [ -d "$pip_cache" ]; then + size=$(du -sh "$pip_cache" 2>/dev/null | awk '{print $1}') + if [ "$MODE" = "dry-run" ]; then + log_action "pip Cache" "would run pip cache purge" "$size" + else + log_action "pip Cache" "running pip cache purge" "$size" + pip3 cache purge 2>/dev/null || pip cache purge 2>/dev/null || true + echo "[done] pip Cache: cleaned" + fi + else + echo "[skip] pip-cache: no pip cache found" + fi + ;; + xcode-derived) + clean_dir "Xcode DerivedData" "$HOME/Library/Developer/Xcode/DerivedData" + ;; + xcode-archives) + clean_dir "Xcode Archives" "$HOME/Library/Developer/Xcode/Archives" + ;; + ios-support) + clean_dir "iOS DeviceSupport" "$HOME/Library/Developer/Xcode/iOS DeviceSupport" + ;; + docker) + if command -v docker &>/dev/null; then + if [ "$MODE" = "dry-run" ]; then + echo "[dry-run] Docker: would run docker system prune" + docker system df 2>/dev/null || echo " (Docker not running)" + else + echo "[execute] Docker: running docker system prune -f" + docker system prune -f 2>/dev/null || echo " Docker prune failed (is Docker running?)" + fi + else + echo "[skip] docker: Docker not installed" + fi + ;; + trash) + trash_dir="$HOME/.Trash" + if [ -d "$trash_dir" ]; then + size=$(du -sh "$trash_dir" 2>/dev/null | awk '{print $1}') + if [ "$MODE" = "dry-run" ]; then + log_action "Trash" "would empty Trash" "$size" + else + log_action "Trash" "emptying Trash" "$size" + rm -rf "$trash_dir"/* 2>/dev/null || true + rm -rf "$trash_dir"/.[!.]* 2>/dev/null || true + echo "[done] Trash: emptied" + fi + fi + ;; + *) + echo "[error] Unknown target: ${target}" + ;; + esac +done