@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
@@ -29,6 +29,9 @@ PROFILE=false
29
29
  RUNS=3
30
30
  TARGET_MS="${DOTFILES_PERF_TARGET_MS:-250}"
31
31
  MAX_MS="${DOTFILES_PERF_MAX_MS:-1000}"
32
+ SHELL_FILTER=""
33
+ BY_TOOL=false
34
+ RESET_TIMINGS=false
32
35
 
33
36
  while [[ $# -gt 0 ]]; do
34
37
  case "$1" in
@@ -48,20 +51,180 @@ while [[ $# -gt 0 ]]; do
48
51
  TARGET_MS="${2:-$TARGET_MS}"
49
52
  shift 2
50
53
  ;;
54
+ --shell | -s)
55
+ SHELL_FILTER="${2:-}"
56
+ shift 2
57
+ ;;
58
+ --by-tool)
59
+ BY_TOOL=true
60
+ shift
61
+ ;;
62
+ --reset)
63
+ RESET_TIMINGS=true
64
+ shift
65
+ ;;
66
+ --baseline)
67
+ # Write the current per-shell means to the baseline file so
68
+ # subsequent runs can warn on regression. Closes part of #863.
69
+ WRITE_BASELINE=true
70
+ shift
71
+ ;;
72
+ --no-baseline-check)
73
+ # Skip the baseline-vs-current comparison entirely. Useful for
74
+ # the very first run on a machine, or for short-lived CI.
75
+ NO_BASELINE_CHECK=true
76
+ shift
77
+ ;;
51
78
  *)
52
79
  shift
53
80
  ;;
54
81
  esac
55
82
  done
56
83
 
57
- time_shell_startup() {
58
- local start end
84
+ WRITE_BASELINE="${WRITE_BASELINE:-false}"
85
+ NO_BASELINE_CHECK="${NO_BASELINE_CHECK:-false}"
86
+ BASELINE_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/dotfiles/perf-baseline.json"
87
+ BASELINE_REGRESSION_PCT="${DOTFILES_PERF_REGRESSION_PCT:-10}"
88
+
89
+ # --by-tool reader: aggregate $XDG_STATE_HOME/dotfiles/eval-timings.jsonl
90
+ # (populated by _cached_eval when EVALCACHE_TIMING=1) and report which
91
+ # tools dominate startup time. Independent of the runtime measurement
92
+ # loop above, so it returns immediately.
93
+ if $BY_TOOL || $RESET_TIMINGS; then
94
+ log_dir="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles"
95
+ log_file="$log_dir/eval-timings.jsonl"
96
+ if $RESET_TIMINGS; then
97
+ : >"$log_file" 2>/dev/null || rm -f "$log_file" 2>/dev/null || true
98
+ ui_ok "eval timings" "cleared $log_file"
99
+ $BY_TOOL || exit 0
100
+ fi
101
+ if [[ ! -s "$log_file" ]]; then
102
+ ui_warn "eval timings" "no data at $log_file"
103
+ echo " Hint: open a new shell with EVALCACHE_TIMING=1 to start collecting." >&2
104
+ exit 0
105
+ fi
106
+ ui_dot_banner "Diagnostics"
107
+ ui_header "Per-tool timing breakdown"
108
+ ui_section "$log_file"
109
+ python3 - "$log_file" <<'PY'
110
+ import json, sys
111
+ from collections import defaultdict
112
+
113
+ def percentile(sorted_vals, p):
114
+ """Linear-interpolation percentile, matching numpy.percentile defaults."""
115
+ if not sorted_vals:
116
+ return 0
117
+ if len(sorted_vals) == 1:
118
+ return sorted_vals[0]
119
+ k = (len(sorted_vals) - 1) * (p / 100.0)
120
+ f = int(k)
121
+ c = min(f + 1, len(sorted_vals) - 1)
122
+ if f == c:
123
+ return sorted_vals[f]
124
+ return sorted_vals[f] + (sorted_vals[c] - sorted_vals[f]) * (k - f)
125
+
126
+ samples = defaultdict(list) # label -> [ms, ms, ...]
127
+ shells_by_label = defaultdict(set) # label -> {shell, ...}
128
+
129
+ with open(sys.argv[1]) as f:
130
+ for line in f:
131
+ line = line.strip()
132
+ if not line:
133
+ continue
134
+ try:
135
+ ev = json.loads(line)
136
+ except Exception:
137
+ continue
138
+ label = ev.get("label", "?")
139
+ try:
140
+ ms = int(ev.get("ms", 0) or 0)
141
+ except (TypeError, ValueError):
142
+ continue
143
+ samples[label].append(ms)
144
+ shells_by_label[label].add(ev.get("shell", "?"))
145
+
146
+ rows = []
147
+ for label, vals in samples.items():
148
+ vals_sorted = sorted(vals)
149
+ rows.append({
150
+ "label": label,
151
+ "count": len(vals_sorted),
152
+ "total": sum(vals_sorted),
153
+ "mean": sum(vals_sorted) // max(len(vals_sorted), 1),
154
+ "min": vals_sorted[0],
155
+ "max": vals_sorted[-1],
156
+ "p50": int(percentile(vals_sorted, 50)),
157
+ "p95": int(percentile(vals_sorted, 95)),
158
+ "p99": int(percentile(vals_sorted, 99)),
159
+ "shells": ",".join(sorted(shells_by_label[label])),
160
+ })
161
+
162
+ rows.sort(key=lambda r: r["total"], reverse=True)
163
+ header = (f" {'label':<20} {'calls':>5} {'total':>8} "
164
+ f"{'mean':>7} {'p50':>5} {'p95':>5} {'p99':>5} shells")
165
+ print(header)
166
+ print(" " + "-" * (len(header) - 2))
167
+ for r in rows:
168
+ print(f" {r['label']:<20} {r['count']:>5} {r['total']:>6}ms "
169
+ f"{r['mean']:>5}ms {r['p50']:>3}ms {r['p95']:>3}ms {r['p99']:>3}ms {r['shells']}")
170
+ PY
171
+ exit 0
172
+ fi
173
+
174
+ # Per-shell defaults. nu and pwsh are genuinely slower than POSIX shells;
175
+ # bash should be quickest. Override via DOTFILES_PERF_TARGET_<SHELL>_MS.
176
+ shell_target_for() {
177
+ case "$1" in
178
+ zsh) echo "${DOTFILES_PERF_TARGET_ZSH_MS:-$TARGET_MS}" ;;
179
+ bash) echo "${DOTFILES_PERF_TARGET_BASH_MS:-60}" ;;
180
+ fish) echo "${DOTFILES_PERF_TARGET_FISH_MS:-200}" ;;
181
+ nu) echo "${DOTFILES_PERF_TARGET_NU_MS:-500}" ;;
182
+ pwsh) echo "${DOTFILES_PERF_TARGET_PWSH_MS:-600}" ;;
183
+ *) echo "$TARGET_MS" ;;
184
+ esac
185
+ }
186
+
187
+ # Invoke a near-empty session for the named shell, picking flags that
188
+ # load the user's interactive profile (matches what a fresh terminal does).
189
+ invoke_shell() {
190
+ case "$1" in
191
+ zsh) zsh -i -c exit </dev/null ;;
192
+ bash) bash -i -c exit </dev/null ;;
193
+ fish) fish -i -c exit </dev/null ;;
194
+ nu) nu -c exit </dev/null ;;
195
+ pwsh) pwsh -Command exit </dev/null ;;
196
+ *) return 1 ;;
197
+ esac
198
+ }
199
+
200
+ time_one_run() {
201
+ local shell_name="$1" start end
59
202
  start=$(python3 -c 'import time; print(int(time.time() * 1000))')
60
- zsh -i -c exit >/dev/null 2>&1
203
+ invoke_shell "$shell_name" >/dev/null 2>&1 || true
61
204
  end=$(python3 -c 'import time; print(int(time.time() * 1000))')
62
205
  echo $((end - start))
63
206
  }
64
207
 
208
+ # Existing single-shell helper retained for backward compatibility with
209
+ # callers/tests that reference time_shell_startup; routes to time_one_run.
210
+ time_shell_startup() { time_one_run zsh; }
211
+
212
+ # Measure a shell across $RUNS iterations, return "mean min max".
213
+ measure_shell() {
214
+ local shell_name="$1" sum=0 min=999999 max=0 t
215
+ local times=()
216
+ # One warm-up run discarded — caches a $_SHELL_CACHE on first invocation.
217
+ invoke_shell "$shell_name" >/dev/null 2>&1 || true
218
+ for _ in $(seq 1 "$RUNS"); do
219
+ t=$(time_one_run "$shell_name")
220
+ times+=("$t")
221
+ sum=$((sum + t))
222
+ [[ "$t" -lt "$min" ]] && min="$t"
223
+ [[ "$t" -gt "$max" ]] && max="$t"
224
+ done
225
+ echo "$((sum / RUNS)) $min $max"
226
+ }
227
+
65
228
  calc_score() {
66
229
  local mean_ms="$1"
67
230
  if [[ "$mean_ms" -le "$TARGET_MS" ]]; then
@@ -86,95 +249,159 @@ run_profile() {
86
249
  ' 2>/dev/null | head -20
87
250
  }
88
251
 
89
- if $JSON_OUTPUT; then
90
- times=()
91
- for _ in $(seq 1 "$RUNS"); do
92
- times+=("$(time_shell_startup)")
252
+ # Discover installed shells. SHELL_FILTER (--shell <name>) restricts to one.
253
+ all_shells=(zsh bash fish nu pwsh)
254
+ shells_to_measure=()
255
+ for s in "${all_shells[@]}"; do
256
+ command -v "$s" >/dev/null 2>&1 || continue
257
+ if [[ -n "$SHELL_FILTER" && "$s" != "$SHELL_FILTER" ]]; then
258
+ continue
259
+ fi
260
+ shells_to_measure+=("$s")
261
+ done
262
+ if ((${#shells_to_measure[@]} == 0)); then
263
+ ui_err "perf" "no measurable shells found${SHELL_FILTER:+ (filter: $SHELL_FILTER)}"
264
+ exit 1
265
+ fi
266
+
267
+ # Measure each shell once into parallel arrays.
268
+ declare -a shell_names shell_means shell_mins shell_maxs shell_targets shell_passes
269
+ for s in "${shells_to_measure[@]}"; do
270
+ read -r m mn mx <<<"$(measure_shell "$s")"
271
+ t=$(shell_target_for "$s")
272
+ shell_names+=("$s")
273
+ shell_means+=("$m")
274
+ shell_mins+=("$mn")
275
+ shell_maxs+=("$mx")
276
+ shell_targets+=("$t")
277
+ if [[ "$m" -le "$t" ]]; then
278
+ shell_passes+=("1")
279
+ else
280
+ shell_passes+=("0")
281
+ fi
282
+ done
283
+
284
+ # Primary "score" continues to use the zsh measurement (when available)
285
+ # so existing dashboards / JSON consumers keep their reference number.
286
+ mean=0
287
+ for i in "${!shell_names[@]}"; do
288
+ if [[ "${shell_names[$i]}" == "zsh" ]]; then
289
+ mean="${shell_means[$i]}"
290
+ break
291
+ fi
292
+ done
293
+ [[ "$mean" -eq 0 && "${#shell_means[@]}" -gt 0 ]] && mean="${shell_means[0]}"
294
+ score=$(calc_score "$mean")
295
+
296
+ # -----------------------------------------------------------------------------
297
+ # Baseline persistence + regression detection (closes part of #863).
298
+ # Stored at $XDG_CACHE_HOME/dotfiles/perf-baseline.json. The file is a
299
+ # JSON object mapping shell name → recorded mean (ms). `--baseline`
300
+ # overwrites it with the current measurement; default behavior reads
301
+ # it (if present) and warns when any shell regresses by more than
302
+ # DOTFILES_PERF_REGRESSION_PCT (default 10%).
303
+ # -----------------------------------------------------------------------------
304
+
305
+ declare -a baseline_warnings=()
306
+ if $WRITE_BASELINE; then
307
+ mkdir -p "$(dirname "$BASELINE_FILE")"
308
+ {
309
+ printf '{\n "recorded_at": "%s",\n "regression_pct": %d,\n "shells": {' \
310
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$BASELINE_REGRESSION_PCT"
311
+ for i in "${!shell_names[@]}"; do
312
+ [[ "$i" -gt 0 ]] && printf ','
313
+ printf '\n "%s": %d' "${shell_names[$i]}" "${shell_means[$i]}"
314
+ done
315
+ printf '\n }\n}\n'
316
+ } >"$BASELINE_FILE"
317
+ fi
318
+
319
+ if ! $NO_BASELINE_CHECK && [[ -s "$BASELINE_FILE" ]]; then
320
+ # Read the baseline and compare each measured shell.
321
+ for i in "${!shell_names[@]}"; do
322
+ name="${shell_names[$i]}"
323
+ m="${shell_means[$i]}"
324
+ baseline_ms=$(python3 -c '
325
+ import json, sys
326
+ try:
327
+ d = json.load(open(sys.argv[1]))
328
+ print(d.get("shells", {}).get(sys.argv[2], 0))
329
+ except Exception:
330
+ print(0)
331
+ ' "$BASELINE_FILE" "$name" 2>/dev/null)
332
+ [[ -z "$baseline_ms" || "$baseline_ms" -eq 0 ]] && continue
333
+ # Threshold: current > baseline * (1 + pct/100)
334
+ threshold=$((baseline_ms * (100 + BASELINE_REGRESSION_PCT) / 100))
335
+ if ((m > threshold)); then
336
+ delta_pct=$((((m - baseline_ms) * 100) / baseline_ms))
337
+ baseline_warnings+=("$name: $m ms vs baseline $baseline_ms ms (+${delta_pct}%, threshold +${BASELINE_REGRESSION_PCT}%)")
338
+ fi
93
339
  done
94
- sum=0
95
- min=999999
96
- max=0
97
- for t in "${times[@]}"; do
98
- sum=$((sum + t))
99
- if [[ "$t" -lt "$min" ]]; then min="$t"; fi
100
- if [[ "$t" -gt "$max" ]]; then max="$t"; fi
340
+ fi
341
+
342
+ if $JSON_OUTPUT; then
343
+ printf '{\n "runs": %d,\n "target_ms": %d,\n "max_ms_target": %d,\n "score": %d,\n "mean_ms": %d,\n "regression_count": %d,\n "shells": {' \
344
+ "$RUNS" "$TARGET_MS" "$MAX_MS" "$score" "$mean" "${#baseline_warnings[@]}"
345
+ for i in "${!shell_names[@]}"; do
346
+ [[ "$i" -gt 0 ]] && printf ','
347
+ printf '\n "%s": {"mean_ms": %d, "min_ms": %d, "max_ms": %d, "target_ms": %d, "pass": %s}' \
348
+ "${shell_names[$i]}" "${shell_means[$i]}" "${shell_mins[$i]}" "${shell_maxs[$i]}" \
349
+ "${shell_targets[$i]}" "$([[ ${shell_passes[$i]} == 1 ]] && echo true || echo false)"
101
350
  done
102
- mean=$((sum / RUNS))
103
- score=$(calc_score "$mean")
104
- cat <<JSON
105
- {
106
- "mean_ms": $mean,
107
- "min_ms": $min,
108
- "max_ms": $max,
109
- "runs": $RUNS,
110
- "target_ms": $TARGET_MS,
111
- "max_ms_target": $MAX_MS,
112
- "score": $score
113
- }
114
- JSON
351
+ printf '\n }'
352
+ if [[ "${#baseline_warnings[@]}" -gt 0 ]]; then
353
+ printf ',\n "regressions": ['
354
+ for i in "${!baseline_warnings[@]}"; do
355
+ [[ "$i" -gt 0 ]] && printf ','
356
+ # JSON-escape the warning text minimally (no embedded quotes expected).
357
+ printf '\n "%s"' "${baseline_warnings[$i]}"
358
+ done
359
+ printf '\n ]'
360
+ fi
361
+ printf '\n}\n'
115
362
  exit 0
116
363
  fi
117
364
 
118
365
  ui_dot_banner "Diagnostics"
119
366
  ui_header "Shell Performance"
120
367
 
121
- ui_section "Startup timing"
368
+ ui_section "Per-shell startup ($RUNS runs each, after one warm-up)"
122
369
 
123
- times=()
124
- for _ in $(seq 1 "$RUNS"); do
125
- times+=("$(time_shell_startup)")
126
- done
127
- sum=0
128
- min=999999
129
- max=0
130
- for t in "${times[@]}"; do
131
- sum=$((sum + t))
132
- if [[ "$t" -lt "$min" ]]; then min="$t"; fi
133
- if [[ "$t" -gt "$max" ]]; then max="$t"; fi
134
- if [[ "$UI_ENABLED" = "1" ]]; then
135
- ui_kv "Run" "${t}ms"
370
+ # Aligned table: name (6) | mean (8) | min/max (16) | target (12) | status
371
+ for i in "${!shell_names[@]}"; do
372
+ name="${shell_names[$i]}"
373
+ m="${shell_means[$i]}"
374
+ mn="${shell_mins[$i]}"
375
+ mx="${shell_maxs[$i]}"
376
+ t="${shell_targets[$i]}"
377
+ if [[ "${shell_passes[$i]}" == "1" ]]; then
378
+ status_marker="✓"
379
+ detail=""
136
380
  else
137
- echo " Run: ${t}ms"
381
+ status_marker=""
382
+ detail=" — over by $((m - t))ms"
138
383
  fi
384
+ printf ' %s %-6s mean %4dms (min %3dms, max %3dms) target %4dms%s\n' \
385
+ "$status_marker" "$name" "$m" "$mn" "$mx" "$t" "$detail"
139
386
  done
140
- mean=$((sum / RUNS))
141
- score=$(calc_score "$mean")
142
387
 
143
- if [[ "$UI_ENABLED" = "1" ]]; then
144
- ui_kv "Average" "${mean}ms"
145
- ui_kv "Min" "${min}ms"
146
- ui_kv "Max" "${max}ms"
147
- ui_kv "Target" "${TARGET_MS}ms"
148
- ui_kv "Score" "${score}/100"
149
- else
150
- echo " Average: ${mean}ms"
151
- echo " Min: ${min}ms"
152
- echo " Max: ${max}ms"
153
- echo " Target: ${TARGET_MS}ms"
154
- echo " Score: ${score}/100"
388
+ # Baseline comparison summary
389
+ if [[ "${#baseline_warnings[@]}" -gt 0 ]]; then
390
+ ui_section "Baseline regressions"
391
+ for w in "${baseline_warnings[@]}"; do
392
+ printf ' %s\n' "$w"
393
+ done
394
+ printf ' (threshold: >%s%% over the recorded baseline at %s)\n' \
395
+ "$BASELINE_REGRESSION_PCT" "$BASELINE_FILE"
396
+ elif [[ -s "$BASELINE_FILE" ]] && ! $NO_BASELINE_CHECK; then
397
+ ui_section "Baseline"
398
+ printf ' ✓ all measured shells within %s%% of baseline (%s)\n' \
399
+ "$BASELINE_REGRESSION_PCT" "$BASELINE_FILE"
400
+ elif $WRITE_BASELINE; then
401
+ ui_section "Baseline"
402
+ printf ' ✓ wrote baseline to %s\n' "$BASELINE_FILE"
155
403
  fi
156
404
 
157
- # Per-shell startup comparison
158
- ui_section "Per-shell startup"
159
-
160
- for shell_name in zsh bash fish; do
161
- if command -v "$shell_name" >/dev/null 2>&1; then
162
- local_start=$(python3 -c 'import time; print(int(time.time() * 1000))')
163
- if [[ "$shell_name" == "fish" ]]; then
164
- fish -c exit >/dev/null 2>&1 || true
165
- else
166
- "$shell_name" -i -c exit >/dev/null 2>&1 || true
167
- fi
168
- local_end=$(python3 -c 'import time; print(int(time.time() * 1000))')
169
- shell_time=$((local_end - local_start))
170
- if [[ "$UI_ENABLED" = "1" ]]; then
171
- ui_kv "$shell_name" "${shell_time}ms"
172
- else
173
- echo " $shell_name: ${shell_time}ms"
174
- fi
175
- fi
176
- done
177
-
178
405
  # Per-component breakdown (Zsh only, uses DOTFILES_DEBUG timing)
179
406
  ui_section "Component breakdown (estimated)"
180
407
  for component in "bare zsh" "paths+env" "aliases" "functions" "tools"; do
@@ -9,6 +9,9 @@ set -euo pipefail
9
9
 
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_ROOT="${REPO_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
12
+ # Export so child processes (the mcp-doctor subprocess below) inherit it
13
+ # without the env-prefix dance that confuses shellcheck (SC2097/SC2098).
14
+ export REPO_ROOT
12
15
 
13
16
  # shellcheck source=../dot/lib/ui.sh
14
17
  source "$SCRIPT_DIR/../dot/lib/ui.sh"
@@ -91,7 +94,9 @@ if [[ -z "$current_profile" ]] && [[ -f "$agent_profile_file" ]]; then
91
94
  current_profile="$(jq -r '.defaultProfile // "ask"' "$agent_profile_file")"
92
95
  fi
93
96
 
94
- mcp_summary="$(REPO_ROOT="$REPO_ROOT" MCP_CONFIG="$REPO_ROOT/dot_config/claude/mcp_servers.json" bash "$REPO_ROOT/scripts/diagnostics/mcp-doctor.sh" --strict --json)"
97
+ MCP_CONFIG="$REPO_ROOT/dot_config/claude/mcp_servers.json"
98
+ export MCP_CONFIG
99
+ mcp_summary="$(bash "$REPO_ROOT/scripts/diagnostics/mcp-doctor.sh" --strict --json)"
95
100
 
96
101
  attestation_json="$(
97
102
  jq -n \
@@ -76,11 +76,20 @@ _agent_checkpoint_file() {
76
76
  _agent_apply_profile_env() {
77
77
  local name="$1"
78
78
  export DOT_AGENT_PROFILE="$name"
79
- export DOT_AGENT_APPROVAL="$(_agent_profile_field "$name" "approval")"
80
- export DOT_AGENT_FILESYSTEM="$(_agent_profile_field "$name" "filesystem")"
81
- export DOT_AGENT_NETWORK="$(_agent_profile_field "$name" "network")"
82
- export DOT_AGENT_MCP_PROFILE="$(_agent_profile_field "$name" "mcpProfile")"
83
- export DOT_AGENT_MAX_STEPS="$(_agent_profile_field "$name" "maxSteps")"
79
+ # Split declare-and-assign to avoid masking the field-getter's exit
80
+ # code (SC2155). If the profile JSON is malformed, the assignment
81
+ # now fails the function instead of silently exporting an empty value.
82
+ local approval filesystem network mcp_profile max_steps
83
+ approval="$(_agent_profile_field "$name" "approval")"
84
+ filesystem="$(_agent_profile_field "$name" "filesystem")"
85
+ network="$(_agent_profile_field "$name" "network")"
86
+ mcp_profile="$(_agent_profile_field "$name" "mcpProfile")"
87
+ max_steps="$(_agent_profile_field "$name" "maxSteps")"
88
+ export DOT_AGENT_APPROVAL="$approval"
89
+ export DOT_AGENT_FILESYSTEM="$filesystem"
90
+ export DOT_AGENT_NETWORK="$network"
91
+ export DOT_AGENT_MCP_PROFILE="$mcp_profile"
92
+ export DOT_AGENT_MAX_STEPS="$max_steps"
84
93
  }
85
94
 
86
95
  _agent_rbac_enforcement() {
@@ -209,14 +218,15 @@ EOF
209
218
  checkpoint_id="$(basename "$checkpoint_file" .json)"
210
219
  ui_info "Agent mode" "$name"
211
220
  dot_agent_session_log "run_start" "$name" "running" "argv=$*" "checkpoint_id=$checkpoint_id"
212
- set +e
213
- "$@"
214
- local exit_code=$?
215
- set -e
216
- if [[ "$exit_code" -eq 0 ]]; then
217
- dot_agent_session_log "run_finish" "$name" "ok" "exit_code=$exit_code" "checkpoint_id=$checkpoint_id"
218
- else
221
+ # `if !` instead of `set +e / "$@" / set -e` — bash's errexit
222
+ # is automatically suspended inside a conditional, so we capture
223
+ # the exit code cleanly without toggling shell options.
224
+ local exit_code=0
225
+ if ! "$@"; then
226
+ exit_code=$?
219
227
  dot_agent_session_log "run_finish" "$name" "failed" "exit_code=$exit_code" "checkpoint_id=$checkpoint_id"
228
+ else
229
+ dot_agent_session_log "run_finish" "$name" "ok" "exit_code=$exit_code" "checkpoint_id=$checkpoint_id"
220
230
  fi
221
231
  return "$exit_code"
222
232
  ;;
@@ -326,14 +336,12 @@ EOF
326
336
  [[ "${#replay_argv[@]}" -gt 0 ]] || die "Checkpoint has no replayable command: $checkpoint_id"
327
337
  _agent_apply_profile_env "$replay_profile"
328
338
  dot_agent_session_log "checkpoint_replay" "$replay_profile" "running" "checkpoint_id=$checkpoint_id"
329
- set +e
330
- "${replay_argv[@]}"
331
- local exit_code=$?
332
- set -e
333
- if [[ "$exit_code" -eq 0 ]]; then
334
- dot_agent_session_log "checkpoint_replay_finish" "$replay_profile" "ok" "checkpoint_id=$checkpoint_id" "exit_code=$exit_code"
335
- else
339
+ local exit_code=0
340
+ if ! "${replay_argv[@]}"; then
341
+ exit_code=$?
336
342
  dot_agent_session_log "checkpoint_replay_finish" "$replay_profile" "failed" "checkpoint_id=$checkpoint_id" "exit_code=$exit_code"
343
+ else
344
+ dot_agent_session_log "checkpoint_replay_finish" "$replay_profile" "ok" "checkpoint_id=$checkpoint_id" "exit_code=$exit_code"
337
345
  fi
338
346
  return "$exit_code"
339
347
  ;;
@@ -373,16 +381,14 @@ EOF
373
381
  export DOT_AGENT_PARENT_PROFILE="$current_profile"
374
382
  dot_agent_session_log "delegate_start" "$delegate_profile" "running" "delegate=$delegate_name" "parent=$current_profile" "timeout=$delegate_timeout"
375
383
  ui_info "Delegating" "$delegate_name (profile: $delegate_profile, timeout: ${delegate_timeout}s)"
376
- set +e
377
- timeout "$delegate_timeout" "$@"
378
- local exit_code=$?
379
- set -e
380
- if [[ "$exit_code" -eq 0 ]]; then
381
- dot_agent_session_log "delegate_finish" "$delegate_profile" "ok" "delegate=$delegate_name" "exit_code=$exit_code"
382
- ui_ok "Delegate" "$delegate_name completed"
383
- else
384
+ local exit_code=0
385
+ if ! timeout "$delegate_timeout" "$@"; then
386
+ exit_code=$?
384
387
  dot_agent_session_log "delegate_finish" "$delegate_profile" "failed" "delegate=$delegate_name" "exit_code=$exit_code"
385
388
  ui_err "Delegate" "$delegate_name failed (exit $exit_code)"
389
+ else
390
+ dot_agent_session_log "delegate_finish" "$delegate_profile" "ok" "delegate=$delegate_name" "exit_code=$exit_code"
391
+ ui_ok "Delegate" "$delegate_name completed"
386
392
  fi
387
393
  return "$exit_code"
388
394
  ;;