@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,350 @@
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
+ done: state.tagRemote,
166
+ blocked: null,
167
+ },
168
+ ];
169
+ }
170
+
171
+ export interface Deps {
172
+ exec: Exec;
173
+ /** Publish the seed version from a checkout of HEAD. */
174
+ seed(target: Target): boolean;
175
+ log(line: string): void;
176
+ ask(question: string): Promise<boolean>;
177
+ }
178
+
179
+ interface Options {
180
+ yes: boolean;
181
+ skipCheck: boolean;
182
+ }
183
+
184
+ function show(deps: Deps, state: State, steps: Step[]): void {
185
+ deps.log(
186
+ `npm ${state.npmVersion ?? 'missing'} · ${state.user ? `signed in as ${state.user}` : 'not signed in'}`
187
+ );
188
+ for (const step of steps)
189
+ deps.log(
190
+ `${step.done ? '✓' : step.blocked ? '✗' : '→'} ${step.title}${step.blocked ? ` (${step.blocked})` : ''}`
191
+ );
192
+ }
193
+
194
+ /** Publish from a throwaway checkout of HEAD so the working tree is never
195
+ * edited and npm records the commit it published.
196
+ */
197
+ function publishSeed(exec: Exec, target: Target): boolean {
198
+ const dir = mkdtempSync(join(tmpdir(), 'dev-fleet-seed-'));
199
+ const checkout = join(dir, 'checkout');
200
+ try {
201
+ exec.capture(
202
+ ['git', 'worktree', 'add', '--detach', checkout, 'HEAD'],
203
+ target.root
204
+ );
205
+ const file = join(checkout, 'package.json');
206
+ const manifest = JSON.parse(readFileSync(file, 'utf8')) as object;
207
+ writeFileSync(
208
+ file,
209
+ `${JSON.stringify({ ...manifest, version: SEED_VERSION }, null, 2)}\n`
210
+ );
211
+ return exec.interactive(['npm', 'publish', '--access', 'public'], checkout);
212
+ } finally {
213
+ exec.capture(
214
+ ['git', 'worktree', 'remove', '--force', checkout],
215
+ target.root
216
+ );
217
+ rmSync(dir, { recursive: true, force: true });
218
+ }
219
+ }
220
+
221
+ function tagPublished(exec: Exec, target: Target, state: State): boolean {
222
+ const commit =
223
+ state.publishedCommit ??
224
+ exec
225
+ .capture(['npm', 'view', `${target.name}@${SEED_VERSION}`, 'gitHead'])
226
+ .stdout.trim();
227
+ if (!commit) return false;
228
+ if (!state.tagLocal)
229
+ exec.capture(['git', 'tag', seedTag, commit], target.root);
230
+ return exec.interactive(
231
+ ['git', 'push', 'origin', `refs/tags/${seedTag}`],
232
+ target.root
233
+ );
234
+ }
235
+
236
+ export async function bootstrap(
237
+ deps: Deps,
238
+ target: Target,
239
+ options: Options
240
+ ): Promise<number> {
241
+ const { exec, log } = deps;
242
+ let state = measure(exec, target);
243
+ const steps = plan(state);
244
+ show(deps, state, steps);
245
+ if (!npmIsRecent(state.npmVersion)) {
246
+ log(
247
+ `npm ${MIN_NPM.join('.')} or newer is required (for "npm trust"). Run: npm install -g npm@latest`
248
+ );
249
+ return 1;
250
+ }
251
+ const todo = steps.filter((step) => !step.done);
252
+ if (todo.length === 0) {
253
+ log('Nothing to do: publishing is bootstrapped.');
254
+ return 0;
255
+ }
256
+ const blocked = todo.find((step) => step.blocked);
257
+ if (blocked) return 1;
258
+ if (
259
+ !options.yes &&
260
+ !(await deps.ask(`Run ${todo.length} step(s) as listed?`))
261
+ )
262
+ return 130;
263
+ if (todo.some((s) => s.id === 'publish') && !options.skipCheck) {
264
+ log('Running bun run check before publishing…');
265
+ if (!exec.interactive(['bun', 'run', 'check'], target.root)) return 1;
266
+ }
267
+ for (const step of todo) {
268
+ log(`→ ${step.title}`);
269
+ if (!(await run(step.id, deps, target, state))) {
270
+ log(`✗ ${step.title} failed; run bun run bootstrap again to continue.`);
271
+ return 1;
272
+ }
273
+ state = measure(exec, target);
274
+ }
275
+ show(deps, state, plan(state));
276
+ return plan(state).every((step) => step.done) ? 0 : 1;
277
+ }
278
+
279
+ async function run(
280
+ id: StepId,
281
+ deps: Deps,
282
+ target: Target,
283
+ state: State
284
+ ): Promise<boolean> {
285
+ const { exec } = deps;
286
+ switch (id) {
287
+ case 'login':
288
+ return exec.interactive(['npm', 'login', '--auth-type=web']);
289
+ case 'publish':
290
+ return deps.seed(target);
291
+ case 'trust':
292
+ return exec.interactive([
293
+ 'npm',
294
+ 'trust',
295
+ 'github',
296
+ target.name,
297
+ '--repo',
298
+ target.repo,
299
+ '--file',
300
+ WORKFLOW_FILE,
301
+ '--allow-publish',
302
+ '--yes',
303
+ ]);
304
+ case 'tag':
305
+ return tagPublished(exec, target, state);
306
+ }
307
+ }
308
+
309
+ const system: Exec = {
310
+ capture(command, cwd) {
311
+ const result = spawnSync(command[0]!, command.slice(1), {
312
+ ...(cwd ? { cwd } : {}),
313
+ encoding: 'utf8',
314
+ });
315
+ return {
316
+ ok: result.status === 0,
317
+ stdout: result.stdout ?? '',
318
+ stderr: result.stderr ?? '',
319
+ };
320
+ },
321
+ interactive(command, cwd) {
322
+ const result = spawnSync(command[0]!, command.slice(1), {
323
+ ...(cwd ? { cwd } : {}),
324
+ stdio: 'inherit',
325
+ });
326
+ return result.status === 0;
327
+ },
328
+ };
329
+
330
+ if (import.meta.main) {
331
+ const args = process.argv.slice(2);
332
+ const options: Options = {
333
+ yes: args.includes('--yes'),
334
+ skipCheck: args.includes('--skip-check'),
335
+ };
336
+ if (!options.yes && !process.stdin.isTTY) {
337
+ console.error('Not a terminal: pass --yes to run unattended.');
338
+ process.exit(2);
339
+ }
340
+ process.exitCode = await bootstrap(
341
+ {
342
+ exec: system,
343
+ seed: (target) => publishSeed(system, target),
344
+ log: (line) => console.log(line),
345
+ ask: (message) => confirm({ message, default: true }),
346
+ },
347
+ readTarget(resolve(import.meta.dir, '..')),
348
+ options
349
+ );
350
+ }
@@ -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
+ }
@@ -0,0 +1,105 @@
1
+ /** Persistent shims make the same tool commands work in interactive shells and agents. */
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+
7
+ export function appendOnce(path: string, block: string): void {
8
+ const current = existsSync(path) ? readFileSync(path, 'utf8') : '';
9
+ if (current.includes(block.trim())) return;
10
+ mkdirSync(dirname(path), { recursive: true });
11
+ writeFileSync(path, `${current}\n${block}\n`);
12
+ }
13
+
14
+ export function miseDataDir(
15
+ platform = process.platform,
16
+ env = process.env,
17
+ home = homedir()
18
+ ): string {
19
+ const fallback =
20
+ platform === 'win32'
21
+ ? env['LOCALAPPDATA'] || join(home, 'AppData', 'Local')
22
+ : join(home, '.local', 'share');
23
+ return env['MISE_DATA_DIR'] || join(env['XDG_DATA_HOME'] || fallback, 'mise');
24
+ }
25
+
26
+ function persistWindowsPath(bin: string, shims: string): void {
27
+ const script =
28
+ "$paths = @($env:WORKSPACE_TOOL_SHIMS, $env:WORKSPACE_TOOL_BIN); $old = [Environment]::GetEnvironmentVariable('Path', 'User'); $new = (($paths + ($old -split ';') | Where-Object { $_ } | Select-Object -Unique) -join ';'); [Environment]::SetEnvironmentVariable('Path', $new, 'User')";
29
+ const result = spawnSync(
30
+ 'powershell.exe',
31
+ ['-NoProfile', '-Command', script],
32
+ {
33
+ stdio: 'inherit',
34
+ env: {
35
+ ...process.env,
36
+ WORKSPACE_TOOL_BIN: bin,
37
+ WORKSPACE_TOOL_SHIMS: shims,
38
+ },
39
+ }
40
+ );
41
+ if (result.status !== 0)
42
+ throw new Error(
43
+ 'Could not persist the user PATH. See docs/machine-setup.md.'
44
+ );
45
+ }
46
+
47
+ export function configurePosixShellPath(
48
+ bin: string,
49
+ shims: string,
50
+ home = homedir(),
51
+ zdotdir = process.env['ZDOTDIR'] || home
52
+ ): void {
53
+ const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
54
+ const paths = quote(`${shims}:${bin}`);
55
+ const legacy = `# dev-fleet toolchain\nexport PATH=${paths}:"$PATH"`;
56
+ const block = `# dev-fleet toolchain\ncase ":$PATH:" in\n *${quote(`:${shims}:${bin}:`)}*) ;;\n *) export PATH=${paths}:"$PATH" ;;\nesac`;
57
+ const targets = new Set([
58
+ join(home, '.profile'),
59
+ ...['.bash_profile', '.bash_login']
60
+ .map((name) => join(home, name))
61
+ .filter(existsSync),
62
+ join(home, '.bashrc'),
63
+ join(zdotdir, '.zshenv'),
64
+ join(zdotdir, '.zshrc'),
65
+ ]);
66
+ // Version managers and vendor installers (nvm, a native Claude installer's
67
+ // ~/.local/bin) prepend their own directories later in these files; the
68
+ // last line puts the pinned shims back in front, so every shell and the
69
+ // apps that inherit its PATH (T3) run the locked tool versions.
70
+ // Guarded like the block above so nested shells do not grow PATH.
71
+ const unguarded = `${TAIL_MARKER}\nexport PATH=${quote(shims)}:"$PATH"\n`;
72
+ const tail = `${TAIL_MARKER}\ncase "$PATH" in\n ${quote(shims)}:*) ;;\n *) export PATH=${quote(shims)}:"$PATH" ;;\nesac\n`;
73
+ for (const path of targets) {
74
+ const current = existsSync(path) ? readFileSync(path, 'utf8') : '';
75
+ const body = current
76
+ .replace(`${block}\n`, '')
77
+ .replace(legacy, '')
78
+ .split(tail)
79
+ .join('')
80
+ .split(unguarded)
81
+ .join('');
82
+ const next = `${block}\n${body.replace(/\n*$/, '\n')}${tail}`;
83
+ if (next === current) continue;
84
+ mkdirSync(dirname(path), { recursive: true });
85
+ writeFileSync(path, next);
86
+ }
87
+ }
88
+
89
+ const TAIL_MARKER = '# pinned tool shims first (dev-fleet, keep this last)';
90
+
91
+ export function configureShellPath(): void {
92
+ const bin = join(homedir(), '.local', 'bin');
93
+ const shims = process.env['MISE_SHIMS_DIR'] || join(miseDataDir(), 'shims');
94
+ if (process.platform === 'win32') {
95
+ persistWindowsPath(bin, shims);
96
+ return;
97
+ }
98
+ configurePosixShellPath(bin, shims);
99
+ const fishQuote = (value: string) =>
100
+ `'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`;
101
+ appendOnce(
102
+ join(homedir(), '.config/fish/config.fish'),
103
+ `# dev-fleet toolchain\nfish_add_path --prepend ${fishQuote(shims)} ${fishQuote(bin)}`
104
+ );
105
+ }
@@ -0,0 +1,49 @@
1
+ """Verify the pinned T3 archive against its user-owned installation.
2
+
3
+ Release-builder numeric identities are not portable. All installed members must
4
+ instead belong to the invoking development user; content, modes and links must
5
+ still match. This reads the archive and installation without modifying either.
6
+ """
7
+ import os
8
+ from pathlib import Path
9
+ import stat
10
+ import sys
11
+ import tarfile
12
+
13
+
14
+ def verify(archive, root):
15
+ root = Path(root).resolve()
16
+ with tarfile.open(archive, 'r:gz') as source:
17
+ for member in source:
18
+ path = root / member.name
19
+ if not path.parent.resolve().is_relative_to(root):
20
+ return False
21
+ observed = path.lstat()
22
+ if observed.st_uid != os.getuid():
23
+ return False
24
+ if member.issym():
25
+ if not stat.S_ISLNK(observed.st_mode) or os.readlink(path) != member.linkname:
26
+ return False
27
+ elif member.isdir():
28
+ if not stat.S_ISDIR(observed.st_mode):
29
+ return False
30
+ elif member.isfile():
31
+ if not stat.S_ISREG(observed.st_mode) or observed.st_size != member.size:
32
+ return False
33
+ if stat.S_IMODE(observed.st_mode) != member.mode:
34
+ return False
35
+ with source.extractfile(member) as expected, path.open('rb') as actual:
36
+ while chunk := expected.read(1024 * 1024):
37
+ if actual.read(len(chunk)) != chunk:
38
+ return False
39
+ else:
40
+ return False
41
+ return True
42
+
43
+
44
+ if __name__ == '__main__':
45
+ try:
46
+ valid = verify(sys.argv[1], sys.argv[2])
47
+ except (OSError, ValueError, tarfile.TarError):
48
+ valid = False
49
+ sys.exit(0 if valid else 1)