@@ -239,12 +239,30 @@ ask_cmd() { printf '%s%s%s%s' "$RESET" "$ACCENT" "$1" "$RESET$DIM"; }
239239# either way, for a caller to show in full when it is the diagnosis.
240240# <stdout-file>, when not empty, takes the command's stdout instead, for a
241241# caller that parses it.
242+ # The child's own colors and control characters would throw off both the
243+ # width and the redraw, so only its text is shown.
244+ run_live_text () {
245+ sed " s/${ESC} \\ [[0-9;?]*[A-Za-z]//g" | tr ' \t' ' ' | tr -d ' \000-\010\013-\037\177'
246+ }
247+
248+ # A line with a URL in it is usually one to open — a sign-in page — and the
249+ # spinner would cut it to the screen and erase it at the end. So each new one
250+ # is printed in full above the spinner, with line wrap back on for it, and
251+ # stays there.
252+ run_live_urls () {
253+ run_total=" $( wc -l < " $run_log " 2> /dev/null | tr -d ' ' ) "
254+ [ -n " $run_total " ] && [ " $run_total " -gt " $run_seen " ] || return 0
255+ sed -n " $(( run_seen + 1 )) ,${run_total} p" " $run_log " | grep -E ' https?://' | run_live_text |
256+ while IFS= read -r run_url; do
257+ printf ' \r\033[2K\n\033[2K\033[1A\r\033[?7h%s %s\n\033[?7l' " $STEP_INDENT " " $run_url "
258+ done
259+ run_seen=" $run_total "
260+ }
261+
242262# One frame of run_live: the spinner and label, then the log's latest line.
243263run_live_draw () {
244- # The child's own colors and control characters would throw off both
245- # the width and the redraw, so only its text is shown.
246- run_line=" $( tail -n 1 " $run_log " 2> /dev/null | sed " s/${ESC} \\ [[0-9;?]*[A-Za-z]//g" |
247- tr ' \t' ' ' | tr -d ' \000-\010\013-\037\177' | cut -c " 1-${run_width} " ) "
264+ run_live_urls
265+ run_line=" $( tail -n 1 " $run_log " 2> /dev/null | run_live_text | cut -c " 1-${run_width} " ) "
248266 printf ' \r\033[2K%s%s%s%s %s\n\033[2K%s %s%s%s\033[1A\r' \
249267 " $STEP_INDENT " " $ACCENT " " $run_frame " " $RESET " " $run_label " " $STEP_INDENT " " $DIM " " $run_line " " $RESET "
250268}
@@ -254,7 +272,8 @@ run_live() { # label log stdout-file command...
254272 shift 3
255273 : > " $run_log "
256274 [ -n " $run_stdout " ] || run_stdout=" $run_log "
257- if [ ! -t 1 ]; then
275+ # A dumb terminal would print the cursor movement as text, every tick.
276+ if [ ! -t 1 ] || [ " ${TERM:- } " = dumb ]; then
258277 printf ' %s%s %s\n' " $STEP_INDENT " " $MARK_WAIT " " $run_label "
259278 if [ " $run_stdout " = " $run_log " ]; then
260279 if " $@ " < /dev/null >> " $run_log " 2>&1 ; then run_status=0; else run_status=$? ; fi
@@ -293,6 +312,7 @@ run_live() { # label log stdout-file command...
293312 TERM_MODES_CHANGED=yes
294313 (
295314 run_frame=" ${SPIN_FRAMES%% * } "
315+ run_seen=0
296316 while [ -f " $run_flag " ]; do
297317 # Word splitting is the point: one frame per word.
298318 # shellcheck disable=SC2086
@@ -577,7 +597,9 @@ cleanup() {
577597 # run_live hides the cursor and turns off line wrap while its spinner
578598 # runs; an interrupt must not leave the user's terminal that way.
579599 if [ " ${TERM_MODES_CHANGED:- no} " = yes ]; then
580- printf ' \033[?7h\033[?25h'
600+ # Stopped first, or it can draw again after the lines are erased.
601+ kill " ${run_spinner:- } " 2> /dev/null || true
602+ printf ' \r\033[2K\n\033[2K\033[1A\r\033[?7h\033[?25h'
581603 fi
582604 if [ -n " $STAGED " ] && [ -e " $STAGED " ]; then
583605 rm -f " $STAGED "
@@ -730,16 +752,46 @@ case "${SHELL:-}" in
730752 * /zsh) profile=" ${ZDOTDIR:- $HOME } /.zshrc" ;;
731753 * /bash)
732754 # macOS Terminal opens login shells, which read .bash_profile and
733- # never .bashrc.
755+ # never .bashrc — or rather the first of .bash_profile, .bash_login
756+ # and .profile that exists, and only that one. Creating a
757+ # .bash_profile next to someone's .profile would stop bash reading
758+ # their .profile at all, so an existing one wins.
734759 if [ " $( uname -s) " = Darwin ]; then
735760 profile=" $HOME /.bash_profile"
761+ for candidate in .bash_profile .bash_login .profile; do
762+ if [ -f " $HOME /$candidate " ]; then
763+ profile=" $HOME /$candidate "
764+ break
765+ fi
766+ done
736767 else
737768 profile=" $HOME /.bashrc"
738769 fi
739770 ;;
740771 * ) profile=" $HOME /.profile" ;;
741772esac
742773
774+ # Whether a profile line other than a comment names <dir> as a whole path,
775+ # not as a prefix of another (/x/bin in /x/bin2) or a word inside one. Catches
776+ # a line the user wrote by hand, in either spelling of the directory.
777+ profile_names_dir () { # profile_names_dir <profile> <dir>
778+ awk -v d=" $2 " -v before=" :=\" ' ( " -v after=" :\" ' ) " '
779+ /^[ \t]*#/ { next }
780+ {
781+ off = 0
782+ rest = $0
783+ while ((i = index(rest, d)) > 0) {
784+ at = off + i
785+ b = at > 1 ? substr($0, at - 1, 1) : ""
786+ a = substr($0, at + length(d), 1)
787+ if ((b == "" || index(before, b)) && (a == "" || index(after, a))) { found = 1; exit }
788+ off = at
789+ rest = substr($0, at + 1)
790+ }
791+ }
792+ END { exit !found }' " $1 " 2> /dev/null
793+ }
794+
743795# yes when a profile now puts the install dir on PATH but this shell, which
744796# a child process cannot change, does not have it yet.
745797needs_new_shell=no
@@ -749,10 +801,11 @@ if [ "$install_dir_on_path" = no ]; then
749801 echo " "
750802 echo " echo '${path_line} ' >> $( tildify " $profile " ) "
751803 echo " "
752- # A second run must not append a second copy. Matching the directory as
753- # well as the exact line also catches one the user wrote by hand.
804+ # A second run must not append a second copy.
754805 elif [ -f " $profile " ] &&
755- { grep -qF -- " $path_line " " $profile " || grep -qF -- " $INSTALL_DIR " " $profile " ; } 2> /dev/null; then
806+ { grep -qF -- " $path_line " " $profile " 2> /dev/null ||
807+ profile_names_dir " $profile " " $INSTALL_DIR " ||
808+ profile_names_dir " $profile " " $path_dir " ; }; then
756809 step_ok " PATH is already set up in $( tildify " $profile " ) "
757810 needs_new_shell=yes
758811 elif mkdir -p " $( dirname " $profile " ) " 2> /dev/null &&
@@ -970,7 +1023,9 @@ mcp_field() { # mcp_field <key> <object>
9701023 printf ' %s' " $2 " | sed -n " s/.*\" $1 \" :\" \([^\" ]*\)\" .*/\1/p"
9711024}
9721025mcp_rows () { # mcp_rows <json>
973- printf ' %s' " $1 " | grep -o ' {[^{}]*}' | while IFS= read -r mcp_row; do
1026+ # "client" sorts first, so every row starts with it; splitting there
1027+ # keeps a row whole even when its "error" text has braces of its own.
1028+ printf ' %s' " $1 " | awk ' { gsub(/[{]"client":"/, "\n{\"client\":\""); print }' | while IFS= read -r mcp_row; do
9741029 mcp_s=" $( mcp_field status " $mcp_row " ) "
9751030 mcp_v=" $( mcp_field server " $mcp_row " ) "
9761031 mcp_c=" $( mcp_field client " $mcp_row " ) "
@@ -1023,7 +1078,12 @@ if agent_setup_allowed; then
10231078 mcp_grouped " $( cat " ${WORK_DIR} /mcp-install.json" 2> /dev/null) " > " ${WORK_DIR} /mcp-rows"
10241079 # One line per server and outcome, so a server added just now
10251080 # reads differently from one that was already there.
1081+ mcp_trouble=no
10261082 while IFS=' |' read -r status what who; do
1083+ case " $status " in
1084+ installed | already_installed) ;;
1085+ * ) mcp_trouble=yes ;;
1086+ esac
10271087 case " $status " in
10281088 installed) step_ok " Added ${what} to ${who} " ;;
10291089 already_installed) step_skip " ${what} was already in ${who} " ;;
@@ -1034,7 +1094,9 @@ if agent_setup_allowed; then
10341094 esac
10351095 done < " ${WORK_DIR} /mcp-rows"
10361096 if [ " $mcp_ok " = no ]; then
1037- [ -s " ${WORK_DIR} /mcp-rows" ] || step_warn " Could not add the Mapbox MCP servers."
1097+ # A failure no row above accounts for still gets a line of
1098+ # its own, rather than a list of successes.
1099+ [ " $mcp_trouble " = yes ] || step_warn " Not every Mapbox MCP server could be added."
10381100 step_detail " Run 'mapbox mcp install --global' to see why and try again."
10391101 fi
10401102 else
0 commit comments