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
@@ -0,0 +1,2801 @@
1
+ import { n as isTimeoutError, r as WorkspaceProcessCleanupError, t as getExecutor } from "./exec-DEoAhcze.mjs";
2
+ import { g as workspaceStateFile$1, m as workspaceLogsDir$1 } from "./paths-DQtTgsBf.mjs";
3
+ import { F as isOnMountedVolume, G as projectDeviceSlots, I as listMountedVolumes, L as measuredDirectorySizeAsync, N as directorySizeAsync, P as formatBytes, _ as loadConfig$1, p as getProject } from "./config-DlrsduwG.mjs";
4
+ import { o as formatLongDuration, u as plural } from "./command-output-gwpm28fu.mjs";
5
+ import { c as gitQuiet, l as gitQuietAsync, n as runOperationFileQuiet, r as childEnvironment } from "./operation-exec-BW0em61G.mjs";
6
+ import { r as readWorkspaceState$1, u as workspaceLastUsed } from "./workspace-state-U08dEzhG.mjs";
7
+ import { E as maintenanceKeepSetting, Et as listWorktreesAsync, Ft as resolveFullRefAsync, It as resolveSourceCheckoutAsync, J as resolveProjectSettingsAsync, Rt as sourceCheckoutOf, St as hasUncommittedWorkAsync, Tt as listWorktrees, Wt as removeTemporaryEntryAsync, bt as hasPopulatedSubmodulesAsync, gt as dirtyPathsAsync, q as resolveProjectSettings, zt as unpushedCommitsAsync } from "./settings-C7O8A6ZI.mjs";
8
+ import { i as findProjectRoot$1 } from "./project-wM_XFTvE.mjs";
9
+ import { a as unregister, n as readManifest } from "./cache-manifest-DSeQjt-Q.mjs";
10
+ import { A as workspaceBuildInProgress, E as collectOwnedDeviceActivityAsync, St as deleteProjectDevices, T as collectOwnedDeviceActivity, Tt as deviceSweepIsScoped, X as emptyCaches, _r as phase, br as discoverCaches, bt as deleteParkedAvds, ct as deleteGradleVersion, d as trimmableCaches, f as stopOwnedMetro, fr as memorySweepIsScoped, gr as stopIdleGradleDaemons, gt as DEVICE_LIST_TIMEOUT_MS, i as enforceBudget, kt as findStaleProjectDevices, nt as planCacheEmptying, o as machineNumber, pt as waitForGradleExit, r as cacheLabel, sr as collectMemoryReport, st as collectGradleVersions, u as resolveBudget, vt as collectParkedAvds, xr as pruneCache, xt as deleteParkedSims, yt as collectParkedSims } from "./budget-Cvwn54qV.mjs";
11
+ import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
12
+ import { V as takeGcResults, a as collectWorkspaceOutputs, c as listWorkspaceDirs, f as withIdleWorkspace, i as collectOrphanedWorkspaces, m as workspaceInUseAsync, o as deleteOrphanedWorkspaces, p as workspaceInUse, r as clearWorkspaceOutputs, s as isInsideWorkspaces, z as GC_OUTPUT } from "./workspaces-CpANmBlq.mjs";
13
+ import { N as listBuildLocks, b as leaseIsExpired, g as workspaceActivity, x as listLeaseFiles } from "./activity-CMLmzp7o.mjs";
14
+ import { F as listAvdsAsync, G as ownedAvdSerialResolverAsync, P as listAvds, V as ownedAvdDirectory, vt as readParked } from "./android-C7sVmyI9.mjs";
15
+ import { d as listAllIosSims, f as listAllIosSimsAsync } from "./ios-hcBpnEGk.mjs";
16
+ import { s as metroStoreRoot } from "./errors-ccaPc4ox.mjs";
17
+ import { t as resolveMaintenanceSettings } from "./settings-Djl-mpjX.mjs";
18
+ import { A as levelFloors, C as measureDisk, E as measurePressureAsync, M as lowestFree, N as protectWindowMs, S as cacheBlocked, T as measurePressure, a as matchWorktreeEntry, c as reclaimKeys, i as excludeWatchmanCookies, k as diskPressureLevel, m as removeWorktreeTarget, o as nestedWorktrees, r as excludePodChurn, s as porcelainPath, w as measureDiskAsync } from "./worktree-CHecZ4Oq.mjs";
19
+ import { c as FETCH_FRESH_MS, d as localDefaultBranchAsync, l as fetchDefaultBranchAsync, o as recordPullRequestLookup, p as mergeStateAsync, u as lastFetchedAtAsync } from "./warm-progress-CA3TgT3-.mjs";
20
+ import { n as describePullRequest, r as endedPullRequest, s as pullRequestLookups } from "./pull-request-W1IuLXDt.mjs";
21
+ import { readClaimSet } from "@stim-cli/core/ownership-claim";
22
+ import { existsSync, lstatSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
23
+ import { tmpdir } from "node:os";
24
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
25
+ import { buildCacheRoot, configDir, metroCacheRoot } from "@stim-cli/core";
26
+ import { LAST_BUILD_KEYS, MAINTENANCE_LOG_NAME, loadConfig, readBuildHistory, readMaintenanceState, readPullRequestCache, settingDefinition, sharedCompilationCache } from "@stim-cli/core/state";
27
+ import { isAbsolute as isAbsolute$1, join as join$1 } from "path";
28
+ import chalk from "chalk";
29
+ import { existsSync as existsSync$1, readdirSync as readdirSync$1, statSync as statSync$1 } from "fs";
30
+ import { lstat, readdir, realpath, rename, stat } from "node:fs/promises";
31
+ //#region src/commands/gc/worktrees.ts
32
+ const DEFAULT_WORKTREE_IDLE_DAYS = 7;
33
+ const DIRTY_PATHS_SHOWN = 3;
34
+ const DAY_MS = 864e5;
35
+ const MINUTE_MS = 6e4;
36
+ const GRACE_SETTING = "gc.worktreeGraceMinutes";
37
+ const NO_OWN_COMMITS_PRESSURE_FLOOR_MS = 5 * MINUTE_MS;
38
+ const PULL_REQUEST_NOT_CHECKED = "not checked since the last maintenance worktree check";
39
+ const WORKTREE_CHECK_SETTING = "maintenance.worktreeCheckMinutes";
40
+ const MERGE_DECIDES = /* @__PURE__ */ new Set([
41
+ "unpushed",
42
+ "not-merged",
43
+ "last-use-unknown",
44
+ "recently-used"
45
+ ]);
46
+ function skip(code, text) {
47
+ return {
48
+ code,
49
+ text
50
+ };
51
+ }
52
+ function pullRequestNote(lookup) {
53
+ const pr = lookup && "pullRequest" in lookup ? lookup.pullRequest : null;
54
+ if (!pr) return null;
55
+ return pr.containsHead ? describePullRequest(pr) : `${describePullRequest(pr)}, and HEAD has commits it does not`;
56
+ }
57
+ /**
58
+ * Why gc keeps a worktree, or null when it removes it. A worktree that would be removed is still kept while its last
59
+ * activity, or its merge into the default branch, is less than `grace.ms` old, and kept when that activity is unknown.
60
+ * A worktree with no commits of its own beyond the default branch is removable when idle for `olderThan` days, by its
61
+ * last Stim use or, when no Stim use is recorded, its last activity; with `noOwnCommitsIdleMs`, as under disk
62
+ * pressure, once its last activity is that old.
63
+ */
64
+ function worktreeSkipReason(facts, olderThan, grace = {
65
+ ms: 0,
66
+ now: 0
67
+ }, noOwnCommitsIdleMs = null) {
68
+ return removalBlocker(facts, olderThan, grace.now, noOwnCommitsIdleMs) ?? graceBlocker(facts, grace);
69
+ }
70
+ function noOwnCommitsActivityBlocker(activity, idleMs, now) {
71
+ if (!activity) return skip("activity-unknown", "its last activity could not be read");
72
+ const eligibleAt = activity.at + idleMs;
73
+ if (now >= eligibleAt) return null;
74
+ const ago = formatLongDuration(Math.max(0, now - activity.at));
75
+ return {
76
+ code: "recent-activity",
77
+ text: `recent activity: ${activity.basis} ${ago} ago; removable after ${new Date(eligibleAt).toISOString()}`,
78
+ eligibleAt
79
+ };
80
+ }
81
+ function hasNoOwnCommits(merge) {
82
+ return merge?.merged === false && merge.noOwnCommits === true;
83
+ }
84
+ function activityIdleDays(activity, now) {
85
+ return Math.max(0, Math.floor((now - activity.at) / DAY_MS));
86
+ }
87
+ function graceBlocker(facts, grace) {
88
+ if (grace.ms <= 0) return null;
89
+ if (!facts.activity) return skip("activity-unknown", "its last activity could not be read");
90
+ const merged = facts.merge?.merged ? facts.merge : null;
91
+ const ended = endedPullRequest(facts.pullRequest);
92
+ const events = [facts.activity];
93
+ if (merged) events.push({
94
+ at: merged.mergedAt,
95
+ basis: `merged into ${merged.into}`
96
+ });
97
+ if (ended?.endedAt) events.push({
98
+ at: ended.endedAt,
99
+ basis: describePullRequest(ended)
100
+ });
101
+ const latest = events.reduce((a, b) => b.at > a.at ? b : a);
102
+ const eligibleAt = latest.at + grace.ms;
103
+ if (grace.now >= eligibleAt) return null;
104
+ const ago = formatLongDuration(Math.max(0, grace.now - latest.at));
105
+ return {
106
+ code: "recent-activity",
107
+ text: `recent activity: ${latest.basis} ${ago} ago; removable after ${new Date(eligibleAt).toISOString()}`,
108
+ eligibleAt
109
+ };
110
+ }
111
+ function removalBlocker(facts, olderThan, now, noOwnCommitsIdleMs) {
112
+ if (facts.bare) return skip("bare-repository", "bare repository");
113
+ if (typeof facts.source === "object") return skip("source-checkout-unknown", `source checkout unknown: ${facts.source.refusal}`);
114
+ if (facts.source === "source") return skip("source-checkout", "source checkout");
115
+ if (facts.locked) return skip("locked", "locked with git worktree lock");
116
+ if (facts.nested?.length) return skip("nested-worktree", `contains the worktree ${facts.nested.join(", ")}`);
117
+ if (facts.inUse.length) return skip("in-use", `in use: ${facts.inUse.join("; ")}`);
118
+ if (facts.porcelain === null) return skip("status-unreadable", "git status could not be read");
119
+ const dirty = excludePodChurn(excludeWatchmanCookies(facts.porcelain).lines).lines;
120
+ if (dirty.length) {
121
+ const paths = dirty.slice(0, DIRTY_PATHS_SHOWN).flatMap((line) => porcelainPath(line) ?? []);
122
+ const more = dirty.length > DIRTY_PATHS_SHOWN ? ", ..." : "";
123
+ return skip("dirty", `dirty: ${plural(dirty.length, "uncommitted or untracked file")} (${paths.join(", ")}${more})`);
124
+ }
125
+ if (facts.unpushed === null) return skip("unpushed-unchecked", "unpushed commits could not be checked");
126
+ const merged = facts.merge?.merged ? facts.merge : null;
127
+ const ended = endedPullRequest(facts.pullRequest);
128
+ if (facts.unpushed.length && !merged?.coversUnpushed && ended?.state !== "merged") return skip("unpushed", `unpushed: ${plural(facts.unpushed.length, "commit")} on no remote or other branch`);
129
+ if (facts.submodules === null) return skip("submodules", "initialized submodules could not be checked");
130
+ if (facts.submodules) return skip("submodules", "initialized submodules");
131
+ if (merged || ended) return null;
132
+ const notMerged = facts.merge && !facts.merge.merged ? facts.merge : null;
133
+ const details = [notMerged?.detail, pullRequestNote(facts.pullRequest)].filter(Boolean);
134
+ const noOwnCommits = hasNoOwnCommits(notMerged);
135
+ if (olderThan === null) {
136
+ if (noOwnCommits && noOwnCommitsIdleMs !== null) return noOwnCommitsActivityBlocker(facts.activity, noOwnCommitsIdleMs, now);
137
+ return skip(notMerged?.unknown ? "merge-unknown" : "not-merged", details.join("; ") || "not merged");
138
+ }
139
+ const also = details.map((detail) => `; ${detail}`).join("");
140
+ if (facts.idleDays === null && noOwnCommits && facts.activity) {
141
+ const days = activityIdleDays(facts.activity, now);
142
+ if (days < olderThan) return skip("recently-used", `recently active: ${facts.activity.basis} ${days}d ago${also}`);
143
+ return null;
144
+ }
145
+ if (facts.idleDays === null) return skip("last-use-unknown", `recently used: its last use is unknown${also}`);
146
+ if (facts.idleDays < olderThan) return skip("recently-used", `recently used ${facts.idleDays}d ago${also}`);
147
+ return null;
148
+ }
149
+ /**
150
+ * Why gc removes a worktree it did not skip: `merged into origin/main`, `PR #12 closed`, `idle 9d`,
151
+ * `no commits of its own beyond origin/main, idle 9d`, or, under disk pressure,
152
+ * `no commits of its own beyond origin/main, inactive for 12m under disk pressure`.
153
+ */
154
+ function worktreeRemovalReason(candidate, sweep) {
155
+ const { merge, activity } = candidate;
156
+ if (merge?.merged) return `merged into ${merge.into}`;
157
+ const ended = endedPullRequest(candidate.pullRequest);
158
+ if (ended) return describePullRequest(ended);
159
+ if (merge && hasNoOwnCommits(merge)) {
160
+ if (sweep.noOwnCommitsIdleMs !== null && activity) return `${merge.detail}, inactive for ${formatLongDuration(Math.max(0, sweep.now - activity.at))} under disk pressure`;
161
+ const days = candidate.idleDays ?? (activity ? activityIdleDays(activity, sweep.now) : null);
162
+ return days === null ? merge.detail : `${merge.detail}, idle ${days}d`;
163
+ }
164
+ return `idle ${candidate.idleDays ?? 0}d`;
165
+ }
166
+ /** The pull request note a kept worktree's report line carries, such as `PR #12 merged`, or null. */
167
+ function worktreePullRequestNote(candidate) {
168
+ return pullRequestNote(candidate.pullRequest);
169
+ }
170
+ function lastUsedOf(keys) {
171
+ const times = keys.map(workspaceLastUsed).filter(Number.isFinite);
172
+ return times.length ? Math.max(...times) : NaN;
173
+ }
174
+ function idleDaysOf(keys, now) {
175
+ const last = lastUsedOf(keys);
176
+ return Number.isFinite(last) ? Math.max(0, Math.floor((now - last) / DAY_MS)) : null;
177
+ }
178
+ async function reportDeviceReads(keys, operation) {
179
+ const signal = operation?.signal;
180
+ signal?.throwIfAborted();
181
+ const slots = keys.flatMap((key) => {
182
+ try {
183
+ return projectDeviceSlots(getProject(key));
184
+ } catch {
185
+ return [];
186
+ }
187
+ });
188
+ const requestedSims = new Set(slots.flatMap(({ platforms }) => {
189
+ const ios = platforms.ios;
190
+ return ios?.owned && ios.deviceUdid ? [ios.deviceUdid] : [];
191
+ }));
192
+ let sims = null;
193
+ if (requestedSims.size) try {
194
+ sims = await listAllIosSimsAsync({
195
+ timeoutMs: DEVICE_LIST_TIMEOUT_MS,
196
+ operation,
197
+ signal
198
+ });
199
+ } catch (error) {
200
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
201
+ signal?.throwIfAborted();
202
+ }
203
+ const names = new Set(slots.flatMap(({ platforms }) => {
204
+ const android = platforms.android;
205
+ return android?.owned && android.avdName ? [android.avdName] : [];
206
+ }));
207
+ const avds = /* @__PURE__ */ new Map();
208
+ const resolver = ownedAvdSerialResolverAsync({
209
+ timeoutMs: DEVICE_LIST_TIMEOUT_MS,
210
+ operation,
211
+ signal
212
+ });
213
+ for (const name of names) {
214
+ signal?.throwIfAborted();
215
+ try {
216
+ avds.set(name, { value: await resolver(name) });
217
+ } catch (error) {
218
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
219
+ signal?.throwIfAborted();
220
+ avds.set(name, { error });
221
+ }
222
+ }
223
+ return {
224
+ sims: (udid) => requestedSims.has(udid) || sims?.some((sim) => sim.udid === udid) ? sims : null,
225
+ avdSerial: (name) => {
226
+ const result = avds.get(name);
227
+ if (!result) throw new Error("The workspace device record changed during inspection.");
228
+ if ("error" in result) throw result.error;
229
+ return result.value;
230
+ }
231
+ };
232
+ }
233
+ async function inUseOf(keys, checks, devices, signal, operation) {
234
+ const reasons = [];
235
+ for (const key of keys) reasons.push(...await workspaceInUseAsync(key, checks, signal, operation));
236
+ return [.../* @__PURE__ */ new Set([...reasons, ...deviceUseOf(keys, devices)])];
237
+ }
238
+ function deviceUseOf(keys, devices) {
239
+ const reasons = [];
240
+ for (const key of keys) {
241
+ let slots;
242
+ try {
243
+ slots = projectDeviceSlots(getProject(key));
244
+ } catch (error) {
245
+ reasons.push(`its device records cannot be read: ${error.message}`);
246
+ continue;
247
+ }
248
+ for (const { platforms } of slots) {
249
+ const ios = platforms.ios;
250
+ if (ios?.owned && ios.deviceUdid) {
251
+ const sims = devices.sims(ios.deviceUdid);
252
+ const sim = sims?.find((entry) => entry.udid === ios.deviceUdid);
253
+ if (sims === null) reasons.push(`the state of its owned simulator ${ios.deviceUdid} cannot be read`);
254
+ else if (sim && sim.state !== "Shutdown") reasons.push(`its owned simulator ${sim.name} is ${sim.state}; \`stim stop\` in ${key} shuts it down`);
255
+ }
256
+ const android = platforms.android;
257
+ if (android?.owned && android.avdName) try {
258
+ if (devices.avdSerial(android.avdName).serial) reasons.push(`its owned emulator ${android.avdName} is running; \`stim stop\` in ${key} shuts it down`);
259
+ } catch {
260
+ reasons.push(`the state of its owned emulator ${android.avdName} cannot be read`);
261
+ }
262
+ }
263
+ }
264
+ const holders = new Set(keys.map(canonicalPath));
265
+ const now = Date.now();
266
+ for (const { lease } of listLeaseFiles()) if (lease && !leaseIsExpired(lease, now) && holders.has(canonicalPath(lease.holder))) reasons.push(`it holds the ${lease.platform} device lease on ${lease.deviceName ?? lease.id}`);
267
+ return reasons;
268
+ }
269
+ function newestMtime(paths) {
270
+ let newest = null;
271
+ for (const { path, basis } of paths) {
272
+ let mtime;
273
+ try {
274
+ mtime = statSync$1(path).mtimeMs;
275
+ } catch (error) {
276
+ if (error.code === "ENOENT") continue;
277
+ return "unreadable";
278
+ }
279
+ if (!newest || mtime > newest.at) newest = {
280
+ at: mtime,
281
+ basis
282
+ };
283
+ }
284
+ return newest;
285
+ }
286
+ function logFiles$1(key) {
287
+ const dir = workspaceLogsDir$1(key);
288
+ try {
289
+ return readdirSync$1(dir).filter((name) => !name.startsWith(MAINTENANCE_LOG_NAME)).map((name) => ({
290
+ path: join$1(dir, name),
291
+ basis: "a Stim log write"
292
+ }));
293
+ } catch (error) {
294
+ return error.code === "ENOENT" ? [] : null;
295
+ }
296
+ }
297
+ function worktreeActivityOf(path, keys) {
298
+ const gitDir = gitQuiet(path, [
299
+ "rev-parse",
300
+ "--path-format=absolute",
301
+ "--git-dir"
302
+ ])?.trim();
303
+ return activityFromGitDir(gitDir, keys);
304
+ }
305
+ async function worktreeActivityOfAsync(path, keys, operation) {
306
+ const gitDir = (await gitQuietAsync(path, [
307
+ "rev-parse",
308
+ "--path-format=absolute",
309
+ "--git-dir"
310
+ ], {
311
+ ...operation ? childEnvironment(operation.env) : {},
312
+ signal: operation?.signal,
313
+ declareFor: operation,
314
+ cwd: operation?.cwd,
315
+ killSignal: "SIGKILL"
316
+ }))?.trim();
317
+ return activityFromGitDir(gitDir, keys);
318
+ }
319
+ function activityFromGitDir(gitDir, keys) {
320
+ if (!gitDir) return null;
321
+ const paths = [
322
+ {
323
+ path: join$1(gitDir, "index"),
324
+ basis: "a git index write"
325
+ },
326
+ {
327
+ path: join$1(gitDir, "HEAD"),
328
+ basis: "a git HEAD change"
329
+ },
330
+ {
331
+ path: join$1(gitDir, "logs", "HEAD"),
332
+ basis: "a git reflog entry"
333
+ }
334
+ ];
335
+ for (const key of keys) {
336
+ const logs = logFiles$1(key);
337
+ if (logs === null) return null;
338
+ paths.push({
339
+ path: workspaceStateFile$1(key),
340
+ basis: "a Stim workspace state write"
341
+ }, ...logs);
342
+ }
343
+ const newest = newestMtime(paths);
344
+ return newest === "unreadable" ? null : newest;
345
+ }
346
+ function graceMsSetting(operation) {
347
+ const { value, error } = machineNumber(GRACE_SETTING, loadConfig$1(), operation?.env ?? process.env);
348
+ if (error) {
349
+ const fallback = Number(settingDefinition(GRACE_SETTING)?.default);
350
+ const line = chalk.yellow(`${error} gc uses the default of ${fallback} minutes.`);
351
+ const context = operation;
352
+ if (context) context.output("stderr", `${line}\n`);
353
+ else console.error(line);
354
+ return fallback * MINUTE_MS;
355
+ }
356
+ return (value ?? 0) * MINUTE_MS;
357
+ }
358
+ async function unlessTimedOut(read, unknown) {
359
+ try {
360
+ return await read();
361
+ } catch (error) {
362
+ if (isTimeoutError(error)) return unknown;
363
+ throw error;
364
+ }
365
+ }
366
+ async function porcelainOf(path, gitAnswered, operation) {
367
+ if (gitAnswered === null) return null;
368
+ if (!gitAnswered) return [];
369
+ const lines = await unlessTimedOut(() => dirtyPathsAsync(path, { limit: Infinity }, {
370
+ env: operation?.env,
371
+ signal: operation?.signal,
372
+ declareFor: operation
373
+ }), null);
374
+ return lines?.length ? lines : null;
375
+ }
376
+ function candidateRoots() {
377
+ const registered = Object.keys(loadConfig$1()?.projects ?? {}).filter(isAbsolute$1);
378
+ const recorded = listWorkspaceDirs().flatMap((entry) => entry.projectRoot ? [entry.projectRoot] : []);
379
+ return [.../* @__PURE__ */ new Set([
380
+ ...registered,
381
+ ...recorded,
382
+ ...linkedWorktreesOf(registered)
383
+ ])].filter((root) => existsSync$1(root)).toSorted();
384
+ }
385
+ async function candidateRootsAsync(operation) {
386
+ operation?.signal.throwIfAborted();
387
+ const options = {
388
+ env: operation?.env,
389
+ signal: operation?.signal,
390
+ declareFor: operation
391
+ };
392
+ const registered = Object.keys(loadConfig$1()?.projects ?? {}).filter(isAbsolute$1);
393
+ const recorded = listWorkspaceDirs().flatMap((entry) => entry.projectRoot ? [entry.projectRoot] : []);
394
+ const linked = /* @__PURE__ */ new Set();
395
+ for (const root of registered) {
396
+ operation?.signal.throwIfAborted();
397
+ if (!existsSync$1(root)) continue;
398
+ const { entries, ...source } = await resolveSourceCheckoutAsync(root, options);
399
+ if ("refusal" in source) continue;
400
+ for (const entry of entries) if (!entry.bare && !entry.prunable && entry.path !== source.path) linked.add(entry.path);
401
+ }
402
+ return [.../* @__PURE__ */ new Set([
403
+ ...registered,
404
+ ...recorded,
405
+ ...linked
406
+ ])].filter((root) => existsSync$1(root)).toSorted();
407
+ }
408
+ function linkedWorktreesOf(roots) {
409
+ const linked = /* @__PURE__ */ new Set();
410
+ for (const root of roots) {
411
+ if (!existsSync$1(root)) continue;
412
+ const entries = listWorktrees(root);
413
+ const source = sourceCheckoutOf(entries);
414
+ if ("refusal" in source) continue;
415
+ for (const entry of entries) if (!entry.bare && !entry.prunable && entry.path !== source.path) linked.add(entry.path);
416
+ }
417
+ return [...linked];
418
+ }
419
+ async function staleFetch(repo, now, freshMs, execution) {
420
+ const at = await lastFetchedAtAsync(repo, execution);
421
+ if (at !== null && now - at < freshMs) return null;
422
+ return at === null ? "the repository has no recorded fetch" : `the repository was last fetched ${formatLongDuration(Math.max(0, now - at))} ago`;
423
+ }
424
+ /**
425
+ * How old a fetch or a cached pull request lookup can be and still count as fresh in a report: twice the maintenance
426
+ * worktree check interval, which refreshes both, and never less than 10 minutes.
427
+ */
428
+ function reportFreshMs(operation) {
429
+ const minutes = machineNumber(WORKTREE_CHECK_SETTING, loadConfig$1(), operation?.env ?? process.env).value ?? Number(settingDefinition(WORKTREE_CHECK_SETTING)?.default);
430
+ return Math.max(FETCH_FRESH_MS, 2 * minutes * MINUTE_MS);
431
+ }
432
+ function notRefreshedNote(fetch, facts) {
433
+ const lookup = facts.pullRequest;
434
+ const stale = [fetch, lookup && "unavailable" in lookup && lookup.unavailable === PULL_REQUEST_NOT_CHECKED ? "its pull request was not checked" : null].filter(Boolean);
435
+ return stale.length ? `merge state not refreshed: ${stale.join(", ")}; the next maintenance worktree check, which a stim command starts when it is due, or \`stim gc --delete\` checks again` : null;
436
+ }
437
+ async function checkMergeStates(pending, idle, grace, noOwnCommitsIdleMs, refresh, freshMs, operation) {
438
+ const repos = /* @__PURE__ */ new Map();
439
+ const execution = {
440
+ ...operation ? childEnvironment(operation.env) : {},
441
+ signal: operation?.signal,
442
+ declareFor: operation,
443
+ cwd: operation?.cwd,
444
+ killSignal: "SIGKILL"
445
+ };
446
+ for (const entry of pending) repos.set(entry.repo, [...repos.get(entry.repo) ?? [], entry]);
447
+ for (const [repo, entries] of repos) {
448
+ operation?.signal.throwIfAborted();
449
+ const target = refresh ? await fetchDefaultBranchAsync(repo, grace.now, execution) : await localDefaultBranchAsync(repo, execution);
450
+ const fetch = refresh ? null : await staleFetch(repo, grace.now, freshMs, execution);
451
+ for (const { candidate, facts } of entries) {
452
+ operation?.signal.throwIfAborted();
453
+ const merge = "error" in target ? {
454
+ merged: false,
455
+ unknown: true,
456
+ detail: `merge state unknown: ${target.error}`
457
+ } : await mergeStateAsync(candidate.path, target, execution);
458
+ const verdict = worktreeSkipReason({
459
+ ...facts,
460
+ merge
461
+ }, idle, grace, noOwnCommitsIdleMs);
462
+ const note = !refresh && verdict && MERGE_DECIDES.has(verdict.code) ? notRefreshedNote(hasNoOwnCommits(merge) ? null : fetch, facts) : null;
463
+ Object.assign(candidate, {
464
+ merge,
465
+ skipCode: note && verdict?.code === "not-merged" ? "merge-unknown" : verdict?.code ?? null,
466
+ skipped: verdict ? note ? `${verdict.text}; ${note}` : verdict.text : null,
467
+ eligibleAt: verdict?.eligibleAt ?? null
468
+ });
469
+ }
470
+ }
471
+ }
472
+ /**
473
+ * Classifies each linked worktree of a registered project or its repository. A merged one is removable; with `idle`, so is one unused for
474
+ * `olderThan` days; with `removesNoOwnCommits`, so is one with no commits of its own beyond the default branch whose
475
+ * last activity is older than the grace period, and never younger than 5 minutes. Device use is read last, in one pass against one simulator list and one AVD resolver, so
476
+ * the resolver's time budget covers only device reads. Pull requests are asked once per repository, sequentially
477
+ * with an operation and concurrently otherwise. Merge state is checked only where it decides the verdict, after one fetch per repository.
478
+ * Without `refresh`, it runs no fetch and no `gh`: merge state comes from the existing remote-tracking ref and pull
479
+ * requests from the cache `status --watch` writes, and a kept verdict that fresher state could change says so.
480
+ */
481
+ async function collectWorktreeSweep({ idle, olderThan, now, graceMs, removesNoOwnCommits = false, refresh = true, operation }) {
482
+ const days = idle ? olderThan ?? DEFAULT_WORKTREE_IDLE_DAYS : null;
483
+ const grace = {
484
+ ms: graceMs ?? graceMsSetting(operation),
485
+ now
486
+ };
487
+ const freshMs = refresh ? 0 : reportFreshMs(operation);
488
+ const noOwnCommitsIdleMs = removesNoOwnCommits ? Math.max(grace.ms, NO_OWN_COMMITS_PRESSURE_FLOOR_MS) : null;
489
+ const groups = /* @__PURE__ */ new Map();
490
+ operation?.signal.throwIfAborted();
491
+ const read = {
492
+ env: operation?.env,
493
+ signal: operation?.signal,
494
+ declareFor: operation
495
+ };
496
+ const outside = [];
497
+ const candidates = await candidateRootsAsync(operation);
498
+ if (candidates.length) phase("worktrees", `checking ${plural(candidates.length, "project")} for merged or idle worktrees`, operation);
499
+ for (const root of candidates) {
500
+ operation?.signal.throwIfAborted();
501
+ const entry = matchWorktreeEntry(await listWorktreesAsync(root, read), root);
502
+ if (!entry) {
503
+ outside.push({
504
+ path: root,
505
+ keys: [root],
506
+ idleDays: null,
507
+ merge: null,
508
+ head: null,
509
+ pullRequest: null,
510
+ skipCode: "not-a-worktree",
511
+ skipped: "not inside a git worktree",
512
+ eligibleAt: null,
513
+ activity: null
514
+ });
515
+ continue;
516
+ }
517
+ groups.set(entry.path, [...groups.get(entry.path) ?? [], root]);
518
+ }
519
+ const inspected = [];
520
+ for (const [path, roots] of groups) {
521
+ operation?.signal.throwIfAborted();
522
+ const { entries, ...source } = await resolveSourceCheckoutAsync(path, read);
523
+ const entry = matchWorktreeEntry(entries, path);
524
+ const keys = [.../* @__PURE__ */ new Set([...roots, ...reclaimKeys(path)])];
525
+ const idleDays = idleDaysOf(keys, now);
526
+ const linked = !("refusal" in source) && entry !== null && source.path !== entry.path;
527
+ const activity = linked && (grace.ms > 0 || days !== null || removesNoOwnCommits) ? await worktreeActivityOfAsync(path, keys, operation) : null;
528
+ const gitAnswered = linked ? await unlessTimedOut(() => hasUncommittedWorkAsync(path, read), null) : null;
529
+ const head = linked ? await resolveFullRefAsync(path, "HEAD", read) : null;
530
+ const facts = {
531
+ source: "refusal" in source ? source : linked ? "linked" : "source",
532
+ bare: Boolean(entry?.bare),
533
+ locked: Boolean(entry?.locked),
534
+ nested: entry ? nestedWorktrees(entries, entry.path) : [],
535
+ porcelain: await porcelainOf(path, gitAnswered, operation),
536
+ unpushed: linked ? await unlessTimedOut(() => unpushedCommitsAsync(path, read), null) : null,
537
+ submodules: linked ? await unlessTimedOut(() => hasPopulatedSubmodulesAsync(path, read), null) : false,
538
+ inUse: [],
539
+ idleDays,
540
+ merge: null,
541
+ pullRequest: null,
542
+ activity
543
+ };
544
+ inspected.push({
545
+ candidate: {
546
+ path,
547
+ keys,
548
+ idleDays,
549
+ merge: null,
550
+ head,
551
+ pullRequest: null,
552
+ skipCode: null,
553
+ skipped: null,
554
+ eligibleAt: null,
555
+ activity
556
+ },
557
+ facts,
558
+ repo: "refusal" in source ? null : source.path,
559
+ branch: head && entry?.branch ? entry.branch : null
560
+ });
561
+ }
562
+ const linked = inspected.filter(({ facts }) => facts.source === "linked");
563
+ for (const { candidate, facts } of linked) for (const key of candidate.keys) facts.inUse.push(...await workspaceInUseAsync(key, { managedLocks: true }, operation?.signal, operation));
564
+ const devices = await reportDeviceReads(linked.flatMap(({ candidate }) => candidate.keys), operation);
565
+ for (const { candidate, facts } of linked) facts.inUse = [.../* @__PURE__ */ new Set([...facts.inUse, ...deviceUseOf(candidate.keys, devices)])];
566
+ if (refresh) await lookUpPullRequests(inspected, operation);
567
+ else readCachedPullRequests(inspected, now, freshMs);
568
+ const worktrees = [];
569
+ const pending = [];
570
+ for (const { candidate, facts, repo } of inspected) {
571
+ const verdict = worktreeSkipReason(facts, days, grace, noOwnCommitsIdleMs);
572
+ Object.assign(candidate, {
573
+ pullRequest: facts.pullRequest,
574
+ skipCode: verdict?.code ?? null,
575
+ skipped: verdict?.text ?? null,
576
+ eligibleAt: verdict?.eligibleAt ?? null
577
+ });
578
+ if (verdict && MERGE_DECIDES.has(verdict.code) && repo !== null) pending.push({
579
+ candidate,
580
+ facts,
581
+ repo
582
+ });
583
+ worktrees.push(candidate);
584
+ }
585
+ await checkMergeStates(pending, days, grace, noOwnCommitsIdleMs, refresh, freshMs, operation);
586
+ const listed = [...worktrees, ...outside].filter((w) => idle || w.skipCode !== "not-a-worktree" && w.skipCode !== "source-checkout");
587
+ return {
588
+ idle: days === null ? null : {
589
+ olderThan: days,
590
+ defaulted: olderThan === null
591
+ },
592
+ graceMs: grace.ms,
593
+ now,
594
+ noOwnCommitsIdleMs,
595
+ worktrees: listed
596
+ };
597
+ }
598
+ async function lookUpPullRequests(inspected, operation) {
599
+ const repos = /* @__PURE__ */ new Map();
600
+ for (const { candidate, facts, repo, branch } of inspected) {
601
+ if (repo === null || branch === null || candidate.head === null) continue;
602
+ const query = {
603
+ cwd: candidate.path,
604
+ branch,
605
+ head: candidate.head
606
+ };
607
+ repos.set(repo, [...repos.get(repo) ?? [], {
608
+ facts,
609
+ query
610
+ }]);
611
+ }
612
+ const lookup = pullRequestLookups({ operation });
613
+ const probes = [...repos].map(([repo, entries]) => async () => {
614
+ const answers = await lookup(repo, entries.map(({ query }) => query));
615
+ const at = (/* @__PURE__ */ new Date()).toISOString();
616
+ entries.forEach(({ facts, query }, i) => {
617
+ operation?.signal.throwIfAborted();
618
+ const answer = answers[i] ?? null;
619
+ facts.pullRequest = answer;
620
+ if (answer && "pullRequest" in answer) recordPullRequestLookup({
621
+ path: query.cwd,
622
+ branch: query.branch,
623
+ head: query.head
624
+ }, answer.pullRequest, at);
625
+ });
626
+ });
627
+ if (operation) for (const probe of probes) {
628
+ operation.signal.throwIfAborted();
629
+ await probe();
630
+ }
631
+ else await Promise.all(probes.map((probe) => probe()));
632
+ }
633
+ function readCachedPullRequests(inspected, now, freshMs) {
634
+ let gh;
635
+ for (const { candidate, facts, repo, branch } of inspected) {
636
+ if (repo === null || branch === null || candidate.head === null) continue;
637
+ const cached = cachedPullRequest(readPullRequestCache(candidate.path), branch, candidate.head, now, freshMs);
638
+ if (!("pullRequest" in cached)) gh ??= getExecutor().findExecutable("gh") !== null;
639
+ facts.pullRequest = "pullRequest" in cached || gh ? cached : { unavailable: "gh is not installed" };
640
+ }
641
+ }
642
+ function cachedPullRequest(cached, branch, head, now, freshMs) {
643
+ const notChecked = { unavailable: PULL_REQUEST_NOT_CHECKED };
644
+ if (!cached || cached.branch !== branch || cached.head !== head) return notChecked;
645
+ if (!(now - Date.parse(cached.checkedAt) < freshMs)) return notChecked;
646
+ const { pullRequest: pr, containsHead, endedAt } = cached;
647
+ if (pr === null) return { pullRequest: null };
648
+ if (containsHead === void 0 || pr.state !== "open" && pr.state !== "draft" && typeof endedAt !== "number") return notChecked;
649
+ const state = pr.state === "draft" ? "open" : pr.state;
650
+ const open = state === "open";
651
+ return { pullRequest: {
652
+ number: pr.number,
653
+ url: pr.url,
654
+ head,
655
+ containsHead,
656
+ state,
657
+ draft: pr.state === "draft",
658
+ endedAt: open ? null : endedAt ?? null
659
+ } };
660
+ }
661
+ async function noOwnCommitsRecheck(sweep, path, keys, now, operation) {
662
+ if (!sweep.idle) {
663
+ if (sweep.noOwnCommitsIdleMs === null) return "it has no commits of its own and is not idle";
664
+ return noOwnCommitsActivityBlocker(await worktreeActivityOfAsync(path, keys, operation), sweep.noOwnCommitsIdleMs, now)?.text ?? null;
665
+ }
666
+ const idleDays = idleDaysOf(keys, now);
667
+ if (idleDays !== null) return idleDays < sweep.idle.olderThan ? `used ${idleDays}d ago since gc checked it` : null;
668
+ const activity = await worktreeActivityOfAsync(path, keys, operation);
669
+ if (!activity) return "its last activity could not be read";
670
+ const days = activityIdleDays(activity, now);
671
+ return days < sweep.idle.olderThan ? `active ${days}d ago since gc checked it: ${activity.basis}` : null;
672
+ }
673
+ async function removeWorktrees(sweep, { now = Date.now(), operation } = {}, output = GC_OUTPUT) {
674
+ let failures = 0;
675
+ for (const candidate of sweep.worktrees) {
676
+ operation?.signal.throwIfAborted();
677
+ if (candidate.skipped) continue;
678
+ let kept = [];
679
+ const guard = async (lockedKeys) => {
680
+ const keys = [.../* @__PURE__ */ new Set([...candidate.keys, ...lockedKeys])];
681
+ const unlocked = keys.filter((key) => !lockedKeys.includes(key));
682
+ const devices = await reportDeviceReads(keys, operation);
683
+ const reasons = [...await inUseOf(lockedKeys, { managedLocks: false }, devices, operation?.signal, operation), ...await inUseOf(unlocked, { managedLocks: true }, devices, operation?.signal, operation)].map((r) => `in use: ${r}`);
684
+ const mergedHead = candidate.merge?.merged ? candidate.merge.head : endedPullRequest(candidate.pullRequest) ? candidate.head : null;
685
+ if (mergedHead) {
686
+ if (await resolveFullRefAsync(candidate.path, "HEAD", {
687
+ env: operation?.env,
688
+ signal: operation?.signal,
689
+ declareFor: operation
690
+ }) !== mergedHead) reasons.push("its HEAD moved since gc checked it");
691
+ } else if (hasNoOwnCommits(candidate.merge)) {
692
+ if (await resolveFullRefAsync(candidate.path, "HEAD", {
693
+ env: operation?.env,
694
+ signal: operation?.signal,
695
+ declareFor: operation
696
+ }) !== candidate.head) reasons.push("its HEAD moved since gc checked it");
697
+ const recent = await noOwnCommitsRecheck(sweep, candidate.path, keys, Date.now(), operation);
698
+ if (recent) reasons.push(recent);
699
+ } else {
700
+ const idleDays = idleDaysOf(keys, now);
701
+ if (idleDays === null || !sweep.idle || idleDays < sweep.idle.olderThan) reasons.push(`used ${idleDays === null ? "at an unknown time" : `${idleDays}d ago`} since gc checked it`);
702
+ }
703
+ if (sweep.graceMs > 0) {
704
+ const recent = graceBlocker({
705
+ merge: candidate.merge,
706
+ pullRequest: candidate.pullRequest,
707
+ activity: await worktreeActivityOfAsync(candidate.path, keys, operation)
708
+ }, {
709
+ ms: sweep.graceMs,
710
+ now: Date.now()
711
+ });
712
+ if (recent) reasons.push(recent.text);
713
+ }
714
+ kept = reasons;
715
+ return reasons;
716
+ };
717
+ let removed = false;
718
+ let error = null;
719
+ try {
720
+ const ended = endedPullRequest(candidate.pullRequest);
721
+ const mergedHead = candidate.merge?.merged && candidate.merge.coversUnpushed ? candidate.merge.head : ended?.state === "merged" ? candidate.head ?? void 0 : void 0;
722
+ const expectedHead = candidate.merge?.merged || ended || hasNoOwnCommits(candidate.merge) ? candidate.head ?? void 0 : void 0;
723
+ removed = await removeWorktreeTarget(candidate.path, {
724
+ linkedOnly: true,
725
+ guard,
726
+ mergedHead,
727
+ expectedHead,
728
+ output,
729
+ operation
730
+ });
731
+ } catch (thrown) {
732
+ if (thrown instanceof WorkspaceProcessCleanupError || thrown instanceof Error && "reportCleanup" in thrown) throw thrown;
733
+ operation?.signal.throwIfAborted();
734
+ error = thrown?.message || String(thrown);
735
+ output.error(chalk.red(`Could not remove ${candidate.path}: ${error}`));
736
+ }
737
+ if (removed) {
738
+ output.log(chalk.green(`Removed the worktree ${candidate.path} (${worktreeRemovalReason(candidate, sweep)})`));
739
+ output.record("worktree", "done", candidate.path, { detail: worktreeRemovalReason(candidate, sweep) });
740
+ } else if (kept.length) {
741
+ output.log(chalk.yellow(`Kept the worktree ${candidate.path}: ${kept.join("; ")}`));
742
+ output.record("worktree", "kept", candidate.path, { detail: kept.join("; ") });
743
+ } else {
744
+ failures++;
745
+ output.error(chalk.yellow(`Kept the worktree ${candidate.path}; see the lines above for why.`));
746
+ output.record("worktree", "failed", candidate.path, { detail: error ?? "stim worktree remove did not remove it; see the details" });
747
+ }
748
+ }
749
+ return failures;
750
+ }
751
+ //#endregion
752
+ //#region src/maintenance/plan.ts
753
+ function plan({ pressure, sizes, settings, budget, diskSteps = [], protectedRoot, scoped = false, projects = [], pinned = () => false, recentlyUsed = () => null }) {
754
+ const actions = [];
755
+ const blocked = [];
756
+ const skips = [];
757
+ const add = (action) => {
758
+ if (action.kind === "would-clear-outputs" && action.workspace && pinned(action.workspace)) {
759
+ skips.push({
760
+ target: action.workspace,
761
+ reason: "pinned by maintenance.keep",
762
+ workspace: action.workspace
763
+ });
764
+ return false;
765
+ }
766
+ const recent = action.kind === "would-clear-outputs" && action.workspace ? recentlyUsed(action.workspace) : null;
767
+ if (recent) {
768
+ skips.push({
769
+ target: action.workspace,
770
+ reason: recent,
771
+ workspace: action.workspace
772
+ });
773
+ return false;
774
+ }
775
+ if (actions.some((entry) => entry.kind === action.kind && entry.target === action.target)) return false;
776
+ actions.push(action);
777
+ return true;
778
+ };
779
+ const floor = Math.max(budget.minFreeDiskMb, budget.hardFloorDiskMb);
780
+ const emergency = pressure?.disk.some((disk) => disk.freeMb < budget.hardFloorDiskMb) ?? false;
781
+ const low = pressure?.disk.filter((disk) => disk.freeMb < floor) ?? [];
782
+ const reason = low.map((disk) => `free disk ${(disk.freeMb / 1024).toFixed(1)}G on ${disk.volume} under the ${(floor / 1024).toFixed(1)}G floor`).join("; ");
783
+ const kinds = {
784
+ "idle-devices": "would-shutdown-device",
785
+ "idle-dev-servers": "would-stop-workspace",
786
+ "workspace-outputs": "would-clear-outputs",
787
+ "stale-cache-entries": "would-trim-cache"
788
+ };
789
+ if (low.length) {
790
+ for (const step of diskSteps) {
791
+ if (scoped && step.step === "idle-devices") continue;
792
+ for (const target of step.targets) {
793
+ const workspace = projects.find((root) => root === target || target.endsWith(` in ${root}`)) ?? (step.step === "idle-dev-servers" || step.step === "workspace-outputs" ? target : void 0);
794
+ if (workspace === protectedRoot || target === protectedRoot) continue;
795
+ const size = workspace && step.step !== "idle-devices" ? sizes.find((entry) => entry.workspace === workspace && entry.category === "workspace-outputs") : void 0;
796
+ add({
797
+ kind: kinds[step.step],
798
+ target,
799
+ bytes: step.step === "workspace-outputs" ? size?.bytes ?? 0 : 0,
800
+ reason: `${size?.idleDays == null ? "" : `idle ${size.idleDays} days, `}${reason}`,
801
+ ...workspace ? { workspace } : {},
802
+ check: "disk",
803
+ ...step.step === "stale-cache-entries" ? { olderThanDays: 14 } : {}
804
+ });
805
+ }
806
+ if (step.failures) blocked.push(`Could not plan ${step.step}: ${step.failures} failures`);
807
+ }
808
+ if (actions.length === 0) blocked.push(`${reason}; no reclaimable disk targets`);
809
+ }
810
+ const caps = [
811
+ [
812
+ "workspace-outputs",
813
+ settings.workspaceOutputsMaxGb,
814
+ "would-clear-outputs"
815
+ ],
816
+ [
817
+ "build-cache",
818
+ settings.buildCacheMaxGb,
819
+ "would-trim-cache"
820
+ ],
821
+ [
822
+ "metro-cache",
823
+ settings.metroCacheMaxGb,
824
+ "would-trim-cache"
825
+ ],
826
+ [
827
+ "compilation-cache",
828
+ settings.swiftCompilationCacheMaxGb,
829
+ "would-empty-cache"
830
+ ]
831
+ ];
832
+ for (const [category, capGb, kind] of caps) {
833
+ const entries = sizes.filter((size) => size.category === category);
834
+ const total = entries.reduce((sum, size) => sum + size.bytes, 0);
835
+ const cap = capGb * 1024 ** 3;
836
+ if (cap === 0 || total <= cap) continue;
837
+ let remaining = total;
838
+ const targetBytes = cap * settings.capTargetPercent / 100;
839
+ const why = `${category} ${(total / 1024 ** 3).toFixed(1)}G over the ${capGb}G cap; target ${(targetBytes / 1024 ** 3).toFixed(1)}G`;
840
+ if (category === "compilation-cache" && !emergency) {
841
+ skips.push({
842
+ target: entries[0]?.dir ?? category,
843
+ reason: `${why}; it can only be emptied whole, which runs only below the ${(budget.hardFloorDiskMb / 1024).toFixed(1)}G hard floor`
844
+ });
845
+ continue;
846
+ }
847
+ for (const size of entries.toSorted((a, b) => (b.idleDays ?? 0) - (a.idleDays ?? 0))) {
848
+ if (remaining <= targetBytes) break;
849
+ const kept = size.workspace === protectedRoot ? "the triggering workspace is protected" : size.blocked;
850
+ if (kept) {
851
+ skips.push({
852
+ target: size.dir,
853
+ reason: kept,
854
+ ...size.workspace ? { workspace: size.workspace } : {}
855
+ });
856
+ continue;
857
+ }
858
+ const bytes = kind === "would-trim-cache" ? Math.round(Math.min(size.bytes, remaining - targetBytes)) : size.bytes;
859
+ if (!add({
860
+ kind,
861
+ target: size.workspace ?? size.dir,
862
+ bytes,
863
+ reason: `${!size.workspace || size.idleDays == null ? "" : `idle ${size.idleDays} days, `}${why}`,
864
+ ...size.workspace ? { workspace: size.workspace } : {},
865
+ ...kind === "would-clear-outputs" ? {} : { dir: size.dir }
866
+ })) continue;
867
+ remaining -= bytes;
868
+ }
869
+ if (remaining > targetBytes) blocked.push(`${why}; eligible targets cannot reach the target`);
870
+ }
871
+ if (pressure?.memory.pressured) skips.push({
872
+ target: "memory",
873
+ reason: "memory pressure observed; automatic maintenance does not stop helpers or idle workspaces yet"
874
+ });
875
+ return {
876
+ actions,
877
+ blocked,
878
+ skips
879
+ };
880
+ }
881
+ //#endregion
882
+ //#region src/maintenance/protect.ts
883
+ const BUILD_RECORDS = ["lastBuild", ...Object.values(LAST_BUILD_KEYS)];
884
+ function recordKeys(record) {
885
+ if (record === null || typeof record !== "object") return [];
886
+ const value = record;
887
+ return [value.cacheKey, value.missReason?.baseline?.cacheKey].filter((key) => typeof key === "string");
888
+ }
889
+ function protectedCacheKeys(config = loadConfig$1()) {
890
+ const keys = /* @__PURE__ */ new Set();
891
+ for (const root of Object.keys(config?.projects ?? {})) {
892
+ const state = readWorkspaceState$1(root);
893
+ for (const name of BUILD_RECORDS) for (const key of recordKeys(state?.[name])) keys.add(key);
894
+ for (const entries of Object.values(readBuildHistory(state))) {
895
+ const key = entries.find((entry) => entry.result === "succeeded")?.cacheKey;
896
+ if (key) keys.add(key);
897
+ }
898
+ }
899
+ for (const platform of ["ios", "android"]) for (const record of readParked(platform, { config })) if (record.cacheKey) keys.add(record.cacheKey);
900
+ for (const lock of listBuildLocks()) if ((lock.alive || lock.unresolved) && lock.key) keys.add(lock.key);
901
+ return keys;
902
+ }
903
+ function liveMetroStores(config = loadConfig$1()) {
904
+ return Object.keys(config?.projects ?? {}).filter((root) => workspaceInUse(root, {
905
+ managedLocks: false,
906
+ hosted: false,
907
+ nativeRun: false
908
+ }).length > 0).map((root) => canonicalPath(metroStoreRoot(root)));
909
+ }
910
+ function cacheEntryProtection(config = loadConfig$1()) {
911
+ try {
912
+ const keys = protectedCacheKeys(config);
913
+ const stores = liveMetroStores(config);
914
+ return (entry) => {
915
+ if (keys.has(basename(entry))) return "a project, parked device or running build still uses this build";
916
+ const path = canonicalPath(entry);
917
+ if (stores.some((store) => path === store || path.startsWith(`${store}${sep}`))) return "its project has a running or unverifiable dev server";
918
+ return null;
919
+ };
920
+ } catch (error) {
921
+ const reason = `cache protection could not be read: ${error instanceof Error ? error.message : String(error)}`;
922
+ return () => reason;
923
+ }
924
+ }
925
+ function maintenancePinned(root) {
926
+ try {
927
+ return maintenanceKeepSetting(resolveProjectSettings(root).settings);
928
+ } catch {
929
+ return true;
930
+ }
931
+ }
932
+ async function maintenancePinnedAsync(root, operation) {
933
+ try {
934
+ return maintenanceKeepSetting((await resolveProjectSettingsAsync(root, {
935
+ env: operation.env,
936
+ signal: operation.signal,
937
+ declareFor: operation
938
+ })).settings);
939
+ } catch (error) {
940
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
941
+ operation.signal.throwIfAborted();
942
+ return true;
943
+ }
944
+ }
945
+ function recentUse(workspace, settings, windowMs = settings.protectRecentHours * 36e5) {
946
+ const since = Date.now() - workspaceLastUsed(workspace);
947
+ if (!Number.isFinite(since) || since >= windowMs) return null;
948
+ return windowMs < 36e5 ? `used within the last ${Math.round(windowMs / 6e4)} minutes` : `used within the last ${windowMs / 36e5} hours`;
949
+ }
950
+ //#endregion
951
+ //#region src/maintenance/activity.ts
952
+ function readDeviceActivity(withSims, now) {
953
+ const sims = withSims ? listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS }) : [];
954
+ return collectOwnedDeviceActivity(loadConfig$1(), sims, now);
955
+ }
956
+ /** A reader that reads device activity at most once per kind and reuses the answer, including a failure. */
957
+ function cachedDeviceActivity(now) {
958
+ const answers = /* @__PURE__ */ new Map();
959
+ return (withSims) => {
960
+ let answer = answers.get(withSims);
961
+ if (!answer) {
962
+ try {
963
+ answer = { devices: readDeviceActivity(withSims, now) };
964
+ } catch (error) {
965
+ answer = { error };
966
+ }
967
+ answers.set(withSims, answer);
968
+ }
969
+ if ("error" in answer) throw answer.error;
970
+ return answer.devices;
971
+ };
972
+ }
973
+ function cachedDeviceActivityAsync(now, env, signal, operation) {
974
+ const answers = /* @__PURE__ */ new Map();
975
+ return (withSims) => {
976
+ let answer = answers.get(withSims);
977
+ if (!answer) {
978
+ answer = (async () => {
979
+ signal?.throwIfAborted();
980
+ const sims = withSims ? await listAllIosSimsAsync({
981
+ timeoutMs: DEVICE_LIST_TIMEOUT_MS,
982
+ signal,
983
+ operation
984
+ }) : [];
985
+ return collectOwnedDeviceActivityAsync(loadConfig$1(), sims, now, env, signal, operation);
986
+ })();
987
+ answers.set(withSims, answer);
988
+ }
989
+ return answer;
990
+ };
991
+ }
992
+ function* workspaceActiveChecks(root, now, windowMs) {
993
+ if (workspaceBuildInProgress(root)) return "a build is in progress";
994
+ const holder = canonicalPath(root);
995
+ for (const { lease } of listLeaseFiles()) if (lease && !leaseIsExpired(lease, now) && canonicalPath(lease.holder) === holder) return `it holds the ${lease.platform} device lease on ${lease.deviceName ?? lease.id}`;
996
+ const owned = ownedPlatforms(root);
997
+ if (owned.ios || owned.android) {
998
+ let devices;
999
+ try {
1000
+ devices = yield owned.ios;
1001
+ } catch {
1002
+ return "its owned devices cannot be listed to prove them idle";
1003
+ }
1004
+ const busy = devices.find((device) => device.project === root && device.activity.state !== "idle");
1005
+ if (busy) return `its owned ${busy.kind} device ${busy.name} is ${busy.activity.state}`;
1006
+ }
1007
+ const activity = workspaceActivity(root, now);
1008
+ if (activity.state !== "idle") return `its activity is ${activity.state}`;
1009
+ const known = [Date.parse(activity.lastActivityAt ?? ""), workspaceLastUsed(root)].filter(Number.isFinite);
1010
+ if (!known.length) return "its last activity is unknown";
1011
+ return now - Math.max(...known) < windowMs ? usedWithin(windowMs) : null;
1012
+ }
1013
+ /** Why a workspace is not inactive for the pressure window, or null when maintenance may stop it. */
1014
+ function workspaceActiveReason(root, now, windowMs, devices = (withSims) => readDeviceActivity(withSims, now)) {
1015
+ const checks = workspaceActiveChecks(root, now, windowMs);
1016
+ let step = checks.next();
1017
+ while (!step.done) try {
1018
+ step = checks.next(devices(step.value));
1019
+ } catch (error) {
1020
+ step = checks.throw(error);
1021
+ }
1022
+ return step.value;
1023
+ }
1024
+ async function workspaceActiveReasonAsync(root, now, windowMs, devices, signal) {
1025
+ const checks = workspaceActiveChecks(root, now, windowMs);
1026
+ let step = checks.next();
1027
+ while (!step.done) try {
1028
+ signal?.throwIfAborted();
1029
+ step = checks.next(await devices(step.value));
1030
+ } catch (error) {
1031
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
1032
+ signal?.throwIfAborted();
1033
+ step = checks.throw(error);
1034
+ }
1035
+ return step.value;
1036
+ }
1037
+ function ownedPlatforms(root) {
1038
+ try {
1039
+ const slots = projectDeviceSlots(loadConfig$1()?.projects?.[root]);
1040
+ return {
1041
+ ios: slots.some(({ platforms }) => platforms.ios?.owned === true),
1042
+ android: slots.some(({ platforms }) => platforms.android?.owned === true)
1043
+ };
1044
+ } catch {
1045
+ return {
1046
+ ios: true,
1047
+ android: true
1048
+ };
1049
+ }
1050
+ }
1051
+ function hasOwnedDevices(root) {
1052
+ try {
1053
+ return projectDeviceSlots(loadConfig$1()?.projects?.[root]).some(({ platforms }) => platforms.ios?.owned === true || platforms.android?.owned === true);
1054
+ } catch {
1055
+ return false;
1056
+ }
1057
+ }
1058
+ function usedWithin(windowMs) {
1059
+ return `used within the last ${Math.round(windowMs / 6e4)} minutes`;
1060
+ }
1061
+ //#endregion
1062
+ //#region src/maintenance/devices.ts
1063
+ const AVD_SIZE_TIMEOUT_MS = 3e4;
1064
+ /** The disk of each owned simulator or emulator of a registered workspace and of each parked device. */
1065
+ async function ownedDeviceUsage(failure, { sims = () => listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS }), avds = () => listAvds({ timeoutMs: DEVICE_LIST_TIMEOUT_MS }), avdDirectory = ownedAvdDirectory, size = (dir) => measuredDirectorySizeAsync(dir, { timeoutMs: AVD_SIZE_TIMEOUT_MS }) } = {}) {
1066
+ const config = loadConfig$1();
1067
+ const owned = [];
1068
+ for (const [root, project] of Object.entries(config?.projects ?? {})) for (const { platforms } of projectDeviceSlots(project)) {
1069
+ if (platforms.ios?.owned && platforms.ios.deviceUdid) owned.push({
1070
+ kind: "ios",
1071
+ id: platforms.ios.deviceUdid,
1072
+ workspace: canonicalPath(root)
1073
+ });
1074
+ if (platforms.android?.owned && platforms.android.avdName) owned.push({
1075
+ kind: "android",
1076
+ id: platforms.android.avdName,
1077
+ workspace: canonicalPath(root)
1078
+ });
1079
+ }
1080
+ for (const record of readParked("ios", { config })) owned.push({
1081
+ kind: "ios",
1082
+ id: record.udid
1083
+ });
1084
+ for (const record of readParked("android", { config })) owned.push({
1085
+ kind: "android",
1086
+ id: record.name
1087
+ });
1088
+ const listed = (read, label) => {
1089
+ if (!owned.length) return [];
1090
+ try {
1091
+ return read();
1092
+ } catch {
1093
+ failure(label);
1094
+ return null;
1095
+ }
1096
+ };
1097
+ const iosRecords = owned.some((device) => device.kind === "ios") ? listed(sims, "the simulator list") : [];
1098
+ const avdNames = owned.some((device) => device.kind === "android") ? listed(avds, "the emulator list") : [];
1099
+ const usage = [];
1100
+ for (const device of owned) {
1101
+ const workspace = device.workspace ? { workspace: device.workspace } : {};
1102
+ if (device.kind === "ios") {
1103
+ const sim = iosRecords?.find((record) => record.udid === device.id);
1104
+ if (sim && typeof sim.dataPathSize === "number") usage.push({
1105
+ kind: "ios",
1106
+ name: sim.name,
1107
+ dir: sim.dataPath ?? device.id,
1108
+ bytes: sim.dataPathSize,
1109
+ ...workspace
1110
+ });
1111
+ continue;
1112
+ }
1113
+ const dir = avdNames?.includes(device.id) ? avdDirectory(device.id) : null;
1114
+ if (!dir) continue;
1115
+ const bytes = await size(dir);
1116
+ if (bytes === null) failure(dir, device.workspace);
1117
+ else usage.push({
1118
+ kind: "android",
1119
+ name: device.id,
1120
+ dir,
1121
+ bytes,
1122
+ ...workspace
1123
+ });
1124
+ }
1125
+ return usage;
1126
+ }
1127
+ async function measureOwnedDevices(now, failure) {
1128
+ return (await ownedDeviceUsage(failure)).map(({ kind, name, dir, bytes, workspace }) => {
1129
+ const size = {
1130
+ name: `${workspace ? "owned" : "parked"} ${kind === "ios" ? "simulator" : "emulator"} ${name}`,
1131
+ category: "owned-devices",
1132
+ dir,
1133
+ bytes,
1134
+ measuredAt: now
1135
+ };
1136
+ if (workspace) size.workspace = workspace;
1137
+ return size;
1138
+ });
1139
+ }
1140
+ function* deviceDeletionChecks({ roots, windowMs, reason, protectedRoot, pinned, sizes, now, extra, env = process.env }) {
1141
+ const plan = {
1142
+ actions: [],
1143
+ blocked: [],
1144
+ skips: []
1145
+ };
1146
+ if (deviceSweepIsScoped(env)) return plan;
1147
+ for (const entry of roots) {
1148
+ const root = canonicalPath(entry.root);
1149
+ if (root === protectedRoot || !hasOwnedDevices(root)) continue;
1150
+ const kept = pinned(root) ? "pinned by maintenance.keep" : now - workspaceLastUsed(root) < windowMs ? usedWithin(windowMs) : workspaceInUse(root).map((why) => `in use: ${why}`)[0] ?? (yield root);
1151
+ if (kept) {
1152
+ plan.skips.push({
1153
+ target: root,
1154
+ reason: kept,
1155
+ workspace: root
1156
+ });
1157
+ continue;
1158
+ }
1159
+ plan.actions.push({
1160
+ kind: "would-delete-devices",
1161
+ target: root,
1162
+ workspace: root,
1163
+ bytes: sizes.filter((size) => size.category === "owned-devices" && size.workspace === root).reduce((sum, size) => sum + size.bytes, 0),
1164
+ reason: entry.reason ? `${entry.reason}, ${reason}` : reason,
1165
+ ...extra
1166
+ });
1167
+ }
1168
+ return plan;
1169
+ }
1170
+ function planDeviceDeletion(options) {
1171
+ const activity = cachedDeviceActivity(options.now);
1172
+ const checks = deviceDeletionChecks(options);
1173
+ let step = checks.next();
1174
+ while (!step.done) step = checks.next(workspaceActiveReason(step.value, options.now, options.windowMs, activity));
1175
+ return step.value;
1176
+ }
1177
+ async function planDeviceDeletionAsync(options, signal, operation) {
1178
+ const activity = cachedDeviceActivityAsync(options.now, options.env ?? process.env, signal, operation);
1179
+ const checks = deviceDeletionChecks(options);
1180
+ let step = checks.next();
1181
+ while (!step.done) {
1182
+ signal.throwIfAborted();
1183
+ step = checks.next(await workspaceActiveReasonAsync(step.value, options.now, options.windowMs, activity, signal));
1184
+ }
1185
+ return step.value;
1186
+ }
1187
+ /**
1188
+ * The workspaces of linked worktrees that gc keeps although their branch is merged or their pull request ended,
1189
+ * such as a dirty one. A worktree gc removes takes its devices with it.
1190
+ */
1191
+ function finishedWorkspaces(sweep, now) {
1192
+ const kept = /* @__PURE__ */ new Set([
1193
+ "not-a-worktree",
1194
+ "bare-repository",
1195
+ "source-checkout",
1196
+ "source-checkout-unknown",
1197
+ "locked",
1198
+ "nested-worktree",
1199
+ "recent-activity",
1200
+ "activity-unknown"
1201
+ ]);
1202
+ if (!sweep) return [];
1203
+ return sweep.worktrees.flatMap((worktree) => {
1204
+ if (worktree.skipped === null || kept.has(worktree.skipCode ?? "")) return [];
1205
+ const merged = worktree.merge?.merged ? worktree.merge.mergedAt : null;
1206
+ const ended = endedPullRequest(worktree.pullRequest);
1207
+ if (merged === null && !ended) return [];
1208
+ if (now - Math.max(merged ?? -Infinity, ended ? ended.endedAt ?? now : -Infinity) < sweep.graceMs) return [];
1209
+ const reason = `its worktree is finished (${worktreeRemovalReason(worktree, sweep)}) but kept`;
1210
+ return worktree.keys.map((root) => ({
1211
+ root: canonicalPath(root),
1212
+ reason
1213
+ }));
1214
+ });
1215
+ }
1216
+ //#endregion
1217
+ //#region src/maintenance/sweep.ts
1218
+ async function planSweep({ settings, sizes, protectedRoot, pinned, now, devices = true, operation }) {
1219
+ const actions = [];
1220
+ const skips = [];
1221
+ const blocked = [];
1222
+ const days = settings.olderThanDays;
1223
+ const outputs = await collectWorkspaceOutputs({
1224
+ olderThan: days,
1225
+ now,
1226
+ measure: false,
1227
+ signal: operation?.signal,
1228
+ operation
1229
+ });
1230
+ for (const entry of outputs.workspaces) {
1231
+ const root = entry.projectRoot === null ? null : canonicalPath(entry.projectRoot);
1232
+ if (root === null || !entry.willClear || root === protectedRoot) continue;
1233
+ if (pinned(root)) {
1234
+ skips.push({
1235
+ target: root,
1236
+ reason: "pinned by maintenance.keep",
1237
+ workspace: root
1238
+ });
1239
+ continue;
1240
+ }
1241
+ const size = sizes.find((candidate) => candidate.workspace === root && candidate.category === "workspace-outputs");
1242
+ actions.push({
1243
+ kind: "would-clear-outputs",
1244
+ target: root,
1245
+ workspace: root,
1246
+ bytes: size?.bytes ?? 0,
1247
+ olderThanDays: days,
1248
+ reason: `idle ${entry.idleDays} days, older than ${days} days (age sweep)`,
1249
+ check: "sweep"
1250
+ });
1251
+ }
1252
+ for (const cache of trimmableCaches(operation?.env, operation?.cwd).filter((candidate) => !isInsideWorkspaces(candidate.dir))) actions.push({
1253
+ kind: "would-trim-cache",
1254
+ target: cacheLabel(cache),
1255
+ dir: cache.dir,
1256
+ bytes: 0,
1257
+ olderThanDays: days,
1258
+ reason: `entries unused for ${days} days (age sweep)`,
1259
+ check: "sweep"
1260
+ });
1261
+ try {
1262
+ const orphans = await collectOrphanedWorkspaces(Object.keys(loadConfig$1()?.projects ?? {}), listMountedVolumes(), {
1263
+ measure: false,
1264
+ operation
1265
+ });
1266
+ for (const orphan of orphans.orphaned) actions.push({
1267
+ kind: "would-remove-orphan",
1268
+ target: orphan.dir,
1269
+ bytes: orphan.bytes ?? 0,
1270
+ reason: `its project ${orphan.projectRoot} no longer exists`,
1271
+ check: "sweep"
1272
+ });
1273
+ for (const kept of orphans.skipped) skips.push({
1274
+ target: kept.dir,
1275
+ reason: kept.reason
1276
+ });
1277
+ } catch (error) {
1278
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
1279
+ operation?.signal.throwIfAborted();
1280
+ blocked.push(`Could not look for orphaned workspace directories: ${error instanceof Error ? error.message : error}`);
1281
+ }
1282
+ if (devices && settings.deviceIdleDays > 0) {
1283
+ const options = {
1284
+ roots: Object.keys(loadConfig$1()?.projects ?? {}).map((root) => ({ root })),
1285
+ windowMs: settings.deviceIdleDays * 864e5,
1286
+ reason: `idle for ${settings.deviceIdleDays} days (age sweep)`,
1287
+ protectedRoot,
1288
+ pinned,
1289
+ sizes,
1290
+ now,
1291
+ extra: {
1292
+ check: "sweep",
1293
+ olderThanDays: settings.deviceIdleDays
1294
+ },
1295
+ env: operation?.env
1296
+ };
1297
+ const reaped = operation ? await planDeviceDeletionAsync(options, operation.signal, operation) : planDeviceDeletion(options);
1298
+ actions.push(...reaped.actions);
1299
+ skips.push(...reaped.skips);
1300
+ }
1301
+ const mounted = listMountedVolumes();
1302
+ for (const cache of readManifest().caches) {
1303
+ if (!isAbsolute(cache.dir) || existsSync(cache.dir) || !isOnMountedVolume(cache.dir, mounted)) continue;
1304
+ actions.push({
1305
+ kind: "would-unregister-cache",
1306
+ target: cache.dir,
1307
+ dir: cache.dir,
1308
+ bytes: 0,
1309
+ reason: "its directory no longer exists",
1310
+ check: "sweep"
1311
+ });
1312
+ }
1313
+ return {
1314
+ actions,
1315
+ blocked,
1316
+ skips
1317
+ };
1318
+ }
1319
+ async function planWorktrees({ protectedRoot, pinned, now, level, graceMs, sizes, protectRecentMs = 0, refresh, operation }) {
1320
+ const sweep = await collectWorktreeSweep({
1321
+ idle: false,
1322
+ olderThan: null,
1323
+ now,
1324
+ removesNoOwnCommits: level === 3,
1325
+ operation,
1326
+ ...graceMs === void 0 ? {} : { graceMs },
1327
+ ...refresh === void 0 ? {} : { refresh }
1328
+ });
1329
+ const actions = [];
1330
+ const skips = [];
1331
+ const keep = [];
1332
+ for (const worktree of sweep.worktrees) {
1333
+ if (worktree.skipped) continue;
1334
+ const path = canonicalPath(worktree.path);
1335
+ const reason = protectedRoot === path || protectedRoot.startsWith(`${path}${sep}`) ? "the command that started this pass runs inside it" : worktree.keys.some(pinned) ? "pinned by maintenance.keep" : null;
1336
+ if (reason) {
1337
+ skips.push({
1338
+ target: worktree.path,
1339
+ reason
1340
+ });
1341
+ continue;
1342
+ }
1343
+ keep.push(worktree);
1344
+ actions.push({
1345
+ kind: "would-remove-worktree",
1346
+ target: worktree.path,
1347
+ bytes: 0,
1348
+ reason: worktreeRemovalReason(worktree, sweep),
1349
+ check: "worktree",
1350
+ ...level ? { level } : {}
1351
+ });
1352
+ }
1353
+ if (sizes) {
1354
+ const options = {
1355
+ roots: finishedWorkspaces(sweep, now),
1356
+ windowMs: Math.max(sweep.graceMs, protectRecentMs),
1357
+ reason: "finished-worktree check",
1358
+ protectedRoot,
1359
+ pinned,
1360
+ sizes,
1361
+ now,
1362
+ extra: { check: "worktree" },
1363
+ env: operation?.env
1364
+ };
1365
+ const devices = operation ? await planDeviceDeletionAsync(options, operation.signal, operation) : planDeviceDeletion(options);
1366
+ actions.push(...devices.actions);
1367
+ skips.push(...devices.skips);
1368
+ }
1369
+ return {
1370
+ actions,
1371
+ blocked: [],
1372
+ skips,
1373
+ sweep: {
1374
+ ...sweep,
1375
+ worktrees: keep
1376
+ }
1377
+ };
1378
+ }
1379
+ //#endregion
1380
+ //#region src/maintenance/artifacts.ts
1381
+ /** Directories that hold only what a build or an install regenerates, relative to an app or package root. */
1382
+ const ARTIFACT_DIRS = {
1383
+ build: [
1384
+ "ios/build",
1385
+ "android/app/build",
1386
+ "android/build",
1387
+ "android/.gradle",
1388
+ ".build"
1389
+ ],
1390
+ dependencies: ["node_modules", "ios/Pods"]
1391
+ };
1392
+ const PACKAGE_PARENTS = ["apps", "packages"];
1393
+ const SCAN_DEPTH = {
1394
+ build: 3,
1395
+ dependencies: 1
1396
+ };
1397
+ const SCAN_LIMIT = 2e4;
1398
+ function isDirectory(path) {
1399
+ try {
1400
+ return lstatSync(path).isDirectory();
1401
+ } catch {
1402
+ return false;
1403
+ }
1404
+ }
1405
+ function artifactWorktrees() {
1406
+ const found = /* @__PURE__ */ new Map();
1407
+ for (const root of candidateRoots()) {
1408
+ const entry = artifactWorktree(root, listWorktrees(root));
1409
+ if (entry) found.set(entry.path, entry);
1410
+ }
1411
+ return [...found.values()];
1412
+ }
1413
+ function artifactWorktree(root, entries, source = sourceCheckoutOf(entries)) {
1414
+ const entry = matchWorktreeEntry(entries, root);
1415
+ if (!entry || entry.bare || "refusal" in source) return null;
1416
+ const keys = reclaimKeys(entry.path).filter((key) => matchWorktreeEntry(entries, key)?.path === entry.path);
1417
+ return {
1418
+ path: entry.path,
1419
+ linked: entry.path !== source.path,
1420
+ keys
1421
+ };
1422
+ }
1423
+ async function artifactWorktreesAsync(signal, operation) {
1424
+ const found = /* @__PURE__ */ new Map();
1425
+ for (const root of await candidateRootsAsync(operation)) {
1426
+ signal.throwIfAborted();
1427
+ const { entries, ...source } = await resolveSourceCheckoutAsync(root, {
1428
+ env: operation?.env,
1429
+ signal,
1430
+ declareFor: operation
1431
+ });
1432
+ const entry = artifactWorktree(root, entries, source);
1433
+ if (entry) found.set(entry.path, entry);
1434
+ }
1435
+ return [...found.values()];
1436
+ }
1437
+ function appRoots(worktree, kind) {
1438
+ const roots = /* @__PURE__ */ new Set([worktree.path, ...worktree.keys]);
1439
+ if (kind === "build") for (const parent of PACKAGE_PARENTS) {
1440
+ const dir = join(worktree.path, parent);
1441
+ try {
1442
+ for (const name of readdirSync(dir)) if (isDirectory(join(dir, name))) roots.add(join(dir, name));
1443
+ } catch {}
1444
+ }
1445
+ return [...roots];
1446
+ }
1447
+ /** The newest mtime under `dir` down to `maxDepth`, or null when the tree is too large or unreadable to tell. */
1448
+ function newestWrite(dir, maxDepth) {
1449
+ let newest = 0;
1450
+ let seen = 0;
1451
+ const walk = (path, depth) => {
1452
+ let stat;
1453
+ try {
1454
+ stat = lstatSync(path);
1455
+ } catch {
1456
+ return false;
1457
+ }
1458
+ if (++seen > SCAN_LIMIT) return false;
1459
+ newest = Math.max(newest, stat.mtimeMs);
1460
+ if (depth === maxDepth || !stat.isDirectory()) return true;
1461
+ let names;
1462
+ try {
1463
+ names = readdirSync(path);
1464
+ } catch {
1465
+ return false;
1466
+ }
1467
+ return names.every((name) => walk(join(path, name), depth + 1));
1468
+ };
1469
+ return walk(dir, 0) ? newest : null;
1470
+ }
1471
+ async function newestWriteAsync(dir, maxDepth, signal) {
1472
+ let newest = 0;
1473
+ let seen = 0;
1474
+ const walk = async (path, depth) => {
1475
+ signal.throwIfAborted();
1476
+ try {
1477
+ const stat = await lstat(path);
1478
+ if (++seen > SCAN_LIMIT) return false;
1479
+ newest = Math.max(newest, stat.mtimeMs);
1480
+ if (depth === maxDepth || !stat.isDirectory()) return true;
1481
+ for (const name of await readdir(path)) if (!await walk(join(path, name), depth + 1)) return false;
1482
+ return true;
1483
+ } catch {
1484
+ signal.throwIfAborted();
1485
+ return false;
1486
+ }
1487
+ };
1488
+ return await walk(dir, 0) ? newest : null;
1489
+ }
1490
+ function keptReason(candidate, kind, { now, windowMs, pinned, locked }, reads) {
1491
+ const guarded = artifactGuard(candidate, kind, pinned, locked);
1492
+ if (guarded) return guarded;
1493
+ const { worktree, path, keys } = candidate;
1494
+ const tracked = reads ? reads.tracked : gitQuiet(worktree, [
1495
+ "ls-files",
1496
+ "--",
1497
+ relative(worktree, path)
1498
+ ]);
1499
+ if (tracked === null) return "git cannot tell whether it holds tracked files";
1500
+ if (tracked.trim()) return "it holds files git tracks";
1501
+ const activity = reads ? reads.activity : worktreeActivityOf(worktree, keys);
1502
+ const written = reads ? reads.written : newestWrite(path, SCAN_DEPTH[kind]);
1503
+ if (!activity || written === null) return "its last activity cannot be read";
1504
+ if (now - Math.max(activity.at, written) < windowMs) return `used within the last ${Math.round(windowMs / 6e4)} minutes`;
1505
+ return null;
1506
+ }
1507
+ function artifactGuard(candidate, kind, pinned, locked) {
1508
+ const { worktree, path, keys } = candidate;
1509
+ if (keys.some(pinned)) return "pinned by maintenance.keep";
1510
+ let real;
1511
+ try {
1512
+ real = canonicalPath(path);
1513
+ } catch {
1514
+ return "its path cannot be resolved";
1515
+ }
1516
+ if (!isDirectory(path) || real !== join(canonicalPath(worktree), relative(worktree, path))) return "it or a directory above it is a link, or it lies outside its worktree";
1517
+ const inUse = keys.flatMap((key) => workspaceInUse(key, {
1518
+ supervisor: kind === "dependencies",
1519
+ nativeRun: key !== locked
1520
+ }));
1521
+ if (inUse.length) return `in use: ${inUse.join("; ")}`;
1522
+ return null;
1523
+ }
1524
+ async function collectArtifactsAsync(kind, options, signal, operation) {
1525
+ const candidates = [];
1526
+ const skips = [];
1527
+ for (const worktree of await artifactWorktreesAsync(signal, operation)) {
1528
+ if (kind === "dependencies" && !worktree.linked) continue;
1529
+ const root = canonicalPath(worktree.path);
1530
+ const protectedHere = options.protectedRoot === root || options.protectedRoot.startsWith(`${root}${sep}`);
1531
+ const bases = /* @__PURE__ */ new Set([worktree.path, ...worktree.keys]);
1532
+ if (kind === "build") for (const parent of PACKAGE_PARENTS) {
1533
+ const dir = join(worktree.path, parent);
1534
+ try {
1535
+ for (const name of await readdir(dir)) {
1536
+ signal.throwIfAborted();
1537
+ if ((await lstat(join(dir, name))).isDirectory()) bases.add(join(dir, name));
1538
+ }
1539
+ } catch {
1540
+ signal.throwIfAborted();
1541
+ }
1542
+ }
1543
+ for (const base of bases) for (const dir of ARTIFACT_DIRS[kind]) {
1544
+ signal.throwIfAborted();
1545
+ const path = join(base, dir);
1546
+ try {
1547
+ await stat(path);
1548
+ } catch {
1549
+ signal.throwIfAborted();
1550
+ continue;
1551
+ }
1552
+ const candidate = {
1553
+ worktree: worktree.path,
1554
+ path,
1555
+ keys: worktree.keys
1556
+ };
1557
+ let reason = protectedHere ? "the command that started this pass runs inside its worktree" : artifactGuard(candidate, kind, options.pinned);
1558
+ if (!reason) {
1559
+ const tracked = await gitQuietAsync(worktree.path, [
1560
+ "ls-files",
1561
+ "--",
1562
+ relative(worktree.path, path)
1563
+ ], {
1564
+ ...operation ? childEnvironment(operation.env) : {},
1565
+ cwd: operation?.cwd,
1566
+ signal,
1567
+ declareFor: operation,
1568
+ killSignal: "SIGKILL"
1569
+ });
1570
+ const untracked = tracked !== null && !tracked.trim();
1571
+ reason = keptReason(candidate, kind, options, {
1572
+ tracked,
1573
+ activity: untracked ? await worktreeActivityOfAsync(worktree.path, worktree.keys, operation) : null,
1574
+ written: untracked ? await newestWriteAsync(path, SCAN_DEPTH[kind], signal) : null
1575
+ });
1576
+ }
1577
+ if (reason) skips.push({
1578
+ target: path,
1579
+ reason
1580
+ });
1581
+ else candidates.push(candidate);
1582
+ }
1583
+ }
1584
+ return {
1585
+ candidates,
1586
+ skips
1587
+ };
1588
+ }
1589
+ /** Regenerable directories of idle worktrees, oldest activity first; dependencies only in linked worktrees. */
1590
+ function collectArtifacts(kind, options) {
1591
+ const candidates = [];
1592
+ const skips = [];
1593
+ for (const worktree of artifactWorktrees()) {
1594
+ if (kind === "dependencies" && !worktree.linked) continue;
1595
+ const root = canonicalPath(worktree.path);
1596
+ const protectedHere = options.protectedRoot === root || options.protectedRoot.startsWith(`${root}${sep}`);
1597
+ for (const base of appRoots(worktree, kind)) for (const dir of ARTIFACT_DIRS[kind]) {
1598
+ const path = join(base, dir);
1599
+ try {
1600
+ statSync(path);
1601
+ } catch {
1602
+ continue;
1603
+ }
1604
+ const candidate = {
1605
+ worktree: worktree.path,
1606
+ path,
1607
+ keys: worktree.keys
1608
+ };
1609
+ const reason = protectedHere ? "the command that started this pass runs inside its worktree" : keptReason(candidate, kind, options);
1610
+ if (reason) skips.push({
1611
+ target: path,
1612
+ reason
1613
+ });
1614
+ else candidates.push(candidate);
1615
+ }
1616
+ }
1617
+ return {
1618
+ candidates,
1619
+ skips
1620
+ };
1621
+ }
1622
+ async function clearArtifact(action, kind, options) {
1623
+ const worktree = options.worktrees.filter((entry) => action.target.startsWith(`${entry.path}${sep}`)).reduce((best, entry) => best && best.path.length >= entry.path.length ? best : entry, null);
1624
+ if (kind === "dependencies" && worktree && !worktree.linked) return {
1625
+ status: "kept",
1626
+ bytes: 0,
1627
+ detail: "it is in the source checkout"
1628
+ };
1629
+ if (!worktree || !ARTIFACT_DIRS[kind].some((dir) => action.target.endsWith(`${sep}${dir.split("/").join(sep)}`))) return {
1630
+ status: "kept",
1631
+ bytes: 0,
1632
+ detail: "it is no longer a regenerable directory of a known worktree"
1633
+ };
1634
+ const root = canonicalPath(worktree.path);
1635
+ if (options.protectedRoot === root || options.protectedRoot.startsWith(`${root}${sep}`)) return {
1636
+ status: "kept",
1637
+ bytes: 0,
1638
+ detail: "the command that started this pass runs inside its worktree"
1639
+ };
1640
+ const candidate = {
1641
+ worktree: worktree.path,
1642
+ path: action.target,
1643
+ keys: worktree.keys
1644
+ };
1645
+ const early = keptReason(candidate, kind, {
1646
+ ...options,
1647
+ now: Date.now()
1648
+ });
1649
+ if (early) return {
1650
+ status: "kept",
1651
+ bytes: 0,
1652
+ detail: early
1653
+ };
1654
+ const before = lowestFree(measureDisk());
1655
+ const [key] = candidate.keys;
1656
+ const remove = () => {
1657
+ const reason = keptReason(candidate, kind, {
1658
+ ...options,
1659
+ now: Date.now(),
1660
+ ...key ? { locked: key } : {}
1661
+ });
1662
+ if (reason) return reason;
1663
+ rmSync(candidate.path, {
1664
+ recursive: true,
1665
+ force: true
1666
+ });
1667
+ return null;
1668
+ };
1669
+ const kept = key ? await withIdleWorkspace(key, remove, {
1670
+ purpose: "maintenance",
1671
+ supervisor: kind === "dependencies"
1672
+ }).then((run) => run.ran ? run.value : `in use: ${run.reasons.join("; ")}`) : remove();
1673
+ if (kept) return {
1674
+ status: "kept",
1675
+ bytes: 0,
1676
+ detail: kept
1677
+ };
1678
+ const freed = Math.max(0, lowestFree(measureDisk()) - before);
1679
+ return {
1680
+ status: "done",
1681
+ bytes: Number.isFinite(freed) ? Math.round(freed * 1024 * 1024) : 0,
1682
+ detail: candidate.path
1683
+ };
1684
+ }
1685
+ //#endregion
1686
+ //#region src/commands/gc/temp-leftovers-guard-env.ts
1687
+ const TEMP_LEFTOVERS_TEST_GUARD_ENV = "STIM_TEST_GUARD_REAL_TMPDIR";
1688
+ //#endregion
1689
+ //#region src/commands/gc/temp-leftovers.ts
1690
+ const TEMP_LEFTOVER_MIN_AGE_HOURS = 24;
1691
+ const MIN_AGE_MS = 864e5;
1692
+ const STIM_PREFIXES = [
1693
+ "stim-claim-",
1694
+ "stim-host-",
1695
+ "stim-fakehome-",
1696
+ "stim-hosted-macos-",
1697
+ "stim-server-",
1698
+ "stim-ws-"
1699
+ ];
1700
+ const STAGING_PREFIX = ".stim-delete-";
1701
+ const TRACE_PATTERN = /^instruments.*\.ktrace$/;
1702
+ const TRACE_PROCESSES = /* @__PURE__ */ new Set(["xctrace", "Instruments"]);
1703
+ const REMEDIES = {
1704
+ stim: "stim gc --delete removes them once they are idle for 24 hours",
1705
+ xctrace: "stim gc --delete removes them once they are idle for 24 hours and no xctrace or Instruments process runs; export a recording you need first",
1706
+ jest: "Jest owns this cache; clear it with `npx jest --clearCache`"
1707
+ };
1708
+ const LABELS = {
1709
+ stim: "Stim temp directories",
1710
+ xctrace: "xctrace recordings (instruments*.ktrace)",
1711
+ jest: "Jest cache"
1712
+ };
1713
+ function kindOf(name, stat, jestName) {
1714
+ const unstaged = name.startsWith(STAGING_PREFIX) ? name.slice(13) : name;
1715
+ if (stat.isDirectory() && STIM_PREFIXES.some((prefix) => unstaged.startsWith(prefix))) return "stim";
1716
+ if ((stat.isFile() || stat.isDirectory()) && TRACE_PATTERN.test(name)) return "xctrace";
1717
+ if (stat.isDirectory() && name === jestName) return "jest";
1718
+ return null;
1719
+ }
1720
+ function errorCode(error) {
1721
+ return error instanceof Error && "code" in error ? String(error.code) : String(error);
1722
+ }
1723
+ async function lstatOrError(path) {
1724
+ try {
1725
+ return await lstat(path);
1726
+ } catch (error) {
1727
+ const code = errorCode(error);
1728
+ return code === "ENOENT" ? void 0 : { error: code };
1729
+ }
1730
+ }
1731
+ async function walk(path, stat, signal, state = {
1732
+ bytes: 0,
1733
+ newest: 0,
1734
+ claimRoots: [],
1735
+ locks: [],
1736
+ unreadable: null
1737
+ }) {
1738
+ signal?.throwIfAborted();
1739
+ state.bytes += stat.size;
1740
+ state.newest = Math.max(state.newest, stat.mtimeMs);
1741
+ if (!stat.isDirectory()) return state;
1742
+ let names;
1743
+ try {
1744
+ names = await readdir(path);
1745
+ } catch (error) {
1746
+ state.unreadable ??= `${path} cannot be listed: ${errorCode(error)}`;
1747
+ return state;
1748
+ }
1749
+ for (const name of names) {
1750
+ const child = join(path, name);
1751
+ const childStat = await lstatOrError(child);
1752
+ if (!childStat) continue;
1753
+ if ("error" in childStat) {
1754
+ state.unreadable ??= `${child} cannot be read: ${childStat.error}`;
1755
+ continue;
1756
+ }
1757
+ if ((name === "exclusive" || name === "shared") && !state.claimRoots.includes(path)) state.claimRoots.push(path);
1758
+ await walk(child, childStat, signal, state);
1759
+ }
1760
+ if (path.endsWith(".lock") && !state.claimRoots.includes(path)) state.locks.push(path);
1761
+ return state;
1762
+ }
1763
+ function canonicalKey(path) {
1764
+ try {
1765
+ const rest = [];
1766
+ let current = resolve(path);
1767
+ while (!lstatSync(current, { throwIfNoEntry: false }) && dirname(current) !== current) {
1768
+ rest.unshift(basename(current));
1769
+ current = dirname(current);
1770
+ }
1771
+ return join(realpathSync(current), ...rest);
1772
+ } catch {
1773
+ return resolve(path);
1774
+ }
1775
+ }
1776
+ function inside(parent, path) {
1777
+ const rel = relative(parent, path);
1778
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
1779
+ }
1780
+ /** Basenames of running executables, or null when the process list cannot be read. */
1781
+ async function runningExecutables(operation, signal) {
1782
+ const out = await runOperationFileQuiet(operation, getExecutor(), "ps", [
1783
+ "-axww",
1784
+ "-o",
1785
+ "comm="
1786
+ ], {
1787
+ timeoutMs: 1e4,
1788
+ cwd: operation?.cwd,
1789
+ signal
1790
+ });
1791
+ if (out === null) return null;
1792
+ return new Set(out.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => basename(line)));
1793
+ }
1794
+ function stimKept(identity, scan, projects) {
1795
+ const registered = projects.find((root) => inside(identity, root) || inside(root, identity));
1796
+ if (registered) return `the registered project ${registered} is in it; gc handles it as a project entry`;
1797
+ if (scan.unreadable) return scan.unreadable;
1798
+ if (scan.locks.length) return `the lock directory ${scan.locks[0]} has no verifiable owner`;
1799
+ for (const root of scan.claimRoots) {
1800
+ const survey = readClaimSet(root);
1801
+ if (survey.live.length) return `it holds a live ownership claim at ${root}`;
1802
+ if (survey.unresolved.length) return `the ownership claim at ${root} cannot be verified`;
1803
+ }
1804
+ return null;
1805
+ }
1806
+ /** The temporary root `env` selects, with Node's `os.tmpdir()` precedence. */
1807
+ function temporaryRoot(env) {
1808
+ if (env === process.env) return tmpdir();
1809
+ const chosen = process.platform === "win32" ? env.TEMP || env.TMP || join(env.SystemRoot || env.windir || "C:\\Windows", "temp") : env.TMPDIR || env.TMP || env.TEMP || "/tmp";
1810
+ const driveRoot = process.platform === "win32" && chosen.endsWith(":\\");
1811
+ return chosen.length > 1 && chosen.endsWith(sep) && !driveRoot ? chosen.slice(0, -1) : chosen;
1812
+ }
1813
+ /**
1814
+ * Recognized leftovers directly inside the user's TMPDIR, never following a symlink: Stim's own temp directories,
1815
+ * xctrace recordings and Jest's cache. Only the first two are ever deleted, and only once nothing in them changed for
1816
+ * 24 hours. Null with STIM_HOME set, because TMPDIR is shared by every Stim home, and for the real TMPDIR a test
1817
+ * process recorded in STIM_TEST_GUARD_REAL_TMPDIR.
1818
+ */
1819
+ async function collectTempLeftovers({ now = Date.now(), env = process.env, signal, operation } = {}) {
1820
+ if (deviceSweepIsScoped(env)) return null;
1821
+ let root;
1822
+ let names;
1823
+ try {
1824
+ root = await realpath(temporaryRoot(env));
1825
+ const guarded = process.env[TEMP_LEFTOVERS_TEST_GUARD_ENV];
1826
+ if (guarded && root === guarded) return null;
1827
+ names = await readdir(root);
1828
+ } catch {
1829
+ return null;
1830
+ }
1831
+ const uid = process.getuid?.();
1832
+ const jestName = uid === void 0 ? null : `jest_${uid.toString(36)}`;
1833
+ const registeredProjects = () => Object.keys(loadConfig$1()?.projects ?? {}).flatMap((key) => [key, canonicalKey(key)]);
1834
+ let projects = registeredProjects();
1835
+ let executables;
1836
+ const traceBusy = async () => {
1837
+ if (executables === void 0) executables = await runningExecutables(operation, signal);
1838
+ const listed = executables;
1839
+ if (listed === null) return "the running processes could not be listed";
1840
+ const running = [...TRACE_PROCESSES].filter((name) => listed.has(name));
1841
+ return running.length ? `${running.join(" and ")} is running` : null;
1842
+ };
1843
+ const judge = async (path, stat, kind, identity = path) => {
1844
+ const scan = await walk(path, stat, signal);
1845
+ const kept = now - scan.newest < MIN_AGE_MS ? `modified within the last ${TEMP_LEFTOVER_MIN_AGE_HOURS}h` : kind === "stim" ? stimKept(identity, scan, projects) : scan.unreadable ?? await traceBusy();
1846
+ return {
1847
+ bytes: scan.bytes,
1848
+ kept
1849
+ };
1850
+ };
1851
+ const recheck = async (staged, original, kind) => {
1852
+ const stat = await lstatOrError(staged);
1853
+ if (!stat || "error" in stat || stat.isSymbolicLink()) return "it changed while it was being deleted";
1854
+ if (kind === "xctrace") executables = void 0;
1855
+ else projects = registeredProjects();
1856
+ return (await judge(staged, stat, kind, original)).kept;
1857
+ };
1858
+ const entries = [];
1859
+ for (const name of names.toSorted()) {
1860
+ const path = join(root, name);
1861
+ const stat = await lstatOrError(path);
1862
+ if (!stat || "error" in stat || stat.isSymbolicLink() || uid !== void 0 && stat.uid !== uid) continue;
1863
+ const kind = kindOf(name, stat, jestName);
1864
+ if (!kind) continue;
1865
+ if (kind === "jest") {
1866
+ const bytes = await directorySizeAsync(path, {
1867
+ timeoutMs: 1e4,
1868
+ signal,
1869
+ operation
1870
+ });
1871
+ entries.push({
1872
+ kind,
1873
+ path,
1874
+ bytes,
1875
+ willDelete: false,
1876
+ kept: "report only: Jest owns it"
1877
+ });
1878
+ continue;
1879
+ }
1880
+ const { bytes, kept } = await judge(path, stat, kind, kind === "stim" && name.startsWith(STAGING_PREFIX) ? join(root, name.slice(13)) : path);
1881
+ entries.push({
1882
+ kind,
1883
+ path,
1884
+ bytes,
1885
+ willDelete: kept === null,
1886
+ kept
1887
+ });
1888
+ }
1889
+ return {
1890
+ root,
1891
+ entries,
1892
+ recheck
1893
+ };
1894
+ }
1895
+ function summarize(report) {
1896
+ return [
1897
+ "stim",
1898
+ "xctrace",
1899
+ "jest"
1900
+ ].flatMap((kind) => {
1901
+ const entries = report.entries.filter((entry) => entry.kind === kind);
1902
+ if (!entries.length) return [];
1903
+ const deleting = entries.filter((entry) => entry.willDelete);
1904
+ const kept = /* @__PURE__ */ new Map();
1905
+ for (const entry of entries) if (entry.kept) kept.set(entry.kept, (kept.get(entry.kept) ?? 0) + 1);
1906
+ return [{
1907
+ kind,
1908
+ count: entries.length,
1909
+ bytes: entries.reduce((sum, entry) => sum + entry.bytes, 0),
1910
+ deleteCount: deleting.length,
1911
+ deleteBytes: deleting.reduce((sum, entry) => sum + entry.bytes, 0),
1912
+ kept: [...kept].map(([reason, count]) => ({
1913
+ reason,
1914
+ count
1915
+ })),
1916
+ remedy: REMEDIES[kind]
1917
+ }];
1918
+ });
1919
+ }
1920
+ function tempLeftoverLines(report) {
1921
+ if (!report?.entries.length) return [];
1922
+ const groups = summarize(report);
1923
+ const total = groups.reduce((sum, group) => sum + group.bytes, 0);
1924
+ const lines = [`TMPDIR leftovers (${formatBytes(total)} in ${report.entries.length}) - ${report.root}:`];
1925
+ for (const group of groups) {
1926
+ lines.push(` ${formatBytes(group.bytes).padStart(10)} ${LABELS[group.kind]}: ${group.count}`);
1927
+ if (group.deleteCount) lines.push(` --delete would DELETE ${group.deleteCount} (${formatBytes(group.deleteBytes)})`);
1928
+ for (const { reason, count } of group.kept) lines.push(` kept ${count}: ${reason}`);
1929
+ lines.push(` remedy: ${group.remedy}`);
1930
+ }
1931
+ return lines;
1932
+ }
1933
+ function tempLeftoverSection(report) {
1934
+ return report ? {
1935
+ root: report.root,
1936
+ groups: summarize(report)
1937
+ } : null;
1938
+ }
1939
+ /** Deletes what a fresh `collectTempLeftovers` marks for deletion. Returns the bytes freed and the failures. */
1940
+ async function deleteTempLeftovers(options = {}, output = GC_OUTPUT) {
1941
+ const report = await collectTempLeftovers(options);
1942
+ const done = {
1943
+ count: 0,
1944
+ bytes: 0,
1945
+ failures: 0
1946
+ };
1947
+ if (!report) return done;
1948
+ for (const entry of report.entries) {
1949
+ if (!entry.willDelete) continue;
1950
+ try {
1951
+ if (entry.kind === "jest") continue;
1952
+ const name = basename(entry.path);
1953
+ const staged = entry.kind === "xctrace" || name.startsWith(STAGING_PREFIX) ? entry.path : join(report.root, `${STAGING_PREFIX}${name}`);
1954
+ const before = staged === entry.path ? null : await report.recheck?.(entry.path, entry.path, entry.kind) ?? null;
1955
+ if (before) {
1956
+ output.record("tempLeftover", "kept", entry.path, {
1957
+ bytes: entry.bytes,
1958
+ detail: before
1959
+ });
1960
+ continue;
1961
+ }
1962
+ if (staged !== entry.path) await rename(entry.path, staged);
1963
+ let kept;
1964
+ try {
1965
+ const original = name.startsWith(STAGING_PREFIX) ? join(report.root, name.slice(13)) : entry.path;
1966
+ kept = await report.recheck?.(staged, original, entry.kind) ?? null;
1967
+ } catch (error) {
1968
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
1969
+ kept = `it could not be checked again: ${errorCode(error)}`;
1970
+ if (options.signal?.aborted) {
1971
+ if (staged !== entry.path) await rename(staged, entry.path).catch(() => {});
1972
+ throw error;
1973
+ }
1974
+ }
1975
+ if (kept) {
1976
+ let detail = kept;
1977
+ try {
1978
+ if (staged !== entry.path) await rename(staged, entry.path);
1979
+ } catch (error) {
1980
+ detail = `${kept}; it stays at ${staged} because it could not be renamed back: ${errorCode(error)}`;
1981
+ output.log(chalk.yellow(`Kept ${entry.path} at ${staged}: ${detail}`));
1982
+ }
1983
+ output.record("tempLeftover", "kept", entry.path, {
1984
+ bytes: entry.bytes,
1985
+ detail
1986
+ });
1987
+ continue;
1988
+ }
1989
+ await removeTemporaryEntryAsync(staged, options.signal);
1990
+ done.count++;
1991
+ done.bytes += entry.bytes;
1992
+ } catch (error) {
1993
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
1994
+ if (options.signal?.aborted) throw error;
1995
+ if (errorCode(error) === "ENOENT") continue;
1996
+ done.failures++;
1997
+ const detail = error instanceof Error ? error.message : String(error);
1998
+ output.log(chalk.red(`Could not delete ${entry.path}: ${detail}`));
1999
+ output.record("tempLeftover", "failed", entry.path, {
2000
+ bytes: entry.bytes,
2001
+ detail
2002
+ });
2003
+ }
2004
+ }
2005
+ if (done.count) {
2006
+ output.log(chalk.green(`Deleted ${plural(done.count, "TMPDIR leftover")} (${formatBytes(done.bytes)}) in ${report.root}`));
2007
+ output.record("tempLeftover", "done", report.root, {
2008
+ bytes: done.bytes,
2009
+ detail: `${plural(done.count, "entry", "entries")} deleted`
2010
+ });
2011
+ }
2012
+ return done;
2013
+ }
2014
+ //#endregion
2015
+ //#region src/maintenance/levels.ts
2016
+ async function planLevel2({ settings, sizes, protectedRoot, pinned, now, operation }) {
2017
+ const windowMs = settings.pressureProtectMinutes * 6e4;
2018
+ const actions = [];
2019
+ const skips = [];
2020
+ const reason = `disk pressure level 2, idle for ${settings.pressureProtectMinutes} minutes`;
2021
+ const outputs = (await collectWorkspaceOutputs({
2022
+ olderThan: null,
2023
+ now,
2024
+ measure: false,
2025
+ signal: operation?.signal,
2026
+ operation
2027
+ })).workspaces.flatMap((entry) => {
2028
+ const root = entry.projectRoot === null ? null : canonicalPath(entry.projectRoot);
2029
+ if (root === null || !entry.willClear || root === protectedRoot || pinned(root)) return [];
2030
+ const used = workspaceLastUsed(root);
2031
+ return Number.isFinite(used) && now - used >= windowMs ? [{
2032
+ root,
2033
+ used
2034
+ }] : [];
2035
+ }).toSorted((a, b) => a.used - b.used);
2036
+ for (const { root } of outputs) {
2037
+ const size = sizes.find((entry) => entry.workspace === root && entry.category === "workspace-outputs");
2038
+ actions.push({
2039
+ kind: "would-clear-outputs",
2040
+ target: root,
2041
+ workspace: root,
2042
+ bytes: size?.bytes ?? 0,
2043
+ reason,
2044
+ level: 2
2045
+ });
2046
+ }
2047
+ if (!deviceSweepIsScoped(operation?.env ?? process.env)) for (const platform of ["ios", "android"]) for (const record of readParked(platform)) actions.push({
2048
+ kind: "would-delete-parked-device",
2049
+ target: platform === "ios" ? record.udid : record.name,
2050
+ bytes: 0,
2051
+ reason: `parked ${platform} device, disk pressure level 2`,
2052
+ level: 2
2053
+ });
2054
+ const leftovers = await collectTempLeftovers({
2055
+ now,
2056
+ env: operation?.env,
2057
+ signal: operation?.signal,
2058
+ operation
2059
+ });
2060
+ const deletable = leftovers?.entries.filter((entry) => entry.willDelete) ?? [];
2061
+ if (leftovers && deletable.length) actions.push({
2062
+ kind: "would-delete-temp-leftovers",
2063
+ target: leftovers.root,
2064
+ bytes: deletable.reduce((sum, entry) => sum + entry.bytes, 0),
2065
+ reason: "disk pressure level 2, Stim temp directories and xctrace recordings unmodified for 24 hours",
2066
+ level: 2
2067
+ });
2068
+ const options = {
2069
+ now,
2070
+ windowMs,
2071
+ protectedRoot,
2072
+ pinned
2073
+ };
2074
+ const build = operation ? await collectArtifactsAsync("build", options, operation.signal, operation) : collectArtifacts("build", options);
2075
+ for (const candidate of build.candidates) actions.push({
2076
+ kind: "would-clear-build-dirs",
2077
+ target: candidate.path,
2078
+ bytes: 0,
2079
+ reason,
2080
+ level: 2
2081
+ });
2082
+ skips.push(...build.skips);
2083
+ const deviceOptions = {
2084
+ roots: Object.keys(loadConfig$1()?.projects ?? {}).map((root) => ({ root })),
2085
+ windowMs,
2086
+ reason,
2087
+ protectedRoot,
2088
+ pinned,
2089
+ sizes,
2090
+ now,
2091
+ extra: { level: 2 },
2092
+ env: operation?.env
2093
+ };
2094
+ const devices = operation ? await planDeviceDeletionAsync(deviceOptions, operation.signal, operation) : planDeviceDeletion(deviceOptions);
2095
+ actions.push(...devices.actions);
2096
+ skips.push(...devices.skips);
2097
+ return {
2098
+ actions,
2099
+ blocked: [],
2100
+ skips
2101
+ };
2102
+ }
2103
+ async function planGradleDaemonStop(blockers, operation) {
2104
+ if (memorySweepIsScoped(operation?.env ?? process.env)) return { kept: "STIM_HOME scopes this config, so the machine-global Gradle daemons that keep it are not stopped" };
2105
+ const report = await collectMemoryReport({
2106
+ watchman: false,
2107
+ gradle: true
2108
+ }, false, operation);
2109
+ const skipped = report.notices.find((notice) => notice.startsWith("memory sweep skipped"));
2110
+ if (skipped) return { kept: `the running Gradle processes could not be checked: ${skipped}` };
2111
+ const plans = new Map(report.processes.map((entry) => [entry.pid, entry]));
2112
+ for (const pid of blockers) {
2113
+ const plan = plans.get(pid);
2114
+ if (!plan || !plan.reclaimable) return { kept: `Gradle pid ${pid} keeps it: ${plan?.detail ?? "it is not a daemon gc --delete --cache gradle-daemons stops"}` };
2115
+ }
2116
+ return { pids: [...blockers] };
2117
+ }
2118
+ async function planLevel3({ settings, sizes, protectedRoot, pinned, now, refresh, operation }) {
2119
+ const windowMs = settings.pressureProtectMinutes * 6e4;
2120
+ const actions = [];
2121
+ const blocked = [];
2122
+ const skips = [];
2123
+ const reason = `disk pressure level 3, idle for ${settings.pressureProtectMinutes} minutes`;
2124
+ for (const size of sizes.filter((entry) => entry.category === "compilation-cache")) if (size.blocked) {
2125
+ skips.push({
2126
+ target: size.dir,
2127
+ reason: size.blocked
2128
+ });
2129
+ blocked.push(`the Swift compilation cache (${formatBytes(size.bytes)}) can only be emptied whole and is kept: ${size.blocked}`);
2130
+ } else actions.push({
2131
+ kind: "would-empty-cache",
2132
+ target: size.dir,
2133
+ dir: size.dir,
2134
+ bytes: size.bytes,
2135
+ reason: "disk pressure level 3",
2136
+ level: 3
2137
+ });
2138
+ const worktrees = settings.removeFinishedWorktrees ? await planWorktrees({
2139
+ protectedRoot,
2140
+ pinned,
2141
+ now,
2142
+ level: 3,
2143
+ graceMs: windowMs,
2144
+ refresh,
2145
+ operation
2146
+ }) : null;
2147
+ actions.push(...worktrees?.actions ?? []);
2148
+ skips.push(...worktrees?.skips ?? []);
2149
+ const activity = operation ? cachedDeviceActivityAsync(now, operation.env, operation.signal, operation) : null;
2150
+ for (const root of Object.keys(loadConfig$1()?.projects ?? {}).map(canonicalPath)) {
2151
+ if (root === protectedRoot || pinned(root)) continue;
2152
+ if (workspaceInUse(root, {
2153
+ managedLocks: false,
2154
+ hosted: false,
2155
+ nativeRun: false
2156
+ }).length === 0) continue;
2157
+ const active = activity ? await workspaceActiveReasonAsync(root, now, windowMs, activity, operation?.signal) : workspaceActiveReason(root, now, windowMs);
2158
+ if (active) skips.push({
2159
+ target: root,
2160
+ reason: active,
2161
+ workspace: root
2162
+ });
2163
+ else actions.push({
2164
+ kind: "would-stop-workspace",
2165
+ target: root,
2166
+ workspace: root,
2167
+ bytes: 0,
2168
+ reason,
2169
+ level: 3
2170
+ });
2171
+ }
2172
+ const artifactOptions = {
2173
+ now,
2174
+ windowMs,
2175
+ protectedRoot,
2176
+ pinned
2177
+ };
2178
+ const dependencies = operation ? await collectArtifactsAsync("dependencies", artifactOptions, operation.signal, operation) : collectArtifacts("dependencies", artifactOptions);
2179
+ for (const candidate of dependencies.candidates) actions.push({
2180
+ kind: "would-delete-dependencies",
2181
+ target: candidate.path,
2182
+ bytes: 0,
2183
+ reason,
2184
+ level: 3
2185
+ });
2186
+ skips.push(...dependencies.skips);
2187
+ if (settings.gradleVersionIdleDays > 0) {
2188
+ const gradle = await collectGradleVersions({
2189
+ operation,
2190
+ olderThanDays: settings.gradleVersionIdleDays,
2191
+ now,
2192
+ measure: false,
2193
+ pastRunningGradle: true
2194
+ });
2195
+ const stop = gradle.gradleRunning && gradle.versions.some((entry) => entry.willDelete) ? await planGradleDaemonStop(gradle.gradleRunningPids, operation) : null;
2196
+ if (stop?.pids) actions.push({
2197
+ kind: "would-stop-gradle-daemons",
2198
+ target: `pid ${stop.pids.join(", ")}`,
2199
+ bytes: 0,
2200
+ reason: "disk pressure level 3, these idle Gradle and Kotlin daemons keep unpinned Gradle versions",
2201
+ level: 3
2202
+ });
2203
+ for (const entry of gradle.versions) if (entry.willDelete && !stop?.kept) actions.push({
2204
+ kind: "would-delete-gradle-version",
2205
+ target: entry.version,
2206
+ dir: gradle.home,
2207
+ bytes: 0,
2208
+ reason: `disk pressure level 3, no registered project pins it and it is unmodified for ${settings.gradleVersionIdleDays} days`,
2209
+ level: 3
2210
+ });
2211
+ else skips.push({
2212
+ target: `Gradle ${entry.version}`,
2213
+ reason: entry.willDelete && stop?.kept || entry.detail || "kept"
2214
+ });
2215
+ }
2216
+ return {
2217
+ actions,
2218
+ blocked,
2219
+ skips,
2220
+ ...worktrees ? { sweep: worktrees.sweep } : {}
2221
+ };
2222
+ }
2223
+ /** Level 2 and 3 actions for a pass that measured `level`; level 1 is the regular plan. */
2224
+ async function planPressureLevels(level, context) {
2225
+ if (level < 1) return {
2226
+ actions: [],
2227
+ blocked: [],
2228
+ skips: []
2229
+ };
2230
+ const second = await planLevel2(context);
2231
+ if (level < 3) return second;
2232
+ const third = await planLevel3(context);
2233
+ return {
2234
+ actions: [...second.actions, ...third.actions],
2235
+ blocked: third.blocked,
2236
+ skips: [...second.skips, ...third.skips],
2237
+ ...third.sweep ? { pressureSweep: third.sweep } : {}
2238
+ };
2239
+ }
2240
+ //#endregion
2241
+ //#region src/maintenance/preview.ts
2242
+ function triggeringRoot(operation) {
2243
+ return canonicalPath(findProjectRoot$1(operation?.cwd ?? process.cwd()) ?? join(configDir(), "maintenance", "no-project"));
2244
+ }
2245
+ async function plannedMaintenance(pressure, sizes, settings, { devices = true, sweep = false, worktrees = false, refresh = true, operation } = {}) {
2246
+ const env = operation?.env ?? process.env;
2247
+ const resolved = resolveBudget({ env });
2248
+ if (resolved.error) throw new Error(resolved.error);
2249
+ const budget = {
2250
+ ...resolved.budget,
2251
+ maxCommittedMemoryMb: 0,
2252
+ maxLiveWorkspaces: 0
2253
+ };
2254
+ const root = triggeringRoot(operation);
2255
+ const floor = Math.max(budget.minFreeDiskMb, budget.hardFloorDiskMb);
2256
+ const low = pressure?.disk.some((disk) => disk.freeMb < floor);
2257
+ const outcome = low ? await enforceBudget({
2258
+ root,
2259
+ note: () => {},
2260
+ dryRun: true,
2261
+ budget,
2262
+ env,
2263
+ cwd: operation?.cwd,
2264
+ signal: operation?.signal,
2265
+ operation
2266
+ }, { volumes: () => pressure.disk }) : null;
2267
+ if (outcome && outcome.status !== "ok") throw new Error(outcome.refusal.message);
2268
+ const outputs = await collectWorkspaceOutputs({
2269
+ olderThan: null,
2270
+ now: Date.now(),
2271
+ measure: false,
2272
+ signal: operation?.signal,
2273
+ operation
2274
+ });
2275
+ const currentSizes = sizes.map((size) => {
2276
+ if (size.category === "compilation-cache") return {
2277
+ ...size,
2278
+ blocked: cacheBlocked(size, discoverCaches(env, operation?.cwd), env)
2279
+ };
2280
+ if (!size.workspace) return size;
2281
+ const current = outputs.workspaces.find((entry) => entry.projectRoot && canonicalPath(entry.projectRoot) === size.workspace);
2282
+ return Object.assign({}, size, { blocked: current?.willClear ? void 0 : current?.keptReason ?? "workspace outputs cannot be resolved" });
2283
+ });
2284
+ const pins = /* @__PURE__ */ new Map();
2285
+ if (operation) {
2286
+ const candidates = await candidateRootsAsync(operation);
2287
+ const roots = /* @__PURE__ */ new Set([
2288
+ ...Object.keys(loadConfig()?.projects ?? {}),
2289
+ ...candidates,
2290
+ ...sizes.flatMap((size) => size.workspace ? [size.workspace] : []),
2291
+ ...outputs.workspaces.flatMap((entry) => entry.projectRoot ? [entry.projectRoot] : [])
2292
+ ]);
2293
+ for (const path of candidates) for (const entry of await listWorktreesAsync(path, {
2294
+ env: operation.env,
2295
+ signal: operation.signal,
2296
+ declareFor: operation
2297
+ })) roots.add(entry.path);
2298
+ for (const path of Array.from(roots, canonicalPath)) roots.add(path);
2299
+ for (const path of roots) pins.set(path, await maintenancePinnedAsync(path, operation));
2300
+ }
2301
+ const pinned = (workspace) => {
2302
+ let value = pins.get(workspace);
2303
+ if (operation) return value ?? true;
2304
+ if (value === void 0) pins.set(workspace, value = maintenancePinned(workspace));
2305
+ return value;
2306
+ };
2307
+ const result = plan({
2308
+ pressure,
2309
+ sizes: currentSizes,
2310
+ settings,
2311
+ budget,
2312
+ diskSteps: outcome?.status === "ok" ? outcome.reclaimed.filter((step) => devices || step.step !== "idle-devices") : [],
2313
+ protectedRoot: root,
2314
+ scoped: Boolean(env.STIM_HOME),
2315
+ projects: Object.keys(loadConfig()?.projects ?? {}).map(canonicalPath),
2316
+ pinned,
2317
+ recentlyUsed: (workspace) => recentUse(workspace, settings)
2318
+ });
2319
+ const merge = (extra) => {
2320
+ for (const action of extra.actions) if (!result.actions.some((entry) => entry.kind === action.kind && entry.target === action.target && (entry.level === action.level || action.kind === "would-delete-devices"))) result.actions.push(action);
2321
+ result.blocked.push(...extra.blocked);
2322
+ result.skips.push(...extra.skips);
2323
+ };
2324
+ if (sweep) merge(await planSweep({
2325
+ settings,
2326
+ sizes,
2327
+ protectedRoot: root,
2328
+ pinned,
2329
+ now: Date.now(),
2330
+ devices,
2331
+ operation
2332
+ }));
2333
+ if (worktrees) {
2334
+ const planned = await planWorktrees({
2335
+ protectedRoot: root,
2336
+ pinned,
2337
+ now: Date.now(),
2338
+ sizes: currentSizes,
2339
+ protectRecentMs: settings.protectRecentHours * 36e5,
2340
+ refresh,
2341
+ operation
2342
+ });
2343
+ merge(planned);
2344
+ result.worktreeSweep = planned.sweep;
2345
+ }
2346
+ if (pressure) {
2347
+ const pressured = await planPressureLevels(diskPressureLevel(lowestFree(pressure.disk), budget, settings), {
2348
+ settings,
2349
+ sizes: currentSizes,
2350
+ protectedRoot: root,
2351
+ pinned,
2352
+ now: Date.now(),
2353
+ refresh,
2354
+ operation
2355
+ });
2356
+ merge(pressured);
2357
+ if (pressured.pressureSweep) result.pressureSweep = pressured.pressureSweep;
2358
+ }
2359
+ if (low) result.skips.push(...outputs.workspaces.filter((entry) => !entry.willClear && entry.keptReason).map((entry) => ({
2360
+ target: entry.dir,
2361
+ reason: entry.keptReason,
2362
+ workspace: entry.projectRoot ?? void 0
2363
+ })));
2364
+ return result;
2365
+ }
2366
+ async function previewMaintenance({ devices = true, refresh = true, operation } = {}) {
2367
+ let settings;
2368
+ try {
2369
+ settings = resolveMaintenanceSettings(void 0, operation?.env ?? process.env);
2370
+ const state = readMaintenanceState();
2371
+ if (settings.invalid && settings.mode === "off") return {
2372
+ mode: "off",
2373
+ pressure: null,
2374
+ actions: [],
2375
+ blocked: [],
2376
+ skips: [],
2377
+ invalid: settings.invalid,
2378
+ note: settings.invalid
2379
+ };
2380
+ if (settings.mode === "off") return {
2381
+ mode: "off",
2382
+ pressure: null,
2383
+ actions: [],
2384
+ blocked: [],
2385
+ skips: [],
2386
+ note: "maintenance.mode is off"
2387
+ };
2388
+ const pressure = operation ? await measurePressureAsync(settings, state?.pressure ?? null, Date.now(), operation) : measurePressure(settings, state?.pressure ?? null, Date.now());
2389
+ return {
2390
+ ...await plannedMaintenance(pressure, state?.sizes ?? [], settings, {
2391
+ devices,
2392
+ sweep: true,
2393
+ refresh,
2394
+ operation
2395
+ }),
2396
+ mode: settings.mode,
2397
+ ...settings.invalid ? { invalid: settings.invalid } : {},
2398
+ pressure,
2399
+ note: settings.invalid ?? (state === null ? "no pass has run yet; sizes have not been measured" : null)
2400
+ };
2401
+ } catch (error) {
2402
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
2403
+ operation?.signal.throwIfAborted();
2404
+ return {
2405
+ mode: settings?.mode ?? "off",
2406
+ ...settings?.invalid ? { invalid: settings.invalid } : {},
2407
+ pressure: null,
2408
+ actions: [],
2409
+ blocked: [String(error)],
2410
+ skips: [],
2411
+ note: "maintenance preview failed"
2412
+ };
2413
+ }
2414
+ }
2415
+ //#endregion
2416
+ //#region src/maintenance/act.ts
2417
+ function freedSince(beforeMb, disk = measureDisk()) {
2418
+ const freed = lowestFree(disk) - beforeMb;
2419
+ return Number.isFinite(freed) && freed > 0 ? Math.round(freed * 1024 * 1024) : 0;
2420
+ }
2421
+ const kept = (detail) => ({
2422
+ status: "kept",
2423
+ bytes: 0,
2424
+ detail
2425
+ });
2426
+ async function quietly(run) {
2427
+ const { log, error } = {
2428
+ log: console.log,
2429
+ error: console.error
2430
+ };
2431
+ const exitCode = process.exitCode;
2432
+ takeGcResults();
2433
+ console.log = () => {};
2434
+ console.error = () => {};
2435
+ try {
2436
+ return {
2437
+ value: await run(),
2438
+ results: takeGcResults()
2439
+ };
2440
+ } finally {
2441
+ console.log = log;
2442
+ console.error = error;
2443
+ process.exitCode = exitCode;
2444
+ takeGcResults();
2445
+ }
2446
+ }
2447
+ function outcomeOf(results, fallbackBytes) {
2448
+ const failed = results.find((result) => result.status === "failed");
2449
+ if (failed) return {
2450
+ status: "failed",
2451
+ bytes: 0,
2452
+ detail: failed.detail ?? `could not process ${failed.label}`
2453
+ };
2454
+ const done = results.filter((result) => result.status === "done");
2455
+ if (done.length) return {
2456
+ status: "done",
2457
+ bytes: done.reduce((sum, result) => sum + (result.bytes ?? 0), 0) || fallbackBytes,
2458
+ detail: done.map((result) => result.label).join(", ")
2459
+ };
2460
+ return kept(results.find((result) => result.detail)?.detail ?? "nothing was eligible when the action ran");
2461
+ }
2462
+ function rootCache(dir) {
2463
+ const name = canonicalPath(dir) === canonicalPath(buildCacheRoot()) ? "Build cache" : canonicalPath(dir) === canonicalPath(metroCacheRoot()) ? "Metro transform caches" : null;
2464
+ return name ? {
2465
+ name,
2466
+ dir,
2467
+ prune: "entries",
2468
+ entriesDepth: name === "Build cache" ? 2 : 3,
2469
+ note: ""
2470
+ } : null;
2471
+ }
2472
+ function cacheOf(action) {
2473
+ const discovered = discoverCaches();
2474
+ if (action.dir) {
2475
+ const dir = canonicalPath(action.dir);
2476
+ return discovered.find((cache) => canonicalPath(cache.dir) === dir) ?? rootCache(action.dir);
2477
+ }
2478
+ return discovered.find((cache) => cacheLabel(cache) === action.target) ?? null;
2479
+ }
2480
+ function trimCache(action, context) {
2481
+ const cache = cacheOf(action);
2482
+ if (!cache) return kept("it is no longer a cache Stim may trim");
2483
+ const [planned] = planCacheEmptying([cache], false);
2484
+ if (!planned || planned.machineGlobal) return kept(planned?.machineGlobal ?? "it is no longer a cache Stim may trim");
2485
+ const aged = action.olderThanDays !== void 0;
2486
+ const result = pruneCache(cache, {
2487
+ olderThanDays: Math.max(action.olderThanDays ?? 0, context.settings.protectRecentHours / 24),
2488
+ byMtime: true,
2489
+ protect: context.protect ??= cacheEntryProtection(),
2490
+ ...aged ? {} : { evictBytes: action.bytes }
2491
+ });
2492
+ if (result.skipped) return kept(result.skipped);
2493
+ if (result.failed) return {
2494
+ status: "failed",
2495
+ bytes: result.bytes,
2496
+ detail: `${result.failed} entries could not be removed`
2497
+ };
2498
+ const protectedNote = result.protectedEntries ? `; ${result.protectedEntries} protected entries kept` : "";
2499
+ if (result.removed === 0) return kept(`no unprotected entry was old enough${protectedNote}`);
2500
+ return {
2501
+ status: "done",
2502
+ bytes: result.bytes,
2503
+ detail: `${result.removed} entries${protectedNote}`,
2504
+ ...!aged && result.bytes < action.bytes ? { shortfall: `${action.target} reached ${result.bytes} of ${action.bytes} bytes${protectedNote}` } : {}
2505
+ };
2506
+ }
2507
+ async function emptyCompilationCache(action, context) {
2508
+ const floor = action.level === 3 ? levelFloors(context.budget, context.settings)[3] : context.budget.hardFloorDiskMb;
2509
+ if (!measureDisk().some((disk) => disk.freeMb < floor)) return kept(action.level === 3 ? "free disk is back above the level 3 floor, and this cache can only be emptied whole" : "free disk is above budget.hardFloorDiskGb, and this cache can only be emptied whole");
2510
+ const dir = sharedCompilationCache();
2511
+ const blocked = cacheBlocked({
2512
+ name: "Swift compilation cache",
2513
+ dir,
2514
+ category: "compilation-cache"
2515
+ });
2516
+ if (blocked) return kept(blocked);
2517
+ const cache = discoverCaches().find((candidate) => canonicalPath(candidate.dir) === canonicalPath(dir));
2518
+ if (cache?.prune !== "atomic") return kept("it is not a registered cache Stim empties whole");
2519
+ const [planned] = planCacheEmptying([{
2520
+ ...cache,
2521
+ bytes: action.bytes
2522
+ }], true);
2523
+ const { results } = await quietly(() => emptyCaches(planned ? [planned] : []));
2524
+ return outcomeOf(results, action.bytes);
2525
+ }
2526
+ async function clearOutputs(action, context) {
2527
+ const root = action.workspace;
2528
+ if (!root) return kept("the action names no workspace");
2529
+ if (root === context.protectedRoot) return kept("the command that started this pass runs in it");
2530
+ if (maintenancePinned(root)) return kept("pinned by maintenance.keep");
2531
+ const recent = recentUse(root, context.settings, protectWindowMs(action, context.settings));
2532
+ if (recent) return kept(recent);
2533
+ const olderThan = action.olderThanDays ?? null;
2534
+ const now = Date.now();
2535
+ const report = collectWorkspaceOutputs({
2536
+ olderThan,
2537
+ now,
2538
+ measure: false
2539
+ });
2540
+ const entry = report.workspaces.find((candidate) => candidate.projectRoot && canonicalPath(candidate.projectRoot) === root);
2541
+ if (!entry) return kept("it has no build outputs any more");
2542
+ const { results } = await quietly(() => clearWorkspaceOutputs({
2543
+ ...report,
2544
+ workspaces: [entry]
2545
+ }, {
2546
+ olderThan,
2547
+ now
2548
+ }));
2549
+ return outcomeOf(results, action.bytes);
2550
+ }
2551
+ async function removeWorktree(action, context) {
2552
+ const sweep = action.level === 3 ? context.pressureSweep : context.worktreeSweep;
2553
+ const worktree = sweep?.worktrees.find((candidate) => candidate.path === action.target);
2554
+ if (!sweep || !worktree) return kept("it is not in this pass's worktree check");
2555
+ const path = canonicalPath(worktree.path);
2556
+ if (context.protectedRoot === path || context.protectedRoot.startsWith(`${path}${sep}`)) return kept("the command that started this pass runs inside it");
2557
+ if (worktree.keys.some(maintenancePinned)) return kept("pinned by maintenance.keep");
2558
+ const before = lowestFree(measureDisk());
2559
+ const { results } = await quietly(() => removeWorktrees({
2560
+ ...sweep,
2561
+ worktrees: [worktree]
2562
+ }));
2563
+ return outcomeOf(results, freedSince(before));
2564
+ }
2565
+ async function removeOrphan(action) {
2566
+ const orphan = (await collectOrphanedWorkspaces(Object.keys(loadConfig$1()?.projects ?? {}), listMountedVolumes())).orphaned.find((candidate) => candidate.dir === action.target);
2567
+ if (!orphan) return kept("it is no longer an orphaned workspace directory");
2568
+ const { results } = await quietly(() => deleteOrphanedWorkspaces([orphan]));
2569
+ return outcomeOf(results, orphan.bytes ?? 0);
2570
+ }
2571
+ async function deleteParkedDevice(action) {
2572
+ if (deviceSweepIsScoped()) return kept("STIM_HOME scopes this config, and devices are machine-wide");
2573
+ const sims = (await collectParkedSims({})).filter((sim) => sim.udid === action.target);
2574
+ const avds = sims.length ? [] : (await collectParkedAvds({})).filter((avd) => avd.name === action.target);
2575
+ if (!sims.length && !avds.length) return kept("it is no longer parked");
2576
+ const before = lowestFree(measureDisk());
2577
+ const { results } = await quietly(async () => await deleteParkedSims(sims) + await deleteParkedAvds(avds));
2578
+ return outcomeOf(results, freedSince(before));
2579
+ }
2580
+ async function deleteOwnedDevicesOf(root, { noneIsKept = false, operation, output = GC_OUTPUT } = {}) {
2581
+ operation?.signal.throwIfAborted();
2582
+ if (deviceSweepIsScoped(operation?.env ?? process.env)) return null;
2583
+ const config = loadConfig$1();
2584
+ const project = config?.projects?.[root];
2585
+ if (!config || !project) return null;
2586
+ const owns = (kind) => projectDeviceSlots(project).some(({ platforms }) => platforms[kind]?.owned === true);
2587
+ let sims = [];
2588
+ let avds = [];
2589
+ try {
2590
+ if (owns("ios")) sims = await listAllIosSimsAsync({
2591
+ timeoutMs: DEVICE_LIST_TIMEOUT_MS,
2592
+ operation,
2593
+ signal: operation?.signal
2594
+ });
2595
+ operation?.signal.throwIfAborted();
2596
+ if (owns("android")) avds = await listAvdsAsync({
2597
+ operation,
2598
+ signal: operation?.signal
2599
+ });
2600
+ operation?.signal.throwIfAborted();
2601
+ } catch (error) {
2602
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
2603
+ operation?.signal.throwIfAborted();
2604
+ return {
2605
+ status: "failed",
2606
+ bytes: 0,
2607
+ detail: `its owned devices could not be listed: ${error instanceof Error ? error.message : String(error)}`
2608
+ };
2609
+ }
2610
+ const devices = findStaleProjectDevices({
2611
+ config: {
2612
+ ...config,
2613
+ projects: { [root]: project }
2614
+ },
2615
+ sims,
2616
+ avds,
2617
+ olderThanDays: 0,
2618
+ lastTouched: () => 0
2619
+ });
2620
+ if (noneIsKept && devices.length === 0) return kept("it owns no listed devices any more");
2621
+ const failures = await deleteProjectDevices([], devices, [], output, operation);
2622
+ return failures ? {
2623
+ status: "failed",
2624
+ bytes: 0,
2625
+ detail: `${failures} owned devices could not be deleted`
2626
+ } : null;
2627
+ }
2628
+ async function stopWorkspace(action, context) {
2629
+ const root = action.workspace ?? action.target;
2630
+ if (root === context.protectedRoot) return kept("the command that started this pass runs in it");
2631
+ if (maintenancePinned(root)) return kept("pinned by maintenance.keep");
2632
+ const windowMs = protectWindowMs(action, context.settings);
2633
+ const active = workspaceActiveReason(root, Date.now(), windowMs);
2634
+ if (active) return kept(active);
2635
+ const before = lowestFree(measureDisk());
2636
+ const run = await withIdleWorkspace(root, async () => {
2637
+ if (workspaceActiveReason(root, Date.now(), windowMs)) return kept("it became active");
2638
+ const metro = await stopOwnedMetro(root, {
2639
+ by: "automatic maintenance",
2640
+ workspace: context.protectedRoot
2641
+ });
2642
+ if ("reason" in metro) return kept(`its dev server: ${metro.reason}`);
2643
+ return (await quietly(() => deleteOwnedDevicesOf(root))).value;
2644
+ }, {
2645
+ purpose: "automatic maintenance",
2646
+ supervisor: false
2647
+ });
2648
+ if (!run.ran) return kept(`in use: ${run.reasons.join("; ")}`);
2649
+ const bytes = freedSince(before);
2650
+ return run.value ? {
2651
+ ...run.value,
2652
+ bytes
2653
+ } : {
2654
+ status: "done",
2655
+ bytes,
2656
+ detail: root
2657
+ };
2658
+ }
2659
+ /**
2660
+ * Deletes the owned simulators and emulators of a workspace that stays idle for `windowMs`, under its native-run
2661
+ * claim, without stopping anything: a running dev server, lease, lock or driver keeps every device.
2662
+ */
2663
+ async function deleteIdleWorkspaceDevices(root, { windowMs, protectedRoot, purpose, operation, output }) {
2664
+ operation?.signal.throwIfAborted();
2665
+ if (root === protectedRoot) return kept("the command that started this pass runs in it");
2666
+ if (operation ? await maintenancePinnedAsync(root, operation) : maintenancePinned(root)) return kept("pinned by maintenance.keep");
2667
+ if (deviceSweepIsScoped(operation?.env ?? process.env)) return kept("STIM_HOME scopes this config, and devices are machine-wide");
2668
+ const activeReason = async () => {
2669
+ const now = Date.now();
2670
+ if (!operation) return workspaceActiveReason(root, now, windowMs);
2671
+ const reason = await workspaceActiveReasonAsync(root, now, windowMs, cachedDeviceActivityAsync(now, operation.env, operation.signal, operation), operation.signal);
2672
+ operation.signal.throwIfAborted();
2673
+ return reason;
2674
+ };
2675
+ const active = await activeReason();
2676
+ if (active) return kept(active);
2677
+ const before = lowestFree(operation ? await measureDiskAsync(operation) : measureDisk());
2678
+ const run = await withIdleWorkspace(root, async () => await activeReason() ? kept("it became active") : deleteOwnedDevicesOf(root, {
2679
+ noneIsKept: true,
2680
+ operation,
2681
+ output
2682
+ }), {
2683
+ purpose,
2684
+ operation
2685
+ });
2686
+ if (!run.ran) return kept(`in use: ${run.reasons.join("; ")}`);
2687
+ const bytes = operation ? freedSince(before, await measureDiskAsync(operation)) : freedSince(before);
2688
+ return run.value ? {
2689
+ ...run.value,
2690
+ bytes
2691
+ } : {
2692
+ status: "done",
2693
+ bytes,
2694
+ detail: root
2695
+ };
2696
+ }
2697
+ function deviceWindowMs(action, context) {
2698
+ if (action.level !== void 0) return protectWindowMs(action, context.settings);
2699
+ if (action.olderThanDays !== void 0) return action.olderThanDays * 864e5;
2700
+ return Math.max(context.worktreeSweep?.graceMs ?? 0, context.settings.protectRecentHours * 36e5);
2701
+ }
2702
+ async function deleteDevices(action, context) {
2703
+ const { value } = await quietly(() => deleteIdleWorkspaceDevices(action.workspace ?? action.target, {
2704
+ windowMs: deviceWindowMs(action, context),
2705
+ protectedRoot: context.protectedRoot,
2706
+ purpose: "automatic maintenance"
2707
+ }));
2708
+ return value;
2709
+ }
2710
+ async function clearArtifacts(action, context) {
2711
+ return clearArtifact(action, action.kind === "would-delete-dependencies" ? "dependencies" : "build", {
2712
+ windowMs: protectWindowMs(action, context.settings),
2713
+ protectedRoot: context.protectedRoot,
2714
+ pinned: maintenancePinned,
2715
+ worktrees: context.worktrees ??= artifactWorktrees()
2716
+ });
2717
+ }
2718
+ const GRADLE_EXIT_WAIT_MS = 3e3;
2719
+ async function stopGradleDaemonsAction(action) {
2720
+ const targets = [...action.target.matchAll(/\d+/g)].map((match) => Number(match[0]));
2721
+ const { value: failures, results } = await quietly(() => stopIdleGradleDaemons(targets));
2722
+ const daemons = results.filter((result) => result.kind === "gradleDaemon" || result.kind === "kotlinDaemon");
2723
+ const stopped = daemons.filter((result) => result.status === "done");
2724
+ if (failures) return {
2725
+ status: "failed",
2726
+ bytes: 0,
2727
+ detail: `${plural(failures, "daemon")} could not be stopped`
2728
+ };
2729
+ if (stopped.length === 0) return kept(daemons.find((result) => result.detail)?.detail ?? "no daemon was stopped");
2730
+ const left = await waitForGradleExit(stopped.flatMap((result) => result.id && /^\d+$/.test(result.id) ? [Number(result.id)] : []), GRADLE_EXIT_WAIT_MS);
2731
+ const labels = stopped.map((result) => result.label).join(", ");
2732
+ return {
2733
+ status: "done",
2734
+ bytes: 0,
2735
+ detail: left.length ? `${labels}; pid ${left.join(", ")} had not exited yet` : labels
2736
+ };
2737
+ }
2738
+ async function deleteGradleVersionAction(action, context) {
2739
+ const before = lowestFree(measureDisk());
2740
+ try {
2741
+ const result = await deleteGradleVersion(action.target, { olderThanDays: context.settings.gradleVersionIdleDays });
2742
+ return result.deleted ? {
2743
+ status: "done",
2744
+ bytes: freedSince(before),
2745
+ detail: result.detail
2746
+ } : kept(result.detail);
2747
+ } catch (error) {
2748
+ return {
2749
+ status: "failed",
2750
+ bytes: freedSince(before),
2751
+ detail: error instanceof Error ? error.message : String(error)
2752
+ };
2753
+ }
2754
+ }
2755
+ async function deleteTempLeftoversAction() {
2756
+ const { value: done } = await quietly(() => deleteTempLeftovers());
2757
+ if (done.failures) return {
2758
+ status: "failed",
2759
+ bytes: done.bytes,
2760
+ detail: `${done.failures} could not be deleted`
2761
+ };
2762
+ return done.count ? {
2763
+ status: "done",
2764
+ bytes: done.bytes,
2765
+ detail: `${done.count} deleted`
2766
+ } : kept("none is idle now");
2767
+ }
2768
+ function unregisterCache(action) {
2769
+ const dir = action.dir ?? action.target;
2770
+ if (existsSync(dir) || !isOnMountedVolume(dir, listMountedVolumes()) || !readManifest().caches.some((c) => c.dir === dir)) return kept("it can no longer be shown to be a stale registration");
2771
+ return unregister(dir) ? {
2772
+ status: "done",
2773
+ bytes: 0,
2774
+ detail: dir
2775
+ } : kept("it was already unregistered");
2776
+ }
2777
+ async function executeAction(action, context) {
2778
+ switch (action.kind) {
2779
+ case "would-clear-outputs": return clearOutputs(action, context);
2780
+ case "would-trim-cache": return trimCache(action, context);
2781
+ case "would-empty-cache": return emptyCompilationCache(action, context);
2782
+ case "would-remove-worktree": return removeWorktree(action, context);
2783
+ case "would-remove-orphan": return removeOrphan(action);
2784
+ case "would-unregister-cache": return unregisterCache(action);
2785
+ case "would-delete-parked-device": return deleteParkedDevice(action);
2786
+ case "would-delete-devices": return deleteDevices(action, context);
2787
+ case "would-stop-workspace": return action.level === 3 ? stopWorkspace(action, context) : kept("automatic maintenance stops dev servers only at disk pressure level 3");
2788
+ case "would-clear-build-dirs":
2789
+ case "would-delete-dependencies": return clearArtifacts(action, context);
2790
+ case "would-stop-gradle-daemons": return stopGradleDaemonsAction(action);
2791
+ case "would-delete-gradle-version": return deleteGradleVersionAction(action, context);
2792
+ case "would-delete-temp-leftovers": return deleteTempLeftoversAction();
2793
+ default: return kept("automatic maintenance does not stop devices or dev servers yet");
2794
+ }
2795
+ }
2796
+ function diskRecovered(budget) {
2797
+ const floor = Math.max(budget.minFreeDiskMb, budget.hardFloorDiskMb);
2798
+ return measureDisk().every((disk) => disk.freeMb >= floor);
2799
+ }
2800
+ //#endregion
2801
+ export { collectWorktreeSweep as _, previewMaintenance as a, worktreeRemovalReason as b, deleteTempLeftovers as c, ARTIFACT_DIRS as d, appRoots as f, candidateRootsAsync as g, ownedDeviceUsage as h, plannedMaintenance as i, tempLeftoverLines as l, measureOwnedDevices as m, diskRecovered as n, triggeringRoot as o, finishedWorkspaces as p, executeAction as r, collectTempLeftovers as s, deleteIdleWorkspaceDevices as t, tempLeftoverSection as u, removeWorktrees as v, worktreePullRequestNote as y };