@sebastienrousseau/dotfiles 0.2.518 → 0.2.520
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.
- package/CHANGELOG.md +191 -0
- package/README.md +1164 -164
- package/docs/ARCHITECTURE.md +117 -0
- package/docs/COPYRIGHT +1 -1
- package/docs/ECOSYSTEM.md +220 -0
- package/docs/GOLD-STANDARD-AUDIT.md +352 -0
- package/docs/GOVERNANCE.md +1 -1
- package/docs/MAINTAINERS.md +1 -1
- package/docs/MINIMUM-TOOLCHAIN.md +100 -0
- package/docs/README.md +1 -1
- package/docs/STRUCTURE.md +1 -1
- package/docs/architecture/ARCHITECTURE.md +11 -108
- package/docs/architecture/REPO_LAYOUT.md +4 -4
- package/docs/guides/MACOS_ICLOUD_SYMLINKS.md +121 -0
- package/docs/index.md +3 -3
- package/docs/manual/00-introduction.md +1 -1
- package/docs/manual/03-reference/01-dot-cli.md +28 -3
- package/docs/manual/command-index.md +24 -4
- package/docs/manual/concept-index.md +2 -0
- package/docs/migration/README.md +81 -0
- package/docs/migration/from-bare-git-repo.md +156 -0
- package/docs/migration/from-gnu-stow.md +165 -0
- package/docs/migration/from-plain-chezmoi.md +148 -0
- package/docs/migration/from-yadm.md +187 -0
- package/docs/operations/PERFORMANCE_BUDGETS.md +196 -0
- package/docs/operations/REGISTRY.md +1 -1
- package/docs/operations/RELEASE_PIPELINE.md +4 -4
- package/docs/operations/TESTING.md +3 -3
- package/docs/operations/TRACEABILITY.md +1 -0
- package/docs/packaging.md +222 -0
- package/docs/reference/FEATURE-MATRIX.md +646 -0
- package/docs/reference/TOOLS.md +1 -1
- package/docs/reference/UTILS.md +1 -0
- package/docs/security/COMPLIANCE.md +1 -1
- package/docs/security/DISCLOSURE.md +4 -4
- package/docs/security/FUZZING.md +113 -18
- package/docs/security/KEY_ROTATION.md +1 -1
- package/docs/security/SCORECARD.md +3 -3
- package/docs/security/VERIFY_RELEASE.md +2 -2
- package/install.sh +6 -6
- package/package.json +2 -2
- package/scripts/ci/check-copyright-headers.sh +1 -1
- package/scripts/ci/check-shell-preamble.sh +1 -1
- package/scripts/ci/guard-gitleaks-checkout.sh +1 -1
- package/scripts/demo/record.sh +1 -1
- package/scripts/diagnostics/a2a-conformance.sh +1 -1
- package/scripts/diagnostics/alias-governance.sh +30 -3
- package/scripts/diagnostics/aliases-cheatsheet.sh +1 -1
- package/scripts/diagnostics/aliases-manifest.sh +1 -1
- package/scripts/diagnostics/attest-verify.sh +147 -0
- package/scripts/diagnostics/benchmark.sh +1 -1
- package/scripts/diagnostics/conflicts.sh +1 -1
- package/scripts/diagnostics/doctor-unified.sh +6 -2
- package/scripts/diagnostics/doctor.sh +56 -10
- package/scripts/diagnostics/drift-dashboard.sh +3 -2
- package/scripts/diagnostics/health.sh +43 -10
- package/scripts/diagnostics/history-analysis.sh +1 -1
- package/scripts/diagnostics/mcp-doctor.sh +2 -2
- package/scripts/diagnostics/perf.sh +1 -1
- package/scripts/diagnostics/scorecard.sh +3 -2
- package/scripts/diagnostics/secret-governance.sh +1 -1
- package/scripts/diagnostics/security-score.sh +1 -1
- package/scripts/diagnostics/smoke-test.sh +1 -1
- package/scripts/diagnostics/snapshot.sh +1 -1
- package/scripts/diagnostics/verify.sh +1 -1
- package/scripts/diagnostics/verify_state.sh +1 -1
- package/scripts/diagnostics/version-locks.sh +1 -1
- package/scripts/diagnostics/workstation-attestation.sh +26 -1
- package/scripts/dot/commands/agent.sh +64 -14
- package/scripts/dot/commands/agents.sh +24 -8
- package/scripts/dot/commands/ai.sh +20 -7
- package/scripts/dot/commands/aliases.sh +1 -1
- package/scripts/dot/commands/appearance.sh +1 -1
- package/scripts/dot/commands/completion.sh +42 -5
- package/scripts/dot/commands/core.sh +1 -1
- package/scripts/dot/commands/diagnostics.sh +1 -1
- package/scripts/dot/commands/env-emit.sh +1 -1
- package/scripts/dot/commands/fleet.sh +41 -18
- package/scripts/dot/commands/init.sh +1 -1
- package/scripts/dot/commands/lint.sh +1 -1
- package/scripts/dot/commands/manual.sh +1 -1
- package/scripts/dot/commands/meta.sh +115 -10
- package/scripts/dot/commands/patterns.sh +1 -1
- package/scripts/dot/commands/registry.sh +43 -7
- package/scripts/dot/commands/restore.sh +1 -1
- package/scripts/dot/commands/secrets.sh +1 -1
- package/scripts/dot/commands/security.sh +1 -1
- package/scripts/dot/commands/tools.sh +21 -7
- package/scripts/fonts/install-nerd-fonts.sh +1 -1
- package/scripts/fonts/patch-fonts.sh +1 -1
- package/scripts/git-hooks/install.sh +1 -1
- package/scripts/git-hooks/pre-commit-audit.sh +2 -2
- package/scripts/lib/secrets_provider.sh +19 -4
- package/scripts/nvim/headless-upgrade.lua +81 -0
- package/scripts/ops/ai-setup.sh +1 -1
- package/scripts/ops/bundle.sh +1 -1
- package/scripts/ops/chaos.sh +1 -1
- package/scripts/ops/chezmoi-apply.sh +1 -1
- package/scripts/ops/chezmoi-diff.sh +1 -1
- package/scripts/ops/chezmoi-remove.sh +3 -3
- package/scripts/ops/chezmoi-update.sh +6 -2
- package/scripts/ops/heal-chezmoi.sh +1 -1
- package/scripts/ops/heal-system.sh +1 -1
- package/scripts/ops/heal-tools.sh +1 -1
- package/scripts/ops/heal.sh +1 -1
- package/scripts/ops/post-apply-repair.sh +1 -1
- package/scripts/ops/prewarm.sh +1 -1
- package/scripts/ops/release.sh +1 -1
- package/scripts/ops/rollback.sh +11 -2
- package/scripts/ops/setup.sh +1 -1
- package/scripts/ops/teleport.sh +1 -1
- package/scripts/qa/check-feature-matrix.sh +296 -0
- package/scripts/qa/check-version-consistency.sh +7 -119
- package/scripts/qa/coverage-baseline.sh +1 -1
- package/scripts/qa/docs-coverage.sh +9 -3
- package/scripts/qa/examples-coverage.sh +1 -1
- package/scripts/qa/reliability-audit.sh +1 -1
- package/scripts/qa/scorecard-snapshot.sh +1 -1
- package/scripts/qa/traceability-coverage.sh +9 -2
- package/scripts/qa/validate-examples.sh +66 -3
- package/scripts/qa/wsl-contract.sh +1 -1
- package/scripts/secrets/age-init.sh +1 -1
- package/scripts/secrets/create-secrets-file.sh +1 -1
- package/scripts/secrets/encrypt-ssh-key.sh +1 -1
- package/scripts/security/backup.sh +1 -1
- package/scripts/security/check-disclosure-key-expiry.sh +1 -1
- package/scripts/security/dns-doh.sh +1 -1
- package/scripts/security/encryption-check.sh +1 -1
- package/scripts/security/enforce-policies.sh +315 -98
- package/scripts/security/firewall.sh +1 -1
- package/scripts/security/lock-configs.sh +1 -1
- package/scripts/security/lock-screen.sh +1 -1
- package/scripts/security/manage-secrets.sh +1 -1
- package/scripts/security/ssh-cert.sh +1 -1
- package/scripts/security/telemetry-kill.sh +1 -1
- package/scripts/security/usb-safety.sh +1 -1
- package/scripts/theme/apply-gnome-theme.sh +1 -1
- package/scripts/theme/extract-heic-frames.sh +1 -1
- package/scripts/theme/extract-theme.py +284 -6
- package/scripts/theme/install-boot-logo.sh +1 -1
- package/scripts/theme/install-catppuccin-themes.sh +1 -1
- package/scripts/theme/install-cursors.sh +1 -1
- package/scripts/theme/install-file-icons.sh +1 -1
- package/scripts/theme/install-grub-theme.sh +1 -1
- package/scripts/theme/install-lock-icon.sh +1 -1
- package/scripts/theme/merge-wallpaper.sh +1 -1
- package/scripts/theme/rebuild-themes.sh +64 -5
- package/scripts/theme/switch.sh +46 -19
- package/scripts/theme/wallpaper-rotate.sh +1 -1
- package/scripts/theme/wallpaper-sync.sh +1 -1
- package/scripts/tools/cmatrix.sh +1 -1
- package/scripts/tools/detect-collisions.py +1 -1
- package/scripts/tools/emoji-picker.sh +1 -1
- package/scripts/tools/figlet-banner.sh +1 -1
- package/scripts/tools/log-rotate.sh +1 -1
- package/scripts/tools/lolcat-wrap.sh +1 -1
- package/scripts/tools/pipes.sh +1 -1
- package/scripts/tuning/linux.sh +1 -1
- package/scripts/tuning/macos.sh +1 -1
- package/scripts/uninstall.sh +1 -1
- package/scripts/verify-release-versions +156 -0
- package/scripts/version-sync.sh +63 -3
- package/LICENSE +0 -21
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
3
|
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
4
|
# Unified doctor orchestrator — parses flags and dispatches to existing scripts.
|
|
5
5
|
set -euo pipefail
|
|
@@ -20,7 +20,11 @@ target="scripts/diagnostics/doctor.sh"
|
|
|
20
20
|
for arg in "$@"; do
|
|
21
21
|
case "$arg" in
|
|
22
22
|
--heal | -H) target="scripts/ops/heal.sh" ;;
|
|
23
|
-
|
|
23
|
+
# scripts/ops/health-check.sh has never existed in this tree, so
|
|
24
|
+
# `dot doctor --audit` died with "Script not found" for every user who
|
|
25
|
+
# tried it. The health dashboard is what that name meant: it is the
|
|
26
|
+
# audit-shaped diagnostic, and `dot health-check` already aliases it.
|
|
27
|
+
--audit | -a) target="scripts/diagnostics/health.sh" ;;
|
|
24
28
|
--score | -s) target="scripts/diagnostics/scorecard.sh" ;;
|
|
25
29
|
--smoke | -m) target="scripts/diagnostics/smoke-test.sh" ;;
|
|
26
30
|
--drift | -d) target="scripts/diagnostics/drift-dashboard.sh" ;;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
3
|
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
4
|
## Dotfiles Doctor.
|
|
5
5
|
##
|
|
@@ -169,11 +169,11 @@ fi
|
|
|
169
169
|
|
|
170
170
|
# --- AI CLIs ---
|
|
171
171
|
_section "AI CLIs"
|
|
172
|
-
for cmd in claude copilot kimi agy sgpt ollama opencode aider kiro-cli; do
|
|
172
|
+
for cmd in codex claude copilot kimi agy sgpt ollama opencode aider kiro-cli; do
|
|
173
173
|
if check_cmd "$cmd"; then
|
|
174
174
|
_ok "$cmd" "$(pretty_path "$(get_cmd_path "$cmd")")"
|
|
175
175
|
else
|
|
176
|
-
|
|
176
|
+
ui_info "$cmd" "optional (not installed)"
|
|
177
177
|
fi
|
|
178
178
|
done
|
|
179
179
|
|
|
@@ -372,7 +372,11 @@ fi
|
|
|
372
372
|
|
|
373
373
|
# --- State ---
|
|
374
374
|
_section "State"
|
|
375
|
-
|
|
375
|
+
# --exclude=always: a run_before_/run_after_ script (the macOS iCloud hook)
|
|
376
|
+
# is "pending" on every apply by design, so plain `verify` exits 1 forever
|
|
377
|
+
# on a fully synchronised machine. Files, dirs, symlinks and run_once/
|
|
378
|
+
# run_onchange scripts still count.
|
|
379
|
+
if chezmoi verify --exclude=always &>/dev/null; then
|
|
376
380
|
_ok "chezmoi" "synchronized"
|
|
377
381
|
else
|
|
378
382
|
_fail "chezmoi" "drifted (run dot drift)"
|
|
@@ -462,28 +466,46 @@ else
|
|
|
462
466
|
_fail "fish_plugins" "missing (~/.config/fish/fish_plugins)"
|
|
463
467
|
fi
|
|
464
468
|
|
|
469
|
+
# Only meaningful when there is a cargo for topgrade to run it through; a
|
|
470
|
+
# machine without a Rust toolchain has nothing to update, so warning about
|
|
471
|
+
# the missing helper there is noise that never goes away.
|
|
465
472
|
if command -v cargo-install-update >/dev/null 2>&1; then
|
|
466
473
|
_ok "cargo-install-update" "$(pretty_path "$(command -v cargo-install-update)")"
|
|
467
|
-
|
|
474
|
+
elif command -v cargo >/dev/null 2>&1; then
|
|
468
475
|
_warn "cargo-install-update" "missing (install: cargo install cargo-update)"
|
|
476
|
+
else
|
|
477
|
+
_ok "cargo-install-update" "not needed (cargo not installed)"
|
|
469
478
|
fi
|
|
470
479
|
|
|
471
480
|
# --- Symlinks ---
|
|
472
481
|
broken_links=0
|
|
482
|
+
broken_list=""
|
|
473
483
|
for root in "$HOME/.config" "$HOME/.local/bin" "$HOME/.local/share" "$HOME/.ssh"; do
|
|
474
484
|
[[ -d "$root" ]] || continue
|
|
475
485
|
while IFS= read -r -d '' link; do
|
|
476
486
|
link_name="$(basename "$link")"
|
|
477
487
|
[[ "$link" == *"google-chrome-backup"* ]] && continue
|
|
478
488
|
[[ "$link_name" == SingletonLock || "$link_name" == SingletonCookie || "$link_name" == SingletonSocket ]] && continue
|
|
479
|
-
[[ -e "$link" ]]
|
|
489
|
+
[[ -e "$link" ]] && continue
|
|
490
|
+
# A link into ~/Library/Caches dangling is macOS working as designed, not a
|
|
491
|
+
# health problem: the OS purges that directory whenever it wants the space,
|
|
492
|
+
# and the owning tool recreates its cache on next use. ~/.config/swiftpm/cache
|
|
493
|
+
# -> ~/Library/Caches/org.swift.swiftpm is the usual one; it was "fixed" by
|
|
494
|
+
# recreating the target earlier the same day and had broken again by evening,
|
|
495
|
+
# which is the tell that it is not fixable, only re-reported.
|
|
496
|
+
case "$(readlink "$link" 2>/dev/null)" in
|
|
497
|
+
"$HOME/Library/Caches/"*) continue ;;
|
|
498
|
+
esac
|
|
499
|
+
broken_links=$((broken_links + 1))
|
|
500
|
+
broken_list="${broken_list:+$broken_list, }$(pretty_path "$link")"
|
|
480
501
|
done < <(find "$root" -maxdepth 3 -type l -print0 2>/dev/null)
|
|
481
502
|
done
|
|
482
503
|
|
|
483
504
|
if [[ $broken_links -eq 0 ]]; then
|
|
484
505
|
_ok "symlinks" "none broken"
|
|
485
506
|
else
|
|
486
|
-
|
|
507
|
+
# Name them. "1 broken" with no path meant hunting for it by hand, twice.
|
|
508
|
+
_warn "symlinks" "$broken_links broken: $broken_list"
|
|
487
509
|
fi
|
|
488
510
|
|
|
489
511
|
# --- Portability ---
|
|
@@ -548,8 +570,10 @@ for tool in mise starship zoxide atuin fzf direnv; do
|
|
|
548
570
|
done
|
|
549
571
|
done
|
|
550
572
|
if [[ $stale_caches -eq 0 ]]; then
|
|
573
|
+
caches_fresh="fresh"
|
|
551
574
|
_ok "shell caches" "fresh"
|
|
552
575
|
else
|
|
576
|
+
caches_fresh="stale"
|
|
553
577
|
_warn "shell caches" "stale ($stale_tools) — run dot prewarm"
|
|
554
578
|
fi
|
|
555
579
|
|
|
@@ -618,8 +642,22 @@ fi
|
|
|
618
642
|
# A mise-managed 2026 dev machine routinely adds 60-90 entries (one per
|
|
619
643
|
# installed tool version + per-shim path), so the warn/fail thresholds
|
|
620
644
|
# reflect that baseline rather than a lean default (~40).
|
|
645
|
+
#
|
|
646
|
+
# The OK ceiling was 75, which contradicted the 60-90 baseline stated right
|
|
647
|
+
# above it and warned on every healthy mise machine. Worse, the implied remedy
|
|
648
|
+
# was backwards: those per-tool entries are what let a command resolve to the
|
|
649
|
+
# real binary instead of falling through to a mise shim. Measured 2026-08-20,
|
|
650
|
+
# 20 invocations of ripgrep --version:
|
|
651
|
+
#
|
|
652
|
+
# via shim 2741ms (137ms per call)
|
|
653
|
+
# direct install dir 62ms (3.1ms per call)
|
|
654
|
+
#
|
|
655
|
+
# ~44x. "Pruning" those entries to satisfy a length check would trade
|
|
656
|
+
# microseconds of PATH scan for ~134ms on every single tool invocation. The
|
|
657
|
+
# ceiling now matches the documented baseline; >90 still warns, because past
|
|
658
|
+
# that the entries are worth auditing for tools you no longer use.
|
|
621
659
|
path_count=$(printf '%s' "${PATH:-}" | tr ':' '\n' | grep -c . || true)
|
|
622
|
-
if [[ "$path_count" -le
|
|
660
|
+
if [[ "$path_count" -le 90 ]]; then
|
|
623
661
|
_ok "PATH length" "$path_count entries"
|
|
624
662
|
elif [[ "$path_count" -le 120 ]]; then
|
|
625
663
|
_warn "PATH length" "$path_count entries — consider pruning"
|
|
@@ -666,10 +704,18 @@ if command -v zsh >/dev/null 2>&1 && command -v timeout >/dev/null 2>&1; then
|
|
|
666
704
|
fi
|
|
667
705
|
|
|
668
706
|
if command -v hyperfine >/dev/null 2>&1; then
|
|
669
|
-
if bash "$SCRIPT_DIR/../../
|
|
707
|
+
if bash "$SCRIPT_DIR/../../benches/bench.sh" 2>/dev/null; then
|
|
670
708
|
_ok "startup latency" "within target thresholds"
|
|
671
709
|
else
|
|
672
|
-
|
|
710
|
+
# Only suggest prewarm when it could actually help. The caches were
|
|
711
|
+
# already reported fresh above in the common case, and telling someone to
|
|
712
|
+
# re-run a no-op sends them in a circle — as it did on 2026-08-20, where
|
|
713
|
+
# prewarm changed nothing because nothing was cold.
|
|
714
|
+
if [[ "${caches_fresh:-unknown}" == "fresh" ]]; then
|
|
715
|
+
_warn "startup latency" "threshold exceeded (caches already fresh — profile with 'dot benchmark')"
|
|
716
|
+
else
|
|
717
|
+
_warn "startup latency" "threshold exceeded (run dot prewarm)"
|
|
718
|
+
fi
|
|
673
719
|
fi
|
|
674
720
|
else
|
|
675
721
|
_warn "hyperfine" "missing (benchmark skipped)"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
3
|
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
4
|
# =============================================================================
|
|
5
5
|
# drift-dashboard.sh — Consolidated drift report.
|
|
@@ -71,7 +71,8 @@ fi
|
|
|
71
71
|
# Class 1: chezmoi-managed drift
|
|
72
72
|
# -----------------------------------------------------------------------------
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
# --exclude=always: always-run scripts are pending by design, not drift.
|
|
75
|
+
cm_status="$(chezmoi status --exclude=always 2>/dev/null || true)"
|
|
75
76
|
cm_count=0
|
|
76
77
|
if [[ -n "$cm_status" ]]; then
|
|
77
78
|
cm_count=$(printf '%s\n' "$cm_status" | wc -l | tr -d ' ')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
3
|
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
4
|
# Dotfiles Health Check Dashboard
|
|
5
5
|
# Usage: dot health [--verbose|-v] [--json|-j] [--fix|-f] [--force|-F]
|
|
@@ -124,7 +124,7 @@ check() {
|
|
|
124
124
|
if [[ "$use_ui" = "1" ]]; then
|
|
125
125
|
ui_warn "$name" "$message"
|
|
126
126
|
else
|
|
127
|
-
printf "${YELLOW}⚠${NC} %-35s ${YELLOW}WARNING${NC}"
|
|
127
|
+
printf "${YELLOW}⚠${NC} %-35s ${YELLOW}WARNING${NC}" "$name"
|
|
128
128
|
[[ -n "$message" ]] && printf " ${GRAY}%s${NC}" "$message"
|
|
129
129
|
printf "\n"
|
|
130
130
|
fi
|
|
@@ -137,7 +137,7 @@ check() {
|
|
|
137
137
|
if [[ "$use_ui" = "1" ]]; then
|
|
138
138
|
ui_err "$name" "$message"
|
|
139
139
|
else
|
|
140
|
-
printf "${RED}✗${NC} %-35s ${RED}FAILED${NC}"
|
|
140
|
+
printf "${RED}✗${NC} %-35s ${RED}FAILED${NC}" "$name"
|
|
141
141
|
[[ -n "$message" ]] && printf " ${GRAY}%s${NC}" "$message"
|
|
142
142
|
printf "\n"
|
|
143
143
|
fi
|
|
@@ -235,7 +235,13 @@ check_dev_tools() {
|
|
|
235
235
|
|
|
236
236
|
if has_command node; then
|
|
237
237
|
local node_version
|
|
238
|
-
|
|
238
|
+
# `|| true` is load-bearing under `set -e`: an assignment takes the exit
|
|
239
|
+
# status of its command substitution, so a `node` that fails to report a
|
|
240
|
+
# version aborts the entire health report mid-section — no Summary, no
|
|
241
|
+
# score, rc=1. That is exactly what happened when `node` resolved to a
|
|
242
|
+
# mise shim under a sandboxed HOME. A diagnostic tool must survive the
|
|
243
|
+
# tools it is diagnosing.
|
|
244
|
+
node_version=$(node --version 2>/dev/null || true)
|
|
239
245
|
check "Node.js ($node_version)" "pass"
|
|
240
246
|
else
|
|
241
247
|
check "Node.js" "warn" "Not installed"
|
|
@@ -251,7 +257,10 @@ check_dev_tools() {
|
|
|
251
257
|
|
|
252
258
|
if has_command python3; then
|
|
253
259
|
local py_version
|
|
254
|
-
|
|
260
|
+
# pipefail is on, so this pipeline reports python3's failure even though
|
|
261
|
+
# `cut` succeeds — and an assignment inherits that status, aborting the
|
|
262
|
+
# report under `set -e`. Same hazard as node_version above.
|
|
263
|
+
py_version=$(python3 --version 2>/dev/null | cut -d' ' -f2 || true)
|
|
255
264
|
check "Python ($py_version)" "pass"
|
|
256
265
|
else
|
|
257
266
|
check "Python" "warn" "Not installed"
|
|
@@ -335,7 +344,12 @@ check_security() {
|
|
|
335
344
|
|
|
336
345
|
if has_command age; then
|
|
337
346
|
check "Age encryption" "pass"
|
|
338
|
-
|
|
347
|
+
local age_identity="${HOME}/.config/chezmoi/key.txt" configured_identity=""
|
|
348
|
+
if has_command chezmoi && has_command jq; then
|
|
349
|
+
configured_identity=$(chezmoi dump-config --format=json 2>/dev/null | jq -r '.age.identity // empty' 2>/dev/null || true)
|
|
350
|
+
[[ -z "$configured_identity" ]] || age_identity="${configured_identity/#\~/$HOME}"
|
|
351
|
+
fi
|
|
352
|
+
if [[ -f "$age_identity" ]]; then
|
|
339
353
|
check "Age key configured" "pass"
|
|
340
354
|
else
|
|
341
355
|
check "Age key configured" "warn" "Key not found"
|
|
@@ -344,17 +358,35 @@ check_security() {
|
|
|
344
358
|
check "Age encryption" "warn" "Not installed"
|
|
345
359
|
fi
|
|
346
360
|
|
|
347
|
-
|
|
361
|
+
local -a ssh_keys=("$HOME/.ssh/id_ed25519" "$HOME/.ssh/id_rsa" "$HOME/.ssh/id_ed25519_sk")
|
|
362
|
+
local public_key key existing_key keys_found=false
|
|
363
|
+
for public_key in "$HOME/.ssh/"*.pub; do
|
|
364
|
+
[[ -f "$public_key" && -f "${public_key%.pub}" ]] || continue
|
|
365
|
+
key="${public_key%.pub}"
|
|
366
|
+
for existing_key in "${ssh_keys[@]}"; do
|
|
367
|
+
[[ "$existing_key" != "$key" ]] || continue 2
|
|
368
|
+
done
|
|
369
|
+
ssh_keys+=("$key")
|
|
370
|
+
done
|
|
371
|
+
for key in "${ssh_keys[@]}"; do
|
|
372
|
+
[[ ! -f "$key" ]] || keys_found=true
|
|
373
|
+
done
|
|
374
|
+
if $keys_found; then
|
|
348
375
|
check "SSH keys present" "pass"
|
|
349
376
|
else
|
|
350
377
|
check "SSH keys present" "warn" "No keys found"
|
|
351
378
|
fi
|
|
352
379
|
|
|
353
380
|
# Check SSH key permissions
|
|
354
|
-
for key in "$
|
|
381
|
+
for key in "${ssh_keys[@]}"; do
|
|
355
382
|
if [[ -f "$key" ]]; then
|
|
356
383
|
local perms
|
|
357
|
-
|
|
384
|
+
# Same hazard as node_version above: if BOTH stat spellings fail (a
|
|
385
|
+
# platform with neither GNU nor BSD stat), the assignment is non-zero
|
|
386
|
+
# and `set -e` kills the report. Empty perms falls through to the
|
|
387
|
+
# "should be 600" warning, which is the right answer when the mode
|
|
388
|
+
# cannot be read.
|
|
389
|
+
perms=$(stat -c '%a' "$key" 2>/dev/null || stat -f '%Lp' "$key" 2>/dev/null || true)
|
|
358
390
|
if [[ "$perms" != "600" && "$perms" != "400" ]]; then
|
|
359
391
|
check "SSH key perms (${key##*/})" "warn" "mode $perms (should be 600)"
|
|
360
392
|
else
|
|
@@ -440,7 +472,8 @@ check_sync_status() {
|
|
|
440
472
|
# needs warned about via the health dashboard.
|
|
441
473
|
if has_command chezmoi; then
|
|
442
474
|
local status_output
|
|
443
|
-
|
|
475
|
+
# --exclude=always: always-run scripts are pending by design, not drift.
|
|
476
|
+
status_output=$(chezmoi status --exclude=always 2>/dev/null || echo "")
|
|
444
477
|
if [[ -z "$status_output" ]]; then
|
|
445
478
|
check "Chezmoi sync" "pass"
|
|
446
479
|
else
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
3
|
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
4
|
## MCP configuration diagnostics and hardening checks.
|
|
5
5
|
##
|
|
@@ -398,7 +398,7 @@ if command -v jq >/dev/null 2>&1; then
|
|
|
398
398
|
{
|
|
399
399
|
jq -r '.mcpServers | to_entries[]? | (.value.env // {}) | to_entries[]?.value' "$MCP_CONFIG"
|
|
400
400
|
jq -r '.mcpServers | to_entries[]? | (.value.args // [])[]?' "$MCP_CONFIG"
|
|
401
|
-
} | sed -n 's/^\${\([A-Z0-9_]
|
|
401
|
+
} | sed -n 's/^\${\([A-Z0-9_][A-Z0-9_]*\)}$/\1/p' | sort -u
|
|
402
402
|
)"
|
|
403
403
|
if [[ -z "$env_vars" ]]; then
|
|
404
404
|
log_success "Server env placeholders" "none declared"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
3
|
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
4
|
# Dotfiles Scorecard
|
|
5
5
|
# Usage: dot scorecard [--json|-j]
|
|
@@ -52,7 +52,8 @@ perf_target=$(python3 -c 'import json,sys; j=json.loads(sys.stdin.read()); print
|
|
|
52
52
|
|
|
53
53
|
# Drift count
|
|
54
54
|
if command -v chezmoi >/dev/null 2>&1; then
|
|
55
|
-
|
|
55
|
+
# --exclude=always: always-run scripts are pending by design, not drift.
|
|
56
|
+
drift_count=$(chezmoi status --exclude=always 2>/dev/null | wc -l | tr -d ' ')
|
|
56
57
|
else
|
|
57
58
|
drift_count=0
|
|
58
59
|
fi
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
3
|
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
4
|
## Workstation attestation export.
|
|
5
5
|
##
|
|
@@ -25,12 +25,23 @@ JSON_MODE=0
|
|
|
25
25
|
WRITE_PATH=""
|
|
26
26
|
FLEET_STORE=""
|
|
27
27
|
FLEET_ID=""
|
|
28
|
+
VERIFY=0
|
|
29
|
+
VERIFY_MAX_AGE=""
|
|
28
30
|
while [[ $# -gt 0 ]]; do
|
|
29
31
|
case "$1" in
|
|
30
32
|
--json | -j)
|
|
31
33
|
JSON_MODE=1
|
|
32
34
|
shift
|
|
33
35
|
;;
|
|
36
|
+
--verify | -V)
|
|
37
|
+
VERIFY=1
|
|
38
|
+
shift
|
|
39
|
+
;;
|
|
40
|
+
--max-age | -a)
|
|
41
|
+
VERIFY_MAX_AGE="${2:-}"
|
|
42
|
+
VERIFY=1
|
|
43
|
+
shift 2
|
|
44
|
+
;;
|
|
34
45
|
--write | -w)
|
|
35
46
|
WRITE_PATH="${2:-}"
|
|
36
47
|
shift 2
|
|
@@ -171,6 +182,20 @@ if [[ -n "$FLEET_STORE" ]]; then
|
|
|
171
182
|
printf '%s\n' "$attestation_json" >"$fleet_timestamp_file"
|
|
172
183
|
fi
|
|
173
184
|
|
|
185
|
+
# Hand the evidence to the WebAssembly verifier rather than printing it.
|
|
186
|
+
# The module has no access to this machine beyond the bytes on its stdin, so
|
|
187
|
+
# the verdict does not depend on the tools the machine under review happens
|
|
188
|
+
# to have installed.
|
|
189
|
+
if [[ "$VERIFY" -eq 1 ]]; then
|
|
190
|
+
verify_args=(--verify)
|
|
191
|
+
[[ "$JSON_MODE" -eq 1 ]] && verify_args+=(--json)
|
|
192
|
+
[[ -n "$VERIFY_MAX_AGE" ]] && verify_args+=(--max-age "$VERIFY_MAX_AGE")
|
|
193
|
+
verify_status=0
|
|
194
|
+
printf '%s\n' "$attestation_json" |
|
|
195
|
+
bash "$SCRIPT_DIR/attest-verify.sh" "${verify_args[@]}" || verify_status=$?
|
|
196
|
+
exit "$verify_status"
|
|
197
|
+
fi
|
|
198
|
+
|
|
174
199
|
if [[ "$JSON_MODE" -eq 1 ]]; then
|
|
175
200
|
printf '%s\n' "$attestation_json"
|
|
176
201
|
else
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
3
|
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
4
|
# Dotfiles CLI - Agent Mode Commands (extracted from meta.sh)
|
|
5
5
|
# mode list|current|show|set|run|doctor|card|log|checkpoint|conformance
|
|
@@ -21,6 +21,55 @@ _agent_repo_root() {
|
|
|
21
21
|
require_source_dir
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
## _agent_run_bounded <seconds> <command…> — run a command under a wall-clock
|
|
25
|
+
## limit on every platform we ship to.
|
|
26
|
+
##
|
|
27
|
+
## `timeout` is GNU coreutils. Stock macOS has neither it nor `gtimeout`
|
|
28
|
+
## (that one arrives with `brew install coreutils`), so the delegate arm's
|
|
29
|
+
## former `timeout "$t" "$@"` meant the delegated command never ran at all on
|
|
30
|
+
## a clean Mac — it died 127, "command not found". perl is present on both
|
|
31
|
+
## macOS and the CI runners and gives us fork + alarm, which is what timeout(1)
|
|
32
|
+
## does. tests/framework/assertions.sh solves the same problem the same way.
|
|
33
|
+
##
|
|
34
|
+
## Exit codes follow GNU timeout: 124 on expiry, 128+N when signalled,
|
|
35
|
+
## otherwise the command's own status.
|
|
36
|
+
_agent_run_bounded() {
|
|
37
|
+
local seconds="$1"
|
|
38
|
+
shift
|
|
39
|
+
local bin
|
|
40
|
+
bin="$(type -P timeout || true)"
|
|
41
|
+
[[ -n "$bin" ]] || bin="$(type -P gtimeout || true)"
|
|
42
|
+
if [[ -n "$bin" ]]; then
|
|
43
|
+
"$bin" "$seconds" "$@"
|
|
44
|
+
return
|
|
45
|
+
fi
|
|
46
|
+
if command -v perl >/dev/null 2>&1; then
|
|
47
|
+
perl -e '
|
|
48
|
+
my $secs = shift @ARGV;
|
|
49
|
+
my $pid = fork();
|
|
50
|
+
die "dot: fork failed: $!\n" unless defined $pid;
|
|
51
|
+
if ($pid == 0) {
|
|
52
|
+
exec { $ARGV[0] } @ARGV;
|
|
53
|
+
exit 127;
|
|
54
|
+
}
|
|
55
|
+
my $timed_out = 0;
|
|
56
|
+
$SIG{ALRM} = sub { $timed_out = 1; kill("TERM", $pid); kill("KILL", $pid); };
|
|
57
|
+
alarm($secs);
|
|
58
|
+
my $reaped;
|
|
59
|
+
do { $reaped = waitpid($pid, 0); } while ($reaped == -1 && $!{EINTR});
|
|
60
|
+
my $status = $?;
|
|
61
|
+
alarm(0);
|
|
62
|
+
exit(124) if $timed_out;
|
|
63
|
+
exit($status & 127 ? 128 + ($status & 127) : $status >> 8);
|
|
64
|
+
' "$seconds" "$@"
|
|
65
|
+
return
|
|
66
|
+
fi
|
|
67
|
+
# Neither available: running the command is still better than refusing to,
|
|
68
|
+
# but the lost guarantee is said out loud rather than assumed.
|
|
69
|
+
ui_warn "Delegate" "no timeout(1) or perl on PATH — running without a time limit"
|
|
70
|
+
"$@"
|
|
71
|
+
}
|
|
72
|
+
|
|
24
73
|
_agent_profiles_file() {
|
|
25
74
|
local repo_root
|
|
26
75
|
repo_root="$(_agent_repo_root)"
|
|
@@ -228,15 +277,16 @@ EOF
|
|
|
228
277
|
checkpoint_id="$(basename "$checkpoint_file" .json)"
|
|
229
278
|
ui_info "Agent mode" "$name"
|
|
230
279
|
dot_agent_session_log "run_start" "$name" "running" "argv=$*" "checkpoint_id=$checkpoint_id"
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
280
|
+
# Run inside the `if` condition (errexit is suspended there) and
|
|
281
|
+
# read `$?` in the else-branch: it holds the command's own status.
|
|
282
|
+
# `if ! cmd; then exit_code=$?` reads the negation's status (0) and
|
|
283
|
+
# silently reported every failure as exit 0.
|
|
234
284
|
local exit_code=0
|
|
235
|
-
if
|
|
285
|
+
if "$@"; then
|
|
286
|
+
dot_agent_session_log "run_finish" "$name" "ok" "exit_code=$exit_code" "checkpoint_id=$checkpoint_id"
|
|
287
|
+
else
|
|
236
288
|
exit_code=$?
|
|
237
289
|
dot_agent_session_log "run_finish" "$name" "failed" "exit_code=$exit_code" "checkpoint_id=$checkpoint_id"
|
|
238
|
-
else
|
|
239
|
-
dot_agent_session_log "run_finish" "$name" "ok" "exit_code=$exit_code" "checkpoint_id=$checkpoint_id"
|
|
240
290
|
fi
|
|
241
291
|
return "$exit_code"
|
|
242
292
|
;;
|
|
@@ -347,11 +397,11 @@ EOF
|
|
|
347
397
|
_agent_apply_profile_env "$replay_profile"
|
|
348
398
|
dot_agent_session_log "checkpoint_replay" "$replay_profile" "running" "checkpoint_id=$checkpoint_id"
|
|
349
399
|
local exit_code=0
|
|
350
|
-
if
|
|
400
|
+
if "${replay_argv[@]}"; then
|
|
401
|
+
dot_agent_session_log "checkpoint_replay_finish" "$replay_profile" "ok" "checkpoint_id=$checkpoint_id" "exit_code=$exit_code"
|
|
402
|
+
else
|
|
351
403
|
exit_code=$?
|
|
352
404
|
dot_agent_session_log "checkpoint_replay_finish" "$replay_profile" "failed" "checkpoint_id=$checkpoint_id" "exit_code=$exit_code"
|
|
353
|
-
else
|
|
354
|
-
dot_agent_session_log "checkpoint_replay_finish" "$replay_profile" "ok" "checkpoint_id=$checkpoint_id" "exit_code=$exit_code"
|
|
355
405
|
fi
|
|
356
406
|
return "$exit_code"
|
|
357
407
|
;;
|
|
@@ -392,13 +442,13 @@ EOF
|
|
|
392
442
|
dot_agent_session_log "delegate_start" "$delegate_profile" "running" "delegate=$delegate_name" "parent=$current_profile" "timeout=$delegate_timeout"
|
|
393
443
|
ui_info "Delegating" "$delegate_name (profile: $delegate_profile, timeout: ${delegate_timeout}s)"
|
|
394
444
|
local exit_code=0
|
|
395
|
-
if
|
|
445
|
+
if _agent_run_bounded "$delegate_timeout" "$@"; then
|
|
446
|
+
dot_agent_session_log "delegate_finish" "$delegate_profile" "ok" "delegate=$delegate_name" "exit_code=$exit_code"
|
|
447
|
+
ui_ok "Delegate" "$delegate_name completed"
|
|
448
|
+
else
|
|
396
449
|
exit_code=$?
|
|
397
450
|
dot_agent_session_log "delegate_finish" "$delegate_profile" "failed" "delegate=$delegate_name" "exit_code=$exit_code"
|
|
398
451
|
ui_err "Delegate" "$delegate_name failed (exit $exit_code)"
|
|
399
|
-
else
|
|
400
|
-
dot_agent_session_log "delegate_finish" "$delegate_profile" "ok" "delegate=$delegate_name" "exit_code=$exit_code"
|
|
401
|
-
ui_ok "Delegate" "$delegate_name completed"
|
|
402
452
|
fi
|
|
403
453
|
return "$exit_code"
|
|
404
454
|
;;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
3
|
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
4
|
# shellcheck shell=bash
|
|
5
5
|
#
|
|
@@ -148,10 +148,18 @@ cmd_agents() {
|
|
|
148
148
|
ui_warn "AGENTS.md" "missing — run 'dot agents render'"
|
|
149
149
|
return 1
|
|
150
150
|
fi
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
# block
|
|
154
|
-
|
|
151
|
+
# Compare the bodies (header comments differ by design), ignoring
|
|
152
|
+
# blank lines: the rendered file carries an extra one after its
|
|
153
|
+
# header block. This deliberately does NOT lean on
|
|
154
|
+
# `diff -q --ignore-blank-lines` — some BSD diff builds (macOS 14's,
|
|
155
|
+
# for one) short-circuit `-q` to a byte comparison and drop the
|
|
156
|
+
# ignore flags, which made `check` report drift on a tree it had
|
|
157
|
+
# just rendered. Stripping the blank lines here is implementation-
|
|
158
|
+
# independent.
|
|
159
|
+
local _claude_body _agents_body_text
|
|
160
|
+
_claude_body="$(_agents_body "$claude_md" | grep -v '^[[:space:]]*$')"
|
|
161
|
+
_agents_body_text="$(_agents_body "$agents_md" | grep -v '^[[:space:]]*$')"
|
|
162
|
+
if [[ "$_claude_body" == "$_agents_body_text" ]]; then
|
|
155
163
|
ui_ok "AGENTS.md" "in sync with CLAUDE.md"
|
|
156
164
|
return 0
|
|
157
165
|
fi
|
|
@@ -185,9 +193,17 @@ cmd_agents() {
|
|
|
185
193
|
-->
|
|
186
194
|
|
|
187
195
|
HEADER
|
|
188
|
-
#
|
|
189
|
-
#
|
|
190
|
-
_agents_body
|
|
196
|
+
# The title is emitted, not substituted. This used to be
|
|
197
|
+
#
|
|
198
|
+
# _agents_body … | sed '1s/^# CLAUDE\.md.*/# AGENTS.md — …/'
|
|
199
|
+
#
|
|
200
|
+
# but _agents_body drops the H1 by design (the two files differ
|
|
201
|
+
# there), so line 1 of its output was never the CLAUDE.md title and
|
|
202
|
+
# the substitution could not fire: the rendered file carried no title
|
|
203
|
+
# at all. `check` is unaffected either way — it compares bodies with
|
|
204
|
+
# the H1 stripped from both sides.
|
|
205
|
+
printf '# AGENTS.md — AI Assistant Guidelines\n'
|
|
206
|
+
_agents_body "$claude_md"
|
|
191
207
|
cat <<'FOOTER'
|
|
192
208
|
|
|
193
209
|
---
|