@crvouga/dev-fleet 0.0.0-stage → 0.1.0
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/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
- package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
- package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
- package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
- package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
- package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
- package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
- package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
- package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
- package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
- package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
- package/LICENSE +21 -0
- package/README.md +157 -2
- package/agents/antigravity-proxy.ts +3 -0
- package/agents/audit.md +89 -0
- package/agents/claude-proxy +5 -0
- package/agents/claude-proxy.ts +3 -0
- package/agents/codex-proxy.ts +3 -0
- package/agents/grok-proxy.ts +3 -0
- package/agents/managed-proxy.ts +55 -0
- package/agents/opencode-proxy.ts +3 -0
- package/agents/registry.json +209 -0
- package/agents/registry.schema.json +149 -0
- package/agents/setup.md +128 -0
- package/agents/t3.json +40 -0
- package/ansible/macos/manager.yml +30 -0
- package/ansible/macos/policy.json +17 -0
- package/ansible/monitoring/README.md +154 -0
- package/ansible/monitoring/agent-darwin.yml +198 -0
- package/ansible/monitoring/agent.yml +134 -0
- package/ansible/monitoring/beszel-agent.service +42 -0
- package/ansible/monitoring/compose.yml +74 -0
- package/ansible/monitoring/hub-darwin.yml +102 -0
- package/ansible/monitoring/lock.json +11 -0
- package/ansible/net/remote-probe.sh +13 -0
- package/ansible/ubuntu/authorize-sudo.sh +51 -0
- package/ansible/ubuntu/baseline.yml +89 -0
- package/ansible/ubuntu/disk-guard.py +104 -0
- package/ansible/ubuntu/policy.json +43 -0
- package/ansible/ubuntu/recovery.yml +15 -0
- package/ansible/ubuntu/t3/policy.json +7 -0
- package/ansible/ubuntu/t3-desktop.yml +220 -0
- package/ansible/ubuntu/t3-health.sh +33 -0
- package/ansible/ubuntu/t3-policy.yml +76 -0
- package/ansible/ubuntu/t3-route.yml +28 -0
- package/ansible/ubuntu/tailscale.yml +75 -0
- package/ansible/ubuntu/tasks/credentials.yml +47 -0
- package/ansible/ubuntu/tasks/docker.yml +31 -0
- package/ansible/ubuntu/tasks/packages.yml +56 -0
- package/ansible/ubuntu/tasks/power.yml +51 -0
- package/ansible/ubuntu/tasks/recovery.yml +39 -0
- package/ansible/ubuntu/tasks/resources.yml +252 -0
- package/ansible/ubuntu/tasks/services.yml +31 -0
- package/ansible/ubuntu/tasks/ssh.yml +71 -0
- package/ansible/ubuntu/tasks/user.yml +120 -0
- package/bin/devfleet.mjs +36 -0
- package/bootstrap/bootstrap.ps1 +50 -0
- package/bootstrap/bootstrap.sh +73 -0
- package/bootstrap/devfleet +7 -0
- package/bootstrap/docker-desktop.sh +33 -0
- package/bootstrap/doctor.ts +189 -0
- package/bootstrap/fleet-manager-run.sh +76 -0
- package/bootstrap/fleet-remote-bootstrap.sh +185 -0
- package/bootstrap/global-config.ts +113 -0
- package/bootstrap/install-launcher.sh +7 -0
- package/bootstrap/native-aws.ts +86 -0
- package/bootstrap/native-t3.ts +154 -0
- package/bootstrap/npm.ts +350 -0
- package/bootstrap/setup-status.ts +51 -0
- package/bootstrap/setup.ts +88 -0
- package/bootstrap/shell-path.ts +105 -0
- package/bootstrap/verify-t3-archive.py +49 -0
- package/bootstrap/worker-auth.ts +65 -0
- package/bootstrap/worker-github.ts +47 -0
- package/bootstrap/worker-tools.ts +24 -0
- package/cliproxy/AGENTS.md +11 -0
- package/cliproxy/README.md +269 -0
- package/cliproxy/clients/bootstrap.py +143 -0
- package/cliproxy/clients/launcher.py +57 -0
- package/cliproxy/compose.yaml +90 -0
- package/cliproxy/config.template.yaml +70 -0
- package/cliproxy/docs/cloud-adoption.md +16 -0
- package/cliproxy/docs/deployment.png +0 -0
- package/cliproxy/docs/deployment.svg +109 -0
- package/cliproxy/docs/health-workflow.example.yml +185 -0
- package/cliproxy/docs/migration.md +131 -0
- package/cliproxy/docs/model-health-report.schema.json +364 -0
- package/cliproxy/docs/model-health.md +393 -0
- package/cliproxy/image.lock.json +5 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
- package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
- package/cliproxy/models.json +1618 -0
- package/cliproxy/requirements.lock +78 -0
- package/cliproxy/scripts/accounts.py +38 -0
- package/cliproxy/scripts/audit.py +123 -0
- package/cliproxy/scripts/backup +234 -0
- package/cliproxy/scripts/check.py +34 -0
- package/cliproxy/scripts/checkpoint.py +226 -0
- package/cliproxy/scripts/cli.py +64 -0
- package/cliproxy/scripts/cliproxy_host.py +257 -0
- package/cliproxy/scripts/cloud_state.py +150 -0
- package/cliproxy/scripts/container-health +6 -0
- package/cliproxy/scripts/docker +24 -0
- package/cliproxy/scripts/external-health.py +79 -0
- package/cliproxy/scripts/fleet_config.py +43 -0
- package/cliproxy/scripts/fleet_sync.py +274 -0
- package/cliproxy/scripts/initialize +84 -0
- package/cliproxy/scripts/install-dependencies +48 -0
- package/cliproxy/scripts/install.py +406 -0
- package/cliproxy/scripts/model_health.py +16 -0
- package/cliproxy/scripts/modelhealth/__init__.py +5 -0
- package/cliproxy/scripts/modelhealth/checks.py +429 -0
- package/cliproxy/scripts/modelhealth/cli.py +117 -0
- package/cliproxy/scripts/modelhealth/config.py +186 -0
- package/cliproxy/scripts/modelhealth/emulator.py +536 -0
- package/cliproxy/scripts/modelhealth/expect.py +88 -0
- package/cliproxy/scripts/modelhealth/findings.py +183 -0
- package/cliproxy/scripts/modelhealth/protocol.py +330 -0
- package/cliproxy/scripts/modelhealth/redact.py +28 -0
- package/cliproxy/scripts/modelhealth/report.py +150 -0
- package/cliproxy/scripts/modelhealth/runner.py +390 -0
- package/cliproxy/scripts/modelhealth/scenario.py +131 -0
- package/cliproxy/scripts/modelhealth/schema.py +63 -0
- package/cliproxy/scripts/modelhealth/simulate.py +111 -0
- package/cliproxy/scripts/modelhealth/transport.py +213 -0
- package/cliproxy/scripts/monitor +68 -0
- package/cliproxy/scripts/ops +527 -0
- package/cliproxy/scripts/public-endpoint +127 -0
- package/cliproxy/scripts/recover_node.py +177 -0
- package/cliproxy/scripts/restore-verify +60 -0
- package/cliproxy/scripts/retention.py +144 -0
- package/cliproxy/scripts/service-start +7 -0
- package/cliproxy/scripts/validate-config +56 -0
- package/cliproxy/scripts/vault-access.py +57 -0
- package/cliproxy/scripts/vault-backup-login +55 -0
- package/cliproxy/scripts/vault-recovery +56 -0
- package/cliproxy/scripts/vault-renew +30 -0
- package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
- package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
- package/cliproxy/systemd/cliproxy-backup.service +13 -0
- package/cliproxy/systemd/cliproxy-backup.timer +8 -0
- package/cliproxy/systemd/cliproxy-fleet.service +17 -0
- package/cliproxy/systemd/cliproxy-gc.service +13 -0
- package/cliproxy/systemd/cliproxy-gc.timer +8 -0
- package/cliproxy/systemd/cliproxy-monitor.service +8 -0
- package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
- package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
- package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
- package/cliproxy/vendor/LICENSE +22 -0
- package/cliproxy/vendor/config.upstream.yaml +1349 -0
- package/examples/fleet.json +95 -0
- package/examples/roster.json +13 -0
- package/mise.lock +1492 -0
- package/mise.toml +99 -0
- package/package.json +65 -4
- package/schema/fleet.schema.json +364 -0
- package/schema/roster.schema.json +77 -0
- package/src/agents/account-relay.ts +111 -0
- package/src/agents/accounts.ts +85 -0
- package/src/agents/audit.ts +77 -0
- package/src/agents/cli.ts +13 -0
- package/src/agents/command.ts +204 -0
- package/src/agents/converge.ts +123 -0
- package/src/agents/doctor.ts +107 -0
- package/src/agents/errors.ts +2 -0
- package/src/agents/files.ts +73 -0
- package/src/agents/fleet-verify.ts +156 -0
- package/src/agents/launch.ts +290 -0
- package/src/agents/native-verify.ts +226 -0
- package/src/agents/provider-errors.ts +50 -0
- package/src/agents/registry.ts +55 -0
- package/src/agents/render.ts +110 -0
- package/src/agents/round-trip.ts +192 -0
- package/src/agents/secrets.ts +13 -0
- package/src/agents/toolchain.ts +34 -0
- package/src/agents/verify-cache.ts +103 -0
- package/src/agents/verify.ts +127 -0
- package/src/auth-checks.ts +163 -0
- package/src/cli/agent-guide.ts +95 -0
- package/src/cli/app.ts +115 -0
- package/src/cli/cli.ts +21 -0
- package/src/cli/command-index.ts +50 -0
- package/src/cli/contracts.ts +25 -0
- package/src/cli/fleet-reference.ts +17 -0
- package/src/cli/fleet-sync-checkout.ts +91 -0
- package/src/cli/fleet-sync-enrollment.ts +144 -0
- package/src/cli/fleet-sync-forward.ts +134 -0
- package/src/cli/fleet-sync-manager.ts +193 -0
- package/src/cli/fleet-sync-monitoring.ts +185 -0
- package/src/cli/fleet-sync-output.ts +314 -0
- package/src/cli/fleet-sync-privilege.ts +27 -0
- package/src/cli/fleet-sync-progress.ts +109 -0
- package/src/cli/fleet-sync-remote.ts +236 -0
- package/src/cli/fleet-sync-report.ts +26 -0
- package/src/cli/fleet-sync-schedule.ts +138 -0
- package/src/cli/fleet-sync-vault.ts +166 -0
- package/src/cli/fleet-sync.ts +309 -0
- package/src/cli/fleet.ts +163 -0
- package/src/cli/managed-health.ts +219 -0
- package/src/cli/node-deployment.ts +34 -0
- package/src/cli/node-sync.ts +114 -0
- package/src/cli/node-t3.ts +31 -0
- package/src/cli/options.ts +141 -0
- package/src/cli/probe-source.ts +7 -0
- package/src/cli/program.ts +169 -0
- package/src/cli/register-fleet.ts +246 -0
- package/src/cli/registry.ts +158 -0
- package/src/cli/repair-apply.ts +67 -0
- package/src/cli/repair-guard.ts +11 -0
- package/src/cli/repair-inspect.ts +150 -0
- package/src/cli/repair-script.ts +76 -0
- package/src/cli/repair.ts +209 -0
- package/src/cli/roster.ts +89 -0
- package/src/cli/search.ts +35 -0
- package/src/cli/settings.ts +64 -0
- package/src/cli/source-operations.ts +78 -0
- package/src/cli/t3/command.ts +90 -0
- package/src/cli/t3/register.ts +55 -0
- package/src/cli/tailnet.ts +127 -0
- package/src/cli/ui-actions.ts +133 -0
- package/src/cli/ui-controller.ts +113 -0
- package/src/cli/ui-data.ts +106 -0
- package/src/cli/ui-execute.ts +163 -0
- package/src/cli/ui-repairs.ts +34 -0
- package/src/cli/ui-reports.ts +211 -0
- package/src/cli/ui-targets.ts +100 -0
- package/src/cli/ui-terminal.ts +109 -0
- package/src/cli/ui-types.ts +49 -0
- package/src/cli/ui-wizard.ts +101 -0
- package/src/cli/ui.ts +9 -0
- package/src/cli/worker-health-output.ts +37 -0
- package/src/config.ts +186 -0
- package/src/configure.ts +255 -0
- package/src/definition.ts +260 -0
- package/src/diagnostics.ts +92 -0
- package/src/doctor-check.ts +10 -0
- package/src/errors.ts +1 -0
- package/src/fingerprint.ts +120 -0
- package/src/fleet-config.ts +88 -0
- package/src/index.ts +6 -0
- package/src/input-hashes.ts +73 -0
- package/src/machine/local-source.ts +52 -0
- package/src/machine/lock-error.ts +2 -0
- package/src/machine/platform.ts +44 -0
- package/src/machine/source.ts +68 -0
- package/src/machine/state.ts +185 -0
- package/src/machine-health.ts +95 -0
- package/src/managed-source.ts +47 -0
- package/src/manager-failure.ts +48 -0
- package/src/manager-sync.ts +400 -0
- package/src/manager.ts +62 -0
- package/src/monitoring/apply.ts +171 -0
- package/src/monitoring/beszel.ts +191 -0
- package/src/monitoring/enroll.ts +110 -0
- package/src/monitoring/hub-access.ts +113 -0
- package/src/monitoring/hub-viewer.ts +142 -0
- package/src/monitoring/hub.ts +93 -0
- package/src/monitoring/manager-agent.ts +73 -0
- package/src/monitoring/manager-hub.ts +232 -0
- package/src/monitoring/remote.ts +65 -0
- package/src/network-definition.ts +83 -0
- package/src/node-converge-linux.ts +372 -0
- package/src/node-converge-shared.ts +83 -0
- package/src/node-converge.ts +40 -0
- package/src/onboard.ts +35 -0
- package/src/platform/ansible-receipt.ts +128 -0
- package/src/platform/ansible.ts +62 -0
- package/src/platform/darwin.ts +203 -0
- package/src/platform/detect.ts +11 -0
- package/src/platform/fallback.ts +110 -0
- package/src/platform/index.ts +38 -0
- package/src/platform/launchd.ts +98 -0
- package/src/platform/linux.ts +139 -0
- package/src/platform/ps.ts +45 -0
- package/src/platform/resource-verify.ts +128 -0
- package/src/platform/systemd-user.ts +95 -0
- package/src/platform/t3-policy.ts +137 -0
- package/src/platform/t3-route.ts +68 -0
- package/src/platform/t3-service.ts +75 -0
- package/src/platform/types.ts +123 -0
- package/src/platform/ubuntu-audit.ts +63 -0
- package/src/platform/ubuntu-verify.ts +105 -0
- package/src/platform/windows.ts +148 -0
- package/src/power-recovery.ts +55 -0
- package/src/probe.ts +51 -0
- package/src/protocol.ts +119 -0
- package/src/proxy-converge.ts +85 -0
- package/src/remote-bootstrap.ts +212 -0
- package/src/remote-entry.ts +89 -0
- package/src/remote-lock.ts +12 -0
- package/src/repair-catalog.ts +206 -0
- package/src/repair-plan.ts +78 -0
- package/src/repair-runner.ts +84 -0
- package/src/report.ts +71 -0
- package/src/runner.ts +104 -0
- package/src/secrets.ts +169 -0
- package/src/service-auth.ts +115 -0
- package/src/ssh.ts +135 -0
- package/src/sync.ts +104 -0
- package/src/t3/accounts.ts +81 -0
- package/src/t3/desired.ts +165 -0
- package/src/t3/files.ts +81 -0
- package/src/t3/names-check.ts +53 -0
- package/src/t3/names.ts +155 -0
- package/src/t3/plan.ts +82 -0
- package/src/t3/providers.ts +74 -0
- package/src/t3/reconcile.ts +123 -0
- package/src/t3-endpoint.ts +48 -0
- package/src/t3-linked-check.ts +101 -0
- package/src/tailnet-health.ts +231 -0
- package/src/tailnet-matching.ts +37 -0
- package/src/tailscale.ts +141 -0
- package/src/test-fixtures.ts +36 -0
- package/src/toolchain-checks.ts +83 -0
- package/src/topology.ts +24 -0
- package/src/transport.ts +122 -0
- package/src/types.ts +95 -0
- package/src/vault-config.ts +38 -0
- package/src/worker-access-checks.ts +115 -0
- package/src/worker-capacity.ts +34 -0
- package/src/worker-host-checks.ts +176 -0
- package/src/worker-policy.ts +64 -0
- package/src/worker-readiness.ts +64 -0
- package/src/workstation-checks.ts +29 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Executed over strict SSH from the selected verified Git revision. stdin is a
|
|
3
|
+
# base64 Git bundle. All executable content remains in this tracked source.
|
|
4
|
+
set -eu
|
|
5
|
+
umask 077
|
|
6
|
+
revision=$1
|
|
7
|
+
bundle_sha=$2
|
|
8
|
+
node=$3
|
|
9
|
+
source_mode=${4:-transfer}
|
|
10
|
+
case "$source_mode" in cached|transfer) ;; *) exit 2;; esac
|
|
11
|
+
case "$revision" in *[!a-f0-9]*|'') exit 2;; esac
|
|
12
|
+
[ "${#revision}" = 40 ] || exit 2
|
|
13
|
+
case "$bundle_sha" in *[!a-f0-9]*|'') exit 2;; esac
|
|
14
|
+
[ "${#bundle_sha}" = 64 ] || exit 2
|
|
15
|
+
case "$node" in *[!a-z0-9-]*|'') exit 2;; esac
|
|
16
|
+
phase=bootstrap
|
|
17
|
+
bundle=''
|
|
18
|
+
code=0
|
|
19
|
+
failure_code=REMOTE_BOOTSTRAP_INCOMPLETE
|
|
20
|
+
die() { failure_code=$1; exit 1; }
|
|
21
|
+
fail() {
|
|
22
|
+
printf '{"version":1,"checks":[{"id":"bootstrap:%s","status":"fail","summary":"%s","remedy":{"action":"Inspect prerequisites and the remote operation lock, then retry the approved revision. No success receipt advanced."}}]}\n' "$phase" "$failure_code"
|
|
23
|
+
}
|
|
24
|
+
trap 'code=$?; [ -z "$bundle" ] || rm -f -- "$bundle"; if [ "$code" != 0 ]; then fail; fi' EXIT
|
|
25
|
+
# Preserve the lock holder if its SSH channel disappears mid-install.
|
|
26
|
+
trap '' HUP
|
|
27
|
+
trap 'exit 1' INT TERM
|
|
28
|
+
# No shell startup assumptions, and no implicit root development user.
|
|
29
|
+
[ "$(id -u)" != 0 ] || die ROOT_DEVELOPMENT_USER_REJECTED
|
|
30
|
+
# Ubuntu 26.04 is the accepted worker platform; macOS 14+ follows the same
|
|
31
|
+
# phases best effort with launchd instead of systemd.
|
|
32
|
+
platform=$(uname -s)
|
|
33
|
+
case "$platform" in
|
|
34
|
+
Linux)
|
|
35
|
+
# shellcheck source=/dev/null
|
|
36
|
+
. /etc/os-release
|
|
37
|
+
[ "$ID" = ubuntu ] && [ "$VERSION_ID" = 26.04 ] || die UBUNTU_BASELINE_MISMATCH
|
|
38
|
+
case "$(uname -m)" in x86_64|aarch64) ;; *) die UBUNTU_ARCHITECTURE_UNSUPPORTED;; esac ;;
|
|
39
|
+
Darwin)
|
|
40
|
+
major=$(sw_vers -productVersion | cut -d. -f1)
|
|
41
|
+
case "$major" in ''|*[!0-9]*) die MACOS_BASELINE_MISMATCH;; esac
|
|
42
|
+
[ "$major" -ge 14 ] || die MACOS_BASELINE_MISMATCH
|
|
43
|
+
case "$(uname -m)" in arm64|x86_64) ;; *) die MACOS_ARCHITECTURE_UNSUPPORTED;; esac ;;
|
|
44
|
+
*) die PLATFORM_UNSUPPORTED;;
|
|
45
|
+
esac
|
|
46
|
+
root="$HOME/.local/share/personal/fleet"
|
|
47
|
+
# Refuse symlinked state roots rather than allowing writes through them.
|
|
48
|
+
for path in "$HOME/.local" "$HOME/.local/share" "$HOME/.local/share/personal" "$root"; do
|
|
49
|
+
[ ! -L "$path" ] || die FLEET_STATE_PATH_UNSAFE
|
|
50
|
+
done
|
|
51
|
+
mkdir -p "$root"
|
|
52
|
+
phase=lock
|
|
53
|
+
[ ! -L "$root/remote-operation.lock" ] || exit 1
|
|
54
|
+
exec 9>"$root/remote-operation.lock"
|
|
55
|
+
# macOS has no flock(1); lock the same descriptor through fcntl. The lock
|
|
56
|
+
# follows this shell's open descriptor and is released when it exits.
|
|
57
|
+
if command -v flock >/dev/null 2>&1; then
|
|
58
|
+
flock -n 9 || die FLEET_REMOTE_LOCKED
|
|
59
|
+
else
|
|
60
|
+
command -v python3 >/dev/null 2>&1 || die BOOTSTRAP_LOCK_TOOL_REQUIRED
|
|
61
|
+
python3 -c 'import fcntl; fcntl.flock(9, fcntl.LOCK_EX | fcntl.LOCK_NB)' 9>&9 || die FLEET_REMOTE_LOCKED
|
|
62
|
+
fi
|
|
63
|
+
printf '{"version":1,"pid":%s,"revision":"%s","node":"%s"}\n' "$$" "$revision" "$node" >"$root/remote-operation.json"
|
|
64
|
+
phase=resources
|
|
65
|
+
# A bootstrap floor protects source transfer and minimal prerequisites. The
|
|
66
|
+
# Workstation baseline subsequently enforces its stricter declared capacity.
|
|
67
|
+
free_kib=$(df -Pk "$root" | awk 'END {print $4}')
|
|
68
|
+
free_inodes=$(df -Pi "$root" | awk 'END {print $4}')
|
|
69
|
+
case "$free_kib:$free_inodes" in *[!0-9:]*|:*|*:) die BOOTSTRAP_CAPACITY_UNVERIFIED;; esac
|
|
70
|
+
[ "$free_kib" -ge 2097152 ] && [ "$free_inodes" -ge 50000 ] || die BOOTSTRAP_DISK_CAPACITY_REQUIRED
|
|
71
|
+
phase=source-transfer
|
|
72
|
+
if [ "$source_mode" = transfer ]; then
|
|
73
|
+
bundle=$(mktemp "$root/source.XXXXXX")
|
|
74
|
+
# Leave no executable payload copies; this temporary file is only Git data.
|
|
75
|
+
base64 -d >"$bundle"
|
|
76
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
77
|
+
printf '%s %s\n' "$bundle_sha" "$bundle" | sha256sum -c - >/dev/null 2>&1 || die SOURCE_DIGEST_MISMATCH
|
|
78
|
+
else
|
|
79
|
+
[ "$(shasum -a 256 "$bundle" | cut -d' ' -f1)" = "$bundle_sha" ] || die SOURCE_DIGEST_MISMATCH
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
phase=prerequisites
|
|
83
|
+
if [ "$platform" = Linux ]; then
|
|
84
|
+
missing=''
|
|
85
|
+
for package in git curl ca-certificates unzip xz-utils build-essential python3 ansible-core; do
|
|
86
|
+
dpkg-query -W -f='${db:Status-Status}' "$package" 2>/dev/null | grep -qx installed || missing="$missing $package"
|
|
87
|
+
done
|
|
88
|
+
if [ -n "$missing" ]; then
|
|
89
|
+
sudo -n true >/dev/null 2>&1 || die SUDO_AUTHORIZATION_REQUIRED
|
|
90
|
+
# Never remove APT locks or kill package processes. Timeout keeps genuine
|
|
91
|
+
# transactions bounded; Ubuntu archive signatures/TLS remain enabled.
|
|
92
|
+
sudo -n timeout 300 apt-get -o DPkg::Lock::Timeout=120 update >/dev/null 2>&1 || die APT_UPDATE_FAILED
|
|
93
|
+
# shellcheck disable=SC2086
|
|
94
|
+
sudo -n timeout 600 env DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=120 --no-install-recommends install -y $missing >/dev/null 2>&1 || die APT_INSTALL_FAILED
|
|
95
|
+
fi
|
|
96
|
+
else
|
|
97
|
+
# Apple Command Line Tools provide git and python3; Ansible runs through the
|
|
98
|
+
# pinned uv tool runner installed by the shared mise toolchain below.
|
|
99
|
+
xcode-select -p >/dev/null 2>&1 || die XCODE_COMMAND_LINE_TOOLS_REQUIRED
|
|
100
|
+
command -v git >/dev/null 2>&1 || die XCODE_COMMAND_LINE_TOOLS_REQUIRED
|
|
101
|
+
fi
|
|
102
|
+
phase=source-verification
|
|
103
|
+
deployment="$root/revisions/$revision"
|
|
104
|
+
[ ! -L "$root/revisions" ] && [ ! -L "$deployment" ] || exit 1
|
|
105
|
+
mkdir -p "$deployment"
|
|
106
|
+
if [ ! -e "$deployment/.git" ]; then
|
|
107
|
+
[ -z "$(ls -A "$deployment")" ] || exit 1
|
|
108
|
+
git -C "$deployment" init -q >/dev/null 2>&1 || exit 1
|
|
109
|
+
fi
|
|
110
|
+
# An interrupted initial fetch/checkout can leave an empty initialized repository.
|
|
111
|
+
# Resume only that state; an existing commit or untracked work is never reset.
|
|
112
|
+
if ! git -C "$deployment" rev-parse --verify HEAD >/dev/null 2>&1; then
|
|
113
|
+
[ "$source_mode" = transfer ] || die SOURCE_CACHE_CHANGED
|
|
114
|
+
[ "$(git -C "$deployment" rev-parse --show-toplevel)" = "$deployment" ] || exit 1
|
|
115
|
+
clean=$(git -C "$deployment" status --porcelain) || die SOURCE_STATUS_FAILED
|
|
116
|
+
[ -z "$clean" ] || die SOURCE_CHECKOUT_CONFLICT
|
|
117
|
+
# The bundle holds this commit without its history: declare the checkout
|
|
118
|
+
# shallow at it so Git accepts a commit whose parents are absent.
|
|
119
|
+
printf '%s\n' "$revision" >"$deployment/.git/shallow"
|
|
120
|
+
git -C "$deployment" fetch --no-tags "$bundle" "$revision" >/dev/null 2>&1 || exit 1
|
|
121
|
+
git -C "$deployment" checkout --detach "$revision" >/dev/null 2>&1 || exit 1
|
|
122
|
+
fi
|
|
123
|
+
[ -z "$bundle" ] || rm -f "$bundle"
|
|
124
|
+
bundle=''
|
|
125
|
+
[ "$(git -C "$deployment" rev-parse HEAD)" = "$revision" ] || exit 1
|
|
126
|
+
[ "$(git -C "$deployment" rev-parse --show-toplevel)" = "$deployment" ] || exit 1
|
|
127
|
+
clean=$(git -C "$deployment" status --porcelain) || die SOURCE_STATUS_FAILED
|
|
128
|
+
[ -z "$clean" ] || die SOURCE_CHECKOUT_CONFLICT
|
|
129
|
+
phase=toolchain
|
|
130
|
+
cd "$deployment"
|
|
131
|
+
export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:/usr/local/bin:/usr/bin:/bin"
|
|
132
|
+
status_file="$root/toolchain-status.json"
|
|
133
|
+
ready_file="$root/toolchain-ready"
|
|
134
|
+
[ ! -L "$status_file" ] && [ ! -L "$ready_file" ] || die FLEET_STATE_PATH_UNSAFE
|
|
135
|
+
# Warm: the same toolchain inputs (tool pins, locks and the setup code itself)
|
|
136
|
+
# passed within a day, so a revision that changes none of them skips the full
|
|
137
|
+
# setup. Changed inputs, a failure or an older stamp run it; agent versions
|
|
138
|
+
# are still checked (and reinstalled on drift) by node convergence.
|
|
139
|
+
toolchain_key=$(git rev-parse HEAD:mise.toml HEAD:mise.lock HEAD:.mise HEAD:bun.lock HEAD:package.json HEAD:bootstrap 2>/dev/null | tr '\n' ' ')
|
|
140
|
+
[ -n "$toolchain_key" ] || toolchain_key=$revision
|
|
141
|
+
if [ "$(cat "$ready_file" 2>/dev/null)" = "$toolchain_key" ] && [ -n "$(find "$ready_file" -mmin -1440 2>/dev/null)" ]; then
|
|
142
|
+
toolchain_warm=1
|
|
143
|
+
else
|
|
144
|
+
toolchain_warm=''
|
|
145
|
+
rm -f -- "$status_file" "$ready_file"
|
|
146
|
+
fi
|
|
147
|
+
if command -v timeout >/dev/null 2>&1; then bounded='timeout 1800'; else bounded=''; fi
|
|
148
|
+
# shellcheck disable=SC2086
|
|
149
|
+
if [ -z "$toolchain_warm" ] && ! $bounded bash bootstrap/bootstrap.sh --worker --non-interactive --status-file "$status_file" >/dev/null 2>&1; then
|
|
150
|
+
tool_phase=$(sed -n 's/.*"phase":"\([a-z0-9-]*\)".*/\1/p' "$status_file" 2>/dev/null) || tool_phase=''
|
|
151
|
+
case "$tool_phase" in
|
|
152
|
+
managed-config) die TOOLCHAIN_CONFIG_CONFLICT;;
|
|
153
|
+
mise) die TOOLCHAIN_MISE_FAILED;;
|
|
154
|
+
aws) die TOOLCHAIN_AWS_FAILED;;
|
|
155
|
+
t3) die TOOLCHAIN_T3_FAILED;;
|
|
156
|
+
global-config|shell) die TOOLCHAIN_PATH_CONFIGURATION_FAILED;;
|
|
157
|
+
dependencies) die TOOLCHAIN_FROZEN_DEPENDENCIES_FAILED;;
|
|
158
|
+
*) die TOOLCHAIN_CONVERGENCE_FAILED;;
|
|
159
|
+
esac
|
|
160
|
+
fi
|
|
161
|
+
# A new revision's checkout still needs its own (cached, frozen) dependencies.
|
|
162
|
+
if [ -n "$toolchain_warm" ] && [ ! -d node_modules ]; then
|
|
163
|
+
# shellcheck disable=SC2086
|
|
164
|
+
$bounded mise exec --locked bun -- bun install --frozen-lockfile >/dev/null 2>&1 || die TOOLCHAIN_FROZEN_DEPENDENCIES_FAILED
|
|
165
|
+
fi
|
|
166
|
+
[ -n "$toolchain_warm" ] || printf '%s' "$toolchain_key" >"$ready_file"
|
|
167
|
+
# Every synced revision is a full checkout with its own dependencies (about
|
|
168
|
+
# 1 GB). Keep this one, the one T3's providers launch from and the two most
|
|
169
|
+
# recently used others; the rest would slowly fill the disk.
|
|
170
|
+
phase=cleanup
|
|
171
|
+
touch "$deployment"
|
|
172
|
+
applied=$(basename "$(readlink "$root/current" 2>/dev/null || true)")
|
|
173
|
+
kept=0
|
|
174
|
+
# shellcheck disable=SC2012
|
|
175
|
+
for old in $(ls -1t "$root/revisions"); do
|
|
176
|
+
case "$old" in *[!a-f0-9]*) continue;; esac
|
|
177
|
+
[ "${#old}" = 40 ] || continue
|
|
178
|
+
[ "$old" != "$revision" ] && [ "$old" != "$applied" ] || continue
|
|
179
|
+
kept=$((kept + 1))
|
|
180
|
+
[ "$kept" -gt 2 ] || continue
|
|
181
|
+
rm -rf -- "${root:?}/revisions/$old" || true
|
|
182
|
+
done
|
|
183
|
+
phase=configuration
|
|
184
|
+
trap - EXIT INT TERM
|
|
185
|
+
exec mise exec --locked bun -- bun src/node-converge.ts "$node" "$revision"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/** Publish one managed global mise config without clobbering unrelated configuration. */
|
|
2
|
+
import {
|
|
3
|
+
cpSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
statSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
|
|
13
|
+
const MARKER = '# Managed by dev-fleet bootstrap/setup.ts\n';
|
|
14
|
+
/**
|
|
15
|
+
* Where agent harnesses create a fresh checkout per task. Every new path would
|
|
16
|
+
* need its own `mise trust`, and an untrusted mise.toml makes every mise shim
|
|
17
|
+
* (claude, codex, bun, ...) exit before the agent starts. mise honours this
|
|
18
|
+
* setting only in the global config, so it is added at publish time and never
|
|
19
|
+
* written into the repository's mise.toml.
|
|
20
|
+
*/
|
|
21
|
+
export const AGENT_WORKTREE_ROOTS = ['~/.t3/worktrees'];
|
|
22
|
+
const MANAGED = /^# Managed by \S+ \S*setup\.ts\n/;
|
|
23
|
+
export const workspaceRoot = join(import.meta.dirname, '..');
|
|
24
|
+
|
|
25
|
+
export function globalConfigDir(): string {
|
|
26
|
+
if (process.env['MISE_GLOBAL_CONFIG_FILE']) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
'Unset MISE_GLOBAL_CONFIG_FILE before applying the shared toolchain.'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return (
|
|
32
|
+
process.env['MISE_CONFIG_DIR'] ??
|
|
33
|
+
join(process.env['XDG_CONFIG_HOME'] ?? join(homedir(), '.config'), 'mise')
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function assertManagedConfig(dir: string): void {
|
|
38
|
+
const config = join(dir, 'config.toml');
|
|
39
|
+
const lock = join(dir, 'mise.lock');
|
|
40
|
+
// Any earlier owner name counts: the marker text identifies the file as
|
|
41
|
+
// managed, never the repository or path it was published from.
|
|
42
|
+
const managed =
|
|
43
|
+
existsSync(config) && MANAGED.test(readFileSync(config, 'utf8'));
|
|
44
|
+
if ((existsSync(config) && !managed) || (existsSync(lock) && !managed)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Existing mise configuration at ${dir}. Back up and move config.toml and mise.lock, then rerun. Nothing was overwritten.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function publishGlobalConfig(dir: string, root = workspaceRoot): void {
|
|
52
|
+
assertManagedConfig(dir);
|
|
53
|
+
mkdirSync(dir, { recursive: true });
|
|
54
|
+
const path = join(dir, 'config.toml');
|
|
55
|
+
const previous = existsSync(path) ? readFileSync(path, 'utf8') : undefined;
|
|
56
|
+
const config =
|
|
57
|
+
MARKER +
|
|
58
|
+
withTrustedPaths(
|
|
59
|
+
readFileSync(join(root, 'mise.toml'), 'utf8'),
|
|
60
|
+
trustedPaths(previous)
|
|
61
|
+
);
|
|
62
|
+
if (previous !== config) writeFileSync(path, config);
|
|
63
|
+
cpSync(join(root, 'mise.lock'), join(dir, 'mise.lock'), {
|
|
64
|
+
filter: changedFile,
|
|
65
|
+
});
|
|
66
|
+
const sidecars = join(root, '.mise/locks');
|
|
67
|
+
if (existsSync(sidecars))
|
|
68
|
+
cpSync(sidecars, join(dir, '.mise/locks'), {
|
|
69
|
+
recursive: true,
|
|
70
|
+
filter: changedFile,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The managed roots plus any entries already in the published config, so a
|
|
76
|
+
* local `mise settings add trusted_config_paths ...` survives the next publish.
|
|
77
|
+
*/
|
|
78
|
+
export function trustedPaths(previous: string | undefined): string[] {
|
|
79
|
+
let local: unknown;
|
|
80
|
+
try {
|
|
81
|
+
local = previous
|
|
82
|
+
? (Bun.TOML.parse(previous) as { settings?: Record<string, unknown> })
|
|
83
|
+
.settings?.['trusted_config_paths']
|
|
84
|
+
: undefined;
|
|
85
|
+
} catch {
|
|
86
|
+
local = undefined;
|
|
87
|
+
}
|
|
88
|
+
const extra = Array.isArray(local)
|
|
89
|
+
? local.filter((path): path is string => typeof path === 'string')
|
|
90
|
+
: [];
|
|
91
|
+
return [...new Set([...AGENT_WORKTREE_ROOTS, ...extra])];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Adds trusted_config_paths to the [settings] table, creating it if absent. */
|
|
95
|
+
export function withTrustedPaths(config: string, paths: string[]): string {
|
|
96
|
+
const line = `trusted_config_paths = [${paths.map((path) => JSON.stringify(path)).join(', ')}]`;
|
|
97
|
+
if (/^trusted_config_paths\s*=/m.test(config))
|
|
98
|
+
throw new Error(
|
|
99
|
+
'mise.toml sets trusted_config_paths; mise ignores it outside the global config. Add roots to AGENT_WORKTREE_ROOTS instead.'
|
|
100
|
+
);
|
|
101
|
+
const header = /^\[settings\][^\S\n]*\n/m;
|
|
102
|
+
return header.test(config)
|
|
103
|
+
? config.replace(header, (match) => `${match}${line}\n`)
|
|
104
|
+
: `${config.replace(/\n*$/, '\n')}\n[settings]\n${line}\n`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function changedFile(source: string, destination: string): boolean {
|
|
108
|
+
return (
|
|
109
|
+
statSync(source).isDirectory() ||
|
|
110
|
+
!existsSync(destination) ||
|
|
111
|
+
!readFileSync(source).equals(readFileSync(destination))
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Links ~/.local/bin/devfleet to this checkout (or the one given). Idempotent.
|
|
3
|
+
set -eu
|
|
4
|
+
root=${1:-$(cd "$(dirname "$0")/.." && pwd)}
|
|
5
|
+
[ -x "$root/bootstrap/devfleet" ] || exit 1
|
|
6
|
+
mkdir -p "$HOME/.local/bin"
|
|
7
|
+
ln -sfn "$root/bootstrap/devfleet" "$HOME/.local/bin/devfleet"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** AWS v2's official Windows MSI; aqua only packages AWS for Unix. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { appendFileSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { workspaceRoot } from './global-config';
|
|
8
|
+
|
|
9
|
+
type AwsConfig = {
|
|
10
|
+
tools: { aws: { version: string } };
|
|
11
|
+
vars: { aws_windows_msi_sha256: string };
|
|
12
|
+
};
|
|
13
|
+
export function awsWindowsArtifact(config: AwsConfig): {
|
|
14
|
+
url: string;
|
|
15
|
+
sha256: string;
|
|
16
|
+
version: string;
|
|
17
|
+
} {
|
|
18
|
+
const version = config.tools.aws.version;
|
|
19
|
+
const sha256 = config.vars.aws_windows_msi_sha256;
|
|
20
|
+
if (!/^2\.\d+\.\d+$/.test(version) || !/^[a-f0-9]{64}$/.test(sha256))
|
|
21
|
+
throw new Error('Invalid pinned AWS Windows artifact.');
|
|
22
|
+
return {
|
|
23
|
+
version,
|
|
24
|
+
url: `https://awscli.amazonaws.com/AWSCLIV2-${version}.msi`,
|
|
25
|
+
sha256,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function installNativeAws(): Promise<void> {
|
|
30
|
+
if (process.platform !== 'win32') return;
|
|
31
|
+
const config = Bun.TOML.parse(
|
|
32
|
+
readFileSync(join(workspaceRoot, 'mise.toml'), 'utf8')
|
|
33
|
+
) as AwsConfig;
|
|
34
|
+
const artifact = awsWindowsArtifact(config);
|
|
35
|
+
const bin = join(
|
|
36
|
+
process.env['ProgramFiles'] ?? 'C:\\Program Files',
|
|
37
|
+
'Amazon',
|
|
38
|
+
'AWSCLIV2'
|
|
39
|
+
);
|
|
40
|
+
const executable = join(bin, 'aws.exe');
|
|
41
|
+
const probe = () =>
|
|
42
|
+
spawnSync(executable, ['--version'], { encoding: 'utf8', timeout: 30_000 });
|
|
43
|
+
if (!probe().stdout?.includes(`aws-cli/${artifact.version} `)) {
|
|
44
|
+
const directory = mkdtempSync(join(tmpdir(), 'workspace-aws-'));
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch(artifact.url);
|
|
47
|
+
if (!response.ok)
|
|
48
|
+
throw new Error(`AWS MSI download failed (${response.status}).`);
|
|
49
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
50
|
+
if (createHash('sha256').update(bytes).digest('hex') !== artifact.sha256)
|
|
51
|
+
throw new Error('AWS MSI checksum mismatch.');
|
|
52
|
+
const msi = join(directory, 'aws.msi');
|
|
53
|
+
await Bun.write(msi, bytes);
|
|
54
|
+
const script = String.raw`$arguments = @('/i', ('"' + $env:WORKSPACE_AWS_MSI + '"'), '/qn', '/norestart'); $options = @{FilePath='msiexec.exe'; ArgumentList=$arguments; Wait=$true; PassThru=$true}; if ($env:WORKSPACE_AWS_ELEVATE -eq '1') { $options.Verb = 'RunAs' }; $p = Start-Process @options; exit $p.ExitCode`;
|
|
55
|
+
const result = spawnSync(
|
|
56
|
+
'powershell.exe',
|
|
57
|
+
['-NoProfile', '-Command', script],
|
|
58
|
+
{
|
|
59
|
+
stdio: 'inherit',
|
|
60
|
+
env: {
|
|
61
|
+
...process.env,
|
|
62
|
+
WORKSPACE_AWS_MSI: msi,
|
|
63
|
+
WORKSPACE_AWS_ELEVATE:
|
|
64
|
+
process.argv.includes('--non-interactive') || process.env['CI']
|
|
65
|
+
? '0'
|
|
66
|
+
: '1',
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
if (result.status !== 0 && result.status !== 3010)
|
|
71
|
+
throw new Error(
|
|
72
|
+
`AWS MSI installation failed (exit ${result.status}, signal ${result.signal}, error ${result.error?.message ?? 'none'}); check for a conflicting installed version or run the reviewed bootstrap as Administrator.`
|
|
73
|
+
);
|
|
74
|
+
} finally {
|
|
75
|
+
rmSync(directory, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!probe().stdout?.includes(`aws-cli/${artifact.version} `))
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Expected AWS CLI ${artifact.version}; resolve the existing installation before retrying.`
|
|
81
|
+
);
|
|
82
|
+
process.env['PATH'] = `${bin};${process.env['PATH'] ?? ''}`;
|
|
83
|
+
// Persist across subsequent hosted-runner steps; normal users reopen the shell.
|
|
84
|
+
if (process.env['GITHUB_PATH'])
|
|
85
|
+
appendFileSync(process.env['GITHUB_PATH'], `${bin}\n`);
|
|
86
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/** Pinned Ubuntu T3 distribution. Shared tool installation owns artifacts;
|
|
2
|
+
* Workstation owns services. Never touches ~/.t3 or replaces an existing launcher.
|
|
3
|
+
*/
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import {
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { workspaceRoot } from './global-config';
|
|
17
|
+
interface Configuration {
|
|
18
|
+
vars: {
|
|
19
|
+
t3_version: string;
|
|
20
|
+
t3_linux_x64_sha256: string;
|
|
21
|
+
t3_linux_arm64_sha256: string;
|
|
22
|
+
t3_desktop_amd64_sha256: string;
|
|
23
|
+
t3_desktop_arm64_sha256: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Pinned desktop .deb for Ubuntu workers. The GUI app is the worker's only T3
|
|
27
|
+
* process and embeds the server; the CLI tarball stays for policy commands. */
|
|
28
|
+
export function desktopT3Artifact(config: Configuration, architecture: string) {
|
|
29
|
+
const deb = { x64: 'amd64', arm64: 'arm64' }[architecture];
|
|
30
|
+
if (!deb) throw new Error('Unsupported T3 architecture.');
|
|
31
|
+
const version = config.vars.t3_version;
|
|
32
|
+
const sha256 =
|
|
33
|
+
deb === 'amd64'
|
|
34
|
+
? config.vars.t3_desktop_amd64_sha256
|
|
35
|
+
: config.vars.t3_desktop_arm64_sha256;
|
|
36
|
+
if (
|
|
37
|
+
!/^\d+\.\d+\.\d+(?:-nightly\.\d{8}\.\d+)?$/.test(version) ||
|
|
38
|
+
!/^[a-f0-9]{64}$/.test(sha256)
|
|
39
|
+
)
|
|
40
|
+
throw new Error('Invalid T3 desktop pin.');
|
|
41
|
+
const name = `T3-Code-${version}-${deb}.deb`;
|
|
42
|
+
return {
|
|
43
|
+
version,
|
|
44
|
+
sha256,
|
|
45
|
+
name,
|
|
46
|
+
url: `https://github.com/pingdotgg/t3code/releases/download/v${version}/${name}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function managedT3Desktop() {
|
|
50
|
+
const config = Bun.TOML.parse(
|
|
51
|
+
readFileSync(join(workspaceRoot, 'mise.toml'), 'utf8')
|
|
52
|
+
) as Configuration;
|
|
53
|
+
return desktopT3Artifact(config, process.arch);
|
|
54
|
+
}
|
|
55
|
+
export function nativeT3Artifact(config: Configuration, architecture: string) {
|
|
56
|
+
if (!['x64', 'arm64'].includes(architecture))
|
|
57
|
+
throw new Error('Unsupported T3 architecture.');
|
|
58
|
+
const version = config.vars.t3_version;
|
|
59
|
+
const sha256 =
|
|
60
|
+
architecture === 'x64'
|
|
61
|
+
? config.vars.t3_linux_x64_sha256
|
|
62
|
+
: config.vars.t3_linux_arm64_sha256;
|
|
63
|
+
if (
|
|
64
|
+
!/^\d+\.\d+\.\d+(?:-nightly\.\d{8}\.\d+)?$/.test(version) ||
|
|
65
|
+
!/^[a-f0-9]{64}$/.test(sha256)
|
|
66
|
+
)
|
|
67
|
+
throw new Error('Invalid T3 pin.');
|
|
68
|
+
const name = `t3-${version}-linux-${architecture}`;
|
|
69
|
+
return {
|
|
70
|
+
version,
|
|
71
|
+
sha256,
|
|
72
|
+
name,
|
|
73
|
+
url: `https://github.com/pingdotgg/t3code/releases/download/v${version}/${name}.tar.gz`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function managedT3(): { path: string; version: string } {
|
|
77
|
+
const config = Bun.TOML.parse(
|
|
78
|
+
readFileSync(join(workspaceRoot, 'mise.toml'), 'utf8')
|
|
79
|
+
) as Configuration;
|
|
80
|
+
const artifact = nativeT3Artifact(config, process.arch);
|
|
81
|
+
return {
|
|
82
|
+
path: join(homedir(), '.local/share/personal/tools', artifact.name, 't3'),
|
|
83
|
+
version: artifact.version,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export async function installNativeT3(): Promise<void> {
|
|
87
|
+
if (process.platform !== 'linux') return;
|
|
88
|
+
const config = Bun.TOML.parse(
|
|
89
|
+
readFileSync(join(workspaceRoot, 'mise.toml'), 'utf8')
|
|
90
|
+
) as Configuration;
|
|
91
|
+
const artifact = nativeT3Artifact(config, process.arch);
|
|
92
|
+
const root = join(homedir(), '.local/share/personal/tools');
|
|
93
|
+
const archive = join(root, `${artifact.name}.tar.gz`);
|
|
94
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
95
|
+
if (!existsSync(archive)) {
|
|
96
|
+
const response = await fetch(artifact.url, {
|
|
97
|
+
signal: AbortSignal.timeout(120_000),
|
|
98
|
+
});
|
|
99
|
+
if (!response.ok) throw new Error('Pinned T3 artifact unavailable.');
|
|
100
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
101
|
+
if (createHash('sha256').update(bytes).digest('hex') !== artifact.sha256)
|
|
102
|
+
throw new Error('T3 artifact checksum mismatch.');
|
|
103
|
+
writeFileSync(`${archive}.partial`, bytes, { mode: 0o600 });
|
|
104
|
+
renameSync(`${archive}.partial`, archive);
|
|
105
|
+
}
|
|
106
|
+
if (
|
|
107
|
+
createHash('sha256').update(readFileSync(archive)).digest('hex') !==
|
|
108
|
+
artifact.sha256
|
|
109
|
+
)
|
|
110
|
+
throw new Error(
|
|
111
|
+
'T3 cached artifact drift; preserve running service and review maintenance recovery.'
|
|
112
|
+
);
|
|
113
|
+
const directory = join(root, artifact.name);
|
|
114
|
+
if (!existsSync(directory)) {
|
|
115
|
+
const staging = join(root, `${artifact.name}.staging`);
|
|
116
|
+
mkdirSync(staging, { recursive: true, mode: 0o700 });
|
|
117
|
+
try {
|
|
118
|
+
if (
|
|
119
|
+
spawnSync(
|
|
120
|
+
'tar',
|
|
121
|
+
['--same-permissions', '-xzf', archive, '-C', staging],
|
|
122
|
+
{ timeout: 120_000 }
|
|
123
|
+
).status !== 0
|
|
124
|
+
)
|
|
125
|
+
throw new Error('T3 extraction failed.');
|
|
126
|
+
verifyArchive(archive, staging);
|
|
127
|
+
renameSync(join(staging, artifact.name), directory);
|
|
128
|
+
} finally {
|
|
129
|
+
rmSync(staging, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
} else verifyArchive(archive, root);
|
|
132
|
+
const version = spawnSync(join(directory, 't3'), ['--version'], {
|
|
133
|
+
encoding: 'utf8',
|
|
134
|
+
timeout: 30_000,
|
|
135
|
+
});
|
|
136
|
+
if (
|
|
137
|
+
version.status !== 0 ||
|
|
138
|
+
version.stdout.trim() !== `t3 v${artifact.version}`
|
|
139
|
+
)
|
|
140
|
+
throw new Error('Pinned T3 executable verification failed.');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function verifyArchive(archive: string, root: string): void {
|
|
144
|
+
if (
|
|
145
|
+
spawnSync(
|
|
146
|
+
'python3',
|
|
147
|
+
[join(workspaceRoot, 'bootstrap/verify-t3-archive.py'), archive, root],
|
|
148
|
+
{ timeout: 120_000 }
|
|
149
|
+
).status !== 0
|
|
150
|
+
)
|
|
151
|
+
throw new Error(
|
|
152
|
+
'Managed T3 files differ from the pinned artifact; maintenance is required.'
|
|
153
|
+
);
|
|
154
|
+
}
|