@sebastienrousseau/dotfiles 0.2.520 → 0.2.522

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 (323) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE-APACHE +190 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +43 -37
  5. package/install.sh +76 -10
  6. package/package.json +7 -7
  7. package/tools/README.md +49 -0
  8. package/tools/ci/install-chezmoi-verified.sh +68 -0
  9. package/docs/.vitepress/reports/localization-readability-audit.md +0 -73
  10. package/docs/AI.md +0 -179
  11. package/docs/ARCHITECTURE.md +0 -117
  12. package/docs/CNAME +0 -1
  13. package/docs/CONFIG_STRATEGY.md +0 -124
  14. package/docs/COPYRIGHT +0 -7
  15. package/docs/ECOSYSTEM.md +0 -220
  16. package/docs/GOLD-STANDARD-AUDIT.md +0 -352
  17. package/docs/GOVERNANCE.md +0 -98
  18. package/docs/MAINTAINERS.md +0 -41
  19. package/docs/MINIMUM-TOOLCHAIN.md +0 -100
  20. package/docs/NAMING_CONVENTIONS.md +0 -102
  21. package/docs/OPENCODE.md +0 -127
  22. package/docs/README.md +0 -84
  23. package/docs/STRUCTURE.md +0 -102
  24. package/docs/adr/ADR-001-ci-cd-pipeline.md +0 -118
  25. package/docs/adr/ADR-002-shell-performance.md +0 -130
  26. package/docs/adr/ADR-003-security-first.md +0 -158
  27. package/docs/adr/ADR-004-cli-architecture.md +0 -171
  28. package/docs/adr/ADR-005-chezmoi-choice.md +0 -99
  29. package/docs/adr/ADR-006-shell-selection.md +0 -124
  30. package/docs/adr/ADR-007-multi-shell-parity.md +0 -62
  31. package/docs/adr/ADR-008-alias-system-architecture.md +0 -95
  32. package/docs/adr/ADR-009-wallpaper-driven-theming.md +0 -131
  33. package/docs/adr/ADR-010-starship-transient-prompt.md +0 -144
  34. package/docs/adr/ADR-011-nushell-tier3-keep.md +0 -144
  35. package/docs/adr/ADR-012-ai-fleet-local-proxy.md +0 -79
  36. package/docs/adr/README.md +0 -40
  37. package/docs/architecture/AI_COST_OPTIMIZATION.md +0 -144
  38. package/docs/architecture/ARCHITECTURE.md +0 -20
  39. package/docs/architecture/INTEROP.md +0 -44
  40. package/docs/architecture/REPO_LAYOUT.md +0 -241
  41. package/docs/architecture/WALKTHROUGH.md +0 -86
  42. package/docs/architecture/fleet-deployment.md +0 -77
  43. package/docs/archive/EUXIS_2026_REVIEW.md +0 -127
  44. package/docs/archive/LEGACY_ROADMAP.md +0 -6
  45. package/docs/archive/MILESTONE_v0.2.493.md +0 -47
  46. package/docs/archive/PLAN.md +0 -199
  47. package/docs/archive/REPO_AUDIT.md +0 -31
  48. package/docs/articles/.pages +0 -6
  49. package/docs/articles/2026-07-05-custom-mkdocs-material-dark-theme.md +0 -216
  50. package/docs/articles/2026-07-05-fish-startup-abbr.md +0 -153
  51. package/docs/articles/2026-07-05-master-to-main-rename-runbook.md +0 -128
  52. package/docs/articles/index.md +0 -36
  53. package/docs/guides/INSTALL.md +0 -144
  54. package/docs/guides/MACOS_ICLOUD_SYMLINKS.md +0 -121
  55. package/docs/guides/NEOVIM_IDE_GUIDE.md +0 -61
  56. package/docs/guides/THEMING.md +0 -230
  57. package/docs/guides/TROUBLESHOOTING.md +0 -176
  58. package/docs/guides/WSL2_NIX_TROUBLESHOOTING.md +0 -792
  59. package/docs/index.md +0 -132
  60. package/docs/interop/A2A.md +0 -39
  61. package/docs/interop/POWERSHELL.md +0 -102
  62. package/docs/manual/00-introduction.md +0 -89
  63. package/docs/manual/01-concepts/01-architecture.md +0 -138
  64. package/docs/manual/01-concepts/02-trust-model.md +0 -183
  65. package/docs/manual/01-concepts/03-theme-engine.md +0 -186
  66. package/docs/manual/01-concepts/04-fleet.md +0 -148
  67. package/docs/manual/01-concepts/05-self-healing.md +0 -204
  68. package/docs/manual/02-tutorials/01-first-install.md +0 -197
  69. package/docs/manual/02-tutorials/02-add-wallpaper.md +0 -216
  70. package/docs/manual/02-tutorials/03-create-profile.md +0 -244
  71. package/docs/manual/02-tutorials/04-encrypt-secret.md +0 -281
  72. package/docs/manual/02-tutorials/05-deploy-fleet.md +0 -283
  73. package/docs/manual/03-reference/01-dot-cli.md +0 -475
  74. package/docs/manual/03-reference/02-config-files.md +0 -265
  75. package/docs/manual/03-reference/03-environment.md +0 -124
  76. package/docs/manual/03-reference/04-templates.md +0 -190
  77. package/docs/manual/03-reference/05-feature-flags.md +0 -187
  78. package/docs/manual/04-cookbook/01-recipes.md +0 -285
  79. package/docs/manual/04-cookbook/02-troubleshooting.md +0 -351
  80. package/docs/manual/04-cookbook/03-faq.md +0 -175
  81. package/docs/manual/05-appendices/A-platform-matrix.md +0 -101
  82. package/docs/manual/05-appendices/B-security-checklist.md +0 -85
  83. package/docs/manual/05-appendices/C-glossary.md +0 -40
  84. package/docs/manual/05-appendices/D-bibliography.md +0 -58
  85. package/docs/manual/05-appendices/E-license.md +0 -38
  86. package/docs/manual/_toc.yml +0 -58
  87. package/docs/manual/command-index.md +0 -175
  88. package/docs/manual/concept-index.md +0 -170
  89. package/docs/manual/index.md +0 -66
  90. package/docs/migration/README.md +0 -81
  91. package/docs/migration/from-bare-git-repo.md +0 -156
  92. package/docs/migration/from-gnu-stow.md +0 -165
  93. package/docs/migration/from-plain-chezmoi.md +0 -148
  94. package/docs/migration/from-yadm.md +0 -187
  95. package/docs/operations/ARCHITECTURE_ROADMAP.md +0 -7
  96. package/docs/operations/ATTESTATION.md +0 -44
  97. package/docs/operations/CI_CADENCE.md +0 -107
  98. package/docs/operations/CI_COMPOSITES.md +0 -156
  99. package/docs/operations/COMPLETIONS.md +0 -123
  100. package/docs/operations/COVERAGE.md +0 -204
  101. package/docs/operations/DRIFT.md +0 -107
  102. package/docs/operations/HARD_AUDIT_2026.md +0 -631
  103. package/docs/operations/MAINTENANCE.md +0 -63
  104. package/docs/operations/MANIFEST.md +0 -127
  105. package/docs/operations/MIGRATION.md +0 -109
  106. package/docs/operations/OPERATIONS.md +0 -188
  107. package/docs/operations/PERFORMANCE.md +0 -133
  108. package/docs/operations/PERFORMANCE_BUDGETS.md +0 -196
  109. package/docs/operations/REGISTRY.md +0 -90
  110. package/docs/operations/RELEASE_PIPELINE.md +0 -128
  111. package/docs/operations/RELIABILITY.md +0 -122
  112. package/docs/operations/RFC_v0_2_503_reorganization.md +0 -280
  113. package/docs/operations/ROADMAP.md +0 -10
  114. package/docs/operations/ROADMAP_2026.md +0 -7
  115. package/docs/operations/ROADMAP_V0_2_503.md +0 -10
  116. package/docs/operations/TESTING.md +0 -216
  117. package/docs/operations/TRACEABILITY.md +0 -44
  118. package/docs/operations/TRUSTED_AGENT_WORKSTATION.md +0 -65
  119. package/docs/operations/VERSION_SYNC.md +0 -393
  120. package/docs/packaging.md +0 -222
  121. package/docs/reference/ALIASES.md +0 -131
  122. package/docs/reference/ALIASES_CHEATSHEET.md +0 -32
  123. package/docs/reference/ALIASES_DEPRECATIONS.md +0 -13
  124. package/docs/reference/FEATURE-MATRIX.md +0 -646
  125. package/docs/reference/FEATURES.md +0 -66
  126. package/docs/reference/FONTS.md +0 -112
  127. package/docs/reference/POWERSHELL_PARITY.md +0 -82
  128. package/docs/reference/PROFILES.md +0 -69
  129. package/docs/reference/SCREENSHOTS.md +0 -121
  130. package/docs/reference/SCRIPTS.md +0 -71
  131. package/docs/reference/SUPPORT_MATRIX.md +0 -80
  132. package/docs/reference/THEMES.md +0 -117
  133. package/docs/reference/TOOLS.md +0 -110
  134. package/docs/reference/UTILS.md +0 -243
  135. package/docs/registry.json +0 -6
  136. package/docs/schema/dot-env-v1.json +0 -110
  137. package/docs/schema/dot-registry-v1.json +0 -33
  138. package/docs/security/AI_ACT_COMPLIANCE.md +0 -94
  139. package/docs/security/AUDIT_BYPASS.md +0 -103
  140. package/docs/security/AUTOMATION_SECRETS.md +0 -26
  141. package/docs/security/CI_EGRESS_ALLOWLIST.md +0 -127
  142. package/docs/security/CI_PINNING.md +0 -129
  143. package/docs/security/COMMIT_SIGNING.md +0 -138
  144. package/docs/security/COMPLIANCE.md +0 -458
  145. package/docs/security/DEPS_DEV_EXCEPTIONS.md +0 -86
  146. package/docs/security/DISCLOSURE.md +0 -130
  147. package/docs/security/ENCRYPTION.md +0 -57
  148. package/docs/security/FMEA.md +0 -159
  149. package/docs/security/FUZZING.md +0 -209
  150. package/docs/security/HISTORY_FILTERING.md +0 -132
  151. package/docs/security/INCIDENT_RESPONSE.md +0 -579
  152. package/docs/security/INSTALL_VERIFICATION.md +0 -122
  153. package/docs/security/KEYS.md +0 -49
  154. package/docs/security/KEY_ROTATION.md +0 -303
  155. package/docs/security/MCP_POLICY.md +0 -78
  156. package/docs/security/POLICY_RELEASES.md +0 -37
  157. package/docs/security/README.md +0 -28
  158. package/docs/security/SCORECARD.md +0 -195
  159. package/docs/security/SECRETS.md +0 -158
  160. package/docs/security/SECURITY.md +0 -45
  161. package/docs/security/SECURITY_CHECKLIST.md +0 -55
  162. package/docs/security/SHELL_EXEMPTIONS.md +0 -145
  163. package/docs/security/SOUP_REGISTER.md +0 -36
  164. package/docs/security/THREAT_MODEL.md +0 -130
  165. package/docs/security/VERIFICATION_VALIDATION.md +0 -228
  166. package/docs/security/VERIFY_RELEASE.md +0 -201
  167. package/docs/security/security-pubkey.asc +0 -15
  168. package/docs/stylesheets/extra.css +0 -444
  169. package/docs/themes/README.md +0 -10
  170. package/docs/themes/VISUAL_INTEGRITY_REPORT.md +0 -30
  171. package/docs/themes/hero-shot.svg +0 -78
  172. package/scripts/README.md +0 -123
  173. package/scripts/ci/check-copyright-headers.sh +0 -8
  174. package/scripts/ci/check-shell-preamble.sh +0 -8
  175. package/scripts/ci/guard-gitleaks-checkout.sh +0 -8
  176. package/scripts/demo/record.sh +0 -43
  177. package/scripts/diagnostics/a2a-conformance.sh +0 -163
  178. package/scripts/diagnostics/alias-governance.sh +0 -165
  179. package/scripts/diagnostics/aliases-cheatsheet.sh +0 -74
  180. package/scripts/diagnostics/aliases-manifest.sh +0 -77
  181. package/scripts/diagnostics/attest-verify.sh +0 -147
  182. package/scripts/diagnostics/benchmark.sh +0 -408
  183. package/scripts/diagnostics/conflicts.sh +0 -73
  184. package/scripts/diagnostics/doctor-unified.sh +0 -43
  185. package/scripts/diagnostics/doctor.sh +0 -797
  186. package/scripts/diagnostics/drift-dashboard.sh +0 -203
  187. package/scripts/diagnostics/health.sh +0 -656
  188. package/scripts/diagnostics/history-analysis.sh +0 -86
  189. package/scripts/diagnostics/mcp-doctor.sh +0 -582
  190. package/scripts/diagnostics/perf.sh +0 -453
  191. package/scripts/diagnostics/scorecard.sh +0 -120
  192. package/scripts/diagnostics/secret-governance.sh +0 -65
  193. package/scripts/diagnostics/security-score.sh +0 -467
  194. package/scripts/diagnostics/smoke-test.sh +0 -88
  195. package/scripts/diagnostics/snapshot.sh +0 -90
  196. package/scripts/diagnostics/verify.sh +0 -108
  197. package/scripts/diagnostics/verify_state.sh +0 -73
  198. package/scripts/diagnostics/version-locks.sh +0 -94
  199. package/scripts/diagnostics/workstation-attestation.sh +0 -212
  200. package/scripts/dot/commands/agent.sh +0 -535
  201. package/scripts/dot/commands/agents.sh +0 -352
  202. package/scripts/dot/commands/ai.sh +0 -600
  203. package/scripts/dot/commands/aliases.sh +0 -277
  204. package/scripts/dot/commands/appearance.sh +0 -110
  205. package/scripts/dot/commands/completion.sh +0 -171
  206. package/scripts/dot/commands/core.sh +0 -217
  207. package/scripts/dot/commands/diagnostics.sh +0 -265
  208. package/scripts/dot/commands/env-emit.sh +0 -203
  209. package/scripts/dot/commands/fleet.sh +0 -711
  210. package/scripts/dot/commands/init.sh +0 -185
  211. package/scripts/dot/commands/lint.sh +0 -208
  212. package/scripts/dot/commands/manual.sh +0 -169
  213. package/scripts/dot/commands/meta.sh +0 -438
  214. package/scripts/dot/commands/patterns.sh +0 -55
  215. package/scripts/dot/commands/registry.sh +0 -455
  216. package/scripts/dot/commands/restore.sh +0 -232
  217. package/scripts/dot/commands/secrets.sh +0 -296
  218. package/scripts/dot/commands/security.sh +0 -102
  219. package/scripts/dot/commands/tools.sh +0 -570
  220. package/scripts/dot/data/alias-deprecations.tsv +0 -2
  221. package/scripts/dot/powershell/Dot.psm1 +0 -319
  222. package/scripts/fonts/install-nerd-fonts.sh +0 -75
  223. package/scripts/fonts/patch-fonts.sh +0 -36
  224. package/scripts/git-hooks/install.sh +0 -12
  225. package/scripts/git-hooks/pre-commit +0 -12
  226. package/scripts/git-hooks/pre-commit-audit.sh +0 -146
  227. package/scripts/git-hooks/pre-push +0 -105
  228. package/scripts/git-hooks/prepare-commit-msg +0 -29
  229. package/scripts/lib/secrets_provider.sh +0 -200
  230. package/scripts/nvim/headless-upgrade.lua +0 -81
  231. package/scripts/ops/ai-setup.sh +0 -71
  232. package/scripts/ops/bundle.sh +0 -104
  233. package/scripts/ops/chaos.sh +0 -50
  234. package/scripts/ops/chezmoi-apply.sh +0 -333
  235. package/scripts/ops/chezmoi-diff.sh +0 -16
  236. package/scripts/ops/chezmoi-remove.sh +0 -46
  237. package/scripts/ops/chezmoi-update.sh +0 -67
  238. package/scripts/ops/heal-chezmoi.sh +0 -87
  239. package/scripts/ops/heal-system.sh +0 -129
  240. package/scripts/ops/heal-tools.sh +0 -297
  241. package/scripts/ops/heal.sh +0 -223
  242. package/scripts/ops/post-apply-repair.sh +0 -107
  243. package/scripts/ops/prewarm.sh +0 -128
  244. package/scripts/ops/release.sh +0 -262
  245. package/scripts/ops/rollback.sh +0 -613
  246. package/scripts/ops/setup.sh +0 -138
  247. package/scripts/ops/teleport.sh +0 -34
  248. package/scripts/qa/check-feature-matrix.sh +0 -296
  249. package/scripts/qa/check-version-consistency.sh +0 -12
  250. package/scripts/qa/coverage-baseline.sh +0 -61
  251. package/scripts/qa/docs-coverage.sh +0 -118
  252. package/scripts/qa/examples-coverage.sh +0 -94
  253. package/scripts/qa/powershell-contract.ps1 +0 -95
  254. package/scripts/qa/reliability-audit.sh +0 -139
  255. package/scripts/qa/scorecard-snapshot.sh +0 -128
  256. package/scripts/qa/traceability-coverage.sh +0 -124
  257. package/scripts/qa/validate-examples.sh +0 -90
  258. package/scripts/qa/wsl-contract.sh +0 -12
  259. package/scripts/secrets/age-init.sh +0 -82
  260. package/scripts/secrets/create-secrets-file.sh +0 -46
  261. package/scripts/secrets/encrypt-ssh-key.sh +0 -44
  262. package/scripts/security/backup.sh +0 -58
  263. package/scripts/security/check-disclosure-key-expiry.sh +0 -111
  264. package/scripts/security/dns-doh.sh +0 -52
  265. package/scripts/security/encryption-check.sh +0 -55
  266. package/scripts/security/enforce-policies.sh +0 -552
  267. package/scripts/security/firewall.sh +0 -91
  268. package/scripts/security/lock-configs.sh +0 -67
  269. package/scripts/security/lock-screen.sh +0 -56
  270. package/scripts/security/manage-secrets.sh +0 -429
  271. package/scripts/security/ssh-cert.sh +0 -204
  272. package/scripts/security/telemetry-kill.sh +0 -51
  273. package/scripts/security/usb-safety.sh +0 -52
  274. package/scripts/theme/apply-gnome-theme.sh +0 -333
  275. package/scripts/theme/extract-heic-frames.sh +0 -115
  276. package/scripts/theme/extract-theme.py +0 -1020
  277. package/scripts/theme/install-boot-logo.sh +0 -63
  278. package/scripts/theme/install-catppuccin-themes.sh +0 -371
  279. package/scripts/theme/install-cursors.sh +0 -26
  280. package/scripts/theme/install-file-icons.sh +0 -27
  281. package/scripts/theme/install-grub-theme.sh +0 -62
  282. package/scripts/theme/install-lock-icon.sh +0 -31
  283. package/scripts/theme/merge-wallpaper.sh +0 -146
  284. package/scripts/theme/rebuild-themes.sh +0 -603
  285. package/scripts/theme/switch.sh +0 -476
  286. package/scripts/theme/wallpaper-rotate.sh +0 -137
  287. package/scripts/theme/wallpaper-sync.sh +0 -690
  288. package/scripts/tools/cmatrix.sh +0 -22
  289. package/scripts/tools/detect-collisions.py +0 -103
  290. package/scripts/tools/emoji-picker.sh +0 -49
  291. package/scripts/tools/figlet-banner.sh +0 -19
  292. package/scripts/tools/log-rotate.sh +0 -31
  293. package/scripts/tools/lolcat-wrap.sh +0 -20
  294. package/scripts/tools/pipes.sh +0 -49
  295. package/scripts/tuning/linux.sh +0 -186
  296. package/scripts/tuning/macos.sh +0 -56
  297. package/scripts/uninstall.sh +0 -86
  298. package/scripts/verify-release-versions +0 -156
  299. package/scripts/version-sync.sh +0 -714
  300. package/templates/chezmoi-data/geekom-a9.toml.example +0 -21
  301. package/templates/chezmoi-data/mac-m1.toml.example +0 -16
  302. package/templates/chezmoi-data/mac-t2-linux.toml.example +0 -21
  303. package/templates/chezmoi-data/surface-pro-7p.toml.example +0 -21
  304. package/templates/projects/go/.github/workflows/ci.yml +0 -31
  305. package/templates/projects/go/README.md +0 -7
  306. package/templates/projects/go/cmd/__PROJECT_NAME__/main.go +0 -8
  307. package/templates/projects/go/go.mod +0 -3
  308. package/templates/projects/go/go.sum +0 -0
  309. package/templates/projects/molecule/README.md +0 -7
  310. package/templates/projects/molecule/converge.yml +0 -7
  311. package/templates/projects/molecule/molecule.yml +0 -16
  312. package/templates/projects/node/.github/workflows/ci.yml +0 -30
  313. package/templates/projects/node/README.md +0 -7
  314. package/templates/projects/node/package-lock.json +0 -12
  315. package/templates/projects/node/package.json +0 -10
  316. package/templates/projects/node/src/index.js +0 -3
  317. package/templates/projects/packer/README.md +0 -15
  318. package/templates/projects/packer/main.pkr.hcl +0 -15
  319. package/templates/projects/python/.github/workflows/ci.yml +0 -34
  320. package/templates/projects/python/README.md +0 -7
  321. package/templates/projects/python/pyproject.toml +0 -25
  322. package/templates/projects/python/src/__PROJECT_NAME__/__init__.py +0 -2
  323. package/templates/projects/python/tests/test_basic.py +0 -3
@@ -1,711 +0,0 @@
1
- #!/usr/bin/env bash
2
- # SPDX-License-Identifier: Apache-2.0 OR MIT
3
- # Copyright (c) 2015-2026 Sebastien Rousseau
4
- # Dotfiles CLI - Fleet Commands
5
- # fleet status|nodes|drift|events|namespace
6
-
7
- set -euo pipefail
8
-
9
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
- # shellcheck source=../../../lib/dot/utils.sh
11
- source "$SCRIPT_DIR/../../../lib/dot/utils.sh"
12
- # shellcheck source=../../../lib/dot/log.sh
13
- source "$SCRIPT_DIR/../../../lib/dot/log.sh"
14
-
15
- dot_ui_command_banner "Fleet" "${1:-}"
16
-
17
- _FLEET_STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/fleet"
18
- _FLEET_EVENTS_FILE="$_FLEET_STATE_DIR/events.jsonl"
19
-
20
- _fleet_enabled() {
21
- local data_file
22
- data_file="$(resolve_chezmoi_source_dir)/.chezmoidata.toml"
23
- if [[ -f "$data_file" ]] && grep -q '^enabled = true' "$data_file" 2>/dev/null; then
24
- return 0
25
- fi
26
- return 1
27
- }
28
-
29
- _fleet_node_id() {
30
- local data_file node_id=""
31
- data_file="$(resolve_chezmoi_source_dir)/.chezmoidata.toml"
32
- if [[ -f "$data_file" ]]; then
33
- node_id="$(sed -n 's/^node_id = "\(.*\)"/\1/p' "$data_file" | head -1)"
34
- fi
35
- if [[ -z "$node_id" ]]; then
36
- node_id="$(hostname -s 2>/dev/null || echo "unknown")"
37
- fi
38
- printf '%s\n' "$node_id"
39
- }
40
-
41
- _fleet_namespace() {
42
- local data_file ns=""
43
- data_file="$(resolve_chezmoi_source_dir)/.chezmoidata.toml"
44
- if [[ -f "$data_file" ]]; then
45
- ns="$(sed -n 's/^namespace = "\(.*\)"/\1/p' "$data_file" | head -1)"
46
- fi
47
- printf '%s\n' "${ns:-default}"
48
- }
49
-
50
- _fleet_emit_event() {
51
- local event="$1" status="${2:-ok}"
52
- shift 2 || true
53
- local ts node_id namespace
54
- ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
55
- node_id="$(_fleet_node_id)"
56
- namespace="$(_fleet_namespace)"
57
- mkdir -p "$_FLEET_STATE_DIR" 2>/dev/null || return 0
58
- local payload
59
- payload=$(printf '{"time":"%s","event":"%s","status":"%s","node_id":"%s","namespace":"%s","trace_id":"%s"' \
60
- "$ts" "$event" "$status" "$node_id" "$namespace" "$DOT_TRACE_ID")
61
- while [[ $# -gt 0 ]]; do
62
- payload+=$(printf ',"%s":"%s"' "${1%%=*}" "${1#*=}")
63
- shift
64
- done
65
- payload+='}'
66
- printf '%s\n' "$payload" >>"$_FLEET_EVENTS_FILE" 2>/dev/null || true
67
-
68
- # Forward to endpoint if configured
69
- local endpoint=""
70
- local data_file
71
- data_file="$(resolve_chezmoi_source_dir)/.chezmoidata.toml"
72
- if [[ -f "$data_file" ]]; then
73
- endpoint="$(sed -n 's/^endpoint = "\(.*\)"/\1/p' "$data_file" | head -1)"
74
- fi
75
- if [[ -n "$endpoint" ]] && [[ "$endpoint" == https://* ]]; then
76
- curl --proto '=https' --tlsv1.2 -fsSL -X POST -H "Content-Type: application/json" \
77
- -d "$payload" "$endpoint" >/dev/null 2>&1 || true
78
- fi
79
- }
80
-
81
- cmd_fleet_status() {
82
- local json_mode=0
83
- [[ "${1:-}" == "--json" || "${1:-}" == "-j" ]] && json_mode=1
84
-
85
- local node_id namespace version os_type kernel shell_type
86
- node_id="$(_fleet_node_id)"
87
- namespace="$(_fleet_namespace)"
88
- version="$(dotfiles_version)"
89
- os_type="$(uname -s)"
90
- kernel="$(uname -r)"
91
- shell_type="${SHELL##*/}"
92
-
93
- local drift_status="clean"
94
- if has_command chezmoi; then
95
- local drift_output
96
- # --exclude=always: always-run scripts are pending by design, not drift.
97
- drift_output="$(chezmoi status --exclude=always 2>/dev/null || true)"
98
- if [[ -n "$drift_output" ]]; then
99
- drift_status="drifted"
100
- fi
101
- fi
102
-
103
- local last_apply=""
104
- local state_log="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/dot.log"
105
- if [[ -f "$state_log" ]]; then
106
- # `set -euo pipefail` at the top of this script kills the whole
107
- # command when grep matches nothing (rc=1). Wrap the pipeline so
108
- # `last_apply` cleanly becomes empty and the UI still renders.
109
- last_apply="$(grep 'apply' "$state_log" 2>/dev/null | tail -1 | sed -n 's/^\[\([^]]*\)\].*/\1/p' || true)"
110
- fi
111
-
112
- if [[ "$json_mode" -eq 1 ]]; then
113
- printf '{"node_id":"%s","namespace":"%s","version":"%s","os":"%s","kernel":"%s","shell":"%s","drift":"%s","last_apply":"%s"}\n' \
114
- "$node_id" "$namespace" "$version" "$os_type" "$kernel" "$shell_type" "$drift_status" "$last_apply"
115
- return 0
116
- fi
117
-
118
- ui_header "Fleet Node Status"
119
- echo ""
120
- ui_ok "Node ID" "$node_id"
121
- ui_ok "Namespace" "$namespace"
122
- ui_ok "Version" "v$version"
123
- ui_ok "OS" "$os_type $kernel"
124
- ui_ok "Shell" "$shell_type"
125
- if [[ "$drift_status" == "clean" ]]; then
126
- ui_ok "Drift" "$drift_status"
127
- else
128
- ui_warn "Drift" "$drift_status"
129
- fi
130
- if [[ -n "$last_apply" ]]; then
131
- ui_info "Last Apply" "$last_apply"
132
- fi
133
-
134
- _fleet_emit_event "status" "ok" "version=$version" "drift=$drift_status"
135
- }
136
-
137
- _DRIFT_HISTORY_FILE="$_FLEET_STATE_DIR/drift-history.jsonl"
138
-
139
- _fleet_drift_append_history() {
140
- local drift_output="$1"
141
- local ts
142
- ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
143
- mkdir -p "$_FLEET_STATE_DIR" 2>/dev/null || return 0
144
- if [[ -z "$drift_output" ]]; then
145
- printf '{"time":"%s","status":"clean","files":[]}\n' "$ts" >>"$_DRIFT_HISTORY_FILE" 2>/dev/null || true
146
- else
147
- local files_json
148
- files_json="$(printf '%s\n' "$drift_output" | awk '{print $NF}' | jq -R . | jq -s . 2>/dev/null || echo '[]')"
149
- printf '{"time":"%s","status":"drifted","files":%s}\n' "$ts" "$files_json" >>"$_DRIFT_HISTORY_FILE" 2>/dev/null || true
150
- fi
151
- }
152
-
153
- cmd_fleet_drift() {
154
- local subcommand="${1:-check}"
155
- if [[ "${1:-}" == --* ]] || [[ -z "${1:-}" ]]; then
156
- subcommand="check"
157
- else
158
- shift || true
159
- fi
160
-
161
- case "$subcommand" in
162
- check)
163
- ui_header "Fleet Drift Report"
164
- echo ""
165
-
166
- if ! has_command chezmoi; then
167
- ui_err "chezmoi" "not installed"
168
- return 1
169
- fi
170
-
171
- local drift_output
172
- drift_output="$(chezmoi status --exclude=always 2>/dev/null || true)"
173
-
174
- _fleet_drift_append_history "$drift_output"
175
-
176
- if [[ -z "$drift_output" ]]; then
177
- ui_ok "Status" "No drift detected"
178
- _fleet_emit_event "drift_check" "clean"
179
- return 0
180
- fi
181
-
182
- ui_warn "Status" "Configuration drift detected"
183
- echo ""
184
- printf '%s\n' "$drift_output" | while IFS= read -r line; do
185
- local change_type="${line:0:2}"
186
- local file_path="${line:3}"
187
- case "$change_type" in
188
- "MM" | "A " | " M")
189
- ui_warn "$change_type" "$file_path"
190
- ;;
191
- *)
192
- ui_info "$change_type" "$file_path"
193
- ;;
194
- esac
195
- done
196
-
197
- _fleet_emit_event "drift_check" "drifted" "count=$(echo "$drift_output" | wc -l | tr -d ' ')"
198
- ;;
199
- history)
200
- ui_header "Drift History"
201
- echo ""
202
- if [[ ! -f "$_DRIFT_HISTORY_FILE" ]]; then
203
- ui_info "No drift history recorded yet."
204
- return 0
205
- fi
206
- local count="${1:-20}"
207
- tail -n "$count" "$_DRIFT_HISTORY_FILE" | while IFS= read -r line; do
208
- local time status file_count
209
- # One jq per line (was three: .time, .status, .files|length).
210
- # `|| true`: on an unparsable line jq prints nothing, `read`
211
- # hits EOF and returns 1, and `set -e` would otherwise abort the
212
- # whole listing instead of rendering the `?` placeholders below.
213
- IFS=$'\t' read -r time status file_count < <(
214
- printf '%s' "$line" | jq -r '[.time, .status, (.files | length)] | @tsv' 2>/dev/null
215
- ) || true
216
- [[ -n "$time" ]] || time="?"
217
- [[ -n "$status" ]] || status="?"
218
- [[ -n "$file_count" ]] || file_count=0
219
- if [[ "$status" == "clean" ]]; then
220
- ui_ok "$time" "clean"
221
- else
222
- ui_warn "$time" "drifted ($file_count files)"
223
- fi
224
- done
225
- ;;
226
- predict)
227
- ui_header "Drift Prediction"
228
- echo ""
229
- if [[ ! -f "$_DRIFT_HISTORY_FILE" ]]; then
230
- ui_info "Not enough history for prediction."
231
- return 0
232
- fi
233
- # Simple heuristic: files that drifted in >50% of the last 10 checks
234
- local threshold=5
235
- jq -r '.files[]?' "$_DRIFT_HISTORY_FILE" | tail -n 1000 | sort | uniq -c | sort -rn | while read -r count file; do
236
- if [[ "$count" -ge "$threshold" ]]; then
237
- ui_warn "Likely to drift" "$file (drifted $count times recently)"
238
- fi
239
- done
240
- local total_checks
241
- total_checks="$(wc -l <"$_DRIFT_HISTORY_FILE" | tr -d ' ')"
242
- ui_info "History" "$total_checks checks recorded"
243
- ;;
244
- *)
245
- die "Usage: dot fleet drift [check|history|predict]"
246
- ;;
247
- esac
248
- }
249
-
250
- cmd_fleet_events() {
251
- local count="${1:-20}"
252
- if [[ ! -f "$_FLEET_EVENTS_FILE" ]]; then
253
- ui_info "No fleet events recorded yet."
254
- ui_info "Events file" "$_FLEET_EVENTS_FILE"
255
- return 0
256
- fi
257
-
258
- ui_header "Fleet Events (last $count)"
259
- echo ""
260
-
261
- if has_command jq; then
262
- tail -n "$count" "$_FLEET_EVENTS_FILE" | jq -r '"\(.time)\t\(.event)\t\(.status)\t\(.node_id)"' | while IFS=$'\t' read -r time event status node; do
263
- if [[ "$status" == "ok" || "$status" == "clean" ]]; then
264
- ui_ok "$event" "$time ($node)"
265
- else
266
- ui_warn "$event" "$time ($node)"
267
- fi
268
- done
269
- else
270
- tail -n "$count" "$_FLEET_EVENTS_FILE"
271
- fi
272
- }
273
-
274
- cmd_fleet_namespace() {
275
- local subcommand="${1:-show}"
276
- shift || true
277
-
278
- case "$subcommand" in
279
- show)
280
- local ns
281
- ns="$(_fleet_namespace)"
282
- ui_header "Fleet Namespace"
283
- ui_ok "Active" "$ns"
284
-
285
- local data_file
286
- data_file="$(resolve_chezmoi_source_dir)/.chezmoidata.toml"
287
- if [[ -f "$data_file" ]]; then
288
- echo ""
289
- ui_section "Available Namespaces"
290
- grep '^\[namespaces\.' "$data_file" | sed 's/\[namespaces\.\(.*\)\]/\1/' | while IFS= read -r name; do
291
- if [[ "$name" == "$ns" ]]; then
292
- ui_ok "$name" "[active]"
293
- else
294
- ui_info "$name" ""
295
- fi
296
- done
297
- fi
298
- ;;
299
- set)
300
- local new_ns="${1:-}"
301
- [[ -n "$new_ns" ]] || die "Usage: dot fleet namespace set <name>"
302
- validate_name "$new_ns" "namespace"
303
- local data_file
304
- data_file="$(resolve_chezmoi_source_dir)/.chezmoidata.toml"
305
- [[ -f "$data_file" ]] || die ".chezmoidata.toml not found: $data_file"
306
- # Atomic write: render into a tempfile + mv so concurrent
307
- # `dot fleet namespace set` callers can't corrupt the TOML.
308
- # Avoids `sed -i` portability dance (GNU `-i` vs BSD `-i ''`).
309
- local _tmp _rendered=0
310
- _tmp="$(mktemp "${data_file}.XXXXXX")" || die "Cannot create tempfile"
311
- # Explicit if/else instead of A && B || C — the latter (SC2015)
312
- # silently runs C when B itself fails, masking real mv errors.
313
- if grep -q "^namespace = " "$data_file"; then
314
- if sed "s/^namespace = \".*\"/namespace = \"$new_ns\"/" "$data_file" >"$_tmp"; then
315
- _rendered=1
316
- fi
317
- else
318
- # No key yet — the shipped .chezmoidata.toml has none, and rewriting
319
- # only when one already existed made `set` a silent no-op on a fresh
320
- # checkout while still reporting success. Insert it after the first
321
- # line: appending at the end would land the key inside whatever
322
- # [table] the file happens to end with, which TOML reads as a
323
- # different key entirely. `dot profile set` does the same for
324
- # `profile`.
325
- if awk -v ns="$new_ns" '
326
- NR == 1 { print; printf "namespace = \"%s\"\n", ns; inserted = 1; next }
327
- { print }
328
- END { if (!inserted) printf "namespace = \"%s\"\n", ns }
329
- ' "$data_file" >"$_tmp"; then
330
- _rendered=1
331
- fi
332
- fi
333
- if [[ "$_rendered" -eq 1 ]]; then
334
- if ! mv "$_tmp" "$data_file"; then
335
- rm -f "$_tmp"
336
- die "Failed to commit namespace update"
337
- fi
338
- else
339
- rm -f "$_tmp"
340
- die "Failed to render namespace update"
341
- fi
342
- ui_ok "Namespace" "Set to '$new_ns'. Run 'dot sync' to apply."
343
- _fleet_emit_event "namespace_set" "ok" "namespace=$new_ns"
344
- ;;
345
- *)
346
- die "Usage: dot fleet namespace [show|set <name>]"
347
- ;;
348
- esac
349
- }
350
-
351
- cmd_fleet_enforce() {
352
- local subcommand="${1:-status}"
353
- shift || true
354
-
355
- local repo_root
356
- repo_root="$(resolve_chezmoi_source_dir)"
357
- [[ -z "$repo_root" ]] && repo_root="$(resolve_source_dir)"
358
- local profiles_file="$repo_root/dot_config/dotfiles/agent-profiles.json"
359
-
360
- case "$subcommand" in
361
- status)
362
- if [[ ! -f "$profiles_file" ]]; then
363
- ui_err "Profiles" "agent-profiles.json not found"
364
- return 1
365
- fi
366
- local enforcement
367
- enforcement="$(jq -r '.rbac.enforcement // "advisory"' "$profiles_file")"
368
- ui_header "RBAC Enforcement"
369
- ui_ok "Mode" "$enforcement"
370
- ui_ok "Default role" "$(jq -r '.rbac.defaultRole // "developer"' "$profiles_file")"
371
- jq -r '.rbac.roles | to_entries[] | "\(.key)\t\(.value.allowedProfiles | join(", "))"' "$profiles_file" | while IFS=$'\t' read -r role profiles; do
372
- ui_info "$role" "$profiles"
373
- done
374
- ;;
375
- set)
376
- local mode="${1:-}"
377
- [[ -n "$mode" ]] || die "Usage: dot fleet enforce set <advisory|strict>"
378
- case "$mode" in
379
- advisory | strict) ;;
380
- *) die "Invalid enforcement mode: $mode (use advisory or strict)" ;;
381
- esac
382
- [[ -f "$profiles_file" ]] || die "agent-profiles.json not found"
383
- local tmp
384
- tmp="$(jq --arg mode "$mode" '.rbac.enforcement = $mode' "$profiles_file")"
385
- printf '%s\n' "$tmp" >"$profiles_file"
386
- ui_ok "Enforcement" "set to '$mode'"
387
- _fleet_emit_event "enforcement_set" "ok" "mode=$mode"
388
- ;;
389
- *)
390
- die "Usage: dot fleet enforce [status|set <advisory|strict>]"
391
- ;;
392
- esac
393
- }
394
-
395
- _fleet_hosts_file() {
396
- printf '%s\n' "${DOTFILES_FLEET_HOSTS:-$HOME/.config/dotfiles/fleet.toml}"
397
- }
398
-
399
- # Parse the hosts file. Format:
400
- # [hosts.laptop]
401
- # ssh = "user@laptop.local"
402
- # profile = "workstation"
403
- #
404
- # Echoes one record per line: "<name>\t<ssh-target>\t<profile>".
405
- _fleet_hosts_iter() {
406
- local f
407
- f="$(_fleet_hosts_file)"
408
- [[ -f "$f" ]] || return 0
409
- awk '
410
- BEGIN { name = ""; ssh = ""; profile = "" }
411
- /^\[hosts\./ {
412
- if (name != "") { printf "%s\t%s\t%s\n", name, ssh, profile }
413
- gsub(/[\[\]]/, "", $0); sub(/^hosts\./, "", $0); name = $0
414
- ssh = ""; profile = ""
415
- next
416
- }
417
- /^ssh[[:space:]]*=/ { sub(/^ssh[[:space:]]*=[[:space:]]*/, ""); gsub(/"/, ""); ssh = $0; next }
418
- /^profile[[:space:]]*=/ { sub(/^profile[[:space:]]*=[[:space:]]*/, ""); gsub(/"/, ""); profile = $0; next }
419
- END {
420
- if (name != "") { printf "%s\t%s\t%s\n", name, ssh, profile }
421
- }
422
- ' "$f"
423
- }
424
-
425
- # SSH-based "dot fleet apply" — push the local dotfiles state out to
426
- # each registered host. The §3 hero-feature: nobody else owns the
427
- # "Ansible for personal devices" niche.
428
- cmd_fleet_apply() {
429
- local dry_run=0 only_host="" cmd="" jobs=4 verify_hosts=0
430
- while [[ $# -gt 0 ]]; do
431
- case "$1" in
432
- --dry-run | -n)
433
- dry_run=1
434
- shift
435
- ;;
436
- --verify-hosts)
437
- # Pre-flight check that every host already has a known_hosts
438
- # entry, closing the TOFU window before any SSH connection.
439
- # R3 audit N4. Without this, accept-new is the default and a
440
- # first-connection MITM can seed an attacker key.
441
- verify_hosts=1
442
- shift
443
- ;;
444
- --host)
445
- only_host="$2"
446
- shift 2
447
- ;;
448
- --cmd)
449
- cmd="$2"
450
- shift 2
451
- ;;
452
- --jobs | -j)
453
- jobs="$2"
454
- shift 2
455
- ;;
456
- --help | -h)
457
- cat <<EOF
458
- Usage: dot fleet apply [--host <name>] [--cmd <shell>] [--dry-run] [--jobs <n>]
459
-
460
- Push dotfiles state to every host registered in:
461
- ${DOTFILES_FLEET_HOSTS:-\$HOME/.config/dotfiles/fleet.toml}
462
-
463
- Format of fleet.toml:
464
- [hosts.laptop]
465
- ssh = "user@laptop.local"
466
- profile = "workstation"
467
-
468
- Hostnames are validated against [A-Za-z0-9._@:+/-]+ before any SSH
469
- fan-out; invalid entries abort the apply.
470
-
471
- First-time SSH connections use StrictHostKeyChecking=accept-new (TOFU).
472
- If your threat model requires no TOFU window, pre-populate
473
- ~/.ssh/known_hosts before running this command.
474
-
475
- Behavior:
476
- By default each host runs: dot sync && dot doctor --quiet
477
- Override with --cmd "<shell>" to run an arbitrary command on every
478
- host (e.g. --cmd "uptime").
479
-
480
- WARNING: --cmd is the trust boundary. Whatever string you pass
481
- executes on every remote host with the credentials your SSH key
482
- carries. Verify the command before running.
483
-
484
- Flags:
485
- --host <name> Apply to a single host only.
486
- --cmd <shell> Run a custom command instead of 'dot sync'.
487
- --dry-run, -n Print resolved hosts + planned command; don't SSH.
488
- --jobs <n> Parallelism (default 4).
489
- --verify-hosts Refuse to open any SSH connection unless every
490
- target host already has a key in ~/.ssh/known_hosts.
491
- Use when your threat model excludes the TOFU window.
492
- EOF
493
- return 0
494
- ;;
495
- *)
496
- ui_err "Unknown arg" "$1"
497
- return 1
498
- ;;
499
- esac
500
- done
501
-
502
- local hosts_file
503
- hosts_file="$(_fleet_hosts_file)"
504
- if [[ ! -f "$hosts_file" ]]; then
505
- ui_err "Fleet" "no hosts file at $hosts_file"
506
- ui_info "Hint" "create it with stanzas like '[hosts.laptop]\\nssh = \"user@laptop.local\"'"
507
- return 1
508
- fi
509
-
510
- local entries
511
- entries="$(_fleet_hosts_iter)"
512
- if [[ -z "$entries" ]]; then
513
- ui_err "Fleet" "hosts file is empty: $hosts_file"
514
- return 1
515
- fi
516
- if [[ -n "$only_host" ]]; then
517
- entries="$(printf '%s\n' "$entries" | awk -F'\t' -v h="$only_host" '$1 == h')"
518
- [[ -n "$entries" ]] || {
519
- ui_err "Fleet" "host not found: $only_host"
520
- return 1
521
- }
522
- fi
523
-
524
- local default_cmd='dot sync && dot doctor --quiet'
525
- local effective_cmd="${cmd:-$default_cmd}"
526
-
527
- ui_header "Fleet apply"
528
- ui_info "Hosts file" "$hosts_file"
529
- ui_info "Command" "$effective_cmd"
530
- ui_info "Parallel" "$jobs"
531
-
532
- if [[ "$dry_run" -eq 1 ]]; then
533
- printf '%s\n' "$entries" | while IFS=$'\t' read -r name ssh profile; do
534
- ui_info "$name" "$ssh profile=$profile cmd=$effective_cmd"
535
- done
536
- ui_ok "Dry-run" "no SSH connections opened"
537
- return 0
538
- fi
539
-
540
- if ! command -v ssh >/dev/null 2>&1; then
541
- ui_err "ssh" "not installed"
542
- return 127
543
- fi
544
-
545
- local total=0 ok=0 fail=0
546
- local tmpdir
547
- # `-t` template includes PID + random, so two concurrent `dot fleet
548
- # apply` invocations from the same user can't collide on $tmpdir.
549
- tmpdir="$(mktemp -d -t dotfiles-fleet.XXXXXX)"
550
- # Capture tmpdir's value at trap-definition time (via the eval-on-
551
- # define `printf -v`), NOT at trap-fire time. A naive
552
- # `trap 'rm -rf "$tmpdir"' RETURN` is unsafe under set -u because
553
- # `local tmpdir` is destroyed before the RETURN trap evaluates.
554
- # The SC2064 warning ("Use single quotes, otherwise this expands now
555
- # rather than when signalled") is exactly the behaviour we want here —
556
- # we explicitly want eager expansion. Suppress per-line.
557
- local _cleanup
558
- printf -v _cleanup 'rm -rf %q' "$tmpdir"
559
- # shellcheck disable=SC2064
560
- trap "$_cleanup" RETURN
561
-
562
- # Validate every hostname against a conservative regex BEFORE fan-out.
563
- # `user@host:port` characters only — refuses single quotes, backticks,
564
- # `$()`, semicolons, spaces, any shell metacharacter. Closes the
565
- # round-2 audit's hostname-injection finding.
566
- while IFS=$'\t' read -r name ssh profile; do
567
- [[ -n "$name" ]] || continue
568
- if [[ ! "$ssh" =~ ^[a-zA-Z0-9._@:+/-]+$ ]]; then
569
- ui_err "$name" "invalid ssh target ($ssh) — only [a-zA-Z0-9._@:+/-] allowed"
570
- return 1
571
- fi
572
- done <<<"$entries"
573
-
574
- # --verify-hosts: refuse the apply when any target host is missing
575
- # from ~/.ssh/known_hosts. Closes the R3 audit N4 TOFU-window gap.
576
- if ((verify_hosts == 1)); then
577
- local known_hosts="${HOME}/.ssh/known_hosts"
578
- if [[ ! -f "$known_hosts" ]]; then
579
- ui_err "verify-hosts" "no $known_hosts — populate before --verify-hosts"
580
- return 1
581
- fi
582
- local unknown_count=0
583
- while IFS=$'\t' read -r name ssh profile; do
584
- [[ -n "$name" && -n "$ssh" ]] || continue
585
- # Strip `user@` prefix and `:port` suffix for the lookup.
586
- local hostpart="${ssh#*@}"
587
- hostpart="${hostpart%%:*}"
588
- if ! ssh-keygen -F "$hostpart" -f "$known_hosts" >/dev/null 2>&1; then
589
- ui_err "$name" "no known_hosts entry for $hostpart — would TOFU on first connect"
590
- unknown_count=$((unknown_count + 1))
591
- fi
592
- done <<<"$entries"
593
- if ((unknown_count > 0)); then
594
- ui_err "verify-hosts" "$unknown_count host(s) missing from known_hosts — aborting"
595
- return 1
596
- fi
597
- ui_ok "verify-hosts" "all hosts found in known_hosts"
598
- fi
599
-
600
- # Run one SSH per host, parallelised via background jobs with a
601
- # semaphore. We DO NOT use `xargs -d` because that flag is GNU-only
602
- # and the §3 hero feature must work on macOS BSD xargs too. Also
603
- # avoids embedding `{}` substitution into a `bash -c` (the previous
604
- # implementation had a quoting hazard around TOML hostnames).
605
- _fleet_apply_one() {
606
- local _name="$1" _ssh="$2" _cmd="$3" _tmp="$4"
607
- if ssh -o BatchMode=yes -o ConnectTimeout=10 \
608
- -o StrictHostKeyChecking=accept-new \
609
- "$_ssh" "$_cmd" </dev/null \
610
- >"$_tmp/$_name.out" 2>"$_tmp/$_name.err"; then
611
- printf 'ok\n' >"$_tmp/$_name.status"
612
- else
613
- printf 'fail %d\n' "$?" >"$_tmp/$_name.status"
614
- fi
615
- }
616
-
617
- # Throttle to `jobs` concurrent workers. `wait -n` (wait for the next
618
- # job to finish) is bash 4.3+, but macOS ships bash 3.2 — there it
619
- # silently fails and the throttle collapses to unbounded parallelism.
620
- # Track PIDs and block on the oldest when at capacity instead; works on
621
- # bash 3.2 and 4+ alike.
622
- local _pids=()
623
- while IFS=$'\t' read -r name ssh profile; do
624
- [[ -n "$name" && -n "$ssh" ]] || continue
625
- total=$((total + 1))
626
- while ((${#_pids[@]} >= jobs)); do
627
- wait "${_pids[0]}" 2>/dev/null || true
628
- _pids=("${_pids[@]:1}")
629
- done
630
- _fleet_apply_one "$name" "$ssh" "$effective_cmd" "$tmpdir" &
631
- _pids+=("$!")
632
- done <<<"$entries"
633
- wait
634
-
635
- # `while < <(printf ...)` instead of `printf ... | while` — the
636
- # pipe form runs the loop body in a subshell, so the ok/fail
637
- # counters never propagate back to the parent. Caught by
638
- # tests/unit/fleet/test_fleet_apply_mocked_ssh.sh which exercised
639
- # the full apply path (the dry-run test missed this).
640
- while IFS=$'\t' read -r name ssh profile; do
641
- [[ -n "$name" ]] || continue
642
- if [[ -s "$tmpdir/$name.status" ]] && head -1 "$tmpdir/$name.status" | grep -q '^ok'; then
643
- ui_ok "$name" "$ssh"
644
- ok=$((ok + 1))
645
- else
646
- local err_summary=""
647
- [[ -s "$tmpdir/$name.err" ]] && err_summary=" — $(head -1 "$tmpdir/$name.err")"
648
- ui_err "$name" "$ssh${err_summary}"
649
- fail=$((fail + 1))
650
- fi
651
- local _evt_status="unknown"
652
- [[ -s "$tmpdir/$name.status" ]] && _evt_status="$(head -1 "$tmpdir/$name.status")"
653
- _fleet_emit_event "apply" "$_evt_status" "host=$name" "cmd=$effective_cmd"
654
- done < <(printf '%s\n' "$entries")
655
-
656
- ui_info "Summary" "$ok ok / $fail failed / $total total"
657
- [[ "$fail" -eq 0 ]]
658
- }
659
-
660
- cmd_fleet() {
661
- local subcommand="${1:-status}"
662
- if [[ "${1:-}" == --* ]] || [[ -z "${1:-}" ]]; then
663
- subcommand="status"
664
- else
665
- shift || true
666
- fi
667
-
668
- case "$subcommand" in
669
- status)
670
- cmd_fleet_status "$@"
671
- ;;
672
- drift)
673
- cmd_fleet_drift "$@"
674
- ;;
675
- events)
676
- cmd_fleet_events "$@"
677
- ;;
678
- namespace | ns)
679
- cmd_fleet_namespace "$@"
680
- ;;
681
- enforce)
682
- cmd_fleet_enforce "$@"
683
- ;;
684
- apply | push)
685
- cmd_fleet_apply "$@"
686
- ;;
687
- *)
688
- ui_header "Fleet Commands"
689
- echo ""
690
- ui_info "Usage" "dot fleet [command]"
691
- echo ""
692
- ui_ok "status" "Show this node's fleet status (--json for machine output)"
693
- ui_ok "drift" "Check for configuration drift"
694
- ui_ok "events" "Show recent fleet events"
695
- ui_ok "namespace" "Show or set the active namespace"
696
- ui_ok "enforce" "Show or set RBAC enforcement mode (advisory|strict)"
697
- ui_ok "apply" "SSH out to every host in fleet.toml and run 'dot sync'"
698
- ;;
699
- esac
700
- }
701
-
702
- # Dispatch
703
- case "${1:-}" in
704
- fleet)
705
- shift
706
- cmd_fleet "$@"
707
- ;;
708
- *)
709
- cmd_fleet "$@"
710
- ;;
711
- esac