Created
February 9, 2026 15:56
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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