stim 1.20.2 → 1.21.0

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