@sebastienrousseau/dotfiles 0.2.508 → 0.2.511

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 (82) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +5 -5
  3. package/docs/COPYRIGHT +1 -1
  4. package/docs/GOVERNANCE.md +2 -2
  5. package/docs/adr/ADR-011-nushell-tier3-keep.md +2 -2
  6. package/docs/articles/.pages +6 -0
  7. package/docs/articles/2026-07-05-custom-mkdocs-material-dark-theme.md +216 -0
  8. package/docs/articles/2026-07-05-fish-startup-abbr.md +153 -0
  9. package/docs/articles/2026-07-05-master-to-main-rename-runbook.md +128 -0
  10. package/docs/articles/index.md +36 -0
  11. package/docs/guides/INSTALL.md +3 -3
  12. package/docs/index.md +116 -27
  13. package/docs/interop/A2A.md +2 -2
  14. package/docs/interop/POWERSHELL.md +4 -4
  15. package/docs/manual/00-introduction.md +3 -3
  16. package/docs/manual/01-concepts/02-trust-model.md +3 -3
  17. package/docs/manual/01-concepts/04-fleet.md +1 -1
  18. package/docs/manual/02-tutorials/01-first-install.md +1 -1
  19. package/docs/manual/02-tutorials/05-deploy-fleet.md +2 -2
  20. package/docs/manual/03-reference/02-config-files.md +1 -1
  21. package/docs/manual/03-reference/04-templates.md +1 -1
  22. package/docs/manual/04-cookbook/01-recipes.md +1 -1
  23. package/docs/manual/04-cookbook/02-troubleshooting.md +1 -1
  24. package/docs/manual/05-appendices/B-security-checklist.md +1 -1
  25. package/docs/manual/command-index.md +76 -1
  26. package/docs/manual/index.md +1 -1
  27. package/docs/operations/ARCHITECTURE_ROADMAP.md +145 -0
  28. package/docs/operations/CI_CADENCE.md +2 -2
  29. package/docs/operations/COMPLETIONS.md +2 -2
  30. package/docs/operations/COVERAGE.md +4 -4
  31. package/docs/operations/DRIFT.md +1 -1
  32. package/docs/operations/MAINTENANCE.md +6 -6
  33. package/docs/operations/PERFORMANCE.md +6 -6
  34. package/docs/operations/RELIABILITY.md +1 -1
  35. package/docs/operations/ROADMAP_V0_2_503.md +1 -1
  36. package/docs/operations/TRACEABILITY.md +2 -0
  37. package/docs/operations/TRUSTED_AGENT_WORKSTATION.md +6 -6
  38. package/docs/operations/VERSION_SYNC.md +3 -3
  39. package/docs/reference/UTILS.md +82 -0
  40. package/docs/security/AUDIT_BYPASS.md +3 -3
  41. package/docs/security/AUTOMATION_SECRETS.md +1 -1
  42. package/docs/security/CI_PINNING.md +10 -10
  43. package/docs/security/COMMIT_SIGNING.md +10 -10
  44. package/docs/security/DEPS_DEV_EXCEPTIONS.md +4 -4
  45. package/docs/security/DISCLOSURE.md +3 -3
  46. package/docs/security/INCIDENT_RESPONSE.md +1 -1
  47. package/docs/security/INSTALL_VERIFICATION.md +2 -2
  48. package/docs/security/MCP_POLICY.md +4 -4
  49. package/docs/security/SCORECARD.md +7 -7
  50. package/docs/security/SECURITY_CHECKLIST.md +1 -1
  51. package/docs/security/SHELL_EXEMPTIONS.md +2 -2
  52. package/docs/security/SOUP_REGISTER.md +4 -4
  53. package/docs/stylesheets/extra.css +444 -0
  54. package/docs/themes/hero-shot.svg +1 -1
  55. package/install.sh +17 -6
  56. package/package.json +1 -1
  57. package/scripts/ci/check-copyright-headers.sh +1 -1
  58. package/scripts/ci/check-shell-preamble.sh +1 -1
  59. package/scripts/ci/guard-gitleaks-checkout.sh +1 -1
  60. package/scripts/diagnostics/aliases-manifest.sh +19 -1
  61. package/scripts/diagnostics/verify_state.sh +6 -2
  62. package/scripts/diagnostics/version-locks.sh +10 -6
  63. package/scripts/dot/commands/agent.sh +12 -7
  64. package/scripts/dot/commands/ai.sh +8 -5
  65. package/scripts/dot/commands/appearance.sh +14 -1
  66. package/scripts/dot/commands/completion.sh +134 -0
  67. package/scripts/dot/commands/core.sh +8 -0
  68. package/scripts/dot/commands/diagnostics.sh +16 -0
  69. package/scripts/dot/commands/fleet.sh +11 -4
  70. package/scripts/dot/commands/lint.sh +55 -30
  71. package/scripts/dot/commands/registry.sh +7 -1
  72. package/scripts/dot/commands/secrets.sh +61 -7
  73. package/scripts/dot/commands/security.sh +8 -0
  74. package/scripts/dot/commands/tools.sh +11 -2
  75. package/scripts/dot/powershell/Dot.psm1 +1 -1
  76. package/scripts/git-hooks/pre-commit-audit.sh +1 -1
  77. package/scripts/ops/heal-tools.sh +28 -0
  78. package/scripts/ops/teleport.sh +2 -2
  79. package/scripts/qa/examples-coverage.sh +94 -0
  80. package/scripts/tools/detect-collisions.py +19 -1
  81. package/scripts/version-sync.sh +36 -2
  82. package/docs/_config.yml +0 -59
@@ -147,9 +147,7 @@ _ai_get_cached_status() {
147
147
 
148
148
  # Look up field $2 (2=present 0/1, 3=version) for binary $1 from the
149
149
  # cached TSV. Replaces a bash-4 associative array for macOS bash 3.2.
150
- _ai_status_field() {
151
- awk -F'\t' -v b="$1" -v f="$2" '$1==b{print $f;exit}' "$AI_STATUS_CACHE_FILE" 2>/dev/null
152
- }
150
+ # (Removed _ai_status_field — cmd_ai_status now reads both fields in one awk.)
153
151
 
154
152
  # _ai_mise_pkg (binary -> mise package map) is defined in lib/dot/ai-install.sh.
155
153
 
@@ -193,8 +191,13 @@ cmd_ai_status() {
193
191
  ui_section "$category"
194
192
  current_category="$category"
195
193
  fi
196
- if [[ "$(_ai_status_field "$bin" 2)" == "1" ]]; then
197
- ver="$(_ai_status_field "$bin" 3)"
194
+ # One awk over the cache line for this tool (was two: field 2 and field 3).
195
+ local _st_installed _st_ver
196
+ IFS=$'\t' read -r _st_installed _st_ver < <(
197
+ awk -F'\t' -v b="$bin" '$1==b{print $2"\t"$3;exit}' "$AI_STATUS_CACHE_FILE" 2>/dev/null
198
+ )
199
+ if [[ "$_st_installed" == "1" ]]; then
200
+ ver="$_st_ver"
198
201
  [[ -z "$ver" ]] && ver="installed"
199
202
  [[ "$bin" == "claude" ]] && ver="${ver%% *}"
200
203
  ui_ok "$name" "$ver — $desc"
@@ -33,7 +33,20 @@ cmd_wallpaper() {
33
33
  }
34
34
 
35
35
  cmd_fonts() {
36
- run_script "scripts/fonts/install-nerd-fonts.sh" "Font install script" "$@"
36
+ local sub="${1:-install}"
37
+ case "$sub" in
38
+ patch)
39
+ shift
40
+ run_script "scripts/fonts/patch-fonts.sh" "Font patch helper" "$@"
41
+ ;;
42
+ install | "")
43
+ [[ "$sub" == install ]] && shift || true
44
+ run_script "scripts/fonts/install-nerd-fonts.sh" "Font install script" "$@"
45
+ ;;
46
+ *)
47
+ run_script "scripts/fonts/install-nerd-fonts.sh" "Font install script" "$@"
48
+ ;;
49
+ esac
37
50
  }
38
51
 
39
52
  cmd_tune() {
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-License-Identifier: MIT
3
+ # Copyright (c) 2015-2026 Sebastien Rousseau
4
+ ## `dot completion <bash|zsh|fish|nu>` — emit shell completion for `dot`,
5
+ ## generated from the canonical _dot_help_specs registry in bin/dot. Using one
6
+ ## source of truth keeps all four shells in sync (the previously hand-maintained
7
+ ## fish/nushell lists drifted and referenced non-existent commands).
8
+ ##
9
+ ## Usage:
10
+ ## dot completion bash >> ~/.bashrc.d/dot.bash
11
+ ## dot completion zsh > "${fpath[1]}/_dot"
12
+ ## dot completion fish > ~/.config/fish/completions/dot.fish
13
+ ## dot completion nu >> ~/.config/nushell/completions.nu
14
+
15
+ set -euo pipefail
16
+
17
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+ # shellcheck source=../../../lib/dot/utils.sh
19
+ source "$SCRIPT_DIR/../../../lib/dot/utils.sh"
20
+
21
+ _completion_dot_cli() {
22
+ local src
23
+ src="$(resolve_source_dir)"
24
+ printf '%s/bin/dot\n' "$src"
25
+ }
26
+
27
+ # Emit "name<TAB>description" for every top-level command (a _dot_help_specs
28
+ # field-2 entry with no space; entries containing a space are subcommands).
29
+ # Strip ": ' from descriptions so they can't break any shell's quoting.
30
+ _completion_commands() {
31
+ local cli
32
+ cli="$(_completion_dot_cli)"
33
+ awk -F'|' '
34
+ /_dot_help_specs\(\)/ { in_func = 1; next }
35
+ in_func && /cat <<'\''EOF'\''/ { in_block = 1; next }
36
+ in_block && /^EOF$/ { exit }
37
+ in_block && NF >= 3 && $2 !~ / / {
38
+ name = $2; desc = $3
39
+ gsub(/^[ \t]+|[ \t]+$/, "", name)
40
+ gsub(/^[ \t]+|[ \t]+$/, "", desc)
41
+ gsub(/[":\047]/, "", desc)
42
+ if (name != "") print name "\t" desc
43
+ }
44
+ ' "$cli"
45
+ }
46
+
47
+ # Emit "parent<TAB>child<TAB>description" for every subcommand (a field-2
48
+ # entry that contains a space, e.g. "theme list" -> parent=theme child=list).
49
+ _completion_subcommands() {
50
+ local cli
51
+ cli="$(_completion_dot_cli)"
52
+ awk -F'|' '
53
+ /_dot_help_specs\(\)/ { in_func = 1; next }
54
+ in_func && /cat <<'\''EOF'\''/ { in_block = 1; next }
55
+ in_block && /^EOF$/ { exit }
56
+ in_block && NF >= 3 && $2 ~ / / {
57
+ full = $2; desc = $3
58
+ gsub(/^[ \t]+|[ \t]+$/, "", full)
59
+ gsub(/^[ \t]+|[ \t]+$/, "", desc)
60
+ gsub(/[":\047]/, "", desc)
61
+ parent = full; sub(/ .*/, "", parent)
62
+ child = full; sub(/^[^ ]+ /, "", child)
63
+ if (parent != "" && child != "") print parent "\t" child "\t" desc
64
+ }
65
+ ' "$cli"
66
+ }
67
+
68
+ gen_bash() {
69
+ local names
70
+ names="$(_completion_commands | cut -f1 | tr '\n' ' ')"
71
+ printf '# dot bash completion — generated by: dot completion bash\n'
72
+ printf "complete -W '%s' dot\n" "${names% }"
73
+ }
74
+
75
+ gen_zsh() {
76
+ local name desc
77
+ printf '#compdef dot\n# generated by: dot completion zsh\n'
78
+ printf '_dot() {\n local -a commands\n commands=(\n'
79
+ while IFS=$'\t' read -r name desc; do
80
+ printf " '%s:%s'\n" "$name" "$desc"
81
+ done < <(_completion_commands)
82
+ printf ' )\n _describe '\''command'\'' commands\n}\n_dot "$@"\n'
83
+ }
84
+
85
+ gen_fish() {
86
+ local name desc
87
+ printf '# dot fish completion — generated by: dot completion fish\n'
88
+ printf 'complete -c dot -f\n'
89
+ while IFS=$'\t' read -r name desc; do
90
+ printf 'complete -c dot -n "__fish_use_subcommand" -a %s -d "%s"\n' "$name" "$desc"
91
+ done < <(_completion_commands)
92
+ local parent child
93
+ while IFS=$'\t' read -r parent child desc; do
94
+ printf 'complete -c dot -n "__fish_seen_subcommand_from %s" -a %s -d "%s"\n' \
95
+ "$parent" "$child" "$desc"
96
+ done < <(_completion_subcommands)
97
+ }
98
+
99
+ gen_nu() {
100
+ local name desc
101
+ printf '# dot nushell completion — generated by: dot completion nu\n'
102
+ printf 'export extern dot [\n command?: string@dot_commands\n ...args: string\n]\n'
103
+ printf 'def dot_commands [] {\n [\n'
104
+ while IFS=$'\t' read -r name desc; do
105
+ printf ' { value: "%s", description: "%s" }\n' "$name" "$desc"
106
+ done < <(_completion_commands)
107
+ printf ' ]\n}\n'
108
+ }
109
+
110
+ usage() {
111
+ cat <<'USAGE'
112
+ Usage: dot completion <bash|zsh|fish|nu>
113
+
114
+ Generate shell completion for `dot` from the canonical command registry.
115
+
116
+ dot completion bash >> ~/.bashrc.d/dot.bash
117
+ dot completion zsh > "${fpath[1]}/_dot"
118
+ dot completion fish > ~/.config/fish/completions/dot.fish
119
+ dot completion nu >> ~/.config/nushell/completions.nu
120
+ USAGE
121
+ }
122
+
123
+ # Dispatch: $1 is the command name ("completion"); $2 is the target shell.
124
+ case "${2:-}" in
125
+ bash) gen_bash ;;
126
+ zsh) gen_zsh ;;
127
+ fish) gen_fish ;;
128
+ nu | nushell) gen_nu ;;
129
+ "" | -h | --help | help) usage ;;
130
+ *)
131
+ echo "dot completion: unknown shell '${2}' (want: bash|zsh|fish|nu)" >&2
132
+ exit 1
133
+ ;;
134
+ esac
@@ -129,6 +129,10 @@ cmd_commit() {
129
129
  run_script "dot_local/bin/executable_git-ai-commit" "Git AI commit script" "$@"
130
130
  }
131
131
 
132
+ cmd_uninstall() {
133
+ run_script "scripts/uninstall.sh" "Uninstall script" "$@"
134
+ }
135
+
132
136
  cmd_clean_cache() {
133
137
  ui_info "Cache" "Clearing shell initialization caches"
134
138
  local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}"
@@ -185,6 +189,10 @@ case "${1:-}" in
185
189
  shift
186
190
  cmd_clean_cache "$@"
187
191
  ;;
192
+ uninstall)
193
+ shift
194
+ cmd_uninstall "$@"
195
+ ;;
188
196
  *)
189
197
  echo "Unknown core command: ${1:-}" >&2
190
198
  exit 1
@@ -110,6 +110,14 @@ cmd_chaos() {
110
110
  run_script "scripts/ops/chaos.sh" "Chaos engineering script" "$@"
111
111
  }
112
112
 
113
+ cmd_teleport() {
114
+ run_script "scripts/ops/teleport.sh" "Teleport script" "$@"
115
+ }
116
+
117
+ cmd_secret_audit() {
118
+ run_script "scripts/diagnostics/secret-governance.sh" "Secret governance script" "$@"
119
+ }
120
+
113
121
  cmd_bundle() {
114
122
  run_script "scripts/ops/bundle.sh" "Offline bundle script" "$@"
115
123
  }
@@ -207,6 +215,14 @@ case "${1:-}" in
207
215
  shift
208
216
  cmd_chaos "$@"
209
217
  ;;
218
+ teleport)
219
+ shift
220
+ cmd_teleport "$@"
221
+ ;;
222
+ secret-audit)
223
+ shift
224
+ cmd_secret_audit "$@"
225
+ ;;
210
226
  bundle)
211
227
  shift
212
228
  cmd_bundle "$@"
@@ -102,7 +102,10 @@ cmd_fleet_status() {
102
102
  local last_apply=""
103
103
  local state_log="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/dot.log"
104
104
  if [[ -f "$state_log" ]]; then
105
- last_apply="$(grep 'apply' "$state_log" 2>/dev/null | tail -1 | sed -n 's/^\[\([^]]*\)\].*/\1/p')"
105
+ # `set -euo pipefail` at the top of this script kills the whole
106
+ # command when grep matches nothing (rc=1). Wrap the pipeline so
107
+ # `last_apply` cleanly becomes empty and the UI still renders.
108
+ last_apply="$(grep 'apply' "$state_log" 2>/dev/null | tail -1 | sed -n 's/^\[\([^]]*\)\].*/\1/p' || true)"
106
109
  fi
107
110
 
108
111
  if [[ "$json_mode" -eq 1 ]]; then
@@ -202,9 +205,13 @@ cmd_fleet_drift() {
202
205
  local count="${1:-20}"
203
206
  tail -n "$count" "$_DRIFT_HISTORY_FILE" | while IFS= read -r line; do
204
207
  local time status file_count
205
- time="$(printf '%s' "$line" | jq -r '.time' 2>/dev/null || echo "?")"
206
- status="$(printf '%s' "$line" | jq -r '.status' 2>/dev/null || echo "?")"
207
- file_count="$(printf '%s' "$line" | jq '.files | length' 2>/dev/null || echo 0)"
208
+ # One jq per line (was three: .time, .status, .files|length).
209
+ IFS=$'\t' read -r time status file_count < <(
210
+ printf '%s' "$line" | jq -r '[.time, .status, (.files | length)] | @tsv' 2>/dev/null
211
+ )
212
+ [[ -n "$time" ]] || time="?"
213
+ [[ -n "$status" ]] || status="?"
214
+ [[ -n "$file_count" ]] || file_count=0
208
215
  if [[ "$status" == "clean" ]]; then
209
216
  ui_ok "$time" "clean"
210
217
  else
@@ -19,6 +19,33 @@ dot_ui_command_banner "Lint" "${1:-}"
19
19
  SHELLCHECK_ARGS=(--severity=error -e SC1091 -e SC2030 -e SC2031)
20
20
  SHFMT_ARGS=(-i 2 -ci)
21
21
 
22
+ # True only if the file's shebang names a shell that both shellcheck and shfmt
23
+ # can process. The old `grep -qiE 'shell|bash|sh'` matched loosely — "sh" hit
24
+ # "fish", and `file` output pulled in python3 scripts — so non-shell files
25
+ # landed in the lint set and produced spurious SC1071 / shfmt parse "errors".
26
+ _lint_is_shell() {
27
+ local first interp
28
+ first="$(head -1 "$1" 2>/dev/null)"
29
+ case "$first" in '#!'*) ;; *) return 1 ;; esac
30
+ interp="${first#\#!}"
31
+ interp="${interp#"${interp%%[![:space:]]*}"}" # ltrim
32
+ case "$interp" in
33
+ */env[[:space:]]*)
34
+ interp="${interp#*/env}"
35
+ interp="${interp#"${interp%%[![:space:]]*}"}"
36
+ interp="${interp%%[[:space:]]*}"
37
+ ;;
38
+ *)
39
+ interp="${interp%%[[:space:]]*}"
40
+ interp="${interp##*/}"
41
+ ;;
42
+ esac
43
+ case "$interp" in
44
+ sh | bash | dash | ksh | mksh | ash) return 0 ;;
45
+ *) return 1 ;;
46
+ esac
47
+ }
48
+
22
49
  cmd_lint() {
23
50
  local mode="${1:-all}"
24
51
  local src_dir
@@ -40,11 +67,10 @@ cmd_lint() {
40
67
  chezmoi_src="$(resolve_chezmoi_source_dir)"
41
68
  [[ -z "$chezmoi_src" ]] && chezmoi_src="$src_dir"
42
69
 
43
- # dot_local/bin/executable_* scripts (shell scripts only)
70
+ # dot_local/bin/executable_* scripts shell scripts only (skip python/zsh/etc
71
+ # by inspecting the shebang, so shellcheck/shfmt never see files they can't parse).
44
72
  while IFS= read -r -d '' f; do
45
- if file "$f" 2>/dev/null | grep -qiE 'shell|bash|sh'; then
46
- files+=("$f")
47
- elif head -1 "$f" 2>/dev/null | grep -qE '^#!.*(bash|sh)'; then
73
+ if _lint_is_shell "$f"; then
48
74
  files+=("$f")
49
75
  fi
50
76
  done < <(find "$chezmoi_src/dot_local/bin" -name 'executable_*' -type f -print0 2>/dev/null)
@@ -69,17 +95,16 @@ cmd_lint() {
69
95
  if has_command shellcheck; then
70
96
  ui_section "ShellCheck"
71
97
  echo ""
72
- local sc_fail=0
73
- for f in "${files[@]}"; do
74
- if ! shellcheck "${SHELLCHECK_ARGS[@]}" "$f" 2>/dev/null; then
75
- sc_fail=$((sc_fail + 1))
76
- fi
77
- done
78
- if [[ "$sc_fail" -eq 0 ]]; then
98
+ # One shellcheck invocation over all files (was one fork per file).
99
+ # gcc format is one line per issue, so failing files = unique paths.
100
+ local sc_out
101
+ sc_out="$(shellcheck -f gcc "${SHELLCHECK_ARGS[@]}" "${files[@]}" 2>/dev/null || true)"
102
+ if [[ -z "$sc_out" ]]; then
79
103
  ui_ok "shellcheck" "$total files clean"
80
104
  else
81
- sc_errors=$sc_fail
82
- ui_err "shellcheck" "$sc_fail file(s) with errors"
105
+ printf '%s\n' "$sc_out"
106
+ sc_errors=$(printf '%s\n' "$sc_out" | cut -d: -f1 | sort -u | grep -c .)
107
+ ui_err "shellcheck" "$sc_errors file(s) with errors"
83
108
  fi
84
109
  echo ""
85
110
  else
@@ -91,17 +116,15 @@ cmd_lint() {
91
116
  if has_command shfmt; then
92
117
  ui_section "shfmt"
93
118
  echo ""
94
- local fmt_fail=0
95
- for f in "${files[@]}"; do
96
- if ! shfmt "${SHFMT_ARGS[@]}" -d "$f" >/dev/null 2>&1; then
97
- fmt_fail=$((fmt_fail + 1))
98
- fi
99
- done
100
- if [[ "$fmt_fail" -eq 0 ]]; then
119
+ # `shfmt -l` lists files needing formatting in one invocation
120
+ # (was `shfmt -d` per file).
121
+ local fmt_list
122
+ fmt_list="$(shfmt "${SHFMT_ARGS[@]}" -l "${files[@]}" 2>/dev/null || true)"
123
+ if [[ -z "$fmt_list" ]]; then
101
124
  ui_ok "shfmt" "$total files formatted correctly"
102
125
  else
103
- fmt_errors=$fmt_fail
104
- ui_err "shfmt" "$fmt_fail file(s) need formatting"
126
+ fmt_errors=$(printf '%s\n' "$fmt_list" | grep -c .)
127
+ ui_err "shfmt" "$fmt_errors file(s) need formatting"
105
128
  fi
106
129
  echo ""
107
130
  else
@@ -118,17 +141,19 @@ cmd_lint() {
118
141
  fi
119
142
  ui_section "Auto-fixing with shfmt"
120
143
  echo ""
121
- local fixed=0
122
- for f in "${files[@]}"; do
123
- if ! shfmt "${SHFMT_ARGS[@]}" -d "$f" >/dev/null 2>&1; then
144
+ # Find files needing formatting in one `shfmt -l` pass, then only
145
+ # rewrite that (usually small) set — was `shfmt -d` per file.
146
+ local fmt_list fixed=0
147
+ fmt_list="$(shfmt "${SHFMT_ARGS[@]}" -l "${files[@]}" 2>/dev/null || true)"
148
+ if [[ -z "$fmt_list" ]]; then
149
+ ui_ok "shfmt" "All files already formatted"
150
+ else
151
+ while IFS= read -r f; do
152
+ [[ -n "$f" ]] || continue
124
153
  shfmt "${SHFMT_ARGS[@]}" -w "$f"
125
154
  ui_ok "fixed" "${f#"$src_dir/"}"
126
155
  fixed=$((fixed + 1))
127
- fi
128
- done
129
- if [[ "$fixed" -eq 0 ]]; then
130
- ui_ok "shfmt" "All files already formatted"
131
- else
156
+ done <<<"$fmt_list"
132
157
  ui_ok "shfmt" "$fixed file(s) reformatted"
133
158
  fi
134
159
  echo ""
@@ -102,7 +102,13 @@ _registry_fetch() {
102
102
  fi
103
103
  local tmp
104
104
  tmp="$(mktemp "${cache_file}.XXXXXX")"
105
- if ! curl -fsSL --max-time 15 -o "$tmp" "$url"; then
105
+ # `-fsSL -o file` should be silent, but with some upstreams (e.g.
106
+ # GitHub Pages) curl still writes a stray newline to stdout. That
107
+ # newline leaks into the caller's `$(_registry_fetch)` and later
108
+ # into `jq FILE` as a two-argument invocation
109
+ # (`jq \n /path/to/file`), producing a confusing
110
+ # "Could not open file" error. Silence stdout explicitly.
111
+ if ! curl -fsSL --max-time 15 -o "$tmp" "$url" >/dev/null; then
106
112
  rm -f "$tmp"
107
113
  if [[ -s "$cache_file" ]]; then
108
114
  ui_warn "registry" "fetch failed; using stale cache at $cache_file"
@@ -152,26 +152,80 @@ cmd_secrets_list() {
152
152
  }
153
153
 
154
154
  cmd_env_load() {
155
- local bucket="${1:-ai}" data_file key value count=0
155
+ # Usage: dot secrets load [<bucket>] [--shell posix|fish|nu]
156
+ # Emits shell code to set the bucket's secrets as env vars, in the target
157
+ # shell's dialect so it works beyond POSIX:
158
+ # posix (default): eval "$(dot secrets load ai)"
159
+ # fish: dot secrets load ai --shell fish | source
160
+ # nu: load-env (dot secrets load ai --shell nu | from nuon)
161
+ local bucket="ai" dialect="posix" data_file key value count=0
162
+ local -a keys=() values=()
163
+ while [[ $# -gt 0 ]]; do
164
+ case "$1" in
165
+ --shell)
166
+ dialect="${2:-posix}"
167
+ shift 2
168
+ ;;
169
+ --shell=*)
170
+ dialect="${1#*=}"
171
+ shift
172
+ ;;
173
+ -*) die "Unknown flag: $1 (usage: dot secrets load [<bucket>] [--shell posix|fish|nu])" ;;
174
+ *)
175
+ bucket="$1"
176
+ shift
177
+ ;;
178
+ esac
179
+ done
180
+
156
181
  data_file="$(dot_data_file)"
157
182
  [[ -f "$data_file" ]] || die "Missing data file: $data_file"
158
183
 
159
184
  while IFS= read -r key; do
160
185
  value="$(dot_secrets_get "$key" || true)"
161
186
  if [[ -n "$value" ]]; then
162
- # shell-safe export lines intended for: eval "$(dot env load <bucket>)"
163
- printf "export %s=%q\n" "$key" "$value"
187
+ keys+=("$key")
188
+ values+=("$value")
164
189
  count=$((count + 1))
165
190
  fi
166
191
  done < <(dot_secrets_bucket_keys "$data_file" "$bucket")
167
192
 
168
- if [[ "$count" -eq 0 ]]; then
169
- die "No secrets loaded for bucket: $bucket"
170
- fi
193
+ [[ "$count" -gt 0 ]] || die "No secrets loaded for bucket: $bucket"
194
+
195
+ local i k v esc
196
+ case "$dialect" in
197
+ posix | sh | bash | zsh)
198
+ for i in "${!keys[@]}"; do
199
+ printf 'export %s=%q\n' "${keys[$i]}" "${values[$i]}"
200
+ done
201
+ ;;
202
+ fish)
203
+ for i in "${!keys[@]}"; do
204
+ v="${values[$i]}"
205
+ esc="${v//\\/\\\\}"
206
+ esc="${esc//\'/\\\'}" # fish single-quote escaping
207
+ printf "set -gx %s '%s'\n" "${keys[$i]}" "$esc"
208
+ done
209
+ ;;
210
+ nu | nushell)
211
+ # Emit a NUON record for: load-env (dot secrets load <bucket> --shell nu | from nuon)
212
+ printf '{\n'
213
+ for i in "${!keys[@]}"; do
214
+ v="${values[$i]}"
215
+ esc="${v//\\/\\\\}"
216
+ esc="${esc//\"/\\\"}" # double-quote escaping
217
+ printf ' "%s": "%s"\n' "${keys[$i]}" "$esc"
218
+ done
219
+ printf '}\n'
220
+ ;;
221
+ *)
222
+ die "Unknown shell dialect: $dialect (want: posix|fish|nu)"
223
+ ;;
224
+ esac
171
225
  }
172
226
 
173
227
  cmd_secrets_load() {
174
- cmd_env_load "${1:-ai}"
228
+ cmd_env_load "$@"
175
229
  }
176
230
 
177
231
  # Dispatch
@@ -40,6 +40,10 @@ cmd_usb_safety() {
40
40
  run_script "scripts/security/usb-safety.sh" "USB safety script" "$@"
41
41
  }
42
42
 
43
+ cmd_policy() {
44
+ run_script "scripts/security/enforce-policies.sh" "Policy enforcement script" "$@"
45
+ }
46
+
43
47
  # Dispatch
44
48
  case "${1:-}" in
45
49
  backup)
@@ -70,6 +74,10 @@ case "${1:-}" in
70
74
  shift
71
75
  cmd_usb_safety "$@"
72
76
  ;;
77
+ policy)
78
+ shift
79
+ cmd_policy "$@"
80
+ ;;
73
81
  *)
74
82
  echo "Unknown security command: ${1:-}" >&2
75
83
  exit 1
@@ -189,8 +189,17 @@ show_language_package_managers() {
189
189
  echo " pip: $(pip3 --version | cut -d' ' -f2)"
190
190
  fi
191
191
  if has_command pipx; then
192
- local pipx_installed
193
- pipx_installed=$(pipx list --short 2>/dev/null | wc -l | tr -d ' ')
192
+ local pipx_installed pipx_list_out
193
+ # `pipx list --short` returns non-zero when any installed package
194
+ # has a broken interpreter (common). Under `set -euo pipefail` a
195
+ # naive `local x=$(pipx …)` cascades and kills `dot packages`
196
+ # mid-output. Capture the pipeline separately so we can fall back
197
+ # cleanly without stray "N/A" lines from pipe-with-|| tricks.
198
+ if pipx_list_out="$(pipx list --short 2>/dev/null)"; then
199
+ pipx_installed="$(printf '%s' "$pipx_list_out" | wc -l | tr -d ' ')"
200
+ else
201
+ pipx_installed="N/A"
202
+ fi
194
203
  echo " pipx: $(pipx --version)"
195
204
  echo " Installed: $pipx_installed"
196
205
  fi
@@ -126,7 +126,7 @@ function Invoke-DotHelp {
126
126
  Write-Host ''
127
127
  Write-Host " Version v$version"
128
128
  Write-Host " Repo https://github.com/sebastienrousseau/dotfiles"
129
- Write-Host " Docs https://github.com/sebastienrousseau/dotfiles/blob/master/docs/manual/"
129
+ Write-Host " Docs https://github.com/sebastienrousseau/dotfiles/blob/main/docs/manual/"
130
130
  Write-Host ''
131
131
  Write-Host 'Native PowerShell commands (no bash required):'
132
132
  Write-Host ''
@@ -141,6 +141,6 @@ if [[ $FAILED -eq 1 ]]; then
141
141
  printf '%b\\n' " (Use --no-verify to bypass if absolutely necessary)"
142
142
  exit 1
143
143
  else
144
- printf '%b\n' "${GREEN}${BOLD}✅ Audit passed.${NC} v0.2.508 standards maintained."
144
+ printf '%b\n' "${GREEN}${BOLD}✅ Audit passed.${NC} v0.2.511 standards maintained."
145
145
  exit 0
146
146
  fi
@@ -31,6 +31,12 @@ detect_pkg_manager() {
31
31
  echo "dnf"
32
32
  elif command -v pacman >/dev/null 2>&1; then
33
33
  echo "pacman"
34
+ elif command -v zypper >/dev/null 2>&1; then
35
+ echo "zypper"
36
+ elif command -v apk >/dev/null 2>&1; then
37
+ echo "apk"
38
+ elif command -v pkg >/dev/null 2>&1; then
39
+ echo "pkg"
34
40
  elif command -v nix-env >/dev/null 2>&1; then
35
41
  echo "nix"
36
42
  else
@@ -66,6 +72,28 @@ install_package() {
66
72
  fi
67
73
  sudo pacman -S --noconfirm --quiet "$pkg" >/dev/null 2>&1
68
74
  ;;
75
+ zypper)
76
+ if ! command -v sudo >/dev/null 2>&1; then
77
+ log_error "sudo not found — cannot install '$pkg' via zypper"
78
+ return 1
79
+ fi
80
+ sudo zypper --quiet install -y "$pkg" >/dev/null 2>&1
81
+ ;;
82
+ apk)
83
+ if command -v sudo >/dev/null 2>&1; then
84
+ sudo apk add --quiet "$pkg" >/dev/null 2>&1
85
+ else
86
+ apk add --quiet "$pkg" >/dev/null 2>&1
87
+ fi
88
+ ;;
89
+ pkg)
90
+ # FreeBSD/OpenBSD/NetBSD — install as root when available.
91
+ if command -v sudo >/dev/null 2>&1; then
92
+ sudo pkg install -y "$pkg" >/dev/null 2>&1
93
+ else
94
+ pkg install -y "$pkg" >/dev/null 2>&1
95
+ fi
96
+ ;;
69
97
  nix) nix-env -iA "nixpkgs.$pkg" >/dev/null 2>&1 ;;
70
98
  *)
71
99
  log_error "No supported package manager found. Install '$pkg' manually."
@@ -7,10 +7,10 @@
7
7
 
8
8
  set -euo pipefail
9
9
 
10
- TARGET="$1"
10
+ TARGET="${1:-}"
11
11
 
12
12
  if [[ -z "$TARGET" ]]; then
13
- echo "Usage: $0 user@host"
13
+ echo "Usage: dot teleport user@host"
14
14
  exit 1
15
15
  fi
16
16