@crvouga/dev-fleet 0.1.0 → 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 (110) hide show
  1. package/README.md +237 -37
  2. package/agents/setup.md +42 -12
  3. package/ansible/macos/manager.yml +7 -7
  4. package/ansible/macos/policy.json +2 -10
  5. package/ansible/monitoring/README.md +86 -25
  6. package/ansible/monitoring/agent.yml +40 -2
  7. package/ansible/monitoring/hub-route-darwin.yml +51 -0
  8. package/ansible/monitoring/lock.json +5 -0
  9. package/ansible/pyproject.toml +12 -0
  10. package/ansible/ubuntu/baseline.yml +1 -1
  11. package/ansible/ubuntu/disk-gc.py +336 -0
  12. package/ansible/ubuntu/policy.json +12 -3
  13. package/ansible/ubuntu/tailscale.yml +8 -5
  14. package/ansible/ubuntu/tasks/resources.yml +61 -1
  15. package/ansible/uv.lock +190 -0
  16. package/bootstrap/bootstrap.sh +25 -18
  17. package/bootstrap/doctor.ts +46 -1
  18. package/bootstrap/fleet-manager-run.sh +42 -15
  19. package/bootstrap/fleet-remote-bootstrap.sh +85 -12
  20. package/bootstrap/global-config.ts +64 -6
  21. package/bootstrap/install-launcher.sh +3 -0
  22. package/bootstrap/npm.ts +10 -2
  23. package/bootstrap/release-version.ts +157 -0
  24. package/docs/consumer-setup.md +408 -0
  25. package/docs/declared.md +89 -0
  26. package/docs/machine-setup.md +399 -0
  27. package/docs/operations.md +276 -0
  28. package/docs/releasing.md +103 -0
  29. package/docs/state.md +254 -0
  30. package/docs/topology.md +67 -0
  31. package/docs/ubuntu-sync.md +351 -0
  32. package/docs/worker-contract.md +67 -0
  33. package/examples/fleet.json +5 -0
  34. package/llms.txt +35 -0
  35. package/mise.lock +29 -0
  36. package/mise.toml +12 -1
  37. package/package.json +11 -2
  38. package/schema/fleet.schema.json +71 -15
  39. package/src/agents/fleet-verify.ts +9 -0
  40. package/src/agents/native-verify.ts +167 -22
  41. package/src/agents/provider-errors.ts +9 -2
  42. package/src/agents/registry.ts +7 -1
  43. package/src/cli/agent-guide.ts +39 -5
  44. package/src/cli/app.ts +11 -4
  45. package/src/cli/cli.ts +7 -0
  46. package/src/cli/command-index.ts +7 -0
  47. package/src/cli/fleet-state.ts +499 -0
  48. package/src/cli/fleet-sync-checkout.ts +56 -17
  49. package/src/cli/fleet-sync-enrollment.ts +19 -23
  50. package/src/cli/fleet-sync-forward.ts +1 -1
  51. package/src/cli/fleet-sync-manager.ts +64 -8
  52. package/src/cli/fleet-sync-monitoring.ts +5 -5
  53. package/src/cli/fleet-sync-output.ts +4 -1
  54. package/src/cli/fleet-sync-remote.ts +11 -5
  55. package/src/cli/fleet-sync-state.ts +86 -0
  56. package/src/cli/fleet-sync.ts +123 -57
  57. package/src/cli/fleet.ts +4 -2
  58. package/src/cli/managed-health.ts +4 -4
  59. package/src/cli/node-sync.ts +7 -0
  60. package/src/cli/program.ts +20 -4
  61. package/src/cli/project.ts +51 -0
  62. package/src/cli/register-fleet.ts +51 -1
  63. package/src/cli/registry.ts +28 -1
  64. package/src/cli/roster.ts +46 -30
  65. package/src/cli/settings.ts +15 -12
  66. package/src/cli/source-operations.ts +27 -3
  67. package/src/cli/tailnet.ts +2 -2
  68. package/src/definition.ts +31 -1
  69. package/src/diagnostics.ts +1 -1
  70. package/src/fleet-config.ts +105 -19
  71. package/src/machine/source.ts +3 -3
  72. package/src/machine/state.ts +15 -27
  73. package/src/manager-sync.ts +8 -3
  74. package/src/monitoring/agent-evidence.ts +146 -0
  75. package/src/monitoring/apply.ts +15 -3
  76. package/src/monitoring/beszel.ts +11 -4
  77. package/src/monitoring/enroll.ts +2 -1
  78. package/src/monitoring/hub-access.ts +4 -3
  79. package/src/monitoring/hub-backup.ts +121 -0
  80. package/src/monitoring/hub-viewer.ts +8 -8
  81. package/src/monitoring/hub.ts +3 -3
  82. package/src/monitoring/manager-hub.ts +30 -14
  83. package/src/node-converge-linux.ts +2 -0
  84. package/src/package-info.ts +45 -0
  85. package/src/platform/ansible.ts +19 -15
  86. package/src/platform/resource-verify.ts +8 -4
  87. package/src/platform/ubuntu-audit.ts +5 -2
  88. package/src/remote-bootstrap.ts +17 -19
  89. package/src/repair-catalog.ts +1 -1
  90. package/src/runner.ts +1 -1
  91. package/src/state/bucket.ts +208 -0
  92. package/src/state/check.ts +74 -0
  93. package/src/state/connection.ts +92 -0
  94. package/src/state/enrollment.ts +45 -0
  95. package/src/state/legacy.ts +89 -0
  96. package/src/state/litestream.ts +143 -0
  97. package/src/state/operator.ts +20 -0
  98. package/src/state/paths.ts +40 -0
  99. package/src/state/roster.ts +62 -0
  100. package/src/state/store.ts +169 -0
  101. package/src/state/sync.ts +352 -0
  102. package/src/t3-endpoint.ts +64 -8
  103. package/src/t3-linked-check.ts +23 -1
  104. package/src/toolchain-checks.ts +25 -0
  105. package/src/types.ts +4 -0
  106. package/src/worker-access-checks.ts +56 -3
  107. package/src/worker-host-checks.ts +5 -0
  108. package/src/workstation-checks.ts +1 -1
  109. package/src/index.ts +0 -6
  110. package/src/test-fixtures.ts +0 -36
@@ -16,13 +16,16 @@
16
16
  "git": "1:2.53.0-1ubuntu1.1",
17
17
  "python3": "3.14.3-0ubuntu2",
18
18
  "unzip": "6.0-29ubuntu1",
19
- "xz-utils": "5.8.3-1ubuntu0.1"
19
+ "xz-utils": "5.8.3-1ubuntu0.1",
20
+ "ansible-core": "2.20.1-1"
21
+ },
22
+ "tailscale": {
23
+ "version": "1.104.5",
24
+ "keySha256": "3e03dacf222698c60b8e2f990b809ca1b3e104de127767864284e6c228f1fb39"
20
25
  },
21
26
  "aptLockTimeoutSeconds": 120,
22
27
  "minimumFreeDiskMiB": 4096,
23
28
  "minimumFreeInodes": 10000,
24
- "serviceStartLimitBurst": 5,
25
- "serviceStartLimitIntervalSeconds": 300,
26
29
  "alwaysOnServices": ["ssh", "tailscaled", "docker", "beszel-agent"],
27
30
  "resourceSafety": {
28
31
  "swapFileMiB": 8192,
@@ -37,6 +40,12 @@
37
40
  "diskGuardFloorMiB": 3072,
38
41
  "diskGuardWriteMiB": 32,
39
42
  "diskGuardIntervalSeconds": 5,
43
+ "diskGcIdleDays": 7,
44
+ "diskGcPressureMiB": 15360,
45
+ "diskGcPressureIdleHours": 12,
46
+ "diskGcDockerAgeHours": 168,
47
+ "diskGcPressureDockerAgeHours": 24,
48
+ "diskGcPressureCaches": [".bun/install/cache"],
40
49
  "dockerLogMaxSize": "50m",
41
50
  "dockerLogMaxFiles": "3"
42
51
  }
@@ -4,18 +4,21 @@
4
4
  connection: local
5
5
  become: true
6
6
  gather_facts: true
7
+ # The approved version and the signing key's digest: `tailscale` in policy.json.
8
+ vars_files:
9
+ - policy.json
7
10
  tasks:
8
11
  - name: Require supported Ubuntu
9
12
  ansible.builtin.assert:
10
13
  that:
11
14
  - ansible_facts.distribution == 'Ubuntu'
12
- - ansible_facts.distribution_version == '26.04'
15
+ - ansible_facts.distribution_version == release
13
16
  - name: Inspect installed packages before possible network interruption
14
17
  ansible.builtin.package_facts:
15
18
  manager: apt
16
19
  - name: Require maintenance for an installed version change
17
20
  ansible.builtin.assert:
18
- that: "'tailscale' not in ansible_facts.packages or ansible_facts.packages.tailscale[0].version == '1.104.5'"
21
+ that: "'tailscale' not in ansible_facts.packages or ansible_facts.packages.tailscale[0].version == tailscale.version"
19
22
  fail_msg: TAILSCALE_UPGRADE_PENDING; upgrading an active VPN can disconnect SSH. Review an explicit maintenance operation.
20
23
  - name: Inspect existing package-source ownership before writing
21
24
  ansible.builtin.find:
@@ -34,7 +37,7 @@
34
37
  ansible.builtin.get_url:
35
38
  url: https://pkgs.tailscale.com/stable/ubuntu/resolute.noarmor.gpg
36
39
  dest: /usr/share/keyrings/tailscale-archive-keyring.gpg
37
- checksum: sha256:3e03dacf222698c60b8e2f990b809ca1b3e104de127767864284e6c228f1fb39
40
+ checksum: 'sha256:{{ tailscale.keySha256 }}'
38
41
  owner: root
39
42
  group: root
40
43
  mode: '0644'
@@ -48,11 +51,11 @@
48
51
  register: tailscale_source
49
52
  - name: Install the approved version only when absent
50
53
  ansible.builtin.apt:
51
- name: tailscale=1.104.5
54
+ name: 'tailscale={{ tailscale.version }}'
52
55
  state: present
53
56
  update_cache: true
54
57
  cache_valid_time: 0
55
- lock_timeout: 120
58
+ lock_timeout: '{{ aptLockTimeoutSeconds }}'
56
59
  allow_downgrade: false
57
60
  allow_unauthenticated: false
58
61
  when: "'tailscale' not in ansible_facts.packages"
@@ -2,7 +2,9 @@
2
2
  # Memory and disk safety: a runaway workload is stopped on its own before the
3
3
  # machine, SSH, Tailscale or the T3 server run out of memory or disk. These
4
4
  # tasks run before the capacity preflight because they matter most when a disk
5
- # is nearly full; they only write small files and never delete user data.
5
+ # is nearly full; they only write small files and never delete user data. The
6
+ # disk GC timer they install deletes only build output and caches a build or
7
+ # install recreates (ansible/ubuntu/disk-gc.py).
6
8
  - name: Inspect active swap
7
9
  ansible.builtin.command:
8
10
  argv: [swapon, --show=NAME, --noheadings]
@@ -250,3 +252,61 @@
250
252
  enabled: true
251
253
  state: '{{ "restarted" if disk_guard_unit.changed else "started" }}'
252
254
  when: development_source is defined and disk_guard_source.stat.exists
255
+
256
+ - name: Read the tracked disk GC
257
+ ansible.builtin.stat:
258
+ path: '{{ development_source }}/ansible/ubuntu/disk-gc.py'
259
+ checksum_algorithm: sha256
260
+ register: disk_gc_source
261
+ when: development_source is defined
262
+ - name: Declare the disk GC service and its hourly timer
263
+ ansible.builtin.copy:
264
+ dest: '{{ item.dest }}'
265
+ owner: root
266
+ group: root
267
+ mode: '0644'
268
+ content: '{{ item.content }}'
269
+ loop:
270
+ - dest: /etc/systemd/system/personal-fleet-disk-gc.service
271
+ content: |
272
+ # Managed by personal fleet-sync: ansible/ubuntu/disk-gc.py
273
+ # Source sha256 {{ disk_gc_source.stat.checksum }}
274
+ [Unit]
275
+ Description=Personal Fleet disk GC (deletes build output and caches a build recreates)
276
+ After=docker.service
277
+ [Service]
278
+ Type=oneshot
279
+ ExecStart=/usr/bin/python3 -I -B {{ disk_gc_source.stat.path }} --home {{ development_home }} --path / --path {{ development_home }} --idle-days {{ resourceSafety.diskGcIdleDays }} --pressure-mib {{ resourceSafety.diskGcPressureMiB }} --pressure-idle-hours {{ resourceSafety.diskGcPressureIdleHours }} --docker-age-hours {{ resourceSafety.diskGcDockerAgeHours }} --pressure-docker-age-hours {{ resourceSafety.diskGcPressureDockerAgeHours }}{% for cache in resourceSafety.diskGcPressureCaches %} --pressure-cache {{ cache }}{% endfor %}
280
+
281
+ TimeoutStartSec=2h
282
+ Nice=19
283
+ IOSchedulingClass=idle
284
+ CPUQuota=50%
285
+ MemoryMax=512M
286
+ NoNewPrivileges=true
287
+ PrivateTmp=true
288
+ LogRateLimitIntervalSec=60
289
+ LogRateLimitBurst=200
290
+ - dest: /etc/systemd/system/personal-fleet-disk-gc.timer
291
+ content: |
292
+ # Managed by personal fleet-sync: ansible/ubuntu/disk-gc.py
293
+ [Unit]
294
+ Description=Run the Personal Fleet disk GC hourly
295
+ [Timer]
296
+ OnBootSec=10min
297
+ OnCalendar=hourly
298
+ RandomizedDelaySec=5min
299
+ Persistent=true
300
+ [Install]
301
+ WantedBy=timers.target
302
+ loop_control:
303
+ label: '{{ item.dest }}'
304
+ when: development_source is defined and disk_gc_source.stat.exists
305
+ register: disk_gc_units
306
+ - name: Schedule the disk GC
307
+ ansible.builtin.systemd_service:
308
+ name: personal-fleet-disk-gc.timer
309
+ daemon_reload: '{{ disk_gc_units.changed }}'
310
+ enabled: true
311
+ state: started
312
+ when: development_source is defined and disk_gc_source.stat.exists
@@ -0,0 +1,190 @@
1
+ version = 1
2
+ revision = 5
3
+ requires-python = "==3.13.16"
4
+
5
+ [[package]]
6
+ name = "ansible-core"
7
+ version = "2.19.9"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ dependencies = [
10
+ { name = "cryptography" },
11
+ { name = "jinja2" },
12
+ { name = "packaging" },
13
+ { name = "pyyaml" },
14
+ { name = "resolvelib" },
15
+ ]
16
+ sdist = { url = "https://files.pythonhosted.org/packages/26/5b/161275ff5fa673dfc1329ed19a44331a4f491166a9f0cb907b44037ad72e/ansible_core-2.19.9.tar.gz", hash = "sha256:74107de13d188ff579fb215bc3eb875c9198803215d6378ed588c7f35aba12f5", size = 3431475, upload-time = "2026-04-21T00:47:09.194Z" }
17
+ wheels = [
18
+ { url = "https://files.pythonhosted.org/packages/3d/b9/f2c07e6cdba6a5882342d666ceea4eeeb6fea20ec325fac68c27bfcea722/ansible_core-2.19.9-py3-none-any.whl", hash = "sha256:18de80e3ea5a89d2ea84e4d4a3c6d7c121ea62fc3af2877ba083784e3d90b419", size = 2416269, upload-time = "2026-04-21T00:47:06.777Z" },
19
+ ]
20
+
21
+ [[package]]
22
+ name = "cffi"
23
+ version = "2.1.1"
24
+ source = { registry = "https://pypi.org/simple" }
25
+ dependencies = [
26
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
27
+ ]
28
+ sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" }
29
+ wheels = [
30
+ { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" },
31
+ { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" },
32
+ { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" },
33
+ { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" },
34
+ { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" },
35
+ { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" },
36
+ { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" },
37
+ { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" },
38
+ { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" },
39
+ { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" },
40
+ { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" },
41
+ { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" },
42
+ { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" },
43
+ { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" },
44
+ ]
45
+
46
+ [[package]]
47
+ name = "cryptography"
48
+ version = "50.0.2"
49
+ source = { registry = "https://pypi.org/simple" }
50
+ dependencies = [
51
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
52
+ ]
53
+ sdist = { url = "https://files.pythonhosted.org/packages/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz", hash = "sha256:7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5", size = 880623, upload-time = "2026-09-30T15:30:04.884Z" }
54
+ wheels = [
55
+ { url = "https://files.pythonhosted.org/packages/e5/56/d194340cc4a57535e82e1bee9e89667ac4b7c13b5d3f59686deae3094dd5/cryptography-50.0.2-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb", size = 3914904, upload-time = "2026-09-30T14:43:44.339Z" },
56
+ { url = "https://files.pythonhosted.org/packages/d9/69/c9bd862c3bf43d6399c433caf002df16e2dffd4be49bdf515cda38038711/cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0", size = 4731146, upload-time = "2026-09-30T14:43:47.113Z" },
57
+ { url = "https://files.pythonhosted.org/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2", size = 4719841, upload-time = "2026-09-30T14:43:49.01Z" },
58
+ { url = "https://files.pythonhosted.org/packages/38/6b/61a3f8d8c5e1e49a6cddccafc4015cc1c0021360ab0acb4080e7a423644a/cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480", size = 4738340, upload-time = "2026-09-30T14:43:50.932Z" },
59
+ { url = "https://files.pythonhosted.org/packages/7b/2e/7212ca32fd43dc91f2f41db20160b268098874b4c9a0e7be94d6835f5b2e/cryptography-50.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a582ab2ae1d34f67112cadc86702774c9ea4374df6bca6afe672817203c99134", size = 5367029, upload-time = "2026-09-30T14:43:52.911Z" },
60
+ { url = "https://files.pythonhosted.org/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856", size = 4753050, upload-time = "2026-09-30T14:43:55.272Z" },
61
+ { url = "https://files.pythonhosted.org/packages/7c/52/9af10e80ac16b0fcc2123f9cbd5e7afbd0fd5075bb7a607c592258a39cda/cryptography-50.0.2-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e", size = 4376724, upload-time = "2026-09-30T14:43:57.24Z" },
62
+ { url = "https://files.pythonhosted.org/packages/71/37/6202e488cc1eb625ea110c292c6bda92823176e023f427d8d5660ce8d632/cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04", size = 4737859, upload-time = "2026-09-30T14:43:59.541Z" },
63
+ { url = "https://files.pythonhosted.org/packages/8f/30/e86d7d518489b0ae2497091a35287abcb1a2ce4037837a34afbe9b1d6964/cryptography-50.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f265528741e048bce55c3463ed721fb0aa45a5888d8add8cfeccb3035451bbdc", size = 5324103, upload-time = "2026-09-30T14:44:01.901Z" },
64
+ { url = "https://files.pythonhosted.org/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079", size = 4752576, upload-time = "2026-09-30T14:44:04.545Z" },
65
+ { url = "https://files.pythonhosted.org/packages/6c/5d/906970b83bbfc1f5bbfb677a143c181f2801f23b6a7204a3b47c42c97e65/cryptography-50.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51", size = 4870819, upload-time = "2026-09-30T14:44:06.884Z" },
66
+ { url = "https://files.pythonhosted.org/packages/68/e3/f2298d3bb55e0c4a91841ec4d01b3f020ba8c5fbf15ccdcc6dcf03f97025/cryptography-50.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93", size = 5030152, upload-time = "2026-09-30T14:44:09.443Z" },
67
+ { url = "https://files.pythonhosted.org/packages/9a/4f/adfc442765721292fff86d314ce385d3249d22db42295c0dd057727b60f3/cryptography-50.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:7afa5a6602a9f29af1f3a2965f831bae7c9d5d597b7cbb716d41ab3b7d89879c", size = 3824692, upload-time = "2026-09-30T14:44:11.671Z" },
68
+ { url = "https://files.pythonhosted.org/packages/2d/49/93f6a6e7a87c9aa68d44d3e1cdb5fe8f60c90d5d2f46acae9a56892816b8/cryptography-50.0.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:edc3342adf8f697fc5f59c887a304356f147b397809440ed64e2fa6af2f50f37", size = 4133708, upload-time = "2026-09-30T14:44:41.807Z" },
69
+ { url = "https://files.pythonhosted.org/packages/8c/75/32ac2a56243d778805c16ca6a32b8f74fb757df7e28d7ecb560afafb59cf/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a", size = 4956267, upload-time = "2026-09-30T14:44:43.693Z" },
70
+ { url = "https://files.pythonhosted.org/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67", size = 4966465, upload-time = "2026-09-30T14:44:45.769Z" },
71
+ { url = "https://files.pythonhosted.org/packages/c2/58/ee288c829a6f41f6235ae9dd33d82fd19b45442b65b4c8a3da36963d9f7a/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl", hash = "sha256:e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc", size = 4959356, upload-time = "2026-09-30T14:44:48.211Z" },
72
+ { url = "https://files.pythonhosted.org/packages/92/20/9ded6d51ddd9897f6b6e81fb9ebea7951d7cc5d6c890b0ed8abf77a51a80/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_ppc64le.whl", hash = "sha256:b13478603dcd0a2479ff8e87e2c19a7d525734686fe3c49542472293a204212d", size = 5548822, upload-time = "2026-09-30T14:44:50.86Z" },
73
+ { url = "https://files.pythonhosted.org/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl", hash = "sha256:58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7", size = 5001199, upload-time = "2026-09-30T14:44:53.379Z" },
74
+ { url = "https://files.pythonhosted.org/packages/8b/f9/36b3022218ce75b7cdf068fb95f809f9bd0d820e4955ef43b90c255cc7ac/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_31_armv7l.whl", hash = "sha256:d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408", size = 4629333, upload-time = "2026-09-30T14:44:55.635Z" },
75
+ { url = "https://files.pythonhosted.org/packages/8c/72/20f99a219f6af47cdd1cbd978c243b92d71496e168a746138af44ded4f29/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl", hash = "sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b", size = 4958822, upload-time = "2026-09-30T14:44:59.639Z" },
76
+ { url = "https://files.pythonhosted.org/packages/f2/20/196f112617fb08eb4d608a2a6c422373d46f9cc2857f38fc0667033c0899/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_ppc64le.whl", hash = "sha256:cbc8738fd8526d80f35cb3a40d41f41a2e7030bb3b18b09a6778ef63d291c2fd", size = 5506351, upload-time = "2026-09-30T14:45:02.267Z" },
77
+ { url = "https://files.pythonhosted.org/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl", hash = "sha256:e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c", size = 5000859, upload-time = "2026-09-30T14:45:05.009Z" },
78
+ { url = "https://files.pythonhosted.org/packages/22/f7/70fd7ae4d1dbfa7ba29b02e1b9068771519a86027756510b700ce81086a8/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be", size = 5092151, upload-time = "2026-09-30T15:29:15.932Z" },
79
+ { url = "https://files.pythonhosted.org/packages/d4/be/688367b74de86984bd58d8efacfc7c9e68b89a6a22ced0fb4f38db50254a/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020", size = 5286120, upload-time = "2026-09-30T15:29:18.309Z" },
80
+ { url = "https://files.pythonhosted.org/packages/39/d1/55f8a3f2ef5d1529e16835ef10cf0fe3d559ce237b46dddc440c0bba3649/cryptography-50.0.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:c423ab384a46c4dff7217b2ea5ba2e11cffdeab6441acd04cf65a369caf0366c", size = 4111557, upload-time = "2026-09-30T15:29:20.155Z" },
81
+ { url = "https://files.pythonhosted.org/packages/23/ad/ac987755d00e1e64273760228d2635ae38dae2be83e3c6e0d3289d91dec3/cryptography-50.0.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0ec5f09541743261e66e291b4a0cbf0fb2997aeaab6d9e9c740b9dba1b58d1c2", size = 3943588, upload-time = "2026-09-30T15:29:22.265Z" },
82
+ { url = "https://files.pythonhosted.org/packages/d5/8d/6d585339bedf85d45044c85d8412dac53f2bb6f918e8b7777efba1787844/cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd", size = 4756166, upload-time = "2026-09-30T15:29:24.58Z" },
83
+ { url = "https://files.pythonhosted.org/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767", size = 4749145, upload-time = "2026-09-30T15:29:26.807Z" },
84
+ { url = "https://files.pythonhosted.org/packages/c1/63/61b15dc1a8de03fe0adbe3fd7608b3ad5c73bf50993bbcb1faaa930afe33/cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454", size = 4763638, upload-time = "2026-09-30T15:29:28.588Z" },
85
+ { url = "https://files.pythonhosted.org/packages/fc/35/b345bdfa40c9126df1a9d33236aa98418367931b8725f84fc3ae2b98dc59/cryptography-50.0.2-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:58ddb5a8e3179d12f19e4ea34d2d32e9d63a4baa142c875c1eb59f41b7243acd", size = 5382217, upload-time = "2026-09-30T15:29:30.589Z" },
86
+ { url = "https://files.pythonhosted.org/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5", size = 4781387, upload-time = "2026-09-30T15:29:32.605Z" },
87
+ { url = "https://files.pythonhosted.org/packages/90/5b/f2fdb13cd0b96f6f932c8627bb292a45f11c64d21620a8e120aee9a3b848/cryptography-50.0.2-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107", size = 4403790, upload-time = "2026-09-30T15:29:34.374Z" },
88
+ { url = "https://files.pythonhosted.org/packages/bc/ce/7e4f662b1e3c393513569e402cfc85ac7da0bd3d5435e122a3140219eb2d/cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602", size = 4764319, upload-time = "2026-09-30T15:29:36.149Z" },
89
+ { url = "https://files.pythonhosted.org/packages/3c/3f/86ff33ce34cc0de6847fb96e035a1a760d81652e38643f617c02ad32ef7a/cryptography-50.0.2-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a6557e5f38e065ca9fbdaf7cfc7435ecb1d113aa81a022d1b51921ee7432e227", size = 5338560, upload-time = "2026-09-30T15:29:39.053Z" },
90
+ { url = "https://files.pythonhosted.org/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c", size = 4780973, upload-time = "2026-09-30T15:29:41.251Z" },
91
+ { url = "https://files.pythonhosted.org/packages/10/bf/8d6ebc7dded797bd0f0160d52188021211f011a2b164ef0ae1dac4587465/cryptography-50.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e", size = 4897738, upload-time = "2026-09-30T15:29:43.106Z" },
92
+ { url = "https://files.pythonhosted.org/packages/d4/aa/f3f6e0de7e6253b8baa8b2d8fb9d50924fa75cee3d4624bd4bc1208ee923/cryptography-50.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94", size = 5058280, upload-time = "2026-09-30T15:29:44.827Z" },
93
+ { url = "https://files.pythonhosted.org/packages/f6/b6/a1faf3a27ae9405fb34b1713cc73b2d8a26b04d5c561578fa2e6ef3e5bb9/cryptography-50.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:4e81d95e5bafc2d6e34e4bed780e53e4d5b9a2f928573428aa4d35fbec1eb0de", size = 3854095, upload-time = "2026-09-30T15:29:46.782Z" },
94
+ ]
95
+
96
+ [[package]]
97
+ name = "dev-fleet-ansible"
98
+ version = "0"
99
+ source = { virtual = "." }
100
+ dependencies = [
101
+ { name = "ansible-core" },
102
+ ]
103
+
104
+ [package.metadata]
105
+ requires-dist = [{ name = "ansible-core", specifier = "==2.19.9" }]
106
+
107
+ [[package]]
108
+ name = "jinja2"
109
+ version = "3.1.6"
110
+ source = { registry = "https://pypi.org/simple" }
111
+ dependencies = [
112
+ { name = "markupsafe" },
113
+ ]
114
+ sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
115
+ wheels = [
116
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
117
+ ]
118
+
119
+ [[package]]
120
+ name = "markupsafe"
121
+ version = "3.0.4"
122
+ source = { registry = "https://pypi.org/simple" }
123
+ sdist = { url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6", size = 153777, upload-time = "2026-10-02T23:07:22.29Z" }
124
+ wheels = [
125
+ { url = "https://files.pythonhosted.org/packages/6d/18/4bc5ba32499e87bb2b0ef5b3a9bb9c00a131fa961ddf0be548cb550f548b/markupsafe-3.0.4-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1", size = 12786, upload-time = "2026-10-02T23:05:08.709Z" },
126
+ { url = "https://files.pythonhosted.org/packages/4e/6f/17f0c099bf25f3e31e63cc19244d9f6af861a9a4ab778c203997903cfdd0/markupsafe-3.0.4-cp313-cp313-android_24_x86_64.whl", hash = "sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1", size = 12510, upload-time = "2026-10-02T23:05:09.93Z" },
127
+ { url = "https://files.pythonhosted.org/packages/11/af/1a141081b905036ee904ec4bd945e1f70b4e1b32d33c4e59e8cf1d58b247/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96", size = 11583, upload-time = "2026-10-02T23:05:10.884Z" },
128
+ { url = "https://files.pythonhosted.org/packages/e7/0a/a89385ae590232622a03e091805cff12f24fabe6c11e0e8bae096cece81c/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148", size = 11801, upload-time = "2026-10-02T23:05:11.913Z" },
129
+ { url = "https://files.pythonhosted.org/packages/ed/85/ea548dc013962eb73653124bc595635fbf9e0fa41d1f181a967ccb784dfb/markupsafe-3.0.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e", size = 11443, upload-time = "2026-10-02T23:05:12.887Z" },
130
+ { url = "https://files.pythonhosted.org/packages/cc/72/15f2e5ec9cf2eb00d5cdfe968d94e4156a7bd7303832c3f3b2c403a36839/markupsafe-3.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248", size = 11665, upload-time = "2026-10-02T23:05:13.829Z" },
131
+ { url = "https://files.pythonhosted.org/packages/ca/e0/4030bea613677e333c8a2c901fd405055f657f9d06acba5b7357984b6ef7/markupsafe-3.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72", size = 12061, upload-time = "2026-10-02T23:05:14.807Z" },
132
+ { url = "https://files.pythonhosted.org/packages/f3/a5/28b76a7449eb702966b88bef599e2360b411fbb3afeee8fe560939be06ec/markupsafe-3.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2", size = 24395, upload-time = "2026-10-02T23:05:15.909Z" },
133
+ { url = "https://files.pythonhosted.org/packages/07/6c/21232811afc3a063b5e934b1ae2efda52f46154ec382f585149c020e61fe/markupsafe-3.0.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85", size = 27183, upload-time = "2026-10-02T23:05:16.976Z" },
134
+ { url = "https://files.pythonhosted.org/packages/14/38/6ccdfa5b59049cb36fb80cbc80aee9cf1fc9bb77d1335ad435f2070b08cf/markupsafe-3.0.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde", size = 24772, upload-time = "2026-10-02T23:05:18.209Z" },
135
+ { url = "https://files.pythonhosted.org/packages/63/e0/cec6865dfe88cb48fedd4b20aed6af5158e41092adcbf3e028bcc6ec2108/markupsafe-3.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6", size = 23017, upload-time = "2026-10-02T23:05:19.286Z" },
136
+ { url = "https://files.pythonhosted.org/packages/ee/76/6ed4940bb7648a9aac457c14f870cfdd5105f139a0fb1f29cd61fafa47d1/markupsafe-3.0.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f", size = 22036, upload-time = "2026-10-02T23:05:20.352Z" },
137
+ { url = "https://files.pythonhosted.org/packages/a1/4f/ed476226d4fe46a09090a36025bf319296810028df55eb12f1253b540f3a/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39", size = 23786, upload-time = "2026-10-02T23:05:21.576Z" },
138
+ { url = "https://files.pythonhosted.org/packages/9a/35/66ff30450e35ef5fba9ebc930c9411747e537fd9447b65e44f5007e2b84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee", size = 22672, upload-time = "2026-10-02T23:05:22.922Z" },
139
+ { url = "https://files.pythonhosted.org/packages/32/0b/72f45ce4b4efcbca4b80cf1b06703eff0be8d37e82abb78f66c85a7ead1e/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2", size = 24107, upload-time = "2026-10-02T23:05:24.175Z" },
140
+ { url = "https://files.pythonhosted.org/packages/d2/03/71776e5fdcba04614b384cc102e8a4198208579d896fd1394cb7cb9aa900/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46", size = 21615, upload-time = "2026-10-02T23:05:25.215Z" },
141
+ { url = "https://files.pythonhosted.org/packages/ab/5f/801ce02a02e7aee0f784b1ec7843026178f6adeb9c93ac67eb1992a9a84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17", size = 23076, upload-time = "2026-10-02T23:05:26.423Z" },
142
+ { url = "https://files.pythonhosted.org/packages/4a/85/c43776625428f3bb4a61e8633940400e3efe6409e3c6f5bff26de5e45618/markupsafe-3.0.4-cp313-cp313-win32.whl", hash = "sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0", size = 14118, upload-time = "2026-10-02T23:05:27.716Z" },
143
+ { url = "https://files.pythonhosted.org/packages/6f/36/163da64de88a13db79214ef75fa041be7fa13bdb42261cf5b7484de14bfb/markupsafe-3.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5", size = 14313, upload-time = "2026-10-02T23:05:28.749Z" },
144
+ { url = "https://files.pythonhosted.org/packages/9f/a8/9b662783ffaa1149221432a923cee562f78b9cbbb8baa3df9b3753e63e1e/markupsafe-3.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc", size = 14189, upload-time = "2026-10-02T23:05:29.917Z" },
145
+ ]
146
+
147
+ [[package]]
148
+ name = "packaging"
149
+ version = "26.3"
150
+ source = { registry = "https://pypi.org/simple" }
151
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
152
+ wheels = [
153
+ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
154
+ ]
155
+
156
+ [[package]]
157
+ name = "pycparser"
158
+ version = "3.12"
159
+ source = { registry = "https://pypi.org/simple" }
160
+ sdist = { url = "https://files.pythonhosted.org/packages/cb/27/eeb9f307bf07060f3d9f9aa0fb8c15b01bdfded0a941e4b2719c7f94cacd/pycparser-3.12.tar.gz", hash = "sha256:f03387877eab82ce01bbdfd0ba7252ee55a90b1c708d5129e2ce915475cf1db6", size = 158785, upload-time = "2026-10-22T13:40:03.56Z" }
161
+ wheels = [
162
+ { url = "https://files.pythonhosted.org/packages/b6/ae/315bbeee126a769cbde43cf4908acb4b40408d8bf8bbf334dd7368d8dadf/pycparser-3.12-py3-none-any.whl", hash = "sha256:a076f3f489b39b43ed13563a1dafe42447445bec2e294154ec5c57fcdda4c85e", size = 62600, upload-time = "2026-10-22T13:40:02.021Z" },
163
+ ]
164
+
165
+ [[package]]
166
+ name = "pyyaml"
167
+ version = "6.0.3"
168
+ source = { registry = "https://pypi.org/simple" }
169
+ sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
170
+ wheels = [
171
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
172
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
173
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
174
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
175
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
176
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
177
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
178
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
179
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
180
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
181
+ ]
182
+
183
+ [[package]]
184
+ name = "resolvelib"
185
+ version = "1.2.1"
186
+ source = { registry = "https://pypi.org/simple" }
187
+ sdist = { url = "https://files.pythonhosted.org/packages/1d/14/4669927e06631070edb968c78fdb6ce8992e27c9ab2cde4b3993e22ac7af/resolvelib-1.2.1.tar.gz", hash = "sha256:7d08a2022f6e16ce405d60b68c390f054efcfd0477d4b9bd019cc941c28fad1c", size = 24575, upload-time = "2025-10-11T01:07:44.582Z" }
188
+ wheels = [
189
+ { url = "https://files.pythonhosted.org/packages/e2/23/c941a0d0353681ca138489983c4309e0f5095dfd902e1357004f2357ddf2/resolvelib-1.2.1-py3-none-any.whl", hash = "sha256:fb06b66c8da04172d9e72a21d7d06186d8919e32ae5ab5cdf5b9d920be805ac2", size = 18737, upload-time = "2025-10-11T01:07:43.081Z" },
190
+ ]
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bash
2
- # Download to a file and run with bash; also works directly from a checkout.
2
+ # Installs the pinned toolchain for the checkout this script is in. fleet-sync
3
+ # runs it on every machine from the checkout of the revision it applies;
4
+ # nobody clones dev-fleet to use it (see README.md).
3
5
  set -euo pipefail
4
- MISE_VERSION=2026.10.24
5
- repo=https://github.com/crvouga/dev-fleet.git
6
6
  os=$(uname -s)
7
7
  case "$os" in
8
8
  Darwin) platform=macos ;;
@@ -37,16 +37,20 @@ for tool in curl tar unzip; do
37
37
  command -v "$tool" >/dev/null || { echo "Install $tool, then rerun." >&2; exit 1; }
38
38
  done
39
39
  script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
40
- if [[ -f "$script_dir/../mise.toml" ]]; then
41
- checkout=$(cd "$script_dir/.." && pwd)
42
- else
43
- checkout=${WORKSPACE_DIR:-"$HOME/workspace"}
44
- if [[ ! -d "$checkout/.git" ]]; then git clone "$repo" "$checkout"; fi
45
- [[ "$(git -C "$checkout" remote get-url origin)" == "$repo" ]] || {
46
- echo "Unexpected checkout at $checkout; choose an empty WORKSPACE_DIR." >&2; exit 1;
47
- }
48
- fi
40
+ [[ -f "$script_dir/../mise.toml" ]] || {
41
+ echo 'Run this from a dev-fleet checkout. To use dev-fleet, run its CLI instead: fleet-sync installs the toolchain.' >&2; exit 1;
42
+ }
43
+ checkout=$(cd "$script_dir/.." && pwd)
49
44
  cd "$checkout"
45
+ # mise is pinned where every other tool is: `min_version` in mise.toml is the
46
+ # version installed, and [vars] holds the digest of its release archive for
47
+ # each platform. mise cannot read them for us before it exists.
48
+ pin() { sed -n "s/^$1 = \"\\(.*\\)\"\$/\\1/p" "$checkout/mise.toml" | head -n 1; }
49
+ MISE_VERSION=$(pin min_version)
50
+ mise_sha256=$(pin "mise_${platform}_${arch}_sha256")
51
+ [[ -n "$MISE_VERSION" && -n "$mise_sha256" ]] || {
52
+ echo 'mise.toml does not pin mise for this platform.' >&2; exit 1;
53
+ }
50
54
  # Reuse an observed matching managed executable. Repeated reconciliation must
51
55
  # not download/reinstall mise solely because a previous run finished.
52
56
  installed_mise=$("$HOME/.local/bin/mise" --version 2>/dev/null | awk '{print $1}') || installed_mise=''
@@ -55,12 +59,15 @@ temp=$(mktemp -d)
55
59
  trap 'rm -rf "$temp"' EXIT
56
60
  asset="mise-v${MISE_VERSION}-${platform}-${arch}.tar.gz"
57
61
  # Alpine/musl is not part of the supported workstation baseline.
58
- base="https://github.com/jdx/mise/releases/download/v${MISE_VERSION}"
59
- curl -fsSL "$base/$asset" -o "$temp/$asset"
60
- curl -fsSL "$base/SHASUMS256.txt" -o "$temp/SHASUMS256.txt"
61
- (cd "$temp"; awk -v file="./$asset" '$2 == file { print }' SHASUMS256.txt > check.txt
62
- test -s check.txt
63
- if command -v sha256sum >/dev/null; then sha256sum -c check.txt; else shasum -a 256 -c check.txt; fi)
62
+ curl -fsSL "https://github.com/jdx/mise/releases/download/v${MISE_VERSION}/$asset" -o "$temp/$asset"
63
+ if command -v sha256sum >/dev/null; then
64
+ actual=$(sha256sum "$temp/$asset" | cut -d' ' -f1)
65
+ else
66
+ actual=$(shasum -a 256 "$temp/$asset" | cut -d' ' -f1)
67
+ fi
68
+ [[ "$actual" == "$mise_sha256" ]] || {
69
+ echo 'The mise archive does not match the digest pinned in mise.toml.' >&2; exit 1;
70
+ }
64
71
  tar -xzf "$temp/$asset" -C "$temp"
65
72
  mkdir -p "$HOME/.local/bin"
66
73
  install -m 0755 "$temp/mise/bin/mise" "$HOME/.local/bin/mise"
@@ -4,7 +4,13 @@ import { spawnSync } from 'node:child_process';
4
4
  import { readFileSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { homedir, tmpdir } from 'node:os';
7
- import { AGENT_WORKTREE_ROOTS, workspaceRoot } from './global-config';
7
+ import {
8
+ AGENT_WORKTREE_ROOTS,
9
+ globalConfigDir,
10
+ globalConfigStanding,
11
+ workspaceRoot,
12
+ type GlobalConfigStanding,
13
+ } from './global-config';
8
14
 
9
15
  type Tool = string | { version: string; lazy?: boolean; os?: string[] };
10
16
  type Config = {
@@ -151,6 +157,44 @@ export function worktreeTrustCheck(
151
157
  };
152
158
  }
153
159
 
160
+ const STANDING: Record<GlobalConfigStanding, string> = {
161
+ published: 'Published by dev-fleet from this revision.',
162
+ missing: 'There is no global mise configuration.',
163
+ replaced:
164
+ 'Another project or tool replaced the global mise configuration dev-fleet publishes.',
165
+ changed:
166
+ 'The global mise configuration is not the one this revision publishes.',
167
+ };
168
+
169
+ /** On a fleet machine the global mise configuration belongs to dev-fleet.
170
+ * When anything else replaced it, its pins and the trusted agent worktree
171
+ * roots are gone until the next fleet-sync.
172
+ */
173
+ export function globalConfigCheck(
174
+ dir?: string,
175
+ root: string = workspaceRoot
176
+ ): Check {
177
+ let standing: GlobalConfigStanding;
178
+ try {
179
+ standing = globalConfigStanding(dir ?? globalConfigDir(), root);
180
+ } catch {
181
+ return {
182
+ id: 'global-mise-config',
183
+ status: 'fail',
184
+ detail:
185
+ 'MISE_GLOBAL_CONFIG_FILE moves the global mise configuration away from the one dev-fleet publishes; unset it.',
186
+ };
187
+ }
188
+ return {
189
+ id: 'global-mise-config',
190
+ status: standing === 'published' ? 'pass' : 'fail',
191
+ detail:
192
+ standing === 'published'
193
+ ? STANDING[standing]
194
+ : `${STANDING[standing]} Run fleet-sync; projects pin their own tools in a project mise.toml and never publish a global one.`,
195
+ };
196
+ }
197
+
154
198
  export function checkMachine(requireDocker = false, worker = false): Check[] {
155
199
  const config = Bun.TOML.parse(
156
200
  readFileSync(join(workspaceRoot, 'mise.toml'), 'utf8')
@@ -164,6 +208,7 @@ export function checkMachine(requireDocker = false, worker = false): Check[] {
164
208
  ),
165
209
  ...dockerChecks(requireDocker),
166
210
  worktreeTrustCheck(),
211
+ globalConfigCheck(),
167
212
  {
168
213
  id: 'github-auth',
169
214
  status: auth.ok ? 'pass' : 'warn',
@@ -12,15 +12,29 @@ shift
12
12
  case "$revision" in *[!a-f0-9]*|'') exit 2;; esac
13
13
  [ "${#revision}" = 40 ] || exit 2
14
14
  say() { printf 'fleet-sync (manager): %s\n' "$1" >&2; }
15
- # The dev-fleet clone to take the revision from: named by the caller, or the
16
- # one the devfleet launcher is linked into.
15
+ # A preview (--dry-run) changes nothing outside dev-fleet's own disposable
16
+ # caches: it never publishes the global mise configuration, never edits shell
17
+ # profiles and never prunes run checkouts. Only an apply does.
18
+ preview=false
19
+ for arg in "$@"; do [ "$arg" != --dry-run ] || preview=true; done
20
+ # mise is installed here by the toolchain step below; a login shell is not
21
+ # required to find it.
22
+ export PATH="$HOME/.local/bin:$PATH"
23
+ fleet="$HOME/.local/share/personal/fleet"
24
+ # The repository to take the revision from. Nobody keeps a clone of dev-fleet:
25
+ # fleet-sync names the one it prepared, and a hand-off (which names none) uses
26
+ # the manager's own copy of the source repository, or the checkout the devfleet
27
+ # launcher is linked into.
17
28
  repo=${DEVFLEET_REPO:-}
29
+ if [ -z "$repo" ] && git -C "$fleet/manager/source" rev-parse --git-dir >/dev/null 2>&1; then
30
+ repo="$fleet/manager/source"
31
+ fi
18
32
  if [ -z "$repo" ]; then
19
- launcher=$(command -v devfleet) || { say 'set DEVFLEET_REPO to a dev-fleet clone, or install the launcher with sh bootstrap/install-launcher.sh.'; exit 3; }
33
+ launcher=$(command -v devfleet) || { say 'this manager has no copy of the source yet; run fleet-sync once on the manager itself, from the project that holds your fleet.json.'; exit 3; }
20
34
  repo=$(dirname "$(readlink -f "$launcher")")
21
35
  fi
22
- repo=$(git -C "$repo" rev-parse --show-toplevel) || { say 'cannot locate the dev-fleet clone on the manager.'; exit 3; }
23
- runs="$HOME/.local/share/personal/fleet/manager-runs"
36
+ repo=$(git -C "$repo" rev-parse --show-toplevel) || { say 'cannot read the source repository on the manager; run fleet-sync once on the manager itself to fetch it again.'; exit 3; }
37
+ runs="$fleet/manager-runs"
24
38
  dir="$runs/$revision"
25
39
  ready() {
26
40
  [ "$(git -C "$dir" rev-parse HEAD 2>/dev/null)" = "$revision" ] &&
@@ -28,6 +42,7 @@ ready() {
28
42
  }
29
43
  if ! ready; then
30
44
  git -C "$repo" cat-file -e "$revision^{commit}" 2>/dev/null || git -C "$repo" fetch --quiet origin || true
45
+ git -C "$repo" cat-file -e "$revision^{commit}" 2>/dev/null || git -C "$repo" fetch --quiet --no-tags origin "$revision" || true
31
46
  git -C "$repo" cat-file -e "$revision^{commit}" 2>/dev/null || { say "revision $revision is neither on the manager nor on origin; push it first."; exit 4; }
32
47
  mkdir -p "$runs"
33
48
  chmod 700 "$runs"
@@ -54,9 +69,16 @@ failed() {
54
69
  say "toolchain or dependency install failed in the run checkout; full log: $log"
55
70
  exit 5
56
71
  }
57
- if [ "$(cat "$stamp" 2>/dev/null)" != "$key" ] || [ -z "$(find "$stamp" -mmin -1440 2>/dev/null)" ]; then
72
+ if [ "$preview" = true ]; then
73
+ # The revision's own locked Bun is enough to preview; mise keeps it beside
74
+ # its other versions without making it the global one.
75
+ command -v mise >/dev/null 2>&1 || { say 'this manager has no toolchain yet, so a preview cannot run; fleet-sync without --dry-run installs it.'; exit 6; }
76
+ mise install --locked bun >"$log" 2>&1 || failed
77
+ elif [ "$(cat "$stamp" 2>/dev/null)" != "$key" ] || [ -z "$(find "$stamp" -mmin -1440 2>/dev/null)" ]; then
58
78
  rm -f -- "$stamp"
59
- mise exec --locked bun -- bun bootstrap/setup.ts --tools-only >"$log" 2>&1 || failed
79
+ # The toolchain bootstrap installs the pinned mise first when the manager
80
+ # has none (or another version), so Git is all a new manager needs.
81
+ bash bootstrap/bootstrap.sh --tools-only >"$log" 2>&1 || failed
60
82
  printf '%s' "$key" >"$stamp"
61
83
  fi
62
84
  if [ ! -d node_modules ]; then
@@ -64,13 +86,18 @@ if [ ! -d node_modules ]; then
64
86
  fi
65
87
  # Keep the three most recent run checkouts (names are commit SHAs) and the
66
88
  # applied one: fleet/current points at it and T3's providers launch from there.
67
- applied=$(basename "$(readlink "$(dirname "$runs")/current" 2>/dev/null || true)")
68
- # shellcheck disable=SC2012
69
- for old in $(ls -1td "$runs"/*/ | tail -n +4); do
70
- [ "$(basename "$old")" != "$applied" ] || continue
71
- git -C "$repo" worktree remove --force "$old" 2>/dev/null || true
72
- rm -f -- "${old%/}.setup.log"
73
- done
74
- git -C "$repo" worktree prune
89
+ # A preview leaves them for the next apply to prune.
90
+ if [ "$preview" = false ]; then
91
+ applied=$(basename "$(readlink "$(dirname "$runs")/current" 2>/dev/null || true)")
92
+ # shellcheck disable=SC2012
93
+ for old in $(ls -1td "$runs"/*/ | tail -n +4); do
94
+ [ "$(basename "$old")" != "$applied" ] || continue
95
+ # A checkout made from another repository (an earlier clone) is not this
96
+ # repository's worktree: remove the directory itself.
97
+ git -C "$repo" worktree remove --force "$old" 2>/dev/null || rm -rf -- "$old"
98
+ rm -f -- "${old%/}.setup.log"
99
+ done
100
+ git -C "$repo" worktree prune
101
+ fi
75
102
  export PERSONAL_WORKSPACE="$dir" PERSONAL_FLEET_RUN_CHECKOUT="$revision"
76
103
  exec mise exec --locked bun -- bun src/cli/cli.ts fleet-sync --revision "$revision" "$@"