stim 1.20.2 → 1.22.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 (231) hide show
  1. package/README.md +141 -8
  2. package/dist/acquire-artifact-Co2k4f7O.mjs +171 -0
  3. package/dist/act-DsuqEkiM.mjs +2801 -0
  4. package/dist/activity-CMLmzp7o.mjs +1397 -0
  5. package/dist/{agent-device-usage-output-Dzgv6lgR.mjs → agent-device-usage-output-DUFaTMdd.mjs} +23 -67
  6. package/dist/{android-DCKZrNvV.mjs → android-ByRMUoiJ.mjs} +157 -93
  7. package/dist/{android-Mz_Inn3w.mjs → android-C7sVmyI9.mjs} +299 -116
  8. package/dist/{android-Dlzxshgp.mjs → android-DGkNqa_c.mjs} +5 -5
  9. package/dist/{android-HqP1_vB_.mjs → android-DR5lZ2Az.mjs} +2 -2
  10. package/dist/android-cas-compiler.mjs +2 -2
  11. package/dist/{android-cas-Bl-cLJBk.mjs → android-cas-ngferVq2.mjs} +7 -6
  12. package/dist/android-project-BCAAmMwz.mjs +12 -0
  13. package/dist/android-project-DVWyCbWU.d.mts +391 -0
  14. package/dist/api.d.mts +810 -64
  15. package/dist/api.mjs +782 -1
  16. package/dist/{artifact-HSwkIWEQ.mjs → artifact-8EY8SAPw.mjs} +240 -213
  17. package/dist/{artifact-D3czlUvT.mjs → artifact-DJPy518Y.mjs} +203 -164
  18. package/dist/artifact-platform-CXcmX_j3.mjs +27 -0
  19. package/dist/{browser-web-CA-F1GQo.mjs → browser-web-Bcv99PBM.mjs} +194 -64
  20. package/dist/budget-Cvwn54qV.mjs +9285 -0
  21. package/dist/{build-DYDbfkk5.mjs → build-CabC3G23.mjs} +153 -98
  22. package/dist/build-cache-Dv7RN200.mjs +602 -0
  23. package/dist/build-plan-DUC8Ka9y.mjs +248 -0
  24. package/dist/{cache-manifest-XH-fvDUj.mjs → cache-manifest-DSeQjt-Q.mjs} +2 -2
  25. package/dist/cache-manifest.mjs +1 -1
  26. package/dist/{ccache-zdKiYQ3E.mjs → ccache-Btwc36-I.mjs} +10 -9
  27. package/dist/{cdp-Dq_ypAlJ.mjs → cdp-3fwICjzi.mjs} +18 -5
  28. package/dist/{chrome-BnzZMqsR.mjs → chrome-Bbw689-6.mjs} +7 -4
  29. package/dist/{cli-CLP3diBm.mjs → cli-CHZa9Lvk.mjs} +20 -20
  30. package/dist/cli.mjs +1 -1
  31. package/dist/client-OyRoEuSr.mjs +1577 -0
  32. package/dist/collector-run.d.mts +4 -40
  33. package/dist/collector-run.mjs +9 -8
  34. package/dist/{command-output-BTy8hZ0F.mjs → command-output-gwpm28fu.mjs} +1 -1
  35. package/dist/config-DlrsduwG.mjs +955 -0
  36. package/dist/{created-devices-Dzd5C2LF.mjs → created-devices-x9rN6bhs.mjs} +14 -4
  37. package/dist/{dependency-state-DG0p31-y.mjs → dependency-state-1I9KZT1Y.mjs} +20 -5
  38. package/dist/{detached-entry-B9C_zeQC.mjs → detached-entry-BwNEtIrP.mjs} +1 -1
  39. package/dist/{dev-client-DAWVIGBu.mjs → dev-client-BSDR5h3F.mjs} +2 -2
  40. package/dist/{device-BECOiufl.mjs → device-B-L3Fbxy.mjs} +91 -34
  41. package/dist/device-DklWZLqb.mjs +1068 -0
  42. package/dist/{device-capacity-CBUAcRyQ.mjs → device-capacity-DK5_EwRS.mjs} +42 -13
  43. package/dist/device-host-worker.mjs +76 -79
  44. package/dist/{device-ios-COSi6oS1.mjs → device-ios-DoB6KFCO.mjs} +77 -51
  45. package/dist/{device-lease-run-Cw2M4z51.mjs → device-lease-run-xJPQ_QQG.mjs} +14 -26
  46. package/dist/{device-pool-Bf-pOhSw.mjs → device-pool-BMkdsefq.mjs} +5 -5
  47. package/dist/{doctor-kfMBnUw6.mjs → doctor-Bo7BXLux.mjs} +271 -320
  48. package/dist/{doctor-CB8oZKCk.mjs → doctor-D5_EzwfA.mjs} +186 -91
  49. package/dist/doctor-fingerprint-DZLoYFVb.mjs +16 -0
  50. package/dist/doctor-fingerprint.mjs +19 -0
  51. package/dist/doctor-thjWAV0z.mjs +2 -0
  52. package/dist/{doctor-watchman-BRyV2adA.mjs → doctor-watchman-N2wDotwQ.mjs} +40 -21
  53. package/dist/{eas-build-JyfqyHfC.mjs → eas-build-CwknaQYj.mjs} +53 -20
  54. package/dist/{build-plan-B7SZsc2N.mjs → eas-fallback-CSgAulr5.mjs} +41 -214
  55. package/dist/{watchman-CPQFZLXY.mjs → eas-session-ledger-DlY_serQ.mjs} +230 -11
  56. package/dist/{error-diagnostics-BR677IDM.mjs → error-diagnostics-DBWO2rxE.mjs} +172 -82
  57. package/dist/exec-DEoAhcze.mjs +858 -0
  58. package/dist/exec-DnYc4HGH.d.mts +78 -0
  59. package/dist/{gc-qWOMdnER.mjs → gc-CXxN-4SE.mjs} +1091 -1029
  60. package/dist/gc-DnNDQyaP.mjs +3 -0
  61. package/dist/{guide-G9cAbVAc.mjs → guide-Br0LEfKy.mjs} +1165 -297
  62. package/dist/{hosted-android-C3MHGld1.mjs → hosted-android-B-9f-jkd.mjs} +10 -8
  63. package/dist/{hosted-client-ZEKpS9vw.mjs → hosted-client-DZAPrcro.mjs} +34 -19
  64. package/dist/{hosted-ios-l-o3iM9J.mjs → hosted-ios-CsCS21hn.mjs} +1 -1
  65. package/dist/{hosted-logs-EzQM7-3Z.mjs → hosted-logs-FIRx9X2f.mjs} +77 -43
  66. package/dist/{hosted-macos-BACVAVqE.mjs → hosted-macos-_awYSpfv.mjs} +35 -22
  67. package/dist/{hosted-native-A4s1QPXe.mjs → hosted-native-BdzE668-.mjs} +49 -27
  68. package/dist/idle-C4cZ757K.d.mts +24 -0
  69. package/dist/{idle-shutdown-DtxhPTKe.mjs → idle-shutdown-DDg3SdvY.mjs} +11 -10
  70. package/dist/ios-CDvlmUs7.d.mts +24 -0
  71. package/dist/{ios-BwVCF7oA.mjs → ios-Ds1clbIt.mjs} +8 -8
  72. package/dist/{ios-device-D7pi2Sca.mjs → ios-device--h_-8ENa.mjs} +51 -26
  73. package/dist/{ios-device-B5veSDDQ.mjs → ios-device-BXZ0c7Kb.mjs} +1 -1
  74. package/dist/{ios-RkW4pnQf.mjs → ios-hcBpnEGk.mjs} +200 -93
  75. package/dist/{ios-Ce7bc7br.mjs → ios-pm4nL6By.mjs} +160 -108
  76. package/dist/{ios-project-K9hY_Jab.mjs → ios-project-CZhKODF3.mjs} +13 -13
  77. package/dist/ios-signing-BtAffm0A.mjs +489 -0
  78. package/dist/{ios-state-CBLe1xYw.mjs → ios-state-Dtw5K2aI.mjs} +1 -1
  79. package/dist/{ios-nOLJGVdy.mjs → ios-xtlRHaeB.mjs} +1 -1
  80. package/dist/{js-swap-Bb3nsS07.mjs → js-swap-Bwhh4S7V.mjs} +30 -25
  81. package/dist/{launch-C3qJ6Agb.mjs → launch-BjxemNpK.mjs} +85 -543
  82. package/dist/{launch-F4sOqAhB.mjs → launch-CCzXlc8r.mjs} +91 -90
  83. package/dist/logs-BfwwadsZ.mjs +479 -0
  84. package/dist/logs-jTS_lHSo.mjs +248 -0
  85. package/dist/{macos-D7uDvO-z.mjs → macos-DVlSSyUg.mjs} +1 -1
  86. package/dist/macos-NfV2D7dO.mjs +694 -0
  87. package/dist/macos-run.mjs +1 -1
  88. package/dist/maintenance-run.mjs +123 -248
  89. package/dist/{metro-DnCEjCxs.mjs → metro-DiXtA3Ty.mjs} +107 -30
  90. package/dist/metro-gateway-CKZqKqx7.mjs +209 -0
  91. package/dist/{named-ports-4WopRPg-.mjs → named-ports-DkEzOfzO.mjs} +42 -90
  92. package/dist/native-android-CqgnOgsT.mjs +645 -0
  93. package/dist/native-gradle-artifact-inputs-DHFnmrVf.mjs +270 -0
  94. package/dist/native-gradle-inputs-Dnh84Zbd.mjs +211 -0
  95. package/dist/native-inputs-TRTTTRxt.mjs +91 -0
  96. package/dist/native-run-NwUuxbkg.mjs +276 -0
  97. package/dist/{native-runtime-k8dTIrbK.mjs → native-runtime-B7KIafpM.mjs} +11 -9
  98. package/dist/native-source-Y0yLhFyQ.mjs +133 -0
  99. package/dist/{native-xcode-inputs-CykopJdM.mjs → native-xcode-inputs-D_VOfFL7.mjs} +26 -18
  100. package/dist/{native-xcode-ios-Bbf30Gm0.mjs → native-xcode-ios-BYDOHWLN.mjs} +104 -60
  101. package/dist/{ndjson-GOnbyvso.mjs → ndjson-CgE_o3Dl.mjs} +3 -3
  102. package/dist/offload-worker.d.mts +59 -41
  103. package/dist/offload-worker.mjs +478 -115
  104. package/dist/operation-exec-BW0em61G.mjs +171 -0
  105. package/dist/{ownership-DCtw_2IA.mjs → ownership-C5fo5Edb.mjs} +3 -3
  106. package/dist/{page-DRHy329N.mjs → page-DBL2PiUU.mjs} +12 -5
  107. package/dist/paths-BsJMoPP2.mjs +12 -0
  108. package/dist/paths-DQtTgsBf.mjs +42 -0
  109. package/dist/{placement-log-MOfPVgaM.mjs → placement-log-BnGgJRRZ.mjs} +5 -5
  110. package/dist/{plan-placement-V0TdzYXp.mjs → plan-placement-DIsMtN-d.mjs} +15 -11
  111. package/dist/{ports-DdSUicVo.mjs → ports-Cy1RkmuQ.mjs} +3 -3
  112. package/dist/prebuild-D4JoEAEs.mjs +484 -0
  113. package/dist/{project-_phNIJfs.mjs → project-wM_XFTvE.mjs} +65 -37
  114. package/dist/{pull-request-CQ8sPbZa.mjs → pull-request-W1IuLXDt.mjs} +102 -20
  115. package/dist/pull-request-eODyIqZF.d.mts +124 -0
  116. package/dist/pull-requests.d.mts +2 -120
  117. package/dist/pull-requests.mjs +2 -2
  118. package/dist/{react-native-android-CM1XRDwv.mjs → react-native-android-6OZ7MSM7.mjs} +121 -72
  119. package/dist/{gradle-DSrXb6lG.mjs → react-native-build-Dvd8hC21.mjs} +292 -15
  120. package/dist/react-native-doctor-DsoSF1Y9.mjs +234 -0
  121. package/dist/{react-native-ios-Dhq_eiYQ.mjs → react-native-ios-4u1Rn-GR.mjs} +71 -39
  122. package/dist/{recordings-D0d5aQt0.mjs → recordings-CQmfzAh7.mjs} +12 -9
  123. package/dist/registration-BcRgF6Ox.mjs +51 -0
  124. package/dist/{reload-C8JhxjSh.mjs → reload-BzkYabB-.mjs} +94 -47
  125. package/dist/reload-D7AVlb8N.mjs +2 -0
  126. package/dist/{remote-BiR6aYjy.mjs → remote-DjGDmlTZ.mjs} +26 -18
  127. package/dist/result-D2Ykz_iD.mjs +435 -0
  128. package/dist/{run-BtPgpKks.mjs → run-DidHghD1.mjs} +5 -5
  129. package/dist/selection-DSMbWEX2.mjs +620 -0
  130. package/dist/{server-bare-R2m_gsH8.mjs → server-bare-CafzuyFB.mjs} +3 -4
  131. package/dist/{server-command-CTtCkmcQ.mjs → server-command-C6QjeupS.mjs} +14 -7
  132. package/dist/{server-expo-DsguMGUF.mjs → server-expo-CrjB29pq.mjs} +54 -11
  133. package/dist/server-expo-D58c9L0M.mjs +2 -0
  134. package/dist/{settings-B717odTD.mjs → settings-C7O8A6ZI.mjs} +309 -119
  135. package/dist/settings-DpR-1Wnn.mjs +114 -0
  136. package/dist/settings-write-CzD60rOD.mjs +360 -0
  137. package/dist/settings.schema.json +176 -16
  138. package/dist/{simslim-CCr3rE-A.mjs → simslim-BNckWpXi.mjs} +14 -12
  139. package/dist/{slot-launch-DPt0m9Nd.mjs → slot-launch-CbfmNgM6.mjs} +15 -20
  140. package/dist/{spawn-entry-Dcb7sDUz.mjs → spawn-entry-jw19m8Nl.mjs} +0 -1
  141. package/dist/stage-Bps5oius.mjs +200 -0
  142. package/dist/{start-TXeMmmO2.mjs → start-Br-oxqMD.mjs} +1 -1
  143. package/dist/start-EojN4WFz.mjs +1364 -0
  144. package/dist/{state-D1Icyjlu.mjs → state--smjFSTL.mjs} +55 -13
  145. package/dist/{state-CG_OFoaA.mjs → state-08I3hw3_.mjs} +3 -2
  146. package/dist/{stats-BoY5sEPU.mjs → stats-D3GerlFW.mjs} +7 -13
  147. package/dist/stats-report-BzaPrRm6.mjs +16 -0
  148. package/dist/{status-BTA7kKiQ.mjs → status-BRROykC8.mjs} +7 -5
  149. package/dist/status-EvrK72L2.mjs +2 -0
  150. package/dist/{status-WgFbkRzj.mjs → status-WBDajew2.mjs} +554 -452
  151. package/dist/{status-watch-C23ILtf9.mjs → status-watch-Bg_u-pwR.mjs} +222 -25
  152. package/dist/status-worker.d.mts +1 -0
  153. package/dist/status-worker.mjs +19 -0
  154. package/dist/stim-desktop-D5OGpYZM.mjs +112 -0
  155. package/dist/{stop-DR6VoDIk.mjs → stop-DTZ9uTw3.mjs} +2 -2
  156. package/dist/{stop-DPEhunlw.mjs → stop-Dky10AEJ.mjs} +33 -23
  157. package/dist/{stop-BNmBMi1F.mjs → stop-Mv6W9FAk.mjs} +137 -98
  158. package/dist/supervisor-run.d.mts +8 -17
  159. package/dist/supervisor-run.mjs +56 -28
  160. package/dist/{support-CwjbFs9H.mjs → support-CNioewsd.mjs} +14 -14
  161. package/dist/{support-DMTqj3Hm.mjs → support-yUiOjnVY.mjs} +20 -13
  162. package/dist/swiftpm-artifact-inputs-DFcGsXDP.mjs +153 -0
  163. package/dist/swiftpm-macos-BIak-Rxv.mjs +532 -0
  164. package/dist/toolchain-BY8X8HXx.mjs +2 -0
  165. package/dist/toolchain-DpJMHq0n.mjs +876 -0
  166. package/dist/{trigger-CigAYjeL.mjs → trigger-Cbyc41d_.mjs} +21 -6
  167. package/dist/trigger-yb_nFzPF.mjs +2 -0
  168. package/dist/{warm-progress-BuY-Fvfn.mjs → warm-progress-CA3TgT3-.mjs} +263 -156
  169. package/dist/web-CZ07gd77.mjs +2 -0
  170. package/dist/{web-BDBMxDpx.mjs → web-DKx6yoFA.mjs} +20 -14
  171. package/dist/web-run.mjs +13 -12
  172. package/dist/windows-report.d.mts +1 -0
  173. package/dist/windows-report.mjs +18 -0
  174. package/dist/windows-report.ps1 +43 -0
  175. package/dist/{workspace-state-DJt_MMvf.mjs → workspace-state-U08dEzhG.mjs} +62 -58
  176. package/dist/{workspaces-C36s2jb9.mjs → workspaces-CpANmBlq.mjs} +1723 -1286
  177. package/dist/{worktree-CgvJYJ-Q.mjs → worktree-CHecZ4Oq.mjs} +1097 -547
  178. package/dist/worktree-ZqZVjD9K.mjs +2 -0
  179. package/dist/xcode-BXn9-xYx.mjs +896 -0
  180. package/package.json +7 -5
  181. package/shim/android-audio/Main.java +24 -0
  182. package/shim/android-audio/README.md +17 -0
  183. package/shim/android-audio.jar +0 -0
  184. package/shim/android-optimizations.gradle +7 -5
  185. package/dist/activity-BSV745Kz.mjs +0 -629
  186. package/dist/api-qvOzWyfO.mjs +0 -154
  187. package/dist/api-run.mjs +0 -294
  188. package/dist/app-install-BIKtMfPq.mjs +0 -896
  189. package/dist/artifact-lifecycle-DzPpBYvK.mjs +0 -30
  190. package/dist/budget-f7516Zk0.mjs +0 -4106
  191. package/dist/build-facts-DcLv2HSd.d.mts +0 -111
  192. package/dist/cancellation-C_22VQTH.mjs +0 -24
  193. package/dist/client-qQAaXSUs.mjs +0 -1771
  194. package/dist/config-BYDxlMY3.mjs +0 -562
  195. package/dist/device-DuiPJQSn.mjs +0 -290
  196. package/dist/device-lease-BX1sJQ2E.mjs +0 -1224
  197. package/dist/device-remote-xRT0Vmo9.mjs +0 -1272
  198. package/dist/eas-session-ledger-Cm0Gwq2F.mjs +0 -207
  199. package/dist/exec-DqKo9GKJ.mjs +0 -268
  200. package/dist/git-DDfFzkMm.mjs +0 -45
  201. package/dist/launch-verify-B6Q8hZP-.mjs +0 -428
  202. package/dist/logs-BGrEUaEs.mjs +0 -383
  203. package/dist/macos-Ch5VJmcS.mjs +0 -584
  204. package/dist/metro-gateway-CX9Xlkq2.mjs +0 -103
  205. package/dist/miss-reason-mKzZ4VvN.mjs +0 -238
  206. package/dist/native-android-D5qc3-_v.mjs +0 -226
  207. package/dist/native-gradle-inputs-BsLRrl7u.mjs +0 -349
  208. package/dist/ndjson-Du3j0AQ9.d.mts +0 -17
  209. package/dist/plan-C4TjdPsR.mjs +0 -260
  210. package/dist/prebuild-DYbucPXy.mjs +0 -212
  211. package/dist/preview-78a9kR_-.mjs +0 -1011
  212. package/dist/react-native-build-CtCccJtb.mjs +0 -239
  213. package/dist/react-native-doctor-CfRnFfjs.mjs +0 -78
  214. package/dist/remote-cache-OyOyVIS3.mjs +0 -526
  215. package/dist/result-CaJg5Iat.mjs +0 -186
  216. package/dist/server-expo-Bigriskl.mjs +0 -2
  217. package/dist/settings-BORNaaCO.mjs +0 -402
  218. package/dist/settings-vk3mJGSm.d.mts +0 -13
  219. package/dist/spawn-claims-DVS5RkMS.mjs +0 -72
  220. package/dist/stage-KNTQopqv.mjs +0 -157
  221. package/dist/start-BWqzODpb.mjs +0 -1116
  222. package/dist/stim-desktop-BzBN2Em2.mjs +0 -63
  223. package/dist/swiftpm-macos-DRNVJWlb.mjs +0 -119
  224. package/dist/tailnet-Dh-qiYzG.mjs +0 -265
  225. package/dist/toolchain-BFkAcstd.mjs +0 -2
  226. package/dist/toolchain-DxSsNssu.mjs +0 -1657
  227. package/dist/trigger-9_USgmxz.mjs +0 -2
  228. package/dist/web-Dq-i3wzJ.mjs +0 -2
  229. package/dist/worktree-DpLdCHga.mjs +0 -2
  230. package/dist/{api-run.d.mts → doctor-fingerprint.d.mts} +0 -0
  231. package/dist/{errors-DzkMYh8Q.mjs → errors-ccaPc4ox.mjs} +2 -2
@@ -1,40 +1,47 @@
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";
19
- 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";
1
+ import { c as declareSpawnsOn, f as stopDeclaringSpawnsOn, g as process_identity_exports, h as withDrainingCancellation, r as WorkspaceProcessCleanupError, t as getExecutor, x as ownership_claim_exports } from "./exec-DEoAhcze.mjs";
2
+ import { d as workspaceDir } from "./paths-DQtTgsBf.mjs";
3
+ import { F as isOnMountedVolume, G as projectDeviceSlots, I as listMountedVolumes, L as measuredDirectorySizeAsync, N as directorySizeAsync, O as withConfigLockAsync$1, P as formatBytes, R as volumeRootFor, S as removeProject, _ as loadConfig$1, d as getConfigDir, j as acquireAvdClaim } from "./config-DlrsduwG.mjs";
4
+ import { h as shortUdid, o as formatLongDuration, u as plural } from "./command-output-gwpm28fu.mjs";
5
+ import { f as throwIfCancelled, t as runOperationFile } from "./operation-exec-BW0em61G.mjs";
6
+ import { u as workspaceLastUsed } from "./workspace-state-U08dEzhG.mjs";
7
+ import { Et as listWorktreesAsync, It as resolveSourceCheckoutAsync } from "./settings-C7O8A6ZI.mjs";
8
+ import { i as findProjectRoot$1, v as detectIsExpo } from "./project-wM_XFTvE.mjs";
9
+ import { n as easMachineStateRoot$1, o as removeEasSessionClaimAsync, r as readEasSessionLedger$1 } from "./eas-session-ledger-DlY_serQ.mjs";
10
+ import { $ as includesRecordings, At as unverifiedDeviceCommand, C as collectIdleDevices, D as idleDeviceLines, Dt as findOrphanedDevices, Et as eraseParkedDevices, Ft as withRemoteSessionGcLocks, In as runProcessOperation, 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-Cvwn54qV.mjs";
11
+ import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
12
+ import { B as createGcResults, L as listBuildSlots, a as collectWorkspaceOutputs, c as listWorkspaceDirs, f as withIdleWorkspace, i as collectOrphanedWorkspaces, l as rootPresence, m as workspaceInUseAsync, n as WORKSPACE_OUTPUT_DIRS, o as deleteOrphanedWorkspaces, r as clearWorkspaceOutputs, s as isInsideWorkspaces, t as REBUILD_COST, u as sizeText, z as GC_OUTPUT } from "./workspaces-CpANmBlq.mjs";
13
+ import { N as listBuildLocks, k as removeExpiredLease } from "./activity-CMLmzp7o.mjs";
14
+ import { n as forgetCreatedDeviceAsync, r as readCreatedDevices$1 } from "./created-devices-x9rN6bhs.mjs";
15
+ import { F as listAvdsAsync, L as listOrphanedAvdDirectories, V as ownedAvdDirectory, _t as parkedMaxSetting, dt as POOL_SETTING_REMEDY, h as avdNameAbsent } from "./android-C7sVmyI9.mjs";
16
+ import { d as listAllIosSims, f as listAllIosSimsAsync } from "./ios-hcBpnEGk.mjs";
17
+ import { r as readCollectors } from "./ownership-C5fo5Edb.mjs";
18
+ import { a as matchWorktreeEntry, b as deleteSelectedArchives, v as describeDereferenced, x as sweepArchiveStaging, y as reclaimProject } from "./worktree-CHecZ4Oq.mjs";
19
+ import { s as writeCacheFile } from "./warm-progress-CA3TgT3-.mjs";
20
+ 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-DsuqEkiM.mjs";
21
+ import { i as getSwiftpmCacheUsage, n as swiftpmCacheLines, r as getAgentDeviceUsage, t as agentDeviceLines } from "./agent-device-usage-output-DUFaTMdd.mjs";
22
+ import { existsSync, lstatSync, readFileSync, readdirSync, statSync } from "node:fs";
23
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
24
+ import { LOG_ROTATE_BYTES, ensurePrivateDir, rotatedLogPath, withDirLock, withDirLockTransaction } from "@stim-cli/core";
25
+ import { assertHostedDeviceLedger, configLockPath, deviceHostArea, deviceHostRoot, diskUsageCacheFile, hostedDeviceId, isJsonObject, listRecordedDevicesAsync, listStatusCacheEntries, maintenanceRunClaims, parseHostedPlatformDevice, readArchivesAsync, readDiskUsage, readHostedDevice, readHostedDeviceLedger, readHostedSessions, recordedBytes } from "@stim-cli/core/state";
26
+ import { isAbsolute as isAbsolute$1, join as join$1 } from "path";
23
27
  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";
25
- import { LOG_ROTATE_BYTES, rotatedLogPath } from "@stim-cli/core";
26
- import { InvalidArgumentError } from "commander";
28
+ import { setImmediate } from "node:timers/promises";
27
29
  import chalk from "chalk";
30
+ import { existsSync as existsSync$1 } from "fs";
31
+ import { InvalidArgumentError } from "commander";
32
+ import { open, readdir, rename, rm, stat, utimes, writeFile } from "node:fs/promises";
28
33
  //#region src/commands/gc/archives.ts
29
- function stagingOrNone() {
34
+ async function stagingOrNone(signal) {
30
35
  try {
31
- return sweepArchiveStaging(false, 5e3);
36
+ return await sweepArchiveStaging(false, 5e3, signal);
32
37
  } catch {
38
+ signal?.throwIfAborted();
33
39
  return [];
34
40
  }
35
41
  }
36
- function collectArchives(scope, olderThan, now) {
37
- const records = readArchives();
42
+ async function collectArchives(scope, olderThan, now, signal) {
43
+ signal?.throwIfAborted();
44
+ const records = await readArchivesAsync(signal);
38
45
  const { kind, id } = parseArchiveSelector(scope) ?? {
39
46
  kind: null,
40
47
  id: null
@@ -53,7 +60,7 @@ function collectArchives(scope, olderThan, now) {
53
60
  expires: record.expires,
54
61
  willDelete: (id === null && olderThan !== null ? now - Date.parse(record.removedAt) >= olderThan * 864e5 : true) && (!kind || record.bytes[kind] > 0)
55
62
  })),
56
- staging: stagingOrNone(),
63
+ staging: await stagingOrNone(signal),
57
64
  unknownId: id !== null && !match ? id : null,
58
65
  knownIds: records.map((record) => record.id),
59
66
  kind
@@ -67,467 +74,50 @@ function archiveLines(selection) {
67
74
  ...selection.staging.map((entry) => ` ${entry.path}: ${entry.kept ? `kept: ${entry.reason}` : "abandoned staging"}${entry.removeCommand ? `; ${entry.removeCommand}` : ""}`)
68
75
  ];
69
76
  }
70
- function deleteArchives(selection) {
77
+ async function deleteArchives(selection, output = GC_OUTPUT, signal) {
78
+ signal?.throwIfAborted();
71
79
  if (!selection) return 0;
72
80
  let failures = 0;
73
- for (const record of selection.records.filter((entry) => entry.willDelete)) try {
74
- deleteSelectedArchives([record.id], selection.kind);
75
- recordGcResult("archive", "done", record.id, {
76
- id: record.id,
77
- bytes: record.bytes
78
- });
79
- } catch (error) {
80
- failures++;
81
- recordGcResult("archive", "failed", record.id, {
82
- id: record.id,
83
- detail: error.message
84
- });
85
- console.error(`Could not delete ${record.id}: ${error.message}`);
81
+ for (const record of selection.records.filter((entry) => entry.willDelete)) {
82
+ signal?.throwIfAborted();
83
+ try {
84
+ await deleteSelectedArchives([record.id], selection.kind, signal);
85
+ output.record("archive", "done", record.id, {
86
+ id: record.id,
87
+ bytes: record.bytes
88
+ });
89
+ } catch (error) {
90
+ signal?.throwIfAborted();
91
+ failures++;
92
+ output.record("archive", "failed", record.id, {
93
+ id: record.id,
94
+ detail: error.message
95
+ });
96
+ output.error(`Could not delete ${record.id}: ${error.message}`);
97
+ }
86
98
  }
87
99
  try {
88
- const remaining = new Set(sweepArchiveStaging(true, 5e3).map((entry) => entry.path));
89
- for (const entry of selection.staging) if (!entry.kept && !remaining.has(entry.path)) recordGcResult("archive", "done", "archive staging", { id: entry.path });
100
+ const remaining = new Set((await sweepArchiveStaging(true, 5e3, signal)).map((entry) => entry.path));
101
+ for (const entry of selection.staging) if (!entry.kept && !remaining.has(entry.path)) output.record("archive", "done", "archive staging", { id: entry.path });
90
102
  } catch (error) {
103
+ signal?.throwIfAborted();
91
104
  failures++;
92
- console.error(`Could not clear archive staging: ${error.message}`);
105
+ output.error(`Could not clear archive staging: ${error.message}`);
93
106
  }
94
107
  return failures;
95
108
  }
96
109
  function archiveRefusal(selection) {
97
110
  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
111
  return {
105
112
  code: "STIM_BAD_ARG",
106
- message,
107
- remedy
113
+ message: `Unknown archive "${selection.unknownId}".`,
114
+ remedy: `Known archive ids: ${selection.knownIds.join(", ") || "(none)"}`
108
115
  };
109
116
  }
110
117
  function archiveWorkPending(selection, otherWork) {
111
118
  return otherWork || Boolean(selection?.records.some((record) => record.willDelete) || selection?.staging.some((entry) => !entry.kept));
112
119
  }
113
120
  //#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
121
  //#region src/commands/gc/eas-ledger.ts
532
122
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
533
123
  /**
@@ -548,20 +138,92 @@ function findStaleEasLedgerEntries(mountedVolumes, deps = {}) {
548
138
  workspaceRoot
549
139
  }));
550
140
  }
551
- function forgetStaleEasLedgerEntries(entries, mountedVolumes, deps = {}) {
141
+ async function forgetStaleEasLedgerEntries(entries, mountedVolumes, deps = {}, output = GC_OUTPUT, signal) {
142
+ signal?.throwIfAborted();
552
143
  let failures = 0;
553
144
  const root = deps.easLedgerRoot ?? easMachineStateRoot$1();
554
- const remove = deps.removeEasSessionClaim ?? removeEasSessionClaim;
555
- for (const entry of entries) try {
556
- if (remove(entry.id, root, (claim) => isStaleEasLedgerEntry(claim, mountedVolumes))) console.log(chalk.green(`Forgot the EAS ledger entry ${entry.id} (invalid id, ${entry.workspaceRoot} is gone)`));
557
- else console.log(chalk.dim(`Kept the EAS ledger entry ${entry.id}: it changed since the report.`));
558
- } catch (error) {
559
- failures++;
560
- console.log(chalk.red(`Could not forget the EAS ledger entry ${entry.id}: ${describeError(error)}`));
145
+ const remove = deps.removeEasSessionClaim ?? removeEasSessionClaimAsync;
146
+ for (const entry of entries) {
147
+ await setImmediate();
148
+ signal?.throwIfAborted();
149
+ try {
150
+ if (await remove(entry.id, root, (claim) => isStaleEasLedgerEntry(claim, mountedVolumes), signal)) output.log(chalk.green(`Forgot the EAS ledger entry ${entry.id} (invalid id, ${entry.workspaceRoot} is gone)`));
151
+ else output.log(chalk.dim(`Kept the EAS ledger entry ${entry.id}: it changed since the report.`));
152
+ } catch (error) {
153
+ signal?.throwIfAborted();
154
+ failures++;
155
+ output.log(chalk.red(`Could not forget the EAS ledger entry ${entry.id}: ${describeError(error)}`));
156
+ }
561
157
  }
158
+ signal?.throwIfAborted();
562
159
  return failures;
563
160
  }
564
161
  //#endregion
162
+ //#region src/devices/machine-cache-usage-output.ts
163
+ const size = (bytes) => bytes === null ? "unknown" : formatBytes(bytes);
164
+ function total(values) {
165
+ const known = values.reduce((sum, value) => sum + (value ?? 0), 0);
166
+ return `${values.some((value) => value === null) ? "at least " : ""}${formatBytes(known)}`;
167
+ }
168
+ function machineCacheLines(report, now = Date.now()) {
169
+ if (!report) return [];
170
+ const { gradle, simRuntimes, deviceSupport, runnerBuilds } = report;
171
+ const body = [];
172
+ if (gradle.unusedVersions.length) {
173
+ body.push(` Gradle versions no registered project pins (${total(gradle.unusedVersions.map((entry) => entry.bytes))}):`);
174
+ for (const entry of gradle.unusedVersions) {
175
+ body.push(` ${entry.version}: ${size(entry.bytes)}`);
176
+ for (const dir of entry.dirs) body.push(` ${dir.dir}`);
177
+ if (entry.remedy) body.push(chalk.dim(` remedy: ${entry.remedy}`));
178
+ }
179
+ 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(", ")}`));
180
+ }
181
+ if (gradle.modules) {
182
+ body.push(` Gradle dependency cache: ${size(gradle.modules.bytes)}`, ` ${gradle.modules.dir}`);
183
+ body.push(chalk.dim(` remedy: ${gradle.modules.remedy}`));
184
+ }
185
+ if (!simRuntimes.checked) body.push(chalk.dim(" Simulator runtimes: not checked, simctl did not answer"));
186
+ else if (simRuntimes.runtimes.length) {
187
+ body.push(` Simulator runtimes (${total(simRuntimes.runtimes.map((entry) => entry.bytes))}):`);
188
+ for (const runtime of simRuntimes.runtimes) {
189
+ const details = [
190
+ runtime.devices ? `${runtime.devices} simulator${runtime.devices === 1 ? "" : "s"}` : "no simulator uses it",
191
+ runtime.lastUsedAt ? `last used ${runtime.lastUsedAt.slice(0, 10)}` : null,
192
+ runtime.deletable ? null : "not deletable"
193
+ ].filter(Boolean);
194
+ body.push(` ${runtime.runtimeIdentifier.replace(/^com\.apple\.CoreSimulator\.SimRuntime\./, "")}${runtime.build ? ` (${runtime.build})` : ""}: ${size(runtime.bytes)}, ${details.join(", ")}`);
195
+ if (runtime.remedy) body.push(chalk.dim(` remedy: ${runtime.remedy}`));
196
+ }
197
+ }
198
+ if (deviceSupport.entries.length) {
199
+ body.push(` Xcode device support (${total(deviceSupport.entries.map((entry) => entry.bytes))}):`);
200
+ for (const entry of deviceSupport.entries) body.push(` ${entry.dir}: ${size(entry.bytes)}`);
201
+ body.push(chalk.dim(` remedy: ${deviceSupport.remedy}`));
202
+ }
203
+ if (runnerBuilds.groups.length) {
204
+ const currentVersions = runnerBuilds.agentDeviceVersion && runnerBuilds.xcodeBuildVersion ? `current: agent-device ${runnerBuilds.agentDeviceVersion}, Xcode ${runnerBuilds.xcodeBuildVersion}` : "current versions unknown";
205
+ body.push(` agent-device runner builds by version (${currentVersions}):`);
206
+ for (const group of runnerBuilds.groups) {
207
+ const details = [
208
+ `${group.entries} entr${group.entries === 1 ? "y" : "ies"}`,
209
+ size(group.bytes),
210
+ group.oldestUseAt ? `oldest use ${group.oldestUseAt.slice(0, 10)}` : null,
211
+ group.inUse ? `${group.inUse} in use` : null,
212
+ group.current ? "current" : group.current === false ? "older toolchain" : null
213
+ ].filter(Boolean);
214
+ body.push(` agent-device ${group.packageVersion ?? "unknown"}, Xcode ${group.xcodeBuildVersion ?? "unknown"}: ${details.join(", ")}`);
215
+ }
216
+ body.push(chalk.dim(` remedy: ${runnerBuilds.remedy}`));
217
+ }
218
+ for (const notice of report.notices) body.push(chalk.dim(` ${notice}`));
219
+ if (!body.length) return [];
220
+ return [
221
+ `Other tools' machine-wide caches${report.complete ? "" : " (incomplete)"} - measured ${formatLongDuration(Math.max(0, now - Date.parse(report.measuredAt)))} ago`,
222
+ ...body,
223
+ chalk.dim(" Stim reports these and never deletes them; use each tool's own cleanup.")
224
+ ];
225
+ }
226
+ //#endregion
565
227
  //#region src/commands/gc/status-caches.ts
566
228
  const KINDS = ["disk-usage", "pull-request"];
567
229
  const KEPT_REASON = {
@@ -627,23 +289,26 @@ function statusCacheLines(stale) {
627
289
  lines.push(" --delete removes them; `stim status` measures a folder again if it comes back.");
628
290
  return lines;
629
291
  }
630
- function deleteStaleStatusCaches(stale) {
292
+ async function deleteStaleStatusCaches(stale, output = GC_OUTPUT, signal) {
293
+ signal?.throwIfAborted();
631
294
  const mountedVolumes = listMountedVolumes();
632
295
  let failures = 0;
633
296
  for (const kind of KINDS) {
634
297
  let removed = 0;
635
298
  let bytes = 0;
636
299
  for (const entry of stale.filter((candidate) => candidate.kind === kind)) {
300
+ signal?.throwIfAborted();
637
301
  if (judge(entry.path, mountedVolumes) !== "gone") continue;
638
302
  try {
639
- rmSync(entry.file, { force: true });
303
+ await rm(entry.file, { force: true });
640
304
  removed++;
641
305
  bytes += entry.bytes;
642
306
  } catch (error) {
307
+ signal?.throwIfAborted();
643
308
  failures++;
644
309
  const detail = error?.message || String(error);
645
- console.log(chalk.red(`Could not remove ${entry.file}: ${detail}`));
646
- recordGcResult("statusCache", "failed", `${kind} cache entry`, {
310
+ output.log(chalk.red(`Could not remove ${entry.file}: ${detail}`));
311
+ output.record("statusCache", "failed", `${kind} cache entry`, {
647
312
  id: entry.file,
648
313
  detail
649
314
  });
@@ -651,11 +316,90 @@ function deleteStaleStatusCaches(stale) {
651
316
  }
652
317
  if (!removed) continue;
653
318
  const label = `${removed} stale ${kind} cache entr${removed === 1 ? "y" : "ies"}`;
654
- console.log(chalk.green(`Removed ${label}`));
655
- recordGcResult("statusCache", "done", label, { bytes });
319
+ output.log(chalk.green(`Removed ${label}`));
320
+ output.record("statusCache", "done", label, { bytes });
321
+ }
322
+ signal?.throwIfAborted();
323
+ return failures;
324
+ }
325
+ //#endregion
326
+ //#region src/commands/gc/finished-devices.ts
327
+ /** The owned devices of workspaces whose linked worktree is finished but kept, such as a dirty one. */
328
+ function collectFinishedDevices({ config, sims, avds, sweep, now = Date.now() }) {
329
+ const keys = Object.keys(config?.projects ?? {});
330
+ const devices = [];
331
+ const seen = /* @__PURE__ */ new Set();
332
+ for (const { root, reason } of finishedWorkspaces(sweep, now)) {
333
+ const key = keys.find((candidate) => canonicalPath(candidate) === root);
334
+ const project = key ? config?.projects[key] : void 0;
335
+ if (!config || !key || !project || seen.has(key)) continue;
336
+ seen.add(key);
337
+ for (const { idleDays: _idleDays, ...device } of findStaleProjectDevices({
338
+ config: {
339
+ ...config,
340
+ projects: { [key]: project }
341
+ },
342
+ sims,
343
+ avds,
344
+ olderThanDays: 0,
345
+ lastTouched: () => 0
346
+ })) devices.push({
347
+ ...device,
348
+ reason
349
+ });
350
+ }
351
+ return devices;
352
+ }
353
+ function finishedDeviceLines(devices = []) {
354
+ if (!devices.length) return [];
355
+ const lines = [`Owned devices of finished worktrees (${devices.length}) - the worktree is kept, its devices are not:`];
356
+ for (const device of devices) {
357
+ lines.push(` ${device.kind} ${device.name} (${device.id})${device.bytes ? ` ${formatBytes(device.bytes)}` : ""}`);
358
+ lines.push(` ${device.project}: ${device.reason}`);
359
+ }
360
+ lines.push(" --delete keeps a workspace that is in use, leased, driven, locked or pinned.");
361
+ return lines;
362
+ }
363
+ async function deleteFinishedDevices({ finishedDevices: devices, worktreeSweep }, output = GC_OUTPUT, operation) {
364
+ const graceMs = worktreeSweep?.graceMs ?? 0;
365
+ let failures = 0;
366
+ for (const project of new Set(devices.map((device) => device.project))) {
367
+ operation?.signal.throwIfAborted();
368
+ const outcome = await deleteIdleWorkspaceDevices(canonicalPath(project), {
369
+ windowMs: graceMs,
370
+ protectedRoot: triggeringRoot(operation),
371
+ purpose: "stim gc --delete",
372
+ operation,
373
+ output
374
+ });
375
+ if (outcome.status === "kept") {
376
+ output.log(chalk.yellow(`Kept the owned devices of ${project}: ${outcome.detail}`));
377
+ output.record("device", "kept", project, { detail: outcome.detail });
378
+ } else if (outcome.status === "failed") {
379
+ failures++;
380
+ output.log(chalk.red(`Could not delete the owned devices of ${project}: ${outcome.detail}`));
381
+ output.record("device", "failed", project, { detail: outcome.detail });
382
+ }
656
383
  }
657
384
  return failures;
658
385
  }
386
+ /** Owned simulator and emulator disk of registered workspaces, by platform. */
387
+ function ownedDeviceDisk(usage, unmeasured = {}) {
388
+ return ["ios", "android"].flatMap((kind) => {
389
+ const owned = usage.filter((device) => device.kind === kind && device.workspace);
390
+ const missing = unmeasured[kind] ?? 0;
391
+ return owned.length || missing ? [{
392
+ kind,
393
+ count: owned.length,
394
+ bytes: owned.reduce((sum, device) => sum + device.bytes, 0),
395
+ unmeasured: missing
396
+ }] : [];
397
+ });
398
+ }
399
+ function ownedDeviceDiskLines(disk = []) {
400
+ if (!disk.length) return [];
401
+ return [`Owned device disk: ${disk.map(({ kind, count, bytes, unmeasured }) => `${count} ${kind === "ios" ? "simulators" : "emulators"} ${formatBytes(bytes)}${unmeasured ? ` (${unmeasured} not measured)` : ""}`).join(", ")}`];
402
+ }
659
403
  //#endregion
660
404
  //#region src/commands/gc/recordings.ts
661
405
  const DAY_MS = 864e5;
@@ -702,23 +446,37 @@ function planRecordings(entries, { whole, olderThan, now }) {
702
446
  * workspace and dead project sweeps remove whole, are listed with them; entries whose project root is on an
703
447
  * unmounted volume are left out.
704
448
  */
705
- function collectRecordings(plan, gone = []) {
449
+ async function collectRecordings(plan, gone = [], signal) {
450
+ signal?.throwIfAborted();
706
451
  const mountedVolumes = listMountedVolumes();
707
- return planRecordings(listWorkspaceDirs().filter((entry) => entry.projectRoot === null || isOnMountedVolume(entry.projectRoot, mountedVolumes)).map((entry) => Object.assign({}, entry, { recordings: join$1(entry.dir, "recordings") })).filter((entry) => existsSync$1(entry.recordings)).map((entry) => Object.assign({}, entry, {
708
- gone: gone.includes(entry.dir),
709
- devices: listRecordedDevices(entry.recordings)
710
- })), plan);
452
+ const entries = [];
453
+ for (const entry of listWorkspaceDirs()) {
454
+ signal?.throwIfAborted();
455
+ if (entry.projectRoot !== null && !isOnMountedVolume(entry.projectRoot, mountedVolumes)) continue;
456
+ const recordings = join$1(entry.dir, "recordings");
457
+ if (!existsSync$1(recordings)) continue;
458
+ entries.push({
459
+ ...entry,
460
+ recordings,
461
+ gone: gone.includes(entry.dir),
462
+ devices: await listRecordedDevicesAsync(recordings, false, signal)
463
+ });
464
+ }
465
+ signal?.throwIfAborted();
466
+ return planRecordings(entries, plan);
711
467
  }
712
468
  /** Deletes what `planRecordings` chose, planning again from the files on disk. */
713
- function deleteRecordings(report, plan) {
469
+ async function deleteRecordings(report, plan, output = GC_OUTPUT, signal) {
470
+ signal?.throwIfAborted();
714
471
  let failures = 0;
715
472
  for (const entry of report) {
473
+ signal?.throwIfAborted();
716
474
  const label = entry.projectRoot ?? entry.dir;
717
475
  if (entry.withWorkspace) continue;
718
476
  if (!entry.willDelete) {
719
477
  if (entry.keptCode === "unresolved" || entry.keptCode === "recently-recorded") {
720
- console.log(chalk.dim(`Kept the device recordings of ${label}: ${entry.keptReason}`));
721
- recordGcResult("recording", "kept", label, {
478
+ output.log(chalk.dim(`Kept the device recordings of ${label}: ${entry.keptReason}`));
479
+ output.record("recording", "kept", label, {
722
480
  bytes: entry.bytes,
723
481
  detail: entry.keptReason
724
482
  });
@@ -728,27 +486,34 @@ function deleteRecordings(report, plan) {
728
486
  try {
729
487
  let deleted = 0;
730
488
  if (plan.whole) {
731
- deleted = recordedBytes(listRecordedDevices(entry.dir));
732
- rmSync$1(entry.dir, {
489
+ deleted = recordedBytes(await listRecordedDevicesAsync(entry.dir, false, signal));
490
+ signal?.throwIfAborted();
491
+ await rm(entry.dir, {
733
492
  recursive: true,
734
493
  force: true
735
494
  });
736
- } else if (plan.olderThan !== null) for (const segment of oldSegments(listRecordedDevices(entry.dir), plan.olderThan, plan.now)) {
737
- rmSync$1(segment.file, { force: true });
738
- deleted += segment.bytes;
495
+ } else if (plan.olderThan !== null) {
496
+ const devices = await listRecordedDevicesAsync(entry.dir, false, signal);
497
+ for (const segment of oldSegments(devices, plan.olderThan, plan.now)) {
498
+ signal?.throwIfAborted();
499
+ await rm(segment.file, { force: true });
500
+ deleted += segment.bytes;
501
+ }
739
502
  }
740
- console.log(chalk.green(`Deleted ${formatBytes(deleted)} of device recordings of ${label}`));
741
- recordGcResult("recording", "done", label, { bytes: deleted });
503
+ output.log(chalk.green(`Deleted ${formatBytes(deleted)} of device recordings of ${label}`));
504
+ output.record("recording", "done", label, { bytes: deleted });
742
505
  } catch (error) {
506
+ signal?.throwIfAborted();
743
507
  failures++;
744
508
  const detail = error?.message || String(error);
745
- console.log(chalk.red(`Could not delete the device recordings of ${label}: ${detail}`));
746
- recordGcResult("recording", "failed", label, {
509
+ output.log(chalk.red(`Could not delete the device recordings of ${label}: ${detail}`));
510
+ output.record("recording", "failed", label, {
747
511
  bytes: entry.bytes,
748
512
  detail
749
513
  });
750
514
  }
751
515
  }
516
+ signal?.throwIfAborted();
752
517
  return failures;
753
518
  }
754
519
  function recordingLines(recordings) {
@@ -850,7 +615,7 @@ function staleEasLedgerLines(entries = []) {
850
615
  " --delete forgets the LEDGER ENTRY only; no EAS session is contacted."
851
616
  ];
852
617
  }
853
- function formatGcReport({ maintenance, archives, skipped = [], deadProjects = [], orphanedPorts, invalidProjects = [], orphanedWorkspaces = [], parkedSims = [], parkedAvds = [], parkedHostedDevices, orphanedDevices = [], unverifiedDevices = [], staleDevices = [], staleDeviceRecords = [], staleLedgerEntries = [], staleEasLedgerEntries, staleStatusCaches = [], buildLocks = {
618
+ function formatGcReport({ maintenance, archives, skipped = [], deadProjects = [], orphanedPorts, invalidProjects = [], orphanedWorkspaces = [], parkedSims = [], parkedAvds = [], parkedHostedDevices, orphanedDevices = [], unverifiedDevices = [], staleDevices = [], finishedDevices, ownedDeviceDisk, staleDeviceRecords = [], staleLedgerEntries = [], staleEasLedgerEntries, staleStatusCaches = [], buildLocks = {
854
619
  stale: [],
855
620
  live: [],
856
621
  unresolved: []
@@ -866,10 +631,11 @@ function formatGcReport({ maintenance, archives, skipped = [], deadProjects = []
866
631
  orphaned: [],
867
632
  notices: [],
868
633
  deletionSafe: true
869
- }, agentDeviceUsage = null, swiftpmCacheUsage, caches = [], workspaceOutputs = null, workspaceLogs = [], recordings = [], worktreeSweep = null, memory, cacheScope = null, olderThan = null }, { now = Date.now() } = {}) {
634
+ }, agentDeviceUsage = null, swiftpmCacheUsage, machineCaches, tempLeftovers, gradleVersions, caches = [], workspaceOutputs = null, workspaceLogs = [], recordings = [], worktreeSweep = null, memory, cacheScope = null, olderThan = null }, { now = Date.now() } = {}) {
870
635
  const lines = archiveLines(archives);
871
636
  const parkedHostedLines = formatParkedHostedDeviceReport(parkedHostedDevices, now);
872
637
  const easLedgerLines = staleEasLedgerLines(staleEasLedgerEntries);
638
+ const finishedLines = finishedDeviceLines(finishedDevices);
873
639
  const staleLocks = buildLocks?.stale ?? [];
874
640
  const liveLocks = buildLocks?.live ?? [];
875
641
  const staleSlots = buildSlots?.stale ?? [];
@@ -884,6 +650,7 @@ function formatGcReport({ maintenance, archives, skipped = [], deadProjects = []
884
650
  parkedHostedLines,
885
651
  orphanedDevices,
886
652
  staleDevices,
653
+ finishedLines,
887
654
  staleDeviceRecords,
888
655
  staleLedgerEntries,
889
656
  easLedgerLines,
@@ -908,6 +675,7 @@ function formatGcReport({ maintenance, archives, skipped = [], deadProjects = []
908
675
  lines.push(...namedPortLines(orphanedPorts));
909
676
  lines.push(...orphanedWorkspaceLines(orphanedWorkspaces));
910
677
  lines.push(...worktreeSweepLines(worktreeSweep));
678
+ lines.push(...unlistedWorktreeLines(worktreeSweep));
911
679
  lines.push(...formatParkedSimReport(parkedSims, now, cacheScope !== null));
912
680
  lines.push(...formatParkedAvdReport(parkedAvds, now, cacheScope !== null));
913
681
  lines.push(...parkedHostedLines);
@@ -930,6 +698,7 @@ function formatGcReport({ maintenance, archives, skipped = [], deadProjects = []
930
698
  lines.push(` ${d.project} (idle ${d.idleDays}d)`);
931
699
  }
932
700
  }
701
+ lines.push(...finishedLines, ...ownedDeviceDiskLines(ownedDeviceDisk));
933
702
  lines.push(...idleDeviceLines(idleDevices));
934
703
  if (staleDeviceRecords.length) {
935
704
  lines.push(`Stale device records (${staleDeviceRecords.length}) - the device is gone, the project is not:`);
@@ -995,6 +764,9 @@ function formatGcReport({ maintenance, archives, skipped = [], deadProjects = []
995
764
  lines.push(...cacheLines(caches, workspaceOutputs));
996
765
  lines.push(...agentDeviceLines(agentDeviceUsage, true, now));
997
766
  lines.push(...swiftpmCacheLines(swiftpmCacheUsage, now));
767
+ lines.push(...machineCacheLines(machineCaches, now));
768
+ lines.push(...tempLeftoverLines(tempLeftovers));
769
+ lines.push(...gradleVersionLines(gradleVersions));
998
770
  lines.push(...memoryLines(memory, memoryCacheKind(cacheScope), now));
999
771
  lines.push(...maintenanceReportLines(maintenance));
1000
772
  return lines;
@@ -1072,18 +844,20 @@ function cacheLines(caches, workspaceOutputs) {
1072
844
  if (outputs) lines.push(...workspaceOutputLines(outputs, outputBytes));
1073
845
  for (const c of caches) {
1074
846
  const tag = c.source ? ` (${c.source})` : "";
1075
- lines.push(` ${formatBytes(c.bytes ?? 0).padStart(10)} ${c.name}${tag}`);
847
+ lines.push(` ${sizeText(c.bytes ?? null).padStart(10)} ${c.name}${tag}`);
1076
848
  lines.push(` ${c.dir}`);
1077
849
  if (c.note) lines.push(` ${c.note}`);
850
+ if (c.sizeError) lines.push(` ${c.sizeError}`);
1078
851
  if (c.willEmpty) lines.push(" would be EMPTIED");
1079
852
  else if (c.emptySkipped) lines.push(` would be left alone: ${c.emptySkipped}`);
1080
853
  else if (c.scopedEmpty?.emptySkipped) lines.push(` --delete --cache would leave it alone: ${c.scopedEmpty.emptySkipped}`);
1081
854
  }
1082
- lines.push(` total: ${formatBytes(total)}`);
855
+ const incomplete = caches.some((c) => c.bytes === void 0) || outputs?.workspaces.some((w) => w.bytes === null);
856
+ lines.push(` total: ${incomplete ? "at least " : ""}${formatBytes(total)}`);
1083
857
  const doomed = caches.filter((c) => c.willEmpty);
1084
858
  if (doomed.length) {
1085
859
  const doomedBytes = doomed.reduce((n, c) => n + (c.bytes ?? 0), 0);
1086
- lines.push(` would empty ${doomed.length} of these (${formatBytes(doomedBytes)})`);
860
+ lines.push(` would empty ${doomed.length} of these (${doomed.some((c) => c.bytes === void 0) ? "at least " : ""}${formatBytes(doomedBytes)})`);
1087
861
  }
1088
862
  const clearing = outputs?.workspaces.filter((w) => w.willClear) ?? [];
1089
863
  if (clearing.length) {
@@ -1093,6 +867,29 @@ function cacheLines(caches, workspaceOutputs) {
1093
867
  }
1094
868
  return lines;
1095
869
  }
870
+ const BUILD_STATE_DIRS_SHOWN = 4;
871
+ function buildStateLine(state) {
872
+ if (!state?.dirs.length) return null;
873
+ const shown = state.dirs.slice(0, BUILD_STATE_DIRS_SHOWN).map((dir) => `${dir.path} ${sizeText(dir.bytes)}`);
874
+ const more = state.dirs.length > BUILD_STATE_DIRS_SHOWN ? ", ..." : "";
875
+ return ` in-tree build state ${sizeText(state.bytes)}: ${shown.join(", ")}${more}`;
876
+ }
877
+ function unlistedWorktreeLines(sweep) {
878
+ const unlisted = sweep?.unlisted ?? [];
879
+ if (!unlisted.length) return [];
880
+ const lines = [`Directories under .worktrees that git does not list as worktrees (${unlisted.length}) - reported, never removed:`];
881
+ for (const dir of unlisted) {
882
+ lines.push(` ${dir.path} (${sizeText(dir.bytes)})`);
883
+ lines.push(` ${unlistedWorktreeDetail(dir)}`);
884
+ }
885
+ return lines;
886
+ }
887
+ function unlistedWorktreeDetail(dir) {
888
+ 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)}\``;
889
+ }
890
+ function unlistedWorktreeRemedy(dir) {
891
+ return `rm -rf '${dir.path.replaceAll("'", "'\\''")}'`;
892
+ }
1096
893
  function worktreeSweepLines(sweep) {
1097
894
  if (!sweep || !sweep.idle && !sweep.worktrees.length) return [];
1098
895
  const removable = sweep.worktrees.filter((w) => !w.skipped);
@@ -1103,8 +900,10 @@ function worktreeSweepLines(sweep) {
1103
900
  for (const w of sweep.worktrees) {
1104
901
  const age = w.idleDays === null ? "" : ` (idle ${w.idleDays}d)`;
1105
902
  lines.push(` ${w.path}${age}`);
903
+ const buildState = buildStateLine(w.buildState);
904
+ if (buildState) lines.push(buildState);
1106
905
  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)}`);
906
+ lines.push(w.skipped ? ` kept: ${w.skipped}${note && !w.skipped.includes(note) ? ` (${note})` : ""}` : ` would be REMOVED by \`stim worktree remove\`: ${worktreeRemovalReason(w, sweep)}`);
1108
907
  }
1109
908
  if (removable.length) lines.push(" --delete runs it without --force; use, idleness and HEAD are re-checked under its removal locks.");
1110
909
  return lines;
@@ -1127,7 +926,22 @@ function jsonPullRequest(lookup) {
1127
926
  containsHead: pr.containsHead
1128
927
  } : null;
1129
928
  }
1130
- function gcReportSections({ archives, skipped = [], deadProjects = [], orphanedPorts = [], invalidProjects = [], orphanedWorkspaces = [], worktreeSweep = null, parkedSims = [], parkedAvds = [], parkedHostedDevices = [], orphanedDevices = [], unverifiedDevices = [], staleDevices = [], staleDeviceRecords = [], staleLedgerEntries = [], staleEasLedgerEntries = [], staleStatusCaches = [], buildLocks = {
929
+ function linkedWorktreesJson(sweep) {
930
+ if (!sweep) return [];
931
+ return sweep.worktrees.map((w) => ({
932
+ path: w.path,
933
+ idleDays: w.idleDays,
934
+ mergedInto: w.merge?.merged ? w.merge.into : null,
935
+ pullRequest: jsonPullRequest(w.pullRequest),
936
+ pullRequestUnknown: w.pullRequest && "unavailable" in w.pullRequest ? w.pullRequest.unavailable : null,
937
+ willRemove: w.skipCode === null,
938
+ reason: w.skipCode,
939
+ detail: w.skipped ?? worktreeRemovalReason(w, sweep),
940
+ eligibleAt: w.eligibleAt === null ? null : new Date(w.eligibleAt).toISOString(),
941
+ buildState: w.buildState ?? null
942
+ }));
943
+ }
944
+ function gcReportSections({ archives, skipped = [], deadProjects = [], orphanedPorts = [], invalidProjects = [], orphanedWorkspaces = [], worktreeSweep = null, parkedSims = [], parkedAvds = [], parkedHostedDevices = [], orphanedDevices = [], unverifiedDevices = [], staleDevices = [], finishedDevices = [], ownedDeviceDisk = [], staleDeviceRecords = [], staleLedgerEntries = [], staleEasLedgerEntries = [], staleStatusCaches = [], buildLocks = {
1131
945
  stale: [],
1132
946
  live: [],
1133
947
  unresolved: []
@@ -1143,7 +957,7 @@ function gcReportSections({ archives, skipped = [], deadProjects = [], orphanedP
1143
957
  orphaned: [],
1144
958
  notices: [],
1145
959
  deletionSafe: true
1146
- }, workspaceOutputs = null, workspaceLogs = [], recordings = [], agentDeviceUsage = null, swiftpmCacheUsage = null, caches = [], memory = null }) {
960
+ }, workspaceOutputs = null, workspaceLogs = [], recordings = [], agentDeviceUsage = null, swiftpmCacheUsage = null, machineCaches = null, tempLeftovers = null, gradleVersions = null, caches = [], memory = null }) {
1147
961
  return {
1148
962
  ...archives ? {
1149
963
  archived: archives.records,
@@ -1161,16 +975,12 @@ function gcReportSections({ archives, skipped = [], deadProjects = [], orphanedP
1161
975
  projectRoot,
1162
976
  bytes: bytes ?? null
1163
977
  })),
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()
978
+ linkedWorktrees: linkedWorktreesJson(worktreeSweep),
979
+ unlistedWorktreeDirectories: (worktreeSweep?.unlisted ?? []).map((dir) => ({
980
+ path: dir.path,
981
+ repository: dir.repository,
982
+ bytes: dir.bytes,
983
+ remedy: unlistedWorktreeRemedy(dir)
1174
984
  })),
1175
985
  parkedSimulators: parkedSims.map(({ udid, name, model, runtime, parkedAt, app, bytes, listed }) => ({
1176
986
  udid,
@@ -1214,6 +1024,16 @@ function gcReportSections({ archives, skipped = [], deadProjects = [], orphanedP
1214
1024
  idleDays: d.idleDays,
1215
1025
  bytes: d.bytes ?? null
1216
1026
  })),
1027
+ finishedDevices: finishedDevices.map((d) => ({
1028
+ kind: d.kind,
1029
+ id: d.id,
1030
+ name: d.name,
1031
+ project: d.project,
1032
+ slot: d.slot ?? null,
1033
+ bytes: d.bytes ?? null,
1034
+ reason: d.reason
1035
+ })),
1036
+ ownedDeviceDisk,
1217
1037
  staleDeviceRecords: staleDeviceRecords.map((r) => ({
1218
1038
  kind: r.kind,
1219
1039
  id: r.id,
@@ -1325,12 +1145,203 @@ function gcReportSections({ archives, skipped = [], deadProjects = [], orphanedP
1325
1145
  })),
1326
1146
  agentDevice: agentDeviceUsage,
1327
1147
  swiftpmCache: swiftpmCacheUsage,
1148
+ machineCaches,
1149
+ tempLeftovers: tempLeftoverSection(tempLeftovers),
1150
+ gradleVersions,
1328
1151
  memory: memory?.processes ?? [],
1329
1152
  watchmanRoots: memory?.watchmanRoots ?? [],
1330
1153
  memoryNotices: (memory?.notices ?? []).map((message) => ({ message }))
1331
1154
  };
1332
1155
  }
1333
1156
  //#endregion
1157
+ //#region src/commands/gc/worktree-sizes.ts
1158
+ const SIZE_TIMEOUT_MS = 3e4;
1159
+ const BUILD_STATE_MAX_AGE_MS = 36e5;
1160
+ const BUILD_STATE_TIMEOUT_MS = 1e4;
1161
+ const BUILD_STATE_CONCURRENCY = 4;
1162
+ const PNPM_STORE = ".pnpm-store";
1163
+ const WORKTREES_DIR = ".worktrees";
1164
+ function isDirectory(path) {
1165
+ try {
1166
+ return lstatSync(path).isDirectory();
1167
+ } catch {
1168
+ return false;
1169
+ }
1170
+ }
1171
+ function inside(root, path) {
1172
+ const rel = relative(root, path);
1173
+ return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
1174
+ }
1175
+ async function buildStateDirs(path, keys, operation) {
1176
+ const entries = await listWorktreesAsync(path, {
1177
+ env: operation?.env,
1178
+ signal: operation?.signal,
1179
+ declareFor: operation
1180
+ });
1181
+ const own = keys.filter((key) => matchWorktreeEntry(entries, key)?.path === path);
1182
+ const root = canonicalPath(path);
1183
+ const worktree = {
1184
+ path,
1185
+ linked: true,
1186
+ keys: own
1187
+ };
1188
+ const dirs = /* @__PURE__ */ new Map();
1189
+ const add = (dir) => {
1190
+ if (!isDirectory(dir)) return;
1191
+ const real = canonicalPath(dir);
1192
+ if (inside(root, real) && !dirs.has(real)) dirs.set(real, dir);
1193
+ };
1194
+ for (const kind of ["build", "dependencies"]) for (const base of appRoots(worktree, kind)) for (const dir of ARTIFACT_DIRS[kind]) add(join(base, dir));
1195
+ add(join(path, PNPM_STORE));
1196
+ return [...dirs.keys()];
1197
+ }
1198
+ function cachedSize(dir, now) {
1199
+ const cached = readDiskUsage(dir);
1200
+ if (!cached) return null;
1201
+ const at = Date.parse(cached.measuredAt);
1202
+ if (now - at >= BUILD_STATE_MAX_AGE_MS) return null;
1203
+ try {
1204
+ return statSync(dir).mtimeMs > at ? null : cached.bytes;
1205
+ } catch {
1206
+ return null;
1207
+ }
1208
+ }
1209
+ async function duSize(dir, operation) {
1210
+ let out;
1211
+ try {
1212
+ out = await runOperationFile(operation, getExecutor(), "du", ["-sk", dir], {
1213
+ timeoutMs: BUILD_STATE_TIMEOUT_MS,
1214
+ killSignal: "SIGKILL",
1215
+ cwd: operation?.cwd,
1216
+ signal: operation?.signal
1217
+ });
1218
+ } catch (error) {
1219
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
1220
+ operation?.signal.throwIfAborted();
1221
+ const stdout = error instanceof Error && "stdout" in error ? error.stdout : null;
1222
+ out = typeof stdout === "string" ? stdout : "";
1223
+ }
1224
+ const kb = /^(\d+)\t/.exec(out)?.[1];
1225
+ return kb ? Number(kb) * 1024 : null;
1226
+ }
1227
+ async function eachLimited(items, limit, run) {
1228
+ const queue = items[Symbol.iterator]();
1229
+ const worker = async () => {
1230
+ for (const item of queue) await run(item);
1231
+ };
1232
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
1233
+ }
1234
+ /**
1235
+ * Sizes the regenerable directories maintenance clears and a project-local `.pnpm-store` in each worktree of the
1236
+ * sweep, from the disk-usage cache while an entry is under an hour old and no entry was added to or removed from the
1237
+ * directory since. Stale directories are measured with one `du` each, sequentially with an operation and at most
1238
+ * 4 at once otherwise, with 10 seconds each, so a
1239
+ * slow tree delays only itself; a directory du could not measure in time is reported unmeasured (null). Also lists
1240
+ * the directories directly under a repository's `.worktrees/` that are no git worktree at all.
1241
+ */
1242
+ async function measureWorktreeSweep(sweep, operation) {
1243
+ operation?.signal.throwIfAborted();
1244
+ const now = Date.now();
1245
+ const pending = [];
1246
+ for (const candidate of sweep.worktrees) {
1247
+ operation?.signal.throwIfAborted();
1248
+ if (candidate.skipCode === "not-a-worktree") continue;
1249
+ const dirs = await buildStateDirs(candidate.path, candidate.keys, operation);
1250
+ pending.push({
1251
+ candidate,
1252
+ root: canonicalPath(candidate.path),
1253
+ dirs,
1254
+ sizes: new Map(dirs.map((dir) => [dir, cachedSize(dir, now)]))
1255
+ });
1256
+ }
1257
+ const due = pending.flatMap(({ sizes }) => [...sizes].flatMap(([dir, bytes]) => bytes === null ? [{
1258
+ dir,
1259
+ sizes
1260
+ }] : []));
1261
+ const worktrees = new Set(due.map(({ sizes }) => sizes)).size;
1262
+ if (due.length) phase("worktrees", `measuring the build state of ${plural(worktrees, "worktree")}`, operation);
1263
+ await eachLimited(due, operation ? 1 : BUILD_STATE_CONCURRENCY, async ({ dir, sizes }) => {
1264
+ operation?.signal.throwIfAborted();
1265
+ const measuredAt = (/* @__PURE__ */ new Date()).toISOString();
1266
+ const bytes = await duSize(dir, operation);
1267
+ if (bytes === null) return;
1268
+ sizes.set(dir, bytes);
1269
+ operation?.signal.throwIfAborted();
1270
+ writeCacheFile(diskUsageCacheFile(dir), {
1271
+ path: dir,
1272
+ measuredAt,
1273
+ bytes
1274
+ });
1275
+ });
1276
+ for (const { candidate, root, dirs, sizes } of pending) {
1277
+ const entries = dirs.map((dir) => ({
1278
+ path: relative(root, dir).split(sep).join("/"),
1279
+ bytes: sizes.get(dir) ?? null
1280
+ }));
1281
+ candidate.buildState = {
1282
+ bytes: entries.every((dir) => dir.bytes !== null) ? entries.reduce((sum, dir) => sum + (dir.bytes ?? 0), 0) : null,
1283
+ dirs: entries.toSorted((a, b) => (b.bytes ?? 0) - (a.bytes ?? 0))
1284
+ };
1285
+ }
1286
+ sweep.unlisted = await unlistedWorktreeDirs(operation);
1287
+ return sweep;
1288
+ }
1289
+ /** Whether `path` is a checkout git still knows: its own `.git` directory, or a `.git` file whose gitdir exists. */
1290
+ function hasLiveGitDir(path) {
1291
+ const dotGit = join(path, ".git");
1292
+ let text;
1293
+ try {
1294
+ if (lstatSync(dotGit).isDirectory()) return true;
1295
+ text = readFileSync(dotGit, "utf-8");
1296
+ } catch {
1297
+ return false;
1298
+ }
1299
+ const gitdir = /^gitdir:\s*(.+)$/m.exec(text)?.[1]?.trim();
1300
+ return Boolean(gitdir) && existsSync(resolve(dirname(dotGit), gitdir ?? ""));
1301
+ }
1302
+ async function unlistedWorktreeDirs(operation) {
1303
+ const repositories = /* @__PURE__ */ new Map();
1304
+ const options = {
1305
+ env: operation?.env,
1306
+ signal: operation?.signal,
1307
+ declareFor: operation
1308
+ };
1309
+ for (const root of await candidateRootsAsync(operation)) {
1310
+ operation?.signal.throwIfAborted();
1311
+ const { entries, ...source } = await resolveSourceCheckoutAsync(root, options);
1312
+ if ("refusal" in source || repositories.has(source.path)) continue;
1313
+ repositories.set(source.path, entries.map((entry) => canonicalPath(entry.path)));
1314
+ }
1315
+ const found = [];
1316
+ for (const [repository, listed] of repositories) {
1317
+ operation?.signal.throwIfAborted();
1318
+ const parent = join(repository, WORKTREES_DIR);
1319
+ let names;
1320
+ try {
1321
+ names = readdirSync(parent);
1322
+ } catch {
1323
+ continue;
1324
+ }
1325
+ for (const name of names.toSorted()) {
1326
+ operation?.signal.throwIfAborted();
1327
+ const path = join(parent, name);
1328
+ if (!isDirectory(path) || hasLiveGitDir(path)) continue;
1329
+ const real = canonicalPath(path);
1330
+ if (listed.some((entry) => entry === real || entry.startsWith(`${real}${sep}`))) continue;
1331
+ found.push({
1332
+ path,
1333
+ repository,
1334
+ bytes: await measuredDirectorySizeAsync(path, {
1335
+ timeoutMs: SIZE_TIMEOUT_MS,
1336
+ operation,
1337
+ signal: operation?.signal
1338
+ })
1339
+ });
1340
+ }
1341
+ }
1342
+ return found;
1343
+ }
1344
+ //#endregion
1334
1345
  //#region src/commands/gc/ledger.ts
1335
1346
  /** `sims` must be a complete listing, unavailable simulators included, taken after `ledger` was read. */
1336
1347
  function findStaleLedgerEntries(ledger, sims) {
@@ -1361,15 +1372,23 @@ function isStaleAvdName(name, config) {
1361
1372
  function reservedForSetup(name, config) {
1362
1373
  return Object.values(config?.projects ?? {}).some((project) => projectDeviceSlots(project).some(({ platforms }) => platforms.android?.setupIncomplete && platforms.android.avdName === name));
1363
1374
  }
1364
- function forgetStaleAvdName(name) {
1375
+ async function forgetStaleAvdName(name, signal) {
1365
1376
  let claim = null;
1366
1377
  try {
1367
1378
  claim = acquireAvdClaim(name);
1368
- return withConfigLock$1(() => {
1369
- if (!isStaleAvdName(name, loadConfig$1())) return false;
1370
- forgetCreatedDevice("android", name);
1371
- return true;
1372
- });
1379
+ while (true) try {
1380
+ return await forgetCreatedDeviceAsync("android", name, signal, (forget) => withDirLock(configLockPath(), () => {
1381
+ if (!isStaleAvdName(name, loadConfig$1())) return false;
1382
+ forget();
1383
+ return true;
1384
+ }, {
1385
+ waitMs: 0,
1386
+ ensureParent: () => ensurePrivateDir(getConfigDir())
1387
+ }));
1388
+ } catch (error) {
1389
+ if (!(error instanceof Error && "code" in error && error.code === "STIM_LOCK_TIMEOUT" && "lockPath" in error && error.lockPath === configLockPath())) throw error;
1390
+ await withConfigLockAsync$1(() => {}, signal);
1391
+ }
1373
1392
  } finally {
1374
1393
  (0, ownership_claim_exports.releaseClaim)(claim);
1375
1394
  }
@@ -1396,23 +1415,28 @@ function isDanglingLink(path) {
1396
1415
  return false;
1397
1416
  }
1398
1417
  }
1399
- function forgetStaleLedgerEntries(entries) {
1418
+ async function forgetStaleLedgerEntries(entries, output = GC_OUTPUT, signal) {
1419
+ signal?.throwIfAborted();
1400
1420
  let failures = 0;
1401
1421
  for (const entry of entries) {
1422
+ await setImmediate();
1423
+ signal?.throwIfAborted();
1402
1424
  if (entry.kind === "web" && !isStaleProfile(entry.id, existsSync)) continue;
1403
1425
  try {
1404
1426
  if (entry.kind === "android") {
1405
- if (!forgetStaleAvdName(entry.id)) {
1406
- console.log(chalk.dim(`Kept the ledger entry for android ${entry.id}: the AVD or its setup reappeared`));
1427
+ if (!await forgetStaleAvdName(entry.id, signal)) {
1428
+ output.log(chalk.dim(`Kept the ledger entry for android ${entry.id}: the AVD or its setup reappeared`));
1407
1429
  continue;
1408
1430
  }
1409
- } else forgetCreatedDevice(entry.kind, entry.id);
1410
- console.log(chalk.green(`Forgot the ledger entry for ${entry.kind} ${entry.id} (not on this machine)`));
1431
+ } else await forgetCreatedDeviceAsync(entry.kind, entry.id, signal);
1432
+ output.log(chalk.green(`Forgot the ledger entry for ${entry.kind} ${entry.id} (not on this machine)`));
1411
1433
  } catch (error) {
1434
+ signal?.throwIfAborted();
1412
1435
  failures++;
1413
- console.log(chalk.red(`Could not forget the ledger entry for ${entry.kind} ${entry.id}: ${error?.message || error}`));
1436
+ output.log(chalk.red(`Could not forget the ledger entry for ${entry.kind} ${entry.id}: ${error?.message || error}`));
1414
1437
  }
1415
1438
  }
1439
+ signal?.throwIfAborted();
1416
1440
  return failures;
1417
1441
  }
1418
1442
  //#endregion
@@ -1423,11 +1447,14 @@ const CAPPED_LOGS = [
1423
1447
  "device.ndjson"
1424
1448
  ];
1425
1449
  const TRIM_ABOVE_BYTES = 2 * LOG_ROTATE_BYTES;
1426
- function fileSize(path) {
1450
+ async function fileSize(path, signal) {
1451
+ signal?.throwIfAborted();
1427
1452
  try {
1428
- const stat = statSync$1(path);
1429
- return stat.isFile() ? stat.size : 0;
1453
+ const metadata = await stat(path);
1454
+ signal?.throwIfAborted();
1455
+ return metadata.isFile() ? metadata.size : 0;
1430
1456
  } catch {
1457
+ signal?.throwIfAborted();
1431
1458
  return 0;
1432
1459
  }
1433
1460
  }
@@ -1446,29 +1473,36 @@ function cappedFiles(logsDir) {
1446
1473
  function trimmable(size) {
1447
1474
  return size > TRIM_ABOVE_BYTES ? size - LOG_ROTATE_BYTES : 0;
1448
1475
  }
1449
- function excess(logsDir) {
1450
- return cappedFiles(logsDir).reduce((sum, { file }) => sum + trimmable(fileSize(file)), 0);
1476
+ async function excess(logsDir, signal) {
1477
+ let bytes = 0;
1478
+ for (const { file } of cappedFiles(logsDir)) bytes += trimmable(await fileSize(file, signal));
1479
+ return bytes;
1451
1480
  }
1452
1481
  function collectorReason(root) {
1453
1482
  const platforms = Object.keys(readCollectors(root));
1454
1483
  return platforms.length ? `a device log collector is recorded for ${platforms.join(", ")}` : null;
1455
1484
  }
1456
- function collectWorkspaceLogs({ exclude = [] } = {}) {
1485
+ async function collectWorkspaceLogs({ exclude = [], operation } = {}) {
1486
+ const signal = operation?.signal;
1487
+ signal?.throwIfAborted();
1457
1488
  const mountedVolumes = listMountedVolumes();
1458
1489
  const entries = [];
1459
1490
  for (const { dir, projectRoot, problem } of listWorkspaceDirs()) {
1491
+ signal?.throwIfAborted();
1460
1492
  if (exclude.includes(dir)) continue;
1461
1493
  if (projectRoot !== null && !isOnMountedVolume(projectRoot, mountedVolumes)) continue;
1462
1494
  const logsDir = join$1(dir, "logs");
1463
1495
  let names;
1464
1496
  try {
1465
- names = readdirSync$1(logsDir);
1497
+ names = await readdir(logsDir);
1466
1498
  } catch {
1499
+ signal?.throwIfAborted();
1467
1500
  continue;
1468
1501
  }
1469
- const bytes = names.reduce((sum, name) => sum + fileSize(join$1(logsDir, name)), 0);
1502
+ let bytes = 0;
1503
+ for (const name of names) bytes += await fileSize(join$1(logsDir, name), signal);
1470
1504
  if (bytes === 0) continue;
1471
- const trimBytes = excess(logsDir);
1505
+ const trimBytes = await excess(logsDir, signal);
1472
1506
  let keptCode = null;
1473
1507
  let keptReason = null;
1474
1508
  if (trimBytes > 0) {
@@ -1476,7 +1510,7 @@ function collectWorkspaceLogs({ exclude = [] } = {}) {
1476
1510
  keptCode = "unresolved";
1477
1511
  keptReason = `workspace directory not resolved: ${problem ?? "unknown project root"}`;
1478
1512
  } else {
1479
- const inUse = workspaceInUse(projectRoot);
1513
+ const inUse = await workspaceInUseAsync(projectRoot, {}, signal, operation);
1480
1514
  const collector = collectorReason(projectRoot);
1481
1515
  if (inUse.length) {
1482
1516
  keptCode = "in-use";
@@ -1497,333 +1531,92 @@ function collectWorkspaceLogs({ exclude = [] } = {}) {
1497
1531
  keptReason
1498
1532
  });
1499
1533
  }
1534
+ signal?.throwIfAborted();
1500
1535
  return entries;
1501
1536
  }
1502
- function trimToNewest({ file, log }, maxBytes) {
1503
- if (!trimmable(fileSize(file))) return 0;
1504
- return withDirLock$1(`${log}.lock`, () => {
1505
- const { size, atime, mtime } = statSync$1(file);
1537
+ async function trimToNewest({ file, log }, maxBytes, signal) {
1538
+ if (!trimmable(await fileSize(file, signal))) return 0;
1539
+ return withDirLockTransaction(`${log}.lock`, async () => {
1540
+ signal?.throwIfAborted();
1541
+ const { size, atime, mtime } = await stat(file);
1542
+ signal?.throwIfAborted();
1506
1543
  if (!trimmable(size)) return 0;
1507
1544
  const tail = Buffer.alloc(maxBytes);
1508
- const fd = openSync$1(file, "r");
1545
+ const fd = await open(file, "r");
1509
1546
  try {
1510
- readSync$1(fd, tail, 0, maxBytes, size - maxBytes);
1547
+ signal?.throwIfAborted();
1548
+ await fd.read(tail, 0, maxBytes, size - maxBytes);
1511
1549
  } finally {
1512
- closeSync$1(fd);
1550
+ await fd.close();
1513
1551
  }
1552
+ signal?.throwIfAborted();
1514
1553
  const newline = tail.indexOf(10);
1515
1554
  const kept = newline === -1 ? Buffer.alloc(0) : tail.subarray(newline + 1);
1516
1555
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
1517
1556
  try {
1518
- writeFileSync$1(tmp, kept);
1519
- utimesSync$1(tmp, atime, mtime);
1520
- renameSync$1(tmp, file);
1521
- } catch (error) {
1522
- rmSync$1(tmp, { force: true });
1523
- throw error;
1557
+ await writeFile(tmp, kept, { signal });
1558
+ await utimes(tmp, atime, mtime);
1559
+ signal?.throwIfAborted();
1560
+ await rename(tmp, file);
1561
+ } finally {
1562
+ await rm(tmp, { force: true });
1524
1563
  }
1525
1564
  return size - kept.length;
1526
- });
1565
+ }, { signal });
1527
1566
  }
1528
- async function trimWorkspaceLogs(entries) {
1567
+ async function trimWorkspaceLogs(entries, output = GC_OUTPUT, operation) {
1568
+ const signal = operation?.signal;
1569
+ signal?.throwIfAborted();
1529
1570
  let failures = 0;
1530
1571
  let trimmed = 0;
1531
1572
  for (const entry of entries) {
1573
+ signal?.throwIfAborted();
1532
1574
  const root = entry.projectRoot;
1533
1575
  if (!entry.willTrim || root === null) {
1534
- if (entry.keptReason) console.log(chalk.dim(`Kept the logs of ${root ?? entry.dir}: ${entry.keptReason}`));
1576
+ if (entry.keptReason) output.log(chalk.dim(`Kept the logs of ${root ?? entry.dir}: ${entry.keptReason}`));
1535
1577
  continue;
1536
1578
  }
1537
1579
  let run;
1538
1580
  try {
1539
- run = await withIdleWorkspace(root, () => {
1581
+ run = await withIdleWorkspace(root, async () => {
1540
1582
  const collector = collectorReason(root);
1541
1583
  if (collector) return {
1542
1584
  kept: collector,
1543
1585
  bytes: 0
1544
1586
  };
1587
+ let bytes = 0;
1588
+ for (const capped of cappedFiles(join$1(entry.dir, "logs"))) {
1589
+ signal?.throwIfAborted();
1590
+ bytes += await trimToNewest(capped, LOG_ROTATE_BYTES, signal);
1591
+ }
1545
1592
  return {
1546
1593
  kept: null,
1547
- bytes: cappedFiles(join$1(entry.dir, "logs")).reduce((sum, capped) => sum + trimToNewest(capped, LOG_ROTATE_BYTES), 0)
1594
+ bytes
1548
1595
  };
1549
- }, { purpose: "gc" });
1596
+ }, {
1597
+ purpose: "gc",
1598
+ operation
1599
+ });
1550
1600
  } catch (error) {
1601
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
1602
+ signal?.throwIfAborted();
1551
1603
  failures++;
1552
- console.log(chalk.red(`Could not trim the logs of ${root}: ${error?.message || String(error)}`));
1604
+ output.log(chalk.red(`Could not trim the logs of ${root}: ${error?.message || String(error)}`));
1553
1605
  continue;
1554
1606
  }
1555
1607
  if (!run.ran || run.value.kept) {
1556
1608
  const kept = run.ran ? run.value.kept : `in use: ${run.reasons.join("; ")}`;
1557
- console.log(chalk.yellow(`Kept the logs of ${root}: ${kept}`));
1609
+ output.log(chalk.yellow(`Kept the logs of ${root}: ${kept}`));
1558
1610
  continue;
1559
1611
  }
1560
1612
  trimmed += run.value.bytes;
1561
- console.log(chalk.green(`Trimmed the logs of ${root} (${formatBytes(run.value.bytes)})`));
1613
+ output.log(chalk.green(`Trimmed the logs of ${root} (${formatBytes(run.value.bytes)})`));
1562
1614
  }
1563
- if (trimmed) console.log(chalk.dim(`Trimmed ${formatBytes(trimmed)} of workspace logs.`));
1615
+ signal?.throwIfAborted();
1616
+ if (trimmed) output.log(chalk.dim(`Trimmed ${formatBytes(trimmed)} of workspace logs.`));
1564
1617
  return failures;
1565
1618
  }
1566
1619
  //#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
1620
  //#region src/commands/gc/hosted-devices.ts
1828
1621
  function collectParkedHostedDevices({ olderThanDays = null, now = Date.now() } = {}) {
1829
1622
  const notices = [];
@@ -1872,15 +1665,17 @@ function collectParkedHostedDevices({ olderThanDays = null, now = Date.now() } =
1872
1665
  notices
1873
1666
  };
1874
1667
  }
1875
- async function stopHostedDevice(record, claim) {
1668
+ async function stopHostedDevice(record, claim, operation) {
1669
+ operation?.signal.throwIfAborted();
1876
1670
  const home = join(deviceHostArea(record.id), "home");
1877
1671
  (0, ownership_claim_exports.markClaimChildPending)(claim);
1672
+ if (operation) declareSpawnsOn(operation, claim);
1878
1673
  let child;
1879
1674
  try {
1880
1675
  child = getExecutor().spawn(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), "device-host-worker.mjs")], {
1881
1676
  cwd: home,
1882
1677
  env: {
1883
- ...process.env,
1678
+ ...operation?.env ?? process.env,
1884
1679
  STIM_HOME: home
1885
1680
  },
1886
1681
  detached: true,
@@ -1897,6 +1692,8 @@ async function stopHostedDevice(record, claim) {
1897
1692
  value: null,
1898
1693
  notice: describeError(error)
1899
1694
  };
1695
+ } finally {
1696
+ if (operation) stopDeclaringSpawnsOn(operation, claim);
1900
1697
  }
1901
1698
  let identity = null;
1902
1699
  let closed = false;
@@ -1917,6 +1714,7 @@ async function stopHostedDevice(record, claim) {
1917
1714
  clearTimeout(killTimer);
1918
1715
  clearTimeout(finishTimer);
1919
1716
  clearTimeout(groupTimer);
1717
+ operation?.signal.removeEventListener("abort", cancel);
1920
1718
  const settled = closed && (!child.pid || !(0, ownership_claim_exports.processGroupAlive)(child.pid));
1921
1719
  if (settled) (0, ownership_claim_exports.clearClaimChild)(claim);
1922
1720
  let value = null;
@@ -1941,12 +1739,12 @@ async function stopHostedDevice(record, claim) {
1941
1739
  if (child.pid && identity && ["same", "gone"].includes((0, process_identity_exports.inspectProcessIdentity)(identity))) try {
1942
1740
  process.kill(-child.pid, name);
1943
1741
  } catch {}
1944
- else if (!identity) child.kill(name);
1742
+ else if (!identity) child.stdin?.destroy();
1945
1743
  };
1946
1744
  const cancel = () => {
1947
1745
  if (finished || cancelling) return;
1948
1746
  cancelling = true;
1949
- notice ??= "Hosted worker exceeded its deadline.";
1747
+ notice ??= operation?.signal.aborted ? "Hosted cleanup cancelled." : "Hosted worker exceeded its deadline.";
1950
1748
  signal("SIGTERM");
1951
1749
  killTimer = setTimeout(() => signal("SIGKILL"), 5e3);
1952
1750
  finishTimer = setTimeout(finish, 1e4);
@@ -1960,6 +1758,7 @@ async function stopHostedDevice(record, claim) {
1960
1758
  }
1961
1759
  };
1962
1760
  timer = setTimeout(cancel, 9e4);
1761
+ operation?.signal.addEventListener("abort", cancel, { once: true });
1963
1762
  child.stdout?.on("data", (chunk) => {
1964
1763
  if (Buffer.byteLength(output) + chunk.length > 16384) {
1965
1764
  notice = "Hosted worker output exceeded its bound.";
@@ -1992,6 +1791,10 @@ async function stopHostedDevice(record, claim) {
1992
1791
  };
1993
1792
  try {
1994
1793
  (0, ownership_claim_exports.setClaimChild)(claim, identity);
1794
+ if (operation?.signal.aborted) {
1795
+ cancel();
1796
+ return;
1797
+ }
1995
1798
  child.stdin?.end(JSON.stringify({
1996
1799
  mode: "stop",
1997
1800
  platform: record.platform,
@@ -2008,14 +1811,15 @@ async function stopHostedDevice(record, claim) {
2008
1811
  }
2009
1812
  });
2010
1813
  }
2011
- async function deleteParkedHostedDevices(devices, { listAvds: list = listAvds } = {}) {
1814
+ async function deleteParkedHostedDevices(devices, { listAvds: list, operation } = {}, output = GC_OUTPUT) {
2012
1815
  let failures = 0;
2013
1816
  for (const device of devices) {
1817
+ operation?.signal.throwIfAborted();
2014
1818
  if (device.listed === false) continue;
2015
1819
  const label = `${device.platform} ${device.name ?? device.session} (hosted session ${device.session})`;
2016
1820
  const kept = (detail) => {
2017
- console.log(`Kept ${label}: ${detail}`);
2018
- recordGcResult("parkedHostedDevice", "kept", label, {
1821
+ output.log(`Kept ${label}: ${detail}`);
1822
+ output.record("parkedHostedDevice", "kept", label, {
2019
1823
  id: device.id,
2020
1824
  detail
2021
1825
  });
@@ -2057,22 +1861,42 @@ async function deleteParkedHostedDevices(devices, { listAvds: list = listAvds }
2057
1861
  kept(describeError(error));
2058
1862
  continue;
2059
1863
  }
2060
- if (device.platform === "android" && !list().includes(device.id)) {
2061
- kept("AVD not visible from this shell's Android environment; run gc with the server's ANDROID_AVD_HOME/HOME");
2062
- continue;
1864
+ if (device.platform === "android") {
1865
+ let avds;
1866
+ if (operation) declareSpawnsOn(operation, claim);
1867
+ try {
1868
+ avds = await (list ? list() : listAvdsAsync({
1869
+ operation,
1870
+ signal: operation?.signal
1871
+ }));
1872
+ } finally {
1873
+ if (operation) stopDeclaringSpawnsOn(operation, claim);
1874
+ }
1875
+ if (!avds.includes(device.id)) {
1876
+ kept("AVD not visible from this shell's Android environment; run gc with the server's ANDROID_AVD_HOME/HOME");
1877
+ continue;
1878
+ }
2063
1879
  }
1880
+ operation?.signal.throwIfAborted();
2064
1881
  settled = false;
2065
- const outcome = await stopHostedDevice(current, claim);
1882
+ const outcome = await stopHostedDevice(current, claim, operation);
2066
1883
  settled = outcome.settled;
1884
+ if (!settled) throw new WorkspaceProcessCleanupError([claim], outcome.notice ?? "Hosted cleanup could not be verified.", "Verify the hosted worker and its descendants have stopped.");
1885
+ operation?.signal.throwIfAborted();
2067
1886
  const result = isJsonObject(outcome.value) ? outcome.value : null;
2068
1887
  const stopped = result && parseHostedPlatformDevice(result.device, device.platform);
2069
- if (!settled || outcome.notice || result?.state !== "stopped" || !stopped || hostedDeviceId(stopped) !== device.id) throw new Error(outcome.notice ?? (typeof result?.notice === "string" ? result.notice : "Hosted deletion could not be verified."));
2070
- console.log(`Deleted ${label}`);
2071
- recordGcResult("parkedHostedDevice", "done", label, { id: device.id });
1888
+ if (outcome.notice || result?.state !== "stopped" || !stopped || hostedDeviceId(stopped) !== device.id) throw new Error(outcome.notice ?? (typeof result?.notice === "string" ? result.notice : "Hosted deletion could not be verified."));
1889
+ output.log(`Deleted ${label}`);
1890
+ output.record("parkedHostedDevice", "done", label, { id: device.id });
2072
1891
  } catch (error) {
1892
+ if (claim && (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error)) {
1893
+ settled = false;
1894
+ throw Object.assign(new WorkspaceProcessCleanupError([.../* @__PURE__ */ new Set([claim, ...error instanceof WorkspaceProcessCleanupError ? error.claims : []])], error.message, "Verify the hosted device operation and its descendants have stopped."), { reportCleanup: true });
1895
+ }
1896
+ operation?.signal.throwIfAborted();
2073
1897
  const detail = describeError(error);
2074
- console.error(`Failed to delete ${label}: ${detail}`);
2075
- recordGcResult("parkedHostedDevice", "failed", label, {
1898
+ output.error(`Failed to delete ${label}: ${detail}`);
1899
+ output.record("parkedHostedDevice", "failed", label, {
2076
1900
  id: device.id,
2077
1901
  detail
2078
1902
  });
@@ -2085,15 +1909,27 @@ async function deleteParkedHostedDevices(devices, { listAvds: list = listAvds }
2085
1909
  }
2086
1910
  //#endregion
2087
1911
  //#region src/commands/gc.ts
2088
- function removeInvalidProjectEntries(invalidProjects) {
1912
+ function readGcReport(options = {}, deps = {}) {
1913
+ return sweep({
1914
+ olderThan: options.olderThan,
1915
+ cache: options.cache,
1916
+ worktrees: options.worktrees,
1917
+ json: true
1918
+ }, deps, {
1919
+ output: GC_OUTPUT,
1920
+ takeResults: () => []
1921
+ }, true);
1922
+ }
1923
+ function removeInvalidProjectEntries(invalidProjects, output, signal) {
2089
1924
  for (const path of invalidProjects) {
1925
+ signal?.throwIfAborted();
2090
1926
  removeProject(path);
2091
- console.log(chalk.green(`Removed the invalid registry entry ${path}`));
2092
- recordGcResult("project", "done", path);
1927
+ output.log(chalk.green(`Removed the invalid registry entry ${path}`));
1928
+ output.record("project", "done", path);
2093
1929
  }
2094
1930
  }
2095
- function collectGcMemory(scope) {
2096
- return memorySweepIsScoped() ? Promise.resolve(scopedMemoryReport()) : collectMemoryReport(scope, true);
1931
+ function collectGcMemory(scope, env, operation) {
1932
+ return memorySweepIsScoped(env) ? Promise.resolve(scopedMemoryReport()) : collectMemoryReport(scope, true, operation);
2097
1933
  }
2098
1934
  function classifyProjectEntries(cfg, mountedVolumes) {
2099
1935
  const deadProjects = [];
@@ -2125,23 +1961,28 @@ function classifyProjectEntries(cfg, mountedVolumes) {
2125
1961
  };
2126
1962
  }
2127
1963
  /** The workspace directories an unscoped run removes whole: those of dead projects and orphaned directories. */
2128
- function removedWholeWorkspaceDirs() {
1964
+ async function removedWholeWorkspaceDirs(operation) {
2129
1965
  const mountedVolumes = listMountedVolumes();
2130
1966
  const cfg = loadConfig$1();
2131
1967
  const { deadProjects } = classifyProjectEntries(cfg, mountedVolumes);
2132
- const { orphaned } = collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes, { measure: false });
1968
+ const { orphaned } = await collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes, {
1969
+ measure: false,
1970
+ operation
1971
+ });
2133
1972
  return [...orphaned.map((entry) => entry.dir), ...deadProjects.map(workspaceDir)];
2134
1973
  }
2135
- async function collectGcReport({ olderThan = null, cache = null, worktrees = false, now = Date.now(), lastTouched = workspaceLastUsed } = {}, deps = {}) {
1974
+ async function collectGcReport({ olderThan = null, cache = null, worktrees = false, refresh = true, now = Date.now(), lastTouched = workspaceLastUsed } = {}, deps = {}) {
1975
+ const operation = deps.operation;
1976
+ const env = operation?.env ?? process.env;
2136
1977
  const scope = typeof cache === "string" && cache.trim() ? cache : null;
2137
1978
  const all = scope !== null && olderThan === null;
2138
1979
  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);
1980
+ const selected = selectCaches(discoverCaches(env, operation?.cwd), scope).filter((c) => !withWorkspaces || !isInsideWorkspaces(c.dir));
1981
+ if (selected.length) phase("caches", `measuring ${plural(selected.length, "shared cache")}`, operation);
1982
+ const sized = await sizeCaches(selected, operation);
1983
+ const caches = scope === null ? previewScopedEmptying(sized, env) : planCacheEmptying(sized, all, env);
2143
1984
  if (scope) return {
2144
- archives: includesArchives(scope) ? collectArchives(scope, olderThan, now) : void 0,
1985
+ archives: includesArchives(scope) ? await collectArchives(scope, olderThan, now, operation?.signal) : void 0,
2145
1986
  skipped: [],
2146
1987
  deadProjects: [],
2147
1988
  invalidProjects: [],
@@ -2149,6 +1990,8 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2149
1990
  orphanedDevices: [],
2150
1991
  unverifiedDevices: [],
2151
1992
  staleDevices: [],
1993
+ finishedDevices: [],
1994
+ ownedDeviceDisk: [],
2152
1995
  staleDeviceRecords: [],
2153
1996
  staleLedgerEntries: [],
2154
1997
  staleEasLedgerEntries: [],
@@ -2175,45 +2018,65 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2175
2018
  notices: [],
2176
2019
  deletionSafe: true
2177
2020
  },
2178
- parkedSims: includesParkedDevices(scope) ? collectParkedSims(deps, {
2021
+ parkedSims: includesParkedDevices(scope) ? await collectParkedSims(deps, {
2179
2022
  olderThanDays: olderThan,
2180
2023
  now
2181
2024
  }) : [],
2182
- parkedAvds: includesParkedDevices(scope) ? collectParkedAvds(deps, {
2025
+ parkedAvds: includesParkedDevices(scope) ? await collectParkedAvds(deps, {
2183
2026
  olderThanDays: olderThan,
2184
2027
  now
2185
2028
  }) : [],
2186
2029
  parkedHostedDevices: [],
2187
2030
  caches,
2188
- workspaceOutputs: withWorkspaces ? collectWorkspaceOutputs({
2031
+ workspaceOutputs: withWorkspaces ? await collectWorkspaceOutputs({
2189
2032
  olderThan,
2190
2033
  now,
2191
- exclude: isWorkspacesScope(scope) ? removedWholeWorkspaceDirs() : []
2034
+ exclude: isWorkspacesScope(scope) ? await removedWholeWorkspaceDirs(operation) : [],
2035
+ signal: operation?.signal,
2036
+ operation
2192
2037
  }) : null,
2193
2038
  workspaceLogs: [],
2194
- recordings: includesRecordings(scope) ? collectRecordings({
2039
+ recordings: includesRecordings(scope) ? await collectRecordings({
2195
2040
  whole: all,
2196
2041
  olderThan,
2197
2042
  now
2198
- }) : [],
2043
+ }, [], operation?.signal) : [],
2199
2044
  worktreeSweep: null,
2200
- memory: memoryCacheKind(scope) ? await collectGcMemory(memoryScope(scope)) : null,
2045
+ memory: memoryCacheKind(scope) ? await collectGcMemory(memoryScope(scope), env, operation) : null,
2201
2046
  agentDeviceUsage: null,
2202
2047
  swiftpmCacheUsage: null,
2048
+ machineCaches: null,
2049
+ tempLeftovers: null,
2050
+ gradleVersions: includesGradleVersions(scope) ? await collectGradleVersions({
2051
+ olderThanDays: olderThan,
2052
+ now,
2053
+ operation
2054
+ }) : null,
2203
2055
  cacheScope: scope,
2204
2056
  olderThan,
2205
2057
  all
2206
2058
  };
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();
2059
+ phase("agent-device", "measuring report-only state", operation);
2060
+ const agentDeviceUsage = await getAgentDeviceUsage({ operation });
2061
+ phase("SwiftPM cache", "measuring report-only state", operation);
2062
+ const swiftpmCacheUsage = await getSwiftpmCacheUsage({ operation });
2063
+ phase("machine caches", "measuring report-only state", operation);
2064
+ const machineCaches = await getMachineCacheReport(agentDeviceUsage, { operation });
2065
+ if (!deviceSweepIsScoped(env)) phase("TMPDIR", "checking recognized leftovers", operation);
2066
+ const tempLeftovers = await collectTempLeftovers({
2067
+ now,
2068
+ env,
2069
+ signal: operation?.signal,
2070
+ operation
2071
+ });
2211
2072
  const mountedVolumes = listMountedVolumes();
2212
2073
  const cfg = loadConfig$1();
2213
2074
  let easSessionSweep = deps.precollectedEasSessionSweep;
2214
2075
  if (!easSessionSweep) try {
2215
- easSessionSweep = collectEasSessionSweep(cfg, deps);
2076
+ easSessionSweep = await collectEasSessionSweep(cfg, deps);
2216
2077
  } catch (error) {
2078
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
2079
+ operation?.signal.throwIfAborted();
2217
2080
  easSessionSweep = {
2218
2081
  projectScope: null,
2219
2082
  orphaned: [],
@@ -2221,16 +2084,16 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2221
2084
  deletionSafe: false
2222
2085
  };
2223
2086
  }
2224
- const parkedSims = collectParkedSims(deps, {
2087
+ const parkedSims = await collectParkedSims(deps, {
2225
2088
  olderThanDays: olderThan,
2226
2089
  now
2227
2090
  });
2228
- const parkedAvds = collectParkedAvds(deps, {
2091
+ const parkedAvds = await collectParkedAvds(deps, {
2229
2092
  olderThanDays: olderThan,
2230
2093
  now
2231
2094
  });
2232
2095
  const { deadProjects, invalidProjects, skipped } = classifyProjectEntries(cfg, mountedVolumes);
2233
- const workspaceDirs = collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes);
2096
+ const workspaceDirs = await collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes, { operation });
2234
2097
  skipped.push(...workspaceDirs.skipped);
2235
2098
  const statusCaches = collectStaleStatusCaches(mountedVolumes);
2236
2099
  skipped.push(...statusCaches.skipped);
@@ -2245,38 +2108,74 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2245
2108
  let staleDeviceRecords = [];
2246
2109
  let staleLedgerEntries = staleBrowserProfiles(readCreatedDevices$1());
2247
2110
  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");
2111
+ let listed = null;
2112
+ let deviceDisk = [];
2113
+ const avdSizes = /* @__PURE__ */ new Map();
2114
+ const avdSize = (dir, options) => {
2115
+ let bytes = avdSizes.get(dir);
2116
+ if (bytes === void 0) avdSizes.set(dir, bytes = Promise.resolve((deps.directorySize ?? directorySizeAsync)(dir, {
2117
+ ...options,
2118
+ operation,
2119
+ signal: operation?.signal
2120
+ })));
2121
+ return bytes;
2122
+ };
2123
+ const unsweepableReason = cfg === null ? "no Stim config found" : deviceSweepIsScoped(env) ? "STIM_HOME scopes this config, but simulators and AVDs are machine-global" : null;
2124
+ phase("devices", "listing simulators and emulators", operation);
2250
2125
  if (unsweepableReason) {
2251
2126
  let simNames = [];
2252
2127
  let avdNames = [];
2253
2128
  try {
2254
- simNames = listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS }).map((s) => s.name);
2255
- } catch {}
2129
+ simNames = (await listAllIosSimsAsync({
2130
+ timeoutMs: DEVICE_LIST_TIMEOUT_MS,
2131
+ operation,
2132
+ signal: operation?.signal
2133
+ })).map((s) => s.name);
2134
+ } catch (error) {
2135
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
2136
+ operation?.signal.throwIfAborted();
2137
+ }
2256
2138
  try {
2257
- avdNames = listAvds({ timeoutMs: DEVICE_LIST_TIMEOUT_MS });
2258
- } catch {}
2139
+ avdNames = await listAvdsAsync({
2140
+ timeoutMs: DEVICE_LIST_TIMEOUT_MS,
2141
+ operation,
2142
+ signal: operation?.signal
2143
+ });
2144
+ } catch (error) {
2145
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
2146
+ operation?.signal.throwIfAborted();
2147
+ }
2259
2148
  deviceSweepNotices.push(...describeUnverifiableDevices(simNames, avdNames, { reason: unsweepableReason }));
2260
2149
  } else {
2261
2150
  let sims = [];
2262
2151
  let simsChecked = true;
2263
2152
  const ledger = readCreatedDevices$1();
2264
2153
  try {
2265
- const allSims = listAllIosSims({
2154
+ const allSims = await listAllIosSimsAsync({
2266
2155
  timeoutMs: DEVICE_LIST_TIMEOUT_MS,
2267
- includeUnavailable: true
2156
+ includeUnavailable: true,
2157
+ operation,
2158
+ signal: operation?.signal
2268
2159
  });
2269
2160
  staleLedgerEntries = [...staleLedgerEntries, ...findStaleLedgerEntries(ledger, allSims)];
2270
2161
  sims = allSims.filter((sim) => sim.available);
2271
- } catch {
2162
+ } catch (error) {
2163
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
2164
+ operation?.signal.throwIfAborted();
2272
2165
  simsChecked = false;
2273
2166
  deviceSweepNotices.push(`ios device sweep skipped: simulator tooling did not answer within ${DEVICE_LIST_TIMEOUT_MS / 1e3}s`);
2274
2167
  }
2275
2168
  let avds = [];
2276
2169
  let avdsChecked = true;
2277
2170
  try {
2278
- avds = listAvds({ timeoutMs: DEVICE_LIST_TIMEOUT_MS });
2279
- } catch {
2171
+ avds = await listAvdsAsync({
2172
+ timeoutMs: DEVICE_LIST_TIMEOUT_MS,
2173
+ operation,
2174
+ signal: operation?.signal
2175
+ });
2176
+ } catch (error) {
2177
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
2178
+ operation?.signal.throwIfAborted();
2280
2179
  avdsChecked = false;
2281
2180
  deviceSweepNotices.push(`android device sweep skipped: emulator tooling did not answer within ${DEVICE_LIST_TIMEOUT_MS / 1e3}s`);
2282
2181
  }
@@ -2302,14 +2201,31 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2302
2201
  const directories = device.kind === "android" && !registeredAvds.has(device.id) ? orphanedAvdDirectories.filter((entry) => entry.name === device.id) : [];
2303
2202
  return directories.length ? directories.map(({ directory }) => Object.assign({}, device, { directory })) : [device];
2304
2203
  });
2305
- orphanedDevices = withAndroidAvdSizes(found.orphaned.flatMap((device) => {
2204
+ orphanedDevices = await withAndroidAvdSizes(found.orphaned.flatMap((device) => {
2306
2205
  const directories = device.kind === "android" ? orphanedAvdDirectories.filter((entry) => entry.name === device.name) : [];
2307
2206
  return directories.length ? directories.map((orphanedDirectory) => Object.assign({}, device, { orphanedDirectory })) : [device];
2308
2207
  }), {
2309
2208
  avdDirectory: deps.avdDirectory ?? ownedAvdDirectory,
2310
- size: deps.directorySize ?? directorySize
2209
+ size: avdSize,
2210
+ operation
2311
2211
  });
2312
- idleDevices = collectIdleDevices(cfg, sims, deadProjects, now);
2212
+ idleDevices = operation ? await collectIdleDevicesAsync(cfg, sims, deadProjects, now, env, operation.signal, operation) : collectIdleDevices(cfg, sims, deadProjects, now);
2213
+ if (simsChecked || avdsChecked) {
2214
+ const lists = {
2215
+ sims,
2216
+ avds: [...registeredAvds]
2217
+ };
2218
+ listed = lists;
2219
+ let unmeasured = 0;
2220
+ deviceDisk = ownedDeviceDisk(await ownedDeviceUsage((_target, workspace) => {
2221
+ if (workspace) unmeasured++;
2222
+ }, {
2223
+ sims: () => lists.sims,
2224
+ avds: () => lists.avds,
2225
+ avdDirectory: deps.avdDirectory ?? ownedAvdDirectory,
2226
+ size: async (dir) => await avdSize(dir, { timeoutMs: 5e3 }) || null
2227
+ }), { android: unmeasured });
2228
+ }
2313
2229
  staleDeviceRecords = findStaleDeviceRecords({
2314
2230
  config: cfg,
2315
2231
  sims,
@@ -2318,7 +2234,7 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2318
2234
  simsChecked,
2319
2235
  avdsChecked
2320
2236
  });
2321
- if (olderThan !== null) staleDevices = withAndroidAvdSizes(findStaleProjectDevices({
2237
+ if (olderThan !== null) staleDevices = await withAndroidAvdSizes(findStaleProjectDevices({
2322
2238
  config: cfg,
2323
2239
  sims,
2324
2240
  avds,
@@ -2328,13 +2244,21 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2328
2244
  deadProjects
2329
2245
  }), {
2330
2246
  avdDirectory: deps.avdDirectory ?? ownedAvdDirectory,
2331
- size: deps.directorySize ?? directorySize
2247
+ size: avdSize,
2248
+ operation
2332
2249
  });
2333
2250
  }
2334
2251
  const locks = listBuildLocks();
2335
2252
  const slots = listBuildSlots();
2336
2253
  const goneWorkspaceDirs = [...workspaceDirs.orphaned.map((entry) => entry.dir), ...deadProjects.map(workspaceDir)];
2337
2254
  const deviceLeases = collectDeviceLeases(now);
2255
+ const worktreeSweep = await measureWorktreeSweep(await collectWorktreeSweep({
2256
+ idle: worktrees,
2257
+ olderThan,
2258
+ now,
2259
+ refresh,
2260
+ operation
2261
+ }), operation);
2338
2262
  return {
2339
2263
  skipped,
2340
2264
  deadProjects,
@@ -2370,58 +2294,79 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2370
2294
  deviceSweepNotices,
2371
2295
  easSessionSweep,
2372
2296
  caches,
2373
- workspaceOutputs: collectWorkspaceOutputs({
2297
+ workspaceOutputs: await collectWorkspaceOutputs({
2374
2298
  olderThan,
2375
2299
  now,
2376
- exclude: goneWorkspaceDirs
2300
+ exclude: goneWorkspaceDirs,
2301
+ signal: operation?.signal,
2302
+ operation
2303
+ }),
2304
+ workspaceLogs: await collectWorkspaceLogs({
2305
+ exclude: goneWorkspaceDirs,
2306
+ operation
2377
2307
  }),
2378
- workspaceLogs: collectWorkspaceLogs({ exclude: goneWorkspaceDirs }),
2379
- recordings: collectRecordings({
2308
+ recordings: await collectRecordings({
2380
2309
  whole: false,
2381
2310
  olderThan,
2382
2311
  now
2383
- }, goneWorkspaceDirs),
2384
- worktreeSweep: await collectWorktreeSweep({
2385
- idle: worktrees,
2386
- olderThan,
2312
+ }, goneWorkspaceDirs, operation?.signal),
2313
+ worktreeSweep,
2314
+ finishedDevices: listed ? collectFinishedDevices({
2315
+ config: cfg,
2316
+ ...listed,
2317
+ sweep: worktreeSweep,
2387
2318
  now
2388
- }),
2319
+ }) : [],
2320
+ ownedDeviceDisk: deviceDisk,
2389
2321
  memory: await collectGcMemory({
2390
2322
  watchman: true,
2391
2323
  gradle: true
2392
- }),
2324
+ }, env, operation),
2393
2325
  agentDeviceUsage,
2394
2326
  swiftpmCacheUsage,
2327
+ machineCaches,
2328
+ tempLeftovers,
2395
2329
  cacheScope: null,
2396
2330
  olderThan,
2397
2331
  all
2398
2332
  };
2399
2333
  }
2400
2334
  async function runGc(opts = {}, deps = {}) {
2401
- if (!opts.json) {
2402
- await maintenanceGuardedSweep(opts, deps);
2403
- return;
2404
- }
2405
- const log = console.log;
2406
- console.log = console.error;
2407
- let payload;
2335
+ let result;
2408
2336
  try {
2409
- payload = await maintenanceGuardedSweep(opts, deps);
2410
- } finally {
2411
- console.log = log;
2337
+ result = await withGcMaintenanceClaim(opts, (claim) => executeGc(opts, deps, (stream, line) => {
2338
+ if (stream === "stderr" || opts.json) console.error(line);
2339
+ else console.log(line);
2340
+ }, claim));
2341
+ } catch (error) {
2342
+ const failure = (0, ownership_claim_exports.claimFailure)(error, opts.delete ? "stim gc --delete" : "stim gc --idle <duration>") ?? (deps.operation?.signal.aborted ? {
2343
+ code: "STIM_CANCELLED",
2344
+ message: "GC was cancelled.",
2345
+ remedy: "Retry the same GC command."
2346
+ } : null);
2347
+ if (!failure) throw error;
2348
+ console.error(`${failure.code}: ${failure.message}`);
2349
+ console.error(failure.remedy);
2350
+ result = {
2351
+ payload: failure,
2352
+ exitCode: 1
2353
+ };
2412
2354
  }
2413
- console.log(JSON.stringify(payload));
2355
+ if (result.exitCode) process.exitCode = result.exitCode;
2356
+ if (opts.json) console.log(JSON.stringify(result.payload));
2414
2357
  }
2415
- async function maintenanceGuardedSweep(opts, deps) {
2416
- if (!opts.delete) return sweep(opts, deps);
2358
+ /** Runs GC under this home's exclusive maintenance claim, retaining it when owned cleanup is unresolved. */
2359
+ async function withGcMaintenanceClaim(opts, work) {
2360
+ if (!opts.delete && opts.idle === void 0) return work(void 0);
2417
2361
  let claim;
2362
+ let retained = false;
2418
2363
  try {
2419
2364
  const root = maintenanceRunClaims();
2420
2365
  const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
2421
2366
  root,
2422
2367
  mode: "exclusive",
2423
2368
  label: "maintenance",
2424
- details: { trigger: "gc --delete" }
2369
+ details: { trigger: opts.delete ? "gc --delete" : "gc --idle" }
2425
2370
  });
2426
2371
  if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
2427
2372
  claim = attempt.acquired;
@@ -2434,27 +2379,58 @@ async function maintenanceGuardedSweep(opts, deps) {
2434
2379
  reason: `held by pid ${holder?.owner.pid ?? "?"} since ${holder?.startedAt ?? "?"} (${String(holder?.details.trigger ?? "maintenance")})`
2435
2380
  });
2436
2381
  }
2437
- return await sweep(opts, deps);
2438
- } catch (error) {
2439
- const failure = (0, ownership_claim_exports.claimFailure)(error, "stim gc --delete");
2440
- if (!failure) throw error;
2441
- console.error(`${failure.code}: ${failure.message}`);
2442
- console.error(failure.remedy);
2443
- process.exitCode = 1;
2444
- return failure;
2382
+ return await work(claim);
2383
+ } catch (cause) {
2384
+ if (claim && (cause instanceof WorkspaceProcessCleanupError || cause instanceof Error && "reportCleanup" in cause)) {
2385
+ retained = true;
2386
+ throw Object.assign(new WorkspaceProcessCleanupError([.../* @__PURE__ */ new Set([claim, ...cause instanceof WorkspaceProcessCleanupError ? cause.claims : []])], cause.message, "Verify GC and its owned children have stopped."), { reportCleanup: true });
2387
+ }
2388
+ throw cause;
2389
+ } finally {
2390
+ if (claim && !retained) (0, ownership_claim_exports.releaseClaim)(claim);
2391
+ }
2392
+ }
2393
+ /** Executes one already-admitted sweep with separate progress, results and exit status. */
2394
+ async function executeGc(opts, deps, write, claim) {
2395
+ const results = createGcResults();
2396
+ let failed = false;
2397
+ const output = {
2398
+ log: (line) => write("stdout", line),
2399
+ error: (line) => write("stderr", line),
2400
+ record: results.record,
2401
+ fail: () => {
2402
+ failed = true;
2403
+ }
2404
+ };
2405
+ try {
2406
+ declareSpawnsOn(deps.operation, claim);
2407
+ return {
2408
+ payload: await sweep(opts, deps, {
2409
+ output,
2410
+ takeResults: results.take
2411
+ }),
2412
+ exitCode: failed ? 1 : 0
2413
+ };
2445
2414
  } finally {
2446
- if (claim) (0, ownership_claim_exports.releaseClaim)(claim);
2415
+ stopDeclaringSpawnsOn(deps.operation, claim);
2447
2416
  }
2448
2417
  }
2449
- async function sweep(opts, deps) {
2418
+ async function sweep(opts, deps, execution, reportOnly = false) {
2419
+ const { output } = execution;
2420
+ const core = (dependencies) => runGcCore(opts, dependencies, execution, reportOnly);
2450
2421
  if (opts.cache && (opts.worktrees || opts.idle !== void 0)) {
2451
2422
  const flag = opts.worktrees ? "--worktrees" : "--idle";
2452
2423
  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
2424
  const remedy = `Run \`stim gc ${flag}${opts.worktrees ? "" : " <duration>"}\` and \`stim gc --cache <name>\` separately.`;
2454
- console.error(chalk.red(message));
2455
- console.error(chalk.dim(remedy));
2456
- console.error(chalk.red("failed: STIM_BAD_ARG"));
2457
- process.exitCode = 1;
2425
+ if (reportOnly) return {
2426
+ code: "STIM_BAD_ARG",
2427
+ message,
2428
+ remedy
2429
+ };
2430
+ output.error(chalk.red(message));
2431
+ output.error(chalk.dim(remedy));
2432
+ output.error(chalk.red("failed: STIM_BAD_ARG"));
2433
+ output.fail();
2458
2434
  return {
2459
2435
  code: "STIM_BAD_ARG",
2460
2436
  message,
@@ -2465,10 +2441,15 @@ async function sweep(opts, deps) {
2465
2441
  if (memoryKind && opts.olderThan !== void 0) {
2466
2442
  const message = `--older-than does not apply to --cache ${memoryKind}, which acts on running processes.`;
2467
2443
  const remedy = `Run \`stim gc --cache ${memoryKind}\` without --older-than.`;
2468
- console.error(chalk.red(message));
2469
- console.error(chalk.dim(remedy));
2470
- console.error(chalk.red("failed: STIM_BAD_ARG"));
2471
- process.exitCode = 1;
2444
+ if (reportOnly) return {
2445
+ code: "STIM_BAD_ARG",
2446
+ message,
2447
+ remedy
2448
+ };
2449
+ output.error(chalk.red(message));
2450
+ output.error(chalk.dim(remedy));
2451
+ output.error(chalk.red("failed: STIM_BAD_ARG"));
2452
+ output.fail();
2472
2453
  return {
2473
2454
  code: "STIM_BAD_ARG",
2474
2455
  message,
@@ -2476,8 +2457,11 @@ async function sweep(opts, deps) {
2476
2457
  };
2477
2458
  }
2478
2459
  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, {
2460
+ if (poolError) {
2461
+ if (reportOnly) phase("note", `${poolError} ${POOL_SETTING_REMEDY}`, deps.operation);
2462
+ else output.error(chalk.yellow(`${poolError} ${POOL_SETTING_REMEDY}`));
2463
+ }
2464
+ if (opts.cache) return core({
2481
2465
  ...deps,
2482
2466
  precollectedEasSessionSweep: {
2483
2467
  projectScope: null,
@@ -2488,8 +2472,8 @@ async function sweep(opts, deps) {
2488
2472
  });
2489
2473
  let projectRoot = null;
2490
2474
  try {
2491
- projectRoot = (deps.findProjectRoot ?? findProjectRoot)(process.cwd());
2492
- if (!projectRoot) return runGcCore(opts, {
2475
+ projectRoot = (deps.findProjectRoot ?? findProjectRoot$1)(deps.operation?.cwd ?? process.cwd());
2476
+ if (!projectRoot) return core({
2493
2477
  ...deps,
2494
2478
  precollectedEasSessionSweep: {
2495
2479
  projectScope: null,
@@ -2498,7 +2482,7 @@ async function sweep(opts, deps) {
2498
2482
  deletionSafe: false
2499
2483
  }
2500
2484
  });
2501
- if (!(deps.detectIsExpo ?? detectIsExpo)(projectRoot)) return runGcCore(opts, {
2485
+ if (!(deps.detectIsExpo ?? detectIsExpo)(projectRoot)) return core({
2502
2486
  ...deps,
2503
2487
  precollectedEasSessionSweep: {
2504
2488
  projectScope: projectRoot,
@@ -2508,7 +2492,7 @@ async function sweep(opts, deps) {
2508
2492
  }
2509
2493
  });
2510
2494
  } catch (error) {
2511
- return runGcCore(opts, {
2495
+ return core({
2512
2496
  ...deps,
2513
2497
  precollectedEasSessionSweep: {
2514
2498
  projectScope: projectRoot,
@@ -2524,13 +2508,19 @@ async function sweep(opts, deps) {
2524
2508
  try {
2525
2509
  easSessionSweep = await withProjectLock(projectRoot, () => {
2526
2510
  sweepStarted = true;
2527
- return withRemoteSessionGcLocks(projectRoot, deps, (coordinatedDeps) => Promise.resolve(collectEasSessionSweep(loadConfig$1(), coordinatedDeps)));
2511
+ return withRemoteSessionGcLocks(projectRoot, deps, (coordinatedDeps) => collectEasSessionSweep(loadConfig$1(), coordinatedDeps));
2528
2512
  }, {
2529
2513
  waitMs: 0,
2530
2514
  ownerPurpose: "EAS orphan sweep",
2531
- machineRoot: deps.easLedgerRoot
2515
+ machineRoot: deps.easLedgerRoot,
2516
+ ...deps.operation ? {
2517
+ signal: deps.operation.signal,
2518
+ declareSpawnsFor: deps.operation
2519
+ } : {}
2532
2520
  });
2533
2521
  } catch (error) {
2522
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
2523
+ deps.operation?.signal.throwIfAborted();
2534
2524
  easSessionSweep = {
2535
2525
  projectScope: projectRoot,
2536
2526
  orphaned: [],
@@ -2538,49 +2528,53 @@ async function sweep(opts, deps) {
2538
2528
  deletionSafe: false
2539
2529
  };
2540
2530
  }
2541
- return runGcCore(opts, {
2531
+ return core({
2542
2532
  ...deps,
2543
2533
  precollectedEasSessionSweep: easSessionSweep
2544
2534
  });
2545
2535
  }
2546
- async function pruneDeadProjects(deadProjects) {
2536
+ async function pruneDeadProjects(deadProjects, output, operation) {
2547
2537
  let deleteFailures = 0;
2548
2538
  for (const path of deadProjects) {
2539
+ operation?.signal.throwIfAborted();
2549
2540
  if (existsSync$1(path) || !isOnMountedVolume(path)) {
2550
- console.log(chalk.yellow(`Kept ${path}: its absence can no longer be confirmed.`));
2551
- recordGcResult("project", "kept", path, { detail: "its absence can no longer be confirmed" });
2541
+ output.log(chalk.yellow(`Kept ${path}: its absence can no longer be confirmed.`));
2542
+ output.record("project", "kept", path, { detail: "its absence can no longer be confirmed" });
2552
2543
  continue;
2553
2544
  }
2554
2545
  const result = await reclaimProject(path, {
2555
2546
  archive: { removedBy: "gc" },
2556
- stopRequester: { by: "stim gc --delete" }
2547
+ stopRequester: { by: "stim gc --delete" },
2548
+ operation
2557
2549
  }).catch((error) => {
2550
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
2551
+ operation?.signal.throwIfAborted();
2558
2552
  deleteFailures++;
2559
- console.log(chalk.red(`Could not prune ${path}; its registry entry was kept: ${error.message}`));
2560
- recordGcResult("project", "failed", path, { detail: `its registry entry was kept: ${error.message}` });
2553
+ output.log(chalk.red(`Could not prune ${path}; its registry entry was kept: ${error.message}`));
2554
+ output.record("project", "failed", path, { detail: `its registry entry was kept: ${error.message}` });
2561
2555
  return null;
2562
2556
  });
2563
2557
  if (!result) continue;
2564
2558
  if (result.keptEntry) {
2565
- console.log(chalk.yellow(`Could not fully prune ${path}; its registry entry was kept.`));
2566
- recordGcResult("project", "kept", path, { detail: "it could not be fully pruned; its registry entry was kept" });
2559
+ output.log(chalk.yellow(`Could not fully prune ${path}; its registry entry was kept.`));
2560
+ output.record("project", "kept", path, { detail: "it could not be fully pruned; its registry entry was kept" });
2567
2561
  } else {
2568
- console.log(chalk.green(`Pruned ${path}`));
2569
- recordGcResult("project", "done", path);
2562
+ output.log(chalk.green(`Pruned ${path}`));
2563
+ output.record("project", "done", path);
2570
2564
  }
2571
- for (const dir of result.removedWorkspaceDirs) console.log(chalk.dim(` removed workspace output ${dir}`));
2565
+ for (const dir of result.removedWorkspaceDirs) output.log(chalk.dim(` removed workspace output ${dir}`));
2572
2566
  for (const dir of result.failedWorkspaceDirs) {
2573
- console.log(chalk.red(` could not remove workspace output ${dir}`));
2574
- recordGcResult("workspaceDirectory", "failed", dir, { detail: `could not remove it while pruning ${path}` });
2567
+ output.log(chalk.red(` could not remove workspace output ${dir}`));
2568
+ output.record("workspaceDirectory", "failed", dir, { detail: `could not remove it while pruning ${path}` });
2575
2569
  deleteFailures += 1;
2576
2570
  }
2577
- if (result.killedPid) console.log(chalk.dim(` killed orphaned Metro pid ${result.killedPid}`));
2578
- if (result.stoppedSession) console.log(chalk.dim(` stopped remote session ${result.stoppedSession}`));
2579
- if (result.stoppedTunnel) console.log(chalk.dim(` stopped ${result.stoppedTunnel} tunnel`));
2580
- for (const name of result.deletedDevices) recordGcResult("device", "done", name);
2571
+ if (result.killedPid) output.log(chalk.dim(` killed orphaned Metro pid ${result.killedPid}`));
2572
+ if (result.stoppedSession) output.log(chalk.dim(` stopped remote session ${result.stoppedSession}`));
2573
+ if (result.stoppedTunnel) output.log(chalk.dim(` stopped ${result.stoppedTunnel} tunnel`));
2574
+ for (const name of result.deletedDevices) output.record("device", "done", name);
2581
2575
  for (const s of result.skippedDevices) {
2582
- console.log(chalk.yellow(` ${s.name}: ${s.reason}`));
2583
- recordGcResult("device", result.failedDevices.includes(s) ? "failed" : "kept", s.name, {
2576
+ output.log(chalk.yellow(` ${s.name}: ${s.reason}`));
2577
+ output.record("device", result.failedDevices.includes(s) ? "failed" : "kept", s.name, {
2584
2578
  id: s.udid ?? null,
2585
2579
  detail: s.reason
2586
2580
  });
@@ -2589,13 +2583,18 @@ async function pruneDeadProjects(deadProjects) {
2589
2583
  }
2590
2584
  return deleteFailures;
2591
2585
  }
2592
- async function attachMaintenancePreview(report, deleting, cache) {
2593
- if (!deleting) report.maintenance = await previewMaintenance({ devices: !cache });
2586
+ async function attachMaintenancePreview(report, deleting, cache, refresh, operation) {
2587
+ if (!deleting) report.maintenance = await previewMaintenance({
2588
+ devices: !cache,
2589
+ refresh,
2590
+ operation
2591
+ });
2594
2592
  }
2595
2593
  function scopedDryRunAction(cache) {
2596
2594
  const memory = memoryCacheKind(cache);
2597
2595
  if (memory === "watchman") return "remove the stale roots and shut down the unused watchman";
2598
2596
  if (memory === "gradle-daemons") return "stop the idle daemons";
2597
+ if (includesGradleVersions(cache)) return "delete the Gradle versions marked deletable";
2599
2598
  return includesParkedDevices(cache) ? "erase the parked devices" : "empty the caches";
2600
2599
  }
2601
2600
  function dryRunHint(cache, all, actionable, caches) {
@@ -2604,45 +2603,72 @@ function dryRunHint(cache, all, actionable, caches) {
2604
2603
  if (caches) return "Pass --delete --cache all to empty the caches above, or --delete --older-than <days> to trim them.";
2605
2604
  return null;
2606
2605
  }
2607
- function pruneStatusCaches(report) {
2606
+ function scopedWorkPending(cache, report) {
2607
+ return memoryWorkPending(cache, report.memory) || gradleVersionsPending(report.gradleVersions);
2608
+ }
2609
+ async function pruneStatusCaches(report, output, signal) {
2608
2610
  if (report.cacheScope !== null) return 0;
2609
- return deleteStaleStatusCaches(collectStaleStatusCaches(listMountedVolumes()).stale);
2611
+ return deleteStaleStatusCaches(collectStaleStatusCaches(listMountedVolumes()).stale, output, signal);
2612
+ }
2613
+ async function reclaimParkedDevices(report, deps, output) {
2614
+ return report.cacheScope ? await eraseParkedDevices(report.parkedSims, report.parkedAvds, output, deps.operation) : await deleteParkedSims(report.parkedSims, deps, output) + await deleteParkedAvds(report.parkedAvds, output, deps.operation);
2610
2615
  }
2611
- function reclaimParkedDevices(report, deps) {
2612
- return report.cacheScope ? eraseParkedDevices(report.parkedSims, report.parkedAvds) : deleteParkedSims(report.parkedSims, deps) + deleteParkedAvds(report.parkedAvds);
2616
+ function reportActionable(report) {
2617
+ const { deadProjects, invalidProjects, orphanedDevices, staleDevices, staleDeviceRecords, buildLocks, buildSlots, deviceLeases, easSessionSweep, caches, olderThan, all, cacheScope: cache } = report;
2618
+ 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));
2613
2619
  }
2614
- async function runGcCore(opts, deps) {
2615
- takeGcResults();
2620
+ async function runGcCore(opts, deps, execution, reportOnly = false) {
2621
+ const { output, takeResults } = execution;
2622
+ const signal = deps.operation?.signal;
2623
+ throwIfCancelled(signal, "GC");
2616
2624
  const olderThan = typeof opts.olderThan === "number" ? opts.olderThan : null;
2617
2625
  const cache = typeof opts.cache === "string" && opts.cache.trim() ? opts.cache : null;
2626
+ const refresh = opts.delete === true;
2618
2627
  const report = await collectGcReport({
2619
2628
  olderThan,
2620
2629
  cache,
2621
- worktrees: Boolean(opts.worktrees)
2630
+ worktrees: Boolean(opts.worktrees),
2631
+ refresh
2622
2632
  }, deps);
2633
+ throwIfCancelled(signal, "GC");
2623
2634
  if (cache && report.caches.length === 0 && report.workspaceOutputs === null && !selectsBeyondCaches(cache)) {
2624
- const names = [...new Set(discoverCaches().map((c) => c.name))];
2635
+ const names = [...new Set(discoverCaches(deps.operation?.env ?? process.env, deps.operation?.cwd).map((c) => c.name))];
2625
2636
  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;
2637
+ if (!reportOnly) {
2638
+ output.log(chalk.yellow(message));
2639
+ if (names.length) output.log(chalk.dim(`Caches on this machine: ${names.join(", ")}`));
2640
+ if (opts.json) output.fail();
2641
+ }
2629
2642
  return {
2630
2643
  code: "STIM_BAD_ARG",
2631
2644
  message,
2632
2645
  remedy: names.length ? `Pass --cache all or a name from: ${names.join(", ")}.` : "No shared cache was found on this machine."
2633
2646
  };
2634
2647
  }
2635
- await attachMaintenancePreview(report, opts.delete === true, cache);
2648
+ await attachMaintenancePreview(report, opts.delete === true, cache, refresh, deps.operation);
2636
2649
  const refusal = archiveRefusal(report.archives);
2637
- if (refusal !== null) return refusal;
2650
+ if (refusal !== null) {
2651
+ if (!reportOnly) {
2652
+ output.fail();
2653
+ output.error(chalk.red(refusal.message));
2654
+ output.error(chalk.dim(refusal.remedy));
2655
+ output.error(chalk.red(`failed: ${refusal.code}`));
2656
+ }
2657
+ return refusal;
2658
+ }
2638
2659
  const all = report.all;
2639
- for (const line of formatGcReport(report)) console.log(line);
2660
+ if (!reportOnly) for (const line of formatGcReport(report)) output.log(line);
2640
2661
  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));
2662
+ const actionable = reportActionable(report);
2642
2663
  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;
2644
- if (idleFailures) process.exitCode = 1;
2645
- const inventory = opts.json && !opts.delete && idle === null && report.cacheScope === null ? collectInventory(loadConfig$1(), deadProjects) : null;
2664
+ throwIfCancelled(signal, "GC");
2665
+ const idleFailures = idle === null ? 0 : (await shutDownIdleDevices(report.idleDevices, idle, async () => deps.operation ? collectIdleDevicesAsync(loadConfig$1(), await listAllIosSimsAsync({
2666
+ timeoutMs: DEVICE_LIST_TIMEOUT_MS,
2667
+ operation: deps.operation,
2668
+ signal
2669
+ }), report.deadProjects, Date.now(), deps.operation.env, signal, deps.operation) : collectIdleDevices(loadConfig$1(), listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS }), report.deadProjects), output, signal, deps.operation)).failures;
2670
+ if (idleFailures) output.fail();
2671
+ const inventory = opts.json && !opts.delete && idle === null && report.cacheScope === null ? await collectInventory(loadConfig$1(), deadProjects, deps.operation?.env ?? process.env, deps.operation) : null;
2646
2672
  const payload = (failures) => ({
2647
2673
  mode: opts.delete ? "delete" : "dry-run",
2648
2674
  idle,
@@ -2653,42 +2679,70 @@ async function runGcCore(opts, deps) {
2653
2679
  failures,
2654
2680
  sections: gcReportSections(report),
2655
2681
  ...report.maintenance ? { maintenance: report.maintenance } : {},
2656
- results: takeGcResults(),
2682
+ results: reportOnly ? [] : takeResults(),
2657
2683
  inventory
2658
2684
  });
2659
2685
  if (!opts.delete) {
2660
2686
  const hint = dryRunHint(cache, all, actionable, caches.length > 0);
2661
- if (hint) console.log(chalk.dim(`\n${hint}`));
2687
+ if (hint && !reportOnly) output.log(chalk.dim(`\n${hint}`));
2662
2688
  return payload(idle === null ? null : idleFailures);
2663
2689
  }
2664
2690
  let deleteFailures = idleFailures;
2665
- deleteFailures += deleteArchives(report.archives);
2666
- deleteFailures += report.workspaceOutputs ? (await clearWorkspaceOutputs(report.workspaceOutputs, { olderThan })).failures : 0;
2667
- deleteFailures += await trimWorkspaceLogs(report.workspaceLogs);
2668
- deleteFailures += deleteRecordings(report.recordings, {
2691
+ throwIfCancelled(signal, "GC");
2692
+ deleteFailures += await deleteArchives(report.archives, output, signal);
2693
+ throwIfCancelled(signal, "GC");
2694
+ deleteFailures += report.workspaceOutputs ? (await clearWorkspaceOutputs(report.workspaceOutputs, {
2695
+ olderThan,
2696
+ signal,
2697
+ operation: deps.operation
2698
+ }, output)).failures : 0;
2699
+ throwIfCancelled(signal, "GC");
2700
+ deleteFailures += await trimWorkspaceLogs(report.workspaceLogs, output, deps.operation);
2701
+ throwIfCancelled(signal, "GC");
2702
+ deleteFailures += await deleteRecordings(report.recordings, {
2669
2703
  whole: all,
2670
2704
  olderThan,
2671
2705
  now: Date.now()
2672
- });
2673
- deleteFailures += reclaimParkedDevices(report, deps);
2674
- deleteFailures += await deleteParkedHostedDevices(report.parkedHostedDevices);
2675
- deleteFailures += await reclaimMemory(cache, report.memory);
2676
- removeInvalidProjectEntries(invalidProjects);
2677
- deleteFailures += await pruneDeadProjects(deadProjects);
2678
- deleteFailures += await deleteOrphanedWorkspaces(report.orphanedWorkspaces);
2679
- deleteFailures += deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords);
2680
- deleteFailures += forgetStaleLedgerEntries(report.staleLedgerEntries);
2681
- deleteFailures += forgetStaleEasLedgerEntries(report.staleEasLedgerEntries, listMountedVolumes(), deps);
2682
- deleteFailures += pruneStatusCaches(report);
2706
+ }, output, signal);
2707
+ throwIfCancelled(signal, "GC");
2708
+ deleteFailures += await reclaimParkedDevices(report, deps, output);
2709
+ throwIfCancelled(signal, "GC");
2710
+ deleteFailures += await deleteParkedHostedDevices(report.parkedHostedDevices, { operation: deps.operation }, output);
2711
+ throwIfCancelled(signal, "GC");
2712
+ deleteFailures += await reclaimMemory(cache, report.memory, output, deps.operation);
2713
+ throwIfCancelled(signal, "GC");
2714
+ deleteFailures += await deleteGradleVersions(report.gradleVersions, output, deps.operation);
2715
+ throwIfCancelled(signal, "GC");
2716
+ removeInvalidProjectEntries(invalidProjects, output, signal);
2717
+ throwIfCancelled(signal, "GC");
2718
+ deleteFailures += await pruneDeadProjects(deadProjects, output, deps.operation);
2719
+ throwIfCancelled(signal, "GC");
2720
+ deleteFailures += await deleteOrphanedWorkspaces(report.orphanedWorkspaces, output, deps.operation);
2721
+ throwIfCancelled(signal, "GC");
2722
+ deleteFailures += await deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords, output, deps.operation);
2723
+ throwIfCancelled(signal, "GC");
2724
+ deleteFailures += await deleteFinishedDevices(report, output, deps.operation);
2725
+ throwIfCancelled(signal, "GC");
2726
+ deleteFailures += await forgetStaleLedgerEntries(report.staleLedgerEntries, output, signal);
2727
+ throwIfCancelled(signal, "GC");
2728
+ deleteFailures += await forgetStaleEasLedgerEntries(report.staleEasLedgerEntries, listMountedVolumes(), deps, output, signal);
2729
+ throwIfCancelled(signal, "GC");
2730
+ deleteFailures += await pruneStatusCaches(report, output, signal);
2731
+ if (report.tempLeftovers) deleteFailures += (await deleteTempLeftovers({
2732
+ env: deps.operation?.env ?? process.env,
2733
+ signal: deps.operation?.signal,
2734
+ operation: deps.operation
2735
+ }, output)).failures;
2683
2736
  for (const lock of buildLocks.stale) {
2737
+ throwIfCancelled(signal, "GC");
2684
2738
  const cleared = (0, ownership_claim_exports.clearFreeClaimSet)({
2685
2739
  root: lock.path,
2686
2740
  label: `${lock.platform} build`
2687
2741
  });
2688
2742
  if (cleared.status === "held") continue;
2689
2743
  if (cleared.status === "refused") {
2690
- console.log(chalk.yellow(`Left the build lock at ${lock.path} alone: ${cleared.reason}`));
2691
- recordGcResult("buildLock", "kept", `${lock.platform} build lock`, {
2744
+ output.log(chalk.yellow(`Left the build lock at ${lock.path} alone: ${cleared.reason}`));
2745
+ output.record("buildLock", "kept", `${lock.platform} build lock`, {
2692
2746
  id: lock.path,
2693
2747
  detail: cleared.reason
2694
2748
  });
@@ -2696,25 +2750,26 @@ async function runGcCore(opts, deps) {
2696
2750
  }
2697
2751
  if (cleared.status === "failed") {
2698
2752
  deleteFailures++;
2699
- console.log(chalk.red(`Failed to clear the build lock at ${lock.path}: ${cleared.reason}`));
2700
- recordGcResult("buildLock", "failed", `${lock.platform} build lock`, {
2753
+ output.log(chalk.red(`Failed to clear the build lock at ${lock.path}: ${cleared.reason}`));
2754
+ output.record("buildLock", "failed", `${lock.platform} build lock`, {
2701
2755
  id: lock.path,
2702
2756
  detail: cleared.reason
2703
2757
  });
2704
2758
  continue;
2705
2759
  }
2706
- recordGcResult("buildLock", "done", `${lock.platform} build lock`, { id: lock.path });
2707
- console.log(chalk.green(`Cleared the ${lock.platform} build lock left by pid ${lock.pid ?? "?"} (${lock.projectRoot || "unrecorded workspace"})`));
2760
+ output.record("buildLock", "done", `${lock.platform} build lock`, { id: lock.path });
2761
+ output.log(chalk.green(`Cleared the ${lock.platform} build lock left by pid ${lock.pid ?? "?"} (${lock.projectRoot || "unrecorded workspace"})`));
2708
2762
  }
2709
2763
  for (const slot of buildSlots.stale) {
2764
+ throwIfCancelled(signal, "GC");
2710
2765
  const cleared = (0, ownership_claim_exports.clearFreeClaimSet)({
2711
2766
  root: slot.path,
2712
2767
  label: "build slot"
2713
2768
  });
2714
2769
  if (cleared.status === "held") continue;
2715
2770
  if (cleared.status === "refused") {
2716
- console.log(chalk.yellow(`Left the build slot at ${slot.path} alone: ${cleared.reason}`));
2717
- recordGcResult("buildSlot", "kept", `build slot ${slot.index ?? "?"}`, {
2771
+ output.log(chalk.yellow(`Left the build slot at ${slot.path} alone: ${cleared.reason}`));
2772
+ output.record("buildSlot", "kept", `build slot ${slot.index ?? "?"}`, {
2718
2773
  id: slot.path,
2719
2774
  detail: cleared.reason
2720
2775
  });
@@ -2722,47 +2777,53 @@ async function runGcCore(opts, deps) {
2722
2777
  }
2723
2778
  if (cleared.status === "failed") {
2724
2779
  deleteFailures++;
2725
- console.log(chalk.red(`Failed to clear the build slot at ${slot.path}: ${cleared.reason}`));
2726
- recordGcResult("buildSlot", "failed", `build slot ${slot.index ?? "?"}`, {
2780
+ output.log(chalk.red(`Failed to clear the build slot at ${slot.path}: ${cleared.reason}`));
2781
+ output.record("buildSlot", "failed", `build slot ${slot.index ?? "?"}`, {
2727
2782
  id: slot.path,
2728
2783
  detail: cleared.reason
2729
2784
  });
2730
2785
  continue;
2731
2786
  }
2732
- recordGcResult("buildSlot", "done", `build slot ${slot.index ?? "?"}`, { id: slot.path });
2733
- console.log(chalk.green(`Cleared build slot ${slot.index ?? "?"} left by pid ${slot.pid ?? "?"} (${slot.projectRoot || "unrecorded workspace"})`));
2734
- }
2735
- for (const entry of deviceLeases.expired) try {
2736
- if (removeExpiredLease(entry)) {
2737
- recordGcResult("deviceLease", "done", `${entry.platform} lease on ${entry.id ?? entry.name}`, { id: entry.path });
2738
- console.log(chalk.green(`Cleared the expired ${entry.platform} device lease on ${entry.id ?? entry.name} (held by ${entry.lease?.holder ?? "an unrecorded workspace"})`));
2739
- } else console.log(chalk.dim(`${entry.name} is no longer an expired lease; left alone.`));
2740
- } catch (err) {
2741
- deleteFailures++;
2742
- console.log(chalk.red(`Failed to clear the device lease at ${entry.path}: ${err?.message || err}`));
2743
- recordGcResult("deviceLease", "failed", `${entry.platform} lease on ${entry.id ?? entry.name}`, {
2744
- id: entry.path,
2745
- detail: err?.message || String(err)
2746
- });
2787
+ output.record("buildSlot", "done", `build slot ${slot.index ?? "?"}`, { id: slot.path });
2788
+ output.log(chalk.green(`Cleared build slot ${slot.index ?? "?"} left by pid ${slot.pid ?? "?"} (${slot.projectRoot || "unrecorded workspace"})`));
2789
+ }
2790
+ for (const entry of deviceLeases.expired) {
2791
+ throwIfCancelled(signal, "GC");
2792
+ try {
2793
+ if (removeExpiredLease(entry)) {
2794
+ output.record("deviceLease", "done", `${entry.platform} lease on ${entry.id ?? entry.name}`, { id: entry.path });
2795
+ output.log(chalk.green(`Cleared the expired ${entry.platform} device lease on ${entry.id ?? entry.name} (held by ${entry.lease?.holder ?? "an unrecorded workspace"})`));
2796
+ } else output.log(chalk.dim(`${entry.name} is no longer an expired lease; left alone.`));
2797
+ } catch (err) {
2798
+ deleteFailures++;
2799
+ output.log(chalk.red(`Failed to clear the device lease at ${entry.path}: ${err?.message || err}`));
2800
+ output.record("deviceLease", "failed", `${entry.platform} lease on ${entry.id ?? entry.name}`, {
2801
+ id: entry.path,
2802
+ detail: err?.message || String(err)
2803
+ });
2804
+ }
2747
2805
  }
2748
- deleteFailures += await deleteEasSessions(easSessionSweep, deps);
2749
- if (report.worktreeSweep) deleteFailures += await removeWorktrees(report.worktreeSweep);
2806
+ throwIfCancelled(signal, "GC");
2807
+ deleteFailures += await deleteEasSessions(easSessionSweep, deps, output);
2808
+ throwIfCancelled(signal, "GC");
2809
+ if (report.worktreeSweep) deleteFailures += await removeWorktrees(report.worktreeSweep, { operation: deps.operation }, output);
2750
2810
  if (deleteFailures) {
2751
- console.log(chalk.red(`\n${deleteFailures} entr${deleteFailures === 1 ? "y" : "ies"} could not be deleted; see above.`));
2752
- process.exitCode = 1;
2811
+ output.log(chalk.red(`\n${deleteFailures} entr${deleteFailures === 1 ? "y" : "ies"} could not be deleted; see above.`));
2812
+ output.fail();
2753
2813
  }
2754
- if (all) emptyCaches(caches);
2814
+ throwIfCancelled(signal, "GC");
2815
+ if (all) await emptyCaches(caches, output, signal, deps.operation);
2755
2816
  else if (olderThan === null) {
2756
- if (caches.length) console.log(chalk.dim("Shared caches left alone: pass --cache all to empty them, or --older-than <days> to trim them."));
2757
- } else trimCaches(caches, olderThan);
2817
+ if (caches.length) output.log(chalk.dim("Shared caches left alone: pass --cache all to empty them, or --older-than <days> to trim them."));
2818
+ } else await trimCaches(caches, olderThan, output, signal, deps.operation);
2758
2819
  return payload(deleteFailures);
2759
2820
  }
2760
2821
  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) => {
2822
+ 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
2823
  const n = parseInt(v, 10);
2763
2824
  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
2825
  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) => {
2826
+ }).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
2827
  if (!v.trim()) throw new InvalidArgumentError("must name a cache, e.g. --cache \"compilation cache\"");
2767
2828
  return v;
2768
2829
  }).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) => {
@@ -2770,8 +2831,9 @@ function gcCommand(program) {
2770
2831
  if (ms === null) throw new InvalidArgumentError("must be a whole number of minutes, hours, or days, e.g. --idle 2h");
2771
2832
  return ms;
2772
2833
  }).option("--json", "print the report as JSON on stdout; every other line goes to stderr").action(async (opts) => {
2773
- await runGc(opts);
2834
+ const { interrupted } = await runProcessOperation(process.cwd(), (operation) => withDrainingCancellation(operation, operation.signal, () => runGc(opts, { operation })));
2835
+ if (interrupted !== null) process.exitCode = interrupted;
2774
2836
  });
2775
2837
  }
2776
2838
  //#endregion
2777
- export { collectGcReport, gcCommand as default, deleteParkedSims, describeParkedSims, describeUnverifiableDevices, findOrphanedDevices, findStaleDeviceRecords, findStaleProjectDevices, formatGcReport, runGc, selectCaches };
2839
+ export { runGc as a, readGcReport as i, executeGc as n, withGcMaintenanceClaim as o, gcCommand as r, formatGcReport as s, collectGcReport as t };