@sebastienrousseau/dotfiles 0.2.501 → 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 (65) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +38 -8
  3. package/docs/COPYRIGHT +1 -1
  4. package/docs/index.md +2 -2
  5. package/docs/manual/00-introduction.md +1 -1
  6. package/docs/manual/03-reference/01-dot-cli.md +63 -45
  7. package/docs/manual/03-reference/02-config-files.md +2 -2
  8. package/docs/manual/03-reference/05-feature-flags.md +62 -3
  9. package/docs/manual/_toc.yml +1 -1
  10. package/docs/manual/command-index.md +12 -8
  11. package/docs/manual/index.md +66 -0
  12. package/docs/operations/COVERAGE.md +36 -2
  13. package/docs/operations/HARD_AUDIT_2026.md +631 -0
  14. package/docs/operations/REGISTRY.md +89 -0
  15. package/docs/operations/ROADMAP_2026.md +665 -0
  16. package/docs/operations/TRACEABILITY.md +3 -0
  17. package/docs/operations/VERSION_SYNC.md +4 -4
  18. package/docs/reference/POWERSHELL_PARITY.md +80 -0
  19. package/docs/registry.json +6 -0
  20. package/docs/security/CI_PINNING.md +113 -0
  21. package/docs/security/COMMIT_SIGNING.md +138 -0
  22. package/docs/security/DISCLOSURE.md +130 -0
  23. package/docs/security/KEY_ROTATION.md +81 -1
  24. package/docs/security/SCORECARD.md +74 -14
  25. package/docs/security/security-pubkey.asc +15 -0
  26. package/dot_config/fish/conf.d/direnv.fish +4 -0
  27. package/dot_config/fish/conf.d/mise-activate.fish +5 -0
  28. package/dot_config/git/hooks/executable_commit-msg +1 -1
  29. package/dot_config/shell/00-core-paths.sh.tmpl +8 -1
  30. package/dot_config/shell/README.md +1 -1
  31. package/dot_config/zsh/dot_zshrc.tmpl +57 -4
  32. package/dot_config/zsh/rc.d/30-options.zsh.tmpl +1 -1
  33. package/dot_local/bin/executable_dot +49 -9
  34. package/dot_local/bin/executable_dot-bootstrap +0 -1
  35. package/dot_local/bin/executable_dot-theme-sync +8 -8
  36. package/dot_local/bin/executable_tour +2 -2
  37. package/dot_local/share/man/man1/dot.1 +1 -1
  38. package/dot_local/share/zsh/completions/_dot +4 -0
  39. package/install.sh +44 -37
  40. package/package.json +1 -1
  41. package/scripts/ci/dot-cli-startup-bench.sh +126 -0
  42. package/scripts/ci/install-chezmoi-verified.sh +4 -1
  43. package/scripts/ci/lint-reusable-pins.sh +78 -0
  44. package/scripts/ci/run-coverage.sh +89 -0
  45. package/scripts/ci/windows-smoke-test.ps1 +136 -0
  46. package/scripts/diagnostics/doctor.sh +39 -9
  47. package/scripts/dot/commands/agent.sh +19 -22
  48. package/scripts/dot/commands/agents.sh +325 -0
  49. package/scripts/dot/commands/aliases.sh +10 -8
  50. package/scripts/dot/commands/core.sh +10 -4
  51. package/scripts/dot/commands/fleet.sh +278 -3
  52. package/scripts/dot/commands/init.sh +184 -0
  53. package/scripts/dot/commands/meta.sh +7 -4
  54. package/scripts/dot/commands/registry.sh +263 -0
  55. package/scripts/dot/commands/tools.sh +49 -1
  56. package/scripts/dot/lib/bento.sh +1 -1
  57. package/scripts/dot/lib/platform.sh +21 -8
  58. package/scripts/dot/lib/ui.sh +134 -2
  59. package/scripts/dot/lib/utils.sh +1 -1
  60. package/scripts/git-hooks/pre-commit-audit.sh +1 -1
  61. package/scripts/lib/secrets_provider.sh +32 -6
  62. package/scripts/ops/rollback.sh +14 -0
  63. package/scripts/security/check-disclosure-key-expiry.sh +110 -0
  64. package/scripts/security/lock-configs.sh +11 -2
  65. package/scripts/version-sync.sh +3 -0
@@ -359,9 +359,13 @@ ui_run_cmd() {
359
359
  done
360
360
  wait "$pid" 2>/dev/null
361
361
 
362
- local rc
363
- rc=$(cat "$rc_file" 2>/dev/null || echo 1)
362
+ # Guard against the rc_file being empty (race between subshell exit
363
+ # and parent read): an unset rc would default to non-numeric "" and
364
+ # fail the `[[ == "0" ]]` check confusingly. Treat empty as failure.
365
+ local rc=""
366
+ [[ -s "$rc_file" ]] && rc="$(<"$rc_file")"
364
367
  rm -f "$rc_file"
368
+ rc="${rc:-1}"
365
369
 
366
370
  ui_clear_line
367
371
  ui_show_cursor
@@ -472,6 +476,134 @@ ui_table_sep() {
472
476
  printf '\n'
473
477
  }
474
478
 
479
+ # ═══════════════════════════════════════════════════════════════════════
480
+ # Buffered table — accumulates rows then renders via `gum table -p`
481
+ # (rounded borders) when gum is on PATH and stdout is a TTY; falls back
482
+ # to the existing printf-based ui_table_header/row otherwise.
483
+ #
484
+ # ui_table_begin "Tool" "Version" "Source" "Requested"
485
+ # ui_table_add "node" "24.14.0" "~/.dotfiles/mise.toml" "24.14.0"
486
+ # ui_table_add "node" "25.9.0" "" ""
487
+ # ui_table_end
488
+ #
489
+ # Cells may contain anything except ASCII US (0x1f), the internal row
490
+ # separator. Empty cells render as blank in both modes.
491
+ #
492
+ # Environment overrides:
493
+ # DOTFILES_NO_TUI=1 force the printf fallback even if gum is installed
494
+ # NO_COLOR=… standard convention; also forces the fallback
495
+ # ═══════════════════════════════════════════════════════════════════════
496
+ _UI_TABLE_HEADERS=()
497
+ _UI_TABLE_ROWS=()
498
+
499
+ ui_table_begin() {
500
+ _UI_TABLE_HEADERS=("$@")
501
+ _UI_TABLE_ROWS=()
502
+ }
503
+
504
+ ui_table_add() {
505
+ if [[ "${#_UI_TABLE_HEADERS[@]}" -eq 0 ]]; then
506
+ return 1
507
+ fi
508
+ local IFS=$'\x1f'
509
+ _UI_TABLE_ROWS+=("$*")
510
+ }
511
+
512
+ ui_table_end() {
513
+ if [[ "${#_UI_TABLE_HEADERS[@]}" -eq 0 ]]; then
514
+ return 0
515
+ fi
516
+
517
+ local use_gum=0
518
+ if command -v gum >/dev/null 2>&1 &&
519
+ [[ -t 1 ]] &&
520
+ [[ "${DOTFILES_NO_TUI:-0}" != "1" ]] &&
521
+ [[ -z "${NO_COLOR:-}" ]]; then
522
+ use_gum=1
523
+ fi
524
+
525
+ if ((use_gum)); then
526
+ local header_row
527
+ local IFS=$'\x1f'
528
+ header_row="${_UI_TABLE_HEADERS[*]}"
529
+ # Charm palette: 212 = hot pink (matches ui_header), 240 = muted grey.
530
+ # Cell colour intentionally left at terminal default so the table
531
+ # reads on both light and dark backgrounds.
532
+ # Override via DOTFILES_TABLE_{BORDER,HEADER}_FG env vars.
533
+ local border_fg="${DOTFILES_TABLE_BORDER_FG:-240}"
534
+ local header_fg="${DOTFILES_TABLE_HEADER_FG:-212}"
535
+ {
536
+ printf '%s\n' "$header_row"
537
+ printf '%s\n' "${_UI_TABLE_ROWS[@]}"
538
+ } | gum table --print \
539
+ --separator=$'\x1f' \
540
+ --border=rounded \
541
+ --border.foreground="$border_fg" \
542
+ --header.foreground="$header_fg" \
543
+ --padding="0 1" 2>/dev/null ||
544
+ _ui_table_printf_fallback
545
+ else
546
+ _ui_table_printf_fallback
547
+ fi
548
+
549
+ _UI_TABLE_HEADERS=()
550
+ _UI_TABLE_ROWS=()
551
+ }
552
+
553
+ _ui_table_printf_fallback() {
554
+ ui_init
555
+ # Pre-scan all buffered rows + headers to compute per-column width so
556
+ # values aren't truncated or collide. Min width 8, +2 padding.
557
+ local ncols="${#_UI_TABLE_HEADERS[@]}"
558
+ local -a widths=()
559
+ local i
560
+ for ((i = 0; i < ncols; i++)); do
561
+ widths[i]="${#_UI_TABLE_HEADERS[i]}"
562
+ done
563
+ local row
564
+ for row in "${_UI_TABLE_ROWS[@]}"; do
565
+ local -a _fields=()
566
+ local IFS=$'\x1f'
567
+ read -ra _fields <<<"$row"
568
+ for ((i = 0; i < ncols; i++)); do
569
+ local len="${#_fields[i]}"
570
+ ((len > widths[i])) && widths[i]=$len
571
+ done
572
+ done
573
+ for ((i = 0; i < ncols; i++)); do
574
+ ((widths[i] += 2))
575
+ ((widths[i] < 8)) && widths[i]=8
576
+ done
577
+
578
+ # Emit header (bold when color available), separator, then rows.
579
+ local out=" "
580
+ for ((i = 0; i < ncols; i++)); do
581
+ if [[ "$UI_COLOR" = "1" ]]; then
582
+ out+="$(printf '%s%-*s%s' "$BOLD" "${widths[i]}" "${_UI_TABLE_HEADERS[i]}" "$NORMAL")"
583
+ else
584
+ out+="$(printf '%-*s' "${widths[i]}" "${_UI_TABLE_HEADERS[i]}")"
585
+ fi
586
+ done
587
+ printf '%s\n' "$out"
588
+
589
+ local total=2
590
+ for ((i = 0; i < ncols; i++)); do ((total += widths[i])); done
591
+ printf ' '
592
+ for ((i = 0; i < total - 2; i++)); do printf '─'; done
593
+ printf '\n'
594
+
595
+ for row in "${_UI_TABLE_ROWS[@]}"; do
596
+ local -a _fields=()
597
+ local IFS=$'\x1f'
598
+ read -ra _fields <<<"$row"
599
+ out=" "
600
+ for ((i = 0; i < ncols; i++)); do
601
+ out+="$(printf '%-*s' "${widths[i]}" "${_fields[i]:-}")"
602
+ done
603
+ printf '%s\n' "$out"
604
+ done
605
+ }
606
+
475
607
  # ═══════════════════════════════════════════════════════════════════════
476
608
  # Logo
477
609
  # ═══════════════════════════════════════════════════════════════════════
@@ -274,7 +274,7 @@ dot_command_summary() {
274
274
  echo "Manage secrets (set/get/list/load/provider)."
275
275
  ;;
276
276
  env)
277
- echo "Load secret buckets into shell exports."
277
+ echo "List mise-managed toolchain versions and sources."
278
278
  ;;
279
279
  secrets-create)
280
280
  echo "Create a new encrypted secrets file."
@@ -140,6 +140,6 @@ if [[ $FAILED -eq 1 ]]; then
140
140
  printf '%b\\n' " (Use --no-verify to bypass if absolutely necessary)"
141
141
  exit 1
142
142
  else
143
- printf '%b\n' "${GREEN}${BOLD}✅ Audit passed.${NC} v0.2.501 standards maintained."
143
+ printf '%b\n' "${GREEN}${BOLD}✅ Audit passed.${NC} v0.2.502 standards maintained."
144
144
  exit 0
145
145
  fi
@@ -126,15 +126,41 @@ dot_secrets_set() {
126
126
  }
127
127
 
128
128
  dot_secrets_get() {
129
- local key="${1:-}" provider
130
- [[ -n "$key" ]] || return 1
129
+ local key="${1:-}" provider value rc=0
130
+ [[ -n "$key" ]] || {
131
+ printf 'dot_secrets_get: empty key\n' >&2
132
+ return 1
133
+ }
131
134
  provider="$(dot_secrets_provider)"
132
135
  case "$provider" in
133
- macos-keychain) dot_secrets_get_macos "$key" ;;
134
- pass) dot_secrets_get_pass "$key" ;;
135
- plain-enc) dot_secrets_get_plain_enc "$key" ;;
136
- *) return 1 ;;
136
+ macos-keychain) value="$(dot_secrets_get_macos "$key")" || rc=$? ;;
137
+ pass) value="$(dot_secrets_get_pass "$key")" || rc=$? ;;
138
+ plain-enc) value="$(dot_secrets_get_plain_enc "$key")" || rc=$? ;;
139
+ none)
140
+ # No usable provider on this host — silent miss is wrong;
141
+ # callers need to know whether to fall back.
142
+ printf 'dot_secrets_get: no provider configured for key=%s\n' "$key" >&2
143
+ return 2
144
+ ;;
145
+ *)
146
+ printf 'dot_secrets_get: unknown provider %q\n' "$provider" >&2
147
+ return 1
148
+ ;;
137
149
  esac
150
+ if [[ "$rc" -ne 0 ]]; then
151
+ printf 'dot_secrets_get: provider %q failed (rc=%d) for key=%s\n' \
152
+ "$provider" "$rc" "$key" >&2
153
+ return "$rc"
154
+ fi
155
+ if [[ -z "$value" ]]; then
156
+ # An empty return from a successful provider call usually means
157
+ # "key not found" — distinguish from "provider crashed" so the
158
+ # caller's fallback can be different.
159
+ printf 'dot_secrets_get: provider %q returned empty for key=%s (not found?)\n' \
160
+ "$provider" "$key" >&2
161
+ return 3
162
+ fi
163
+ printf '%s' "$value"
138
164
  }
139
165
 
140
166
  dot_secrets_bucket_keys() {
@@ -122,6 +122,10 @@ list_backups() {
122
122
  }
123
123
 
124
124
  # Create a backup
125
+ # LCOV_EXCL_START — body mutates real $HOME (cp/mkdir of dotfiles);
126
+ # can't be exercised under the coverage sandbox without destroying
127
+ # fixture state. The dispatcher reaches this function but the body
128
+ # is genuinely off-limits.
125
129
  create_backup() {
126
130
  local reason="${1:-manual}"
127
131
  local timestamp
@@ -183,7 +187,9 @@ EOF
183
187
  # Cleanup old backups
184
188
  cleanup_old_backups
185
189
  }
190
+ # LCOV_EXCL_STOP
186
191
 
192
+ # LCOV_EXCL_START — rm -rf of real backup dirs.
187
193
  # Cleanup old backups, keeping only MAX_BACKUPS
188
194
  cleanup_old_backups() {
189
195
  local count
@@ -199,6 +205,7 @@ cleanup_old_backups() {
199
205
  done
200
206
  fi
201
207
  }
208
+ # LCOV_EXCL_STOP
202
209
 
203
210
  # Get the most recent backup
204
211
  get_latest_backup() {
@@ -211,6 +218,8 @@ get_backup_by_index() {
211
218
  find "$BACKUP_DIR" -maxdepth 1 -type d -name "backup_*" | sort -r | sed -n "${index}p"
212
219
  }
213
220
 
221
+ # LCOV_EXCL_START — body restores files into real $HOME; dispatcher
222
+ # reaches it but the actual mutation is off-limits under coverage.
214
223
  # Rollback to a specific backup
215
224
  perform_rollback() {
216
225
  local backup_path="$1"
@@ -278,7 +287,9 @@ Analyze why the environment may have reached a state requiring rollback and sugg
278
287
  fi
279
288
  fi
280
289
  }
290
+ # LCOV_EXCL_STOP
281
291
 
292
+ # LCOV_EXCL_START — runs git reset --hard, mutates working tree.
282
293
  # Git-based rollback
283
294
  git_reset() {
284
295
  local dry_run="${1:-0}"
@@ -338,7 +349,9 @@ git_reset() {
338
349
  persist_log "GIT_RESET: to $target_commit"
339
350
  fi
340
351
  }
352
+ # LCOV_EXCL_STOP
341
353
 
354
+ # LCOV_EXCL_START — body copies files into real $HOME.
342
355
  # Restore a specific file from the latest backup
343
356
  restore_file() {
344
357
  local file_path="$1"
@@ -417,6 +430,7 @@ restore_file() {
417
430
  persist_log "RESTORE_FILE: $file_path"
418
431
  fi
419
432
  }
433
+ # LCOV_EXCL_STOP
420
434
 
421
435
  # Show current status
422
436
  show_status() {
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env bash
2
+ # Copyright (c) 2015-2026 Dotfiles. All rights reserved.
3
+ #
4
+ # scripts/security/check-disclosure-key-expiry.sh
5
+ #
6
+ # Reads the disclosure public key from `docs/security/security-pubkey.asc`,
7
+ # parses its expiry, and exits non-zero when the remaining lifetime is
8
+ # below threshold. Closes the R3 audit N2 finding (no automated alert
9
+ # for the 2029-05-15 expiry).
10
+ #
11
+ # Two thresholds:
12
+ # --warn-days N Emit a `::warning::` if expiry is < N days away.
13
+ # Default 90.
14
+ # --fail-days N Exit 1 (CI failure) if expiry is < N days away.
15
+ # Default 30.
16
+ #
17
+ # Usage:
18
+ # bash scripts/security/check-disclosure-key-expiry.sh
19
+ # bash scripts/security/check-disclosure-key-expiry.sh --warn-days 180 --fail-days 60
20
+ #
21
+ # Designed to run from `.github/workflows/verify-gpg-wkd.yml` on the
22
+ # weekly schedule, and from a maintainer's local pre-release sanity
23
+ # check. Does NOT require network — reads the in-repo .asc only.
24
+
25
+ set -euo pipefail
26
+
27
+ WARN_DAYS=90
28
+ FAIL_DAYS=30
29
+
30
+ while [[ $# -gt 0 ]]; do
31
+ case "$1" in
32
+ --warn-days)
33
+ WARN_DAYS="$2"
34
+ shift 2
35
+ ;;
36
+ --fail-days)
37
+ FAIL_DAYS="$2"
38
+ shift 2
39
+ ;;
40
+ -h | --help)
41
+ sed -n '5,25p' "${BASH_SOURCE[0]}"
42
+ exit 0
43
+ ;;
44
+ *)
45
+ echo "Unknown arg: $1" >&2
46
+ exit 2
47
+ ;;
48
+ esac
49
+ done
50
+
51
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
52
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
53
+ ASC="$REPO_ROOT/docs/security/security-pubkey.asc"
54
+
55
+ if [[ ! -f "$ASC" ]]; then
56
+ echo "::error::no public key at $ASC" >&2
57
+ exit 2
58
+ fi
59
+
60
+ if ! command -v gpg >/dev/null 2>&1; then
61
+ echo "::warning::gpg not installed — cannot verify key expiry" >&2
62
+ exit 0
63
+ fi
64
+
65
+ # Import into a throwaway keyring so we don't pollute the user's real one.
66
+ GNUPGHOME="$(mktemp -d -t dot-disclosure-key.XXXXXX)"
67
+ export GNUPGHOME
68
+ chmod 700 "$GNUPGHOME"
69
+ trap 'rm -rf "$GNUPGHOME"' EXIT
70
+
71
+ # gpg can emit harmless warnings ("keyboxd older than us", "gpg-agent
72
+ # IPC failed" in fresh GNUPGHOME, etc.) and exit non-zero under
73
+ # `set -e` even when the import itself succeeded. Verify success by
74
+ # checking the err log for the literal `imported: N` (N >= 1) line
75
+ # rather than trusting gpg's exit code.
76
+ gpg --batch --import "$ASC" >/dev/null 2>"$GNUPGHOME/import.err" || true
77
+ if ! grep -qE '^gpg:[[:space:]]*imported: [1-9]' "$GNUPGHOME/import.err"; then
78
+ echo "::error::gpg import failed for $ASC" >&2
79
+ cat "$GNUPGHOME/import.err" >&2
80
+ exit 2
81
+ fi
82
+
83
+ # `--with-colons` field 7 of the `pub:` line is the expiry in Unix
84
+ # seconds; 0 means no expiry.
85
+ expires=$(gpg --with-colons --fingerprint 2>/dev/null | awk -F: '/^pub:/{print $7; exit}')
86
+
87
+ if [[ -z "$expires" || "$expires" == "0" ]]; then
88
+ echo "::warning::disclosure key has no expiry — set one per docs/security/KEY_ROTATION.md" >&2
89
+ exit 0
90
+ fi
91
+
92
+ now=$(date -u +%s)
93
+ remaining_days=$(((expires - now) / 86400))
94
+
95
+ expires_date=$(date -u -r "$expires" '+%Y-%m-%d' 2>/dev/null || date -u -d "@$expires" '+%Y-%m-%d' 2>/dev/null || echo "(date format unknown)")
96
+
97
+ echo "Disclosure key expires: $expires_date ($remaining_days days from now)"
98
+ echo " warn threshold: $WARN_DAYS days"
99
+ echo " fail threshold: $FAIL_DAYS days"
100
+
101
+ if ((remaining_days < FAIL_DAYS)); then
102
+ echo "::error::Disclosure key expires in $remaining_days days. Rotate now per docs/security/KEY_ROTATION.md." >&2
103
+ exit 1
104
+ fi
105
+
106
+ if ((remaining_days < WARN_DAYS)); then
107
+ echo "::warning::Disclosure key expires in $remaining_days days. Schedule rotation per docs/security/KEY_ROTATION.md." >&2
108
+ fi
109
+
110
+ exit 0
@@ -22,13 +22,22 @@ if [[ "$OSTYPE" == "darwin"* ]]; then
22
22
  UNLOCK_CMD="chflags nouchg"
23
23
  CHECK_CMD="ls -lO"
24
24
  else
25
- # Linux (requires root usually, but we'll try)
25
+ # Linux (chattr +i requires root; fail loudly when sudo is missing
26
+ # rather than printing a confusing error per file in automation)
26
27
  if command -v chattr >/dev/null; then
28
+ if ! command -v sudo >/dev/null; then
29
+ echo " 'sudo' not found; chattr requires root. Aborting." >&2
30
+ exit 1
31
+ fi
32
+ if ! sudo -n true 2>/dev/null && [[ ! -t 0 ]]; then
33
+ echo " sudo requires a password but no TTY is attached. Aborting." >&2
34
+ exit 1
35
+ fi
27
36
  LOCK_CMD="sudo chattr +i"
28
37
  UNLOCK_CMD="sudo chattr -i"
29
38
  CHECK_CMD="lsattr"
30
39
  else
31
- echo " 'chattr' not found. Cannot set immutability on Linux without it."
40
+ echo " 'chattr' not found. Cannot set immutability on Linux without it." >&2
32
41
  exit 1
33
42
  fi
34
43
  fi
@@ -142,6 +142,8 @@ sed_in_place() {
142
142
  fi
143
143
  }
144
144
 
145
+ # LCOV_EXCL_START — rm -rf BACKUP_DIR + cp of real release files;
146
+ # only run during a real release flow, not safe under coverage.
145
147
  create_backup() {
146
148
  local files=("$@")
147
149
 
@@ -166,6 +168,7 @@ create_backup() {
166
168
 
167
169
  log_success "Backed up $backup_count files"
168
170
  }
171
+ # LCOV_EXCL_STOP
169
172
 
170
173
  update_version_references() {
171
174
  local target_version="$1"