@sebastienrousseau/dotfiles 0.2.507 → 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 (73) hide show
  1. package/CHANGELOG.md +93 -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/chezmoi-apply.sh +42 -8
  68. package/scripts/ops/chezmoi-update.sh +14 -0
  69. package/scripts/ops/heal-tools.sh +28 -0
  70. package/scripts/ops/teleport.sh +2 -2
  71. package/scripts/qa/examples-coverage.sh +94 -0
  72. package/scripts/version-sync.sh +29 -2
  73. package/docs/_config.yml +0 -59
@@ -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.507 standards maintained."
144
+ printf '%b\n' "${GREEN}${BOLD}✅ Audit passed.${NC} v0.2.510 standards maintained."
145
145
  exit 0
146
146
  fi
@@ -19,9 +19,11 @@ export DOT_COMMAND="apply"
19
19
  # (macOS) expanding an empty array under `set -u` is an "unbound
20
20
  # variable" error, which would fire on every clean `dot sync`.
21
21
  _TMPFILES=()
22
+ _LOCK_DIR=""
22
23
  cleanup() {
23
24
  set +u
24
25
  rm -f "${_TMPFILES[@]}" 2>/dev/null
26
+ [[ -n "$_LOCK_DIR" ]] && rmdir "$_LOCK_DIR" 2>/dev/null
25
27
  set -u
26
28
  }
27
29
  trap cleanup EXIT
@@ -39,7 +41,9 @@ Environment Variables:
39
41
  DOTFILES_CHEZMOI_APPLY_FLAGS Extra flags for chezmoi apply
40
42
  DOTFILES_CHEZMOI_VERBOSE=1 Enable verbose output
41
43
  DOTFILES_CHEZMOI_KEEP_GOING=1 Continue on errors
42
- DOTFILES_NONINTERACTIVE=1 Force non-interactive mode
44
+ DOTFILES_NONINTERACTIVE=1 Skip interactive menus (AI provider installer)
45
+ DOTFILES_INTERACTIVE_APPLY=1 Re-enable chezmoi overwrite prompts
46
+ (apply is unattended/--force by default)
43
47
  DOTFILES_ALIAS_STRICT_MODE=1 Run alias governance checks
44
48
  DOTFILES_SNAPSHOT_ON_APPLY=1 Create baseline snapshot (default)
45
49
  DOTFILES_POST_APPLY_REPAIR=1 Run post-apply repairs (default)
@@ -67,25 +71,55 @@ fi
67
71
  has_flag() {
68
72
  local needle="$1"
69
73
  local arg
74
+ # Guard the expansion: on bash 3.2 (macOS) iterating an empty array
75
+ # under `set -u` is an unbound-variable error.
76
+ [[ ${#args[@]} -eq 0 ]] && return 1
70
77
  for arg in "${args[@]}"; do
71
78
  [[ "$arg" == "$needle" ]] && return 0
72
79
  done
73
80
  return 1
74
81
  }
75
82
 
76
- # In non-interactive runs, prevent TTY prompts from blocking apply.
77
- if [[ "${DOTFILES_NONINTERACTIVE:-0}" == "1" ]] && ! has_flag "--force"; then
83
+ # Apply unattended by default, like an OS package manager. chezmoi is
84
+ # always run below under `gum spin` or with its output captured, so it
85
+ # never has a controlling TTY; its "<file> has changed since chezmoi last
86
+ # wrote it" confirmation prompt therefore cannot be answered and aborts
87
+ # the run with "could not open a new TTY". Passing --force applies the
88
+ # canonical source without prompting, so local drift to *managed* files
89
+ # yields to the source — exactly how `apt`/system updates behave. Keep
90
+ # machine-specific tweaks in the unmanaged ~/.zshrc.local or
91
+ # ~/.config/zsh/rc.d.local/*.zsh, which chezmoi never overwrites.
92
+ # Opt back into chezmoi's prompts with DOTFILES_INTERACTIVE_APPLY=1.
93
+ if [[ "${DOTFILES_INTERACTIVE_APPLY:-0}" != "1" ]] && ! has_flag "--force"; then
78
94
  args+=("--force")
79
95
  fi
80
96
 
97
+ # Whether interactive menus (the optional AI-provider installer below) may
98
+ # prompt. Suppressed without a TTY, under CI, or when non-interactive is
99
+ # requested — so unattended runs never hang waiting on input.
100
+ INTERACTIVE=1
101
+ if [[ "${DOTFILES_NONINTERACTIVE:-0}" == "1" ]] || [[ -n "${CI:-}" ]] || [[ ! -t 0 ]] || [[ ! -t 1 ]]; then
102
+ INTERACTIVE=0
103
+ fi
104
+
81
105
  ui_init
82
106
 
83
- # Prevent concurrent execution
84
- LOCK_FILE="${XDG_RUNTIME_DIR:-/tmp}/dotfiles-chezmoi-apply.lock"
85
- exec 9>"$LOCK_FILE"
86
- if ! flock -n 9; then
107
+ # Prevent concurrent execution. flock(1) is Linux-only — it does not exist
108
+ # on macOS, where `! flock` previously took the "already running" branch and
109
+ # made `dot apply` a silent no-op. Use flock where present, otherwise fall
110
+ # back to an atomic mkdir lock (portable to macOS/BSD).
111
+ _lock_base="${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/dotfiles-chezmoi-apply"
112
+ if command -v flock >/dev/null 2>&1; then
113
+ exec 9>"${_lock_base}.lock"
114
+ if ! flock -n 9; then
115
+ ui_warn "Already running" "Another instance is active"
116
+ exit 0
117
+ fi
118
+ elif ! mkdir "${_lock_base}.lock.d" 2>/dev/null; then
87
119
  ui_warn "Already running" "Another instance is active"
88
120
  exit 0
121
+ else
122
+ _LOCK_DIR="${_lock_base}.lock.d" # removed by cleanup() on exit
89
123
  fi
90
124
 
91
125
  run_step() {
@@ -187,7 +221,7 @@ for _entry in "${_AI_PROVIDERS[@]}"; do
187
221
  fi
188
222
  done
189
223
 
190
- if [[ ${#_ai_missing[@]} -gt 0 ]] && [[ "${DOTFILES_NONINTERACTIVE:-0}" != "1" ]]; then
224
+ if [[ ${#_ai_missing[@]} -gt 0 ]] && [[ "$INTERACTIVE" == "1" ]]; then
191
225
  if command -v mise &>/dev/null; then
192
226
  echo ""
193
227
  _ai_install_action=""
@@ -14,6 +14,20 @@ if [[ "${DOTFILES_CHEZMOI_VERBOSE:-0}" = "1" ]]; then
14
14
  args+=("--verbose")
15
15
  fi
16
16
 
17
+ # Apply unattended by default (see chezmoi-apply.sh). `chezmoi update` runs
18
+ # `apply` internally, so without --force it blocks on the "<file> has
19
+ # changed since chezmoi last wrote it" prompt — and `dot update` is often
20
+ # run from cron/CI/--async with no TTY, where that prompt aborts the run.
21
+ # --force makes updates land like an OS package-manager update; opt back
22
+ # into prompting with DOTFILES_INTERACTIVE_APPLY=1.
23
+ if [[ "${DOTFILES_INTERACTIVE_APPLY:-0}" != "1" ]]; then
24
+ _has_force=0
25
+ if [[ ${#args[@]} -gt 0 ]]; then
26
+ for _a in "${args[@]}"; do [[ "$_a" == "--force" ]] && _has_force=1; done
27
+ fi
28
+ [[ "$_has_force" == "0" ]] && args+=("--force")
29
+ fi
30
+
17
31
  ASYNC=false
18
32
  if [[ "${DOTFILES_ASYNC_UPDATE:-0}" = "1" ]]; then
19
33
  ASYNC=true
@@ -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