stim 1.20.2 → 1.21.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 (217) hide show
  1. package/README.md +88 -8
  2. package/dist/acquire-artifact-CD4lbiWE.mjs +153 -0
  3. package/dist/act-DHAw2A2S.mjs +2667 -0
  4. package/dist/activity-BzVfHSrY.mjs +1376 -0
  5. package/dist/{agent-device-usage-output-Dzgv6lgR.mjs → agent-device-usage-output-zh1vhbLp.mjs} +21 -65
  6. package/dist/{android-HqP1_vB_.mjs → android-BEJ8Jpl7.mjs} +2 -2
  7. package/dist/{android-DCKZrNvV.mjs → android-BLgbNqox.mjs} +128 -78
  8. package/dist/{android-Dlzxshgp.mjs → android-CV_GCELZ.mjs} +5 -5
  9. package/dist/{android-Mz_Inn3w.mjs → android-DUYkNF4u.mjs} +184 -102
  10. package/dist/{android-cas-Bl-cLJBk.mjs → android-cas-Ct5b1xCK.mjs} +7 -6
  11. package/dist/android-cas-compiler.mjs +2 -2
  12. package/dist/android-project-BCAAmMwz.mjs +12 -0
  13. package/dist/api.d.mts +740 -8
  14. package/dist/api.mjs +585 -1
  15. package/dist/{artifact-HSwkIWEQ.mjs → artifact-B8T3S5gi.mjs} +223 -201
  16. package/dist/{artifact-D3czlUvT.mjs → artifact-DAuZ-FNU.mjs} +158 -148
  17. package/dist/{browser-web-CA-F1GQo.mjs → browser-web-QQzVjz3O.mjs} +26 -24
  18. package/dist/budget-Bs6SxxzO.mjs +9154 -0
  19. package/dist/{build-DYDbfkk5.mjs → build-CoPzQRNw.mjs} +144 -96
  20. package/dist/build-cache-DmyR9Zzc.mjs +602 -0
  21. package/dist/build-plan-CaFskB7_.mjs +188 -0
  22. package/dist/{cache-manifest-XH-fvDUj.mjs → cache-manifest-DkkqguoK.mjs} +2 -2
  23. package/dist/cache-manifest.mjs +1 -1
  24. package/dist/{ccache-zdKiYQ3E.mjs → ccache-BJRZ9pG3.mjs} +10 -9
  25. package/dist/{cdp-Dq_ypAlJ.mjs → cdp-CWmIUMbw.mjs} +18 -5
  26. package/dist/{chrome-BnzZMqsR.mjs → chrome-B6tfCta1.mjs} +7 -4
  27. package/dist/{cli-CLP3diBm.mjs → cli-0DYf8QhQ.mjs} +20 -20
  28. package/dist/cli.mjs +1 -1
  29. package/dist/client-B7QVOlCr.d.mts +253 -0
  30. package/dist/client-UXD70ZbM.mjs +1406 -0
  31. package/dist/collector-run.d.mts +4 -40
  32. package/dist/collector-run.mjs +9 -8
  33. package/dist/{command-output-BTy8hZ0F.mjs → command-output-B1UrjZHL.mjs} +1 -1
  34. package/dist/config-CYcBF478.mjs +949 -0
  35. package/dist/{created-devices-Dzd5C2LF.mjs → created-devices-CqeBlrEn.mjs} +3 -3
  36. package/dist/{dependency-state-DG0p31-y.mjs → dependency-state-BbKQW83h.mjs} +20 -5
  37. package/dist/{detached-entry-B9C_zeQC.mjs → detached-entry-DFRyG1_o.mjs} +1 -1
  38. package/dist/{dev-client-DAWVIGBu.mjs → dev-client-UOeESKm6.mjs} +2 -2
  39. package/dist/device-DCxH7_By.mjs +1068 -0
  40. package/dist/{device-BECOiufl.mjs → device-ExJq8Ei7.mjs} +35 -35
  41. package/dist/{device-capacity-CBUAcRyQ.mjs → device-capacity-CMlqEcvw.mjs} +43 -13
  42. package/dist/device-host-worker.mjs +65 -61
  43. package/dist/{device-ios-COSi6oS1.mjs → device-ios-DRUeKaFV.mjs} +48 -45
  44. package/dist/{device-lease-run-Cw2M4z51.mjs → device-lease-run-C8raGFoP.mjs} +14 -26
  45. package/dist/{device-pool-Bf-pOhSw.mjs → device-pool-DMRpVX9a.mjs} +5 -5
  46. package/dist/doctor-BLHCYxzi.mjs +2 -0
  47. package/dist/{doctor-kfMBnUw6.mjs → doctor-DTvaDOda.mjs} +281 -317
  48. package/dist/{doctor-CB8oZKCk.mjs → doctor-Dhqebm0q.mjs} +183 -91
  49. package/dist/doctor-fingerprint.mjs +19 -0
  50. package/dist/{doctor-watchman-BRyV2adA.mjs → doctor-watchman-Cv1F10Kj.mjs} +40 -21
  51. package/dist/{eas-build-JyfqyHfC.mjs → eas-build-BomfRnFT.mjs} +53 -20
  52. package/dist/{build-plan-B7SZsc2N.mjs → eas-fallback-DJwZpy0c.mjs} +26 -208
  53. package/dist/{watchman-CPQFZLXY.mjs → eas-session-ledger-op6A54yi.mjs} +229 -11
  54. package/dist/{error-diagnostics-BR677IDM.mjs → error-diagnostics-Bf0rZvG9.mjs} +172 -82
  55. package/dist/exec-H20xSg2T.d.mts +65 -0
  56. package/dist/exec-_UbtCQcG.mjs +750 -0
  57. package/dist/gc-C-WwL0yi.mjs +3 -0
  58. package/dist/{gc-qWOMdnER.mjs → gc-_-dCXdyL.mjs} +584 -796
  59. package/dist/git-C3iTsJaZ.mjs +73 -0
  60. package/dist/{guide-G9cAbVAc.mjs → guide-Yqwt2bd6.mjs} +941 -269
  61. package/dist/{hosted-android-C3MHGld1.mjs → hosted-android-Bw-honsd.mjs} +8 -6
  62. package/dist/{hosted-client-ZEKpS9vw.mjs → hosted-client-DB_gBPfY.mjs} +34 -19
  63. package/dist/{hosted-ios-l-o3iM9J.mjs → hosted-ios-CUvOf4Vr.mjs} +1 -1
  64. package/dist/{hosted-logs-EzQM7-3Z.mjs → hosted-logs-DL_P5AdN.mjs} +77 -43
  65. package/dist/{hosted-macos-BACVAVqE.mjs → hosted-macos-QI141yKk.mjs} +21 -17
  66. package/dist/{hosted-native-A4s1QPXe.mjs → hosted-native-Ksyjgzq-.mjs} +30 -21
  67. package/dist/idle-BVeRGroT.d.mts +24 -0
  68. package/dist/{idle-shutdown-DtxhPTKe.mjs → idle-shutdown-DaKmZi0z.mjs} +11 -10
  69. package/dist/{ios-BwVCF7oA.mjs → ios-CQ68hG9s.mjs} +8 -8
  70. package/dist/{ios-RkW4pnQf.mjs → ios-CpByZy12.mjs} +140 -80
  71. package/dist/{ios-device-B5veSDDQ.mjs → ios-device-CXGP1bsD.mjs} +1 -1
  72. package/dist/{ios-device-D7pi2Sca.mjs → ios-device-CwFMehUS.mjs} +51 -26
  73. package/dist/{ios-project-K9hY_Jab.mjs → ios-project-WuwwBdvW.mjs} +13 -13
  74. package/dist/{ios-nOLJGVdy.mjs → ios-qNJt4dFm.mjs} +1 -1
  75. package/dist/ios-signing-DTTIChGs.mjs +489 -0
  76. package/dist/{ios-state-CBLe1xYw.mjs → ios-state-DvFX-K_l.mjs} +1 -1
  77. package/dist/{ios-Ce7bc7br.mjs → ios-ubFsBzwq.mjs} +129 -86
  78. package/dist/{js-swap-Bb3nsS07.mjs → js-swap-Cc1dttps.mjs} +53 -26
  79. package/dist/{launch-F4sOqAhB.mjs → launch-CD1yVc7U.mjs} +91 -90
  80. package/dist/{launch-C3qJ6Agb.mjs → launch-Zq2mOSDy.mjs} +82 -541
  81. package/dist/logs-BS00Anxf.mjs +352 -0
  82. package/dist/logs-CMm05HxN.mjs +255 -0
  83. package/dist/{macos-D7uDvO-z.mjs → macos-B65--pPd.mjs} +1 -1
  84. package/dist/{macos-Ch5VJmcS.mjs → macos-C-0qJ6zq.mjs} +225 -212
  85. package/dist/macos-run.mjs +1 -1
  86. package/dist/maintenance-run.mjs +122 -247
  87. package/dist/{metro-DnCEjCxs.mjs → metro-DEXpIhHC.mjs} +67 -12
  88. package/dist/{metro-gateway-CX9Xlkq2.mjs → metro-gateway-BMJNlj3E.mjs} +20 -12
  89. package/dist/{named-ports-4WopRPg-.mjs → named-ports-BWTzt6mu.mjs} +8 -63
  90. package/dist/native-android-JACyjTZs.mjs +844 -0
  91. package/dist/{native-gradle-inputs-BsLRrl7u.mjs → native-gradle-inputs-Bep9JcSF.mjs} +128 -140
  92. package/dist/native-inputs-TRTTTRxt.mjs +91 -0
  93. package/dist/native-run-DY_RLx3C.mjs +277 -0
  94. package/dist/{native-runtime-k8dTIrbK.mjs → native-runtime-Dg1Q7tfF.mjs} +9 -8
  95. package/dist/{native-xcode-inputs-CykopJdM.mjs → native-xcode-inputs-CB2iQeWq.mjs} +27 -18
  96. package/dist/{native-xcode-ios-Bbf30Gm0.mjs → native-xcode-ios-CXX39F7o.mjs} +96 -47
  97. package/dist/{ndjson-GOnbyvso.mjs → ndjson-CgE_o3Dl.mjs} +3 -3
  98. package/dist/offload-worker.d.mts +1 -46
  99. package/dist/offload-worker.mjs +197 -66
  100. package/dist/{ownership-DCtw_2IA.mjs → ownership-DEmM5vhh.mjs} +3 -3
  101. package/dist/{page-DRHy329N.mjs → page-CPaHeYqj.mjs} +7 -4
  102. package/dist/paths-BsJMoPP2.mjs +12 -0
  103. package/dist/paths-DQtTgsBf.mjs +42 -0
  104. package/dist/{placement-log-MOfPVgaM.mjs → placement-log-BqGbTa61.mjs} +5 -5
  105. package/dist/{plan-placement-V0TdzYXp.mjs → plan-placement-Bwj-IjY1.mjs} +6 -6
  106. package/dist/{ports-DdSUicVo.mjs → ports-D-MmCTdv.mjs} +3 -3
  107. package/dist/prebuild-uD_ipCxF.mjs +449 -0
  108. package/dist/{project-_phNIJfs.mjs → project-BZNjmW_8.mjs} +65 -37
  109. package/dist/{pull-request-CQ8sPbZa.mjs → pull-request-Lfetn8Kr.mjs} +2 -2
  110. package/dist/pull-request-zjCSAryH.d.mts +120 -0
  111. package/dist/pull-requests.d.mts +1 -119
  112. package/dist/pull-requests.mjs +1 -1
  113. package/dist/{react-native-android-CM1XRDwv.mjs → react-native-android-DuzH0cn3.mjs} +74 -48
  114. package/dist/{gradle-DSrXb6lG.mjs → react-native-build-rhk4smwQ.mjs} +292 -15
  115. package/dist/react-native-doctor-CQpuIMso.mjs +234 -0
  116. package/dist/{react-native-ios-Dhq_eiYQ.mjs → react-native-ios-Bhq1rJeV.mjs} +46 -28
  117. package/dist/{recordings-D0d5aQt0.mjs → recordings-DYteQSu4.mjs} +12 -9
  118. package/dist/{reload-C8JhxjSh.mjs → reload-CmTRFJaX.mjs} +93 -47
  119. package/dist/reload-DzjSl5P_.mjs +2 -0
  120. package/dist/{remote-BiR6aYjy.mjs → remote-ByYkXdGD.mjs} +15 -10
  121. package/dist/result-DNiS-tTP.mjs +434 -0
  122. package/dist/{run-BtPgpKks.mjs → run-D23HmJMs.mjs} +4 -5
  123. package/dist/selection-CSl9i5qU.mjs +619 -0
  124. package/dist/{server-bare-R2m_gsH8.mjs → server-bare-CxTrvDg8.mjs} +3 -4
  125. package/dist/{server-command-CTtCkmcQ.mjs → server-command-CtzloH3v.mjs} +14 -7
  126. package/dist/server-expo-C-90EhcG.mjs +2 -0
  127. package/dist/{server-expo-DsguMGUF.mjs → server-expo-DT_F_uVI.mjs} +50 -9
  128. package/dist/{settings-B717odTD.mjs → settings-CsJDEXgK.mjs} +234 -24
  129. package/dist/{settings-vk3mJGSm.d.mts → settings-DkdmlelE.d.mts} +6 -4
  130. package/dist/settings-T1gtc3bf.mjs +114 -0
  131. package/dist/settings-write-HyVZ8eQ8.mjs +359 -0
  132. package/dist/settings.schema.json +163 -16
  133. package/dist/{simslim-CCr3rE-A.mjs → simslim-Cgk17SXV.mjs} +14 -12
  134. package/dist/{slot-launch-DPt0m9Nd.mjs → slot-launch-DF8VfT64.mjs} +15 -20
  135. package/dist/{spawn-entry-Dcb7sDUz.mjs → spawn-entry-jw19m8Nl.mjs} +0 -1
  136. package/dist/stage-BIS0ydsV.mjs +183 -0
  137. package/dist/{start-BWqzODpb.mjs → start-7lpH7hmN.mjs} +63 -44
  138. package/dist/{start-TXeMmmO2.mjs → start-RChynRDp.mjs} +1 -1
  139. package/dist/{state-D1Icyjlu.mjs → state-C4ZtUisW.mjs} +40 -13
  140. package/dist/{state-CG_OFoaA.mjs → state-XMziexv2.mjs} +3 -2
  141. package/dist/{stats-BoY5sEPU.mjs → stats-53T7OTb9.mjs} +7 -13
  142. package/dist/stats-report-BFfMpWWF.mjs +16 -0
  143. package/dist/{status-WgFbkRzj.mjs → status-BL7jVyAB.mjs} +535 -447
  144. package/dist/status-CSi6tlRq.mjs +2 -0
  145. package/dist/{status-BTA7kKiQ.mjs → status-DqEUSSxs.mjs} +6 -4
  146. package/dist/{status-watch-C23ILtf9.mjs → status-watch-GDCfqpTE.mjs} +187 -22
  147. package/dist/status-worker.d.mts +1 -0
  148. package/dist/status-worker.mjs +19 -0
  149. package/dist/stim-desktop-C12we7lz.mjs +112 -0
  150. package/dist/{stop-DR6VoDIk.mjs → stop-Bk_1qiV6.mjs} +2 -2
  151. package/dist/{stop-BNmBMi1F.mjs → stop-CPzlM1L-.mjs} +138 -98
  152. package/dist/{stop-DPEhunlw.mjs → stop-D7h6jAXQ.mjs} +11 -10
  153. package/dist/supervisor-run.d.mts +8 -17
  154. package/dist/supervisor-run.mjs +63 -27
  155. package/dist/{support-DMTqj3Hm.mjs → support-67vraa1Q.mjs} +18 -11
  156. package/dist/{support-CwjbFs9H.mjs → support-Dlu37g6H.mjs} +12 -12
  157. package/dist/{swiftpm-macos-DRNVJWlb.mjs → swiftpm-macos-p8WZbxKU.mjs} +35 -24
  158. package/dist/toolchain-DhKwp9iI.mjs +2 -0
  159. package/dist/toolchain-Hp9sCpK4.mjs +873 -0
  160. package/dist/{trigger-CigAYjeL.mjs → trigger-BAahxUPq.mjs} +20 -5
  161. package/dist/trigger-Bf3uDcpv.mjs +2 -0
  162. package/dist/{warm-progress-BuY-Fvfn.mjs → warm-progress-BHX5r0Ei.mjs} +250 -156
  163. package/dist/{web-BDBMxDpx.mjs → web-CJmIj6AI.mjs} +18 -13
  164. package/dist/web-DknQETPs.mjs +2 -0
  165. package/dist/web-run.mjs +11 -12
  166. package/dist/windows-report.d.mts +1 -0
  167. package/dist/windows-report.mjs +18 -0
  168. package/dist/windows-report.ps1 +43 -0
  169. package/dist/workspace-process-lock-1_1OoVW5.mjs +57 -0
  170. package/dist/{workspace-state-DJt_MMvf.mjs → workspace-state-Cjuigeeu.mjs} +62 -58
  171. package/dist/{workspaces-C36s2jb9.mjs → workspaces-BccMFmGQ.mjs} +1188 -1079
  172. package/dist/worktree-CK2MpJW5.mjs +2 -0
  173. package/dist/{worktree-CgvJYJ-Q.mjs → worktree-DQ6Zi2Mv.mjs} +304 -53
  174. package/dist/xcode-Zlx3qjEs.mjs +896 -0
  175. package/package.json +7 -5
  176. package/shim/android-optimizations.gradle +7 -5
  177. package/dist/activity-BSV745Kz.mjs +0 -629
  178. package/dist/api-qvOzWyfO.mjs +0 -154
  179. package/dist/api-run.mjs +0 -294
  180. package/dist/app-install-BIKtMfPq.mjs +0 -896
  181. package/dist/artifact-lifecycle-DzPpBYvK.mjs +0 -30
  182. package/dist/budget-f7516Zk0.mjs +0 -4106
  183. package/dist/build-facts-DcLv2HSd.d.mts +0 -111
  184. package/dist/cancellation-C_22VQTH.mjs +0 -24
  185. package/dist/client-qQAaXSUs.mjs +0 -1771
  186. package/dist/config-BYDxlMY3.mjs +0 -562
  187. package/dist/device-DuiPJQSn.mjs +0 -290
  188. package/dist/device-lease-BX1sJQ2E.mjs +0 -1224
  189. package/dist/device-remote-xRT0Vmo9.mjs +0 -1272
  190. package/dist/eas-session-ledger-Cm0Gwq2F.mjs +0 -207
  191. package/dist/exec-DqKo9GKJ.mjs +0 -268
  192. package/dist/git-DDfFzkMm.mjs +0 -45
  193. package/dist/launch-verify-B6Q8hZP-.mjs +0 -428
  194. package/dist/logs-BGrEUaEs.mjs +0 -383
  195. package/dist/miss-reason-mKzZ4VvN.mjs +0 -238
  196. package/dist/native-android-D5qc3-_v.mjs +0 -226
  197. package/dist/ndjson-Du3j0AQ9.d.mts +0 -17
  198. package/dist/plan-C4TjdPsR.mjs +0 -260
  199. package/dist/prebuild-DYbucPXy.mjs +0 -212
  200. package/dist/preview-78a9kR_-.mjs +0 -1011
  201. package/dist/react-native-build-CtCccJtb.mjs +0 -239
  202. package/dist/react-native-doctor-CfRnFfjs.mjs +0 -78
  203. package/dist/remote-cache-OyOyVIS3.mjs +0 -526
  204. package/dist/result-CaJg5Iat.mjs +0 -186
  205. package/dist/server-expo-Bigriskl.mjs +0 -2
  206. package/dist/settings-BORNaaCO.mjs +0 -402
  207. package/dist/spawn-claims-DVS5RkMS.mjs +0 -72
  208. package/dist/stage-KNTQopqv.mjs +0 -157
  209. package/dist/stim-desktop-BzBN2Em2.mjs +0 -63
  210. package/dist/tailnet-Dh-qiYzG.mjs +0 -265
  211. package/dist/toolchain-BFkAcstd.mjs +0 -2
  212. package/dist/toolchain-DxSsNssu.mjs +0 -1657
  213. package/dist/trigger-9_USgmxz.mjs +0 -2
  214. package/dist/web-Dq-i3wzJ.mjs +0 -2
  215. package/dist/worktree-DpLdCHga.mjs +0 -2
  216. package/dist/{api-run.d.mts → doctor-fingerprint.d.mts} +0 -0
  217. package/dist/{errors-DzkMYh8Q.mjs → errors-ccaPc4ox.mjs} +2 -2
@@ -1,30 +1,32 @@
1
- import { A as acquireAvdClaim, D as withConfigLock$1, F as listMountedVolumes, L as volumeRootFor, M as directorySize, N as formatBytes, P as isOnMountedVolume, S as removeProject, W as projectDeviceSlots, _ as loadConfig$1, j as ownership_claim_exports, k as process_identity_exports } from "./config-BYDxlMY3.mjs";
2
- import { t as getExecutor } from "./exec-DqKo9GKJ.mjs";
3
- import { d as removeExpiredLease, h as listBuildLocks } from "./device-lease-BX1sJQ2E.mjs";
4
- import { D as withDirLock$1, E as workspaceStateFile, s as workspaceLastUsed, x as workspaceDir } from "./workspace-state-DJt_MMvf.mjs";
5
- import { h as shortUdid, o as formatLongDuration, u as plural } from "./command-output-BTy8hZ0F.mjs";
6
- import { _ as detectIsExpo, r as findProjectRoot } from "./project-_phNIJfs.mjs";
7
- import { A as listAvds, H as parseAvdSystemImage, I as ownedAvdDirectory, M as listOrphanedAvdDirectories, V as parseAvdDeviceProfile, _ as avdStorageRoots, at as POOL_SETTING_REMEDY, d as androidToolPath, dt as parkedMaxSetting, ft as readParked, it as isStimOwnedSim, j as listInstalledSystemImages, m as avdNameAbsent, o as androidHome, p as avdIniPaths, rt as isStimOwnedAvd } from "./android-Mz_Inn3w.mjs";
8
- import { n as readCreatedDevices$1, t as forgetCreatedDevice } from "./created-devices-Dzd5C2LF.mjs";
9
- import { C as recordGcResult, I as listBuildSlots, T as canonicalPath, a as collectWorkspaceOutputs, c as listWorkspaceDirs, f as withIdleWorkspace, i as collectOrphanedWorkspaces, l as rootPresence, n as WORKSPACE_OUTPUT_DIRS, o as deleteOrphanedWorkspaces, p as workspaceInUse, r as clearWorkspaceOutputs, s as isInsideWorkspaces, t as REBUILD_COST, u as sizeText, w as takeGcResults } from "./workspaces-C36s2jb9.mjs";
10
- import { $ as memoryWorkPending, A as findStaleProjectDevices, B as previewScopedEmptying, C as deleteProjectDevices, D as eraseParkedDevices, E as deviceSweepIsScoped, F as includesParkedDevices, H as selectsBeyondCaches, I as includesRecordings, L as includesWorkspaceOutputs, M as withAndroidAvdSizes, N as emptyCaches, O as findOrphanedDevices, P as includesArchives, Q as memorySweepIsScoped, R as isWorkspacesScope, S as deleteParkedSims, T as describeUnverifiableDevices, U as trimCaches, V as selectCaches, W as parseArchiveSelector, X as memoryLines, Y as memoryCacheKind, Z as memoryScope, _ as DEVICE_LIST_TIMEOUT_MS, at as discoverCaches, b as collectParkedSims, d as collectIdleDevices, et as reclaimMemory, h as shutDownIdleDevices, j as unverifiedDeviceCommand, k as findStaleDeviceRecords, m as parseIdleDuration, nt as phase, p as idleDeviceLines, q as collectMemoryReport, st as sizeCaches, tt as scopedMemoryReport, v as collectDeviceLeases, w as describeParkedSims, x as deleteParkedAvds, y as collectParkedAvds, z as planCacheEmptying } from "./budget-f7516Zk0.mjs";
11
- import { f as resolveEasCliBin } from "./remote-cache-OyOyVIS3.mjs";
12
- import { d as listAllIosSims } from "./ios-RkW4pnQf.mjs";
13
- import { C as parseSessionListPage, T as verifyStoppedSession, _ as findOrphanedOwnedSessions, b as isDefinitiveMissingSessionError, d as withRemoteSessionLock, f as withEasProjectLock, v as getSessionArgs, w as stopSessionArgs, x as listOwnedSessionsArgs, y as inspectSessionForTeardown } from "./device-remote-xRT0Vmo9.mjs";
14
- import { a as removeEasSessionClaim, n as easMachineStateRoot$1, r as readEasSessionLedger$1 } from "./eas-session-ledger-Cm0Gwq2F.mjs";
15
- import { r as readCollectors } from "./ownership-DCtw_2IA.mjs";
16
- import { c as removeWorktrees, l as worktreePullRequestNote, n as previewMaintenance, s as collectWorktreeSweep, u as worktreeRemovalReason } from "./preview-78a9kR_-.mjs";
17
- import { _ as reclaimProject, g as describeDereferenced, v as deleteSelectedArchives, y as sweepArchiveStaging } from "./worktree-CgvJYJ-Q.mjs";
18
- import { i as getSwiftpmCacheUsage, n as swiftpmCacheLines, r as getAgentDeviceUsage, t as agentDeviceLines } from "./agent-device-usage-output-Dzgv6lgR.mjs";
1
+ import { f as ownership_claim_exports, p as process_identity_exports, t as getExecutor } from "./exec-_UbtCQcG.mjs";
2
+ import { d as workspaceDir, v as withDirLock$1 } from "./paths-DQtTgsBf.mjs";
3
+ import { D as withConfigLock$1, F as isOnMountedVolume, I as listMountedVolumes, K as projectDeviceSlots, N as directorySizeAsync, P as formatBytes, R as measuredDirectorySizeAsync, S as removeProject, _ as loadConfig$1, j as acquireAvdClaim, z as volumeRootFor } from "./config-CYcBF478.mjs";
4
+ import { h as shortUdid, o as formatLongDuration, u as plural } from "./command-output-B1UrjZHL.mjs";
5
+ import { u as workspaceLastUsed } from "./workspace-state-Cjuigeeu.mjs";
6
+ import { kt as listWorktreesAsync, zt as resolveSourceCheckoutAsync } from "./settings-CsJDEXgK.mjs";
7
+ import { i as findProjectRoot$1, v as detectIsExpo } from "./project-BZNjmW_8.mjs";
8
+ import { a as removeEasSessionClaim, n as easMachineStateRoot$1, r as readEasSessionLedger$1 } from "./eas-session-ledger-op6A54yi.mjs";
9
+ import { $ as includesRecordings, At as unverifiedDeviceCommand, C as collectIdleDevices, D as idleDeviceLines, Dt as findOrphanedDevices, Et as eraseParkedDevices, Ft as withRemoteSessionGcLocks, Kt as withEasProjectLock, Mt as collectEasSessionSweep, Nt as deleteEasSessions, O as parseIdleDuration, Ot as findStaleDeviceRecords, Pt as describeError, Q as includesParkedDevices, Sr as sizeCaches, St as deleteProjectDevices, Tt as deviceSweepIsScoped, X as emptyCaches, Z as includesArchives, _r as phase, _t as collectDeviceLeases, at as selectsBeyondCaches, br as discoverCaches, bt as deleteParkedAvds, dr as memoryScope, dt as gradleVersionsPending, et as includesWorkspaceOutputs, fr as memorySweepIsScoped, ft as includesGradleVersions, gt as DEVICE_LIST_TIMEOUT_MS, hr as scopedMemoryReport, ht as collectInventory, ir as parseArchiveSelector, it as selectCaches, jt as withAndroidAvdSizes, k as shutDownIdleDevices, kt as findStaleProjectDevices, lr as memoryCacheKind, lt as deleteGradleVersions, mr as reclaimMemory, mt as getMachineCacheReport, nt as planCacheEmptying, ot as trimCaches, pr as memoryWorkPending, rt as previewScopedEmptying, sr as collectMemoryReport, st as collectGradleVersions, tt as isWorkspacesScope, ur as memoryLines, ut as gradleVersionLines, vt as collectParkedAvds, w as collectIdleDevicesAsync, wt as describeUnverifiableDevices, xt as deleteParkedSims, yt as collectParkedSims } from "./budget-Bs6SxxzO.mjs";
10
+ import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
11
+ import { B as takeGcResults, I as listBuildSlots, a as collectWorkspaceOutputs, c as listWorkspaceDirs, f as withIdleWorkspace, i as collectOrphanedWorkspaces, l as rootPresence, n as WORKSPACE_OUTPUT_DIRS, o as deleteOrphanedWorkspaces, p as workspaceInUse, r as clearWorkspaceOutputs, s as isInsideWorkspaces, t as REBUILD_COST, u as sizeText, z as recordGcResult } from "./workspaces-BccMFmGQ.mjs";
12
+ import { N as listBuildLocks, k as removeExpiredLease } from "./activity-BzVfHSrY.mjs";
13
+ import { n as readCreatedDevices$1, t as forgetCreatedDevice } from "./created-devices-CqeBlrEn.mjs";
14
+ import { F as listOrphanedAvdDirectories, M as listAvds, N as listAvdsAsync, ht as parkedMaxSetting, lt as POOL_SETTING_REMEDY, m as avdNameAbsent, z as ownedAvdDirectory } from "./android-DUYkNF4u.mjs";
15
+ import { d as listAllIosSims, f as listAllIosSimsAsync } from "./ios-CpByZy12.mjs";
16
+ import { r as readCollectors } from "./ownership-DEmM5vhh.mjs";
17
+ import { _ as describeDereferenced, a as matchWorktreeEntry, b as sweepArchiveStaging, v as reclaimProject, y as deleteSelectedArchives } from "./worktree-DQ6Zi2Mv.mjs";
18
+ import { s as writeCacheFile } from "./warm-progress-BHX5r0Ei.mjs";
19
+ import { _ as collectWorktreeSweep, a as previewMaintenance, b as worktreeRemovalReason, c as deleteTempLeftovers, d as ARTIFACT_DIRS, f as appRoots, g as candidateRootsAsync, h as ownedDeviceUsage, l as tempLeftoverLines, o as triggeringRoot, p as finishedWorkspaces, s as collectTempLeftovers, t as deleteIdleWorkspaceDevices, u as tempLeftoverSection, v as removeWorktrees, y as worktreePullRequestNote } from "./act-DHAw2A2S.mjs";
20
+ import { i as getSwiftpmCacheUsage, n as swiftpmCacheLines, r as getAgentDeviceUsage, t as agentDeviceLines } from "./agent-device-usage-output-zh1vhbLp.mjs";
19
21
  import { existsSync, lstatSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
20
- import { basename, dirname, isAbsolute, join } from "node:path";
21
- import { basename as basename$1, dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1 } from "path";
22
- import { assertHostedDeviceLedger, deviceHostArea, deviceHostRoot, hostedDeviceId, isJsonObject, listRecordedDevices, listStatusCacheEntries, maintenanceRunClaims, parseHostedPlatformDevice, readArchives, readHostedDevice, readHostedDeviceLedger, readHostedSessions, recordedBytes } from "@stim-cli/core/state";
23
- import { fileURLToPath } from "node:url";
24
- import { closeSync as closeSync$1, existsSync as existsSync$1, openSync as openSync$1, readFileSync as readFileSync$1, readSync as readSync$1, readdirSync as readdirSync$1, renameSync as renameSync$1, rmSync as rmSync$1, statSync as statSync$1, utimesSync as utimesSync$1, writeFileSync as writeFileSync$1 } from "fs";
22
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
25
23
  import { LOG_ROTATE_BYTES, rotatedLogPath } from "@stim-cli/core";
26
- import { InvalidArgumentError } from "commander";
24
+ import { assertHostedDeviceLedger, deviceHostArea, deviceHostRoot, diskUsageCacheFile, hostedDeviceId, isJsonObject, listRecordedDevices, listStatusCacheEntries, maintenanceRunClaims, parseHostedPlatformDevice, readArchives, readDiskUsage, readHostedDevice, readHostedDeviceLedger, readHostedSessions, recordedBytes } from "@stim-cli/core/state";
25
+ import { isAbsolute as isAbsolute$1, join as join$1 } from "path";
26
+ import { fileURLToPath } from "node:url";
27
27
  import chalk from "chalk";
28
+ import { closeSync as closeSync$1, existsSync as existsSync$1, openSync as openSync$1, readSync as readSync$1, readdirSync as readdirSync$1, renameSync as renameSync$1, rmSync as rmSync$1, statSync as statSync$1, utimesSync as utimesSync$1, writeFileSync as writeFileSync$1 } from "fs";
29
+ import { InvalidArgumentError } from "commander";
28
30
  //#region src/commands/gc/archives.ts
29
31
  function stagingOrNone() {
30
32
  try {
@@ -95,439 +97,16 @@ function deleteArchives(selection) {
95
97
  }
96
98
  function archiveRefusal(selection) {
97
99
  if (selection?.unknownId === void 0 || selection.unknownId === null) return null;
98
- process.exitCode = 1;
99
- const message = `Unknown archive "${selection.unknownId}".`;
100
- const remedy = `Known archive ids: ${selection.knownIds.join(", ") || "(none)"}`;
101
- console.error(chalk.red(message));
102
- console.error(chalk.dim(remedy));
103
- console.error(chalk.red("failed: STIM_BAD_ARG"));
104
100
  return {
105
101
  code: "STIM_BAD_ARG",
106
- message,
107
- remedy
102
+ message: `Unknown archive "${selection.unknownId}".`,
103
+ remedy: `Known archive ids: ${selection.knownIds.join(", ") || "(none)"}`
108
104
  };
109
105
  }
110
106
  function archiveWorkPending(selection, otherWork) {
111
107
  return otherWork || Boolean(selection?.records.some((record) => record.willDelete) || selection?.staging.some((entry) => !entry.kept));
112
108
  }
113
109
  //#endregion
114
- //#region src/commands/gc/eas-sessions.ts
115
- const EAS_OPERATION_TIMEOUT_MS = 3e4;
116
- const EAS_COLLECTION_TIMEOUT_MS = 6e4;
117
- const EAS_MAX_LIST_PAGES = 100;
118
- const EAS_LOCK_WAIT_MS = 0;
119
- const EAS_LOCK_SNAPSHOT_RETRIES = 3;
120
- const PROXY_CREDENTIAL_ENV = ["AGENT_DEVICE_DAEMON_BASE_URL", "AGENT_DEVICE_DAEMON_AUTH_TOKEN"];
121
- function describeError(error) {
122
- const candidate = error;
123
- if (typeof candidate?.stderr === "string" && candidate.stderr.trim()) return candidate.stderr.trim();
124
- if (typeof candidate?.message === "string" && candidate.message.trim()) return candidate.message.trim();
125
- return String(error);
126
- }
127
- function readRecordedRemoteSessionIds(roots) {
128
- const ids = /* @__PURE__ */ new Set();
129
- const notices = [];
130
- let safe = true;
131
- for (const root of new Set(roots)) {
132
- try {
133
- if (!statSync$1(root).isDirectory()) throw new Error("path is not a directory");
134
- readdirSync$1(root);
135
- } catch (error) {
136
- safe = false;
137
- notices.push(`${root} is not available as a readable workspace directory: ${describeError(error)}`);
138
- continue;
139
- }
140
- const path = workspaceStateFile(root);
141
- if (!existsSync$1(path)) continue;
142
- let state;
143
- try {
144
- state = JSON.parse(readFileSync$1(path, "utf-8"));
145
- } catch (error) {
146
- safe = false;
147
- notices.push(`${path} could not be read as valid JSON: ${describeError(error)}`);
148
- continue;
149
- }
150
- if (!state || typeof state !== "object" || Array.isArray(state)) {
151
- safe = false;
152
- notices.push(`${path} does not contain a valid workspace state object.`);
153
- continue;
154
- }
155
- const remote = state.remoteDevice;
156
- if (remote === void 0) continue;
157
- if (!remote || typeof remote !== "object" || Array.isArray(remote)) {
158
- safe = false;
159
- notices.push(`${path} has a malformed remote session record.`);
160
- continue;
161
- }
162
- const id = remote.sessionId;
163
- if (typeof id !== "string" || id.length === 0) {
164
- safe = false;
165
- notices.push(`${path} has a remote session record with no valid session id.`);
166
- continue;
167
- }
168
- ids.add(id);
169
- }
170
- return {
171
- ids: [...ids],
172
- notices,
173
- safe
174
- };
175
- }
176
- function claimStateAbsence(claim) {
177
- const stateDir = dirname$1(claim.stateFile);
178
- let entries;
179
- try {
180
- if (!statSync$1(stateDir).isDirectory()) throw new Error("path is not a directory");
181
- entries = readdirSync$1(stateDir);
182
- } catch (error) {
183
- return {
184
- absent: false,
185
- notice: `${stateDir} is not available for EAS ownership verification: ${describeError(error)}`
186
- };
187
- }
188
- if (!entries.includes(basename$1(claim.stateFile))) return {
189
- absent: true,
190
- notice: null
191
- };
192
- let state;
193
- try {
194
- state = JSON.parse(readFileSync$1(claim.stateFile, "utf-8"));
195
- } catch (error) {
196
- return {
197
- absent: false,
198
- notice: `${claim.stateFile} could not be read as valid JSON: ${describeError(error)}`
199
- };
200
- }
201
- if (!state || typeof state !== "object" || Array.isArray(state)) return {
202
- absent: false,
203
- notice: `${claim.stateFile} does not contain a valid workspace state object.`
204
- };
205
- const remote = state.remoteDevice;
206
- if (remote === void 0) return {
207
- absent: true,
208
- notice: null
209
- };
210
- if (!remote || typeof remote !== "object" || Array.isArray(remote)) return {
211
- absent: false,
212
- notice: `${claim.stateFile} has a malformed remote session record.`
213
- };
214
- const sessionId = remote.sessionId;
215
- if (typeof sessionId !== "string" || !sessionId) return {
216
- absent: false,
217
- notice: `${claim.stateFile} has a remote session record with no valid session id.`
218
- };
219
- return {
220
- absent: sessionId !== claim.sessionId,
221
- notice: null
222
- };
223
- }
224
- function collectEasSessionSweep(config, deps) {
225
- const projectRoot = (deps.findProjectRoot ?? findProjectRoot)(process.cwd());
226
- const empty = (notices = []) => ({
227
- projectScope: projectRoot,
228
- orphaned: [],
229
- notices,
230
- deletionSafe: notices.length === 0
231
- });
232
- if (!projectRoot || !(deps.detectIsExpo ?? detectIsExpo)(projectRoot)) return empty();
233
- if (deps.easSweepBlockedNotice) return empty([deps.easSweepBlockedNotice]);
234
- const eas = (deps.resolveEasCliBin ?? resolveEasCliBin)(projectRoot);
235
- if (!eas) return empty(["EAS session sweep skipped: eas-cli is not available for the current Expo project."]);
236
- const recorded = readRecordedRemoteSessionIds([...Object.keys(config?.projects ?? {}), projectRoot]);
237
- const ledgerRoot = deps.easLedgerRoot ?? easMachineStateRoot$1();
238
- const ledger = readEasSessionLedger$1(ledgerRoot);
239
- const run = deps.runEasFile ?? ((file, args, options) => getExecutor().runFile(file, args, options));
240
- const sessions = [];
241
- const seenCursors = /* @__PURE__ */ new Set();
242
- const now = deps.easNow ?? Date.now;
243
- const collectionTimeoutMs = deps.easCollectionTimeoutMs ?? EAS_COLLECTION_TIMEOUT_MS;
244
- const maxPages = deps.easMaxPages ?? EAS_MAX_LIST_PAGES;
245
- const deadline = now() + collectionTimeoutMs;
246
- let pageCount = 0;
247
- let after = null;
248
- while (true) {
249
- if (pageCount >= maxPages) return empty([...recorded.notices, `EAS session list exceeded the maximum page limit of ${maxPages} for ${projectRoot}.`]);
250
- const remainingMs = deadline - now();
251
- if (remainingMs <= 0) return empty([...recorded.notices, `EAS session list exceeded its total time limit for ${projectRoot}.`]);
252
- let stdout;
253
- try {
254
- stdout = run(eas.file, listOwnedSessionsArgs(after), {
255
- cwd: projectRoot,
256
- timeoutMs: Math.min(EAS_OPERATION_TIMEOUT_MS, remainingMs),
257
- omitEnv: PROXY_CREDENTIAL_ENV
258
- });
259
- } catch (error) {
260
- return empty([...recorded.notices, `EAS session list failed for ${projectRoot}: ${describeError(error)}`]);
261
- }
262
- pageCount++;
263
- if (now() > deadline) return empty([...recorded.notices, `EAS session list exceeded its total time limit for ${projectRoot}.`]);
264
- const parsed = parseSessionListPage(stdout);
265
- if (!parsed.ok) return empty([...recorded.notices, parsed.reason]);
266
- sessions.push(...parsed.page.sessions);
267
- if (!parsed.page.hasNextPage) break;
268
- const cursor = parsed.page.endCursor?.trim();
269
- if (!cursor) return empty([...recorded.notices, "EAS session list has a next page but returned no pagination cursor."]);
270
- if (seenCursors.has(cursor)) return empty([...recorded.notices, `EAS session list repeated pagination cursor ${cursor}.`]);
271
- seenCursors.add(cursor);
272
- after = cursor;
273
- }
274
- const lockedRoots = deps.easLockedRoots;
275
- if (lockedRoots) {
276
- if (easLockRoots(projectRoot, loadConfig$1()).some((root) => !lockedRoots.includes(root))) return empty([...recorded.notices, "EAS session sweep skipped: registered workspace roots changed after remote-session locks were acquired."]);
277
- }
278
- const compared = findOrphanedOwnedSessions({
279
- sessions,
280
- recordedSessionIds: recorded.ids,
281
- projectScope: projectRoot
282
- });
283
- const notices = [...recorded.notices, ...compared.notices];
284
- if (ledger.notice) notices.push(ledger.notice);
285
- const orphaned = [];
286
- if (ledger.safe && recorded.safe) for (const session of compared.orphaned) {
287
- const claim = ledger.claims.get(session.id);
288
- if (!claim || claim.name !== session.name || claim.platform !== session.platform) {
289
- notices.push(`EAS session ${session.id} (${session.name}) is unclaimed or its fixed ownership record does not match; it was not selected for deletion.`);
290
- continue;
291
- }
292
- const state = claimStateAbsence(claim);
293
- if (state.notice) {
294
- notices.push(state.notice);
295
- continue;
296
- }
297
- if (state.absent) orphaned.push(session);
298
- }
299
- return {
300
- projectScope: projectRoot,
301
- orphaned,
302
- notices,
303
- deletionSafe: recorded.safe && ledger.safe
304
- };
305
- }
306
- function easLockRoots(projectRoot, config) {
307
- return [...new Set([...Object.keys(config?.projects ?? {}), projectRoot].map(canonicalPath))].toSorted();
308
- }
309
- function unavailableLockRootNotice(roots) {
310
- for (const root of roots) try {
311
- if (!statSync$1(root).isDirectory()) throw new Error("path is not a directory");
312
- readdirSync$1(root);
313
- } catch (error) {
314
- return `EAS session sweep skipped: ${root} is not available for remote-session lock inspection: ${describeError(error)}`;
315
- }
316
- return null;
317
- }
318
- function malformedRemoteSessionLockNotice(roots) {
319
- for (const root of roots) {
320
- const lockPath = join$1(workspaceDir(root), "remote-session.lock");
321
- if (!existsSync$1(lockPath)) continue;
322
- if ((0, ownership_claim_exports.readClaimSet)(lockPath).unresolved.length === 0) continue;
323
- return `EAS session sweep skipped: the remote-session lock at ${lockPath} has a malformed owner record.`;
324
- }
325
- return null;
326
- }
327
- async function withRemoteSessionGcLocks(projectRoot, deps, fn) {
328
- const withLock = deps.withRemoteSessionLock ?? withRemoteSessionLock;
329
- const waitMs = deps.easLockWaitMs ?? EAS_LOCK_WAIT_MS;
330
- const maxSnapshots = deps.easLockSnapshotRetries ?? EAS_LOCK_SNAPSHOT_RETRIES;
331
- for (let attempt = 0; attempt < maxSnapshots; attempt++) {
332
- const roots = easLockRoots(projectRoot, loadConfig$1());
333
- const unavailable = unavailableLockRootNotice(roots);
334
- if (unavailable) return fn({
335
- ...deps,
336
- easSweepBlockedNotice: unavailable
337
- });
338
- const malformed = malformedRemoteSessionLockNotice(roots);
339
- if (malformed) return fn({
340
- ...deps,
341
- easSweepBlockedNotice: malformed
342
- });
343
- let expanded = false;
344
- let coreStarted = false;
345
- const acquire = (index) => index === roots.length ? (async () => {
346
- if (easLockRoots(projectRoot, loadConfig$1()).some((root) => !roots.includes(root))) {
347
- expanded = true;
348
- return null;
349
- }
350
- coreStarted = true;
351
- return fn({
352
- ...deps,
353
- easSweepBlockedNotice: void 0,
354
- easLockedRoots: roots
355
- });
356
- })() : withLock(roots[index], () => acquire(index + 1), { waitMs });
357
- try {
358
- const result = await acquire(0);
359
- if (!expanded) return result;
360
- } catch (error) {
361
- if (coreStarted) throw error;
362
- return fn({
363
- ...deps,
364
- easSweepBlockedNotice: `EAS session sweep skipped: remote-session lock acquisition failed: ${describeError(error)}`
365
- });
366
- }
367
- }
368
- return fn({
369
- ...deps,
370
- easSweepBlockedNotice: `EAS session sweep skipped: registered workspace roots kept changing during remote-session lock acquisition.`
371
- });
372
- }
373
- async function deleteEasSessions(easSessionSweep, deps) {
374
- let deleteFailures = 0;
375
- const refuseAll = (detail) => {
376
- deleteFailures += easSessionSweep.orphaned.length;
377
- for (const session of easSessionSweep.orphaned) recordGcResult("easSession", "failed", session.name, {
378
- id: session.id,
379
- detail
380
- });
381
- };
382
- if (easSessionSweep.orphaned.length && easSessionSweep.deletionSafe && easSessionSweep.projectScope) {
383
- const projectScope = easSessionSweep.projectScope;
384
- const withProjectLock = deps.withEasProjectLock ?? withEasProjectLock;
385
- let finalSweepStarted = false;
386
- try {
387
- await withProjectLock(projectScope, () => {
388
- finalSweepStarted = true;
389
- return withRemoteSessionGcLocks(projectScope, deps, async (lockedDeps) => {
390
- if (lockedDeps.easSweepBlockedNotice) {
391
- refuseAll(`EAS session deletion refused: ${lockedDeps.easSweepBlockedNotice}`);
392
- console.log(chalk.red(`EAS session deletion refused: ${lockedDeps.easSweepBlockedNotice}`));
393
- return;
394
- }
395
- const coordinatedDeps = lockedDeps;
396
- const currentConfig = loadConfig$1();
397
- const currentRoots = easLockRoots(projectScope, currentConfig);
398
- const registryExpanded = coordinatedDeps.easLockedRoots && currentRoots.some((root) => !coordinatedDeps.easLockedRoots.includes(root));
399
- const currentRecords = registryExpanded ? null : readRecordedRemoteSessionIds([...Object.keys(currentConfig?.projects ?? {}), projectScope]);
400
- const ledgerRoot = coordinatedDeps.easLedgerRoot ?? easMachineStateRoot$1();
401
- const ledger = readEasSessionLedger$1(ledgerRoot);
402
- const removeClaim = coordinatedDeps.removeEasSessionClaim ?? removeEasSessionClaim;
403
- const reconcileClaim = (sessionId) => {
404
- let detail = "the claim store did not remove it";
405
- try {
406
- if (removeClaim(sessionId, ledgerRoot)) return;
407
- } catch (error) {
408
- detail = describeError(error);
409
- }
410
- deleteFailures++;
411
- console.log(chalk.red(`EAS session ${sessionId} is resolved, but its ownership claim could not be removed: ${detail}. The claim was retained for reconciliation.`));
412
- };
413
- if (registryExpanded) {
414
- refuseAll("EAS session deletion refused: registered workspace roots changed after classification.");
415
- console.log(chalk.red("EAS session deletion refused: registered workspace roots changed after classification."));
416
- } else if (!currentRecords?.safe || !ledger.safe) {
417
- refuseAll("EAS session deletion refused: the session records could not be read safely.");
418
- for (const notice of [...currentRecords?.notices ?? [], ...ledger.notice ? [ledger.notice] : []]) console.log(chalk.red(`EAS session deletion refused: ${notice}`));
419
- } else {
420
- const recorded = new Set(currentRecords.ids);
421
- const eas = (coordinatedDeps.resolveEasCliBin ?? resolveEasCliBin)(projectScope);
422
- const run = coordinatedDeps.runEasFile ?? ((file, args, options) => getExecutor().runFile(file, args, options));
423
- for (const session of easSessionSweep.orphaned) {
424
- if (recorded.has(session.id)) {
425
- console.log(chalk.dim(`EAS session ${session.id} has a workspace record and was left running.`));
426
- recordGcResult("easSession", "kept", session.name, {
427
- id: session.id,
428
- detail: "it has a workspace record and was left running"
429
- });
430
- continue;
431
- }
432
- const claim = ledger.claims.get(session.id);
433
- const claimState = claim ? claimStateAbsence(claim) : null;
434
- if (!claim || claim.name !== session.name || claim.platform !== session.platform || !claimState?.absent || claimState.notice) {
435
- deleteFailures++;
436
- console.log(chalk.red(`Could not verify fixed ownership for EAS session ${session.id}: ${claimState?.notice ?? "the claim is missing, mismatched, or still recorded in workspace state"}.`));
437
- recordGcResult("easSession", "failed", session.name, {
438
- id: session.id,
439
- detail: `Could not verify fixed ownership for EAS session ${session.id}: ${claimState?.notice ?? "the claim is missing, mismatched, or still recorded in workspace state"}.`
440
- });
441
- continue;
442
- }
443
- if (!eas) {
444
- deleteFailures++;
445
- console.log(chalk.red(`Could not verify EAS session ${session.id}: eas-cli is not available.`));
446
- recordGcResult("easSession", "failed", session.name, {
447
- id: session.id,
448
- detail: `Could not verify EAS session ${session.id}: eas-cli is not available.`
449
- });
450
- continue;
451
- }
452
- const options = {
453
- cwd: session.projectScope,
454
- timeoutMs: EAS_OPERATION_TIMEOUT_MS,
455
- omitEnv: PROXY_CREDENTIAL_ENV
456
- };
457
- let live;
458
- try {
459
- live = run(eas.file, getSessionArgs(session.id), options);
460
- } catch (error) {
461
- if (isDefinitiveMissingSessionError(error, session.id)) {
462
- console.log(chalk.dim(`EAS session ${session.id} is already gone.`));
463
- reconcileClaim(session.id);
464
- } else {
465
- deleteFailures++;
466
- console.log(chalk.red(`Could not verify EAS session ${session.id}: ${describeError(error)}`));
467
- recordGcResult("easSession", "failed", session.name, {
468
- id: session.id,
469
- detail: `Could not verify EAS session ${session.id}: ${describeError(error)}`
470
- });
471
- }
472
- continue;
473
- }
474
- const inspection = inspectSessionForTeardown(live, session.id);
475
- if (inspection.action === "already-stopped") {
476
- console.log(chalk.dim(`EAS session ${session.id} is already stopped (${inspection.status}).`));
477
- reconcileClaim(session.id);
478
- continue;
479
- }
480
- if (inspection.action === "refused") {
481
- deleteFailures++;
482
- console.log(chalk.red(`Could not stop EAS session ${session.id}: ${inspection.reason}`));
483
- recordGcResult("easSession", "failed", session.name, {
484
- id: session.id,
485
- detail: `Could not stop EAS session ${session.id}: ${inspection.reason}`
486
- });
487
- continue;
488
- }
489
- let stopped;
490
- try {
491
- stopped = run(eas.file, stopSessionArgs(session.id), options);
492
- } catch (error) {
493
- deleteFailures++;
494
- console.log(chalk.red(`Could not stop EAS session ${session.id}: ${describeError(error)}`));
495
- recordGcResult("easSession", "failed", session.name, {
496
- id: session.id,
497
- detail: `Could not stop EAS session ${session.id}: ${describeError(error)}`
498
- });
499
- continue;
500
- }
501
- const verified = verifyStoppedSession(stopped, session.id);
502
- if (!verified.ok) {
503
- deleteFailures++;
504
- console.log(chalk.red(`Could not verify EAS session ${session.id} stopped: ${verified.reason}`));
505
- recordGcResult("easSession", "failed", session.name, {
506
- id: session.id,
507
- detail: `Could not verify EAS session ${session.id} stopped: ${verified.reason}`
508
- });
509
- continue;
510
- }
511
- console.log(chalk.green(`Stopped EAS session ${session.id} (${session.name})`));
512
- recordGcResult("easSession", "done", session.name, { id: session.id });
513
- reconcileClaim(session.id);
514
- }
515
- }
516
- });
517
- }, {
518
- waitMs: 0,
519
- ownerPurpose: "EAS orphan deletion",
520
- machineRoot: deps.easLedgerRoot
521
- });
522
- } catch (error) {
523
- const phase = finalSweepStarted ? "failed" : "lock acquisition failed";
524
- refuseAll(`EAS session deletion ${phase}: ${describeError(error)}`);
525
- console.log(chalk.red(`EAS session deletion ${phase}: ${describeError(error)}`));
526
- }
527
- }
528
- return deleteFailures;
529
- }
530
- //#endregion
531
110
  //#region src/commands/gc/eas-ledger.ts
532
111
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
533
112
  /**
@@ -562,6 +141,71 @@ function forgetStaleEasLedgerEntries(entries, mountedVolumes, deps = {}) {
562
141
  return failures;
563
142
  }
564
143
  //#endregion
144
+ //#region src/devices/machine-cache-usage-output.ts
145
+ const size = (bytes) => bytes === null ? "unknown" : formatBytes(bytes);
146
+ function total(values) {
147
+ const known = values.reduce((sum, value) => sum + (value ?? 0), 0);
148
+ return `${values.some((value) => value === null) ? "at least " : ""}${formatBytes(known)}`;
149
+ }
150
+ function machineCacheLines(report, now = Date.now()) {
151
+ if (!report) return [];
152
+ const { gradle, simRuntimes, deviceSupport, runnerBuilds } = report;
153
+ const body = [];
154
+ if (gradle.unusedVersions.length) {
155
+ body.push(` Gradle versions no registered project pins (${total(gradle.unusedVersions.map((entry) => entry.bytes))}):`);
156
+ for (const entry of gradle.unusedVersions) {
157
+ body.push(` ${entry.version}: ${size(entry.bytes)}`);
158
+ for (const dir of entry.dirs) body.push(` ${dir.dir}`);
159
+ if (entry.remedy) body.push(chalk.dim(` remedy: ${entry.remedy}`));
160
+ }
161
+ if (gradle.unreadableProjects.length) body.push(chalk.dim(` ${gradle.unreadableProjects.length} registered project${gradle.unreadableProjects.length === 1 ? " is" : "s are"} missing or unreadable, so a version listed here may still be in use: ${gradle.unreadableProjects.join(", ")}`));
162
+ }
163
+ if (gradle.modules) {
164
+ body.push(` Gradle dependency cache: ${size(gradle.modules.bytes)}`, ` ${gradle.modules.dir}`);
165
+ body.push(chalk.dim(` remedy: ${gradle.modules.remedy}`));
166
+ }
167
+ if (!simRuntimes.checked) body.push(chalk.dim(" Simulator runtimes: not checked, simctl did not answer"));
168
+ else if (simRuntimes.runtimes.length) {
169
+ body.push(` Simulator runtimes (${total(simRuntimes.runtimes.map((entry) => entry.bytes))}):`);
170
+ for (const runtime of simRuntimes.runtimes) {
171
+ const details = [
172
+ runtime.devices ? `${runtime.devices} simulator${runtime.devices === 1 ? "" : "s"}` : "no simulator uses it",
173
+ runtime.lastUsedAt ? `last used ${runtime.lastUsedAt.slice(0, 10)}` : null,
174
+ runtime.deletable ? null : "not deletable"
175
+ ].filter(Boolean);
176
+ body.push(` ${runtime.runtimeIdentifier.replace(/^com\.apple\.CoreSimulator\.SimRuntime\./, "")}${runtime.build ? ` (${runtime.build})` : ""}: ${size(runtime.bytes)}, ${details.join(", ")}`);
177
+ if (runtime.remedy) body.push(chalk.dim(` remedy: ${runtime.remedy}`));
178
+ }
179
+ }
180
+ if (deviceSupport.entries.length) {
181
+ body.push(` Xcode device support (${total(deviceSupport.entries.map((entry) => entry.bytes))}):`);
182
+ for (const entry of deviceSupport.entries) body.push(` ${entry.dir}: ${size(entry.bytes)}`);
183
+ body.push(chalk.dim(` remedy: ${deviceSupport.remedy}`));
184
+ }
185
+ if (runnerBuilds.groups.length) {
186
+ const currentVersions = runnerBuilds.agentDeviceVersion && runnerBuilds.xcodeBuildVersion ? `current: agent-device ${runnerBuilds.agentDeviceVersion}, Xcode ${runnerBuilds.xcodeBuildVersion}` : "current versions unknown";
187
+ body.push(` agent-device runner builds by version (${currentVersions}):`);
188
+ for (const group of runnerBuilds.groups) {
189
+ const details = [
190
+ `${group.entries} entr${group.entries === 1 ? "y" : "ies"}`,
191
+ size(group.bytes),
192
+ group.oldestUseAt ? `oldest use ${group.oldestUseAt.slice(0, 10)}` : null,
193
+ group.inUse ? `${group.inUse} in use` : null,
194
+ group.current ? "current" : group.current === false ? "older toolchain" : null
195
+ ].filter(Boolean);
196
+ body.push(` agent-device ${group.packageVersion ?? "unknown"}, Xcode ${group.xcodeBuildVersion ?? "unknown"}: ${details.join(", ")}`);
197
+ }
198
+ body.push(chalk.dim(` remedy: ${runnerBuilds.remedy}`));
199
+ }
200
+ for (const notice of report.notices) body.push(chalk.dim(` ${notice}`));
201
+ if (!body.length) return [];
202
+ return [
203
+ `Other tools' machine-wide caches${report.complete ? "" : " (incomplete)"} - measured ${formatLongDuration(Math.max(0, now - Date.parse(report.measuredAt)))} ago`,
204
+ ...body,
205
+ chalk.dim(" Stim reports these and never deletes them; use each tool's own cleanup.")
206
+ ];
207
+ }
208
+ //#endregion
565
209
  //#region src/commands/gc/status-caches.ts
566
210
  const KINDS = ["disk-usage", "pull-request"];
567
211
  const KEPT_REASON = {
@@ -657,6 +301,81 @@ function deleteStaleStatusCaches(stale) {
657
301
  return failures;
658
302
  }
659
303
  //#endregion
304
+ //#region src/commands/gc/finished-devices.ts
305
+ /** The owned devices of workspaces whose linked worktree is finished but kept, such as a dirty one. */
306
+ function collectFinishedDevices({ config, sims, avds, sweep, now = Date.now() }) {
307
+ const keys = Object.keys(config?.projects ?? {});
308
+ const devices = [];
309
+ const seen = /* @__PURE__ */ new Set();
310
+ for (const { root, reason } of finishedWorkspaces(sweep, now)) {
311
+ const key = keys.find((candidate) => canonicalPath(candidate) === root);
312
+ const project = key ? config?.projects[key] : void 0;
313
+ if (!config || !key || !project || seen.has(key)) continue;
314
+ seen.add(key);
315
+ for (const { idleDays: _idleDays, ...device } of findStaleProjectDevices({
316
+ config: {
317
+ ...config,
318
+ projects: { [key]: project }
319
+ },
320
+ sims,
321
+ avds,
322
+ olderThanDays: 0,
323
+ lastTouched: () => 0
324
+ })) devices.push({
325
+ ...device,
326
+ reason
327
+ });
328
+ }
329
+ return devices;
330
+ }
331
+ function finishedDeviceLines(devices = []) {
332
+ if (!devices.length) return [];
333
+ const lines = [`Owned devices of finished worktrees (${devices.length}) - the worktree is kept, its devices are not:`];
334
+ for (const device of devices) {
335
+ lines.push(` ${device.kind} ${device.name} (${device.id})${device.bytes ? ` ${formatBytes(device.bytes)}` : ""}`);
336
+ lines.push(` ${device.project}: ${device.reason}`);
337
+ }
338
+ lines.push(" --delete keeps a workspace that is in use, leased, driven, locked or pinned.");
339
+ return lines;
340
+ }
341
+ async function deleteFinishedDevices({ finishedDevices: devices, worktreeSweep }) {
342
+ const graceMs = worktreeSweep?.graceMs ?? 0;
343
+ let failures = 0;
344
+ for (const project of new Set(devices.map((device) => device.project))) {
345
+ const outcome = await deleteIdleWorkspaceDevices(canonicalPath(project), {
346
+ windowMs: graceMs,
347
+ protectedRoot: triggeringRoot(),
348
+ purpose: "stim gc --delete"
349
+ });
350
+ if (outcome.status === "kept") {
351
+ console.log(chalk.yellow(`Kept the owned devices of ${project}: ${outcome.detail}`));
352
+ recordGcResult("device", "kept", project, { detail: outcome.detail });
353
+ } else if (outcome.status === "failed") {
354
+ failures++;
355
+ console.log(chalk.red(`Could not delete the owned devices of ${project}: ${outcome.detail}`));
356
+ recordGcResult("device", "failed", project, { detail: outcome.detail });
357
+ }
358
+ }
359
+ return failures;
360
+ }
361
+ /** Owned simulator and emulator disk of registered workspaces, by platform. */
362
+ function ownedDeviceDisk(usage, unmeasured = {}) {
363
+ return ["ios", "android"].flatMap((kind) => {
364
+ const owned = usage.filter((device) => device.kind === kind && device.workspace);
365
+ const missing = unmeasured[kind] ?? 0;
366
+ return owned.length || missing ? [{
367
+ kind,
368
+ count: owned.length,
369
+ bytes: owned.reduce((sum, device) => sum + device.bytes, 0),
370
+ unmeasured: missing
371
+ }] : [];
372
+ });
373
+ }
374
+ function ownedDeviceDiskLines(disk = []) {
375
+ if (!disk.length) return [];
376
+ return [`Owned device disk: ${disk.map(({ kind, count, bytes, unmeasured }) => `${count} ${kind === "ios" ? "simulators" : "emulators"} ${formatBytes(bytes)}${unmeasured ? ` (${unmeasured} not measured)` : ""}`).join(", ")}`];
377
+ }
378
+ //#endregion
660
379
  //#region src/commands/gc/recordings.ts
661
380
  const DAY_MS = 864e5;
662
381
  function oldSegments(devices, olderThan, now) {
@@ -850,7 +569,7 @@ function staleEasLedgerLines(entries = []) {
850
569
  " --delete forgets the LEDGER ENTRY only; no EAS session is contacted."
851
570
  ];
852
571
  }
853
- function formatGcReport({ maintenance, archives, skipped = [], deadProjects = [], orphanedPorts, invalidProjects = [], orphanedWorkspaces = [], parkedSims = [], parkedAvds = [], parkedHostedDevices, orphanedDevices = [], unverifiedDevices = [], staleDevices = [], staleDeviceRecords = [], staleLedgerEntries = [], staleEasLedgerEntries, staleStatusCaches = [], buildLocks = {
572
+ function formatGcReport({ maintenance, archives, skipped = [], deadProjects = [], orphanedPorts, invalidProjects = [], orphanedWorkspaces = [], parkedSims = [], parkedAvds = [], parkedHostedDevices, orphanedDevices = [], unverifiedDevices = [], staleDevices = [], finishedDevices, ownedDeviceDisk, staleDeviceRecords = [], staleLedgerEntries = [], staleEasLedgerEntries, staleStatusCaches = [], buildLocks = {
854
573
  stale: [],
855
574
  live: [],
856
575
  unresolved: []
@@ -866,10 +585,11 @@ function formatGcReport({ maintenance, archives, skipped = [], deadProjects = []
866
585
  orphaned: [],
867
586
  notices: [],
868
587
  deletionSafe: true
869
- }, agentDeviceUsage = null, swiftpmCacheUsage, caches = [], workspaceOutputs = null, workspaceLogs = [], recordings = [], worktreeSweep = null, memory, cacheScope = null, olderThan = null }, { now = Date.now() } = {}) {
588
+ }, agentDeviceUsage = null, swiftpmCacheUsage, machineCaches, tempLeftovers, gradleVersions, caches = [], workspaceOutputs = null, workspaceLogs = [], recordings = [], worktreeSweep = null, memory, cacheScope = null, olderThan = null }, { now = Date.now() } = {}) {
870
589
  const lines = archiveLines(archives);
871
590
  const parkedHostedLines = formatParkedHostedDeviceReport(parkedHostedDevices, now);
872
591
  const easLedgerLines = staleEasLedgerLines(staleEasLedgerEntries);
592
+ const finishedLines = finishedDeviceLines(finishedDevices);
873
593
  const staleLocks = buildLocks?.stale ?? [];
874
594
  const liveLocks = buildLocks?.live ?? [];
875
595
  const staleSlots = buildSlots?.stale ?? [];
@@ -884,6 +604,7 @@ function formatGcReport({ maintenance, archives, skipped = [], deadProjects = []
884
604
  parkedHostedLines,
885
605
  orphanedDevices,
886
606
  staleDevices,
607
+ finishedLines,
887
608
  staleDeviceRecords,
888
609
  staleLedgerEntries,
889
610
  easLedgerLines,
@@ -908,6 +629,7 @@ function formatGcReport({ maintenance, archives, skipped = [], deadProjects = []
908
629
  lines.push(...namedPortLines(orphanedPorts));
909
630
  lines.push(...orphanedWorkspaceLines(orphanedWorkspaces));
910
631
  lines.push(...worktreeSweepLines(worktreeSweep));
632
+ lines.push(...unlistedWorktreeLines(worktreeSweep));
911
633
  lines.push(...formatParkedSimReport(parkedSims, now, cacheScope !== null));
912
634
  lines.push(...formatParkedAvdReport(parkedAvds, now, cacheScope !== null));
913
635
  lines.push(...parkedHostedLines);
@@ -930,6 +652,7 @@ function formatGcReport({ maintenance, archives, skipped = [], deadProjects = []
930
652
  lines.push(` ${d.project} (idle ${d.idleDays}d)`);
931
653
  }
932
654
  }
655
+ lines.push(...finishedLines, ...ownedDeviceDiskLines(ownedDeviceDisk));
933
656
  lines.push(...idleDeviceLines(idleDevices));
934
657
  if (staleDeviceRecords.length) {
935
658
  lines.push(`Stale device records (${staleDeviceRecords.length}) - the device is gone, the project is not:`);
@@ -995,6 +718,9 @@ function formatGcReport({ maintenance, archives, skipped = [], deadProjects = []
995
718
  lines.push(...cacheLines(caches, workspaceOutputs));
996
719
  lines.push(...agentDeviceLines(agentDeviceUsage, true, now));
997
720
  lines.push(...swiftpmCacheLines(swiftpmCacheUsage, now));
721
+ lines.push(...machineCacheLines(machineCaches, now));
722
+ lines.push(...tempLeftoverLines(tempLeftovers));
723
+ lines.push(...gradleVersionLines(gradleVersions));
998
724
  lines.push(...memoryLines(memory, memoryCacheKind(cacheScope), now));
999
725
  lines.push(...maintenanceReportLines(maintenance));
1000
726
  return lines;
@@ -1072,18 +798,20 @@ function cacheLines(caches, workspaceOutputs) {
1072
798
  if (outputs) lines.push(...workspaceOutputLines(outputs, outputBytes));
1073
799
  for (const c of caches) {
1074
800
  const tag = c.source ? ` (${c.source})` : "";
1075
- lines.push(` ${formatBytes(c.bytes ?? 0).padStart(10)} ${c.name}${tag}`);
801
+ lines.push(` ${sizeText(c.bytes ?? null).padStart(10)} ${c.name}${tag}`);
1076
802
  lines.push(` ${c.dir}`);
1077
803
  if (c.note) lines.push(` ${c.note}`);
804
+ if (c.sizeError) lines.push(` ${c.sizeError}`);
1078
805
  if (c.willEmpty) lines.push(" would be EMPTIED");
1079
806
  else if (c.emptySkipped) lines.push(` would be left alone: ${c.emptySkipped}`);
1080
807
  else if (c.scopedEmpty?.emptySkipped) lines.push(` --delete --cache would leave it alone: ${c.scopedEmpty.emptySkipped}`);
1081
808
  }
1082
- lines.push(` total: ${formatBytes(total)}`);
809
+ const incomplete = caches.some((c) => c.bytes === void 0) || outputs?.workspaces.some((w) => w.bytes === null);
810
+ lines.push(` total: ${incomplete ? "at least " : ""}${formatBytes(total)}`);
1083
811
  const doomed = caches.filter((c) => c.willEmpty);
1084
812
  if (doomed.length) {
1085
813
  const doomedBytes = doomed.reduce((n, c) => n + (c.bytes ?? 0), 0);
1086
- lines.push(` would empty ${doomed.length} of these (${formatBytes(doomedBytes)})`);
814
+ lines.push(` would empty ${doomed.length} of these (${doomed.some((c) => c.bytes === void 0) ? "at least " : ""}${formatBytes(doomedBytes)})`);
1087
815
  }
1088
816
  const clearing = outputs?.workspaces.filter((w) => w.willClear) ?? [];
1089
817
  if (clearing.length) {
@@ -1093,6 +821,29 @@ function cacheLines(caches, workspaceOutputs) {
1093
821
  }
1094
822
  return lines;
1095
823
  }
824
+ const BUILD_STATE_DIRS_SHOWN = 4;
825
+ function buildStateLine(state) {
826
+ if (!state?.dirs.length) return null;
827
+ const shown = state.dirs.slice(0, BUILD_STATE_DIRS_SHOWN).map((dir) => `${dir.path} ${sizeText(dir.bytes)}`);
828
+ const more = state.dirs.length > BUILD_STATE_DIRS_SHOWN ? ", ..." : "";
829
+ return ` in-tree build state ${sizeText(state.bytes)}: ${shown.join(", ")}${more}`;
830
+ }
831
+ function unlistedWorktreeLines(sweep) {
832
+ const unlisted = sweep?.unlisted ?? [];
833
+ if (!unlisted.length) return [];
834
+ const lines = [`Directories under .worktrees that git does not list as worktrees (${unlisted.length}) - reported, never removed:`];
835
+ for (const dir of unlisted) {
836
+ lines.push(` ${dir.path} (${sizeText(dir.bytes)})`);
837
+ lines.push(` ${unlistedWorktreeDetail(dir)}`);
838
+ }
839
+ return lines;
840
+ }
841
+ function unlistedWorktreeDetail(dir) {
842
+ return `git worktree list in ${dir.repository} does not show it, so gc cannot tell whether it holds work; look through it, then remove it with \`${unlistedWorktreeRemedy(dir)}\``;
843
+ }
844
+ function unlistedWorktreeRemedy(dir) {
845
+ return `rm -rf '${dir.path.replaceAll("'", "'\\''")}'`;
846
+ }
1096
847
  function worktreeSweepLines(sweep) {
1097
848
  if (!sweep || !sweep.idle && !sweep.worktrees.length) return [];
1098
849
  const removable = sweep.worktrees.filter((w) => !w.skipped);
@@ -1103,8 +854,10 @@ function worktreeSweepLines(sweep) {
1103
854
  for (const w of sweep.worktrees) {
1104
855
  const age = w.idleDays === null ? "" : ` (idle ${w.idleDays}d)`;
1105
856
  lines.push(` ${w.path}${age}`);
857
+ const buildState = buildStateLine(w.buildState);
858
+ if (buildState) lines.push(buildState);
1106
859
  const note = worktreePullRequestNote(w);
1107
- lines.push(w.skipped ? ` kept: ${w.skipped}${note && !w.skipped.includes(note) ? ` (${note})` : ""}` : ` would be REMOVED by \`stim worktree remove\`: ${worktreeRemovalReason(w)}`);
860
+ lines.push(w.skipped ? ` kept: ${w.skipped}${note && !w.skipped.includes(note) ? ` (${note})` : ""}` : ` would be REMOVED by \`stim worktree remove\`: ${worktreeRemovalReason(w, sweep)}`);
1108
861
  }
1109
862
  if (removable.length) lines.push(" --delete runs it without --force; use, idleness and HEAD are re-checked under its removal locks.");
1110
863
  return lines;
@@ -1127,7 +880,22 @@ function jsonPullRequest(lookup) {
1127
880
  containsHead: pr.containsHead
1128
881
  } : null;
1129
882
  }
1130
- function gcReportSections({ archives, skipped = [], deadProjects = [], orphanedPorts = [], invalidProjects = [], orphanedWorkspaces = [], worktreeSweep = null, parkedSims = [], parkedAvds = [], parkedHostedDevices = [], orphanedDevices = [], unverifiedDevices = [], staleDevices = [], staleDeviceRecords = [], staleLedgerEntries = [], staleEasLedgerEntries = [], staleStatusCaches = [], buildLocks = {
883
+ function linkedWorktreesJson(sweep) {
884
+ if (!sweep) return [];
885
+ return sweep.worktrees.map((w) => ({
886
+ path: w.path,
887
+ idleDays: w.idleDays,
888
+ mergedInto: w.merge?.merged ? w.merge.into : null,
889
+ pullRequest: jsonPullRequest(w.pullRequest),
890
+ pullRequestUnknown: w.pullRequest && "unavailable" in w.pullRequest ? w.pullRequest.unavailable : null,
891
+ willRemove: w.skipCode === null,
892
+ reason: w.skipCode,
893
+ detail: w.skipped ?? worktreeRemovalReason(w, sweep),
894
+ eligibleAt: w.eligibleAt === null ? null : new Date(w.eligibleAt).toISOString(),
895
+ buildState: w.buildState ?? null
896
+ }));
897
+ }
898
+ function gcReportSections({ archives, skipped = [], deadProjects = [], orphanedPorts = [], invalidProjects = [], orphanedWorkspaces = [], worktreeSweep = null, parkedSims = [], parkedAvds = [], parkedHostedDevices = [], orphanedDevices = [], unverifiedDevices = [], staleDevices = [], finishedDevices = [], ownedDeviceDisk = [], staleDeviceRecords = [], staleLedgerEntries = [], staleEasLedgerEntries = [], staleStatusCaches = [], buildLocks = {
1131
899
  stale: [],
1132
900
  live: [],
1133
901
  unresolved: []
@@ -1143,7 +911,7 @@ function gcReportSections({ archives, skipped = [], deadProjects = [], orphanedP
1143
911
  orphaned: [],
1144
912
  notices: [],
1145
913
  deletionSafe: true
1146
- }, workspaceOutputs = null, workspaceLogs = [], recordings = [], agentDeviceUsage = null, swiftpmCacheUsage = null, caches = [], memory = null }) {
914
+ }, workspaceOutputs = null, workspaceLogs = [], recordings = [], agentDeviceUsage = null, swiftpmCacheUsage = null, machineCaches = null, tempLeftovers = null, gradleVersions = null, caches = [], memory = null }) {
1147
915
  return {
1148
916
  ...archives ? {
1149
917
  archived: archives.records,
@@ -1161,16 +929,12 @@ function gcReportSections({ archives, skipped = [], deadProjects = [], orphanedP
1161
929
  projectRoot,
1162
930
  bytes: bytes ?? null
1163
931
  })),
1164
- linkedWorktrees: (worktreeSweep?.worktrees ?? []).map((w) => ({
1165
- path: w.path,
1166
- idleDays: w.idleDays,
1167
- mergedInto: w.merge?.merged ? w.merge.into : null,
1168
- pullRequest: jsonPullRequest(w.pullRequest),
1169
- pullRequestUnknown: w.pullRequest && "unavailable" in w.pullRequest ? w.pullRequest.unavailable : null,
1170
- willRemove: w.skipCode === null,
1171
- reason: w.skipCode,
1172
- detail: w.skipped ?? worktreeRemovalReason(w),
1173
- eligibleAt: w.eligibleAt === null ? null : new Date(w.eligibleAt).toISOString()
932
+ linkedWorktrees: linkedWorktreesJson(worktreeSweep),
933
+ unlistedWorktreeDirectories: (worktreeSweep?.unlisted ?? []).map((dir) => ({
934
+ path: dir.path,
935
+ repository: dir.repository,
936
+ bytes: dir.bytes,
937
+ remedy: unlistedWorktreeRemedy(dir)
1174
938
  })),
1175
939
  parkedSimulators: parkedSims.map(({ udid, name, model, runtime, parkedAt, app, bytes, listed }) => ({
1176
940
  udid,
@@ -1214,6 +978,16 @@ function gcReportSections({ archives, skipped = [], deadProjects = [], orphanedP
1214
978
  idleDays: d.idleDays,
1215
979
  bytes: d.bytes ?? null
1216
980
  })),
981
+ finishedDevices: finishedDevices.map((d) => ({
982
+ kind: d.kind,
983
+ id: d.id,
984
+ name: d.name,
985
+ project: d.project,
986
+ slot: d.slot ?? null,
987
+ bytes: d.bytes ?? null,
988
+ reason: d.reason
989
+ })),
990
+ ownedDeviceDisk,
1217
991
  staleDeviceRecords: staleDeviceRecords.map((r) => ({
1218
992
  kind: r.kind,
1219
993
  id: r.id,
@@ -1325,12 +1099,179 @@ function gcReportSections({ archives, skipped = [], deadProjects = [], orphanedP
1325
1099
  })),
1326
1100
  agentDevice: agentDeviceUsage,
1327
1101
  swiftpmCache: swiftpmCacheUsage,
1102
+ machineCaches,
1103
+ tempLeftovers: tempLeftoverSection(tempLeftovers),
1104
+ gradleVersions,
1328
1105
  memory: memory?.processes ?? [],
1329
1106
  watchmanRoots: memory?.watchmanRoots ?? [],
1330
1107
  memoryNotices: (memory?.notices ?? []).map((message) => ({ message }))
1331
1108
  };
1332
1109
  }
1333
1110
  //#endregion
1111
+ //#region src/commands/gc/worktree-sizes.ts
1112
+ const SIZE_TIMEOUT_MS = 3e4;
1113
+ const BUILD_STATE_MAX_AGE_MS = 36e5;
1114
+ const BUILD_STATE_TIMEOUT_MS = 1e4;
1115
+ const BUILD_STATE_CONCURRENCY = 4;
1116
+ const PNPM_STORE = ".pnpm-store";
1117
+ const WORKTREES_DIR = ".worktrees";
1118
+ function isDirectory(path) {
1119
+ try {
1120
+ return lstatSync(path).isDirectory();
1121
+ } catch {
1122
+ return false;
1123
+ }
1124
+ }
1125
+ function inside(root, path) {
1126
+ const rel = relative(root, path);
1127
+ return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
1128
+ }
1129
+ async function buildStateDirs(path, keys) {
1130
+ const entries = await listWorktreesAsync(path);
1131
+ const own = keys.filter((key) => matchWorktreeEntry(entries, key)?.path === path);
1132
+ const root = canonicalPath(path);
1133
+ const worktree = {
1134
+ path,
1135
+ linked: true,
1136
+ keys: own
1137
+ };
1138
+ const dirs = /* @__PURE__ */ new Map();
1139
+ const add = (dir) => {
1140
+ if (!isDirectory(dir)) return;
1141
+ const real = canonicalPath(dir);
1142
+ if (inside(root, real) && !dirs.has(real)) dirs.set(real, dir);
1143
+ };
1144
+ for (const kind of ["build", "dependencies"]) for (const base of appRoots(worktree, kind)) for (const dir of ARTIFACT_DIRS[kind]) add(join(base, dir));
1145
+ add(join(path, PNPM_STORE));
1146
+ return [...dirs.keys()];
1147
+ }
1148
+ function cachedSize(dir, now) {
1149
+ const cached = readDiskUsage(dir);
1150
+ if (!cached) return null;
1151
+ const at = Date.parse(cached.measuredAt);
1152
+ if (now - at >= BUILD_STATE_MAX_AGE_MS) return null;
1153
+ try {
1154
+ return statSync(dir).mtimeMs > at ? null : cached.bytes;
1155
+ } catch {
1156
+ return null;
1157
+ }
1158
+ }
1159
+ async function duSize(dir) {
1160
+ let out;
1161
+ try {
1162
+ out = await getExecutor().runFileAsync("du", ["-sk", dir], {
1163
+ timeoutMs: BUILD_STATE_TIMEOUT_MS,
1164
+ killSignal: "SIGKILL"
1165
+ });
1166
+ } catch (error) {
1167
+ const stdout = error instanceof Error && "stdout" in error ? error.stdout : null;
1168
+ out = typeof stdout === "string" ? stdout : "";
1169
+ }
1170
+ const kb = /^(\d+)\t/.exec(out)?.[1];
1171
+ return kb ? Number(kb) * 1024 : null;
1172
+ }
1173
+ async function eachLimited(items, limit, run) {
1174
+ const queue = items[Symbol.iterator]();
1175
+ const worker = async () => {
1176
+ for (const item of queue) await run(item);
1177
+ };
1178
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
1179
+ }
1180
+ /**
1181
+ * Sizes the regenerable directories maintenance clears and a project-local `.pnpm-store` in each worktree of the
1182
+ * sweep, from the disk-usage cache while an entry is under an hour old and no entry was added to or removed from the
1183
+ * directory since. Stale directories are measured with one `du` each, at most 4 at once and 10 seconds each, so a
1184
+ * slow tree delays only itself; a directory du could not measure in time is reported unmeasured (null). Also lists
1185
+ * the directories directly under a repository's `.worktrees/` that are no git worktree at all.
1186
+ */
1187
+ async function measureWorktreeSweep(sweep, operation) {
1188
+ const now = Date.now();
1189
+ const pending = [];
1190
+ for (const candidate of sweep.worktrees) {
1191
+ if (candidate.skipCode === "not-a-worktree") continue;
1192
+ const dirs = await buildStateDirs(candidate.path, candidate.keys);
1193
+ pending.push({
1194
+ candidate,
1195
+ root: canonicalPath(candidate.path),
1196
+ dirs,
1197
+ sizes: new Map(dirs.map((dir) => [dir, cachedSize(dir, now)]))
1198
+ });
1199
+ }
1200
+ const due = pending.flatMap(({ sizes }) => [...sizes].flatMap(([dir, bytes]) => bytes === null ? [{
1201
+ dir,
1202
+ sizes
1203
+ }] : []));
1204
+ const worktrees = new Set(due.map(({ sizes }) => sizes)).size;
1205
+ if (due.length) phase("worktrees", `measuring the build state of ${plural(worktrees, "worktree")}`, operation);
1206
+ await eachLimited(due, BUILD_STATE_CONCURRENCY, async ({ dir, sizes }) => {
1207
+ const measuredAt = (/* @__PURE__ */ new Date()).toISOString();
1208
+ const bytes = await duSize(dir);
1209
+ if (bytes === null) return;
1210
+ sizes.set(dir, bytes);
1211
+ operation?.signal.throwIfAborted();
1212
+ writeCacheFile(diskUsageCacheFile(dir), {
1213
+ path: dir,
1214
+ measuredAt,
1215
+ bytes
1216
+ });
1217
+ });
1218
+ for (const { candidate, root, dirs, sizes } of pending) {
1219
+ const entries = dirs.map((dir) => ({
1220
+ path: relative(root, dir).split(sep).join("/"),
1221
+ bytes: sizes.get(dir) ?? null
1222
+ }));
1223
+ candidate.buildState = {
1224
+ bytes: entries.every((dir) => dir.bytes !== null) ? entries.reduce((sum, dir) => sum + (dir.bytes ?? 0), 0) : null,
1225
+ dirs: entries.toSorted((a, b) => (b.bytes ?? 0) - (a.bytes ?? 0))
1226
+ };
1227
+ }
1228
+ sweep.unlisted = await unlistedWorktreeDirs();
1229
+ return sweep;
1230
+ }
1231
+ /** Whether `path` is a checkout git still knows: its own `.git` directory, or a `.git` file whose gitdir exists. */
1232
+ function hasLiveGitDir(path) {
1233
+ const dotGit = join(path, ".git");
1234
+ let text;
1235
+ try {
1236
+ if (lstatSync(dotGit).isDirectory()) return true;
1237
+ text = readFileSync(dotGit, "utf-8");
1238
+ } catch {
1239
+ return false;
1240
+ }
1241
+ const gitdir = /^gitdir:\s*(.+)$/m.exec(text)?.[1]?.trim();
1242
+ return Boolean(gitdir) && existsSync(resolve(dirname(dotGit), gitdir ?? ""));
1243
+ }
1244
+ async function unlistedWorktreeDirs() {
1245
+ const repositories = /* @__PURE__ */ new Map();
1246
+ for (const root of await candidateRootsAsync()) {
1247
+ const { entries, ...source } = await resolveSourceCheckoutAsync(root);
1248
+ if ("refusal" in source || repositories.has(source.path)) continue;
1249
+ repositories.set(source.path, entries.map((entry) => canonicalPath(entry.path)));
1250
+ }
1251
+ const found = [];
1252
+ for (const [repository, listed] of repositories) {
1253
+ const parent = join(repository, WORKTREES_DIR);
1254
+ let names;
1255
+ try {
1256
+ names = readdirSync(parent);
1257
+ } catch {
1258
+ continue;
1259
+ }
1260
+ for (const name of names.toSorted()) {
1261
+ const path = join(parent, name);
1262
+ if (!isDirectory(path) || hasLiveGitDir(path)) continue;
1263
+ const real = canonicalPath(path);
1264
+ if (listed.some((entry) => entry === real || entry.startsWith(`${real}${sep}`))) continue;
1265
+ found.push({
1266
+ path,
1267
+ repository,
1268
+ bytes: await measuredDirectorySizeAsync(path, { timeoutMs: SIZE_TIMEOUT_MS })
1269
+ });
1270
+ }
1271
+ }
1272
+ return found;
1273
+ }
1274
+ //#endregion
1334
1275
  //#region src/commands/gc/ledger.ts
1335
1276
  /** `sims` must be a complete listing, unavailable simulators included, taken after `ledger` was read. */
1336
1277
  function findStaleLedgerEntries(ledger, sims) {
@@ -1564,266 +1505,6 @@ async function trimWorkspaceLogs(entries) {
1564
1505
  return failures;
1565
1506
  }
1566
1507
  //#endregion
1567
- //#region src/commands/gc/inventory.ts
1568
- const IOS_SIMULATOR_PLATFORM = "com.apple.platform.iphonesimulator";
1569
- function text(value) {
1570
- return typeof value === "string" && value.length > 0 ? value : null;
1571
- }
1572
- function parseSimRuntimeList(jsonOutput) {
1573
- const parsed = JSON.parse(jsonOutput);
1574
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Expected simctl runtime list to return a JSON object.");
1575
- const runtimes = [];
1576
- for (const [key, value] of Object.entries(parsed)) {
1577
- if (!isJsonObject(value)) continue;
1578
- const entry = value;
1579
- if (entry.platformIdentifier !== IOS_SIMULATOR_PLATFORM) continue;
1580
- runtimes.push({
1581
- identifier: text(entry.identifier) ?? key,
1582
- runtimeIdentifier: text(entry.runtimeIdentifier),
1583
- version: text(entry.version),
1584
- build: text(entry.build),
1585
- sizeBytes: typeof entry.sizeBytes === "number" && Number.isFinite(entry.sizeBytes) ? entry.sizeBytes : null,
1586
- deletable: entry.deletable === true,
1587
- lastUsedAt: text(entry.lastUsedAt)
1588
- });
1589
- }
1590
- return runtimes;
1591
- }
1592
- function unlistedRuntimes(jsonOutput, images) {
1593
- const known = new Set(images.map((image) => image.runtimeIdentifier));
1594
- const parsed = JSON.parse(jsonOutput);
1595
- if (!Array.isArray(parsed.runtimes)) throw new Error("Expected simctl list runtimes to return a runtimes array.");
1596
- return parsed.runtimes.flatMap((value) => {
1597
- if (value === null || typeof value !== "object") return [];
1598
- const entry = value;
1599
- const identifier = text(entry.identifier);
1600
- if (!identifier || !/\.iOS-/.test(identifier) || known.has(identifier)) return [];
1601
- return [{
1602
- identifier,
1603
- runtimeIdentifier: identifier,
1604
- version: text(entry.version),
1605
- build: text(entry.buildversion),
1606
- sizeBytes: null,
1607
- deletable: false,
1608
- lastUsedAt: null
1609
- }];
1610
- });
1611
- }
1612
- function modelName(deviceTypeIdentifier) {
1613
- return deviceTypeIdentifier.replace(/^com\.apple\.CoreSimulator\.SimDeviceType\./, "").replaceAll("-", " ");
1614
- }
1615
- function sdkmanagerCommand(pkg) {
1616
- const tool = androidToolPath("sdkmanager");
1617
- return `${tool === "sdkmanager" ? tool : JSON.stringify(tool)} --uninstall ${JSON.stringify(pkg)}`;
1618
- }
1619
- function buildInventory({ sims, avds, runtimes, systemImages, config, deadProjects = [] }) {
1620
- const dead = new Set(deadProjects);
1621
- const assigned = /* @__PURE__ */ new Map();
1622
- for (const [project, record] of Object.entries(config?.projects ?? {})) {
1623
- if (dead.has(project)) continue;
1624
- let slots;
1625
- try {
1626
- slots = projectDeviceSlots(record);
1627
- } catch {
1628
- continue;
1629
- }
1630
- for (const { slot, platforms } of slots) {
1631
- if (platforms.ios?.deviceUdid) assigned.set(`ios:${platforms.ios.deviceUdid}`, {
1632
- project,
1633
- slot
1634
- });
1635
- if (platforms.android?.avdName) assigned.set(`android:${platforms.android.avdName}`, {
1636
- project,
1637
- slot
1638
- });
1639
- }
1640
- }
1641
- const parked = /* @__PURE__ */ new Set([...readParked("ios", { config }).map((sim) => `ios:${sim.udid}`), ...readParked("android", { config }).map((avd) => `android:${avd.name}`)]);
1642
- const owner = (key, name, created) => {
1643
- const workspace = assigned.get(key);
1644
- if (workspace && created()) return {
1645
- owner: "workspace",
1646
- project: workspace.project,
1647
- slot: workspace.slot
1648
- };
1649
- if (parked.has(key)) return {
1650
- owner: "parked",
1651
- project: null,
1652
- slot: null
1653
- };
1654
- if (!name.startsWith("stim-")) return {
1655
- owner: "user",
1656
- project: null,
1657
- slot: null
1658
- };
1659
- return {
1660
- owner: created() ? "orphaned" : "otherStimHome",
1661
- project: null,
1662
- slot: null
1663
- };
1664
- };
1665
- const devices = [...sims.map((sim) => ({
1666
- kind: "ios",
1667
- id: sim.udid,
1668
- name: sim.name,
1669
- model: modelName(sim.deviceTypeIdentifier),
1670
- runtime: sim.runtime,
1671
- state: sim.state,
1672
- lastUsedAt: sim.lastUsedAt ?? null,
1673
- bytes: sim.dataPathSize ?? null,
1674
- directory: sim.dataPath ? sim.dataPath.replace(/\/data\/?$/, "") : null,
1675
- ...owner(`ios:${sim.udid}`, sim.name, () => isStimOwnedSim({
1676
- udid: sim.udid,
1677
- name: sim.name
1678
- }))
1679
- })), ...avds.map((avd) => ({
1680
- kind: "android",
1681
- id: avd.name,
1682
- name: avd.name,
1683
- model: avd.deviceProfile,
1684
- runtime: avd.systemImage,
1685
- state: null,
1686
- lastUsedAt: avd.lastUsedAt,
1687
- bytes: null,
1688
- directory: avd.directory,
1689
- ...owner(`android:${avd.name}`, avd.name, () => isStimOwnedAvd(avd.name))
1690
- }))];
1691
- const simsByRuntime = /* @__PURE__ */ new Map();
1692
- for (const sim of sims) simsByRuntime.set(sim.runtime, (simsByRuntime.get(sim.runtime) ?? 0) + 1);
1693
- const avdsByImage = /* @__PURE__ */ new Map();
1694
- for (const avd of avds) if (avd.systemImage) avdsByImage.set(avd.systemImage, (avdsByImage.get(avd.systemImage) ?? 0) + 1);
1695
- return {
1696
- devices,
1697
- runtimes: runtimes.map((runtime) => ({
1698
- identifier: runtime.identifier,
1699
- runtimeIdentifier: runtime.runtimeIdentifier,
1700
- version: runtime.version,
1701
- build: runtime.build,
1702
- bytes: runtime.sizeBytes,
1703
- lastUsedAt: runtime.lastUsedAt,
1704
- deviceCount: runtime.runtimeIdentifier ? simsByRuntime.get(runtime.runtimeIdentifier) ?? 0 : 0,
1705
- command: runtime.deletable ? `xcrun simctl runtime delete ${runtime.identifier}` : null
1706
- })),
1707
- systemImages: systemImages.map((image) => ({
1708
- package: image.pkg,
1709
- directory: image.directory,
1710
- avdCount: avdsByImage.get(image.pkg) ?? 0,
1711
- command: sdkmanagerCommand(image.pkg)
1712
- }))
1713
- };
1714
- }
1715
- function modifiedAt(path) {
1716
- try {
1717
- return statSync(path).mtime.toISOString();
1718
- } catch {
1719
- return null;
1720
- }
1721
- }
1722
- function isMissing(error) {
1723
- const code = error.code;
1724
- return code === "ENOENT" || code === "ENOTDIR";
1725
- }
1726
- function unreadableNotice(what, error) {
1727
- const notice = `${what} not listed: ${describeError(error)}`;
1728
- if (process.platform !== "darwin" || error.code !== "EPERM") return notice;
1729
- return `${notice}. macOS privacy protection blocked the read; allow ${process.env.STIM_DESKTOP_APP ? "Stim Desktop" : "the app that runs stim"} under System Settings > Privacy & Security > Files and Folders (Removable Volumes for an external disk) or Full Disk Access`;
1730
- }
1731
- function listAvdRecords(notices, roots = avdStorageRoots()) {
1732
- const records = /* @__PURE__ */ new Map();
1733
- for (const root of roots) {
1734
- let names;
1735
- try {
1736
- names = readdirSync(root);
1737
- } catch (error) {
1738
- if (!isMissing(error)) notices.push(unreadableNotice(`AVDs in ${root}`, error));
1739
- continue;
1740
- }
1741
- for (const entry of names) {
1742
- if (!entry.endsWith(".ini")) continue;
1743
- const name = entry.slice(0, -4);
1744
- if (records.has(name)) continue;
1745
- let ini;
1746
- try {
1747
- ini = readFileSync(join(root, entry), "utf8");
1748
- } catch (error) {
1749
- if (!isMissing(error)) notices.push(unreadableNotice(`AVD ${name}`, error));
1750
- continue;
1751
- }
1752
- let directory = null;
1753
- let config = "";
1754
- let unreadable = null;
1755
- for (const candidate of avdIniPaths(root, ini)) try {
1756
- config = readFileSync(join(candidate, "config.ini"), "utf8");
1757
- directory = candidate;
1758
- break;
1759
- } catch (error) {
1760
- if (!isMissing(error)) unreadable ??= error;
1761
- }
1762
- if (!directory && unreadable) notices.push(unreadableNotice(`details of AVD ${name}`, unreadable));
1763
- records.set(name, {
1764
- name,
1765
- directory,
1766
- systemImage: parseAvdSystemImage(config),
1767
- deviceProfile: parseAvdDeviceProfile(config),
1768
- lastUsedAt: directory ? modifiedAt(join(directory, "hardware-qemu.ini")) : null
1769
- });
1770
- }
1771
- }
1772
- return [...records.values()];
1773
- }
1774
- function simctl(args) {
1775
- return getExecutor().runFile("xcrun", ["simctl", ...args], {
1776
- timeoutMs: DEVICE_LIST_TIMEOUT_MS,
1777
- killSignal: "SIGKILL"
1778
- });
1779
- }
1780
- function collectInventory(config, deadProjects) {
1781
- const notices = [];
1782
- let sims = [];
1783
- let runtimes = [];
1784
- if (process.platform === "darwin") {
1785
- try {
1786
- sims = listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS });
1787
- } catch (error) {
1788
- notices.push(`simulators not listed: ${describeError(error)}`);
1789
- }
1790
- try {
1791
- runtimes = parseSimRuntimeList(simctl([
1792
- "runtime",
1793
- "list",
1794
- "-j"
1795
- ]));
1796
- } catch (error) {
1797
- notices.push(`simulator runtime images not listed: ${describeError(error)}`);
1798
- }
1799
- try {
1800
- runtimes.push(...unlistedRuntimes(simctl([
1801
- "list",
1802
- "runtimes",
1803
- "-j"
1804
- ]), runtimes));
1805
- } catch (error) {
1806
- notices.push(`simulator runtimes not listed: ${describeError(error)}`);
1807
- }
1808
- }
1809
- const imagesRoot = join(androidHome(), "system-images");
1810
- const images = listInstalledSystemImages((error) => notices.push(unreadableNotice("Android system images", error))).map((image) => ({
1811
- pkg: image.pkg,
1812
- directory: join(imagesRoot, `android-${image.api}`, image.tag, image.arch)
1813
- }));
1814
- return {
1815
- ...buildInventory({
1816
- sims,
1817
- avds: listAvdRecords(notices),
1818
- runtimes,
1819
- systemImages: images,
1820
- config,
1821
- deadProjects
1822
- }),
1823
- notices
1824
- };
1825
- }
1826
- //#endregion
1827
1508
  //#region src/commands/gc/hosted-devices.ts
1828
1509
  function collectParkedHostedDevices({ olderThanDays = null, now = Date.now() } = {}) {
1829
1510
  const notices = [];
@@ -2085,6 +1766,14 @@ async function deleteParkedHostedDevices(devices, { listAvds: list = listAvds }
2085
1766
  }
2086
1767
  //#endregion
2087
1768
  //#region src/commands/gc.ts
1769
+ function readGcReport(options = {}, deps = {}) {
1770
+ return sweep({
1771
+ olderThan: options.olderThan,
1772
+ cache: options.cache,
1773
+ worktrees: options.worktrees,
1774
+ json: true
1775
+ }, deps, true);
1776
+ }
2088
1777
  function removeInvalidProjectEntries(invalidProjects) {
2089
1778
  for (const path of invalidProjects) {
2090
1779
  removeProject(path);
@@ -2092,8 +1781,8 @@ function removeInvalidProjectEntries(invalidProjects) {
2092
1781
  recordGcResult("project", "done", path);
2093
1782
  }
2094
1783
  }
2095
- function collectGcMemory(scope) {
2096
- return memorySweepIsScoped() ? Promise.resolve(scopedMemoryReport()) : collectMemoryReport(scope, true);
1784
+ function collectGcMemory(scope, env, operation) {
1785
+ return memorySweepIsScoped(env) ? Promise.resolve(scopedMemoryReport()) : collectMemoryReport(scope, true, operation);
2097
1786
  }
2098
1787
  function classifyProjectEntries(cfg, mountedVolumes) {
2099
1788
  const deadProjects = [];
@@ -2132,14 +1821,16 @@ function removedWholeWorkspaceDirs() {
2132
1821
  const { orphaned } = collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes, { measure: false });
2133
1822
  return [...orphaned.map((entry) => entry.dir), ...deadProjects.map(workspaceDir)];
2134
1823
  }
2135
- async function collectGcReport({ olderThan = null, cache = null, worktrees = false, now = Date.now(), lastTouched = workspaceLastUsed } = {}, deps = {}) {
1824
+ async function collectGcReport({ olderThan = null, cache = null, worktrees = false, refresh = true, now = Date.now(), lastTouched = workspaceLastUsed } = {}, deps = {}) {
1825
+ const operation = deps.operation;
1826
+ const env = operation?.env ?? process.env;
2136
1827
  const scope = typeof cache === "string" && cache.trim() ? cache : null;
2137
1828
  const all = scope !== null && olderThan === null;
2138
1829
  const withWorkspaces = includesWorkspaceOutputs(scope);
2139
- const selected = selectCaches(discoverCaches(), scope).filter((c) => !withWorkspaces || !isInsideWorkspaces(c.dir));
2140
- if (selected.length) phase("caches", `measuring ${plural(selected.length, "shared cache")}`);
2141
- const sized = sizeCaches(selected);
2142
- const caches = scope === null ? previewScopedEmptying(sized) : planCacheEmptying(sized, all);
1830
+ const selected = selectCaches(discoverCaches(env, operation?.cwd), scope).filter((c) => !withWorkspaces || !isInsideWorkspaces(c.dir));
1831
+ if (selected.length) phase("caches", `measuring ${plural(selected.length, "shared cache")}`, operation);
1832
+ const sized = await sizeCaches(selected);
1833
+ const caches = scope === null ? previewScopedEmptying(sized, env) : planCacheEmptying(sized, all, env);
2143
1834
  if (scope) return {
2144
1835
  archives: includesArchives(scope) ? collectArchives(scope, olderThan, now) : void 0,
2145
1836
  skipped: [],
@@ -2149,6 +1840,8 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2149
1840
  orphanedDevices: [],
2150
1841
  unverifiedDevices: [],
2151
1842
  staleDevices: [],
1843
+ finishedDevices: [],
1844
+ ownedDeviceDisk: [],
2152
1845
  staleDeviceRecords: [],
2153
1846
  staleLedgerEntries: [],
2154
1847
  staleEasLedgerEntries: [],
@@ -2175,17 +1868,17 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2175
1868
  notices: [],
2176
1869
  deletionSafe: true
2177
1870
  },
2178
- parkedSims: includesParkedDevices(scope) ? collectParkedSims(deps, {
1871
+ parkedSims: includesParkedDevices(scope) ? await collectParkedSims(deps, {
2179
1872
  olderThanDays: olderThan,
2180
1873
  now
2181
1874
  }) : [],
2182
- parkedAvds: includesParkedDevices(scope) ? collectParkedAvds(deps, {
1875
+ parkedAvds: includesParkedDevices(scope) ? await collectParkedAvds(deps, {
2183
1876
  olderThanDays: olderThan,
2184
1877
  now
2185
1878
  }) : [],
2186
1879
  parkedHostedDevices: [],
2187
1880
  caches,
2188
- workspaceOutputs: withWorkspaces ? collectWorkspaceOutputs({
1881
+ workspaceOutputs: withWorkspaces ? await collectWorkspaceOutputs({
2189
1882
  olderThan,
2190
1883
  now,
2191
1884
  exclude: isWorkspacesScope(scope) ? removedWholeWorkspaceDirs() : []
@@ -2197,22 +1890,37 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2197
1890
  now
2198
1891
  }) : [],
2199
1892
  worktreeSweep: null,
2200
- memory: memoryCacheKind(scope) ? await collectGcMemory(memoryScope(scope)) : null,
1893
+ memory: memoryCacheKind(scope) ? await collectGcMemory(memoryScope(scope), env, operation) : null,
2201
1894
  agentDeviceUsage: null,
2202
1895
  swiftpmCacheUsage: null,
1896
+ machineCaches: null,
1897
+ tempLeftovers: null,
1898
+ gradleVersions: includesGradleVersions(scope) ? await collectGradleVersions({
1899
+ olderThanDays: olderThan,
1900
+ now,
1901
+ operation
1902
+ }) : null,
2203
1903
  cacheScope: scope,
2204
1904
  olderThan,
2205
1905
  all
2206
1906
  };
2207
- phase("agent-device", "measuring report-only state");
2208
- const agentDeviceUsage = await getAgentDeviceUsage();
2209
- phase("SwiftPM cache", "measuring report-only state");
2210
- const swiftpmCacheUsage = await getSwiftpmCacheUsage();
1907
+ phase("agent-device", "measuring report-only state", operation);
1908
+ const agentDeviceUsage = await getAgentDeviceUsage({ operation });
1909
+ phase("SwiftPM cache", "measuring report-only state", operation);
1910
+ const swiftpmCacheUsage = await getSwiftpmCacheUsage({ operation });
1911
+ phase("machine caches", "measuring report-only state", operation);
1912
+ const machineCaches = await getMachineCacheReport(agentDeviceUsage, { operation });
1913
+ if (!deviceSweepIsScoped(env)) phase("TMPDIR", "checking recognized leftovers", operation);
1914
+ const tempLeftovers = await collectTempLeftovers({
1915
+ now,
1916
+ env,
1917
+ signal: operation?.signal
1918
+ });
2211
1919
  const mountedVolumes = listMountedVolumes();
2212
1920
  const cfg = loadConfig$1();
2213
1921
  let easSessionSweep = deps.precollectedEasSessionSweep;
2214
1922
  if (!easSessionSweep) try {
2215
- easSessionSweep = collectEasSessionSweep(cfg, deps);
1923
+ easSessionSweep = await collectEasSessionSweep(cfg, deps);
2216
1924
  } catch (error) {
2217
1925
  easSessionSweep = {
2218
1926
  projectScope: null,
@@ -2221,16 +1929,16 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2221
1929
  deletionSafe: false
2222
1930
  };
2223
1931
  }
2224
- const parkedSims = collectParkedSims(deps, {
1932
+ const parkedSims = await collectParkedSims(deps, {
2225
1933
  olderThanDays: olderThan,
2226
1934
  now
2227
1935
  });
2228
- const parkedAvds = collectParkedAvds(deps, {
1936
+ const parkedAvds = await collectParkedAvds(deps, {
2229
1937
  olderThanDays: olderThan,
2230
1938
  now
2231
1939
  });
2232
1940
  const { deadProjects, invalidProjects, skipped } = classifyProjectEntries(cfg, mountedVolumes);
2233
- const workspaceDirs = collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes);
1941
+ const workspaceDirs = await collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes);
2234
1942
  skipped.push(...workspaceDirs.skipped);
2235
1943
  const statusCaches = collectStaleStatusCaches(mountedVolumes);
2236
1944
  skipped.push(...statusCaches.skipped);
@@ -2245,16 +1953,24 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2245
1953
  let staleDeviceRecords = [];
2246
1954
  let staleLedgerEntries = staleBrowserProfiles(readCreatedDevices$1());
2247
1955
  let idleDevices = [];
2248
- const unsweepableReason = cfg === null ? "no Stim config found" : deviceSweepIsScoped() ? "STIM_HOME scopes this config, but simulators and AVDs are machine-global" : null;
2249
- phase("devices", "listing simulators and emulators");
1956
+ let listed = null;
1957
+ let deviceDisk = [];
1958
+ const avdSizes = /* @__PURE__ */ new Map();
1959
+ const avdSize = (dir, options) => {
1960
+ let bytes = avdSizes.get(dir);
1961
+ if (bytes === void 0) avdSizes.set(dir, bytes = Promise.resolve((deps.directorySize ?? directorySizeAsync)(dir, options)));
1962
+ return bytes;
1963
+ };
1964
+ const unsweepableReason = cfg === null ? "no Stim config found" : deviceSweepIsScoped(env) ? "STIM_HOME scopes this config, but simulators and AVDs are machine-global" : null;
1965
+ phase("devices", "listing simulators and emulators", operation);
2250
1966
  if (unsweepableReason) {
2251
1967
  let simNames = [];
2252
1968
  let avdNames = [];
2253
1969
  try {
2254
- simNames = listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS }).map((s) => s.name);
1970
+ simNames = (await listAllIosSimsAsync({ timeoutMs: DEVICE_LIST_TIMEOUT_MS })).map((s) => s.name);
2255
1971
  } catch {}
2256
1972
  try {
2257
- avdNames = listAvds({ timeoutMs: DEVICE_LIST_TIMEOUT_MS });
1973
+ avdNames = await listAvdsAsync({ timeoutMs: DEVICE_LIST_TIMEOUT_MS });
2258
1974
  } catch {}
2259
1975
  deviceSweepNotices.push(...describeUnverifiableDevices(simNames, avdNames, { reason: unsweepableReason }));
2260
1976
  } else {
@@ -2262,7 +1978,7 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2262
1978
  let simsChecked = true;
2263
1979
  const ledger = readCreatedDevices$1();
2264
1980
  try {
2265
- const allSims = listAllIosSims({
1981
+ const allSims = await listAllIosSimsAsync({
2266
1982
  timeoutMs: DEVICE_LIST_TIMEOUT_MS,
2267
1983
  includeUnavailable: true
2268
1984
  });
@@ -2275,7 +1991,7 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2275
1991
  let avds = [];
2276
1992
  let avdsChecked = true;
2277
1993
  try {
2278
- avds = listAvds({ timeoutMs: DEVICE_LIST_TIMEOUT_MS });
1994
+ avds = await listAvdsAsync({ timeoutMs: DEVICE_LIST_TIMEOUT_MS });
2279
1995
  } catch {
2280
1996
  avdsChecked = false;
2281
1997
  deviceSweepNotices.push(`android device sweep skipped: emulator tooling did not answer within ${DEVICE_LIST_TIMEOUT_MS / 1e3}s`);
@@ -2302,14 +2018,30 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2302
2018
  const directories = device.kind === "android" && !registeredAvds.has(device.id) ? orphanedAvdDirectories.filter((entry) => entry.name === device.id) : [];
2303
2019
  return directories.length ? directories.map(({ directory }) => Object.assign({}, device, { directory })) : [device];
2304
2020
  });
2305
- orphanedDevices = withAndroidAvdSizes(found.orphaned.flatMap((device) => {
2021
+ orphanedDevices = await withAndroidAvdSizes(found.orphaned.flatMap((device) => {
2306
2022
  const directories = device.kind === "android" ? orphanedAvdDirectories.filter((entry) => entry.name === device.name) : [];
2307
2023
  return directories.length ? directories.map((orphanedDirectory) => Object.assign({}, device, { orphanedDirectory })) : [device];
2308
2024
  }), {
2309
2025
  avdDirectory: deps.avdDirectory ?? ownedAvdDirectory,
2310
- size: deps.directorySize ?? directorySize
2026
+ size: avdSize
2311
2027
  });
2312
- idleDevices = collectIdleDevices(cfg, sims, deadProjects, now);
2028
+ idleDevices = operation ? await collectIdleDevicesAsync(cfg, sims, deadProjects, now, env) : collectIdleDevices(cfg, sims, deadProjects, now);
2029
+ if (simsChecked || avdsChecked) {
2030
+ const lists = {
2031
+ sims,
2032
+ avds: [...registeredAvds]
2033
+ };
2034
+ listed = lists;
2035
+ let unmeasured = 0;
2036
+ deviceDisk = ownedDeviceDisk(await ownedDeviceUsage((_target, workspace) => {
2037
+ if (workspace) unmeasured++;
2038
+ }, {
2039
+ sims: () => lists.sims,
2040
+ avds: () => lists.avds,
2041
+ avdDirectory: deps.avdDirectory ?? ownedAvdDirectory,
2042
+ size: async (dir) => await avdSize(dir, { timeoutMs: 5e3 }) || null
2043
+ }), { android: unmeasured });
2044
+ }
2313
2045
  staleDeviceRecords = findStaleDeviceRecords({
2314
2046
  config: cfg,
2315
2047
  sims,
@@ -2318,7 +2050,7 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2318
2050
  simsChecked,
2319
2051
  avdsChecked
2320
2052
  });
2321
- if (olderThan !== null) staleDevices = withAndroidAvdSizes(findStaleProjectDevices({
2053
+ if (olderThan !== null) staleDevices = await withAndroidAvdSizes(findStaleProjectDevices({
2322
2054
  config: cfg,
2323
2055
  sims,
2324
2056
  avds,
@@ -2328,13 +2060,20 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2328
2060
  deadProjects
2329
2061
  }), {
2330
2062
  avdDirectory: deps.avdDirectory ?? ownedAvdDirectory,
2331
- size: deps.directorySize ?? directorySize
2063
+ size: avdSize
2332
2064
  });
2333
2065
  }
2334
2066
  const locks = listBuildLocks();
2335
2067
  const slots = listBuildSlots();
2336
2068
  const goneWorkspaceDirs = [...workspaceDirs.orphaned.map((entry) => entry.dir), ...deadProjects.map(workspaceDir)];
2337
2069
  const deviceLeases = collectDeviceLeases(now);
2070
+ const worktreeSweep = await measureWorktreeSweep(await collectWorktreeSweep({
2071
+ idle: worktrees,
2072
+ olderThan,
2073
+ now,
2074
+ refresh,
2075
+ operation
2076
+ }), operation);
2338
2077
  return {
2339
2078
  skipped,
2340
2079
  deadProjects,
@@ -2370,7 +2109,7 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2370
2109
  deviceSweepNotices,
2371
2110
  easSessionSweep,
2372
2111
  caches,
2373
- workspaceOutputs: collectWorkspaceOutputs({
2112
+ workspaceOutputs: await collectWorkspaceOutputs({
2374
2113
  olderThan,
2375
2114
  now,
2376
2115
  exclude: goneWorkspaceDirs
@@ -2381,17 +2120,22 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2381
2120
  olderThan,
2382
2121
  now
2383
2122
  }, goneWorkspaceDirs),
2384
- worktreeSweep: await collectWorktreeSweep({
2385
- idle: worktrees,
2386
- olderThan,
2123
+ worktreeSweep,
2124
+ finishedDevices: listed ? collectFinishedDevices({
2125
+ config: cfg,
2126
+ ...listed,
2127
+ sweep: worktreeSweep,
2387
2128
  now
2388
- }),
2129
+ }) : [],
2130
+ ownedDeviceDisk: deviceDisk,
2389
2131
  memory: await collectGcMemory({
2390
2132
  watchman: true,
2391
2133
  gradle: true
2392
- }),
2134
+ }, env, operation),
2393
2135
  agentDeviceUsage,
2394
2136
  swiftpmCacheUsage,
2137
+ machineCaches,
2138
+ tempLeftovers,
2395
2139
  cacheScope: null,
2396
2140
  olderThan,
2397
2141
  all
@@ -2446,11 +2190,17 @@ async function maintenanceGuardedSweep(opts, deps) {
2446
2190
  if (claim) (0, ownership_claim_exports.releaseClaim)(claim);
2447
2191
  }
2448
2192
  }
2449
- async function sweep(opts, deps) {
2193
+ async function sweep(opts, deps, reportOnly = false) {
2194
+ const core = (dependencies) => runGcCore(opts, dependencies, reportOnly);
2450
2195
  if (opts.cache && (opts.worktrees || opts.idle !== void 0)) {
2451
2196
  const flag = opts.worktrees ? "--worktrees" : "--idle";
2452
2197
  const message = opts.worktrees ? "--cache acts only on the named caches, and --worktrees sweeps linked worktrees." : "--cache acts only on the named caches, and --idle shuts down idle owned devices.";
2453
2198
  const remedy = `Run \`stim gc ${flag}${opts.worktrees ? "" : " <duration>"}\` and \`stim gc --cache <name>\` separately.`;
2199
+ if (reportOnly) return {
2200
+ code: "STIM_BAD_ARG",
2201
+ message,
2202
+ remedy
2203
+ };
2454
2204
  console.error(chalk.red(message));
2455
2205
  console.error(chalk.dim(remedy));
2456
2206
  console.error(chalk.red("failed: STIM_BAD_ARG"));
@@ -2465,6 +2215,11 @@ async function sweep(opts, deps) {
2465
2215
  if (memoryKind && opts.olderThan !== void 0) {
2466
2216
  const message = `--older-than does not apply to --cache ${memoryKind}, which acts on running processes.`;
2467
2217
  const remedy = `Run \`stim gc --cache ${memoryKind}\` without --older-than.`;
2218
+ if (reportOnly) return {
2219
+ code: "STIM_BAD_ARG",
2220
+ message,
2221
+ remedy
2222
+ };
2468
2223
  console.error(chalk.red(message));
2469
2224
  console.error(chalk.dim(remedy));
2470
2225
  console.error(chalk.red("failed: STIM_BAD_ARG"));
@@ -2476,8 +2231,11 @@ async function sweep(opts, deps) {
2476
2231
  };
2477
2232
  }
2478
2233
  const poolError = parkedMaxSetting("ios").error || parkedMaxSetting("android").error;
2479
- if (poolError) console.error(chalk.yellow(`${poolError} ${POOL_SETTING_REMEDY}`));
2480
- if (opts.cache) return runGcCore(opts, {
2234
+ if (poolError) {
2235
+ if (reportOnly) phase("note", `${poolError} ${POOL_SETTING_REMEDY}`, deps.operation);
2236
+ else console.error(chalk.yellow(`${poolError} ${POOL_SETTING_REMEDY}`));
2237
+ }
2238
+ if (opts.cache) return core({
2481
2239
  ...deps,
2482
2240
  precollectedEasSessionSweep: {
2483
2241
  projectScope: null,
@@ -2488,8 +2246,8 @@ async function sweep(opts, deps) {
2488
2246
  });
2489
2247
  let projectRoot = null;
2490
2248
  try {
2491
- projectRoot = (deps.findProjectRoot ?? findProjectRoot)(process.cwd());
2492
- if (!projectRoot) return runGcCore(opts, {
2249
+ projectRoot = (deps.findProjectRoot ?? findProjectRoot$1)(deps.operation?.cwd ?? process.cwd());
2250
+ if (!projectRoot) return core({
2493
2251
  ...deps,
2494
2252
  precollectedEasSessionSweep: {
2495
2253
  projectScope: null,
@@ -2498,7 +2256,7 @@ async function sweep(opts, deps) {
2498
2256
  deletionSafe: false
2499
2257
  }
2500
2258
  });
2501
- if (!(deps.detectIsExpo ?? detectIsExpo)(projectRoot)) return runGcCore(opts, {
2259
+ if (!(deps.detectIsExpo ?? detectIsExpo)(projectRoot)) return core({
2502
2260
  ...deps,
2503
2261
  precollectedEasSessionSweep: {
2504
2262
  projectScope: projectRoot,
@@ -2508,7 +2266,7 @@ async function sweep(opts, deps) {
2508
2266
  }
2509
2267
  });
2510
2268
  } catch (error) {
2511
- return runGcCore(opts, {
2269
+ return core({
2512
2270
  ...deps,
2513
2271
  precollectedEasSessionSweep: {
2514
2272
  projectScope: projectRoot,
@@ -2524,7 +2282,7 @@ async function sweep(opts, deps) {
2524
2282
  try {
2525
2283
  easSessionSweep = await withProjectLock(projectRoot, () => {
2526
2284
  sweepStarted = true;
2527
- return withRemoteSessionGcLocks(projectRoot, deps, (coordinatedDeps) => Promise.resolve(collectEasSessionSweep(loadConfig$1(), coordinatedDeps)));
2285
+ return withRemoteSessionGcLocks(projectRoot, deps, (coordinatedDeps) => collectEasSessionSweep(loadConfig$1(), coordinatedDeps));
2528
2286
  }, {
2529
2287
  waitMs: 0,
2530
2288
  ownerPurpose: "EAS orphan sweep",
@@ -2538,7 +2296,7 @@ async function sweep(opts, deps) {
2538
2296
  deletionSafe: false
2539
2297
  };
2540
2298
  }
2541
- return runGcCore(opts, {
2299
+ return core({
2542
2300
  ...deps,
2543
2301
  precollectedEasSessionSweep: easSessionSweep
2544
2302
  });
@@ -2589,13 +2347,18 @@ async function pruneDeadProjects(deadProjects) {
2589
2347
  }
2590
2348
  return deleteFailures;
2591
2349
  }
2592
- async function attachMaintenancePreview(report, deleting, cache) {
2593
- if (!deleting) report.maintenance = await previewMaintenance({ devices: !cache });
2350
+ async function attachMaintenancePreview(report, deleting, cache, refresh, operation) {
2351
+ if (!deleting) report.maintenance = await previewMaintenance({
2352
+ devices: !cache,
2353
+ refresh,
2354
+ operation
2355
+ });
2594
2356
  }
2595
2357
  function scopedDryRunAction(cache) {
2596
2358
  const memory = memoryCacheKind(cache);
2597
2359
  if (memory === "watchman") return "remove the stale roots and shut down the unused watchman";
2598
2360
  if (memory === "gradle-daemons") return "stop the idle daemons";
2361
+ if (includesGradleVersions(cache)) return "delete the Gradle versions marked deletable";
2599
2362
  return includesParkedDevices(cache) ? "erase the parked devices" : "empty the caches";
2600
2363
  }
2601
2364
  function dryRunHint(cache, all, actionable, caches) {
@@ -2604,45 +2367,64 @@ function dryRunHint(cache, all, actionable, caches) {
2604
2367
  if (caches) return "Pass --delete --cache all to empty the caches above, or --delete --older-than <days> to trim them.";
2605
2368
  return null;
2606
2369
  }
2370
+ function scopedWorkPending(cache, report) {
2371
+ return memoryWorkPending(cache, report.memory) || gradleVersionsPending(report.gradleVersions);
2372
+ }
2607
2373
  function pruneStatusCaches(report) {
2608
2374
  if (report.cacheScope !== null) return 0;
2609
2375
  return deleteStaleStatusCaches(collectStaleStatusCaches(listMountedVolumes()).stale);
2610
2376
  }
2611
- function reclaimParkedDevices(report, deps) {
2612
- return report.cacheScope ? eraseParkedDevices(report.parkedSims, report.parkedAvds) : deleteParkedSims(report.parkedSims, deps) + deleteParkedAvds(report.parkedAvds);
2377
+ async function reclaimParkedDevices(report, deps) {
2378
+ return report.cacheScope ? await eraseParkedDevices(report.parkedSims, report.parkedAvds) : await deleteParkedSims(report.parkedSims, deps) + await deleteParkedAvds(report.parkedAvds);
2613
2379
  }
2614
- async function runGcCore(opts, deps) {
2615
- takeGcResults();
2380
+ function reportActionable(report) {
2381
+ const { deadProjects, invalidProjects, orphanedDevices, staleDevices, staleDeviceRecords, buildLocks, buildSlots, deviceLeases, easSessionSweep, caches, olderThan, all, cacheScope: cache } = report;
2382
+ return archiveWorkPending(report.archives, deadProjects.length + invalidProjects.length > 0 || report.orphanedWorkspaces.length > 0 || Boolean(report.workspaceOutputs?.workspaces.some((entry) => entry.willClear)) || report.workspaceLogs.some((entry) => entry.willTrim) || report.recordings.some((entry) => entry.willDelete && !entry.withWorkspace) || Boolean(report.tempLeftovers?.entries.some((entry) => entry.willDelete)) || Boolean(report.worktreeSweep?.worktrees.some((entry) => !entry.skipped)) || report.parkedSims.length > 0 || report.parkedAvds.length + report.parkedHostedDevices.filter((device) => device.listed !== false).length > 0 || orphanedDevices.length > 0 || staleDevices.length + report.finishedDevices.length > 0 || staleDeviceRecords.length > 0 || report.staleLedgerEntries.length + report.staleEasLedgerEntries.length > 0 || report.staleStatusCaches.length > 0 || buildLocks.stale.length > 0 || buildSlots.stale.length > 0 || deviceLeases.expired.length > 0 || easSessionSweep.orphaned.length > 0 || (olderThan !== null || all) && caches.length > 0 || scopedWorkPending(cache, report));
2383
+ }
2384
+ async function runGcCore(opts, deps, reportOnly = false) {
2385
+ if (!reportOnly) takeGcResults();
2616
2386
  const olderThan = typeof opts.olderThan === "number" ? opts.olderThan : null;
2617
2387
  const cache = typeof opts.cache === "string" && opts.cache.trim() ? opts.cache : null;
2388
+ const refresh = opts.delete === true;
2618
2389
  const report = await collectGcReport({
2619
2390
  olderThan,
2620
2391
  cache,
2621
- worktrees: Boolean(opts.worktrees)
2392
+ worktrees: Boolean(opts.worktrees),
2393
+ refresh
2622
2394
  }, deps);
2623
2395
  if (cache && report.caches.length === 0 && report.workspaceOutputs === null && !selectsBeyondCaches(cache)) {
2624
- const names = [...new Set(discoverCaches().map((c) => c.name))];
2396
+ const names = [...new Set(discoverCaches(deps.operation?.env ?? process.env, deps.operation?.cwd).map((c) => c.name))];
2625
2397
  const message = `No shared cache carries "${cache}" in its name or directory.`;
2626
- console.log(chalk.yellow(message));
2627
- if (names.length) console.log(chalk.dim(`Caches on this machine: ${names.join(", ")}`));
2628
- if (opts.json) process.exitCode = 1;
2398
+ if (!reportOnly) {
2399
+ console.log(chalk.yellow(message));
2400
+ if (names.length) console.log(chalk.dim(`Caches on this machine: ${names.join(", ")}`));
2401
+ if (opts.json) process.exitCode = 1;
2402
+ }
2629
2403
  return {
2630
2404
  code: "STIM_BAD_ARG",
2631
2405
  message,
2632
2406
  remedy: names.length ? `Pass --cache all or a name from: ${names.join(", ")}.` : "No shared cache was found on this machine."
2633
2407
  };
2634
2408
  }
2635
- await attachMaintenancePreview(report, opts.delete === true, cache);
2409
+ await attachMaintenancePreview(report, opts.delete === true, cache, refresh, deps.operation);
2636
2410
  const refusal = archiveRefusal(report.archives);
2637
- if (refusal !== null) return refusal;
2411
+ if (refusal !== null) {
2412
+ if (!reportOnly) {
2413
+ process.exitCode = 1;
2414
+ console.error(chalk.red(refusal.message));
2415
+ console.error(chalk.dim(refusal.remedy));
2416
+ console.error(chalk.red(`failed: ${refusal.code}`));
2417
+ }
2418
+ return refusal;
2419
+ }
2638
2420
  const all = report.all;
2639
- for (const line of formatGcReport(report)) console.log(line);
2421
+ if (!reportOnly) for (const line of formatGcReport(report)) console.log(line);
2640
2422
  const { deadProjects, invalidProjects, orphanedDevices, staleDevices, staleDeviceRecords, buildLocks, buildSlots, deviceLeases, easSessionSweep, caches } = report;
2641
- const actionable = archiveWorkPending(report.archives, deadProjects.length + invalidProjects.length > 0 || report.orphanedWorkspaces.length > 0 || Boolean(report.workspaceOutputs?.workspaces.some((entry) => entry.willClear)) || report.workspaceLogs.some((entry) => entry.willTrim) || report.recordings.some((entry) => entry.willDelete && !entry.withWorkspace) || Boolean(report.worktreeSweep?.worktrees.some((entry) => !entry.skipped)) || report.parkedSims.length > 0 || report.parkedAvds.length + report.parkedHostedDevices.filter((device) => device.listed !== false).length > 0 || orphanedDevices.length > 0 || staleDevices.length > 0 || staleDeviceRecords.length > 0 || report.staleLedgerEntries.length + report.staleEasLedgerEntries.length > 0 || report.staleStatusCaches.length > 0 || buildLocks.stale.length > 0 || buildSlots.stale.length > 0 || deviceLeases.expired.length > 0 || easSessionSweep.orphaned.length > 0 || (olderThan !== null || all) && caches.length > 0 || memoryWorkPending(cache, report.memory));
2423
+ const actionable = reportActionable(report);
2642
2424
  const idle = opts.idle ?? null;
2643
- const idleFailures = idle === null ? 0 : shutDownIdleDevices(report.idleDevices, idle, () => collectIdleDevices(loadConfig$1(), listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS }), report.deadProjects)).failures;
2425
+ const idleFailures = idle === null ? 0 : (await shutDownIdleDevices(report.idleDevices, idle, () => collectIdleDevices(loadConfig$1(), listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS }), report.deadProjects))).failures;
2644
2426
  if (idleFailures) process.exitCode = 1;
2645
- const inventory = opts.json && !opts.delete && idle === null && report.cacheScope === null ? collectInventory(loadConfig$1(), deadProjects) : null;
2427
+ const inventory = opts.json && !opts.delete && idle === null && report.cacheScope === null ? await collectInventory(loadConfig$1(), deadProjects, deps.operation?.env ?? process.env) : null;
2646
2428
  const payload = (failures) => ({
2647
2429
  mode: opts.delete ? "delete" : "dry-run",
2648
2430
  idle,
@@ -2653,12 +2435,12 @@ async function runGcCore(opts, deps) {
2653
2435
  failures,
2654
2436
  sections: gcReportSections(report),
2655
2437
  ...report.maintenance ? { maintenance: report.maintenance } : {},
2656
- results: takeGcResults(),
2438
+ results: reportOnly ? [] : takeGcResults(),
2657
2439
  inventory
2658
2440
  });
2659
2441
  if (!opts.delete) {
2660
2442
  const hint = dryRunHint(cache, all, actionable, caches.length > 0);
2661
- if (hint) console.log(chalk.dim(`\n${hint}`));
2443
+ if (hint && !reportOnly) console.log(chalk.dim(`\n${hint}`));
2662
2444
  return payload(idle === null ? null : idleFailures);
2663
2445
  }
2664
2446
  let deleteFailures = idleFailures;
@@ -2670,16 +2452,22 @@ async function runGcCore(opts, deps) {
2670
2452
  olderThan,
2671
2453
  now: Date.now()
2672
2454
  });
2673
- deleteFailures += reclaimParkedDevices(report, deps);
2455
+ deleteFailures += await reclaimParkedDevices(report, deps);
2674
2456
  deleteFailures += await deleteParkedHostedDevices(report.parkedHostedDevices);
2675
2457
  deleteFailures += await reclaimMemory(cache, report.memory);
2458
+ deleteFailures += await deleteGradleVersions(report.gradleVersions);
2676
2459
  removeInvalidProjectEntries(invalidProjects);
2677
2460
  deleteFailures += await pruneDeadProjects(deadProjects);
2678
2461
  deleteFailures += await deleteOrphanedWorkspaces(report.orphanedWorkspaces);
2679
- deleteFailures += deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords);
2462
+ deleteFailures += await deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords);
2463
+ deleteFailures += await deleteFinishedDevices(report);
2680
2464
  deleteFailures += forgetStaleLedgerEntries(report.staleLedgerEntries);
2681
2465
  deleteFailures += forgetStaleEasLedgerEntries(report.staleEasLedgerEntries, listMountedVolumes(), deps);
2682
2466
  deleteFailures += pruneStatusCaches(report);
2467
+ if (report.tempLeftovers) deleteFailures += (await deleteTempLeftovers({
2468
+ env: deps.operation?.env ?? process.env,
2469
+ signal: deps.operation?.signal
2470
+ })).failures;
2683
2471
  for (const lock of buildLocks.stale) {
2684
2472
  const cleared = (0, ownership_claim_exports.clearFreeClaimSet)({
2685
2473
  root: lock.path,
@@ -2758,11 +2546,11 @@ async function runGcCore(opts, deps) {
2758
2546
  return payload(deleteFailures);
2759
2547
  }
2760
2548
  function gcCommand(program) {
2761
- program.command("gc").description("Report what Stim has left behind: dead project entries, orphaned workspace directories, clean Stim-managed linked worktrees whose branch is merged or whose pull request was merged or closed, orphaned owned devices and EAS sessions, records of devices that no longer exist, build locks whose builder is gone, expired physical-device leases, the shared build caches, the build outputs and logs of each workspace, and the memory of watchman and of the Gradle and Kotlin daemons. Reports by default; pass --delete to act.").option("--worktrees", "also remove clean, idle Stim-managed linked worktrees, not only merged ones: with --delete run `stim worktree remove` (never --force) on each; idle means unused for --older-than days, 7 without it").option("--delete", "actually prune the reported entries, reap the reported devices, clear the build outputs of every workspace not in use, and trim its Metro, client and device logs to their newest 8 MiB").option("--older-than <days>", "also reap owned devices whose workspace has not been used this long, trim shared cache entries nothing has used in that time and device recordings made before it, and clear workspace build outputs only for workspaces idle this long and parked devices only once parked this long", (v) => {
2549
+ program.command("gc").description("Report what Stim has left behind: dead project entries, orphaned workspace directories, clean Stim-managed linked worktrees whose branch is merged or whose pull request was merged or closed, orphaned owned devices and EAS sessions, records of devices that no longer exist, build locks whose builder is gone, expired physical-device leases, the shared build caches, the build outputs and logs of each workspace, the memory of watchman and of the Gradle and Kotlin daemons, and recognized leftovers in TMPDIR. Reports by default; pass --delete to act.").option("--worktrees", "also remove clean, idle Stim-managed linked worktrees, not only merged ones: with --delete run `stim worktree remove` (never --force) on each; idle means unused for --older-than days, 7 without it").option("--delete", "actually prune the reported entries, reap the reported devices, clear the build outputs of every workspace not in use, and trim its Metro, client and device logs to their newest 8 MiB").option("--older-than <days>", "also reap owned devices whose workspace has not been used this long, trim shared cache entries nothing has used in that time and device recordings made before it, and clear workspace build outputs only for workspaces idle this long and parked devices only once parked this long", (v) => {
2762
2550
  const n = parseInt(v, 10);
2763
2551
  if (!Number.isFinite(n) || String(n) !== String(v).trim()) throw new InvalidArgumentError("must be a whole number of days, e.g. --older-than 30");
2764
2552
  return n;
2765
- }).option("--cache <name>", "act on the shared caches whose name or directory contains <name>, every cache, the workspace build outputs and the device recordings with --cache all, only the workspace build outputs with --cache workspaces, only the device recordings with --cache recordings, only the parked simulators and emulators with --cache parked, the watchman daemon and its stale roots with --cache watchman, or the Gradle and Kotlin compile daemons with --cache gradle-daemons; archived, archived:<id>, archived-logs, archived-recordings and archived-agent select archived history explicitly, and the three kind selectors also take :<id> (archived-logs:<id>) to clear one kind of one archive; --cache all excludes archives. all, workspaces, recordings, parked, archived, archived:<id>, archived-logs, archived-recordings, archived-agent, their :<id> forms, watchman and gradle-daemons are reserved names that never select a single cache, and --cache all never includes watchman or gradle-daemons. With --delete they are emptied whole (a parked device is erased and stays parked, watchman loses its stale roots and shuts down only when no client uses it, and only daemons proven idle stop), which is the only way to clear an index-backed cache; add --older-than <days> to trim them by age instead. Archive ids, including the :<id> of a kind selector, ignore --older-than; archived and per-kind archive selectors without an id filter by removal age. Only those caches are reported; other devices and project entries are not inspected. Caches outside the config dir are refused while STIM_HOME is set.", (v) => {
2553
+ }).option("--cache <name>", "act on the shared caches whose name or directory contains <name>, every cache, the workspace build outputs and the device recordings with --cache all, only the workspace build outputs with --cache workspaces, only the device recordings with --cache recordings, only the parked simulators and emulators with --cache parked, the watchman daemon and its stale roots with --cache watchman, the Gradle and Kotlin compile daemons with --cache gradle-daemons, or the Gradle version caches and wrapper distributions no registered project pins with --cache gradle-versions; archived, archived:<id>, archived-logs, archived-recordings and archived-agent select archived history explicitly, and the three kind selectors also take :<id> (archived-logs:<id>) to clear one kind of one archive; --cache all excludes archives. all, workspaces, recordings, parked, archived, archived:<id>, archived-logs, archived-recordings, archived-agent, their :<id> forms, watchman, gradle-daemons and gradle-versions are reserved names that never select a single cache, and --cache all never includes watchman, gradle-daemons or gradle-versions. With --delete they are emptied whole (a parked device is erased and stays parked, watchman loses its stale roots and shuts down only when no client uses it, and only daemons proven idle stop), which is the only way to clear an index-backed cache; add --older-than <days> to trim them by age instead. Archive ids, including the :<id> of a kind selector, ignore --older-than; archived and per-kind archive selectors without an id filter by removal age. Only those caches are reported; other devices and project entries are not inspected. Caches outside the config dir are refused while STIM_HOME is set.", (v) => {
2766
2554
  if (!v.trim()) throw new InvalidArgumentError("must name a cache, e.g. --cache \"compilation cache\"");
2767
2555
  return v;
2768
2556
  }).option("--idle <duration>", "shut down (never delete) owned simulators and emulators that have had no driver, claim, or activity for at least <duration>, such as 30m, 2h, or 1d; acts without --delete", (v) => {
@@ -2774,4 +2562,4 @@ function gcCommand(program) {
2774
2562
  });
2775
2563
  }
2776
2564
  //#endregion
2777
- export { collectGcReport, gcCommand as default, deleteParkedSims, describeParkedSims, describeUnverifiableDevices, findOrphanedDevices, findStaleDeviceRecords, findStaleProjectDevices, formatGcReport, runGc, selectCaches };
2565
+ export { formatGcReport as a, runGc as i, gcCommand as n, readGcReport as r, collectGcReport as t };