Browse Source

feat(mac-ops): Add brew-health, update-state, media-libraries

brew-health.sh        Homebrew state: doctor warnings, outdated formulae +
                      casks, cleanup opportunities, architecture sanity
                      (Apple Silicon vs Intel), pinned formulae, brew services,
                      third-party taps. Exits cleanly if brew not installed.

update-state.sh       macOS auto-update policy + pending Software Updates
                      + Mac App Store auto-update + mas CLI integration.
                      Surfaces "security updates auto-install OFF" as a
                      warning since patches won't apply otherwise.

media-libraries.sh    Photos / Music / TV / FCP / Logic / iMovie library
                      inventory + sizes + iCloud Drive cache. Sync daemon
                      CPU snapshot (photolibraryd, cloudphotod, photoanalysisd,
                      cloudd, bird, fileproviderd). Recent sync errors from log.

Total now: 19 scripts, 10 reference docs, 5,918 lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0xDarkMatter 2 weeks ago
parent
commit
111f9767f9

+ 1 - 1
.claude-plugin/plugin.json

@@ -1,6 +1,6 @@
 {
   "name": "claude-mods",
-  "version": "2.7.2",
+  "version": "2.7.3",
   "description": "Custom commands, skills, and agents for Claude Code - session continuity, 23 expert agents, 78 skills, 2 commands, 6 rules, 4 hooks, 13 output styles, modern CLI tools",
   "author": "0xDarkMatter",
   "repository": "https://github.com/0xDarkMatter/claude-mods",

File diff suppressed because it is too large
+ 3 - 0
README.md


+ 187 - 0
skills/mac-ops/scripts/brew-health.sh

@@ -0,0 +1,187 @@
+#!/usr/bin/env bash
+# mac-ops :: brew-health.sh
+# Homebrew state audit. Most Mac developers have brew installed; outdated
+# packages, broken casks, and brew doctor warnings are a frequent silent
+# cause of "this dev tool stopped working".
+
+set -u
+
+while [[ $# -gt 0 ]]; do
+    case "$1" in
+        --help|-h)
+            cat <<EOF
+Usage: $0 [options]
+
+  --json, --redact, --quiet, --verbose
+
+Reports:
+  1. brew + paths sanity (Intel /usr/local vs Apple Silicon /opt/homebrew)
+  2. brew doctor summary
+  3. Outdated formulae + casks
+  4. Cleanup opportunities (caches, deprecated)
+  5. Pinned formulae (held back on purpose vs by accident)
+  6. brew services state
+  7. Tap inventory
+
+If brew isn't installed, the script exits cleanly with [INFO] markers.
+EOF
+            exit 0 ;;
+        *) shift ;;
+    esac
+done
+
+source "$(dirname "$0")/_lib/common.sh"
+parse_common_flags "$@"
+maybe_filter_self "$@"
+
+# ----------------------------------------------------------------------------
+section "1. BREW INSTALLATION"
+# ----------------------------------------------------------------------------
+if ! command -v brew >/dev/null 2>&1; then
+    log_info "Homebrew" "not installed on this Mac"
+    emit_summary
+    exit 0
+fi
+
+brew_path=$(command -v brew)
+brew_prefix=$(brew --prefix 2>/dev/null)
+brew_version=$(brew --version 2>/dev/null | head -1)
+log_pass "Homebrew" "$brew_version"
+note "  brew: $brew_path"
+note "  prefix: $brew_prefix"
+
+# Architecture sanity
+if is_apple_silicon; then
+    if [[ "$brew_prefix" == "/opt/homebrew"* ]]; then
+        log_pass "Architecture match" "Apple Silicon + native /opt/homebrew"
+    elif [[ "$brew_prefix" == "/usr/local"* ]]; then
+        log_warn "Architecture mismatch" "Apple Silicon Mac running x86_64 brew via Rosetta"
+    fi
+else
+    if [[ "$brew_prefix" == "/usr/local"* ]]; then
+        log_pass "Architecture match" "Intel + /usr/local"
+    fi
+fi
+
+# ----------------------------------------------------------------------------
+section "2. BREW DOCTOR"
+# ----------------------------------------------------------------------------
+doctor_out=$(brew doctor 2>&1)
+if echo "$doctor_out" | grep -q "Your system is ready to brew"; then
+    log_pass "brew doctor" "Your system is ready to brew"
+else
+    warnings=$(echo "$doctor_out" | grep -c "^Warning:" || echo 0)
+    log_warn "brew doctor" "$warnings warning(s) — see below"
+    note "  Top doctor messages (first 15 lines):"
+    echo "$doctor_out" | head -15 | sed 's/^/    /'
+fi
+
+# ----------------------------------------------------------------------------
+section "3. OUTDATED PACKAGES"
+# ----------------------------------------------------------------------------
+formulae_out=$(brew outdated --formula 2>/dev/null)
+formulae_count=$(echo "$formulae_out" | grep -c . 2>/dev/null || echo 0)
+casks_out=$(brew outdated --cask 2>/dev/null)
+casks_count=$(echo "$casks_out" | grep -c . 2>/dev/null || echo 0)
+
+if [[ "$formulae_count" -gt 0 ]]; then
+    log_info "Outdated formulae" "$formulae_count"
+    echo "$formulae_out" | head -10 | sed 's/^/    /'
+else
+    log_pass "Outdated formulae" "0"
+fi
+
+if [[ "$casks_count" -gt 0 ]]; then
+    log_info "Outdated casks" "$casks_count"
+    echo "$casks_out" | head -10 | sed 's/^/    /'
+else
+    log_pass "Outdated casks" "0"
+fi
+
+# ----------------------------------------------------------------------------
+section "4. CLEANUP OPPORTUNITIES"
+# ----------------------------------------------------------------------------
+# brew cleanup --dry-run reports what would be removed
+cleanup_dry=$(brew cleanup --dry-run 2>/dev/null | tail -5)
+if [[ -n "$cleanup_dry" ]]; then
+    note "  brew cleanup --dry-run (last 5 lines):"
+    echo "$cleanup_dry" | sed 's/^/    /'
+fi
+
+# Cache size
+cache_dir=$(brew --cache 2>/dev/null)
+if [[ -d "$cache_dir" ]]; then
+    cache_size=$(du -sh "$cache_dir" 2>/dev/null | awk '{print $1}')
+    log_info "Brew cache size" "${cache_size:-?}"
+fi
+
+# Deprecated/abandoned packages
+deprecated=$(brew list --formula 2>/dev/null | while read -r f; do
+    if brew info --json=v1 "$f" 2>/dev/null | grep -q '"deprecated":true\|"disabled":true'; then
+        echo "$f"
+    fi
+done | head -10)
+if [[ -n "$deprecated" ]]; then
+    n=$(echo "$deprecated" | wc -l | tr -d ' ')
+    log_warn "Deprecated/disabled formulae installed" "$n"
+    echo "$deprecated" | sed 's/^/    /'
+fi
+
+# ----------------------------------------------------------------------------
+section "5. PINNED FORMULAE"
+# ----------------------------------------------------------------------------
+pinned=$(brew list --pinned 2>/dev/null)
+if [[ -n "$pinned" ]]; then
+    n=$(echo "$pinned" | wc -l | tr -d ' ')
+    log_info "Pinned formulae" "$n"
+    echo "$pinned" | sed 's/^/    /'
+    note ""
+    note "  Pinned packages don't get upgraded by 'brew upgrade'. Unpin:"
+    note "    brew unpin <name>"
+else
+    log_pass "Pinned formulae" "0"
+fi
+
+# ----------------------------------------------------------------------------
+section "6. BREW SERVICES"
+# ----------------------------------------------------------------------------
+if command -v brew >/dev/null 2>&1 && brew help services >/dev/null 2>&1; then
+    services_out=$(brew services list 2>/dev/null | tail -n +2)
+    if [[ -n "$services_out" ]]; then
+        running=$(echo "$services_out" | awk '$2=="started"' | wc -l | tr -d ' ')
+        total=$(echo "$services_out" | wc -l | tr -d ' ')
+        log_info "Brew services" "$running running of $total"
+        echo "$services_out" | sed 's/^/    /' | head -15
+    else
+        log_pass "Brew services" "none configured"
+    fi
+fi
+
+# ----------------------------------------------------------------------------
+section "7. TAPS"
+# ----------------------------------------------------------------------------
+taps=$(brew tap 2>/dev/null)
+tap_count=$(echo "$taps" | grep -c . 2>/dev/null || echo 0)
+log_info "Brew taps" "$tap_count"
+echo "$taps" | head -10 | sed 's/^/    /'
+
+# Third-party taps (not homebrew/*)
+third_party_taps=$(echo "$taps" | grep -v "^homebrew/" || true)
+if [[ -n "$third_party_taps" ]]; then
+    n=$(echo "$third_party_taps" | wc -l | tr -d ' ')
+    note ""
+    note "  Third-party taps ($n) — these are external trust:"
+    echo "$third_party_taps" | sed 's/^/    /'
+fi
+
+# ----------------------------------------------------------------------------
+emit_summary
+
+if [[ "$JSON_MODE" -eq 0 ]]; then
+    echo
+    note "  Quick cleanup playbook:"
+    note "    brew update && brew upgrade        # update everything"
+    note "    brew cleanup -s                    # remove old versions + caches"
+    note "    brew autoremove                    # remove orphaned dependencies"
+    note "    brew doctor                        # full doctor scan"
+fi

+ 167 - 0
skills/mac-ops/scripts/media-libraries.sh

@@ -0,0 +1,167 @@
+#!/usr/bin/env bash
+# mac-ops :: media-libraries.sh
+# Audit Photos, Music, TV, and other media libraries: sizes, locations,
+# integrity status, and the sync daemons that manage them.
+
+set -u
+
+while [[ $# -gt 0 ]]; do
+    case "$1" in
+        --help|-h)
+            cat <<EOF
+Usage: $0 [options]
+
+  --json, --redact, --quiet, --verbose
+
+Reports:
+  1. Photos library locations + size
+  2. Music / TV library
+  3. Pro app libraries (Final Cut, Logic, iMovie)
+  4. iCloud Drive / Mobile Documents size
+  5. Photos / Music sync daemons (photolibraryd, cloudphotod, photoanalysisd,
+     amsondemandinstalld, cloudd, bird) — CPU + memory
+  6. Suspended sync state (frequent silent cause of "Photos won't sync")
+
+These libraries can be 10s-100s of GB and show as "Other" in About This Mac.
+EOF
+            exit 0 ;;
+        *) shift ;;
+    esac
+done
+
+source "$(dirname "$0")/_lib/common.sh"
+parse_common_flags "$@"
+maybe_filter_self "$@"
+
+# ----------------------------------------------------------------------------
+section "1. PHOTOS LIBRARIES"
+# ----------------------------------------------------------------------------
+# Default location:
+default_photos="$HOME/Pictures/Photos Library.photoslibrary"
+# Find any .photoslibrary anywhere reasonable
+photo_libs=$(find "$HOME/Pictures" /Users/Shared "$HOME/Documents" -maxdepth 2 -name "*.photoslibrary" -type d 2>/dev/null | head -5)
+# Also check external volumes if mounted
+ext_photo_libs=$(find /Volumes -maxdepth 3 -name "*.photoslibrary" -type d 2>/dev/null | head -5)
+photo_libs="$photo_libs"$'\n'"$ext_photo_libs"
+photo_libs=$(echo "$photo_libs" | grep -v '^$')
+
+if [[ -z "$photo_libs" ]]; then
+    log_info "Photos libraries" "none found"
+else
+    n=$(echo "$photo_libs" | wc -l | tr -d ' ')
+    log_info "Photos libraries" "$n found"
+    while IFS= read -r lib; do
+        [[ -z "$lib" ]] && continue
+        size=$(du -sh "$lib" 2>/dev/null | awk '{print $1}')
+        printf "    %s  =  %s\n" "$lib" "${size:-?}"
+    done <<< "$photo_libs"
+fi
+
+# Current Photos system library
+sys_photos=$(defaults read com.apple.Photos UserLibrarySelectionMethod 2>/dev/null || echo "")
+if [[ -n "$sys_photos" ]]; then
+    note ""
+    note "  System Photos library: per Photos.app preferences"
+fi
+
+# ----------------------------------------------------------------------------
+section "2. MUSIC / TV LIBRARIES"
+# ----------------------------------------------------------------------------
+music_lib="$HOME/Music/Music"
+if [[ -d "$music_lib" ]]; then
+    size=$(du -sh "$music_lib" 2>/dev/null | awk '{print $1}')
+    log_info "Music library" "${size:-?} ($music_lib)"
+fi
+
+# .musiclibrary
+musiclibs=$(find "$HOME/Music" -maxdepth 2 -name "*.musiclibrary" -type d 2>/dev/null | head -3)
+if [[ -n "$musiclibs" ]]; then
+    while IFS= read -r lib; do
+        [[ -z "$lib" ]] && continue
+        size=$(du -sh "$lib" 2>/dev/null | awk '{print $1}')
+        note "    $lib = ${size:-?}"
+    done <<< "$musiclibs"
+fi
+
+# TV
+tv_dir="$HOME/Movies/TV"
+if [[ -d "$tv_dir" ]]; then
+    size=$(du -sh "$tv_dir" 2>/dev/null | awk '{print $1}')
+    log_info "TV library" "${size:-?} ($tv_dir)"
+fi
+
+# ----------------------------------------------------------------------------
+section "3. PRO APP LIBRARIES"
+# ----------------------------------------------------------------------------
+note "  Scanning for FCP / Logic / iMovie libraries..."
+pro_libs=$(find "$HOME/Movies" "$HOME/Documents" "$HOME/Logic" /Volumes -maxdepth 3 \
+    \( -name "*.fcpbundle" -o -name "*.logicx" -o -name "*.imovielibrary" -o -name "*.band" \) -type d 2>/dev/null | head -10)
+if [[ -z "$pro_libs" ]]; then
+    log_info "Pro app libraries" "none found in standard locations"
+else
+    n=$(echo "$pro_libs" | wc -l | tr -d ' ')
+    log_info "Pro app libraries" "$n found"
+    while IFS= read -r lib; do
+        [[ -z "$lib" ]] && continue
+        size=$(du -sh "$lib" 2>/dev/null | awk '{print $1}')
+        printf "    %s = %s\n" "$lib" "${size:-?}"
+    done <<< "$pro_libs"
+fi
+
+# ----------------------------------------------------------------------------
+section "4. iCLOUD DRIVE / MOBILE DOCUMENTS"
+# ----------------------------------------------------------------------------
+icloud="$HOME/Library/Mobile Documents"
+if [[ -d "$icloud" ]]; then
+    size=$(du -sh "$icloud" 2>/dev/null | awk '{print $1}')
+    log_info "iCloud Drive cache" "${size:-?}"
+    note "  (Subset is downloaded; rest is placeholders. macOS evicts under pressure.)"
+fi
+
+# Top iCloud Drive consumers
+note ""
+note "  Top consumers under iCloud Drive (du -sh, may be slow):"
+find "$icloud" -maxdepth 2 -type d 2>/dev/null | head -10 | while read -r d; do
+    [[ "$d" == "$icloud" ]] && continue
+    size=$(du -sh "$d" 2>/dev/null | awk '{print $1}')
+    printf "    %s = %s\n" "$d" "${size:-?}"
+done | sort -k3 -h -r | head -5
+
+# ----------------------------------------------------------------------------
+section "5. SYNC DAEMONS — CPU SNAPSHOT"
+# ----------------------------------------------------------------------------
+note "  Process CPU%:"
+for proc in photolibraryd cloudphotod photoanalysisd amsondemandinstalld cloudd bird fileproviderd; do
+    cpu=$(ps -ArcS -o pcpu,comm 2>/dev/null | awk -v p="$proc" '$2==p{print $1; exit}')
+    cpu="${cpu:-0}"
+    cpu_int=${cpu%.*}
+    if [[ "${cpu_int:-0}" -gt 50 ]]; then
+        log_warn "$proc" "${cpu}% — sustained sync activity"
+    elif [[ "${cpu_int:-0}" -gt 0 ]]; then
+        log_info "$proc" "${cpu}%"
+    fi
+done
+
+# ----------------------------------------------------------------------------
+section "6. SYNC HEALTH"
+# ----------------------------------------------------------------------------
+# Check for suspended sync / iCloud sign-out / etc.
+icloud_status=$(brctl status 2>/dev/null || true)
+if [[ -n "$icloud_status" ]] && echo "$icloud_status" | grep -q "iCloud Drive Disabled"; then
+    log_warn "iCloud Drive" "disabled"
+fi
+
+# Photos cloud status
+note ""
+note "  Recent photolibraryd / cloudphotod errors (24h):"
+photo_errs=$(log show --last 24h --style compact \
+    --predicate 'process == "photolibraryd" OR process == "cloudphotod"' \
+    2>/dev/null | grep -iE "(error|fail|fault)" | tail -5)
+if [[ -n "$photo_errs" ]]; then
+    echo "$photo_errs" | sed 's/^/    /'
+else
+    note "    (none)"
+fi
+
+# ----------------------------------------------------------------------------
+emit_summary

+ 153 - 0
skills/mac-ops/scripts/update-state.sh

@@ -0,0 +1,153 @@
+#!/usr/bin/env bash
+# mac-ops :: update-state.sh
+# macOS Software Update audit: auto-update settings, pending updates,
+# update history, App Store update settings.
+
+set -u
+
+while [[ $# -gt 0 ]]; do
+    case "$1" in
+        --help|-h)
+            cat <<EOF
+Usage: $0 [options]
+
+  --json, --redact, --quiet, --verbose
+
+Reports:
+  1. macOS Software Update auto-policy
+  2. Pending updates (softwareupdate -l)
+  3. macOS version + build
+  4. Update install history (last 10)
+  5. App Store auto-update settings
+  6. Pending app updates from Mac App Store
+
+Skip the spinner: 'softwareupdate -l' contacts Apple's servers and can take
+30-60 seconds. The script prints expected-delay markers.
+EOF
+            exit 0 ;;
+        *) shift ;;
+    esac
+done
+
+source "$(dirname "$0")/_lib/common.sh"
+parse_common_flags "$@"
+maybe_filter_self "$@"
+
+# ----------------------------------------------------------------------------
+section "1. AUTO-UPDATE POLICY"
+# ----------------------------------------------------------------------------
+# Read from com.apple.SoftwareUpdate
+auto_check=$(defaults read /Library/Preferences/com.apple.SoftwareUpdate AutomaticCheckEnabled 2>/dev/null)
+auto_download=$(defaults read /Library/Preferences/com.apple.SoftwareUpdate AutomaticDownload 2>/dev/null)
+auto_install_macos=$(defaults read /Library/Preferences/com.apple.SoftwareUpdate AutomaticallyInstallMacOSUpdates 2>/dev/null)
+auto_install_app=$(defaults read /Library/Preferences/com.apple.commerce AutoUpdate 2>/dev/null)
+auto_install_security=$(defaults read /Library/Preferences/com.apple.SoftwareUpdate ConfigDataInstall 2>/dev/null)
+
+show_bool() {
+    case "$1" in
+        1) echo "ON" ;;
+        0) echo "OFF" ;;
+        *) echo "(unset)" ;;
+    esac
+}
+
+note "  Software Update preferences:"
+note "    Check for updates automatically: $(show_bool "$auto_check")"
+note "    Download updates when available: $(show_bool "$auto_download")"
+note "    Install macOS updates:           $(show_bool "$auto_install_macos")"
+note "    Install app updates from App Store: $(show_bool "$auto_install_app")"
+note "    Install system data + security:  $(show_bool "$auto_install_security")"
+
+if [[ "$auto_check" == "1" ]] && [[ "$auto_install_security" == "1" ]]; then
+    log_pass "Security updates" "auto-install ON"
+else
+    log_warn "Security updates" "auto-install OFF — patches won't apply without manual action"
+fi
+
+# ----------------------------------------------------------------------------
+section "2. MACOS VERSION"
+# ----------------------------------------------------------------------------
+prod=$(sw_vers -productName 2>/dev/null)
+ver=$(sw_vers -productVersion 2>/dev/null)
+build=$(sw_vers -buildVersion 2>/dev/null)
+note "  $prod $ver ($build)"
+log_info "macOS version" "$ver build $build"
+
+# ----------------------------------------------------------------------------
+section "3. PENDING UPDATES"
+# ----------------------------------------------------------------------------
+note "  Checking with Apple's servers (this can take 30-60s)..."
+pending=$(softwareupdate -l 2>&1 | tail -20)
+if echo "$pending" | grep -q "No new software available"; then
+    log_pass "Pending updates" "none — system is current"
+elif echo "$pending" | grep -q "Software Update found"; then
+    note "  Pending list:"
+    echo "$pending" | grep -E "(\*|^Software|Title:|Action:|Recommended:)" | head -20 | sed 's/^/    /'
+    update_count=$(echo "$pending" | grep -c "Title:" || echo 0)
+    log_warn "Pending updates" "$update_count items pending"
+else
+    log_info "softwareupdate output" "$(echo "$pending" | head -3 | tr '\n' ' ')"
+fi
+
+# ----------------------------------------------------------------------------
+section "4. RECENT UPDATE HISTORY"
+# ----------------------------------------------------------------------------
+history_plist="/Library/Updates/index.plist"
+note "  Last 10 install events from /Library/Updates (best-effort):"
+if [[ -r "$history_plist" ]]; then
+    # Can't easily parse the plist without knowing the schema; show
+    # mtime of the directory entries as a proxy
+    ls -lt /Library/Updates 2>/dev/null | head -10 | sed 's/^/    /'
+fi
+
+# Also check installer logs
+recent_installs=$(log show --last 30d --style compact \
+    --predicate 'process == "softwareupdated" AND eventMessage CONTAINS[c] "installed"' \
+    2>/dev/null | tail -5)
+if [[ -n "$recent_installs" ]]; then
+    note ""
+    note "  Recent softwareupdated install entries (log, last 30d):"
+    echo "$recent_installs" | sed 's/^/    /'
+fi
+
+# ----------------------------------------------------------------------------
+section "5. MAS / APP STORE"
+# ----------------------------------------------------------------------------
+mas_check=$(defaults read /Library/Preferences/com.apple.commerce AutoUpdate 2>/dev/null)
+note "  App Store auto-update: $(show_bool "$mas_check")"
+
+# Is mas installed (third-party MAS CLI)?
+if command -v mas >/dev/null 2>&1; then
+    note ""
+    note "  mas (Mac App Store CLI) is installed."
+    mas_outdated=$(mas outdated 2>/dev/null)
+    if [[ -n "$mas_outdated" ]]; then
+        n=$(echo "$mas_outdated" | wc -l | tr -d ' ')
+        log_info "Pending MAS updates" "$n (via mas outdated)"
+        echo "$mas_outdated" | head -10 | sed 's/^/    /'
+    else
+        log_pass "MAS apps" "all up to date"
+    fi
+fi
+
+# ----------------------------------------------------------------------------
+section "6. RECOMMENDED UPDATES"
+# ----------------------------------------------------------------------------
+# Recommended security/system updates that Apple wants you to install
+# (subset of softwareupdate -l output)
+recommended=$(softwareupdate -l 2>&1 | awk '/Recommended: YES/{print prev} {prev=$0}' | head -5)
+if [[ -n "$recommended" ]]; then
+    log_warn "Recommended updates pending" "see list"
+    echo "$recommended" | sed 's/^/    /'
+fi
+
+# ----------------------------------------------------------------------------
+emit_summary
+
+if [[ "$JSON_MODE" -eq 0 ]]; then
+    echo
+    note "  Install playbook:"
+    note "    softwareupdate -l                # list pending"
+    note "    sudo softwareupdate -i -a -R     # install all (recommended), reboot if needed"
+    note "    softwareupdate --install <name>  # specific update"
+fi

+ 1 - 1
skills/mac-ops/tests/run.sh

@@ -174,7 +174,7 @@ expected_scripts=(
     disk-health.sh drive-dependencies.sh boot-perf.sh recover-clone.sh
     tcc-audit.sh wake-reasons.sh spotlight-status.sh storage-pressure.sh
     kext-audit.sh firewall-audit.sh network-locations.sh
-    sysdiagnose-helper.sh
+    sysdiagnose-helper.sh brew-health.sh update-state.sh media-libraries.sh
 )
 for s in "${expected_scripts[@]}"; do
     assert "script exists: $s" test -f "$root/scripts/$s"