@sebastienrousseau/dotfiles 0.2.508 → 0.2.510

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 (71) hide show
  1. package/CHANGELOG.md +30 -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/guides/INSTALL.md +3 -3
  7. package/docs/index.md +116 -27
  8. package/docs/interop/A2A.md +2 -2
  9. package/docs/interop/POWERSHELL.md +4 -4
  10. package/docs/manual/00-introduction.md +3 -3
  11. package/docs/manual/01-concepts/02-trust-model.md +3 -3
  12. package/docs/manual/01-concepts/04-fleet.md +1 -1
  13. package/docs/manual/02-tutorials/01-first-install.md +1 -1
  14. package/docs/manual/02-tutorials/05-deploy-fleet.md +2 -2
  15. package/docs/manual/03-reference/02-config-files.md +1 -1
  16. package/docs/manual/03-reference/04-templates.md +1 -1
  17. package/docs/manual/04-cookbook/01-recipes.md +1 -1
  18. package/docs/manual/04-cookbook/02-troubleshooting.md +1 -1
  19. package/docs/manual/05-appendices/B-security-checklist.md +1 -1
  20. package/docs/manual/command-index.md +76 -1
  21. package/docs/manual/index.md +1 -1
  22. package/docs/operations/ARCHITECTURE_ROADMAP.md +145 -0
  23. package/docs/operations/CI_CADENCE.md +2 -2
  24. package/docs/operations/COMPLETIONS.md +2 -2
  25. package/docs/operations/COVERAGE.md +4 -4
  26. package/docs/operations/DRIFT.md +1 -1
  27. package/docs/operations/MAINTENANCE.md +6 -6
  28. package/docs/operations/PERFORMANCE.md +6 -6
  29. package/docs/operations/RELIABILITY.md +1 -1
  30. package/docs/operations/ROADMAP_V0_2_503.md +1 -1
  31. package/docs/operations/TRACEABILITY.md +2 -0
  32. package/docs/operations/TRUSTED_AGENT_WORKSTATION.md +6 -6
  33. package/docs/operations/VERSION_SYNC.md +3 -3
  34. package/docs/reference/UTILS.md +82 -0
  35. package/docs/security/AUDIT_BYPASS.md +3 -3
  36. package/docs/security/AUTOMATION_SECRETS.md +1 -1
  37. package/docs/security/CI_PINNING.md +10 -10
  38. package/docs/security/COMMIT_SIGNING.md +10 -10
  39. package/docs/security/DEPS_DEV_EXCEPTIONS.md +4 -4
  40. package/docs/security/DISCLOSURE.md +3 -3
  41. package/docs/security/INCIDENT_RESPONSE.md +1 -1
  42. package/docs/security/INSTALL_VERIFICATION.md +2 -2
  43. package/docs/security/MCP_POLICY.md +4 -4
  44. package/docs/security/SCORECARD.md +7 -7
  45. package/docs/security/SECURITY_CHECKLIST.md +1 -1
  46. package/docs/security/SHELL_EXEMPTIONS.md +2 -2
  47. package/docs/security/SOUP_REGISTER.md +4 -4
  48. package/docs/stylesheets/extra.css +444 -0
  49. package/docs/themes/hero-shot.svg +1 -1
  50. package/install.sh +17 -6
  51. package/package.json +1 -1
  52. package/scripts/ci/check-copyright-headers.sh +1 -1
  53. package/scripts/ci/check-shell-preamble.sh +1 -1
  54. package/scripts/ci/guard-gitleaks-checkout.sh +1 -1
  55. package/scripts/dot/commands/agent.sh +7 -6
  56. package/scripts/dot/commands/ai.sh +8 -5
  57. package/scripts/dot/commands/appearance.sh +14 -1
  58. package/scripts/dot/commands/completion.sh +134 -0
  59. package/scripts/dot/commands/core.sh +8 -0
  60. package/scripts/dot/commands/diagnostics.sh +16 -0
  61. package/scripts/dot/commands/fleet.sh +7 -3
  62. package/scripts/dot/commands/lint.sh +55 -30
  63. package/scripts/dot/commands/secrets.sh +61 -7
  64. package/scripts/dot/commands/security.sh +8 -0
  65. package/scripts/dot/powershell/Dot.psm1 +1 -1
  66. package/scripts/git-hooks/pre-commit-audit.sh +1 -1
  67. package/scripts/ops/heal-tools.sh +28 -0
  68. package/scripts/ops/teleport.sh +2 -2
  69. package/scripts/qa/examples-coverage.sh +94 -0
  70. package/scripts/version-sync.sh +29 -2
  71. package/docs/_config.yml +0 -59
package/install.sh CHANGED
@@ -2,9 +2,20 @@
2
2
  # SPDX-License-Identifier: MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Universal Dotfiles Installer (Zero-Dependency)
5
- # Usage: bash -c "$(curl -fsSL https://raw.githubusercontent.com/sebastienrousseau/dotfiles/master/install.sh)"
5
+ # Usage: bash -c "$(curl -fsSL https://raw.githubusercontent.com/sebastienrousseau/dotfiles/main/install.sh)"
6
6
  # (or ./install.sh locally)
7
7
 
8
+ # This installer uses bash features (set -o pipefail, arrays, [[ ]]). If it is
9
+ # run under a POSIX/other shell — e.g. `sh install.sh` or piped to `sh` where
10
+ # /bin/sh is dash — fail fast with a clear message instead of the cryptic
11
+ # "set: Illegal option -o pipefail" from the next line. Kept strictly POSIX so
12
+ # it parses in any shell before bash takes over.
13
+ if [ -z "${BASH_VERSION:-}" ]; then
14
+ echo "install.sh requires bash. Run: bash install.sh" >&2
15
+ echo " or: bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/sebastienrousseau/dotfiles/main/install.sh)\"" >&2
16
+ exit 1
17
+ fi
18
+
8
19
  set -euo pipefail
9
20
 
10
21
  # Restrict the permissions of any file/dir we create during bootstrap.
@@ -60,7 +71,7 @@ show_help() {
60
71
  Usage: install.sh [version] [options]
61
72
 
62
73
  Arguments:
63
- version The version (tag or branch) to install (default: v0.2.508)
74
+ version The version (tag or branch) to install (default: v0.2.510)
64
75
 
65
76
  Options:
66
77
  --help Show this help message
@@ -76,7 +87,7 @@ EOF
76
87
  }
77
88
 
78
89
  main() {
79
- local version="v0.2.508"
90
+ local version="v0.2.510"
80
91
  local version_set=0
81
92
  local minimal=0
82
93
  local provision="${DOTFILES_PROVISION:-0}"
@@ -106,7 +117,7 @@ main() {
106
117
  # like `foobar` doesn't trigger a 30s+ network download attempt.
107
118
  # Caught by the install.sh fuzz harness (#881).
108
119
  if [[ ! "$arg" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+([-+][a-zA-Z0-9.-]+)?$ ]]; then
109
- error "Unrecognized positional argument '$arg' — expected a semver version (e.g. v0.2.508)."
120
+ error "Unrecognized positional argument '$arg' — expected a semver version (e.g. v0.2.510)."
110
121
  fi
111
122
  version="$arg"
112
123
  version_set=1
@@ -351,7 +362,7 @@ main() {
351
362
  # 6. Initialize & Apply
352
363
  step "Applying Configuration..."
353
364
 
354
- # ── Auto-migration for v0.2.508 reorg ─────────────────────────────────
365
+ # ── Auto-migration for v0.2.510 reorg ─────────────────────────────────
355
366
  # If the user is upgrading from a pre-0.2.503 install, run the
356
367
  # migration script BEFORE `chezmoi apply` so the reorg's source-
357
368
  # path moves don't cause chezmoi to delete deployed files.
@@ -360,7 +371,7 @@ main() {
360
371
  for migrate_src in "$SOURCE_DIR" "$LEGACY_SOURCE_DIR"; do
361
372
  migrate_script="$migrate_src/install/migrate/migrate-v0_2-to-v0_2_503.sh"
362
373
  if [[ -x "$migrate_script" ]]; then
363
- echo " Running v0.2.508 migration (idempotent; safe on fresh installs)..."
374
+ echo " Running v0.2.510 migration (idempotent; safe on fresh installs)..."
364
375
  "$migrate_script" || echo " migration exited non-zero — continuing apply"
365
376
  break
366
377
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sebastienrousseau/dotfiles",
3
- "version": "0.2.508",
3
+ "version": "0.2.510",
4
4
  "description": "The Trusted Shell Platform — Universal dotfiles managed by Chezmoi. Features Bash & Zsh for macOS, Linux & WSL. Rust modern tooling & enterprise-grade security.",
5
5
  "main": "install.sh",
6
6
  "bin": {
@@ -2,7 +2,7 @@
2
2
  # SPDX-License-Identifier: MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Compat shim: the canonical script moved to tools/ci/ during the repo reorg.
5
- # Pinned reusable workflows on master still reference the old path;
5
+ # Pinned reusable workflows on main still reference the old path;
6
6
  # this delegates so they keep working until the next pin bump.
7
7
  set -euo pipefail
8
8
  exec bash "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/tools/ci/check-copyright-headers.sh" "$@"
@@ -2,7 +2,7 @@
2
2
  # SPDX-License-Identifier: MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Compat shim: the canonical script moved to tools/ci/ during the repo reorg.
5
- # Pinned reusable workflows on master still reference the old path;
5
+ # Pinned reusable workflows on main still reference the old path;
6
6
  # this delegates so they keep working until the next pin bump.
7
7
  set -euo pipefail
8
8
  exec bash "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/tools/ci/check-shell-preamble.sh" "$@"
@@ -2,7 +2,7 @@
2
2
  # SPDX-License-Identifier: MIT
3
3
  # Copyright (c) 2015-2026 Sebastien Rousseau
4
4
  # Compat shim: the canonical script moved to tools/ci/ during the repo reorg.
5
- # Pinned reusable workflows on master still reference the old path;
5
+ # Pinned reusable workflows on main still reference the old path;
6
6
  # this delegates so they keep working until the next pin bump.
7
7
  set -euo pipefail
8
8
  exec bash "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/tools/ci/guard-gitleaks-checkout.sh" "$@"
@@ -88,12 +88,13 @@ _agent_apply_profile_env() {
88
88
  # Split declare-and-assign to avoid masking the field-getter's exit
89
89
  # code (SC2155). If the profile JSON is malformed, the assignment
90
90
  # now fails the function instead of silently exporting an empty value.
91
- local approval filesystem network mcp_profile max_steps
92
- approval="$(_agent_profile_field "$name" "approval")"
93
- filesystem="$(_agent_profile_field "$name" "filesystem")"
94
- network="$(_agent_profile_field "$name" "network")"
95
- mcp_profile="$(_agent_profile_field "$name" "mcpProfile")"
96
- max_steps="$(_agent_profile_field "$name" "maxSteps")"
91
+ local approval filesystem network mcp_profile max_steps tuple
92
+ # One jq read of all five policy fields (was five separate jq processes on
93
+ # every profile application). `|| return 1` preserves fail-on-malformed.
94
+ tuple="$(jq -r --arg name "$name" \
95
+ '.profiles[$name] | [.approval, .filesystem, .network, .mcpProfile, .maxSteps] | @tsv' \
96
+ "$(_agent_profiles_file)")" || return 1
97
+ IFS=$'\t' read -r approval filesystem network mcp_profile max_steps <<<"$tuple"
97
98
  export DOT_AGENT_APPROVAL="$approval"
98
99
  export DOT_AGENT_FILESYSTEM="$filesystem"
99
100
  export DOT_AGENT_NETWORK="$network"
@@ -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 "$@"
@@ -202,9 +202,13 @@ cmd_fleet_drift() {
202
202
  local count="${1:-20}"
203
203
  tail -n "$count" "$_DRIFT_HISTORY_FILE" | while IFS= read -r line; do
204
204
  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)"
205
+ # One jq per line (was three: .time, .status, .files|length).
206
+ IFS=$'\t' read -r time status file_count < <(
207
+ printf '%s' "$line" | jq -r '[.time, .status, (.files | length)] | @tsv' 2>/dev/null
208
+ )
209
+ [[ -n "$time" ]] || time="?"
210
+ [[ -n "$status" ]] || status="?"
211
+ [[ -n "$file_count" ]] || file_count=0
208
212
  if [[ "$status" == "clean" ]]; then
209
213
  ui_ok "$time" "clean"
210
214
  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 ""
@@ -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
@@ -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.510 standards maintained."
145
145
  exit 0
146
146
  fi