@sebastienrousseau/dotfiles 0.2.518 → 0.2.520

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/README.md +1164 -164
  3. package/docs/ARCHITECTURE.md +117 -0
  4. package/docs/COPYRIGHT +1 -1
  5. package/docs/ECOSYSTEM.md +220 -0
  6. package/docs/GOLD-STANDARD-AUDIT.md +352 -0
  7. package/docs/GOVERNANCE.md +1 -1
  8. package/docs/MAINTAINERS.md +1 -1
  9. package/docs/MINIMUM-TOOLCHAIN.md +100 -0
  10. package/docs/README.md +1 -1
  11. package/docs/STRUCTURE.md +1 -1
  12. package/docs/architecture/ARCHITECTURE.md +11 -108
  13. package/docs/architecture/REPO_LAYOUT.md +4 -4
  14. package/docs/guides/MACOS_ICLOUD_SYMLINKS.md +121 -0
  15. package/docs/index.md +3 -3
  16. package/docs/manual/00-introduction.md +1 -1
  17. package/docs/manual/03-reference/01-dot-cli.md +28 -3
  18. package/docs/manual/command-index.md +24 -4
  19. package/docs/manual/concept-index.md +2 -0
  20. package/docs/migration/README.md +81 -0
  21. package/docs/migration/from-bare-git-repo.md +156 -0
  22. package/docs/migration/from-gnu-stow.md +165 -0
  23. package/docs/migration/from-plain-chezmoi.md +148 -0
  24. package/docs/migration/from-yadm.md +187 -0
  25. package/docs/operations/PERFORMANCE_BUDGETS.md +196 -0
  26. package/docs/operations/REGISTRY.md +1 -1
  27. package/docs/operations/RELEASE_PIPELINE.md +4 -4
  28. package/docs/operations/TESTING.md +3 -3
  29. package/docs/operations/TRACEABILITY.md +1 -0
  30. package/docs/packaging.md +222 -0
  31. package/docs/reference/FEATURE-MATRIX.md +646 -0
  32. package/docs/reference/TOOLS.md +1 -1
  33. package/docs/reference/UTILS.md +1 -0
  34. package/docs/security/COMPLIANCE.md +1 -1
  35. package/docs/security/DISCLOSURE.md +4 -4
  36. package/docs/security/FUZZING.md +113 -18
  37. package/docs/security/KEY_ROTATION.md +1 -1
  38. package/docs/security/SCORECARD.md +3 -3
  39. package/docs/security/VERIFY_RELEASE.md +2 -2
  40. package/install.sh +6 -6
  41. package/package.json +2 -2
  42. package/scripts/ci/check-copyright-headers.sh +1 -1
  43. package/scripts/ci/check-shell-preamble.sh +1 -1
  44. package/scripts/ci/guard-gitleaks-checkout.sh +1 -1
  45. package/scripts/demo/record.sh +1 -1
  46. package/scripts/diagnostics/a2a-conformance.sh +1 -1
  47. package/scripts/diagnostics/alias-governance.sh +30 -3
  48. package/scripts/diagnostics/aliases-cheatsheet.sh +1 -1
  49. package/scripts/diagnostics/aliases-manifest.sh +1 -1
  50. package/scripts/diagnostics/attest-verify.sh +147 -0
  51. package/scripts/diagnostics/benchmark.sh +1 -1
  52. package/scripts/diagnostics/conflicts.sh +1 -1
  53. package/scripts/diagnostics/doctor-unified.sh +6 -2
  54. package/scripts/diagnostics/doctor.sh +56 -10
  55. package/scripts/diagnostics/drift-dashboard.sh +3 -2
  56. package/scripts/diagnostics/health.sh +43 -10
  57. package/scripts/diagnostics/history-analysis.sh +1 -1
  58. package/scripts/diagnostics/mcp-doctor.sh +2 -2
  59. package/scripts/diagnostics/perf.sh +1 -1
  60. package/scripts/diagnostics/scorecard.sh +3 -2
  61. package/scripts/diagnostics/secret-governance.sh +1 -1
  62. package/scripts/diagnostics/security-score.sh +1 -1
  63. package/scripts/diagnostics/smoke-test.sh +1 -1
  64. package/scripts/diagnostics/snapshot.sh +1 -1
  65. package/scripts/diagnostics/verify.sh +1 -1
  66. package/scripts/diagnostics/verify_state.sh +1 -1
  67. package/scripts/diagnostics/version-locks.sh +1 -1
  68. package/scripts/diagnostics/workstation-attestation.sh +26 -1
  69. package/scripts/dot/commands/agent.sh +64 -14
  70. package/scripts/dot/commands/agents.sh +24 -8
  71. package/scripts/dot/commands/ai.sh +20 -7
  72. package/scripts/dot/commands/aliases.sh +1 -1
  73. package/scripts/dot/commands/appearance.sh +1 -1
  74. package/scripts/dot/commands/completion.sh +42 -5
  75. package/scripts/dot/commands/core.sh +1 -1
  76. package/scripts/dot/commands/diagnostics.sh +1 -1
  77. package/scripts/dot/commands/env-emit.sh +1 -1
  78. package/scripts/dot/commands/fleet.sh +41 -18
  79. package/scripts/dot/commands/init.sh +1 -1
  80. package/scripts/dot/commands/lint.sh +1 -1
  81. package/scripts/dot/commands/manual.sh +1 -1
  82. package/scripts/dot/commands/meta.sh +115 -10
  83. package/scripts/dot/commands/patterns.sh +1 -1
  84. package/scripts/dot/commands/registry.sh +43 -7
  85. package/scripts/dot/commands/restore.sh +1 -1
  86. package/scripts/dot/commands/secrets.sh +1 -1
  87. package/scripts/dot/commands/security.sh +1 -1
  88. package/scripts/dot/commands/tools.sh +21 -7
  89. package/scripts/fonts/install-nerd-fonts.sh +1 -1
  90. package/scripts/fonts/patch-fonts.sh +1 -1
  91. package/scripts/git-hooks/install.sh +1 -1
  92. package/scripts/git-hooks/pre-commit-audit.sh +2 -2
  93. package/scripts/lib/secrets_provider.sh +19 -4
  94. package/scripts/nvim/headless-upgrade.lua +81 -0
  95. package/scripts/ops/ai-setup.sh +1 -1
  96. package/scripts/ops/bundle.sh +1 -1
  97. package/scripts/ops/chaos.sh +1 -1
  98. package/scripts/ops/chezmoi-apply.sh +1 -1
  99. package/scripts/ops/chezmoi-diff.sh +1 -1
  100. package/scripts/ops/chezmoi-remove.sh +3 -3
  101. package/scripts/ops/chezmoi-update.sh +6 -2
  102. package/scripts/ops/heal-chezmoi.sh +1 -1
  103. package/scripts/ops/heal-system.sh +1 -1
  104. package/scripts/ops/heal-tools.sh +1 -1
  105. package/scripts/ops/heal.sh +1 -1
  106. package/scripts/ops/post-apply-repair.sh +1 -1
  107. package/scripts/ops/prewarm.sh +1 -1
  108. package/scripts/ops/release.sh +1 -1
  109. package/scripts/ops/rollback.sh +11 -2
  110. package/scripts/ops/setup.sh +1 -1
  111. package/scripts/ops/teleport.sh +1 -1
  112. package/scripts/qa/check-feature-matrix.sh +296 -0
  113. package/scripts/qa/check-version-consistency.sh +7 -119
  114. package/scripts/qa/coverage-baseline.sh +1 -1
  115. package/scripts/qa/docs-coverage.sh +9 -3
  116. package/scripts/qa/examples-coverage.sh +1 -1
  117. package/scripts/qa/reliability-audit.sh +1 -1
  118. package/scripts/qa/scorecard-snapshot.sh +1 -1
  119. package/scripts/qa/traceability-coverage.sh +9 -2
  120. package/scripts/qa/validate-examples.sh +66 -3
  121. package/scripts/qa/wsl-contract.sh +1 -1
  122. package/scripts/secrets/age-init.sh +1 -1
  123. package/scripts/secrets/create-secrets-file.sh +1 -1
  124. package/scripts/secrets/encrypt-ssh-key.sh +1 -1
  125. package/scripts/security/backup.sh +1 -1
  126. package/scripts/security/check-disclosure-key-expiry.sh +1 -1
  127. package/scripts/security/dns-doh.sh +1 -1
  128. package/scripts/security/encryption-check.sh +1 -1
  129. package/scripts/security/enforce-policies.sh +315 -98
  130. package/scripts/security/firewall.sh +1 -1
  131. package/scripts/security/lock-configs.sh +1 -1
  132. package/scripts/security/lock-screen.sh +1 -1
  133. package/scripts/security/manage-secrets.sh +1 -1
  134. package/scripts/security/ssh-cert.sh +1 -1
  135. package/scripts/security/telemetry-kill.sh +1 -1
  136. package/scripts/security/usb-safety.sh +1 -1
  137. package/scripts/theme/apply-gnome-theme.sh +1 -1
  138. package/scripts/theme/extract-heic-frames.sh +1 -1
  139. package/scripts/theme/extract-theme.py +284 -6
  140. package/scripts/theme/install-boot-logo.sh +1 -1
  141. package/scripts/theme/install-catppuccin-themes.sh +1 -1
  142. package/scripts/theme/install-cursors.sh +1 -1
  143. package/scripts/theme/install-file-icons.sh +1 -1
  144. package/scripts/theme/install-grub-theme.sh +1 -1
  145. package/scripts/theme/install-lock-icon.sh +1 -1
  146. package/scripts/theme/merge-wallpaper.sh +1 -1
  147. package/scripts/theme/rebuild-themes.sh +64 -5
  148. package/scripts/theme/switch.sh +46 -19
  149. package/scripts/theme/wallpaper-rotate.sh +1 -1
  150. package/scripts/theme/wallpaper-sync.sh +1 -1
  151. package/scripts/tools/cmatrix.sh +1 -1
  152. package/scripts/tools/detect-collisions.py +1 -1
  153. package/scripts/tools/emoji-picker.sh +1 -1
  154. package/scripts/tools/figlet-banner.sh +1 -1
  155. package/scripts/tools/log-rotate.sh +1 -1
  156. package/scripts/tools/lolcat-wrap.sh +1 -1
  157. package/scripts/tools/pipes.sh +1 -1
  158. package/scripts/tuning/linux.sh +1 -1
  159. package/scripts/tuning/macos.sh +1 -1
  160. package/scripts/uninstall.sh +1 -1
  161. package/scripts/verify-release-versions +156 -0
  162. package/scripts/version-sync.sh +63 -3
  163. package/LICENSE +0 -21
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  set -euo pipefail
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  set -euo pipefail
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  set -euo pipefail
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Dotfiles Pre-Commit Sentinel (Audit Hook)
5
5
  # Prevents secrets, hardcoded paths, and shell hygiene violations.
@@ -141,6 +141,6 @@ if [[ $FAILED -eq 1 ]]; then
141
141
  printf '%b\\n' " (Use --no-verify to bypass if absolutely necessary)"
142
142
  exit 1
143
143
  else
144
- printf '%b\n' "${GREEN}${BOLD}✅ Audit passed.${NC} v0.2.518 standards maintained."
144
+ printf '%b\n' "${GREEN}${BOLD}✅ Audit passed.${NC} v0.2.520 standards maintained."
145
145
  exit 0
146
146
  fi
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Provider-agnostic secrets bridge for dotfiles.
5
5
 
@@ -96,9 +96,24 @@ dot_secrets_store_plain_enc() {
96
96
  dot_secrets_ensure_layout
97
97
  tmp_rec="$(umask 077 && mktemp)"
98
98
  file="$DOT_SECRETS_STORE_DIR/${key}.age"
99
- trap 'rm -f "$tmp_rec"' RETURN
100
- age-keygen -y "$DOT_SECRETS_AGE_KEY" >"$tmp_rec"
101
- printf "%s" "$value" | age -R "$tmp_rec" -o "$file"
99
+
100
+ # No RETURN trap here. A RETURN trap set inside a function also fires when
101
+ # its CALLER returns, and by then `tmp_rec` local to this function — is
102
+ # out of scope: under `set -u` the trap aborted dot_secrets_set with
103
+ # "tmp_rec: unbound variable" AFTER the encrypted file had been written, so
104
+ # `dot secrets set` reported failure on a write that had succeeded. Clean up
105
+ # explicitly on both paths instead.
106
+ if ! age-keygen -y "$DOT_SECRETS_AGE_KEY" >"$tmp_rec"; then
107
+ rm -f "$tmp_rec"
108
+ echo "failed to derive the age recipient from $DOT_SECRETS_AGE_KEY" >&2
109
+ return 1
110
+ fi
111
+ if ! printf "%s" "$value" | age -R "$tmp_rec" -o "$file"; then
112
+ rm -f "$tmp_rec"
113
+ echo "failed to encrypt secret: $key" >&2
114
+ return 1
115
+ fi
116
+ rm -f "$tmp_rec"
102
117
  chmod 600 "$file" 2>/dev/null || true
103
118
  }
104
119
 
@@ -0,0 +1,81 @@
1
+ -- Copyright (c) 2015-2026 Dotfiles. All rights reserved.
2
+ -- headless-upgrade.lua — plugin/tool refresh for `dot upgrade`.
3
+ --
4
+ -- Runs Lazy sync and then waits for Mason's async install queue to drain
5
+ -- before exiting. The naive invocation
6
+ -- nvim --headless "+Lazy! sync" +qa
7
+ -- races on two fronts:
8
+ -- 1. Lazy's own async ops (git fetch/checkout, `build` hooks) can outlive
9
+ -- the `Lazy! sync` command return.
10
+ -- 2. `mason-nvim-dap` and `mason-lspconfig` fire `ensure_installed` on
11
+ -- plugin load; those installs are enqueued to mason.nvim and run
12
+ -- async, so `+qa` aborts them mid-download (leaving codelldb / debugpy
13
+ -- / delve half-installed, per repeated user reports on `dot upgrade`).
14
+ --
15
+ -- We explicitly wait for both queues before quitting.
16
+
17
+ local LAZY_TIMEOUT_MS = 300000 -- 5 min per phase
18
+ local MASON_TIMEOUT_MS = 300000
19
+
20
+ local function log(msg)
21
+ io.stderr:write(("[headless-upgrade] %s\n"):format(msg))
22
+ end
23
+
24
+ -- ---------------------------------------------------------------------------
25
+ -- Phase 1: Lazy sync (blocking)
26
+ -- ---------------------------------------------------------------------------
27
+ local ok_lazy, lazy = pcall(require, "lazy")
28
+ if not ok_lazy then
29
+ log("lazy.nvim not available; skipping plugin update")
30
+ vim.cmd("quitall!")
31
+ return
32
+ end
33
+
34
+ lazy.sync({ wait = true, show = false })
35
+
36
+ -- Belt-and-braces: give any residual runner tasks a chance to drain even if
37
+ -- `wait = true` returned early on the last task in a batch.
38
+ pcall(function()
39
+ local ok_runner, runner = pcall(require, "lazy.manage.runner")
40
+ if not ok_runner then return end
41
+ vim.wait(LAZY_TIMEOUT_MS, function()
42
+ return not (runner.running and runner.running())
43
+ end, 200)
44
+ end)
45
+
46
+ -- ---------------------------------------------------------------------------
47
+ -- Phase 2: drain Mason's async install queue
48
+ -- ---------------------------------------------------------------------------
49
+ pcall(function()
50
+ local ok_reg, registry = pcall(require, "mason-registry")
51
+ if not ok_reg then return end
52
+
53
+ -- Refresh the remote registry so ensure_installed picks up latest versions.
54
+ local refreshed = false
55
+ registry.refresh(function() refreshed = true end)
56
+ vim.wait(30000, function() return refreshed end, 200)
57
+
58
+ local function any_installing()
59
+ for _, pkg in ipairs(registry.get_all_packages() or {}) do
60
+ if pkg.is_installing and pkg:is_installing() then
61
+ return true
62
+ end
63
+ end
64
+ return false
65
+ end
66
+
67
+ -- Small settle time so installs triggered by ensure_installed hooks have
68
+ -- a chance to enter the installing state before we start polling.
69
+ vim.wait(2000, function() return false end, 200)
70
+
71
+ local done = vim.wait(MASON_TIMEOUT_MS, function()
72
+ return not any_installing()
73
+ end, 500)
74
+
75
+ if not done then
76
+ log(("Mason install queue still active after %ds; exiting anyway"):format(
77
+ MASON_TIMEOUT_MS / 1000))
78
+ end
79
+ end)
80
+
81
+ vim.cmd("quitall!")
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Universal AI Toolchain Setup & Authentication.
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Create an offline bundle of the dotfiles environment for air-gapped systems.
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Chaos Engineering: Randomly corrupt config files to test self-healing (dot heal)
5
5
  # USE WITH CAUTION: This intentionally breaks your environment.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  set -euo pipefail
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  set -euo pipefail
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  set -euo pipefail
5
5
 
@@ -36,11 +36,11 @@ if [[ $remove_source -eq 0 ]]; then
36
36
  args+=("--keep-source")
37
37
  fi
38
38
 
39
- printf "About to run: chezmoi remove %s %s\n" "${args[*]}" "${paths[*]}"
39
+ printf "About to run: chezmoi remove %s %s\n" "${args[*]:-}" "${paths[*]}"
40
40
  read -r -p "Proceed? [y/N] " confirm
41
41
  if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
42
42
  echo "Aborted."
43
43
  exit 1
44
44
  fi
45
45
 
46
- chezmoi remove "${args[@]}" "${paths[@]}"
46
+ chezmoi remove ${args[@]+"${args[@]}"} "${paths[@]}"
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  set -euo pipefail
5
5
 
@@ -45,7 +45,11 @@ NOTICE_FILE="$STATE_DIR/notice"
45
45
 
46
46
  run_update() {
47
47
  echo "Updating dotfiles..."
48
- chezmoi update "${args[@]}"
48
+ # `${args[@]+...}`: with DOTFILES_INTERACTIVE_APPLY=1 no flags are added,
49
+ # and bash 3.2 (the system bash on macOS) treats an unguarded empty array
50
+ # expansion under `set -u` as an unbound variable — `dot update` then died
51
+ # instead of running chezmoi.
52
+ chezmoi update ${args[@]+"${args[@]}"}
49
53
  }
50
54
 
51
55
  if $ASYNC; then
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # shellcheck disable=SC2034
5
5
  # =============================================================================
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # shellcheck disable=SC2034
5
5
  # =============================================================================
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # shellcheck disable=SC2034
5
5
  # =============================================================================
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # shellcheck disable=SC2034
5
5
  # =============================================================================
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  set -euo pipefail
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Pre-warm shell initialization caches for ultra-fast startup.
5
5
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # =============================================================================
5
5
  # Dotfiles Release Script
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # shellcheck disable=SC2034
5
5
  # =============================================================================
@@ -35,6 +35,11 @@ BACKUP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/dotfiles/backups"
35
35
  STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles"
36
36
  ROLLBACK_LOG="$STATE_DIR/rollback.log"
37
37
  MAX_BACKUPS=10
38
+ # sysexits.h EX_TEMPFAIL: the command did nothing because another instance
39
+ # holds the lock. Distinct from 1 (the rollback ran and failed) and from 0
40
+ # (the rollback ran and succeeded) — a caller has to be able to tell those
41
+ # three apart.
42
+ EXIT_BUSY=75
38
43
 
39
44
  # Logging — delegates to shared ui.sh primitives
40
45
  ui_init
@@ -525,8 +530,12 @@ main() {
525
530
  if command -v flock >/dev/null 2>&1; then
526
531
  exec 9>"$LOCK_FILE"
527
532
  if ! flock -n 9; then
533
+ # Losing the lock means this invocation did nothing. Exiting 0 made
534
+ # "did nothing" indistinguishable from "rolled back" to any caller or
535
+ # CI step, which is the one thing a recovery tool must never be
536
+ # ambiguous about. 75 is EX_TEMPFAIL: nothing is wrong, try again.
528
537
  ui_warn "Already running" "Another rollback instance is active"
529
- exit 0
538
+ exit "$EXIT_BUSY"
530
539
  fi
531
540
  fi
532
541
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Interactive setup for dotfiles preferences
5
5
  # Usage: dot setup
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Script: teleport.sh
5
5
  # Description: Deploys dotfiles to a remote host ephemerally.
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-License-Identifier: Apache-2.0 OR MIT
3
+ # Copyright (c) 2015-2026 Sebastien Rousseau
4
+ #
5
+ # check-feature-matrix.sh — drift gate for docs/reference/FEATURE-MATRIX.md.
6
+ #
7
+ # The matrix is a contract: every user-facing feature of the `dot` CLI has a
8
+ # row naming the regression test, benchmark, example and manual section that
9
+ # cover it. A contract nobody checks rots, so this script checks it, and
10
+ # ci.yml runs it on every push.
11
+ #
12
+ # Four checks:
13
+ #
14
+ # 1. Command coverage — every command the dispatcher can route (bin/dot's
15
+ # route table, which is what `scripts/dot/commands/*.sh` are reached
16
+ # through) and every command in the generated docs/manual/command-index.md
17
+ # has at least one row.
18
+ # 2. Test coverage — every regression test function named in a row is
19
+ # defined by one of tests/regression/test_feature_matrix_*.sh.
20
+ # 3. Benchmark coverage — every benchmark id named in a row is produced by
21
+ # `benches/dot_command_bench.sh --list-ids`.
22
+ # 4. Example coverage — every example file named in a row exists and is
23
+ # executable by scripts/qa/validate-examples.sh.
24
+ #
25
+ # Usage:
26
+ # scripts/qa/check-feature-matrix.sh [--quiet]
27
+ #
28
+ # Exit codes:
29
+ # 0 the matrix is in sync
30
+ # 1 drift detected (details on stderr)
31
+ # 2 bad invocation, or a file the gate depends on is missing
32
+
33
+ set -euo pipefail
34
+
35
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
36
+ REPO_ROOT="${REPO_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
37
+ cd "$REPO_ROOT"
38
+
39
+ MATRIX="docs/reference/FEATURE-MATRIX.md"
40
+ DOT_BIN="bin/dot"
41
+ COMMAND_INDEX="docs/manual/command-index.md"
42
+ BENCH="benches/dot_command_bench.sh"
43
+ TEST_GLOB="tests/regression/test_feature_matrix_*.sh"
44
+
45
+ QUIET=0
46
+ case "${1:-}" in
47
+ --quiet | -q) QUIET=1 ;;
48
+ -h | --help)
49
+ sed -n '5,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
50
+ exit 0
51
+ ;;
52
+ "") ;;
53
+ *)
54
+ printf 'Unknown option: %s\n' "$1" >&2
55
+ exit 2
56
+ ;;
57
+ esac
58
+
59
+ say() { [[ "$QUIET" -eq 1 ]] || printf '%s\n' "$*"; }
60
+
61
+ for required in "$MATRIX" "$DOT_BIN" "$COMMAND_INDEX" "$BENCH"; do
62
+ if [[ ! -f "$required" ]]; then
63
+ printf '::error::required file missing: %s\n' "$required" >&2
64
+ exit 2
65
+ fi
66
+ done
67
+
68
+ failures=0
69
+ fail() {
70
+ printf '::error::%s\n' "$1" >&2
71
+ failures=$((failures + 1))
72
+ }
73
+
74
+ work="$(mktemp -d)"
75
+ trap 'rm -rf "$work"' EXIT
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Parse the matrix.
79
+ #
80
+ # Rows look like:
81
+ # | `dot fleet apply` | --dry-run | `test_fm_x` | `run:y` | `examples/z.sh` | `docs/…` | regression |
82
+ #
83
+ # Field 1 is the command (backticked, "dot " prefix), 3 the test function,
84
+ # 4 the benchmark id, 5 the example path. The two explanatory tables at the
85
+ # top of the document have different shapes and are skipped by requiring a
86
+ # leading "| `dot ".
87
+ # ---------------------------------------------------------------------------
88
+
89
+ awk '
90
+ # A cell may legitimately contain an escaped pipe (a variant like
91
+ # "set advisory\|strict"). Neutralise those BEFORE splitting on "|", or the
92
+ # row shifts a column and every later check reads the wrong cell.
93
+ /^\| `dot / {
94
+ line = $0
95
+ gsub(/\\\|/, "\002", line)
96
+ n = split(line, cell, "|")
97
+ # 7 content columns become 9 fields: the empty strings either side of the
98
+ # leading and trailing pipes. The document also carries a 3-column summary
99
+ # table of the unmeasurable rows, which must not be parsed as feature rows.
100
+ if (n < 9) next
101
+ for (i = 1; i <= n; i++) {
102
+ gsub(/^[ \t]+|[ \t]+$/, "", cell[i])
103
+ gsub(/`/, "", cell[i])
104
+ gsub(/\002/, "|", cell[i])
105
+ }
106
+ # cell[2] command, cell[4] test, cell[5] bench, cell[6] example, cell[8] coverage
107
+ print cell[2] "\t" cell[4] "\t" cell[5] "\t" cell[6] "\t" cell[8]
108
+ }
109
+ ' "$MATRIX" >"$work/rows.tsv"
110
+
111
+ row_count="$(wc -l <"$work/rows.tsv" | tr -d ' ')"
112
+ if [[ "$row_count" -lt 50 ]]; then
113
+ fail "only $row_count rows parsed from $MATRIX — has the table format changed?"
114
+ exit 1
115
+ fi
116
+
117
+ # The command cell is "dot <command>"; a row may name a subcommand
118
+ # ("dot fleet apply"), so the routable command is the first word after "dot".
119
+ cut -f1 "$work/rows.tsv" | sed 's/^dot *//' | awk '{print $1}' |
120
+ sort -u >"$work/matrix-commands.txt"
121
+ cut -f2 "$work/rows.tsv" | sort -u | grep -v '^$' >"$work/matrix-tests.txt"
122
+ cut -f3 "$work/rows.tsv" | sort -u | grep -v '^$' >"$work/matrix-benches.txt"
123
+ cut -f4 "$work/rows.tsv" | sort -u | grep -v '^$' >"$work/matrix-examples.txt"
124
+
125
+ say "Feature matrix: $row_count rows, $(wc -l <"$work/matrix-commands.txt" | tr -d ' ') distinct commands"
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # 1. Command coverage
129
+ # ---------------------------------------------------------------------------
130
+
131
+ # bin/dot's route table is the authority on what the dispatcher can reach.
132
+ # Flag aliases (--help/-h/--version/-v) are not commands and are excluded.
133
+ awk '/^_dot_command_routes\(\)/,/^EOF$/' "$DOT_BIN" |
134
+ awk -F'|' '/^[a-z][a-z0-9-]*\|[a-z]+$/ { print $1 }' |
135
+ sort -u >"$work/routed.txt"
136
+
137
+ # The generated command index is the documented surface.
138
+ grep -oE '^\| `dot [a-z][a-z0-9-]*' "$COMMAND_INDEX" |
139
+ sed 's/^| `dot //' | sort -u >"$work/indexed.txt"
140
+
141
+ missing_routed="$(comm -23 "$work/routed.txt" "$work/matrix-commands.txt")"
142
+ if [[ -n "$missing_routed" ]]; then
143
+ while IFS= read -r cmd; do
144
+ [[ -n "$cmd" ]] || continue
145
+ fail "routable command has no FEATURE-MATRIX row: dot $cmd"
146
+ done <<<"$missing_routed"
147
+ else
148
+ say " ✓ every routable command has a row ($(wc -l <"$work/routed.txt" | tr -d ' ') commands)"
149
+ fi
150
+
151
+ missing_indexed="$(comm -23 "$work/indexed.txt" "$work/matrix-commands.txt")"
152
+ if [[ -n "$missing_indexed" ]]; then
153
+ while IFS= read -r cmd; do
154
+ [[ -n "$cmd" ]] || continue
155
+ fail "command documented in $COMMAND_INDEX has no FEATURE-MATRIX row: dot $cmd"
156
+ done <<<"$missing_indexed"
157
+ else
158
+ say " ✓ every documented command has a row ($(wc -l <"$work/indexed.txt" | tr -d ' ') commands)"
159
+ fi
160
+
161
+ # A row naming a command the dispatcher cannot route is a phantom: the
162
+ # feature was removed but its coverage claim was not.
163
+ phantom="$(comm -13 <(sort -u "$work/routed.txt" "$work/indexed.txt") \
164
+ "$work/matrix-commands.txt")"
165
+ if [[ -n "$phantom" ]]; then
166
+ while IFS= read -r cmd; do
167
+ [[ -n "$cmd" ]] || continue
168
+ fail "FEATURE-MATRIX row names a command that is neither routable nor documented: dot $cmd"
169
+ done <<<"$phantom"
170
+ else
171
+ say " ✓ no phantom command rows"
172
+ fi
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # 2. Test coverage
176
+ # ---------------------------------------------------------------------------
177
+
178
+ # `|| true`: this runs at top level under `set -euo pipefail`, so without it an
179
+ # unmatched glob (or a grep that simply finds nothing) aborts the whole script
180
+ # with grep's status before the emptiness check below can run — exit 2 and not
181
+ # a word about why, instead of the diagnostic that check exists to print.
182
+ # shellcheck disable=SC2086
183
+ grep -ho '^test_fm_[a-z0-9_]*()' $TEST_GLOB 2>/dev/null |
184
+ sed 's/()//' | sort -u >"$work/defined-tests.txt" || true
185
+
186
+ if [[ ! -s "$work/defined-tests.txt" ]]; then
187
+ fail "no test functions found in $TEST_GLOB"
188
+ else
189
+ undefined="$(comm -23 "$work/matrix-tests.txt" "$work/defined-tests.txt")"
190
+ if [[ -n "$undefined" ]]; then
191
+ while IFS= read -r fn; do
192
+ [[ -n "$fn" ]] || continue
193
+ fail "FEATURE-MATRIX names a test function that does not exist: $fn"
194
+ done <<<"$undefined"
195
+ else
196
+ say " ✓ every named test function exists ($(wc -l <"$work/matrix-tests.txt" | tr -d ' ') referenced, $(wc -l <"$work/defined-tests.txt" | tr -d ' ') defined)"
197
+ fi
198
+
199
+ # A defined-but-unreferenced test is not an error — it may be a helper or a
200
+ # cross-cutting invariant — but it is worth surfacing so coverage does not
201
+ # quietly drift out of the table.
202
+ orphan_count="$(comm -13 "$work/matrix-tests.txt" "$work/defined-tests.txt" | grep -c . || true)"
203
+ if [[ "$orphan_count" -gt 0 ]]; then
204
+ say " · $orphan_count test function(s) defined but not referenced by any row"
205
+ fi
206
+ fi
207
+
208
+ # Every function the matrix names must also actually be CALLED by its file;
209
+ # a defined-but-never-invoked test passes vacuously.
210
+ uncalled=0
211
+ while IFS= read -r fn; do
212
+ [[ -n "$fn" ]] || continue
213
+ # shellcheck disable=SC2086
214
+ if ! grep -hqE "^[[:space:]]*${fn}([[:space:]]|$)" $TEST_GLOB 2>/dev/null; then
215
+ fail "test function is defined but never called: $fn"
216
+ uncalled=$((uncalled + 1))
217
+ fi
218
+ done <"$work/matrix-tests.txt"
219
+ [[ "$uncalled" -eq 0 ]] && say " ✓ every named test function is invoked by its file"
220
+
221
+ # ---------------------------------------------------------------------------
222
+ # 3. Benchmark coverage
223
+ # ---------------------------------------------------------------------------
224
+
225
+ if bash "$BENCH" --list-ids >"$work/bench-ids.txt" 2>/dev/null &&
226
+ [[ -s "$work/bench-ids.txt" ]]; then
227
+ sort -u -o "$work/bench-ids.txt" "$work/bench-ids.txt"
228
+ unknown_bench="$(comm -23 "$work/matrix-benches.txt" "$work/bench-ids.txt")"
229
+ if [[ -n "$unknown_bench" ]]; then
230
+ while IFS= read -r id; do
231
+ [[ -n "$id" ]] || continue
232
+ fail "FEATURE-MATRIX names a benchmark id the harness does not produce: $id"
233
+ done <<<"$unknown_bench"
234
+ else
235
+ say " ✓ every named benchmark id exists ($(wc -l <"$work/matrix-benches.txt" | tr -d ' ') referenced, $(wc -l <"$work/bench-ids.txt" | tr -d ' ') available)"
236
+ fi
237
+
238
+ # Cold-start coverage must be total: one help benchmark per routable command.
239
+ missing_bench=0
240
+ while IFS= read -r cmd; do
241
+ [[ -n "$cmd" ]] || continue
242
+ if ! grep -qx "help:$cmd" "$work/bench-ids.txt"; then
243
+ fail "no cold-start benchmark for routable command: dot $cmd"
244
+ missing_bench=$((missing_bench + 1))
245
+ fi
246
+ done <"$work/routed.txt"
247
+ [[ "$missing_bench" -eq 0 ]] &&
248
+ say " ✓ every routable command has a cold-start benchmark"
249
+ else
250
+ fail "could not list benchmark ids: $BENCH --list-ids"
251
+ fi
252
+
253
+ # ---------------------------------------------------------------------------
254
+ # 4. Example coverage
255
+ # ---------------------------------------------------------------------------
256
+
257
+ missing_example=0
258
+ while IFS= read -r path; do
259
+ [[ -n "$path" ]] || continue
260
+ if [[ ! -f "$path" ]]; then
261
+ fail "FEATURE-MATRIX names an example that does not exist: $path"
262
+ missing_example=$((missing_example + 1))
263
+ elif [[ "$path" != examples/*.sh ]]; then
264
+ fail "example is outside the directory validate-examples.sh runs: $path"
265
+ missing_example=$((missing_example + 1))
266
+ fi
267
+ done <"$work/matrix-examples.txt"
268
+ [[ "$missing_example" -eq 0 ]] &&
269
+ say " ✓ every named example exists ($(wc -l <"$work/matrix-examples.txt" | tr -d ' ') referenced)"
270
+
271
+ # Every command module should be demonstrated by at least one example.
272
+ missing_group=0
273
+ for module in scripts/dot/commands/*.sh; do
274
+ [[ -f "$module" ]] || continue
275
+ name="$(basename "$module" .sh)"
276
+ if ! grep -rqlE "scripts/dot/commands/${name}\.sh" examples/ 2>/dev/null; then
277
+ fail "no example references the command module: $module"
278
+ missing_group=$((missing_group + 1))
279
+ fi
280
+ done
281
+ [[ "$missing_group" -eq 0 ]] &&
282
+ say " ✓ every command module is referenced by an example"
283
+
284
+ # ---------------------------------------------------------------------------
285
+ # Verdict
286
+ # ---------------------------------------------------------------------------
287
+
288
+ if [[ "$failures" -ne 0 ]]; then
289
+ printf '\nFEATURE-MATRIX drift: %s problem(s).\n' "$failures" >&2
290
+ printf 'Add or correct the row(s) in %s, then re-run this check.\n' "$MATRIX" >&2
291
+ exit 1
292
+ fi
293
+
294
+ say ""
295
+ say "FEATURE-MATRIX is in sync."
296
+ exit 0