Restructured 92-mac-optimizer from a CLAUDE.md-based skill into a full Claude Code plugin with .claude-plugin/plugin.json, 6 slash commands (/mac-doctor, /mac-packages, /mac-environment, /mac-security, /mac-cleanup, /mac-resources), and auto-trigger SKILL.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
128 lines
4.1 KiB
Bash
Executable File
128 lines
4.1 KiB
Bash
Executable File
#!/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
|