@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,358 @@
1
+ /**
2
+ * `bun run bootstrap`: the one-time npm setup for publishing this package.
3
+ *
4
+ * npm can publish new versions through OIDC but cannot create a package that
5
+ * does not exist yet. So once, with your own npm login: publish the seed
6
+ * version, make the Release workflow the package's trusted publisher, and tag
7
+ * the commit that was published. Every step measures first and skips what is
8
+ * already done, so it is safe to run again after any interruption.
9
+ *
10
+ * bun run bootstrap walk through and do it
11
+ * bun run bootstrap --yes do not ask before publishing
12
+ * bun run bootstrap --skip-check do not run `bun run check` first
13
+ */
14
+ import { spawnSync } from 'node:child_process';
15
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
16
+ import { tmpdir } from 'node:os';
17
+ import { join, resolve } from 'node:path';
18
+ import { confirm } from '@inquirer/prompts';
19
+
20
+ export const SEED_VERSION = '0.1.0';
21
+ export const WORKFLOW_FILE = 'release.yml';
22
+ const MIN_NPM = [11, 10] as const;
23
+
24
+ export interface Exec {
25
+ /** Run quietly and return the output. */
26
+ capture(command: string[], cwd?: string): Result;
27
+ /** Run with the terminal attached, for prompts and browser sign-in. */
28
+ interactive(command: string[], cwd?: string): boolean;
29
+ }
30
+
31
+ export interface Result {
32
+ ok: boolean;
33
+ stdout: string;
34
+ stderr: string;
35
+ }
36
+
37
+ export interface Target {
38
+ /** `@scope/name` from package.json. */
39
+ name: string;
40
+ /** `owner/repo` from package.json. */
41
+ repo: string;
42
+ root: string;
43
+ }
44
+
45
+ export interface State {
46
+ npmVersion: string | null;
47
+ user: string | null;
48
+ published: boolean;
49
+ /** Null until the package exists: there is nothing to look at yet. */
50
+ trusted: boolean | null;
51
+ tagLocal: boolean;
52
+ tagRemote: boolean;
53
+ /** Commit npm recorded for the seed version, when it was published. */
54
+ publishedCommit: string | null;
55
+ clean: boolean;
56
+ onMain: boolean;
57
+ }
58
+
59
+ export type StepId = 'login' | 'publish' | 'trust' | 'tag';
60
+
61
+ export interface Step {
62
+ id: StepId;
63
+ title: string;
64
+ done: boolean;
65
+ /** Why the step cannot run right now. */
66
+ blocked: string | null;
67
+ }
68
+
69
+ export const seedTag = `v${SEED_VERSION}`;
70
+
71
+ export function readTarget(root: string): Target {
72
+ const manifest = JSON.parse(
73
+ readFileSync(join(root, 'package.json'), 'utf8')
74
+ ) as { name: string; repository: string };
75
+ return {
76
+ name: manifest.name,
77
+ repo: manifest.repository.replace(/^github:/, ''),
78
+ root,
79
+ };
80
+ }
81
+
82
+ export function npmIsRecent(version: string | null): boolean {
83
+ const [major = 0, minor = 0] = (version ?? '').split('.').map(Number);
84
+ return major > MIN_NPM[0] || (major === MIN_NPM[0] && minor >= MIN_NPM[1]);
85
+ }
86
+
87
+ export function measure(exec: Exec, target: Target): State {
88
+ const { name, repo, root } = target;
89
+ const npm = exec.capture(['npm', '--version']);
90
+ const whoami = exec.capture(['npm', 'whoami']);
91
+ const view = exec.capture([
92
+ 'npm',
93
+ 'view',
94
+ `${name}@${SEED_VERSION}`,
95
+ 'gitHead',
96
+ ]);
97
+ const any = exec.capture(['npm', 'view', name, 'name']);
98
+ if (!any.ok && !/E404/.test(any.stderr))
99
+ throw new Error('Could not reach the npm registry. Check the network.');
100
+ const published = any.ok;
101
+ const trust = published
102
+ ? exec.capture(['npm', 'trust', 'list', name, '--json'])
103
+ : null;
104
+ exec.capture(['git', 'fetch', '--quiet', 'origin', 'main', '--tags'], root);
105
+ const head = exec.capture(['git', 'rev-parse', 'HEAD'], root).stdout.trim();
106
+ const main = exec
107
+ .capture(['git', 'rev-parse', 'origin/main'], root)
108
+ .stdout.trim();
109
+ return {
110
+ npmVersion: npm.ok ? npm.stdout.trim() : null,
111
+ user: whoami.ok ? whoami.stdout.trim() : null,
112
+ published,
113
+ trusted: trust
114
+ ? trust.ok &&
115
+ trust.stdout.includes(repo) &&
116
+ trust.stdout.includes(WORKFLOW_FILE)
117
+ : null,
118
+ tagLocal: exec.capture(
119
+ ['git', 'rev-parse', '--verify', '--quiet', `refs/tags/${seedTag}`],
120
+ root
121
+ ).ok,
122
+ tagRemote:
123
+ exec
124
+ .capture(
125
+ ['git', 'ls-remote', '--tags', 'origin', `refs/tags/${seedTag}`],
126
+ root
127
+ )
128
+ .stdout.trim() !== '',
129
+ publishedCommit: view.ok ? view.stdout.trim() || null : null,
130
+ clean:
131
+ exec.capture(['git', 'status', '--porcelain'], root).stdout.trim() === '',
132
+ onMain: head !== '' && head === main,
133
+ };
134
+ }
135
+
136
+ export function plan(state: State): Step[] {
137
+ const needsLogin = state.user === null;
138
+ const publishBlocked = !state.clean
139
+ ? 'The working tree has uncommitted changes; the seed is published from a commit.'
140
+ : !state.onMain
141
+ ? 'HEAD is not origin/main. Merge the change, then run this on main.'
142
+ : null;
143
+ return [
144
+ {
145
+ id: 'login',
146
+ title: 'Sign in to npm',
147
+ done: !needsLogin,
148
+ blocked: null,
149
+ },
150
+ {
151
+ id: 'publish',
152
+ title: `Publish ${SEED_VERSION}, the seed version`,
153
+ done: state.published,
154
+ blocked: state.published ? null : publishBlocked,
155
+ },
156
+ {
157
+ id: 'trust',
158
+ title: `Trust ${WORKFLOW_FILE} to publish every later version`,
159
+ done: state.trusted === true,
160
+ blocked: null,
161
+ },
162
+ {
163
+ id: 'tag',
164
+ title: `Tag ${seedTag} on the commit that was published`,
165
+ // npm recorded no commit for a seed published without one, so there is
166
+ // nothing to tag: the Release workflow then counts from the newest
167
+ // version on npm (bootstrap/release-version.ts).
168
+ done:
169
+ state.tagRemote || (state.published && state.publishedCommit === null),
170
+ blocked: null,
171
+ },
172
+ ];
173
+ }
174
+
175
+ export interface Deps {
176
+ exec: Exec;
177
+ /** Publish the seed version from a checkout of HEAD. */
178
+ seed(target: Target): boolean;
179
+ log(line: string): void;
180
+ ask(question: string): Promise<boolean>;
181
+ }
182
+
183
+ interface Options {
184
+ yes: boolean;
185
+ skipCheck: boolean;
186
+ }
187
+
188
+ function show(deps: Deps, state: State, steps: Step[]): void {
189
+ deps.log(
190
+ `npm ${state.npmVersion ?? 'missing'} · ${state.user ? `signed in as ${state.user}` : 'not signed in'}`
191
+ );
192
+ for (const step of steps)
193
+ deps.log(
194
+ `${step.done ? '✓' : step.blocked ? '✗' : '→'} ${step.title}${step.blocked ? ` (${step.blocked})` : ''}`
195
+ );
196
+ }
197
+
198
+ /** Publish from a throwaway checkout of HEAD so the working tree is never
199
+ * edited and npm records the commit it published.
200
+ */
201
+ function publishSeed(exec: Exec, target: Target): boolean {
202
+ const dir = mkdtempSync(join(tmpdir(), 'dev-fleet-seed-'));
203
+ const checkout = join(dir, 'checkout');
204
+ try {
205
+ exec.capture(
206
+ ['git', 'worktree', 'add', '--detach', checkout, 'HEAD'],
207
+ target.root
208
+ );
209
+ const file = join(checkout, 'package.json');
210
+ const manifest = JSON.parse(readFileSync(file, 'utf8')) as object;
211
+ // As the Release workflow does: the package names the commit it is.
212
+ const gitHead = exec
213
+ .capture(['git', 'rev-parse', 'HEAD'], checkout)
214
+ .stdout.trim();
215
+ writeFileSync(
216
+ file,
217
+ `${JSON.stringify({ ...manifest, version: SEED_VERSION, gitHead }, null, 2)}\n`
218
+ );
219
+ return exec.interactive(['npm', 'publish', '--access', 'public'], checkout);
220
+ } finally {
221
+ exec.capture(
222
+ ['git', 'worktree', 'remove', '--force', checkout],
223
+ target.root
224
+ );
225
+ rmSync(dir, { recursive: true, force: true });
226
+ }
227
+ }
228
+
229
+ function tagPublished(exec: Exec, target: Target, state: State): boolean {
230
+ const commit =
231
+ state.publishedCommit ??
232
+ exec
233
+ .capture(['npm', 'view', `${target.name}@${SEED_VERSION}`, 'gitHead'])
234
+ .stdout.trim();
235
+ if (!commit) return false;
236
+ if (!state.tagLocal)
237
+ exec.capture(['git', 'tag', seedTag, commit], target.root);
238
+ return exec.interactive(
239
+ ['git', 'push', 'origin', `refs/tags/${seedTag}`],
240
+ target.root
241
+ );
242
+ }
243
+
244
+ export async function bootstrap(
245
+ deps: Deps,
246
+ target: Target,
247
+ options: Options
248
+ ): Promise<number> {
249
+ const { exec, log } = deps;
250
+ let state = measure(exec, target);
251
+ const steps = plan(state);
252
+ show(deps, state, steps);
253
+ if (!npmIsRecent(state.npmVersion)) {
254
+ log(
255
+ `npm ${MIN_NPM.join('.')} or newer is required (for "npm trust"). Run: npm install -g npm@latest`
256
+ );
257
+ return 1;
258
+ }
259
+ const todo = steps.filter((step) => !step.done);
260
+ if (todo.length === 0) {
261
+ log('Nothing to do: publishing is bootstrapped.');
262
+ return 0;
263
+ }
264
+ const blocked = todo.find((step) => step.blocked);
265
+ if (blocked) return 1;
266
+ if (
267
+ !options.yes &&
268
+ !(await deps.ask(`Run ${todo.length} step(s) as listed?`))
269
+ )
270
+ return 130;
271
+ if (todo.some((s) => s.id === 'publish') && !options.skipCheck) {
272
+ log('Running bun run check before publishing…');
273
+ if (!exec.interactive(['bun', 'run', 'check'], target.root)) return 1;
274
+ }
275
+ for (const step of todo) {
276
+ log(`→ ${step.title}`);
277
+ if (!(await run(step.id, deps, target, state))) {
278
+ log(`✗ ${step.title} failed; run bun run bootstrap again to continue.`);
279
+ return 1;
280
+ }
281
+ state = measure(exec, target);
282
+ }
283
+ show(deps, state, plan(state));
284
+ return plan(state).every((step) => step.done) ? 0 : 1;
285
+ }
286
+
287
+ async function run(
288
+ id: StepId,
289
+ deps: Deps,
290
+ target: Target,
291
+ state: State
292
+ ): Promise<boolean> {
293
+ const { exec } = deps;
294
+ switch (id) {
295
+ case 'login':
296
+ return exec.interactive(['npm', 'login', '--auth-type=web']);
297
+ case 'publish':
298
+ return deps.seed(target);
299
+ case 'trust':
300
+ return exec.interactive([
301
+ 'npm',
302
+ 'trust',
303
+ 'github',
304
+ target.name,
305
+ '--repo',
306
+ target.repo,
307
+ '--file',
308
+ WORKFLOW_FILE,
309
+ '--allow-publish',
310
+ '--yes',
311
+ ]);
312
+ case 'tag':
313
+ return tagPublished(exec, target, state);
314
+ }
315
+ }
316
+
317
+ const system: Exec = {
318
+ capture(command, cwd) {
319
+ const result = spawnSync(command[0]!, command.slice(1), {
320
+ ...(cwd ? { cwd } : {}),
321
+ encoding: 'utf8',
322
+ });
323
+ return {
324
+ ok: result.status === 0,
325
+ stdout: result.stdout ?? '',
326
+ stderr: result.stderr ?? '',
327
+ };
328
+ },
329
+ interactive(command, cwd) {
330
+ const result = spawnSync(command[0]!, command.slice(1), {
331
+ ...(cwd ? { cwd } : {}),
332
+ stdio: 'inherit',
333
+ });
334
+ return result.status === 0;
335
+ },
336
+ };
337
+
338
+ if (import.meta.main) {
339
+ const args = process.argv.slice(2);
340
+ const options: Options = {
341
+ yes: args.includes('--yes'),
342
+ skipCheck: args.includes('--skip-check'),
343
+ };
344
+ if (!options.yes && !process.stdin.isTTY) {
345
+ console.error('Not a terminal: pass --yes to run unattended.');
346
+ process.exit(2);
347
+ }
348
+ process.exitCode = await bootstrap(
349
+ {
350
+ exec: system,
351
+ seed: (target) => publishSeed(system, target),
352
+ log: (line) => console.log(line),
353
+ ask: (message) => confirm({ message, default: true }),
354
+ },
355
+ readTarget(resolve(import.meta.dir, '..')),
356
+ options
357
+ );
358
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Prints the version the Release workflow should publish for `HEAD`, or
3
+ * nothing when there is nothing to release (the reason goes to stderr).
4
+ *
5
+ * The newest `vX.Y.Z` tag is the last release. Without one, the newest
6
+ * version on npm is: it was published by hand before the workflow tagged
7
+ * anything, and npm may not have recorded its commit, so every commit up to
8
+ * `HEAD` counts. Commits since the last release pick the bump, Conventional
9
+ * Commits style:
10
+ *
11
+ * `feat!:` / `BREAKING CHANGE:` major (minor while the major is 0)
12
+ * `feat:` minor
13
+ * anything else patch
14
+ *
15
+ * Exits 1 when nothing can be released because publishing was never set up
16
+ * (no tag and nothing on npm), so the Release workflow does not report a
17
+ * skipped release as a success.
18
+ *
19
+ * bun bootstrap/release-version.ts
20
+ */
21
+ import { spawnSync } from 'node:child_process';
22
+ import { appendFileSync, readFileSync } from 'node:fs';
23
+ import { join } from 'node:path';
24
+
25
+ export interface Git {
26
+ /** Run git and return stdout, or null when it exits non-zero. */
27
+ (args: string[]): string | null;
28
+ }
29
+
30
+ export type Decision =
31
+ | { release: true; version: string; previous: string }
32
+ | { release: false; reason: string; setup?: true };
33
+
34
+ const TAG = /^v(\d+)\.(\d+)\.(\d+)$/;
35
+ const BREAKING_SUBJECT = /^\w+(\([^)]*\))?!:/;
36
+ const BREAKING_FOOTER = /^BREAKING[ -]CHANGE:/m;
37
+ const FEATURE = /^feat(\([^)]*\))?:/;
38
+
39
+ export type Bump = 'major' | 'minor' | 'patch';
40
+
41
+ export function bumpFor(messages: string[]): Bump {
42
+ let bump: Bump = 'patch';
43
+ for (const message of messages) {
44
+ const subject = message.split('\n', 1)[0]!.trim();
45
+ if (BREAKING_SUBJECT.test(subject) || BREAKING_FOOTER.test(message))
46
+ return 'major';
47
+ if (FEATURE.test(subject)) bump = 'minor';
48
+ }
49
+ return bump;
50
+ }
51
+
52
+ export function bumpVersion(tag: string, bump: Bump): string {
53
+ const match = TAG.exec(tag);
54
+ if (!match) throw new Error(`Not a release tag: ${tag}`);
55
+ const [major, minor, patch] = match.slice(1).map(Number) as [
56
+ number,
57
+ number,
58
+ number,
59
+ ];
60
+ if (bump === 'major' && major > 0) return `${major + 1}.0.0`;
61
+ if (bump === 'patch') return `${major}.${minor}.${patch + 1}`;
62
+ return `${major}.${minor + 1}.0`;
63
+ }
64
+
65
+ export function latestTag(git: Git): string | null {
66
+ const tags = git(['tag', '--list', 'v*', '--sort=-v:refname']) ?? '';
67
+ return (
68
+ tags
69
+ .split('\n')
70
+ .map((tag) => tag.trim())
71
+ .find((tag) => TAG.test(tag)) ?? null
72
+ );
73
+ }
74
+
75
+ /** The newest stable `X.Y.Z` of these npm versions, or null. */
76
+ export function newestPublished(versions: string[]): string | null {
77
+ const stable = versions
78
+ .filter((version) => TAG.test(`v${version}`))
79
+ .map((version) => version.split('.').map(Number) as number[]);
80
+ stable.sort((a, b) => b[0]! - a[0]! || b[1]! - a[1]! || b[2]! - a[2]!);
81
+ return stable[0]?.join('.') ?? null;
82
+ }
83
+
84
+ /**
85
+ * `published` is the newest stable version on npm, or null when the package
86
+ * is not there.
87
+ */
88
+ export function decide(git: Git, published: string | null = null): Decision {
89
+ const tag = latestTag(git);
90
+ if (!tag && !published)
91
+ return {
92
+ release: false,
93
+ setup: true,
94
+ reason:
95
+ 'No vX.Y.Z tag and nothing on npm yet: run `bun run bootstrap` once first.',
96
+ };
97
+ const previous = tag ?? `v${published}`;
98
+ if (tag && git(['merge-base', '--is-ancestor', 'HEAD', tag]) !== null)
99
+ return { release: false, reason: `HEAD is already in ${tag}.` };
100
+ const log =
101
+ git(['log', '--format=%B%x00', tag ? `${tag}..HEAD` : 'HEAD']) ?? '';
102
+ const messages = log
103
+ .split('\0')
104
+ .map((message) => message.trim())
105
+ .filter(Boolean);
106
+ if (messages.length === 0)
107
+ return { release: false, reason: `No commits since ${previous}.` };
108
+ return {
109
+ release: true,
110
+ version: bumpVersion(previous, bumpFor(messages)),
111
+ previous,
112
+ };
113
+ }
114
+
115
+ const system: Git = (args) => {
116
+ const result = spawnSync('git', args, { encoding: 'utf8' });
117
+ return result.status === 0 ? (result.stdout ?? '') : null;
118
+ };
119
+
120
+ /** The versions of the package on npm; empty when it is not there. */
121
+ function npmVersions(): string[] {
122
+ const name = (
123
+ JSON.parse(
124
+ readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf8')
125
+ ) as { name: string }
126
+ ).name;
127
+ const result = spawnSync('npm', ['view', name, 'versions', '--json'], {
128
+ encoding: 'utf8',
129
+ });
130
+ if (result.status !== 0) return [];
131
+ const versions: unknown = JSON.parse(result.stdout || '[]');
132
+ return Array.isArray(versions)
133
+ ? versions.filter((v): v is string => typeof v === 'string')
134
+ : typeof versions === 'string'
135
+ ? [versions]
136
+ : [];
137
+ }
138
+
139
+ if (import.meta.main) {
140
+ const decision = decide(system, newestPublished(npmVersions()));
141
+ const summary = process.env['GITHUB_STEP_SUMMARY'];
142
+ const note = (line: string) => {
143
+ if (summary) appendFileSync(summary, `${line}\n`);
144
+ };
145
+ if (decision.release) {
146
+ console.error(`${decision.previous} -> v${decision.version}`);
147
+ note(`Releasing v${decision.version} (after ${decision.previous}).`);
148
+ console.log(decision.version);
149
+ } else if (decision.setup) {
150
+ console.error(`::error::${decision.reason}`);
151
+ note(`**Nothing was released.** ${decision.reason}`);
152
+ process.exitCode = 1;
153
+ } else {
154
+ console.error(decision.reason);
155
+ note(`Nothing to release: ${decision.reason}`);
156
+ }
157
+ }
@@ -0,0 +1,51 @@
1
+ /** Nonsecret installer evidence; never contains commands, paths or raw errors. */
2
+ import { lstatSync, writeFileSync } from 'node:fs';
3
+ import { isAbsolute } from 'node:path';
4
+ export type SetupPhase =
5
+ | 'managed-config'
6
+ | 'mise'
7
+ | 'aws'
8
+ | 't3'
9
+ | 'global-config'
10
+ | 'shell'
11
+ | 'dependencies';
12
+ export function setupFailureCode(error: unknown): string | undefined {
13
+ const known: Record<string, string> = {
14
+ 'Pinned T3 artifact unavailable.': 'T3_DOWNLOAD_UNAVAILABLE',
15
+ 'T3 artifact checksum mismatch.': 'T3_CHECKSUM_MISMATCH',
16
+ 'T3 cached artifact drift; preserve running service and review maintenance recovery.':
17
+ 'T3_CACHE_DRIFT',
18
+ 'T3 extraction failed.': 'T3_EXTRACTION_FAILED',
19
+ 'Managed T3 files differ from the pinned artifact; maintenance is required.':
20
+ 'T3_INSTALLATION_DRIFT',
21
+ 'Pinned T3 executable verification failed.': 'T3_EXECUTABLE_FAILED',
22
+ };
23
+ return error instanceof Error && Object.hasOwn(known, error.message)
24
+ ? known[error.message]
25
+ : undefined;
26
+ }
27
+ export function setupStatus(path: string | undefined) {
28
+ if (path && !isAbsolute(path))
29
+ throw new Error('Setup status path must be absolute.');
30
+ return (
31
+ phase: SetupPhase,
32
+ status: 'running' | 'failed' | 'passed',
33
+ code?: string
34
+ ): void => {
35
+ if (!path) return;
36
+ try {
37
+ if (!lstatSync(path).isFile())
38
+ throw new Error('Unsafe setup status file.');
39
+ } catch (error) {
40
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
41
+ }
42
+ writeFileSync(
43
+ path,
44
+ JSON.stringify({ version: 1, phase, status, ...(code ? { code } : {}) }) +
45
+ '\n',
46
+ {
47
+ mode: 0o600,
48
+ }
49
+ );
50
+ };
51
+ }
@@ -0,0 +1,88 @@
1
+ import { workerTools } from './worker-tools';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ /** Used by both OS bootstraps, dev-fleet tools sync and dev-fleet update. No workspace dependencies. */
5
+ import { setupStatus, setupFailureCode, type SetupPhase } from './setup-status';
6
+ import { spawnSync } from 'node:child_process';
7
+ import {
8
+ assertManagedConfig,
9
+ globalConfigDir,
10
+ publishGlobalConfig,
11
+ workspaceRoot,
12
+ } from './global-config';
13
+ import { configureShellPath } from './shell-path';
14
+ import { installNativeAws } from './native-aws';
15
+ import { installNativeT3 } from './native-t3';
16
+
17
+ function run(args: string[]): void {
18
+ const result = spawnSync(args[0]!, args.slice(1), {
19
+ cwd: workspaceRoot,
20
+ stdio: 'inherit',
21
+ });
22
+ if (result.error) throw result.error;
23
+ if (result.status !== 0)
24
+ throw new Error(`${args.join(' ')} failed (${result.status})`);
25
+ }
26
+
27
+ export async function syncTools(
28
+ observe: (phase: SetupPhase) => void = () => {},
29
+ worker = false
30
+ ): Promise<void> {
31
+ observe('managed-config');
32
+ const dir = globalConfigDir();
33
+ assertManagedConfig(dir);
34
+ observe('mise');
35
+ const config = Bun.TOML.parse(
36
+ readFileSync(join(workspaceRoot, 'mise.toml'), 'utf8')
37
+ ) as { tools: Record<string, unknown>; vars: { worker_tools: unknown } };
38
+ const selected = worker
39
+ ? Object.keys(workerTools(config.tools, config.vars.worker_tools))
40
+ : [];
41
+ run(['mise', 'install', '--locked', ...selected]);
42
+ observe('aws');
43
+ if (!worker) await installNativeAws();
44
+ observe('t3');
45
+ await installNativeT3();
46
+ observe('global-config');
47
+ publishGlobalConfig(dir);
48
+ run(['mise', 'reshim']);
49
+ observe('shell');
50
+ configureShellPath();
51
+ }
52
+
53
+ if (import.meta.main) {
54
+ const flag = process.argv.indexOf('--status-file');
55
+ const report = setupStatus(flag < 0 ? undefined : process.argv[flag + 1]);
56
+ let phase: SetupPhase = 'managed-config';
57
+ try {
58
+ await syncTools((current) => {
59
+ phase = current;
60
+ report(phase, 'running');
61
+ }, process.argv.includes('--worker'));
62
+ if (!process.argv.includes('--tools-only')) {
63
+ phase = 'dependencies';
64
+ report(phase, 'running');
65
+ run([
66
+ 'mise',
67
+ 'exec',
68
+ '--locked',
69
+ '--',
70
+ 'bun',
71
+ 'install',
72
+ '--frozen-lockfile',
73
+ ]);
74
+ }
75
+ report(phase, 'passed');
76
+ } catch (error) {
77
+ report(phase, 'failed', setupFailureCode(error));
78
+ throw new Error(
79
+ `Shared toolchain phase ${phase} failed; inspect the declared installer prerequisites and retry.`
80
+ );
81
+ }
82
+ console.log(
83
+ 'Shared toolchain installed globally. Reopen terminals/editors, or refresh this terminal with: exec "$SHELL" -l (Unix).'
84
+ );
85
+ console.log(
86
+ 'Verify: mise exec -- bun bootstrap/doctor.ts (add --docker to require a running Docker engine).'
87
+ );
88
+ }