@sebastienrousseau/dotfiles 0.2.520 → 0.2.522
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 +52 -0
- package/LICENSE-APACHE +190 -0
- package/LICENSE-MIT +21 -0
- package/README.md +43 -37
- package/install.sh +76 -10
- package/package.json +7 -7
- package/tools/README.md +49 -0
- package/tools/ci/install-chezmoi-verified.sh +68 -0
- package/docs/.vitepress/reports/localization-readability-audit.md +0 -73
- package/docs/AI.md +0 -179
- package/docs/ARCHITECTURE.md +0 -117
- package/docs/CNAME +0 -1
- package/docs/CONFIG_STRATEGY.md +0 -124
- package/docs/COPYRIGHT +0 -7
- package/docs/ECOSYSTEM.md +0 -220
- package/docs/GOLD-STANDARD-AUDIT.md +0 -352
- package/docs/GOVERNANCE.md +0 -98
- package/docs/MAINTAINERS.md +0 -41
- package/docs/MINIMUM-TOOLCHAIN.md +0 -100
- package/docs/NAMING_CONVENTIONS.md +0 -102
- package/docs/OPENCODE.md +0 -127
- package/docs/README.md +0 -84
- package/docs/STRUCTURE.md +0 -102
- package/docs/adr/ADR-001-ci-cd-pipeline.md +0 -118
- package/docs/adr/ADR-002-shell-performance.md +0 -130
- package/docs/adr/ADR-003-security-first.md +0 -158
- package/docs/adr/ADR-004-cli-architecture.md +0 -171
- package/docs/adr/ADR-005-chezmoi-choice.md +0 -99
- package/docs/adr/ADR-006-shell-selection.md +0 -124
- package/docs/adr/ADR-007-multi-shell-parity.md +0 -62
- package/docs/adr/ADR-008-alias-system-architecture.md +0 -95
- package/docs/adr/ADR-009-wallpaper-driven-theming.md +0 -131
- package/docs/adr/ADR-010-starship-transient-prompt.md +0 -144
- package/docs/adr/ADR-011-nushell-tier3-keep.md +0 -144
- package/docs/adr/ADR-012-ai-fleet-local-proxy.md +0 -79
- package/docs/adr/README.md +0 -40
- package/docs/architecture/AI_COST_OPTIMIZATION.md +0 -144
- package/docs/architecture/ARCHITECTURE.md +0 -20
- package/docs/architecture/INTEROP.md +0 -44
- package/docs/architecture/REPO_LAYOUT.md +0 -241
- package/docs/architecture/WALKTHROUGH.md +0 -86
- package/docs/architecture/fleet-deployment.md +0 -77
- package/docs/archive/EUXIS_2026_REVIEW.md +0 -127
- package/docs/archive/LEGACY_ROADMAP.md +0 -6
- package/docs/archive/MILESTONE_v0.2.493.md +0 -47
- package/docs/archive/PLAN.md +0 -199
- package/docs/archive/REPO_AUDIT.md +0 -31
- package/docs/articles/.pages +0 -6
- package/docs/articles/2026-07-05-custom-mkdocs-material-dark-theme.md +0 -216
- package/docs/articles/2026-07-05-fish-startup-abbr.md +0 -153
- package/docs/articles/2026-07-05-master-to-main-rename-runbook.md +0 -128
- package/docs/articles/index.md +0 -36
- package/docs/guides/INSTALL.md +0 -144
- package/docs/guides/MACOS_ICLOUD_SYMLINKS.md +0 -121
- package/docs/guides/NEOVIM_IDE_GUIDE.md +0 -61
- package/docs/guides/THEMING.md +0 -230
- package/docs/guides/TROUBLESHOOTING.md +0 -176
- package/docs/guides/WSL2_NIX_TROUBLESHOOTING.md +0 -792
- package/docs/index.md +0 -132
- package/docs/interop/A2A.md +0 -39
- package/docs/interop/POWERSHELL.md +0 -102
- package/docs/manual/00-introduction.md +0 -89
- package/docs/manual/01-concepts/01-architecture.md +0 -138
- package/docs/manual/01-concepts/02-trust-model.md +0 -183
- package/docs/manual/01-concepts/03-theme-engine.md +0 -186
- package/docs/manual/01-concepts/04-fleet.md +0 -148
- package/docs/manual/01-concepts/05-self-healing.md +0 -204
- package/docs/manual/02-tutorials/01-first-install.md +0 -197
- package/docs/manual/02-tutorials/02-add-wallpaper.md +0 -216
- package/docs/manual/02-tutorials/03-create-profile.md +0 -244
- package/docs/manual/02-tutorials/04-encrypt-secret.md +0 -281
- package/docs/manual/02-tutorials/05-deploy-fleet.md +0 -283
- package/docs/manual/03-reference/01-dot-cli.md +0 -475
- package/docs/manual/03-reference/02-config-files.md +0 -265
- package/docs/manual/03-reference/03-environment.md +0 -124
- package/docs/manual/03-reference/04-templates.md +0 -190
- package/docs/manual/03-reference/05-feature-flags.md +0 -187
- package/docs/manual/04-cookbook/01-recipes.md +0 -285
- package/docs/manual/04-cookbook/02-troubleshooting.md +0 -351
- package/docs/manual/04-cookbook/03-faq.md +0 -175
- package/docs/manual/05-appendices/A-platform-matrix.md +0 -101
- package/docs/manual/05-appendices/B-security-checklist.md +0 -85
- package/docs/manual/05-appendices/C-glossary.md +0 -40
- package/docs/manual/05-appendices/D-bibliography.md +0 -58
- package/docs/manual/05-appendices/E-license.md +0 -38
- package/docs/manual/_toc.yml +0 -58
- package/docs/manual/command-index.md +0 -175
- package/docs/manual/concept-index.md +0 -170
- package/docs/manual/index.md +0 -66
- package/docs/migration/README.md +0 -81
- package/docs/migration/from-bare-git-repo.md +0 -156
- package/docs/migration/from-gnu-stow.md +0 -165
- package/docs/migration/from-plain-chezmoi.md +0 -148
- package/docs/migration/from-yadm.md +0 -187
- package/docs/operations/ARCHITECTURE_ROADMAP.md +0 -7
- package/docs/operations/ATTESTATION.md +0 -44
- package/docs/operations/CI_CADENCE.md +0 -107
- package/docs/operations/CI_COMPOSITES.md +0 -156
- package/docs/operations/COMPLETIONS.md +0 -123
- package/docs/operations/COVERAGE.md +0 -204
- package/docs/operations/DRIFT.md +0 -107
- package/docs/operations/HARD_AUDIT_2026.md +0 -631
- package/docs/operations/MAINTENANCE.md +0 -63
- package/docs/operations/MANIFEST.md +0 -127
- package/docs/operations/MIGRATION.md +0 -109
- package/docs/operations/OPERATIONS.md +0 -188
- package/docs/operations/PERFORMANCE.md +0 -133
- package/docs/operations/PERFORMANCE_BUDGETS.md +0 -196
- package/docs/operations/REGISTRY.md +0 -90
- package/docs/operations/RELEASE_PIPELINE.md +0 -128
- package/docs/operations/RELIABILITY.md +0 -122
- package/docs/operations/RFC_v0_2_503_reorganization.md +0 -280
- package/docs/operations/ROADMAP.md +0 -10
- package/docs/operations/ROADMAP_2026.md +0 -7
- package/docs/operations/ROADMAP_V0_2_503.md +0 -10
- package/docs/operations/TESTING.md +0 -216
- package/docs/operations/TRACEABILITY.md +0 -44
- package/docs/operations/TRUSTED_AGENT_WORKSTATION.md +0 -65
- package/docs/operations/VERSION_SYNC.md +0 -393
- package/docs/packaging.md +0 -222
- package/docs/reference/ALIASES.md +0 -131
- package/docs/reference/ALIASES_CHEATSHEET.md +0 -32
- package/docs/reference/ALIASES_DEPRECATIONS.md +0 -13
- package/docs/reference/FEATURE-MATRIX.md +0 -646
- package/docs/reference/FEATURES.md +0 -66
- package/docs/reference/FONTS.md +0 -112
- package/docs/reference/POWERSHELL_PARITY.md +0 -82
- package/docs/reference/PROFILES.md +0 -69
- package/docs/reference/SCREENSHOTS.md +0 -121
- package/docs/reference/SCRIPTS.md +0 -71
- package/docs/reference/SUPPORT_MATRIX.md +0 -80
- package/docs/reference/THEMES.md +0 -117
- package/docs/reference/TOOLS.md +0 -110
- package/docs/reference/UTILS.md +0 -243
- package/docs/registry.json +0 -6
- package/docs/schema/dot-env-v1.json +0 -110
- package/docs/schema/dot-registry-v1.json +0 -33
- package/docs/security/AI_ACT_COMPLIANCE.md +0 -94
- package/docs/security/AUDIT_BYPASS.md +0 -103
- package/docs/security/AUTOMATION_SECRETS.md +0 -26
- package/docs/security/CI_EGRESS_ALLOWLIST.md +0 -127
- package/docs/security/CI_PINNING.md +0 -129
- package/docs/security/COMMIT_SIGNING.md +0 -138
- package/docs/security/COMPLIANCE.md +0 -458
- package/docs/security/DEPS_DEV_EXCEPTIONS.md +0 -86
- package/docs/security/DISCLOSURE.md +0 -130
- package/docs/security/ENCRYPTION.md +0 -57
- package/docs/security/FMEA.md +0 -159
- package/docs/security/FUZZING.md +0 -209
- package/docs/security/HISTORY_FILTERING.md +0 -132
- package/docs/security/INCIDENT_RESPONSE.md +0 -579
- package/docs/security/INSTALL_VERIFICATION.md +0 -122
- package/docs/security/KEYS.md +0 -49
- package/docs/security/KEY_ROTATION.md +0 -303
- package/docs/security/MCP_POLICY.md +0 -78
- package/docs/security/POLICY_RELEASES.md +0 -37
- package/docs/security/README.md +0 -28
- package/docs/security/SCORECARD.md +0 -195
- package/docs/security/SECRETS.md +0 -158
- package/docs/security/SECURITY.md +0 -45
- package/docs/security/SECURITY_CHECKLIST.md +0 -55
- package/docs/security/SHELL_EXEMPTIONS.md +0 -145
- package/docs/security/SOUP_REGISTER.md +0 -36
- package/docs/security/THREAT_MODEL.md +0 -130
- package/docs/security/VERIFICATION_VALIDATION.md +0 -228
- package/docs/security/VERIFY_RELEASE.md +0 -201
- package/docs/security/security-pubkey.asc +0 -15
- package/docs/stylesheets/extra.css +0 -444
- package/docs/themes/README.md +0 -10
- package/docs/themes/VISUAL_INTEGRITY_REPORT.md +0 -30
- package/docs/themes/hero-shot.svg +0 -78
- package/scripts/README.md +0 -123
- package/scripts/ci/check-copyright-headers.sh +0 -8
- package/scripts/ci/check-shell-preamble.sh +0 -8
- package/scripts/ci/guard-gitleaks-checkout.sh +0 -8
- package/scripts/demo/record.sh +0 -43
- package/scripts/diagnostics/a2a-conformance.sh +0 -163
- package/scripts/diagnostics/alias-governance.sh +0 -165
- package/scripts/diagnostics/aliases-cheatsheet.sh +0 -74
- package/scripts/diagnostics/aliases-manifest.sh +0 -77
- package/scripts/diagnostics/attest-verify.sh +0 -147
- package/scripts/diagnostics/benchmark.sh +0 -408
- package/scripts/diagnostics/conflicts.sh +0 -73
- package/scripts/diagnostics/doctor-unified.sh +0 -43
- package/scripts/diagnostics/doctor.sh +0 -797
- package/scripts/diagnostics/drift-dashboard.sh +0 -203
- package/scripts/diagnostics/health.sh +0 -656
- package/scripts/diagnostics/history-analysis.sh +0 -86
- package/scripts/diagnostics/mcp-doctor.sh +0 -582
- package/scripts/diagnostics/perf.sh +0 -453
- package/scripts/diagnostics/scorecard.sh +0 -120
- package/scripts/diagnostics/secret-governance.sh +0 -65
- package/scripts/diagnostics/security-score.sh +0 -467
- package/scripts/diagnostics/smoke-test.sh +0 -88
- package/scripts/diagnostics/snapshot.sh +0 -90
- package/scripts/diagnostics/verify.sh +0 -108
- package/scripts/diagnostics/verify_state.sh +0 -73
- package/scripts/diagnostics/version-locks.sh +0 -94
- package/scripts/diagnostics/workstation-attestation.sh +0 -212
- package/scripts/dot/commands/agent.sh +0 -535
- package/scripts/dot/commands/agents.sh +0 -352
- package/scripts/dot/commands/ai.sh +0 -600
- package/scripts/dot/commands/aliases.sh +0 -277
- package/scripts/dot/commands/appearance.sh +0 -110
- package/scripts/dot/commands/completion.sh +0 -171
- package/scripts/dot/commands/core.sh +0 -217
- package/scripts/dot/commands/diagnostics.sh +0 -265
- package/scripts/dot/commands/env-emit.sh +0 -203
- package/scripts/dot/commands/fleet.sh +0 -711
- package/scripts/dot/commands/init.sh +0 -185
- package/scripts/dot/commands/lint.sh +0 -208
- package/scripts/dot/commands/manual.sh +0 -169
- package/scripts/dot/commands/meta.sh +0 -438
- package/scripts/dot/commands/patterns.sh +0 -55
- package/scripts/dot/commands/registry.sh +0 -455
- package/scripts/dot/commands/restore.sh +0 -232
- package/scripts/dot/commands/secrets.sh +0 -296
- package/scripts/dot/commands/security.sh +0 -102
- package/scripts/dot/commands/tools.sh +0 -570
- package/scripts/dot/data/alias-deprecations.tsv +0 -2
- package/scripts/dot/powershell/Dot.psm1 +0 -319
- package/scripts/fonts/install-nerd-fonts.sh +0 -75
- package/scripts/fonts/patch-fonts.sh +0 -36
- package/scripts/git-hooks/install.sh +0 -12
- package/scripts/git-hooks/pre-commit +0 -12
- package/scripts/git-hooks/pre-commit-audit.sh +0 -146
- package/scripts/git-hooks/pre-push +0 -105
- package/scripts/git-hooks/prepare-commit-msg +0 -29
- package/scripts/lib/secrets_provider.sh +0 -200
- package/scripts/nvim/headless-upgrade.lua +0 -81
- package/scripts/ops/ai-setup.sh +0 -71
- package/scripts/ops/bundle.sh +0 -104
- package/scripts/ops/chaos.sh +0 -50
- package/scripts/ops/chezmoi-apply.sh +0 -333
- package/scripts/ops/chezmoi-diff.sh +0 -16
- package/scripts/ops/chezmoi-remove.sh +0 -46
- package/scripts/ops/chezmoi-update.sh +0 -67
- package/scripts/ops/heal-chezmoi.sh +0 -87
- package/scripts/ops/heal-system.sh +0 -129
- package/scripts/ops/heal-tools.sh +0 -297
- package/scripts/ops/heal.sh +0 -223
- package/scripts/ops/post-apply-repair.sh +0 -107
- package/scripts/ops/prewarm.sh +0 -128
- package/scripts/ops/release.sh +0 -262
- package/scripts/ops/rollback.sh +0 -613
- package/scripts/ops/setup.sh +0 -138
- package/scripts/ops/teleport.sh +0 -34
- package/scripts/qa/check-feature-matrix.sh +0 -296
- package/scripts/qa/check-version-consistency.sh +0 -12
- package/scripts/qa/coverage-baseline.sh +0 -61
- package/scripts/qa/docs-coverage.sh +0 -118
- package/scripts/qa/examples-coverage.sh +0 -94
- package/scripts/qa/powershell-contract.ps1 +0 -95
- package/scripts/qa/reliability-audit.sh +0 -139
- package/scripts/qa/scorecard-snapshot.sh +0 -128
- package/scripts/qa/traceability-coverage.sh +0 -124
- package/scripts/qa/validate-examples.sh +0 -90
- package/scripts/qa/wsl-contract.sh +0 -12
- package/scripts/secrets/age-init.sh +0 -82
- package/scripts/secrets/create-secrets-file.sh +0 -46
- package/scripts/secrets/encrypt-ssh-key.sh +0 -44
- package/scripts/security/backup.sh +0 -58
- package/scripts/security/check-disclosure-key-expiry.sh +0 -111
- package/scripts/security/dns-doh.sh +0 -52
- package/scripts/security/encryption-check.sh +0 -55
- package/scripts/security/enforce-policies.sh +0 -552
- package/scripts/security/firewall.sh +0 -91
- package/scripts/security/lock-configs.sh +0 -67
- package/scripts/security/lock-screen.sh +0 -56
- package/scripts/security/manage-secrets.sh +0 -429
- package/scripts/security/ssh-cert.sh +0 -204
- package/scripts/security/telemetry-kill.sh +0 -51
- package/scripts/security/usb-safety.sh +0 -52
- package/scripts/theme/apply-gnome-theme.sh +0 -333
- package/scripts/theme/extract-heic-frames.sh +0 -115
- package/scripts/theme/extract-theme.py +0 -1020
- package/scripts/theme/install-boot-logo.sh +0 -63
- package/scripts/theme/install-catppuccin-themes.sh +0 -371
- package/scripts/theme/install-cursors.sh +0 -26
- package/scripts/theme/install-file-icons.sh +0 -27
- package/scripts/theme/install-grub-theme.sh +0 -62
- package/scripts/theme/install-lock-icon.sh +0 -31
- package/scripts/theme/merge-wallpaper.sh +0 -146
- package/scripts/theme/rebuild-themes.sh +0 -603
- package/scripts/theme/switch.sh +0 -476
- package/scripts/theme/wallpaper-rotate.sh +0 -137
- package/scripts/theme/wallpaper-sync.sh +0 -690
- package/scripts/tools/cmatrix.sh +0 -22
- package/scripts/tools/detect-collisions.py +0 -103
- package/scripts/tools/emoji-picker.sh +0 -49
- package/scripts/tools/figlet-banner.sh +0 -19
- package/scripts/tools/log-rotate.sh +0 -31
- package/scripts/tools/lolcat-wrap.sh +0 -20
- package/scripts/tools/pipes.sh +0 -49
- package/scripts/tuning/linux.sh +0 -186
- package/scripts/tuning/macos.sh +0 -56
- package/scripts/uninstall.sh +0 -86
- package/scripts/verify-release-versions +0 -156
- package/scripts/version-sync.sh +0 -714
- package/templates/chezmoi-data/geekom-a9.toml.example +0 -21
- package/templates/chezmoi-data/mac-m1.toml.example +0 -16
- package/templates/chezmoi-data/mac-t2-linux.toml.example +0 -21
- package/templates/chezmoi-data/surface-pro-7p.toml.example +0 -21
- package/templates/projects/go/.github/workflows/ci.yml +0 -31
- package/templates/projects/go/README.md +0 -7
- package/templates/projects/go/cmd/__PROJECT_NAME__/main.go +0 -8
- package/templates/projects/go/go.mod +0 -3
- package/templates/projects/go/go.sum +0 -0
- package/templates/projects/molecule/README.md +0 -7
- package/templates/projects/molecule/converge.yml +0 -7
- package/templates/projects/molecule/molecule.yml +0 -16
- package/templates/projects/node/.github/workflows/ci.yml +0 -30
- package/templates/projects/node/README.md +0 -7
- package/templates/projects/node/package-lock.json +0 -12
- package/templates/projects/node/package.json +0 -10
- package/templates/projects/node/src/index.js +0 -3
- package/templates/projects/packer/README.md +0 -15
- package/templates/projects/packer/main.pkr.hcl +0 -15
- package/templates/projects/python/.github/workflows/ci.yml +0 -34
- package/templates/projects/python/README.md +0 -7
- package/templates/projects/python/pyproject.toml +0 -25
- package/templates/projects/python/src/__PROJECT_NAME__/__init__.py +0 -2
- package/templates/projects/python/tests/test_basic.py +0 -3
|
@@ -1,711 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
3
|
-
# Copyright (c) 2015-2026 Sebastien Rousseau
|
|
4
|
-
# Dotfiles CLI - Fleet Commands
|
|
5
|
-
# fleet status|nodes|drift|events|namespace
|
|
6
|
-
|
|
7
|
-
set -euo pipefail
|
|
8
|
-
|
|
9
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
-
# shellcheck source=../../../lib/dot/utils.sh
|
|
11
|
-
source "$SCRIPT_DIR/../../../lib/dot/utils.sh"
|
|
12
|
-
# shellcheck source=../../../lib/dot/log.sh
|
|
13
|
-
source "$SCRIPT_DIR/../../../lib/dot/log.sh"
|
|
14
|
-
|
|
15
|
-
dot_ui_command_banner "Fleet" "${1:-}"
|
|
16
|
-
|
|
17
|
-
_FLEET_STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/fleet"
|
|
18
|
-
_FLEET_EVENTS_FILE="$_FLEET_STATE_DIR/events.jsonl"
|
|
19
|
-
|
|
20
|
-
_fleet_enabled() {
|
|
21
|
-
local data_file
|
|
22
|
-
data_file="$(resolve_chezmoi_source_dir)/.chezmoidata.toml"
|
|
23
|
-
if [[ -f "$data_file" ]] && grep -q '^enabled = true' "$data_file" 2>/dev/null; then
|
|
24
|
-
return 0
|
|
25
|
-
fi
|
|
26
|
-
return 1
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
_fleet_node_id() {
|
|
30
|
-
local data_file node_id=""
|
|
31
|
-
data_file="$(resolve_chezmoi_source_dir)/.chezmoidata.toml"
|
|
32
|
-
if [[ -f "$data_file" ]]; then
|
|
33
|
-
node_id="$(sed -n 's/^node_id = "\(.*\)"/\1/p' "$data_file" | head -1)"
|
|
34
|
-
fi
|
|
35
|
-
if [[ -z "$node_id" ]]; then
|
|
36
|
-
node_id="$(hostname -s 2>/dev/null || echo "unknown")"
|
|
37
|
-
fi
|
|
38
|
-
printf '%s\n' "$node_id"
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
_fleet_namespace() {
|
|
42
|
-
local data_file ns=""
|
|
43
|
-
data_file="$(resolve_chezmoi_source_dir)/.chezmoidata.toml"
|
|
44
|
-
if [[ -f "$data_file" ]]; then
|
|
45
|
-
ns="$(sed -n 's/^namespace = "\(.*\)"/\1/p' "$data_file" | head -1)"
|
|
46
|
-
fi
|
|
47
|
-
printf '%s\n' "${ns:-default}"
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
_fleet_emit_event() {
|
|
51
|
-
local event="$1" status="${2:-ok}"
|
|
52
|
-
shift 2 || true
|
|
53
|
-
local ts node_id namespace
|
|
54
|
-
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
55
|
-
node_id="$(_fleet_node_id)"
|
|
56
|
-
namespace="$(_fleet_namespace)"
|
|
57
|
-
mkdir -p "$_FLEET_STATE_DIR" 2>/dev/null || return 0
|
|
58
|
-
local payload
|
|
59
|
-
payload=$(printf '{"time":"%s","event":"%s","status":"%s","node_id":"%s","namespace":"%s","trace_id":"%s"' \
|
|
60
|
-
"$ts" "$event" "$status" "$node_id" "$namespace" "$DOT_TRACE_ID")
|
|
61
|
-
while [[ $# -gt 0 ]]; do
|
|
62
|
-
payload+=$(printf ',"%s":"%s"' "${1%%=*}" "${1#*=}")
|
|
63
|
-
shift
|
|
64
|
-
done
|
|
65
|
-
payload+='}'
|
|
66
|
-
printf '%s\n' "$payload" >>"$_FLEET_EVENTS_FILE" 2>/dev/null || true
|
|
67
|
-
|
|
68
|
-
# Forward to endpoint if configured
|
|
69
|
-
local endpoint=""
|
|
70
|
-
local data_file
|
|
71
|
-
data_file="$(resolve_chezmoi_source_dir)/.chezmoidata.toml"
|
|
72
|
-
if [[ -f "$data_file" ]]; then
|
|
73
|
-
endpoint="$(sed -n 's/^endpoint = "\(.*\)"/\1/p' "$data_file" | head -1)"
|
|
74
|
-
fi
|
|
75
|
-
if [[ -n "$endpoint" ]] && [[ "$endpoint" == https://* ]]; then
|
|
76
|
-
curl --proto '=https' --tlsv1.2 -fsSL -X POST -H "Content-Type: application/json" \
|
|
77
|
-
-d "$payload" "$endpoint" >/dev/null 2>&1 || true
|
|
78
|
-
fi
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
cmd_fleet_status() {
|
|
82
|
-
local json_mode=0
|
|
83
|
-
[[ "${1:-}" == "--json" || "${1:-}" == "-j" ]] && json_mode=1
|
|
84
|
-
|
|
85
|
-
local node_id namespace version os_type kernel shell_type
|
|
86
|
-
node_id="$(_fleet_node_id)"
|
|
87
|
-
namespace="$(_fleet_namespace)"
|
|
88
|
-
version="$(dotfiles_version)"
|
|
89
|
-
os_type="$(uname -s)"
|
|
90
|
-
kernel="$(uname -r)"
|
|
91
|
-
shell_type="${SHELL##*/}"
|
|
92
|
-
|
|
93
|
-
local drift_status="clean"
|
|
94
|
-
if has_command chezmoi; then
|
|
95
|
-
local drift_output
|
|
96
|
-
# --exclude=always: always-run scripts are pending by design, not drift.
|
|
97
|
-
drift_output="$(chezmoi status --exclude=always 2>/dev/null || true)"
|
|
98
|
-
if [[ -n "$drift_output" ]]; then
|
|
99
|
-
drift_status="drifted"
|
|
100
|
-
fi
|
|
101
|
-
fi
|
|
102
|
-
|
|
103
|
-
local last_apply=""
|
|
104
|
-
local state_log="${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/dot.log"
|
|
105
|
-
if [[ -f "$state_log" ]]; then
|
|
106
|
-
# `set -euo pipefail` at the top of this script kills the whole
|
|
107
|
-
# command when grep matches nothing (rc=1). Wrap the pipeline so
|
|
108
|
-
# `last_apply` cleanly becomes empty and the UI still renders.
|
|
109
|
-
last_apply="$(grep 'apply' "$state_log" 2>/dev/null | tail -1 | sed -n 's/^\[\([^]]*\)\].*/\1/p' || true)"
|
|
110
|
-
fi
|
|
111
|
-
|
|
112
|
-
if [[ "$json_mode" -eq 1 ]]; then
|
|
113
|
-
printf '{"node_id":"%s","namespace":"%s","version":"%s","os":"%s","kernel":"%s","shell":"%s","drift":"%s","last_apply":"%s"}\n' \
|
|
114
|
-
"$node_id" "$namespace" "$version" "$os_type" "$kernel" "$shell_type" "$drift_status" "$last_apply"
|
|
115
|
-
return 0
|
|
116
|
-
fi
|
|
117
|
-
|
|
118
|
-
ui_header "Fleet Node Status"
|
|
119
|
-
echo ""
|
|
120
|
-
ui_ok "Node ID" "$node_id"
|
|
121
|
-
ui_ok "Namespace" "$namespace"
|
|
122
|
-
ui_ok "Version" "v$version"
|
|
123
|
-
ui_ok "OS" "$os_type $kernel"
|
|
124
|
-
ui_ok "Shell" "$shell_type"
|
|
125
|
-
if [[ "$drift_status" == "clean" ]]; then
|
|
126
|
-
ui_ok "Drift" "$drift_status"
|
|
127
|
-
else
|
|
128
|
-
ui_warn "Drift" "$drift_status"
|
|
129
|
-
fi
|
|
130
|
-
if [[ -n "$last_apply" ]]; then
|
|
131
|
-
ui_info "Last Apply" "$last_apply"
|
|
132
|
-
fi
|
|
133
|
-
|
|
134
|
-
_fleet_emit_event "status" "ok" "version=$version" "drift=$drift_status"
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
_DRIFT_HISTORY_FILE="$_FLEET_STATE_DIR/drift-history.jsonl"
|
|
138
|
-
|
|
139
|
-
_fleet_drift_append_history() {
|
|
140
|
-
local drift_output="$1"
|
|
141
|
-
local ts
|
|
142
|
-
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
143
|
-
mkdir -p "$_FLEET_STATE_DIR" 2>/dev/null || return 0
|
|
144
|
-
if [[ -z "$drift_output" ]]; then
|
|
145
|
-
printf '{"time":"%s","status":"clean","files":[]}\n' "$ts" >>"$_DRIFT_HISTORY_FILE" 2>/dev/null || true
|
|
146
|
-
else
|
|
147
|
-
local files_json
|
|
148
|
-
files_json="$(printf '%s\n' "$drift_output" | awk '{print $NF}' | jq -R . | jq -s . 2>/dev/null || echo '[]')"
|
|
149
|
-
printf '{"time":"%s","status":"drifted","files":%s}\n' "$ts" "$files_json" >>"$_DRIFT_HISTORY_FILE" 2>/dev/null || true
|
|
150
|
-
fi
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
cmd_fleet_drift() {
|
|
154
|
-
local subcommand="${1:-check}"
|
|
155
|
-
if [[ "${1:-}" == --* ]] || [[ -z "${1:-}" ]]; then
|
|
156
|
-
subcommand="check"
|
|
157
|
-
else
|
|
158
|
-
shift || true
|
|
159
|
-
fi
|
|
160
|
-
|
|
161
|
-
case "$subcommand" in
|
|
162
|
-
check)
|
|
163
|
-
ui_header "Fleet Drift Report"
|
|
164
|
-
echo ""
|
|
165
|
-
|
|
166
|
-
if ! has_command chezmoi; then
|
|
167
|
-
ui_err "chezmoi" "not installed"
|
|
168
|
-
return 1
|
|
169
|
-
fi
|
|
170
|
-
|
|
171
|
-
local drift_output
|
|
172
|
-
drift_output="$(chezmoi status --exclude=always 2>/dev/null || true)"
|
|
173
|
-
|
|
174
|
-
_fleet_drift_append_history "$drift_output"
|
|
175
|
-
|
|
176
|
-
if [[ -z "$drift_output" ]]; then
|
|
177
|
-
ui_ok "Status" "No drift detected"
|
|
178
|
-
_fleet_emit_event "drift_check" "clean"
|
|
179
|
-
return 0
|
|
180
|
-
fi
|
|
181
|
-
|
|
182
|
-
ui_warn "Status" "Configuration drift detected"
|
|
183
|
-
echo ""
|
|
184
|
-
printf '%s\n' "$drift_output" | while IFS= read -r line; do
|
|
185
|
-
local change_type="${line:0:2}"
|
|
186
|
-
local file_path="${line:3}"
|
|
187
|
-
case "$change_type" in
|
|
188
|
-
"MM" | "A " | " M")
|
|
189
|
-
ui_warn "$change_type" "$file_path"
|
|
190
|
-
;;
|
|
191
|
-
*)
|
|
192
|
-
ui_info "$change_type" "$file_path"
|
|
193
|
-
;;
|
|
194
|
-
esac
|
|
195
|
-
done
|
|
196
|
-
|
|
197
|
-
_fleet_emit_event "drift_check" "drifted" "count=$(echo "$drift_output" | wc -l | tr -d ' ')"
|
|
198
|
-
;;
|
|
199
|
-
history)
|
|
200
|
-
ui_header "Drift History"
|
|
201
|
-
echo ""
|
|
202
|
-
if [[ ! -f "$_DRIFT_HISTORY_FILE" ]]; then
|
|
203
|
-
ui_info "No drift history recorded yet."
|
|
204
|
-
return 0
|
|
205
|
-
fi
|
|
206
|
-
local count="${1:-20}"
|
|
207
|
-
tail -n "$count" "$_DRIFT_HISTORY_FILE" | while IFS= read -r line; do
|
|
208
|
-
local time status file_count
|
|
209
|
-
# One jq per line (was three: .time, .status, .files|length).
|
|
210
|
-
# `|| true`: on an unparsable line jq prints nothing, `read`
|
|
211
|
-
# hits EOF and returns 1, and `set -e` would otherwise abort the
|
|
212
|
-
# whole listing instead of rendering the `?` placeholders below.
|
|
213
|
-
IFS=$'\t' read -r time status file_count < <(
|
|
214
|
-
printf '%s' "$line" | jq -r '[.time, .status, (.files | length)] | @tsv' 2>/dev/null
|
|
215
|
-
) || true
|
|
216
|
-
[[ -n "$time" ]] || time="?"
|
|
217
|
-
[[ -n "$status" ]] || status="?"
|
|
218
|
-
[[ -n "$file_count" ]] || file_count=0
|
|
219
|
-
if [[ "$status" == "clean" ]]; then
|
|
220
|
-
ui_ok "$time" "clean"
|
|
221
|
-
else
|
|
222
|
-
ui_warn "$time" "drifted ($file_count files)"
|
|
223
|
-
fi
|
|
224
|
-
done
|
|
225
|
-
;;
|
|
226
|
-
predict)
|
|
227
|
-
ui_header "Drift Prediction"
|
|
228
|
-
echo ""
|
|
229
|
-
if [[ ! -f "$_DRIFT_HISTORY_FILE" ]]; then
|
|
230
|
-
ui_info "Not enough history for prediction."
|
|
231
|
-
return 0
|
|
232
|
-
fi
|
|
233
|
-
# Simple heuristic: files that drifted in >50% of the last 10 checks
|
|
234
|
-
local threshold=5
|
|
235
|
-
jq -r '.files[]?' "$_DRIFT_HISTORY_FILE" | tail -n 1000 | sort | uniq -c | sort -rn | while read -r count file; do
|
|
236
|
-
if [[ "$count" -ge "$threshold" ]]; then
|
|
237
|
-
ui_warn "Likely to drift" "$file (drifted $count times recently)"
|
|
238
|
-
fi
|
|
239
|
-
done
|
|
240
|
-
local total_checks
|
|
241
|
-
total_checks="$(wc -l <"$_DRIFT_HISTORY_FILE" | tr -d ' ')"
|
|
242
|
-
ui_info "History" "$total_checks checks recorded"
|
|
243
|
-
;;
|
|
244
|
-
*)
|
|
245
|
-
die "Usage: dot fleet drift [check|history|predict]"
|
|
246
|
-
;;
|
|
247
|
-
esac
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
cmd_fleet_events() {
|
|
251
|
-
local count="${1:-20}"
|
|
252
|
-
if [[ ! -f "$_FLEET_EVENTS_FILE" ]]; then
|
|
253
|
-
ui_info "No fleet events recorded yet."
|
|
254
|
-
ui_info "Events file" "$_FLEET_EVENTS_FILE"
|
|
255
|
-
return 0
|
|
256
|
-
fi
|
|
257
|
-
|
|
258
|
-
ui_header "Fleet Events (last $count)"
|
|
259
|
-
echo ""
|
|
260
|
-
|
|
261
|
-
if has_command jq; then
|
|
262
|
-
tail -n "$count" "$_FLEET_EVENTS_FILE" | jq -r '"\(.time)\t\(.event)\t\(.status)\t\(.node_id)"' | while IFS=$'\t' read -r time event status node; do
|
|
263
|
-
if [[ "$status" == "ok" || "$status" == "clean" ]]; then
|
|
264
|
-
ui_ok "$event" "$time ($node)"
|
|
265
|
-
else
|
|
266
|
-
ui_warn "$event" "$time ($node)"
|
|
267
|
-
fi
|
|
268
|
-
done
|
|
269
|
-
else
|
|
270
|
-
tail -n "$count" "$_FLEET_EVENTS_FILE"
|
|
271
|
-
fi
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
cmd_fleet_namespace() {
|
|
275
|
-
local subcommand="${1:-show}"
|
|
276
|
-
shift || true
|
|
277
|
-
|
|
278
|
-
case "$subcommand" in
|
|
279
|
-
show)
|
|
280
|
-
local ns
|
|
281
|
-
ns="$(_fleet_namespace)"
|
|
282
|
-
ui_header "Fleet Namespace"
|
|
283
|
-
ui_ok "Active" "$ns"
|
|
284
|
-
|
|
285
|
-
local data_file
|
|
286
|
-
data_file="$(resolve_chezmoi_source_dir)/.chezmoidata.toml"
|
|
287
|
-
if [[ -f "$data_file" ]]; then
|
|
288
|
-
echo ""
|
|
289
|
-
ui_section "Available Namespaces"
|
|
290
|
-
grep '^\[namespaces\.' "$data_file" | sed 's/\[namespaces\.\(.*\)\]/\1/' | while IFS= read -r name; do
|
|
291
|
-
if [[ "$name" == "$ns" ]]; then
|
|
292
|
-
ui_ok "$name" "[active]"
|
|
293
|
-
else
|
|
294
|
-
ui_info "$name" ""
|
|
295
|
-
fi
|
|
296
|
-
done
|
|
297
|
-
fi
|
|
298
|
-
;;
|
|
299
|
-
set)
|
|
300
|
-
local new_ns="${1:-}"
|
|
301
|
-
[[ -n "$new_ns" ]] || die "Usage: dot fleet namespace set <name>"
|
|
302
|
-
validate_name "$new_ns" "namespace"
|
|
303
|
-
local data_file
|
|
304
|
-
data_file="$(resolve_chezmoi_source_dir)/.chezmoidata.toml"
|
|
305
|
-
[[ -f "$data_file" ]] || die ".chezmoidata.toml not found: $data_file"
|
|
306
|
-
# Atomic write: render into a tempfile + mv so concurrent
|
|
307
|
-
# `dot fleet namespace set` callers can't corrupt the TOML.
|
|
308
|
-
# Avoids `sed -i` portability dance (GNU `-i` vs BSD `-i ''`).
|
|
309
|
-
local _tmp _rendered=0
|
|
310
|
-
_tmp="$(mktemp "${data_file}.XXXXXX")" || die "Cannot create tempfile"
|
|
311
|
-
# Explicit if/else instead of A && B || C — the latter (SC2015)
|
|
312
|
-
# silently runs C when B itself fails, masking real mv errors.
|
|
313
|
-
if grep -q "^namespace = " "$data_file"; then
|
|
314
|
-
if sed "s/^namespace = \".*\"/namespace = \"$new_ns\"/" "$data_file" >"$_tmp"; then
|
|
315
|
-
_rendered=1
|
|
316
|
-
fi
|
|
317
|
-
else
|
|
318
|
-
# No key yet — the shipped .chezmoidata.toml has none, and rewriting
|
|
319
|
-
# only when one already existed made `set` a silent no-op on a fresh
|
|
320
|
-
# checkout while still reporting success. Insert it after the first
|
|
321
|
-
# line: appending at the end would land the key inside whatever
|
|
322
|
-
# [table] the file happens to end with, which TOML reads as a
|
|
323
|
-
# different key entirely. `dot profile set` does the same for
|
|
324
|
-
# `profile`.
|
|
325
|
-
if awk -v ns="$new_ns" '
|
|
326
|
-
NR == 1 { print; printf "namespace = \"%s\"\n", ns; inserted = 1; next }
|
|
327
|
-
{ print }
|
|
328
|
-
END { if (!inserted) printf "namespace = \"%s\"\n", ns }
|
|
329
|
-
' "$data_file" >"$_tmp"; then
|
|
330
|
-
_rendered=1
|
|
331
|
-
fi
|
|
332
|
-
fi
|
|
333
|
-
if [[ "$_rendered" -eq 1 ]]; then
|
|
334
|
-
if ! mv "$_tmp" "$data_file"; then
|
|
335
|
-
rm -f "$_tmp"
|
|
336
|
-
die "Failed to commit namespace update"
|
|
337
|
-
fi
|
|
338
|
-
else
|
|
339
|
-
rm -f "$_tmp"
|
|
340
|
-
die "Failed to render namespace update"
|
|
341
|
-
fi
|
|
342
|
-
ui_ok "Namespace" "Set to '$new_ns'. Run 'dot sync' to apply."
|
|
343
|
-
_fleet_emit_event "namespace_set" "ok" "namespace=$new_ns"
|
|
344
|
-
;;
|
|
345
|
-
*)
|
|
346
|
-
die "Usage: dot fleet namespace [show|set <name>]"
|
|
347
|
-
;;
|
|
348
|
-
esac
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
cmd_fleet_enforce() {
|
|
352
|
-
local subcommand="${1:-status}"
|
|
353
|
-
shift || true
|
|
354
|
-
|
|
355
|
-
local repo_root
|
|
356
|
-
repo_root="$(resolve_chezmoi_source_dir)"
|
|
357
|
-
[[ -z "$repo_root" ]] && repo_root="$(resolve_source_dir)"
|
|
358
|
-
local profiles_file="$repo_root/dot_config/dotfiles/agent-profiles.json"
|
|
359
|
-
|
|
360
|
-
case "$subcommand" in
|
|
361
|
-
status)
|
|
362
|
-
if [[ ! -f "$profiles_file" ]]; then
|
|
363
|
-
ui_err "Profiles" "agent-profiles.json not found"
|
|
364
|
-
return 1
|
|
365
|
-
fi
|
|
366
|
-
local enforcement
|
|
367
|
-
enforcement="$(jq -r '.rbac.enforcement // "advisory"' "$profiles_file")"
|
|
368
|
-
ui_header "RBAC Enforcement"
|
|
369
|
-
ui_ok "Mode" "$enforcement"
|
|
370
|
-
ui_ok "Default role" "$(jq -r '.rbac.defaultRole // "developer"' "$profiles_file")"
|
|
371
|
-
jq -r '.rbac.roles | to_entries[] | "\(.key)\t\(.value.allowedProfiles | join(", "))"' "$profiles_file" | while IFS=$'\t' read -r role profiles; do
|
|
372
|
-
ui_info "$role" "$profiles"
|
|
373
|
-
done
|
|
374
|
-
;;
|
|
375
|
-
set)
|
|
376
|
-
local mode="${1:-}"
|
|
377
|
-
[[ -n "$mode" ]] || die "Usage: dot fleet enforce set <advisory|strict>"
|
|
378
|
-
case "$mode" in
|
|
379
|
-
advisory | strict) ;;
|
|
380
|
-
*) die "Invalid enforcement mode: $mode (use advisory or strict)" ;;
|
|
381
|
-
esac
|
|
382
|
-
[[ -f "$profiles_file" ]] || die "agent-profiles.json not found"
|
|
383
|
-
local tmp
|
|
384
|
-
tmp="$(jq --arg mode "$mode" '.rbac.enforcement = $mode' "$profiles_file")"
|
|
385
|
-
printf '%s\n' "$tmp" >"$profiles_file"
|
|
386
|
-
ui_ok "Enforcement" "set to '$mode'"
|
|
387
|
-
_fleet_emit_event "enforcement_set" "ok" "mode=$mode"
|
|
388
|
-
;;
|
|
389
|
-
*)
|
|
390
|
-
die "Usage: dot fleet enforce [status|set <advisory|strict>]"
|
|
391
|
-
;;
|
|
392
|
-
esac
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
_fleet_hosts_file() {
|
|
396
|
-
printf '%s\n' "${DOTFILES_FLEET_HOSTS:-$HOME/.config/dotfiles/fleet.toml}"
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
# Parse the hosts file. Format:
|
|
400
|
-
# [hosts.laptop]
|
|
401
|
-
# ssh = "user@laptop.local"
|
|
402
|
-
# profile = "workstation"
|
|
403
|
-
#
|
|
404
|
-
# Echoes one record per line: "<name>\t<ssh-target>\t<profile>".
|
|
405
|
-
_fleet_hosts_iter() {
|
|
406
|
-
local f
|
|
407
|
-
f="$(_fleet_hosts_file)"
|
|
408
|
-
[[ -f "$f" ]] || return 0
|
|
409
|
-
awk '
|
|
410
|
-
BEGIN { name = ""; ssh = ""; profile = "" }
|
|
411
|
-
/^\[hosts\./ {
|
|
412
|
-
if (name != "") { printf "%s\t%s\t%s\n", name, ssh, profile }
|
|
413
|
-
gsub(/[\[\]]/, "", $0); sub(/^hosts\./, "", $0); name = $0
|
|
414
|
-
ssh = ""; profile = ""
|
|
415
|
-
next
|
|
416
|
-
}
|
|
417
|
-
/^ssh[[:space:]]*=/ { sub(/^ssh[[:space:]]*=[[:space:]]*/, ""); gsub(/"/, ""); ssh = $0; next }
|
|
418
|
-
/^profile[[:space:]]*=/ { sub(/^profile[[:space:]]*=[[:space:]]*/, ""); gsub(/"/, ""); profile = $0; next }
|
|
419
|
-
END {
|
|
420
|
-
if (name != "") { printf "%s\t%s\t%s\n", name, ssh, profile }
|
|
421
|
-
}
|
|
422
|
-
' "$f"
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
# SSH-based "dot fleet apply" — push the local dotfiles state out to
|
|
426
|
-
# each registered host. The §3 hero-feature: nobody else owns the
|
|
427
|
-
# "Ansible for personal devices" niche.
|
|
428
|
-
cmd_fleet_apply() {
|
|
429
|
-
local dry_run=0 only_host="" cmd="" jobs=4 verify_hosts=0
|
|
430
|
-
while [[ $# -gt 0 ]]; do
|
|
431
|
-
case "$1" in
|
|
432
|
-
--dry-run | -n)
|
|
433
|
-
dry_run=1
|
|
434
|
-
shift
|
|
435
|
-
;;
|
|
436
|
-
--verify-hosts)
|
|
437
|
-
# Pre-flight check that every host already has a known_hosts
|
|
438
|
-
# entry, closing the TOFU window before any SSH connection.
|
|
439
|
-
# R3 audit N4. Without this, accept-new is the default and a
|
|
440
|
-
# first-connection MITM can seed an attacker key.
|
|
441
|
-
verify_hosts=1
|
|
442
|
-
shift
|
|
443
|
-
;;
|
|
444
|
-
--host)
|
|
445
|
-
only_host="$2"
|
|
446
|
-
shift 2
|
|
447
|
-
;;
|
|
448
|
-
--cmd)
|
|
449
|
-
cmd="$2"
|
|
450
|
-
shift 2
|
|
451
|
-
;;
|
|
452
|
-
--jobs | -j)
|
|
453
|
-
jobs="$2"
|
|
454
|
-
shift 2
|
|
455
|
-
;;
|
|
456
|
-
--help | -h)
|
|
457
|
-
cat <<EOF
|
|
458
|
-
Usage: dot fleet apply [--host <name>] [--cmd <shell>] [--dry-run] [--jobs <n>]
|
|
459
|
-
|
|
460
|
-
Push dotfiles state to every host registered in:
|
|
461
|
-
${DOTFILES_FLEET_HOSTS:-\$HOME/.config/dotfiles/fleet.toml}
|
|
462
|
-
|
|
463
|
-
Format of fleet.toml:
|
|
464
|
-
[hosts.laptop]
|
|
465
|
-
ssh = "user@laptop.local"
|
|
466
|
-
profile = "workstation"
|
|
467
|
-
|
|
468
|
-
Hostnames are validated against [A-Za-z0-9._@:+/-]+ before any SSH
|
|
469
|
-
fan-out; invalid entries abort the apply.
|
|
470
|
-
|
|
471
|
-
First-time SSH connections use StrictHostKeyChecking=accept-new (TOFU).
|
|
472
|
-
If your threat model requires no TOFU window, pre-populate
|
|
473
|
-
~/.ssh/known_hosts before running this command.
|
|
474
|
-
|
|
475
|
-
Behavior:
|
|
476
|
-
By default each host runs: dot sync && dot doctor --quiet
|
|
477
|
-
Override with --cmd "<shell>" to run an arbitrary command on every
|
|
478
|
-
host (e.g. --cmd "uptime").
|
|
479
|
-
|
|
480
|
-
WARNING: --cmd is the trust boundary. Whatever string you pass
|
|
481
|
-
executes on every remote host with the credentials your SSH key
|
|
482
|
-
carries. Verify the command before running.
|
|
483
|
-
|
|
484
|
-
Flags:
|
|
485
|
-
--host <name> Apply to a single host only.
|
|
486
|
-
--cmd <shell> Run a custom command instead of 'dot sync'.
|
|
487
|
-
--dry-run, -n Print resolved hosts + planned command; don't SSH.
|
|
488
|
-
--jobs <n> Parallelism (default 4).
|
|
489
|
-
--verify-hosts Refuse to open any SSH connection unless every
|
|
490
|
-
target host already has a key in ~/.ssh/known_hosts.
|
|
491
|
-
Use when your threat model excludes the TOFU window.
|
|
492
|
-
EOF
|
|
493
|
-
return 0
|
|
494
|
-
;;
|
|
495
|
-
*)
|
|
496
|
-
ui_err "Unknown arg" "$1"
|
|
497
|
-
return 1
|
|
498
|
-
;;
|
|
499
|
-
esac
|
|
500
|
-
done
|
|
501
|
-
|
|
502
|
-
local hosts_file
|
|
503
|
-
hosts_file="$(_fleet_hosts_file)"
|
|
504
|
-
if [[ ! -f "$hosts_file" ]]; then
|
|
505
|
-
ui_err "Fleet" "no hosts file at $hosts_file"
|
|
506
|
-
ui_info "Hint" "create it with stanzas like '[hosts.laptop]\\nssh = \"user@laptop.local\"'"
|
|
507
|
-
return 1
|
|
508
|
-
fi
|
|
509
|
-
|
|
510
|
-
local entries
|
|
511
|
-
entries="$(_fleet_hosts_iter)"
|
|
512
|
-
if [[ -z "$entries" ]]; then
|
|
513
|
-
ui_err "Fleet" "hosts file is empty: $hosts_file"
|
|
514
|
-
return 1
|
|
515
|
-
fi
|
|
516
|
-
if [[ -n "$only_host" ]]; then
|
|
517
|
-
entries="$(printf '%s\n' "$entries" | awk -F'\t' -v h="$only_host" '$1 == h')"
|
|
518
|
-
[[ -n "$entries" ]] || {
|
|
519
|
-
ui_err "Fleet" "host not found: $only_host"
|
|
520
|
-
return 1
|
|
521
|
-
}
|
|
522
|
-
fi
|
|
523
|
-
|
|
524
|
-
local default_cmd='dot sync && dot doctor --quiet'
|
|
525
|
-
local effective_cmd="${cmd:-$default_cmd}"
|
|
526
|
-
|
|
527
|
-
ui_header "Fleet apply"
|
|
528
|
-
ui_info "Hosts file" "$hosts_file"
|
|
529
|
-
ui_info "Command" "$effective_cmd"
|
|
530
|
-
ui_info "Parallel" "$jobs"
|
|
531
|
-
|
|
532
|
-
if [[ "$dry_run" -eq 1 ]]; then
|
|
533
|
-
printf '%s\n' "$entries" | while IFS=$'\t' read -r name ssh profile; do
|
|
534
|
-
ui_info "$name" "$ssh profile=$profile cmd=$effective_cmd"
|
|
535
|
-
done
|
|
536
|
-
ui_ok "Dry-run" "no SSH connections opened"
|
|
537
|
-
return 0
|
|
538
|
-
fi
|
|
539
|
-
|
|
540
|
-
if ! command -v ssh >/dev/null 2>&1; then
|
|
541
|
-
ui_err "ssh" "not installed"
|
|
542
|
-
return 127
|
|
543
|
-
fi
|
|
544
|
-
|
|
545
|
-
local total=0 ok=0 fail=0
|
|
546
|
-
local tmpdir
|
|
547
|
-
# `-t` template includes PID + random, so two concurrent `dot fleet
|
|
548
|
-
# apply` invocations from the same user can't collide on $tmpdir.
|
|
549
|
-
tmpdir="$(mktemp -d -t dotfiles-fleet.XXXXXX)"
|
|
550
|
-
# Capture tmpdir's value at trap-definition time (via the eval-on-
|
|
551
|
-
# define `printf -v`), NOT at trap-fire time. A naive
|
|
552
|
-
# `trap 'rm -rf "$tmpdir"' RETURN` is unsafe under set -u because
|
|
553
|
-
# `local tmpdir` is destroyed before the RETURN trap evaluates.
|
|
554
|
-
# The SC2064 warning ("Use single quotes, otherwise this expands now
|
|
555
|
-
# rather than when signalled") is exactly the behaviour we want here —
|
|
556
|
-
# we explicitly want eager expansion. Suppress per-line.
|
|
557
|
-
local _cleanup
|
|
558
|
-
printf -v _cleanup 'rm -rf %q' "$tmpdir"
|
|
559
|
-
# shellcheck disable=SC2064
|
|
560
|
-
trap "$_cleanup" RETURN
|
|
561
|
-
|
|
562
|
-
# Validate every hostname against a conservative regex BEFORE fan-out.
|
|
563
|
-
# `user@host:port` characters only — refuses single quotes, backticks,
|
|
564
|
-
# `$()`, semicolons, spaces, any shell metacharacter. Closes the
|
|
565
|
-
# round-2 audit's hostname-injection finding.
|
|
566
|
-
while IFS=$'\t' read -r name ssh profile; do
|
|
567
|
-
[[ -n "$name" ]] || continue
|
|
568
|
-
if [[ ! "$ssh" =~ ^[a-zA-Z0-9._@:+/-]+$ ]]; then
|
|
569
|
-
ui_err "$name" "invalid ssh target ($ssh) — only [a-zA-Z0-9._@:+/-] allowed"
|
|
570
|
-
return 1
|
|
571
|
-
fi
|
|
572
|
-
done <<<"$entries"
|
|
573
|
-
|
|
574
|
-
# --verify-hosts: refuse the apply when any target host is missing
|
|
575
|
-
# from ~/.ssh/known_hosts. Closes the R3 audit N4 TOFU-window gap.
|
|
576
|
-
if ((verify_hosts == 1)); then
|
|
577
|
-
local known_hosts="${HOME}/.ssh/known_hosts"
|
|
578
|
-
if [[ ! -f "$known_hosts" ]]; then
|
|
579
|
-
ui_err "verify-hosts" "no $known_hosts — populate before --verify-hosts"
|
|
580
|
-
return 1
|
|
581
|
-
fi
|
|
582
|
-
local unknown_count=0
|
|
583
|
-
while IFS=$'\t' read -r name ssh profile; do
|
|
584
|
-
[[ -n "$name" && -n "$ssh" ]] || continue
|
|
585
|
-
# Strip `user@` prefix and `:port` suffix for the lookup.
|
|
586
|
-
local hostpart="${ssh#*@}"
|
|
587
|
-
hostpart="${hostpart%%:*}"
|
|
588
|
-
if ! ssh-keygen -F "$hostpart" -f "$known_hosts" >/dev/null 2>&1; then
|
|
589
|
-
ui_err "$name" "no known_hosts entry for $hostpart — would TOFU on first connect"
|
|
590
|
-
unknown_count=$((unknown_count + 1))
|
|
591
|
-
fi
|
|
592
|
-
done <<<"$entries"
|
|
593
|
-
if ((unknown_count > 0)); then
|
|
594
|
-
ui_err "verify-hosts" "$unknown_count host(s) missing from known_hosts — aborting"
|
|
595
|
-
return 1
|
|
596
|
-
fi
|
|
597
|
-
ui_ok "verify-hosts" "all hosts found in known_hosts"
|
|
598
|
-
fi
|
|
599
|
-
|
|
600
|
-
# Run one SSH per host, parallelised via background jobs with a
|
|
601
|
-
# semaphore. We DO NOT use `xargs -d` because that flag is GNU-only
|
|
602
|
-
# and the §3 hero feature must work on macOS BSD xargs too. Also
|
|
603
|
-
# avoids embedding `{}` substitution into a `bash -c` (the previous
|
|
604
|
-
# implementation had a quoting hazard around TOML hostnames).
|
|
605
|
-
_fleet_apply_one() {
|
|
606
|
-
local _name="$1" _ssh="$2" _cmd="$3" _tmp="$4"
|
|
607
|
-
if ssh -o BatchMode=yes -o ConnectTimeout=10 \
|
|
608
|
-
-o StrictHostKeyChecking=accept-new \
|
|
609
|
-
"$_ssh" "$_cmd" </dev/null \
|
|
610
|
-
>"$_tmp/$_name.out" 2>"$_tmp/$_name.err"; then
|
|
611
|
-
printf 'ok\n' >"$_tmp/$_name.status"
|
|
612
|
-
else
|
|
613
|
-
printf 'fail %d\n' "$?" >"$_tmp/$_name.status"
|
|
614
|
-
fi
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
# Throttle to `jobs` concurrent workers. `wait -n` (wait for the next
|
|
618
|
-
# job to finish) is bash 4.3+, but macOS ships bash 3.2 — there it
|
|
619
|
-
# silently fails and the throttle collapses to unbounded parallelism.
|
|
620
|
-
# Track PIDs and block on the oldest when at capacity instead; works on
|
|
621
|
-
# bash 3.2 and 4+ alike.
|
|
622
|
-
local _pids=()
|
|
623
|
-
while IFS=$'\t' read -r name ssh profile; do
|
|
624
|
-
[[ -n "$name" && -n "$ssh" ]] || continue
|
|
625
|
-
total=$((total + 1))
|
|
626
|
-
while ((${#_pids[@]} >= jobs)); do
|
|
627
|
-
wait "${_pids[0]}" 2>/dev/null || true
|
|
628
|
-
_pids=("${_pids[@]:1}")
|
|
629
|
-
done
|
|
630
|
-
_fleet_apply_one "$name" "$ssh" "$effective_cmd" "$tmpdir" &
|
|
631
|
-
_pids+=("$!")
|
|
632
|
-
done <<<"$entries"
|
|
633
|
-
wait
|
|
634
|
-
|
|
635
|
-
# `while < <(printf ...)` instead of `printf ... | while` — the
|
|
636
|
-
# pipe form runs the loop body in a subshell, so the ok/fail
|
|
637
|
-
# counters never propagate back to the parent. Caught by
|
|
638
|
-
# tests/unit/fleet/test_fleet_apply_mocked_ssh.sh which exercised
|
|
639
|
-
# the full apply path (the dry-run test missed this).
|
|
640
|
-
while IFS=$'\t' read -r name ssh profile; do
|
|
641
|
-
[[ -n "$name" ]] || continue
|
|
642
|
-
if [[ -s "$tmpdir/$name.status" ]] && head -1 "$tmpdir/$name.status" | grep -q '^ok'; then
|
|
643
|
-
ui_ok "$name" "$ssh"
|
|
644
|
-
ok=$((ok + 1))
|
|
645
|
-
else
|
|
646
|
-
local err_summary=""
|
|
647
|
-
[[ -s "$tmpdir/$name.err" ]] && err_summary=" — $(head -1 "$tmpdir/$name.err")"
|
|
648
|
-
ui_err "$name" "$ssh${err_summary}"
|
|
649
|
-
fail=$((fail + 1))
|
|
650
|
-
fi
|
|
651
|
-
local _evt_status="unknown"
|
|
652
|
-
[[ -s "$tmpdir/$name.status" ]] && _evt_status="$(head -1 "$tmpdir/$name.status")"
|
|
653
|
-
_fleet_emit_event "apply" "$_evt_status" "host=$name" "cmd=$effective_cmd"
|
|
654
|
-
done < <(printf '%s\n' "$entries")
|
|
655
|
-
|
|
656
|
-
ui_info "Summary" "$ok ok / $fail failed / $total total"
|
|
657
|
-
[[ "$fail" -eq 0 ]]
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
cmd_fleet() {
|
|
661
|
-
local subcommand="${1:-status}"
|
|
662
|
-
if [[ "${1:-}" == --* ]] || [[ -z "${1:-}" ]]; then
|
|
663
|
-
subcommand="status"
|
|
664
|
-
else
|
|
665
|
-
shift || true
|
|
666
|
-
fi
|
|
667
|
-
|
|
668
|
-
case "$subcommand" in
|
|
669
|
-
status)
|
|
670
|
-
cmd_fleet_status "$@"
|
|
671
|
-
;;
|
|
672
|
-
drift)
|
|
673
|
-
cmd_fleet_drift "$@"
|
|
674
|
-
;;
|
|
675
|
-
events)
|
|
676
|
-
cmd_fleet_events "$@"
|
|
677
|
-
;;
|
|
678
|
-
namespace | ns)
|
|
679
|
-
cmd_fleet_namespace "$@"
|
|
680
|
-
;;
|
|
681
|
-
enforce)
|
|
682
|
-
cmd_fleet_enforce "$@"
|
|
683
|
-
;;
|
|
684
|
-
apply | push)
|
|
685
|
-
cmd_fleet_apply "$@"
|
|
686
|
-
;;
|
|
687
|
-
*)
|
|
688
|
-
ui_header "Fleet Commands"
|
|
689
|
-
echo ""
|
|
690
|
-
ui_info "Usage" "dot fleet [command]"
|
|
691
|
-
echo ""
|
|
692
|
-
ui_ok "status" "Show this node's fleet status (--json for machine output)"
|
|
693
|
-
ui_ok "drift" "Check for configuration drift"
|
|
694
|
-
ui_ok "events" "Show recent fleet events"
|
|
695
|
-
ui_ok "namespace" "Show or set the active namespace"
|
|
696
|
-
ui_ok "enforce" "Show or set RBAC enforcement mode (advisory|strict)"
|
|
697
|
-
ui_ok "apply" "SSH out to every host in fleet.toml and run 'dot sync'"
|
|
698
|
-
;;
|
|
699
|
-
esac
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
# Dispatch
|
|
703
|
-
case "${1:-}" in
|
|
704
|
-
fleet)
|
|
705
|
-
shift
|
|
706
|
-
cmd_fleet "$@"
|
|
707
|
-
;;
|
|
708
|
-
*)
|
|
709
|
-
cmd_fleet "$@"
|
|
710
|
-
;;
|
|
711
|
-
esac
|