@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
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env bash
2
+ # Copyright (c) 2015-2026 Dotfiles. All rights reserved.
3
+ # shellcheck shell=bash
4
+ #
5
+ # scripts/dot/commands/init.sh
6
+ #
7
+ # `dot init <github-user>` — bootstrap a foreign dotfiles repo through
8
+ # this framework's harness. The §3 audit roadmap calls for an analogue
9
+ # to `chezmoi init <user>` that turns this tool from "one person's
10
+ # config" into a runtime for any dotfiles user.
11
+ #
12
+ # Behavior:
13
+ # dot init alice # clones github.com/alice/dotfiles via chezmoi,
14
+ # # applies through the dot harness
15
+ # dot init https://... # explicit URL
16
+ # dot init alice --dry-run # preview without writing
17
+ # dot init alice --no-apply # clone only; don't run chezmoi apply
18
+ #
19
+ # Safety:
20
+ # - Refuses to clobber an existing chezmoi source dir without --force.
21
+ # - Disclaims that the target's scripts will run with the user's
22
+ # privileges and prints the source URL for inspection.
23
+ # - Verifies the source URL is HTTPS (no plain git://) so MITM is harder.
24
+
25
+ set -euo pipefail
26
+
27
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28
+ # shellcheck source=../lib/ui.sh disable=SC1091
29
+ source "$SCRIPT_DIR/../lib/ui.sh"
30
+ # shellcheck source=../lib/utils.sh disable=SC1091
31
+ source "$SCRIPT_DIR/../lib/utils.sh"
32
+
33
+ _init_resolve_url() {
34
+ local arg="$1"
35
+ case "$arg" in
36
+ http://*)
37
+ printf "dot init: refusing plain HTTP (use HTTPS or git+ssh)\n" >&2
38
+ return 2
39
+ ;;
40
+ https://* | git@*:*)
41
+ # Trust full URLs as-is — these are the user's explicit intent.
42
+ printf "%s\n" "$arg"
43
+ ;;
44
+ */*)
45
+ # owner/repo shorthand: validate before constructing the URL so
46
+ # a crafted argument can't smuggle shell metacharacters into
47
+ # downstream tooling (chezmoi init, git clone).
48
+ if [[ ! "$arg" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]]; then
49
+ printf "dot init: invalid owner/repo (only [A-Za-z0-9._-]+/[A-Za-z0-9._-]+ allowed): %s\n" "$arg" >&2
50
+ return 2
51
+ fi
52
+ printf "https://github.com/%s.git\n" "$arg"
53
+ ;;
54
+ *)
55
+ # Bare user shorthand: same character whitelist as the path
56
+ # segment of a GitHub URL.
57
+ if [[ ! "$arg" =~ ^[A-Za-z0-9._-]+$ ]]; then
58
+ printf "dot init: invalid user (only [A-Za-z0-9._-] allowed): %s\n" "$arg" >&2
59
+ return 2
60
+ fi
61
+ printf "https://github.com/%s/dotfiles.git\n" "$arg"
62
+ ;;
63
+ esac
64
+ }
65
+
66
+ cmd_init() {
67
+ local user_arg="" dry_run=0 apply=1 force=0
68
+ while [[ $# -gt 0 ]]; do
69
+ case "$1" in
70
+ --dry-run | -n)
71
+ dry_run=1
72
+ shift
73
+ ;;
74
+ --no-apply)
75
+ apply=0
76
+ shift
77
+ ;;
78
+ --force | -f)
79
+ force=1
80
+ shift
81
+ ;;
82
+ --help | -h)
83
+ cat <<EOF
84
+ Usage: dot init <github-user|owner/repo|url> [--dry-run] [--no-apply] [--force]
85
+
86
+ Bootstrap a foreign dotfiles repo through chezmoi + the dot harness.
87
+
88
+ Examples:
89
+ dot init alice # github.com/alice/dotfiles
90
+ dot init alice/configs # github.com/alice/configs
91
+ dot init https://example.com/repo.git
92
+ dot init alice --dry-run # preview only
93
+ dot init alice --no-apply # clone but don't apply
94
+
95
+ Flags:
96
+ --dry-run, -n Show resolved URL + target dir without cloning.
97
+ --no-apply Clone the source but skip 'chezmoi apply'.
98
+ --force, -f Overwrite an existing chezmoi source dir.
99
+ EOF
100
+ return 0
101
+ ;;
102
+ --)
103
+ shift
104
+ break
105
+ ;;
106
+ -*)
107
+ ui_err "Unknown flag" "$1"
108
+ return 1
109
+ ;;
110
+ *)
111
+ if [[ -n "$user_arg" ]]; then
112
+ ui_err "Too many arguments" "$1"
113
+ return 1
114
+ fi
115
+ user_arg="$1"
116
+ shift
117
+ ;;
118
+ esac
119
+ done
120
+
121
+ [[ -n "$user_arg" ]] || {
122
+ ui_err "init" "missing <user|repo|url>"
123
+ return 1
124
+ }
125
+
126
+ local url
127
+ url="$(_init_resolve_url "$user_arg")" || return $?
128
+
129
+ if ! command -v chezmoi >/dev/null 2>&1; then
130
+ ui_err "chezmoi" "not installed — run install.sh first"
131
+ return 127
132
+ fi
133
+
134
+ local source_dir
135
+ source_dir="${CHEZMOI_SOURCE_DIR:-$(chezmoi source-path 2>/dev/null || echo "$HOME/.local/share/chezmoi")}"
136
+
137
+ ui_header "Bootstrap dotfiles from $user_arg"
138
+ ui_info "Source URL" "$url"
139
+ ui_info "Target dir" "$source_dir"
140
+ local _apply_label="no"
141
+ [[ "$apply" -eq 1 ]] && _apply_label="yes"
142
+ ui_info "Apply after?" "$_apply_label"
143
+
144
+ if [[ "$dry_run" -eq 1 ]]; then
145
+ ui_ok "Dry-run" "no changes made"
146
+ return 0
147
+ fi
148
+
149
+ if [[ -e "$source_dir" ]] && [[ "$force" -ne 1 ]]; then
150
+ ui_err "Refusing" "$source_dir already exists; pass --force to overwrite"
151
+ return 1
152
+ fi
153
+
154
+ ui_warn "Trust" "The target repo's scripts will run with your user privileges."
155
+ ui_warn "Trust" "Inspect $url before proceeding if you don't know the author."
156
+ if [[ -t 0 ]] && [[ "${DOTFILES_NONINTERACTIVE:-0}" != "1" ]]; then
157
+ read -r -p " Continue? [y/N] " resp
158
+ case "$resp" in
159
+ [yY] | [yY][eE][sS]) ;;
160
+ *)
161
+ ui_info "Init" "aborted by user"
162
+ return 1
163
+ ;;
164
+ esac
165
+ fi
166
+
167
+ local chezmoi_args=(init --source "$source_dir")
168
+ [[ "$apply" -eq 1 ]] && chezmoi_args+=(--apply)
169
+ chezmoi_args+=("$url")
170
+
171
+ ui_info "chezmoi" "${chezmoi_args[*]}"
172
+ if chezmoi "${chezmoi_args[@]}"; then
173
+ ui_ok "Init" "complete"
174
+ if [[ "$apply" -eq 1 ]]; then
175
+ ui_info "Hint" "run 'dot doctor' to validate the new environment"
176
+ else
177
+ ui_info "Hint" "run 'chezmoi diff' then 'chezmoi apply' when ready"
178
+ fi
179
+ else
180
+ local rc=$?
181
+ ui_err "Init" "chezmoi exited $rc"
182
+ return "$rc"
183
+ fi
184
+ }
@@ -61,10 +61,13 @@ cmd_prewarm() {
61
61
  # Clear caches first
62
62
  ui_info "Cache" "Clearing shell initialization caches"
63
63
  local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}"
64
- rm -rf "$cache_dir/zsh"/*-init.zsh "$cache_dir/zsh"/*.zwc 2>/dev/null || true
65
- rm -rf "$cache_dir/bash"/*-init.bash 2>/dev/null || true
66
- rm -rf "$cache_dir/fish"/*-init.fish 2>/dev/null || true
67
- rm -rf "$cache_dir/nushell"/*.nu 2>/dev/null || true
64
+ # `find ... -delete` instead of `rm -rf $glob` so an unmatched literal
65
+ # pattern can't accidentally remove a real same-named directory, and
66
+ # so we don't pass `-r` on what should always be files.
67
+ [[ -d "$cache_dir/zsh" ]] && find "$cache_dir/zsh" -maxdepth 1 -type f \( -name '*-init.zsh' -o -name '*.zwc' \) -delete 2>/dev/null
68
+ [[ -d "$cache_dir/bash" ]] && find "$cache_dir/bash" -maxdepth 1 -type f -name '*-init.bash' -delete 2>/dev/null
69
+ [[ -d "$cache_dir/fish" ]] && find "$cache_dir/fish" -maxdepth 1 -type f -name '*-init.fish' -delete 2>/dev/null
70
+ [[ -d "$cache_dir/nushell" ]] && find "$cache_dir/nushell" -maxdepth 1 -type f -name '*.nu' -delete 2>/dev/null
68
71
  ui_info "Cache" "Cleared. Regenerating..."
69
72
  src_dir="$(resolve_source_dir)"
70
73
  if [ -n "$src_dir" ] && [ -f "$src_dir/scripts/ops/prewarm.sh" ]; then
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env bash
2
+ # Copyright (c) 2015-2026 Dotfiles. All rights reserved.
3
+ # shellcheck shell=bash
4
+ #
5
+ # scripts/dot/commands/registry.sh
6
+ #
7
+ # `dot registry` — initial scaffold for the dot module registry.
8
+ #
9
+ # §3 audit roadmap: ship a registry of reusable dotfile modules
10
+ # ("rust-dev-setup", "k8s-operator-laptop") to seed network effects.
11
+ # Hosted as a GitHub-Pages-indexed JSON file to keep ops cost near
12
+ # zero.
13
+ #
14
+ # Current state: SCAFFOLD ONLY. This file ships the CLI surface and
15
+ # the JSON contract (see _registry_default_url). The registry itself
16
+ # is empty; populating it is its own roadmap item.
17
+ #
18
+ # Subcommands:
19
+ # list Show modules in the configured registry
20
+ # search <q> Filter modules by keyword (name, description, tags)
21
+ # info <name> Print full metadata for a module
22
+ # install <name> Apply a module to the current workstation (stub —
23
+ # prints what would be installed; full implementation
24
+ # needs a sandboxed apply pipeline)
25
+ # url Show the active registry URL
26
+ # set-url <u> Override the registry URL (writes to user config)
27
+ #
28
+ # Registry JSON shape:
29
+ # {
30
+ # "version": 1,
31
+ # "updated": "2026-05-15T16:00:00Z",
32
+ # "modules": [
33
+ # { "name": "rust-dev-setup",
34
+ # "description": "Rust toolchain + cargo plugins + IDE config",
35
+ # "repo": "https://github.com/example/rust-dev-setup",
36
+ # "tags": ["rust", "dev", "language"],
37
+ # "maintainer": "alice@example.com",
38
+ # "version": "1.2.0",
39
+ # "sha256": "abc123..." }
40
+ # ]
41
+ # }
42
+
43
+ set -euo pipefail
44
+
45
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
46
+ # shellcheck source=../lib/ui.sh disable=SC1091
47
+ source "$SCRIPT_DIR/../lib/ui.sh"
48
+ # shellcheck source=../lib/utils.sh disable=SC1091
49
+ source "$SCRIPT_DIR/../lib/utils.sh"
50
+
51
+ _registry_default_url() {
52
+ printf '%s\n' "https://sebastienrousseau.github.io/dotfiles/registry.json"
53
+ }
54
+
55
+ _registry_config_file() {
56
+ printf '%s/dotfiles/registry.toml\n' "${XDG_CONFIG_HOME:-$HOME/.config}"
57
+ }
58
+
59
+ _registry_url() {
60
+ if [[ -n "${DOTFILES_REGISTRY_URL:-}" ]]; then
61
+ printf '%s\n' "$DOTFILES_REGISTRY_URL"
62
+ return
63
+ fi
64
+ local cfg
65
+ cfg="$(_registry_config_file)"
66
+ if [[ -f "$cfg" ]]; then
67
+ local u
68
+ u="$(awk -F'[ \t]*=[ \t]*' '/^url[ \t]*=/{gsub(/"/,"",$2); print $2; exit}' "$cfg")"
69
+ [[ -n "$u" ]] && {
70
+ printf '%s\n' "$u"
71
+ return
72
+ }
73
+ fi
74
+ _registry_default_url
75
+ }
76
+
77
+ _registry_cache_dir() {
78
+ printf '%s/dotfiles/registry\n' "${XDG_CACHE_HOME:-$HOME/.cache}"
79
+ }
80
+
81
+ _registry_fetch() {
82
+ local url cache_dir cache_file
83
+ url="$(_registry_url)"
84
+ cache_dir="$(_registry_cache_dir)"
85
+ cache_file="$cache_dir/index.json"
86
+ mkdir -p "$cache_dir"
87
+ # Refresh if older than 6h or missing.
88
+ local now mtime
89
+ now="$(date +%s)"
90
+ if [[ -s "$cache_file" ]]; then
91
+ if mtime="$(stat -f %m "$cache_file" 2>/dev/null || stat -c %Y "$cache_file" 2>/dev/null)"; then
92
+ if ((now - mtime < 21600)); then
93
+ printf '%s\n' "$cache_file"
94
+ return 0
95
+ fi
96
+ fi
97
+ fi
98
+ if ! command -v curl >/dev/null 2>&1; then
99
+ ui_err "registry" "curl not installed"
100
+ return 127
101
+ fi
102
+ local tmp
103
+ tmp="$(mktemp "${cache_file}.XXXXXX")"
104
+ if ! curl -fsSL --max-time 15 -o "$tmp" "$url"; then
105
+ rm -f "$tmp"
106
+ if [[ -s "$cache_file" ]]; then
107
+ ui_warn "registry" "fetch failed; using stale cache at $cache_file"
108
+ printf '%s\n' "$cache_file"
109
+ return 0
110
+ fi
111
+ ui_err "registry" "could not fetch $url"
112
+ return 1
113
+ fi
114
+ mv "$tmp" "$cache_file"
115
+ printf '%s\n' "$cache_file"
116
+ }
117
+
118
+ _registry_require_jq() {
119
+ command -v jq >/dev/null 2>&1 || {
120
+ ui_err "registry" "jq is required"
121
+ return 127
122
+ }
123
+ }
124
+
125
+ cmd_registry() {
126
+ local subcommand="${1:-list}"
127
+ shift || true
128
+
129
+ case "$subcommand" in
130
+ url)
131
+ printf '%s\n' "$(_registry_url)"
132
+ ;;
133
+ set-url)
134
+ local new_url="${1:-}"
135
+ [[ -n "$new_url" ]] || {
136
+ ui_err "set-url" "missing URL"
137
+ return 1
138
+ }
139
+ # Refuse non-HTTPS schemes. The registry index is unsigned today,
140
+ # so HTTPS is the only transport that gives us cert-pinned
141
+ # integrity. The `file://` exemption is for local testing only
142
+ # (the bench-script and unit tests use it).
143
+ if [[ ! "$new_url" =~ ^(https://|file://) ]]; then
144
+ ui_err "set-url" "registry URL must use https:// (or file:// for local testing) — got: $new_url"
145
+ return 1
146
+ fi
147
+ local cfg
148
+ cfg="$(_registry_config_file)"
149
+ mkdir -p "$(dirname "$cfg")"
150
+ # Atomic write so a concurrent invocation can't read a half-
151
+ # written file. Explicit if/else (avoid SC2015 A && B || C).
152
+ local _tmp
153
+ _tmp="$(mktemp "${cfg}.XXXXXX")"
154
+ if printf 'url = "%s"\n' "$new_url" >"$_tmp"; then
155
+ if ! mv "$_tmp" "$cfg"; then
156
+ rm -f "$_tmp"
157
+ ui_err "set-url" "failed to commit $cfg"
158
+ return 1
159
+ fi
160
+ else
161
+ rm -f "$_tmp"
162
+ ui_err "set-url" "failed to write $cfg"
163
+ return 1
164
+ fi
165
+ ui_ok "registry" "set to $new_url ($cfg)"
166
+ ;;
167
+ list)
168
+ _registry_require_jq || return $?
169
+ local index
170
+ index="$(_registry_fetch)" || return $?
171
+ ui_header "Registry modules"
172
+ ui_info "Source" "$(_registry_url)"
173
+ echo ""
174
+ if ! jq -e '.modules | length > 0' "$index" >/dev/null 2>&1; then
175
+ ui_warn "registry" "no modules published yet — see docs/operations/REGISTRY.md to contribute one"
176
+ return 0
177
+ fi
178
+ ui_table_begin "Module" "Version" "Description"
179
+ while IFS=$'\t' read -r name ver desc; do
180
+ ui_table_add "$name" "v$ver" "$desc"
181
+ done < <(jq -r '.modules[] | "\(.name)\t\(.version // "-")\t\(.description // "")"' "$index")
182
+ ui_table_end
183
+ ;;
184
+ search)
185
+ _registry_require_jq || return $?
186
+ local q="${1:-}"
187
+ [[ -n "$q" ]] || {
188
+ ui_err "search" "missing query"
189
+ return 1
190
+ }
191
+ local index
192
+ index="$(_registry_fetch)" || return $?
193
+ ui_header "Registry search: $q"
194
+ echo ""
195
+ ui_table_begin "Module" "Version" "Description"
196
+ while IFS=$'\t' read -r name ver desc; do
197
+ ui_table_add "$name" "v$ver" "$desc"
198
+ done < <(jq -r --arg q "$q" '
199
+ .modules[]
200
+ | select(
201
+ (.name // "" | ascii_downcase | contains($q | ascii_downcase)) or
202
+ (.description // "" | ascii_downcase | contains($q | ascii_downcase)) or
203
+ ((.tags // []) | map(ascii_downcase) | index($q | ascii_downcase))
204
+ )
205
+ | "\(.name)\t\(.version // "-")\t\(.description // "")"
206
+ ' "$index")
207
+ ui_table_end
208
+ ;;
209
+ info)
210
+ _registry_require_jq || return $?
211
+ local name="${1:-}"
212
+ [[ -n "$name" ]] || {
213
+ ui_err "info" "missing module name"
214
+ return 1
215
+ }
216
+ local index
217
+ index="$(_registry_fetch)" || return $?
218
+ local found
219
+ found="$(jq -r --arg n "$name" '.modules[] | select(.name == $n) | "OK"' "$index" 2>/dev/null)"
220
+ if [[ "$found" != "OK" ]]; then
221
+ ui_err "info" "module not found: $name"
222
+ return 1
223
+ fi
224
+ jq -r --arg n "$name" '.modules[] | select(.name == $n) | to_entries[] | "\(.key)\t\(.value | if type == "array" then join(", ") else tostring end)"' "$index" |
225
+ while IFS=$'\t' read -r key value; do
226
+ ui_ok "$key" "$value"
227
+ done
228
+ ;;
229
+ install)
230
+ local name="${1:-}"
231
+ [[ -n "$name" ]] || {
232
+ ui_err "install" "missing module name"
233
+ return 1
234
+ }
235
+ ui_warn "install" "scaffold only — full module installer is a roadmap item"
236
+ ui_info "install" "would fetch module $name from registry and apply it as a chezmoi sub-source"
237
+ return 0
238
+ ;;
239
+ --help | -h | help)
240
+ cat <<EOF
241
+ Usage: dot registry <subcommand>
242
+
243
+ Subcommands:
244
+ list List modules in the configured registry
245
+ search <q> Filter modules by keyword (name, description, tags)
246
+ info <name> Print metadata for a single module
247
+ install <name> Install a module (scaffold — see docs/operations/REGISTRY.md)
248
+ url Show the active registry URL
249
+ set-url <url> Override the registry URL (persists to user config)
250
+
251
+ Env overrides:
252
+ DOTFILES_REGISTRY_URL One-shot override of the registry URL.
253
+
254
+ Default registry: $(_registry_default_url)
255
+ EOF
256
+ ;;
257
+ *)
258
+ ui_err "Unknown subcommand" "$subcommand"
259
+ echo "Run 'dot registry --help' for usage." >&2
260
+ return 1
261
+ ;;
262
+ esac
263
+ }
@@ -353,7 +353,55 @@ cmd_env_mise() {
353
353
  exit 1
354
354
  fi
355
355
  case "${1:-list}" in
356
- list | ls) mise ls ;;
356
+ list | ls)
357
+ if has_command jq; then
358
+ ui_table_begin "Tool" "Version" "Source" "Requested"
359
+ while IFS=$'\t' read -r tool ver source req; do
360
+ ui_table_add "$tool" "$ver" "${source/$HOME/\~}" "$req"
361
+ done < <(mise ls --json 2>/dev/null | jq -r '
362
+ to_entries[] as $t
363
+ | $t.value[]
364
+ | [
365
+ $t.key,
366
+ (.version // "-"),
367
+ (.source.path // ""),
368
+ (.requested_version // "")
369
+ ]
370
+ | @tsv
371
+ ')
372
+ ui_table_end
373
+ else
374
+ mise ls
375
+ fi
376
+ # Surface orphan installs (tool versions on disk that no active config
377
+ # claims). `--dry-run-code` exits 1 iff there is something to prune;
378
+ # the call is fast (no I/O beyond reading the tracked-configs index).
379
+ if mise prune --dry-run-code --quiet >/dev/null 2>&1; then
380
+ :
381
+ else
382
+ echo ""
383
+ ui_warn "orphan installs" "tool versions on disk are not requested by any config"
384
+ ui_info "to clean" "run 'dot env prune' (dry-run) or 'dot env prune --yes' to commit"
385
+ fi
386
+ ;;
387
+ prune)
388
+ shift
389
+ local commit=0
390
+ for arg in "$@"; do
391
+ case "$arg" in
392
+ --yes | -y) commit=1 ;;
393
+ esac
394
+ done
395
+ if ((commit)); then
396
+ mise prune
397
+ else
398
+ ui_info "dry-run" "showing what 'mise prune' would remove (pass --yes to commit)"
399
+ echo ""
400
+ mise prune --dry-run
401
+ echo ""
402
+ ui_info "next" "re-run with 'dot env prune --yes' to actually uninstall"
403
+ fi
404
+ ;;
357
405
  install)
358
406
  shift
359
407
  # Validate tool names to prevent injection
@@ -31,7 +31,7 @@ _dotfiles_bento_render() {
31
31
 
32
32
  # Render a fixed-width card (42 cols) so output aligns in any terminal width
33
33
  printf "\n"
34
- printf " ${c_cyan}${c_bold}💎 D O T F I L E S${c_reset} ${c_slate}[v0.2.501]${c_reset}\n"
34
+ printf " ${c_cyan}${c_bold}💎 D O T F I L E S${c_reset} ${c_slate}[v0.2.502]${c_reset}\n"
35
35
  printf " ${c_slate}──────────────────────────────────────────${c_reset}\n"
36
36
 
37
37
  # Key system properties at a glance — answers "is my env healthy?" in 1 second
@@ -39,12 +39,22 @@ dot_host_os() {
39
39
  }
40
40
 
41
41
  # Convert host-native path into Linux path when inside WSL.
42
+ #
43
+ # Contract:
44
+ # - In WSL: requires `wslpath`. Returns 2 (with a stderr error) if
45
+ # missing — callers must not silently treat a Windows-style path
46
+ # as Linux-safe.
47
+ # - Outside WSL: paths are already Linux/macOS paths; echoes the
48
+ # input unchanged and returns 0.
49
+ # - Empty input returns 1 (usage error).
42
50
  dot_path_to_unix() {
43
51
  local p="${1:-}"
44
- if [[ -z "$p" ]]; then
45
- return 1
46
- fi
47
- if dot_is_wsl && command -v wslpath >/dev/null 2>&1; then
52
+ [[ -n "$p" ]] || return 1
53
+ if dot_is_wsl; then
54
+ if ! command -v wslpath >/dev/null 2>&1; then
55
+ printf "dot_path_to_unix: wslpath required in WSL but not found\n" >&2
56
+ return 2
57
+ fi
48
58
  wslpath -u "$p"
49
59
  return
50
60
  fi
@@ -52,12 +62,15 @@ dot_path_to_unix() {
52
62
  }
53
63
 
54
64
  # Convert Linux path into host-native path when inside WSL.
65
+ # Same contract as dot_path_to_unix (see above).
55
66
  dot_path_to_native() {
56
67
  local p="${1:-}"
57
- if [[ -z "$p" ]]; then
58
- return 1
59
- fi
60
- if dot_is_wsl && command -v wslpath >/dev/null 2>&1; then
68
+ [[ -n "$p" ]] || return 1
69
+ if dot_is_wsl; then
70
+ if ! command -v wslpath >/dev/null 2>&1; then
71
+ printf "dot_path_to_native: wslpath required in WSL but not found\n" >&2
72
+ return 2
73
+ fi
61
74
  wslpath -w "$p"
62
75
  return
63
76
  fi