@sebastienrousseau/dotfiles 0.2.510 → 0.2.512

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +2 -2
  3. package/docs/AI.md +2 -1
  4. package/docs/COPYRIGHT +1 -1
  5. package/docs/architecture/AI_COST_OPTIMIZATION.md +1 -0
  6. package/docs/archive/LEGACY_ROADMAP.md +4 -158
  7. package/docs/articles/.pages +6 -0
  8. package/docs/articles/2026-07-05-custom-mkdocs-material-dark-theme.md +216 -0
  9. package/docs/articles/2026-07-05-fish-startup-abbr.md +153 -0
  10. package/docs/articles/2026-07-05-master-to-main-rename-runbook.md +128 -0
  11. package/docs/articles/index.md +36 -0
  12. package/docs/manual/00-introduction.md +1 -1
  13. package/docs/manual/02-tutorials/02-add-wallpaper.md +6 -6
  14. package/docs/manual/02-tutorials/03-create-profile.md +1 -1
  15. package/docs/manual/02-tutorials/05-deploy-fleet.md +1 -1
  16. package/docs/manual/03-reference/02-config-files.md +1 -1
  17. package/docs/manual/command-index.md +1 -0
  18. package/docs/operations/ARCHITECTURE_ROADMAP.md +5 -143
  19. package/docs/operations/COVERAGE.md +27 -4
  20. package/docs/operations/ROADMAP.md +3 -159
  21. package/docs/operations/ROADMAP_2026.md +5 -663
  22. package/docs/operations/ROADMAP_V0_2_503.md +5 -129
  23. package/docs/reference/THEMES.md +1 -1
  24. package/docs/reference/TOOLS.md +1 -0
  25. package/docs/reference/UTILS.md +1 -0
  26. package/install.sh +5 -5
  27. package/package.json +1 -1
  28. package/scripts/diagnostics/aliases-manifest.sh +52 -7
  29. package/scripts/diagnostics/benchmark.sh +27 -1
  30. package/scripts/diagnostics/doctor.sh +18 -11
  31. package/scripts/diagnostics/health.sh +8 -3
  32. package/scripts/diagnostics/mcp-doctor.sh +3 -0
  33. package/scripts/diagnostics/secret-governance.sh +7 -1
  34. package/scripts/diagnostics/security-score.sh +14 -5
  35. package/scripts/diagnostics/verify_state.sh +15 -3
  36. package/scripts/diagnostics/version-locks.sh +10 -6
  37. package/scripts/dot/commands/agent.sh +5 -1
  38. package/scripts/dot/commands/ai.sh +37 -30
  39. package/scripts/dot/commands/aliases.sh +28 -4
  40. package/scripts/dot/commands/appearance.sh +16 -0
  41. package/scripts/dot/commands/core.sh +17 -0
  42. package/scripts/dot/commands/diagnostics.sh +19 -0
  43. package/scripts/dot/commands/fleet.sh +4 -1
  44. package/scripts/dot/commands/manual.sh +4 -4
  45. package/scripts/dot/commands/meta.sh +87 -12
  46. package/scripts/dot/commands/registry.sh +8 -2
  47. package/scripts/dot/commands/secrets.sh +17 -0
  48. package/scripts/dot/commands/security.sh +17 -0
  49. package/scripts/dot/commands/tools.sh +30 -2
  50. package/scripts/git-hooks/pre-commit-audit.sh +1 -1
  51. package/scripts/ops/ai-setup.sh +13 -6
  52. package/scripts/ops/bundle.sh +31 -5
  53. package/scripts/ops/chezmoi-apply.sh +5 -0
  54. package/scripts/ops/release.sh +14 -5
  55. package/scripts/qa/docs-coverage.sh +1 -1
  56. package/scripts/qa/reliability-audit.sh +2 -2
  57. package/scripts/secrets/age-init.sh +15 -4
  58. package/scripts/theme/apply-gnome-theme.sh +6 -2
  59. package/scripts/theme/extract-theme.py +97 -33
  60. package/scripts/theme/rebuild-themes.sh +240 -35
  61. package/scripts/theme/switch.sh +26 -16
  62. package/scripts/theme/wallpaper-sync.sh +95 -7
  63. package/scripts/tools/detect-collisions.py +19 -1
  64. package/scripts/version-sync.sh +87 -43
@@ -78,15 +78,33 @@ fi
78
78
  # Theme Database
79
79
  # =============================================================================
80
80
 
81
- # Default preferences
82
- DEFAULT_DARK="macos-monterey-dark"
83
- DEFAULT_LIGHT="macos-monterey-light"
81
+ # Default preferences (must be present keys in themes.toml — the theme
82
+ # regeneration removed the old macos-monterey-* names; bloom is the current
83
+ # default family and always ships a dark+light pair).
84
+ DEFAULT_DARK="bloom-dark"
85
+ DEFAULT_LIGHT="bloom-light"
86
+
87
+ # Machine-local chezmoi override (chezmoi.toml [data] theme) takes precedence
88
+ # over .chezmoidata.toml when rendering, so it is the authoritative "current"
89
+ # value. Kept in sync by dot-theme-sync's write_theme().
90
+ CHEZMOI_CFG="${XDG_CONFIG_HOME:-$HOME/.config}/chezmoi/chezmoi.toml"
84
91
 
85
92
  # =============================================================================
86
93
  # Theme Functions
87
94
  # =============================================================================
88
95
 
89
96
  current_theme() {
97
+ # Prefer the machine-local override (what chezmoi actually renders), so
98
+ # `dot theme current` never disagrees with the deployed configs. Fall back
99
+ # to the repo default in .chezmoidata.toml when no override is set.
100
+ if [[ -f "$CHEZMOI_CFG" ]]; then
101
+ local override
102
+ override="$(awk -F'"' '/^theme =/ {print $2}' "$CHEZMOI_CFG" | head -n 1)"
103
+ if [[ -n "$override" ]]; then
104
+ echo "$override"
105
+ return 0
106
+ fi
107
+ fi
90
108
  awk -F'"' '/^theme =/ {print $2}' "$DATA_FILE" | head -n 1
91
109
  }
92
110
 
@@ -124,6 +142,9 @@ paired_families() {
124
142
  done < <(all_theme_names)
125
143
 
126
144
  for family in $(printf '%s\n' "${!has_dark[@]}" | sort); do
145
+ # `fallback` is a synthetic safety theme (see themes.toml) that templates
146
+ # degrade to when .theme is unset/invalid — never a user-selectable one.
147
+ [[ "$family" == "fallback" ]] && continue
127
148
  [[ -n "${has_light[$family]+x}" ]] && echo "$family"
128
149
  done
129
150
  }
@@ -185,12 +206,6 @@ set_theme() {
185
206
 
186
207
  # Interactive theme picker
187
208
  pick_theme() {
188
- if ! command -v fzf &>/dev/null; then
189
- ui_err "fzf" "required for interactive picker"
190
- ui_info "Usage" "dot theme set <name>"
191
- exit 1
192
- fi
193
-
194
209
  local current
195
210
  current="$(current_theme)"
196
211
 
@@ -220,14 +235,9 @@ pick_theme() {
220
235
  done < <(paired_families)
221
236
 
222
237
  local selected_family
223
- selected_family="$(echo "$theme_list" | fzf \
238
+ selected_family="$(printf '%s' "$theme_list" | ui_pick \
224
239
  --header "Select wallpaper theme (current: $current_family [$current_mode])" \
225
- --prompt "Theme > " \
226
- --height 30 \
227
- --reverse \
228
- --no-sort \
229
- --no-preview \
230
- --ansi |
240
+ --prompt "Theme >" |
231
241
  awk '$1 !~ /^#/ && NF >= 2 {print $2}')" || return 0
232
242
 
233
243
  if [[ -n "$selected_family" ]]; then
@@ -43,6 +43,16 @@ detect_mode() {
43
43
  esac
44
44
  fi
45
45
 
46
+ if [[ "$(uname -s)" == "Darwin" ]]; then
47
+ # AppleInterfaceStyle is "Dark" in dark mode and unset in light mode.
48
+ if [[ "$(defaults read -g AppleInterfaceStyle 2>/dev/null || true)" == "Dark" ]]; then
49
+ echo "dark"
50
+ else
51
+ echo "light"
52
+ fi
53
+ return 0
54
+ fi
55
+
46
56
  if command -v gsettings &>/dev/null; then
47
57
  local scheme
48
58
  scheme="$(gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null || echo "")"
@@ -125,10 +135,16 @@ wallpaper_for_theme() {
125
135
  found && /^wallpaper/ { sub(/.*= *"/, ""); sub(/".*/, ""); print; exit }
126
136
  ' "$themes_file")"
127
137
  if [[ -n "$stored_wp" ]]; then
128
- # Cross-platform: resolve macOS paths on Linux
138
+ # Home-relative form (current generator): ~/Pictures/... → $HOME/Pictures/...
139
+ # Prefix-strip comparison avoids a quoted-tilde glob (SC2088); the tilde
140
+ # here is a literal leading character, not a path to expand.
141
+ if [[ "$stored_wp" != "${stored_wp#\~/}" ]]; then
142
+ stored_wp="${HOME}/${stored_wp#\~/}"
143
+ fi
144
+ # Cross-platform: resolve legacy absolute macOS paths on Linux
129
145
  if [[ ! -f "$stored_wp" ]]; then
130
146
  if [[ "$stored_wp" == /Users/* ]]; then
131
- # /Users/<user>/Pictures/... → $HOME/Pictures/...
147
+ # /Users/<user>/Pictures/... → $HOME/Pictures/... (pre-relativize data)
132
148
  stored_wp="${HOME}/${stored_wp#/Users/*/}"
133
149
  elif [[ "$stored_wp" == /System/* ]]; then
134
150
  # macOS system wallpapers don't exist on Linux — skip to next fallback
@@ -397,6 +413,56 @@ ensure_linux_compatible() {
397
413
  printf '%s\n' "$wp"
398
414
  }
399
415
 
416
+ # macOS: a dynamic appearance HEIC (two frames + apple_desktop:apr metadata)
417
+ # set as a plain imageFile gets pinned to a single frame and stops tracking
418
+ # Light/Dark — so Light mode can show the dark frame. Extract the frame that
419
+ # matches the requested mode (0 = light, 1 = dark, per Apple's apr convention,
420
+ # same as the Linux -0/-1 path) to a small single-image HEIC and apply that
421
+ # instead. Single-image HEICs and non-HEICs are returned unchanged.
422
+ macos_appearance_frame() {
423
+ local wp="$1" mode="$2"
424
+ [[ "${wp##*.}" == "heic" ]] || {
425
+ printf '%s\n' "$wp"
426
+ return
427
+ }
428
+ command -v magick &>/dev/null || {
429
+ printf '%s\n' "$wp"
430
+ return
431
+ }
432
+ local frames
433
+ frames="$(magick identify "$wp" 2>/dev/null | wc -l | tr -d ' ')"
434
+ [[ "${frames:-0}" -ge 2 ]] || {
435
+ printf '%s\n' "$wp"
436
+ return
437
+ }
438
+
439
+ local idx=0
440
+ [[ "$mode" == "dark" ]] && idx=1
441
+ local cache_dir="$WALLPAPER_DIR/.dot-frames"
442
+ mkdir -p "$cache_dir" 2>/dev/null || {
443
+ printf '%s\n' "$wp"
444
+ return
445
+ }
446
+ local frame
447
+ frame="$cache_dir/$(basename "${wp%.heic}")-${mode}.heic"
448
+
449
+ # Reuse a cached frame that is newer than its source wallpaper.
450
+ if [[ -f "$frame" && "$frame" -nt "$wp" ]]; then
451
+ printf '%s\n' "$frame"
452
+ return
453
+ fi
454
+ local tmp
455
+ tmp="$(mktemp "${frame%.heic}.XXXXXX.heic")"
456
+ if magick "${wp}[${idx}]" "$tmp" 2>/dev/null && [[ -s "$tmp" ]]; then
457
+ if mv -f "$tmp" "$frame" 2>/dev/null; then
458
+ printf '%s\n' "$frame"
459
+ return
460
+ fi
461
+ fi
462
+ rm -f "$tmp" 2>/dev/null
463
+ printf '%s\n' "$wp"
464
+ }
465
+
400
466
  # Apply wallpaper based on platform and compositor
401
467
  apply_wallpaper() {
402
468
  local wp="$1"
@@ -408,6 +474,10 @@ apply_wallpaper() {
408
474
 
409
475
  case "$(uname -s)" in
410
476
  Darwin)
477
+ # Resolve a dynamic HEIC down to the mode-appropriate static frame so
478
+ # macOS renders the right appearance instead of pinning one frame.
479
+ wp="$(macos_appearance_frame "$wp" "$mode")"
480
+
411
481
  # macOS Sonoma+ moved wallpaper state to ~/Library/Application Support/
412
482
  # com.apple.wallpaper/Store/Index.plist, owned by WallpaperAgent. Each
413
483
  # Space and Display has its own entry, and AppleScript's `every desktop`
@@ -494,12 +564,29 @@ if updated:
494
564
  print(updated)
495
565
  PYEOF
496
566
 
497
- # Restart WallpaperAgent so it re-reads the store. launchd respawns it.
498
- killall WallpaperAgent 2>/dev/null || true
567
+ # Restart WallpaperAgent so it re-reads the store and repaints EVERY
568
+ # Space (the store rewrite above covers all Spaces; the agent only
569
+ # applies it on respawn). Then BLOCK until it's actually back and the
570
+ # wallpaper has re-applied — the caller reports "Done" once this returns,
571
+ # so it must not return while Spaces are still repainting. Set
572
+ # DOT_THEME_SKIP_WALLPAPER_AGENT=1 to skip the restart (faster, but
573
+ # background Spaces won't refresh until next login).
574
+ if [[ "${DOT_THEME_SKIP_WALLPAPER_AGENT:-0}" != "1" ]]; then
575
+ killall WallpaperAgent 2>/dev/null || true
576
+
577
+ # Wait for launchd to bring WallpaperAgent back (KeepAlive respawns it
578
+ # within ~1s; cap the wait so a theme switch can never hang).
579
+ local _waited=0
580
+ while ! pgrep -x WallpaperAgent >/dev/null 2>&1; do
581
+ sleep 0.1
582
+ _waited=$((_waited + 1))
583
+ [[ "$_waited" -ge 60 ]] && break
584
+ done
585
+ fi
499
586
 
500
- # Also nudge the active Space via the public API. This covers the
501
- # initial render before WallpaperAgent comes back, and handles screens
502
- # the Index.plist rewrite somehow missed.
587
+ # Re-assert the wallpaper through the (now running) agent covers the
588
+ # active Space's first paint and any screen the store rewrite missed —
589
+ # then give that paint a moment to finish before returning.
503
590
  if command -v wallpaper &>/dev/null; then
504
591
  wallpaper set "$wp" --screen all 2>/dev/null || true
505
592
  else
@@ -511,6 +598,7 @@ tell application \"System Events\"
511
598
  end repeat
512
599
  end tell" 2>/dev/null || true
513
600
  fi
601
+ sleep 0.5
514
602
  ;;
515
603
  Linux)
516
604
  # DMS owns wallpaper display and runs matugen for color extraction.
@@ -8,7 +8,25 @@ import os
8
8
  import re
9
9
  import sys
10
10
 
11
- BASE_DIR = os.path.expanduser("~/.dotfiles/.chezmoitemplates")
11
+ # Post-Phase-4b (v0.2.503): chezmoi source is under the subdir
12
+ # named in .chezmoiroot (typically "defaults/"). Prefer the
13
+ # descended path; fall back to the old top-level layout for
14
+ # backwards compatibility.
15
+ _DOTFILES_ROOT = os.path.expanduser("~/.dotfiles")
16
+ _CHEZMOIROOT_FILE = os.path.join(_DOTFILES_ROOT, ".chezmoiroot")
17
+
18
+
19
+ def _resolve_chezmoi_root() -> str:
20
+ if os.path.isfile(_CHEZMOIROOT_FILE):
21
+ with open(_CHEZMOIROOT_FILE, "r", encoding="utf-8") as _f:
22
+ sub = _f.read().strip()
23
+ cand = os.path.join(_DOTFILES_ROOT, sub)
24
+ if os.path.isdir(cand):
25
+ return cand
26
+ return _DOTFILES_ROOT
27
+
28
+
29
+ BASE_DIR = os.path.join(_resolve_chezmoi_root(), ".chezmoitemplates")
12
30
  ALIASES_DIR = os.path.join(BASE_DIR, "aliases")
13
31
  FUNCTIONS_DIR = os.path.join(BASE_DIR, "functions")
14
32
 
@@ -84,6 +84,13 @@ EXCLUDE_FILES=(
84
84
  # that are NOT the current dotfiles_version. Auto-syncing
85
85
  # rewrites them incorrectly.
86
86
  "docs/operations/ARCHITECTURE_ROADMAP.md"
87
+
88
+ # Dated release write-ups: each article records the release it
89
+ # shipped in (e.g. "shipped in vX.Y.Z" + a link to that release
90
+ # tag). Those refs are historical fact, not current-version claims —
91
+ # bumping them would falsify the history.
92
+ "docs/articles/2026-07-05-fish-startup-abbr.md"
93
+ "docs/articles/2026-07-05-master-to-main-rename-runbook.md"
87
94
  )
88
95
 
89
96
  # shellcheck source=../lib/dot/ui.sh
@@ -149,7 +156,11 @@ get_package_version() {
149
156
  fi
150
157
 
151
158
  local version
152
- version=$(jq -r '.version' "$package_file" 2>/dev/null)
159
+ if command -v jq &>/dev/null; then
160
+ version=$(jq -r '.version' "$package_file" 2>/dev/null)
161
+ else
162
+ version=$(sed -nE 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' "$package_file" | head -n 1)
163
+ fi
153
164
  if [[ "$version" == "null" || -z "$version" ]]; then
154
165
  log_error "Could not extract version from package.json"
155
166
  exit 1
@@ -179,22 +190,32 @@ find_version_files() {
179
190
  # the paths match the EXCLUDE_FILES entries (which have no "./"),
180
191
  # otherwise excluded historical docs get rewritten.
181
192
  cd "$PROJECT_ROOT"
182
- rg -l "v?$VERSION_PATTERN" --type md . 2>/dev/null | sed 's|^\./||' >"$temp_file" || true
193
+ if command -v rg &>/dev/null; then
194
+ rg -l "v?$VERSION_PATTERN" --type md . 2>/dev/null | sed 's|^\./||' >"$temp_file" || true
195
+ else
196
+ {
197
+ while IFS= read -r file; do
198
+ if grep -Eq "v?$VERSION_PATTERN" "$file" 2>/dev/null; then
199
+ printf '%s\n' "${file#./}"
200
+ fi
201
+ done < <(find . -type f -name '*.md' -print)
202
+ } >"$temp_file"
203
+ fi
183
204
 
184
205
  # Add known files that should be checked even if they don't have versions yet
185
- echo "README.md" >>"$temp_file"
186
- echo "docs/reference/FEATURES.md" >>"$temp_file"
187
- echo "docs/COPYRIGHT" >>"$temp_file"
188
-
189
206
  # Non-markdown and hidden-directory surfaces rg's `--type md` scan above
190
207
  # cannot reach: a shell script, and READMEs under the dotfile-hidden
191
208
  # `.chezmoitemplates/` tree (rg skips dot-dirs without --hidden). These
192
209
  # carry "current version" stamps that check-version-consistency.sh and
193
210
  # the test_version_consistency unit test enforce, so keep them in sync.
194
- echo "scripts/git-hooks/pre-commit-audit.sh" >>"$temp_file"
195
- echo "defaults/.chezmoitemplates/README.md" >>"$temp_file"
196
- echo "defaults/.chezmoitemplates/functions/README.md" >>"$temp_file"
197
- echo "defaults/.chezmoitemplates/aliases/README.md" >>"$temp_file"
211
+ printf '%s\n' \
212
+ "README.md" \
213
+ "docs/reference/FEATURES.md" \
214
+ "docs/COPYRIGHT" \
215
+ "scripts/git-hooks/pre-commit-audit.sh" \
216
+ "defaults/.chezmoitemplates/README.md" \
217
+ "defaults/.chezmoitemplates/functions/README.md" \
218
+ "defaults/.chezmoitemplates/aliases/README.md" >>"$temp_file"
198
219
 
199
220
  # Remove duplicates and filter existing files
200
221
  sort -u "$temp_file" | while IFS= read -r file; do
@@ -261,6 +282,7 @@ update_version_references() {
261
282
  log_warning "File not found: $file"
262
283
  continue
263
284
  fi
285
+ files_processed=$((files_processed + 1))
264
286
 
265
287
  # Skip milestone and other historical files
266
288
  if [[ "$(basename "$file")" == MILESTONE_* ]]; then
@@ -321,6 +343,7 @@ update_version_references() {
321
343
  cat "$temp_file" >"$file"
322
344
  log_success "Updated: $file"
323
345
  fi
346
+ rm -f "$temp_file"
324
347
  changes_made=$((changes_made + 1))
325
348
  else
326
349
  log_info "No changes needed: $file"
@@ -360,26 +383,49 @@ verify_version_consistency() {
360
383
 
361
384
  # Extract only dotfiles-targeted version references (ignore unrelated tool versions).
362
385
  local versions_in_file=()
363
- while IFS= read -r match; do
364
- while IFS= read -r version; do
365
- versions_in_file+=("$version")
366
- done < <(printf "%s\n" "$match" | rg -o "v?$VERSION_PATTERN" || true)
367
- done < <(
368
- rg -v "MILESTONE" "$file" 2>/dev/null | rg -o \
369
- -e "Version-v$VERSION_PATTERN" \
370
- -e "/releases/tag/v$VERSION_PATTERN" \
371
- -e "/dotfiles/v$VERSION_PATTERN/" \
372
- -e "\\*\\*Version\\*\\*:[[:space:]]*v?$VERSION_PATTERN" \
373
- -e "\\*\\*Dotfiles Version\\*\\*:[[:space:]]*v?$VERSION_PATTERN" \
374
- -e "(^|[[:space:]])Version:[[:space:]]*v?$VERSION_PATTERN" \
375
- -e "(^|[[:space:]])Dotfiles Version:[[:space:]]*v?$VERSION_PATTERN" \
376
- -e "Version[[:space:]]*\`v?$VERSION_PATTERN\`" \
377
- -e "\\(v$VERSION_PATTERN\\)" \
378
- -e "/v$VERSION_PATTERN/" \
379
- -e "v$VERSION_PATTERN\\b" \
380
- -e "dotfiles:v?$VERSION_PATTERN" \
381
- -e "notes — v$VERSION_PATTERN" || true
382
- )
386
+ if command -v rg &>/dev/null; then
387
+ while IFS= read -r match; do
388
+ while IFS= read -r version; do
389
+ versions_in_file+=("$version")
390
+ done < <(printf "%s\n" "$match" | rg -o "v?$VERSION_PATTERN" || true)
391
+ done < <(
392
+ rg -v "MILESTONE" "$file" 2>/dev/null | rg -o \
393
+ -e "Version-v$VERSION_PATTERN" \
394
+ -e "/releases/tag/v$VERSION_PATTERN" \
395
+ -e "/dotfiles/v$VERSION_PATTERN/" \
396
+ -e "\\*\\*Version\\*\\*:[[:space:]]*v?$VERSION_PATTERN" \
397
+ -e "\\*\\*Dotfiles Version\\*\\*:[[:space:]]*v?$VERSION_PATTERN" \
398
+ -e "(^|[[:space:]])Version:[[:space:]]*v?$VERSION_PATTERN" \
399
+ -e "(^|[[:space:]])Dotfiles Version:[[:space:]]*v?$VERSION_PATTERN" \
400
+ -e "Version[[:space:]]*\`v?$VERSION_PATTERN\`" \
401
+ -e "\\(v$VERSION_PATTERN\\)" \
402
+ -e "/v$VERSION_PATTERN/" \
403
+ -e "v$VERSION_PATTERN\\b" \
404
+ -e "dotfiles:v?$VERSION_PATTERN" \
405
+ -e "notes — v$VERSION_PATTERN" || true
406
+ )
407
+ else
408
+ while IFS= read -r match; do
409
+ while IFS= read -r version; do
410
+ versions_in_file+=("$version")
411
+ done < <(printf "%s\n" "$match" | grep -Eo "v?$VERSION_PATTERN" || true)
412
+ done < <(
413
+ grep -Ev "MILESTONE" "$file" 2>/dev/null | grep -Eo \
414
+ -e "Version-v$VERSION_PATTERN" \
415
+ -e "/releases/tag/v$VERSION_PATTERN" \
416
+ -e "/dotfiles/v$VERSION_PATTERN/" \
417
+ -e "\\*\\*Version\\*\\*:[[:space:]]*v?$VERSION_PATTERN" \
418
+ -e "\\*\\*Dotfiles Version\\*\\*:[[:space:]]*v?$VERSION_PATTERN" \
419
+ -e "(^|[[:space:]])Version:[[:space:]]*v?$VERSION_PATTERN" \
420
+ -e "(^|[[:space:]])Dotfiles Version:[[:space:]]*v?$VERSION_PATTERN" \
421
+ -e "Version[[:space:]]*\`v?$VERSION_PATTERN\`" \
422
+ -e "\\(v$VERSION_PATTERN\\)" \
423
+ -e "/v$VERSION_PATTERN/" \
424
+ -e "v$VERSION_PATTERN([^0-9.]|$)" \
425
+ -e "dotfiles:v?$VERSION_PATTERN" \
426
+ -e "notes — v$VERSION_PATTERN" || true
427
+ )
428
+ fi
383
429
 
384
430
  if [[ ${#versions_in_file[@]} -eq 0 ]]; then
385
431
  continue
@@ -450,6 +496,12 @@ main() {
450
496
  esac
451
497
  done
452
498
 
499
+ if [[ -n "${DOTFILES_COV_TMPDIR:-}" && "${DOTFILES_ALLOW_COVERAGE_WRITES:-0}" != "1" ]]; then
500
+ dry_run="true"
501
+ create_backup_flag="false"
502
+ log_info "Coverage sandbox detected; forcing --dry-run"
503
+ fi
504
+
453
505
  # Change to project root
454
506
  cd "$PROJECT_ROOT"
455
507
 
@@ -463,8 +515,11 @@ main() {
463
515
  fi
464
516
 
465
517
  # Find files with version references
466
- local version_files
467
- readarray -t version_files < <(find_version_files)
518
+ local version_files=()
519
+ local version_file
520
+ while IFS= read -r version_file; do
521
+ version_files+=("$version_file")
522
+ done < <(find_version_files)
468
523
 
469
524
  if [[ ${#version_files[@]} -eq 0 ]]; then
470
525
  log_warning "No files with version references found"
@@ -567,16 +622,5 @@ EOF
567
622
  fi
568
623
  }
569
624
 
570
- # Ensure we have required tools
571
- if ! command -v jq &>/dev/null; then
572
- log_warning "jq is not installed — skipping version sync"
573
- exit 0
574
- fi
575
-
576
- if ! command -v rg &>/dev/null; then
577
- log_warning "ripgrep (rg) is not installed — skipping version sync"
578
- exit 0
579
- fi
580
-
581
625
  # Run main function
582
626
  main "$@"