@sebastienrousseau/dotfiles 0.2.500 → 0.2.501

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 (204) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +82 -44
  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 +4 -0
  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 +10 -0
  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 +4 -0
  66. package/docs/manual/concept-index.md +4 -0
  67. package/docs/operations/ATTESTATION.md +5 -0
  68. package/docs/operations/CI_CADENCE.md +107 -0
  69. package/docs/operations/CI_COMPOSITES.md +156 -0
  70. package/docs/operations/COMPLETIONS.md +123 -0
  71. package/docs/operations/COVERAGE.md +148 -0
  72. package/docs/operations/DRIFT.md +107 -0
  73. package/docs/operations/MAINTENANCE.md +5 -1
  74. package/docs/operations/MIGRATION.md +14 -6
  75. package/docs/operations/OPERATIONS.md +24 -0
  76. package/docs/operations/PERFORMANCE.md +133 -0
  77. package/docs/operations/RELIABILITY.md +6 -0
  78. package/docs/operations/ROADMAP.md +36 -18
  79. package/docs/operations/TESTING.md +4 -0
  80. package/docs/operations/TRACEABILITY.md +4 -0
  81. package/docs/operations/TRUSTED_AGENT_WORKSTATION.md +4 -0
  82. package/docs/operations/VERSION_SYNC.md +54 -5
  83. package/docs/reference/ALIASES.md +7 -0
  84. package/docs/reference/ALIASES_CHEATSHEET.md +5 -1
  85. package/docs/reference/ALIASES_DEPRECATIONS.md +5 -1
  86. package/docs/reference/FEATURES.md +4 -0
  87. package/docs/reference/FONTS.md +4 -0
  88. package/docs/reference/PROFILES.md +4 -0
  89. package/docs/reference/SCREENSHOTS.md +4 -0
  90. package/docs/reference/SCRIPTS.md +4 -0
  91. package/docs/reference/SUPPORT_MATRIX.md +10 -4
  92. package/docs/reference/THEMES.md +4 -0
  93. package/docs/reference/TOOLS.md +4 -0
  94. package/docs/reference/UTILS.md +16 -12
  95. package/docs/security/AI_ACT_COMPLIANCE.md +4 -0
  96. package/docs/security/AUDIT_BYPASS.md +103 -0
  97. package/docs/security/AUTOMATION_SECRETS.md +4 -0
  98. package/docs/security/CI_EGRESS_ALLOWLIST.md +127 -0
  99. package/docs/security/COMPLIANCE.md +5 -0
  100. package/docs/security/DEPS_DEV_EXCEPTIONS.md +86 -0
  101. package/docs/security/ENCRYPTION.md +5 -0
  102. package/docs/security/FMEA.md +4 -0
  103. package/docs/security/HISTORY_FILTERING.md +132 -0
  104. package/docs/security/INCIDENT_RESPONSE.md +4 -0
  105. package/docs/security/INSTALL_VERIFICATION.md +122 -0
  106. package/docs/security/KEYS.md +4 -0
  107. package/docs/security/KEY_ROTATION.md +4 -0
  108. package/docs/security/MCP_POLICY.md +9 -0
  109. package/docs/security/POLICY_RELEASES.md +4 -0
  110. package/docs/security/README.md +4 -0
  111. package/docs/security/SCORECARD.md +80 -0
  112. package/docs/security/SECRETS.md +12 -0
  113. package/docs/security/SECURITY.md +4 -0
  114. package/docs/security/SECURITY_CHECKLIST.md +11 -0
  115. package/docs/security/SHELL_EXEMPTIONS.md +145 -0
  116. package/docs/security/SOUP_REGISTER.md +4 -0
  117. package/docs/security/THREAT_MODEL.md +10 -0
  118. package/docs/security/VERIFICATION_VALIDATION.md +5 -1
  119. package/docs/themes/README.md +4 -0
  120. package/docs/themes/VISUAL_INTEGRITY_REPORT.md +4 -0
  121. package/dot_config/ai/identity.md +3 -0
  122. package/dot_config/ai/patterns/architect.md +2 -0
  123. package/dot_config/ai/patterns/hardener.md +2 -0
  124. package/dot_config/ai/patterns/refactor.md +2 -0
  125. package/dot_config/alacritty/alacritty.toml.tmpl +3 -3
  126. package/dot_config/atuin/config.toml.tmpl +47 -0
  127. package/dot_config/dotfiles/agent-card.json +1 -1
  128. package/dot_config/dotfiles/boot/README.md +2 -0
  129. package/dot_config/dotfiles/grub/README.md +2 -0
  130. package/dot_config/dotfiles/lock/README.md +2 -0
  131. package/dot_config/fish/conf.d/init.fish.tmpl +21 -0
  132. package/dot_config/fish/functions/_cached_eval.fish +84 -11
  133. package/dot_config/fish/functions/_cached_eval_clear.fish +17 -0
  134. package/dot_config/foot/foot.ini.tmpl +3 -3
  135. package/dot_config/fuzzel/fuzzel.ini.tmpl +2 -2
  136. package/dot_config/ghostty/config.tmpl +3 -3
  137. package/dot_config/git/hooks/executable_commit-msg +146 -0
  138. package/dot_config/goose/config.yaml +2 -2
  139. package/dot_config/gtk-3.0/gtk.css.tmpl +2 -2
  140. package/dot_config/gtk-3.0/settings.ini.tmpl +2 -2
  141. package/dot_config/gtk-4.0/gtk.css.tmpl +2 -2
  142. package/dot_config/gtk-4.0/settings.ini.tmpl +2 -2
  143. package/dot_config/kitty/kitty.conf.tmpl +3 -3
  144. package/dot_config/mise/config.toml +1 -1
  145. package/dot_config/niri/config.kdl.tmpl +2 -2
  146. package/dot_config/nushell/cached_eval.nu +80 -0
  147. package/dot_config/nushell/env.nu.tmpl +21 -13
  148. package/dot_config/shell/00-core-paths.sh.tmpl +1 -0
  149. package/dot_config/shell/05-core-safety.sh +1 -0
  150. package/dot_config/shell/10-secrets.sh +1 -0
  151. package/dot_config/shell/40-fzf-defaults.sh.tmpl +1 -0
  152. package/dot_config/shell/40-ls-colors.sh +1 -0
  153. package/dot_config/shell/50-logic-functions-core.sh.tmpl +1 -0
  154. package/dot_config/shell/50-logic-functions.sh.tmpl +1 -0
  155. package/dot_config/shell/51-logic-functions-extra.sh.tmpl +1 -0
  156. package/dot_config/shell/90-ux-aliases.sh.tmpl +1 -0
  157. package/dot_config/shell/91-ux-aliases-lazy.sh.tmpl +1 -0
  158. package/dot_config/shell/README.md +25 -8
  159. package/dot_config/starship.toml.tmpl +2 -2
  160. package/dot_config/tmux/tmux.conf.tmpl +3 -3
  161. package/dot_config/user-dirs.dirs +1 -0
  162. package/dot_config/vscode/settings.json.tmpl +2 -2
  163. package/dot_config/waybar/config.jsonc.tmpl +2 -2
  164. package/dot_config/waybar/style.css.tmpl +2 -2
  165. package/dot_config/wezterm/wezterm.lua.tmpl +3 -3
  166. package/dot_config/zsh/dot_zshrc.tmpl +154 -17
  167. package/dot_config/zsh/rc.d/00-alias-shims.zsh +28 -6
  168. package/dot_config/zsh/rc.d/30-options.zsh.tmpl +26 -6
  169. package/dot_local/bin/executable_bm +2 -0
  170. package/dot_local/bin/executable_dot +9 -2
  171. package/dot_local/bin/executable_dot-load-benchmark +1 -1
  172. package/dot_local/bin/executable_notify +2 -0
  173. package/dot_local/bin/executable_open +2 -0
  174. package/dot_local/bin/executable_tour +4 -2
  175. package/dot_local/bin/executable_up +3 -1
  176. package/install.sh +12 -4
  177. package/package.json +1 -1
  178. package/scripts/ci/check-dangerous-chmod.sh +19 -0
  179. package/scripts/ci/check-deps-dev.sh +236 -0
  180. package/scripts/ci/check-insecure-tls.sh +61 -0
  181. package/scripts/ci/check-regression-traceability.sh +67 -0
  182. package/scripts/ci/check-shell-preamble.sh +106 -0
  183. package/scripts/ci/run-coverage.sh +362 -0
  184. package/scripts/ci/validate-chezmoidata.sh +25 -0
  185. package/scripts/diagnostics/doctor.sh +173 -5
  186. package/scripts/diagnostics/drift-dashboard.sh +177 -13
  187. package/scripts/diagnostics/health.sh +21 -4
  188. package/scripts/diagnostics/perf.sh +304 -77
  189. package/scripts/diagnostics/workstation-attestation.sh +6 -1
  190. package/scripts/dot/commands/agent.sh +14 -5
  191. package/scripts/dot/commands/ai.sh +75 -10
  192. package/scripts/dot/lib/bento.sh +2 -1
  193. package/scripts/dot/lib/log.sh +6 -0
  194. package/scripts/dot/lib/platform.sh +1 -0
  195. package/scripts/dot/lib/ui.sh +11 -0
  196. package/scripts/dot/lib/utils.sh +5 -0
  197. package/scripts/git-hooks/pre-commit-audit.sh +1 -1
  198. package/scripts/git-hooks/pre-push +66 -3
  199. package/scripts/ops/heal-chezmoi.sh +41 -6
  200. package/scripts/qa/powershell-contract.ps1 +95 -0
  201. package/scripts/theme/merge-wallpaper.sh +4 -0
  202. package/scripts/theme/switch.sh +20 -10
  203. package/dot_config/atuin/config.toml +0 -40
  204. package/dot_local/bin/__pycache__/executable_dot-load-benchmark-ptycpython-312.pyc +0 -0
@@ -0,0 +1,362 @@
1
+ #!/usr/bin/env bash
2
+ # Copyright (c) 2015-2026 Dotfiles. All rights reserved.
3
+ # =============================================================================
4
+ # run-coverage.sh — Bash code-coverage runner using pure xtrace
5
+ # instrumentation. Emits lcov.info at $COVERAGE_OUT.
6
+ #
7
+ # Why pure xtrace (not kcov):
8
+ # kcov v43 on Ubuntu 24.04 + bash 5.2 won't emit bash-script
9
+ # coverage in either mode: without bash-dbgsym it captures nothing,
10
+ # and with bash-dbgsym it captures bash's *C-binary* internals
11
+ # (ctype.h, stdio.h) instead of the .sh file lines we care about.
12
+ # We bypass kcov entirely by using bash's own xtrace mechanism:
13
+ #
14
+ # PS4='+:${LINENO}:${BASH_SOURCE}:' # encode line + file in trace
15
+ # BASH_ENV=/setup-that-runs-set-x # turn on xtrace in every
16
+ # # non-interactive bash, so
17
+ # # children inherit tracing
18
+ # bash test.sh 2>traces/test.trace # capture lines per test
19
+ #
20
+ # Then we parse all traces with regex and write lcov.info. This is
21
+ # the same mechanism bashcov uses, minus the Ruby runtime.
22
+ #
23
+ # Linux + macOS both work (macOS bash 3.2 supports BASH_XTRACEFD as
24
+ # well — verified locally on bash 5.3 via Homebrew). The pre-commit
25
+ # hook in this repo can invoke this on any platform.
26
+ #
27
+ # Closes the runner half of #856 / Slice 1 of #883.
28
+ # =============================================================================
29
+
30
+ set -uo pipefail
31
+
32
+ REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
33
+ TESTS_DIR="${TESTS_DIR:-$REPO_ROOT/tests}"
34
+ COVERAGE_DIR="${COVERAGE_DIR:-$REPO_ROOT/coverage}"
35
+ COVERAGE_OUT="${COVERAGE_OUT:-$COVERAGE_DIR/lcov.info}"
36
+ MIN_COVERAGE_PCT="${MIN_COVERAGE_PCT:-0}" # initial floor; tighten per slice
37
+ COV_INCLUDE_DIRS="${COV_INCLUDE_DIRS:-$REPO_ROOT/scripts:$REPO_ROOT/dot_local/bin:$REPO_ROOT/.chezmoitemplates/functions}"
38
+ JOBS="${JOBS:-$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)}"
39
+ COV_TEST_TIMEOUT="${COV_TEST_TIMEOUT:-60}"
40
+
41
+ mkdir -p "$COVERAGE_DIR"
42
+ trace_dir="$COVERAGE_DIR/traces"
43
+ rm -rf "$trace_dir"
44
+ mkdir -p "$trace_dir"
45
+
46
+ # -----------------------------------------------------------------------------
47
+ # BASH_ENV setup file: enables xtrace in every non-interactive bash that
48
+ # inherits it. Child processes spawned via `bash $SCRIPT` get their own
49
+ # tracing turned on automatically.
50
+ # -----------------------------------------------------------------------------
51
+ bash_env="$COVERAGE_DIR/_cov_bashenv.sh"
52
+ cat >"$bash_env" <<'SETUP'
53
+ # coverage runtime — enable xtrace, define PS4 with file+line markers.
54
+ # `${BASH_SOURCE:-}` (not `${BASH_SOURCE}`) prevents PS4 evaluation
55
+ # from failing under `set -u`: at the top level of a `bash -c`
56
+ # script, BASH_SOURCE[0] is unbound; an unguarded expansion under
57
+ # `set -u` aborts the shell, taking out any test that sources a
58
+ # `set -euo pipefail` library file.
59
+ set -x
60
+ PS4='+@COV@:${LINENO}:${BASH_SOURCE:-}:@ '
61
+ SETUP
62
+
63
+ # Sanity-probe: run one trivial script through the pipeline so a
64
+ # subsequent failure of the real sweep can be diagnosed quickly.
65
+ probe_target="$COVERAGE_DIR/_probe_target.sh"
66
+ cat >"$probe_target" <<'PROBE'
67
+ #!/usr/bin/env bash
68
+ set -uo pipefail
69
+ echo "probe-line-a"
70
+ x=1; y=2
71
+ echo "probe-sum=$((x + y))"
72
+ PROBE
73
+ chmod +x "$probe_target"
74
+
75
+ PS4='+@COV@:${LINENO}:${BASH_SOURCE}:@ ' \
76
+ BASH_ENV="$bash_env" \
77
+ bash "$probe_target" 2>"$trace_dir/_probe.trace" >/dev/null || true
78
+ probe_lines=$(grep -cE "^\+@COV@:[0-9]+:.*${probe_target}:@" "$trace_dir/_probe.trace" 2>/dev/null || echo 0)
79
+ echo "probe: ${probe_lines} traced lines from ${probe_target}" >&2
80
+ if [[ "$probe_lines" -eq 0 ]]; then
81
+ echo "::error::xtrace probe captured no lines — coverage mechanism broken" >&2
82
+ exit 2
83
+ fi
84
+
85
+ # -----------------------------------------------------------------------------
86
+ # Collect test files (mirror tests/framework/test_runner.sh discovery).
87
+ # -----------------------------------------------------------------------------
88
+ mapfile -t test_files < <(find "$TESTS_DIR/unit" -name 'test_*.sh' -type f | sort)
89
+
90
+ if [[ "${#test_files[@]}" -eq 0 ]]; then
91
+ echo "::error::no unit test files discovered under $TESTS_DIR/unit" >&2
92
+ exit 1
93
+ fi
94
+
95
+ echo "Tracing ${#test_files[@]} test files (parallel × $JOBS, timeout ${COV_TEST_TIMEOUT}s/test)..." >&2
96
+
97
+ # -----------------------------------------------------------------------------
98
+ # Worker function — runs one test under xtrace, captures stderr.
99
+ # -----------------------------------------------------------------------------
100
+ # shellcheck disable=SC2329 # called indirectly via xargs subshell
101
+ run_one() {
102
+ local f="$1"
103
+ local trace
104
+ trace="$COV_TRACE_DIR/$(basename "$f" .sh).trace"
105
+ PS4='+@COV@:${LINENO}:${BASH_SOURCE}:@ ' \
106
+ BASH_ENV="$COV_BASH_ENV" \
107
+ timeout --kill-after=5 "$COV_TEST_TIMEOUT" \
108
+ bash "$f" 2>"$trace" >/dev/null || true
109
+ }
110
+
111
+ export COV_TRACE_DIR="$trace_dir"
112
+ export COV_BASH_ENV="$bash_env"
113
+ export COV_TEST_TIMEOUT
114
+ export -f run_one
115
+
116
+ start_ts=$(date +%s)
117
+ printf '%s\n' "${test_files[@]}" |
118
+ xargs -I{} -n1 -P"$JOBS" bash -c 'run_one "$@"' _ {} ||
119
+ true
120
+ elapsed=$(($(date +%s) - start_ts))
121
+ echo "trace phase done in ${elapsed}s" >&2
122
+
123
+ # -----------------------------------------------------------------------------
124
+ # Aggregate trace files → lcov.info.
125
+ # -----------------------------------------------------------------------------
126
+ python3 - "$COVERAGE_OUT" "$trace_dir" "$REPO_ROOT" "$COV_INCLUDE_DIRS" <<'PY'
127
+ """Aggregate bash xtrace output into lcov.info."""
128
+ import os
129
+ import re
130
+ import sys
131
+ from collections import defaultdict
132
+ from pathlib import Path
133
+
134
+ out_path, trace_dir, repo_root, include_dirs_spec = sys.argv[1:5]
135
+ include_dirs = [Path(p).resolve() for p in include_dirs_spec.split(":") if p]
136
+ trace_dir = Path(trace_dir)
137
+ repo_root = Path(repo_root).resolve()
138
+
139
+ # Pattern: +@COV@:<lineno>:<source>:@
140
+ hit_re = re.compile(r"^\+@COV@:(\d+):([^:]+):@")
141
+
142
+ # files[abs_path][line] = total hits
143
+ files = defaultdict(lambda: defaultdict(int))
144
+
145
+ def in_includes(path: Path) -> bool:
146
+ try:
147
+ rp = path.resolve()
148
+ except (OSError, RuntimeError):
149
+ return False
150
+ for inc in include_dirs:
151
+ try:
152
+ rp.relative_to(inc)
153
+ return True
154
+ except ValueError:
155
+ continue
156
+ return False
157
+
158
+ # Parse every trace file. Each trace can be MBs; iterate line by line.
159
+ for trace_path in sorted(trace_dir.glob("*.trace")):
160
+ try:
161
+ with open(trace_path, "r", errors="replace") as f:
162
+ for line in f:
163
+ m = hit_re.match(line)
164
+ if not m:
165
+ continue
166
+ lineno = int(m.group(1))
167
+ src = m.group(2).strip()
168
+ if not src or src == "main":
169
+ continue
170
+ src_path = Path(src)
171
+ if not src_path.is_absolute():
172
+ src_path = (repo_root / src_path)
173
+ # Always resolve so `..`-style paths from inside
174
+ # `commands/foo.sh` sourcing `../lib/log.sh` collapse
175
+ # to the same canonical SF: key as a direct hit on
176
+ # `lib/log.sh`. Without this we get three SF: blocks
177
+ # for one file and the denominator inflates.
178
+ try:
179
+ src_path = src_path.resolve()
180
+ except (OSError, RuntimeError):
181
+ pass
182
+ if not in_includes(src_path):
183
+ continue
184
+ files[str(src_path)][lineno] += 1
185
+ except OSError as e:
186
+ print(f"warn: read error {trace_path}: {e}", file=sys.stderr)
187
+
188
+ # Sweep through include-dirs and add executable lines for files we never
189
+ # touched, so lcov coverage % reflects total source surface, not just
190
+ # touched files.
191
+ ws_re = re.compile(r"^\s*$")
192
+ comment_re = re.compile(r"^\s*#")
193
+ shebang_re = re.compile(r"^\s*#!")
194
+
195
+ def executable_lines(path: Path):
196
+ """Lines that bash xtrace can plausibly emit.
197
+
198
+ Excludes:
199
+ - blanks / shebangs / comments (already)
200
+ - structural-only keywords (`fi`, `done`, `else`, `esac`, `then`,
201
+ `do`, `in`, bare `{`/`}` braces) — these are syntax, not
202
+ commands; xtrace never traces them
203
+ - heredoc body lines (between `<<EOF` and `EOF`) — content, not
204
+ executed
205
+ - `case` pattern labels (`foo)`) — the matched body executes,
206
+ not the pattern itself
207
+
208
+ This matches what `bash -x` actually produces a `+`-prefixed
209
+ line for, so the denominator reflects measurable lines.
210
+
211
+ Honor LCOV_EXCL_LINE / LCOV_EXCL_START / LCOV_EXCL_STOP markers
212
+ so authors can exempt genuinely unreachable lines (platform-
213
+ gated branches, root-only paths, dead-code stubs).
214
+ """
215
+ import re as _re
216
+ structural = _re.compile(
217
+ r"^\s*("
218
+ r"fi|done|else|elif|esac|then|do|in|"
219
+ r"\}|\{|"
220
+ r"\)\s*;?;?\s*$|" # bare `)` (case pattern close)
221
+ r";;\s*$" # `;;` case-clause terminator
222
+ r")\s*(#.*)?$"
223
+ )
224
+ case_pattern = _re.compile(r"^\s*[a-zA-Z0-9_\*\?\[\|/\-\.+]+\)\s*(#.*)?$")
225
+ heredoc_open = _re.compile(r"<<-?\s*[\"\']?([A-Za-z_][A-Za-z0-9_]*)[\"\']?")
226
+ excl_line = _re.compile(r"#\s*LCOV_EXCL_LINE")
227
+ excl_start = _re.compile(r"#\s*LCOV_EXCL_START")
228
+ excl_stop = _re.compile(r"#\s*LCOV_EXCL_STOP")
229
+ # Scripts that explicitly turn off xtrace can't be measured by
230
+ # this mechanism — the bash runtime simply stops emitting trace
231
+ # records. Treat everything after `set +x` / `set +o xtrace`
232
+ # as excluded so it doesn't sink the denominator.
233
+ xtrace_off = _re.compile(r"^\s*set\s+(\+x|\+o\s+xtrace)\b")
234
+
235
+ try:
236
+ with open(path, "r", errors="replace") as f:
237
+ text = f.read().splitlines()
238
+ except OSError:
239
+ return []
240
+
241
+ out = []
242
+ in_heredoc = None # the terminator we're waiting for
243
+ excluding = False
244
+ xtrace_disabled = False
245
+ for i, line in enumerate(text, 1):
246
+ # LCOV_EXCL handling
247
+ if excl_start.search(line):
248
+ excluding = True
249
+ continue
250
+ if excl_stop.search(line):
251
+ excluding = False
252
+ continue
253
+ if excluding:
254
+ continue
255
+ if excl_line.search(line):
256
+ continue
257
+
258
+ # Once a script disables xtrace, no further lines can be
259
+ # measured by this mechanism — drop them from the denominator.
260
+ if xtrace_disabled:
261
+ continue
262
+ if xtrace_off.match(line):
263
+ xtrace_disabled = True
264
+ continue
265
+
266
+ # Heredoc body — track and skip
267
+ if in_heredoc is not None:
268
+ if line.strip() == in_heredoc:
269
+ in_heredoc = None
270
+ continue
271
+ hd = heredoc_open.search(line)
272
+ if hd and not comment_re.match(line):
273
+ in_heredoc = hd.group(1)
274
+
275
+ if not line or ws_re.match(line):
276
+ continue
277
+ if shebang_re.match(line):
278
+ continue
279
+ if comment_re.match(line):
280
+ continue
281
+ if structural.match(line):
282
+ continue
283
+ if case_pattern.match(line):
284
+ continue
285
+ out.append(i)
286
+ return out
287
+
288
+ for inc in include_dirs:
289
+ if not inc.exists():
290
+ continue
291
+ for path in inc.rglob("*.sh"):
292
+ ap = str(path.resolve())
293
+ existing = files[ap] # creates the entry on touch
294
+ for ln in executable_lines(path):
295
+ if ln not in existing:
296
+ existing[ln] = 0
297
+ for path in inc.rglob("*"):
298
+ # also include shebanged shell scripts without .sh
299
+ if path.is_file() and not path.suffix and path.stat().st_size > 0:
300
+ try:
301
+ with open(path, "r", errors="replace") as f:
302
+ first = f.readline()
303
+ except OSError:
304
+ continue
305
+ if first.startswith("#!") and ("bash" in first or "sh" in first):
306
+ ap = str(path.resolve())
307
+ existing = files[ap]
308
+ for ln in executable_lines(path):
309
+ if ln not in existing:
310
+ existing[ln] = 0
311
+
312
+ # Emit lcov.info
313
+ with open(out_path, "w") as out:
314
+ for filename in sorted(files):
315
+ out.write(f"SF:{filename}\n")
316
+ for ln in sorted(files[filename]):
317
+ out.write(f"DA:{ln},{files[filename][ln]}\n")
318
+ out.write("end_of_record\n")
319
+
320
+ # Summary
321
+ total_files = len(files)
322
+ total_lines = sum(len(lines) for lines in files.values())
323
+ covered = sum(1 for lines in files.values() for h in lines.values() if h > 0)
324
+ pct = (covered * 100.0 / total_lines) if total_lines else 0.0
325
+ print(f"Aggregated: {total_files} files, {covered}/{total_lines} lines = {pct:.2f}%",
326
+ file=sys.stderr)
327
+ PY
328
+
329
+ if [[ ! -s "$COVERAGE_OUT" ]]; then
330
+ echo "::error::failed to produce lcov.info — aggregator wrote empty file." >&2
331
+ exit 1
332
+ fi
333
+
334
+ # -----------------------------------------------------------------------------
335
+ # Threshold check.
336
+ # -----------------------------------------------------------------------------
337
+ summary=$(
338
+ python3 - "$COVERAGE_OUT" <<'PY'
339
+ import sys
340
+ total = covered = 0
341
+ with open(sys.argv[1]) as f:
342
+ for line in f:
343
+ if line.startswith("DA:"):
344
+ _, rest = line.split(":", 1)
345
+ _, hits = rest.strip().split(",")
346
+ total += 1
347
+ if int(hits) > 0:
348
+ covered += 1
349
+ pct = (covered * 100.0 / total) if total else 0.0
350
+ print(f"{covered} {total} {pct:.2f}")
351
+ PY
352
+ )
353
+ read -r covered total pct <<<"$summary"
354
+ echo "Coverage: ${covered}/${total} lines = ${pct}% (output: $COVERAGE_OUT)"
355
+
356
+ below=$(awk -v p="$pct" -v t="$MIN_COVERAGE_PCT" 'BEGIN{print (p+0 < t+0) ? "1" : "0"}')
357
+ if [[ "$below" == "1" ]]; then
358
+ echo "::error::coverage ${pct}% is below the floor ${MIN_COVERAGE_PCT}%" >&2
359
+ exit 1
360
+ fi
361
+
362
+ exit 0
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env bash
2
+ # Copyright (c) 2015-2026 Dotfiles. All rights reserved.
3
+ # Validate .chezmoidata.toml against config/chezmoidata.schema.json via taplo.
4
+ #
5
+ # Usage:
6
+ # bash scripts/ci/validate-chezmoidata.sh
7
+ #
8
+ # In CI the workflow installs taplo before invoking this script. Locally,
9
+ # install via: cargo install --locked taplo-cli (or `brew install taplo`).
10
+
11
+ set -euo pipefail
12
+
13
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
14
+ cd "$REPO_ROOT"
15
+
16
+ if ! command -v taplo >/dev/null 2>&1; then
17
+ echo "error: taplo not found in PATH" >&2
18
+ echo " install: cargo install --locked taplo-cli" >&2
19
+ echo " or: brew install taplo" >&2
20
+ exit 127
21
+ fi
22
+
23
+ # `taplo check` reads .taplo.toml at the repo root, applies the schema
24
+ # rule, and reports schema violations + TOML syntax errors.
25
+ taplo check
@@ -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,98 @@ 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
+ unwrapped=""
552
+ for tool in nvm fnm pyenv rbenv jenv asdf sdkman conda kubectl helm gh cargo pnpm yarn thefuck broot mcfly; do
553
+ command -v "$tool" >/dev/null 2>&1 || continue
554
+ found=0
555
+ for shell_dir in zsh bash fish; do
556
+ case "$shell_dir" in
557
+ fish) [[ -f "$cache_base/$shell_dir/${tool}-init.fish" ]] && found=1 ;;
558
+ *) [[ -f "$cache_base/$shell_dir/${tool}-init.$shell_dir" ]] && found=1 ;;
559
+ esac
560
+ ((found == 1)) && break
561
+ done
562
+ ((found == 0)) && unwrapped="${unwrapped:+$unwrapped, }$tool"
563
+ done
564
+ if [[ -z "$unwrapped" ]]; then
565
+ _ok "uncached slow-init tools" "none detected"
566
+ else
567
+ _warn "uncached slow-init tools" "$unwrapped — consider wrapping in _cached_eval"
568
+ fi
569
+
570
+ # 3. Zsh completion dump health. compinit is usually the single biggest
571
+ # cost on a zsh startup; a stale or uncompiled .zcompdump compounds it.
572
+ if command -v zsh >/dev/null 2>&1; then
573
+ zcompdump="${HOME}/.zcompdump"
574
+ if [[ -f "$zcompdump" ]]; then
575
+ dump_mtime=$(stat -c %Y "$zcompdump" 2>/dev/null || stat -f %m "$zcompdump" 2>/dev/null || echo 0)
576
+ age_days=$((($(date +%s) - dump_mtime) / 86400))
577
+ if ((age_days > 7)); then
578
+ _warn ".zcompdump" "${age_days}d old — refresh: rm ~/.zcompdump* && zsh -ic exit"
579
+ else
580
+ _ok ".zcompdump" "fresh (${age_days}d)"
581
+ fi
582
+ if [[ ! -f "${zcompdump}.zwc" ]]; then
583
+ _warn ".zcompdump.zwc" "missing — completion init slower than necessary"
584
+ fi
585
+ fi
586
+ fi
587
+
588
+ # 4. PATH length. Each entry is searched on every command resolution;
589
+ # >40 entries is noticeably slow on cold-cache filesystems.
590
+ path_count=$(printf '%s' "${PATH:-}" | tr ':' '\n' | grep -c . || true)
591
+ if [[ "$path_count" -le 40 ]]; then
592
+ _ok "PATH length" "$path_count entries"
593
+ elif [[ "$path_count" -le 80 ]]; then
594
+ _warn "PATH length" "$path_count entries — consider pruning"
595
+ else
596
+ _fail "PATH length" "$path_count entries — likely slowing every command"
597
+ fi
598
+
599
+ # 5. Shell coverage. Surface installed shells that the project's caching
600
+ # infrastructure (zsh/bash/fish) doesn't currently maintain caches for.
601
+ shells_unmanaged=""
602
+ for sh in nu pwsh; do
603
+ command -v "$sh" >/dev/null 2>&1 && shells_unmanaged="${shells_unmanaged:+$shells_unmanaged, }$sh"
604
+ done
605
+ if [[ -z "$shells_unmanaged" ]]; then
606
+ _ok "shell coverage" "all installed shells (zsh/bash/fish) are managed"
607
+ else
608
+ _warn "shell coverage" "$shells_unmanaged installed — startup not measured by dot perf"
609
+ fi
610
+
611
+ # 6. Zsh hook count. Heavy precmd/preexec functions compound per-prompt.
612
+ # Probe an interactive zsh with a hard timeout so a broken zshrc doesn't
613
+ # stall doctor; skip cleanly if the probe fails.
614
+ if command -v zsh >/dev/null 2>&1 && command -v timeout >/dev/null 2>&1; then
615
+ hook_counts=$(timeout 5 zsh -i -c 'echo "$#precmd_functions $#preexec_functions"' 2>/dev/null || echo "")
616
+ if [[ -n "$hook_counts" ]]; then
617
+ read -r precmd_n preexec_n <<<"$hook_counts"
618
+ if [[ "${precmd_n:-0}" -le 5 && "${preexec_n:-0}" -le 5 ]]; then
619
+ _ok "zsh hooks" "precmd=$precmd_n preexec=$preexec_n"
620
+ else
621
+ _warn "zsh hooks" "precmd=$precmd_n preexec=$preexec_n — heavy per-prompt work"
622
+ fi
623
+ fi
624
+ fi
625
+
505
626
  if command -v hyperfine >/dev/null 2>&1; then
506
627
  if bash "$SCRIPT_DIR/../../tests/performance/bench.sh" 2>/dev/null; then
507
628
  _ok "startup latency" "within target thresholds"
@@ -512,6 +633,53 @@ else
512
633
  _warn "hyperfine" "missing (benchmark skipped)"
513
634
  fi
514
635
 
636
+ # 7. Baseline check + top-3 slowest tools from EVALCACHE_TIMING.
637
+ # Closes part of #863. Reads the same baseline file `dot perf` writes,
638
+ # and the same eval-timings.jsonl _cached_eval populates. Skipped
639
+ # silently when either file is absent (first-run state).
640
+ baseline_file="$cache_base/dotfiles/perf-baseline.json"
641
+ if [[ -s "$baseline_file" ]] && command -v python3 >/dev/null 2>&1; then
642
+ baseline_age_days=$(python3 -c '
643
+ import json, sys, datetime
644
+ try:
645
+ d = json.load(open(sys.argv[1]))
646
+ rec = d.get("recorded_at", "")
647
+ if not rec: print(-1); sys.exit(0)
648
+ rec = rec.replace("Z", "+00:00")
649
+ age = (datetime.datetime.now(datetime.timezone.utc) - datetime.datetime.fromisoformat(rec)).days
650
+ print(age)
651
+ except Exception:
652
+ print(-1)
653
+ ' "$baseline_file" 2>/dev/null)
654
+ if [[ "$baseline_age_days" -ge 0 ]]; then
655
+ _ok "perf baseline" "recorded ${baseline_age_days}d ago — run \`dot perf\` to compare"
656
+ fi
657
+ fi
658
+
659
+ timings_file="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/eval-timings.jsonl"
660
+ if [[ -s "$timings_file" ]] && command -v python3 >/dev/null 2>&1; then
661
+ top_tools=$(python3 -c '
662
+ import json, sys
663
+ from collections import defaultdict
664
+ samples = defaultdict(list)
665
+ try:
666
+ for line in open(sys.argv[1]):
667
+ try:
668
+ ev = json.loads(line)
669
+ ms = int(ev.get("ms", 0) or 0)
670
+ samples[ev.get("label", "?")].append(ms)
671
+ except Exception:
672
+ pass
673
+ rows = sorted(samples.items(), key=lambda kv: sum(kv[1]) // max(len(kv[1]), 1), reverse=True)[:3]
674
+ print(", ".join(f"{lbl}({sum(v)//max(len(v),1)}ms)" for lbl, v in rows))
675
+ except Exception:
676
+ pass
677
+ ' "$timings_file" 2>/dev/null)
678
+ if [[ -n "$top_tools" ]]; then
679
+ _ok "perf top-tools" "$top_tools"
680
+ fi
681
+ fi
682
+
515
683
  # --- Summary ---
516
684
  dot_log info "doctor_complete" "errors=$Errors" "warnings=$Warnings"
517
685
  dot_metric "doctor_errors" "$Errors" "count"