@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
@@ -1,8 +1,26 @@
1
1
  #!/usr/bin/env bash
2
+ # Copyright (c) 2015-2026 Dotfiles. All rights reserved.
3
+ # =============================================================================
4
+ # pre-push hook
5
+ #
6
+ # 1. Refuses to push any commit that is not GPG/SSH-signed.
7
+ # 2. Runs `scripts/qa/reliability-audit.sh --quick`. The audit is mandatory
8
+ # by default; explicit per-push bypass requires `DOTFILES_ALLOW_UNSKIPPED_PUSH=1`,
9
+ # and every bypass is logged to `~/.local/share/dotfiles/audit-bypass.log`.
10
+ #
11
+ # The legacy `DOTFILES_SKIP_PRE_PUSH_AUDIT=1` env var (opt-out) is rejected
12
+ # with a migration message. The new variable is intentionally awkwardly
13
+ # named so that setting it requires deliberate intent. See
14
+ # `docs/security/AUDIT_BYPASS.md`.
15
+ # =============================================================================
16
+
2
17
  set -euo pipefail
3
18
 
4
19
  repo_root="$(git rev-parse --show-toplevel)"
5
20
 
21
+ # -----------------------------------------------------------------------------
22
+ # Step 1 — signed-commit enforcement (unchanged)
23
+ # -----------------------------------------------------------------------------
6
24
  while read -r local_ref local_sha remote_ref remote_sha; do
7
25
  [ "$local_sha" = "0000000000000000000000000000000000000000" ] && continue
8
26
 
@@ -18,12 +36,57 @@ while read -r local_ref local_sha remote_ref remote_sha; do
18
36
 
19
37
  for c in $(git rev-list "$range"); do
20
38
  if ! git verify-commit "$c" >/dev/null 2>&1; then
21
- echo "Blocked: unsigned commit $c"
39
+ echo "Blocked: unsigned commit $c" >&2
22
40
  exit 1
23
41
  fi
24
42
  done
25
43
  done
26
44
 
27
- if [ "${DOTFILES_SKIP_PRE_PUSH_AUDIT:-0}" != "1" ]; then
28
- bash "$repo_root/scripts/qa/reliability-audit.sh" --quick
45
+ # -----------------------------------------------------------------------------
46
+ # Step 2 — reject the legacy opt-out variable
47
+ # -----------------------------------------------------------------------------
48
+ if [ "${DOTFILES_SKIP_PRE_PUSH_AUDIT:-}" = "1" ]; then
49
+ cat >&2 <<'EOF'
50
+ ERROR: DOTFILES_SKIP_PRE_PUSH_AUDIT is no longer honored.
51
+
52
+ The pre-push audit was previously opt-out — setting one env var was enough
53
+ to skip it. That made bypass invisible and routine. Effective with #871,
54
+ the policy is fail-closed: the audit always runs unless you explicitly
55
+ authorize a bypass for this single push.
56
+
57
+ To bypass for one push:
58
+ DOTFILES_ALLOW_UNSKIPPED_PUSH=1 git push
59
+
60
+ Every bypass is recorded to ~/.local/share/dotfiles/audit-bypass.log
61
+ and surfaced by `dot doctor` if any are recent.
62
+
63
+ See: docs/security/AUDIT_BYPASS.md
64
+ EOF
65
+ exit 1
29
66
  fi
67
+
68
+ # -----------------------------------------------------------------------------
69
+ # Step 3 — run the audit (or log + skip if explicitly bypassed)
70
+ # -----------------------------------------------------------------------------
71
+ if [ "${DOTFILES_ALLOW_UNSKIPPED_PUSH:-}" = "1" ]; then
72
+ bypass_log_dir="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles"
73
+ # The audit-bypass log lives under XDG_STATE_HOME (user state — never
74
+ # checked in, never deployed by chezmoi to other locations).
75
+ mkdir -p "$bypass_log_dir"
76
+ bypass_log="$bypass_log_dir/audit-bypass.log"
77
+
78
+ ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
79
+ branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
80
+ remote="$(git remote get-url --push @{upstream} 2>/dev/null ||
81
+ git config --get remote.origin.url 2>/dev/null ||
82
+ echo unknown)"
83
+ reason="${DOTFILES_BYPASS_REASON:-no reason given}"
84
+
85
+ printf '%s\tbranch=%s\tremote=%s\treason=%s\n' \
86
+ "$ts" "$branch" "$remote" "$reason" >>"$bypass_log"
87
+
88
+ echo "WARN: pre-push audit bypassed (logged to $bypass_log)" >&2
89
+ exit 0
90
+ fi
91
+
92
+ bash "$repo_root/scripts/qa/reliability-audit.sh" --quick
@@ -126,15 +126,41 @@ dot_secrets_set() {
126
126
  }
127
127
 
128
128
  dot_secrets_get() {
129
- local key="${1:-}" provider
130
- [[ -n "$key" ]] || return 1
129
+ local key="${1:-}" provider value rc=0
130
+ [[ -n "$key" ]] || {
131
+ printf 'dot_secrets_get: empty key\n' >&2
132
+ return 1
133
+ }
131
134
  provider="$(dot_secrets_provider)"
132
135
  case "$provider" in
133
- macos-keychain) dot_secrets_get_macos "$key" ;;
134
- pass) dot_secrets_get_pass "$key" ;;
135
- plain-enc) dot_secrets_get_plain_enc "$key" ;;
136
- *) return 1 ;;
136
+ macos-keychain) value="$(dot_secrets_get_macos "$key")" || rc=$? ;;
137
+ pass) value="$(dot_secrets_get_pass "$key")" || rc=$? ;;
138
+ plain-enc) value="$(dot_secrets_get_plain_enc "$key")" || rc=$? ;;
139
+ none)
140
+ # No usable provider on this host — silent miss is wrong;
141
+ # callers need to know whether to fall back.
142
+ printf 'dot_secrets_get: no provider configured for key=%s\n' "$key" >&2
143
+ return 2
144
+ ;;
145
+ *)
146
+ printf 'dot_secrets_get: unknown provider %q\n' "$provider" >&2
147
+ return 1
148
+ ;;
137
149
  esac
150
+ if [[ "$rc" -ne 0 ]]; then
151
+ printf 'dot_secrets_get: provider %q failed (rc=%d) for key=%s\n' \
152
+ "$provider" "$rc" "$key" >&2
153
+ return "$rc"
154
+ fi
155
+ if [[ -z "$value" ]]; then
156
+ # An empty return from a successful provider call usually means
157
+ # "key not found" — distinguish from "provider crashed" so the
158
+ # caller's fallback can be different.
159
+ printf 'dot_secrets_get: provider %q returned empty for key=%s (not found?)\n' \
160
+ "$provider" "$key" >&2
161
+ return 3
162
+ fi
163
+ printf '%s' "$value"
138
164
  }
139
165
 
140
166
  dot_secrets_bucket_keys() {
@@ -37,15 +37,50 @@ heal_chezmoi_drift() {
37
37
  return 0
38
38
  fi
39
39
 
40
+ # Partition the drift: column 2 is what `chezmoi apply` would fix
41
+ # (destination differs from source). Column 1 is source-only drift
42
+ # (unstaged edits in the chezmoi source tree). Apply can't help with
43
+ # the second case — that needs `git commit` in the source repo.
44
+ local applicable
45
+ local source_only
46
+ applicable=$(printf '%s\n' "$status_output" | awk 'substr($0,2,1)!=" "' | wc -l | tr -d ' ')
47
+ source_only=$(printf '%s\n' "$status_output" | awk 'substr($0,1,1)!=" " && substr($0,2,1)==" "' | wc -l | tr -d ' ')
48
+
40
49
  ISSUES_FOUND=$((ISSUES_FOUND + 1))
41
50
 
42
51
  if [[ "$DRY_RUN" == "1" ]]; then
43
- log_dry "run 'chezmoi apply --force' to re-sync"
44
- else
45
- if _pkg_install "chezmoi re-apply" 0 1 chezmoi apply --force; then
46
- FIXES_APPLIED=$((FIXES_APPLIED + 1))
47
- CHEZMOI_APPLIED=1
48
- persist_log "HEAL: chezmoi apply --force"
52
+ log_dry "run 'chezmoi apply --force' to re-sync ($applicable file(s))"
53
+ return 0
54
+ fi
55
+
56
+ # Skip the apply if every file is source-only drift; nothing to fix.
57
+ if [[ "$applicable" -eq 0 ]]; then
58
+ printf ' \033[38;5;220m⚠\033[0m chezmoi state %s file(s) modified in source only — commit or revert in the source repo\n' "$source_only"
59
+ return 0
60
+ fi
61
+
62
+ # Capture apply output so we can surface failure reasons.
63
+ local apply_log
64
+ apply_log=$(mktemp)
65
+ if chezmoi apply --force >"$apply_log" 2>&1; then
66
+ FIXES_APPLIED=$((FIXES_APPLIED + 1))
67
+ CHEZMOI_APPLIED=1
68
+ persist_log "HEAL: chezmoi apply --force"
69
+
70
+ # Verify: did apply actually clean the drift?
71
+ local remaining
72
+ remaining=$(chezmoi status 2>/dev/null | awk 'substr($0,2,1)!=" "' | wc -l | tr -d ' ')
73
+ if [[ "$remaining" -eq 0 ]]; then
74
+ printf ' \033[38;5;42m✓\033[0m chezmoi re-apply %s file(s) synced\n' "$applicable"
75
+ else
76
+ # shellcheck disable=SC2016
77
+ printf ' \033[38;5;220m⚠\033[0m chezmoi re-apply %s applied, %s still drifted — run `chezmoi diff` to inspect\n' \
78
+ "$((applicable - remaining))" "$remaining"
49
79
  fi
80
+ else
81
+ printf ' \033[38;5;196m✗\033[0m chezmoi re-apply failed; see %s\n' "$apply_log"
82
+ tail -5 "$apply_log" | sed 's/^/ /'
83
+ return 1
50
84
  fi
85
+ rm -f "$apply_log"
51
86
  }
@@ -122,6 +122,10 @@ list_backups() {
122
122
  }
123
123
 
124
124
  # Create a backup
125
+ # LCOV_EXCL_START — body mutates real $HOME (cp/mkdir of dotfiles);
126
+ # can't be exercised under the coverage sandbox without destroying
127
+ # fixture state. The dispatcher reaches this function but the body
128
+ # is genuinely off-limits.
125
129
  create_backup() {
126
130
  local reason="${1:-manual}"
127
131
  local timestamp
@@ -183,7 +187,9 @@ EOF
183
187
  # Cleanup old backups
184
188
  cleanup_old_backups
185
189
  }
190
+ # LCOV_EXCL_STOP
186
191
 
192
+ # LCOV_EXCL_START — rm -rf of real backup dirs.
187
193
  # Cleanup old backups, keeping only MAX_BACKUPS
188
194
  cleanup_old_backups() {
189
195
  local count
@@ -199,6 +205,7 @@ cleanup_old_backups() {
199
205
  done
200
206
  fi
201
207
  }
208
+ # LCOV_EXCL_STOP
202
209
 
203
210
  # Get the most recent backup
204
211
  get_latest_backup() {
@@ -211,6 +218,8 @@ get_backup_by_index() {
211
218
  find "$BACKUP_DIR" -maxdepth 1 -type d -name "backup_*" | sort -r | sed -n "${index}p"
212
219
  }
213
220
 
221
+ # LCOV_EXCL_START — body restores files into real $HOME; dispatcher
222
+ # reaches it but the actual mutation is off-limits under coverage.
214
223
  # Rollback to a specific backup
215
224
  perform_rollback() {
216
225
  local backup_path="$1"
@@ -278,7 +287,9 @@ Analyze why the environment may have reached a state requiring rollback and sugg
278
287
  fi
279
288
  fi
280
289
  }
290
+ # LCOV_EXCL_STOP
281
291
 
292
+ # LCOV_EXCL_START — runs git reset --hard, mutates working tree.
282
293
  # Git-based rollback
283
294
  git_reset() {
284
295
  local dry_run="${1:-0}"
@@ -338,7 +349,9 @@ git_reset() {
338
349
  persist_log "GIT_RESET: to $target_commit"
339
350
  fi
340
351
  }
352
+ # LCOV_EXCL_STOP
341
353
 
354
+ # LCOV_EXCL_START — body copies files into real $HOME.
342
355
  # Restore a specific file from the latest backup
343
356
  restore_file() {
344
357
  local file_path="$1"
@@ -417,6 +430,7 @@ restore_file() {
417
430
  persist_log "RESTORE_FILE: $file_path"
418
431
  fi
419
432
  }
433
+ # LCOV_EXCL_STOP
420
434
 
421
435
  # Show current status
422
436
  show_status() {
@@ -0,0 +1,95 @@
1
+ # Copyright (c) 2015-2026 Dotfiles. All rights reserved.
2
+ #
3
+ # PowerShell parity contract — smoke test for the dotfiles PowerShell
4
+ # profile. Run by the windows-latest CI job (#860). Asserts that:
5
+ #
6
+ # 1. The profile parses + loads without throwing.
7
+ # 2. Core function shims (`dot`, `ll`, `la`, `cat`) are defined.
8
+ # 3. PSScriptAnalyzer reports no errors against the profile (warnings
9
+ # surface but don't fail the gate; tighten later).
10
+ #
11
+ # Exit codes:
12
+ # 0 contract intact.
13
+ # 1 any check failed; details printed to host.
14
+
15
+ $ErrorActionPreference = 'Stop'
16
+ $Failures = New-Object System.Collections.Generic.List[string]
17
+
18
+ $RepoRoot = if ($env:GITHUB_WORKSPACE) { $env:GITHUB_WORKSPACE } else { (Get-Location).Path }
19
+ $ProfileSrc = Join-Path $RepoRoot 'dot_config/powershell/Microsoft.PowerShell_profile.ps1.tmpl'
20
+
21
+ if (-not (Test-Path $ProfileSrc)) {
22
+ Write-Output "FAIL: profile source not found at $ProfileSrc"
23
+ exit 1
24
+ }
25
+
26
+ # Step 1 — chezmoi-render the template into a real .ps1 we can dot-source.
27
+ # In CI, chezmoi is on PATH (installed by setup-chezmoi composite).
28
+ $Rendered = Join-Path $env:TEMP 'Microsoft.PowerShell_profile.ps1'
29
+ if (Get-Command chezmoi -ErrorAction SilentlyContinue) {
30
+ & chezmoi execute-template --init --promptString version=0.2.501 `
31
+ --in $ProfileSrc | Set-Content -Path $Rendered -NoNewline
32
+ } else {
33
+ # Fallback: strip chezmoi template syntax with a simple regex so the
34
+ # contract still runs locally without chezmoi installed.
35
+ (Get-Content $ProfileSrc -Raw) -replace '\{\{[^}]+\}\}', '0.0.0-fallback' |
36
+ Set-Content -Path $Rendered -NoNewline
37
+ }
38
+
39
+ if (-not (Test-Path $Rendered)) {
40
+ $Failures.Add("profile template did not render to $Rendered")
41
+ }
42
+
43
+ # Step 2 — dot-source the rendered profile and verify it didn't throw.
44
+ $loadOk = $false
45
+ try {
46
+ . $Rendered
47
+ $loadOk = $true
48
+ Write-Output "OK: profile dot-sourced"
49
+ } catch {
50
+ Write-Output "FAIL: profile threw during load: $_"
51
+ $Failures.Add("profile load: $_")
52
+ }
53
+
54
+ # Step 3 — verify required function shims are defined.
55
+ $RequiredFunctions = @('dot', 'd', 'll', 'la', 'cat')
56
+ foreach ($fn in $RequiredFunctions) {
57
+ if (Get-Command $fn -CommandType Function -ErrorAction SilentlyContinue) {
58
+ Write-Output "OK: function $fn defined"
59
+ } else {
60
+ Write-Output "FAIL: function $fn not defined"
61
+ $Failures.Add("missing function: $fn")
62
+ }
63
+ }
64
+
65
+ # Step 4 — PSScriptAnalyzer (static analysis). Pre-installed on the
66
+ # windows-latest GitHub-hosted runner; install on demand if missing.
67
+ if (-not (Get-Module -ListAvailable -Name PSScriptAnalyzer)) {
68
+ Write-Output "Installing PSScriptAnalyzer..."
69
+ Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser -ErrorAction SilentlyContinue | Out-Null
70
+ }
71
+
72
+ if (Get-Module -ListAvailable -Name PSScriptAnalyzer) {
73
+ Import-Module PSScriptAnalyzer -Force
74
+ $Findings = Invoke-ScriptAnalyzer -Path $Rendered -Severity 'Error' -ErrorAction SilentlyContinue
75
+ if ($Findings) {
76
+ foreach ($f in $Findings) {
77
+ Write-Output "FAIL: PSScriptAnalyzer error: $($f.RuleName) at line $($f.Line): $($f.Message)"
78
+ $Failures.Add("PSScriptAnalyzer error: $($f.RuleName)")
79
+ }
80
+ } else {
81
+ Write-Output "OK: PSScriptAnalyzer clean (Error severity)"
82
+ }
83
+ } else {
84
+ Write-Output "WARN: PSScriptAnalyzer not available — static analysis skipped"
85
+ }
86
+
87
+ Write-Output ""
88
+ if ($Failures.Count -eq 0) {
89
+ Write-Output "PowerShell contract: PASS"
90
+ exit 0
91
+ } else {
92
+ Write-Output "PowerShell contract: FAIL ($($Failures.Count) issue(s))"
93
+ foreach ($f in $Failures) { Write-Output " - $f" }
94
+ exit 1
95
+ }
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env bash
2
+ # Copyright (c) 2015-2026 Dotfiles. All rights reserved.
3
+ #
4
+ # scripts/security/check-disclosure-key-expiry.sh
5
+ #
6
+ # Reads the disclosure public key from `docs/security/security-pubkey.asc`,
7
+ # parses its expiry, and exits non-zero when the remaining lifetime is
8
+ # below threshold. Closes the R3 audit N2 finding (no automated alert
9
+ # for the 2029-05-15 expiry).
10
+ #
11
+ # Two thresholds:
12
+ # --warn-days N Emit a `::warning::` if expiry is < N days away.
13
+ # Default 90.
14
+ # --fail-days N Exit 1 (CI failure) if expiry is < N days away.
15
+ # Default 30.
16
+ #
17
+ # Usage:
18
+ # bash scripts/security/check-disclosure-key-expiry.sh
19
+ # bash scripts/security/check-disclosure-key-expiry.sh --warn-days 180 --fail-days 60
20
+ #
21
+ # Designed to run from `.github/workflows/verify-gpg-wkd.yml` on the
22
+ # weekly schedule, and from a maintainer's local pre-release sanity
23
+ # check. Does NOT require network — reads the in-repo .asc only.
24
+
25
+ set -euo pipefail
26
+
27
+ WARN_DAYS=90
28
+ FAIL_DAYS=30
29
+
30
+ while [[ $# -gt 0 ]]; do
31
+ case "$1" in
32
+ --warn-days)
33
+ WARN_DAYS="$2"
34
+ shift 2
35
+ ;;
36
+ --fail-days)
37
+ FAIL_DAYS="$2"
38
+ shift 2
39
+ ;;
40
+ -h | --help)
41
+ sed -n '5,25p' "${BASH_SOURCE[0]}"
42
+ exit 0
43
+ ;;
44
+ *)
45
+ echo "Unknown arg: $1" >&2
46
+ exit 2
47
+ ;;
48
+ esac
49
+ done
50
+
51
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
52
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
53
+ ASC="$REPO_ROOT/docs/security/security-pubkey.asc"
54
+
55
+ if [[ ! -f "$ASC" ]]; then
56
+ echo "::error::no public key at $ASC" >&2
57
+ exit 2
58
+ fi
59
+
60
+ if ! command -v gpg >/dev/null 2>&1; then
61
+ echo "::warning::gpg not installed — cannot verify key expiry" >&2
62
+ exit 0
63
+ fi
64
+
65
+ # Import into a throwaway keyring so we don't pollute the user's real one.
66
+ GNUPGHOME="$(mktemp -d -t dot-disclosure-key.XXXXXX)"
67
+ export GNUPGHOME
68
+ chmod 700 "$GNUPGHOME"
69
+ trap 'rm -rf "$GNUPGHOME"' EXIT
70
+
71
+ # gpg can emit harmless warnings ("keyboxd older than us", "gpg-agent
72
+ # IPC failed" in fresh GNUPGHOME, etc.) and exit non-zero under
73
+ # `set -e` even when the import itself succeeded. Verify success by
74
+ # checking the err log for the literal `imported: N` (N >= 1) line
75
+ # rather than trusting gpg's exit code.
76
+ gpg --batch --import "$ASC" >/dev/null 2>"$GNUPGHOME/import.err" || true
77
+ if ! grep -qE '^gpg:[[:space:]]*imported: [1-9]' "$GNUPGHOME/import.err"; then
78
+ echo "::error::gpg import failed for $ASC" >&2
79
+ cat "$GNUPGHOME/import.err" >&2
80
+ exit 2
81
+ fi
82
+
83
+ # `--with-colons` field 7 of the `pub:` line is the expiry in Unix
84
+ # seconds; 0 means no expiry.
85
+ expires=$(gpg --with-colons --fingerprint 2>/dev/null | awk -F: '/^pub:/{print $7; exit}')
86
+
87
+ if [[ -z "$expires" || "$expires" == "0" ]]; then
88
+ echo "::warning::disclosure key has no expiry — set one per docs/security/KEY_ROTATION.md" >&2
89
+ exit 0
90
+ fi
91
+
92
+ now=$(date -u +%s)
93
+ remaining_days=$(((expires - now) / 86400))
94
+
95
+ expires_date=$(date -u -r "$expires" '+%Y-%m-%d' 2>/dev/null || date -u -d "@$expires" '+%Y-%m-%d' 2>/dev/null || echo "(date format unknown)")
96
+
97
+ echo "Disclosure key expires: $expires_date ($remaining_days days from now)"
98
+ echo " warn threshold: $WARN_DAYS days"
99
+ echo " fail threshold: $FAIL_DAYS days"
100
+
101
+ if ((remaining_days < FAIL_DAYS)); then
102
+ echo "::error::Disclosure key expires in $remaining_days days. Rotate now per docs/security/KEY_ROTATION.md." >&2
103
+ exit 1
104
+ fi
105
+
106
+ if ((remaining_days < WARN_DAYS)); then
107
+ echo "::warning::Disclosure key expires in $remaining_days days. Schedule rotation per docs/security/KEY_ROTATION.md." >&2
108
+ fi
109
+
110
+ exit 0
@@ -22,13 +22,22 @@ if [[ "$OSTYPE" == "darwin"* ]]; then
22
22
  UNLOCK_CMD="chflags nouchg"
23
23
  CHECK_CMD="ls -lO"
24
24
  else
25
- # Linux (requires root usually, but we'll try)
25
+ # Linux (chattr +i requires root; fail loudly when sudo is missing
26
+ # rather than printing a confusing error per file in automation)
26
27
  if command -v chattr >/dev/null; then
28
+ if ! command -v sudo >/dev/null; then
29
+ echo " 'sudo' not found; chattr requires root. Aborting." >&2
30
+ exit 1
31
+ fi
32
+ if ! sudo -n true 2>/dev/null && [[ ! -t 0 ]]; then
33
+ echo " sudo requires a password but no TTY is attached. Aborting." >&2
34
+ exit 1
35
+ fi
27
36
  LOCK_CMD="sudo chattr +i"
28
37
  UNLOCK_CMD="sudo chattr -i"
29
38
  CHECK_CMD="lsattr"
30
39
  else
31
- echo " 'chattr' not found. Cannot set immutability on Linux without it."
40
+ echo " 'chattr' not found. Cannot set immutability on Linux without it." >&2
32
41
  exit 1
33
42
  fi
34
43
  fi
@@ -81,6 +81,10 @@ merge_pair() {
81
81
 
82
82
  local tmpdir
83
83
  tmpdir="$(mktemp -d)"
84
+ # Single-quotes prevent the variable expanding at trap-definition time;
85
+ # `$tmpdir` resolves when the trap fires (still scoped to this function
86
+ # because RETURN is per-function).
87
+ # shellcheck disable=SC2064 # intentionally expand $tmpdir at trap time within this function scope
84
88
  trap "rm -rf '$tmpdir'" RETURN
85
89
 
86
90
  # Convert both to JPEG for heif-enc input (it handles JPEG best)
@@ -184,7 +184,7 @@ pick_theme() {
184
184
  fi
185
185
 
186
186
  local current
187
- local current="$(current_theme)"
187
+ current="$(current_theme)"
188
188
 
189
189
  if [[ ! -f "$THEMES_FILE" ]]; then
190
190
  ui_err "Missing" "$THEMES_FILE"
@@ -234,7 +234,8 @@ pick_theme() {
234
234
  }
235
235
 
236
236
  list_themes() {
237
- local current="$(current_theme)"
237
+ local current
238
+ current="$(current_theme)"
238
239
  local current_family="${current%-dark}"
239
240
  [[ "$current_family" != "$current" ]] || current_family="${current%-light}"
240
241
 
@@ -260,7 +261,8 @@ list_themes() {
260
261
 
261
262
  # Toggle between light and dark within the same family, or switch families
262
263
  toggle_theme() {
263
- local current="$(current_theme)"
264
+ local current
265
+ current="$(current_theme)"
264
266
 
265
267
  if is_dark_theme "$current"; then
266
268
  if [[ "$current" == *-dark ]]; then
@@ -279,8 +281,9 @@ toggle_theme() {
279
281
 
280
282
  # Switch to the next wallpaper family while preserving mode.
281
283
  switch_family() {
282
- local current="$(current_theme)"
283
- local family="$(get_theme_family "$current")"
284
+ local current family
285
+ current="$(current_theme)"
286
+ family="$(get_theme_family "$current")"
284
287
  local mode="dark"
285
288
  local families=()
286
289
  local idx=0
@@ -316,8 +319,9 @@ switch_family() {
316
319
 
317
320
  # Show current theme info
318
321
  show_current() {
319
- local current="$(current_theme)"
320
- local family="$(get_theme_family "$current")"
322
+ local current family
323
+ current="$(current_theme)"
324
+ family="$(get_theme_family "$current")"
321
325
  local mode="dark"
322
326
  if ! is_dark_theme "$current" 2>/dev/null; then
323
327
  mode="light"
@@ -338,8 +342,13 @@ sync_theme() {
338
342
  ;;
339
343
  Linux)
340
344
  if command -v gsettings >/dev/null 2>&1; then
341
- # Check GNOME color scheme
342
- local scheme=scheme=$(gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null | tr -d "'")
345
+ # Check GNOME color scheme. Fixes a long-standing bug where the
346
+ # variable was declared as `local scheme=scheme=$(...)` the
347
+ # extra `scheme=` prefix made the value always start with that
348
+ # literal, so the `prefer-light` test below could never match
349
+ # and the system always fell through to the dark branch.
350
+ local scheme
351
+ scheme=$(gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null | tr -d "'")
343
352
  if [[ "$scheme" == "prefer-light" ]]; then
344
353
  os_mode="light"
345
354
  else
@@ -349,7 +358,8 @@ sync_theme() {
349
358
  ;;
350
359
  esac
351
360
 
352
- local current="$(current_theme)"
361
+ local current
362
+ current="$(current_theme)"
353
363
  if is_dark_theme "$current" && [[ "$os_mode" == "light" ]]; then
354
364
  ui_info "Sync" "System is light, switching dotfiles to light..."
355
365
  toggle_theme
@@ -142,6 +142,8 @@ sed_in_place() {
142
142
  fi
143
143
  }
144
144
 
145
+ # LCOV_EXCL_START — rm -rf BACKUP_DIR + cp of real release files;
146
+ # only run during a real release flow, not safe under coverage.
145
147
  create_backup() {
146
148
  local files=("$@")
147
149
 
@@ -166,6 +168,7 @@ create_backup() {
166
168
 
167
169
  log_success "Backed up $backup_count files"
168
170
  }
171
+ # LCOV_EXCL_STOP
169
172
 
170
173
  update_version_references() {
171
174
  local target_version="$1"
@@ -1,40 +0,0 @@
1
- # Atuin Configuration (~/.config/atuin/config.toml)
2
- # https://atuin.sh/docs/config
3
-
4
- # Core
5
- update_check = false
6
- db_path = "~/.local/share/atuin/history.db"
7
- key_path = "~/.local/share/atuin/key"
8
- session_path = "~/.local/share/atuin/session"
9
-
10
- # Sync
11
- auto_sync = true
12
- sync_frequency = "10m"
13
- sync_address = "https://api.atuin.sh"
14
-
15
- # UI
16
- style = "compact"
17
- inline_height = 20
18
- show_preview = true
19
- enter_accept = true
20
-
21
- # Search
22
- search_mode = "fuzzy"
23
- filter_mode = "global"
24
- workspaces = true
25
-
26
- # Privacy: skip commands that may contain secrets
27
- history_filter = [
28
- "^export (SECRET|TOKEN|PASSWORD|API_KEY|AWS_)",
29
- "^PASSWORD=",
30
- "^ssh-keygen",
31
- "^(aws|gcloud|az|kubectl).*(configure|auth|login|-k |--kubeconfig)",
32
- "curl.*-H.*[Aa]uthorization",
33
- "git clone.*https://.*:.*@",
34
- "^vault ",
35
- "^op (read|inject|run)",
36
- ]
37
-
38
- # Skip ephemeral directories
39
- secrets_filter = true
40
- network_timeout = 30