Files
script/homebrew/brew-upgrade-manager.sh
T
orion aa85eb32c7 refactor(homebrew): 重构 Homebrew 升级管理脚本的配色和清理策略
- 实现终端主题检测功能,支持深色/浅色模式自动切换配色方案
- 添加 256 色终端支持,优化不同背景下的颜色对比度
- 引入版本变更高亮显示,突出包名、旧版本、新版本的变化
- 集成 OSC 11 协议查询终端背景色,提升跨终端兼容性
- 将清理策略从按日期清理改为统一执行 brew cleanup --prune=all
- 移除自定义缓存清理逻辑,使用 Homebrew 内置清理机制
- 增强输出着色功能,按语义对 Homebrew 输出进行彩色标记
- 更新文档说明新的清理流程和终端配色特性
2026-07-19 01:18:46 +08:00

786 lines
24 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Homebrew 智能升级脚本(失败隔离、选择性重试与性能优化版)
set -euo pipefail
prepend_path_once() {
local dir="$1"
[[ -d "$dir" ]] || return 0
case ":$PATH:" in
*":$dir:"*) ;;
*) PATH="$dir:$PATH" ;;
esac
}
prepend_path_once "/usr/local/sbin"
prepend_path_once "/opt/homebrew/sbin"
export PATH
# 禁止 Homebrew 在每个安装步骤后自动清理;统一在最后一步彻底清理。
export HOMEBREW_NO_INSTALL_CLEANUP=1
hex_channel_to_16bit() {
local hex="$1" value maximum bits
[[ "$hex" =~ ^[[:xdigit:]]{1,4}$ ]] || return 1
value=$((16#$hex))
bits=$((${#hex} * 4))
maximum=$(((1 << bits) - 1))
printf '%s' "$((value * 65535 / maximum))"
}
query_terminal_background_rgb() {
local response="" red_hex green_hex blue_hex red green blue
[[ -r /dev/tty && -w /dev/tty ]] || return 1
command -v stty >/dev/null 2>&1 || return 1
command -v dd >/dev/null 2>&1 || return 1
# OSC 11 是跨终端的背景色查询协议。使用子 shell 确保任何退出路径都会
# 恢复 TTY 设置;短暂无输入超时也避免不支持该协议的终端阻塞流程。
response="$({
saved_tty="$(stty -g < /dev/tty 2>/dev/null)" || exit 1
trap 'stty "$saved_tty" < /dev/tty 2>/dev/null || true' EXIT HUP INT TERM
stty -echo -icanon min 0 time 3 < /dev/tty 2>/dev/null || exit 1
printf '\033]11;?\007' > /dev/tty
LC_ALL=C dd bs=1 count=128 < /dev/tty 2>/dev/null || true
} 2>/dev/null)"
if [[ "$response" =~ rgb:([[:xdigit:]]{1,4})/([[:xdigit:]]{1,4})/([[:xdigit:]]{1,4}) ]]; then
red_hex="${BASH_REMATCH[1]}"
green_hex="${BASH_REMATCH[2]}"
blue_hex="${BASH_REMATCH[3]}"
red="$(hex_channel_to_16bit "$red_hex")" || return 1
green="$(hex_channel_to_16bit "$green_hex")" || return 1
blue="$(hex_channel_to_16bit "$blue_hex")" || return 1
printf '%s %s %s' "$red" "$green" "$blue"
return 0
fi
return 1
}
detect_terminal_theme() {
local requested="${HB_COLOR_THEME:-auto}" background_index="" rgb=""
local red green blue brightness
case "$requested" in
dark|light)
printf '%s' "$requested"
return
;;
auto|'') ;;
*) requested=auto ;;
esac
# COLORFGBG 的最后一项是背景色索引,常见终端会主动提供。
if [[ -n "${COLORFGBG:-}" ]]; then
background_index="${COLORFGBG##*[;:]}"
if [[ "$background_index" =~ ^[0-9]+$ ]]; then
if ((background_index == 0 || background_index == 8 || background_index < 7)); then
printf 'dark'
else
printf 'light'
fi
return
fi
fi
# Apple Terminal 不设置 COLORFGBG,但能返回当前标签页的实际背景色。
if [[ "${TERM_PROGRAM:-}" == "Apple_Terminal" ]] && command -v osascript >/dev/null 2>&1; then
rgb="$(osascript -e 'tell application "Terminal" to get background color of current settings of selected tab of front window' 2>/dev/null || true)"
rgb="${rgb//, / }"
read -r red green blue <<< "$rgb"
if [[ "$red" =~ ^[0-9]+$ && "$green" =~ ^[0-9]+$ && "$blue" =~ ^[0-9]+$ ]]; then
brightness=$(((red * 299 + green * 587 + blue * 114) / 1000))
if ((brightness >= 32768)); then
printf 'light'
else
printf 'dark'
fi
return
fi
fi
# iTerm2、Termius、Kitty、WezTerm、Alacritty 等终端通常支持 OSC 11
# 它也可经由 SSH 返回客户端终端的真实背景色。
rgb="$(query_terminal_background_rgb || true)"
read -r red green blue <<< "$rgb"
if [[ "$red" =~ ^[0-9]+$ && "$green" =~ ^[0-9]+$ && "$blue" =~ ^[0-9]+$ ]]; then
brightness=$(((red * 299 + green * 587 + blue * 114) / 1000))
if ((brightness >= 32768)); then
printf 'light'
else
printf 'dark'
fi
return
fi
# 终端未报告背景时,用 macOS 外观作合理回退;其他平台默认深色。
if [[ "$(uname -s 2>/dev/null || true)" == "Darwin" ]] && \
[[ "$(defaults read -g AppleInterfaceStyle 2>/dev/null || true)" != "Dark" ]]; then
printf 'light'
else
printf 'dark'
fi
}
COLOR_THEME="none"
if [[ -t 1 ]]; then
COLOR_COUNT="$(tput colors 2>/dev/null || true)"
[[ "$COLOR_COUNT" =~ ^[0-9]+$ ]] || COLOR_COUNT=8
COLOR_THEME="$(detect_terminal_theme)"
if ((COLOR_COUNT >= 256)) && [[ "$COLOR_THEME" == "light" ]]; then
# 浅色背景使用更深的色阶,尤其避免亮黄、亮青在白底上失去对比。
RED='\033[38;5;124m'
GREEN='\033[38;5;28m'
YELLOW='\033[38;5;130m'
BLUE='\033[38;5;25m'
MAGENTA='\033[38;5;90m'
CYAN='\033[38;5;30m'
elif ((COLOR_COUNT >= 256)); then
# 深色背景使用偏亮的色阶,保持正文和状态信息清晰。
RED='\033[38;5;203m'
GREEN='\033[38;5;114m'
YELLOW='\033[38;5;221m'
BLUE='\033[38;5;75m'
MAGENTA='\033[38;5;177m'
CYAN='\033[38;5;80m'
elif [[ "$COLOR_THEME" == "light" ]]; then
RED='\033[31m'
GREEN='\033[32m'
YELLOW='\033[33m'
BLUE='\033[34m'
MAGENTA='\033[35m'
CYAN='\033[36m'
else
RED='\033[91m'
GREEN='\033[92m'
YELLOW='\033[93m'
BLUE='\033[94m'
MAGENTA='\033[95m'
CYAN='\033[96m'
fi
BOLD='\033[1m'
NC='\033[0m'
export HOMEBREW_COLOR=1
else
COLOR_COUNT=0
RED=''
GREEN=''
YELLOW=''
BLUE=''
MAGENTA=''
CYAN=''
BOLD=''
NC=''
fi
TERMINAL_WIDTH=""
ACTIVE_LOG_FILE=""
LOCK_ACQUIRED=0
LOCK_DIR="${HB_LOCK_DIR:-${TMPDIR:-/tmp}/brewup-$(id -u).lock}"
FAILED_FORMULAE=()
FAILED_CASKS=()
CLEANUP_FAILED=0
usage() {
cat <<'EOF'
Usage: brew-upgrade-manager.sh [--width N]
Options:
--width N, --width=N Set output width (40-500).
-h, --help Show this help.
EOF
}
parse_args() {
while (($# > 0)); do
case "$1" in
--width)
[[ $# -ge 2 ]] || { print_error "--width requires a value."; exit 2; }
TERMINAL_WIDTH="$2"
shift 2
;;
--width=*)
TERMINAL_WIDTH="${1#*=}"
shift
;;
-h|--help)
usage
exit 0
;;
*)
print_error "Unknown argument: $1"
usage >&2
exit 2
;;
esac
done
}
resolve_terminal_width() {
if [[ -z "$TERMINAL_WIDTH" ]]; then
TERMINAL_WIDTH="${HB_TERMINAL_WIDTH:-}"
fi
if [[ -z "$TERMINAL_WIDTH" ]] && [[ -t 1 ]] && command -v tput >/dev/null 2>&1; then
TERMINAL_WIDTH="$(tput cols 2>/dev/null || true)"
fi
[[ -n "$TERMINAL_WIDTH" ]] || TERMINAL_WIDTH=130
if [[ ! "$TERMINAL_WIDTH" =~ ^[0-9]+$ ]] || ((TERMINAL_WIDTH < 40 || TERMINAL_WIDTH > 500)); then
print_error "terminal width must be an integer between 40 and 500."
exit 2
fi
export COLUMNS="$TERMINAL_WIDTH"
}
separator() {
local i
printf '%b' "$BLUE"
for ((i = 0; i < TERMINAL_WIDTH; i++)); do
printf '='
done
printf '%b\n' "$NC"
}
print_header() {
printf '%b%s%b\n' "$BLUE" "$1" "$NC"
}
print_error() {
printf '%bError:%b %s\n' "$RED" "$NC" "$*" >&2
}
print_warning() {
printf '%bWarning:%b %s\n' "$YELLOW" "$NC" "$*"
}
print_notice() {
printf '%b%s%b\n' "$YELLOW" "$*" "$NC"
}
print_success() {
printf '%b%s%b\n' "$GREEN" "$*" "$NC"
}
print_info() {
printf '%b%s%b\n' "$CYAN" "$*" "$NC"
}
print_colored_list() {
local label="$1" color="$2" items="$3" item
printf '%b%s%b\n' "$color" "$label" "$NC"
while IFS= read -r item; do
[[ -n "$item" ]] || continue
printf ' %b%s%b\n' "$color" "$item" "$NC"
done <<< "$items"
}
print_version_transition() {
local line="$1" indent left arrow new_version suffix old_version package
# Homebrew 常见版本行:
# package 1.0.0 -> 1.1.0
# 1.0.0 -> 1.1.0
# 包名、旧版本、方向和新版本分别着色,让变化能被快速识别。
if [[ "$line" =~ ^([[:space:]]*)(.*[^[:space:]])[[:space:]]+(\-\>|=\>)[[:space:]]+([^[:space:]]+)(.*)$ ]]; then
indent="${BASH_REMATCH[1]}"
left="${BASH_REMATCH[2]}"
arrow="${BASH_REMATCH[3]}"
new_version="${BASH_REMATCH[4]}"
suffix="${BASH_REMATCH[5]}"
old_version="${left##*[[:space:]]}"
package="${left%"$old_version"}"
printf '%s' "$indent"
if [[ -n "$package" ]]; then
printf '%b%s%b' "$BOLD$CYAN" "$package" "$NC"
fi
printf '%b%s%b %b%s%b %b%s%b%s\n' \
"$YELLOW" "$old_version" "$NC" \
"$BLUE" "$arrow" "$NC" \
"$BOLD$GREEN" "$new_version" "$NC" "$suffix"
return 0
fi
return 1
}
colorize_brew_heading() {
local text="${1#==> }" action subject
case "$text" in
Upgrading\ *|Installing\ *|Reinstalling\ *|Fetching\ *|Downloading\ *|Pouring\ *|Would\ upgrade\ *)
action="${text%% *}"
subject="${text#* }"
printf '%b==>%b %b%s%b %b%s%b\n' \
"$BLUE" "$NC" "$CYAN" "$action" "$NC" "$BOLD$GREEN" "$subject" "$NC"
;;
Upgraded\ *|Installed\ *|Successfully\ *)
printf '%b==>%b %b%s%b\n' "$BLUE" "$NC" "$BOLD$GREEN" "$text" "$NC"
;;
Outdated\ *|Disabled\ *|Warning*)
printf '%b==>%b %b%s%b\n' "$BLUE" "$NC" "$YELLOW" "$text" "$NC"
;;
Formulae|Casks|New\ Formulae|New\ Casks|Caveats|Summary)
printf '%b==>%b %b%s%b\n' "$BLUE" "$NC" "$BOLD$MAGENTA" "$text" "$NC"
;;
*)
printf '%b==>%b %b%s%b\n' "$BLUE" "$NC" "$CYAN" "$text" "$NC"
;;
esac
}
colorize_brew_output() {
local line
while IFS= read -r line || [[ -n "$line" ]]; do
if print_version_transition "$line"; then
continue
fi
case "$line" in
'==> '*)
colorize_brew_heading "$line"
;;
Warning:*|warning:*)
printf '%b%s%b\n' "$YELLOW" "$line" "$NC"
;;
Error:*|error:*|Failed:*|failed:*)
printf '%b%s%b\n' "$RED" "$line" "$NC"
;;
Removing:*)
printf '%b%s%b\n' "$YELLOW" "$line" "$NC"
;;
'✔︎ '*|'✔ '*|'🍺 '*|*'was successfully upgraded!'|*'successfully installed!'|All\ dependencies\ satisfied.)
printf '%b%s%b\n' "$GREEN" "$line" "$NC"
;;
Already\ up-to-date.*|Updated\ Homebrew\ *|Updated\ *tap*|Successfully\ rebased\ *|Pruned\ *|This\ operation\ has\ freed\ *)
printf '%b%s%b\n' "$GREEN" "$line" "$NC"
;;
' Downloaded '*|' Verified '*)
printf '%b%s%b\n' "$GREEN" "$line" "$NC"
;;
*)
printf '%s\n' "$line"
;;
esac
done
}
run_brew_colored() {
local pipeline_status=()
# 管道输出时禁用 Homebrew 自带的不完整配色,再统一按语义着色。
if env -u HOMEBREW_COLOR brew "$@" 2>&1 | colorize_brew_output; then
return 0
fi
pipeline_status=("${PIPESTATUS[@]}")
if ((${pipeline_status[1]:-0} != 0)); then
print_error "failed to colorize Homebrew output."
return "${pipeline_status[1]}"
fi
return "${pipeline_status[0]}"
}
is_enabled() {
case "${1:-}" in
1|true|TRUE|yes|YES|on|ON) return 0 ;;
*) return 1 ;;
esac
}
# shellcheck disable=SC2329 # Invoked by EXIT trap.
cleanup_runtime() {
if [[ -n "${ACTIVE_LOG_FILE:-}" && -f "$ACTIVE_LOG_FILE" ]]; then
rm -f "$ACTIVE_LOG_FILE"
fi
if ((LOCK_ACQUIRED)); then
rm -f "$LOCK_DIR/pid"
rmdir "$LOCK_DIR" 2>/dev/null || true
fi
}
# shellcheck disable=SC2329 # Invoked by INT trap.
on_interrupt() {
exit 130
}
# shellcheck disable=SC2329 # Invoked by TERM trap.
on_terminate() {
exit 143
}
trap cleanup_runtime EXIT
trap on_interrupt INT
trap on_terminate TERM
acquire_lock() {
local existing_pid=""
if mkdir "$LOCK_DIR" 2>/dev/null; then
LOCK_ACQUIRED=1
printf '%s\n' "$$" > "$LOCK_DIR/pid"
return 0
fi
if [[ -r "$LOCK_DIR/pid" ]]; then
IFS= read -r existing_pid < "$LOCK_DIR/pid" || true
fi
if [[ "$existing_pid" =~ ^[0-9]+$ ]] && kill -0 "$existing_pid" 2>/dev/null; then
print_error "another brewup process is already running (PID ${existing_pid})."
exit 75
fi
rm -f "$LOCK_DIR/pid" 2>/dev/null || true
rmdir "$LOCK_DIR" 2>/dev/null || {
print_error "unable to remove stale lock directory: $LOCK_DIR"
exit 75
}
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
print_error "another brewup process acquired the lock."
exit 75
fi
LOCK_ACQUIRED=1
printf '%s\n' "$$" > "$LOCK_DIR/pid"
}
retry_attempts() {
local attempts="${HB_RETRY_ATTEMPTS:-3}"
if [[ ! "$attempts" =~ ^[1-9][0-9]*$ ]] || ((attempts > 10)); then
attempts=3
fi
printf '%s' "$attempts"
}
retry_delay_seconds() {
local delay="${HB_RETRY_DELAY_SECONDS:-5}"
if [[ ! "$delay" =~ ^[0-9]+$ ]] || ((delay > 300)); then
delay=5
fi
printf '%s' "$delay"
}
is_non_retryable_brew_error() {
local log_file="$1"
grep -Eiq \
'Running Homebrew as root|Permission denied|Operation not permitted|No space left on device|checksum mismatch|SHA-?256 mismatch|certificate verify failed|SSL certificate problem|curl: \((3|23|26|60)\)|unknown (option|argument)|invalid (option|argument)' \
"$log_file"
}
is_retryable_brew_error() {
local log_file="$1"
grep -Eiq \
'curl: \((5|6|7|18|28|35|52|55|56|92)\)|Transferred a partial file|Could not resolve host|timed out|timeout|connection (reset|refused|closed)|HTTP[^0-9]*(429|5[0-9]{2})|returned error: (429|5[0-9]{2})|temporarily unavailable|failed to download|download.*failed|RPC failed|early EOF|remote end hung up|unexpected disconnect' \
"$log_file"
}
run_brew_with_retries() {
local label="$1"
shift
local attempts delay attempt status next_delay
local pipeline_status=()
attempts="$(retry_attempts)"
delay="$(retry_delay_seconds)"
ACTIVE_LOG_FILE="$(mktemp "${TMPDIR:-/tmp}/brewup-retry.XXXXXX.log")"
for ((attempt = 1; attempt <= attempts; attempt++)); do
if ((attempt > 1)); then
print_info "Retrying ${label} (${attempt}/${attempts}) after ${delay}s..."
sleep "$delay"
next_delay=$((delay * 2))
((next_delay > 300)) && next_delay=300
delay="$next_delay"
fi
: > "$ACTIVE_LOG_FILE"
if env -u HOMEBREW_COLOR brew "$@" 2>&1 | tee "$ACTIVE_LOG_FILE" | colorize_brew_output; then
status=0
else
pipeline_status=("${PIPESTATUS[@]}")
status="${pipeline_status[0]}"
if ((${pipeline_status[1]:-0} != 0)); then
print_error "failed to write retry log."
status="${pipeline_status[1]}"
elif ((${pipeline_status[2]:-0} != 0)); then
print_error "failed to colorize Homebrew output."
status="${pipeline_status[2]}"
fi
fi
if ((status == 0)); then
rm -f "$ACTIVE_LOG_FILE"
ACTIVE_LOG_FILE=""
return 0
fi
if is_non_retryable_brew_error "$ACTIVE_LOG_FILE"; then
rm -f "$ACTIVE_LOG_FILE"
ACTIVE_LOG_FILE=""
return "$status"
fi
if ((attempt == attempts)) || ! is_retryable_brew_error "$ACTIVE_LOG_FILE"; then
rm -f "$ACTIVE_LOG_FILE"
ACTIVE_LOG_FILE=""
return "$status"
fi
print_notice "${label} failed with a retryable error."
done
}
get_outdated_formulae() {
env -u HOMEBREW_COLOR brew outdated --formula 2>/dev/null
}
get_outdated_casks() {
env -u HOMEBREW_COLOR brew outdated --cask --greedy 2>/dev/null
}
run_brew_doctor() {
local output status=0 line
if output="$(env -u HOMEBREW_COLOR brew doctor 2>&1)"; then
status=0
else
status=$?
fi
while IFS= read -r line; do
case "$line" in
Warning:*|*deprecated*|*disabled*|*pre-release*|*"find replacements"*|*"do not provide support"*|*"Tier 2 configuration"*)
printf '%b%s%b\n' "$YELLOW" "$line" "$NC"
;;
' '*|' https://'*)
printf '%b%s%b\n' "$CYAN" "$line" "$NC"
;;
*)
printf '%s\n' "$line"
;;
esac
done <<< "$output"
return "$status"
}
run_formula_upgrade() {
local bulk_status=0 initial_formulae="" initial_formulae_known=1
local outdated_formulae remaining_formulae formula
local failed_formulae=()
if ! initial_formulae="$(get_outdated_formulae)"; then
initial_formulae_known=0
print_warning "unable to list Formulae requiring updates before upgrade."
elif [[ -n "$initial_formulae" ]]; then
print_colored_list "Formulae requiring updates:" "$YELLOW" "$initial_formulae"
else
print_success "No Formulae require updates."
fi
run_brew_colored upgrade --formula || bulk_status=$?
if ((bulk_status == 0)); then
if ((initial_formulae_known)) && [[ -n "$initial_formulae" ]]; then
print_colored_list "Updated Formulae:" "$GREEN" "$initial_formulae"
else
print_success "Formula upgrade completed."
fi
return 0
fi
print_warning "Formula upgrade returned ${bulk_status}; checking remaining Formulae..."
if ! outdated_formulae="$(get_outdated_formulae)"; then
print_warning "unable to verify outdated Formulae."
return "$bulk_status"
fi
if [[ -z "$outdated_formulae" ]]; then
if [[ -n "$initial_formulae" ]]; then
print_colored_list "Updated Formulae:" "$GREEN" "$initial_formulae"
else
print_success "No outdated Formulae remain; the bulk error was recovered."
fi
return 0
fi
while IFS= read -r formula; do
[[ -n "$formula" ]] || continue
print_info "Retrying Formula: ${formula}"
if run_brew_with_retries "Formula ${formula}" upgrade --formula "$formula"; then
:
else
failed_formulae+=("$formula")
fi
done <<< "$outdated_formulae"
if ! remaining_formulae="$(get_outdated_formulae)"; then
print_warning "unable to verify Formulae after individual retries."
FAILED_FORMULAE=("${failed_formulae[@]}")
return 1
fi
if [[ -z "$remaining_formulae" ]]; then
if [[ -n "$initial_formulae" ]]; then
print_colored_list "Updated Formulae after isolated retries:" "$GREEN" "$initial_formulae"
else
print_success "All outdated Formulae were upgraded after isolated retries."
fi
return 0
fi
while IFS= read -r formula; do
[[ -n "$formula" ]] && FAILED_FORMULAE+=("$formula")
done <<< "$remaining_formulae"
print_colored_list "Outdated Formulae remaining after retries:" "$RED" "$remaining_formulae"
return 1
}
run_cask_upgrade() {
local bulk_status=0 initial_casks="" initial_casks_known=1
local outdated_casks remaining_casks cask
local bulk_args=(upgrade --cask --greedy)
local failed_casks=()
if is_enabled "${HB_CASK_FORCE:-0}"; then
bulk_args+=(--force)
print_warning "HB_CASK_FORCE is enabled; existing Cask files may be overwritten."
fi
if ! initial_casks="$(get_outdated_casks)"; then
initial_casks_known=0
print_warning "unable to list Casks requiring updates before upgrade."
elif [[ -n "$initial_casks" ]]; then
print_colored_list "Casks requiring updates:" "$YELLOW" "$initial_casks"
else
print_success "No Casks require updates."
fi
run_brew_colored "${bulk_args[@]}" || bulk_status=$?
if ((bulk_status == 0)); then
if ((initial_casks_known)) && [[ -n "$initial_casks" ]]; then
print_colored_list "Updated Casks:" "$GREEN" "$initial_casks"
else
print_success "Cask upgrade completed."
fi
return 0
fi
print_warning "Cask upgrade returned ${bulk_status}; checking remaining Casks..."
if ! outdated_casks="$(get_outdated_casks)"; then
print_warning "unable to verify outdated Casks."
return "$bulk_status"
fi
if [[ -z "$outdated_casks" ]]; then
if [[ -n "$initial_casks" ]]; then
print_colored_list "Updated Casks:" "$GREEN" "$initial_casks"
else
print_success "No outdated Casks remain; the bulk error was recovered."
fi
return 0
fi
while IFS= read -r cask; do
[[ -n "$cask" ]] || continue
print_info "Retrying Cask: ${cask} (without --force)"
if run_brew_with_retries "Cask ${cask}" upgrade --cask --greedy "$cask"; then
:
else
failed_casks+=("$cask")
fi
done <<< "$outdated_casks"
if ! remaining_casks="$(get_outdated_casks)"; then
print_warning "unable to verify Casks after individual retries."
FAILED_CASKS=("${failed_casks[@]}")
return 1
fi
if [[ -z "$remaining_casks" ]]; then
if [[ -n "$initial_casks" ]]; then
print_colored_list "Updated Casks after isolated retries:" "$GREEN" "$initial_casks"
else
print_success "All outdated Casks were upgraded after isolated retries."
fi
return 0
fi
while IFS= read -r cask; do
[[ -n "$cask" ]] && FAILED_CASKS+=("$cask")
done <<< "$remaining_casks"
print_colored_list "Outdated Casks remaining after retries:" "$RED" "$remaining_casks"
return 1
}
run_cleanup() {
run_brew_colored cleanup --prune=all
}
print_summary() {
local overall_status="$1"
separator
if ((overall_status == 0)); then
print_success "All operations completed successfully."
else
printf '%bHomebrew maintenance completed with unresolved failures.%b\n' "$RED" "$NC"
if ((${#FAILED_FORMULAE[@]} > 0)); then
printf '%bFailed Formulae:%b %s\n' "$RED" "$NC" "${FAILED_FORMULAE[*]}"
fi
if ((${#FAILED_CASKS[@]} > 0)); then
printf '%bFailed Casks:%b %s\n' "$RED" "$NC" "${FAILED_CASKS[*]}"
fi
if ((CLEANUP_FAILED)); then
printf '%bHomebrew cleanup failed.%b\n' "$RED" "$NC"
fi
fi
separator
}
parse_args "$@"
resolve_terminal_width
command -v brew >/dev/null 2>&1 || { print_error "Homebrew is not installed or not in PATH."; exit 127; }
command -v tee >/dev/null 2>&1 || { print_error "tee is required."; exit 127; }
acquire_lock
separator
print_header "Step 1: Updating Homebrew repositories (brew update -v)"
run_brew_with_retries "Homebrew repository update" update -v
separator
printf '\n'
separator
print_header "Step 2: Performing health check (brew doctor)"
if is_enabled "${HB_SKIP_DOCTOR:-0}"; then
print_notice "brew doctor skipped because HB_SKIP_DOCTOR is enabled."
elif ! run_brew_doctor; then
print_warning "'brew doctor' detected issues. Manual review is recommended."
else
print_success "Homebrew environment is in good health."
fi
separator
printf '\n'
separator
print_header "Step 3: Upgrading Formulae and Casks with failure isolation"
overall_status=0
printf '\n'
print_info ">>> [1/2] Upgrading CLI Formulae..."
if ! run_formula_upgrade; then
overall_status=1
fi
printf '\n'
print_info ">>> [2/2] Upgrading GUI Casks..."
if ! run_cask_upgrade; then
overall_status=1
fi
separator
printf '\n'
separator
print_header "Step 4: Cleaning all Homebrew cache and old versions (brew cleanup --prune=all)"
if ! run_cleanup; then
overall_status=1
CLEANUP_FAILED=1
fi
separator
printf '\n'
print_summary "$overall_status"
exit "$overall_status"