@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.
Files changed (333) 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 +157 -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 +128 -0
  30. package/agents/t3.json +40 -0
  31. package/ansible/macos/manager.yml +30 -0
  32. package/ansible/macos/policy.json +17 -0
  33. package/ansible/monitoring/README.md +154 -0
  34. package/ansible/monitoring/agent-darwin.yml +198 -0
  35. package/ansible/monitoring/agent.yml +134 -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/lock.json +11 -0
  40. package/ansible/net/remote-probe.sh +13 -0
  41. package/ansible/ubuntu/authorize-sudo.sh +51 -0
  42. package/ansible/ubuntu/baseline.yml +89 -0
  43. package/ansible/ubuntu/disk-guard.py +104 -0
  44. package/ansible/ubuntu/policy.json +43 -0
  45. package/ansible/ubuntu/recovery.yml +15 -0
  46. package/ansible/ubuntu/t3/policy.json +7 -0
  47. package/ansible/ubuntu/t3-desktop.yml +220 -0
  48. package/ansible/ubuntu/t3-health.sh +33 -0
  49. package/ansible/ubuntu/t3-policy.yml +76 -0
  50. package/ansible/ubuntu/t3-route.yml +28 -0
  51. package/ansible/ubuntu/tailscale.yml +75 -0
  52. package/ansible/ubuntu/tasks/credentials.yml +47 -0
  53. package/ansible/ubuntu/tasks/docker.yml +31 -0
  54. package/ansible/ubuntu/tasks/packages.yml +56 -0
  55. package/ansible/ubuntu/tasks/power.yml +51 -0
  56. package/ansible/ubuntu/tasks/recovery.yml +39 -0
  57. package/ansible/ubuntu/tasks/resources.yml +252 -0
  58. package/ansible/ubuntu/tasks/services.yml +31 -0
  59. package/ansible/ubuntu/tasks/ssh.yml +71 -0
  60. package/ansible/ubuntu/tasks/user.yml +120 -0
  61. package/bin/devfleet.mjs +36 -0
  62. package/bootstrap/bootstrap.ps1 +50 -0
  63. package/bootstrap/bootstrap.sh +73 -0
  64. package/bootstrap/devfleet +7 -0
  65. package/bootstrap/docker-desktop.sh +33 -0
  66. package/bootstrap/doctor.ts +189 -0
  67. package/bootstrap/fleet-manager-run.sh +76 -0
  68. package/bootstrap/fleet-remote-bootstrap.sh +185 -0
  69. package/bootstrap/global-config.ts +113 -0
  70. package/bootstrap/install-launcher.sh +7 -0
  71. package/bootstrap/native-aws.ts +86 -0
  72. package/bootstrap/native-t3.ts +154 -0
  73. package/bootstrap/npm.ts +350 -0
  74. package/bootstrap/setup-status.ts +51 -0
  75. package/bootstrap/setup.ts +88 -0
  76. package/bootstrap/shell-path.ts +105 -0
  77. package/bootstrap/verify-t3-archive.py +49 -0
  78. package/bootstrap/worker-auth.ts +65 -0
  79. package/bootstrap/worker-github.ts +47 -0
  80. package/bootstrap/worker-tools.ts +24 -0
  81. package/cliproxy/AGENTS.md +11 -0
  82. package/cliproxy/README.md +269 -0
  83. package/cliproxy/clients/bootstrap.py +143 -0
  84. package/cliproxy/clients/launcher.py +57 -0
  85. package/cliproxy/compose.yaml +90 -0
  86. package/cliproxy/config.template.yaml +70 -0
  87. package/cliproxy/docs/cloud-adoption.md +16 -0
  88. package/cliproxy/docs/deployment.png +0 -0
  89. package/cliproxy/docs/deployment.svg +109 -0
  90. package/cliproxy/docs/health-workflow.example.yml +185 -0
  91. package/cliproxy/docs/migration.md +131 -0
  92. package/cliproxy/docs/model-health-report.schema.json +364 -0
  93. package/cliproxy/docs/model-health.md +393 -0
  94. package/cliproxy/image.lock.json +5 -0
  95. package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
  96. package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
  97. package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
  98. package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
  99. package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
  100. package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
  101. package/cliproxy/models.json +1618 -0
  102. package/cliproxy/requirements.lock +78 -0
  103. package/cliproxy/scripts/accounts.py +38 -0
  104. package/cliproxy/scripts/audit.py +123 -0
  105. package/cliproxy/scripts/backup +234 -0
  106. package/cliproxy/scripts/check.py +34 -0
  107. package/cliproxy/scripts/checkpoint.py +226 -0
  108. package/cliproxy/scripts/cli.py +64 -0
  109. package/cliproxy/scripts/cliproxy_host.py +257 -0
  110. package/cliproxy/scripts/cloud_state.py +150 -0
  111. package/cliproxy/scripts/container-health +6 -0
  112. package/cliproxy/scripts/docker +24 -0
  113. package/cliproxy/scripts/external-health.py +79 -0
  114. package/cliproxy/scripts/fleet_config.py +43 -0
  115. package/cliproxy/scripts/fleet_sync.py +274 -0
  116. package/cliproxy/scripts/initialize +84 -0
  117. package/cliproxy/scripts/install-dependencies +48 -0
  118. package/cliproxy/scripts/install.py +406 -0
  119. package/cliproxy/scripts/model_health.py +16 -0
  120. package/cliproxy/scripts/modelhealth/__init__.py +5 -0
  121. package/cliproxy/scripts/modelhealth/checks.py +429 -0
  122. package/cliproxy/scripts/modelhealth/cli.py +117 -0
  123. package/cliproxy/scripts/modelhealth/config.py +186 -0
  124. package/cliproxy/scripts/modelhealth/emulator.py +536 -0
  125. package/cliproxy/scripts/modelhealth/expect.py +88 -0
  126. package/cliproxy/scripts/modelhealth/findings.py +183 -0
  127. package/cliproxy/scripts/modelhealth/protocol.py +330 -0
  128. package/cliproxy/scripts/modelhealth/redact.py +28 -0
  129. package/cliproxy/scripts/modelhealth/report.py +150 -0
  130. package/cliproxy/scripts/modelhealth/runner.py +390 -0
  131. package/cliproxy/scripts/modelhealth/scenario.py +131 -0
  132. package/cliproxy/scripts/modelhealth/schema.py +63 -0
  133. package/cliproxy/scripts/modelhealth/simulate.py +111 -0
  134. package/cliproxy/scripts/modelhealth/transport.py +213 -0
  135. package/cliproxy/scripts/monitor +68 -0
  136. package/cliproxy/scripts/ops +527 -0
  137. package/cliproxy/scripts/public-endpoint +127 -0
  138. package/cliproxy/scripts/recover_node.py +177 -0
  139. package/cliproxy/scripts/restore-verify +60 -0
  140. package/cliproxy/scripts/retention.py +144 -0
  141. package/cliproxy/scripts/service-start +7 -0
  142. package/cliproxy/scripts/validate-config +56 -0
  143. package/cliproxy/scripts/vault-access.py +57 -0
  144. package/cliproxy/scripts/vault-backup-login +55 -0
  145. package/cliproxy/scripts/vault-recovery +56 -0
  146. package/cliproxy/scripts/vault-renew +30 -0
  147. package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
  148. package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
  149. package/cliproxy/systemd/cliproxy-backup.service +13 -0
  150. package/cliproxy/systemd/cliproxy-backup.timer +8 -0
  151. package/cliproxy/systemd/cliproxy-fleet.service +17 -0
  152. package/cliproxy/systemd/cliproxy-gc.service +13 -0
  153. package/cliproxy/systemd/cliproxy-gc.timer +8 -0
  154. package/cliproxy/systemd/cliproxy-monitor.service +8 -0
  155. package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
  156. package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
  157. package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
  158. package/cliproxy/vendor/LICENSE +22 -0
  159. package/cliproxy/vendor/config.upstream.yaml +1349 -0
  160. package/examples/fleet.json +95 -0
  161. package/examples/roster.json +13 -0
  162. package/mise.lock +1492 -0
  163. package/mise.toml +99 -0
  164. package/package.json +65 -4
  165. package/schema/fleet.schema.json +364 -0
  166. package/schema/roster.schema.json +77 -0
  167. package/src/agents/account-relay.ts +111 -0
  168. package/src/agents/accounts.ts +85 -0
  169. package/src/agents/audit.ts +77 -0
  170. package/src/agents/cli.ts +13 -0
  171. package/src/agents/command.ts +204 -0
  172. package/src/agents/converge.ts +123 -0
  173. package/src/agents/doctor.ts +107 -0
  174. package/src/agents/errors.ts +2 -0
  175. package/src/agents/files.ts +73 -0
  176. package/src/agents/fleet-verify.ts +156 -0
  177. package/src/agents/launch.ts +290 -0
  178. package/src/agents/native-verify.ts +226 -0
  179. package/src/agents/provider-errors.ts +50 -0
  180. package/src/agents/registry.ts +55 -0
  181. package/src/agents/render.ts +110 -0
  182. package/src/agents/round-trip.ts +192 -0
  183. package/src/agents/secrets.ts +13 -0
  184. package/src/agents/toolchain.ts +34 -0
  185. package/src/agents/verify-cache.ts +103 -0
  186. package/src/agents/verify.ts +127 -0
  187. package/src/auth-checks.ts +163 -0
  188. package/src/cli/agent-guide.ts +95 -0
  189. package/src/cli/app.ts +115 -0
  190. package/src/cli/cli.ts +21 -0
  191. package/src/cli/command-index.ts +50 -0
  192. package/src/cli/contracts.ts +25 -0
  193. package/src/cli/fleet-reference.ts +17 -0
  194. package/src/cli/fleet-sync-checkout.ts +91 -0
  195. package/src/cli/fleet-sync-enrollment.ts +144 -0
  196. package/src/cli/fleet-sync-forward.ts +134 -0
  197. package/src/cli/fleet-sync-manager.ts +193 -0
  198. package/src/cli/fleet-sync-monitoring.ts +185 -0
  199. package/src/cli/fleet-sync-output.ts +314 -0
  200. package/src/cli/fleet-sync-privilege.ts +27 -0
  201. package/src/cli/fleet-sync-progress.ts +109 -0
  202. package/src/cli/fleet-sync-remote.ts +236 -0
  203. package/src/cli/fleet-sync-report.ts +26 -0
  204. package/src/cli/fleet-sync-schedule.ts +138 -0
  205. package/src/cli/fleet-sync-vault.ts +166 -0
  206. package/src/cli/fleet-sync.ts +309 -0
  207. package/src/cli/fleet.ts +163 -0
  208. package/src/cli/managed-health.ts +219 -0
  209. package/src/cli/node-deployment.ts +34 -0
  210. package/src/cli/node-sync.ts +114 -0
  211. package/src/cli/node-t3.ts +31 -0
  212. package/src/cli/options.ts +141 -0
  213. package/src/cli/probe-source.ts +7 -0
  214. package/src/cli/program.ts +169 -0
  215. package/src/cli/register-fleet.ts +246 -0
  216. package/src/cli/registry.ts +158 -0
  217. package/src/cli/repair-apply.ts +67 -0
  218. package/src/cli/repair-guard.ts +11 -0
  219. package/src/cli/repair-inspect.ts +150 -0
  220. package/src/cli/repair-script.ts +76 -0
  221. package/src/cli/repair.ts +209 -0
  222. package/src/cli/roster.ts +89 -0
  223. package/src/cli/search.ts +35 -0
  224. package/src/cli/settings.ts +64 -0
  225. package/src/cli/source-operations.ts +78 -0
  226. package/src/cli/t3/command.ts +90 -0
  227. package/src/cli/t3/register.ts +55 -0
  228. package/src/cli/tailnet.ts +127 -0
  229. package/src/cli/ui-actions.ts +133 -0
  230. package/src/cli/ui-controller.ts +113 -0
  231. package/src/cli/ui-data.ts +106 -0
  232. package/src/cli/ui-execute.ts +163 -0
  233. package/src/cli/ui-repairs.ts +34 -0
  234. package/src/cli/ui-reports.ts +211 -0
  235. package/src/cli/ui-targets.ts +100 -0
  236. package/src/cli/ui-terminal.ts +109 -0
  237. package/src/cli/ui-types.ts +49 -0
  238. package/src/cli/ui-wizard.ts +101 -0
  239. package/src/cli/ui.ts +9 -0
  240. package/src/cli/worker-health-output.ts +37 -0
  241. package/src/config.ts +186 -0
  242. package/src/configure.ts +255 -0
  243. package/src/definition.ts +260 -0
  244. package/src/diagnostics.ts +92 -0
  245. package/src/doctor-check.ts +10 -0
  246. package/src/errors.ts +1 -0
  247. package/src/fingerprint.ts +120 -0
  248. package/src/fleet-config.ts +88 -0
  249. package/src/index.ts +6 -0
  250. package/src/input-hashes.ts +73 -0
  251. package/src/machine/local-source.ts +52 -0
  252. package/src/machine/lock-error.ts +2 -0
  253. package/src/machine/platform.ts +44 -0
  254. package/src/machine/source.ts +68 -0
  255. package/src/machine/state.ts +185 -0
  256. package/src/machine-health.ts +95 -0
  257. package/src/managed-source.ts +47 -0
  258. package/src/manager-failure.ts +48 -0
  259. package/src/manager-sync.ts +400 -0
  260. package/src/manager.ts +62 -0
  261. package/src/monitoring/apply.ts +171 -0
  262. package/src/monitoring/beszel.ts +191 -0
  263. package/src/monitoring/enroll.ts +110 -0
  264. package/src/monitoring/hub-access.ts +113 -0
  265. package/src/monitoring/hub-viewer.ts +142 -0
  266. package/src/monitoring/hub.ts +93 -0
  267. package/src/monitoring/manager-agent.ts +73 -0
  268. package/src/monitoring/manager-hub.ts +232 -0
  269. package/src/monitoring/remote.ts +65 -0
  270. package/src/network-definition.ts +83 -0
  271. package/src/node-converge-linux.ts +372 -0
  272. package/src/node-converge-shared.ts +83 -0
  273. package/src/node-converge.ts +40 -0
  274. package/src/onboard.ts +35 -0
  275. package/src/platform/ansible-receipt.ts +128 -0
  276. package/src/platform/ansible.ts +62 -0
  277. package/src/platform/darwin.ts +203 -0
  278. package/src/platform/detect.ts +11 -0
  279. package/src/platform/fallback.ts +110 -0
  280. package/src/platform/index.ts +38 -0
  281. package/src/platform/launchd.ts +98 -0
  282. package/src/platform/linux.ts +139 -0
  283. package/src/platform/ps.ts +45 -0
  284. package/src/platform/resource-verify.ts +128 -0
  285. package/src/platform/systemd-user.ts +95 -0
  286. package/src/platform/t3-policy.ts +137 -0
  287. package/src/platform/t3-route.ts +68 -0
  288. package/src/platform/t3-service.ts +75 -0
  289. package/src/platform/types.ts +123 -0
  290. package/src/platform/ubuntu-audit.ts +63 -0
  291. package/src/platform/ubuntu-verify.ts +105 -0
  292. package/src/platform/windows.ts +148 -0
  293. package/src/power-recovery.ts +55 -0
  294. package/src/probe.ts +51 -0
  295. package/src/protocol.ts +119 -0
  296. package/src/proxy-converge.ts +85 -0
  297. package/src/remote-bootstrap.ts +212 -0
  298. package/src/remote-entry.ts +89 -0
  299. package/src/remote-lock.ts +12 -0
  300. package/src/repair-catalog.ts +206 -0
  301. package/src/repair-plan.ts +78 -0
  302. package/src/repair-runner.ts +84 -0
  303. package/src/report.ts +71 -0
  304. package/src/runner.ts +104 -0
  305. package/src/secrets.ts +169 -0
  306. package/src/service-auth.ts +115 -0
  307. package/src/ssh.ts +135 -0
  308. package/src/sync.ts +104 -0
  309. package/src/t3/accounts.ts +81 -0
  310. package/src/t3/desired.ts +165 -0
  311. package/src/t3/files.ts +81 -0
  312. package/src/t3/names-check.ts +53 -0
  313. package/src/t3/names.ts +155 -0
  314. package/src/t3/plan.ts +82 -0
  315. package/src/t3/providers.ts +74 -0
  316. package/src/t3/reconcile.ts +123 -0
  317. package/src/t3-endpoint.ts +48 -0
  318. package/src/t3-linked-check.ts +101 -0
  319. package/src/tailnet-health.ts +231 -0
  320. package/src/tailnet-matching.ts +37 -0
  321. package/src/tailscale.ts +141 -0
  322. package/src/test-fixtures.ts +36 -0
  323. package/src/toolchain-checks.ts +83 -0
  324. package/src/topology.ts +24 -0
  325. package/src/transport.ts +122 -0
  326. package/src/types.ts +95 -0
  327. package/src/vault-config.ts +38 -0
  328. package/src/worker-access-checks.ts +115 -0
  329. package/src/worker-capacity.ts +34 -0
  330. package/src/worker-host-checks.ts +176 -0
  331. package/src/worker-policy.ts +64 -0
  332. package/src/worker-readiness.ts +64 -0
  333. package/src/workstation-checks.ts +29 -0
@@ -0,0 +1,309 @@
1
+ import { localCheckoutRevision } from '../machine/local-source';
2
+ import { fleet as canonicalDefinition } from '../fleet-config';
3
+ import { managerFailure } from '../manager-failure';
4
+ import { managerText, stdoutColor } from './fleet-sync-output';
5
+ import { managerSchedule, type ScheduleAction } from './fleet-sync-schedule';
6
+ import { enrollManager } from './fleet-sync-enrollment';
7
+ import { authorizeManager } from './fleet-sync-vault';
8
+ import { homedir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import { existsSync, renameSync } from 'node:fs';
11
+ import {
12
+ readEnrollment,
13
+ isRevision,
14
+ saveEnrollment,
15
+ withMachineLock,
16
+ ensurePrivateRoot,
17
+ } from '../machine/state';
18
+ import { runCommand } from '../runner';
19
+ import type { Runner } from '../types';
20
+ import type { Settings } from './settings';
21
+ import type { CommandOutcome } from './contracts';
22
+ import { FleetUsageError } from '../errors';
23
+ import { FleetLockError } from '../machine/lock-error';
24
+ import { selected } from './fleet-sync-remote';
25
+ import { managerIdentity } from './fleet-sync-manager';
26
+ import { trackedScript } from '../remote-bootstrap';
27
+ import { rehome } from './fleet-sync-checkout';
28
+ import {
29
+ forwardingDisabled,
30
+ forwardToManager,
31
+ type ForwardSpawn,
32
+ } from './fleet-sync-forward';
33
+
34
+ declare const PERSONAL_SOURCE_REVISION: string | null;
35
+ export interface FleetSyncOptions {
36
+ revision?: string;
37
+ dryRun?: boolean;
38
+ apply?: boolean;
39
+ yes?: boolean;
40
+ json?: boolean;
41
+ nonInteractive?: boolean;
42
+ node?: string[];
43
+ all?: boolean;
44
+ schedule?: ScheduleAction;
45
+ /** Internal: this revision already converged the whole fleet, so no canary
46
+ * has to prove it first and every worker converges at once.
47
+ */
48
+ rolledOut?: boolean;
49
+ /** Internal: set for interactive terminal runs (sudo enrollment prompts). */
50
+ interactiveAuthorization?: boolean;
51
+ expectedPlanFingerprint?: string;
52
+ }
53
+ export function machineRoot(): string {
54
+ const root = join(homedir(), '.local/share/personal/fleet/manager');
55
+ // One-time move from the pre-topology name; keeps the approved revision.
56
+ const legacy = join(homedir(), '.local/share/personal/fleet/controller');
57
+ if (!existsSync(root) && existsSync(legacy)) renameSync(legacy, root);
58
+ return root;
59
+ }
60
+
61
+ /** Public contract v3: the manager converges itself, then every enrolled worker. */
62
+ export async function fleetSync(
63
+ options: FleetSyncOptions,
64
+ settings: Settings,
65
+ run: Runner = runCommand,
66
+ forward?: ForwardSpawn
67
+ ): Promise<CommandOutcome> {
68
+ validate(options);
69
+ if (!(await onManager(run)))
70
+ return offManager(options, settings, run, forward);
71
+ if (options.schedule === 'status') return scheduleStatus(settings, run);
72
+ const root = machineRoot();
73
+ await ensurePrivateRoot(root);
74
+ const state = await readEnrollment(root);
75
+ const revision = await selectedRevision(
76
+ options.revision,
77
+ state?.approvedRevision,
78
+ settings,
79
+ run
80
+ );
81
+ if (!isRevision(revision))
82
+ return actionRequired(
83
+ 'FLEET_REVISION_REQUIRED',
84
+ 'Select an approved full SHA with --revision, or use a published package containing clean source provenance.'
85
+ );
86
+ if (options.schedule)
87
+ return scheduleRequest(options, revision, settings, run);
88
+ const rehomed = await rehome(
89
+ options,
90
+ { revision, workspace: settings.workspace },
91
+ run
92
+ );
93
+ if (rehomed) return rehomed;
94
+ const request = { ...options, revision };
95
+ if (request.dryRun) return selected(request, settings, run);
96
+ // Applies without a confirmation prompt; --dry-run is the preview.
97
+ request.apply = true;
98
+ request.yes = true;
99
+ if (interactive(options)) {
100
+ await enrollManager(settings.config, run);
101
+ // Needed to hand each node its own Vault token; nodes report any gap.
102
+ await authorizeManager({ interactive: true }, run);
103
+ request.interactiveAuthorization = true;
104
+ }
105
+ return applyLocked(root, state, request, { settings, run });
106
+ }
107
+
108
+ /** Records the approved revision, converges, then records what was applied. */
109
+ function applyLocked(
110
+ root: string,
111
+ state: Awaited<ReturnType<typeof readEnrollment>>,
112
+ request: FleetSyncOptions & { revision: string },
113
+ context: { settings: Settings; run: Runner }
114
+ ): Promise<CommandOutcome> {
115
+ const { revision } = request;
116
+ const { settings, run } = context;
117
+ return withMachineLock(root, async () => {
118
+ const approval = {
119
+ ...state,
120
+ version: 1 as const,
121
+ id: 'manager',
122
+ approvedRevision: revision,
123
+ };
124
+ await saveEnrollment(root, approval);
125
+ const result = await selected(
126
+ { ...request, rolledOut: state?.appliedRevision === revision },
127
+ settings,
128
+ run
129
+ );
130
+ if (result.exitCode === 0)
131
+ await saveEnrollment(root, {
132
+ ...approval,
133
+ appliedRevision: revision,
134
+ ...(state?.appliedRevision && state.appliedRevision !== revision
135
+ ? { previousRevision: state.appliedRevision }
136
+ : {}),
137
+ });
138
+ return result;
139
+ }).catch((error: unknown) => {
140
+ if (error instanceof FleetLockError)
141
+ return actionRequired(
142
+ 'FLEET_MANAGER_LOCKED',
143
+ 'Another manager run or interrupted lock exists. Inspect manager and remote processes before removing an abandoned sync.lock; never steal active package-manager locks.'
144
+ );
145
+ throw error;
146
+ });
147
+ }
148
+ /** Every run (previews and schedules too) happens on the manager: a worker can
149
+ * neither converge the manager's singletons nor observe its T3 links, so a
150
+ * partial run there only produces misleading failures.
151
+ */
152
+ async function onManager(run: Runner): Promise<boolean> {
153
+ const { manager } = canonicalDefinition;
154
+ return (await managerIdentity(manager.id, run)).status === 'pass';
155
+ }
156
+
157
+ /** Hands the request to the manager; refuses (without touching any worker)
158
+ * when hand-off is disabled, e.g. on the manager side of a hand-off.
159
+ */
160
+ async function offManager(
161
+ options: FleetSyncOptions,
162
+ settings: Settings,
163
+ run: Runner,
164
+ forward: ForwardSpawn | undefined
165
+ ): Promise<CommandOutcome> {
166
+ const definition = canonicalDefinition;
167
+ const revision =
168
+ options.revision ??
169
+ packageRevision() ??
170
+ (await localCheckoutRevision(settings.workspace, run));
171
+ if (forwardingDisabled())
172
+ return actionRequired(
173
+ 'FLEET_NOT_MANAGER',
174
+ `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.`,
175
+ revision ?? null
176
+ );
177
+ const runScript = revision
178
+ ? await trackedScript(
179
+ settings.workspace,
180
+ revision,
181
+ 'bootstrap/fleet-manager-run.sh',
182
+ run
183
+ )
184
+ : null;
185
+ return forwardToManager(definition, options, revision, {
186
+ spawn: forward,
187
+ script: runScript,
188
+ });
189
+ }
190
+
191
+ function scheduleStatus(
192
+ settings: Settings,
193
+ run: Runner
194
+ ): Promise<CommandOutcome> {
195
+ return managerSchedule(
196
+ {
197
+ action: 'status',
198
+ revision: '',
199
+ config: settings.config,
200
+ authorized: false,
201
+ },
202
+ run
203
+ );
204
+ }
205
+
206
+ function actionRequired(
207
+ code: string,
208
+ remedy: string,
209
+ revision: string | null = null
210
+ ): CommandOutcome {
211
+ const report = managerFailure(canonicalDefinition, revision, code, remedy);
212
+ return {
213
+ exitCode: 1,
214
+ data: report,
215
+ text: managerText(report, { color: stdoutColor() }),
216
+ };
217
+ }
218
+
219
+ function validate(options: FleetSyncOptions): void {
220
+ validateSchedule(options.schedule);
221
+ if (options.all && options.node?.length)
222
+ throw new FleetUsageError('Choose --all or --node, not both.');
223
+ if (options.revision !== undefined && !isRevision(options.revision))
224
+ throw new FleetUsageError(
225
+ '--revision requires an exact lowercase 40-character commit SHA.'
226
+ );
227
+ if (options.dryRun && (options.apply || options.yes))
228
+ throw new FleetUsageError(
229
+ '--dry-run cannot be combined with apply confirmation.'
230
+ );
231
+ }
232
+ function interactive(options: FleetSyncOptions): boolean {
233
+ return (
234
+ ![
235
+ options.dryRun,
236
+ options.apply,
237
+ options.yes,
238
+ options.json,
239
+ options.nonInteractive,
240
+ process.env['CI'],
241
+ ].some(Boolean) && Boolean(process.stdin.isTTY && process.stderr.isTTY)
242
+ );
243
+ }
244
+
245
+ function packageRevision(): string | undefined {
246
+ return typeof PERSONAL_SOURCE_REVISION === 'string'
247
+ ? PERSONAL_SOURCE_REVISION
248
+ : undefined;
249
+ }
250
+
251
+ async function scheduleRequest(
252
+ options: FleetSyncOptions,
253
+ revision: string,
254
+ settings: Settings,
255
+ run: Runner
256
+ ): Promise<CommandOutcome> {
257
+ const root = machineRoot();
258
+
259
+ if (options.dryRun)
260
+ return actionRequired(
261
+ 'FLEET_SCHEDULE_PREVIEW',
262
+ 'No schedule changed. Inspect --schedule status; scheduling writes require --apply --yes.'
263
+ );
264
+ if (!options.apply || !options.yes)
265
+ return managerSchedule(
266
+ {
267
+ action: options.schedule!,
268
+ revision,
269
+ config: settings.config,
270
+ authorized: false,
271
+ },
272
+ run
273
+ );
274
+ return withMachineLock(root, () =>
275
+ managerSchedule(
276
+ {
277
+ action: options.schedule!,
278
+ revision,
279
+ config: settings.config,
280
+ authorized: Boolean(options.apply && options.yes),
281
+ },
282
+ run
283
+ )
284
+ );
285
+ }
286
+
287
+ function validateSchedule(schedule: unknown): void {
288
+ if (
289
+ schedule !== undefined &&
290
+ !['enable', 'pause', 'resume', 'disable', 'status'].includes(
291
+ String(schedule)
292
+ )
293
+ )
294
+ throw new FleetUsageError('Unknown manager schedule action.');
295
+ }
296
+
297
+ async function selectedRevision(
298
+ explicit: string | undefined,
299
+ approved: string | undefined,
300
+ settings: Settings,
301
+ run: Runner
302
+ ): Promise<string | undefined> {
303
+ return (
304
+ explicit ??
305
+ packageRevision() ??
306
+ (await localCheckoutRevision(settings.workspace, run)) ??
307
+ approved
308
+ );
309
+ }
@@ -0,0 +1,163 @@
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 config = 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
+ node,
100
+ nodes: config.nodes,
101
+ next: options.apply
102
+ ? 'Run fleet-nodes --onboard, then fleet-status --json.'
103
+ : 'Review this preview, then repeat with --apply --yes.',
104
+ });
105
+ }
106
+
107
+ async function diagnose(
108
+ nodes: FleetNode[],
109
+ options: CliOptions,
110
+ settings: Settings & { missingRoster: boolean },
111
+ services: FleetServices
112
+ ): Promise<CommandOutcome> {
113
+ const source = nodes.length ? await services.source() : undefined;
114
+ const reports = await doctor(
115
+ nodes,
116
+ {
117
+ root: settings.workspace,
118
+ local: options.local === true,
119
+ required: capabilities(options.require),
120
+ timeoutMs: timeout(options),
121
+ ...(source ? { source } : {}),
122
+ },
123
+ services.run
124
+ );
125
+ if (!options.local && settings.missingRoster) reports.push(missingRoster());
126
+ const report = {
127
+ ...makeReport('doctor', reports),
128
+ coverage: {
129
+ scope: options.local
130
+ ? 'local'
131
+ : options.node?.length
132
+ ? 'selected'
133
+ : 'all-configured',
134
+ manager: true,
135
+ configuredNodesTested: nodes.map((node) => node.id),
136
+ rosterAvailable: !settings.missingRoster,
137
+ note: 'Directed SSH checks from this manager only. T3 relay/peer and VS Code forwarding require operator tests.',
138
+ },
139
+ };
140
+ return {
141
+ exitCode: reportExitCode(report, options.strict === true),
142
+ data: report,
143
+ text: renderReport(report),
144
+ };
145
+ }
146
+
147
+ function missingRoster(): NodeReport {
148
+ return {
149
+ node: 'fleet',
150
+ checks: [
151
+ {
152
+ id: 'fleet:roster',
153
+ status: 'fail',
154
+ summary:
155
+ 'No developer fleet roster is configured. Only this manager could be checked; fleet-wide health is unknown.',
156
+ remedy: {
157
+ action:
158
+ 'Open personal --interactive and choose Register a developer computer, or run devfleet fleet-node-add --help. Then run fleet-status --json.',
159
+ },
160
+ },
161
+ ],
162
+ };
163
+ }
@@ -0,0 +1,219 @@
1
+ import { verifyT3Linked } from '../t3-linked-check';
2
+ import { verifyT3Endpoint } from '../t3-endpoint';
3
+ import type { Check, NodeReport, Runner } from '../types';
4
+ import { fleet as definition } from '../fleet-config';
5
+ import { enrolledRoster } from '../definition';
6
+ import type { Tailnet } from '../tailscale';
7
+ import type { FleetNode } from '../types';
8
+ import {
9
+ PUBLIC_HUB_URL,
10
+ verifyBeszel,
11
+ type BeszelRequest,
12
+ } from '../monitoring/beszel';
13
+ import { hubAccess } from '../monitoring/hub-access';
14
+ import { hubUrl } from '../topology';
15
+
16
+ export interface DashboardAccess {
17
+ (hub: string): Promise<{ request: BeszelRequest }>;
18
+ }
19
+
20
+ /** The hub always runs on the manager, reached by its MagicDNS name. */
21
+ export function dashboardUrl(tailnet: Tailnet): string | null {
22
+ const canonical = definition;
23
+ const visible = tailnet.computers.some(
24
+ (computer) =>
25
+ computer.dns.replace(/\.$/, '') ===
26
+ canonical.manager.network.tailscaleName
27
+ );
28
+ return visible ? hubUrl(canonical) : null;
29
+ }
30
+
31
+ /** Centralized dashboard evidence for every canonical worker: the hub must answer
32
+ * privately, and each node must be registered, connected and reporting fresh
33
+ * samples. A missing manager Vault session is reported, never silently passed.
34
+ */
35
+ export async function appendDashboardEvidence(
36
+ reports: NodeReport[],
37
+ tailnet: Tailnet,
38
+ options: { access?: DashboardAccess; fetcher?: typeof fetch } = {}
39
+ ): Promise<string | null> {
40
+ const hub = dashboardUrl(tailnet);
41
+ const dashboard: NodeReport = { node: 'dashboard', checks: [] };
42
+ reports.push(dashboard);
43
+ if (!hub) {
44
+ dashboard.checks.push(
45
+ fail(
46
+ 'beszel:hub',
47
+ 'BESZEL_HUB_SELECTION_REQUIRED',
48
+ 'The hub runs on the manager, which is not visible in Tailscale. Wake the manager and reconnect it to the tailnet.'
49
+ )
50
+ );
51
+ return null;
52
+ }
53
+ const fetcher = options.fetcher ?? fetch;
54
+ dashboard.checks.push(await hubReachable(hub, fetcher));
55
+ dashboard.checks.push(await publicEndpoint(fetcher));
56
+ await nodeEvidence(reports, dashboard, hub, options.access ?? hubAccess);
57
+ return hub;
58
+ }
59
+
60
+ async function nodeEvidence(
61
+ reports: NodeReport[],
62
+ dashboard: NodeReport,
63
+ hub: string,
64
+ access: DashboardAccess
65
+ ): Promise<void> {
66
+ const canonical = definition;
67
+ try {
68
+ const { request } = await access(hub);
69
+ for (const machine of canonical.workers) {
70
+ const evidence = await verifyBeszel(hub, machine.id, request);
71
+ const report =
72
+ reports.find(
73
+ (entry) =>
74
+ entry.node === machine.network?.tailscaleName ||
75
+ entry.node === machine.id
76
+ ) ?? dashboard;
77
+ report.checks.push(...evidence.checks);
78
+ }
79
+ } catch (error) {
80
+ dashboard.checks.push({
81
+ id: 'beszel:access',
82
+ status: 'skip',
83
+ summary:
84
+ error instanceof Error && error.message.startsWith('BESZEL_')
85
+ ? error.message
86
+ : 'BESZEL_ACCOUNT_REQUIRED',
87
+ remedy: {
88
+ action:
89
+ 'Authenticate this manager to Vault so BESZEL_FLEET_AUTH can be read, then rerun fleet-status --workers. Node registration and metric freshness are unverified until then.',
90
+ },
91
+ });
92
+ }
93
+ }
94
+
95
+ async function hubReachable(
96
+ hub: string,
97
+ fetcher: typeof fetch
98
+ ): Promise<Check> {
99
+ try {
100
+ const response = await fetcher(`${hub}/api/health`, {
101
+ redirect: 'error',
102
+ signal: AbortSignal.timeout(10_000),
103
+ });
104
+ if (response.ok)
105
+ return {
106
+ id: 'beszel:hub',
107
+ status: 'pass',
108
+ summary: `Dashboard answers privately at ${hub}.`,
109
+ };
110
+ } catch {
111
+ /* Connection details are not evidence; fall through. */
112
+ }
113
+ return fail(
114
+ 'beszel:hub',
115
+ 'BESZEL_HUB_UNREACHABLE',
116
+ `The private dashboard did not answer at ${hub}/api/health. The hub runs on the manager: keep it awake with Docker Desktop running, then run devfleet fleet-sync there.`
117
+ );
118
+ }
119
+
120
+ /** The Cloudflare hostname is convenience access; the private hub stays canonical,
121
+ * so an offline connector warns instead of failing the audit.
122
+ */
123
+ async function publicEndpoint(fetcher: typeof fetch): Promise<Check> {
124
+ try {
125
+ const response = await fetcher(`${PUBLIC_HUB_URL}/api/health`, {
126
+ redirect: 'error',
127
+ signal: AbortSignal.timeout(10_000),
128
+ });
129
+ if (response.ok)
130
+ return {
131
+ id: 'beszel:public-endpoint',
132
+ status: 'pass',
133
+ summary: `Dashboard answers publicly at ${PUBLIC_HUB_URL}.`,
134
+ };
135
+ } catch {
136
+ /* Connection details are not evidence; fall through. */
137
+ }
138
+ return {
139
+ id: 'beszel:public-endpoint',
140
+ status: 'warn',
141
+ summary: 'BESZEL_PUBLIC_ENDPOINT_OFFLINE',
142
+ remedy: {
143
+ action: `${PUBLIC_HUB_URL} did not answer. Create the Cloudflare tunnel and store its token in the fleet's secrets, then run fleet-sync on the manager so the hub starts its Cloudflare connector. The private dashboard is unaffected.`,
144
+ },
145
+ };
146
+ }
147
+
148
+ function fail(id: string, summary: string, action: string): Check {
149
+ return { id, status: 'fail', summary, remedy: { action } };
150
+ }
151
+
152
+ /** Explicit developer scope; the ordinary health command still audits all peers. */
153
+ export function managedHealthScope(
154
+ tailnet: Tailnet,
155
+ roster: FleetNode[],
156
+ includeManager = true
157
+ ) {
158
+ const canonical = definition;
159
+ const declared = roster.filter((node) =>
160
+ canonical.workers.some((entry) => entry.id === node.id)
161
+ );
162
+ const nodes = declared.length
163
+ ? enrolledRoster(canonical, { version: 1, nodes: declared }).nodes
164
+ : [];
165
+ const computers = tailnet.computers.filter(
166
+ (peer) =>
167
+ (includeManager && peer.self) ||
168
+ canonical.workers.some(
169
+ (node) =>
170
+ peer.dns === node.network?.tailscaleName ||
171
+ peer.dns.split('.')[0] === node.ssh ||
172
+ nodes.some(
173
+ (entry) => entry.id === node.id && entry.tailscaleId === peer.id
174
+ )
175
+ )
176
+ );
177
+ const missing = canonical.workers.filter(
178
+ (node) => !nodes.some((entry) => entry.id === node.id)
179
+ );
180
+ return {
181
+ nodes,
182
+ tailnet: {
183
+ ...tailnet,
184
+ computers,
185
+ checks: [
186
+ ...tailnet.checks,
187
+ ...missing.map((node) => ({
188
+ id: `fleet:enrollment:${node.id}`,
189
+ status: 'fail' as const,
190
+ summary:
191
+ 'Canonical developer node lacks a verified SSH/check-out registration.',
192
+ remedy: {
193
+ action: `Enroll ${node.id} with devfleet fleet-node-add, or use interactive fleet-sync enrollment.`,
194
+ },
195
+ })),
196
+ ],
197
+ },
198
+ excluded: tailnet.computers
199
+ .filter((peer) => !computers.includes(peer))
200
+ .map((peer) => peer.dns),
201
+ };
202
+ }
203
+
204
+ export async function appendManagerLinks(
205
+ reports: NodeReport[],
206
+ registered: FleetNode[],
207
+ run: Runner
208
+ ): Promise<void> {
209
+ for (const node of registered) {
210
+ const machine = definition.workers.find((entry) => entry.id === node.id);
211
+ const report = reports.find(
212
+ (entry) =>
213
+ entry.node === machine?.network?.tailscaleName || entry.node === node.id
214
+ );
215
+ if (!report) continue;
216
+ report.checks.push(...(await verifyT3Endpoint(node, run)));
217
+ report.checks.push(await verifyT3Linked(node, run));
218
+ }
219
+ }