@@ -257,144 +257,6 @@ _json_string() {
257257 sed -n " s/.*\" $key \" [[:space:]]*:[[:space:]]*\"\\ ([^\" ]*\\ )\" .*/\\ 1/p" " $file " | head -n 1
258258}
259259
260- _installed_release_tag () {
261- local dir=" $1 "
262-
263- _json_string " $dir /.shdeps-install.json" " tag"
264- }
265-
266- _release_tag_newer () {
267- local latest=" $1 " current=" $2 "
268-
269- [[ -n " $latest " ]] || return 1
270- # When no current tag exists we cannot compute "newer than"; let the
271- # caller decide whether to install. (The bootstrap refresh path already
272- # short-circuits this case; this branch is here for direct callers.)
273- [[ -n " $current " ]] || return 0
274- [[ " $( _natural_tag_cmp " $latest " " $current " ) " == " gt" ]]
275- }
276-
277- # Natural-order tag comparison shared with the Rust self-update path.
278- #
279- # Stable lexical comparison of release tags works for shdeps' own
280- # `YYYYMMDD-HHMMSS-<hash>` format because the timestamp prefix is
281- # zero-padded. It silently breaks down for any tag scheme with a numeric
282- # component that grows past 9 (e.g., `v1.10` vs `v1.9`, where lexical
283- # comparison incorrectly orders `v1.10` before `v1.9`). The Rust
284- # implementation in `src/self_update.rs::compare_tags` already does a
285- # natural sort that treats numeric runs by trimmed length and then by
286- # bytes, with numeric runs sorting after text runs. Mirror that here so
287- # bootstrap-time and runtime self-update agree on which release is newer
288- # for every possible tag scheme.
289- #
290- # Echoes one of `lt`, `eq`, `gt` to stdout. Stays Bash 3.2-compatible
291- # because `install.sh` runs from the curl-pipe path on macOS's stock
292- # /bin/bash.
293- _natural_tag_cmp () {
294- local left=" $1 " right=" $2 "
295- local left_pos=0 right_pos=0
296- local left_len=${# left} right_len=${# right}
297- local lc rc l_run r_run l_digit r_digit l_trim r_trim
298-
299- while [[ $left_pos -lt $left_len || $right_pos -lt $right_len ]]; do
300- if [[ $left_pos -ge $left_len ]]; then
301- echo " lt"
302- return
303- fi
304- if [[ $right_pos -ge $right_len ]]; then
305- echo " gt"
306- return
307- fi
308-
309- # Determine whether the next run on each side is numeric or textual.
310- lc=" ${left: $left_pos : 1} "
311- rc=" ${right: $right_pos : 1} "
312- case " $lc " in [0-9]) l_digit=1 ;; * ) l_digit=0 ;; esac
313- case " $rc " in [0-9]) r_digit=1 ;; * ) r_digit=0 ;; esac
314-
315- # Extract the maximal same-class run from each side.
316- l_run=" "
317- if [[ $l_digit -eq 1 ]]; then
318- while [[ $left_pos -lt $left_len ]]; do
319- case " ${left: $left_pos : 1} " in [0-9]) ;; * ) break ;; esac
320- l_run=" $l_run ${left: $left_pos : 1} "
321- left_pos=$(( left_pos + 1 ))
322- done
323- else
324- while [[ $left_pos -lt $left_len ]]; do
325- case " ${left: $left_pos : 1} " in [0-9]) break ;; esac
326- l_run=" $l_run ${left: $left_pos : 1} "
327- left_pos=$(( left_pos + 1 ))
328- done
329- fi
330- r_run=" "
331- if [[ $r_digit -eq 1 ]]; then
332- while [[ $right_pos -lt $right_len ]]; do
333- case " ${right: $right_pos : 1} " in [0-9]) ;; * ) break ;; esac
334- r_run=" $r_run ${right: $right_pos : 1} "
335- right_pos=$(( right_pos + 1 ))
336- done
337- else
338- while [[ $right_pos -lt $right_len ]]; do
339- case " ${right: $right_pos : 1} " in [0-9]) break ;; esac
340- r_run=" $r_run ${right: $right_pos : 1} "
341- right_pos=$(( right_pos + 1 ))
342- done
343- fi
344-
345- # Cross-class: numeric runs sort AFTER text runs so `v10` > `vbeta`.
346- if [[ $l_digit -ne $r_digit ]]; then
347- if [[ $l_digit -eq 1 ]]; then echo " gt" ; else echo " lt" ; fi
348- return
349- fi
350-
351- if [[ $l_digit -eq 1 ]]; then
352- # Numeric: trim leading zeros, compare by length first (so 10 > 9
353- # without ever fitting into a fixed-width integer), then by bytes
354- # for the same-length case.
355- l_trim=" ${l_run# " ${l_run%% [!0]* } " } "
356- r_trim=" ${r_run# " ${r_run%% [!0]* } " } "
357- [[ -z " $l_trim " ]] && l_trim=" 0"
358- [[ -z " $r_trim " ]] && r_trim=" 0"
359- if [[ ${# l_trim} -lt ${# r_trim} ]]; then
360- echo " lt"
361- return
362- fi
363- if [[ ${# l_trim} -gt ${# r_trim} ]]; then
364- echo " gt"
365- return
366- fi
367- # `<` / `>` inside `[[ ]]` is lexicographic; on equal-length pure
368- # digit strings that matches numeric ordering exactly. Use it
369- # instead of `(( ))` so the comparison stays correct for numbers
370- # that exceed bash's integer width. shellcheck SC2071 misreads
371- # the intent so disable it inline rather than reshape the code.
372- # shellcheck disable=SC2071
373- if [[ " $l_trim " < " $r_trim " ]]; then
374- echo " lt"
375- return
376- fi
377- # shellcheck disable=SC2071
378- if [[ " $l_trim " > " $r_trim " ]]; then
379- echo " gt"
380- return
381- fi
382- else
383- # shellcheck disable=SC2071
384- if [[ " $l_run " < " $r_run " ]]; then
385- echo " lt"
386- return
387- fi
388- # shellcheck disable=SC2071
389- if [[ " $l_run " > " $r_run " ]]; then
390- echo " gt"
391- return
392- fi
393- fi
394- done
395- echo " eq"
396- }
397-
398260_asset_url () {
399261 local file=" $1 " name=" $2 "
400262
@@ -693,34 +555,6 @@ _cleanup_installed_state_for_source_checkout() {
693555 fi
694556}
695557
696- _refresh_release_install_if_stale () {
697- local current latest repo
698-
699- _uses_default_repo_slug || return 0
700- _is_release_install_dir " $SHDEPS_DIR " || return 0
701-
702- repo=$( _repo_slug)
703- current=$( _installed_release_tag " $SHDEPS_DIR " )
704- # When the local install metadata is missing a `tag` field, we have no
705- # comparison baseline. Treating "no current tag" as "always stale" used
706- # to fire a `_install_release` call on every shell startup, hammering
707- # GitHub for an install that was probably fine. A missing tag indicates
708- # either a fresh install (which `_install_release` ran moments ago) or
709- # a corrupted state file (which the user repairs by re-running the
710- # installer explicitly). Either way, the background refresh should be
711- # a no-op rather than a silent re-download loop.
712- [[ -n " $current " ]] || return 0
713- latest=$( _latest_release_tag " $repo " ) || return 0
714-
715- # Bootstrap self-update has to be conservative but useful. Release tags are
716- # timestamp-prefixed (`YYYYMMDD-HHMMSS-<hash>`), so lexical ordering is enough
717- # to avoid replacing a newer local archive with an older or deleted GitHub
718- # "latest" release while still advancing normal fleet installs promptly.
719- if _release_tag_newer " $latest " " $current " ; then
720- _install_release > /dev/null 2>&1 || true
721- fi
722- }
723-
724558_install_release () {
725559 local platform repo api_url token tmp json tag archive checksum
726560 local archive_url checksum_url archive_api_url checksum_api_url bundle
@@ -1030,7 +864,9 @@ _install() {
1030864# ---------------------------------------------------------------------------
1031865# Designed to be sourced: `. /path/to/install.sh --bootstrap`
1032866#
1033- # Finds shdeps.sh, sources it, symlinks the CLI, and runs self-update.
867+ # Finds shdeps.sh, sources it, and symlinks the CLI. Source checkouts still
868+ # self-update during bootstrap; release installs stay local unless forced so
869+ # callers are not blocked on GitHub before they can render their own UI.
1034870# Clients set env vars (SHDEPS_CONF_DIR, SHDEPS_HOOKS_DIR, etc.) before
1035871# sourcing.
1036872#
@@ -1040,7 +876,7 @@ _bootstrap() {
1040876 # Idempotent — skip if already bootstrapped
1041877 declare -f shdeps_update & > /dev/null && return 0
1042878
1043- local _bs_lib=" " _bs_dir=" "
879+ local _bs_lib=" " _bs_dir=" " _bs_release_install=0
1044880 local _dev_dir=" ${SHDEPS_GIT_DEV_DIR:- $HOME / git} "
1045881
1046882 # Find shdeps.sh: installed-tree env hint → env override → dev clone →
@@ -1051,8 +887,8 @@ _bootstrap() {
1051887 if _bootstrap_lib_is_installed_tree; then
1052888 if _uses_default_repo_slug && [[ -d " $SHDEPS_DIR /.git" ]]; then
1053889 _install_release > /dev/null 2>&1 || true
1054- else
1055- _refresh_release_install_if_stale
890+ elif [[ " ${SHDEPS_FORCE :- 0} " == 1 ]] && _is_release_install_dir " $SHDEPS_DIR " ; then
891+ _install_release > /dev/null 2>&1 || true
1056892 fi
1057893 _bs_lib=" $SHDEPS_DIR /shdeps.sh"
1058894 _bs_dir=" $SHDEPS_DIR "
@@ -1070,8 +906,8 @@ _bootstrap() {
1070906 # this opportunistic: failed downloads, dirty checkouts, or unsupported
1071907 # platforms fall through to the source path so bootstrap still converges.
1072908 _install_release > /dev/null 2>&1 || true
1073- else
1074- _refresh_release_install_if_stale
909+ elif [[ " ${SHDEPS_FORCE :- 0} " == 1 ]] && _is_release_install_dir " $SHDEPS_DIR " ; then
910+ _install_release > /dev/null 2>&1 || true
1075911 fi
1076912 _bs_lib=" $SHDEPS_DIR /shdeps.sh"
1077913 _bs_dir=" $SHDEPS_DIR "
@@ -1086,6 +922,10 @@ _bootstrap() {
1086922 fi
1087923 fi
1088924
925+ if [[ -n " $_bs_dir " ]] && _is_release_install_dir " $_bs_dir " ; then
926+ _bs_release_install=1
927+ fi
928+
1089929 # Bootstrap may discover a dev checkout that has not gone through install.sh
1090930 # yet. Create the root binary link first so sourcing the Rust wrapper and
1091931 # linking the CLI both target this checkout instead of an older PATH command.
@@ -1097,8 +937,12 @@ _bootstrap() {
1097937 # shellcheck source=/dev/null
1098938 . " $_bs_lib " || return 1
1099939
1100- # Pull latest shdeps (skips dirty clones / active development)
1101- if [[ -n " $_bs_dir " ]]; then
940+ # Pull source checkouts during bootstrap, but keep release installs local.
941+ # Release freshness checks can involve GitHub redirects or API calls; doing
942+ # that before the caller has rendered any UI caused slow/noisy `dot update`
943+ # startup during transient GitHub 504s. `SHDEPS_FORCE=1` still refreshes
944+ # release installs above, which keeps explicit "check now" behavior.
945+ if [[ -n " $_bs_dir " && " $_bs_release_install " -eq 0 ]]; then
1102946 # Bootstrap is sourced into callers, so stdout belongs to the caller's
1103947 # script. Self-update is opportunistic here and ignored on failure; keep it
1104948 # quiet for the same reason so status chatter cannot pollute command
0 commit comments