@sebastienrousseau/dotfiles 0.2.512 → 0.2.513

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 (34) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +2 -2
  3. package/docs/COPYRIGHT +1 -1
  4. package/docs/manual/00-introduction.md +1 -1
  5. package/docs/manual/01-concepts/02-trust-model.md +2 -1
  6. package/docs/manual/03-reference/03-environment.md +1 -1
  7. package/docs/manual/04-cookbook/03-faq.md +1 -1
  8. package/docs/manual/05-appendices/B-security-checklist.md +2 -1
  9. package/docs/operations/COVERAGE.md +7 -8
  10. package/docs/operations/REGISTRY.md +20 -19
  11. package/docs/reference/POWERSHELL_PARITY.md +28 -27
  12. package/docs/schema/dot-registry-v1.json +33 -0
  13. package/docs/security/SCORECARD.md +1 -1
  14. package/install.sh +20 -11
  15. package/package.json +1 -1
  16. package/scripts/diagnostics/a2a-conformance.sh +0 -0
  17. package/scripts/diagnostics/health.sh +4 -1
  18. package/scripts/diagnostics/mcp-doctor.sh +0 -0
  19. package/scripts/diagnostics/verify_state.sh +0 -0
  20. package/scripts/diagnostics/workstation-attestation.sh +0 -0
  21. package/scripts/dot/commands/ai.sh +3 -1
  22. package/scripts/dot/commands/fleet.sh +1 -1
  23. package/scripts/dot/commands/registry.sh +164 -15
  24. package/scripts/dot/powershell/Dot.psm1 +146 -26
  25. package/scripts/fonts/install-nerd-fonts.sh +16 -10
  26. package/scripts/git-hooks/pre-commit-audit.sh +1 -1
  27. package/scripts/ops/heal-tools.sh +39 -111
  28. package/scripts/ops/post-apply-repair.sh +0 -0
  29. package/scripts/qa/reliability-audit.sh +3 -3
  30. package/scripts/qa/scorecard-snapshot.sh +3 -3
  31. package/scripts/qa/validate-examples.sh +0 -0
  32. package/scripts/qa/wsl-contract.sh +0 -0
  33. package/scripts/tools/detect-collisions.py +0 -0
  34. package/scripts/version-sync.sh +27 -0
@@ -5,24 +5,18 @@
5
5
  #
6
6
  # scripts/dot/commands/registry.sh
7
7
  #
8
- # `dot registry` — initial scaffold for the dot module registry.
8
+ # `dot registry` — verified module registry for reusable chezmoi sources.
9
9
  #
10
10
  # §3 audit roadmap: ship a registry of reusable dotfile modules
11
11
  # ("rust-dev-setup", "k8s-operator-laptop") to seed network effects.
12
12
  # Hosted as a GitHub-Pages-indexed JSON file to keep ops cost near
13
13
  # zero.
14
14
  #
15
- # Current state: SCAFFOLD ONLY. This file ships the CLI surface and
16
- # the JSON contract (see _registry_default_url). The registry itself
17
- # is empty; populating it is its own roadmap item.
18
- #
19
15
  # Subcommands:
20
16
  # list Show modules in the configured registry
21
17
  # search <q> Filter modules by keyword (name, description, tags)
22
18
  # info <name> Print full metadata for a module
23
- # install <name> Apply a module to the current workstation (stub —
24
- # prints what would be installed; full implementation
25
- # needs a sandboxed apply pipeline)
19
+ # install <name> Verify and preview a module; --yes applies it.
26
20
  # url Show the active registry URL
27
21
  # set-url <u> Override the registry URL (writes to user config)
28
22
  #
@@ -37,7 +31,8 @@
37
31
  # "tags": ["rust", "dev", "language"],
38
32
  # "maintainer": "alice@example.com",
39
33
  # "version": "1.2.0",
40
- # "sha256": "abc123..." }
34
+ # "archive_url": "https://example.com/rust-dev-setup-1.2.0.tar.gz",
35
+ # "sha256": "<64 lowercase hexadecimal characters>" }
41
36
  # ]
42
37
  # }
43
38
 
@@ -48,6 +43,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
48
43
  source "$SCRIPT_DIR/../../../lib/dot/ui.sh"
49
44
  # shellcheck source=../../../lib/dot/utils.sh disable=SC1091
50
45
  source "$SCRIPT_DIR/../../../lib/dot/utils.sh"
46
+ # shellcheck source=../../../lib/dot/verified-download.sh disable=SC1091
47
+ source "$SCRIPT_DIR/../../../lib/dot/verified-download.sh"
51
48
 
52
49
  _registry_default_url() {
53
50
  printf '%s\n' "https://sebastienrousseau.github.io/dotfiles/registry.json"
@@ -79,9 +76,32 @@ _registry_cache_dir() {
79
76
  printf '%s/dotfiles/registry\n' "${XDG_CACHE_HOME:-$HOME/.cache}"
80
77
  }
81
78
 
79
+ _registry_data_dir() {
80
+ printf '%s/dotfiles/modules\n' "${XDG_DATA_HOME:-$HOME/.local/share}"
81
+ }
82
+
83
+ _registry_validate_index() {
84
+ local index="$1"
85
+ jq -e '
86
+ .version == 1 and
87
+ (.modules | type == "array") and
88
+ all(.modules[];
89
+ (.name | test("^[a-z0-9][a-z0-9-]{0,31}$")) and
90
+ (.version | test("^[0-9]+\\.[0-9]+\\.[0-9]+([+-][0-9A-Za-z.-]+)?$")) and
91
+ (.description | type == "string" and length <= 200) and
92
+ (.archive_url | test("^(https|file)://")) and
93
+ (.sha256 | test("^[0-9a-f]{64}$"))
94
+ )
95
+ ' "$index" >/dev/null 2>&1
96
+ }
97
+
82
98
  _registry_fetch() {
83
99
  local url cache_dir cache_file
84
100
  url="$(_registry_url)"
101
+ [[ "$url" =~ ^(https://|file://) ]] || {
102
+ ui_err "registry" "registry URL must use https:// (or file:// for local testing)"
103
+ return 1
104
+ }
85
105
  cache_dir="$(_registry_cache_dir)"
86
106
  cache_file="$cache_dir/index.json"
87
107
  mkdir -p "$cache_dir"
@@ -89,7 +109,9 @@ _registry_fetch() {
89
109
  local now mtime
90
110
  now="$(date +%s)"
91
111
  if [[ -s "$cache_file" ]]; then
92
- if mtime="$(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file" 2>/dev/null)"; then
112
+ if ! _registry_validate_index "$cache_file"; then
113
+ rm -f "$cache_file"
114
+ elif mtime="$(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file" 2>/dev/null)"; then
93
115
  if ((now - mtime < 21600)); then
94
116
  printf '%s\n' "$cache_file"
95
117
  return 0
@@ -108,7 +130,11 @@ _registry_fetch() {
108
130
  # into `jq FILE` as a two-argument invocation
109
131
  # (`jq \n /path/to/file`), producing a confusing
110
132
  # "Could not open file" error. Silence stdout explicitly.
111
- if ! curl -fsSL --max-time 15 -o "$tmp" "$url" >/dev/null; then
133
+ local curl_args=(-fsSL --max-time 15)
134
+ if [[ "$url" == https://* ]]; then
135
+ curl_args+=(--proto '=https' --tlsv1.2)
136
+ fi
137
+ if ! curl "${curl_args[@]}" -o "$tmp" "$url" >/dev/null; then
112
138
  rm -f "$tmp"
113
139
  if [[ -s "$cache_file" ]]; then
114
140
  ui_warn "registry" "fetch failed; using stale cache at $cache_file"
@@ -119,6 +145,11 @@ _registry_fetch() {
119
145
  return 1
120
146
  fi
121
147
  mv "$tmp" "$cache_file"
148
+ if ! _registry_validate_index "$cache_file"; then
149
+ rm -f "$cache_file"
150
+ ui_err "registry" "index failed schema and integrity validation"
151
+ return 1
152
+ fi
122
153
  printf '%s\n' "$cache_file"
123
154
  }
124
155
 
@@ -129,6 +160,101 @@ _registry_require_jq() {
129
160
  }
130
161
  }
131
162
 
163
+ _registry_archive_is_safe() {
164
+ local archive="$1"
165
+ local entry
166
+ while IFS= read -r entry; do
167
+ [[ -n "$entry" ]] || continue
168
+ if [[ "$entry" == /* || "$entry" == ../* || "$entry" == *"/../"* || "$entry" == *"/.." ]]; then
169
+ ui_err "registry" "archive contains unsafe path: $entry"
170
+ return 1
171
+ fi
172
+ done < <(tar -tzf "$archive")
173
+ if tar -tvzf "$archive" | awk 'substr($1,1,1) == "l" || substr($1,1,1) == "h" { found=1 } END { exit !found }'; then
174
+ ui_err "registry" "archive contains links; links are forbidden in registry modules"
175
+ return 1
176
+ fi
177
+ }
178
+
179
+ _registry_install() (
180
+ local name="$1"
181
+ local apply="${2:-0}"
182
+ local index metadata version archive_url expected tmp archive extract module_root actual destination
183
+
184
+ [[ "$name" =~ ^[a-z0-9][a-z0-9-]{0,31}$ ]] || {
185
+ ui_err "install" "invalid module name: $name"
186
+ return 1
187
+ }
188
+ _registry_require_jq || return $?
189
+ index="$(_registry_fetch)" || return $?
190
+ _registry_validate_index "$index" || {
191
+ ui_err "registry" "index failed schema validation"
192
+ return 1
193
+ }
194
+ metadata="$(jq -c --arg name "$name" '.modules[] | select(.name == $name)' "$index")"
195
+ [[ -n "$metadata" ]] || {
196
+ ui_err "install" "module not found: $name"
197
+ return 1
198
+ }
199
+ version="$(jq -r '.version' <<<"$metadata")"
200
+ archive_url="$(jq -r '.archive_url' <<<"$metadata")"
201
+ expected="$(jq -r '.sha256' <<<"$metadata")"
202
+
203
+ tmp="$(mktemp -d -t dot-registry.XXXXXX)"
204
+ archive="$tmp/module.tar.gz"
205
+ extract="$tmp/source"
206
+ trap 'rm -rf "$tmp"' EXIT
207
+ mkdir -p "$extract"
208
+ local curl_args=(-fsSL --max-time 60)
209
+ if [[ "$archive_url" == https://* ]]; then
210
+ curl_args+=(--proto '=https' --tlsv1.2)
211
+ fi
212
+ if ! curl "${curl_args[@]}" -o "$archive" "$archive_url"; then
213
+ ui_err "install" "could not download $archive_url"
214
+ return 1
215
+ fi
216
+ local archive_size
217
+ archive_size="$(wc -c <"$archive" | tr -d '[:space:]')"
218
+ if ((archive_size > 52428800)); then
219
+ ui_err "install" "archive exceeds the 50 MiB safety limit"
220
+ return 1
221
+ fi
222
+ actual="$(_dot_sha256_file "$archive")" || return $?
223
+ [[ "$actual" == "$expected" ]] || {
224
+ ui_err "install" "SHA-256 mismatch for $name@$version"
225
+ return 1
226
+ }
227
+ _registry_archive_is_safe "$archive" || return $?
228
+ tar -xzf "$archive" -C "$extract" --no-same-owner --no-same-permissions
229
+
230
+ module_root="$extract"
231
+ local roots=()
232
+ while IFS= read -r entry; do roots+=("$entry"); done < <(find "$extract" -mindepth 1 -maxdepth 1 -print)
233
+ if [[ ${#roots[@]} -eq 1 && -d "${roots[0]}" ]]; then
234
+ module_root="${roots[0]}"
235
+ fi
236
+ [[ -n "$(find "$module_root" -mindepth 1 -print -quit)" ]] || {
237
+ ui_err "install" "module archive is empty"
238
+ return 1
239
+ }
240
+
241
+ ui_ok "Verified" "$name@$version ($actual)"
242
+ ui_section "Chezmoi preview"
243
+ chezmoi apply --source "$module_root" --destination "$HOME" --dry-run --no-tty
244
+ if [[ "$apply" != "1" ]]; then
245
+ ui_info "Preview only" "rerun with --yes to install and apply"
246
+ return 0
247
+ fi
248
+
249
+ destination="$(_registry_data_dir)/$name/$version"
250
+ mkdir -p "$(dirname "$destination")"
251
+ rm -rf "$destination"
252
+ mv "$module_root" "$destination"
253
+ chezmoi apply --source "$destination" --destination "$HOME" --no-tty
254
+ printf '%s\n' "$metadata" >"$(dirname "$destination")/installed.json"
255
+ ui_ok "Installed" "$name@$version"
256
+ )
257
+
132
258
  cmd_registry() {
133
259
  local subcommand="${1:-list}"
134
260
  shift || true
@@ -239,9 +365,31 @@ cmd_registry() {
239
365
  ui_err "install" "missing module name"
240
366
  return 1
241
367
  }
242
- ui_warn "install" "scaffold only — full module installer is a roadmap item"
243
- ui_info "install" "would fetch module $name from registry and apply it as a chezmoi sub-source"
244
- return 0
368
+ shift || true
369
+ local apply=0
370
+ case "${1:-}" in
371
+ "") ;;
372
+ --yes | -y) apply=1 ;;
373
+ --dry-run | -n) apply=0 ;;
374
+ *)
375
+ ui_err "install" "unknown option: $1"
376
+ return 2
377
+ ;;
378
+ esac
379
+ _registry_install "$name" "$apply"
380
+ ;;
381
+ installed)
382
+ _registry_require_jq || return $?
383
+ local modules_dir
384
+ modules_dir="$(_registry_data_dir)"
385
+ if [[ ! -d "$modules_dir" ]]; then
386
+ ui_info "registry" "no modules installed"
387
+ return 0
388
+ fi
389
+ find "$modules_dir" -name installed.json -type f -exec jq -r '"\(.name)\t\(.version)\t\(.description)"' {} \; |
390
+ while IFS=$'\t' read -r module version description; do
391
+ ui_ok "$module" "v$version — $description"
392
+ done
245
393
  ;;
246
394
  --help | -h | help)
247
395
  cat <<EOF
@@ -251,7 +399,8 @@ Subcommands:
251
399
  list List modules in the configured registry
252
400
  search <q> Filter modules by keyword (name, description, tags)
253
401
  info <name> Print metadata for a single module
254
- install <name> Install a module (scaffold see docs/operations/REGISTRY.md)
402
+ install <name> Verify and preview a module; pass --yes to apply
403
+ installed List locally installed modules
255
404
  url Show the active registry URL
256
405
  set-url <url> Override the registry URL (persists to user config)
257
406
 
@@ -1,8 +1,8 @@
1
1
  # Dot — native PowerShell module for the dotfiles framework.
2
2
  #
3
- # This module provides native PowerShell cmdlets for the subset of
4
- # `dot` subcommands that don't require bash. Closes Stub rows in
5
- # docs/reference/POWERSHELL_PARITY.md.
3
+ # This module provides native PowerShell cmdlets for the daily `dot`
4
+ # workflow. Commands with Unix-specific orchestration remain available
5
+ # through the dispatcher's explicit bash bridge.
6
6
  #
7
7
  # Loaded by bin/dot.ps1 dispatcher; cmdlets are also callable
8
8
  # directly:
@@ -11,7 +11,7 @@
11
11
  # Invoke-DotHelp
12
12
  # Test-DotAgentsSync -Verbose
13
13
  #
14
- # Tested via scripts/ci/windows-smoke-test.ps1.
14
+ # Tested via tools/ci/windows-smoke-test.ps1.
15
15
 
16
16
  #requires -Version 7.0
17
17
 
@@ -66,6 +66,25 @@ function script:Write-DotBanner {
66
66
  Write-Host "$esc[1;38;5;212mDot · $Section$esc[0m"
67
67
  }
68
68
 
69
+ function script:Assert-DotCommand {
70
+ param([Parameter(Mandatory)][string]$Name)
71
+ if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
72
+ throw "$Name is required but was not found on PATH"
73
+ }
74
+ }
75
+
76
+ function script:Invoke-DotNativeCommand {
77
+ param(
78
+ [Parameter(Mandatory)][string]$Name,
79
+ [Parameter(Mandatory)][string[]]$Arguments
80
+ )
81
+ Assert-DotCommand -Name $Name
82
+ & $Name @Arguments
83
+ if ($LASTEXITCODE -ne 0) {
84
+ throw "$Name exited with status $LASTEXITCODE"
85
+ }
86
+ }
87
+
69
88
  # Public cmdlets --------------------------------------------------------------
70
89
 
71
90
  <#
@@ -110,9 +129,8 @@ function Get-DotVersion {
110
129
  Print the dot CLI help overview.
111
130
 
112
131
  .DESCRIPTION
113
- Native PowerShell implementation of `dot help`. Reads command
114
- metadata from CLAUDE.md / the dispatcher's help table; does
115
- NOT shell out to bash.
132
+ Native PowerShell implementation of `dot help`. Presents the
133
+ native command surface without shelling out to bash.
116
134
 
117
135
  The full Linux-style help (every subcommand grouped by category)
118
136
  requires the bash dispatcher; this cmdlet shows the Windows-
@@ -132,15 +150,19 @@ function Invoke-DotHelp {
132
150
  Write-Host ''
133
151
  Write-Host ' Get-DotVersion Print framework version'
134
152
  Write-Host ' Invoke-DotHelp This screen'
135
- Write-Host ' Test-DotAgentsSync Check AGENTS.md CLAUDE.md sync'
153
+ Write-Host ' Test-DotAgentsSync Check AGENTS.md against CLAUDE.md'
154
+ Write-Host ' Get-DotStatus Show chezmoi drift'
155
+ Write-Host ' Invoke-DotChezmoi Run a core chezmoi operation'
156
+ Write-Host ' Invoke-DotDoctor Audit the native Windows setup'
157
+ Write-Host ' Get-DotEnvironment List mise-managed tools'
158
+ Write-Host ' Get-DotAgents List agent harness targets'
159
+ Write-Host ' Get-DotFleetStatus Show local fleet node status'
136
160
  Write-Host ''
137
161
  Write-Host 'Bash-bridged subcommands (require bash on PATH):'
138
162
  Write-Host ''
139
- Write-Host ' dot doctor Deep environment audit'
140
- Write-Host ' dot env list List installed mise tools'
141
163
  Write-Host ' dot env emit Emit v1 environment manifest'
142
164
  Write-Host ' dot agents render Re-render every AGENTS.md harness'
143
- Write-Host ' dot fleet status Single-node fleet status (Full on PowerShell)'
165
+ Write-Host ' dot registry ... Verified module registry operations'
144
166
  Write-Host ''
145
167
  Write-Host 'See: dot help all (via bash) — full command index'
146
168
  }
@@ -176,24 +198,122 @@ function Test-DotAgentsSync {
176
198
  return $false
177
199
  }
178
200
  }
179
- # Both files declare: "Chezmoi-managed dotfiles for macOS, Linux,
180
- # WSL, and PowerShell 7.5+. Version `0.2.503`."
181
- # That single line is the canonical sync surface — its content
182
- # is generated from the same template by `dot agents render`.
183
- $claudeLine = (Select-String -Path $claude -Pattern '^Chezmoi-managed dotfiles' -SimpleMatch:$false |
184
- Select-Object -First 1).Line
185
- $agentsLine = (Select-String -Path $agents -Pattern '^Chezmoi-managed dotfiles' -SimpleMatch:$false |
186
- Select-Object -First 1).Line
187
- if (-not $claudeLine -or -not $agentsLine) {
188
- Write-Verbose "sync surface line not found — claude:[$claudeLine] agents:[$agentsLine]"
189
- return $false
201
+ $claudeBody = Get-Content -Raw $claude
202
+ $agentsBody = Get-Content -Raw $agents
203
+ foreach ($name in @('claudeBody', 'agentsBody')) {
204
+ $value = Get-Variable -Name $name -ValueOnly
205
+ $value = $value -replace '(?s)^\s*<!--.*?-->\s*', ''
206
+ $value = $value -replace '(?m)^# (CLAUDE|AGENTS)\.md.*\r?\n', ''
207
+ $value = $value -replace '(?s)\r?\n---\r?\n\r?\n\*\*Need richer context\?.*$', ''
208
+ $value = ($value -replace "`r`n", "`n").Trim()
209
+ Set-Variable -Name $name -Value $value
190
210
  }
191
- if ($claudeLine -ne $agentsLine) {
192
- Write-Verbose "drift detected:`n CLAUDE.md: $claudeLine`n AGENTS.md: $agentsLine"
211
+ if ($claudeBody -cne $agentsBody) {
212
+ Write-Verbose 'AGENTS.md content differs from the canonical CLAUDE.md body'
193
213
  return $false
194
214
  }
195
- Write-Verbose 'AGENTS.md sync surface matches CLAUDE.md'
215
+ Write-Verbose 'AGENTS.md content matches the canonical CLAUDE.md body'
196
216
  return $true
197
217
  }
198
218
 
199
- Export-ModuleMember -Function Get-DotVersion, Invoke-DotHelp, Test-DotAgentsSync
219
+ function Invoke-DotChezmoi {
220
+ [CmdletBinding()]
221
+ param(
222
+ [Parameter(Mandatory)]
223
+ [ValidateSet('apply', 'diff', 'update', 'add', 'remove', 'init')]
224
+ [string]$Operation,
225
+ [Parameter(ValueFromRemainingArguments = $true)]
226
+ [string[]]$Arguments = @()
227
+ )
228
+ Invoke-DotNativeCommand -Name 'chezmoi' -Arguments (@($Operation) + $Arguments)
229
+ }
230
+
231
+ function Get-DotStatus {
232
+ [CmdletBinding()]
233
+ param([switch]$AsObject)
234
+ Assert-DotCommand -Name 'chezmoi'
235
+ $output = @(& chezmoi status 2>&1)
236
+ if ($LASTEXITCODE -ne 0) {
237
+ throw "chezmoi status exited with status $LASTEXITCODE`: $($output -join [Environment]::NewLine)"
238
+ }
239
+ $state = if ($output.Count -eq 0) { 'clean' } else { 'drifted' }
240
+ if ($AsObject) {
241
+ return [pscustomobject]@{ State = $state; Changes = $output; Native = $true }
242
+ }
243
+ Write-DotBanner -Section 'Status'
244
+ if ($state -eq 'clean') { Write-Host '[OK] Clean - no local drift detected' }
245
+ else { $output | Write-Output }
246
+ }
247
+
248
+ function Get-DotSourcePath {
249
+ [CmdletBinding()]
250
+ param()
251
+ Assert-DotCommand -Name 'chezmoi'
252
+ $path = (& chezmoi source-path 2>&1 | Out-String).Trim()
253
+ if ($LASTEXITCODE -ne 0 -or -not $path) { throw 'chezmoi source-path failed' }
254
+ return $path
255
+ }
256
+
257
+ function Invoke-DotDoctor {
258
+ [CmdletBinding()]
259
+ param([switch]$AsObject)
260
+ $checks = @(
261
+ [pscustomobject]@{ Name = 'PowerShell 7.4+'; Ok = ($PSVersionTable.PSVersion -ge [version]'7.4') }
262
+ [pscustomobject]@{ Name = 'chezmoi'; Ok = [bool](Get-Command chezmoi -ErrorAction SilentlyContinue) }
263
+ [pscustomobject]@{ Name = 'git'; Ok = [bool](Get-Command git -ErrorAction SilentlyContinue) }
264
+ [pscustomobject]@{ Name = 'repository data'; Ok = (Test-Path $script:DataFile) }
265
+ [pscustomobject]@{ Name = 'PowerShell module'; Ok = (Test-Path (Join-Path $PSScriptRoot 'Dot.psm1')) }
266
+ )
267
+ if ($AsObject) { return $checks }
268
+ Write-DotBanner -Section 'Doctor'
269
+ foreach ($check in $checks) {
270
+ $prefix = if ($check.Ok) { '[OK]' } else { '[FAIL]' }
271
+ Write-Host "$prefix $($check.Name)"
272
+ }
273
+ return -not ($checks.Ok -contains $false)
274
+ }
275
+
276
+ function Get-DotEnvironment {
277
+ [CmdletBinding()]
278
+ param([switch]$AsJson)
279
+ Assert-DotCommand -Name 'mise'
280
+ $arguments = if ($AsJson) { @('ls', '--json') } else { @('ls') }
281
+ Invoke-DotNativeCommand -Name 'mise' -Arguments $arguments
282
+ }
283
+
284
+ function Get-DotAgents {
285
+ [CmdletBinding()]
286
+ param()
287
+ $targets = [ordered]@{
288
+ 'agents-md' = 'AGENTS.md'; 'cursor' = '.cursor/rules/dotfiles.mdc'
289
+ 'codex' = '.codex/config.toml'; 'windsurf' = '.windsurf/rules.md'
290
+ 'zed' = '.zed/agent-config.toml'; 'roo' = '.roo/rules.md'
291
+ 'cline' = '.clinerules'; 'aider' = '.aider.conf.yml'
292
+ 'continue' = '.continuerc.json'; 'jules' = '.jules/system.md'
293
+ 'agy' = '.agy/AGY.md'
294
+ }
295
+ foreach ($entry in $targets.GetEnumerator()) {
296
+ $path = Join-Path $script:RepoRoot $entry.Value
297
+ [pscustomobject]@{ Harness = $entry.Key; Path = $path; Rendered = (Test-Path $path) }
298
+ }
299
+ }
300
+
301
+ function Get-DotFleetStatus {
302
+ [CmdletBinding()]
303
+ param([switch]$AsJson)
304
+ $status = Get-DotStatus -AsObject
305
+ $record = [pscustomobject]@{
306
+ NodeId = [Environment]::MachineName
307
+ Namespace = 'default'
308
+ Version = Get-DotfilesVersionFromData
309
+ OS = [System.Runtime.InteropServices.RuntimeInformation]::OSDescription
310
+ Architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()
311
+ Drift = $status.State
312
+ }
313
+ if ($AsJson) { return $record | ConvertTo-Json -Compress }
314
+ return $record
315
+ }
316
+
317
+ Export-ModuleMember -Function Get-DotVersion, Invoke-DotHelp, Test-DotAgentsSync, `
318
+ Invoke-DotChezmoi, Get-DotStatus, Get-DotSourcePath, Invoke-DotDoctor, `
319
+ Get-DotEnvironment, Get-DotAgents, Get-DotFleetStatus
@@ -7,32 +7,38 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
7
  # shellcheck source=../../lib/dot/ui.sh
8
8
  # shellcheck disable=SC1091
9
9
  source "$SCRIPT_DIR/../../lib/dot/ui.sh"
10
+ # shellcheck source=../../lib/dot/verified-download.sh disable=SC1091
11
+ source "$SCRIPT_DIR/../../lib/dot/verified-download.sh"
10
12
 
11
13
  ui_init
12
14
  ui_header "Nerd Fonts"
13
15
 
14
16
  DEFAULT_FONTS="JetBrainsMono FiraCode Iosevka"
15
17
  FONT_LIST="${*:-$DEFAULT_FONTS}"
18
+ FONT_VERSION="v3.4.0"
19
+ FONT_BASE_URL="https://github.com/ryanoasis/nerd-fonts/releases/download/${FONT_VERSION}"
20
+ FONT_CHECKSUM_URL="${FONT_BASE_URL}/SHA-256.txt"
16
21
 
17
- install_linux() {
18
- font_name="$1"
19
- target_dir="$HOME/.local/share/fonts/${font_name}NerdFont"
22
+ install_linux() (
23
+ local font_name="$1"
24
+ local target_dir="$HOME/.local/share/fonts/${font_name}NerdFont"
20
25
  mkdir -p "$target_dir"
26
+ local tmp_dir
21
27
  tmp_dir="$(umask 077 && mktemp -d)"
22
- # shellcheck disable=SC2064
23
- trap "rm -rf '$tmp_dir'" RETURN
24
- url="https://github.com/ryanoasis/nerd-fonts/releases/latest/download/${font_name}.zip"
28
+ trap 'rm -rf "$tmp_dir"' EXIT
29
+ local asset="${font_name}.zip"
30
+ local url="${FONT_BASE_URL}/${asset}"
25
31
  ui_info "Downloading" "$font_name Nerd Font"
26
- curl -fL --connect-timeout 10 --max-time 300 "$url" -o "$tmp_dir/${font_name}.zip"
27
- if ! unzip -o "$tmp_dir/${font_name}.zip" -d "$target_dir" >/dev/null; then
28
- ui_err "Unzip failed" "${font_name}.zip" >&2
32
+ download_verified_asset "$url" "$FONT_CHECKSUM_URL" "$asset" "$tmp_dir/$asset" 104857600
33
+ if ! unzip -o "$tmp_dir/$asset" -d "$target_dir" >/dev/null; then
34
+ ui_err "Unzip failed" "$asset" >&2
29
35
  return 1
30
36
  fi
31
37
  if command -v fc-cache >/dev/null; then
32
38
  fc-cache -f "$target_dir"
33
39
  fi
34
40
  ui_ok "Installed" "$target_dir"
35
- }
41
+ )
36
42
 
37
43
  install_macos() {
38
44
  font_name="$1"
@@ -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.512 standards maintained."
144
+ printf '%b\n' "${GREEN}${BOLD}✅ Audit passed.${NC} v0.2.513 standards maintained."
145
145
  exit 0
146
146
  fi