@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,375 @@
1
+ import { localCheckoutRevision } from '../machine/local-source';
2
+ import {
3
+ definitionSource,
4
+ fleet as canonicalDefinition,
5
+ reloadFleet,
6
+ } from '../fleet-config';
7
+ import { managerFailure } from '../manager-failure';
8
+ import { managerText, stdoutColor } from './fleet-sync-output';
9
+ import { managerSchedule, type ScheduleAction } from './fleet-sync-schedule';
10
+ import { enrollManager } from './fleet-sync-enrollment';
11
+ import { authorizeManager } from './fleet-sync-vault';
12
+ import { homedir } from 'node:os';
13
+ import { join } from 'node:path';
14
+ import { existsSync, renameSync } from 'node:fs';
15
+ import {
16
+ isRevision,
17
+ withMachineLock,
18
+ ensurePrivateRoot,
19
+ type Enrollment,
20
+ } from '../machine/state';
21
+ import { loadEnrollment } from '../state/enrollment';
22
+ import { legacyHome } from '../state/paths';
23
+ import {
24
+ DIVERGED_REMEDY,
25
+ refresh,
26
+ stateContext,
27
+ type Standing,
28
+ } from '../state/sync';
29
+ import { runRecord } from './fleet-sync-state';
30
+ import { runCommand } from '../runner';
31
+ import type { Runner } from '../types';
32
+ import type { Settings } from './settings';
33
+ import type { CommandOutcome } from './contracts';
34
+ import { FleetUsageError } from '../errors';
35
+ import { FleetLockError } from '../machine/lock-error';
36
+ import { selected } from './fleet-sync-remote';
37
+ import { managerIdentity } from './fleet-sync-manager';
38
+ import { managerRunScript, rehome } from './fleet-sync-checkout';
39
+ import { packageInfo } from '../package-info';
40
+ import {
41
+ forwardingDisabled,
42
+ forwardToManager,
43
+ type ForwardSpawn,
44
+ } from './fleet-sync-forward';
45
+
46
+ export interface FleetSyncOptions {
47
+ revision?: string;
48
+ dryRun?: boolean;
49
+ apply?: boolean;
50
+ yes?: boolean;
51
+ json?: boolean;
52
+ nonInteractive?: boolean;
53
+ node?: string[];
54
+ all?: boolean;
55
+ schedule?: ScheduleAction;
56
+ /** Internal: this revision already converged the whole fleet, so no canary
57
+ * has to prove it first and every worker converges at once.
58
+ */
59
+ rolledOut?: boolean;
60
+ /** Internal: set for interactive terminal runs (sudo enrollment prompts). */
61
+ interactiveAuthorization?: boolean;
62
+ expectedPlanFingerprint?: string;
63
+ }
64
+ export function machineRoot(): string {
65
+ const root = join(homedir(), '.local/share/personal/fleet/manager');
66
+ // One-time move from the pre-topology name; keeps the approved revision.
67
+ const legacy = join(homedir(), '.local/share/personal/fleet/controller');
68
+ if (!existsSync(root) && existsSync(legacy)) renameSync(legacy, root);
69
+ return root;
70
+ }
71
+
72
+ /** Public contract v3: the manager converges itself, then every enrolled worker. */
73
+ export async function fleetSync(
74
+ options: FleetSyncOptions,
75
+ settings: Settings,
76
+ run: Runner = runCommand,
77
+ forward?: ForwardSpawn
78
+ ): Promise<CommandOutcome> {
79
+ validate(options);
80
+ if (!(await onManager(run)))
81
+ return offManager(options, settings, run, forward);
82
+ if (options.schedule === 'status') return scheduleStatus(settings, run);
83
+ return managerSync(options, settings, {
84
+ run,
85
+ root: machineRoot(),
86
+ restart: () => fleetSync(options, settings, run, forward),
87
+ });
88
+ }
89
+
90
+ export interface ManagerSync {
91
+ run: Runner;
92
+ /** This machine's run lock and caches. */
93
+ root: string;
94
+ /** Starts the request over once the fleet state has been replaced. */
95
+ restart: () => Promise<CommandOutcome>;
96
+ }
97
+
98
+ /** fleet-sync on the machine already known to be the manager. */
99
+ export async function managerSync(
100
+ options: FleetSyncOptions,
101
+ settings: Settings,
102
+ { run, root, restart }: ManagerSync
103
+ ): Promise<CommandOutcome> {
104
+ await ensurePrivateRoot(root);
105
+ const state = await loadEnrollment(legacyHome() ? root : null);
106
+ const revision = await selectedRevision(
107
+ options.revision,
108
+ state?.approvedRevision,
109
+ settings,
110
+ run
111
+ );
112
+ if (!isRevision(revision))
113
+ return actionRequired(
114
+ 'FLEET_REVISION_REQUIRED',
115
+ 'No revision to converge to. Run the published dev-fleet package (it names the commit it was built from), or select a full commit SHA with --revision.'
116
+ );
117
+ if (options.schedule)
118
+ return scheduleRequest(options, revision, settings, run);
119
+ const rehomed = await rehome(
120
+ options,
121
+ { revision, workspace: settings.workspace, root },
122
+ run
123
+ );
124
+ if (rehomed === 'unavailable')
125
+ return actionRequired(
126
+ 'FLEET_SOURCE_UNAVAILABLE',
127
+ `Revision ${revision} could not be taken from ${canonicalDefinition.source.repository}, so nothing was changed. Check the network and Git access to it, and that the revision is pushed there; fleet-sync never converges from the package or working tree it was started in.`,
128
+ revision
129
+ );
130
+ if (rehomed !== 'here') return rehomed;
131
+ if (options.dryRun) return selected({ ...options, revision }, settings, run);
132
+ // The bucket is the source of truth: a run starts from its version of the
133
+ // configuration, the roster and the approval, not from a stale local copy.
134
+ const fresh = await refresh(stateContext(run));
135
+ if (fresh.standing === 'diverged')
136
+ return actionRequired('FLEET_STATE_DIVERGED', DIVERGED_REMEDY, revision);
137
+ if (fresh.replaced) {
138
+ // Everything read so far came from the stale copy, the manager's own
139
+ // identity included.
140
+ reloadFleet();
141
+ return restart();
142
+ }
143
+ return authorizeAndApply(root, state, options, {
144
+ settings,
145
+ run,
146
+ revision,
147
+ standing: fresh.standing,
148
+ });
149
+ }
150
+
151
+ interface ApplyContext {
152
+ settings: Settings;
153
+ run: Runner;
154
+ revision: string;
155
+ /** How the fleet state stood with the bucket when this run started. */
156
+ standing: Standing;
157
+ }
158
+
159
+ /** Applies without a confirmation prompt; --dry-run is the preview. */
160
+ async function authorizeAndApply(
161
+ root: string,
162
+ state: Enrollment | null,
163
+ options: FleetSyncOptions,
164
+ context: ApplyContext
165
+ ): Promise<CommandOutcome> {
166
+ const { settings, run, revision } = context;
167
+ const request = { ...options, revision, apply: true, yes: true };
168
+ if (!interactive(options)) return applyLocked(root, state, request, context);
169
+ await enrollManager(settings.config, run);
170
+ // Needed to hand each node its own Vault token; nodes report any gap.
171
+ await authorizeManager({ interactive: true }, run);
172
+ return applyLocked(
173
+ root,
174
+ state,
175
+ { ...request, interactiveAuthorization: true },
176
+ context
177
+ );
178
+ }
179
+
180
+ /** Records the approved revision, converges, records what was applied, then
181
+ * uploads the fleet state: the report says whether the bucket holds it.
182
+ */
183
+ function applyLocked(
184
+ root: string,
185
+ state: Enrollment | null,
186
+ request: FleetSyncOptions & { revision: string },
187
+ context: ApplyContext
188
+ ): Promise<CommandOutcome> {
189
+ const { settings, run, revision } = context;
190
+ const record = runRecord(state, revision, {
191
+ context: stateContext(run),
192
+ standing: context.standing,
193
+ ...(definitionSource().kind === 'file'
194
+ ? { definition: JSON.stringify(canonicalDefinition, null, 2) + '\n' }
195
+ : {}),
196
+ });
197
+ return withMachineLock(root, async () => {
198
+ record.approve();
199
+ try {
200
+ return await selected(
201
+ { ...request, rolledOut: state?.appliedRevision === revision },
202
+ settings,
203
+ run,
204
+ (report) => record.settle(report.ok)
205
+ );
206
+ } finally {
207
+ await record.abandon();
208
+ }
209
+ }).catch((error: unknown) => {
210
+ if (error instanceof FleetLockError)
211
+ return actionRequired(
212
+ 'FLEET_MANAGER_LOCKED',
213
+ 'Another manager run or interrupted lock exists. Inspect manager and remote processes before removing an abandoned sync.lock; never steal active package-manager locks.'
214
+ );
215
+ throw error;
216
+ });
217
+ }
218
+ /** Every run (previews and schedules too) happens on the manager: a worker can
219
+ * neither converge the manager's singletons nor observe its T3 links, so a
220
+ * partial run there only produces misleading failures.
221
+ */
222
+ async function onManager(run: Runner): Promise<boolean> {
223
+ const { manager } = canonicalDefinition;
224
+ return (await managerIdentity(manager.id, run)).status === 'pass';
225
+ }
226
+
227
+ /** Hands the request to the manager; refuses (without touching any worker)
228
+ * when hand-off is disabled, e.g. on the manager side of a hand-off.
229
+ */
230
+ async function offManager(
231
+ options: FleetSyncOptions,
232
+ settings: Settings,
233
+ run: Runner,
234
+ forward: ForwardSpawn | undefined
235
+ ): Promise<CommandOutcome> {
236
+ const definition = canonicalDefinition;
237
+ const revision =
238
+ options.revision ??
239
+ packageRevision() ??
240
+ (await localCheckoutRevision(settings.workspace, run));
241
+ if (forwardingDisabled())
242
+ return actionRequired(
243
+ 'FLEET_NOT_MANAGER',
244
+ `fleet-sync runs only on the manager (${definition.manager.id}); nothing was changed. Run it there, or from a worker that can SSH to the manager.`,
245
+ revision ?? null
246
+ );
247
+ const runScript = revision
248
+ ? await managerRunScript(settings.workspace, revision, run)
249
+ : null;
250
+ return forwardToManager(definition, options, revision, {
251
+ spawn: forward,
252
+ script: runScript,
253
+ });
254
+ }
255
+
256
+ function scheduleStatus(
257
+ settings: Settings,
258
+ run: Runner
259
+ ): Promise<CommandOutcome> {
260
+ return managerSchedule(
261
+ {
262
+ action: 'status',
263
+ revision: '',
264
+ config: settings.config,
265
+ authorized: false,
266
+ },
267
+ run
268
+ );
269
+ }
270
+
271
+ function actionRequired(
272
+ code: string,
273
+ remedy: string,
274
+ revision: string | null = null
275
+ ): CommandOutcome {
276
+ const report = managerFailure(canonicalDefinition, revision, code, remedy);
277
+ return {
278
+ exitCode: 1,
279
+ data: report,
280
+ text: managerText(report, { color: stdoutColor() }),
281
+ };
282
+ }
283
+
284
+ function validate(options: FleetSyncOptions): void {
285
+ validateSchedule(options.schedule);
286
+ if (options.all && options.node?.length)
287
+ throw new FleetUsageError('Choose --all or --node, not both.');
288
+ if (options.revision !== undefined && !isRevision(options.revision))
289
+ throw new FleetUsageError(
290
+ '--revision requires an exact lowercase 40-character commit SHA.'
291
+ );
292
+ if (options.dryRun && (options.apply || options.yes))
293
+ throw new FleetUsageError(
294
+ '--dry-run cannot be combined with apply confirmation.'
295
+ );
296
+ }
297
+ function interactive(options: FleetSyncOptions): boolean {
298
+ return (
299
+ ![
300
+ options.dryRun,
301
+ options.apply,
302
+ options.yes,
303
+ options.json,
304
+ options.nonInteractive,
305
+ process.env['CI'],
306
+ ].some(Boolean) && Boolean(process.stdin.isTTY && process.stderr.isTTY)
307
+ );
308
+ }
309
+
310
+ /** The commit an installed package was published from: the version of
311
+ * dev-fleet you run is the revision the fleet moves to.
312
+ */
313
+ function packageRevision(): string | undefined {
314
+ return packageInfo().revision ?? undefined;
315
+ }
316
+
317
+ async function scheduleRequest(
318
+ options: FleetSyncOptions,
319
+ revision: string,
320
+ settings: Settings,
321
+ run: Runner
322
+ ): Promise<CommandOutcome> {
323
+ const root = machineRoot();
324
+
325
+ if (options.dryRun)
326
+ return actionRequired(
327
+ 'FLEET_SCHEDULE_PREVIEW',
328
+ 'No schedule changed. Inspect --schedule status; scheduling writes require --apply --yes.'
329
+ );
330
+ if (!options.apply || !options.yes)
331
+ return managerSchedule(
332
+ {
333
+ action: options.schedule!,
334
+ revision,
335
+ config: settings.config,
336
+ authorized: false,
337
+ },
338
+ run
339
+ );
340
+ return withMachineLock(root, () =>
341
+ managerSchedule(
342
+ {
343
+ action: options.schedule!,
344
+ revision,
345
+ config: settings.config,
346
+ authorized: Boolean(options.apply && options.yes),
347
+ },
348
+ run
349
+ )
350
+ );
351
+ }
352
+
353
+ function validateSchedule(schedule: unknown): void {
354
+ if (
355
+ schedule !== undefined &&
356
+ !['enable', 'pause', 'resume', 'disable', 'status'].includes(
357
+ String(schedule)
358
+ )
359
+ )
360
+ throw new FleetUsageError('Unknown manager schedule action.');
361
+ }
362
+
363
+ async function selectedRevision(
364
+ explicit: string | undefined,
365
+ approved: string | undefined,
366
+ settings: Settings,
367
+ run: Runner
368
+ ): Promise<string | undefined> {
369
+ return (
370
+ explicit ??
371
+ packageRevision() ??
372
+ (await localCheckoutRevision(settings.workspace, run)) ??
373
+ approved
374
+ );
375
+ }
@@ -0,0 +1,165 @@
1
+ import { selectNodes } from '../config';
2
+ import { doctor } from '../manager';
3
+ import { FleetUsageError } from '../errors';
4
+ import { onboarding } from '../onboard';
5
+ import { makeReport, renderReport, reportExitCode } from '../report';
6
+ import { runCommand } from '../runner';
7
+ import schema from '../../schema/roster.schema.json';
8
+ import type { FleetConfig, FleetNode, NodeReport, Runner } from '../types';
9
+ import type { CommandOutcome } from './contracts';
10
+ import type { Settings } from './settings';
11
+ import { capabilities, timeout, validateOptions } from './options';
12
+ import type { CliOptions } from './options';
13
+ import { addNode, nodeFromOptions, readRoster } from './roster';
14
+ import { executeTailnet } from './tailnet';
15
+ import { probeSource } from './probe-source';
16
+ import { executeRepairCommand } from './repair';
17
+
18
+ export interface FleetServices {
19
+ run: Runner;
20
+ source: () => Promise<string>;
21
+ }
22
+
23
+ const SERVICES: FleetServices = { run: runCommand, source: probeSource };
24
+
25
+ export async function executeFleet(
26
+ command: string,
27
+ options: CliOptions,
28
+ settings: Settings,
29
+ services: FleetServices = SERVICES
30
+ ): Promise<CommandOutcome> {
31
+ validateOptions(command, options);
32
+ if (['repair', 'repairs'].includes(command))
33
+ return executeRepairCommand(command, options, settings, services);
34
+ if (['discover', 'health'].includes(command))
35
+ return executeTailnet(command, options, settings, services);
36
+ if (command === 'schema') return result(schema);
37
+ if (command === 'add') return register(settings, options);
38
+ const roster = options.local ? null : await readRoster(settings.config);
39
+ const nodes = resolveNodes(command, options, roster);
40
+ if (['nodes', 'onboard'].includes(command))
41
+ return rosterResult(command, nodes, roster !== null);
42
+ return diagnose(
43
+ nodes,
44
+ options,
45
+ { ...settings, missingRoster: !roster },
46
+ services
47
+ );
48
+ }
49
+
50
+ function rosterResult(
51
+ command: string,
52
+ nodes: FleetNode[],
53
+ configured: boolean
54
+ ): CommandOutcome {
55
+ return result({
56
+ version: 1,
57
+ command,
58
+ ok: true,
59
+ ...(command === 'nodes' ? { configured } : {}),
60
+ nodes:
61
+ command === 'onboard'
62
+ ? nodes.map((node) => onboarding(node, 'devfleet'))
63
+ : nodes,
64
+ });
65
+ }
66
+
67
+ function resolveNodes(
68
+ command: string,
69
+ options: CliOptions,
70
+ roster: FleetConfig | null
71
+ ): FleetNode[] {
72
+ if (!roster && !options.local && !['doctor', 'nodes'].includes(command))
73
+ throw new FleetUsageError(
74
+ 'No fleet roster. Use fleet-node-add or the interactive Register action; use fleet-status --local for this machine.'
75
+ );
76
+ if (options.node?.length && !roster)
77
+ throw new FleetUsageError(
78
+ 'Cannot select nodes without a roster. Use fleet-node-add first.'
79
+ );
80
+ return roster ? selectNodes(roster, options.node ?? []) : [];
81
+ }
82
+
83
+ function result(data: unknown, text?: string): CommandOutcome {
84
+ return { exitCode: 0, data, ...(text ? { text } : {}) };
85
+ }
86
+
87
+ async function register(
88
+ settings: Settings,
89
+ options: CliOptions
90
+ ): Promise<CommandOutcome> {
91
+ const node = nodeFromOptions(options);
92
+ const added = await addNode(settings.config, node, options.apply === true);
93
+ return result({
94
+ version: 1,
95
+ command: 'add',
96
+ ok: true,
97
+ applied: options.apply === true,
98
+ path: settings.config,
99
+ // How the fleet state stands with the bucket after this change.
100
+ ...(added.standing ? { state: added.standing } : {}),
101
+ node,
102
+ nodes: added.roster.nodes,
103
+ next: options.apply
104
+ ? 'Run fleet-nodes --onboard, then fleet-status --json.'
105
+ : 'Review this preview, then repeat with --apply --yes.',
106
+ });
107
+ }
108
+
109
+ async function diagnose(
110
+ nodes: FleetNode[],
111
+ options: CliOptions,
112
+ settings: Settings & { missingRoster: boolean },
113
+ services: FleetServices
114
+ ): Promise<CommandOutcome> {
115
+ const source = nodes.length ? await services.source() : undefined;
116
+ const reports = await doctor(
117
+ nodes,
118
+ {
119
+ root: settings.workspace,
120
+ local: options.local === true,
121
+ required: capabilities(options.require),
122
+ timeoutMs: timeout(options),
123
+ ...(source ? { source } : {}),
124
+ },
125
+ services.run
126
+ );
127
+ if (!options.local && settings.missingRoster) reports.push(missingRoster());
128
+ const report = {
129
+ ...makeReport('doctor', reports),
130
+ coverage: {
131
+ scope: options.local
132
+ ? 'local'
133
+ : options.node?.length
134
+ ? 'selected'
135
+ : 'all-configured',
136
+ manager: true,
137
+ configuredNodesTested: nodes.map((node) => node.id),
138
+ rosterAvailable: !settings.missingRoster,
139
+ note: 'Directed SSH checks from this manager only. T3 relay/peer and VS Code forwarding require operator tests.',
140
+ },
141
+ };
142
+ return {
143
+ exitCode: reportExitCode(report, options.strict === true),
144
+ data: report,
145
+ text: renderReport(report),
146
+ };
147
+ }
148
+
149
+ function missingRoster(): NodeReport {
150
+ return {
151
+ node: 'fleet',
152
+ checks: [
153
+ {
154
+ id: 'fleet:roster',
155
+ status: 'fail',
156
+ summary:
157
+ 'No developer fleet roster is configured. Only this manager could be checked; fleet-wide health is unknown.',
158
+ remedy: {
159
+ action:
160
+ 'Open personal --interactive and choose Register a developer computer, or run devfleet fleet-node-add --help. Then run fleet-status --json.',
161
+ },
162
+ },
163
+ ],
164
+ };
165
+ }