Skip to content

Instantly share code, notes, and snippets.

@MathieuLopes
Created February 9, 2026 15:56
Show Gist options
  • Select an option

  • Save MathieuLopes/c80457b899517a6e508ef241a6386c3b to your computer and use it in GitHub Desktop.

Select an option

Save MathieuLopes/c80457b899517a6e508ef241a6386c3b to your computer and use it in GitHub Desktop.
πŸ–₯️ Automated macOS maintenance tool. Updates system & 23+ dev tools (Homebrew, Node.js, Python, Ruby, Rust, Docker, npm, Yarn, pip) with beautiful CLI dashboard. Features: error handling, logging, debug mode, MAMP support. https://github.com/MathieuLopes/Macbook-Update-Suite
#!/bin/bash
#
# MacBook Update Suite β€” Premium CLI Dashboard (v4)
# Professional system and development stack maintenance tool
#
# πŸ“– For complete documentation, installation instructions, and details, visit:
# https://github.com/MathieuLopes/Macbook-Update-Suite
#
# Purpose: One-shot maintenance for system updates, Xcode CLT, Homebrew,
# Node/Python/Ruby/Rust/Go, Android SDK, Composer, Docker, npm/Yarn/pip,
# and optional cleanup. Optional sections run only when the tool is present.
# Usage: ./macbook-update-suite.sh or alias macbook-updates=".../macbook-update-suite.sh"
# Mode 1 = Full update (no prompts); Mode 2 = Custom (ask before each section).
# Prerequisites: Bash 4+, macOS, Gum (charmbracelet/gum). Optional tools are detected and skipped if absent.
# Source: https://gist.github.com/MathieuLopes/0a307909faff780896845f1e664bbdac
# https://github.com/MathieuLopes/Macbook-Update-Suite
#
# Changes in v4:
# - Version 4.0.0 release (major)
# Changes in v3.4:
# - Added Debug & Verification Mode for comprehensive testing of detection and update methods
# Changes in v3.2:
# - Enhanced error handling with proper exit code checking
# - Improved version extraction with validation
# - Dynamic MAMP detection (all PHP versions)
# - Network error handling with timeouts
# - Comprehensive logging to ~/.macbook-updates.log
# - Proper cleanup of temporary files
# - Better initialization of version managers (nvm, rbenv, pyenv, sdkman)
# =============================================================================
# INITIALIZATION & DEPENDENCIES
# =============================================================================
# Check for Gum dependency
if ! command -v gum &> /dev/null; then
echo "⚠️ Gum is not installed."
echo ""
echo "Gum is required for the modern interface of this script."
echo ""
if command -v brew &> /dev/null; then
read -rp "Install Gum via Homebrew? [y/N]: " install_gum
if [[ "$install_gum" =~ ^[Yy]$ ]]; then
brew install gum
if ! command -v gum &> /dev/null; then
echo "❌ Installation failed. Please install Gum manually: brew install gum"
exit 1
fi
else
echo "❌ Gum is required. Installation cancelled."
exit 1
fi
else
echo "❌ Homebrew is not installed. Please install Gum manually: brew install gum"
exit 1
fi
fi
# =============================================================================
# DASHBOARD CONFIGURATION
# =============================================================================
VERSION="4"
SECTION_REPORT=()
UPDATES_COUNT=0
LOG_HISTORY=()
START_TIME=$SECONDS
CURRENT_STEP=0
TOTAL_STEPS=23
TEMP_FILES=()
LOG_FILE="${HOME}/.macbook-updates.log"
# Track temporary files for cleanup
TEMP_FILES=()
# Handle Ctrl+C gracefully with cleanup
on_sigint_term() {
echo ""
cleanup_temp_files
exit 130
}
trap on_sigint_term INT TERM
# Cleanup temporary files on exit
cleanup_temp_files() {
for temp_file in "${TEMP_FILES[@]}"; do
[ -f "$temp_file" ] && rm -f "$temp_file" 2>/dev/null
done
}
trap cleanup_temp_files EXIT
# Logging function
log_error() {
local message="$1"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] ERROR: $message" >> "$LOG_FILE" 2>/dev/null
}
log_info() {
local message="$1"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] INFO: $message" >> "$LOG_FILE" 2>/dev/null
}
# Execute command with error checking and logging
# Usage: execute_command "description" "command" [optional_error_message]
# Returns: 0 on success, 1 on failure
execute_command() {
local description="$1"
local command="$2"
local error_msg="${3:-Command failed: $command}"
local exit_code
log_info "Executing: $description"
eval "$command"
exit_code=$?
if [ $exit_code -ne 0 ]; then
log_error "$error_msg (exit code: $exit_code)"
return 1
fi
log_info "Success: $description"
return 0
}
# Execute command with timeout
# Usage: execute_with_timeout "description" "command" timeout_seconds [optional_error_message]
execute_with_timeout() {
local description="$1"
local command="$2"
local timeout="${3:-300}"
local error_msg="${4:-Command timed out: $command}"
log_info "Executing with timeout (${timeout}s): $description"
if command -v timeout &> /dev/null; then
timeout "$timeout" bash -c "$command"
elif command -v gtimeout &> /dev/null; then
gtimeout "$timeout" bash -c "$command"
else
# Fallback: run without timeout (macOS doesn't have timeout by default)
eval "$command"
fi
local exit_code=$?
if [ $exit_code -eq 124 ] || [ $exit_code -eq 143 ]; then
log_error "$error_msg (timeout after ${timeout}s)"
return 1
elif [ $exit_code -ne 0 ]; then
log_error "Command failed: $command (exit code: $exit_code)"
return 1
fi
return 0
}
# Validate version string
# Usage: validate_version "version_string"
# Returns: 0 if valid, 1 if invalid
validate_version() {
local version="$1"
if [ -z "$version" ] || [ "$version" = "unknown" ] || [ "$version" = "β€”" ]; then
return 1
fi
# Check if it looks like a version (contains at least one digit)
if [[ "$version" =~ [0-9] ]]; then
return 0
fi
return 1
}
# Extract version from command output with validation
# Usage: extract_version "command" [fallback_command]
extract_version() {
local cmd="$1"
local fallback="${2:-}"
local version
local exit_code
version=$(eval "$cmd" 2>/dev/null)
exit_code=$?
if [ $exit_code -eq 0 ] && validate_version "$version"; then
echo "$version"
return 0
fi
if [ -n "$fallback" ]; then
version=$(eval "$fallback" 2>/dev/null)
if validate_version "$version"; then
echo "$version"
return 0
fi
fi
echo "unknown"
return 1
}
# Find MAMP PHP path dynamically
find_mamp_php() {
local mamp_base="/Applications/MAMP/bin/php"
if [ ! -d "$mamp_base" ]; then
return 1
fi
# Find the latest PHP version directory
local latest_php
latest_php=$(find "$mamp_base" -maxdepth 1 -type d -name "php*" | sort -V | tail -1)
if [ -n "$latest_php" ] && [ -f "$latest_php/bin/php" ]; then
echo "$latest_php/bin/php"
return 0
fi
return 1
}
# Check if MAMP Python is being used
is_mamp_python() {
local python_path
python_path=$(which python3 2>/dev/null || command -v python3 2>/dev/null)
# Check if python3 points to MAMP Python
if echo "$python_path" | grep -q "/Applications/MAMP/bin/python"; then
return 0
fi
# Also check if MAMP Python exists and is being used via alias
if [ -f "/Applications/MAMP/bin/python/python3/bin/python3" ]; then
# Check if there's an alias pointing to MAMP
if alias python3 2>/dev/null | grep -q MAMP; then
return 0
fi
fi
return 1
}
# Initialize version managers (nvm, rbenv, pyenv, sdkman)
init_version_manager() {
local manager="$1"
case "$manager" in
nvm)
if [ -s "$HOME/.nvm/nvm.sh" ]; then
source "$HOME/.nvm/nvm.sh" 2>/dev/null
elif [ -s "/usr/local/opt/nvm/nvm.sh" ]; then
source "/usr/local/opt/nvm/nvm.sh" 2>/dev/null
fi
;;
rbenv)
if command -v rbenv &> /dev/null; then
eval "$(rbenv init - bash 2>/dev/null)" || eval "$(rbenv init - zsh 2>/dev/null)"
fi
;;
pyenv)
if command -v pyenv &> /dev/null; then
eval "$(pyenv init - bash 2>/dev/null)" || eval "$(pyenv init - zsh 2>/dev/null)"
fi
;;
sdkman)
if [ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]; then
source "$HOME/.sdkman/bin/sdkman-init.sh" 2>/dev/null
fi
;;
esac
}
# =============================================================================
# UTILITY FUNCTIONS
# =============================================================================
# Display header (called once at start)
show_header() {
local mac_version
mac_version=$(sw_vers -productVersion 2>/dev/null || echo "β€”")
local host_short
host_short=$(hostname -s 2>/dev/null || echo "β€”")
echo ""
gum style \
--border rounded \
--border-foreground 57 \
--padding "1 3" \
--margin "0 0" \
--align center \
--width 60 \
"$(gum style --foreground 57 "β—’β—€") $(gum style --bold "MACBOOK UPDATE SUITE v${VERSION}") $(gum style --foreground 57 "β—₯β—£")" \
"$(gum style --foreground 245 "──────────────────────────────────────")" \
"$(gum style --foreground 250 "Automated Maintenance & DevStack")" \
"$(gum style --foreground 240 "macOS $mac_version β€’ $host_short")" \
"$(gum style --foreground 87 "Author: Mathieu Lopes")" \
"$(gum style --foreground 87 "Website: https://www.mathieu42.com")"
echo ""
}
# Add a completed task to history
add_to_history() {
local status="$1"
local message="$2"
LOG_HISTORY+=("$status $message")
}
# Format title with progress indicator
# Usage: format_title_with_progress "Title"
format_title_with_progress() {
local title="$1"
local progress_text
progress_text=$(gum style --bold --foreground 212 "($CURRENT_STEP/$TOTAL_STEPS)")
echo "$title $progress_text"
}
# Display section header with progress
# Usage: show_section_header "🍎 macOS System Updates"
show_section_header() {
CURRENT_STEP=$((CURRENT_STEP + 1))
local title="$1"
echo ""
local progress_indicator
progress_indicator=$(gum style --bold --foreground 212 "($CURRENT_STEP/$TOTAL_STEPS)")
gum style --bold --foreground 63 "$title: $progress_indicator"
}
# Smart spin wrapper: ensures spinner stays visible for at least 1 second to avoid flickering
# Usage: smart_spin "Title" "command with args"
# Returns the exit code of the actual command
smart_spin() {
local title="$1"
local command="$2"
local title_with_progress
title_with_progress=$(format_title_with_progress "$title")
# Run command with spinner, ensuring minimum 1 second visibility
# The sleep 0.8 ensures the spinner is visible even for very fast commands
gum spin --spinner minidot --title "$title_with_progress" -- bash -c "sleep 0.8; $command" >/dev/null 2>&1
local exit_code=$?
return $exit_code
}
# Smart spin wrapper with output capture
# Usage: output=$(smart_spin_capture "Title" "command with args")
# Returns the output of the command
smart_spin_capture() {
local title="$1"
local command="$2"
local output
local temp_file
# Create temp file and track it for cleanup
temp_file=$(mktemp 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$temp_file" ]; then
log_error "Failed to create temporary file"
echo ""
return 1
fi
TEMP_FILES+=("$temp_file")
local title_with_progress
title_with_progress=$(format_title_with_progress "$title")
# Run command with spinner, ensuring minimum 1 second visibility
# Capture output to temp file since gum spin may interfere with stdout
gum spin --spinner minidot --title "$title_with_progress" -- bash -c "sleep 0.8; $command > '$temp_file' 2>&1"
local exit_code=$?
# Read output from temp file
if [ -f "$temp_file" ]; then
output=$(cat "$temp_file" 2>/dev/null)
else
output=""
log_error "Temporary file not found: $temp_file"
fi
echo "$output"
return $exit_code
}
# Run a task simply (backward compatibility - uses smart_spin)
# Usage: run_task "Title" "command with args"
# Returns the exit code of the actual command
run_task() {
smart_spin "$1" "$2"
}
# Run a task and capture output (backward compatibility - uses smart_spin_capture)
# Usage: output=$(run_task_capture "Title" "command with args")
# Returns the output of the command
run_task_capture() {
smart_spin_capture "$1" "$2"
}
# Confirm section check: returns 0 (proceed) when UPDATE_MODE=full; otherwise prompts
# with gum confirm asking if user wants to check for updates for this section.
# Returns 0 if user wants to check, 1 if user skips the section.
# Usage: confirm_section_check "Section Name" [custom_message]
confirm_section_check() {
if [[ "${UPDATE_MODE:-custom}" == "full" ]]; then
return 0
fi
local section_name="$1"
local custom_message="${2:-}"
if [ -n "$custom_message" ]; then
gum confirm --affirmative "Yes" --negative "No" "$custom_message"
else
gum confirm --affirmative "Yes" --negative "No" "πŸ” Do you want to check for updates for $section_name?"
fi
}
# Confirm action: returns 0 (proceed) when UPDATE_MODE=full; otherwise prompts
# with gum confirm and returns 0 only if the user confirms.
confirm() {
if [[ "${UPDATE_MODE:-custom}" == "full" ]]; then
return 0
fi
gum confirm --affirmative "Yes" --negative "No" "πŸ‘‰ $1"
}
# Summary report: record section name, previous version, and new version or status
# Usage: report_section "Section" "PreviousVersion" "NewVersionOrStatus" (use "β€”" when no version)
report_section() {
SECTION_REPORT+=("$1|$2|$3")
}
# Returns an emoji for the given status (for display in summary table)
# Usage: status_emoji "PreviousValue" "NewOrStatus"
status_emoji() {
local prev="$1"
local status="$2"
case "$status" in
*[Aa]lready*[Uu]p*to*[Dd]ate*) echo "βœ…" ;;
[Uu]pdated) echo "βœ…πŸ”„" ;;
*[Oo]utdated*) echo "⚠️" ;;
[Ss]kipped) echo "⏭️" ;;
[Dd]one) echo "βœ…" ;;
*[Cc]hecked*) echo "πŸ”" ;;
*[Uu]pdates*[Aa]vailable*) echo "πŸ“‹" ;;
*)
# Version-like value (digits, dots, or v-prefix): updated if different from prev
if [[ "$status" =~ ^[vV]?[0-9] ]] && [[ -n "$prev" && "$prev" != "β€”" && "$status" != "$prev" ]]; then
echo "βœ…πŸ”„"
else
echo "βœ…"
fi
;;
esac
}
# Truncate string to max length, append "..." if truncated (for table alignment)
# Usage: truncate_str "string" max_len
truncate_str() {
local s="$1"
local max="${2:-24}"
if [ "${#s}" -le "$max" ]; then
echo "$s"
else
echo "${s:0:$((max - 3))}..."
fi
}
# =============================================================================
# DEBUG & VERIFICATION MODE
# =============================================================================
# Structure to store verification results (compatible with bash 3.2)
# Using prefixed variables instead of associative arrays
DEBUG_RESULTS_PREFIX="DEBUG_RESULT_"
DEBUG_RESULTS_KEYS=""
# Helper functions for debug results (bash 3.2 compatible)
# Use a temp file to store results to avoid escaping issues
DEBUG_RESULTS_FILE="${TMPDIR:-/tmp}/macbook-updates-debug-$$.txt"
set_debug_result() {
local key="$1"
local value="$2"
# Append to temp file: key|value
echo "${key}|${value}" >> "$DEBUG_RESULTS_FILE"
# Add key to list if not already present
if ! echo "$DEBUG_RESULTS_KEYS" | grep -qE "(^| )${key}( |$)"; then
DEBUG_RESULTS_KEYS="${DEBUG_RESULTS_KEYS} ${key}"
fi
}
get_debug_result() {
local key="$1"
# Search for the key in temp file and extract value
# Use awk for exact match on first field to handle special characters like parentheses
# Join all fields after the first one (in case value contains |)
if [ -f "$DEBUG_RESULTS_FILE" ]; then
awk -F'|' -v k="$key" '$1 == k {
# Reconstruct value by joining all fields after the first
value = $2
for (i = 3; i <= NF; i++) {
value = value "|" $i
}
print value
exit
}' "$DEBUG_RESULTS_FILE" 2>/dev/null || echo ""
else
echo ""
fi
}
# Cleanup function for debug results file
cleanup_debug_results() {
[ -f "$DEBUG_RESULTS_FILE" ] && rm -f "$DEBUG_RESULTS_FILE"
}
trap cleanup_debug_results EXIT
# Function to verify presence detection of a tool
# Usage: verify_presence_detection "tool_name" "detection_command" "expected_result"
verify_presence_detection() {
local tool_name="$1"
local detection_cmd="$2"
local expected_result="${3:-installed}"
echo ""
gum style --bold --foreground 212 "πŸ” Verification: Presence Detection - $tool_name"
echo " Detection command: $detection_cmd"
# Handle empty command
if [ -z "$detection_cmd" ]; then
gum style --foreground 3 " ⚠️ Detected: NO (no detection command)"
set_debug_result "${tool_name}_presence" "❌ Not detected"
set_debug_result "${tool_name}_presence_output" ""
return 1
fi
local result
local exit_code
result=$(eval "$detection_cmd" 2>&1)
exit_code=$?
if [ $exit_code -eq 0 ]; then
gum style --foreground 10 " βœ… Detected: YES"
echo " Output: $result"
set_debug_result "${tool_name}_presence" "βœ… Detected"
set_debug_result "${tool_name}_presence_output" "$result"
return 0
else
gum style --foreground 3 " ⚠️ Detected: NO"
echo " Output: $result"
set_debug_result "${tool_name}_presence" "❌ Not detected"
set_debug_result "${tool_name}_presence_output" "$result"
return 1
fi
}
# Function to verify version detection
# Usage: verify_version_detection "tool_name" "version_command" "fallback_command"
verify_version_detection() {
local tool_name="$1"
local version_cmd="$2"
local fallback_cmd="${3:-}"
echo ""
gum style --bold --foreground 212 "πŸ” Verification: Version Detection - $tool_name"
echo " Primary command: $version_cmd"
[ -n "$fallback_cmd" ] && echo " Fallback command: $fallback_cmd"
local version
local exit_code
version=$(eval "$version_cmd" 2>&1)
exit_code=$?
if [ $exit_code -eq 0 ]; then
# Check if version is valid or if it's a meaningful message (like "No toolchain installed")
if validate_version "$version"; then
gum style --foreground 10 " βœ… Version detected: $version"
set_debug_result "${tool_name}_version" "$version"
set_debug_result "${tool_name}_version_method" "Primary command"
return 0
elif echo "$version" | grep -qiE "(no toolchain|not installed|not available)"; then
# Meaningful message about why version can't be detected
gum style --foreground 3 " ⚠️ Version: $version"
set_debug_result "${tool_name}_version" "$version"
set_debug_result "${tool_name}_version_method" "Primary command"
return 0
fi
fi
if [ -n "$fallback_cmd" ]; then
version=$(eval "$fallback_cmd" 2>&1)
exit_code=$?
if [ $exit_code -eq 0 ]; then
if validate_version "$version"; then
gum style --foreground 10 " βœ… Version detected (fallback): $version"
set_debug_result "${tool_name}_version" "$version"
set_debug_result "${tool_name}_version_method" "Fallback command"
return 0
elif echo "$version" | grep -qiE "(no toolchain|not installed|not available)"; then
gum style --foreground 3 " ⚠️ Version: $version"
set_debug_result "${tool_name}_version" "$version"
set_debug_result "${tool_name}_version_method" "Fallback command"
return 0
fi
fi
fi
gum style --foreground 3 " ⚠️ Version not detected or invalid"
echo " Output: $version"
set_debug_result "${tool_name}_version" "unknown"
set_debug_result "${tool_name}_version_method" "Failed"
return 1
}
# Function to verify update check method
# Usage: verify_update_check_method "tool_name" "check_command" "expected_output_pattern"
verify_update_check_method() {
local tool_name="$1"
local check_cmd="$2"
local expected_pattern="${3:-}"
echo ""
gum style --bold --foreground 212 "πŸ” Verification: Update Check Method - $tool_name"
echo " Command: $check_cmd"
[ -n "$expected_pattern" ] && echo " Expected pattern: $expected_pattern"
local output
local exit_code
output=$(eval "$check_cmd" 2>&1)
exit_code=$?
echo " Exit code: $exit_code"
echo " Output (first 500 characters):"
echo "$output" | head -20 | sed 's/^/ /'
# For grep-based commands, exit code 1 means "not found" which is OK
# For other commands, exit code 0 means success
if echo "$check_cmd" | grep -qE "(grep|awk|sed)"; then
# For grep/awk/sed commands, any output or exit code is acceptable
if [ -n "$output" ] || [ $exit_code -eq 0 ] || [ $exit_code -eq 1 ]; then
if [ -n "$expected_pattern" ] && echo "$output" | grep -qE "$expected_pattern"; then
gum style --foreground 10 " βœ… Method works (pattern found)"
set_debug_result "${tool_name}_update_check" "βœ… Works"
else
gum style --foreground 10 " βœ… Method works"
set_debug_result "${tool_name}_update_check" "βœ… Works (pattern not verified)"
fi
set_debug_result "${tool_name}_update_check_output" "$output"
return 0
else
gum style --foreground 3 " ⚠️ Method fails"
set_debug_result "${tool_name}_update_check" "❌ Fails"
set_debug_result "${tool_name}_update_check_output" "$output"
return 1
fi
else
# For non-grep commands, only exit code 0 is success
if [ $exit_code -eq 0 ]; then
if [ -n "$expected_pattern" ] && echo "$output" | grep -qE "$expected_pattern"; then
gum style --foreground 10 " βœ… Method works (pattern found)"
set_debug_result "${tool_name}_update_check" "βœ… Works"
else
gum style --foreground 10 " βœ… Method works"
set_debug_result "${tool_name}_update_check" "βœ… Works (pattern not verified)"
fi
set_debug_result "${tool_name}_update_check_output" "$output"
return 0
else
gum style --foreground 3 " ⚠️ Method fails"
set_debug_result "${tool_name}_update_check" "❌ Fails"
set_debug_result "${tool_name}_update_check_output" "$output"
return 1
fi
fi
}
# Function to verify update procedure (without executing it)
# Usage: verify_update_method "tool_name" "update_command" "dry_run_flag"
verify_update_method() {
local tool_name="$1"
local update_cmd="$2"
local dry_run_flag="${3:-}"
echo ""
gum style --bold --foreground 212 "πŸ” Verification: Update Procedure - $tool_name"
echo " Command: $update_cmd"
if [ -n "$dry_run_flag" ]; then
echo " Dry-run mode enabled: $dry_run_flag"
local test_cmd="$update_cmd $dry_run_flag"
local output
local exit_code
output=$(eval "$test_cmd" 2>&1)
exit_code=$?
echo " Exit code: $exit_code"
echo " Output (first 500 characters):"
echo "$output" | head -20 | sed 's/^/ /'
if [ $exit_code -eq 0 ]; then
gum style --foreground 10 " βœ… Update command valid (dry-run)"
set_debug_result "${tool_name}_update_method" "βœ… Valid (dry-run)"
return 0
else
gum style --foreground 3 " ⚠️ Update command invalid"
set_debug_result "${tool_name}_update_method" "❌ Invalid"
return 1
fi
else
echo " ⚠️ Dry-run mode not available - syntax check only"
set_debug_result "${tool_name}_update_method" "⚠️ Not tested (no dry-run)"
return 0
fi
}
# Function to verify displayed information
# Usage: verify_displayed_info "tool_name" "info_to_check" "expected_value"
verify_displayed_info() {
local tool_name="$1"
local info_name="$2"
local expected_value="${3:-}"
echo ""
gum style --bold --foreground 212 "πŸ” Verification: Displayed Information - $tool_name"
echo " Information: $info_name"
[ -n "$expected_value" ] && echo " Expected value: $expected_value"
set_debug_result "${tool_name}_display_${info_name}" "Verified"
return 0
}
# Main verification function for a tool
# Usage: verify_tool "tool_name" "presence_cmd" "version_cmd" "fallback_version_cmd" "update_check_cmd" "update_cmd" "dry_run_flag"
verify_tool() {
local tool_name="$1"
local presence_cmd="$2"
local version_cmd="$3"
local fallback_version_cmd="${4:-}"
local update_check_cmd="${5:-}"
local update_cmd="${6:-}"
local dry_run_flag="${7:-}"
gum style --border rounded --border-foreground 57 --padding "1 2" --margin "1 0" \
"$(gum style --bold --foreground 57 "πŸ” Complete Verification: $tool_name")"
# Verify presence
verify_presence_detection "$tool_name" "$presence_cmd"
local presence_result=$?
# If present, verify version
if [ $presence_result -eq 0 ]; then
verify_version_detection "$tool_name" "$version_cmd" "$fallback_version_cmd"
# Verify update check method if provided
if [ -n "$update_check_cmd" ]; then
verify_update_check_method "$tool_name" "$update_check_cmd"
else
set_debug_result "${tool_name}_update_check" "⚠️ Not verified (command not provided)"
fi
# Verify update procedure if provided
if [ -n "$update_cmd" ]; then
verify_update_method "$tool_name" "$update_cmd" "$dry_run_flag"
else
set_debug_result "${tool_name}_update_method" "⚠️ Not verified (command not provided)"
fi
else
# Tool not present - mark other verifications as not applicable
set_debug_result "${tool_name}_version" "N/A (tool not installed)"
set_debug_result "${tool_name}_update_check" "N/A (tool not installed)"
set_debug_result "${tool_name}_update_method" "N/A (tool not installed)"
fi
echo ""
}
# Function to generate verification report
generate_verification_report() {
echo ""
gum style --border double --border-foreground 63 --foreground 63 --padding "1 2" \
"πŸ“‹ Detailed Verification Report"
local report_file="${HOME}/.macbook-updates-verification-$(date +%Y%m%d-%H%M%S).txt"
{
echo "=================================================================================="
echo " VERIFICATION REPORT - MacBook Update Suite v${VERSION}"
echo "=================================================================================="
echo "Date: $(date)"
echo "macOS: $(sw_vers -productVersion 2>/dev/null || echo 'unknown')"
echo "Hostname: $(hostname -s 2>/dev/null || echo 'unknown')"
echo ""
echo "=================================================================================="
echo " SUMMARY BY TOOL"
echo "=================================================================================="
echo ""
# Extract unique tool names from keys (handle keys with special chars)
# Use a temporary file from the start to preserve spaces in tool names
local processed_tools_file="${TMPDIR:-/tmp}/tools-list-$$.txt"
> "$processed_tools_file"
for key in $DEBUG_RESULTS_KEYS; do
# Extract tool name (everything before first underscore)
tool_name="${key%%_*}"
[ -z "$tool_name" ] && continue
# Map internal names to display names
case "$tool_name" in
"XcodeCLT") tool_display="Xcode CLT" ;;
"CLT") continue ;; # Skip standalone CLT (will be handled by XcodeCLT mapping)
*) tool_display="$tool_name" ;;
esac
# Add to file if not already present (one per line, preserves spaces)
if ! grep -Fxq "$tool_display" "$processed_tools_file" 2>/dev/null; then
echo "$tool_display" >> "$processed_tools_file"
fi
done
# Remove "CLT" if "Xcode CLT" exists (avoid duplicate)
if grep -Fxq "Xcode CLT" "$processed_tools_file" 2>/dev/null; then
grep -vFx "CLT" "$processed_tools_file" > "${processed_tools_file}.tmp" 2>/dev/null && mv "${processed_tools_file}.tmp" "$processed_tools_file"
fi
# Generate report for each tool (read from file to preserve spaces)
sort -u "$processed_tools_file" | while IFS= read -r tool; do
[ -z "$tool" ] && continue
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "TOOL: $tool"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Map display name back to internal key prefix for lookup
internal_tool="$tool"
case "$tool" in
"Xcode CLT") internal_tool="XcodeCLT" ;;
esac
# Presence - ALWAYS display if value exists (even if empty, show "Not detected")
local presence_val=$(get_debug_result "${internal_tool}_presence")
local tool_not_detected=false
if [ -n "$presence_val" ] && [ "$presence_val" != "${internal_tool}_presence" ] && [ "$presence_val" != "(empty)" ]; then
echo " βœ“ Presence detection: $presence_val"
if [ "$presence_val" = "❌ Not detected" ]; then
tool_not_detected=true
fi
local presence_output=$(get_debug_result "${internal_tool}_presence_output")
if [ -n "$presence_output" ] && [ "$presence_output" != "${internal_tool}_presence_output" ] && [ "$presence_output" != "(empty)" ]; then
echo " Output: $presence_output"
fi
elif [ -z "$presence_val" ] || [ "$presence_val" = "${internal_tool}_presence" ] || [ "$presence_val" = "(empty)" ]; then
# If no presence value, check if tool was verified but not detected
echo " βœ“ Presence detection: ❌ Not detected"
tool_not_detected=true
fi
# If tool is not detected, skip all other sections
if [ "$tool_not_detected" = true ]; then
continue
fi
# Version - ALWAYS display if value exists
local version_val=$(get_debug_result "${internal_tool}_version")
if [ -n "$version_val" ] && [ "$version_val" != "${internal_tool}_version" ] && [ "$version_val" != "(empty)" ]; then
echo " βœ“ Version detected: $version_val"
local version_method=$(get_debug_result "${internal_tool}_version_method")
if [ -n "$version_method" ] && [ "$version_method" != "${internal_tool}_version_method" ] && [ "$version_method" != "(empty)" ]; then
echo " Method: $version_method"
fi
fi
# Update check - ALWAYS display if value exists
local update_check_val=$(get_debug_result "${internal_tool}_update_check")
if [ -n "$update_check_val" ] && [ "$update_check_val" != "${internal_tool}_update_check" ] && [ "$update_check_val" != "(empty)" ]; then
echo " βœ“ Update check: $update_check_val"
local update_check_output=$(get_debug_result "${internal_tool}_update_check_output")
if [ -n "$update_check_output" ] && [ "$update_check_output" != "${internal_tool}_update_check_output" ] && [ "$update_check_output" != "(empty)" ]; then
local output_preview=$(echo "$update_check_output" | head -5 | sed 's/^/ /')
echo " Output (preview):"
echo "$output_preview"
fi
fi
# Update method - ALWAYS display if value exists
local update_method_val=$(get_debug_result "${internal_tool}_update_method")
if [ -n "$update_method_val" ] && [ "$update_method_val" != "${internal_tool}_update_method" ] && [ "$update_method_val" != "(empty)" ]; then
echo " βœ“ Update method: $update_method_val"
fi
echo ""
done
# Cleanup temp file
[ -f "$processed_tools_file" ] && rm -f "$processed_tools_file"
# Cleanup temp file
[ -f "$processed_tools_file" ] && rm -f "$processed_tools_file"
echo "=================================================================================="
echo " RAW DATA (for advanced debugging)"
echo "=================================================================================="
echo ""
# Sort keys and display with values
for key in $(echo "$DEBUG_RESULTS_KEYS" | tr ' ' '\n' | grep -v '^$' | sort -u); do
local value=$(get_debug_result "$key")
# Display all entries, including empty ones for debugging
if [ -n "$value" ]; then
echo "$key: $value"
else
# Show empty values too for debugging
echo "$key: (empty)"
fi
done
echo ""
echo "=================================================================================="
echo " END OF REPORT"
echo "=================================================================================="
} | tee "$report_file"
echo ""
gum style --foreground 10 "βœ… Report saved: $report_file"
echo ""
gum style --foreground 240 "πŸ’‘ Use this report to verify that all detections work correctly."
}
# Helper function to initialize Node.js environment for verification
init_node_env_for_verification() {
# Try fnm first
if command -v fnm &> /dev/null; then
eval "$(fnm env 2>/dev/null)" || eval "$(fnm env --use-on-cd 2>/dev/null)" || true
fi
# Then try nvm
if [ -s "$HOME/.nvm/nvm.sh" ]; then
source "$HOME/.nvm/nvm.sh" 2>/dev/null || true
elif [ -s "/usr/local/opt/nvm/nvm.sh" ]; then
source "/usr/local/opt/nvm/nvm.sh" 2>/dev/null || true
fi
}
# Helper function to initialize Python environment for verification
init_python_env_for_verification() {
if command -v pyenv &> /dev/null; then
eval "$(pyenv init - bash 2>/dev/null)" || eval "$(pyenv init - zsh 2>/dev/null)" || true
fi
}
# Helper function to initialize Ruby environment for verification
init_ruby_env_for_verification() {
if command -v rbenv &> /dev/null; then
eval "$(rbenv init - bash 2>/dev/null)" || eval "$(rbenv init - zsh 2>/dev/null)" || true
fi
}
# Function to run all verifications
run_all_verifications() {
# Initialize debug results file
DEBUG_RESULTS_FILE="${TMPDIR:-/tmp}/macbook-updates-debug-$$.txt"
DEBUG_RESULTS_KEYS=""
> "$DEBUG_RESULTS_FILE" # Create empty file
echo ""
gum style --border double --border-foreground 212 --foreground 212 --padding "1 2" \
"πŸ” DEBUG & VERIFICATION MODE"
echo ""
gum style --foreground 240 "This mode verifies all aspects of the script:"
echo " β€’ Tool presence detection"
echo " β€’ Version detection"
echo " β€’ Update check methods"
echo " β€’ Update procedures"
echo " β€’ Displayed information"
echo ""
# Check for MAMP Python alias BEFORE initializing pyenv (to preserve MAMP detection)
# Store original python3 path if it's MAMP
ORIGINAL_PYTHON3_PATH=""
if alias python3 2>/dev/null | grep -q MAMP || [ -f "/Applications/MAMP/bin/python/python3/bin/python3" ]; then
ORIGINAL_PYTHON3_PATH="/Applications/MAMP/bin/python/python3/bin/python3"
fi
# Pre-initialize version managers to detect versions actually in use
# This ensures we detect the versions the user is actually using, not just system versions
init_node_env_for_verification
# Only initialize pyenv if MAMP Python is not being used
if [ -z "$ORIGINAL_PYTHON3_PATH" ]; then
init_python_env_for_verification
fi
init_ruby_env_for_verification
# macOS System Updates
verify_tool "macOS" \
"command -v softwareupdate &> /dev/null" \
"sw_vers -productVersion 2>/dev/null" \
"" \
"softwareupdate -l 2>&1" \
"softwareupdate -i -a" \
""
# Xcode Command Line Tools (use name without space for easier parsing)
verify_tool "XcodeCLT" \
"xcode-select -p &> /dev/null 2>&1" \
"pkgutil --pkg-info=com.apple.pkg.CLTools_Executables 2>/dev/null | grep '^version:' | awk '{print \$2}' || pkgutil --pkg-info=com.apple.pkg.CLTools_Executables 2>/dev/null | grep 'version' | awk '{print \$2}' || pkgutil --pkg-info \$(pkgutil --pkgs 2>/dev/null | grep CLTools | head -1) 2>/dev/null | grep 'version' | awk '{print \$2}'" \
"" \
"softwareupdate -l 2>&1 | grep -i 'command line tools' || echo 'No CLT updates available'" \
"softwareupdate --install --all" \
""
# Xcode (full IDE) - check if installed
verify_tool "Xcode" \
"[ -d \"/Applications/Xcode.app\" ]" \
"xcodebuild -version 2>/dev/null | head -1 | awk '{print \$2}'" \
"" \
"mas outdated 2>&1 | grep -i xcode || softwareupdate -l 2>&1 | grep -i xcode || echo 'Check available via: App Store or softwareupdate'" \
"mas upgrade Xcode || softwareupdate --install --all" \
""
# Homebrew
verify_tool "Homebrew" \
"command -v brew &> /dev/null" \
"brew --version 2>/dev/null | head -1 | awk '{print \$2}'" \
"" \
"brew outdated 2>&1" \
"brew upgrade" \
""
# Git
verify_tool "Git" \
"command -v git &> /dev/null" \
"git --version 2>/dev/null | sed 's/git version //' | sed 's/\"//g'" \
"" \
"brew outdated 2>&1 | grep -E '^git$'" \
"brew upgrade git" \
""
# Node.js (FNM)
init_node_env_for_verification
if command -v fnm &> /dev/null; then
verify_tool "Node.js (FNM)" \
"command -v node &> /dev/null" \
"fnm current 2>/dev/null | sed 's/v//'" \
"node --version 2>/dev/null | sed 's/v//'" \
"fnm list-remote --lts 2>&1 | tail -1 | awk '{print \$1}' | sed 's/v//'" \
"fnm install --lts" \
""
else
# FNM not installed - still create entry
verify_tool "(FNM)" \
"command -v fnm &> /dev/null" \
"" \
"" \
"" \
"" \
""
fi
# Node.js (NVM)
init_node_env_for_verification
if [ -s "$HOME/.nvm/nvm.sh" ] || [ -s "/usr/local/opt/nvm/nvm.sh" ]; then
verify_tool "Node.js (NVM)" \
"command -v node &> /dev/null" \
"nvm current 2>/dev/null | sed 's/v//'" \
"node --version 2>/dev/null | sed 's/v//'" \
"nvm ls-remote --lts 2>&1 | tail -1 | awk '{print \$1}' | sed 's/v//'" \
"nvm install --lts" \
""
else
# NVM not installed - still create entry
verify_tool "(NVM)" \
"[ -s \"\$HOME/.nvm/nvm.sh\" ] || [ -s \"/usr/local/opt/nvm/nvm.sh\" ]" \
"" \
"" \
"" \
"" \
""
fi
# Node.js (system/other) - check after trying version managers
init_node_env_for_verification
if command -v node &> /dev/null && ! command -v fnm &> /dev/null && [ ! -s "$HOME/.nvm/nvm.sh" ] && [ ! -s "/usr/local/opt/nvm/nvm.sh" ]; then
verify_tool "Node.js (system)" \
"command -v node &> /dev/null" \
"node --version 2>/dev/null | sed 's/v//'" \
"" \
"" \
"" \
""
else
# Node.js not installed or managed by version manager - still check
init_node_env_for_verification
verify_tool "Node.js" \
"command -v node &> /dev/null" \
"node --version 2>/dev/null | sed 's/v//'" \
"" \
"" \
"" \
""
fi
# Bun
verify_tool "Bun" \
"command -v bun &> /dev/null" \
"bun --version 2>/dev/null" \
"" \
"" \
"bun upgrade" \
""
# Python (pyenv)
if command -v pyenv &> /dev/null; then
init_python_env_for_verification
verify_tool "Python (pyenv)" \
"command -v python3 &> /dev/null" \
"pyenv global 2>/dev/null" \
"python3 --version 2>&1 | sed 's/Python //'" \
"pyenv install --list 2>&1 | grep -E '^\s*3\.[0-9]+\.[0-9]+$' | tail -1 | tr -d '[:space:]'" \
"pyenv install" \
""
else
# pyenv not installed - still create entry
verify_tool "(pyenv)" \
"command -v pyenv &> /dev/null" \
"" \
"" \
"" \
"" \
""
fi
# Python (MAMP)
if is_mamp_python || (command -v python3 &> /dev/null && which python3 2>/dev/null | grep -q MAMP); then
verify_tool "Python (MAMP)" \
"command -v python3 &> /dev/null && (is_mamp_python || which python3 2>/dev/null | grep -q MAMP)" \
"python3 --version 2>&1 | sed 's/Python //'" \
"" \
"" \
"" \
""
fi
# Python - check actual version in use
# Use MAMP Python if it was detected before pyenv initialization
if [ -n "$ORIGINAL_PYTHON3_PATH" ] && [ -f "$ORIGINAL_PYTHON3_PATH" ]; then
# MAMP Python is being used
verify_tool "Python" \
"command -v python3 &> /dev/null || [ -f \"$ORIGINAL_PYTHON3_PATH\" ]" \
"$ORIGINAL_PYTHON3_PATH --version 2>&1 | sed 's/Python //'" \
"" \
"" \
"" \
""
elif command -v python3 &> /dev/null; then
# Use whatever python3 is in PATH (might be pyenv or system)
verify_tool "Python" \
"command -v python3 &> /dev/null" \
"python3 --version 2>&1 | sed 's/Python //'" \
"" \
"" \
"" \
""
else
# Python not installed - still create entry
verify_tool "Python" \
"command -v python3 &> /dev/null" \
"" \
"" \
"" \
"" \
""
fi
# Ruby (rbenv)
if command -v rbenv &> /dev/null; then
init_ruby_env_for_verification
verify_tool "Ruby (rbenv)" \
"command -v ruby &> /dev/null" \
"rbenv global 2>/dev/null" \
"ruby --version 2>/dev/null | awk '{print \$2}'" \
"rbenv install -l 2>&1 | grep -E '^\s*[0-9]+\.[0-9]+\.[0-9]+$' | grep -vE '(dev|pre|rc|alpha|beta)' | tail -1 | tr -d '[:space:]'" \
"rbenv install" \
""
else
# rbenv not installed - still create entry
verify_tool "(rbenv)" \
"command -v rbenv &> /dev/null" \
"" \
"" \
"" \
"" \
""
fi
# Ruby - check actual version in use (respects PATH and version managers)
# Use the actual ruby command as it appears in the shell environment
if command -v ruby &> /dev/null; then
# Use ruby directly to get the version actually being used
# This respects PATH order and any version managers already initialized
verify_tool "Ruby" \
"command -v ruby &> /dev/null" \
"ruby --version 2>/dev/null | awk '{print \$2}'" \
"" \
"" \
"" \
""
else
# Ruby not installed - still create entry
verify_tool "Ruby" \
"command -v ruby &> /dev/null" \
"" \
"" \
"" \
"" \
""
fi
# Rust
verify_tool "Rust" \
"command -v rustup &> /dev/null" \
"(rustup show 2>/dev/null 2>&1 | grep -q 'no active toolchain' && echo 'No toolchain installed') || rustup show active-toolchain 2>/dev/null | awk '{print \$1}' | sed 's/-.*//' || (rustup show 2>/dev/null | grep -A 10 'installed toolchains' | grep -E 'stable|beta|nightly' | head -1 | awk '{print \$1}' | sed 's/-.*//') || rustc --version 2>/dev/null | sed 's/rustc //' | awk '{print \$1}'" \
"rustc --version 2>/dev/null | sed 's/rustc //' | awk '{print \$1}'" \
"rustup check 2>&1" \
"rustup update" \
""
# Go
verify_tool "Go" \
"command -v go &> /dev/null" \
"go version 2>/dev/null | awk '{print \$3}' | sed 's/go//'" \
"" \
"brew outdated 2>&1 | grep -E '^go$'" \
"brew upgrade go" \
""
# Java (SDKMAN)
if [ -d "$HOME/.sdkman" ]; then
if [ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]; then
source "$HOME/.sdkman/bin/sdkman-init.sh" 2>/dev/null || true
fi
verify_tool "Java (SDKMAN)" \
"command -v java &> /dev/null" \
"sdk current java 2>/dev/null | tail -1" \
"java -version 2>&1 | head -1 | awk -F'\"' '{print \$2}'" \
"sdk list java 2>&1 | grep -i 'installed' || echo 'Check available via: sdk list java'" \
"sdk upgrade java" \
""
else
# SDKMAN not installed - still create entry
verify_tool "SDK" \
"[ -d \"\$HOME/.sdkman\" ]" \
"" \
"" \
"" \
"" \
""
fi
# Java (Homebrew) - only if SDKMAN is not present
if command -v java &> /dev/null && [ ! -d "$HOME/.sdkman" ]; then
if command -v brew &> /dev/null; then
brew_java=$(brew list --formula 2>/dev/null | grep -E "openjdk|java" | head -1)
if [ -n "$brew_java" ]; then
verify_tool "Java (Homebrew)" \
"command -v java &> /dev/null" \
"java -version 2>&1 | head -1 | awk -F'\"' '{print \$2}'" \
"" \
"brew outdated 2>&1 | grep -E '^${brew_java}$'" \
"brew upgrade ${brew_java}" \
""
fi
fi
fi
# .NET
verify_tool ".NET" \
"command -v dotnet &> /dev/null" \
"dotnet --version 2>/dev/null" \
"" \
"dotnet sdk check 2>&1 || dotnet --list-sdks 2>&1 | tail -1 || echo 'Check available via: dotnet sdk check'" \
"dotnet tool update -g" \
""
# Flutter
verify_tool "Flutter" \
"command -v flutter &> /dev/null" \
"flutter --version 2>/dev/null | head -1 | awk '{print \$2}'" \
"" \
"flutter channel 2>&1 && flutter --version 2>&1 | head -3 || echo 'Check available via: flutter upgrade'" \
"flutter upgrade" \
""
# Android SDK
if [ -d "$HOME/Library/Android/sdk" ]; then
local sdkmanager_bin="$HOME/Library/Android/sdk/cmdline-tools/latest/bin/sdkmanager"
[ ! -f "$sdkmanager_bin" ] && sdkmanager_bin="$HOME/Library/Android/sdk/tools/bin/sdkmanager"
if [ -f "$sdkmanager_bin" ] && [ -x "$sdkmanager_bin" ]; then
verify_tool "Android SDK" \
"[ -f \"$sdkmanager_bin\" ] && [ -x \"$sdkmanager_bin\" ]" \
"$sdkmanager_bin --version 2>/dev/null | head -1" \
"" \
"$sdkmanager_bin --list 2>&1 | grep -A 100 'Installed packages' | head -30" \
"$sdkmanager_bin --update" \
""
else
# Android SDK directory exists but sdkmanager not found
verify_tool "Android" \
"[ -d \"\$HOME/Library/Android/sdk\" ]" \
"" \
"" \
"" \
"" \
""
fi
else
# Android SDK not installed - still create entry
verify_tool "Android" \
"[ -d \"\$HOME/Library/Android/sdk\" ]" \
"" \
"" \
"" \
"" \
""
fi
# Composer
verify_tool "Composer" \
"command -v composer &> /dev/null" \
"composer --version 2>/dev/null | awk '{print \$3}'" \
"" \
"composer self-update --check 2>&1 || composer self-update --dry-run 2>&1 || curl -s https://api.github.com/repos/composer/composer/releases/latest 2>/dev/null | grep 'tag_name' | head -1 | sed 's/.*tag_name\": \"v\\(.*\\)\".*/\\1/' || echo 'Check available via: composer self-update'" \
"composer self-update" \
""
# Docker
verify_tool "Docker" \
"command -v docker &> /dev/null" \
"docker --version 2>/dev/null | sed 's/Docker version //' | sed 's/, build.*//'" \
"" \
"docker desktop update --check-only 2>&1 | grep -v 'not running' || (docker info &>/dev/null && docker desktop update --check-only 2>&1) || echo 'Docker Desktop update check available (requires Docker Desktop running)'" \
"docker desktop update -q" \
""
# kubectl
verify_tool "kubectl" \
"command -v kubectl &> /dev/null" \
"kubectl version --client --short 2>/dev/null | sed 's/.*v/v/' | awk '{print \$1}' | sed 's/v//' || kubectl version --client 2>/dev/null | grep 'Client Version' | awk '{print \$3}' | sed 's/\"//g' | sed 's/v//'" \
"kubectl version --client 2>/dev/null | grep 'Client Version' | awk '{print \$3}' | sed 's/\"//g' | sed 's/v//'" \
"brew outdated 2>&1 | grep -E '^kubectl$'" \
"brew upgrade kubectl" \
""
# Terraform
verify_tool "Terraform" \
"command -v terraform &> /dev/null" \
"terraform version 2>/dev/null | head -1 | awk '{print \$2}' | sed 's/v//'" \
"" \
"brew outdated 2>&1 | grep -E '^terraform$' || (brew tap hashicorp/tap 2>/dev/null && brew outdated hashicorp/tap/terraform 2>&1) || echo 'Note: Terraform updates via HashiCorp tap: brew tap hashicorp/tap && brew install hashicorp/tap/terraform'" \
"brew upgrade terraform || (brew tap hashicorp/tap && brew install hashicorp/tap/terraform)" \
""
# npm (requires Node.js to be initialized)
init_node_env_for_verification
if command -v node &> /dev/null; then
verify_tool "npm" \
"command -v npm &> /dev/null" \
"npm --version 2>/dev/null" \
"" \
"npm outdated -g --depth=0 2>&1" \
"npm update -g" \
""
fi
# Yarn (requires Node.js)
init_node_env_for_verification
if command -v node &> /dev/null; then
verify_tool "Yarn" \
"command -v yarn &> /dev/null" \
"yarn --version 2>/dev/null" \
"" \
"" \
"corepack prepare yarn@stable --activate" \
""
fi
# pnpm (requires Node.js)
init_node_env_for_verification
if command -v node &> /dev/null; then
verify_tool "pnpm" \
"command -v pnpm &> /dev/null" \
"pnpm --version 2>/dev/null" \
"" \
"" \
"pnpm update -g" \
""
fi
# pip
verify_tool "pip" \
"command -v pip3 &> /dev/null" \
"pip3 --version 2>/dev/null | awk '{print \$2}'" \
"" \
"pip3 list --outdated 2>&1" \
"pip3 install -U" \
""
# Generate final report
generate_verification_report
}
# =============================================================================
# MAIN MENU
# =============================================================================
# Support for command-line argument (before menu)
if [ "$1" = "--debug" ] || [ "$1" = "-d" ] || [ "$1" = "--verify" ] || [ "$1" = "-v" ]; then
UPDATE_MODE="debug"
run_all_verifications
exit 0
fi
show_header
gum style --foreground 240 "This script checks and updates (when you confirm):"
echo " 🍎 macOS system updates & Xcode Command Line Tools"
echo " 🍺 Homebrew (formulae & casks) πŸ“Œ Git"
echo " 🟒 Node.js (fnm / nvm), npm, Yarn 4+, 🍞 Bun"
echo " 🐍 Python (pyenv / MAMP), pip πŸ’Ž Ruby πŸ¦€ Rust 🐹 Go"
echo " β˜• Java πŸ”· .NET πŸ¦‹ Flutter πŸ“± Android SDK"
echo " 🐘 Composer (PHP) 🐳 Docker ⎈ kubectl πŸ“ Terraform"
echo " πŸ“¦ pnpm global packages 🧹 Optional cleanup (caches)"
mode_choice=$(gum choose --height 4 --cursor "β†’ " \
"πŸš€ Full Update β€” Update everything automatically without prompts" \
"βš™οΈ Custom Selection β€” Choose which sections to update interactively" \
"πŸ” Debug & Verification β€” Verify detection, versions, and update methods" \
"❌ Exit β€” Exit without making any changes")
case "$mode_choice" in
"πŸš€ Full Update β€” Update everything automatically without prompts")
UPDATE_MODE="full"
echo ""
gum style --foreground 10 "βœ“ Full update mode selected"
;;
"βš™οΈ Custom Selection β€” Choose which sections to update interactively")
UPDATE_MODE="custom"
echo ""
gum style --foreground 10 "βœ“ Custom mode selected"
;;
"πŸ” Debug & Verification β€” Verify detection, versions, and update methods")
UPDATE_MODE="debug"
echo ""
gum style --foreground 212 "βœ“ Debug & Verification mode selected"
run_all_verifications
gum style --foreground 240 "πŸ‘‹ Goodbye!"
exit 0
;;
"❌ Exit β€” Exit without making any changes")
gum style --foreground 240 "πŸ‘‹ Goodbye!"
exit 0
;;
*)
# Empty or unknown choice (could be ESC)
gum style --foreground 240 "πŸ‘‹ Goodbye!"
exit 0
;;
esac
# =============================================================================
# UPDATE SECTIONS
# =============================================================================
# 🍎 macOS System Updates
show_section_header "🍎 macOS System Updates"
if confirm_section_check "🍎 macOS System Updates"; then
softwareupdate_output=$(run_task_capture "Checking macOS updates" "softwareupdate -l 2>&1")
softwareupdate_exit_code=$?
if [ $softwareupdate_exit_code -ne 0 ]; then
log_error "Failed to check macOS updates (exit code: $softwareupdate_exit_code)"
gum style --foreground 3 " ⚠️ Could not check for macOS updates. Please check your network connection."
report_section "macOS" "β€”" "Check failed"
else
softwareupdate_list=$(echo "$softwareupdate_output" | grep -E "^\*" | sed 's/^[[:space:]]*\*[[:space:]]*//')
if [ -z "$softwareupdate_list" ]; then
gum style --foreground 10 " βœ“ macOS: Already up to date"
report_section "macOS" "β€”" "Already up to date"
else
gum style --foreground 3 " ⚠️ Available macOS updates:"
echo "$softwareupdate_list"
if confirm "Do you want to install macOS updates?"; then
if execute_command "Installing macOS updates" "softwareupdate -i -a" "Failed to install macOS updates"; then
gum style --foreground 10 " βœ“ macOS: Updated"
report_section "macOS" "β€”" "Updated"
else
gum style --foreground 1 " ❌ macOS: Update failed. Check logs: $LOG_FILE"
report_section "macOS" "β€”" "Update failed"
fi
else
gum style --foreground 3 " ⚠ macOS: Updates available"
report_section "macOS" "β€”" "Updates available"
fi
fi
fi
else
gum style --foreground 240 " ⏭ macOS: Skipped"
report_section "macOS" "β€”" "Skipped"
fi
# πŸ› οΈ Xcode Command Line Tools
show_section_header "πŸ› οΈ Xcode Command Line Tools"
if confirm_section_check "πŸ› οΈ Xcode Command Line Tools"; then
if run_task "Checking Xcode CLT" "xcode-select -p &> /dev/null"; then
current_version=$(extract_version "pkgutil --pkg-info=com.apple.pkg.CLTools_Executables 2>/dev/null | grep version | awk '{print \$2}'")
if validate_version "$current_version"; then
gum style --foreground 2 " βœ… Command Line Tools installed (version: $current_version)"
else
current_version="installed"
gum style --foreground 2 " βœ… Command Line Tools installed"
fi
# Check for updates using softwareupdate
clt_check_output=$(run_task_capture "Checking Xcode CLT updates" "softwareupdate -l 2>&1")
clt_check_exit_code=$?
if [ $clt_check_exit_code -eq 0 ]; then
clt_updates=$(echo "$clt_check_output" | grep -i "command line tools")
if [ -n "$clt_updates" ]; then
if confirm "Update Command Line Tools?"; then
if execute_command "Updating Command Line Tools" "softwareupdate --install --all" "Failed to update Command Line Tools"; then
new_version=$(extract_version "pkgutil --pkg-info=com.apple.pkg.CLTools_Executables 2>/dev/null | grep version | awk '{print \$2}'")
if validate_version "$new_version" && [ "$new_version" != "$current_version" ]; then
gum style --foreground 10 " βœ“ Xcode CLT: Updated ($current_version β†’ $new_version)"
report_section "Xcode CLT" "$current_version" "$new_version"
else
gum style --foreground 10 " βœ“ Xcode CLT: Updated"
report_section "Xcode CLT" "$current_version" "Updated"
fi
else
gum style --foreground 1 " ❌ Xcode CLT: Update failed. Check logs: $LOG_FILE"
report_section "Xcode CLT" "$current_version" "Update failed"
fi
else
gum style --foreground 3 " ⚠ Xcode CLT: Outdated"
report_section "Xcode CLT" "$current_version" "Outdated"
fi
else
gum style --foreground 10 " βœ“ Xcode CLT: Already up to date ($current_version)"
report_section "Xcode CLT" "$current_version" "Already up to date"
fi
else
log_error "Failed to check for Xcode CLT updates"
gum style --foreground 3 " ⚠️ Could not check for Xcode CLT updates"
report_section "Xcode CLT" "$current_version" "Check failed"
fi
else
gum style --foreground 3 " ⚠️ Command Line Tools not installed"
if confirm "Install Command Line Tools?"; then
if execute_command "Installing Command Line Tools" "xcode-select --install" "Failed to initiate Command Line Tools installation"; then
gum style --foreground 2 " βœ… Command Line Tools installation initiated. Please complete the installation in the popup window."
report_section "Xcode CLT" "β€”" "Installation initiated"
else
gum style --foreground 1 " ❌ Failed to initiate Command Line Tools installation"
report_section "Xcode CLT" "β€”" "Installation failed"
fi
else
gum style --foreground 240 " ⏭ Xcode CLT: Skipped"
report_section "Xcode CLT" "β€”" "Skipped"
fi
fi
else
gum style --foreground 240 " ⏭ Xcode CLT: Skipped"
report_section "Xcode CLT" "β€”" "Skipped"
fi
# 🍺 Homebrew
show_section_header "🍺 Homebrew"
if confirm_section_check "🍺 Homebrew"; then
if run_task "Checking Homebrew" "command -v brew &> /dev/null"; then
# Suppress Homebrew hints
export HOMEBREW_NO_ENV_HINTS=1
export HOMEBREW_NO_INSTALL_CLEANUP=1
# Capture brew update output with timeout
brew_update_output=$(run_task_capture "Updating Homebrew" "brew update 2>&1")
brew_update_exit_code=$?
brew_was_updated=false
if [ $brew_update_exit_code -eq 0 ]; then
if echo "$brew_update_output" | grep -qE "^Updated|^==> Updated"; then
brew_was_updated=true
fi
else
log_error "Homebrew update failed (exit code: $brew_update_exit_code)"
fi
brew_outdated=$(run_task_capture "Checking outdated Homebrew packages" "brew outdated 2>&1")
brew_outdated_exit_code=$?
if [ $brew_outdated_exit_code -ne 0 ]; then
log_error "Failed to check for outdated Homebrew packages"
gum style --foreground 3 " ⚠️ Could not check for outdated packages"
report_section "Homebrew" "β€”" "Check failed"
elif [ -z "$brew_outdated" ]; then
if [ "$brew_was_updated" = true ]; then
gum style --foreground 10 " βœ“ Homebrew: Updated (taps/formulae)"
report_section "Homebrew" "β€”" "Updated"
else
gum style --foreground 10 " βœ“ Homebrew: Already up to date"
report_section "Homebrew" "β€”" "Already up to date"
fi
else
gum style --foreground 3 " ⚠️ Outdated brew packages:"
echo "$brew_outdated"
if confirm "Do you want to upgrade all brew packages?"; then
if execute_with_timeout "Upgrading Homebrew packages" "brew upgrade" 1800 "Homebrew upgrade timed out"; then
gum style --foreground 10 " βœ“ Homebrew: Updated"
report_section "Homebrew" "β€”" "Updated"
else
gum style --foreground 1 " ❌ Homebrew: Upgrade failed. Check logs: $LOG_FILE"
report_section "Homebrew" "β€”" "Upgrade failed"
fi
else
gum style --foreground 3 " ⚠ Homebrew: Outdated"
report_section "Homebrew" "β€”" "Outdated"
fi
fi
else
gum style --foreground 3 " ⚠️ Homebrew not installed"
if confirm "Install Homebrew?"; then
if execute_with_timeout "Installing Homebrew" "/bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" 600 "Homebrew installation timed out"; then
gum style --foreground 2 " βœ… Homebrew installed. Please restart your terminal or run 'source ~/.zshrc' to use brew commands."
report_section "Homebrew" "β€”" "Installed"
else
gum style --foreground 1 " ❌ Homebrew installation failed. Check logs: $LOG_FILE"
report_section "Homebrew" "β€”" "Installation failed"
fi
else
gum style --foreground 240 " ⏭ Homebrew: Skipped"
report_section "Homebrew" "β€”" "Skipped"
fi
fi
else
gum style --foreground 240 " ⏭ Homebrew: Skipped"
report_section "Homebrew" "β€”" "Skipped"
fi
# πŸ“Œ Git
show_section_header "πŸ“Œ Git"
if confirm_section_check "πŸ“Œ Git"; then
if run_task "Checking Git" "command -v git &> /dev/null"; then
git_version=$(extract_version "git --version 2>/dev/null | head -1 | sed 's/git version //' | sed 's/\"//g'")
if validate_version "$git_version"; then
gum style --foreground 2 " βœ… Git version: $git_version"
else
git_version=$(git --version 2>/dev/null | head -1)
gum style --foreground 2 " βœ… $git_version"
git_version="installed"
fi
if command -v brew &> /dev/null; then
brew_list_git=$(brew list git 2>/dev/null)
if [ -n "$brew_list_git" ]; then
brew_outdated_git=$(run_task_capture "Checking Git updates" "brew outdated 2>&1" | grep -E "^git$")
if [ -n "$brew_outdated_git" ]; then
if confirm "Update Git via Homebrew?"; then
if execute_command "Updating Git" "brew upgrade git" "Failed to update Git"; then
git_version_new=$(extract_version "git --version 2>/dev/null | head -1 | sed 's/git version //' | sed 's/\"//g'")
if validate_version "$git_version_new" && [ "$git_version_new" != "$git_version" ]; then
gum style --foreground 10 " βœ“ Git: Updated ($git_version β†’ $git_version_new)"
report_section "Git" "$git_version" "$git_version_new"
else
gum style --foreground 10 " βœ“ Git: Updated"
report_section "Git" "$git_version" "Updated"
fi
else
gum style --foreground 1 " ❌ Git: Update failed. Check logs: $LOG_FILE"
report_section "Git" "$git_version" "Update failed"
fi
else
gum style --foreground 3 " ⚠ Git: Outdated"
report_section "Git" "$git_version" "Outdated"
fi
else
gum style --foreground 10 " βœ“ Git: Already up to date ($git_version)"
report_section "Git" "$git_version" "Already up to date"
fi
else
gum style --foreground 10 " βœ“ Git: Already up to date ($git_version)"
report_section "Git" "$git_version" "Already up to date"
fi
else
gum style --foreground 10 " βœ“ Git: Already up to date ($git_version)"
report_section "Git" "$git_version" "Already up to date"
fi
else
gum style --foreground 3 " ⚠️ Git not installed β€” skipping. Install via Xcode CLT or: brew install git"
gum style --foreground 240 " ⏭ Git: Skipped"
report_section "Git" "β€”" "Skipped"
fi
else
gum style --foreground 240 " ⏭ Git: Skipped"
report_section "Git" "β€”" "Skipped"
fi
# 🟒 Node.js (FNM preferred over NVM)
show_section_header "🟒 Node.js"
if confirm_section_check "🟒 Node.js"; then
if run_task "Checking Node.js" "command -v node &> /dev/null"; then
if command -v fnm &> /dev/null; then
echo "🟒 Node.js (fnm):"
current_node=$(extract_version "fnm current 2>/dev/null" "node --version 2>/dev/null | sed 's/v//'")
if ! validate_version "$current_node"; then
current_node=$(node --version 2>/dev/null | sed 's/v//')
fi
# Normalize version: remove 'v' prefix if present
current_node=$(echo "$current_node" | sed 's/^v//')
gum style --foreground 2 " βœ… Current Node.js version: $current_node"
latest_node_output=$(run_task_capture "Checking Node.js LTS versions" "fnm list-remote --lts 2>&1")
latest_node=$(echo "$latest_node_output" | tail -1 | awk '{print $1}' | sed 's/v//')
if validate_version "$latest_node" && [ -n "$current_node" ] && [ "$current_node" != "$latest_node" ]; then
gum style --foreground 3 " ⚠️ Node.js is outdated (current: $current_node, latest: $latest_node)"
if confirm "Update Node.js to $latest_node?"; then
if execute_with_timeout "Installing Node.js $latest_node" "fnm install $latest_node" 600 "Node.js installation timed out" && \
execute_command "Switching to Node.js $latest_node" "fnm use $latest_node" "Failed to switch to Node.js $latest_node"; then
gum style --foreground 10 " βœ“ Node.js: Updated ($current_node β†’ $latest_node)"
report_section "Node.js" "$current_node" "$latest_node"
else
gum style --foreground 1 " ❌ Node.js: Update failed. Check logs: $LOG_FILE"
report_section "Node.js" "$current_node" "Update failed"
fi
else
gum style --foreground 3 " ⚠ Node.js: Outdated ($current_node)"
report_section "Node.js" "$current_node" "Outdated"
fi
else
gum style --foreground 10 " βœ“ Node.js: Already up to date ($current_node)"
report_section "Node.js" "$current_node" "Already up to date"
fi
elif [ -s "$HOME/.nvm/nvm.sh" ] || [ -s "/usr/local/opt/nvm/nvm.sh" ]; then
init_version_manager "nvm"
echo "🟒 Node.js (nvm):"
current_node=$(extract_version "nvm current 2>/dev/null" "node --version 2>/dev/null | sed 's/v//'")
if ! validate_version "$current_node"; then
current_node=$(node --version 2>/dev/null | sed 's/v//')
fi
# Normalize version: remove 'v' prefix if present
current_node=$(echo "$current_node" | sed 's/^v//')
gum style --foreground 2 " βœ… Current Node.js version: $current_node"
latest_node_output=$(run_task_capture "Checking Node.js LTS versions" "nvm ls-remote --lts 2>&1")
latest_node=$(echo "$latest_node_output" | tail -1 | awk '{print $1}' | sed 's/v//')
if validate_version "$latest_node" && [ -n "$current_node" ] && [ "$current_node" != "$latest_node" ]; then
gum style --foreground 3 " ⚠️ Node.js is outdated (current: $current_node, latest: $latest_node)"
if confirm "Update Node.js to $latest_node?"; then
if execute_with_timeout "Installing Node.js $latest_node" "nvm install $latest_node" 600 "Node.js installation timed out" && \
execute_command "Switching to Node.js $latest_node" "nvm use $latest_node" "Failed to switch to Node.js $latest_node"; then
gum style --foreground 10 " βœ“ Node.js: Updated ($current_node β†’ $latest_node)"
report_section "Node.js" "$current_node" "$latest_node"
else
gum style --foreground 1 " ❌ Node.js: Update failed. Check logs: $LOG_FILE"
report_section "Node.js" "$current_node" "Update failed"
fi
else
gum style --foreground 3 " ⚠ Node.js: Outdated ($current_node)"
report_section "Node.js" "$current_node" "Outdated"
fi
else
gum style --foreground 10 " βœ“ Node.js: Already up to date ($current_node)"
report_section "Node.js" "$current_node" "Already up to date"
fi
else
current_node=$(extract_version "node --version 2>/dev/null | sed 's/v//'")
if validate_version "$current_node"; then
gum style --foreground 10 " βœ“ Node.js: Already up to date ($current_node)"
report_section "Node.js" "$current_node" "Already up to date"
else
gum style --foreground 10 " βœ“ Node.js: Installed"
report_section "Node.js" "installed" "Already up to date"
fi
fi
else
gum style --foreground 3 " ⚠️ Node.js not installed"
gum style --foreground 240 " ⏭ Node.js: Skipped"
report_section "Node.js" "β€”" "Skipped"
# Check if fnm is already installed
if command -v fnm &> /dev/null; then
if confirm "Install Node.js via fnm (already installed)?"; then
echo "πŸ’‘ fnm is already installed. Please run:"
echo " fnm install --lts"
echo " fnm use --lts"
echo "Or restart your terminal to use fnm."
fi
elif confirm "Install Node.js via fnm (recommended)?"; then
if command -v brew &> /dev/null; then
if execute_command "Installing fnm" "brew install fnm" "Failed to install fnm"; then
echo "βœ… fnm installed. Please restart your terminal and run:"
echo " fnm install --lts"
echo " fnm use --lts"
fi
else
if execute_with_timeout "Installing fnm" "curl -fsSL https://fnm.vercel.app/install | bash" 300 "fnm installation timed out"; then
echo "βœ… fnm installed. Please restart your terminal and run:"
echo " fnm install --lts"
echo " fnm use --lts"
fi
fi
fi
fi
else
gum style --foreground 240 " ⏭ Node.js: Skipped"
report_section "Node.js" "β€”" "Skipped"
fi
# 🍞 Bun (optional: runtime alternative to Node)
show_section_header "🍞 Bun"
if confirm_section_check "🍞 Bun"; then
if run_task "Checking Bun" "command -v bun &> /dev/null"; then
bun_version=$(extract_version "bun --version 2>/dev/null")
if ! validate_version "$bun_version"; then
bun_version=$(bun --version 2>/dev/null || echo "unknown")
fi
gum style --foreground 2 " βœ… Bun version: $bun_version"
if confirm "Update Bun to latest version?"; then
if execute_command "Updating Bun" "bun upgrade" "Failed to update Bun"; then
bun_new=$(extract_version "bun --version 2>/dev/null")
if ! validate_version "$bun_new"; then
bun_new="Updated"
fi
if validate_version "$bun_new" && [ "$bun_new" != "$bun_version" ] && [ "$bun_new" != "unknown" ]; then
gum style --foreground 10 " βœ“ Bun: Updated ($bun_version β†’ $bun_new)"
report_section "Bun" "$bun_version" "$bun_new"
else
gum style --foreground 10 " βœ“ Bun: Updated"
report_section "Bun" "$bun_version" "Updated"
fi
else
gum style --foreground 1 " ❌ Bun: Update failed. Check logs: $LOG_FILE"
report_section "Bun" "$bun_version" "Update failed"
fi
else
gum style --foreground 10 " βœ“ Bun: Already up to date ($bun_version)"
report_section "Bun" "$bun_version" "Already up to date"
fi
else
gum style --foreground 3 " ⚠️ Not installed β€” skipped"
gum style --foreground 240 " ⏭ Bun: Skipped"
report_section "Bun" "β€”" "Skipped"
fi
else
gum style --foreground 240 " ⏭ Bun: Skipped"
report_section "Bun" "β€”" "Skipped"
fi
# 🐍 Python (MAMP compatible)
show_section_header "🐍 Python"
if confirm_section_check "🐍 Python"; then
if run_task "Checking Python" "command -v python3 &> /dev/null"; then
# Check if MAMP Python is being used
if is_mamp_python; then
echo "🐍 Python (MAMP):"
current_python=$(extract_version "python3 --version 2>&1 | sed 's/Python //'")
if ! validate_version "$current_python"; then
current_python=$(python3 --version 2>&1)
fi
gum style --foreground 2 " βœ… Python installed via MAMP ($current_python)"
gum style --foreground 4 "πŸ’‘ MAMP Python is being used - updates should be managed through MAMP"
gum style --foreground 10 " βœ“ Python: Checked (MAMP - $current_python)"
report_section "Python" "$current_python" "Checked (MAMP)"
elif command -v pyenv &> /dev/null; then
init_version_manager "pyenv"
echo "🐍 Python (pyenv):"
current_python=$(extract_version "pyenv global 2>/dev/null")
if ! validate_version "$current_python"; then
current_python=$(python3 --version 2>&1 | sed 's/Python //')
fi
latest_python_output=$(run_task_capture "Checking available Python versions" "pyenv install --list 2>&1")
latest_python=$(echo "$latest_python_output" | grep -E '^\s*3\.[0-9]+\.[0-9]+$' | tail -1 | tr -d '[:space:]')
if validate_version "$latest_python" && [ -n "$current_python" ] && [ "$current_python" != "$latest_python" ]; then
gum style --foreground 3 " ⚠️ Python is outdated (current: $current_python, latest: $latest_python)"
if confirm "Update Python to $latest_python?"; then
if execute_with_timeout "Installing Python $latest_python" "pyenv install $latest_python" 1800 "Python installation timed out" && \
execute_command "Setting Python $latest_python as global" "pyenv global $latest_python" "Failed to set Python $latest_python as global"; then
gum style --foreground 10 " βœ“ Python: Updated ($current_python β†’ $latest_python)"
report_section "Python" "$current_python" "$latest_python"
else
gum style --foreground 1 " ❌ Python: Update failed. Check logs: $LOG_FILE"
report_section "Python" "$current_python" "Update failed"
fi
else
gum style --foreground 3 " ⚠ Python: Outdated ($current_python)"
report_section "Python" "$current_python" "Outdated"
fi
else
gum style --foreground 10 " βœ“ Python: Already up to date ($current_python)"
report_section "Python" "$current_python" "Already up to date"
fi
else
current_python=$(extract_version "python3 --version 2>&1 | sed 's/Python //'")
if ! validate_version "$current_python"; then
current_python=$(python3 --version 2>&1)
fi
gum style --foreground 10 " βœ“ Python: Already up to date ($current_python)"
report_section "Python" "$current_python" "Already up to date"
fi
else
gum style --foreground 3 " ⚠️ Python not installed"
gum style --foreground 240 " ⏭ Python: Skipped"
report_section "Python" "β€”" "Skipped"
fi
else
gum style --foreground 240 " ⏭ Python: Skipped"
report_section "Python" "β€”" "Skipped"
fi
# πŸ’Ž Ruby
show_section_header "πŸ’Ž Ruby"
if confirm_section_check "πŸ’Ž Ruby"; then
if run_task "Checking Ruby" "command -v ruby &> /dev/null"; then
if command -v rbenv &> /dev/null; then
init_version_manager "rbenv"
echo "πŸ’Ž Ruby (rbenv):"
current_ruby=$(extract_version "rbenv global 2>/dev/null")
if ! validate_version "$current_ruby"; then
current_ruby=$(ruby --version 2>/dev/null | awk '{print $2}')
fi
latest_ruby_output=$(run_task_capture "Checking available Ruby versions" "rbenv install -l 2>&1")
latest_ruby=$(echo "$latest_ruby_output" | grep -E '^\s*[0-9]+\.[0-9]+\.[0-9]+$' | tail -1 | tr -d '[:space:]')
if validate_version "$latest_ruby" && [ -n "$current_ruby" ] && [ "$current_ruby" != "$latest_ruby" ]; then
gum style --foreground 3 " ⚠️ Ruby is outdated (current: $current_ruby, latest: $latest_ruby)"
if confirm "Update Ruby to $latest_ruby?"; then
if execute_with_timeout "Installing Ruby $latest_ruby" "rbenv install $latest_ruby" 1800 "Ruby installation timed out" && \
execute_command "Setting Ruby $latest_ruby as global" "rbenv global $latest_ruby" "Failed to set Ruby $latest_ruby as global"; then
gum style --foreground 10 " βœ“ Ruby: Updated ($current_ruby β†’ $latest_ruby)"
report_section "Ruby" "$current_ruby" "$latest_ruby"
else
gum style --foreground 1 " ❌ Ruby: Update failed. Check logs: $LOG_FILE"
report_section "Ruby" "$current_ruby" "Update failed"
fi
else
gum style --foreground 3 " ⚠ Ruby: Outdated ($current_ruby)"
report_section "Ruby" "$current_ruby" "Outdated"
fi
else
gum style --foreground 10 " βœ“ Ruby: Already up to date ($current_ruby)"
report_section "Ruby" "$current_ruby" "Already up to date"
fi
else
current_ruby=$(extract_version "ruby --version 2>/dev/null | awk '{print \$2}'")
if ! validate_version "$current_ruby"; then
current_ruby=$(ruby --version 2>/dev/null | awk '{print $2}')
fi
gum style --foreground 10 " βœ“ Ruby: Already up to date ($current_ruby)"
report_section "Ruby" "$current_ruby" "Already up to date"
fi
else
gum style --foreground 3 " ⚠️ Ruby not installed β€” skipped"
gum style --foreground 240 " ⏭ Ruby: Skipped"
report_section "Ruby" "β€”" "Skipped"
fi
else
gum style --foreground 240 " ⏭ Ruby: Skipped"
report_section "Ruby" "β€”" "Skipped"
fi
# πŸ¦€ Rust
show_section_header "πŸ¦€ Rust"
if confirm_section_check "πŸ¦€ Rust"; then
if run_task "Checking Rust" "command -v rustup &> /dev/null"; then
rustc_version=$(extract_version "rustc --version 2>/dev/null | sed 's/rustc //' | awk '{print \$1}'")
if ! validate_version "$rustc_version"; then
rustc_version=$(rustc --version 2>/dev/null || echo "unknown")
fi
gum style --foreground 2 " βœ… Rust toolchain: $rustc_version"
if confirm "Update Rust toolchain?"; then
prev_rustc_version="$rustc_version"
rust_update_output=$(run_task_capture "Updating Rust toolchain" "rustup update 2>&1")
rust_update_exit_code=$?
if [ $rust_update_exit_code -eq 0 ]; then
# Check if it's already up to date (unchanged)
if echo "$rust_update_output" | grep -qi "unchanged\|already up to date"; then
gum style --foreground 10 " βœ“ Rust: Already up to date ($rustc_version)"
report_section "Rust" "$rustc_version" "Already up to date"
else
rustc_version=$(extract_version "rustc --version 2>/dev/null | sed 's/rustc //' | awk '{print \$1}'")
if ! validate_version "$rustc_version"; then
rustc_version=$(rustc --version 2>/dev/null || echo "Updated")
fi
if validate_version "$rustc_version" && [ "$rustc_version" != "$prev_rustc_version" ]; then
gum style --foreground 10 " βœ“ Rust: Updated ($prev_rustc_version β†’ $rustc_version)"
report_section "Rust" "$prev_rustc_version" "$rustc_version"
else
gum style --foreground 10 " βœ“ Rust: Updated"
report_section "Rust" "$prev_rustc_version" "Updated"
fi
fi
else
gum style --foreground 1 " ❌ Rust: Update failed. Check logs: $LOG_FILE"
report_section "Rust" "$rustc_version" "Update failed"
fi
else
gum style --foreground 10 " βœ“ Rust: Already up to date ($rustc_version)"
report_section "Rust" "$rustc_version" "Already up to date"
fi
else
gum style --foreground 3 " ⚠️ Rust not installed β€” skipped"
gum style --foreground 240 " ⏭ Rust: Skipped"
report_section "Rust" "β€”" "Skipped"
fi
else
gum style --foreground 240 " ⏭ Rust: Skipped"
report_section "Rust" "β€”" "Skipped"
fi
# 🐹 Go
show_section_header "🐹 Go"
if confirm_section_check "🐹 Go"; then
if run_task "Checking Go" "command -v go &> /dev/null"; then
go_version=$(extract_version "go version 2>/dev/null | awk '{print \$3}' | sed 's/go//'")
if ! validate_version "$go_version"; then
go_version=$(go version 2>/dev/null | awk '{print $3}' | sed 's/go//')
fi
gum style --foreground 2 " βœ… Go version: $go_version"
if command -v brew &> /dev/null; then
brew_outdated_go=$(run_task_capture "Checking Go updates" "brew outdated 2>&1" | grep -E "^go$")
if [ -n "$brew_outdated_go" ]; then
if confirm "Update Go via Homebrew?"; then
if execute_command "Updating Go" "brew upgrade go" "Failed to update Go"; then
go_version_new=$(extract_version "go version 2>/dev/null | awk '{print \$3}' | sed 's/go//'")
if ! validate_version "$go_version_new"; then
go_version_new="Updated"
fi
if validate_version "$go_version_new" && [ "$go_version_new" != "$go_version" ]; then
gum style --foreground 10 " βœ“ Go: Updated ($go_version β†’ $go_version_new)"
report_section "Go" "$go_version" "$go_version_new"
else
gum style --foreground 10 " βœ“ Go: Updated"
report_section "Go" "$go_version" "Updated"
fi
else
gum style --foreground 1 " ❌ Go: Update failed. Check logs: $LOG_FILE"
report_section "Go" "$go_version" "Update failed"
fi
else
gum style --foreground 3 " ⚠ Go: Outdated ($go_version)"
report_section "Go" "$go_version" "Outdated"
fi
else
gum style --foreground 10 " βœ“ Go: Already up to date ($go_version)"
report_section "Go" "$go_version" "Already up to date"
fi
else
gum style --foreground 10 " βœ“ Go: Already up to date ($go_version)"
report_section "Go" "$go_version" "Already up to date"
fi
else
gum style --foreground 3 " ⚠️ Go not installed β€” skipped"
gum style --foreground 240 " ⏭ Go: Skipped"
report_section "Go" "β€”" "Skipped"
fi
else
gum style --foreground 240 " ⏭ Go: Skipped"
report_section "Go" "β€”" "Skipped"
fi
# β˜• Java
show_section_header "β˜• Java"
if confirm_section_check "β˜• Java"; then
if [ -d "$HOME/.sdkman" ]; then
init_version_manager "sdkman"
if command -v sdk &> /dev/null; then
gum style --foreground 2 " βœ… SDKMAN detected"
java_prev=$(extract_version "sdk current java 2>/dev/null | tail -1" "java -version 2>&1 | head -1 | awk -F'\"' '{print \$2}'")
if ! validate_version "$java_prev"; then
java_prev=$(java -version 2>&1 | head -1 | awk -F'"' '{print $2}' || echo "β€”")
fi
if confirm "Upgrade Java via SDKMAN?"; then
if execute_command "Upgrading Java via SDKMAN" "sdk upgrade java" "Failed to upgrade Java via SDKMAN"; then
java_new=$(extract_version "sdk current java 2>/dev/null | tail -1" "java -version 2>&1 | head -1 | awk -F'\"' '{print \$2}'")
if ! validate_version "$java_new"; then
java_new="Updated"
fi
if validate_version "$java_new" && [ "$java_new" != "$java_prev" ]; then
gum style --foreground 10 " βœ“ Java: Updated ($java_prev β†’ $java_new)"
report_section "Java" "$java_prev" "$java_new"
else
gum style --foreground 10 " βœ“ Java: Updated"
report_section "Java" "$java_prev" "Updated"
fi
else
gum style --foreground 1 " ❌ Java: Update failed. Check logs: $LOG_FILE"
report_section "Java" "$java_prev" "Update failed"
fi
else
gum style --foreground 10 " βœ“ Java: Already up to date"
report_section "Java" "$java_prev" "Already up to date"
fi
else
echo "πŸ’‘ SDKMAN directory found but sdk not in PATH β€” run: source ~/.sdkman/bin/sdkman-init.sh && sdk upgrade java"
add_to_history "$(gum style --foreground 240 "πŸ”")" "Java: Checked"
report_section "Java" "β€”" "Checked"
fi
elif command -v java &> /dev/null; then
java_version=$(java -version 2>&1 | head -1)
gum style --foreground 2 "$java_version"
java_prev_short=$(extract_version "java -version 2>&1 | head -1 | awk -F'\"' '{print \$2}'")
if ! validate_version "$java_prev_short"; then
java_prev_short=$(java -version 2>&1 | head -1 | awk -F'"' '{print $2}' || echo "β€”")
fi
if command -v brew &> /dev/null; then
brew_java=$(brew list --formula 2>/dev/null | grep -E "openjdk|java" | head -1)
if [ -n "$brew_java" ]; then
brew_outdated_java=$(run_task_capture "Checking Java updates" "brew outdated 2>&1" | grep -E "^$brew_java$")
if [ -n "$brew_outdated_java" ]; then
if confirm "Update $brew_java via Homebrew?"; then
if execute_command "Updating $brew_java" "brew upgrade $brew_java" "Failed to update $brew_java"; then
java_new_short=$(extract_version "java -version 2>&1 | head -1 | awk -F'\"' '{print \$2}'")
if ! validate_version "$java_new_short"; then
java_new_short="Updated"
fi
if validate_version "$java_new_short" && [ "$java_new_short" != "$java_prev_short" ]; then
gum style --foreground 10 " βœ“ Java: Updated ($java_prev_short β†’ $java_new_short)"
report_section "Java" "$java_prev_short" "$java_new_short"
else
gum style --foreground 10 " βœ“ Java: Updated"
report_section "Java" "$java_prev_short" "Updated"
fi
else
gum style --foreground 1 " ❌ Java: Update failed. Check logs: $LOG_FILE"
report_section "Java" "$java_prev_short" "Update failed"
fi
else
gum style --foreground 3 " ⚠ Java: Outdated ($java_prev_short)"
report_section "Java" "$java_prev_short" "Outdated"
fi
else
gum style --foreground 10 " βœ“ Java: Already up to date ($java_prev_short)"
report_section "Java" "$java_prev_short" "Already up to date"
fi
else
echo "πŸ’‘ Java not managed by Homebrew β€” skipped. Use SDKMAN or brew install openjdk."
gum style --foreground 10 " βœ“ Java: Already up to date ($java_prev_short)"
report_section "Java" "$java_prev_short" "Already up to date"
fi
else
gum style --foreground 10 " βœ“ Java: Already up to date ($java_prev_short)"
report_section "Java" "$java_prev_short" "Already up to date"
fi
else
gum style --foreground 3 " ⚠️ Java not installed β€” skipping."
gum style --foreground 240 " ⏭ Java: Skipped"
report_section "Java" "β€”" "Skipped"
fi
else
gum style --foreground 240 " ⏭ Java: Skipped"
report_section "Java" "β€”" "Skipped"
fi
# πŸ”· .NET
show_section_header "πŸ”· .NET"
if confirm_section_check "πŸ”· .NET"; then
if run_task "Checking .NET" "command -v dotnet &> /dev/null"; then
dotnet_version=$(extract_version "dotnet --version 2>/dev/null")
if ! validate_version "$dotnet_version"; then
dotnet_version=$(dotnet --version 2>/dev/null || echo "unknown")
fi
gum style --foreground 2 " βœ… .NET SDK: $dotnet_version"
if confirm "Update global .NET tools?"; then
if execute_command "Updating global .NET tools" "dotnet tool update -g" "Failed to update global .NET tools"; then
gum style --foreground 10 " βœ“ .NET: Tools updated"
report_section ".NET" "$dotnet_version" "Tools updated"
else
gum style --foreground 3 " ⚠ .NET: No global tools or update failed"
report_section ".NET" "$dotnet_version" "Update failed"
fi
else
gum style --foreground 10 " βœ“ .NET: Already up to date ($dotnet_version)"
report_section ".NET" "$dotnet_version" "Already up to date"
fi
else
gum style --foreground 3 " ⚠️ Not installed β€” skipped"
gum style --foreground 240 " ⏭ .NET: Skipped"
report_section ".NET" "β€”" "Skipped"
fi
else
gum style --foreground 240 " ⏭ .NET: Skipped"
report_section ".NET" "β€”" "Skipped"
fi
# πŸ¦‹ Flutter
show_section_header "πŸ¦‹ Flutter"
if confirm_section_check "πŸ¦‹ Flutter"; then
if run_task "Checking Flutter" "command -v flutter &> /dev/null"; then
flutter_version=$(extract_version "flutter --version 2>/dev/null | head -1 | awk '{print \$2}'" "flutter --version 2>/dev/null | head -1")
if ! validate_version "$flutter_version"; then
flutter_version=$(flutter --version 2>/dev/null | head -1 || echo "unknown")
fi
gum style --foreground 2 " βœ… Flutter version: $flutter_version"
prev_flutter="$flutter_version"
if confirm "Run Flutter upgrade?"; then
if execute_with_timeout "Updating Flutter" "flutter upgrade" 600 "Flutter upgrade timed out"; then
new_flutter=$(extract_version "flutter --version 2>/dev/null | head -1 | awk '{print \$2}'" "flutter --version 2>/dev/null | head -1")
if ! validate_version "$new_flutter"; then
new_flutter="Updated"
fi
if validate_version "$new_flutter" && [ "$new_flutter" != "$prev_flutter" ] && [ "$new_flutter" != "Updated" ]; then
gum style --foreground 10 " βœ“ Flutter: Updated ($prev_flutter β†’ $new_flutter)"
report_section "Flutter" "$prev_flutter" "$new_flutter"
else
gum style --foreground 10 " βœ“ Flutter: Updated"
report_section "Flutter" "$prev_flutter" "Updated"
fi
else
gum style --foreground 1 " ❌ Flutter: Update failed. Check logs: $LOG_FILE"
report_section "Flutter" "$prev_flutter" "Update failed"
fi
else
gum style --foreground 10 " βœ“ Flutter: Already up to date"
report_section "Flutter" "$prev_flutter" "Already up to date"
fi
else
gum style --foreground 3 " ⚠️ Not installed β€” skipped"
gum style --foreground 240 " ⏭ Flutter: Skipped"
report_section "Flutter" "β€”" "Skipped"
fi
else
gum style --foreground 240 " ⏭ Flutter: Skipped"
report_section "Flutter" "β€”" "Skipped"
fi
# πŸ“± Android SDK
show_section_header "πŸ“± Android SDK"
if confirm_section_check "πŸ“± Android SDK"; then
if [ -d "$HOME/Library/Android/sdk" ]; then
gum style --foreground 2 " βœ… Android SDK found at $HOME/Library/Android/sdk"
sdkmanager_bin="$HOME/Library/Android/sdk/cmdline-tools/latest/bin/sdkmanager"
if [ ! -f "$sdkmanager_bin" ] || [ ! -x "$sdkmanager_bin" ]; then
sdkmanager_bin="$HOME/Library/Android/sdk/tools/bin/sdkmanager"
fi
if [ -f "$sdkmanager_bin" ] && [ -x "$sdkmanager_bin" ]; then
gum style --foreground 2 "πŸ“± Checking Android SDK updates..."
installed_packages_output=$(run_task_capture "Checking Android SDK packages" "$sdkmanager_bin --list 2>&1")
installed_packages_exit_code=$?
if [ $installed_packages_exit_code -eq 0 ]; then
# Filter out XML warnings and parse package list
installed_packages=$(echo "$installed_packages_output" | \
grep -v "Warning:" | \
grep -v "SDK XML" | \
grep -v "package.xml parsing" | \
grep -v "Γ©lΓ©ment inattendu" | \
grep -v "Les Γ©lΓ©ments attendus" | \
awk -F'|' '{
gsub(/^[ \t]+|[ \t]+$/, "", $1);
if ($1 != "" && $1 != "package" && $1 != "Path" && $1 != "Installed" && $1 !~ /^----/) print $1
}')
if [ -n "$installed_packages" ]; then
echo "πŸ“¦ Installed Android SDK packages:"
echo "$installed_packages"
if confirm "Update Android SDK packages?"; then
# Run update (warnings are filtered in output but don't affect exit code)
update_output=$(run_task_capture "Updating Android SDK packages" "$sdkmanager_bin --update 2>&1")
update_exit_code=$?
# Filter warnings from output for display
update_output_clean=$(echo "$update_output" | grep -v "Warning:" | grep -v "SDK XML" | grep -v "package.xml parsing" | grep -v "Γ©lΓ©ment inattendu" | grep -v "Les Γ©lΓ©ments attendus")
if [ $update_exit_code -eq 0 ]; then
# Check if updates were actually available
if echo "$update_output_clean" | grep -q "No updates available"; then
gum style --foreground 10 " βœ“ Android SDK: Already up to date"
report_section "Android SDK" "β€”" "Already up to date"
else
gum style --foreground 10 " βœ“ Android SDK: Updated"
report_section "Android SDK" "β€”" "Updated"
fi
else
gum style --foreground 1 " ❌ Android SDK: Update failed. Check logs: $LOG_FILE"
report_section "Android SDK" "β€”" "Update failed"
fi
else
gum style --foreground 10 " βœ“ Android SDK: Already up to date"
report_section "Android SDK" "β€”" "Already up to date"
fi
else
gum style --foreground 10 " βœ“ Android SDK: Already up to date"
report_section "Android SDK" "β€”" "Already up to date"
fi
else
log_error "Failed to list Android SDK packages"
gum style --foreground 3 " ⚠️ Could not check Android SDK packages"
report_section "Android SDK" "β€”" "Check failed"
fi
else
gum style --foreground 3 " ⚠️ Android SDK command-line tools not found or not executable"
add_to_history "$(gum style --foreground 240 "πŸ”")" "Android SDK: Checked"
report_section "Android SDK" "β€”" "Checked"
fi
else
gum style --foreground 3 " ⚠️ Android SDK not installed β€” skipped"
gum style --foreground 240 " ⏭ Android SDK: Skipped"
report_section "Android SDK" "β€”" "Skipped"
fi
else
gum style --foreground 240 " ⏭ Android SDK: Skipped"
report_section "Android SDK" "β€”" "Skipped"
fi
# 🐘 Composer (MAMP compatible)
show_section_header "🐘 Composer"
if confirm_section_check "🐘 Composer"; then
if run_task "Checking Composer" "command -v composer &> /dev/null"; then
current_composer=$(extract_version "composer --version 2>/dev/null | awk '{print \$3}'")
if ! validate_version "$current_composer"; then
current_composer=$(composer --version 2>/dev/null | awk '{print $3}')
fi
gum style --foreground 2 " βœ… Composer version: $current_composer"
# Check for Composer self-update
if confirm "Update Composer to latest version?"; then
composer_update_output=$(run_task_capture "Updating Composer" "composer self-update 2>&1")
composer_update_exit_code=$?
if [ $composer_update_exit_code -eq 0 ]; then
# Check if it's already up to date
if echo "$composer_update_output" | grep -qi "already using the latest\|already up to date\|nothing to update"; then
gum style --foreground 10 " βœ“ Composer: Already up to date ($current_composer)"
report_section "Composer" "$current_composer" "Already up to date"
else
new_composer=$(extract_version "composer --version 2>/dev/null | awk '{print \$3}'")
if ! validate_version "$new_composer"; then
new_composer="Updated"
fi
if validate_version "$new_composer" && [ "$new_composer" != "$current_composer" ] && [ "$new_composer" != "Updated" ]; then
gum style --foreground 10 " βœ“ Composer: Updated ($current_composer β†’ $new_composer)"
report_section "Composer" "$current_composer" "$new_composer"
else
gum style --foreground 10 " βœ“ Composer: Updated"
report_section "Composer" "$current_composer" "Updated"
fi
fi
else
gum style --foreground 1 " ❌ Composer: Update failed. Check logs: $LOG_FILE"
report_section "Composer" "$current_composer" "Update failed"
fi
else
gum style --foreground 10 " βœ“ Composer: Already up to date ($current_composer)"
report_section "Composer" "$current_composer" "Already up to date"
fi
# Check for global Composer packages updates
# Only check if there are actually packages installed (check for vendor-dir or packages)
composer_global_dir=""
if [ -d "$HOME/.composer/vendor" ]; then
composer_global_dir="$HOME/.composer/vendor"
elif [ -d "$HOME/.config/composer/vendor" ]; then
composer_global_dir="$HOME/.config/composer/vendor"
fi
if [ -n "$composer_global_dir" ] && [ "$(ls -A "$composer_global_dir" 2>/dev/null)" ]; then
echo "πŸ“¦ Checking global Composer packages..."
outdated_packages_output=$(run_task_capture "Checking global Composer packages" "composer global outdated 2>&1")
outdated_packages_exit_code=$?
# Filter out error messages about missing composer.json
outdated_packages=$(echo "$outdated_packages_output" | grep -v "composer.json file" | grep -v "To initialize a project" | grep -v "Changed current directory" | tail -n +2)
if [ $outdated_packages_exit_code -eq 0 ] && [ -n "$outdated_packages" ] && ! echo "$outdated_packages" | grep -q "No global packages"; then
echo "⚠️ Outdated global Composer packages:"
echo "$outdated_packages"
if confirm "Update all global Composer packages?"; then
if execute_command "Updating global Composer packages" "composer global update" "Failed to update global Composer packages"; then
echo "βœ… Global Composer packages updated"
else
echo "❌ Failed to update global Composer packages. Check logs: $LOG_FILE"
fi
fi
else
echo "βœ… All global Composer packages are up to date"
fi
fi
else
gum style --foreground 3 " ⚠️ Composer not installed"
gum style --foreground 240 " ⏭ Composer: Skipped"
report_section "Composer" "β€”" "Skipped"
if confirm "Install Composer?"; then
echo "πŸ“¦ Installing Composer..."
# Use MAMP PHP if available, otherwise use system PHP
MAMP_PHP=$(find_mamp_php)
COMPOSER_SETUP_FILE="composer-setup.php"
TEMP_FILES+=("$COMPOSER_SETUP_FILE")
if [ -n "$MAMP_PHP" ] && [ -f "$MAMP_PHP" ]; then
if execute_with_timeout "Downloading Composer installer" "$MAMP_PHP -r \"copy('https://getcomposer.org/installer', '$COMPOSER_SETUP_FILE');\"" 60 "Composer download timed out" && \
[ -f "$COMPOSER_SETUP_FILE" ]; then
if execute_command "Installing Composer" "$MAMP_PHP $COMPOSER_SETUP_FILE --install-dir=/usr/local/bin --filename=composer" "Failed to install Composer"; then
rm -f "$COMPOSER_SETUP_FILE"
echo "βœ… Composer installed using MAMP PHP"
else
rm -f "$COMPOSER_SETUP_FILE"
echo "❌ Composer installation failed. Check logs: $LOG_FILE"
fi
else
echo "❌ Failed to download Composer installer"
fi
elif command -v php &> /dev/null; then
if execute_with_timeout "Downloading Composer installer" "php -r \"copy('https://getcomposer.org/installer', '$COMPOSER_SETUP_FILE');\"" 60 "Composer download timed out" && \
[ -f "$COMPOSER_SETUP_FILE" ]; then
if execute_command "Installing Composer" "php $COMPOSER_SETUP_FILE --install-dir=/usr/local/bin --filename=composer" "Failed to install Composer"; then
rm -f "$COMPOSER_SETUP_FILE"
echo "βœ… Composer installed"
else
rm -f "$COMPOSER_SETUP_FILE"
echo "❌ Composer installation failed. Check logs: $LOG_FILE"
fi
else
echo "❌ Failed to download Composer installer"
fi
else
echo "❌ PHP not found. Cannot install Composer."
fi
fi
fi
else
gum style --foreground 240 " ⏭ Composer: Skipped"
report_section "Composer" "β€”" "Skipped"
fi
# 🐳 Docker
show_section_header "🐳 Docker"
if confirm_section_check "🐳 Docker"; then
if run_task "Checking Docker" "command -v docker &> /dev/null"; then
docker_version=$(docker --version 2>/dev/null || echo "unknown")
gum style --foreground 2 "$docker_version"
if confirm "Check for Docker Desktop updates?"; then
echo "⚠️ Please check Docker Desktop app for updates manually"
fi
gum style --foreground 10 " βœ“ Docker: Already up to date ($docker_version)"
report_section "Docker" "$docker_version" "Already up to date"
else
gum style --foreground 3 " ⚠️ Not installed β€” skipped"
gum style --foreground 240 " ⏭ Docker: Skipped"
report_section "Docker" "β€”" "Skipped"
fi
else
gum style --foreground 240 " ⏭ Docker: Skipped"
report_section "Docker" "β€”" "Skipped"
fi
# ⎈ kubectl
show_section_header "⎈ kubectl"
if confirm_section_check "⎈ kubectl"; then
if run_task "Checking kubectl" "command -v kubectl &> /dev/null"; then
kubectl_version=$(extract_version "kubectl version --client --short 2>/dev/null | sed 's/.*v/v/' | awk '{print \$1}' | sed 's/v//'" "kubectl version --client 2>/dev/null | grep 'Client Version' | awk '{print \$3}' | sed 's/\"//g' | sed 's/v//'")
if ! validate_version "$kubectl_version"; then
kubectl_version=$(kubectl version --client --short 2>/dev/null || kubectl version --client 2>/dev/null | head -1 || echo "unknown")
fi
gum style --foreground 2 " βœ… kubectl version: $kubectl_version"
prev_kubectl="$kubectl_version"
if command -v brew &> /dev/null && brew list kubectl &>/dev/null; then
brew_outdated_kubectl=$(run_task_capture "Checking kubectl updates" "brew outdated 2>&1" | grep -E "^kubectl$")
if [ -n "$brew_outdated_kubectl" ]; then
if confirm "Update kubectl via Homebrew?"; then
if execute_command "Updating kubectl" "brew upgrade kubectl" "Failed to update kubectl"; then
new_kubectl=$(extract_version "kubectl version --client --short 2>/dev/null | sed 's/.*v/v/' | awk '{print \$1}' | sed 's/v//'" "kubectl version --client 2>/dev/null | grep 'Client Version' | awk '{print \$3}' | sed 's/\"//g' | sed 's/v//'")
if ! validate_version "$new_kubectl"; then
new_kubectl="Updated"
fi
gum style --foreground 10 " βœ“ kubectl: Updated"
report_section "kubectl" "$prev_kubectl" "$new_kubectl"
else
gum style --foreground 1 " ❌ kubectl: Update failed. Check logs: $LOG_FILE"
report_section "kubectl" "$prev_kubectl" "Update failed"
fi
else
gum style --foreground 3 " ⚠ kubectl: Outdated"
report_section "kubectl" "$prev_kubectl" "Outdated"
fi
else
gum style --foreground 10 " βœ“ kubectl: Already up to date"
report_section "kubectl" "$prev_kubectl" "Already up to date"
fi
else
gum style --foreground 10 " βœ“ kubectl: Already up to date"
report_section "kubectl" "$prev_kubectl" "Already up to date"
fi
else
gum style --foreground 3 " ⚠️ Not installed β€” skipped"
gum style --foreground 240 " ⏭ kubectl: Skipped"
report_section "kubectl" "β€”" "Skipped"
fi
else
gum style --foreground 240 " ⏭ kubectl: Skipped"
report_section "kubectl" "β€”" "Skipped"
fi
# πŸ“ Terraform
show_section_header "πŸ“ Terraform"
if confirm_section_check "πŸ“ Terraform"; then
if run_task "Checking Terraform" "command -v terraform &> /dev/null"; then
terraform_version=$(terraform version 2>/dev/null | head -1 || echo "unknown")
gum style --foreground 2 "$terraform_version"
prev_tf="$terraform_version"
if command -v brew &> /dev/null; then
# Check if installed via standard Homebrew
if brew list terraform &>/dev/null; then
brew_outdated_tf=$(run_task_capture "Checking Terraform updates" "brew outdated 2>&1" | grep -E "^terraform$")
if [ -n "$brew_outdated_tf" ]; then
if confirm "Update Terraform via Homebrew?"; then
if execute_command "Updating Terraform" "brew upgrade terraform" "Failed to update Terraform"; then
new_tf=$(terraform version 2>/dev/null | head -1 || echo "Updated")
gum style --foreground 10 " βœ“ Terraform: Updated"
report_section "Terraform" "$prev_tf" "$new_tf"
else
gum style --foreground 1 " ❌ Terraform: Update failed. Check logs: $LOG_FILE"
report_section "Terraform" "$prev_tf" "Update failed"
fi
else
gum style --foreground 3 " ⚠ Terraform: Outdated"
report_section "Terraform" "$prev_tf" "Outdated"
fi
else
gum style --foreground 10 " βœ“ Terraform: Already up to date"
gum style --foreground 240 " πŸ’‘ Note: Homebrew no longer updates Terraform. Use HashiCorp tap for latest: brew tap hashicorp/tap && brew install hashicorp/tap/terraform"
report_section "Terraform" "$prev_tf" "Already up to date"
fi
# Check if installed via HashiCorp tap
elif brew list hashicorp/tap/terraform &>/dev/null 2>/dev/null; then
brew_outdated_tf=$(run_task_capture "Checking Terraform updates" "brew outdated hashicorp/tap/terraform 2>&1" | grep -E "terraform$")
if [ -n "$brew_outdated_tf" ]; then
if confirm "Update Terraform via HashiCorp tap?"; then
if execute_command "Updating Terraform" "brew upgrade hashicorp/tap/terraform" "Failed to update Terraform"; then
new_tf=$(terraform version 2>/dev/null | head -1 || echo "Updated")
gum style --foreground 10 " βœ“ Terraform: Updated"
report_section "Terraform" "$prev_tf" "$new_tf"
else
gum style --foreground 1 " ❌ Terraform: Update failed. Check logs: $LOG_FILE"
report_section "Terraform" "$prev_tf" "Update failed"
fi
else
gum style --foreground 3 " ⚠ Terraform: Outdated"
report_section "Terraform" "$prev_tf" "Outdated"
fi
else
gum style --foreground 10 " βœ“ Terraform: Already up to date"
report_section "Terraform" "$prev_tf" "Already up to date"
fi
else
gum style --foreground 10 " βœ“ Terraform: Already up to date"
gum style --foreground 240 " πŸ’‘ Note: For latest Terraform versions, use HashiCorp tap: brew tap hashicorp/tap && brew install hashicorp/tap/terraform"
report_section "Terraform" "$prev_tf" "Already up to date"
fi
else
gum style --foreground 10 " βœ“ Terraform: Already up to date"
report_section "Terraform" "$prev_tf" "Already up to date"
fi
else
gum style --foreground 3 " ⚠️ Not installed β€” skipped"
gum style --foreground 240 " ⏭ Terraform: Skipped"
report_section "Terraform" "β€”" "Skipped"
fi
else
gum style --foreground 240 " ⏭ Terraform: Skipped"
report_section "Terraform" "β€”" "Skipped"
fi
# πŸ“¦ npm global packages
show_section_header "πŸ“¦ npm global packages"
if confirm_section_check "πŸ“¦ npm global packages"; then
if run_task "Checking npm" "command -v npm &> /dev/null"; then
npm_outdated_output=$(run_task_capture "Checking global npm packages" "npm outdated -g --depth=0 2>&1")
npm_outdated_exit_code=$?
# npm outdated returns 1 when packages are outdated (normal), 0 when up to date
# Check for actual errors (network issues, permission problems, etc.)
if [ $npm_outdated_exit_code -ne 0 ] && [ $npm_outdated_exit_code -ne 1 ]; then
log_error "Failed to check for outdated npm packages (exit code: $npm_outdated_exit_code)"
gum style --foreground 3 " ⚠️ Could not check for outdated npm packages"
report_section "npm global" "β€”" "Check failed"
else
# Filter out header line and empty lines
npm_outdated=$(echo "$npm_outdated_output" | grep -v "^Package\|^-\+\$\|^$" | tail -n +1)
if [ -z "$npm_outdated" ]; then
gum style --foreground 10 " βœ“ npm global: Already up to date"
report_section "npm global" "β€”" "Already up to date"
else
gum style --foreground 3 " ⚠️ Outdated npm global packages:"
echo "$npm_outdated"
if confirm "Update all npm global packages?"; then
if execute_with_timeout "Updating npm global packages" "npm update -g" 600 "npm update timed out"; then
gum style --foreground 10 " βœ“ npm global: Updated"
report_section "npm global" "β€”" "Updated"
else
gum style --foreground 1 " ❌ npm global: Update failed. Check logs: $LOG_FILE"
report_section "npm global" "β€”" "Update failed"
fi
else
gum style --foreground 3 " ⚠ npm global: Outdated"
report_section "npm global" "β€”" "Outdated"
fi
fi
fi
else
gum style --foreground 3 " ⚠️ Not installed β€” skipped"
gum style --foreground 240 " ⏭ npm global: Skipped"
report_section "npm global" "β€”" "Skipped"
fi
else
gum style --foreground 240 " ⏭ npm global: Skipped"
report_section "npm global" "β€”" "Skipped"
fi
# 🧢 Yarn (v4+)
show_section_header "🧢 Yarn"
if confirm_section_check "🧢 Yarn"; then
if command -v yarn &> /dev/null; then
current_yarn=$(run_task_capture "Checking Yarn" "yarn --version 2>/dev/null" || echo "unknown")
gum style --foreground 2 " βœ… Yarn version: $current_yarn"
# Check if using Yarn 4+ (recommended)
if [[ "$current_yarn" =~ ^[4-9]\. ]]; then
gum style --foreground 2 " βœ… Using Yarn 4+ (recommended)"
# Check for Yarn 4+ self-update
if confirm "Update Yarn to latest version?"; then
corepack enable
corepack prepare yarn@stable --activate
new_yarn=$(yarn --version 2>/dev/null || echo "")
if [ -n "$new_yarn" ] && [ "$new_yarn" != "$current_yarn" ]; then
gum style --foreground 10 " βœ“ Yarn: Updated ($current_yarn β†’ $new_yarn)"
report_section "Yarn" "$current_yarn" "$new_yarn"
elif [ -n "$new_yarn" ] && [ "$new_yarn" = "$current_yarn" ]; then
gum style --foreground 10 " βœ“ Yarn: Already up to date ($current_yarn)"
report_section "Yarn" "$current_yarn" "Already up to date"
else
gum style --foreground 10 " βœ“ Yarn: Updated"
report_section "Yarn" "$current_yarn" "Updated"
fi
else
gum style --foreground 10 " βœ“ Yarn: Already up to date ($current_yarn)"
report_section "Yarn" "$current_yarn" "Already up to date"
fi
echo "πŸ’‘ Yarn 4+ uses project-specific packages instead of global packages"
echo 'πŸ’‘ Use "yarn dlx" for one-time package execution'
elif [[ "$current_yarn" =~ ^[1-3]\. ]]; then
echo "⚠️ Using Yarn $current_yarn (legacy version)"
if confirm "Upgrade to Yarn 4+ (recommended)?"; then
echo "πŸ“¦ Upgrading to Yarn 4+ via Corepack..."
corepack enable
corepack prepare yarn@stable --activate
new_yarn=$(yarn --version 2>/dev/null || echo "")
if [ -n "$new_yarn" ] && [ "$new_yarn" != "$current_yarn" ]; then
gum style --foreground 10 " βœ“ Yarn: Updated ($current_yarn β†’ $new_yarn)"
report_section "Yarn" "$current_yarn" "$new_yarn"
else
gum style --foreground 10 " βœ“ Yarn: Updated"
report_section "Yarn" "$current_yarn" "Updated"
fi
else
gum style --foreground 10 " βœ“ Yarn: Already up to date ($current_yarn)"
report_section "Yarn" "$current_yarn" "Already up to date"
fi
echo "πŸ’‘ Yarn 4+ uses project-specific packages instead of global packages"
else
echo "⚠️ Unknown Yarn version: $current_yarn"
if confirm "Update to latest Yarn 4+?"; then
corepack enable
corepack prepare yarn@stable --activate
new_yarn=$(yarn --version 2>/dev/null || echo "")
if [ -n "$new_yarn" ] && [ "$new_yarn" != "$current_yarn" ]; then
gum style --foreground 10 " βœ“ Yarn: Updated ($current_yarn β†’ $new_yarn)"
report_section "Yarn" "$current_yarn" "$new_yarn"
elif [ -n "$new_yarn" ] && [ "$new_yarn" = "$current_yarn" ]; then
gum style --foreground 10 " βœ“ Yarn: Already up to date ($current_yarn)"
report_section "Yarn" "$current_yarn" "Already up to date"
else
gum style --foreground 10 " βœ“ Yarn: Updated"
report_section "Yarn" "$current_yarn" "Updated"
fi
else
gum style --foreground 10 " βœ“ Yarn: Already up to date ($current_yarn)"
report_section "Yarn" "$current_yarn" "Already up to date"
fi
fi
else
gum style --foreground 3 " ⚠️ Not installed β€” skipped"
gum style --foreground 240 " ⏭ Yarn: Skipped"
report_section "Yarn" "β€”" "Skipped"
fi
else
gum style --foreground 240 " ⏭ Yarn: Skipped"
report_section "Yarn" "β€”" "Skipped"
fi
# πŸ“¦ pnpm
show_section_header "πŸ“¦ pnpm"
if confirm_section_check "πŸ“¦ pnpm"; then
if run_task "Checking pnpm" "command -v pnpm &> /dev/null"; then
pnpm_version=$(pnpm --version 2>/dev/null || echo "unknown")
gum style --foreground 2 " βœ… pnpm version: $pnpm_version"
if confirm "Update global pnpm packages?"; then
pnpm_update_output=$(pnpm update -g 2>&1)
pnpm_new=$(pnpm --version 2>/dev/null || echo "$pnpm_version")
if [ "$pnpm_new" != "$pnpm_version" ]; then
gum style --foreground 10 " βœ“ pnpm: Updated ($pnpm_version β†’ $pnpm_new)"
report_section "pnpm" "$pnpm_version" "$pnpm_new"
else
gum style --foreground 10 " βœ“ pnpm: Updated"
report_section "pnpm" "$pnpm_version" "Updated"
fi
else
gum style --foreground 10 " βœ“ pnpm: Already up to date ($pnpm_version)"
report_section "pnpm" "$pnpm_version" "Already up to date"
fi
else
gum style --foreground 3 " ⚠️ Not installed β€” skipped"
gum style --foreground 240 " ⏭ pnpm: Skipped"
report_section "pnpm" "β€”" "Skipped"
fi
else
gum style --foreground 240 " ⏭ pnpm: Skipped"
report_section "pnpm" "β€”" "Skipped"
fi
# 🐍 pip packages (MAMP compatible)
show_section_header "🐍 pip packages"
if confirm_section_check "🐍 pip packages"; then
if run_task "Checking pip" "command -v pip3 &> /dev/null"; then
# Set locale to avoid warnings
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
pip_list_output=$(run_task_capture "Checking pip packages" "pip3 list --outdated 2>&1")
pip_list_exit_code=$?
if [ $pip_list_exit_code -ne 0 ]; then
log_error "Failed to check for outdated pip packages"
gum style --foreground 3 " ⚠️ Could not check for outdated pip packages"
report_section "pip" "β€”" "Check failed"
else
# Filter out notice messages and empty lines, keep only package lines
pip_list=$(echo "$pip_list_output" | grep -v "^\[notice\]" | grep -v "^Notice:" | grep -v "^WARNING:" | grep -E "^[a-zA-Z0-9_-]+" | tail -n +3)
if [ -z "$pip_list" ]; then
gum style --foreground 10 " βœ“ pip: Already up to date"
report_section "pip" "β€”" "Already up to date"
else
gum style --foreground 3 " ⚠️ Outdated pip packages:"
echo "$pip_list"
if confirm "Update all pip packages?"; then
# Upgrade pip first
execute_command "Upgrading pip" "python3 -m pip install --upgrade pip --quiet" "Failed to upgrade pip"
# Update packages one by one to catch individual failures
# Extract package names (first column) and filter out invalid entries
outdated_packages=$(echo "$pip_list" | awk '{print $1}' | grep -v "^\[" | grep -v "^Notice" | grep -v "^WARNING" | grep -E "^[a-zA-Z0-9_-]+")
failed_packages=()
updated_count=0
for package in $outdated_packages; do
# Skip if package name looks invalid (contains brackets, spaces, etc.)
if [[ "$package" =~ ^[a-zA-Z0-9_-]+$ ]] && [ -n "$package" ]; then
if execute_command "Updating pip package: $package" "pip3 install -U $package" "Failed to update $package"; then
updated_count=$((updated_count + 1))
else
failed_packages+=("$package")
fi
fi
done
if [ ${#failed_packages[@]} -eq 0 ]; then
gum style --foreground 10 " βœ“ pip: Updated ($updated_count packages)"
report_section "pip" "β€”" "Updated"
elif [ $updated_count -gt 0 ]; then
gum style --foreground 3 " ⚠ pip: Partially updated ($updated_count succeeded, ${#failed_packages[@]} failed)"
echo "Failed packages: ${failed_packages[*]}"
report_section "pip" "β€”" "Partially updated"
else
gum style --foreground 1 " ❌ pip: Update failed. Check logs: $LOG_FILE"
report_section "pip" "β€”" "Update failed"
fi
echo "πŸ’‘ If you saw dependency conflict messages, consider updating the conflicting package or using a virtual environment per project."
else
gum style --foreground 3 " ⚠ pip: Outdated"
report_section "pip" "β€”" "Outdated"
fi
fi
fi
fi
else
gum style --foreground 3 " ⚠️ Not installed β€” skipped"
gum style --foreground 240 " ⏭ pip: Skipped"
report_section "pip" "β€”" "Skipped"
fi
# 🧼 Enhanced Cleanup
show_section_header "🧼 Cleanup"
if confirm_section_check "🧼 Cleanup" "🧹 Do you want to run cleanup (remove old packages and caches)?"; then
if confirm "Do you want to clean up old packages and caches?"; then
report_section "Cleanup" "β€”" "Done"
echo "🧹 Cleaning up..."
cleanup_errors=0
# Homebrew cleanup
if command -v brew &> /dev/null; then
if execute_command "Homebrew cleanup" "brew cleanup" "Homebrew cleanup failed"; then
execute_command "Homebrew doctor" "brew doctor" "Homebrew doctor check failed"
else
cleanup_errors=$((cleanup_errors + 1))
fi
fi
# npm cache cleanup
if command -v npm &> /dev/null; then
execute_command "npm cache cleanup" "npm cache clean --force" "npm cache cleanup failed"
fi
# pip cache cleanup
if command -v pip3 &> /dev/null; then
execute_command "pip cache cleanup" "pip3 cache purge" "pip cache cleanup failed"
fi
if [ $cleanup_errors -eq 0 ]; then
echo "βœ… Cleanup completed"
gum style --foreground 10 " βœ“ Cleanup: Done"
else
echo "⚠️ Cleanup completed with some errors. Check logs: $LOG_FILE"
gum style --foreground 3 " ⚠ Cleanup: Done (with errors)"
fi
else
gum style --foreground 240 " ⏭ Cleanup: Skipped"
report_section "Cleanup" "β€”" "Skipped"
fi
else
gum style --foreground 240 " ⏭ Cleanup: Skipped"
report_section "Cleanup" "β€”" "Skipped"
fi
# =============================================================================
# FINAL SUMMARY
# =============================================================================
gum style --border double --border-foreground 63 --foreground 63 --padding "1 2" \
"πŸ“‹ Summary Report"
# Build table data for gum table
UPDATES_COUNT=0
if [ ${#SECTION_REPORT[@]} -gt 0 ]; then
# Prepare table data with pipe separators for gum table
TABLE_HEADER="Status|Section|Previous|New / Status"
TABLE_ROWS=()
for entry in "${SECTION_REPORT[@]}"; do
name="${entry%%|*}"
rest="${entry#*|}"
prev="${rest%%|*}"
new_or_status="${rest#*|}"
# Replace "Updated" with actual version if available
if [ "$new_or_status" = "Updated" ]; then
case "$name" in
"Bun") new_or_status=$(bun --version 2>/dev/null || echo "Updated") ;;
"Android SDK") new_or_status="Updated" ;;
"pnpm") new_or_status=$(pnpm --version 2>/dev/null || echo "Updated") ;;
"Homebrew") new_or_status="Updated" ;;
"macOS") new_or_status="Updated" ;;
"npm global") new_or_status="Updated" ;;
"pip") new_or_status="Updated" ;;
*) new_or_status="Updated" ;;
esac
fi
emoji=$(status_emoji "$prev" "$new_or_status")
prev_display=$(truncate_str "$prev" 30)
new_display=$(truncate_str "$new_or_status" 40)
# Ensure no empty fields and proper formatting
emoji="${emoji:-βœ…}"
name="${name:-Unknown}"
prev_display="${prev_display:-β€”}"
new_display="${new_display:-β€”}"
# Remove any pipe characters from values to avoid breaking table format
emoji=$(echo "$emoji" | tr '|' ' ')
name=$(echo "$name" | tr '|' ' ')
prev_display=$(echo "$prev_display" | tr '|' ' ')
new_display=$(echo "$new_display" | tr '|' ' ')
TABLE_ROWS+=("$emoji|$name|$prev_display|$new_display")
# Count updates: only "Updated" or version change
case "$new_or_status" in
[Uu]pdated) UPDATES_COUNT=$((UPDATES_COUNT + 1)) ;;
*)
if [[ "$new_or_status" =~ ^[vV]?[0-9] ]] && [[ -n "$prev" && "$prev" != "β€”" && "$new_or_status" != "$prev" ]]; then
UPDATES_COUNT=$((UPDATES_COUNT + 1))
fi
;;
esac
done
# Display final summary table (column -t for aligned columns)
{
echo "$TABLE_HEADER"
for row in "${TABLE_ROWS[@]}"; do
clean_row=$(echo "$row" | sed 's/\x1b\[[0-9;]*m//g')
col_count=$(echo "$clean_row" | awk -F'|' '{print NF}')
if [ "$col_count" -eq 4 ]; then
echo "$clean_row"
fi
done
} | column -t -s "|" | sed 's/^/ /'
echo ""
fi
# Calculate total time
TOTAL_TIME=$SECONDS
hours=$((TOTAL_TIME / 3600))
minutes=$(((TOTAL_TIME % 3600) / 60))
seconds=$((TOTAL_TIME % 60))
time_str=$(printf "%02d:%02d:%02d" $hours $minutes $seconds)
# Final success card
gum style --border double --border-foreground 63 --foreground 63 --padding "1 2" \
"πŸŽ‰ Update Complete!"
if [ "$UPDATES_COUNT" -eq 0 ]; then
gum style --foreground 240 "No updates performed β€” everything was already up to date."
else
gum style --foreground 2 "$UPDATES_COUNT update(s) performed."
fi
gum style --foreground 240 "Total time: $time_str"
gum format <<EOF
A Mac that's up to date means:
β€’ Better security (recent patches and fixes)
β€’ Better performance and battery life
β€’ Up-to-date dev tools and runtimes for your projects
β€’ Fewer bugs and vulnerabilities.
EOF
# Highlight final message: title in pink, total time on same line in another color, no parentheses
FINAL_TIME_COLOR=$'\033[38;5;87m'
FINAL_TIME_RESET=$'\033[0m'
final_msg="πŸŽ‰ Update check complete! Total time: ${FINAL_TIME_COLOR}${time_str}${FINAL_TIME_RESET}"
gum style --foreground 212 --border-foreground 212 --border double --align center --width 56 --margin "0 0" --padding "2 4" \
"$final_msg"
gum format <<EOF
πŸ’‘ Run this script regularly (e.g. weekly) to keep your Mac healthy:
system and app updates, dev stack, and caches stay in good shape.
EOF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment