@sebastienrousseau/dotfiles 0.2.518 → 0.2.520

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 (163) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/README.md +1164 -164
  3. package/docs/ARCHITECTURE.md +117 -0
  4. package/docs/COPYRIGHT +1 -1
  5. package/docs/ECOSYSTEM.md +220 -0
  6. package/docs/GOLD-STANDARD-AUDIT.md +352 -0
  7. package/docs/GOVERNANCE.md +1 -1
  8. package/docs/MAINTAINERS.md +1 -1
  9. package/docs/MINIMUM-TOOLCHAIN.md +100 -0
  10. package/docs/README.md +1 -1
  11. package/docs/STRUCTURE.md +1 -1
  12. package/docs/architecture/ARCHITECTURE.md +11 -108
  13. package/docs/architecture/REPO_LAYOUT.md +4 -4
  14. package/docs/guides/MACOS_ICLOUD_SYMLINKS.md +121 -0
  15. package/docs/index.md +3 -3
  16. package/docs/manual/00-introduction.md +1 -1
  17. package/docs/manual/03-reference/01-dot-cli.md +28 -3
  18. package/docs/manual/command-index.md +24 -4
  19. package/docs/manual/concept-index.md +2 -0
  20. package/docs/migration/README.md +81 -0
  21. package/docs/migration/from-bare-git-repo.md +156 -0
  22. package/docs/migration/from-gnu-stow.md +165 -0
  23. package/docs/migration/from-plain-chezmoi.md +148 -0
  24. package/docs/migration/from-yadm.md +187 -0
  25. package/docs/operations/PERFORMANCE_BUDGETS.md +196 -0
  26. package/docs/operations/REGISTRY.md +1 -1
  27. package/docs/operations/RELEASE_PIPELINE.md +4 -4
  28. package/docs/operations/TESTING.md +3 -3
  29. package/docs/operations/TRACEABILITY.md +1 -0
  30. package/docs/packaging.md +222 -0
  31. package/docs/reference/FEATURE-MATRIX.md +646 -0
  32. package/docs/reference/TOOLS.md +1 -1
  33. package/docs/reference/UTILS.md +1 -0
  34. package/docs/security/COMPLIANCE.md +1 -1
  35. package/docs/security/DISCLOSURE.md +4 -4
  36. package/docs/security/FUZZING.md +113 -18
  37. package/docs/security/KEY_ROTATION.md +1 -1
  38. package/docs/security/SCORECARD.md +3 -3
  39. package/docs/security/VERIFY_RELEASE.md +2 -2
  40. package/install.sh +6 -6
  41. package/package.json +2 -2
  42. package/scripts/ci/check-copyright-headers.sh +1 -1
  43. package/scripts/ci/check-shell-preamble.sh +1 -1
  44. package/scripts/ci/guard-gitleaks-checkout.sh +1 -1
  45. package/scripts/demo/record.sh +1 -1
  46. package/scripts/diagnostics/a2a-conformance.sh +1 -1
  47. package/scripts/diagnostics/alias-governance.sh +30 -3
  48. package/scripts/diagnostics/aliases-cheatsheet.sh +1 -1
  49. package/scripts/diagnostics/aliases-manifest.sh +1 -1
  50. package/scripts/diagnostics/attest-verify.sh +147 -0
  51. package/scripts/diagnostics/benchmark.sh +1 -1
  52. package/scripts/diagnostics/conflicts.sh +1 -1
  53. package/scripts/diagnostics/doctor-unified.sh +6 -2
  54. package/scripts/diagnostics/doctor.sh +56 -10
  55. package/scripts/diagnostics/drift-dashboard.sh +3 -2
  56. package/scripts/diagnostics/health.sh +43 -10
  57. package/scripts/diagnostics/history-analysis.sh +1 -1
  58. package/scripts/diagnostics/mcp-doctor.sh +2 -2
  59. package/scripts/diagnostics/perf.sh +1 -1
  60. package/scripts/diagnostics/scorecard.sh +3 -2
  61. package/scripts/diagnostics/secret-governance.sh +1 -1
  62. package/scripts/diagnostics/security-score.sh +1 -1
  63. package/scripts/diagnostics/smoke-test.sh +1 -1
  64. package/scripts/diagnostics/snapshot.sh +1 -1
  65. package/scripts/diagnostics/verify.sh +1 -1
  66. package/scripts/diagnostics/verify_state.sh +1 -1
  67. package/scripts/diagnostics/version-locks.sh +1 -1
  68. package/scripts/diagnostics/workstation-attestation.sh +26 -1
  69. package/scripts/dot/commands/agent.sh +64 -14
  70. package/scripts/dot/commands/agents.sh +24 -8
  71. package/scripts/dot/commands/ai.sh +20 -7
  72. package/scripts/dot/commands/aliases.sh +1 -1
  73. package/scripts/dot/commands/appearance.sh +1 -1
  74. package/scripts/dot/commands/completion.sh +42 -5
  75. package/scripts/dot/commands/core.sh +1 -1
  76. package/scripts/dot/commands/diagnostics.sh +1 -1
  77. package/scripts/dot/commands/env-emit.sh +1 -1
  78. package/scripts/dot/commands/fleet.sh +41 -18
  79. package/scripts/dot/commands/init.sh +1 -1
  80. package/scripts/dot/commands/lint.sh +1 -1
  81. package/scripts/dot/commands/manual.sh +1 -1
  82. package/scripts/dot/commands/meta.sh +115 -10
  83. package/scripts/dot/commands/patterns.sh +1 -1
  84. package/scripts/dot/commands/registry.sh +43 -7
  85. package/scripts/dot/commands/restore.sh +1 -1
  86. package/scripts/dot/commands/secrets.sh +1 -1
  87. package/scripts/dot/commands/security.sh +1 -1
  88. package/scripts/dot/commands/tools.sh +21 -7
  89. package/scripts/fonts/install-nerd-fonts.sh +1 -1
  90. package/scripts/fonts/patch-fonts.sh +1 -1
  91. package/scripts/git-hooks/install.sh +1 -1
  92. package/scripts/git-hooks/pre-commit-audit.sh +2 -2
  93. package/scripts/lib/secrets_provider.sh +19 -4
  94. package/scripts/nvim/headless-upgrade.lua +81 -0
  95. package/scripts/ops/ai-setup.sh +1 -1
  96. package/scripts/ops/bundle.sh +1 -1
  97. package/scripts/ops/chaos.sh +1 -1
  98. package/scripts/ops/chezmoi-apply.sh +1 -1
  99. package/scripts/ops/chezmoi-diff.sh +1 -1
  100. package/scripts/ops/chezmoi-remove.sh +3 -3
  101. package/scripts/ops/chezmoi-update.sh +6 -2
  102. package/scripts/ops/heal-chezmoi.sh +1 -1
  103. package/scripts/ops/heal-system.sh +1 -1
  104. package/scripts/ops/heal-tools.sh +1 -1
  105. package/scripts/ops/heal.sh +1 -1
  106. package/scripts/ops/post-apply-repair.sh +1 -1
  107. package/scripts/ops/prewarm.sh +1 -1
  108. package/scripts/ops/release.sh +1 -1
  109. package/scripts/ops/rollback.sh +11 -2
  110. package/scripts/ops/setup.sh +1 -1
  111. package/scripts/ops/teleport.sh +1 -1
  112. package/scripts/qa/check-feature-matrix.sh +296 -0
  113. package/scripts/qa/check-version-consistency.sh +7 -119
  114. package/scripts/qa/coverage-baseline.sh +1 -1
  115. package/scripts/qa/docs-coverage.sh +9 -3
  116. package/scripts/qa/examples-coverage.sh +1 -1
  117. package/scripts/qa/reliability-audit.sh +1 -1
  118. package/scripts/qa/scorecard-snapshot.sh +1 -1
  119. package/scripts/qa/traceability-coverage.sh +9 -2
  120. package/scripts/qa/validate-examples.sh +66 -3
  121. package/scripts/qa/wsl-contract.sh +1 -1
  122. package/scripts/secrets/age-init.sh +1 -1
  123. package/scripts/secrets/create-secrets-file.sh +1 -1
  124. package/scripts/secrets/encrypt-ssh-key.sh +1 -1
  125. package/scripts/security/backup.sh +1 -1
  126. package/scripts/security/check-disclosure-key-expiry.sh +1 -1
  127. package/scripts/security/dns-doh.sh +1 -1
  128. package/scripts/security/encryption-check.sh +1 -1
  129. package/scripts/security/enforce-policies.sh +315 -98
  130. package/scripts/security/firewall.sh +1 -1
  131. package/scripts/security/lock-configs.sh +1 -1
  132. package/scripts/security/lock-screen.sh +1 -1
  133. package/scripts/security/manage-secrets.sh +1 -1
  134. package/scripts/security/ssh-cert.sh +1 -1
  135. package/scripts/security/telemetry-kill.sh +1 -1
  136. package/scripts/security/usb-safety.sh +1 -1
  137. package/scripts/theme/apply-gnome-theme.sh +1 -1
  138. package/scripts/theme/extract-heic-frames.sh +1 -1
  139. package/scripts/theme/extract-theme.py +284 -6
  140. package/scripts/theme/install-boot-logo.sh +1 -1
  141. package/scripts/theme/install-catppuccin-themes.sh +1 -1
  142. package/scripts/theme/install-cursors.sh +1 -1
  143. package/scripts/theme/install-file-icons.sh +1 -1
  144. package/scripts/theme/install-grub-theme.sh +1 -1
  145. package/scripts/theme/install-lock-icon.sh +1 -1
  146. package/scripts/theme/merge-wallpaper.sh +1 -1
  147. package/scripts/theme/rebuild-themes.sh +64 -5
  148. package/scripts/theme/switch.sh +46 -19
  149. package/scripts/theme/wallpaper-rotate.sh +1 -1
  150. package/scripts/theme/wallpaper-sync.sh +1 -1
  151. package/scripts/tools/cmatrix.sh +1 -1
  152. package/scripts/tools/detect-collisions.py +1 -1
  153. package/scripts/tools/emoji-picker.sh +1 -1
  154. package/scripts/tools/figlet-banner.sh +1 -1
  155. package/scripts/tools/log-rotate.sh +1 -1
  156. package/scripts/tools/lolcat-wrap.sh +1 -1
  157. package/scripts/tools/pipes.sh +1 -1
  158. package/scripts/tuning/linux.sh +1 -1
  159. package/scripts/tuning/macos.sh +1 -1
  160. package/scripts/uninstall.sh +1 -1
  161. package/scripts/verify-release-versions +156 -0
  162. package/scripts/version-sync.sh +63 -3
  163. package/LICENSE +0 -21
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Alias/Command Conflicts Report
5
5
  # Usage: dot conflicts
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Unified doctor orchestrator — parses flags and dispatches to existing scripts.
5
5
  set -euo pipefail
@@ -20,7 +20,11 @@ target="scripts/diagnostics/doctor.sh"
20
20
  for arg in "$@"; do
21
21
  case "$arg" in
22
22
  --heal | -H) target="scripts/ops/heal.sh" ;;
23
- --audit | -a) target="scripts/ops/health-check.sh" ;;
23
+ # scripts/ops/health-check.sh has never existed in this tree, so
24
+ # `dot doctor --audit` died with "Script not found" for every user who
25
+ # tried it. The health dashboard is what that name meant: it is the
26
+ # audit-shaped diagnostic, and `dot health-check` already aliases it.
27
+ --audit | -a) target="scripts/diagnostics/health.sh" ;;
24
28
  --score | -s) target="scripts/diagnostics/scorecard.sh" ;;
25
29
  --smoke | -m) target="scripts/diagnostics/smoke-test.sh" ;;
26
30
  --drift | -d) target="scripts/diagnostics/drift-dashboard.sh" ;;
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  ## Dotfiles Doctor.
5
5
  ##
@@ -169,11 +169,11 @@ fi
169
169
 
170
170
  # --- AI CLIs ---
171
171
  _section "AI CLIs"
172
- for cmd in claude copilot kimi agy sgpt ollama opencode aider kiro-cli; do
172
+ for cmd in codex claude copilot kimi agy sgpt ollama opencode aider kiro-cli; do
173
173
  if check_cmd "$cmd"; then
174
174
  _ok "$cmd" "$(pretty_path "$(get_cmd_path "$cmd")")"
175
175
  else
176
- _warn "$cmd" "optional"
176
+ ui_info "$cmd" "optional (not installed)"
177
177
  fi
178
178
  done
179
179
 
@@ -372,7 +372,11 @@ fi
372
372
 
373
373
  # --- State ---
374
374
  _section "State"
375
- if chezmoi verify &>/dev/null; then
375
+ # --exclude=always: a run_before_/run_after_ script (the macOS iCloud hook)
376
+ # is "pending" on every apply by design, so plain `verify` exits 1 forever
377
+ # on a fully synchronised machine. Files, dirs, symlinks and run_once/
378
+ # run_onchange scripts still count.
379
+ if chezmoi verify --exclude=always &>/dev/null; then
376
380
  _ok "chezmoi" "synchronized"
377
381
  else
378
382
  _fail "chezmoi" "drifted (run dot drift)"
@@ -462,28 +466,46 @@ else
462
466
  _fail "fish_plugins" "missing (~/.config/fish/fish_plugins)"
463
467
  fi
464
468
 
469
+ # Only meaningful when there is a cargo for topgrade to run it through; a
470
+ # machine without a Rust toolchain has nothing to update, so warning about
471
+ # the missing helper there is noise that never goes away.
465
472
  if command -v cargo-install-update >/dev/null 2>&1; then
466
473
  _ok "cargo-install-update" "$(pretty_path "$(command -v cargo-install-update)")"
467
- else
474
+ elif command -v cargo >/dev/null 2>&1; then
468
475
  _warn "cargo-install-update" "missing (install: cargo install cargo-update)"
476
+ else
477
+ _ok "cargo-install-update" "not needed (cargo not installed)"
469
478
  fi
470
479
 
471
480
  # --- Symlinks ---
472
481
  broken_links=0
482
+ broken_list=""
473
483
  for root in "$HOME/.config" "$HOME/.local/bin" "$HOME/.local/share" "$HOME/.ssh"; do
474
484
  [[ -d "$root" ]] || continue
475
485
  while IFS= read -r -d '' link; do
476
486
  link_name="$(basename "$link")"
477
487
  [[ "$link" == *"google-chrome-backup"* ]] && continue
478
488
  [[ "$link_name" == SingletonLock || "$link_name" == SingletonCookie || "$link_name" == SingletonSocket ]] && continue
479
- [[ -e "$link" ]] || broken_links=$((broken_links + 1))
489
+ [[ -e "$link" ]] && continue
490
+ # A link into ~/Library/Caches dangling is macOS working as designed, not a
491
+ # health problem: the OS purges that directory whenever it wants the space,
492
+ # and the owning tool recreates its cache on next use. ~/.config/swiftpm/cache
493
+ # -> ~/Library/Caches/org.swift.swiftpm is the usual one; it was "fixed" by
494
+ # recreating the target earlier the same day and had broken again by evening,
495
+ # which is the tell that it is not fixable, only re-reported.
496
+ case "$(readlink "$link" 2>/dev/null)" in
497
+ "$HOME/Library/Caches/"*) continue ;;
498
+ esac
499
+ broken_links=$((broken_links + 1))
500
+ broken_list="${broken_list:+$broken_list, }$(pretty_path "$link")"
480
501
  done < <(find "$root" -maxdepth 3 -type l -print0 2>/dev/null)
481
502
  done
482
503
 
483
504
  if [[ $broken_links -eq 0 ]]; then
484
505
  _ok "symlinks" "none broken"
485
506
  else
486
- _warn "symlinks" "$broken_links broken"
507
+ # Name them. "1 broken" with no path meant hunting for it by hand, twice.
508
+ _warn "symlinks" "$broken_links broken: $broken_list"
487
509
  fi
488
510
 
489
511
  # --- Portability ---
@@ -548,8 +570,10 @@ for tool in mise starship zoxide atuin fzf direnv; do
548
570
  done
549
571
  done
550
572
  if [[ $stale_caches -eq 0 ]]; then
573
+ caches_fresh="fresh"
551
574
  _ok "shell caches" "fresh"
552
575
  else
576
+ caches_fresh="stale"
553
577
  _warn "shell caches" "stale ($stale_tools) — run dot prewarm"
554
578
  fi
555
579
 
@@ -618,8 +642,22 @@ fi
618
642
  # A mise-managed 2026 dev machine routinely adds 60-90 entries (one per
619
643
  # installed tool version + per-shim path), so the warn/fail thresholds
620
644
  # reflect that baseline rather than a lean default (~40).
645
+ #
646
+ # The OK ceiling was 75, which contradicted the 60-90 baseline stated right
647
+ # above it and warned on every healthy mise machine. Worse, the implied remedy
648
+ # was backwards: those per-tool entries are what let a command resolve to the
649
+ # real binary instead of falling through to a mise shim. Measured 2026-08-20,
650
+ # 20 invocations of ripgrep --version:
651
+ #
652
+ # via shim 2741ms (137ms per call)
653
+ # direct install dir 62ms (3.1ms per call)
654
+ #
655
+ # ~44x. "Pruning" those entries to satisfy a length check would trade
656
+ # microseconds of PATH scan for ~134ms on every single tool invocation. The
657
+ # ceiling now matches the documented baseline; >90 still warns, because past
658
+ # that the entries are worth auditing for tools you no longer use.
621
659
  path_count=$(printf '%s' "${PATH:-}" | tr ':' '\n' | grep -c . || true)
622
- if [[ "$path_count" -le 75 ]]; then
660
+ if [[ "$path_count" -le 90 ]]; then
623
661
  _ok "PATH length" "$path_count entries"
624
662
  elif [[ "$path_count" -le 120 ]]; then
625
663
  _warn "PATH length" "$path_count entries — consider pruning"
@@ -666,10 +704,18 @@ if command -v zsh >/dev/null 2>&1 && command -v timeout >/dev/null 2>&1; then
666
704
  fi
667
705
 
668
706
  if command -v hyperfine >/dev/null 2>&1; then
669
- if bash "$SCRIPT_DIR/../../tests/performance/bench.sh" 2>/dev/null; then
707
+ if bash "$SCRIPT_DIR/../../benches/bench.sh" 2>/dev/null; then
670
708
  _ok "startup latency" "within target thresholds"
671
709
  else
672
- _warn "startup latency" "threshold exceeded (run dot prewarm)"
710
+ # Only suggest prewarm when it could actually help. The caches were
711
+ # already reported fresh above in the common case, and telling someone to
712
+ # re-run a no-op sends them in a circle — as it did on 2026-08-20, where
713
+ # prewarm changed nothing because nothing was cold.
714
+ if [[ "${caches_fresh:-unknown}" == "fresh" ]]; then
715
+ _warn "startup latency" "threshold exceeded (caches already fresh — profile with 'dot benchmark')"
716
+ else
717
+ _warn "startup latency" "threshold exceeded (run dot prewarm)"
718
+ fi
673
719
  fi
674
720
  else
675
721
  _warn "hyperfine" "missing (benchmark skipped)"
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # =============================================================================
5
5
  # drift-dashboard.sh — Consolidated drift report.
@@ -71,7 +71,8 @@ fi
71
71
  # Class 1: chezmoi-managed drift
72
72
  # -----------------------------------------------------------------------------
73
73
 
74
- cm_status="$(chezmoi status 2>/dev/null || true)"
74
+ # --exclude=always: always-run scripts are pending by design, not drift.
75
+ cm_status="$(chezmoi status --exclude=always 2>/dev/null || true)"
75
76
  cm_count=0
76
77
  if [[ -n "$cm_status" ]]; then
77
78
  cm_count=$(printf '%s\n' "$cm_status" | wc -l | tr -d ' ')
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Dotfiles Health Check Dashboard
5
5
  # Usage: dot health [--verbose|-v] [--json|-j] [--fix|-f] [--force|-F]
@@ -124,7 +124,7 @@ check() {
124
124
  if [[ "$use_ui" = "1" ]]; then
125
125
  ui_warn "$name" "$message"
126
126
  else
127
- printf "${YELLOW}⚠${NC} %-35s ${YELLOW}WARNING${NC}"
127
+ printf "${YELLOW}⚠${NC} %-35s ${YELLOW}WARNING${NC}" "$name"
128
128
  [[ -n "$message" ]] && printf " ${GRAY}%s${NC}" "$message"
129
129
  printf "\n"
130
130
  fi
@@ -137,7 +137,7 @@ check() {
137
137
  if [[ "$use_ui" = "1" ]]; then
138
138
  ui_err "$name" "$message"
139
139
  else
140
- printf "${RED}✗${NC} %-35s ${RED}FAILED${NC}"
140
+ printf "${RED}✗${NC} %-35s ${RED}FAILED${NC}" "$name"
141
141
  [[ -n "$message" ]] && printf " ${GRAY}%s${NC}" "$message"
142
142
  printf "\n"
143
143
  fi
@@ -235,7 +235,13 @@ check_dev_tools() {
235
235
 
236
236
  if has_command node; then
237
237
  local node_version
238
- node_version=$(node --version 2>/dev/null)
238
+ # `|| true` is load-bearing under `set -e`: an assignment takes the exit
239
+ # status of its command substitution, so a `node` that fails to report a
240
+ # version aborts the entire health report mid-section — no Summary, no
241
+ # score, rc=1. That is exactly what happened when `node` resolved to a
242
+ # mise shim under a sandboxed HOME. A diagnostic tool must survive the
243
+ # tools it is diagnosing.
244
+ node_version=$(node --version 2>/dev/null || true)
239
245
  check "Node.js ($node_version)" "pass"
240
246
  else
241
247
  check "Node.js" "warn" "Not installed"
@@ -251,7 +257,10 @@ check_dev_tools() {
251
257
 
252
258
  if has_command python3; then
253
259
  local py_version
254
- py_version=$(python3 --version 2>/dev/null | cut -d' ' -f2)
260
+ # pipefail is on, so this pipeline reports python3's failure even though
261
+ # `cut` succeeds — and an assignment inherits that status, aborting the
262
+ # report under `set -e`. Same hazard as node_version above.
263
+ py_version=$(python3 --version 2>/dev/null | cut -d' ' -f2 || true)
255
264
  check "Python ($py_version)" "pass"
256
265
  else
257
266
  check "Python" "warn" "Not installed"
@@ -335,7 +344,12 @@ check_security() {
335
344
 
336
345
  if has_command age; then
337
346
  check "Age encryption" "pass"
338
- if [[ -f "${HOME}/.config/chezmoi/key.txt" ]]; then
347
+ local age_identity="${HOME}/.config/chezmoi/key.txt" configured_identity=""
348
+ if has_command chezmoi && has_command jq; then
349
+ configured_identity=$(chezmoi dump-config --format=json 2>/dev/null | jq -r '.age.identity // empty' 2>/dev/null || true)
350
+ [[ -z "$configured_identity" ]] || age_identity="${configured_identity/#\~/$HOME}"
351
+ fi
352
+ if [[ -f "$age_identity" ]]; then
339
353
  check "Age key configured" "pass"
340
354
  else
341
355
  check "Age key configured" "warn" "Key not found"
@@ -344,17 +358,35 @@ check_security() {
344
358
  check "Age encryption" "warn" "Not installed"
345
359
  fi
346
360
 
347
- if [[ -f "${HOME}/.ssh/id_ed25519" ]] || [[ -f "${HOME}/.ssh/id_rsa" ]]; then
361
+ local -a ssh_keys=("$HOME/.ssh/id_ed25519" "$HOME/.ssh/id_rsa" "$HOME/.ssh/id_ed25519_sk")
362
+ local public_key key existing_key keys_found=false
363
+ for public_key in "$HOME/.ssh/"*.pub; do
364
+ [[ -f "$public_key" && -f "${public_key%.pub}" ]] || continue
365
+ key="${public_key%.pub}"
366
+ for existing_key in "${ssh_keys[@]}"; do
367
+ [[ "$existing_key" != "$key" ]] || continue 2
368
+ done
369
+ ssh_keys+=("$key")
370
+ done
371
+ for key in "${ssh_keys[@]}"; do
372
+ [[ ! -f "$key" ]] || keys_found=true
373
+ done
374
+ if $keys_found; then
348
375
  check "SSH keys present" "pass"
349
376
  else
350
377
  check "SSH keys present" "warn" "No keys found"
351
378
  fi
352
379
 
353
380
  # Check SSH key permissions
354
- for key in "$HOME/.ssh/id_ed25519" "$HOME/.ssh/id_rsa" "$HOME/.ssh/id_ed25519_sk"; do
381
+ for key in "${ssh_keys[@]}"; do
355
382
  if [[ -f "$key" ]]; then
356
383
  local perms
357
- perms=$(stat -c '%a' "$key" 2>/dev/null || stat -f '%Lp' "$key" 2>/dev/null)
384
+ # Same hazard as node_version above: if BOTH stat spellings fail (a
385
+ # platform with neither GNU nor BSD stat), the assignment is non-zero
386
+ # and `set -e` kills the report. Empty perms falls through to the
387
+ # "should be 600" warning, which is the right answer when the mode
388
+ # cannot be read.
389
+ perms=$(stat -c '%a' "$key" 2>/dev/null || stat -f '%Lp' "$key" 2>/dev/null || true)
358
390
  if [[ "$perms" != "600" && "$perms" != "400" ]]; then
359
391
  check "SSH key perms (${key##*/})" "warn" "mode $perms (should be 600)"
360
392
  else
@@ -440,7 +472,8 @@ check_sync_status() {
440
472
  # needs warned about via the health dashboard.
441
473
  if has_command chezmoi; then
442
474
  local status_output
443
- status_output=$(chezmoi status 2>/dev/null || echo "")
475
+ # --exclude=always: always-run scripts are pending by design, not drift.
476
+ status_output=$(chezmoi status --exclude=always 2>/dev/null || echo "")
444
477
  if [[ -z "$status_output" ]]; then
445
478
  check "Chezmoi sync" "pass"
446
479
  else
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  set -euo pipefail
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  ## MCP configuration diagnostics and hardening checks.
5
5
  ##
@@ -398,7 +398,7 @@ if command -v jq >/dev/null 2>&1; then
398
398
  {
399
399
  jq -r '.mcpServers | to_entries[]? | (.value.env // {}) | to_entries[]?.value' "$MCP_CONFIG"
400
400
  jq -r '.mcpServers | to_entries[]? | (.value.args // [])[]?' "$MCP_CONFIG"
401
- } | sed -n 's/^\${\([A-Z0-9_]\+\)}$/\1/p' | sort -u
401
+ } | sed -n 's/^\${\([A-Z0-9_][A-Z0-9_]*\)}$/\1/p' | sort -u
402
402
  )"
403
403
  if [[ -z "$env_vars" ]]; then
404
404
  log_success "Server env placeholders" "none declared"
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Shell Performance Profiling
5
5
  # Usage: dot perf [--json|-j] [--profile|-p] [--runs|-r N] [--target|-t MS]
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Dotfiles Scorecard
5
5
  # Usage: dot scorecard [--json|-j]
@@ -52,7 +52,8 @@ perf_target=$(python3 -c 'import json,sys; j=json.loads(sys.stdin.read()); print
52
52
 
53
53
  # Drift count
54
54
  if command -v chezmoi >/dev/null 2>&1; then
55
- drift_count=$(chezmoi status 2>/dev/null | wc -l | tr -d ' ')
55
+ # --exclude=always: always-run scripts are pending by design, not drift.
56
+ drift_count=$(chezmoi status --exclude=always 2>/dev/null | wc -l | tr -d ' ')
56
57
  else
57
58
  drift_count=0
58
59
  fi
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  set -euo pipefail
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  ## Security Score Assessment.
5
5
  ##
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Smoke tests to quickly verify toolchain health after installation/updates.
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Snapshot current system/tooling state
5
5
  # Usage: dot snapshot [--baseline|-b] [--force|-f]
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Dotfiles post-merge verification checks
5
5
  # Runs doctor + status + diff checks and exits non-zero on issues.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # MIT License
3
- # SPDX-License-Identifier: MIT
3
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
4
4
  # Copyright (c) 2015-2026 Sebastien Rousseau
5
5
  # See LICENSE file for details.
6
6
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Version Locks Summary
5
5
  # Usage: dot locks
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  ## Workstation attestation export.
5
5
  ##
@@ -25,12 +25,23 @@ JSON_MODE=0
25
25
  WRITE_PATH=""
26
26
  FLEET_STORE=""
27
27
  FLEET_ID=""
28
+ VERIFY=0
29
+ VERIFY_MAX_AGE=""
28
30
  while [[ $# -gt 0 ]]; do
29
31
  case "$1" in
30
32
  --json | -j)
31
33
  JSON_MODE=1
32
34
  shift
33
35
  ;;
36
+ --verify | -V)
37
+ VERIFY=1
38
+ shift
39
+ ;;
40
+ --max-age | -a)
41
+ VERIFY_MAX_AGE="${2:-}"
42
+ VERIFY=1
43
+ shift 2
44
+ ;;
34
45
  --write | -w)
35
46
  WRITE_PATH="${2:-}"
36
47
  shift 2
@@ -171,6 +182,20 @@ if [[ -n "$FLEET_STORE" ]]; then
171
182
  printf '%s\n' "$attestation_json" >"$fleet_timestamp_file"
172
183
  fi
173
184
 
185
+ # Hand the evidence to the WebAssembly verifier rather than printing it.
186
+ # The module has no access to this machine beyond the bytes on its stdin, so
187
+ # the verdict does not depend on the tools the machine under review happens
188
+ # to have installed.
189
+ if [[ "$VERIFY" -eq 1 ]]; then
190
+ verify_args=(--verify)
191
+ [[ "$JSON_MODE" -eq 1 ]] && verify_args+=(--json)
192
+ [[ -n "$VERIFY_MAX_AGE" ]] && verify_args+=(--max-age "$VERIFY_MAX_AGE")
193
+ verify_status=0
194
+ printf '%s\n' "$attestation_json" |
195
+ bash "$SCRIPT_DIR/attest-verify.sh" "${verify_args[@]}" || verify_status=$?
196
+ exit "$verify_status"
197
+ fi
198
+
174
199
  if [[ "$JSON_MODE" -eq 1 ]]; then
175
200
  printf '%s\n' "$attestation_json"
176
201
  else
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Dotfiles CLI - Agent Mode Commands (extracted from meta.sh)
5
5
  # mode list|current|show|set|run|doctor|card|log|checkpoint|conformance
@@ -21,6 +21,55 @@ _agent_repo_root() {
21
21
  require_source_dir
22
22
  }
23
23
 
24
+ ## _agent_run_bounded <seconds> <command…> — run a command under a wall-clock
25
+ ## limit on every platform we ship to.
26
+ ##
27
+ ## `timeout` is GNU coreutils. Stock macOS has neither it nor `gtimeout`
28
+ ## (that one arrives with `brew install coreutils`), so the delegate arm's
29
+ ## former `timeout "$t" "$@"` meant the delegated command never ran at all on
30
+ ## a clean Mac — it died 127, "command not found". perl is present on both
31
+ ## macOS and the CI runners and gives us fork + alarm, which is what timeout(1)
32
+ ## does. tests/framework/assertions.sh solves the same problem the same way.
33
+ ##
34
+ ## Exit codes follow GNU timeout: 124 on expiry, 128+N when signalled,
35
+ ## otherwise the command's own status.
36
+ _agent_run_bounded() {
37
+ local seconds="$1"
38
+ shift
39
+ local bin
40
+ bin="$(type -P timeout || true)"
41
+ [[ -n "$bin" ]] || bin="$(type -P gtimeout || true)"
42
+ if [[ -n "$bin" ]]; then
43
+ "$bin" "$seconds" "$@"
44
+ return
45
+ fi
46
+ if command -v perl >/dev/null 2>&1; then
47
+ perl -e '
48
+ my $secs = shift @ARGV;
49
+ my $pid = fork();
50
+ die "dot: fork failed: $!\n" unless defined $pid;
51
+ if ($pid == 0) {
52
+ exec { $ARGV[0] } @ARGV;
53
+ exit 127;
54
+ }
55
+ my $timed_out = 0;
56
+ $SIG{ALRM} = sub { $timed_out = 1; kill("TERM", $pid); kill("KILL", $pid); };
57
+ alarm($secs);
58
+ my $reaped;
59
+ do { $reaped = waitpid($pid, 0); } while ($reaped == -1 && $!{EINTR});
60
+ my $status = $?;
61
+ alarm(0);
62
+ exit(124) if $timed_out;
63
+ exit($status & 127 ? 128 + ($status & 127) : $status >> 8);
64
+ ' "$seconds" "$@"
65
+ return
66
+ fi
67
+ # Neither available: running the command is still better than refusing to,
68
+ # but the lost guarantee is said out loud rather than assumed.
69
+ ui_warn "Delegate" "no timeout(1) or perl on PATH — running without a time limit"
70
+ "$@"
71
+ }
72
+
24
73
  _agent_profiles_file() {
25
74
  local repo_root
26
75
  repo_root="$(_agent_repo_root)"
@@ -228,15 +277,16 @@ EOF
228
277
  checkpoint_id="$(basename "$checkpoint_file" .json)"
229
278
  ui_info "Agent mode" "$name"
230
279
  dot_agent_session_log "run_start" "$name" "running" "argv=$*" "checkpoint_id=$checkpoint_id"
231
- # `if !` instead of `set +e / "$@" / set -e` — bash's errexit
232
- # is automatically suspended inside a conditional, so we capture
233
- # the exit code cleanly without toggling shell options.
280
+ # Run inside the `if` condition (errexit is suspended there) and
281
+ # read `$?` in the else-branch: it holds the command's own status.
282
+ # `if ! cmd; then exit_code=$?` reads the negation's status (0) and
283
+ # silently reported every failure as exit 0.
234
284
  local exit_code=0
235
- if ! "$@"; then
285
+ if "$@"; then
286
+ dot_agent_session_log "run_finish" "$name" "ok" "exit_code=$exit_code" "checkpoint_id=$checkpoint_id"
287
+ else
236
288
  exit_code=$?
237
289
  dot_agent_session_log "run_finish" "$name" "failed" "exit_code=$exit_code" "checkpoint_id=$checkpoint_id"
238
- else
239
- dot_agent_session_log "run_finish" "$name" "ok" "exit_code=$exit_code" "checkpoint_id=$checkpoint_id"
240
290
  fi
241
291
  return "$exit_code"
242
292
  ;;
@@ -347,11 +397,11 @@ EOF
347
397
  _agent_apply_profile_env "$replay_profile"
348
398
  dot_agent_session_log "checkpoint_replay" "$replay_profile" "running" "checkpoint_id=$checkpoint_id"
349
399
  local exit_code=0
350
- if ! "${replay_argv[@]}"; then
400
+ if "${replay_argv[@]}"; then
401
+ dot_agent_session_log "checkpoint_replay_finish" "$replay_profile" "ok" "checkpoint_id=$checkpoint_id" "exit_code=$exit_code"
402
+ else
351
403
  exit_code=$?
352
404
  dot_agent_session_log "checkpoint_replay_finish" "$replay_profile" "failed" "checkpoint_id=$checkpoint_id" "exit_code=$exit_code"
353
- else
354
- dot_agent_session_log "checkpoint_replay_finish" "$replay_profile" "ok" "checkpoint_id=$checkpoint_id" "exit_code=$exit_code"
355
405
  fi
356
406
  return "$exit_code"
357
407
  ;;
@@ -392,13 +442,13 @@ EOF
392
442
  dot_agent_session_log "delegate_start" "$delegate_profile" "running" "delegate=$delegate_name" "parent=$current_profile" "timeout=$delegate_timeout"
393
443
  ui_info "Delegating" "$delegate_name (profile: $delegate_profile, timeout: ${delegate_timeout}s)"
394
444
  local exit_code=0
395
- if ! timeout "$delegate_timeout" "$@"; then
445
+ if _agent_run_bounded "$delegate_timeout" "$@"; then
446
+ dot_agent_session_log "delegate_finish" "$delegate_profile" "ok" "delegate=$delegate_name" "exit_code=$exit_code"
447
+ ui_ok "Delegate" "$delegate_name completed"
448
+ else
396
449
  exit_code=$?
397
450
  dot_agent_session_log "delegate_finish" "$delegate_profile" "failed" "delegate=$delegate_name" "exit_code=$exit_code"
398
451
  ui_err "Delegate" "$delegate_name failed (exit $exit_code)"
399
- else
400
- dot_agent_session_log "delegate_finish" "$delegate_profile" "ok" "delegate=$delegate_name" "exit_code=$exit_code"
401
- ui_ok "Delegate" "$delegate_name completed"
402
452
  fi
403
453
  return "$exit_code"
404
454
  ;;
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # shellcheck shell=bash
5
5
  #
@@ -148,10 +148,18 @@ cmd_agents() {
148
148
  ui_warn "AGENTS.md" "missing — run 'dot agents render'"
149
149
  return 1
150
150
  fi
151
- # Diff the bodies (header comments differ by design).
152
- # `--ignore-blank-lines` so the trailing newline from the footer
153
- # block doesn't false-positive as drift.
154
- if diff -q --ignore-blank-lines <(_agents_body "$claude_md") <(_agents_body "$agents_md") >/dev/null 2>&1; then
151
+ # Compare the bodies (header comments differ by design), ignoring
152
+ # blank lines: the rendered file carries an extra one after its
153
+ # header block. This deliberately does NOT lean on
154
+ # `diff -q --ignore-blank-lines` some BSD diff builds (macOS 14's,
155
+ # for one) short-circuit `-q` to a byte comparison and drop the
156
+ # ignore flags, which made `check` report drift on a tree it had
157
+ # just rendered. Stripping the blank lines here is implementation-
158
+ # independent.
159
+ local _claude_body _agents_body_text
160
+ _claude_body="$(_agents_body "$claude_md" | grep -v '^[[:space:]]*$')"
161
+ _agents_body_text="$(_agents_body "$agents_md" | grep -v '^[[:space:]]*$')"
162
+ if [[ "$_claude_body" == "$_agents_body_text" ]]; then
155
163
  ui_ok "AGENTS.md" "in sync with CLAUDE.md"
156
164
  return 0
157
165
  fi
@@ -185,9 +193,17 @@ cmd_agents() {
185
193
  -->
186
194
 
187
195
  HEADER
188
- # Replace the title line so the rendered file declares its
189
- # purpose, but pass everything else through unchanged.
190
- _agents_body "$claude_md" | sed '1s/^# CLAUDE\.md.*/# AGENTS.md — AI Assistant Guidelines/'
196
+ # The title is emitted, not substituted. This used to be
197
+ #
198
+ # _agents_body | sed '1s/^# CLAUDE\.md.*/# AGENTS.md — …/'
199
+ #
200
+ # but _agents_body drops the H1 by design (the two files differ
201
+ # there), so line 1 of its output was never the CLAUDE.md title and
202
+ # the substitution could not fire: the rendered file carried no title
203
+ # at all. `check` is unaffected either way — it compares bodies with
204
+ # the H1 stripped from both sides.
205
+ printf '# AGENTS.md — AI Assistant Guidelines\n'
206
+ _agents_body "$claude_md"
191
207
  cat <<'FOOTER'
192
208
 
193
209
  ---