@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
@@ -291,10 +291,21 @@ cmd_fleet_namespace() {
291
291
  local data_file
292
292
  data_file="$(resolve_source_dir)/.chezmoidata.toml"
293
293
  if grep -q "^namespace = " "$data_file" 2>/dev/null; then
294
- if sed --version >/dev/null 2>&1; then
295
- sed -i "s/^namespace = \".*\"/namespace = \"$new_ns\"/" "$data_file"
294
+ # Atomic write: render into a tempfile + mv so concurrent
295
+ # `dot fleet namespace set` callers can't corrupt the TOML.
296
+ # Avoids `sed -i` portability dance (GNU `-i` vs BSD `-i ''`).
297
+ local _tmp
298
+ _tmp="$(mktemp "${data_file}.XXXXXX")" || die "Cannot create tempfile"
299
+ # Explicit if/else instead of A && B || C — the latter (SC2015)
300
+ # silently runs C when B itself fails, masking real mv errors.
301
+ if sed "s/^namespace = \".*\"/namespace = \"$new_ns\"/" "$data_file" >"$_tmp"; then
302
+ if ! mv "$_tmp" "$data_file"; then
303
+ rm -f "$_tmp"
304
+ die "Failed to commit namespace update"
305
+ fi
296
306
  else
297
- sed -i '' "s/^namespace = \".*\"/namespace = \"$new_ns\"/" "$data_file"
307
+ rm -f "$_tmp"
308
+ die "Failed to render namespace update"
298
309
  fi
299
310
  fi
300
311
  ui_ok "Namespace" "Set to '$new_ns'. Run 'dot sync' to apply."
@@ -349,6 +360,266 @@ cmd_fleet_enforce() {
349
360
  esac
350
361
  }
351
362
 
363
+ _fleet_hosts_file() {
364
+ printf '%s\n' "${DOTFILES_FLEET_HOSTS:-$HOME/.config/dotfiles/fleet.toml}"
365
+ }
366
+
367
+ # Parse the hosts file. Format:
368
+ # [hosts.laptop]
369
+ # ssh = "user@laptop.local"
370
+ # profile = "workstation"
371
+ #
372
+ # Echoes one record per line: "<name>\t<ssh-target>\t<profile>".
373
+ _fleet_hosts_iter() {
374
+ local f
375
+ f="$(_fleet_hosts_file)"
376
+ [[ -f "$f" ]] || return 0
377
+ awk '
378
+ BEGIN { name = ""; ssh = ""; profile = "" }
379
+ /^\[hosts\./ {
380
+ if (name != "") { printf "%s\t%s\t%s\n", name, ssh, profile }
381
+ gsub(/[\[\]]/, "", $0); sub(/^hosts\./, "", $0); name = $0
382
+ ssh = ""; profile = ""
383
+ next
384
+ }
385
+ /^ssh[[:space:]]*=/ { sub(/^ssh[[:space:]]*=[[:space:]]*/, ""); gsub(/"/, ""); ssh = $0; next }
386
+ /^profile[[:space:]]*=/ { sub(/^profile[[:space:]]*=[[:space:]]*/, ""); gsub(/"/, ""); profile = $0; next }
387
+ END {
388
+ if (name != "") { printf "%s\t%s\t%s\n", name, ssh, profile }
389
+ }
390
+ ' "$f"
391
+ }
392
+
393
+ # SSH-based "dot fleet apply" — push the local dotfiles state out to
394
+ # each registered host. The §3 hero-feature: nobody else owns the
395
+ # "Ansible for personal devices" niche.
396
+ cmd_fleet_apply() {
397
+ local dry_run=0 only_host="" cmd="" jobs=4 verify_hosts=0
398
+ while [[ $# -gt 0 ]]; do
399
+ case "$1" in
400
+ --dry-run | -n)
401
+ dry_run=1
402
+ shift
403
+ ;;
404
+ --verify-hosts)
405
+ # Pre-flight check that every host already has a known_hosts
406
+ # entry, closing the TOFU window before any SSH connection.
407
+ # R3 audit N4. Without this, accept-new is the default and a
408
+ # first-connection MITM can seed an attacker key.
409
+ verify_hosts=1
410
+ shift
411
+ ;;
412
+ --host)
413
+ only_host="$2"
414
+ shift 2
415
+ ;;
416
+ --cmd)
417
+ cmd="$2"
418
+ shift 2
419
+ ;;
420
+ --jobs | -j)
421
+ jobs="$2"
422
+ shift 2
423
+ ;;
424
+ --help | -h)
425
+ cat <<EOF
426
+ Usage: dot fleet apply [--host <name>] [--cmd <shell>] [--dry-run] [--jobs <n>]
427
+
428
+ Push dotfiles state to every host registered in:
429
+ ${DOTFILES_FLEET_HOSTS:-\$HOME/.config/dotfiles/fleet.toml}
430
+
431
+ Format of fleet.toml:
432
+ [hosts.laptop]
433
+ ssh = "user@laptop.local"
434
+ profile = "workstation"
435
+
436
+ Hostnames are validated against [A-Za-z0-9._@:+/-]+ before any SSH
437
+ fan-out; invalid entries abort the apply.
438
+
439
+ First-time SSH connections use StrictHostKeyChecking=accept-new (TOFU).
440
+ If your threat model requires no TOFU window, pre-populate
441
+ ~/.ssh/known_hosts before running this command.
442
+
443
+ Behavior:
444
+ By default each host runs: dot sync && dot doctor --quiet
445
+ Override with --cmd "<shell>" to run an arbitrary command on every
446
+ host (e.g. --cmd "uptime").
447
+
448
+ WARNING: --cmd is the trust boundary. Whatever string you pass
449
+ executes on every remote host with the credentials your SSH key
450
+ carries. Verify the command before running.
451
+
452
+ Flags:
453
+ --host <name> Apply to a single host only.
454
+ --cmd <shell> Run a custom command instead of 'dot sync'.
455
+ --dry-run, -n Print resolved hosts + planned command; don't SSH.
456
+ --jobs <n> Parallelism (default 4).
457
+ --verify-hosts Refuse to open any SSH connection unless every
458
+ target host already has a key in ~/.ssh/known_hosts.
459
+ Use when your threat model excludes the TOFU window.
460
+ EOF
461
+ return 0
462
+ ;;
463
+ *)
464
+ ui_err "Unknown arg" "$1"
465
+ return 1
466
+ ;;
467
+ esac
468
+ done
469
+
470
+ local hosts_file
471
+ hosts_file="$(_fleet_hosts_file)"
472
+ if [[ ! -f "$hosts_file" ]]; then
473
+ ui_err "Fleet" "no hosts file at $hosts_file"
474
+ ui_info "Hint" "create it with stanzas like '[hosts.laptop]\\nssh = \"user@laptop.local\"'"
475
+ return 1
476
+ fi
477
+
478
+ local entries
479
+ entries="$(_fleet_hosts_iter)"
480
+ if [[ -z "$entries" ]]; then
481
+ ui_err "Fleet" "hosts file is empty: $hosts_file"
482
+ return 1
483
+ fi
484
+ if [[ -n "$only_host" ]]; then
485
+ entries="$(printf '%s\n' "$entries" | awk -F'\t' -v h="$only_host" '$1 == h')"
486
+ [[ -n "$entries" ]] || {
487
+ ui_err "Fleet" "host not found: $only_host"
488
+ return 1
489
+ }
490
+ fi
491
+
492
+ local default_cmd='dot sync && dot doctor --quiet'
493
+ local effective_cmd="${cmd:-$default_cmd}"
494
+
495
+ ui_header "Fleet apply"
496
+ ui_info "Hosts file" "$hosts_file"
497
+ ui_info "Command" "$effective_cmd"
498
+ ui_info "Parallel" "$jobs"
499
+
500
+ if [[ "$dry_run" -eq 1 ]]; then
501
+ printf '%s\n' "$entries" | while IFS=$'\t' read -r name ssh profile; do
502
+ ui_info "$name" "$ssh profile=$profile cmd=$effective_cmd"
503
+ done
504
+ ui_ok "Dry-run" "no SSH connections opened"
505
+ return 0
506
+ fi
507
+
508
+ if ! command -v ssh >/dev/null 2>&1; then
509
+ ui_err "ssh" "not installed"
510
+ return 127
511
+ fi
512
+
513
+ local total=0 ok=0 fail=0
514
+ local tmpdir
515
+ # `-t` template includes PID + random, so two concurrent `dot fleet
516
+ # apply` invocations from the same user can't collide on $tmpdir.
517
+ tmpdir="$(mktemp -d -t dotfiles-fleet.XXXXXX)"
518
+ # Capture tmpdir's value at trap-definition time (via the eval-on-
519
+ # define `printf -v`), NOT at trap-fire time. A naive
520
+ # `trap 'rm -rf "$tmpdir"' RETURN` is unsafe under set -u because
521
+ # `local tmpdir` is destroyed before the RETURN trap evaluates.
522
+ # The SC2064 warning ("Use single quotes, otherwise this expands now
523
+ # rather than when signalled") is exactly the behaviour we want here —
524
+ # we explicitly want eager expansion. Suppress per-line.
525
+ local _cleanup
526
+ printf -v _cleanup 'rm -rf %q' "$tmpdir"
527
+ # shellcheck disable=SC2064
528
+ trap "$_cleanup" RETURN
529
+
530
+ # Validate every hostname against a conservative regex BEFORE fan-out.
531
+ # `user@host:port` characters only — refuses single quotes, backticks,
532
+ # `$()`, semicolons, spaces, any shell metacharacter. Closes the
533
+ # round-2 audit's hostname-injection finding.
534
+ while IFS=$'\t' read -r name ssh profile; do
535
+ [[ -n "$name" ]] || continue
536
+ if [[ ! "$ssh" =~ ^[a-zA-Z0-9._@:+/-]+$ ]]; then
537
+ ui_err "$name" "invalid ssh target ($ssh) — only [a-zA-Z0-9._@:+/-] allowed"
538
+ return 1
539
+ fi
540
+ done <<<"$entries"
541
+
542
+ # --verify-hosts: refuse the apply when any target host is missing
543
+ # from ~/.ssh/known_hosts. Closes the R3 audit N4 TOFU-window gap.
544
+ if ((verify_hosts == 1)); then
545
+ local known_hosts="${HOME}/.ssh/known_hosts"
546
+ if [[ ! -f "$known_hosts" ]]; then
547
+ ui_err "verify-hosts" "no $known_hosts — populate before --verify-hosts"
548
+ return 1
549
+ fi
550
+ local unknown_count=0
551
+ while IFS=$'\t' read -r name ssh profile; do
552
+ [[ -n "$name" && -n "$ssh" ]] || continue
553
+ # Strip `user@` prefix and `:port` suffix for the lookup.
554
+ local hostpart="${ssh#*@}"
555
+ hostpart="${hostpart%%:*}"
556
+ if ! ssh-keygen -F "$hostpart" -f "$known_hosts" >/dev/null 2>&1; then
557
+ ui_err "$name" "no known_hosts entry for $hostpart — would TOFU on first connect"
558
+ unknown_count=$((unknown_count + 1))
559
+ fi
560
+ done <<<"$entries"
561
+ if ((unknown_count > 0)); then
562
+ ui_err "verify-hosts" "$unknown_count host(s) missing from known_hosts — aborting"
563
+ return 1
564
+ fi
565
+ ui_ok "verify-hosts" "all hosts found in known_hosts"
566
+ fi
567
+
568
+ # Run one SSH per host, parallelised via background jobs with a
569
+ # semaphore. We DO NOT use `xargs -d` because that flag is GNU-only
570
+ # and the §3 hero feature must work on macOS BSD xargs too. Also
571
+ # avoids embedding `{}` substitution into a `bash -c` (the previous
572
+ # implementation had a quoting hazard around TOML hostnames).
573
+ _fleet_apply_one() {
574
+ local _name="$1" _ssh="$2" _cmd="$3" _tmp="$4"
575
+ if ssh -o BatchMode=yes -o ConnectTimeout=10 \
576
+ -o StrictHostKeyChecking=accept-new \
577
+ "$_ssh" "$_cmd" </dev/null \
578
+ >"$_tmp/$_name.out" 2>"$_tmp/$_name.err"; then
579
+ printf 'ok\n' >"$_tmp/$_name.status"
580
+ else
581
+ printf 'fail %d\n' "$?" >"$_tmp/$_name.status"
582
+ fi
583
+ }
584
+
585
+ local running=0
586
+ while IFS=$'\t' read -r name ssh profile; do
587
+ [[ -n "$name" && -n "$ssh" ]] || continue
588
+ total=$((total + 1))
589
+ while ((running >= jobs)); do
590
+ wait -n 2>/dev/null || break
591
+ running=$((running - 1))
592
+ done
593
+ _fleet_apply_one "$name" "$ssh" "$effective_cmd" "$tmpdir" &
594
+ running=$((running + 1))
595
+ done <<<"$entries"
596
+ wait
597
+
598
+ # `while < <(printf ...)` instead of `printf ... | while` — the
599
+ # pipe form runs the loop body in a subshell, so the ok/fail
600
+ # counters never propagate back to the parent. Caught by
601
+ # tests/unit/fleet/test_fleet_apply_mocked_ssh.sh which exercised
602
+ # the full apply path (the dry-run test missed this).
603
+ while IFS=$'\t' read -r name ssh profile; do
604
+ [[ -n "$name" ]] || continue
605
+ if [[ -s "$tmpdir/$name.status" ]] && head -1 "$tmpdir/$name.status" | grep -q '^ok'; then
606
+ ui_ok "$name" "$ssh"
607
+ ok=$((ok + 1))
608
+ else
609
+ local err_summary=""
610
+ [[ -s "$tmpdir/$name.err" ]] && err_summary=" — $(head -1 "$tmpdir/$name.err")"
611
+ ui_err "$name" "$ssh${err_summary}"
612
+ fail=$((fail + 1))
613
+ fi
614
+ local _evt_status="unknown"
615
+ [[ -s "$tmpdir/$name.status" ]] && _evt_status="$(head -1 "$tmpdir/$name.status")"
616
+ _fleet_emit_event "apply" "$_evt_status" "host=$name" "cmd=$effective_cmd"
617
+ done < <(printf '%s\n' "$entries")
618
+
619
+ ui_info "Summary" "$ok ok / $fail failed / $total total"
620
+ [[ "$fail" -eq 0 ]]
621
+ }
622
+
352
623
  cmd_fleet() {
353
624
  local subcommand="${1:-status}"
354
625
  if [[ "${1:-}" == --* ]] || [[ -z "${1:-}" ]]; then
@@ -373,6 +644,9 @@ cmd_fleet() {
373
644
  enforce)
374
645
  cmd_fleet_enforce "$@"
375
646
  ;;
647
+ apply | push)
648
+ cmd_fleet_apply "$@"
649
+ ;;
376
650
  *)
377
651
  ui_header "Fleet Commands"
378
652
  echo ""
@@ -383,6 +657,7 @@ cmd_fleet() {
383
657
  ui_ok "events" "Show recent fleet events"
384
658
  ui_ok "namespace" "Show or set the active namespace"
385
659
  ui_ok "enforce" "Show or set RBAC enforcement mode (advisory|strict)"
660
+ ui_ok "apply" "SSH out to every host in fleet.toml and run 'dot sync'"
386
661
  ;;
387
662
  esac
388
663
  }
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env bash
2
+ # Copyright (c) 2015-2026 Dotfiles. All rights reserved.
3
+ # shellcheck shell=bash
4
+ #
5
+ # scripts/dot/commands/init.sh
6
+ #
7
+ # `dot init <github-user>` — bootstrap a foreign dotfiles repo through
8
+ # this framework's harness. The §3 audit roadmap calls for an analogue
9
+ # to `chezmoi init <user>` that turns this tool from "one person's
10
+ # config" into a runtime for any dotfiles user.
11
+ #
12
+ # Behavior:
13
+ # dot init alice # clones github.com/alice/dotfiles via chezmoi,
14
+ # # applies through the dot harness
15
+ # dot init https://... # explicit URL
16
+ # dot init alice --dry-run # preview without writing
17
+ # dot init alice --no-apply # clone only; don't run chezmoi apply
18
+ #
19
+ # Safety:
20
+ # - Refuses to clobber an existing chezmoi source dir without --force.
21
+ # - Disclaims that the target's scripts will run with the user's
22
+ # privileges and prints the source URL for inspection.
23
+ # - Verifies the source URL is HTTPS (no plain git://) so MITM is harder.
24
+
25
+ set -euo pipefail
26
+
27
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28
+ # shellcheck source=../lib/ui.sh disable=SC1091
29
+ source "$SCRIPT_DIR/../lib/ui.sh"
30
+ # shellcheck source=../lib/utils.sh disable=SC1091
31
+ source "$SCRIPT_DIR/../lib/utils.sh"
32
+
33
+ _init_resolve_url() {
34
+ local arg="$1"
35
+ case "$arg" in
36
+ http://*)
37
+ printf "dot init: refusing plain HTTP (use HTTPS or git+ssh)\n" >&2
38
+ return 2
39
+ ;;
40
+ https://* | git@*:*)
41
+ # Trust full URLs as-is — these are the user's explicit intent.
42
+ printf "%s\n" "$arg"
43
+ ;;
44
+ */*)
45
+ # owner/repo shorthand: validate before constructing the URL so
46
+ # a crafted argument can't smuggle shell metacharacters into
47
+ # downstream tooling (chezmoi init, git clone).
48
+ if [[ ! "$arg" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]]; then
49
+ printf "dot init: invalid owner/repo (only [A-Za-z0-9._-]+/[A-Za-z0-9._-]+ allowed): %s\n" "$arg" >&2
50
+ return 2
51
+ fi
52
+ printf "https://github.com/%s.git\n" "$arg"
53
+ ;;
54
+ *)
55
+ # Bare user shorthand: same character whitelist as the path
56
+ # segment of a GitHub URL.
57
+ if [[ ! "$arg" =~ ^[A-Za-z0-9._-]+$ ]]; then
58
+ printf "dot init: invalid user (only [A-Za-z0-9._-] allowed): %s\n" "$arg" >&2
59
+ return 2
60
+ fi
61
+ printf "https://github.com/%s/dotfiles.git\n" "$arg"
62
+ ;;
63
+ esac
64
+ }
65
+
66
+ cmd_init() {
67
+ local user_arg="" dry_run=0 apply=1 force=0
68
+ while [[ $# -gt 0 ]]; do
69
+ case "$1" in
70
+ --dry-run | -n)
71
+ dry_run=1
72
+ shift
73
+ ;;
74
+ --no-apply)
75
+ apply=0
76
+ shift
77
+ ;;
78
+ --force | -f)
79
+ force=1
80
+ shift
81
+ ;;
82
+ --help | -h)
83
+ cat <<EOF
84
+ Usage: dot init <github-user|owner/repo|url> [--dry-run] [--no-apply] [--force]
85
+
86
+ Bootstrap a foreign dotfiles repo through chezmoi + the dot harness.
87
+
88
+ Examples:
89
+ dot init alice # github.com/alice/dotfiles
90
+ dot init alice/configs # github.com/alice/configs
91
+ dot init https://example.com/repo.git
92
+ dot init alice --dry-run # preview only
93
+ dot init alice --no-apply # clone but don't apply
94
+
95
+ Flags:
96
+ --dry-run, -n Show resolved URL + target dir without cloning.
97
+ --no-apply Clone the source but skip 'chezmoi apply'.
98
+ --force, -f Overwrite an existing chezmoi source dir.
99
+ EOF
100
+ return 0
101
+ ;;
102
+ --)
103
+ shift
104
+ break
105
+ ;;
106
+ -*)
107
+ ui_err "Unknown flag" "$1"
108
+ return 1
109
+ ;;
110
+ *)
111
+ if [[ -n "$user_arg" ]]; then
112
+ ui_err "Too many arguments" "$1"
113
+ return 1
114
+ fi
115
+ user_arg="$1"
116
+ shift
117
+ ;;
118
+ esac
119
+ done
120
+
121
+ [[ -n "$user_arg" ]] || {
122
+ ui_err "init" "missing <user|repo|url>"
123
+ return 1
124
+ }
125
+
126
+ local url
127
+ url="$(_init_resolve_url "$user_arg")" || return $?
128
+
129
+ if ! command -v chezmoi >/dev/null 2>&1; then
130
+ ui_err "chezmoi" "not installed — run install.sh first"
131
+ return 127
132
+ fi
133
+
134
+ local source_dir
135
+ source_dir="${CHEZMOI_SOURCE_DIR:-$(chezmoi source-path 2>/dev/null || echo "$HOME/.local/share/chezmoi")}"
136
+
137
+ ui_header "Bootstrap dotfiles from $user_arg"
138
+ ui_info "Source URL" "$url"
139
+ ui_info "Target dir" "$source_dir"
140
+ local _apply_label="no"
141
+ [[ "$apply" -eq 1 ]] && _apply_label="yes"
142
+ ui_info "Apply after?" "$_apply_label"
143
+
144
+ if [[ "$dry_run" -eq 1 ]]; then
145
+ ui_ok "Dry-run" "no changes made"
146
+ return 0
147
+ fi
148
+
149
+ if [[ -e "$source_dir" ]] && [[ "$force" -ne 1 ]]; then
150
+ ui_err "Refusing" "$source_dir already exists; pass --force to overwrite"
151
+ return 1
152
+ fi
153
+
154
+ ui_warn "Trust" "The target repo's scripts will run with your user privileges."
155
+ ui_warn "Trust" "Inspect $url before proceeding if you don't know the author."
156
+ if [[ -t 0 ]] && [[ "${DOTFILES_NONINTERACTIVE:-0}" != "1" ]]; then
157
+ read -r -p " Continue? [y/N] " resp
158
+ case "$resp" in
159
+ [yY] | [yY][eE][sS]) ;;
160
+ *)
161
+ ui_info "Init" "aborted by user"
162
+ return 1
163
+ ;;
164
+ esac
165
+ fi
166
+
167
+ local chezmoi_args=(init --source "$source_dir")
168
+ [[ "$apply" -eq 1 ]] && chezmoi_args+=(--apply)
169
+ chezmoi_args+=("$url")
170
+
171
+ ui_info "chezmoi" "${chezmoi_args[*]}"
172
+ if chezmoi "${chezmoi_args[@]}"; then
173
+ ui_ok "Init" "complete"
174
+ if [[ "$apply" -eq 1 ]]; then
175
+ ui_info "Hint" "run 'dot doctor' to validate the new environment"
176
+ else
177
+ ui_info "Hint" "run 'chezmoi diff' then 'chezmoi apply' when ready"
178
+ fi
179
+ else
180
+ local rc=$?
181
+ ui_err "Init" "chezmoi exited $rc"
182
+ return "$rc"
183
+ fi
184
+ }
@@ -61,10 +61,13 @@ cmd_prewarm() {
61
61
  # Clear caches first
62
62
  ui_info "Cache" "Clearing shell initialization caches"
63
63
  local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}"
64
- rm -rf "$cache_dir/zsh"/*-init.zsh "$cache_dir/zsh"/*.zwc 2>/dev/null || true
65
- rm -rf "$cache_dir/bash"/*-init.bash 2>/dev/null || true
66
- rm -rf "$cache_dir/fish"/*-init.fish 2>/dev/null || true
67
- rm -rf "$cache_dir/nushell"/*.nu 2>/dev/null || true
64
+ # `find ... -delete` instead of `rm -rf $glob` so an unmatched literal
65
+ # pattern can't accidentally remove a real same-named directory, and
66
+ # so we don't pass `-r` on what should always be files.
67
+ [[ -d "$cache_dir/zsh" ]] && find "$cache_dir/zsh" -maxdepth 1 -type f \( -name '*-init.zsh' -o -name '*.zwc' \) -delete 2>/dev/null
68
+ [[ -d "$cache_dir/bash" ]] && find "$cache_dir/bash" -maxdepth 1 -type f -name '*-init.bash' -delete 2>/dev/null
69
+ [[ -d "$cache_dir/fish" ]] && find "$cache_dir/fish" -maxdepth 1 -type f -name '*-init.fish' -delete 2>/dev/null
70
+ [[ -d "$cache_dir/nushell" ]] && find "$cache_dir/nushell" -maxdepth 1 -type f -name '*.nu' -delete 2>/dev/null
68
71
  ui_info "Cache" "Cleared. Regenerating..."
69
72
  src_dir="$(resolve_source_dir)"
70
73
  if [ -n "$src_dir" ] && [ -f "$src_dir/scripts/ops/prewarm.sh" ]; then