11#! /usr/bin/env bash
22set -euo pipefail
33trap ' ' PIPE
4+ # Graceful interrupt handling
5+ INTERRUPTED=0
6+ trap ' INTERRUPTED=1; echo; echo "⚠️ Interrupted. Partial summary:"; print_summary; exit 130' INT
47
58DIR=" $( cd " $( dirname " ${BASH_SOURCE[0]} " ) " && pwd) "
69ROOT=" $( cd " $DIR /.." && pwd) "
710VERBOSE=" ${VERBOSE:- 0} "
11+ # Suppress Homebrew auto-update during upgrade runs to reduce noise
12+ export HOMEBREW_NO_AUTO_UPDATE=1
813OFFLINE=" ${OFFLINE:- 0} "
914CLI=" ${PYTHON:- python3} "
1015
1116# Ignore pins: IGNORE_PINS=1 to show all tools regardless of pin status
1217IGNORE_PINS=" ${IGNORE_PINS:- 0} "
1318
19+ # Summary counters
20+ SUMMARY_UPDATED=0
21+ SUMMARY_INSTALLED=0
22+ SUMMARY_SKIPPED=0
23+ SUMMARY_FAILED=0
24+ SUMMARY_REMOVED=0
25+
26+ print_summary () {
27+ local label=" ${1:- interrupted} "
28+ echo " ================================================================================"
29+ echo " Summary${label: + ($label )} "
30+ echo " ================================================================================"
31+ printf " Installed: %d\n" " $SUMMARY_INSTALLED "
32+ printf " Updated: %d\n" " $SUMMARY_UPDATED "
33+ printf " Removed: %d\n" " $SUMMARY_REMOVED "
34+ printf " Skipped: %d\n" " $SUMMARY_SKIPPED "
35+ printf " Failed: %d\n" " $SUMMARY_FAILED "
36+ echo
37+ echo " Re-run: make audit"
38+ }
39+
1440# Category filter: CATEGORY=python,go or --category=python
1541CATEGORY_FILTER=" ${CATEGORY:- } "
1642for arg in " $@ " ; do
@@ -139,6 +165,17 @@ osc8() {
139165 [ -n " $url " ] && printf ' \e]8;;%s\e\\%s\e]8;;\e\\' " $url " " $text " || printf ' %s' " $text "
140166}
141167
168+ # Print installed status line (reusable for auto-update and interactive prompts)
169+ print_installed_status () {
170+ local installed=" $1 "
171+ local method=" $2 "
172+ if [ -z " $installed " ]; then
173+ printf " installed: not installed\n"
174+ else
175+ printf " installed: %s via %s\n" " $installed " " ${method:- unknown} "
176+ fi
177+ }
178+
142179# Check for multiple installations and print warning if found
143180# Args: catalog_tool_name
144181# Returns: 0 always (informational only)
@@ -241,6 +278,7 @@ process_tool() {
241278 printf " installed: %s via %s\n" " $installed " " $method "
242279 printf " target: %s (same)\n" " $( osc8 " $url " " $latest " ) "
243280 check_multi_installs " $catalog_tool "
281+ SUMMARY_SKIPPED=$(( SUMMARY_SKIPPED + 1 ))
244282 printf " up-to-date; skipping.\n"
245283 return 0
246284 fi
@@ -264,8 +302,14 @@ process_tool() {
264302 # BUT: multi-version tools always prompt (more significant operation)
265303 if [ " $auto_update " = " true" ] && [ -z " $is_multi_version " ]; then
266304 printf " \n==> %s %s [auto-update]\n" " $icon " " $display "
267- printf " installed: %s via %s\n" " ${installed:- <none>} " " ${method:- unknown} "
268- printf " target: %s\n" " $( osc8 " $url " " ${latest:- <unknown>} " ) "
305+ print_installed_status " $installed " " $method "
306+ # Show target; for self-managed tools (skip_upstream) show "self-managed" instead of <unknown>
307+ local target_display=" ${latest:- <unknown>} "
308+ local skip_upstream=" $( catalog_get_property " $catalog_tool " skip_upstream) "
309+ if [ " $target_display " = " <unknown>" ] && [ " $skip_upstream " = " true" ]; then
310+ target_display=" self-managed"
311+ fi
312+ printf " target: %s\n" " $( osc8 " $url " " $target_display " ) "
269313 check_multi_installs " $catalog_tool "
270314 printf " auto-updating...\n"
271315
@@ -278,37 +322,51 @@ process_tool() {
278322 fi
279323
280324 # Execute the install with version-specific environment variables
325+ local auto_update_success=0
281326 if [ " $catalog_tool " = " python" ] || [ -n " $is_multi_version " ] && [ " $catalog_tool " = " python" ]; then
282- UV_PYTHON_SPEC=" $latest " " $ROOT " /scripts/$install_cmd || true
327+ UV_PYTHON_SPEC=" $latest " " $ROOT " /scripts/$install_cmd && auto_update_success=1 || true
283328 elif [ " $catalog_tool " = " ruby" ]; then
284- RUBY_VERSION=" $latest " " $ROOT " /scripts/$install_cmd || true
329+ RUBY_VERSION=" $latest " " $ROOT " /scripts/$install_cmd && auto_update_success=1 || true
285330 elif [ " $catalog_tool " = " php" ] && [ -n " $version_cycle " ]; then
286- PHP_VERSION=" $version_cycle " " $ROOT " /scripts/$install_cmd || true
331+ PHP_VERSION=" $version_cycle " " $ROOT " /scripts/$install_cmd && auto_update_success=1 || true
287332 elif [ " $catalog_tool " = " node" ] && [ -n " $version_cycle " ]; then
288- NODE_VERSION=" $version_cycle " " $ROOT " /scripts/$install_cmd || true
333+ NODE_VERSION=" $version_cycle " " $ROOT " /scripts/$install_cmd && auto_update_success=1 || true
289334 elif [ " $catalog_tool " = " go" ] && [ -n " $version_cycle " ]; then
290- GO_VERSION=" $version_cycle " " $ROOT " /scripts/$install_cmd || true
335+ GO_VERSION=" $version_cycle " " $ROOT " /scripts/$install_cmd && auto_update_success=1 || true
291336 else
292- " $ROOT " /scripts/$install_cmd || true
337+ " $ROOT " /scripts/$install_cmd && auto_update_success=1 || true
293338 fi
294339
295340 # Re-audit with fresh collection for this specific tool
296341 CLI_AUDIT_JSON=1 CLI_AUDIT_COLLECT=1 CLI_AUDIT_MERGE=1 " $CLI " audit.py " $tool " > /dev/null 2>&1 || true
297342 reload_audit_json
298343 # Clean up any already-current marker left by installer
299344 rm -f " /tmp/.cli-audit/${catalog_tool} .already-current"
345+ if [ " $auto_update_success " = " 0" ]; then
346+ SUMMARY_FAILED=$(( SUMMARY_FAILED + 1 ))
347+ elif [ -z " $installed " ]; then
348+ SUMMARY_INSTALLED=$(( SUMMARY_INSTALLED + 1 ))
349+ else
350+ SUMMARY_UPDATED=$(( SUMMARY_UPDATED + 1 ))
351+ fi
300352 return 0
301353 fi
302354
303355 # Prompt for installation/update
304356 printf " \n==> %s %s\n" " $icon " " $display "
305357 [ -n " $description " ] && printf " %s\n" " $description "
306358 [ -n " $homepage " ] && printf " Homepage: %s\n" " $( osc8 " $homepage " " $homepage " ) "
307- printf " installed: %s via %s\n " " ${ installed:- <none>} " " ${ method:- unknown} "
359+ print_installed_status " $ installed" " $method "
308360
309361 check_multi_installs " $catalog_tool "
310362
311- printf " target: %s\n" " $( osc8 " $url " " ${latest:- <unknown>} " ) "
363+ # Show target; for self-managed tools (skip_upstream) show "self-managed" instead of <unknown>
364+ local target_display_p=" ${latest:- <unknown>} "
365+ local skip_upstream_p=" $( catalog_get_property " $catalog_tool " skip_upstream) "
366+ if [ " $target_display_p " = " <unknown>" ] && [ " $skip_upstream_p " = " true" ]; then
367+ target_display_p=" self-managed"
368+ fi
369+ printf " target: %s\n" " $( osc8 " $url " " $target_display_p " ) "
312370
313371 # Build install command from catalog metadata (use catalog_tool for script name)
314372 local install_cmd=" install_tool.sh $catalog_tool "
@@ -335,7 +393,7 @@ process_tool() {
335393 fi
336394 printf " r = Remove/uninstall this tool\n"
337395 if [ -n " $is_multi_version " ]; then
338- printf " P = Skip ALL %s cycles (never install any %s)\n " " $catalog_tool " " $catalog_tool "
396+ printf " P = Skip ALL outdated %s cycles\n " " $catalog_tool "
339397 fi
340398 else
341399 printf " y = Install now\n"
@@ -344,7 +402,7 @@ process_tool() {
344402 printf " s = Skip only %s (ask again when newer patch available)\n" " $latest "
345403 if [ -n " $is_multi_version " ]; then
346404 printf " p = Never install %s (skip entire %s.x cycle)\n" " $display " " $version_cycle "
347- printf " P = Skip ALL %s cycles (never install any %s)\n " " $catalog_tool " " $catalog_tool "
405+ printf " P = Skip ALL outdated %s cycles\n " " $catalog_tool "
348406 else
349407 printf " p = Never install (permanently skip this tool)\n"
350408 fi
@@ -419,6 +477,7 @@ process_tool() {
419477 if [ " $upgrade_success " = " 0" ]; then
420478 # Install script failed
421479 printf " \n ⚠️ Upgrade failed (install script error)\n"
480+ SUMMARY_FAILED=$(( SUMMARY_FAILED + 1 ))
422481 prompt_pin_version " $tool " " $installed "
423482 elif [ -n " $binary_already_current " ]; then
424483 # Binary hash matches target release - upgrade succeeded despite version string
@@ -435,6 +494,7 @@ process_tool() {
435494 fi
436495 else
437496 # Upgrade succeeded - remove any existing pin to avoid stale pins
497+ SUMMARY_UPDATED=$(( SUMMARY_UPDATED + 1 ))
438498 local existing_pin=" $( pins_get " $tool " ) "
439499 if [ -n " $existing_pin " ] && [ " $existing_pin " != " never" ]; then
440500 " $ROOT " /scripts/unpin_version.sh " $tool " || true
@@ -478,18 +538,23 @@ process_tool() {
478538 if [ " $upgrade_success_a " = " 0" ]; then
479539 printf " \n ⚠️ Upgrade failed (install script error)\n"
480540 printf " Auto-update is still enabled - will try again next time.\n"
541+ SUMMARY_FAILED=$(( SUMMARY_FAILED + 1 ))
481542 elif [ -n " $binary_already_current_a " ]; then
482543 printf " ✓ Auto-update enabled. Binary already matches target release.\n"
544+ SUMMARY_UPDATED=$(( SUMMARY_UPDATED + 1 ))
483545 elif [ " $new_installed_a " = " $installed " ] && [ " $new_installed_a " != " $latest " ]; then
484546 # Version didn't change - but check for prefix match (e.g., 3.13 vs 3.13.11)
485547 if [[ " $latest " == " $new_installed_a " * ]] || [[ " $new_installed_a " == " $latest " * ]]; then
486548 printf " ✓ Auto-update enabled. This tool will update automatically in future.\n"
549+ SUMMARY_UPDATED=$(( SUMMARY_UPDATED + 1 ))
487550 else
488551 printf " \n ⚠️ Upgrade did not succeed (version unchanged)\n"
489552 printf " Auto-update is still enabled - will try again next time.\n"
553+ SUMMARY_FAILED=$(( SUMMARY_FAILED + 1 ))
490554 fi
491555 else
492556 printf " ✓ Auto-update enabled. This tool will update automatically in future.\n"
557+ SUMMARY_UPDATED=$(( SUMMARY_UPDATED + 1 ))
493558 # Remove any existing pin
494559 local existing_pin_a=" $( pins_get " $tool " ) "
495560 if [ -n " $existing_pin_a " ]; then
@@ -501,6 +566,7 @@ process_tool() {
501566 # Skip this specific patch version only
502567 printf " Skipping only %s (will prompt again when newer patch available)\n" " $latest "
503568 " $ROOT " /scripts/pin_version.sh " $tool " " $latest " || true
569+ SUMMARY_SKIPPED=$(( SUMMARY_SKIPPED + 1 ))
504570 ;;
505571 [p])
506572 if [ -n " $installed " ]; then
@@ -550,8 +616,18 @@ process_tool() {
550616 local still_installed=" $( json_field " $tool " installed) "
551617 if [ -z " $still_installed " ]; then
552618 printf " ✓ %s has been removed\n" " $tool "
619+ SUMMARY_REMOVED=$(( SUMMARY_REMOVED + 1 ))
553620 else
554- printf " ⚠️ %s may not have been fully removed (still detected: %s)\n" " $tool " " $still_installed "
621+ # Check if remaining installation is a system/apt binary that we can't remove
622+ local remaining_method=" $( json_field " $tool " installed_method) "
623+ if [ " $remaining_method " = " apt" ] || [ " $remaining_method " = " system" ]; then
624+ printf " ✓ User-managed %s removed (system %s still present at %s — managed by OS)\n" \
625+ " $tool " " $still_installed " " $remaining_method "
626+ SUMMARY_REMOVED=$(( SUMMARY_REMOVED + 1 ))
627+ else
628+ printf " ⚠️ %s may not have been fully removed (still detected: %s via %s)\n" " $tool " " $still_installed " " ${remaining_method:- unknown} "
629+ SUMMARY_FAILED=$(( SUMMARY_FAILED + 1 ))
630+ fi
555631 fi
556632 else
557633 printf " Tool is not installed, nothing to remove\n"
@@ -570,6 +646,7 @@ process_tool() {
570646 ;;
571647 * )
572648 # User declined (N or empty)
649+ SUMMARY_SKIPPED=$(( SUMMARY_SKIPPED + 1 ))
573650 ;;
574651 esac
575652}
@@ -863,7 +940,7 @@ for category in $(printf '%s\n' "${!CATEGORY_TOOLS[@]}" | while read c; do echo
863940 # Category-level prompt (skip if auto-yes mode)
864941 if [ " ${AUTO_YES_ALL:- } " != " 1" ]; then
865942 printf " Tools: %s\n" " $( echo $tools | tr ' ' ' , ' | sed ' s/^, //' ) "
866- printf " Process this category? [Y/n/a=all/s=skip-all] "
943+ printf " Process this category? [Y/n/a=all categories /s=skip-all] "
867944
868945 cat_ans=" "
869946 if [ -t 0 ]; then
@@ -935,5 +1012,6 @@ if [ -n "$DEPRECATED_TOOLS" ]; then
9351012 fi
9361013fi
9371014
1015+ # Print final summary
9381016echo
939- echo " All done. Re-run: make audit "
1017+ print_summary " "
0 commit comments