@crvouga/dev-fleet 0.0.0-stage → 0.2.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.
Files changed (363) hide show
  1. package/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
  2. package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
  3. package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
  4. package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
  5. package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
  6. package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
  7. package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
  8. package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
  9. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
  10. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
  11. package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
  12. package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
  13. package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
  14. package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
  15. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
  16. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
  17. package/LICENSE +21 -0
  18. package/README.md +357 -2
  19. package/agents/antigravity-proxy.ts +3 -0
  20. package/agents/audit.md +89 -0
  21. package/agents/claude-proxy +5 -0
  22. package/agents/claude-proxy.ts +3 -0
  23. package/agents/codex-proxy.ts +3 -0
  24. package/agents/grok-proxy.ts +3 -0
  25. package/agents/managed-proxy.ts +55 -0
  26. package/agents/opencode-proxy.ts +3 -0
  27. package/agents/registry.json +209 -0
  28. package/agents/registry.schema.json +149 -0
  29. package/agents/setup.md +158 -0
  30. package/agents/t3.json +40 -0
  31. package/ansible/macos/manager.yml +30 -0
  32. package/ansible/macos/policy.json +9 -0
  33. package/ansible/monitoring/README.md +215 -0
  34. package/ansible/monitoring/agent-darwin.yml +198 -0
  35. package/ansible/monitoring/agent.yml +172 -0
  36. package/ansible/monitoring/beszel-agent.service +42 -0
  37. package/ansible/monitoring/compose.yml +74 -0
  38. package/ansible/monitoring/hub-darwin.yml +102 -0
  39. package/ansible/monitoring/hub-route-darwin.yml +51 -0
  40. package/ansible/monitoring/lock.json +16 -0
  41. package/ansible/net/remote-probe.sh +13 -0
  42. package/ansible/pyproject.toml +12 -0
  43. package/ansible/ubuntu/authorize-sudo.sh +51 -0
  44. package/ansible/ubuntu/baseline.yml +89 -0
  45. package/ansible/ubuntu/disk-gc.py +336 -0
  46. package/ansible/ubuntu/disk-guard.py +104 -0
  47. package/ansible/ubuntu/policy.json +52 -0
  48. package/ansible/ubuntu/recovery.yml +15 -0
  49. package/ansible/ubuntu/t3/policy.json +7 -0
  50. package/ansible/ubuntu/t3-desktop.yml +220 -0
  51. package/ansible/ubuntu/t3-health.sh +33 -0
  52. package/ansible/ubuntu/t3-policy.yml +76 -0
  53. package/ansible/ubuntu/t3-route.yml +28 -0
  54. package/ansible/ubuntu/tailscale.yml +78 -0
  55. package/ansible/ubuntu/tasks/credentials.yml +47 -0
  56. package/ansible/ubuntu/tasks/docker.yml +31 -0
  57. package/ansible/ubuntu/tasks/packages.yml +56 -0
  58. package/ansible/ubuntu/tasks/power.yml +51 -0
  59. package/ansible/ubuntu/tasks/recovery.yml +39 -0
  60. package/ansible/ubuntu/tasks/resources.yml +312 -0
  61. package/ansible/ubuntu/tasks/services.yml +31 -0
  62. package/ansible/ubuntu/tasks/ssh.yml +71 -0
  63. package/ansible/ubuntu/tasks/user.yml +120 -0
  64. package/ansible/uv.lock +190 -0
  65. package/bin/devfleet.mjs +36 -0
  66. package/bootstrap/bootstrap.ps1 +50 -0
  67. package/bootstrap/bootstrap.sh +80 -0
  68. package/bootstrap/devfleet +7 -0
  69. package/bootstrap/docker-desktop.sh +33 -0
  70. package/bootstrap/doctor.ts +234 -0
  71. package/bootstrap/fleet-manager-run.sh +103 -0
  72. package/bootstrap/fleet-remote-bootstrap.sh +258 -0
  73. package/bootstrap/global-config.ts +171 -0
  74. package/bootstrap/install-launcher.sh +10 -0
  75. package/bootstrap/native-aws.ts +86 -0
  76. package/bootstrap/native-t3.ts +154 -0
  77. package/bootstrap/npm.ts +358 -0
  78. package/bootstrap/release-version.ts +157 -0
  79. package/bootstrap/setup-status.ts +51 -0
  80. package/bootstrap/setup.ts +88 -0
  81. package/bootstrap/shell-path.ts +105 -0
  82. package/bootstrap/verify-t3-archive.py +49 -0
  83. package/bootstrap/worker-auth.ts +65 -0
  84. package/bootstrap/worker-github.ts +47 -0
  85. package/bootstrap/worker-tools.ts +24 -0
  86. package/cliproxy/AGENTS.md +11 -0
  87. package/cliproxy/README.md +269 -0
  88. package/cliproxy/clients/bootstrap.py +143 -0
  89. package/cliproxy/clients/launcher.py +57 -0
  90. package/cliproxy/compose.yaml +90 -0
  91. package/cliproxy/config.template.yaml +70 -0
  92. package/cliproxy/docs/cloud-adoption.md +16 -0
  93. package/cliproxy/docs/deployment.png +0 -0
  94. package/cliproxy/docs/deployment.svg +109 -0
  95. package/cliproxy/docs/health-workflow.example.yml +185 -0
  96. package/cliproxy/docs/migration.md +131 -0
  97. package/cliproxy/docs/model-health-report.schema.json +364 -0
  98. package/cliproxy/docs/model-health.md +393 -0
  99. package/cliproxy/image.lock.json +5 -0
  100. package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
  101. package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
  102. package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
  103. package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
  104. package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
  105. package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
  106. package/cliproxy/models.json +1618 -0
  107. package/cliproxy/requirements.lock +78 -0
  108. package/cliproxy/scripts/accounts.py +38 -0
  109. package/cliproxy/scripts/audit.py +123 -0
  110. package/cliproxy/scripts/backup +234 -0
  111. package/cliproxy/scripts/check.py +34 -0
  112. package/cliproxy/scripts/checkpoint.py +226 -0
  113. package/cliproxy/scripts/cli.py +64 -0
  114. package/cliproxy/scripts/cliproxy_host.py +257 -0
  115. package/cliproxy/scripts/cloud_state.py +150 -0
  116. package/cliproxy/scripts/container-health +6 -0
  117. package/cliproxy/scripts/docker +24 -0
  118. package/cliproxy/scripts/external-health.py +79 -0
  119. package/cliproxy/scripts/fleet_config.py +43 -0
  120. package/cliproxy/scripts/fleet_sync.py +274 -0
  121. package/cliproxy/scripts/initialize +84 -0
  122. package/cliproxy/scripts/install-dependencies +48 -0
  123. package/cliproxy/scripts/install.py +406 -0
  124. package/cliproxy/scripts/model_health.py +16 -0
  125. package/cliproxy/scripts/modelhealth/__init__.py +5 -0
  126. package/cliproxy/scripts/modelhealth/checks.py +429 -0
  127. package/cliproxy/scripts/modelhealth/cli.py +117 -0
  128. package/cliproxy/scripts/modelhealth/config.py +186 -0
  129. package/cliproxy/scripts/modelhealth/emulator.py +536 -0
  130. package/cliproxy/scripts/modelhealth/expect.py +88 -0
  131. package/cliproxy/scripts/modelhealth/findings.py +183 -0
  132. package/cliproxy/scripts/modelhealth/protocol.py +330 -0
  133. package/cliproxy/scripts/modelhealth/redact.py +28 -0
  134. package/cliproxy/scripts/modelhealth/report.py +150 -0
  135. package/cliproxy/scripts/modelhealth/runner.py +390 -0
  136. package/cliproxy/scripts/modelhealth/scenario.py +131 -0
  137. package/cliproxy/scripts/modelhealth/schema.py +63 -0
  138. package/cliproxy/scripts/modelhealth/simulate.py +111 -0
  139. package/cliproxy/scripts/modelhealth/transport.py +213 -0
  140. package/cliproxy/scripts/monitor +68 -0
  141. package/cliproxy/scripts/ops +527 -0
  142. package/cliproxy/scripts/public-endpoint +127 -0
  143. package/cliproxy/scripts/recover_node.py +177 -0
  144. package/cliproxy/scripts/restore-verify +60 -0
  145. package/cliproxy/scripts/retention.py +144 -0
  146. package/cliproxy/scripts/service-start +7 -0
  147. package/cliproxy/scripts/validate-config +56 -0
  148. package/cliproxy/scripts/vault-access.py +57 -0
  149. package/cliproxy/scripts/vault-backup-login +55 -0
  150. package/cliproxy/scripts/vault-recovery +56 -0
  151. package/cliproxy/scripts/vault-renew +30 -0
  152. package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
  153. package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
  154. package/cliproxy/systemd/cliproxy-backup.service +13 -0
  155. package/cliproxy/systemd/cliproxy-backup.timer +8 -0
  156. package/cliproxy/systemd/cliproxy-fleet.service +17 -0
  157. package/cliproxy/systemd/cliproxy-gc.service +13 -0
  158. package/cliproxy/systemd/cliproxy-gc.timer +8 -0
  159. package/cliproxy/systemd/cliproxy-monitor.service +8 -0
  160. package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
  161. package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
  162. package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
  163. package/cliproxy/vendor/LICENSE +22 -0
  164. package/cliproxy/vendor/config.upstream.yaml +1349 -0
  165. package/docs/consumer-setup.md +408 -0
  166. package/docs/declared.md +89 -0
  167. package/docs/machine-setup.md +399 -0
  168. package/docs/operations.md +276 -0
  169. package/docs/releasing.md +103 -0
  170. package/docs/state.md +254 -0
  171. package/docs/topology.md +67 -0
  172. package/docs/ubuntu-sync.md +351 -0
  173. package/docs/worker-contract.md +67 -0
  174. package/examples/fleet.json +100 -0
  175. package/examples/roster.json +13 -0
  176. package/llms.txt +35 -0
  177. package/mise.lock +1521 -0
  178. package/mise.toml +110 -0
  179. package/package.json +74 -4
  180. package/schema/fleet.schema.json +420 -0
  181. package/schema/roster.schema.json +77 -0
  182. package/src/agents/account-relay.ts +111 -0
  183. package/src/agents/accounts.ts +85 -0
  184. package/src/agents/audit.ts +77 -0
  185. package/src/agents/cli.ts +13 -0
  186. package/src/agents/command.ts +204 -0
  187. package/src/agents/converge.ts +123 -0
  188. package/src/agents/doctor.ts +107 -0
  189. package/src/agents/errors.ts +2 -0
  190. package/src/agents/files.ts +73 -0
  191. package/src/agents/fleet-verify.ts +165 -0
  192. package/src/agents/launch.ts +290 -0
  193. package/src/agents/native-verify.ts +371 -0
  194. package/src/agents/provider-errors.ts +57 -0
  195. package/src/agents/registry.ts +61 -0
  196. package/src/agents/render.ts +110 -0
  197. package/src/agents/round-trip.ts +192 -0
  198. package/src/agents/secrets.ts +13 -0
  199. package/src/agents/toolchain.ts +34 -0
  200. package/src/agents/verify-cache.ts +103 -0
  201. package/src/agents/verify.ts +127 -0
  202. package/src/auth-checks.ts +163 -0
  203. package/src/cli/agent-guide.ts +129 -0
  204. package/src/cli/app.ts +122 -0
  205. package/src/cli/cli.ts +28 -0
  206. package/src/cli/command-index.ts +57 -0
  207. package/src/cli/contracts.ts +25 -0
  208. package/src/cli/fleet-reference.ts +17 -0
  209. package/src/cli/fleet-state.ts +499 -0
  210. package/src/cli/fleet-sync-checkout.ts +130 -0
  211. package/src/cli/fleet-sync-enrollment.ts +140 -0
  212. package/src/cli/fleet-sync-forward.ts +134 -0
  213. package/src/cli/fleet-sync-manager.ts +249 -0
  214. package/src/cli/fleet-sync-monitoring.ts +185 -0
  215. package/src/cli/fleet-sync-output.ts +317 -0
  216. package/src/cli/fleet-sync-privilege.ts +27 -0
  217. package/src/cli/fleet-sync-progress.ts +109 -0
  218. package/src/cli/fleet-sync-remote.ts +242 -0
  219. package/src/cli/fleet-sync-report.ts +26 -0
  220. package/src/cli/fleet-sync-schedule.ts +138 -0
  221. package/src/cli/fleet-sync-state.ts +86 -0
  222. package/src/cli/fleet-sync-vault.ts +166 -0
  223. package/src/cli/fleet-sync.ts +375 -0
  224. package/src/cli/fleet.ts +165 -0
  225. package/src/cli/managed-health.ts +219 -0
  226. package/src/cli/node-deployment.ts +34 -0
  227. package/src/cli/node-sync.ts +121 -0
  228. package/src/cli/node-t3.ts +31 -0
  229. package/src/cli/options.ts +141 -0
  230. package/src/cli/probe-source.ts +7 -0
  231. package/src/cli/program.ts +185 -0
  232. package/src/cli/project.ts +51 -0
  233. package/src/cli/register-fleet.ts +296 -0
  234. package/src/cli/registry.ts +185 -0
  235. package/src/cli/repair-apply.ts +67 -0
  236. package/src/cli/repair-guard.ts +11 -0
  237. package/src/cli/repair-inspect.ts +150 -0
  238. package/src/cli/repair-script.ts +76 -0
  239. package/src/cli/repair.ts +209 -0
  240. package/src/cli/roster.ts +105 -0
  241. package/src/cli/search.ts +35 -0
  242. package/src/cli/settings.ts +67 -0
  243. package/src/cli/source-operations.ts +102 -0
  244. package/src/cli/t3/command.ts +90 -0
  245. package/src/cli/t3/register.ts +55 -0
  246. package/src/cli/tailnet.ts +127 -0
  247. package/src/cli/ui-actions.ts +133 -0
  248. package/src/cli/ui-controller.ts +113 -0
  249. package/src/cli/ui-data.ts +106 -0
  250. package/src/cli/ui-execute.ts +163 -0
  251. package/src/cli/ui-repairs.ts +34 -0
  252. package/src/cli/ui-reports.ts +211 -0
  253. package/src/cli/ui-targets.ts +100 -0
  254. package/src/cli/ui-terminal.ts +109 -0
  255. package/src/cli/ui-types.ts +49 -0
  256. package/src/cli/ui-wizard.ts +101 -0
  257. package/src/cli/ui.ts +9 -0
  258. package/src/cli/worker-health-output.ts +37 -0
  259. package/src/config.ts +186 -0
  260. package/src/configure.ts +255 -0
  261. package/src/definition.ts +290 -0
  262. package/src/diagnostics.ts +92 -0
  263. package/src/doctor-check.ts +10 -0
  264. package/src/errors.ts +1 -0
  265. package/src/fingerprint.ts +120 -0
  266. package/src/fleet-config.ts +174 -0
  267. package/src/input-hashes.ts +73 -0
  268. package/src/machine/local-source.ts +52 -0
  269. package/src/machine/lock-error.ts +2 -0
  270. package/src/machine/platform.ts +44 -0
  271. package/src/machine/source.ts +68 -0
  272. package/src/machine/state.ts +173 -0
  273. package/src/machine-health.ts +95 -0
  274. package/src/managed-source.ts +47 -0
  275. package/src/manager-failure.ts +48 -0
  276. package/src/manager-sync.ts +405 -0
  277. package/src/manager.ts +62 -0
  278. package/src/monitoring/agent-evidence.ts +146 -0
  279. package/src/monitoring/apply.ts +183 -0
  280. package/src/monitoring/beszel.ts +198 -0
  281. package/src/monitoring/enroll.ts +111 -0
  282. package/src/monitoring/hub-access.ts +114 -0
  283. package/src/monitoring/hub-backup.ts +121 -0
  284. package/src/monitoring/hub-viewer.ts +142 -0
  285. package/src/monitoring/hub.ts +93 -0
  286. package/src/monitoring/manager-agent.ts +73 -0
  287. package/src/monitoring/manager-hub.ts +248 -0
  288. package/src/monitoring/remote.ts +65 -0
  289. package/src/network-definition.ts +83 -0
  290. package/src/node-converge-linux.ts +374 -0
  291. package/src/node-converge-shared.ts +83 -0
  292. package/src/node-converge.ts +40 -0
  293. package/src/onboard.ts +35 -0
  294. package/src/package-info.ts +45 -0
  295. package/src/platform/ansible-receipt.ts +128 -0
  296. package/src/platform/ansible.ts +66 -0
  297. package/src/platform/darwin.ts +203 -0
  298. package/src/platform/detect.ts +11 -0
  299. package/src/platform/fallback.ts +110 -0
  300. package/src/platform/index.ts +38 -0
  301. package/src/platform/launchd.ts +98 -0
  302. package/src/platform/linux.ts +139 -0
  303. package/src/platform/ps.ts +45 -0
  304. package/src/platform/resource-verify.ts +132 -0
  305. package/src/platform/systemd-user.ts +95 -0
  306. package/src/platform/t3-policy.ts +137 -0
  307. package/src/platform/t3-route.ts +68 -0
  308. package/src/platform/t3-service.ts +75 -0
  309. package/src/platform/types.ts +123 -0
  310. package/src/platform/ubuntu-audit.ts +66 -0
  311. package/src/platform/ubuntu-verify.ts +105 -0
  312. package/src/platform/windows.ts +148 -0
  313. package/src/power-recovery.ts +55 -0
  314. package/src/probe.ts +51 -0
  315. package/src/protocol.ts +119 -0
  316. package/src/proxy-converge.ts +85 -0
  317. package/src/remote-bootstrap.ts +210 -0
  318. package/src/remote-entry.ts +89 -0
  319. package/src/remote-lock.ts +12 -0
  320. package/src/repair-catalog.ts +206 -0
  321. package/src/repair-plan.ts +78 -0
  322. package/src/repair-runner.ts +84 -0
  323. package/src/report.ts +71 -0
  324. package/src/runner.ts +104 -0
  325. package/src/secrets.ts +169 -0
  326. package/src/service-auth.ts +115 -0
  327. package/src/ssh.ts +135 -0
  328. package/src/state/bucket.ts +208 -0
  329. package/src/state/check.ts +74 -0
  330. package/src/state/connection.ts +92 -0
  331. package/src/state/enrollment.ts +45 -0
  332. package/src/state/legacy.ts +89 -0
  333. package/src/state/litestream.ts +143 -0
  334. package/src/state/operator.ts +20 -0
  335. package/src/state/paths.ts +40 -0
  336. package/src/state/roster.ts +62 -0
  337. package/src/state/store.ts +169 -0
  338. package/src/state/sync.ts +352 -0
  339. package/src/sync.ts +104 -0
  340. package/src/t3/accounts.ts +81 -0
  341. package/src/t3/desired.ts +165 -0
  342. package/src/t3/files.ts +81 -0
  343. package/src/t3/names-check.ts +53 -0
  344. package/src/t3/names.ts +155 -0
  345. package/src/t3/plan.ts +82 -0
  346. package/src/t3/providers.ts +74 -0
  347. package/src/t3/reconcile.ts +123 -0
  348. package/src/t3-endpoint.ts +104 -0
  349. package/src/t3-linked-check.ts +123 -0
  350. package/src/tailnet-health.ts +231 -0
  351. package/src/tailnet-matching.ts +37 -0
  352. package/src/tailscale.ts +141 -0
  353. package/src/toolchain-checks.ts +108 -0
  354. package/src/topology.ts +24 -0
  355. package/src/transport.ts +122 -0
  356. package/src/types.ts +99 -0
  357. package/src/vault-config.ts +38 -0
  358. package/src/worker-access-checks.ts +168 -0
  359. package/src/worker-capacity.ts +34 -0
  360. package/src/worker-host-checks.ts +181 -0
  361. package/src/worker-policy.ts +64 -0
  362. package/src/worker-readiness.ts +64 -0
  363. package/src/workstation-checks.ts +29 -0
@@ -0,0 +1,234 @@
1
+ import { workerTools } from './worker-tools';
2
+ /** Read-only toolchain checks; no logins, installations or changes to Docker. */
3
+ import { spawnSync } from 'node:child_process';
4
+ import { readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { homedir, tmpdir } from 'node:os';
7
+ import {
8
+ AGENT_WORKTREE_ROOTS,
9
+ globalConfigDir,
10
+ globalConfigStanding,
11
+ workspaceRoot,
12
+ type GlobalConfigStanding,
13
+ } from './global-config';
14
+
15
+ type Tool = string | { version: string; lazy?: boolean; os?: string[] };
16
+ type Config = {
17
+ tools: Record<string, Tool>;
18
+ vars?: { worker_tools?: unknown };
19
+ };
20
+ export type Check = {
21
+ id: string;
22
+ status: 'pass' | 'warn' | 'fail';
23
+ detail: string;
24
+ };
25
+ export type Runner = (command: string[]) => { ok: boolean; output: string };
26
+ const COMMANDS: Record<string, string> = {
27
+ 'npm:@yarnpkg/cli-dist': 'yarn',
28
+ 'npm:vercel': 'vercel',
29
+ 'npm:wrangler': 'wrangler',
30
+ 'aqua:anomalyco/opencode': 'opencode',
31
+ 'docker-cli': 'docker',
32
+ opentofu: 'tofu',
33
+ ripgrep: 'rg',
34
+ rust: 'rustc',
35
+ 'npm:@devcontainers/cli': 'devcontainer',
36
+ 'http:grok': 'grok',
37
+ 'http:antigravity': 'antigravity',
38
+ };
39
+ const VERSION_ARGS: Record<string, string[]> = {
40
+ go: ['version'],
41
+ gitleaks: ['version'],
42
+ kubectl: ['version', '--client=true', '--output=json'],
43
+ helm: ['version', '--short'],
44
+ };
45
+ const run: Runner = (command) => {
46
+ const result = spawnSync(command[0]!, command.slice(1), {
47
+ encoding: 'utf8',
48
+ timeout: 30_000,
49
+ windowsHide: true,
50
+ cwd: tmpdir(),
51
+ });
52
+ return {
53
+ ok: result.status === 0,
54
+ output: `${result.stdout ?? ''}${result.stderr ?? ''}`.trim(),
55
+ };
56
+ };
57
+
58
+ /** mise's `os` names for the platforms it installs a tool on. */
59
+ const MISE_OS: Record<string, string> = {
60
+ darwin: 'macos',
61
+ linux: 'linux',
62
+ win32: 'windows',
63
+ };
64
+ /** Installed on Windows by a native installer (native-aws.ts), not by mise. */
65
+ const NATIVE_WINDOWS = new Set(['aws']);
66
+
67
+ function unavailable(id: string, tool: Tool, platform: string): boolean {
68
+ if (typeof tool === 'string' || !tool.os) return false;
69
+ if (platform === 'win32' && NATIVE_WINDOWS.has(id)) return false;
70
+ return !tool.os.includes(MISE_OS[platform] ?? platform);
71
+ }
72
+
73
+ export function toolChecks(
74
+ tools: Record<string, Tool>,
75
+ execute: Runner = run,
76
+ platform: string = process.platform
77
+ ): Check[] {
78
+ const checks: Check[] = [];
79
+ for (const [id, tool] of Object.entries(tools)) {
80
+ if (typeof tool !== 'string' && tool.lazy) continue;
81
+ // A tool pinned only for other platforms has no artifact to verify here.
82
+ if (unavailable(id, tool, platform)) continue;
83
+ const version = typeof tool === 'string' ? tool : tool.version;
84
+ const command = COMMANDS[id] ?? id;
85
+ const result = execute([command, ...(VERSION_ARGS[id] ?? ['--version'])]);
86
+ const exact = new RegExp(
87
+ `(?:^|[^0-9])${version.replaceAll('.', '\\.')}($|(?![0-9]|\\.[0-9]))`
88
+ );
89
+ const matches = result.ok && exact.test(result.output);
90
+ checks.push({
91
+ id,
92
+ status: matches ? 'pass' : 'fail',
93
+ detail: matches
94
+ ? version
95
+ : `Expected ${command} ${version}; run dev-fleet tools sync and reopen the shell. Check for competing PATH entries.`,
96
+ });
97
+ }
98
+ for (const command of ['git', 'npm']) {
99
+ const result = execute([command, '--version']);
100
+ checks.push({
101
+ id: command,
102
+ status: result.ok ? 'pass' : 'fail',
103
+ detail: result.ok
104
+ ? result.output
105
+ : `Missing ${command}; rerun the OS bootstrap.`,
106
+ });
107
+ }
108
+ return checks;
109
+ }
110
+
111
+ export function dockerChecks(
112
+ required: boolean,
113
+ execute: Runner = run
114
+ ): Check[] {
115
+ return [
116
+ ['docker-engine', ['docker', 'info', '--format', '{{.ServerVersion}}']],
117
+ ['docker-compose', ['docker', 'compose', 'version']],
118
+ ['docker-buildx', ['docker', 'buildx', 'version']],
119
+ ].map(([id, command]) => {
120
+ const result = execute(command as string[]);
121
+ return {
122
+ id: id as string,
123
+ status: result.ok ? 'pass' : required ? 'fail' : 'warn',
124
+ detail: result.ok
125
+ ? result.output
126
+ : 'Install/start Docker Desktop or Docker Engine with Compose + Buildx. See docs/machine-setup.md.',
127
+ };
128
+ });
129
+ }
130
+
131
+ /** Agent worktree roots must be trusted, or every mise shim fails there. */
132
+ export function worktreeTrustCheck(
133
+ execute: Runner = run,
134
+ home: string = homedir()
135
+ ): Check {
136
+ const expand = (path: string) =>
137
+ path === '~' || path.startsWith('~/') ? home + path.slice(1) : path;
138
+ const result = execute(['mise', 'settings', 'get', 'trusted_config_paths']);
139
+ let trusted: string[] = [];
140
+ try {
141
+ const parsed: unknown = result.ok ? JSON.parse(result.output) : [];
142
+ if (Array.isArray(parsed))
143
+ trusted = parsed.filter((p): p is string => typeof p === 'string');
144
+ } catch {
145
+ trusted = [];
146
+ }
147
+ const have = new Set(trusted.map(expand));
148
+ const missing = AGENT_WORKTREE_ROOTS.filter(
149
+ (root) => !have.has(expand(root))
150
+ );
151
+ return {
152
+ id: 'agent-worktree-trust',
153
+ status: missing.length ? 'fail' : 'pass',
154
+ detail: missing.length
155
+ ? `mise does not trust ${missing.join(', ')}; agent CLIs fail in new worktrees there. Run dev-fleet tools sync.`
156
+ : AGENT_WORKTREE_ROOTS.join(', '),
157
+ };
158
+ }
159
+
160
+ const STANDING: Record<GlobalConfigStanding, string> = {
161
+ published: 'Published by dev-fleet from this revision.',
162
+ missing: 'There is no global mise configuration.',
163
+ replaced:
164
+ 'Another project or tool replaced the global mise configuration dev-fleet publishes.',
165
+ changed:
166
+ 'The global mise configuration is not the one this revision publishes.',
167
+ };
168
+
169
+ /** On a fleet machine the global mise configuration belongs to dev-fleet.
170
+ * When anything else replaced it, its pins and the trusted agent worktree
171
+ * roots are gone until the next fleet-sync.
172
+ */
173
+ export function globalConfigCheck(
174
+ dir?: string,
175
+ root: string = workspaceRoot
176
+ ): Check {
177
+ let standing: GlobalConfigStanding;
178
+ try {
179
+ standing = globalConfigStanding(dir ?? globalConfigDir(), root);
180
+ } catch {
181
+ return {
182
+ id: 'global-mise-config',
183
+ status: 'fail',
184
+ detail:
185
+ 'MISE_GLOBAL_CONFIG_FILE moves the global mise configuration away from the one dev-fleet publishes; unset it.',
186
+ };
187
+ }
188
+ return {
189
+ id: 'global-mise-config',
190
+ status: standing === 'published' ? 'pass' : 'fail',
191
+ detail:
192
+ standing === 'published'
193
+ ? STANDING[standing]
194
+ : `${STANDING[standing]} Run fleet-sync; projects pin their own tools in a project mise.toml and never publish a global one.`,
195
+ };
196
+ }
197
+
198
+ export function checkMachine(requireDocker = false, worker = false): Check[] {
199
+ const config = Bun.TOML.parse(
200
+ readFileSync(join(workspaceRoot, 'mise.toml'), 'utf8')
201
+ ) as Config;
202
+ const auth = run(['gh', 'auth', 'status']);
203
+ return [
204
+ ...toolChecks(
205
+ worker
206
+ ? workerTools(config.tools, config.vars?.worker_tools)
207
+ : config.tools
208
+ ),
209
+ ...dockerChecks(requireDocker),
210
+ worktreeTrustCheck(),
211
+ globalConfigCheck(),
212
+ {
213
+ id: 'github-auth',
214
+ status: auth.ok ? 'pass' : 'warn',
215
+ detail: auth.ok
216
+ ? 'Authenticated'
217
+ : 'Run gh auth login, then gh auth setup-git.',
218
+ },
219
+ ];
220
+ }
221
+
222
+ if (import.meta.main) {
223
+ const checks = checkMachine(
224
+ process.argv.includes('--docker'),
225
+ process.argv.includes('--worker')
226
+ );
227
+ const ok = checks.every((check) => check.status !== 'fail');
228
+ if (process.argv.includes('--json'))
229
+ console.log(JSON.stringify({ ok, checks }, null, 2));
230
+ else
231
+ for (const check of checks)
232
+ console.log(`${check.status.toUpperCase()} ${check.id}: ${check.detail}`);
233
+ process.exitCode = ok ? 0 : 1;
234
+ }
@@ -0,0 +1,103 @@
1
+ #!/bin/sh
2
+ # Every fleet-sync on the manager runs through this, whether started there or
3
+ # handed off from a worker over strict SSH. Runs fleet-sync from a clean,
4
+ # fleet-owned checkout of the exact approved revision, so the result never
5
+ # depends on the working tree it was started in (uncommitted edits, another
6
+ # branch, a removed worktree). Checkouts are reused when warm and pruned to the
7
+ # three most recent plus the one T3's providers launch from. Never touches the
8
+ # human's working tree.
9
+ set -eu
10
+ revision=$1
11
+ shift
12
+ case "$revision" in *[!a-f0-9]*|'') exit 2;; esac
13
+ [ "${#revision}" = 40 ] || exit 2
14
+ say() { printf 'fleet-sync (manager): %s\n' "$1" >&2; }
15
+ # A preview (--dry-run) changes nothing outside dev-fleet's own disposable
16
+ # caches: it never publishes the global mise configuration, never edits shell
17
+ # profiles and never prunes run checkouts. Only an apply does.
18
+ preview=false
19
+ for arg in "$@"; do [ "$arg" != --dry-run ] || preview=true; done
20
+ # mise is installed here by the toolchain step below; a login shell is not
21
+ # required to find it.
22
+ export PATH="$HOME/.local/bin:$PATH"
23
+ fleet="$HOME/.local/share/personal/fleet"
24
+ # The repository to take the revision from. Nobody keeps a clone of dev-fleet:
25
+ # fleet-sync names the one it prepared, and a hand-off (which names none) uses
26
+ # the manager's own copy of the source repository, or the checkout the devfleet
27
+ # launcher is linked into.
28
+ repo=${DEVFLEET_REPO:-}
29
+ if [ -z "$repo" ] && git -C "$fleet/manager/source" rev-parse --git-dir >/dev/null 2>&1; then
30
+ repo="$fleet/manager/source"
31
+ fi
32
+ if [ -z "$repo" ]; then
33
+ launcher=$(command -v devfleet) || { say 'this manager has no copy of the source yet; run fleet-sync once on the manager itself, from the project that holds your fleet.json.'; exit 3; }
34
+ repo=$(dirname "$(readlink -f "$launcher")")
35
+ fi
36
+ repo=$(git -C "$repo" rev-parse --show-toplevel) || { say 'cannot read the source repository on the manager; run fleet-sync once on the manager itself to fetch it again.'; exit 3; }
37
+ runs="$fleet/manager-runs"
38
+ dir="$runs/$revision"
39
+ ready() {
40
+ [ "$(git -C "$dir" rev-parse HEAD 2>/dev/null)" = "$revision" ] &&
41
+ [ -z "$(git -C "$dir" status --porcelain 2>/dev/null)" ]
42
+ }
43
+ if ! ready; then
44
+ git -C "$repo" cat-file -e "$revision^{commit}" 2>/dev/null || git -C "$repo" fetch --quiet origin || true
45
+ git -C "$repo" cat-file -e "$revision^{commit}" 2>/dev/null || git -C "$repo" fetch --quiet --no-tags origin "$revision" || true
46
+ git -C "$repo" cat-file -e "$revision^{commit}" 2>/dev/null || { say "revision $revision is neither on the manager nor on origin; push it first."; exit 4; }
47
+ mkdir -p "$runs"
48
+ chmod 700 "$runs"
49
+ # A fleet-owned, disposable run checkout: recreate it when stale or dirty.
50
+ if [ -e "$dir" ]; then git -C "$repo" worktree remove --force "$dir" 2>/dev/null || rm -rf -- "$dir"; fi
51
+ git -C "$repo" worktree prune
52
+ git -C "$repo" worktree add --quiet --detach "$dir" "$revision"
53
+ fi
54
+ touch "$dir"
55
+ cd "$dir"
56
+ mise trust --quiet "$dir" >/dev/null 2>&1 || true
57
+ # The manager runs the same locked toolchain (and so the same agent versions)
58
+ # as every worker: publish this revision's global config, install it and the
59
+ # frozen dependencies. Warm: the toolchain is skipped when the same inputs
60
+ # (tool pins, locks, setup code) were installed within a day, so a revision
61
+ # that changes none of them only installs its own dependencies.
62
+ # Stamp and log sit beside the checkout: inside it they would make it dirty.
63
+ stamp="$runs/toolchain-ready"
64
+ log="$runs/$revision.setup.log"
65
+ 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' ' ')
66
+ [ -n "$key" ] || key=$revision
67
+ failed() {
68
+ tail -n 20 "$log" >&2
69
+ say "toolchain or dependency install failed in the run checkout; full log: $log"
70
+ exit 5
71
+ }
72
+ if [ "$preview" = true ]; then
73
+ # The revision's own locked Bun is enough to preview; mise keeps it beside
74
+ # its other versions without making it the global one.
75
+ command -v mise >/dev/null 2>&1 || { say 'this manager has no toolchain yet, so a preview cannot run; fleet-sync without --dry-run installs it.'; exit 6; }
76
+ mise install --locked bun >"$log" 2>&1 || failed
77
+ elif [ "$(cat "$stamp" 2>/dev/null)" != "$key" ] || [ -z "$(find "$stamp" -mmin -1440 2>/dev/null)" ]; then
78
+ rm -f -- "$stamp"
79
+ # The toolchain bootstrap installs the pinned mise first when the manager
80
+ # has none (or another version), so Git is all a new manager needs.
81
+ bash bootstrap/bootstrap.sh --tools-only >"$log" 2>&1 || failed
82
+ printf '%s' "$key" >"$stamp"
83
+ fi
84
+ if [ ! -d node_modules ]; then
85
+ mise exec --locked bun -- bun install --frozen-lockfile --silent >>"$log" 2>&1 || failed
86
+ fi
87
+ # Keep the three most recent run checkouts (names are commit SHAs) and the
88
+ # applied one: fleet/current points at it and T3's providers launch from there.
89
+ # A preview leaves them for the next apply to prune.
90
+ if [ "$preview" = false ]; then
91
+ applied=$(basename "$(readlink "$(dirname "$runs")/current" 2>/dev/null || true)")
92
+ # shellcheck disable=SC2012
93
+ for old in $(ls -1td "$runs"/*/ | tail -n +4); do
94
+ [ "$(basename "$old")" != "$applied" ] || continue
95
+ # A checkout made from another repository (an earlier clone) is not this
96
+ # repository's worktree: remove the directory itself.
97
+ git -C "$repo" worktree remove --force "$old" 2>/dev/null || rm -rf -- "$old"
98
+ rm -f -- "${old%/}.setup.log"
99
+ done
100
+ git -C "$repo" worktree prune
101
+ fi
102
+ export PERSONAL_WORKSPACE="$dir" PERSONAL_FLEET_RUN_CHECKOUT="$revision"
103
+ exec mise exec --locked bun -- bun src/cli/cli.ts fleet-sync --revision "$revision" "$@"
@@ -0,0 +1,258 @@
1
+ #!/bin/sh
2
+ # Executed over strict SSH from the selected verified Git revision. stdin is
3
+ # one line holding the base64 fleet configuration (empty when there is none),
4
+ # then the base64 Git bundle (nothing in cached mode). All executable content
5
+ # remains in this tracked source.
6
+ set -eu
7
+ umask 077
8
+ revision=$1
9
+ bundle_sha=$2
10
+ node=$3
11
+ source_mode=${4:-transfer}
12
+ case "$source_mode" in cached|transfer) ;; *) exit 2;; esac
13
+ case "$revision" in *[!a-f0-9]*|'') exit 2;; esac
14
+ [ "${#revision}" = 40 ] || exit 2
15
+ case "$bundle_sha" in *[!a-f0-9]*|'') exit 2;; esac
16
+ [ "${#bundle_sha}" = 64 ] || exit 2
17
+ case "$node" in *[!a-z0-9-]*|'') exit 2;; esac
18
+ phase=bootstrap
19
+ bundle=''
20
+ code=0
21
+ apt_log=''
22
+ failure_code=REMOTE_BOOTSTRAP_INCOMPLETE
23
+ die() { failure_code=$1; exit 1; }
24
+ # One remedy per failure: what the operator does next. Never raw output.
25
+ remedy() {
26
+ case "$1" in
27
+ FLEET_REMOTE_LOCKED) echo 'Another fleet operation holds this machine. Wait for it to finish; rerun fleet-sync afterwards. Never remove the lock while its holder runs.';;
28
+ ROOT_DEVELOPMENT_USER_REJECTED) echo 'Connect as the non-root developer account the fleet configuration names.';;
29
+ UBUNTU_BASELINE_MISMATCH|UBUNTU_ARCHITECTURE_UNSUPPORTED|MACOS_BASELINE_MISMATCH|MACOS_ARCHITECTURE_UNSUPPORTED|PLATFORM_UNSUPPORTED) echo 'Workers run Ubuntu 26.04 on x86_64 or aarch64. Reinstall the machine; fleet-sync never upgrades an operating system.';;
30
+ FLEET_STATE_PATH_UNSAFE) echo 'A fleet state directory under ~/.local/share/personal is a symbolic link. Replace it with a real directory.';;
31
+ BOOTSTRAP_CAPACITY_UNVERIFIED|BOOTSTRAP_DISK_CAPACITY_REQUIRED) echo 'Free at least 2 GiB and 50000 inodes on the home file system, then rerun fleet-sync.';;
32
+ SOURCE_DIGEST_MISMATCH) echo 'The bundle changed in transit. Rerun fleet-sync; if it repeats, check the SSH route.';;
33
+ SUDO_AUTHORIZATION_REQUIRED) echo 'Grant the developer account passwordless sudo, or run fleet-sync interactively once to authorize it.';;
34
+ APT_LOCK_CONTENDED) echo 'Another package operation held the APT lock past the bounded wait. Let it finish (it is never killed), then rerun fleet-sync.';;
35
+ APT_DNS_UNAVAILABLE) echo 'The worker cannot resolve the Ubuntu archive. Restore its DNS, then rerun fleet-sync.';;
36
+ APT_REPOSITORY_UNAVAILABLE) echo 'The Ubuntu archive could not be reached. Check the network or archive status, then rerun fleet-sync.';;
37
+ APT_DISK_FULL) echo 'Package installation ran out of disk space. Free space under /var and /usr, then rerun fleet-sync.';;
38
+ DPKG_INTERRUPTED) echo 'An earlier package installation that fleet-sync did not start was interrupted. Run sudo dpkg --configure -a once no package process is running, then rerun fleet-sync.';;
39
+ APT_UPDATE_FAILED|APT_INSTALL_FAILED) echo 'Run sudo apt-get update and install the bootstrap packages by hand to see the error, then rerun fleet-sync.';;
40
+ XCODE_COMMAND_LINE_TOOLS_REQUIRED) echo 'Install the Xcode Command Line Tools (xcode-select --install), then rerun fleet-sync.';;
41
+ BOOTSTRAP_LOCK_TOOL_REQUIRED) echo 'Install python3 so the operation lock can be taken, then rerun fleet-sync.';;
42
+ SOURCE_CACHE_CHANGED|SOURCE_STATUS_FAILED|SOURCE_CHECKOUT_CONFLICT) echo 'The fleet-owned checkout of this revision holds changes fleet-sync did not make. Inspect and remove that revisions directory, then rerun fleet-sync.';;
43
+ CONFIGURATION_INSTALL_FAILED) echo 'The fleet configuration could not be written under ~/.config/dev-fleet. Check that directory, then rerun fleet-sync.';;
44
+ TOOLCHAIN_*) echo 'The pinned toolchain did not converge. Rerun fleet-sync; it resumes where it stopped. If it repeats, run devfleet doctor on the worker.';;
45
+ *) echo 'Inspect the remote operation lock and rerun the approved revision. No success receipt advanced.';;
46
+ esac
47
+ }
48
+ # Names an APT failure by the fixed messages of the pinned Ubuntu release.
49
+ apt_failure() {
50
+ if grep -q 'Could not get lock' "$apt_log" 2>/dev/null; then die APT_LOCK_CONTENDED
51
+ elif grep -q 'No space left on device' "$apt_log" 2>/dev/null; then die APT_DISK_FULL
52
+ elif grep -qE 'Temporary failure resolving|Could not resolve' "$apt_log" 2>/dev/null; then die APT_DNS_UNAVAILABLE
53
+ elif grep -qE 'Failed to fetch|Unable to connect|Connection failed|Unable to locate package' "$apt_log" 2>/dev/null; then die APT_REPOSITORY_UNAVAILABLE
54
+ elif grep -q 'dpkg was interrupted' "$apt_log" 2>/dev/null; then die DPKG_INTERRUPTED
55
+ else die "$1"
56
+ fi
57
+ }
58
+ fail() {
59
+ printf '{"version":1,"checks":[{"id":"bootstrap:%s","status":"fail","summary":"%s","remedy":{"action":"%s"}}]}\n' "$phase" "$failure_code" "$(remedy "$failure_code")"
60
+ }
61
+ trap 'code=$?; [ -z "$bundle" ] || rm -f -- "$bundle"; [ -z "$apt_log" ] || rm -f -- "$apt_log"; if [ "$code" != 0 ]; then fail; fi' EXIT
62
+ # Preserve the lock holder if its SSH channel disappears mid-install.
63
+ trap '' HUP
64
+ trap 'exit 1' INT TERM
65
+ # No shell startup assumptions, and no implicit root development user.
66
+ [ "$(id -u)" != 0 ] || die ROOT_DEVELOPMENT_USER_REJECTED
67
+ # Ubuntu 26.04 is the accepted worker platform; macOS 14+ follows the same
68
+ # phases best effort with launchd instead of systemd.
69
+ platform=$(uname -s)
70
+ case "$platform" in
71
+ Linux)
72
+ # shellcheck source=/dev/null
73
+ . /etc/os-release
74
+ [ "$ID" = ubuntu ] && [ "$VERSION_ID" = 26.04 ] || die UBUNTU_BASELINE_MISMATCH
75
+ case "$(uname -m)" in x86_64|aarch64) ;; *) die UBUNTU_ARCHITECTURE_UNSUPPORTED;; esac ;;
76
+ Darwin)
77
+ major=$(sw_vers -productVersion | cut -d. -f1)
78
+ case "$major" in ''|*[!0-9]*) die MACOS_BASELINE_MISMATCH;; esac
79
+ [ "$major" -ge 14 ] || die MACOS_BASELINE_MISMATCH
80
+ case "$(uname -m)" in arm64|x86_64) ;; *) die MACOS_ARCHITECTURE_UNSUPPORTED;; esac ;;
81
+ *) die PLATFORM_UNSUPPORTED;;
82
+ esac
83
+ root="$HOME/.local/share/personal/fleet"
84
+ # Refuse symlinked state roots rather than allowing writes through them.
85
+ for path in "$HOME/.local" "$HOME/.local/share" "$HOME/.local/share/personal" "$root"; do
86
+ [ ! -L "$path" ] || die FLEET_STATE_PATH_UNSAFE
87
+ done
88
+ mkdir -p "$root"
89
+ phase=lock
90
+ [ ! -L "$root/remote-operation.lock" ] || exit 1
91
+ exec 9>"$root/remote-operation.lock"
92
+ # macOS has no flock(1); lock the same descriptor through fcntl. The lock
93
+ # follows this shell's open descriptor and is released when it exits.
94
+ if command -v flock >/dev/null 2>&1; then
95
+ flock -n 9 || die FLEET_REMOTE_LOCKED
96
+ else
97
+ command -v python3 >/dev/null 2>&1 || die BOOTSTRAP_LOCK_TOOL_REQUIRED
98
+ python3 -c 'import fcntl; fcntl.flock(9, fcntl.LOCK_EX | fcntl.LOCK_NB)' 9>&9 || die FLEET_REMOTE_LOCKED
99
+ fi
100
+ printf '{"version":1,"pid":%s,"revision":"%s","node":"%s"}\n' "$$" "$revision" "$node" >"$root/remote-operation.json"
101
+ # The fleet configuration is installed under the lock, so two overlapping
102
+ # runs never write it at once. It holds no secret.
103
+ phase='configuration-install'
104
+ IFS= read -r configuration || configuration=''
105
+ if [ -n "$configuration" ]; then
106
+ config_dir="$HOME/.config/dev-fleet"
107
+ mkdir -p "$config_dir" || die CONFIGURATION_INSTALL_FAILED
108
+ printf '%s' "$configuration" | base64 -d >"$config_dir/fleet.json.new" || die CONFIGURATION_INSTALL_FAILED
109
+ mv "$config_dir/fleet.json.new" "$config_dir/fleet.json" || die CONFIGURATION_INSTALL_FAILED
110
+ fi
111
+ phase=resources
112
+ # A bootstrap floor protects source transfer and minimal prerequisites. The
113
+ # Workstation baseline subsequently enforces its stricter declared capacity.
114
+ free_kib=$(df -Pk "$root" | awk 'END {print $4}')
115
+ free_inodes=$(df -Pi "$root" | awk 'END {print $4}')
116
+ case "$free_kib:$free_inodes" in *[!0-9:]*|:*|*:) die BOOTSTRAP_CAPACITY_UNVERIFIED;; esac
117
+ [ "$free_kib" -ge 2097152 ] && [ "$free_inodes" -ge 50000 ] || die BOOTSTRAP_DISK_CAPACITY_REQUIRED
118
+ phase=source-transfer
119
+ if [ "$source_mode" = transfer ]; then
120
+ bundle=$(mktemp "$root/source.XXXXXX")
121
+ # Leave no executable payload copies; this temporary file is only Git data.
122
+ base64 -d >"$bundle"
123
+ if command -v sha256sum >/dev/null 2>&1; then
124
+ printf '%s %s\n' "$bundle_sha" "$bundle" | sha256sum -c - >/dev/null 2>&1 || die SOURCE_DIGEST_MISMATCH
125
+ else
126
+ [ "$(shasum -a 256 "$bundle" | cut -d' ' -f1)" = "$bundle_sha" ] || die SOURCE_DIGEST_MISMATCH
127
+ fi
128
+ fi
129
+ phase=prerequisites
130
+ if [ "$platform" = Linux ]; then
131
+ missing=''
132
+ for package in git curl ca-certificates unzip xz-utils build-essential python3 ansible-core; do
133
+ dpkg-query -W -f='${db:Status-Status}' "$package" 2>/dev/null | grep -qx installed || missing="$missing $package"
134
+ done
135
+ if [ -n "$missing" ]; then
136
+ sudo -n true >/dev/null 2>&1 || die SUDO_AUTHORIZATION_REQUIRED
137
+ # APT's own messages, in a private file read only for fixed patterns and
138
+ # removed on exit: they tell a held lock, DNS, an unreachable archive and a
139
+ # full disk apart. Nothing of them is reported.
140
+ apt_log=$(mktemp "$root/apt.XXXXXX")
141
+ pending="$root/prerequisites-pending"
142
+ # An installation this script started and that was interrupted (the shell
143
+ # killed, the machine powered off) leaves dpkg half-configured. Finishing
144
+ # our own transaction is safe; one fleet-sync did not start is reported.
145
+ # dpkg reads its database lock even to audit, which needs root.
146
+ if [ -n "$(sudo -n dpkg --audit 2>/dev/null)" ]; then
147
+ [ -f "$pending" ] || die DPKG_INTERRUPTED
148
+ # shellcheck disable=SC2024
149
+ sudo -n timeout 600 env DEBIAN_FRONTEND=noninteractive LC_ALL=C dpkg --configure -a >"$apt_log" 2>&1 || apt_failure APT_INSTALL_FAILED
150
+ fi
151
+ : >"$pending"
152
+ # The log belongs to the developer account; sudo only runs the command.
153
+ # shellcheck disable=SC2024
154
+ # Never remove APT locks or kill package processes. Timeout keeps genuine
155
+ # transactions bounded; Ubuntu archive signatures/TLS remain enabled.
156
+ sudo -n timeout 300 env LC_ALL=C apt-get -o DPkg::Lock::Timeout=120 update >"$apt_log" 2>&1 || apt_failure APT_UPDATE_FAILED
157
+ # shellcheck disable=SC2086,SC2024
158
+ sudo -n timeout 600 env DEBIAN_FRONTEND=noninteractive LC_ALL=C apt-get -o DPkg::Lock::Timeout=120 --no-install-recommends install -y $missing >>"$apt_log" 2>&1 || apt_failure APT_INSTALL_FAILED
159
+ rm -f -- "$pending" "$apt_log"
160
+ apt_log=''
161
+ fi
162
+ else
163
+ # Apple Command Line Tools provide git and python3; Ansible runs through the
164
+ # pinned uv tool runner installed by the shared mise toolchain below.
165
+ xcode-select -p >/dev/null 2>&1 || die XCODE_COMMAND_LINE_TOOLS_REQUIRED
166
+ command -v git >/dev/null 2>&1 || die XCODE_COMMAND_LINE_TOOLS_REQUIRED
167
+ fi
168
+ phase=source-verification
169
+ deployment="$root/revisions/$revision"
170
+ [ ! -L "$root/revisions" ] && [ ! -L "$deployment" ] || exit 1
171
+ mkdir -p "$deployment"
172
+ if [ ! -e "$deployment/.git" ]; then
173
+ [ -z "$(ls -A "$deployment")" ] || exit 1
174
+ git -C "$deployment" init -q >/dev/null 2>&1 || exit 1
175
+ fi
176
+ # An interrupted initial fetch or checkout leaves a repository without a
177
+ # commit, possibly with some of the revision's files written. Resume only
178
+ # that state: every file present must be a file of the revision, so the
179
+ # checkout may write over it. An existing commit or other work is never reset.
180
+ if ! git -C "$deployment" rev-parse --verify HEAD >/dev/null 2>&1; then
181
+ [ "$source_mode" = transfer ] || die SOURCE_CACHE_CHANGED
182
+ [ "$(git -C "$deployment" rev-parse --show-toplevel)" = "$deployment" ] || exit 1
183
+ # The bundle holds this commit without its history: declare the checkout
184
+ # shallow at it so Git accepts a commit whose parents are absent.
185
+ printf '%s\n' "$revision" >"$deployment/.git/shallow"
186
+ git -C "$deployment" fetch --no-tags "$bundle" "$revision" >/dev/null 2>&1 || exit 1
187
+ present=$(mktemp "$root/present.XXXXXX")
188
+ expected=$(mktemp "$root/expected.XXXXXX")
189
+ git -C "$deployment" -c core.quotePath=false ls-files --others | LC_ALL=C sort >"$present" || { rm -f -- "$present" "$expected"; die SOURCE_STATUS_FAILED; }
190
+ git -C "$deployment" -c core.quotePath=false ls-tree -r --name-only "$revision" | LC_ALL=C sort >"$expected" || { rm -f -- "$present" "$expected"; die SOURCE_STATUS_FAILED; }
191
+ foreign=$(LC_ALL=C comm -23 "$present" "$expected")
192
+ rm -f -- "$present" "$expected"
193
+ [ -z "$foreign" ] || die SOURCE_CHECKOUT_CONFLICT
194
+ git -C "$deployment" checkout --force --detach "$revision" >/dev/null 2>&1 || exit 1
195
+ fi
196
+ [ -z "$bundle" ] || rm -f "$bundle"
197
+ bundle=''
198
+ [ "$(git -C "$deployment" rev-parse HEAD)" = "$revision" ] || exit 1
199
+ [ "$(git -C "$deployment" rev-parse --show-toplevel)" = "$deployment" ] || exit 1
200
+ clean=$(git -C "$deployment" status --porcelain) || die SOURCE_STATUS_FAILED
201
+ [ -z "$clean" ] || die SOURCE_CHECKOUT_CONFLICT
202
+ phase=toolchain
203
+ cd "$deployment"
204
+ export PATH="$HOME/.local/bin:$HOME/.local/share/mise/shims:/usr/local/bin:/usr/bin:/bin"
205
+ status_file="$root/toolchain-status.json"
206
+ ready_file="$root/toolchain-ready"
207
+ [ ! -L "$status_file" ] && [ ! -L "$ready_file" ] || die FLEET_STATE_PATH_UNSAFE
208
+ # Warm: the same toolchain inputs (tool pins, locks and the setup code itself)
209
+ # passed within a day, so a revision that changes none of them skips the full
210
+ # setup. Changed inputs, a failure or an older stamp run it; agent versions
211
+ # are still checked (and reinstalled on drift) by node convergence.
212
+ 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' ' ')
213
+ [ -n "$toolchain_key" ] || toolchain_key=$revision
214
+ if [ "$(cat "$ready_file" 2>/dev/null)" = "$toolchain_key" ] && [ -n "$(find "$ready_file" -mmin -1440 2>/dev/null)" ]; then
215
+ toolchain_warm=1
216
+ else
217
+ toolchain_warm=''
218
+ rm -f -- "$status_file" "$ready_file"
219
+ fi
220
+ if command -v timeout >/dev/null 2>&1; then bounded='timeout 1800'; else bounded=''; fi
221
+ # shellcheck disable=SC2086
222
+ if [ -z "$toolchain_warm" ] && ! $bounded bash bootstrap/bootstrap.sh --worker --non-interactive --status-file "$status_file" >/dev/null 2>&1; then
223
+ tool_phase=$(sed -n 's/.*"phase":"\([a-z0-9-]*\)".*/\1/p' "$status_file" 2>/dev/null) || tool_phase=''
224
+ case "$tool_phase" in
225
+ managed-config) die TOOLCHAIN_CONFIG_CONFLICT;;
226
+ mise) die TOOLCHAIN_MISE_FAILED;;
227
+ aws) die TOOLCHAIN_AWS_FAILED;;
228
+ t3) die TOOLCHAIN_T3_FAILED;;
229
+ global-config|shell) die TOOLCHAIN_PATH_CONFIGURATION_FAILED;;
230
+ dependencies) die TOOLCHAIN_FROZEN_DEPENDENCIES_FAILED;;
231
+ *) die TOOLCHAIN_CONVERGENCE_FAILED;;
232
+ esac
233
+ fi
234
+ # A new revision's checkout still needs its own (cached, frozen) dependencies.
235
+ if [ -n "$toolchain_warm" ] && [ ! -d node_modules ]; then
236
+ # shellcheck disable=SC2086
237
+ $bounded mise exec --locked bun -- bun install --frozen-lockfile >/dev/null 2>&1 || die TOOLCHAIN_FROZEN_DEPENDENCIES_FAILED
238
+ fi
239
+ [ -n "$toolchain_warm" ] || printf '%s' "$toolchain_key" >"$ready_file"
240
+ # Every synced revision is a full checkout with its own dependencies (about
241
+ # 1 GB). Keep this one, the one T3's providers launch from and the two most
242
+ # recently used others; the rest would slowly fill the disk.
243
+ phase=cleanup
244
+ touch "$deployment"
245
+ applied=$(basename "$(readlink "$root/current" 2>/dev/null || true)")
246
+ kept=0
247
+ # shellcheck disable=SC2012
248
+ for old in $(ls -1t "$root/revisions"); do
249
+ case "$old" in *[!a-f0-9]*) continue;; esac
250
+ [ "${#old}" = 40 ] || continue
251
+ [ "$old" != "$revision" ] && [ "$old" != "$applied" ] || continue
252
+ kept=$((kept + 1))
253
+ [ "$kept" -gt 2 ] || continue
254
+ rm -rf -- "${root:?}/revisions/$old" || true
255
+ done
256
+ phase=configuration
257
+ trap - EXIT INT TERM
258
+ exec mise exec --locked bun -- bun src/node-converge.ts "$node" "$revision"