-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathupdate.sh
More file actions
executable file
·563 lines (521 loc) · 24.7 KB
/
update.sh
File metadata and controls
executable file
·563 lines (521 loc) · 24.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
#!/usr/bin/env bash
# Update all managed tools and plugins
# Safe to run periodically — updates in-place, no reinstall needed
#
# Usage: update.sh [--check] [tool ...]
#
# --check, -c Show current vs latest versions without making any changes
# --help, -h Show this message
#
# Available tools (pass one or more to update only those):
# apt omz tmux-plugins zsh-plugins fzf rg fd shellcheck
# zoxide delta eza uv ruff neovim cheat pre-commit xcape
#
# A PATH shadow check always runs at the end (read-only). It detects older
# binaries at higher-priority PATH locations that would hide managed versions
# installed to /usr/local/bin (e.g. nvim, xcape).
#
# Examples:
# ./update.sh --check # check all versions + shadow report
# ./update.sh --check neovim fzf # check only neovim and fzf (+ shadow report)
# ./update.sh neovim # update only neovim (+ shadow report)
# NOSUDO=1 ./update.sh # skip apt upgrade, skip all sudo operations
set -euo pipefail
DOTFILES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${DOTFILES_DIR}/lib/utils.sh"
ZSH_CUSTOM="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}"
ARCH=$(uname -m) # x86_64, aarch64, armv7l, …
# ── Argument parsing ───────────────────────────────────────────────────────────
CHECK_ONLY=false
SELECTED=()
while [[ $# -gt 0 ]]; do
case "$1" in
--check|-c) CHECK_ONLY=true ;;
--help|-h)
sed -n '5,17p' "${BASH_SOURCE[0]}" | sed 's/^# \?//'
exit 0
;;
-*) echo "Unknown option: $1" >&2; exit 1 ;;
*) SELECTED+=("$1") ;;
esac
shift
done
_KNOWN_TOOLS=(apt omz tmux-plugins zsh-plugins fzf rg fd shellcheck
zoxide delta eza uv ruff neovim cheat pre-commit xcape)
# Validate SELECTED against known tool names.
for _sel in "${SELECTED[@]+"${SELECTED[@]}"}"; do
_found=false
for _known in "${_KNOWN_TOOLS[@]}"; do
[[ "$_sel" == "$_known" ]] && _found=true && break
done
if ! $_found; then
echo "Unknown tool: '$_sel'" >&2
echo "Run '${BASH_SOURCE[0]} --help' for available tools." >&2
exit 1
fi
done
unset _sel _found _known
# Returns 0 (run this step) or 1 (skip it).
_should_run() {
[[ ${#SELECTED[@]} -eq 0 ]] && return 0
local name
for name in "${SELECTED[@]}"; do
[[ "$name" == "$1" ]] && return 0
done
return 1
}
# Update a standard GitHub binary release using Rust triple naming (tar.gz).
# Usage: _update_std_tool CMD LABEL REPO GNU_ARM [BINARY] [ASSET_PREFIX]
# GNU_ARM: "gnu" → aarch64 uses GNU triple; otherwise musl for both arches
_update_std_tool() {
local cmd="$1" label="$2" repo="$3" gnu_arm="$4"
local binary="${5:-$1}" prefix="${6:-}"
_should_run "$cmd" || return 0
log_step "$label"
if ! has "$cmd"; then
log_warn "$label not installed — skipping"
return
fi
local arch
case "$ARCH" in
x86_64) arch="x86_64-unknown-linux-musl" ;;
aarch64) [ "$gnu_arm" = "gnu" ] && arch="aarch64-unknown-linux-gnu" || arch="aarch64-unknown-linux-musl" ;;
*) log_warn "$label: unsupported arch $ARCH — skipping"; return ;;
esac
local dest; dest=$(_resolve_dest "$binary" ~/.local/bin/"$binary")
_gh_update_binary "$cmd" "$repo" "${prefix}${arch}.tar.gz" "$binary" "$dest" || true
}
# uv installs two binaries (uv + uvx) from one tarball — needs a custom handler.
_do_update_uv() {
local uv_arch
case "$ARCH" in
x86_64) uv_arch="x86_64-unknown-linux-musl" ;;
aarch64) uv_arch="aarch64-unknown-linux-musl" ;;
*) log_warn "uv: unsupported arch $ARCH — skipping"; return ;;
esac
if $CHECK_ONLY; then
local current; current=$(_cmd_version uv --version) || current=""
local latest; latest=$(_gh_latest_tag "astral-sh/uv") || latest=""
_report_version uv "$current" "${latest:-unknown}"
return
fi
local url; url=$(_gh_latest_release "astral-sh/uv" "uv-${uv_arch}.tar.gz") || url=""
if [ -z "$url" ]; then log_warn "uv: could not fetch release URL — skipping"; return; fi
local tmp; tmp=$(mktemp -d)
# shellcheck disable=SC2064
trap "rm -rf '$tmp'" RETURN
if has curl; then curl -sfL "$url" | tar -xz -C "$tmp" \
|| { log_warn "uv: download/extraction failed — skipping"; return; }
else wget -qO- "$url" | tar -xz -C "$tmp" \
|| { log_warn "uv: download/extraction failed — skipping"; return; }; fi
for bin in uv uvx; do
local found; found=$(find "$tmp" -name "$bin" -type f | head -1)
[ -n "$found" ] && mv "$found" ~/.local/bin/"$bin" && chmod +x ~/.local/bin/"$bin"
done
if has uv; then
log_ok "uv updated → ~/.local/bin ($(uv --version 2>/dev/null))"
_verify_dest uv ~/.local/bin/uv
_verify_dest uvx ~/.local/bin/uvx
else
log_warn "uv: binary not found in archive — update may have failed"
fi
}
# neovim uses a directory tree (not a single binary) and skips when already current.
_do_update_neovim() {
local nvim_arch
case "$ARCH" in
x86_64) nvim_arch="linux-x86_64" ;;
aarch64) nvim_arch="linux-arm64" ;;
*) log_warn "neovim: unsupported arch $ARCH — skipping"; return ;;
esac
local latest_tag="" nvim_url=""
read -r latest_tag nvim_url < <(_gh_release_info "neovim/neovim" "nvim-${nvim_arch}.tar.gz") || true
local latest="${latest_tag#v}"
local current; current=$(_cmd_version nvim --version) || current=""
if [ -z "$latest" ]; then
log_warn "neovim: could not fetch release info — skipping"
return
fi
if $CHECK_ONLY; then
_report_version neovim "${current:-none}" "$latest_tag"
if $CAN_SUDO; then
log_info " → update would install to /usr/local/ (sudo available)"
else
log_info " → update would install to ~/.local/ (no sudo)"
fi
return
fi
if [ "$current" = "$latest" ]; then
log_ok "neovim $latest_tag already up to date"
return
fi
log_info "neovim: upgrading ${current:-none} → $latest"
if [ -z "$nvim_url" ]; then
log_warn "neovim: could not fetch download URL — skipping"
return
fi
local tmp; tmp=$(mktemp -d)
# shellcheck disable=SC2064
trap "rm -rf '$tmp'" RETURN
if has curl; then curl -sfL "$nvim_url" | tar -xz -C "$tmp" \
|| { log_warn "neovim: download/extraction failed — skipping"; return; }
else wget -qO- "$nvim_url" | tar -xz -C "$tmp" \
|| { log_warn "neovim: download/extraction failed — skipping"; return; }; fi
local extracted; extracted=$(find "$tmp" -maxdepth 1 -type d -name 'nvim-*' | head -1)
if [ -z "$extracted" ]; then log_warn "neovim: unexpected archive layout — skipping"; return; fi
local nvim_dest
if $CAN_SUDO; then
$SUDO cp -r "$extracted"/. /usr/local/
nvim_dest=/usr/local/bin/nvim
else
cp -r "$extracted"/. "$HOME/.local/"
nvim_dest=$HOME/.local/bin/nvim
fi
log_ok "neovim updated → $(nvim --version 2>/dev/null | head -1)"
_verify_dest nvim "$nvim_dest"
}
# xcape is source-built (no versioned releases) — tmpdir must live in a
# function so `trap ... RETURN` fires correctly on function exit.
_do_update_xcape() {
if has xcape; then
if $CHECK_ONLY; then
_check_git_updates "xcape" "" 2>/dev/null || true
log_info "xcape: installed at $(command -v xcape) — source-built from alols/xcape (no version tags)"
if $CAN_SUDO; then
log_info " → run './update.sh xcape' to rebuild from latest source"
else
log_warn " → sudo required to reinstall xcape"
fi
else
if ! $CAN_SUDO; then
log_warn "xcape: sudo required to install build deps and binary — skipping"
else
log_info "xcape: rebuilding from source (alols/xcape)"
apt_install libxtst-dev libx11-dev pkg-config make gcc
local _xc_tmp; _xc_tmp=$(mktemp -d)
# shellcheck disable=SC2064
trap "rm -rf '$_xc_tmp'" RETURN
if git clone --depth=1 https://github.com/alols/xcape.git "$_xc_tmp/xcape" 2>/dev/null \
&& make -C "$_xc_tmp/xcape" 2>/dev/null; then
$SUDO install -m 755 "$_xc_tmp/xcape/xcape" /usr/local/bin/xcape
log_ok "xcape rebuilt → /usr/local/bin/xcape"
else
log_warn "xcape: build failed — skipping"
fi
fi
fi
else
log_warn "xcape not installed — run install.sh workstation first"
log_warn " Requires: sudo apt-get install -y libxtst-dev libx11-dev pkg-config make gcc"
fi
}
# ── PATH shadow check ─────────────────────────────────────────────────────────
# Detect binaries at higher-priority PATH locations that shadow dotfiles-managed
# versions at /usr/local/bin. Runs always (read-only, no side effects).
#
# Only /usr/local/bin tools need this check — tools at ~/.local/bin are already
# at the highest dotfiles-managed PATH priority and cannot be shadowed by the
# installer itself.
_check_path_shadows() {
log_step "PATH shadow check"
# Collect PATH dirs that appear before /usr/local/bin.
# These are the only locations that can shadow /usr/local/bin binaries.
local -a _before=() _all_dirs=()
local _dir _found_usr_local=false
IFS=: read -ra _all_dirs <<< "${PATH:-}"
for _dir in "${_all_dirs[@]}"; do
if [ "$_dir" = "/usr/local/bin" ]; then
_found_usr_local=true
break
fi
_before+=("$_dir")
done
# If /usr/local/bin is not in PATH at all, the check is meaningless —
# managed binaries there are unreachable regardless of shadows.
if ! $_found_usr_local; then
log_info "PATH shadow check: /usr/local/bin not in PATH — skipping (run from zsh after exec zsh)"
return 0
fi
if [ ${#_before[@]} -eq 0 ]; then
log_ok "/usr/local/bin is first in PATH — no shadow risk"
return 0
fi
local _any_issue=false _any_checked=false _tool _canonical _shadow _cv _sv
for _tool in nvim xcape; do
_canonical="/usr/local/bin/$_tool"
[ -x "$_canonical" ] || continue # not installed at /usr/local/bin
_any_checked=true
_shadow=""
for _dir in "${_before[@]}"; do
[ -x "$_dir/$_tool" ] && _shadow="$_dir/$_tool" && break
done
[ -z "$_shadow" ] && continue # no shadow for this tool — silent in clean case
_cv=$(_cmd_version "$_canonical" --version) || _cv=""
_sv=$(_cmd_version "$_shadow" --version) || _sv=""
if [ -z "$_cv" ] || [ -z "$_sv" ]; then
log_warn "$_tool: $_shadow shadows /usr/local/bin/$_tool (cannot read versions — inspect manually)"
_any_issue=true
elif _ver_older_than "$_sv" "$_cv"; then
log_warn "$_tool: $_shadow ($_sv) shadows /usr/local/bin/$_tool ($_cv)"
log_warn " Fix: rm $_shadow"
_any_issue=true
elif [ "$_sv" = "$_cv" ]; then
log_warn "$_tool: duplicate at $_shadow — same version as /usr/local/bin/$_tool"
log_warn " Consider: rm $_shadow"
_any_issue=true
else
log_info "$_tool: $_shadow ($_sv) supersedes /usr/local/bin/$_tool ($_cv) — custom newer version"
fi
done
if ! $_any_checked; then
log_info "No tools installed at /usr/local/bin — shadow check not applicable"
elif ! $_any_issue; then
log_ok "No PATH shadows detected for /usr/local/bin tools"
fi
}
# ── System packages ────────────────────────────────────────────────────────────
log_info "Checking sudo access…"
detect_sudo
case "$SUDO_STATUS" in
root) log_ok "Running as root — apt upgrades will run directly" ;;
sudo_passwordless) log_ok "sudo available — apt upgrades will run via sudo" ;;
sudo_password)
log_ok "sudo available — apt upgrades will run via sudo"
log_warn "sudo requires a password — you will be prompted when apt runs" ;;
nosudo) log_warn "No sudo — apt upgrade skipped" ;;
esac
if _should_run apt; then
log_step "System packages (apt)"
if $CAN_SUDO; then
if $CHECK_ONLY; then
apt list --upgradable 2>/dev/null | grep -v '^Listing' || true
log_info " → sudo required to apply these updates"
else
$SUDO apt-get -yq update
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get -yq upgrade
log_ok "System packages updated"
fi
else
log_warn "No sudo — skipping apt upgrade"
fi
fi
# ── oh-my-zsh ──────────────────────────────────────────────────────────────────
if _should_run omz; then
log_step "oh-my-zsh"
if [ -d ~/.oh-my-zsh ]; then
if $CHECK_ONLY; then
_check_git_updates "oh-my-zsh" ~/.oh-my-zsh
else
if zsh -c 'source ~/.oh-my-zsh/oh-my-zsh.sh; omz update --unattended' 2>/dev/null; then
log_ok "oh-my-zsh updated"
elif git -C ~/.oh-my-zsh pull --quiet; then
log_ok "oh-my-zsh updated (via git pull)"
else
log_warn "oh-my-zsh: update failed (both omz and git pull) — skipping"
fi
fi
else
log_warn "oh-my-zsh not installed — skipping"
fi
fi
# ── tmux plugins ───────────────────────────────────────────────────────────────
if _should_run tmux-plugins; then
log_step "tmux plugins"
for _tmux_plugin in tmux-resurrect tmux-continuum tmux-fzf tmux-cpu; do
if $CHECK_ONLY; then
_check_git_updates "$_tmux_plugin" "$HOME/.tmux/plugins/$_tmux_plugin"
else
_update_plugin "$_tmux_plugin" "$HOME/.tmux/plugins/$_tmux_plugin"
fi
done
fi
# ── zsh plugins ────────────────────────────────────────────────────────────────
if _should_run zsh-plugins; then
log_step "zsh plugins"
if $CHECK_ONLY; then
_check_git_updates "powerlevel10k" "$ZSH_CUSTOM/themes/powerlevel10k"
_check_git_updates "zsh-autosuggestions" "$ZSH_CUSTOM/plugins/zsh-autosuggestions"
_check_git_updates "fast-syntax-highlighting" "$ZSH_CUSTOM/plugins/fast-syntax-highlighting"
_check_git_updates "fzf-tab" "$ZSH_CUSTOM/plugins/fzf-tab"
else
_update_plugin "powerlevel10k" "$ZSH_CUSTOM/themes/powerlevel10k"
_update_plugin "zsh-autosuggestions" "$ZSH_CUSTOM/plugins/zsh-autosuggestions"
_update_plugin "fast-syntax-highlighting" "$ZSH_CUSTOM/plugins/fast-syntax-highlighting"
_update_plugin "fzf-tab" "$ZSH_CUSTOM/plugins/fzf-tab"
fi
fi
# ── fzf ────────────────────────────────────────────────────────────────────────
# Installed via git clone (~/.fzf) — update by pulling the repo.
# Shell integration lives in ~/.fzf.zsh (generated by ~/.fzf/install at
# install time) and is sourced by .zshrc.
if _should_run fzf; then
log_step "fzf"
if [ -d ~/.fzf/.git ]; then
if $CHECK_ONLY; then
_check_git_updates "fzf" ~/.fzf
else
if git -C ~/.fzf pull --quiet --rebase; then
ver=$(_cmd_version ~/.fzf/bin/fzf --version) || ver="?"
log_ok "fzf updated → ~/.fzf ($ver)"
else
log_warn "fzf: git pull failed — skipping"
fi
fi
elif has fzf; then
log_warn "fzf: not managed as a git clone at ~/.fzf — skipping (update manually)"
else
log_warn "fzf not installed — skipping"
fi
fi
# ── ripgrep ────────────────────────────────────────────────────────────────────
_update_std_tool rg "ripgrep" "BurntSushi/ripgrep" gnu
# ── fd ─────────────────────────────────────────────────────────────────────────
# Special case: fd may be installed as fdfind (Debian/Ubuntu); always put the
# updated binary at ~/.local/bin/fd regardless of where the current one lives.
if _should_run fd; then
log_step "fd"
if has fd || has fdfind; then
case "$ARCH" in
x86_64) _gh_update_binary fd "sharkdp/fd" "x86_64-unknown-linux-musl.tar.gz" fd ~/.local/bin/fd || true ;;
aarch64) _gh_update_binary fd "sharkdp/fd" "aarch64-unknown-linux-gnu.tar.gz" fd ~/.local/bin/fd || true ;;
*) log_warn "fd: unsupported arch $ARCH — skipping" ;;
esac
else
log_warn "fd not installed — skipping"
fi
fi
# ── shellcheck ─────────────────────────────────────────────────────────────────
# Uses bare arch names (x86_64/aarch64), not Rust triples — keep its own section.
if _should_run shellcheck; then
log_step "shellcheck"
if has shellcheck; then
sc_arch=""
case "$ARCH" in
x86_64) sc_arch="x86_64" ;;
aarch64) sc_arch="aarch64" ;;
*) log_warn "shellcheck: unsupported arch $ARCH — skipping" ;;
esac
if [ -n "$sc_arch" ]; then
dest=$(_resolve_dest shellcheck ~/.local/bin/shellcheck)
_gh_update_binary shellcheck "koalaman/shellcheck" \
"linux.${sc_arch}.tar.xz" shellcheck "$dest" || true
fi
else
log_warn "shellcheck not installed — skipping"
fi
fi
# ── zoxide ──────────────────────────────────────────────────────────────────────────────
_update_std_tool zoxide "zoxide" "ajeetdsouza/zoxide" musl
# ── delta ───────────────────────────────────────────────────────────────────────────────
_update_std_tool delta "delta" "dandavison/delta" gnu
# ── eza ────────────────────────────────────────────────────────────────────────────────
# Asset pattern has an eza_ prefix before the arch triple.
_update_std_tool eza "eza" "eza-community/eza" musl eza "eza_"
# ── uv ───────────────────────────────────────────────────────────────────────────
# Custom: installs two binaries (uv + uvx) from a single tarball.
if _should_run uv; then
log_step "uv"
if has uv; then
_do_update_uv
else
log_warn "uv not installed — skipping (run install.sh workstation first)"
fi
fi
# ── ruff ───────────────────────────────────────────────────────────────────────
if _should_run ruff; then
log_step "ruff"
if has uv && uv tool list 2>/dev/null | grep -q '^ruff '; then
if $CHECK_ONLY; then
log_info "ruff: installed=$(ruff --version 2>/dev/null) (latest: pypi.org/project/ruff)"
else
uv tool upgrade ruff --quiet
log_ok "ruff updated ($(ruff --version 2>/dev/null))"
fi
elif has ruff; then
log_warn "ruff: not managed by uv tool — skipping (update manually)"
else
log_warn "ruff not installed — skipping"
fi
fi
# ── neovim ────────────────────────────────────────────────────────────────────────────
# Custom: extracts a directory tree, single API call, skips when already current.
if _should_run neovim; then
log_step "neovim"
_do_update_neovim
fi
# ── cheat ──────────────────────────────────────────────────────────────────────
# Custom: single compressed binary (gunzip, not tarball).
if _should_run cheat; then
log_step "cheat"
if [ -f ~/.local/bin/cheat ]; then
if $CHECK_ONLY; then
current=$(_cmd_version ~/.local/bin/cheat --version) || current=""
latest=$(_gh_latest_tag "cheat/cheat") || latest=""
_report_version cheat "$current" "${latest:-unknown}"
else
case "$ARCH" in
x86_64) cheat_arch="amd64" ;;
aarch64) cheat_arch="arm64" ;;
*) cheat_arch="" ;;
esac
if [ -z "$cheat_arch" ]; then
log_warn "cheat: unsupported arch $ARCH — skipping"
else
url=$(_gh_latest_release "cheat/cheat" "linux-${cheat_arch}.gz") || url=""
if [ -n "$url" ]; then
_cheat_ok=true
if has curl; then curl -sfL "$url" | gunzip > ~/.local/bin/cheat || _cheat_ok=false
else wget -qO- "$url" | gunzip > ~/.local/bin/cheat || _cheat_ok=false; fi
if $_cheat_ok; then
chmod +x ~/.local/bin/cheat
log_ok "cheat updated"
_verify_dest cheat ~/.local/bin/cheat
else
log_warn "cheat: download/decompression failed — skipping"
rm -f ~/.local/bin/cheat
fi
else
log_warn "cheat: could not fetch release URL — skipping"
fi
fi
fi
else
log_warn "cheat not installed — run install.sh workstation first"
fi
fi
# ── xcape ──────────────────────────────────────────────────────────────────────
# Source-built from https://github.com/alols/xcape (no versioned releases).
# Deps: libxtst-dev libx11-dev pkg-config make gcc
if _should_run xcape; then
log_step "xcape"
_do_update_xcape
fi
# ── pre-commit common repo ─────────────────────────────────────────────────────
if _should_run pre-commit; then
log_step "pre-commit (common repo)"
PRECOMMIT_REPO="${PRECOMMIT_REPO:-$HOME/projects/common/pre-commit}"
if [ -d "$PRECOMMIT_REPO/.git" ]; then
if $CHECK_ONLY; then
_check_git_updates "pre-commit" "$PRECOMMIT_REPO"
else
if git -C "$PRECOMMIT_REPO" pull --quiet --rebase; then
log_ok "pre-commit repo updated ($PRECOMMIT_REPO)"
log_info " Note: re-run pre-commit/install.sh in each project to upgrade the tool version"
else
log_warn "pre-commit repo pull failed — skipping (local changes or network issue?)"
fi
fi
else
log_warn "pre-commit repo not found at $PRECOMMIT_REPO — skipping"
log_warn " To override: PRECOMMIT_REPO=/your/path ./update.sh"
fi
fi
# ── PATH shadow check (always runs — read-only) ───────────────────────────────
_check_path_shadows
echo ""
if $CHECK_ONLY; then
log_ok "Check complete"
else
log_ok "Update complete — restart your shell to apply changes"
fi
echo ""