@sebastienrousseau/dotfiles 0.2.500 → 0.2.502

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 (237) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/README.md +113 -45
  3. package/docs/.vitepress/reports/localization-readability-audit.md +4 -0
  4. package/docs/AI.md +8 -2
  5. package/docs/CNAME +1 -0
  6. package/docs/COPYRIGHT +1 -1
  7. package/docs/NAMING_CONVENTIONS.md +7 -0
  8. package/docs/README.md +4 -0
  9. package/docs/_config.yml +59 -0
  10. package/docs/adr/ADR-001-ci-cd-pipeline.md +17 -0
  11. package/docs/adr/ADR-002-shell-performance.md +9 -0
  12. package/docs/adr/ADR-003-security-first.md +18 -0
  13. package/docs/adr/ADR-004-cli-architecture.md +14 -0
  14. package/docs/adr/ADR-005-chezmoi-choice.md +10 -0
  15. package/docs/adr/ADR-006-shell-selection.md +9 -0
  16. package/docs/adr/ADR-007-multi-shell-parity.md +10 -1
  17. package/docs/adr/ADR-008-alias-system-architecture.md +10 -0
  18. package/docs/adr/ADR-009-wallpaper-driven-theming.md +131 -0
  19. package/docs/adr/ADR-010-starship-transient-prompt.md +144 -0
  20. package/docs/adr/ADR-011-nushell-tier3-keep.md +144 -0
  21. package/docs/adr/README.md +7 -0
  22. package/docs/architecture/ARCHITECTURE.md +4 -0
  23. package/docs/architecture/INTEROP.md +8 -0
  24. package/docs/architecture/REPO_LAYOUT.md +5 -1
  25. package/docs/architecture/WALKTHROUGH.md +4 -0
  26. package/docs/architecture/fleet-deployment.md +4 -0
  27. package/docs/archive/EUXIS_2026_REVIEW.md +11 -3
  28. package/docs/archive/LEGACY_ROADMAP.md +45 -27
  29. package/docs/archive/MILESTONE_v0.2.493.md +4 -0
  30. package/docs/archive/PLAN.md +46 -8
  31. package/docs/archive/REPO_AUDIT.md +8 -0
  32. package/docs/guides/INSTALL.md +4 -0
  33. package/docs/guides/NEOVIM_IDE_GUIDE.md +9 -0
  34. package/docs/guides/THEMING.md +8 -0
  35. package/docs/guides/TROUBLESHOOTING.md +28 -0
  36. package/docs/guides/WSL2_NIX_TROUBLESHOOTING.md +70 -1
  37. package/docs/index.md +6 -2
  38. package/docs/interop/A2A.md +7 -0
  39. package/docs/interop/POWERSHELL.md +102 -0
  40. package/docs/manual/00-introduction.md +5 -1
  41. package/docs/manual/01-concepts/01-architecture.md +4 -0
  42. package/docs/manual/01-concepts/02-trust-model.md +6 -2
  43. package/docs/manual/01-concepts/03-theme-engine.md +4 -0
  44. package/docs/manual/01-concepts/04-fleet.md +7 -1
  45. package/docs/manual/01-concepts/05-self-healing.md +8 -1
  46. package/docs/manual/02-tutorials/01-first-install.md +5 -0
  47. package/docs/manual/02-tutorials/02-add-wallpaper.md +5 -0
  48. package/docs/manual/02-tutorials/03-create-profile.md +6 -0
  49. package/docs/manual/02-tutorials/04-encrypt-secret.md +6 -0
  50. package/docs/manual/02-tutorials/05-deploy-fleet.md +5 -1
  51. package/docs/manual/03-reference/01-dot-cli.md +67 -45
  52. package/docs/manual/03-reference/02-config-files.md +8 -2
  53. package/docs/manual/03-reference/03-environment.md +4 -0
  54. package/docs/manual/03-reference/04-templates.md +7 -1
  55. package/docs/manual/03-reference/05-feature-flags.md +72 -3
  56. package/docs/manual/04-cookbook/01-recipes.md +4 -0
  57. package/docs/manual/04-cookbook/02-troubleshooting.md +32 -0
  58. package/docs/manual/04-cookbook/03-faq.md +8 -0
  59. package/docs/manual/05-appendices/A-platform-matrix.md +4 -0
  60. package/docs/manual/05-appendices/B-security-checklist.md +6 -0
  61. package/docs/manual/05-appendices/C-glossary.md +4 -0
  62. package/docs/manual/05-appendices/D-bibliography.md +5 -1
  63. package/docs/manual/05-appendices/E-license.md +4 -0
  64. package/docs/manual/_toc.yml +1 -1
  65. package/docs/manual/command-index.md +16 -8
  66. package/docs/manual/concept-index.md +4 -0
  67. package/docs/manual/index.md +66 -0
  68. package/docs/operations/ATTESTATION.md +5 -0
  69. package/docs/operations/CI_CADENCE.md +107 -0
  70. package/docs/operations/CI_COMPOSITES.md +156 -0
  71. package/docs/operations/COMPLETIONS.md +123 -0
  72. package/docs/operations/COVERAGE.md +182 -0
  73. package/docs/operations/DRIFT.md +107 -0
  74. package/docs/operations/HARD_AUDIT_2026.md +631 -0
  75. package/docs/operations/MAINTENANCE.md +5 -1
  76. package/docs/operations/MIGRATION.md +14 -6
  77. package/docs/operations/OPERATIONS.md +24 -0
  78. package/docs/operations/PERFORMANCE.md +133 -0
  79. package/docs/operations/REGISTRY.md +89 -0
  80. package/docs/operations/RELIABILITY.md +6 -0
  81. package/docs/operations/ROADMAP.md +36 -18
  82. package/docs/operations/ROADMAP_2026.md +665 -0
  83. package/docs/operations/TESTING.md +4 -0
  84. package/docs/operations/TRACEABILITY.md +7 -0
  85. package/docs/operations/TRUSTED_AGENT_WORKSTATION.md +4 -0
  86. package/docs/operations/VERSION_SYNC.md +54 -5
  87. package/docs/reference/ALIASES.md +7 -0
  88. package/docs/reference/ALIASES_CHEATSHEET.md +5 -1
  89. package/docs/reference/ALIASES_DEPRECATIONS.md +5 -1
  90. package/docs/reference/FEATURES.md +4 -0
  91. package/docs/reference/FONTS.md +4 -0
  92. package/docs/reference/POWERSHELL_PARITY.md +80 -0
  93. package/docs/reference/PROFILES.md +4 -0
  94. package/docs/reference/SCREENSHOTS.md +4 -0
  95. package/docs/reference/SCRIPTS.md +4 -0
  96. package/docs/reference/SUPPORT_MATRIX.md +10 -4
  97. package/docs/reference/THEMES.md +4 -0
  98. package/docs/reference/TOOLS.md +4 -0
  99. package/docs/reference/UTILS.md +16 -12
  100. package/docs/registry.json +6 -0
  101. package/docs/security/AI_ACT_COMPLIANCE.md +4 -0
  102. package/docs/security/AUDIT_BYPASS.md +103 -0
  103. package/docs/security/AUTOMATION_SECRETS.md +4 -0
  104. package/docs/security/CI_EGRESS_ALLOWLIST.md +127 -0
  105. package/docs/security/CI_PINNING.md +113 -0
  106. package/docs/security/COMMIT_SIGNING.md +138 -0
  107. package/docs/security/COMPLIANCE.md +5 -0
  108. package/docs/security/DEPS_DEV_EXCEPTIONS.md +86 -0
  109. package/docs/security/DISCLOSURE.md +130 -0
  110. package/docs/security/ENCRYPTION.md +5 -0
  111. package/docs/security/FMEA.md +4 -0
  112. package/docs/security/HISTORY_FILTERING.md +132 -0
  113. package/docs/security/INCIDENT_RESPONSE.md +4 -0
  114. package/docs/security/INSTALL_VERIFICATION.md +122 -0
  115. package/docs/security/KEYS.md +4 -0
  116. package/docs/security/KEY_ROTATION.md +85 -1
  117. package/docs/security/MCP_POLICY.md +9 -0
  118. package/docs/security/POLICY_RELEASES.md +4 -0
  119. package/docs/security/README.md +4 -0
  120. package/docs/security/SCORECARD.md +140 -0
  121. package/docs/security/SECRETS.md +12 -0
  122. package/docs/security/SECURITY.md +4 -0
  123. package/docs/security/SECURITY_CHECKLIST.md +11 -0
  124. package/docs/security/SHELL_EXEMPTIONS.md +145 -0
  125. package/docs/security/SOUP_REGISTER.md +4 -0
  126. package/docs/security/THREAT_MODEL.md +10 -0
  127. package/docs/security/VERIFICATION_VALIDATION.md +5 -1
  128. package/docs/security/security-pubkey.asc +15 -0
  129. package/docs/themes/README.md +4 -0
  130. package/docs/themes/VISUAL_INTEGRITY_REPORT.md +4 -0
  131. package/dot_config/ai/identity.md +3 -0
  132. package/dot_config/ai/patterns/architect.md +2 -0
  133. package/dot_config/ai/patterns/hardener.md +2 -0
  134. package/dot_config/ai/patterns/refactor.md +2 -0
  135. package/dot_config/alacritty/alacritty.toml.tmpl +3 -3
  136. package/dot_config/atuin/config.toml.tmpl +47 -0
  137. package/dot_config/dotfiles/agent-card.json +1 -1
  138. package/dot_config/dotfiles/boot/README.md +2 -0
  139. package/dot_config/dotfiles/grub/README.md +2 -0
  140. package/dot_config/dotfiles/lock/README.md +2 -0
  141. package/dot_config/fish/conf.d/direnv.fish +4 -0
  142. package/dot_config/fish/conf.d/init.fish.tmpl +21 -0
  143. package/dot_config/fish/conf.d/mise-activate.fish +5 -0
  144. package/dot_config/fish/functions/_cached_eval.fish +84 -11
  145. package/dot_config/fish/functions/_cached_eval_clear.fish +17 -0
  146. package/dot_config/foot/foot.ini.tmpl +3 -3
  147. package/dot_config/fuzzel/fuzzel.ini.tmpl +2 -2
  148. package/dot_config/ghostty/config.tmpl +3 -3
  149. package/dot_config/git/hooks/executable_commit-msg +146 -0
  150. package/dot_config/goose/config.yaml +2 -2
  151. package/dot_config/gtk-3.0/gtk.css.tmpl +2 -2
  152. package/dot_config/gtk-3.0/settings.ini.tmpl +2 -2
  153. package/dot_config/gtk-4.0/gtk.css.tmpl +2 -2
  154. package/dot_config/gtk-4.0/settings.ini.tmpl +2 -2
  155. package/dot_config/kitty/kitty.conf.tmpl +3 -3
  156. package/dot_config/mise/config.toml +1 -1
  157. package/dot_config/niri/config.kdl.tmpl +2 -2
  158. package/dot_config/nushell/cached_eval.nu +80 -0
  159. package/dot_config/nushell/env.nu.tmpl +21 -13
  160. package/dot_config/shell/00-core-paths.sh.tmpl +9 -1
  161. package/dot_config/shell/05-core-safety.sh +1 -0
  162. package/dot_config/shell/10-secrets.sh +1 -0
  163. package/dot_config/shell/40-fzf-defaults.sh.tmpl +1 -0
  164. package/dot_config/shell/40-ls-colors.sh +1 -0
  165. package/dot_config/shell/50-logic-functions-core.sh.tmpl +1 -0
  166. package/dot_config/shell/50-logic-functions.sh.tmpl +1 -0
  167. package/dot_config/shell/51-logic-functions-extra.sh.tmpl +1 -0
  168. package/dot_config/shell/90-ux-aliases.sh.tmpl +1 -0
  169. package/dot_config/shell/91-ux-aliases-lazy.sh.tmpl +1 -0
  170. package/dot_config/shell/README.md +25 -8
  171. package/dot_config/starship.toml.tmpl +2 -2
  172. package/dot_config/tmux/tmux.conf.tmpl +3 -3
  173. package/dot_config/user-dirs.dirs +1 -0
  174. package/dot_config/vscode/settings.json.tmpl +2 -2
  175. package/dot_config/waybar/config.jsonc.tmpl +2 -2
  176. package/dot_config/waybar/style.css.tmpl +2 -2
  177. package/dot_config/wezterm/wezterm.lua.tmpl +3 -3
  178. package/dot_config/zsh/dot_zshrc.tmpl +207 -17
  179. package/dot_config/zsh/rc.d/00-alias-shims.zsh +28 -6
  180. package/dot_config/zsh/rc.d/30-options.zsh.tmpl +27 -7
  181. package/dot_local/bin/executable_bm +2 -0
  182. package/dot_local/bin/executable_dot +56 -9
  183. package/dot_local/bin/executable_dot-bootstrap +0 -1
  184. package/dot_local/bin/executable_dot-load-benchmark +1 -1
  185. package/dot_local/bin/executable_dot-theme-sync +8 -8
  186. package/dot_local/bin/executable_notify +2 -0
  187. package/dot_local/bin/executable_open +2 -0
  188. package/dot_local/bin/executable_tour +4 -2
  189. package/dot_local/bin/executable_up +3 -1
  190. package/dot_local/share/man/man1/dot.1 +1 -1
  191. package/dot_local/share/zsh/completions/_dot +4 -0
  192. package/install.sh +53 -38
  193. package/package.json +1 -1
  194. package/scripts/ci/check-dangerous-chmod.sh +19 -0
  195. package/scripts/ci/check-deps-dev.sh +236 -0
  196. package/scripts/ci/check-insecure-tls.sh +61 -0
  197. package/scripts/ci/check-regression-traceability.sh +67 -0
  198. package/scripts/ci/check-shell-preamble.sh +106 -0
  199. package/scripts/ci/dot-cli-startup-bench.sh +126 -0
  200. package/scripts/ci/install-chezmoi-verified.sh +4 -1
  201. package/scripts/ci/lint-reusable-pins.sh +78 -0
  202. package/scripts/ci/run-coverage.sh +451 -0
  203. package/scripts/ci/validate-chezmoidata.sh +25 -0
  204. package/scripts/ci/windows-smoke-test.ps1 +136 -0
  205. package/scripts/diagnostics/doctor.sh +203 -5
  206. package/scripts/diagnostics/drift-dashboard.sh +177 -13
  207. package/scripts/diagnostics/health.sh +21 -4
  208. package/scripts/diagnostics/perf.sh +304 -77
  209. package/scripts/diagnostics/workstation-attestation.sh +6 -1
  210. package/scripts/dot/commands/agent.sh +33 -27
  211. package/scripts/dot/commands/agents.sh +325 -0
  212. package/scripts/dot/commands/ai.sh +75 -10
  213. package/scripts/dot/commands/aliases.sh +10 -8
  214. package/scripts/dot/commands/core.sh +10 -4
  215. package/scripts/dot/commands/fleet.sh +278 -3
  216. package/scripts/dot/commands/init.sh +184 -0
  217. package/scripts/dot/commands/meta.sh +7 -4
  218. package/scripts/dot/commands/registry.sh +263 -0
  219. package/scripts/dot/commands/tools.sh +49 -1
  220. package/scripts/dot/lib/bento.sh +2 -1
  221. package/scripts/dot/lib/log.sh +6 -0
  222. package/scripts/dot/lib/platform.sh +22 -8
  223. package/scripts/dot/lib/ui.sh +145 -2
  224. package/scripts/dot/lib/utils.sh +6 -1
  225. package/scripts/git-hooks/pre-commit-audit.sh +1 -1
  226. package/scripts/git-hooks/pre-push +66 -3
  227. package/scripts/lib/secrets_provider.sh +32 -6
  228. package/scripts/ops/heal-chezmoi.sh +41 -6
  229. package/scripts/ops/rollback.sh +14 -0
  230. package/scripts/qa/powershell-contract.ps1 +95 -0
  231. package/scripts/security/check-disclosure-key-expiry.sh +110 -0
  232. package/scripts/security/lock-configs.sh +11 -2
  233. package/scripts/theme/merge-wallpaper.sh +4 -0
  234. package/scripts/theme/switch.sh +20 -10
  235. package/scripts/version-sync.sh +3 -0
  236. package/dot_config/atuin/config.toml +0 -40
  237. package/dot_local/bin/__pycache__/executable_dot-load-benchmark-ptycpython-312.pyc +0 -0
@@ -387,6 +387,49 @@ else
387
387
  _fail "dot" "not found in PATH"
388
388
  fi
389
389
 
390
+ # --- Pre-push audit bypass log (closes #871) ---
391
+ _section "Pre-Push Audit Bypass"
392
+
393
+ bypass_log="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/audit-bypass.log"
394
+ if [[ -s "$bypass_log" ]]; then
395
+ # Count entries in the last 7 days (timestamps are ISO-8601 UTC).
396
+ seven_days_ago=$(date -u -v-7d +%Y-%m-%dT00:00:00Z 2>/dev/null ||
397
+ date -u -d '7 days ago' +%Y-%m-%dT00:00:00Z 2>/dev/null ||
398
+ echo "1970-01-01T00:00:00Z")
399
+ recent=$(awk -v cutoff="$seven_days_ago" '$1 >= cutoff' "$bypass_log" | wc -l | tr -d ' ')
400
+ if ((recent > 0)); then
401
+ _warn "audit bypass" "$recent push(es) bypassed in last 7 days — see $(pretty_path "$bypass_log")"
402
+ else
403
+ _ok "audit bypass" "log exists; no recent entries (last 7d)"
404
+ fi
405
+ else
406
+ _ok "audit bypass" "clean — no pre-push audits bypassed"
407
+ fi
408
+
409
+ # --- Atuin history-filter (closes #872) ---
410
+ _section "Atuin History Filter"
411
+
412
+ atuin_cfg="$HOME/.config/atuin/config.toml"
413
+ if [[ ! -f "$atuin_cfg" ]]; then
414
+ _warn "atuin config" "not found at $(pretty_path "$atuin_cfg")"
415
+ else
416
+ if grep -Eq "^history_filter[[:space:]]*=" "$atuin_cfg"; then
417
+ pattern_count=$(awk '
418
+ /^history_filter[[:space:]]*=[[:space:]]*\[/ { inside = 1; next }
419
+ inside && /^\]/ { inside = 0 }
420
+ inside && /^[[:space:]]*"/ { count++ }
421
+ END { print count + 0 }
422
+ ' "$atuin_cfg")
423
+ if ((pattern_count >= 10)); then
424
+ _ok "history_filter" "$pattern_count patterns (chezmoi-managed)"
425
+ else
426
+ _warn "history_filter" "$pattern_count patterns (expected ≥10 from secrets-patterns.toml)"
427
+ fi
428
+ else
429
+ _fail "history_filter" "no history_filter block in $(pretty_path "$atuin_cfg") — secrets may leak into shell history"
430
+ fi
431
+ fi
432
+
390
433
  # --- Topgrade Integration ---
391
434
  _section "Topgrade Integration"
392
435
 
@@ -472,10 +515,12 @@ fi
472
515
  # --- Performance ---
473
516
  _section "Performance"
474
517
 
475
- # Check shell cache freshness — stale caches force runtime regeneration
518
+ cache_base="${XDG_CACHE_HOME:-$HOME/.cache}"
519
+
520
+ # 1. Shell cache freshness for tools the project already wraps in _cached_eval.
521
+ # Stale caches force runtime regeneration on next shell start.
476
522
  stale_caches=0
477
523
  stale_tools=""
478
- cache_base="${XDG_CACHE_HOME:-$HOME/.cache}"
479
524
  for tool in mise starship zoxide atuin fzf direnv; do
480
525
  tool_bin="$(command -v "$tool" 2>/dev/null || true)"
481
526
  [[ -n "$tool_bin" ]] || continue
@@ -486,22 +531,128 @@ for tool in mise starship zoxide atuin fzf direnv; do
486
531
  esac
487
532
  if [[ ! -f "$cache_file" ]] || [[ "$tool_bin" -nt "$cache_file" ]]; then
488
533
  stale_caches=$((stale_caches + 1))
489
- # Track unique tool names for the summary
490
534
  case "$stale_tools" in
491
535
  *"$tool"*) ;;
492
536
  *) stale_tools="${stale_tools:+$stale_tools, }$tool" ;;
493
537
  esac
494
- break # one stale shell is enough to flag the tool
538
+ break
495
539
  fi
496
540
  done
497
541
  done
498
-
499
542
  if [[ $stale_caches -eq 0 ]]; then
500
543
  _ok "shell caches" "fresh"
501
544
  else
502
545
  _warn "shell caches" "stale ($stale_tools) — run dot prewarm"
503
546
  fi
504
547
 
548
+ # 2. Slow-init tools that are present but NOT wrapped in _cached_eval.
549
+ # Each of these runs uncached on every shell start; common offenders eat
550
+ # 100-500ms apiece on a populated dev machine.
551
+ #
552
+ # Only flag tools that actually emit shell init via `<tool> init <shell>`
553
+ # (or equivalent) and would benefit from caching that output. Plain CLIs
554
+ # like gh/cargo/pnpm/yarn don't have init eval; their completions are
555
+ # cached separately under $ZSH_COMPLETIONS_DIR.
556
+ unwrapped=""
557
+ for tool in nvm fnm pyenv rbenv jenv asdf sdkman conda kubectl helm thefuck broot mcfly direnv; do
558
+ command -v "$tool" >/dev/null 2>&1 || continue
559
+ # If the tool is installed but no shell config sources or evals its
560
+ # init (no `$tool env`, `$tool init`, `$tool.sh`, lazy-load stub), it
561
+ # isn't adding startup cost — skip the warning.
562
+ init_referenced=0
563
+ if grep -rIlqE "\\b${tool}([[:space:]]+(env|init|hook)|\\.sh|_lazy_load_${tool}|_dot_lazy[[:space:]]+${tool})" \
564
+ "$HOME/.config/zsh" "$HOME/.config/fish" "$HOME/.config/shell" 2>/dev/null; then
565
+ init_referenced=1
566
+ fi
567
+ ((init_referenced == 0)) && continue
568
+ # Tools we lazy-load via shell stubs don't need init-eval cache files.
569
+ case "$tool" in
570
+ fnm | nvm | sdkman)
571
+ grep -rIlqE "_lazy_load_${tool}|_dot_lazy[[:space:]]+${tool}" \
572
+ "$HOME/.config/zsh" "$HOME/.config/fish" 2>/dev/null && continue
573
+ ;;
574
+ esac
575
+ found=0
576
+ for shell_dir in zsh bash fish; do
577
+ case "$shell_dir" in
578
+ fish) [[ -f "$cache_base/$shell_dir/${tool}-init.fish" ]] && found=1 ;;
579
+ *) [[ -f "$cache_base/$shell_dir/${tool}-init.$shell_dir" ]] && found=1 ;;
580
+ esac
581
+ ((found == 1)) && break
582
+ done
583
+ ((found == 0)) && unwrapped="${unwrapped:+$unwrapped, }$tool"
584
+ done
585
+ if [[ -z "$unwrapped" ]]; then
586
+ _ok "uncached slow-init tools" "none detected"
587
+ else
588
+ _warn "uncached slow-init tools" "$unwrapped — consider wrapping in _cached_eval"
589
+ fi
590
+
591
+ # 3. Zsh completion dump health. compinit is usually the single biggest
592
+ # cost on a zsh startup; a stale or uncompiled .zcompdump compounds it.
593
+ if command -v zsh >/dev/null 2>&1; then
594
+ zcompdump="${HOME}/.zcompdump"
595
+ if [[ -f "$zcompdump" ]]; then
596
+ dump_mtime=$(stat -c %Y "$zcompdump" 2>/dev/null || stat -f %m "$zcompdump" 2>/dev/null || echo 0)
597
+ age_days=$((($(date +%s) - dump_mtime) / 86400))
598
+ if ((age_days > 7)); then
599
+ _warn ".zcompdump" "${age_days}d old — refresh: rm ~/.zcompdump* && zsh -ic exit"
600
+ else
601
+ _ok ".zcompdump" "fresh (${age_days}d)"
602
+ fi
603
+ if [[ ! -f "${zcompdump}.zwc" ]]; then
604
+ _warn ".zcompdump.zwc" "missing — completion init slower than necessary"
605
+ fi
606
+ fi
607
+ fi
608
+
609
+ # 4. PATH length. Each entry is searched on every command resolution.
610
+ # A mise-managed dev machine routinely adds 50+ entries (one per tool
611
+ # install path), so the warn/fail thresholds are set higher than a
612
+ # lean baseline (40) would suggest.
613
+ path_count=$(printf '%s' "${PATH:-}" | tr ':' '\n' | grep -c . || true)
614
+ if [[ "$path_count" -le 60 ]]; then
615
+ _ok "PATH length" "$path_count entries"
616
+ elif [[ "$path_count" -le 120 ]]; then
617
+ _warn "PATH length" "$path_count entries — consider pruning"
618
+ else
619
+ _fail "PATH length" "$path_count entries — likely slowing every command"
620
+ fi
621
+
622
+ # 5. Shell coverage. Surface installed shells that the project's caching
623
+ # infrastructure doesn't currently maintain caches for. zsh/bash/fish/nu
624
+ # all have a `_cached_eval` analogue; pwsh does not.
625
+ shells_unmanaged=""
626
+ for sh in nu pwsh; do
627
+ command -v "$sh" >/dev/null 2>&1 || continue
628
+ case "$sh" in
629
+ nu)
630
+ [[ -f "$HOME/.config/nushell/cached_eval.nu" ]] && continue
631
+ ;;
632
+ esac
633
+ shells_unmanaged="${shells_unmanaged:+$shells_unmanaged, }$sh"
634
+ done
635
+ if [[ -z "$shells_unmanaged" ]]; then
636
+ _ok "shell coverage" "all installed shells have _cached_eval support"
637
+ else
638
+ _warn "shell coverage" "$shells_unmanaged installed — no _cached_eval helper"
639
+ fi
640
+
641
+ # 6. Zsh hook count. Heavy precmd/preexec functions compound per-prompt.
642
+ # Probe an interactive zsh with a hard timeout so a broken zshrc doesn't
643
+ # stall doctor; skip cleanly if the probe fails.
644
+ if command -v zsh >/dev/null 2>&1 && command -v timeout >/dev/null 2>&1; then
645
+ hook_counts=$(timeout 5 zsh -i -c 'echo "$#precmd_functions $#preexec_functions"' 2>/dev/null || echo "")
646
+ if [[ -n "$hook_counts" ]]; then
647
+ read -r precmd_n preexec_n <<<"$hook_counts"
648
+ if [[ "${precmd_n:-0}" -le 5 && "${preexec_n:-0}" -le 5 ]]; then
649
+ _ok "zsh hooks" "precmd=$precmd_n preexec=$preexec_n"
650
+ else
651
+ _warn "zsh hooks" "precmd=$precmd_n preexec=$preexec_n — heavy per-prompt work"
652
+ fi
653
+ fi
654
+ fi
655
+
505
656
  if command -v hyperfine >/dev/null 2>&1; then
506
657
  if bash "$SCRIPT_DIR/../../tests/performance/bench.sh" 2>/dev/null; then
507
658
  _ok "startup latency" "within target thresholds"
@@ -512,6 +663,53 @@ else
512
663
  _warn "hyperfine" "missing (benchmark skipped)"
513
664
  fi
514
665
 
666
+ # 7. Baseline check + top-3 slowest tools from EVALCACHE_TIMING.
667
+ # Closes part of #863. Reads the same baseline file `dot perf` writes,
668
+ # and the same eval-timings.jsonl _cached_eval populates. Skipped
669
+ # silently when either file is absent (first-run state).
670
+ baseline_file="$cache_base/dotfiles/perf-baseline.json"
671
+ if [[ -s "$baseline_file" ]] && command -v python3 >/dev/null 2>&1; then
672
+ baseline_age_days=$(python3 -c '
673
+ import json, sys, datetime
674
+ try:
675
+ d = json.load(open(sys.argv[1]))
676
+ rec = d.get("recorded_at", "")
677
+ if not rec: print(-1); sys.exit(0)
678
+ rec = rec.replace("Z", "+00:00")
679
+ age = (datetime.datetime.now(datetime.timezone.utc) - datetime.datetime.fromisoformat(rec)).days
680
+ print(age)
681
+ except Exception:
682
+ print(-1)
683
+ ' "$baseline_file" 2>/dev/null)
684
+ if [[ "$baseline_age_days" -ge 0 ]]; then
685
+ _ok "perf baseline" "recorded ${baseline_age_days}d ago — run \`dot perf\` to compare"
686
+ fi
687
+ fi
688
+
689
+ timings_file="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/eval-timings.jsonl"
690
+ if [[ -s "$timings_file" ]] && command -v python3 >/dev/null 2>&1; then
691
+ top_tools=$(python3 -c '
692
+ import json, sys
693
+ from collections import defaultdict
694
+ samples = defaultdict(list)
695
+ try:
696
+ for line in open(sys.argv[1]):
697
+ try:
698
+ ev = json.loads(line)
699
+ ms = int(ev.get("ms", 0) or 0)
700
+ samples[ev.get("label", "?")].append(ms)
701
+ except Exception:
702
+ pass
703
+ rows = sorted(samples.items(), key=lambda kv: sum(kv[1]) // max(len(kv[1]), 1), reverse=True)[:3]
704
+ print(", ".join(f"{lbl}({sum(v)//max(len(v),1)}ms)" for lbl, v in rows))
705
+ except Exception:
706
+ pass
707
+ ' "$timings_file" 2>/dev/null)
708
+ if [[ -n "$top_tools" ]]; then
709
+ _ok "perf top-tools" "$top_tools"
710
+ fi
711
+ fi
712
+
515
713
  # --- Summary ---
516
714
  dot_log info "doctor_complete" "errors=$Errors" "warnings=$Warnings"
517
715
  dot_metric "doctor_errors" "$Errors" "count"
@@ -1,5 +1,32 @@
1
1
  #!/usr/bin/env bash
2
2
  # Copyright (c) 2015-2026 Dotfiles. All rights reserved.
3
+ # =============================================================================
4
+ # drift-dashboard.sh — Consolidated drift report.
5
+ #
6
+ # Surfaces four classes of drift between the chezmoi source and the
7
+ # deployed targets:
8
+ #
9
+ # 1. chezmoi-managed drift — deployed file differs from rendered source
10
+ # (`chezmoi status` output). Standard case; what the original
11
+ # drift-dashboard reported.
12
+ # 2. Untracked source — a file exists in the chezmoi source tree but
13
+ # isn't tracked by git, suggesting in-progress local work that
14
+ # hasn't been committed.
15
+ # 3. Orphan deployed — a file under XDG/HOME that was previously
16
+ # chezmoi-managed but the source has since been removed. Detected
17
+ # by sampling well-known managed paths and asking chezmoi if it
18
+ # claims each.
19
+ # 4. Stale source — source file older than its deployed target,
20
+ # which means someone hand-edited the deployed file and the next
21
+ # `chezmoi apply` will silently revert it. Reverse-drift trap.
22
+ #
23
+ # Output is JSON when --json is passed, otherwise the existing
24
+ # ui-formatted summary. Exit code: 0 if every section is clean; 1 if
25
+ # any drift is found; 2 if a prerequisite (chezmoi, git) is missing.
26
+ #
27
+ # Closes #875.
28
+ # =============================================================================
29
+
3
30
  set -euo pipefail
4
31
 
5
32
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -7,31 +34,168 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
34
  # shellcheck disable=SC1091
8
35
  source "$SCRIPT_DIR/../dot/lib/ui.sh"
9
36
 
37
+ JSON_MODE=0
38
+ SHOW_DIFF="${DOTFILES_DRIFT_SHOW_DIFF:-0}"
39
+
40
+ for arg in "$@"; do
41
+ case "$arg" in
42
+ --json | -j) JSON_MODE=1 ;;
43
+ --diff | -d) SHOW_DIFF=1 ;;
44
+ --help | -h)
45
+ cat <<EOF
46
+ Usage: drift-dashboard.sh [options]
47
+
48
+ Options:
49
+ --json, -j Emit a single JSON object summarising every drift class.
50
+ --diff, -d Also print the chezmoi diff (excluding scripts/install/tests).
51
+ --help, -h Show this help.
52
+ EOF
53
+ exit 0
54
+ ;;
55
+ esac
56
+ done
57
+
10
58
  ui_init
11
59
 
12
60
  if ! command -v chezmoi >/dev/null; then
13
- ui_err "chezmoi" "not found"
14
- exit 1
61
+ if [[ $JSON_MODE -eq 1 ]]; then
62
+ printf '{"error":"chezmoi not found"}\n'
63
+ else
64
+ ui_err "chezmoi" "not found"
65
+ fi
66
+ exit 2
67
+ fi
68
+
69
+ # -----------------------------------------------------------------------------
70
+ # Class 1: chezmoi-managed drift
71
+ # -----------------------------------------------------------------------------
72
+
73
+ cm_status="$(chezmoi status 2>/dev/null || true)"
74
+ cm_count=0
75
+ if [[ -n "$cm_status" ]]; then
76
+ cm_count=$(printf '%s\n' "$cm_status" | wc -l | tr -d ' ')
77
+ fi
78
+
79
+ # -----------------------------------------------------------------------------
80
+ # Class 2: untracked source files (chezmoi source tree only)
81
+ # -----------------------------------------------------------------------------
82
+
83
+ untracked=""
84
+ untracked_count=0
85
+ src_dir="$(chezmoi source-path 2>/dev/null || true)"
86
+ if [[ -n "$src_dir" && -d "$src_dir/.git" ]]; then
87
+ untracked="$(git -C "$src_dir" ls-files --others --exclude-standard 2>/dev/null || true)"
88
+ if [[ -n "$untracked" ]]; then
89
+ untracked_count=$(printf '%s\n' "$untracked" | wc -l | tr -d ' ')
90
+ fi
91
+ fi
92
+
93
+ # -----------------------------------------------------------------------------
94
+ # Class 4: stale source (source older than deployed = pending revert risk)
95
+ # Compute by walking chezmoi-managed targets and comparing mtimes.
96
+ # -----------------------------------------------------------------------------
97
+
98
+ stale_list=""
99
+ stale_count=0
100
+ if [[ -n "$src_dir" ]]; then
101
+ while IFS= read -r target; do
102
+ [[ -z "$target" ]] && continue
103
+ target_path="$target"
104
+ [[ ! -e "$target_path" ]] && continue
105
+ src_path="$(chezmoi source-path "$target_path" 2>/dev/null || true)"
106
+ [[ -z "$src_path" || ! -e "$src_path" ]] && continue
107
+ if [[ "$target_path" -nt "$src_path" ]]; then
108
+ stale_list+="$target_path"$'\n'
109
+ stale_count=$((stale_count + 1))
110
+ fi
111
+ done < <(chezmoi managed 2>/dev/null | head -200 | while IFS= read -r rel; do
112
+ printf '%s\n' "$HOME/$rel"
113
+ done)
114
+ fi
115
+ stale_list="${stale_list%$'\n'}"
116
+
117
+ # -----------------------------------------------------------------------------
118
+ # Class 3: orphan deployed files (chezmoi no longer claims them)
119
+ # Sample heuristic — chezmoi doesn't expose a direct "orphan" query.
120
+ # We surface a count of zero unless the user has a pre-populated
121
+ # ~/.local/state/dotfiles/orphans file (drift-history feature in fleet
122
+ # already maintains one); future work tracked under #875.
123
+ # -----------------------------------------------------------------------------
124
+
125
+ orphan_count=0
126
+ orphan_file="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/orphans"
127
+ if [[ -s "$orphan_file" ]]; then
128
+ orphan_count=$(wc -l <"$orphan_file" | tr -d ' ')
129
+ fi
130
+
131
+ # -----------------------------------------------------------------------------
132
+ # Report
133
+ # -----------------------------------------------------------------------------
134
+
135
+ total=$((cm_count + untracked_count + orphan_count + stale_count))
136
+
137
+ if [[ $JSON_MODE -eq 1 ]]; then
138
+ python3 - <<PY
139
+ import json, sys
140
+ print(json.dumps({
141
+ "managed_drift": $cm_count,
142
+ "untracked_source": $untracked_count,
143
+ "orphan_deployed": $orphan_count,
144
+ "stale_source": $stale_count,
145
+ "total": $total
146
+ }))
147
+ PY
148
+ if ((total > 0)); then exit 1; else exit 0; fi
15
149
  fi
16
150
 
17
151
  ui_header "Dotfiles Drift Dashboard"
18
152
 
19
- status=$(chezmoi status || true)
20
- if [[ -z "$status" ]]; then
21
- ui_ok "Clean" "no local drift detected"
22
- exit 0
153
+ # Class 1
154
+ if ((cm_count > 0)); then
155
+ echo ""
156
+ ui_warn "Managed drift" "$cm_count file(s) — deployed differs from rendered source"
157
+ printf '%s\n' "$cm_status"
158
+ else
159
+ ui_ok "Managed drift" "clean"
23
160
  fi
24
161
 
25
- echo ""
26
- ui_header "Changed files"
27
- printf '%s\n' "$status"
162
+ # Class 2
163
+ if ((untracked_count > 0)); then
164
+ echo ""
165
+ ui_warn "Untracked source" "$untracked_count file(s) in chezmoi source not tracked by git"
166
+ printf '%s\n' "$untracked"
167
+ else
168
+ ui_ok "Untracked source" "clean"
169
+ fi
170
+
171
+ # Class 3
172
+ if ((orphan_count > 0)); then
173
+ echo ""
174
+ ui_warn "Orphan deployed" "$orphan_count file(s) — review $(pretty_path "$orphan_file")"
175
+ else
176
+ ui_ok "Orphan deployed" "clean"
177
+ fi
178
+
179
+ # Class 4
180
+ if ((stale_count > 0)); then
181
+ echo ""
182
+ ui_warn "Stale source" "$stale_count target(s) newer than source — next \`chezmoi apply\` would revert"
183
+ printf '%s\n' "$stale_list"
184
+ else
185
+ ui_ok "Stale source" "clean"
186
+ fi
28
187
 
29
- count=$(printf '%s\n' "$status" | wc -l | tr -d ' ')
30
188
  echo ""
31
- ui_info "Total" "$count"
189
+ if ((total > 0)); then
190
+ ui_warn "Total drift signals" "$total"
191
+ else
192
+ ui_ok "Total" "no drift detected"
193
+ fi
32
194
 
33
- if [[ "${DOTFILES_DRIFT_SHOW_DIFF:-0}" = "1" ]]; then
195
+ if [[ "$SHOW_DIFF" = "1" && $cm_count -gt 0 ]]; then
34
196
  echo ""
35
- ui_header "Diff (excluding scripts/install/tests)"
197
+ ui_header "chezmoi diff (excluding scripts/install/tests)"
36
198
  chezmoi diff --exclude scripts --exclude install --exclude tests || true
37
199
  fi
200
+
201
+ ((total > 0)) && exit 1 || exit 0
@@ -420,16 +420,33 @@ check_config_directories() {
420
420
  check_sync_status() {
421
421
  check_section "Sync Status"
422
422
 
423
- # Chezmoi status
423
+ # Chezmoi status. The two-character prefix encodes both directions:
424
+ # col 1 = last-applied state vs actual (was the file edited in
425
+ # place since last apply?)
426
+ # col 2 = actual state vs target (what `chezmoi apply`
427
+ # would still change)
428
+ # Only col 2 matters for "is $HOME out of sync with the source" —
429
+ # col 1 alone just means the source repo has uncommitted edits,
430
+ # which is normal during development and not something the user
431
+ # needs warned about via the health dashboard.
424
432
  if has_command chezmoi; then
425
433
  local status_output
426
434
  status_output=$(chezmoi status 2>/dev/null || echo "")
427
435
  if [[ -z "$status_output" ]]; then
428
436
  check "Chezmoi sync" "pass"
429
437
  else
430
- local changes
431
- changes=$(printf '%s\n' "$status_output" | wc -l | tr -d ' ')
432
- check "Chezmoi sync" "warn" "$changes file(s) out of sync"
438
+ # Count only entries where column 2 is non-space (apply would do something).
439
+ local applicable
440
+ applicable=$(printf '%s\n' "$status_output" | awk 'substr($0,2,1)!=" "' | wc -l | tr -d ' ')
441
+ if [[ "$applicable" -eq 0 ]]; then
442
+ # All drift is source-only (unstaged edits in the source repo).
443
+ # That's not a sync issue; mention it but pass.
444
+ local source_only
445
+ source_only=$(printf '%s\n' "$status_output" | wc -l | tr -d ' ')
446
+ check "Chezmoi sync" "pass" "$source_only source-only edit(s) (run 'chezmoi diff' to inspect)"
447
+ else
448
+ check "Chezmoi sync" "warn" "$applicable file(s) out of sync"
449
+ fi
433
450
  fi
434
451
  else
435
452
  check "Chezmoi sync" "warn" "Not installed, skipped"