#!/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