stim 1.21.0 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +60 -7
  2. package/dist/{acquire-artifact-CD4lbiWE.mjs → acquire-artifact-Co2k4f7O.mjs} +21 -3
  3. package/dist/{act-DHAw2A2S.mjs → act-DsuqEkiM.mjs} +298 -164
  4. package/dist/{activity-BzVfHSrY.mjs → activity-CMLmzp7o.mjs} +33 -12
  5. package/dist/{agent-device-usage-output-zh1vhbLp.mjs → agent-device-usage-output-DUFaTMdd.mjs} +7 -7
  6. package/dist/{android-BLgbNqox.mjs → android-ByRMUoiJ.mjs} +66 -52
  7. package/dist/{android-DUYkNF4u.mjs → android-C7sVmyI9.mjs} +160 -59
  8. package/dist/{android-CV_GCELZ.mjs → android-DGkNqa_c.mjs} +5 -5
  9. package/dist/{android-BEJ8Jpl7.mjs → android-DR5lZ2Az.mjs} +2 -2
  10. package/dist/android-cas-compiler.mjs +1 -1
  11. package/dist/{android-cas-Ct5b1xCK.mjs → android-cas-ngferVq2.mjs} +3 -3
  12. package/dist/{client-B7QVOlCr.d.mts → android-project-DVWyCbWU.d.mts} +145 -7
  13. package/dist/api.d.mts +132 -118
  14. package/dist/api.mjs +239 -42
  15. package/dist/{artifact-B8T3S5gi.mjs → artifact-8EY8SAPw.mjs} +40 -35
  16. package/dist/{artifact-DAuZ-FNU.mjs → artifact-DJPy518Y.mjs} +69 -40
  17. package/dist/artifact-platform-CXcmX_j3.mjs +27 -0
  18. package/dist/{browser-web-QQzVjz3O.mjs → browser-web-Bcv99PBM.mjs} +187 -59
  19. package/dist/{budget-Bs6SxxzO.mjs → budget-Cvwn54qV.mjs} +830 -699
  20. package/dist/{build-CoPzQRNw.mjs → build-CabC3G23.mjs} +34 -27
  21. package/dist/{build-cache-DmyR9Zzc.mjs → build-cache-Dv7RN200.mjs} +4 -4
  22. package/dist/{build-plan-CaFskB7_.mjs → build-plan-DUC8Ka9y.mjs} +74 -14
  23. package/dist/{cache-manifest-DkkqguoK.mjs → cache-manifest-DSeQjt-Q.mjs} +1 -1
  24. package/dist/cache-manifest.mjs +1 -1
  25. package/dist/{ccache-BJRZ9pG3.mjs → ccache-Btwc36-I.mjs} +6 -6
  26. package/dist/{cdp-CWmIUMbw.mjs → cdp-3fwICjzi.mjs} +1 -1
  27. package/dist/{chrome-B6tfCta1.mjs → chrome-Bbw689-6.mjs} +1 -1
  28. package/dist/{cli-0DYf8QhQ.mjs → cli-CHZa9Lvk.mjs} +20 -20
  29. package/dist/cli.mjs +1 -1
  30. package/dist/{client-UXD70ZbM.mjs → client-OyRoEuSr.mjs} +218 -47
  31. package/dist/collector-run.d.mts +1 -1
  32. package/dist/collector-run.mjs +6 -6
  33. package/dist/{command-output-B1UrjZHL.mjs → command-output-gwpm28fu.mjs} +1 -1
  34. package/dist/{config-CYcBF478.mjs → config-DlrsduwG.mjs} +17 -11
  35. package/dist/{created-devices-CqeBlrEn.mjs → created-devices-x9rN6bhs.mjs} +13 -3
  36. package/dist/{dependency-state-BbKQW83h.mjs → dependency-state-1I9KZT1Y.mjs} +2 -2
  37. package/dist/{detached-entry-DFRyG1_o.mjs → detached-entry-BwNEtIrP.mjs} +1 -1
  38. package/dist/{dev-client-UOeESKm6.mjs → dev-client-BSDR5h3F.mjs} +2 -2
  39. package/dist/{device-ExJq8Ei7.mjs → device-B-L3Fbxy.mjs} +71 -14
  40. package/dist/{device-DCxH7_By.mjs → device-DklWZLqb.mjs} +16 -16
  41. package/dist/{device-capacity-CMlqEcvw.mjs → device-capacity-DK5_EwRS.mjs} +9 -10
  42. package/dist/device-host-worker.mjs +25 -32
  43. package/dist/{device-ios-DRUeKaFV.mjs → device-ios-DoB6KFCO.mjs} +39 -16
  44. package/dist/{device-lease-run-C8raGFoP.mjs → device-lease-run-xJPQ_QQG.mjs} +5 -5
  45. package/dist/{device-pool-DMRpVX9a.mjs → device-pool-BMkdsefq.mjs} +3 -3
  46. package/dist/{doctor-DTvaDOda.mjs → doctor-Bo7BXLux.mjs} +24 -37
  47. package/dist/{doctor-Dhqebm0q.mjs → doctor-D5_EzwfA.mjs} +19 -16
  48. package/dist/doctor-fingerprint-DZLoYFVb.mjs +16 -0
  49. package/dist/doctor-fingerprint.mjs +1 -1
  50. package/dist/{doctor-BLHCYxzi.mjs → doctor-thjWAV0z.mjs} +1 -1
  51. package/dist/{doctor-watchman-Cv1F10Kj.mjs → doctor-watchman-N2wDotwQ.mjs} +5 -5
  52. package/dist/{eas-build-BomfRnFT.mjs → eas-build-CwknaQYj.mjs} +4 -4
  53. package/dist/{eas-fallback-DJwZpy0c.mjs → eas-fallback-CSgAulr5.mjs} +27 -18
  54. package/dist/{eas-session-ledger-op6A54yi.mjs → eas-session-ledger-DlY_serQ.mjs} +8 -7
  55. package/dist/{error-diagnostics-Bf0rZvG9.mjs → error-diagnostics-DBWO2rxE.mjs} +10 -10
  56. package/dist/{exec-_UbtCQcG.mjs → exec-DEoAhcze.mjs} +155 -47
  57. package/dist/{exec-H20xSg2T.d.mts → exec-DnYc4HGH.d.mts} +17 -4
  58. package/dist/{gc-_-dCXdyL.mjs → gc-CXxN-4SE.mjs} +587 -313
  59. package/dist/gc-DnNDQyaP.mjs +3 -0
  60. package/dist/{guide-Yqwt2bd6.mjs → guide-Br0LEfKy.mjs} +239 -43
  61. package/dist/{hosted-android-Bw-honsd.mjs → hosted-android-B-9f-jkd.mjs} +6 -6
  62. package/dist/{hosted-client-DB_gBPfY.mjs → hosted-client-DZAPrcro.mjs} +4 -4
  63. package/dist/{hosted-ios-CUvOf4Vr.mjs → hosted-ios-CsCS21hn.mjs} +1 -1
  64. package/dist/{hosted-logs-DL_P5AdN.mjs → hosted-logs-FIRx9X2f.mjs} +5 -5
  65. package/dist/{hosted-macos-QI141yKk.mjs → hosted-macos-_awYSpfv.mjs} +21 -12
  66. package/dist/{hosted-native-Ksyjgzq-.mjs → hosted-native-BdzE668-.mjs} +27 -14
  67. package/dist/{idle-BVeRGroT.d.mts → idle-C4cZ757K.d.mts} +3 -3
  68. package/dist/{idle-shutdown-DaKmZi0z.mjs → idle-shutdown-DDg3SdvY.mjs} +7 -7
  69. package/dist/ios-CDvlmUs7.d.mts +24 -0
  70. package/dist/{ios-CQ68hG9s.mjs → ios-Ds1clbIt.mjs} +8 -8
  71. package/dist/{ios-device-CwFMehUS.mjs → ios-device--h_-8ENa.mjs} +3 -3
  72. package/dist/{ios-device-CXGP1bsD.mjs → ios-device-BXZ0c7Kb.mjs} +1 -1
  73. package/dist/{ios-CpByZy12.mjs → ios-hcBpnEGk.mjs} +81 -34
  74. package/dist/{ios-ubFsBzwq.mjs → ios-pm4nL6By.mjs} +59 -50
  75. package/dist/{ios-project-WuwwBdvW.mjs → ios-project-CZhKODF3.mjs} +5 -5
  76. package/dist/{ios-signing-DTTIChGs.mjs → ios-signing-BtAffm0A.mjs} +2 -2
  77. package/dist/{ios-state-DvFX-K_l.mjs → ios-state-Dtw5K2aI.mjs} +1 -1
  78. package/dist/{ios-qNJt4dFm.mjs → ios-xtlRHaeB.mjs} +1 -1
  79. package/dist/{js-swap-Cc1dttps.mjs → js-swap-Bwhh4S7V.mjs} +7 -29
  80. package/dist/{launch-Zq2mOSDy.mjs → launch-BjxemNpK.mjs} +40 -39
  81. package/dist/{launch-CD1yVc7U.mjs → launch-CCzXlc8r.mjs} +16 -16
  82. package/dist/{logs-BS00Anxf.mjs → logs-BfwwadsZ.mjs} +151 -24
  83. package/dist/{logs-CMm05HxN.mjs → logs-jTS_lHSo.mjs} +10 -17
  84. package/dist/{macos-B65--pPd.mjs → macos-DVlSSyUg.mjs} +1 -1
  85. package/dist/{macos-C-0qJ6zq.mjs → macos-NfV2D7dO.mjs} +143 -46
  86. package/dist/macos-run.mjs +1 -1
  87. package/dist/maintenance-run.mjs +8 -8
  88. package/dist/{metro-DEXpIhHC.mjs → metro-DiXtA3Ty.mjs} +59 -37
  89. package/dist/{metro-gateway-BMJNlj3E.mjs → metro-gateway-CKZqKqx7.mjs} +120 -22
  90. package/dist/{named-ports-BWTzt6mu.mjs → named-ports-DkEzOfzO.mjs} +40 -33
  91. package/dist/{native-android-JACyjTZs.mjs → native-android-CqgnOgsT.mjs} +43 -242
  92. package/dist/native-gradle-artifact-inputs-DHFnmrVf.mjs +270 -0
  93. package/dist/{native-gradle-inputs-Bep9JcSF.mjs → native-gradle-inputs-Dnh84Zbd.mjs} +3 -129
  94. package/dist/{native-run-DY_RLx3C.mjs → native-run-NwUuxbkg.mjs} +4 -5
  95. package/dist/{native-runtime-Dg1Q7tfF.mjs → native-runtime-B7KIafpM.mjs} +8 -7
  96. package/dist/native-source-Y0yLhFyQ.mjs +133 -0
  97. package/dist/{native-xcode-inputs-CB2iQeWq.mjs → native-xcode-inputs-D_VOfFL7.mjs} +2 -3
  98. package/dist/{native-xcode-ios-CXX39F7o.mjs → native-xcode-ios-BYDOHWLN.mjs} +27 -32
  99. package/dist/offload-worker.d.mts +65 -2
  100. package/dist/offload-worker.mjs +305 -73
  101. package/dist/operation-exec-BW0em61G.mjs +171 -0
  102. package/dist/{ownership-DEmM5vhh.mjs → ownership-C5fo5Edb.mjs} +3 -3
  103. package/dist/{page-CPaHeYqj.mjs → page-DBL2PiUU.mjs} +8 -4
  104. package/dist/{placement-log-BqGbTa61.mjs → placement-log-BnGgJRRZ.mjs} +1 -1
  105. package/dist/{plan-placement-Bwj-IjY1.mjs → plan-placement-DIsMtN-d.mjs} +12 -8
  106. package/dist/{ports-D-MmCTdv.mjs → ports-Cy1RkmuQ.mjs} +3 -3
  107. package/dist/{prebuild-uD_ipCxF.mjs → prebuild-D4JoEAEs.mjs} +49 -14
  108. package/dist/{project-BZNjmW_8.mjs → project-wM_XFTvE.mjs} +16 -16
  109. package/dist/{pull-request-Lfetn8Kr.mjs → pull-request-W1IuLXDt.mjs} +102 -20
  110. package/dist/{pull-request-zjCSAryH.d.mts → pull-request-eODyIqZF.d.mts} +6 -2
  111. package/dist/pull-requests.d.mts +2 -2
  112. package/dist/pull-requests.mjs +2 -2
  113. package/dist/{react-native-android-DuzH0cn3.mjs → react-native-android-6OZ7MSM7.mjs} +71 -48
  114. package/dist/{react-native-build-rhk4smwQ.mjs → react-native-build-Dvd8hC21.mjs} +8 -8
  115. package/dist/{react-native-doctor-CQpuIMso.mjs → react-native-doctor-DsoSF1Y9.mjs} +7 -7
  116. package/dist/{react-native-ios-Bhq1rJeV.mjs → react-native-ios-4u1Rn-GR.mjs} +40 -26
  117. package/dist/{recordings-DYteQSu4.mjs → recordings-CQmfzAh7.mjs} +1 -1
  118. package/dist/registration-BcRgF6Ox.mjs +51 -0
  119. package/dist/{reload-CmTRFJaX.mjs → reload-BzkYabB-.mjs} +16 -15
  120. package/dist/{reload-DzjSl5P_.mjs → reload-D7AVlb8N.mjs} +1 -1
  121. package/dist/{remote-ByYkXdGD.mjs → remote-DjGDmlTZ.mjs} +18 -15
  122. package/dist/{result-DNiS-tTP.mjs → result-D2Ykz_iD.mjs} +24 -23
  123. package/dist/{run-D23HmJMs.mjs → run-DidHghD1.mjs} +3 -2
  124. package/dist/{selection-CSl9i5qU.mjs → selection-DSMbWEX2.mjs} +6 -5
  125. package/dist/{server-bare-CxTrvDg8.mjs → server-bare-CafzuyFB.mjs} +2 -2
  126. package/dist/{server-command-CtzloH3v.mjs → server-command-C6QjeupS.mjs} +5 -5
  127. package/dist/{server-expo-DT_F_uVI.mjs → server-expo-CrjB29pq.mjs} +7 -5
  128. package/dist/server-expo-D58c9L0M.mjs +2 -0
  129. package/dist/{settings-CsJDEXgK.mjs → settings-C7O8A6ZI.mjs} +223 -243
  130. package/dist/{settings-T1gtc3bf.mjs → settings-DpR-1Wnn.mjs} +1 -1
  131. package/dist/{settings-write-HyVZ8eQ8.mjs → settings-write-CzD60rOD.mjs} +7 -6
  132. package/dist/settings.schema.json +13 -0
  133. package/dist/{simslim-Cgk17SXV.mjs → simslim-BNckWpXi.mjs} +2 -2
  134. package/dist/{slot-launch-DF8VfT64.mjs → slot-launch-CbfmNgM6.mjs} +6 -6
  135. package/dist/{stage-BIS0ydsV.mjs → stage-Bps5oius.mjs} +21 -4
  136. package/dist/{start-RChynRDp.mjs → start-Br-oxqMD.mjs} +1 -1
  137. package/dist/{start-7lpH7hmN.mjs → start-EojN4WFz.mjs} +622 -393
  138. package/dist/{state-C4ZtUisW.mjs → state--smjFSTL.mjs} +22 -7
  139. package/dist/{state-XMziexv2.mjs → state-08I3hw3_.mjs} +2 -2
  140. package/dist/{stats-53T7OTb9.mjs → stats-D3GerlFW.mjs} +5 -5
  141. package/dist/{stats-report-BFfMpWWF.mjs → stats-report-BzaPrRm6.mjs} +1 -1
  142. package/dist/{status-DqEUSSxs.mjs → status-BRROykC8.mjs} +2 -2
  143. package/dist/{status-CSi6tlRq.mjs → status-EvrK72L2.mjs} +1 -1
  144. package/dist/{status-BL7jVyAB.mjs → status-WBDajew2.mjs} +43 -29
  145. package/dist/{status-watch-GDCfqpTE.mjs → status-watch-Bg_u-pwR.mjs} +48 -16
  146. package/dist/status-worker.mjs +1 -1
  147. package/dist/{stim-desktop-C12we7lz.mjs → stim-desktop-D5OGpYZM.mjs} +2 -2
  148. package/dist/{stop-Bk_1qiV6.mjs → stop-DTZ9uTw3.mjs} +2 -2
  149. package/dist/{stop-D7h6jAXQ.mjs → stop-Dky10AEJ.mjs} +31 -22
  150. package/dist/{stop-CPzlM1L-.mjs → stop-Mv6W9FAk.mjs} +15 -16
  151. package/dist/supervisor-run.d.mts +3 -3
  152. package/dist/supervisor-run.mjs +28 -36
  153. package/dist/{support-Dlu37g6H.mjs → support-CNioewsd.mjs} +10 -10
  154. package/dist/{support-67vraa1Q.mjs → support-yUiOjnVY.mjs} +8 -8
  155. package/dist/swiftpm-artifact-inputs-DFcGsXDP.mjs +153 -0
  156. package/dist/swiftpm-macos-BIak-Rxv.mjs +532 -0
  157. package/dist/toolchain-BY8X8HXx.mjs +2 -0
  158. package/dist/{toolchain-Hp9sCpK4.mjs → toolchain-DpJMHq0n.mjs} +9 -6
  159. package/dist/{trigger-BAahxUPq.mjs → trigger-Cbyc41d_.mjs} +3 -3
  160. package/dist/trigger-yb_nFzPF.mjs +2 -0
  161. package/dist/{warm-progress-BHX5r0Ei.mjs → warm-progress-CA3TgT3-.mjs} +29 -16
  162. package/dist/web-CZ07gd77.mjs +2 -0
  163. package/dist/{web-CJmIj6AI.mjs → web-DKx6yoFA.mjs} +11 -10
  164. package/dist/web-run.mjs +11 -9
  165. package/dist/windows-report.mjs +1 -1
  166. package/dist/{workspace-state-Cjuigeeu.mjs → workspace-state-U08dEzhG.mjs} +3 -3
  167. package/dist/{workspaces-BccMFmGQ.mjs → workspaces-CpANmBlq.mjs} +594 -266
  168. package/dist/{worktree-DQ6Zi2Mv.mjs → worktree-CHecZ4Oq.mjs} +850 -551
  169. package/dist/worktree-ZqZVjD9K.mjs +2 -0
  170. package/dist/{xcode-Zlx3qjEs.mjs → xcode-BXn9-xYx.mjs} +5 -5
  171. package/package.json +7 -7
  172. package/shim/android-audio/Main.java +24 -0
  173. package/shim/android-audio/README.md +17 -0
  174. package/shim/android-audio.jar +0 -0
  175. package/dist/gc-C-WwL0yi.mjs +0 -3
  176. package/dist/git-C3iTsJaZ.mjs +0 -73
  177. package/dist/server-expo-C-90EhcG.mjs +0 -2
  178. package/dist/settings-DkdmlelE.d.mts +0 -15
  179. package/dist/swiftpm-macos-p8WZbxKU.mjs +0 -130
  180. package/dist/toolchain-DhKwp9iI.mjs +0 -2
  181. package/dist/trigger-Bf3uDcpv.mjs +0 -2
  182. package/dist/web-DknQETPs.mjs +0 -2
  183. package/dist/workspace-process-lock-1_1OoVW5.mjs +0 -57
  184. package/dist/worktree-CK2MpJW5.mjs +0 -2
@@ -1,42 +1,47 @@
1
- import { f as ownership_claim_exports, p as process_identity_exports, t as getExecutor } from "./exec-_UbtCQcG.mjs";
2
- import { d as workspaceDir, v as withDirLock$1 } from "./paths-DQtTgsBf.mjs";
3
- import { D as withConfigLock$1, F as isOnMountedVolume, I as listMountedVolumes, K as projectDeviceSlots, N as directorySizeAsync, P as formatBytes, R as measuredDirectorySizeAsync, S as removeProject, _ as loadConfig$1, j as acquireAvdClaim, z as volumeRootFor } from "./config-CYcBF478.mjs";
4
- import { h as shortUdid, o as formatLongDuration, u as plural } from "./command-output-B1UrjZHL.mjs";
5
- import { u as workspaceLastUsed } from "./workspace-state-Cjuigeeu.mjs";
6
- import { kt as listWorktreesAsync, zt as resolveSourceCheckoutAsync } from "./settings-CsJDEXgK.mjs";
7
- import { i as findProjectRoot$1, v as detectIsExpo } from "./project-BZNjmW_8.mjs";
8
- import { a as removeEasSessionClaim, n as easMachineStateRoot$1, r as readEasSessionLedger$1 } from "./eas-session-ledger-op6A54yi.mjs";
9
- import { $ as includesRecordings, At as unverifiedDeviceCommand, C as collectIdleDevices, D as idleDeviceLines, Dt as findOrphanedDevices, Et as eraseParkedDevices, Ft as withRemoteSessionGcLocks, Kt as withEasProjectLock, Mt as collectEasSessionSweep, Nt as deleteEasSessions, O as parseIdleDuration, Ot as findStaleDeviceRecords, Pt as describeError, Q as includesParkedDevices, Sr as sizeCaches, St as deleteProjectDevices, Tt as deviceSweepIsScoped, X as emptyCaches, Z as includesArchives, _r as phase, _t as collectDeviceLeases, at as selectsBeyondCaches, br as discoverCaches, bt as deleteParkedAvds, dr as memoryScope, dt as gradleVersionsPending, et as includesWorkspaceOutputs, fr as memorySweepIsScoped, ft as includesGradleVersions, gt as DEVICE_LIST_TIMEOUT_MS, hr as scopedMemoryReport, ht as collectInventory, ir as parseArchiveSelector, it as selectCaches, jt as withAndroidAvdSizes, k as shutDownIdleDevices, kt as findStaleProjectDevices, lr as memoryCacheKind, lt as deleteGradleVersions, mr as reclaimMemory, mt as getMachineCacheReport, nt as planCacheEmptying, ot as trimCaches, pr as memoryWorkPending, rt as previewScopedEmptying, sr as collectMemoryReport, st as collectGradleVersions, tt as isWorkspacesScope, ur as memoryLines, ut as gradleVersionLines, vt as collectParkedAvds, w as collectIdleDevicesAsync, wt as describeUnverifiableDevices, xt as deleteParkedSims, yt as collectParkedSims } from "./budget-Bs6SxxzO.mjs";
1
+ import { c as declareSpawnsOn, f as stopDeclaringSpawnsOn, g as process_identity_exports, h as withDrainingCancellation, r as WorkspaceProcessCleanupError, t as getExecutor, x as ownership_claim_exports } from "./exec-DEoAhcze.mjs";
2
+ import { d as workspaceDir } from "./paths-DQtTgsBf.mjs";
3
+ import { F as isOnMountedVolume, G as projectDeviceSlots, I as listMountedVolumes, L as measuredDirectorySizeAsync, N as directorySizeAsync, O as withConfigLockAsync$1, P as formatBytes, R as volumeRootFor, S as removeProject, _ as loadConfig$1, d as getConfigDir, j as acquireAvdClaim } from "./config-DlrsduwG.mjs";
4
+ import { h as shortUdid, o as formatLongDuration, u as plural } from "./command-output-gwpm28fu.mjs";
5
+ import { f as throwIfCancelled, t as runOperationFile } from "./operation-exec-BW0em61G.mjs";
6
+ import { u as workspaceLastUsed } from "./workspace-state-U08dEzhG.mjs";
7
+ import { Et as listWorktreesAsync, It as resolveSourceCheckoutAsync } from "./settings-C7O8A6ZI.mjs";
8
+ import { i as findProjectRoot$1, v as detectIsExpo } from "./project-wM_XFTvE.mjs";
9
+ import { n as easMachineStateRoot$1, o as removeEasSessionClaimAsync, r as readEasSessionLedger$1 } from "./eas-session-ledger-DlY_serQ.mjs";
10
+ import { $ as includesRecordings, At as unverifiedDeviceCommand, C as collectIdleDevices, D as idleDeviceLines, Dt as findOrphanedDevices, Et as eraseParkedDevices, Ft as withRemoteSessionGcLocks, In as runProcessOperation, Kt as withEasProjectLock, Mt as collectEasSessionSweep, Nt as deleteEasSessions, O as parseIdleDuration, Ot as findStaleDeviceRecords, Pt as describeError, Q as includesParkedDevices, Sr as sizeCaches, St as deleteProjectDevices, Tt as deviceSweepIsScoped, X as emptyCaches, Z as includesArchives, _r as phase, _t as collectDeviceLeases, at as selectsBeyondCaches, br as discoverCaches, bt as deleteParkedAvds, dr as memoryScope, dt as gradleVersionsPending, et as includesWorkspaceOutputs, fr as memorySweepIsScoped, ft as includesGradleVersions, gt as DEVICE_LIST_TIMEOUT_MS, hr as scopedMemoryReport, ht as collectInventory, ir as parseArchiveSelector, it as selectCaches, jt as withAndroidAvdSizes, k as shutDownIdleDevices, kt as findStaleProjectDevices, lr as memoryCacheKind, lt as deleteGradleVersions, mr as reclaimMemory, mt as getMachineCacheReport, nt as planCacheEmptying, ot as trimCaches, pr as memoryWorkPending, rt as previewScopedEmptying, sr as collectMemoryReport, st as collectGradleVersions, tt as isWorkspacesScope, ur as memoryLines, ut as gradleVersionLines, vt as collectParkedAvds, w as collectIdleDevicesAsync, wt as describeUnverifiableDevices, xt as deleteParkedSims, yt as collectParkedSims } from "./budget-Cvwn54qV.mjs";
10
11
  import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
11
- import { B as takeGcResults, I as listBuildSlots, a as collectWorkspaceOutputs, c as listWorkspaceDirs, f as withIdleWorkspace, i as collectOrphanedWorkspaces, l as rootPresence, n as WORKSPACE_OUTPUT_DIRS, o as deleteOrphanedWorkspaces, p as workspaceInUse, r as clearWorkspaceOutputs, s as isInsideWorkspaces, t as REBUILD_COST, u as sizeText, z as recordGcResult } from "./workspaces-BccMFmGQ.mjs";
12
- import { N as listBuildLocks, k as removeExpiredLease } from "./activity-BzVfHSrY.mjs";
13
- import { n as readCreatedDevices$1, t as forgetCreatedDevice } from "./created-devices-CqeBlrEn.mjs";
14
- import { F as listOrphanedAvdDirectories, M as listAvds, N as listAvdsAsync, ht as parkedMaxSetting, lt as POOL_SETTING_REMEDY, m as avdNameAbsent, z as ownedAvdDirectory } from "./android-DUYkNF4u.mjs";
15
- import { d as listAllIosSims, f as listAllIosSimsAsync } from "./ios-CpByZy12.mjs";
16
- import { r as readCollectors } from "./ownership-DEmM5vhh.mjs";
17
- import { _ as describeDereferenced, a as matchWorktreeEntry, b as sweepArchiveStaging, v as reclaimProject, y as deleteSelectedArchives } from "./worktree-DQ6Zi2Mv.mjs";
18
- import { s as writeCacheFile } from "./warm-progress-BHX5r0Ei.mjs";
19
- import { _ as collectWorktreeSweep, a as previewMaintenance, b as worktreeRemovalReason, c as deleteTempLeftovers, d as ARTIFACT_DIRS, f as appRoots, g as candidateRootsAsync, h as ownedDeviceUsage, l as tempLeftoverLines, o as triggeringRoot, p as finishedWorkspaces, s as collectTempLeftovers, t as deleteIdleWorkspaceDevices, u as tempLeftoverSection, v as removeWorktrees, y as worktreePullRequestNote } from "./act-DHAw2A2S.mjs";
20
- import { i as getSwiftpmCacheUsage, n as swiftpmCacheLines, r as getAgentDeviceUsage, t as agentDeviceLines } from "./agent-device-usage-output-zh1vhbLp.mjs";
21
- import { existsSync, lstatSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
12
+ import { B as createGcResults, L as listBuildSlots, a as collectWorkspaceOutputs, c as listWorkspaceDirs, f as withIdleWorkspace, i as collectOrphanedWorkspaces, l as rootPresence, m as workspaceInUseAsync, n as WORKSPACE_OUTPUT_DIRS, o as deleteOrphanedWorkspaces, r as clearWorkspaceOutputs, s as isInsideWorkspaces, t as REBUILD_COST, u as sizeText, z as GC_OUTPUT } from "./workspaces-CpANmBlq.mjs";
13
+ import { N as listBuildLocks, k as removeExpiredLease } from "./activity-CMLmzp7o.mjs";
14
+ import { n as forgetCreatedDeviceAsync, r as readCreatedDevices$1 } from "./created-devices-x9rN6bhs.mjs";
15
+ import { F as listAvdsAsync, L as listOrphanedAvdDirectories, V as ownedAvdDirectory, _t as parkedMaxSetting, dt as POOL_SETTING_REMEDY, h as avdNameAbsent } from "./android-C7sVmyI9.mjs";
16
+ import { d as listAllIosSims, f as listAllIosSimsAsync } from "./ios-hcBpnEGk.mjs";
17
+ import { r as readCollectors } from "./ownership-C5fo5Edb.mjs";
18
+ import { a as matchWorktreeEntry, b as deleteSelectedArchives, v as describeDereferenced, x as sweepArchiveStaging, y as reclaimProject } from "./worktree-CHecZ4Oq.mjs";
19
+ import { s as writeCacheFile } from "./warm-progress-CA3TgT3-.mjs";
20
+ import { _ as collectWorktreeSweep, a as previewMaintenance, b as worktreeRemovalReason, c as deleteTempLeftovers, d as ARTIFACT_DIRS, f as appRoots, g as candidateRootsAsync, h as ownedDeviceUsage, l as tempLeftoverLines, o as triggeringRoot, p as finishedWorkspaces, s as collectTempLeftovers, t as deleteIdleWorkspaceDevices, u as tempLeftoverSection, v as removeWorktrees, y as worktreePullRequestNote } from "./act-DsuqEkiM.mjs";
21
+ import { i as getSwiftpmCacheUsage, n as swiftpmCacheLines, r as getAgentDeviceUsage, t as agentDeviceLines } from "./agent-device-usage-output-DUFaTMdd.mjs";
22
+ import { existsSync, lstatSync, readFileSync, readdirSync, statSync } from "node:fs";
22
23
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
23
- import { LOG_ROTATE_BYTES, rotatedLogPath } from "@stim-cli/core";
24
- import { assertHostedDeviceLedger, deviceHostArea, deviceHostRoot, diskUsageCacheFile, hostedDeviceId, isJsonObject, listRecordedDevices, listStatusCacheEntries, maintenanceRunClaims, parseHostedPlatformDevice, readArchives, readDiskUsage, readHostedDevice, readHostedDeviceLedger, readHostedSessions, recordedBytes } from "@stim-cli/core/state";
24
+ import { LOG_ROTATE_BYTES, ensurePrivateDir, rotatedLogPath, withDirLock, withDirLockTransaction } from "@stim-cli/core";
25
+ import { assertHostedDeviceLedger, configLockPath, deviceHostArea, deviceHostRoot, diskUsageCacheFile, hostedDeviceId, isJsonObject, listRecordedDevicesAsync, listStatusCacheEntries, maintenanceRunClaims, parseHostedPlatformDevice, readArchivesAsync, readDiskUsage, readHostedDevice, readHostedDeviceLedger, readHostedSessions, recordedBytes } from "@stim-cli/core/state";
25
26
  import { isAbsolute as isAbsolute$1, join as join$1 } from "path";
26
27
  import { fileURLToPath } from "node:url";
28
+ import { setImmediate } from "node:timers/promises";
27
29
  import chalk from "chalk";
28
- import { closeSync as closeSync$1, existsSync as existsSync$1, openSync as openSync$1, readSync as readSync$1, readdirSync as readdirSync$1, renameSync as renameSync$1, rmSync as rmSync$1, statSync as statSync$1, utimesSync as utimesSync$1, writeFileSync as writeFileSync$1 } from "fs";
30
+ import { existsSync as existsSync$1 } from "fs";
29
31
  import { InvalidArgumentError } from "commander";
32
+ import { open, readdir, rename, rm, stat, utimes, writeFile } from "node:fs/promises";
30
33
  //#region src/commands/gc/archives.ts
31
- function stagingOrNone() {
34
+ async function stagingOrNone(signal) {
32
35
  try {
33
- return sweepArchiveStaging(false, 5e3);
36
+ return await sweepArchiveStaging(false, 5e3, signal);
34
37
  } catch {
38
+ signal?.throwIfAborted();
35
39
  return [];
36
40
  }
37
41
  }
38
- function collectArchives(scope, olderThan, now) {
39
- const records = readArchives();
42
+ async function collectArchives(scope, olderThan, now, signal) {
43
+ signal?.throwIfAborted();
44
+ const records = await readArchivesAsync(signal);
40
45
  const { kind, id } = parseArchiveSelector(scope) ?? {
41
46
  kind: null,
42
47
  id: null
@@ -55,7 +60,7 @@ function collectArchives(scope, olderThan, now) {
55
60
  expires: record.expires,
56
61
  willDelete: (id === null && olderThan !== null ? now - Date.parse(record.removedAt) >= olderThan * 864e5 : true) && (!kind || record.bytes[kind] > 0)
57
62
  })),
58
- staging: stagingOrNone(),
63
+ staging: await stagingOrNone(signal),
59
64
  unknownId: id !== null && !match ? id : null,
60
65
  knownIds: records.map((record) => record.id),
61
66
  kind
@@ -69,29 +74,35 @@ function archiveLines(selection) {
69
74
  ...selection.staging.map((entry) => ` ${entry.path}: ${entry.kept ? `kept: ${entry.reason}` : "abandoned staging"}${entry.removeCommand ? `; ${entry.removeCommand}` : ""}`)
70
75
  ];
71
76
  }
72
- function deleteArchives(selection) {
77
+ async function deleteArchives(selection, output = GC_OUTPUT, signal) {
78
+ signal?.throwIfAborted();
73
79
  if (!selection) return 0;
74
80
  let failures = 0;
75
- for (const record of selection.records.filter((entry) => entry.willDelete)) try {
76
- deleteSelectedArchives([record.id], selection.kind);
77
- recordGcResult("archive", "done", record.id, {
78
- id: record.id,
79
- bytes: record.bytes
80
- });
81
- } catch (error) {
82
- failures++;
83
- recordGcResult("archive", "failed", record.id, {
84
- id: record.id,
85
- detail: error.message
86
- });
87
- console.error(`Could not delete ${record.id}: ${error.message}`);
81
+ for (const record of selection.records.filter((entry) => entry.willDelete)) {
82
+ signal?.throwIfAborted();
83
+ try {
84
+ await deleteSelectedArchives([record.id], selection.kind, signal);
85
+ output.record("archive", "done", record.id, {
86
+ id: record.id,
87
+ bytes: record.bytes
88
+ });
89
+ } catch (error) {
90
+ signal?.throwIfAborted();
91
+ failures++;
92
+ output.record("archive", "failed", record.id, {
93
+ id: record.id,
94
+ detail: error.message
95
+ });
96
+ output.error(`Could not delete ${record.id}: ${error.message}`);
97
+ }
88
98
  }
89
99
  try {
90
- const remaining = new Set(sweepArchiveStaging(true, 5e3).map((entry) => entry.path));
91
- for (const entry of selection.staging) if (!entry.kept && !remaining.has(entry.path)) recordGcResult("archive", "done", "archive staging", { id: entry.path });
100
+ const remaining = new Set((await sweepArchiveStaging(true, 5e3, signal)).map((entry) => entry.path));
101
+ for (const entry of selection.staging) if (!entry.kept && !remaining.has(entry.path)) output.record("archive", "done", "archive staging", { id: entry.path });
92
102
  } catch (error) {
103
+ signal?.throwIfAborted();
93
104
  failures++;
94
- console.error(`Could not clear archive staging: ${error.message}`);
105
+ output.error(`Could not clear archive staging: ${error.message}`);
95
106
  }
96
107
  return failures;
97
108
  }
@@ -127,17 +138,24 @@ function findStaleEasLedgerEntries(mountedVolumes, deps = {}) {
127
138
  workspaceRoot
128
139
  }));
129
140
  }
130
- function forgetStaleEasLedgerEntries(entries, mountedVolumes, deps = {}) {
141
+ async function forgetStaleEasLedgerEntries(entries, mountedVolumes, deps = {}, output = GC_OUTPUT, signal) {
142
+ signal?.throwIfAborted();
131
143
  let failures = 0;
132
144
  const root = deps.easLedgerRoot ?? easMachineStateRoot$1();
133
- const remove = deps.removeEasSessionClaim ?? removeEasSessionClaim;
134
- for (const entry of entries) try {
135
- if (remove(entry.id, root, (claim) => isStaleEasLedgerEntry(claim, mountedVolumes))) console.log(chalk.green(`Forgot the EAS ledger entry ${entry.id} (invalid id, ${entry.workspaceRoot} is gone)`));
136
- else console.log(chalk.dim(`Kept the EAS ledger entry ${entry.id}: it changed since the report.`));
137
- } catch (error) {
138
- failures++;
139
- console.log(chalk.red(`Could not forget the EAS ledger entry ${entry.id}: ${describeError(error)}`));
145
+ const remove = deps.removeEasSessionClaim ?? removeEasSessionClaimAsync;
146
+ for (const entry of entries) {
147
+ await setImmediate();
148
+ signal?.throwIfAborted();
149
+ try {
150
+ if (await remove(entry.id, root, (claim) => isStaleEasLedgerEntry(claim, mountedVolumes), signal)) output.log(chalk.green(`Forgot the EAS ledger entry ${entry.id} (invalid id, ${entry.workspaceRoot} is gone)`));
151
+ else output.log(chalk.dim(`Kept the EAS ledger entry ${entry.id}: it changed since the report.`));
152
+ } catch (error) {
153
+ signal?.throwIfAborted();
154
+ failures++;
155
+ output.log(chalk.red(`Could not forget the EAS ledger entry ${entry.id}: ${describeError(error)}`));
156
+ }
140
157
  }
158
+ signal?.throwIfAborted();
141
159
  return failures;
142
160
  }
143
161
  //#endregion
@@ -271,23 +289,26 @@ function statusCacheLines(stale) {
271
289
  lines.push(" --delete removes them; `stim status` measures a folder again if it comes back.");
272
290
  return lines;
273
291
  }
274
- function deleteStaleStatusCaches(stale) {
292
+ async function deleteStaleStatusCaches(stale, output = GC_OUTPUT, signal) {
293
+ signal?.throwIfAborted();
275
294
  const mountedVolumes = listMountedVolumes();
276
295
  let failures = 0;
277
296
  for (const kind of KINDS) {
278
297
  let removed = 0;
279
298
  let bytes = 0;
280
299
  for (const entry of stale.filter((candidate) => candidate.kind === kind)) {
300
+ signal?.throwIfAborted();
281
301
  if (judge(entry.path, mountedVolumes) !== "gone") continue;
282
302
  try {
283
- rmSync(entry.file, { force: true });
303
+ await rm(entry.file, { force: true });
284
304
  removed++;
285
305
  bytes += entry.bytes;
286
306
  } catch (error) {
307
+ signal?.throwIfAborted();
287
308
  failures++;
288
309
  const detail = error?.message || String(error);
289
- console.log(chalk.red(`Could not remove ${entry.file}: ${detail}`));
290
- recordGcResult("statusCache", "failed", `${kind} cache entry`, {
310
+ output.log(chalk.red(`Could not remove ${entry.file}: ${detail}`));
311
+ output.record("statusCache", "failed", `${kind} cache entry`, {
291
312
  id: entry.file,
292
313
  detail
293
314
  });
@@ -295,9 +316,10 @@ function deleteStaleStatusCaches(stale) {
295
316
  }
296
317
  if (!removed) continue;
297
318
  const label = `${removed} stale ${kind} cache entr${removed === 1 ? "y" : "ies"}`;
298
- console.log(chalk.green(`Removed ${label}`));
299
- recordGcResult("statusCache", "done", label, { bytes });
319
+ output.log(chalk.green(`Removed ${label}`));
320
+ output.record("statusCache", "done", label, { bytes });
300
321
  }
322
+ signal?.throwIfAborted();
301
323
  return failures;
302
324
  }
303
325
  //#endregion
@@ -338,22 +360,25 @@ function finishedDeviceLines(devices = []) {
338
360
  lines.push(" --delete keeps a workspace that is in use, leased, driven, locked or pinned.");
339
361
  return lines;
340
362
  }
341
- async function deleteFinishedDevices({ finishedDevices: devices, worktreeSweep }) {
363
+ async function deleteFinishedDevices({ finishedDevices: devices, worktreeSweep }, output = GC_OUTPUT, operation) {
342
364
  const graceMs = worktreeSweep?.graceMs ?? 0;
343
365
  let failures = 0;
344
366
  for (const project of new Set(devices.map((device) => device.project))) {
367
+ operation?.signal.throwIfAborted();
345
368
  const outcome = await deleteIdleWorkspaceDevices(canonicalPath(project), {
346
369
  windowMs: graceMs,
347
- protectedRoot: triggeringRoot(),
348
- purpose: "stim gc --delete"
370
+ protectedRoot: triggeringRoot(operation),
371
+ purpose: "stim gc --delete",
372
+ operation,
373
+ output
349
374
  });
350
375
  if (outcome.status === "kept") {
351
- console.log(chalk.yellow(`Kept the owned devices of ${project}: ${outcome.detail}`));
352
- recordGcResult("device", "kept", project, { detail: outcome.detail });
376
+ output.log(chalk.yellow(`Kept the owned devices of ${project}: ${outcome.detail}`));
377
+ output.record("device", "kept", project, { detail: outcome.detail });
353
378
  } else if (outcome.status === "failed") {
354
379
  failures++;
355
- console.log(chalk.red(`Could not delete the owned devices of ${project}: ${outcome.detail}`));
356
- recordGcResult("device", "failed", project, { detail: outcome.detail });
380
+ output.log(chalk.red(`Could not delete the owned devices of ${project}: ${outcome.detail}`));
381
+ output.record("device", "failed", project, { detail: outcome.detail });
357
382
  }
358
383
  }
359
384
  return failures;
@@ -421,23 +446,37 @@ function planRecordings(entries, { whole, olderThan, now }) {
421
446
  * workspace and dead project sweeps remove whole, are listed with them; entries whose project root is on an
422
447
  * unmounted volume are left out.
423
448
  */
424
- function collectRecordings(plan, gone = []) {
449
+ async function collectRecordings(plan, gone = [], signal) {
450
+ signal?.throwIfAborted();
425
451
  const mountedVolumes = listMountedVolumes();
426
- return planRecordings(listWorkspaceDirs().filter((entry) => entry.projectRoot === null || isOnMountedVolume(entry.projectRoot, mountedVolumes)).map((entry) => Object.assign({}, entry, { recordings: join$1(entry.dir, "recordings") })).filter((entry) => existsSync$1(entry.recordings)).map((entry) => Object.assign({}, entry, {
427
- gone: gone.includes(entry.dir),
428
- devices: listRecordedDevices(entry.recordings)
429
- })), plan);
452
+ const entries = [];
453
+ for (const entry of listWorkspaceDirs()) {
454
+ signal?.throwIfAborted();
455
+ if (entry.projectRoot !== null && !isOnMountedVolume(entry.projectRoot, mountedVolumes)) continue;
456
+ const recordings = join$1(entry.dir, "recordings");
457
+ if (!existsSync$1(recordings)) continue;
458
+ entries.push({
459
+ ...entry,
460
+ recordings,
461
+ gone: gone.includes(entry.dir),
462
+ devices: await listRecordedDevicesAsync(recordings, false, signal)
463
+ });
464
+ }
465
+ signal?.throwIfAborted();
466
+ return planRecordings(entries, plan);
430
467
  }
431
468
  /** Deletes what `planRecordings` chose, planning again from the files on disk. */
432
- function deleteRecordings(report, plan) {
469
+ async function deleteRecordings(report, plan, output = GC_OUTPUT, signal) {
470
+ signal?.throwIfAborted();
433
471
  let failures = 0;
434
472
  for (const entry of report) {
473
+ signal?.throwIfAborted();
435
474
  const label = entry.projectRoot ?? entry.dir;
436
475
  if (entry.withWorkspace) continue;
437
476
  if (!entry.willDelete) {
438
477
  if (entry.keptCode === "unresolved" || entry.keptCode === "recently-recorded") {
439
- console.log(chalk.dim(`Kept the device recordings of ${label}: ${entry.keptReason}`));
440
- recordGcResult("recording", "kept", label, {
478
+ output.log(chalk.dim(`Kept the device recordings of ${label}: ${entry.keptReason}`));
479
+ output.record("recording", "kept", label, {
441
480
  bytes: entry.bytes,
442
481
  detail: entry.keptReason
443
482
  });
@@ -447,27 +486,34 @@ function deleteRecordings(report, plan) {
447
486
  try {
448
487
  let deleted = 0;
449
488
  if (plan.whole) {
450
- deleted = recordedBytes(listRecordedDevices(entry.dir));
451
- rmSync$1(entry.dir, {
489
+ deleted = recordedBytes(await listRecordedDevicesAsync(entry.dir, false, signal));
490
+ signal?.throwIfAborted();
491
+ await rm(entry.dir, {
452
492
  recursive: true,
453
493
  force: true
454
494
  });
455
- } else if (plan.olderThan !== null) for (const segment of oldSegments(listRecordedDevices(entry.dir), plan.olderThan, plan.now)) {
456
- rmSync$1(segment.file, { force: true });
457
- deleted += segment.bytes;
495
+ } else if (plan.olderThan !== null) {
496
+ const devices = await listRecordedDevicesAsync(entry.dir, false, signal);
497
+ for (const segment of oldSegments(devices, plan.olderThan, plan.now)) {
498
+ signal?.throwIfAborted();
499
+ await rm(segment.file, { force: true });
500
+ deleted += segment.bytes;
501
+ }
458
502
  }
459
- console.log(chalk.green(`Deleted ${formatBytes(deleted)} of device recordings of ${label}`));
460
- recordGcResult("recording", "done", label, { bytes: deleted });
503
+ output.log(chalk.green(`Deleted ${formatBytes(deleted)} of device recordings of ${label}`));
504
+ output.record("recording", "done", label, { bytes: deleted });
461
505
  } catch (error) {
506
+ signal?.throwIfAborted();
462
507
  failures++;
463
508
  const detail = error?.message || String(error);
464
- console.log(chalk.red(`Could not delete the device recordings of ${label}: ${detail}`));
465
- recordGcResult("recording", "failed", label, {
509
+ output.log(chalk.red(`Could not delete the device recordings of ${label}: ${detail}`));
510
+ output.record("recording", "failed", label, {
466
511
  bytes: entry.bytes,
467
512
  detail
468
513
  });
469
514
  }
470
515
  }
516
+ signal?.throwIfAborted();
471
517
  return failures;
472
518
  }
473
519
  function recordingLines(recordings) {
@@ -1126,8 +1172,12 @@ function inside(root, path) {
1126
1172
  const rel = relative(root, path);
1127
1173
  return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
1128
1174
  }
1129
- async function buildStateDirs(path, keys) {
1130
- const entries = await listWorktreesAsync(path);
1175
+ async function buildStateDirs(path, keys, operation) {
1176
+ const entries = await listWorktreesAsync(path, {
1177
+ env: operation?.env,
1178
+ signal: operation?.signal,
1179
+ declareFor: operation
1180
+ });
1131
1181
  const own = keys.filter((key) => matchWorktreeEntry(entries, key)?.path === path);
1132
1182
  const root = canonicalPath(path);
1133
1183
  const worktree = {
@@ -1156,14 +1206,18 @@ function cachedSize(dir, now) {
1156
1206
  return null;
1157
1207
  }
1158
1208
  }
1159
- async function duSize(dir) {
1209
+ async function duSize(dir, operation) {
1160
1210
  let out;
1161
1211
  try {
1162
- out = await getExecutor().runFileAsync("du", ["-sk", dir], {
1212
+ out = await runOperationFile(operation, getExecutor(), "du", ["-sk", dir], {
1163
1213
  timeoutMs: BUILD_STATE_TIMEOUT_MS,
1164
- killSignal: "SIGKILL"
1214
+ killSignal: "SIGKILL",
1215
+ cwd: operation?.cwd,
1216
+ signal: operation?.signal
1165
1217
  });
1166
1218
  } catch (error) {
1219
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
1220
+ operation?.signal.throwIfAborted();
1167
1221
  const stdout = error instanceof Error && "stdout" in error ? error.stdout : null;
1168
1222
  out = typeof stdout === "string" ? stdout : "";
1169
1223
  }
@@ -1180,16 +1234,19 @@ async function eachLimited(items, limit, run) {
1180
1234
  /**
1181
1235
  * Sizes the regenerable directories maintenance clears and a project-local `.pnpm-store` in each worktree of the
1182
1236
  * sweep, from the disk-usage cache while an entry is under an hour old and no entry was added to or removed from the
1183
- * directory since. Stale directories are measured with one `du` each, at most 4 at once and 10 seconds each, so a
1237
+ * directory since. Stale directories are measured with one `du` each, sequentially with an operation and at most
1238
+ * 4 at once otherwise, with 10 seconds each, so a
1184
1239
  * slow tree delays only itself; a directory du could not measure in time is reported unmeasured (null). Also lists
1185
1240
  * the directories directly under a repository's `.worktrees/` that are no git worktree at all.
1186
1241
  */
1187
1242
  async function measureWorktreeSweep(sweep, operation) {
1243
+ operation?.signal.throwIfAborted();
1188
1244
  const now = Date.now();
1189
1245
  const pending = [];
1190
1246
  for (const candidate of sweep.worktrees) {
1247
+ operation?.signal.throwIfAborted();
1191
1248
  if (candidate.skipCode === "not-a-worktree") continue;
1192
- const dirs = await buildStateDirs(candidate.path, candidate.keys);
1249
+ const dirs = await buildStateDirs(candidate.path, candidate.keys, operation);
1193
1250
  pending.push({
1194
1251
  candidate,
1195
1252
  root: canonicalPath(candidate.path),
@@ -1203,9 +1260,10 @@ async function measureWorktreeSweep(sweep, operation) {
1203
1260
  }] : []));
1204
1261
  const worktrees = new Set(due.map(({ sizes }) => sizes)).size;
1205
1262
  if (due.length) phase("worktrees", `measuring the build state of ${plural(worktrees, "worktree")}`, operation);
1206
- await eachLimited(due, BUILD_STATE_CONCURRENCY, async ({ dir, sizes }) => {
1263
+ await eachLimited(due, operation ? 1 : BUILD_STATE_CONCURRENCY, async ({ dir, sizes }) => {
1264
+ operation?.signal.throwIfAborted();
1207
1265
  const measuredAt = (/* @__PURE__ */ new Date()).toISOString();
1208
- const bytes = await duSize(dir);
1266
+ const bytes = await duSize(dir, operation);
1209
1267
  if (bytes === null) return;
1210
1268
  sizes.set(dir, bytes);
1211
1269
  operation?.signal.throwIfAborted();
@@ -1225,7 +1283,7 @@ async function measureWorktreeSweep(sweep, operation) {
1225
1283
  dirs: entries.toSorted((a, b) => (b.bytes ?? 0) - (a.bytes ?? 0))
1226
1284
  };
1227
1285
  }
1228
- sweep.unlisted = await unlistedWorktreeDirs();
1286
+ sweep.unlisted = await unlistedWorktreeDirs(operation);
1229
1287
  return sweep;
1230
1288
  }
1231
1289
  /** Whether `path` is a checkout git still knows: its own `.git` directory, or a `.git` file whose gitdir exists. */
@@ -1241,15 +1299,22 @@ function hasLiveGitDir(path) {
1241
1299
  const gitdir = /^gitdir:\s*(.+)$/m.exec(text)?.[1]?.trim();
1242
1300
  return Boolean(gitdir) && existsSync(resolve(dirname(dotGit), gitdir ?? ""));
1243
1301
  }
1244
- async function unlistedWorktreeDirs() {
1302
+ async function unlistedWorktreeDirs(operation) {
1245
1303
  const repositories = /* @__PURE__ */ new Map();
1246
- for (const root of await candidateRootsAsync()) {
1247
- const { entries, ...source } = await resolveSourceCheckoutAsync(root);
1304
+ const options = {
1305
+ env: operation?.env,
1306
+ signal: operation?.signal,
1307
+ declareFor: operation
1308
+ };
1309
+ for (const root of await candidateRootsAsync(operation)) {
1310
+ operation?.signal.throwIfAborted();
1311
+ const { entries, ...source } = await resolveSourceCheckoutAsync(root, options);
1248
1312
  if ("refusal" in source || repositories.has(source.path)) continue;
1249
1313
  repositories.set(source.path, entries.map((entry) => canonicalPath(entry.path)));
1250
1314
  }
1251
1315
  const found = [];
1252
1316
  for (const [repository, listed] of repositories) {
1317
+ operation?.signal.throwIfAborted();
1253
1318
  const parent = join(repository, WORKTREES_DIR);
1254
1319
  let names;
1255
1320
  try {
@@ -1258,6 +1323,7 @@ async function unlistedWorktreeDirs() {
1258
1323
  continue;
1259
1324
  }
1260
1325
  for (const name of names.toSorted()) {
1326
+ operation?.signal.throwIfAborted();
1261
1327
  const path = join(parent, name);
1262
1328
  if (!isDirectory(path) || hasLiveGitDir(path)) continue;
1263
1329
  const real = canonicalPath(path);
@@ -1265,7 +1331,11 @@ async function unlistedWorktreeDirs() {
1265
1331
  found.push({
1266
1332
  path,
1267
1333
  repository,
1268
- bytes: await measuredDirectorySizeAsync(path, { timeoutMs: SIZE_TIMEOUT_MS })
1334
+ bytes: await measuredDirectorySizeAsync(path, {
1335
+ timeoutMs: SIZE_TIMEOUT_MS,
1336
+ operation,
1337
+ signal: operation?.signal
1338
+ })
1269
1339
  });
1270
1340
  }
1271
1341
  }
@@ -1302,15 +1372,23 @@ function isStaleAvdName(name, config) {
1302
1372
  function reservedForSetup(name, config) {
1303
1373
  return Object.values(config?.projects ?? {}).some((project) => projectDeviceSlots(project).some(({ platforms }) => platforms.android?.setupIncomplete && platforms.android.avdName === name));
1304
1374
  }
1305
- function forgetStaleAvdName(name) {
1375
+ async function forgetStaleAvdName(name, signal) {
1306
1376
  let claim = null;
1307
1377
  try {
1308
1378
  claim = acquireAvdClaim(name);
1309
- return withConfigLock$1(() => {
1310
- if (!isStaleAvdName(name, loadConfig$1())) return false;
1311
- forgetCreatedDevice("android", name);
1312
- return true;
1313
- });
1379
+ while (true) try {
1380
+ return await forgetCreatedDeviceAsync("android", name, signal, (forget) => withDirLock(configLockPath(), () => {
1381
+ if (!isStaleAvdName(name, loadConfig$1())) return false;
1382
+ forget();
1383
+ return true;
1384
+ }, {
1385
+ waitMs: 0,
1386
+ ensureParent: () => ensurePrivateDir(getConfigDir())
1387
+ }));
1388
+ } catch (error) {
1389
+ if (!(error instanceof Error && "code" in error && error.code === "STIM_LOCK_TIMEOUT" && "lockPath" in error && error.lockPath === configLockPath())) throw error;
1390
+ await withConfigLockAsync$1(() => {}, signal);
1391
+ }
1314
1392
  } finally {
1315
1393
  (0, ownership_claim_exports.releaseClaim)(claim);
1316
1394
  }
@@ -1337,23 +1415,28 @@ function isDanglingLink(path) {
1337
1415
  return false;
1338
1416
  }
1339
1417
  }
1340
- function forgetStaleLedgerEntries(entries) {
1418
+ async function forgetStaleLedgerEntries(entries, output = GC_OUTPUT, signal) {
1419
+ signal?.throwIfAborted();
1341
1420
  let failures = 0;
1342
1421
  for (const entry of entries) {
1422
+ await setImmediate();
1423
+ signal?.throwIfAborted();
1343
1424
  if (entry.kind === "web" && !isStaleProfile(entry.id, existsSync)) continue;
1344
1425
  try {
1345
1426
  if (entry.kind === "android") {
1346
- if (!forgetStaleAvdName(entry.id)) {
1347
- console.log(chalk.dim(`Kept the ledger entry for android ${entry.id}: the AVD or its setup reappeared`));
1427
+ if (!await forgetStaleAvdName(entry.id, signal)) {
1428
+ output.log(chalk.dim(`Kept the ledger entry for android ${entry.id}: the AVD or its setup reappeared`));
1348
1429
  continue;
1349
1430
  }
1350
- } else forgetCreatedDevice(entry.kind, entry.id);
1351
- console.log(chalk.green(`Forgot the ledger entry for ${entry.kind} ${entry.id} (not on this machine)`));
1431
+ } else await forgetCreatedDeviceAsync(entry.kind, entry.id, signal);
1432
+ output.log(chalk.green(`Forgot the ledger entry for ${entry.kind} ${entry.id} (not on this machine)`));
1352
1433
  } catch (error) {
1434
+ signal?.throwIfAborted();
1353
1435
  failures++;
1354
- console.log(chalk.red(`Could not forget the ledger entry for ${entry.kind} ${entry.id}: ${error?.message || error}`));
1436
+ output.log(chalk.red(`Could not forget the ledger entry for ${entry.kind} ${entry.id}: ${error?.message || error}`));
1355
1437
  }
1356
1438
  }
1439
+ signal?.throwIfAborted();
1357
1440
  return failures;
1358
1441
  }
1359
1442
  //#endregion
@@ -1364,11 +1447,14 @@ const CAPPED_LOGS = [
1364
1447
  "device.ndjson"
1365
1448
  ];
1366
1449
  const TRIM_ABOVE_BYTES = 2 * LOG_ROTATE_BYTES;
1367
- function fileSize(path) {
1450
+ async function fileSize(path, signal) {
1451
+ signal?.throwIfAborted();
1368
1452
  try {
1369
- const stat = statSync$1(path);
1370
- return stat.isFile() ? stat.size : 0;
1453
+ const metadata = await stat(path);
1454
+ signal?.throwIfAborted();
1455
+ return metadata.isFile() ? metadata.size : 0;
1371
1456
  } catch {
1457
+ signal?.throwIfAborted();
1372
1458
  return 0;
1373
1459
  }
1374
1460
  }
@@ -1387,29 +1473,36 @@ function cappedFiles(logsDir) {
1387
1473
  function trimmable(size) {
1388
1474
  return size > TRIM_ABOVE_BYTES ? size - LOG_ROTATE_BYTES : 0;
1389
1475
  }
1390
- function excess(logsDir) {
1391
- return cappedFiles(logsDir).reduce((sum, { file }) => sum + trimmable(fileSize(file)), 0);
1476
+ async function excess(logsDir, signal) {
1477
+ let bytes = 0;
1478
+ for (const { file } of cappedFiles(logsDir)) bytes += trimmable(await fileSize(file, signal));
1479
+ return bytes;
1392
1480
  }
1393
1481
  function collectorReason(root) {
1394
1482
  const platforms = Object.keys(readCollectors(root));
1395
1483
  return platforms.length ? `a device log collector is recorded for ${platforms.join(", ")}` : null;
1396
1484
  }
1397
- function collectWorkspaceLogs({ exclude = [] } = {}) {
1485
+ async function collectWorkspaceLogs({ exclude = [], operation } = {}) {
1486
+ const signal = operation?.signal;
1487
+ signal?.throwIfAborted();
1398
1488
  const mountedVolumes = listMountedVolumes();
1399
1489
  const entries = [];
1400
1490
  for (const { dir, projectRoot, problem } of listWorkspaceDirs()) {
1491
+ signal?.throwIfAborted();
1401
1492
  if (exclude.includes(dir)) continue;
1402
1493
  if (projectRoot !== null && !isOnMountedVolume(projectRoot, mountedVolumes)) continue;
1403
1494
  const logsDir = join$1(dir, "logs");
1404
1495
  let names;
1405
1496
  try {
1406
- names = readdirSync$1(logsDir);
1497
+ names = await readdir(logsDir);
1407
1498
  } catch {
1499
+ signal?.throwIfAborted();
1408
1500
  continue;
1409
1501
  }
1410
- const bytes = names.reduce((sum, name) => sum + fileSize(join$1(logsDir, name)), 0);
1502
+ let bytes = 0;
1503
+ for (const name of names) bytes += await fileSize(join$1(logsDir, name), signal);
1411
1504
  if (bytes === 0) continue;
1412
- const trimBytes = excess(logsDir);
1505
+ const trimBytes = await excess(logsDir, signal);
1413
1506
  let keptCode = null;
1414
1507
  let keptReason = null;
1415
1508
  if (trimBytes > 0) {
@@ -1417,7 +1510,7 @@ function collectWorkspaceLogs({ exclude = [] } = {}) {
1417
1510
  keptCode = "unresolved";
1418
1511
  keptReason = `workspace directory not resolved: ${problem ?? "unknown project root"}`;
1419
1512
  } else {
1420
- const inUse = workspaceInUse(projectRoot);
1513
+ const inUse = await workspaceInUseAsync(projectRoot, {}, signal, operation);
1421
1514
  const collector = collectorReason(projectRoot);
1422
1515
  if (inUse.length) {
1423
1516
  keptCode = "in-use";
@@ -1438,70 +1531,89 @@ function collectWorkspaceLogs({ exclude = [] } = {}) {
1438
1531
  keptReason
1439
1532
  });
1440
1533
  }
1534
+ signal?.throwIfAborted();
1441
1535
  return entries;
1442
1536
  }
1443
- function trimToNewest({ file, log }, maxBytes) {
1444
- if (!trimmable(fileSize(file))) return 0;
1445
- return withDirLock$1(`${log}.lock`, () => {
1446
- const { size, atime, mtime } = statSync$1(file);
1537
+ async function trimToNewest({ file, log }, maxBytes, signal) {
1538
+ if (!trimmable(await fileSize(file, signal))) return 0;
1539
+ return withDirLockTransaction(`${log}.lock`, async () => {
1540
+ signal?.throwIfAborted();
1541
+ const { size, atime, mtime } = await stat(file);
1542
+ signal?.throwIfAborted();
1447
1543
  if (!trimmable(size)) return 0;
1448
1544
  const tail = Buffer.alloc(maxBytes);
1449
- const fd = openSync$1(file, "r");
1545
+ const fd = await open(file, "r");
1450
1546
  try {
1451
- readSync$1(fd, tail, 0, maxBytes, size - maxBytes);
1547
+ signal?.throwIfAborted();
1548
+ await fd.read(tail, 0, maxBytes, size - maxBytes);
1452
1549
  } finally {
1453
- closeSync$1(fd);
1550
+ await fd.close();
1454
1551
  }
1552
+ signal?.throwIfAborted();
1455
1553
  const newline = tail.indexOf(10);
1456
1554
  const kept = newline === -1 ? Buffer.alloc(0) : tail.subarray(newline + 1);
1457
1555
  const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
1458
1556
  try {
1459
- writeFileSync$1(tmp, kept);
1460
- utimesSync$1(tmp, atime, mtime);
1461
- renameSync$1(tmp, file);
1462
- } catch (error) {
1463
- rmSync$1(tmp, { force: true });
1464
- throw error;
1557
+ await writeFile(tmp, kept, { signal });
1558
+ await utimes(tmp, atime, mtime);
1559
+ signal?.throwIfAborted();
1560
+ await rename(tmp, file);
1561
+ } finally {
1562
+ await rm(tmp, { force: true });
1465
1563
  }
1466
1564
  return size - kept.length;
1467
- });
1565
+ }, { signal });
1468
1566
  }
1469
- async function trimWorkspaceLogs(entries) {
1567
+ async function trimWorkspaceLogs(entries, output = GC_OUTPUT, operation) {
1568
+ const signal = operation?.signal;
1569
+ signal?.throwIfAborted();
1470
1570
  let failures = 0;
1471
1571
  let trimmed = 0;
1472
1572
  for (const entry of entries) {
1573
+ signal?.throwIfAborted();
1473
1574
  const root = entry.projectRoot;
1474
1575
  if (!entry.willTrim || root === null) {
1475
- if (entry.keptReason) console.log(chalk.dim(`Kept the logs of ${root ?? entry.dir}: ${entry.keptReason}`));
1576
+ if (entry.keptReason) output.log(chalk.dim(`Kept the logs of ${root ?? entry.dir}: ${entry.keptReason}`));
1476
1577
  continue;
1477
1578
  }
1478
1579
  let run;
1479
1580
  try {
1480
- run = await withIdleWorkspace(root, () => {
1581
+ run = await withIdleWorkspace(root, async () => {
1481
1582
  const collector = collectorReason(root);
1482
1583
  if (collector) return {
1483
1584
  kept: collector,
1484
1585
  bytes: 0
1485
1586
  };
1587
+ let bytes = 0;
1588
+ for (const capped of cappedFiles(join$1(entry.dir, "logs"))) {
1589
+ signal?.throwIfAborted();
1590
+ bytes += await trimToNewest(capped, LOG_ROTATE_BYTES, signal);
1591
+ }
1486
1592
  return {
1487
1593
  kept: null,
1488
- bytes: cappedFiles(join$1(entry.dir, "logs")).reduce((sum, capped) => sum + trimToNewest(capped, LOG_ROTATE_BYTES), 0)
1594
+ bytes
1489
1595
  };
1490
- }, { purpose: "gc" });
1596
+ }, {
1597
+ purpose: "gc",
1598
+ operation
1599
+ });
1491
1600
  } catch (error) {
1601
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
1602
+ signal?.throwIfAborted();
1492
1603
  failures++;
1493
- console.log(chalk.red(`Could not trim the logs of ${root}: ${error?.message || String(error)}`));
1604
+ output.log(chalk.red(`Could not trim the logs of ${root}: ${error?.message || String(error)}`));
1494
1605
  continue;
1495
1606
  }
1496
1607
  if (!run.ran || run.value.kept) {
1497
1608
  const kept = run.ran ? run.value.kept : `in use: ${run.reasons.join("; ")}`;
1498
- console.log(chalk.yellow(`Kept the logs of ${root}: ${kept}`));
1609
+ output.log(chalk.yellow(`Kept the logs of ${root}: ${kept}`));
1499
1610
  continue;
1500
1611
  }
1501
1612
  trimmed += run.value.bytes;
1502
- console.log(chalk.green(`Trimmed the logs of ${root} (${formatBytes(run.value.bytes)})`));
1613
+ output.log(chalk.green(`Trimmed the logs of ${root} (${formatBytes(run.value.bytes)})`));
1503
1614
  }
1504
- if (trimmed) console.log(chalk.dim(`Trimmed ${formatBytes(trimmed)} of workspace logs.`));
1615
+ signal?.throwIfAborted();
1616
+ if (trimmed) output.log(chalk.dim(`Trimmed ${formatBytes(trimmed)} of workspace logs.`));
1505
1617
  return failures;
1506
1618
  }
1507
1619
  //#endregion
@@ -1553,15 +1665,17 @@ function collectParkedHostedDevices({ olderThanDays = null, now = Date.now() } =
1553
1665
  notices
1554
1666
  };
1555
1667
  }
1556
- async function stopHostedDevice(record, claim) {
1668
+ async function stopHostedDevice(record, claim, operation) {
1669
+ operation?.signal.throwIfAborted();
1557
1670
  const home = join(deviceHostArea(record.id), "home");
1558
1671
  (0, ownership_claim_exports.markClaimChildPending)(claim);
1672
+ if (operation) declareSpawnsOn(operation, claim);
1559
1673
  let child;
1560
1674
  try {
1561
1675
  child = getExecutor().spawn(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), "device-host-worker.mjs")], {
1562
1676
  cwd: home,
1563
1677
  env: {
1564
- ...process.env,
1678
+ ...operation?.env ?? process.env,
1565
1679
  STIM_HOME: home
1566
1680
  },
1567
1681
  detached: true,
@@ -1578,6 +1692,8 @@ async function stopHostedDevice(record, claim) {
1578
1692
  value: null,
1579
1693
  notice: describeError(error)
1580
1694
  };
1695
+ } finally {
1696
+ if (operation) stopDeclaringSpawnsOn(operation, claim);
1581
1697
  }
1582
1698
  let identity = null;
1583
1699
  let closed = false;
@@ -1598,6 +1714,7 @@ async function stopHostedDevice(record, claim) {
1598
1714
  clearTimeout(killTimer);
1599
1715
  clearTimeout(finishTimer);
1600
1716
  clearTimeout(groupTimer);
1717
+ operation?.signal.removeEventListener("abort", cancel);
1601
1718
  const settled = closed && (!child.pid || !(0, ownership_claim_exports.processGroupAlive)(child.pid));
1602
1719
  if (settled) (0, ownership_claim_exports.clearClaimChild)(claim);
1603
1720
  let value = null;
@@ -1622,12 +1739,12 @@ async function stopHostedDevice(record, claim) {
1622
1739
  if (child.pid && identity && ["same", "gone"].includes((0, process_identity_exports.inspectProcessIdentity)(identity))) try {
1623
1740
  process.kill(-child.pid, name);
1624
1741
  } catch {}
1625
- else if (!identity) child.kill(name);
1742
+ else if (!identity) child.stdin?.destroy();
1626
1743
  };
1627
1744
  const cancel = () => {
1628
1745
  if (finished || cancelling) return;
1629
1746
  cancelling = true;
1630
- notice ??= "Hosted worker exceeded its deadline.";
1747
+ notice ??= operation?.signal.aborted ? "Hosted cleanup cancelled." : "Hosted worker exceeded its deadline.";
1631
1748
  signal("SIGTERM");
1632
1749
  killTimer = setTimeout(() => signal("SIGKILL"), 5e3);
1633
1750
  finishTimer = setTimeout(finish, 1e4);
@@ -1641,6 +1758,7 @@ async function stopHostedDevice(record, claim) {
1641
1758
  }
1642
1759
  };
1643
1760
  timer = setTimeout(cancel, 9e4);
1761
+ operation?.signal.addEventListener("abort", cancel, { once: true });
1644
1762
  child.stdout?.on("data", (chunk) => {
1645
1763
  if (Buffer.byteLength(output) + chunk.length > 16384) {
1646
1764
  notice = "Hosted worker output exceeded its bound.";
@@ -1673,6 +1791,10 @@ async function stopHostedDevice(record, claim) {
1673
1791
  };
1674
1792
  try {
1675
1793
  (0, ownership_claim_exports.setClaimChild)(claim, identity);
1794
+ if (operation?.signal.aborted) {
1795
+ cancel();
1796
+ return;
1797
+ }
1676
1798
  child.stdin?.end(JSON.stringify({
1677
1799
  mode: "stop",
1678
1800
  platform: record.platform,
@@ -1689,14 +1811,15 @@ async function stopHostedDevice(record, claim) {
1689
1811
  }
1690
1812
  });
1691
1813
  }
1692
- async function deleteParkedHostedDevices(devices, { listAvds: list = listAvds } = {}) {
1814
+ async function deleteParkedHostedDevices(devices, { listAvds: list, operation } = {}, output = GC_OUTPUT) {
1693
1815
  let failures = 0;
1694
1816
  for (const device of devices) {
1817
+ operation?.signal.throwIfAborted();
1695
1818
  if (device.listed === false) continue;
1696
1819
  const label = `${device.platform} ${device.name ?? device.session} (hosted session ${device.session})`;
1697
1820
  const kept = (detail) => {
1698
- console.log(`Kept ${label}: ${detail}`);
1699
- recordGcResult("parkedHostedDevice", "kept", label, {
1821
+ output.log(`Kept ${label}: ${detail}`);
1822
+ output.record("parkedHostedDevice", "kept", label, {
1700
1823
  id: device.id,
1701
1824
  detail
1702
1825
  });
@@ -1738,22 +1861,42 @@ async function deleteParkedHostedDevices(devices, { listAvds: list = listAvds }
1738
1861
  kept(describeError(error));
1739
1862
  continue;
1740
1863
  }
1741
- if (device.platform === "android" && !list().includes(device.id)) {
1742
- kept("AVD not visible from this shell's Android environment; run gc with the server's ANDROID_AVD_HOME/HOME");
1743
- continue;
1864
+ if (device.platform === "android") {
1865
+ let avds;
1866
+ if (operation) declareSpawnsOn(operation, claim);
1867
+ try {
1868
+ avds = await (list ? list() : listAvdsAsync({
1869
+ operation,
1870
+ signal: operation?.signal
1871
+ }));
1872
+ } finally {
1873
+ if (operation) stopDeclaringSpawnsOn(operation, claim);
1874
+ }
1875
+ if (!avds.includes(device.id)) {
1876
+ kept("AVD not visible from this shell's Android environment; run gc with the server's ANDROID_AVD_HOME/HOME");
1877
+ continue;
1878
+ }
1744
1879
  }
1880
+ operation?.signal.throwIfAborted();
1745
1881
  settled = false;
1746
- const outcome = await stopHostedDevice(current, claim);
1882
+ const outcome = await stopHostedDevice(current, claim, operation);
1747
1883
  settled = outcome.settled;
1884
+ if (!settled) throw new WorkspaceProcessCleanupError([claim], outcome.notice ?? "Hosted cleanup could not be verified.", "Verify the hosted worker and its descendants have stopped.");
1885
+ operation?.signal.throwIfAborted();
1748
1886
  const result = isJsonObject(outcome.value) ? outcome.value : null;
1749
1887
  const stopped = result && parseHostedPlatformDevice(result.device, device.platform);
1750
- if (!settled || outcome.notice || result?.state !== "stopped" || !stopped || hostedDeviceId(stopped) !== device.id) throw new Error(outcome.notice ?? (typeof result?.notice === "string" ? result.notice : "Hosted deletion could not be verified."));
1751
- console.log(`Deleted ${label}`);
1752
- recordGcResult("parkedHostedDevice", "done", label, { id: device.id });
1888
+ if (outcome.notice || result?.state !== "stopped" || !stopped || hostedDeviceId(stopped) !== device.id) throw new Error(outcome.notice ?? (typeof result?.notice === "string" ? result.notice : "Hosted deletion could not be verified."));
1889
+ output.log(`Deleted ${label}`);
1890
+ output.record("parkedHostedDevice", "done", label, { id: device.id });
1753
1891
  } catch (error) {
1892
+ if (claim && (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error)) {
1893
+ settled = false;
1894
+ throw Object.assign(new WorkspaceProcessCleanupError([.../* @__PURE__ */ new Set([claim, ...error instanceof WorkspaceProcessCleanupError ? error.claims : []])], error.message, "Verify the hosted device operation and its descendants have stopped."), { reportCleanup: true });
1895
+ }
1896
+ operation?.signal.throwIfAborted();
1754
1897
  const detail = describeError(error);
1755
- console.error(`Failed to delete ${label}: ${detail}`);
1756
- recordGcResult("parkedHostedDevice", "failed", label, {
1898
+ output.error(`Failed to delete ${label}: ${detail}`);
1899
+ output.record("parkedHostedDevice", "failed", label, {
1757
1900
  id: device.id,
1758
1901
  detail
1759
1902
  });
@@ -1772,13 +1915,17 @@ function readGcReport(options = {}, deps = {}) {
1772
1915
  cache: options.cache,
1773
1916
  worktrees: options.worktrees,
1774
1917
  json: true
1775
- }, deps, true);
1918
+ }, deps, {
1919
+ output: GC_OUTPUT,
1920
+ takeResults: () => []
1921
+ }, true);
1776
1922
  }
1777
- function removeInvalidProjectEntries(invalidProjects) {
1923
+ function removeInvalidProjectEntries(invalidProjects, output, signal) {
1778
1924
  for (const path of invalidProjects) {
1925
+ signal?.throwIfAborted();
1779
1926
  removeProject(path);
1780
- console.log(chalk.green(`Removed the invalid registry entry ${path}`));
1781
- recordGcResult("project", "done", path);
1927
+ output.log(chalk.green(`Removed the invalid registry entry ${path}`));
1928
+ output.record("project", "done", path);
1782
1929
  }
1783
1930
  }
1784
1931
  function collectGcMemory(scope, env, operation) {
@@ -1814,11 +1961,14 @@ function classifyProjectEntries(cfg, mountedVolumes) {
1814
1961
  };
1815
1962
  }
1816
1963
  /** The workspace directories an unscoped run removes whole: those of dead projects and orphaned directories. */
1817
- function removedWholeWorkspaceDirs() {
1964
+ async function removedWholeWorkspaceDirs(operation) {
1818
1965
  const mountedVolumes = listMountedVolumes();
1819
1966
  const cfg = loadConfig$1();
1820
1967
  const { deadProjects } = classifyProjectEntries(cfg, mountedVolumes);
1821
- const { orphaned } = collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes, { measure: false });
1968
+ const { orphaned } = await collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes, {
1969
+ measure: false,
1970
+ operation
1971
+ });
1822
1972
  return [...orphaned.map((entry) => entry.dir), ...deadProjects.map(workspaceDir)];
1823
1973
  }
1824
1974
  async function collectGcReport({ olderThan = null, cache = null, worktrees = false, refresh = true, now = Date.now(), lastTouched = workspaceLastUsed } = {}, deps = {}) {
@@ -1829,10 +1979,10 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
1829
1979
  const withWorkspaces = includesWorkspaceOutputs(scope);
1830
1980
  const selected = selectCaches(discoverCaches(env, operation?.cwd), scope).filter((c) => !withWorkspaces || !isInsideWorkspaces(c.dir));
1831
1981
  if (selected.length) phase("caches", `measuring ${plural(selected.length, "shared cache")}`, operation);
1832
- const sized = await sizeCaches(selected);
1982
+ const sized = await sizeCaches(selected, operation);
1833
1983
  const caches = scope === null ? previewScopedEmptying(sized, env) : planCacheEmptying(sized, all, env);
1834
1984
  if (scope) return {
1835
- archives: includesArchives(scope) ? collectArchives(scope, olderThan, now) : void 0,
1985
+ archives: includesArchives(scope) ? await collectArchives(scope, olderThan, now, operation?.signal) : void 0,
1836
1986
  skipped: [],
1837
1987
  deadProjects: [],
1838
1988
  invalidProjects: [],
@@ -1881,14 +2031,16 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
1881
2031
  workspaceOutputs: withWorkspaces ? await collectWorkspaceOutputs({
1882
2032
  olderThan,
1883
2033
  now,
1884
- exclude: isWorkspacesScope(scope) ? removedWholeWorkspaceDirs() : []
2034
+ exclude: isWorkspacesScope(scope) ? await removedWholeWorkspaceDirs(operation) : [],
2035
+ signal: operation?.signal,
2036
+ operation
1885
2037
  }) : null,
1886
2038
  workspaceLogs: [],
1887
- recordings: includesRecordings(scope) ? collectRecordings({
2039
+ recordings: includesRecordings(scope) ? await collectRecordings({
1888
2040
  whole: all,
1889
2041
  olderThan,
1890
2042
  now
1891
- }) : [],
2043
+ }, [], operation?.signal) : [],
1892
2044
  worktreeSweep: null,
1893
2045
  memory: memoryCacheKind(scope) ? await collectGcMemory(memoryScope(scope), env, operation) : null,
1894
2046
  agentDeviceUsage: null,
@@ -1914,7 +2066,8 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
1914
2066
  const tempLeftovers = await collectTempLeftovers({
1915
2067
  now,
1916
2068
  env,
1917
- signal: operation?.signal
2069
+ signal: operation?.signal,
2070
+ operation
1918
2071
  });
1919
2072
  const mountedVolumes = listMountedVolumes();
1920
2073
  const cfg = loadConfig$1();
@@ -1922,6 +2075,8 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
1922
2075
  if (!easSessionSweep) try {
1923
2076
  easSessionSweep = await collectEasSessionSweep(cfg, deps);
1924
2077
  } catch (error) {
2078
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
2079
+ operation?.signal.throwIfAborted();
1925
2080
  easSessionSweep = {
1926
2081
  projectScope: null,
1927
2082
  orphaned: [],
@@ -1938,7 +2093,7 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
1938
2093
  now
1939
2094
  });
1940
2095
  const { deadProjects, invalidProjects, skipped } = classifyProjectEntries(cfg, mountedVolumes);
1941
- const workspaceDirs = await collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes);
2096
+ const workspaceDirs = await collectOrphanedWorkspaces(Object.keys(cfg?.projects ?? {}), mountedVolumes, { operation });
1942
2097
  skipped.push(...workspaceDirs.skipped);
1943
2098
  const statusCaches = collectStaleStatusCaches(mountedVolumes);
1944
2099
  skipped.push(...statusCaches.skipped);
@@ -1958,7 +2113,11 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
1958
2113
  const avdSizes = /* @__PURE__ */ new Map();
1959
2114
  const avdSize = (dir, options) => {
1960
2115
  let bytes = avdSizes.get(dir);
1961
- if (bytes === void 0) avdSizes.set(dir, bytes = Promise.resolve((deps.directorySize ?? directorySizeAsync)(dir, options)));
2116
+ if (bytes === void 0) avdSizes.set(dir, bytes = Promise.resolve((deps.directorySize ?? directorySizeAsync)(dir, {
2117
+ ...options,
2118
+ operation,
2119
+ signal: operation?.signal
2120
+ })));
1962
2121
  return bytes;
1963
2122
  };
1964
2123
  const unsweepableReason = cfg === null ? "no Stim config found" : deviceSweepIsScoped(env) ? "STIM_HOME scopes this config, but simulators and AVDs are machine-global" : null;
@@ -1967,11 +2126,25 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
1967
2126
  let simNames = [];
1968
2127
  let avdNames = [];
1969
2128
  try {
1970
- simNames = (await listAllIosSimsAsync({ timeoutMs: DEVICE_LIST_TIMEOUT_MS })).map((s) => s.name);
1971
- } catch {}
2129
+ simNames = (await listAllIosSimsAsync({
2130
+ timeoutMs: DEVICE_LIST_TIMEOUT_MS,
2131
+ operation,
2132
+ signal: operation?.signal
2133
+ })).map((s) => s.name);
2134
+ } catch (error) {
2135
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
2136
+ operation?.signal.throwIfAborted();
2137
+ }
1972
2138
  try {
1973
- avdNames = await listAvdsAsync({ timeoutMs: DEVICE_LIST_TIMEOUT_MS });
1974
- } catch {}
2139
+ avdNames = await listAvdsAsync({
2140
+ timeoutMs: DEVICE_LIST_TIMEOUT_MS,
2141
+ operation,
2142
+ signal: operation?.signal
2143
+ });
2144
+ } catch (error) {
2145
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
2146
+ operation?.signal.throwIfAborted();
2147
+ }
1975
2148
  deviceSweepNotices.push(...describeUnverifiableDevices(simNames, avdNames, { reason: unsweepableReason }));
1976
2149
  } else {
1977
2150
  let sims = [];
@@ -1980,19 +2153,29 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
1980
2153
  try {
1981
2154
  const allSims = await listAllIosSimsAsync({
1982
2155
  timeoutMs: DEVICE_LIST_TIMEOUT_MS,
1983
- includeUnavailable: true
2156
+ includeUnavailable: true,
2157
+ operation,
2158
+ signal: operation?.signal
1984
2159
  });
1985
2160
  staleLedgerEntries = [...staleLedgerEntries, ...findStaleLedgerEntries(ledger, allSims)];
1986
2161
  sims = allSims.filter((sim) => sim.available);
1987
- } catch {
2162
+ } catch (error) {
2163
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
2164
+ operation?.signal.throwIfAborted();
1988
2165
  simsChecked = false;
1989
2166
  deviceSweepNotices.push(`ios device sweep skipped: simulator tooling did not answer within ${DEVICE_LIST_TIMEOUT_MS / 1e3}s`);
1990
2167
  }
1991
2168
  let avds = [];
1992
2169
  let avdsChecked = true;
1993
2170
  try {
1994
- avds = await listAvdsAsync({ timeoutMs: DEVICE_LIST_TIMEOUT_MS });
1995
- } catch {
2171
+ avds = await listAvdsAsync({
2172
+ timeoutMs: DEVICE_LIST_TIMEOUT_MS,
2173
+ operation,
2174
+ signal: operation?.signal
2175
+ });
2176
+ } catch (error) {
2177
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
2178
+ operation?.signal.throwIfAborted();
1996
2179
  avdsChecked = false;
1997
2180
  deviceSweepNotices.push(`android device sweep skipped: emulator tooling did not answer within ${DEVICE_LIST_TIMEOUT_MS / 1e3}s`);
1998
2181
  }
@@ -2023,9 +2206,10 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2023
2206
  return directories.length ? directories.map((orphanedDirectory) => Object.assign({}, device, { orphanedDirectory })) : [device];
2024
2207
  }), {
2025
2208
  avdDirectory: deps.avdDirectory ?? ownedAvdDirectory,
2026
- size: avdSize
2209
+ size: avdSize,
2210
+ operation
2027
2211
  });
2028
- idleDevices = operation ? await collectIdleDevicesAsync(cfg, sims, deadProjects, now, env) : collectIdleDevices(cfg, sims, deadProjects, now);
2212
+ idleDevices = operation ? await collectIdleDevicesAsync(cfg, sims, deadProjects, now, env, operation.signal, operation) : collectIdleDevices(cfg, sims, deadProjects, now);
2029
2213
  if (simsChecked || avdsChecked) {
2030
2214
  const lists = {
2031
2215
  sims,
@@ -2060,7 +2244,8 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2060
2244
  deadProjects
2061
2245
  }), {
2062
2246
  avdDirectory: deps.avdDirectory ?? ownedAvdDirectory,
2063
- size: avdSize
2247
+ size: avdSize,
2248
+ operation
2064
2249
  });
2065
2250
  }
2066
2251
  const locks = listBuildLocks();
@@ -2112,14 +2297,19 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2112
2297
  workspaceOutputs: await collectWorkspaceOutputs({
2113
2298
  olderThan,
2114
2299
  now,
2115
- exclude: goneWorkspaceDirs
2300
+ exclude: goneWorkspaceDirs,
2301
+ signal: operation?.signal,
2302
+ operation
2303
+ }),
2304
+ workspaceLogs: await collectWorkspaceLogs({
2305
+ exclude: goneWorkspaceDirs,
2306
+ operation
2116
2307
  }),
2117
- workspaceLogs: collectWorkspaceLogs({ exclude: goneWorkspaceDirs }),
2118
- recordings: collectRecordings({
2308
+ recordings: await collectRecordings({
2119
2309
  whole: false,
2120
2310
  olderThan,
2121
2311
  now
2122
- }, goneWorkspaceDirs),
2312
+ }, goneWorkspaceDirs, operation?.signal),
2123
2313
  worktreeSweep,
2124
2314
  finishedDevices: listed ? collectFinishedDevices({
2125
2315
  config: cfg,
@@ -2142,30 +2332,41 @@ async function collectGcReport({ olderThan = null, cache = null, worktrees = fal
2142
2332
  };
2143
2333
  }
2144
2334
  async function runGc(opts = {}, deps = {}) {
2145
- if (!opts.json) {
2146
- await maintenanceGuardedSweep(opts, deps);
2147
- return;
2148
- }
2149
- const log = console.log;
2150
- console.log = console.error;
2151
- let payload;
2335
+ let result;
2152
2336
  try {
2153
- payload = await maintenanceGuardedSweep(opts, deps);
2154
- } finally {
2155
- console.log = log;
2337
+ result = await withGcMaintenanceClaim(opts, (claim) => executeGc(opts, deps, (stream, line) => {
2338
+ if (stream === "stderr" || opts.json) console.error(line);
2339
+ else console.log(line);
2340
+ }, claim));
2341
+ } catch (error) {
2342
+ const failure = (0, ownership_claim_exports.claimFailure)(error, opts.delete ? "stim gc --delete" : "stim gc --idle <duration>") ?? (deps.operation?.signal.aborted ? {
2343
+ code: "STIM_CANCELLED",
2344
+ message: "GC was cancelled.",
2345
+ remedy: "Retry the same GC command."
2346
+ } : null);
2347
+ if (!failure) throw error;
2348
+ console.error(`${failure.code}: ${failure.message}`);
2349
+ console.error(failure.remedy);
2350
+ result = {
2351
+ payload: failure,
2352
+ exitCode: 1
2353
+ };
2156
2354
  }
2157
- console.log(JSON.stringify(payload));
2355
+ if (result.exitCode) process.exitCode = result.exitCode;
2356
+ if (opts.json) console.log(JSON.stringify(result.payload));
2158
2357
  }
2159
- async function maintenanceGuardedSweep(opts, deps) {
2160
- if (!opts.delete) return sweep(opts, deps);
2358
+ /** Runs GC under this home's exclusive maintenance claim, retaining it when owned cleanup is unresolved. */
2359
+ async function withGcMaintenanceClaim(opts, work) {
2360
+ if (!opts.delete && opts.idle === void 0) return work(void 0);
2161
2361
  let claim;
2362
+ let retained = false;
2162
2363
  try {
2163
2364
  const root = maintenanceRunClaims();
2164
2365
  const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
2165
2366
  root,
2166
2367
  mode: "exclusive",
2167
2368
  label: "maintenance",
2168
- details: { trigger: "gc --delete" }
2369
+ details: { trigger: opts.delete ? "gc --delete" : "gc --idle" }
2169
2370
  });
2170
2371
  if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
2171
2372
  claim = attempt.acquired;
@@ -2178,20 +2379,45 @@ async function maintenanceGuardedSweep(opts, deps) {
2178
2379
  reason: `held by pid ${holder?.owner.pid ?? "?"} since ${holder?.startedAt ?? "?"} (${String(holder?.details.trigger ?? "maintenance")})`
2179
2380
  });
2180
2381
  }
2181
- return await sweep(opts, deps);
2182
- } catch (error) {
2183
- const failure = (0, ownership_claim_exports.claimFailure)(error, "stim gc --delete");
2184
- if (!failure) throw error;
2185
- console.error(`${failure.code}: ${failure.message}`);
2186
- console.error(failure.remedy);
2187
- process.exitCode = 1;
2188
- return failure;
2382
+ return await work(claim);
2383
+ } catch (cause) {
2384
+ if (claim && (cause instanceof WorkspaceProcessCleanupError || cause instanceof Error && "reportCleanup" in cause)) {
2385
+ retained = true;
2386
+ throw Object.assign(new WorkspaceProcessCleanupError([.../* @__PURE__ */ new Set([claim, ...cause instanceof WorkspaceProcessCleanupError ? cause.claims : []])], cause.message, "Verify GC and its owned children have stopped."), { reportCleanup: true });
2387
+ }
2388
+ throw cause;
2389
+ } finally {
2390
+ if (claim && !retained) (0, ownership_claim_exports.releaseClaim)(claim);
2391
+ }
2392
+ }
2393
+ /** Executes one already-admitted sweep with separate progress, results and exit status. */
2394
+ async function executeGc(opts, deps, write, claim) {
2395
+ const results = createGcResults();
2396
+ let failed = false;
2397
+ const output = {
2398
+ log: (line) => write("stdout", line),
2399
+ error: (line) => write("stderr", line),
2400
+ record: results.record,
2401
+ fail: () => {
2402
+ failed = true;
2403
+ }
2404
+ };
2405
+ try {
2406
+ declareSpawnsOn(deps.operation, claim);
2407
+ return {
2408
+ payload: await sweep(opts, deps, {
2409
+ output,
2410
+ takeResults: results.take
2411
+ }),
2412
+ exitCode: failed ? 1 : 0
2413
+ };
2189
2414
  } finally {
2190
- if (claim) (0, ownership_claim_exports.releaseClaim)(claim);
2415
+ stopDeclaringSpawnsOn(deps.operation, claim);
2191
2416
  }
2192
2417
  }
2193
- async function sweep(opts, deps, reportOnly = false) {
2194
- const core = (dependencies) => runGcCore(opts, dependencies, reportOnly);
2418
+ async function sweep(opts, deps, execution, reportOnly = false) {
2419
+ const { output } = execution;
2420
+ const core = (dependencies) => runGcCore(opts, dependencies, execution, reportOnly);
2195
2421
  if (opts.cache && (opts.worktrees || opts.idle !== void 0)) {
2196
2422
  const flag = opts.worktrees ? "--worktrees" : "--idle";
2197
2423
  const message = opts.worktrees ? "--cache acts only on the named caches, and --worktrees sweeps linked worktrees." : "--cache acts only on the named caches, and --idle shuts down idle owned devices.";
@@ -2201,10 +2427,10 @@ async function sweep(opts, deps, reportOnly = false) {
2201
2427
  message,
2202
2428
  remedy
2203
2429
  };
2204
- console.error(chalk.red(message));
2205
- console.error(chalk.dim(remedy));
2206
- console.error(chalk.red("failed: STIM_BAD_ARG"));
2207
- process.exitCode = 1;
2430
+ output.error(chalk.red(message));
2431
+ output.error(chalk.dim(remedy));
2432
+ output.error(chalk.red("failed: STIM_BAD_ARG"));
2433
+ output.fail();
2208
2434
  return {
2209
2435
  code: "STIM_BAD_ARG",
2210
2436
  message,
@@ -2220,10 +2446,10 @@ async function sweep(opts, deps, reportOnly = false) {
2220
2446
  message,
2221
2447
  remedy
2222
2448
  };
2223
- console.error(chalk.red(message));
2224
- console.error(chalk.dim(remedy));
2225
- console.error(chalk.red("failed: STIM_BAD_ARG"));
2226
- process.exitCode = 1;
2449
+ output.error(chalk.red(message));
2450
+ output.error(chalk.dim(remedy));
2451
+ output.error(chalk.red("failed: STIM_BAD_ARG"));
2452
+ output.fail();
2227
2453
  return {
2228
2454
  code: "STIM_BAD_ARG",
2229
2455
  message,
@@ -2233,7 +2459,7 @@ async function sweep(opts, deps, reportOnly = false) {
2233
2459
  const poolError = parkedMaxSetting("ios").error || parkedMaxSetting("android").error;
2234
2460
  if (poolError) {
2235
2461
  if (reportOnly) phase("note", `${poolError} ${POOL_SETTING_REMEDY}`, deps.operation);
2236
- else console.error(chalk.yellow(`${poolError} ${POOL_SETTING_REMEDY}`));
2462
+ else output.error(chalk.yellow(`${poolError} ${POOL_SETTING_REMEDY}`));
2237
2463
  }
2238
2464
  if (opts.cache) return core({
2239
2465
  ...deps,
@@ -2286,9 +2512,15 @@ async function sweep(opts, deps, reportOnly = false) {
2286
2512
  }, {
2287
2513
  waitMs: 0,
2288
2514
  ownerPurpose: "EAS orphan sweep",
2289
- machineRoot: deps.easLedgerRoot
2515
+ machineRoot: deps.easLedgerRoot,
2516
+ ...deps.operation ? {
2517
+ signal: deps.operation.signal,
2518
+ declareSpawnsFor: deps.operation
2519
+ } : {}
2290
2520
  });
2291
2521
  } catch (error) {
2522
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
2523
+ deps.operation?.signal.throwIfAborted();
2292
2524
  easSessionSweep = {
2293
2525
  projectScope: projectRoot,
2294
2526
  orphaned: [],
@@ -2301,44 +2533,48 @@ async function sweep(opts, deps, reportOnly = false) {
2301
2533
  precollectedEasSessionSweep: easSessionSweep
2302
2534
  });
2303
2535
  }
2304
- async function pruneDeadProjects(deadProjects) {
2536
+ async function pruneDeadProjects(deadProjects, output, operation) {
2305
2537
  let deleteFailures = 0;
2306
2538
  for (const path of deadProjects) {
2539
+ operation?.signal.throwIfAborted();
2307
2540
  if (existsSync$1(path) || !isOnMountedVolume(path)) {
2308
- console.log(chalk.yellow(`Kept ${path}: its absence can no longer be confirmed.`));
2309
- recordGcResult("project", "kept", path, { detail: "its absence can no longer be confirmed" });
2541
+ output.log(chalk.yellow(`Kept ${path}: its absence can no longer be confirmed.`));
2542
+ output.record("project", "kept", path, { detail: "its absence can no longer be confirmed" });
2310
2543
  continue;
2311
2544
  }
2312
2545
  const result = await reclaimProject(path, {
2313
2546
  archive: { removedBy: "gc" },
2314
- stopRequester: { by: "stim gc --delete" }
2547
+ stopRequester: { by: "stim gc --delete" },
2548
+ operation
2315
2549
  }).catch((error) => {
2550
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
2551
+ operation?.signal.throwIfAborted();
2316
2552
  deleteFailures++;
2317
- console.log(chalk.red(`Could not prune ${path}; its registry entry was kept: ${error.message}`));
2318
- recordGcResult("project", "failed", path, { detail: `its registry entry was kept: ${error.message}` });
2553
+ output.log(chalk.red(`Could not prune ${path}; its registry entry was kept: ${error.message}`));
2554
+ output.record("project", "failed", path, { detail: `its registry entry was kept: ${error.message}` });
2319
2555
  return null;
2320
2556
  });
2321
2557
  if (!result) continue;
2322
2558
  if (result.keptEntry) {
2323
- console.log(chalk.yellow(`Could not fully prune ${path}; its registry entry was kept.`));
2324
- recordGcResult("project", "kept", path, { detail: "it could not be fully pruned; its registry entry was kept" });
2559
+ output.log(chalk.yellow(`Could not fully prune ${path}; its registry entry was kept.`));
2560
+ output.record("project", "kept", path, { detail: "it could not be fully pruned; its registry entry was kept" });
2325
2561
  } else {
2326
- console.log(chalk.green(`Pruned ${path}`));
2327
- recordGcResult("project", "done", path);
2562
+ output.log(chalk.green(`Pruned ${path}`));
2563
+ output.record("project", "done", path);
2328
2564
  }
2329
- for (const dir of result.removedWorkspaceDirs) console.log(chalk.dim(` removed workspace output ${dir}`));
2565
+ for (const dir of result.removedWorkspaceDirs) output.log(chalk.dim(` removed workspace output ${dir}`));
2330
2566
  for (const dir of result.failedWorkspaceDirs) {
2331
- console.log(chalk.red(` could not remove workspace output ${dir}`));
2332
- recordGcResult("workspaceDirectory", "failed", dir, { detail: `could not remove it while pruning ${path}` });
2567
+ output.log(chalk.red(` could not remove workspace output ${dir}`));
2568
+ output.record("workspaceDirectory", "failed", dir, { detail: `could not remove it while pruning ${path}` });
2333
2569
  deleteFailures += 1;
2334
2570
  }
2335
- if (result.killedPid) console.log(chalk.dim(` killed orphaned Metro pid ${result.killedPid}`));
2336
- if (result.stoppedSession) console.log(chalk.dim(` stopped remote session ${result.stoppedSession}`));
2337
- if (result.stoppedTunnel) console.log(chalk.dim(` stopped ${result.stoppedTunnel} tunnel`));
2338
- for (const name of result.deletedDevices) recordGcResult("device", "done", name);
2571
+ if (result.killedPid) output.log(chalk.dim(` killed orphaned Metro pid ${result.killedPid}`));
2572
+ if (result.stoppedSession) output.log(chalk.dim(` stopped remote session ${result.stoppedSession}`));
2573
+ if (result.stoppedTunnel) output.log(chalk.dim(` stopped ${result.stoppedTunnel} tunnel`));
2574
+ for (const name of result.deletedDevices) output.record("device", "done", name);
2339
2575
  for (const s of result.skippedDevices) {
2340
- console.log(chalk.yellow(` ${s.name}: ${s.reason}`));
2341
- recordGcResult("device", result.failedDevices.includes(s) ? "failed" : "kept", s.name, {
2576
+ output.log(chalk.yellow(` ${s.name}: ${s.reason}`));
2577
+ output.record("device", result.failedDevices.includes(s) ? "failed" : "kept", s.name, {
2342
2578
  id: s.udid ?? null,
2343
2579
  detail: s.reason
2344
2580
  });
@@ -2370,19 +2606,21 @@ function dryRunHint(cache, all, actionable, caches) {
2370
2606
  function scopedWorkPending(cache, report) {
2371
2607
  return memoryWorkPending(cache, report.memory) || gradleVersionsPending(report.gradleVersions);
2372
2608
  }
2373
- function pruneStatusCaches(report) {
2609
+ async function pruneStatusCaches(report, output, signal) {
2374
2610
  if (report.cacheScope !== null) return 0;
2375
- return deleteStaleStatusCaches(collectStaleStatusCaches(listMountedVolumes()).stale);
2611
+ return deleteStaleStatusCaches(collectStaleStatusCaches(listMountedVolumes()).stale, output, signal);
2376
2612
  }
2377
- async function reclaimParkedDevices(report, deps) {
2378
- return report.cacheScope ? await eraseParkedDevices(report.parkedSims, report.parkedAvds) : await deleteParkedSims(report.parkedSims, deps) + await deleteParkedAvds(report.parkedAvds);
2613
+ async function reclaimParkedDevices(report, deps, output) {
2614
+ return report.cacheScope ? await eraseParkedDevices(report.parkedSims, report.parkedAvds, output, deps.operation) : await deleteParkedSims(report.parkedSims, deps, output) + await deleteParkedAvds(report.parkedAvds, output, deps.operation);
2379
2615
  }
2380
2616
  function reportActionable(report) {
2381
2617
  const { deadProjects, invalidProjects, orphanedDevices, staleDevices, staleDeviceRecords, buildLocks, buildSlots, deviceLeases, easSessionSweep, caches, olderThan, all, cacheScope: cache } = report;
2382
2618
  return archiveWorkPending(report.archives, deadProjects.length + invalidProjects.length > 0 || report.orphanedWorkspaces.length > 0 || Boolean(report.workspaceOutputs?.workspaces.some((entry) => entry.willClear)) || report.workspaceLogs.some((entry) => entry.willTrim) || report.recordings.some((entry) => entry.willDelete && !entry.withWorkspace) || Boolean(report.tempLeftovers?.entries.some((entry) => entry.willDelete)) || Boolean(report.worktreeSweep?.worktrees.some((entry) => !entry.skipped)) || report.parkedSims.length > 0 || report.parkedAvds.length + report.parkedHostedDevices.filter((device) => device.listed !== false).length > 0 || orphanedDevices.length > 0 || staleDevices.length + report.finishedDevices.length > 0 || staleDeviceRecords.length > 0 || report.staleLedgerEntries.length + report.staleEasLedgerEntries.length > 0 || report.staleStatusCaches.length > 0 || buildLocks.stale.length > 0 || buildSlots.stale.length > 0 || deviceLeases.expired.length > 0 || easSessionSweep.orphaned.length > 0 || (olderThan !== null || all) && caches.length > 0 || scopedWorkPending(cache, report));
2383
2619
  }
2384
- async function runGcCore(opts, deps, reportOnly = false) {
2385
- if (!reportOnly) takeGcResults();
2620
+ async function runGcCore(opts, deps, execution, reportOnly = false) {
2621
+ const { output, takeResults } = execution;
2622
+ const signal = deps.operation?.signal;
2623
+ throwIfCancelled(signal, "GC");
2386
2624
  const olderThan = typeof opts.olderThan === "number" ? opts.olderThan : null;
2387
2625
  const cache = typeof opts.cache === "string" && opts.cache.trim() ? opts.cache : null;
2388
2626
  const refresh = opts.delete === true;
@@ -2392,13 +2630,14 @@ async function runGcCore(opts, deps, reportOnly = false) {
2392
2630
  worktrees: Boolean(opts.worktrees),
2393
2631
  refresh
2394
2632
  }, deps);
2633
+ throwIfCancelled(signal, "GC");
2395
2634
  if (cache && report.caches.length === 0 && report.workspaceOutputs === null && !selectsBeyondCaches(cache)) {
2396
2635
  const names = [...new Set(discoverCaches(deps.operation?.env ?? process.env, deps.operation?.cwd).map((c) => c.name))];
2397
2636
  const message = `No shared cache carries "${cache}" in its name or directory.`;
2398
2637
  if (!reportOnly) {
2399
- console.log(chalk.yellow(message));
2400
- if (names.length) console.log(chalk.dim(`Caches on this machine: ${names.join(", ")}`));
2401
- if (opts.json) process.exitCode = 1;
2638
+ output.log(chalk.yellow(message));
2639
+ if (names.length) output.log(chalk.dim(`Caches on this machine: ${names.join(", ")}`));
2640
+ if (opts.json) output.fail();
2402
2641
  }
2403
2642
  return {
2404
2643
  code: "STIM_BAD_ARG",
@@ -2410,21 +2649,26 @@ async function runGcCore(opts, deps, reportOnly = false) {
2410
2649
  const refusal = archiveRefusal(report.archives);
2411
2650
  if (refusal !== null) {
2412
2651
  if (!reportOnly) {
2413
- process.exitCode = 1;
2414
- console.error(chalk.red(refusal.message));
2415
- console.error(chalk.dim(refusal.remedy));
2416
- console.error(chalk.red(`failed: ${refusal.code}`));
2652
+ output.fail();
2653
+ output.error(chalk.red(refusal.message));
2654
+ output.error(chalk.dim(refusal.remedy));
2655
+ output.error(chalk.red(`failed: ${refusal.code}`));
2417
2656
  }
2418
2657
  return refusal;
2419
2658
  }
2420
2659
  const all = report.all;
2421
- if (!reportOnly) for (const line of formatGcReport(report)) console.log(line);
2660
+ if (!reportOnly) for (const line of formatGcReport(report)) output.log(line);
2422
2661
  const { deadProjects, invalidProjects, orphanedDevices, staleDevices, staleDeviceRecords, buildLocks, buildSlots, deviceLeases, easSessionSweep, caches } = report;
2423
2662
  const actionable = reportActionable(report);
2424
2663
  const idle = opts.idle ?? null;
2425
- const idleFailures = idle === null ? 0 : (await shutDownIdleDevices(report.idleDevices, idle, () => collectIdleDevices(loadConfig$1(), listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS }), report.deadProjects))).failures;
2426
- if (idleFailures) process.exitCode = 1;
2427
- const inventory = opts.json && !opts.delete && idle === null && report.cacheScope === null ? await collectInventory(loadConfig$1(), deadProjects, deps.operation?.env ?? process.env) : null;
2664
+ throwIfCancelled(signal, "GC");
2665
+ const idleFailures = idle === null ? 0 : (await shutDownIdleDevices(report.idleDevices, idle, async () => deps.operation ? collectIdleDevicesAsync(loadConfig$1(), await listAllIosSimsAsync({
2666
+ timeoutMs: DEVICE_LIST_TIMEOUT_MS,
2667
+ operation: deps.operation,
2668
+ signal
2669
+ }), report.deadProjects, Date.now(), deps.operation.env, signal, deps.operation) : collectIdleDevices(loadConfig$1(), listAllIosSims({ timeoutMs: DEVICE_LIST_TIMEOUT_MS }), report.deadProjects), output, signal, deps.operation)).failures;
2670
+ if (idleFailures) output.fail();
2671
+ const inventory = opts.json && !opts.delete && idle === null && report.cacheScope === null ? await collectInventory(loadConfig$1(), deadProjects, deps.operation?.env ?? process.env, deps.operation) : null;
2428
2672
  const payload = (failures) => ({
2429
2673
  mode: opts.delete ? "delete" : "dry-run",
2430
2674
  idle,
@@ -2435,48 +2679,70 @@ async function runGcCore(opts, deps, reportOnly = false) {
2435
2679
  failures,
2436
2680
  sections: gcReportSections(report),
2437
2681
  ...report.maintenance ? { maintenance: report.maintenance } : {},
2438
- results: reportOnly ? [] : takeGcResults(),
2682
+ results: reportOnly ? [] : takeResults(),
2439
2683
  inventory
2440
2684
  });
2441
2685
  if (!opts.delete) {
2442
2686
  const hint = dryRunHint(cache, all, actionable, caches.length > 0);
2443
- if (hint && !reportOnly) console.log(chalk.dim(`\n${hint}`));
2687
+ if (hint && !reportOnly) output.log(chalk.dim(`\n${hint}`));
2444
2688
  return payload(idle === null ? null : idleFailures);
2445
2689
  }
2446
2690
  let deleteFailures = idleFailures;
2447
- deleteFailures += deleteArchives(report.archives);
2448
- deleteFailures += report.workspaceOutputs ? (await clearWorkspaceOutputs(report.workspaceOutputs, { olderThan })).failures : 0;
2449
- deleteFailures += await trimWorkspaceLogs(report.workspaceLogs);
2450
- deleteFailures += deleteRecordings(report.recordings, {
2691
+ throwIfCancelled(signal, "GC");
2692
+ deleteFailures += await deleteArchives(report.archives, output, signal);
2693
+ throwIfCancelled(signal, "GC");
2694
+ deleteFailures += report.workspaceOutputs ? (await clearWorkspaceOutputs(report.workspaceOutputs, {
2695
+ olderThan,
2696
+ signal,
2697
+ operation: deps.operation
2698
+ }, output)).failures : 0;
2699
+ throwIfCancelled(signal, "GC");
2700
+ deleteFailures += await trimWorkspaceLogs(report.workspaceLogs, output, deps.operation);
2701
+ throwIfCancelled(signal, "GC");
2702
+ deleteFailures += await deleteRecordings(report.recordings, {
2451
2703
  whole: all,
2452
2704
  olderThan,
2453
2705
  now: Date.now()
2454
- });
2455
- deleteFailures += await reclaimParkedDevices(report, deps);
2456
- deleteFailures += await deleteParkedHostedDevices(report.parkedHostedDevices);
2457
- deleteFailures += await reclaimMemory(cache, report.memory);
2458
- deleteFailures += await deleteGradleVersions(report.gradleVersions);
2459
- removeInvalidProjectEntries(invalidProjects);
2460
- deleteFailures += await pruneDeadProjects(deadProjects);
2461
- deleteFailures += await deleteOrphanedWorkspaces(report.orphanedWorkspaces);
2462
- deleteFailures += await deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords);
2463
- deleteFailures += await deleteFinishedDevices(report);
2464
- deleteFailures += forgetStaleLedgerEntries(report.staleLedgerEntries);
2465
- deleteFailures += forgetStaleEasLedgerEntries(report.staleEasLedgerEntries, listMountedVolumes(), deps);
2466
- deleteFailures += pruneStatusCaches(report);
2706
+ }, output, signal);
2707
+ throwIfCancelled(signal, "GC");
2708
+ deleteFailures += await reclaimParkedDevices(report, deps, output);
2709
+ throwIfCancelled(signal, "GC");
2710
+ deleteFailures += await deleteParkedHostedDevices(report.parkedHostedDevices, { operation: deps.operation }, output);
2711
+ throwIfCancelled(signal, "GC");
2712
+ deleteFailures += await reclaimMemory(cache, report.memory, output, deps.operation);
2713
+ throwIfCancelled(signal, "GC");
2714
+ deleteFailures += await deleteGradleVersions(report.gradleVersions, output, deps.operation);
2715
+ throwIfCancelled(signal, "GC");
2716
+ removeInvalidProjectEntries(invalidProjects, output, signal);
2717
+ throwIfCancelled(signal, "GC");
2718
+ deleteFailures += await pruneDeadProjects(deadProjects, output, deps.operation);
2719
+ throwIfCancelled(signal, "GC");
2720
+ deleteFailures += await deleteOrphanedWorkspaces(report.orphanedWorkspaces, output, deps.operation);
2721
+ throwIfCancelled(signal, "GC");
2722
+ deleteFailures += await deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords, output, deps.operation);
2723
+ throwIfCancelled(signal, "GC");
2724
+ deleteFailures += await deleteFinishedDevices(report, output, deps.operation);
2725
+ throwIfCancelled(signal, "GC");
2726
+ deleteFailures += await forgetStaleLedgerEntries(report.staleLedgerEntries, output, signal);
2727
+ throwIfCancelled(signal, "GC");
2728
+ deleteFailures += await forgetStaleEasLedgerEntries(report.staleEasLedgerEntries, listMountedVolumes(), deps, output, signal);
2729
+ throwIfCancelled(signal, "GC");
2730
+ deleteFailures += await pruneStatusCaches(report, output, signal);
2467
2731
  if (report.tempLeftovers) deleteFailures += (await deleteTempLeftovers({
2468
2732
  env: deps.operation?.env ?? process.env,
2469
- signal: deps.operation?.signal
2470
- })).failures;
2733
+ signal: deps.operation?.signal,
2734
+ operation: deps.operation
2735
+ }, output)).failures;
2471
2736
  for (const lock of buildLocks.stale) {
2737
+ throwIfCancelled(signal, "GC");
2472
2738
  const cleared = (0, ownership_claim_exports.clearFreeClaimSet)({
2473
2739
  root: lock.path,
2474
2740
  label: `${lock.platform} build`
2475
2741
  });
2476
2742
  if (cleared.status === "held") continue;
2477
2743
  if (cleared.status === "refused") {
2478
- console.log(chalk.yellow(`Left the build lock at ${lock.path} alone: ${cleared.reason}`));
2479
- recordGcResult("buildLock", "kept", `${lock.platform} build lock`, {
2744
+ output.log(chalk.yellow(`Left the build lock at ${lock.path} alone: ${cleared.reason}`));
2745
+ output.record("buildLock", "kept", `${lock.platform} build lock`, {
2480
2746
  id: lock.path,
2481
2747
  detail: cleared.reason
2482
2748
  });
@@ -2484,25 +2750,26 @@ async function runGcCore(opts, deps, reportOnly = false) {
2484
2750
  }
2485
2751
  if (cleared.status === "failed") {
2486
2752
  deleteFailures++;
2487
- console.log(chalk.red(`Failed to clear the build lock at ${lock.path}: ${cleared.reason}`));
2488
- recordGcResult("buildLock", "failed", `${lock.platform} build lock`, {
2753
+ output.log(chalk.red(`Failed to clear the build lock at ${lock.path}: ${cleared.reason}`));
2754
+ output.record("buildLock", "failed", `${lock.platform} build lock`, {
2489
2755
  id: lock.path,
2490
2756
  detail: cleared.reason
2491
2757
  });
2492
2758
  continue;
2493
2759
  }
2494
- recordGcResult("buildLock", "done", `${lock.platform} build lock`, { id: lock.path });
2495
- console.log(chalk.green(`Cleared the ${lock.platform} build lock left by pid ${lock.pid ?? "?"} (${lock.projectRoot || "unrecorded workspace"})`));
2760
+ output.record("buildLock", "done", `${lock.platform} build lock`, { id: lock.path });
2761
+ output.log(chalk.green(`Cleared the ${lock.platform} build lock left by pid ${lock.pid ?? "?"} (${lock.projectRoot || "unrecorded workspace"})`));
2496
2762
  }
2497
2763
  for (const slot of buildSlots.stale) {
2764
+ throwIfCancelled(signal, "GC");
2498
2765
  const cleared = (0, ownership_claim_exports.clearFreeClaimSet)({
2499
2766
  root: slot.path,
2500
2767
  label: "build slot"
2501
2768
  });
2502
2769
  if (cleared.status === "held") continue;
2503
2770
  if (cleared.status === "refused") {
2504
- console.log(chalk.yellow(`Left the build slot at ${slot.path} alone: ${cleared.reason}`));
2505
- recordGcResult("buildSlot", "kept", `build slot ${slot.index ?? "?"}`, {
2771
+ output.log(chalk.yellow(`Left the build slot at ${slot.path} alone: ${cleared.reason}`));
2772
+ output.record("buildSlot", "kept", `build slot ${slot.index ?? "?"}`, {
2506
2773
  id: slot.path,
2507
2774
  detail: cleared.reason
2508
2775
  });
@@ -2510,39 +2777,45 @@ async function runGcCore(opts, deps, reportOnly = false) {
2510
2777
  }
2511
2778
  if (cleared.status === "failed") {
2512
2779
  deleteFailures++;
2513
- console.log(chalk.red(`Failed to clear the build slot at ${slot.path}: ${cleared.reason}`));
2514
- recordGcResult("buildSlot", "failed", `build slot ${slot.index ?? "?"}`, {
2780
+ output.log(chalk.red(`Failed to clear the build slot at ${slot.path}: ${cleared.reason}`));
2781
+ output.record("buildSlot", "failed", `build slot ${slot.index ?? "?"}`, {
2515
2782
  id: slot.path,
2516
2783
  detail: cleared.reason
2517
2784
  });
2518
2785
  continue;
2519
2786
  }
2520
- recordGcResult("buildSlot", "done", `build slot ${slot.index ?? "?"}`, { id: slot.path });
2521
- console.log(chalk.green(`Cleared build slot ${slot.index ?? "?"} left by pid ${slot.pid ?? "?"} (${slot.projectRoot || "unrecorded workspace"})`));
2522
- }
2523
- for (const entry of deviceLeases.expired) try {
2524
- if (removeExpiredLease(entry)) {
2525
- recordGcResult("deviceLease", "done", `${entry.platform} lease on ${entry.id ?? entry.name}`, { id: entry.path });
2526
- console.log(chalk.green(`Cleared the expired ${entry.platform} device lease on ${entry.id ?? entry.name} (held by ${entry.lease?.holder ?? "an unrecorded workspace"})`));
2527
- } else console.log(chalk.dim(`${entry.name} is no longer an expired lease; left alone.`));
2528
- } catch (err) {
2529
- deleteFailures++;
2530
- console.log(chalk.red(`Failed to clear the device lease at ${entry.path}: ${err?.message || err}`));
2531
- recordGcResult("deviceLease", "failed", `${entry.platform} lease on ${entry.id ?? entry.name}`, {
2532
- id: entry.path,
2533
- detail: err?.message || String(err)
2534
- });
2787
+ output.record("buildSlot", "done", `build slot ${slot.index ?? "?"}`, { id: slot.path });
2788
+ output.log(chalk.green(`Cleared build slot ${slot.index ?? "?"} left by pid ${slot.pid ?? "?"} (${slot.projectRoot || "unrecorded workspace"})`));
2789
+ }
2790
+ for (const entry of deviceLeases.expired) {
2791
+ throwIfCancelled(signal, "GC");
2792
+ try {
2793
+ if (removeExpiredLease(entry)) {
2794
+ output.record("deviceLease", "done", `${entry.platform} lease on ${entry.id ?? entry.name}`, { id: entry.path });
2795
+ output.log(chalk.green(`Cleared the expired ${entry.platform} device lease on ${entry.id ?? entry.name} (held by ${entry.lease?.holder ?? "an unrecorded workspace"})`));
2796
+ } else output.log(chalk.dim(`${entry.name} is no longer an expired lease; left alone.`));
2797
+ } catch (err) {
2798
+ deleteFailures++;
2799
+ output.log(chalk.red(`Failed to clear the device lease at ${entry.path}: ${err?.message || err}`));
2800
+ output.record("deviceLease", "failed", `${entry.platform} lease on ${entry.id ?? entry.name}`, {
2801
+ id: entry.path,
2802
+ detail: err?.message || String(err)
2803
+ });
2804
+ }
2535
2805
  }
2536
- deleteFailures += await deleteEasSessions(easSessionSweep, deps);
2537
- if (report.worktreeSweep) deleteFailures += await removeWorktrees(report.worktreeSweep);
2806
+ throwIfCancelled(signal, "GC");
2807
+ deleteFailures += await deleteEasSessions(easSessionSweep, deps, output);
2808
+ throwIfCancelled(signal, "GC");
2809
+ if (report.worktreeSweep) deleteFailures += await removeWorktrees(report.worktreeSweep, { operation: deps.operation }, output);
2538
2810
  if (deleteFailures) {
2539
- console.log(chalk.red(`\n${deleteFailures} entr${deleteFailures === 1 ? "y" : "ies"} could not be deleted; see above.`));
2540
- process.exitCode = 1;
2811
+ output.log(chalk.red(`\n${deleteFailures} entr${deleteFailures === 1 ? "y" : "ies"} could not be deleted; see above.`));
2812
+ output.fail();
2541
2813
  }
2542
- if (all) emptyCaches(caches);
2814
+ throwIfCancelled(signal, "GC");
2815
+ if (all) await emptyCaches(caches, output, signal, deps.operation);
2543
2816
  else if (olderThan === null) {
2544
- if (caches.length) console.log(chalk.dim("Shared caches left alone: pass --cache all to empty them, or --older-than <days> to trim them."));
2545
- } else trimCaches(caches, olderThan);
2817
+ if (caches.length) output.log(chalk.dim("Shared caches left alone: pass --cache all to empty them, or --older-than <days> to trim them."));
2818
+ } else await trimCaches(caches, olderThan, output, signal, deps.operation);
2546
2819
  return payload(deleteFailures);
2547
2820
  }
2548
2821
  function gcCommand(program) {
@@ -2558,8 +2831,9 @@ function gcCommand(program) {
2558
2831
  if (ms === null) throw new InvalidArgumentError("must be a whole number of minutes, hours, or days, e.g. --idle 2h");
2559
2832
  return ms;
2560
2833
  }).option("--json", "print the report as JSON on stdout; every other line goes to stderr").action(async (opts) => {
2561
- await runGc(opts);
2834
+ const { interrupted } = await runProcessOperation(process.cwd(), (operation) => withDrainingCancellation(operation, operation.signal, () => runGc(opts, { operation })));
2835
+ if (interrupted !== null) process.exitCode = interrupted;
2562
2836
  });
2563
2837
  }
2564
2838
  //#endregion
2565
- export { formatGcReport as a, runGc as i, gcCommand as n, readGcReport as r, collectGcReport as t };
2839
+ export { runGc as a, readGcReport as i, executeGc as n, withGcMaintenanceClaim as o, gcCommand as r, formatGcReport as s, collectGcReport as t };