stim 1.21.0 → 1.23.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-ybouwJSR.mjs} +21 -3
  3. package/dist/{act-DHAw2A2S.mjs → act-DG54PWtJ.mjs} +298 -164
  4. package/dist/{activity-BzVfHSrY.mjs → activity-B5RtpFda.mjs} +33 -12
  5. package/dist/{agent-device-usage-output-zh1vhbLp.mjs → agent-device-usage-output-UJ5gC80l.mjs} +7 -7
  6. package/dist/{android-BLgbNqox.mjs → android-BoUJKbfz.mjs} +66 -52
  7. package/dist/{android-DUYkNF4u.mjs → android-C7sVmyI9.mjs} +160 -59
  8. package/dist/{android-CV_GCELZ.mjs → android-CHH4KLOR.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-DAuZ-FNU.mjs → artifact-5RteZem7.mjs} +69 -40
  16. package/dist/{artifact-B8T3S5gi.mjs → artifact-Dwdo7k1E.mjs} +40 -35
  17. package/dist/artifact-platform-CXcmX_j3.mjs +27 -0
  18. package/dist/{browser-web-QQzVjz3O.mjs → browser-web-CeEVlGug.mjs} +187 -59
  19. package/dist/{budget-Bs6SxxzO.mjs → budget-DnPX3DXj.mjs} +830 -699
  20. package/dist/{build-CoPzQRNw.mjs → build-CqufZFqy.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-C8CIvRyi.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-RcAy92gH.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-D-J9fWGV.mjs} +20 -20
  29. package/dist/cli.mjs +1 -1
  30. package/dist/{client-UXD70ZbM.mjs → client-yyso1bwi.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-Db8BqzxH.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-BnYSDiKa.mjs} +2 -2
  39. package/dist/{device-DCxH7_By.mjs → device-BvzPUBs3.mjs} +16 -16
  40. package/dist/{device-ExJq8Ei7.mjs → device-CCkWmkAA.mjs} +71 -14
  41. package/dist/{device-capacity-CMlqEcvw.mjs → device-capacity-CjaSdrhg.mjs} +9 -10
  42. package/dist/device-host-worker.mjs +25 -32
  43. package/dist/{device-ios-DRUeKaFV.mjs → device-ios-uOiEETX2.mjs} +39 -16
  44. package/dist/{device-lease-run-C8raGFoP.mjs → device-lease-run-BgwPHc6u.mjs} +5 -5
  45. package/dist/{device-pool-DMRpVX9a.mjs → device-pool-C-ptmZTa.mjs} +3 -3
  46. package/dist/{doctor-DTvaDOda.mjs → doctor-8sr2tFOL.mjs} +24 -37
  47. package/dist/{doctor-Dhqebm0q.mjs → doctor-D2SN3Ltx.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-pxxvipYC.mjs} +1 -1
  51. package/dist/{doctor-watchman-Cv1F10Kj.mjs → doctor-watchman-sy_RNhFt.mjs} +5 -5
  52. package/dist/{eas-build-BomfRnFT.mjs → eas-build-CyjU5K7E.mjs} +4 -4
  53. package/dist/{eas-fallback-DJwZpy0c.mjs → eas-fallback-DV3Wytf1.mjs} +27 -18
  54. package/dist/{eas-session-ledger-op6A54yi.mjs → eas-session-ledger-BzzsX97d.mjs} +8 -7
  55. package/dist/{error-diagnostics-Bf0rZvG9.mjs → error-diagnostics-C6sVoeqO.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-CDoogxAI.mjs} +587 -313
  59. package/dist/gc-DLbzvTP2.mjs +3 -0
  60. package/dist/{guide-Yqwt2bd6.mjs → guide-BC37GcDG.mjs} +288 -59
  61. package/dist/{hosted-android-Bw-honsd.mjs → hosted-android-CkclQig_.mjs} +6 -6
  62. package/dist/{hosted-client-DB_gBPfY.mjs → hosted-client-ByuMMO5s.mjs} +4 -4
  63. package/dist/{hosted-ios-CUvOf4Vr.mjs → hosted-ios-DtYHeuYR.mjs} +1 -1
  64. package/dist/{hosted-logs-DL_P5AdN.mjs → hosted-logs-Br84qWy1.mjs} +5 -5
  65. package/dist/{hosted-macos-QI141yKk.mjs → hosted-macos-DdtYajZt.mjs} +21 -12
  66. package/dist/{hosted-native-Ksyjgzq-.mjs → hosted-native-UPI9zdZ4.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-DO7oIRlc.mjs} +7 -7
  69. package/dist/ios-CDvlmUs7.d.mts +24 -0
  70. package/dist/{ios-ubFsBzwq.mjs → ios-DZZbNYlY.mjs} +59 -50
  71. package/dist/{ios-device-CXGP1bsD.mjs → ios-device-BXZ0c7Kb.mjs} +1 -1
  72. package/dist/{ios-device-CwFMehUS.mjs → ios-device-JC3uWM4K.mjs} +3 -3
  73. package/dist/{ios-CpByZy12.mjs → ios-hcBpnEGk.mjs} +81 -34
  74. package/dist/{ios-project-WuwwBdvW.mjs → ios-project-CVyNwjAF.mjs} +5 -5
  75. package/dist/{ios-signing-DTTIChGs.mjs → ios-signing-BtAffm0A.mjs} +2 -2
  76. package/dist/{ios-state-DvFX-K_l.mjs → ios-state-Dtw5K2aI.mjs} +1 -1
  77. package/dist/{ios-CQ68hG9s.mjs → ios-vbUOqGXu.mjs} +8 -8
  78. package/dist/{ios-qNJt4dFm.mjs → ios-xtlRHaeB.mjs} +1 -1
  79. package/dist/{js-swap-Cc1dttps.mjs → js-swap-C3QxUFJR.mjs} +7 -29
  80. package/dist/{launch-CD1yVc7U.mjs → launch-Bz7JtaC4.mjs} +16 -16
  81. package/dist/{launch-Zq2mOSDy.mjs → launch-CHY3p-yz.mjs} +40 -39
  82. package/dist/{logs-CMm05HxN.mjs → logs-CtNeNDuq.mjs} +10 -17
  83. package/dist/{logs-BS00Anxf.mjs → logs-mxqsWpJs.mjs} +151 -24
  84. package/dist/{macos-B65--pPd.mjs → macos-inNFbBWc.mjs} +1 -1
  85. package/dist/macos-run.mjs +1 -1
  86. package/dist/{macos-C-0qJ6zq.mjs → macos-u0BoHKnO.mjs} +143 -46
  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-CdCSylTn.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-BUTu8T7A.mjs} +4 -5
  95. package/dist/{native-runtime-Dg1Q7tfF.mjs → native-runtime-CRSOZxKY.mjs} +8 -7
  96. package/dist/native-source-Y0yLhFyQ.mjs +133 -0
  97. package/dist/{native-xcode-inputs-CB2iQeWq.mjs → native-xcode-inputs-G6rkkhxu.mjs} +2 -3
  98. package/dist/{native-xcode-ios-CXX39F7o.mjs → native-xcode-ios-_os2PlCD.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-BU42oOyT.mjs} +12 -8
  106. package/dist/{ports-D-MmCTdv.mjs → ports-BSmQKzW7.mjs} +3 -3
  107. package/dist/{prebuild-uD_ipCxF.mjs → prebuild-BFm63bQ0.mjs} +63 -16
  108. package/dist/{project-BZNjmW_8.mjs → project-CTLNHCfE.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-CFgUroxW.mjs} +73 -53
  114. package/dist/{react-native-build-rhk4smwQ.mjs → react-native-build-CmJkTljQ.mjs} +8 -8
  115. package/dist/{react-native-doctor-CQpuIMso.mjs → react-native-doctor-LTF-Xt8l.mjs} +7 -7
  116. package/dist/{react-native-ios-Bhq1rJeV.mjs → react-native-ios-ChBF1MkB.mjs} +43 -29
  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-DF9hqUUB.mjs} +16 -15
  120. package/dist/{reload-DzjSl5P_.mjs → reload-RBi1nj6Z.mjs} +1 -1
  121. package/dist/{remote-ByYkXdGD.mjs → remote-BmvMCcJw.mjs} +18 -15
  122. package/dist/{result-DNiS-tTP.mjs → result-Cx5CRLvS.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-BTiGPEjS.mjs} +5 -5
  127. package/dist/server-expo-C-Buqvn_.mjs +2 -0
  128. package/dist/{server-expo-DT_F_uVI.mjs → server-expo-D9lEz4aq.mjs} +7 -5
  129. package/dist/{settings-CsJDEXgK.mjs → settings-C7O8A6ZI.mjs} +223 -243
  130. package/dist/{settings-T1gtc3bf.mjs → settings-eKF_vJJs.mjs} +1 -1
  131. package/dist/{settings-write-HyVZ8eQ8.mjs → settings-write-79mQRr9C.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-D7M2nE4b.mjs} +6 -6
  135. package/dist/{stage-BIS0ydsV.mjs → stage-DKNM4ZPd.mjs} +21 -4
  136. package/dist/{start-RChynRDp.mjs → start-CRfN1kWV.mjs} +1 -1
  137. package/dist/{start-7lpH7hmN.mjs → start-bjfiScgz.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-DioA6cBU.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-Eqx_20WC.mjs} +1 -1
  144. package/dist/{status-BL7jVyAB.mjs → status-kzbeWk4x.mjs} +43 -29
  145. package/dist/{status-watch-GDCfqpTE.mjs → status-watch-BIqqbJ6q.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-B18d8KDw.mjs} +2 -2
  149. package/dist/{stop-CPzlM1L-.mjs → stop-B2uLQfoM.mjs} +15 -16
  150. package/dist/{stop-D7h6jAXQ.mjs → stop-DzLJc7Ei.mjs} +31 -22
  151. package/dist/supervisor-run.d.mts +3 -3
  152. package/dist/supervisor-run.mjs +28 -36
  153. package/dist/{support-Dlu37g6H.mjs → support-BTpZ6RAJ.mjs} +15 -11
  154. package/dist/{support-67vraa1Q.mjs → support-DTBV56gy.mjs} +8 -8
  155. package/dist/swiftpm-artifact-inputs-DFcGsXDP.mjs +153 -0
  156. package/dist/swiftpm-macos-r_WSKlXr.mjs +532 -0
  157. package/dist/{toolchain-Hp9sCpK4.mjs → toolchain-CWeGJgCc.mjs} +9 -6
  158. package/dist/toolchain-GVgnf8d1.mjs +2 -0
  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-Cduk0NSE.mjs +2 -0
  163. package/dist/{web-CJmIj6AI.mjs → web-CwREhjD-.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-BxzYlo3P.mjs} +594 -266
  168. package/dist/{worktree-DQ6Zi2Mv.mjs → worktree-8lqn_lBp.mjs} +850 -551
  169. package/dist/worktree-BxfVxG_b.mjs +2 -0
  170. package/dist/{xcode-Zlx3qjEs.mjs → xcode-iQnROM5o.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,40 +1,38 @@
1
- import { _ as debugLog, f as ownership_claim_exports, l as isTermination, m as reportSignal, n as isTimeoutError, p as process_identity_exports, t as getExecutor, u as operationState } from "./exec-_UbtCQcG.mjs";
1
+ import { a as workspaceProcessLockError, b as debugLog, g as process_identity_exports, i as withWorkspaceProcessLock, m as operationState, n as isTimeoutError, p as isTermination, r as WorkspaceProcessCleanupError, t as getExecutor, x as ownership_claim_exports } from "./exec-DEoAhcze.mjs";
2
2
  import { d as workspaceDir, f as workspaceId$1, g as workspaceStateFile$1, i as sharedBuildCache, m as workspaceLogsDir$1 } from "./paths-DQtTgsBf.mjs";
3
- import { D as withConfigLock$1, F as isOnMountedVolume, K as projectDeviceSlots, L as measuredDirectorySize, M as directorySize, N as directorySizeAsync, O as withConfigLockAsync$1, P as formatBytes, R as measuredDirectorySizeAsync, U as deviceSlotPlatforms, _ as loadConfig$1, a as clearDevice, d as getConfigDir, o as clearSupervisor, p as getProject, z as volumeRootFor } from "./config-CYcBF478.mjs";
3
+ import { D as withConfigLock$1, F as isOnMountedVolume, G as projectDeviceSlots, H as deviceSlotPlatforms, L as measuredDirectorySizeAsync, M as directorySize, N as directorySizeAsync, O as withConfigLockAsync$1, P as formatBytes, R as volumeRootFor, _ as loadConfig$1, a as clearDevice, d as getConfigDir, o as clearSupervisor, p as getProject } from "./config-DlrsduwG.mjs";
4
4
  import { n as stripAnsi } from "./process-output-Fpl8PYi8.mjs";
5
- import { a as formatElapsed, l as phaseLine, o as formatLongDuration, s as isAppLaunchError, u as plural } from "./command-output-B1UrjZHL.mjs";
6
- import { d as writeWorkspaceState, o as updateWorkspaceState, r as readWorkspaceState$1, t as clearWorkspaceStateKey } from "./workspace-state-Cjuigeeu.mjs";
7
- import { $t as cancelledError, Gt as childEnvironment, J as resolveProjectSettingsAsync, Qt as cancellableSleep, d as androidLayoutSetting, en as throwIfCancelled, lt as planMetroReach, qt as killOnAbort } from "./settings-CsJDEXgK.mjs";
8
- import { C as resolvePackageJson, M as readMetroTunnel, N as readPidFile, P as readRemoteSession, i as findProjectRoot$1, j as clearWorkspaceSupervisor, k as clearRemoteSession, s as ownedDeviceLabel, v as detectIsExpo } from "./project-BZNjmW_8.mjs";
5
+ import { a as formatElapsed, l as phaseLine, o as formatLongDuration, s as isAppLaunchError, u as plural } from "./command-output-Db8BqzxH.mjs";
6
+ import { a as killOnAbort, d as cancelledError, f as throwIfCancelled, n as runOperationFileQuiet, r as childEnvironment, t as runOperationFile, u as cancellableSleep } from "./operation-exec-BW0em61G.mjs";
7
+ import { d as writeWorkspaceState, l as withWorkspaceStateLockAsync, o as updateWorkspaceState, r as readWorkspaceState$1, t as clearWorkspaceStateKey } from "./workspace-state-U08dEzhG.mjs";
8
+ import { J as resolveProjectSettingsAsync, d as androidLayoutSetting, lt as planMetroReach } from "./settings-C7O8A6ZI.mjs";
9
+ import { C as resolvePackageJson, M as readMetroTunnel, N as readPidFile, P as readRemoteSession, i as findProjectRoot$1, j as clearWorkspaceSupervisor, k as clearRemoteSession, s as ownedDeviceLabel, v as detectIsExpo } from "./project-CTLNHCfE.mjs";
9
10
  import { r as createNdjsonWriter, s as readNdjsonGenerations$1 } from "./ndjson-CgE_o3Dl.mjs";
10
- import { f as resolveProjectMetro, l as pidExists, r as killMetroTree } from "./metro-DEXpIhHC.mjs";
11
- import { C as environmentState, a as removeEasSessionClaim, d as parseWatchRoots, f as parseWatchmanClients, i as recordEasSessionClaim, l as parseSubscriberNames, n as easMachineStateRoot$1, o as removeEasSessionClaimAsync, p as parseWatchmanPid, r as readEasSessionLedger$1, s as compareStimVersions, t as assertEasMachineRootWritable, u as parseTriggerCount, w as formatSpace, y as capacity } from "./eas-session-ledger-op6A54yi.mjs";
12
- import { i as registeredCaches } from "./cache-manifest-DkkqguoK.mjs";
11
+ import { a as listeningPidsAsync, c as pidExists, d as resolveProjectMetro, h as isMetroRunning, r as killMetroTreeAsync, u as processCwds } from "./metro-DiXtA3Ty.mjs";
12
+ import { C as environmentState, a as removeEasSessionClaim, d as parseWatchRoots, f as parseWatchmanClients, i as recordEasSessionClaim, l as parseSubscriberNames, n as easMachineStateRoot$1, o as removeEasSessionClaimAsync, p as parseWatchmanPid, r as readEasSessionLedger$1, s as compareStimVersions, t as assertEasMachineRootWritable, u as parseTriggerCount, w as formatSpace, y as capacity } from "./eas-session-ledger-BzzsX97d.mjs";
13
+ import { i as registeredCaches } from "./cache-manifest-DSeQjt-Q.mjs";
13
14
  import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
14
- import { c as nativeRunWaitNotice, d as runCancellationSignal, m as claimForEmptying } from "./native-run-DY_RLx3C.mjs";
15
- import { A as teardownParkedIosSim, C as eraseParkedAvd, E as teardownOwnedAvd, I as listBuildSlots, M as canonical, N as measureDu, O as teardownOwnedIosSim, P as writeUsageCache, R as GC_OUTPUT, a as collectWorkspaceOutputs, f as withIdleWorkspace, j as probePublicHttp, k as teardownParkedAvd, m as resolveSupervisorTarget, r as clearWorkspaceOutputs, w as eraseParkedIosSim, z as recordGcResult } from "./workspaces-BccMFmGQ.mjs";
16
- import { N as listBuildLocks, a as createDeviceProcessTables, b as leaseIsExpired, g as workspaceActivity, i as createActivityReaderAsync, o as envDir, p as readHostProcessTableAsync, r as createActivityReader, s as idleForMs, x as listLeaseFiles } from "./activity-BzVfHSrY.mjs";
17
- import { G as parseAvdDeviceProfile, H as ownedAvdSerialResolver, K as parseAvdSystemImage, N as listAvdsAsync, P as listInstalledSystemImages, U as ownedAvdSerialResolverAsync, _ as avdStorageRoots, ct as isStimOwnedSim, d as androidToolPath, dt as dropParked, gt as readParked, o as androidHome, p as avdIniPaths, st as isStimOwnedAvd, z as ownedAvdDirectory } from "./android-DUYkNF4u.mjs";
18
- import { B as deviceHoldsBundle, D as sanitizeDeviceLabel, b as parseSimctlList, f as listAllIosSimsAsync, g as listUserApps, k as uninstallIosApp, p as listIosDeviceTypesAsync, u as iosSimulatorFailureAdvice, y as parseRuntimeVersion, z as deviceHoldsApk } from "./ios-CpByZy12.mjs";
19
- import { n as workspaceProcessLockError, t as withWorkspaceProcessLock } from "./workspace-process-lock-1_1OoVW5.mjs";
15
+ import { c as nativeRunWaitNotice, d as runCancellationSignal, m as claimForEmptying } from "./native-run-BUTu8T7A.mjs";
16
+ import { A as teardownParkedAvd, D as teardownOwnedAvd, F as writeUsageCache, L as listBuildSlots, M as probePublicHttp, N as canonical, P as measureDu, T as eraseParkedIosSim, a as collectWorkspaceOutputs, f as withIdleWorkspace, h as resolveSupervisorTarget, j as teardownParkedIosSim, k as teardownOwnedIosSim, r as clearWorkspaceOutputs, w as eraseParkedAvd, z as GC_OUTPUT } from "./workspaces-BxzYlo3P.mjs";
17
+ import { N as listBuildLocks, b as leaseIsExpired, g as workspaceActivity, i as createActivityReaderAsync, o as envDir, p as readHostProcessTableAsync, r as createActivityReader, s as idleForMs, x as listLeaseFiles } from "./activity-B5RtpFda.mjs";
18
+ import { F as listAvdsAsync, G as ownedAvdSerialResolverAsync, I as listInstalledSystemImages, J as parseAvdSystemImage, V as ownedAvdDirectory, W as ownedAvdSerialResolver, d as androidToolPath, lt as isStimOwnedAvd, m as avdIniPaths, o as androidHome, pt as dropParked, q as parseAvdDeviceProfile, ut as isStimOwnedSim, v as avdStorageRoots, vt as readParked } from "./android-C7sVmyI9.mjs";
19
+ import { B as deviceHoldsBundle, D as sanitizeDeviceLabel, b as parseSimctlList, f as listAllIosSimsAsync, g as listUserApps, k as uninstallIosApp, p as listIosDeviceTypesAsync, u as iosSimulatorFailureAdvice, y as parseRuntimeVersion, z as deviceHoldsApk } from "./ios-hcBpnEGk.mjs";
20
20
  import { n as supervisorError } from "./errors-ccaPc4ox.mjs";
21
- import { n as expoBinFromPackage, r as expoBinPath, s as findBinUpward } from "./server-expo-DT_F_uVI.mjs";
22
- import { t as DEV_MENU_LAUNCH_ARGS } from "./ios-device-CXGP1bsD.mjs";
21
+ import { n as expoBinFromPackage, r as expoBinPath, s as findBinUpward } from "./server-expo-D9lEz4aq.mjs";
22
+ import { t as DEV_MENU_LAUNCH_ARGS } from "./ios-device-BXZ0c7Kb.mjs";
23
23
  import { createRequire } from "node:module";
24
24
  import { randomUUID } from "node:crypto";
25
25
  import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, statfsSync, writeFileSync } from "node:fs";
26
- import { format } from "node:util";
27
26
  import { homedir, platform, totalmem } from "node:os";
28
27
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
29
28
  import { LOG_ROTATE_BYTES, METRO_NAMED_CACHE_LAYOUT, ambientContext, cacheDirRefusal, compiledHelper, configDir, withAmbientContext } from "@stim-cli/core";
30
29
  import { BUILD_HISTORY_KEY, BUILD_HISTORY_LIMIT, BUILD_PHASES, DEV_SERVER_STOP_REQUEST_KEY, LAST_BUILD_KEYS, MACHINE_OWNER_KINDS, STATS_VERSION, buildWorkerRoot, decodeStats, isJsonObject, loadConfig, machineCacheUsageFile, parseMissReason, readDevServerStop, readDevServerStopRequest, readJsonObject, readMachineCacheUsage, readRunEstimates, readStats as readStats$1, runId, settingDefinition, settingValueError, statsFile, statsProjectKey as statsProjectKey$1, trimCapacityRefusals, trimCapacityWaits, trimSamples, wholePhases, workspaceBuildDetailFile } from "@stim-cli/core/state";
31
30
  import { basename as basename$1, dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1, resolve as resolve$1 } from "path";
32
- import { fileURLToPath, pathToFileURL } from "node:url";
31
+ import { fileURLToPath } from "node:url";
33
32
  import chalk from "chalk";
34
33
  import { existsSync as existsSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, rmSync as rmSync$1, statSync as statSync$1 } from "fs";
35
- import { access, lstat, readFile, readdir, stat, statfs } from "node:fs/promises";
34
+ import { access, lstat, readFile, readdir, rm, stat, statfs } from "node:fs/promises";
36
35
  import { homedir as homedir$1, tmpdir as tmpdir$1 } from "os";
37
- import { AsyncLocalStorage } from "node:async_hooks";
38
36
  //#region src/cache/caches.ts
39
37
  function xcodeDerivedDataRoot(env) {
40
38
  return join$1(env.HOME || env.USERPROFILE || homedir$1(), "Library", "Developer", "Xcode", "DerivedData");
@@ -204,10 +202,15 @@ function canonicalCacheDir(dir) {
204
202
  }
205
203
  }
206
204
  const CACHE_SIZE_TIMEOUT_MS = 6e4;
207
- async function sizeCaches(caches) {
205
+ async function sizeCaches(caches, operation) {
208
206
  const sized = [];
209
207
  for (const cache of caches) {
210
- const bytes = cache.bytes ?? await measuredDirectorySizeAsync(cache.dir, { timeoutMs: CACHE_SIZE_TIMEOUT_MS });
208
+ operation?.signal.throwIfAborted();
209
+ const bytes = cache.bytes ?? await measuredDirectorySizeAsync(cache.dir, {
210
+ timeoutMs: CACHE_SIZE_TIMEOUT_MS,
211
+ operation,
212
+ signal: operation?.signal
213
+ });
211
214
  sized.push(Object.assign({}, cache, bytes === null ? { sizeError: "Size unavailable: measurement failed or exceeded 60 seconds." } : { bytes }));
212
215
  }
213
216
  return sized;
@@ -274,6 +277,69 @@ function pruneCache(cache, { olderThanDays, now = Date.now(), protect, evictByte
274
277
  protectedEntries
275
278
  };
276
279
  }
280
+ async function pruneCacheAsync(cache, { olderThanDays, now = Date.now(), signal, operation }) {
281
+ if (cache.prune === "report-only") return {
282
+ removed: 0,
283
+ bytes: 0,
284
+ skipped: "report-only shared cache; Stim never deletes it"
285
+ };
286
+ if (cache.prune === "atomic") return {
287
+ removed: 0,
288
+ bytes: 0,
289
+ skipped: "index-backed; empty it whole or not at all"
290
+ };
291
+ if (!cache.files && cacheDirRefusal(cache.dir)) return {
292
+ removed: 0,
293
+ bytes: 0,
294
+ skipped: `${cache.dir} is not a directory this cache owns`
295
+ };
296
+ const cutoff = now - olderThanDays * 24 * 60 * 60 * 1e3;
297
+ let entries = cache.files ?? [cache.dir];
298
+ for (let depth = 0; !cache.files && depth < (cache.entriesDepth ?? 1); depth++) {
299
+ const next = [];
300
+ for (const parent of entries) {
301
+ signal?.throwIfAborted();
302
+ const names = await readdir(parent).catch(() => []);
303
+ next.push(...names.map((name) => join$1(parent, name)));
304
+ }
305
+ entries = next;
306
+ }
307
+ let removed = 0;
308
+ let bytes = 0;
309
+ let failed = 0;
310
+ for (const entry of entries) {
311
+ signal?.throwIfAborted();
312
+ try {
313
+ const st = await stat(entry);
314
+ if (Math.max(st.atimeMs, st.mtimeMs) >= cutoff) continue;
315
+ const size = st.isDirectory() ? await measuredDirectorySizeAsync(entry, {
316
+ timeoutMs: CACHE_SIZE_TIMEOUT_MS,
317
+ signal,
318
+ operation
319
+ }) : st.size;
320
+ signal?.throwIfAborted();
321
+ if ((await stat(entry)).mtimeMs >= cutoff) continue;
322
+ signal?.throwIfAborted();
323
+ await rm(entry, {
324
+ recursive: true,
325
+ force: true
326
+ });
327
+ removed++;
328
+ bytes += size ?? 0;
329
+ } catch (error) {
330
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
331
+ signal?.throwIfAborted();
332
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) failed++;
333
+ }
334
+ }
335
+ signal?.throwIfAborted();
336
+ return {
337
+ removed,
338
+ bytes,
339
+ failed,
340
+ skipped: null
341
+ };
342
+ }
277
343
  function entriesAtDepth(dir, depth) {
278
344
  let level = [dir];
279
345
  for (let i = 0; i < depth; i++) {
@@ -303,7 +369,10 @@ function helperSource() {
303
369
  }
304
370
  async function buildHelper(operation) {
305
371
  const exec = getExecutor();
306
- if (!(operation ? await exec.runFileQuietAsync("xcode-select", ["-p"], { timeoutMs: READ_TIMEOUT_MS }) : exec.runFileQuiet("xcode-select", ["-p"], { timeoutMs: READ_TIMEOUT_MS }))) return null;
372
+ if (!(operation ? await runOperationFileQuiet(operation, exec, "xcode-select", ["-p"], {
373
+ timeoutMs: READ_TIMEOUT_MS,
374
+ cwd: operation.cwd
375
+ }) : exec.runFileQuiet("xcode-select", ["-p"], { timeoutMs: READ_TIMEOUT_MS }))) return null;
307
376
  const source = helperSource();
308
377
  try {
309
378
  return await compiledHelper({
@@ -313,7 +382,7 @@ async function buildHelper(operation) {
313
382
  version: "",
314
383
  prune: !operation,
315
384
  compile: async (output) => {
316
- await exec.runFileAsync("xcrun", [
385
+ await runOperationFile(operation, exec, "xcrun", [
317
386
  "swiftc",
318
387
  "-O",
319
388
  "-swift-version",
@@ -321,10 +390,15 @@ async function buildHelper(operation) {
321
390
  "-o",
322
391
  output,
323
392
  source
324
- ], { timeoutMs: COMPILE_TIMEOUT_MS });
393
+ ], {
394
+ timeoutMs: COMPILE_TIMEOUT_MS,
395
+ cwd: operation?.cwd
396
+ });
325
397
  }
326
398
  });
327
- } catch {
399
+ } catch (error) {
400
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
401
+ operation?.signal.throwIfAborted();
328
402
  return null;
329
403
  }
330
404
  }
@@ -334,6 +408,7 @@ let built = null;
334
408
  let failure = null;
335
409
  /** The `stim-footprint` helper, compiled on first use. A success is kept for the process's life; a failure retries after a doubling backoff instead of being pinned forever. */
336
410
  function footprintHelper(operation) {
411
+ if (operation) return buildHelper(operation);
337
412
  const home = configDir();
338
413
  if (built?.home === home) return built.promise;
339
414
  if (failure?.home === home && Date.now() < failure.nextRetryAt) return Promise.resolve(null);
@@ -368,7 +443,10 @@ function parseFootprints(output) {
368
443
  /** Every readable process's physical footprint in bytes, or null when the helper is unavailable or fails. */
369
444
  async function readFootprints(operation) {
370
445
  const helper = await footprintHelper(operation);
371
- const output = helper ? operation ? await getExecutor().runFileQuietAsync(helper, [], { timeoutMs: READ_TIMEOUT_MS }) : getExecutor().runFileQuiet(helper, [], { timeoutMs: READ_TIMEOUT_MS }) : null;
446
+ const output = helper ? operation ? await runOperationFileQuiet(operation, getExecutor(), helper, [], {
447
+ timeoutMs: READ_TIMEOUT_MS,
448
+ cwd: operation.cwd
449
+ }) : getExecutor().runFileQuiet(helper, [], { timeoutMs: READ_TIMEOUT_MS }) : null;
372
450
  return output === null ? null : parseFootprints(output);
373
451
  }
374
452
  //#endregion
@@ -890,16 +968,23 @@ function stimWorkspaceOf(pid, parentOf, supervisors) {
890
968
  function reclaimableBytes(report, kind) {
891
969
  return report.processes.filter((entry) => entry.reclaimable && (kind === void 0 || entry.cacheKind === kind)).reduce((sum, entry) => sum + entry.bytes, 0);
892
970
  }
893
- const boundWatchman = async (args) => {
894
- const answer = JSON.parse(await getExecutor().runFileAsync("watchman", ["--no-spawn", ...args], { timeoutMs: WATCHMAN_TIMEOUT_MS }));
971
+ const boundWatchman = async (args, operation) => {
972
+ const options = {
973
+ timeoutMs: WATCHMAN_TIMEOUT_MS,
974
+ cwd: operation?.cwd
975
+ };
976
+ const output = await runOperationFile(operation, getExecutor(), "watchman", ["--no-spawn", ...args], options);
977
+ const answer = JSON.parse(output);
895
978
  const error = answer?.error;
896
979
  if (error !== void 0) throw new Error(String(error));
897
980
  return answer;
898
981
  };
899
- async function quietly(fn) {
982
+ async function quietly(fn, operation) {
900
983
  try {
901
984
  return await fn();
902
- } catch {
985
+ } catch (error) {
986
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
987
+ operation?.signal.throwIfAborted();
903
988
  return null;
904
989
  }
905
990
  }
@@ -908,10 +993,11 @@ async function quietly(fn) {
908
993
  * known: debug-status lists the asking CLI as a client too.
909
994
  */
910
995
  async function watchmanClients(operation) {
911
- if (reportSignal(operation)) {
996
+ if (operation) {
912
997
  let self;
913
- const output = await getExecutor().runFileQuietAsync("watchman", ["--no-spawn", "debug-status"], {
998
+ const output = await runOperationFileQuiet(operation, getExecutor(), "watchman", ["--no-spawn", "debug-status"], {
914
999
  timeoutMs: WATCHMAN_TIMEOUT_MS,
1000
+ cwd: operation.cwd,
915
1001
  onSpawn: (child) => {
916
1002
  self = child.pid;
917
1003
  }
@@ -955,25 +1041,29 @@ async function watchmanClients(operation) {
955
1041
  }
956
1042
  async function collectWatchmanRoots(pid, progress, operation) {
957
1043
  const watchman = boundWatchman;
958
- const paths = parseWatchRoots(await quietly(() => watchman(["watch-list"])));
1044
+ const paths = parseWatchRoots(await quietly(() => watchman(["watch-list"], operation), operation));
959
1045
  if (!paths) return null;
960
1046
  if (progress) phase("daemons", `watchman pid ${pid}: checking ${plural(paths.length, "root")}`, operation);
961
1047
  const roots = [];
962
1048
  let next = 0;
963
1049
  const probe = async () => {
964
1050
  while (next < paths.length) {
1051
+ operation?.signal.throwIfAborted();
965
1052
  const index = next++;
966
1053
  const path = paths[index];
967
- const [subscriptions, triggers] = await Promise.all([quietly(() => watchman(["debug-get-subscriptions", path])), quietly(() => watchman(["trigger-list", path]))]);
1054
+ const subscriptions = () => quietly(() => watchman(["debug-get-subscriptions", path], operation), operation);
1055
+ const triggers = () => quietly(() => watchman(["trigger-list", path], operation), operation);
1056
+ const [subscribers, triggered] = operation ? [await subscriptions(), await triggers()] : await Promise.all([subscriptions(), triggers()]);
968
1057
  roots[index] = {
969
1058
  path,
970
1059
  stale: watchmanRootStaleness(path),
971
- subscribers: parseSubscriberNames(subscriptions),
972
- triggers: parseTriggerCount(triggers)
1060
+ subscribers: parseSubscriberNames(subscribers),
1061
+ triggers: parseTriggerCount(triggered)
973
1062
  };
974
1063
  }
975
1064
  };
976
- await Promise.all(Array.from({ length: Math.min(ROOT_PROBES_AT_ONCE, paths.length) }, probe));
1065
+ if (operation) await probe();
1066
+ else await Promise.all(Array.from({ length: Math.min(ROOT_PROBES_AT_ONCE, paths.length) }, probe));
977
1067
  return roots;
978
1068
  }
979
1069
  function measureOf(pid, rows, footprints) {
@@ -995,39 +1085,60 @@ function supervisorPids() {
995
1085
  }
996
1086
  return supervisors;
997
1087
  }
998
- async function readLsof(pids) {
1088
+ async function readLsof(pids, operation) {
999
1089
  if (pids.length === 0) return /* @__PURE__ */ new Map();
1000
1090
  try {
1001
- return parseLsof(await getExecutor().runFileAsync("lsof", [
1091
+ return parseLsof(await runOperationFile(operation, getExecutor(), "lsof", [
1002
1092
  "-n",
1003
1093
  "-P",
1004
1094
  "-F",
1005
1095
  "pnT",
1006
1096
  "-p",
1007
1097
  pids.join(",")
1008
- ], { timeoutMs: LSOF_TIMEOUT_MS }));
1098
+ ], {
1099
+ timeoutMs: LSOF_TIMEOUT_MS,
1100
+ cwd: operation?.cwd,
1101
+ signal: operation?.signal
1102
+ }));
1009
1103
  } catch (error) {
1104
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
1105
+ operation?.signal.throwIfAborted();
1010
1106
  const { status, signal: killed, stdout } = error;
1011
1107
  return status === 1 && !killed && typeof stdout === "string" && stdout ? parseLsof(stdout) : null;
1012
1108
  }
1013
1109
  }
1014
- async function gradleStatus(command, home) {
1110
+ async function gradleStatus(command, home, operation) {
1015
1111
  if (!command.distribution) return { unknown: "could not find its Gradle distribution on its classpath" };
1016
1112
  const launcher = join(command.distribution, "bin", "gradle");
1017
1113
  if (!existsSync(launcher)) return { unknown: `its distribution has no ${launcher}` };
1018
1114
  try {
1019
- return parseGradleStatus(await getExecutor().runFileAsync(launcher, [
1115
+ const environment = operation ? childEnvironment(operation.env) : {
1116
+ env: {},
1117
+ omitEnv: []
1118
+ };
1119
+ return parseGradleStatus(await runOperationFile(operation, getExecutor(), launcher, [
1020
1120
  "--status",
1021
1121
  "-q",
1022
1122
  "-g",
1023
1123
  home
1024
1124
  ], {
1125
+ ...environment,
1025
1126
  cwd: home,
1026
- env: { JAVA_HOME: command.javaHome },
1027
- omitEnv: ["GRADLE_OPTS", "JAVA_OPTS"],
1127
+ env: {
1128
+ ...environment.env,
1129
+ JAVA_HOME: command.javaHome
1130
+ },
1131
+ omitEnv: [
1132
+ ...environment.omitEnv,
1133
+ "GRADLE_OPTS",
1134
+ "JAVA_OPTS"
1135
+ ],
1136
+ signal: operation?.signal,
1028
1137
  timeoutMs: GRADLE_STATUS_TIMEOUT_MS
1029
1138
  }));
1030
1139
  } catch (error) {
1140
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
1141
+ operation?.signal.throwIfAborted();
1031
1142
  return { unknown: `gradle --status failed: ${error.message.split("\n")[0]}` };
1032
1143
  }
1033
1144
  }
@@ -1053,7 +1164,7 @@ async function collectGradleFacts(rows, byPid, footprints, stimServerRunning, pr
1053
1164
  kotlin: []
1054
1165
  };
1055
1166
  if (progress) phase("daemons", `checking ${plural(gradleRows.length, "Gradle daemon")} and ${plural(kotlinRows.length, "Kotlin daemon")}`, operation);
1056
- const lsof = await readLsof([...gradleRows.map(({ row }) => row.pid), ...kotlinRows.map((row) => row.pid)]);
1167
+ const lsof = await readLsof([...gradleRows.map(({ row }) => row.pid), ...kotlinRows.map((row) => row.pid)], operation);
1057
1168
  const workerRoot = buildWorkerRoot();
1058
1169
  const homes = gradleRows.map(({ row, command }) => {
1059
1170
  const files = lsof?.get(row.pid)?.files;
@@ -1061,14 +1172,16 @@ async function collectGradleFacts(rows, byPid, footprints, stimServerRunning, pr
1061
1172
  });
1062
1173
  const offload = gradleRows.map(({ row }, index) => offloadClientOf(row.command, workerRoot, homes[index] ?? null));
1063
1174
  const statuses = /* @__PURE__ */ new Map();
1064
- gradleRows.forEach(({ command }, index) => {
1175
+ for (const [index, { command }] of gradleRows.entries()) {
1065
1176
  const home = homes[index];
1066
- if (offload[index] !== null && stimServerRunning) return;
1177
+ if (offload[index] !== null && stimServerRunning) continue;
1067
1178
  if (home && !statuses.has(groupKey(command, home))) {
1068
1179
  if (progress) phase("daemons", `gradle --status for Gradle ${command.version} in ${home}`, operation);
1069
- statuses.set(groupKey(command, home), gradleStatus(command, home));
1180
+ const status = gradleStatus(command, home, operation);
1181
+ statuses.set(groupKey(command, home), status);
1182
+ if (operation) await status;
1070
1183
  }
1071
- });
1184
+ }
1072
1185
  const gradle = [];
1073
1186
  for (const [index, { row, command }] of gradleRows.entries()) {
1074
1187
  const home = homes[index] ?? null;
@@ -1102,7 +1215,7 @@ async function collectGradleFacts(rows, byPid, footprints, stimServerRunning, pr
1102
1215
  };
1103
1216
  }
1104
1217
  async function collectWatchmanFacts(byPid, footprints, progress, operation) {
1105
- const pid = parseWatchmanPid(await quietly(() => boundWatchman(["get-pid"])));
1218
+ const pid = parseWatchmanPid(await quietly(() => boundWatchman(["get-pid"], operation), operation));
1106
1219
  if (pid === null) return null;
1107
1220
  const roots = await collectWatchmanRoots(pid, progress, operation);
1108
1221
  const clients = (await watchmanClients(operation))?.filter((client) => client.pid !== pid) ?? null;
@@ -1140,7 +1253,8 @@ async function collectMemoryReport(scope, progress = false, operation) {
1140
1253
  notices: []
1141
1254
  };
1142
1255
  if (process.platform === "win32") return report;
1143
- const rows = await readHostProcessTableAsync();
1256
+ const rows = await readHostProcessTableAsync(operation?.signal, operation);
1257
+ operation?.signal.throwIfAborted();
1144
1258
  if (rows === null) {
1145
1259
  report.notices.push("memory sweep skipped: ps did not answer");
1146
1260
  return report;
@@ -1206,7 +1320,8 @@ function sameProcess(entry, rows) {
1206
1320
  const row = rows?.get(entry.pid);
1207
1321
  return Boolean(row && entry.startedAt !== null && row.startedAt === entry.startedAt);
1208
1322
  }
1209
- function signal(pid) {
1323
+ function signal(pid, abort) {
1324
+ abort?.throwIfAborted();
1210
1325
  try {
1211
1326
  process.kill(pid, "SIGTERM");
1212
1327
  return null;
@@ -1214,64 +1329,69 @@ function signal(pid) {
1214
1329
  return error.code === "ESRCH" ? null : error.message;
1215
1330
  }
1216
1331
  }
1217
- function keep(entry, kind, detail) {
1218
- console.log(chalk.yellow(`Kept ${processLabel(entry)}: ${detail}`));
1219
- recordGcResult(kind, "kept", processLabel(entry), {
1332
+ function keep(output, entry, kind, detail) {
1333
+ output.log(chalk.yellow(`Kept ${processLabel(entry)}: ${detail}`));
1334
+ output.record(kind, "kept", processLabel(entry), {
1220
1335
  id: String(entry.pid),
1221
1336
  bytes: entry.bytes,
1222
1337
  detail
1223
1338
  });
1224
1339
  }
1225
- async function reclaimWatchman(before) {
1340
+ async function reclaimWatchman(before, output, operation) {
1226
1341
  let failures = 0;
1227
1342
  const fresh = await collectMemoryReport({
1228
1343
  watchman: true,
1229
1344
  gradle: false
1230
- }, true);
1345
+ }, true, operation);
1231
1346
  const daemon = fresh.processes.find((entry) => entry.kind === "watchman");
1232
1347
  const planned = before.processes.find((entry) => entry.kind === "watchman");
1233
1348
  if (!daemon || !planned || daemon.pid !== planned.pid) {
1234
- if (planned) keep(planned, "watchman", "the daemon changed or stopped since the report");
1349
+ if (planned) keep(output, planned, "watchman", "the daemon changed or stopped since the report");
1235
1350
  return 0;
1236
1351
  }
1237
1352
  for (const root of fresh.watchmanRoots.filter((r) => r.stale !== null)) {
1353
+ operation?.signal.throwIfAborted();
1238
1354
  if (!root.removable) {
1239
- console.log(chalk.yellow(`Kept the watchman root ${root.path}: ${root.detail}`));
1240
- recordGcResult("watchmanRoot", "kept", root.path, { detail: root.detail });
1355
+ output.log(chalk.yellow(`Kept the watchman root ${root.path}: ${root.detail}`));
1356
+ output.record("watchmanRoot", "kept", root.path, { detail: root.detail });
1241
1357
  continue;
1242
1358
  }
1243
1359
  try {
1244
- phase("daemons", `removing stale watchman root ${root.path}`);
1245
- await boundWatchman(["watch-del", root.path]);
1246
- console.log(chalk.green(`Removed the stale watchman root ${root.path} (${root.detail})`));
1247
- recordGcResult("watchmanRoot", "done", root.path, { detail: root.detail });
1360
+ phase("daemons", `removing stale watchman root ${root.path}`, operation);
1361
+ await boundWatchman(["watch-del", root.path], operation);
1362
+ output.log(chalk.green(`Removed the stale watchman root ${root.path} (${root.detail})`));
1363
+ output.record("watchmanRoot", "done", root.path, { detail: root.detail });
1248
1364
  } catch (error) {
1365
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
1366
+ operation?.signal.throwIfAborted();
1249
1367
  failures++;
1250
- console.log(chalk.red(`Could not remove the watchman root ${root.path}: ${error.message}`));
1251
- recordGcResult("watchmanRoot", "failed", root.path, { detail: error.message });
1368
+ output.log(chalk.red(`Could not remove the watchman root ${root.path}: ${error.message}`));
1369
+ output.record("watchmanRoot", "failed", root.path, { detail: error.message });
1252
1370
  }
1253
1371
  }
1254
1372
  const current = (await collectMemoryReport({
1255
1373
  watchman: true,
1256
1374
  gradle: false
1257
- }, true)).processes.find((entry) => entry.kind === "watchman");
1375
+ }, true, operation)).processes.find((entry) => entry.kind === "watchman");
1258
1376
  if (!current || current.pid !== daemon.pid) return failures;
1259
1377
  if (!current.reclaimable) {
1260
- keep(current, "watchman", current.detail ?? "it could not be proven unused");
1378
+ keep(output, current, "watchman", current.detail ?? "it could not be proven unused");
1261
1379
  return failures;
1262
1380
  }
1263
1381
  try {
1264
- phase("daemons", `shutting down watchman pid ${current.pid}: no client uses it`);
1265
- await boundWatchman(["shutdown-server"]);
1266
- console.log(chalk.green(`Shut down watchman pid ${current.pid}, freeing ${formatBytes(current.bytes)}; the next client that needs it starts it again`));
1267
- recordGcResult("watchman", "done", processLabel(current), {
1382
+ phase("daemons", `shutting down watchman pid ${current.pid}: no client uses it`, operation);
1383
+ await boundWatchman(["shutdown-server"], operation);
1384
+ output.log(chalk.green(`Shut down watchman pid ${current.pid}, freeing ${formatBytes(current.bytes)}; the next client that needs it starts it again`));
1385
+ output.record("watchman", "done", processLabel(current), {
1268
1386
  id: String(current.pid),
1269
1387
  bytes: current.bytes
1270
1388
  });
1271
1389
  } catch (error) {
1390
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
1391
+ operation?.signal.throwIfAborted();
1272
1392
  failures++;
1273
- console.log(chalk.red(`Could not shut down watchman pid ${current.pid}: ${error.message}`));
1274
- recordGcResult("watchman", "failed", processLabel(current), {
1393
+ output.log(chalk.red(`Could not shut down watchman pid ${current.pid}: ${error.message}`));
1394
+ output.record("watchman", "failed", processLabel(current), {
1275
1395
  id: String(current.pid),
1276
1396
  bytes: current.bytes,
1277
1397
  detail: error.message
@@ -1279,23 +1399,23 @@ async function reclaimWatchman(before) {
1279
1399
  }
1280
1400
  return failures;
1281
1401
  }
1282
- function stopKotlin(entry, rows) {
1402
+ function stopKotlin(entry, rows, output, operation) {
1283
1403
  if (!sameProcess(entry, rows)) {
1284
- keep(entry, "kotlinDaemon", "the process changed or exited since the check");
1404
+ keep(output, entry, "kotlinDaemon", "the process changed or exited since the check");
1285
1405
  return 0;
1286
1406
  }
1287
- const error = signal(entry.pid);
1407
+ const error = signal(entry.pid, operation?.signal);
1288
1408
  if (error) {
1289
- console.log(chalk.red(`Could not stop ${processLabel(entry)}: ${error}`));
1290
- recordGcResult("kotlinDaemon", "failed", processLabel(entry), {
1409
+ output.log(chalk.red(`Could not stop ${processLabel(entry)}: ${error}`));
1410
+ output.record("kotlinDaemon", "failed", processLabel(entry), {
1291
1411
  id: String(entry.pid),
1292
1412
  bytes: entry.bytes,
1293
1413
  detail: error
1294
1414
  });
1295
1415
  return 1;
1296
1416
  }
1297
- console.log(chalk.green(`Stopped ${processLabel(entry)}, freeing ${formatBytes(entry.bytes)}`));
1298
- recordGcResult("kotlinDaemon", "done", processLabel(entry), {
1417
+ output.log(chalk.green(`Stopped ${processLabel(entry)}, freeing ${formatBytes(entry.bytes)}`));
1418
+ output.record("kotlinDaemon", "done", processLabel(entry), {
1299
1419
  id: String(entry.pid),
1300
1420
  bytes: entry.bytes
1301
1421
  });
@@ -1306,33 +1426,34 @@ function stopKotlin(entry, rows) {
1306
1426
  * version is one of them, the daemon's own `gradle --stop` runs for that home; otherwise each idle one gets SIGTERM,
1307
1427
  * because `--stop` also stops the busy daemons of that home and version.
1308
1428
  */
1309
- async function stopGradle(idle, rows) {
1429
+ async function stopGradle(idle, rows, output, operation) {
1310
1430
  let failures = 0;
1311
1431
  const groups = /* @__PURE__ */ new Map();
1312
1432
  for (const entry of idle) {
1313
1433
  const command = parseGradleDaemonCommand(rows?.get(entry.pid)?.command ?? "");
1314
1434
  if (!command || !entry.gradleHome || !sameProcess(entry, rows)) {
1315
- keep(entry, "gradleDaemon", "the process changed or exited since the check");
1435
+ keep(output, entry, "gradleDaemon", "the process changed or exited since the check");
1316
1436
  continue;
1317
1437
  }
1318
1438
  const key = groupKey(command, entry.gradleHome);
1319
1439
  groups.set(key, [...groups.get(key) ?? [], entry]);
1320
1440
  }
1321
1441
  for (const [key, entries] of groups) {
1442
+ operation?.signal.throwIfAborted();
1322
1443
  const [home, , distribution, javaHome] = JSON.parse(key);
1323
1444
  const command = parseGradleDaemonCommand(rows.get(entries[0].pid).command);
1324
1445
  const building = runningBuild(listBuildLocks(), listBuildSlots());
1325
1446
  if (building) {
1326
- for (const entry of entries) keep(entry, "gradleDaemon", building);
1447
+ for (const entry of entries) keep(output, entry, "gradleDaemon", building);
1327
1448
  continue;
1328
1449
  }
1329
- phase("daemons", `gradle --status for Gradle ${command.version} in ${home}`);
1330
- let states = await gradleStatus(command, home);
1331
- let current = hostRows();
1450
+ phase("daemons", `gradle --status for Gradle ${command.version} in ${home}`, operation);
1451
+ let states = await gradleStatus(command, home, operation);
1452
+ let current = await hostRows(operation);
1332
1453
  const listed = states instanceof Map ? [...states].filter(([, state]) => state !== "stopped") : [];
1333
1454
  if (distribution !== null && listed.length > 0 && listed.every(([pid, state]) => state === "idle" && entries.some((entry) => entry.pid === pid))) try {
1334
- phase("daemons", `gradle --stop for Gradle ${command.version} in ${home}`);
1335
- await getExecutor().runFileAsync(join(distribution, "bin", "gradle"), [
1455
+ phase("daemons", `gradle --stop for Gradle ${command.version} in ${home}`, operation);
1456
+ await runOperationFile(operation, getExecutor(), join(distribution, "bin", "gradle"), [
1336
1457
  "--stop",
1337
1458
  "-q",
1338
1459
  "-g",
@@ -1344,41 +1465,43 @@ async function stopGradle(idle, rows) {
1344
1465
  timeoutMs: GRADLE_STOP_TIMEOUT_MS
1345
1466
  });
1346
1467
  for (const entry of entries) {
1347
- console.log(chalk.green(`Stopped ${processLabel(entry)} with gradle --stop, freeing ${formatBytes(entry.bytes)}`));
1348
- recordGcResult("gradleDaemon", "done", processLabel(entry), {
1468
+ output.log(chalk.green(`Stopped ${processLabel(entry)} with gradle --stop, freeing ${formatBytes(entry.bytes)}`));
1469
+ output.record("gradleDaemon", "done", processLabel(entry), {
1349
1470
  id: String(entry.pid),
1350
1471
  bytes: entry.bytes
1351
1472
  });
1352
1473
  }
1353
1474
  continue;
1354
1475
  } catch (error) {
1355
- console.log(chalk.dim(`gradle --stop failed for ${home}; signalling each idle daemon: ${error.message.split("\n")[0]}`));
1356
- states = await gradleStatus(command, home);
1357
- current = hostRows();
1476
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
1477
+ operation?.signal.throwIfAborted();
1478
+ output.log(chalk.dim(`gradle --stop failed for ${home}; signalling each idle daemon: ${error.message.split("\n")[0]}`));
1479
+ states = await gradleStatus(command, home, operation);
1480
+ current = await hostRows(operation);
1358
1481
  }
1359
1482
  for (const entry of entries) {
1360
1483
  const state = states instanceof Map ? states.get(entry.pid) : void 0;
1361
1484
  if (!sameProcess(entry, current)) {
1362
- keep(entry, "gradleDaemon", "the process changed or exited since the check");
1485
+ keep(output, entry, "gradleDaemon", "the process changed or exited since the check");
1363
1486
  continue;
1364
1487
  }
1365
1488
  if (state !== "idle") {
1366
- keep(entry, "gradleDaemon", state ? `gradle --status now reports it ${state}` : "gradle --status no longer lists it idle");
1489
+ keep(output, entry, "gradleDaemon", state ? `gradle --status now reports it ${state}` : "gradle --status no longer lists it idle");
1367
1490
  continue;
1368
1491
  }
1369
- const error = signal(entry.pid);
1492
+ const error = signal(entry.pid, operation?.signal);
1370
1493
  if (error) {
1371
1494
  failures++;
1372
- console.log(chalk.red(`Could not stop ${processLabel(entry)}: ${error}`));
1373
- recordGcResult("gradleDaemon", "failed", processLabel(entry), {
1495
+ output.log(chalk.red(`Could not stop ${processLabel(entry)}: ${error}`));
1496
+ output.record("gradleDaemon", "failed", processLabel(entry), {
1374
1497
  id: String(entry.pid),
1375
1498
  bytes: entry.bytes,
1376
1499
  detail: error
1377
1500
  });
1378
1501
  continue;
1379
1502
  }
1380
- console.log(chalk.green(`Stopped ${processLabel(entry)}, freeing ${formatBytes(entry.bytes)}`));
1381
- recordGcResult("gradleDaemon", "done", processLabel(entry), {
1503
+ output.log(chalk.green(`Stopped ${processLabel(entry)}, freeing ${formatBytes(entry.bytes)}`));
1504
+ output.record("gradleDaemon", "done", processLabel(entry), {
1382
1505
  id: String(entry.pid),
1383
1506
  bytes: entry.bytes
1384
1507
  });
@@ -1390,8 +1513,9 @@ function stillPlanned(fresh, before, kind) {
1390
1513
  const planned = new Map(before.processes.filter((entry) => entry.kind === kind).map((entry) => [entry.pid, entry]));
1391
1514
  return fresh.processes.filter((entry) => entry.kind === kind && planned.get(entry.pid)?.startedAt === entry.startedAt);
1392
1515
  }
1393
- function hostRows() {
1394
- const rows = createDeviceProcessTables().host();
1516
+ async function hostRows(operation) {
1517
+ const rows = await readHostProcessTableAsync(operation?.signal, operation);
1518
+ operation?.signal.throwIfAborted();
1395
1519
  return rows ? new Map(rows.map((row) => [row.pid, row])) : null;
1396
1520
  }
1397
1521
  const EXIT_WAIT_MS = 1e4;
@@ -1403,37 +1527,40 @@ function alive(pid) {
1403
1527
  return error.code !== "ESRCH";
1404
1528
  }
1405
1529
  }
1406
- async function waitForExit(pids) {
1530
+ async function waitForExit(pids, operation) {
1407
1531
  const running = pids.filter(alive);
1408
1532
  if (running.length === 0) return;
1409
- phase("daemons", `waiting up to ${EXIT_WAIT_MS / 1e3}s for ${plural(running.length, "Gradle daemon")} to exit: pid ${running.join(", ")}`);
1533
+ phase("daemons", `waiting up to ${EXIT_WAIT_MS / 1e3}s for ${plural(running.length, "Gradle daemon")} to exit: pid ${running.join(", ")}`, operation);
1410
1534
  const deadline = Date.now() + EXIT_WAIT_MS;
1411
- while (running.some(alive) && Date.now() < deadline) await new Promise((done) => setTimeout(done, 200));
1535
+ while (running.some(alive) && Date.now() < deadline) await cancellableSleep(200, {
1536
+ signal: operation?.signal,
1537
+ what: "GC daemon shutdown"
1538
+ });
1412
1539
  }
1413
1540
  /** Stops the Gradle daemons first, then re-checks the Kotlin daemons, whose clients those Gradle daemons were. */
1414
- async function reclaimDaemons(before) {
1541
+ async function reclaimDaemons(before, output, operation) {
1415
1542
  const scope = {
1416
1543
  watchman: false,
1417
1544
  gradle: true
1418
1545
  };
1419
- const gradle = stillPlanned(await collectMemoryReport(scope, true), before, "gradleDaemon");
1420
- for (const entry of gradle.filter((e) => !e.reclaimable)) keep(entry, "gradleDaemon", entry.detail ?? "it could not be proven idle");
1546
+ const gradle = stillPlanned(await collectMemoryReport(scope, true, operation), before, "gradleDaemon");
1547
+ for (const entry of gradle.filter((e) => !e.reclaimable)) keep(output, entry, "gradleDaemon", entry.detail ?? "it could not be proven idle");
1421
1548
  const stopping = gradle.filter((entry) => entry.reclaimable);
1422
- let failures = await stopGradle(stopping, hostRows());
1549
+ let failures = await stopGradle(stopping, await hostRows(operation), output, operation);
1423
1550
  if (!before.processes.some((entry) => entry.kind === "kotlinDaemon")) return failures;
1424
- await waitForExit(stopping.map((entry) => entry.pid));
1425
- const kotlin = stillPlanned(await collectMemoryReport(scope, true), before, "kotlinDaemon");
1551
+ await waitForExit(stopping.map((entry) => entry.pid), operation);
1552
+ const kotlin = stillPlanned(await collectMemoryReport(scope, true, operation), before, "kotlinDaemon");
1426
1553
  for (const entry of before.processes.filter((e) => e.kind === "kotlinDaemon" && e.reclaimable)) {
1427
1554
  if (kotlin.some((current) => current.pid === entry.pid) || alive(entry.pid)) continue;
1428
- console.log(chalk.green(`${processLabel(entry)} exited with its Gradle daemon, freeing ${formatBytes(entry.bytes)}`));
1429
- recordGcResult("kotlinDaemon", "done", processLabel(entry), {
1555
+ output.log(chalk.green(`${processLabel(entry)} exited with its Gradle daemon, freeing ${formatBytes(entry.bytes)}`));
1556
+ output.record("kotlinDaemon", "done", processLabel(entry), {
1430
1557
  id: String(entry.pid),
1431
1558
  bytes: entry.bytes
1432
1559
  });
1433
1560
  }
1434
- const rows = hostRows();
1435
- for (const entry of kotlin) if (entry.reclaimable) failures += stopKotlin(entry, rows);
1436
- else keep(entry, "kotlinDaemon", entry.detail ?? "it could not be proven idle");
1561
+ const rows = await hostRows(operation);
1562
+ for (const entry of kotlin) if (entry.reclaimable) failures += stopKotlin(entry, rows, output, operation);
1563
+ else keep(output, entry, "kotlinDaemon", entry.detail ?? "it could not be proven idle");
1437
1564
  return failures;
1438
1565
  }
1439
1566
  /**
@@ -1442,7 +1569,7 @@ async function reclaimDaemons(before) {
1442
1569
  * Gradle daemon is in `pids` too, since one left running may still be its client. Nothing stops while STIM_HOME scopes
1443
1570
  * the config.
1444
1571
  */
1445
- async function stopIdleGradleDaemons(pids) {
1572
+ async function stopIdleGradleDaemons(pids, output = GC_OUTPUT) {
1446
1573
  if (memorySweepIsScoped()) return 0;
1447
1574
  const report = await collectMemoryReport({
1448
1575
  watchman: false,
@@ -1454,7 +1581,7 @@ async function stopIdleGradleDaemons(pids) {
1454
1581
  return processes.length ? reclaimDaemons({
1455
1582
  ...report,
1456
1583
  processes
1457
- }) : 0;
1584
+ }, output) : 0;
1458
1585
  }
1459
1586
  /** Whether `gc --delete --cache <cache>` would act on a memory kind: stop a process or remove a watchman root. */
1460
1587
  function memoryWorkPending(cache, report) {
@@ -1462,10 +1589,11 @@ function memoryWorkPending(cache, report) {
1462
1589
  return reclaimableBytes(report) > 0 || report.watchmanRoots.some((root) => root.removable);
1463
1590
  }
1464
1591
  /** Acts on the one memory kind `--cache` names: never on an unscoped run or `--cache all`. */
1465
- async function reclaimMemory(cache, report) {
1592
+ async function reclaimMemory(cache, report, output = GC_OUTPUT, operation) {
1593
+ operation?.signal.throwIfAborted();
1466
1594
  const kind = memoryCacheKind(cache);
1467
1595
  if (!kind || !report || report.processes.length === 0) return 0;
1468
- return kind === "watchman" ? reclaimWatchman(report) : reclaimDaemons(report);
1596
+ return kind === "watchman" ? reclaimWatchman(report, output, operation) : reclaimDaemons(report, output, operation);
1469
1597
  }
1470
1598
  /** A doctor note when watchman's footprint is over 2 GiB: only a restart shrinks it, and gc says when one is safe. */
1471
1599
  function watchmanFinding(report) {
@@ -2024,7 +2152,6 @@ const DYNAMIC_CONFIG_FILES = [
2024
2152
  "app.config.mjs",
2025
2153
  "app.config.cjs"
2026
2154
  ];
2027
- const TIMED_OUT = Symbol("timed-out");
2028
2155
  function dynamicConfigFile(root) {
2029
2156
  for (const name of DYNAMIC_CONFIG_FILES) if (existsSync(join(root, name))) return name;
2030
2157
  return null;
@@ -2045,8 +2172,7 @@ function normalizeProvider(raw) {
2045
2172
  if (raw === null || raw === void 0 || raw === "") return null;
2046
2173
  if (raw === "eas") return {
2047
2174
  name: "eas",
2048
- reference: EAS_PROVIDER_PACKAGE,
2049
- options: {}
2175
+ reference: EAS_PROVIDER_PACKAGE
2050
2176
  };
2051
2177
  if (typeof raw === "object") {
2052
2178
  const obj = raw;
@@ -2054,8 +2180,7 @@ function normalizeProvider(raw) {
2054
2180
  const reference = obj.plugin.trim();
2055
2181
  return {
2056
2182
  name: reference,
2057
- reference,
2058
- options: obj.options && typeof obj.options === "object" ? obj.options : {}
2183
+ reference
2059
2184
  };
2060
2185
  }
2061
2186
  }
@@ -2064,13 +2189,6 @@ function normalizeProvider(raw) {
2064
2189
  function runOptionsFor(platform) {
2065
2190
  return platform === "android" ? { variant: "debug" } : { configuration: "Debug" };
2066
2191
  }
2067
- function isProviderPlugin(plugin) {
2068
- if (!plugin || typeof plugin !== "object") return false;
2069
- const p = plugin;
2070
- const resolves = typeof p.resolveBuildCache === "function" || typeof p.resolveRemoteBuildCache === "function";
2071
- const uploads = typeof p.uploadBuildCache === "function" || typeof p.uploadRemoteBuildCache === "function";
2072
- return resolves && uploads;
2073
- }
2074
2192
  function cacheLevel(value) {
2075
2193
  return value === "local" || value === "remote" ? value : false;
2076
2194
  }
@@ -2108,6 +2226,8 @@ async function readProjectConfig(root, { context, run = null, timeoutMs = CONFIG
2108
2226
  root
2109
2227
  ], { timeoutMs });
2110
2228
  } catch (err) {
2229
+ if (err instanceof Error && "code" in err && err.code === "STIM_CLAIM_UNAVAILABLE") throw err;
2230
+ context?.signal.throwIfAborted();
2111
2231
  return { unavailable: `\`expo config --json\` failed: ${firstLine(err)}` };
2112
2232
  }
2113
2233
  try {
@@ -2119,20 +2239,6 @@ async function readProjectConfig(root, { context, run = null, timeoutMs = CONFIG
2119
2239
  return { unavailable: `\`expo config --json\` did not print JSON: ${firstLine(err)}` };
2120
2240
  }
2121
2241
  }
2122
- async function loadPlugin(projectRoot, reference, { requireFrom = null } = {}) {
2123
- const localRequire = requireFrom ? requireFrom(projectRoot) : createRequire(join(projectRoot, "package.json"));
2124
- const file = localRequire.resolve(reference);
2125
- let mod;
2126
- try {
2127
- mod = localRequire(file);
2128
- } catch (err) {
2129
- if (err?.code !== "ERR_REQUIRE_ESM") throw err;
2130
- mod = await import(pathToFileURL(file).href);
2131
- }
2132
- const plugin = mod?.default ?? mod;
2133
- if (!isProviderPlugin(plugin)) throw new Error(`"${reference}" does not export resolveBuildCache and uploadBuildCache functions`);
2134
- return plugin;
2135
- }
2136
2242
  async function loadProjectProvider(projectRoot, { context, isExpo = true, run = null, requireFrom = null, timeoutMs = CONFIG_TIMEOUT_MS } = {}) {
2137
2243
  if (!isExpo) return { none: true };
2138
2244
  const read = await readProjectConfig(projectRoot, {
@@ -2145,41 +2251,33 @@ async function loadProjectProvider(projectRoot, { context, isExpo = true, run =
2145
2251
  if (!normalized) return { none: true };
2146
2252
  if (normalized.invalid) return { unavailable: `buildCacheProvider is ${normalized.invalid}, which is not "eas" or { plugin: <module> }` };
2147
2253
  if (normalized.reference === "@stim-cli/expo-build-cache") return { none: true };
2148
- let plugin;
2254
+ if (normalized.reference !== "eas-build-cache-provider") return { unavailable: "Custom Expo buildCacheProvider plugins are not supported. Use EAS or a Stim cache provider." };
2149
2255
  try {
2150
- if (normalized.reference === "eas-build-cache-provider") {
2151
- (requireFrom ? requireFrom(projectRoot) : createRequire(join(projectRoot, "package.json"))).resolve(EAS_PROVIDER_PACKAGE);
2152
- const bin = resolveEasCliBin(projectRoot);
2153
- if (!bin) return { unavailable: "the EAS build cache needs an installed eas-cli executable" };
2154
- const entry = expoBinFromPackage(resolvePackageJson(projectRoot, "eas-cli"), "eas") ?? expoBinFromPackage(resolvePackageJson(dirname(realpathSync(bin.file)), "eas-cli"), "eas");
2155
- if (entry && !/\.(cmd|bat)$/i.test(entry)) return {
2156
- provider: {
2157
- easCli: entry,
2158
- nodeEntry: true
2159
- },
2160
- name: normalized.name,
2161
- owner: ownerFromConfig(read.config)
2162
- };
2163
- if (process.platform === "win32") return { unavailable: "the EAS build cache needs an installed eas-cli package to cancel its Node process" };
2164
- return {
2165
- provider: { easCli: bin.file },
2166
- name: normalized.name,
2167
- owner: ownerFromConfig(read.config)
2168
- };
2169
- }
2170
- plugin = await loadPlugin(projectRoot, normalized.reference, { requireFrom });
2256
+ (requireFrom ? requireFrom(projectRoot) : createRequire(join(projectRoot, "package.json"))).resolve(EAS_PROVIDER_PACKAGE);
2257
+ const bin = resolveEasCliBin(projectRoot);
2258
+ if (!bin) return { unavailable: "the EAS build cache needs an installed eas-cli executable" };
2259
+ const entry = expoBinFromPackage(resolvePackageJson(projectRoot, "eas-cli"), "eas") ?? expoBinFromPackage(resolvePackageJson(dirname(realpathSync(bin.file)), "eas-cli"), "eas");
2260
+ if (entry && !/\.(cmd|bat)$/i.test(entry)) return {
2261
+ provider: {
2262
+ easCli: entry,
2263
+ nodeEntry: true
2264
+ },
2265
+ name: normalized.name,
2266
+ owner: ownerFromConfig(read.config)
2267
+ };
2268
+ if (process.platform === "win32") return { unavailable: "the EAS build cache needs an installed eas-cli package to cancel its Node process" };
2269
+ return {
2270
+ provider: { easCli: bin.file },
2271
+ name: normalized.name,
2272
+ owner: ownerFromConfig(read.config)
2273
+ };
2171
2274
  } catch (err) {
2275
+ if (err instanceof WorkspaceProcessCleanupError || err instanceof Error && "reportCleanup" in err) throw err;
2276
+ context?.signal.throwIfAborted();
2277
+ operationState(context).runSignal?.throwIfAborted();
2172
2278
  const missing = err?.code === "MODULE_NOT_FOUND";
2173
2279
  return { unavailable: normalized.name === "eas" && missing ? `the EAS build cache needs the \`${EAS_PROVIDER_PACKAGE}\` package, which is not installed in this project` : `${normalized.name} could not be loaded: ${firstLine(err)}` };
2174
2280
  }
2175
- return {
2176
- provider: {
2177
- plugin,
2178
- options: normalized.options ?? {}
2179
- },
2180
- name: normalized.name,
2181
- owner: ownerFromConfig(read.config)
2182
- };
2183
2281
  }
2184
2282
  const WHOAMI_TIMEOUT_MS = 15e3;
2185
2283
  const EAS_CLI_PACKAGE = "eas-cli";
@@ -2318,275 +2416,39 @@ async function probeEasAuth({ context, projectRoot, owner, run, resolveBin, time
2318
2416
  source: bin.source
2319
2417
  };
2320
2418
  }
2321
- const capturing = [];
2322
- const abandoned = /* @__PURE__ */ new Set();
2323
- let patched = null;
2324
- const PROVIDER_CONTEXT = new AsyncLocalStorage();
2325
- function uploadDestination(lines) {
2326
- const text = (lines || []).map(stripAnsi);
2327
- for (const line of text) {
2328
- const url = /(https?:\/\/[^\s'"]+)/.exec(line)?.[1];
2329
- if (url !== void 0) return url.replace(/[.,)\]]+$/, "");
2330
- }
2331
- for (const line of text) {
2332
- const slug = /\bto\s+(@?[\w.-]+\/[\w.-]+)/i.exec(line)?.[1];
2333
- if (slug !== void 0) return slug.replace(/[.,)\]]+$/, "");
2334
- }
2335
- return null;
2336
- }
2337
- function providerNote(text) {
2338
- return `${"cache".padEnd(11)} ${text}`;
2339
- }
2340
- function currentFrame() {
2341
- const store = PROVIDER_CONTEXT.getStore();
2342
- return store && (capturing.includes(store) || abandoned.has(store)) ? store : null;
2343
- }
2344
- function install() {
2345
- if (patched && process.stdout.write === patched.patchedWrite) return;
2346
- const stdout = process.stdout;
2347
- const write = stdout.write.bind(stdout);
2348
- const log = console.log;
2349
- const passthrough = write;
2350
- const patchedWrite = ((chunk, encoding, callback) => {
2351
- const frame = currentFrame();
2352
- if (!frame) return passthrough(chunk, encoding, callback);
2353
- const cb = typeof encoding === "function" ? encoding : callback;
2354
- absorb(frame, typeof chunk === "string" ? chunk : String(chunk));
2355
- if (typeof cb === "function") cb();
2356
- return true;
2357
- });
2358
- const patchedLog = (...args) => {
2359
- const frame = currentFrame();
2360
- if (!frame) return log(...args);
2361
- absorb(frame, `${format(...args)}\n`);
2362
- };
2363
- stdout.write = patchedWrite;
2364
- console.log = patchedLog;
2365
- patched = {
2366
- stdout,
2367
- write,
2368
- log,
2369
- patchedWrite,
2370
- patchedLog
2371
- };
2372
- }
2373
- function uninstall() {
2374
- if (!patched || capturing.length || abandoned.size) return;
2375
- if (patched.stdout.write === patched.patchedWrite) patched.stdout.write = patched.write;
2376
- if (console.log === patched.patchedLog) console.log = patched.log;
2377
- patched = null;
2378
- }
2379
- function absorb(frame, chunk) {
2380
- try {
2381
- process.stderr.write(chunk);
2382
- } catch {}
2383
- frame.pending += chunk;
2384
- let index = frame.pending.indexOf("\n");
2385
- while (index !== -1) {
2386
- emitLine(frame, frame.pending.slice(0, index));
2387
- frame.pending = frame.pending.slice(index + 1);
2388
- index = frame.pending.indexOf("\n");
2389
- }
2390
- }
2391
- function emitLine(frame, raw) {
2392
- const line = stripAnsi(raw).trim();
2393
- if (line === "") return;
2394
- frame.lines.push(line);
2395
- const writer = frame.writer();
2396
- try {
2397
- writer?.write({
2398
- src: "build",
2399
- level: "debug",
2400
- event: "provider",
2401
- msg: line
2402
- });
2403
- } catch {}
2404
- frame.onLine?.(line);
2405
- }
2406
- function beginCapture({ logWriter = null, projectRoot = null, platform = null, onLine = null } = {}) {
2407
- let own = null;
2408
- const frame = {
2409
- lines: [],
2410
- pending: "",
2411
- onLine,
2412
- writer: () => {
2413
- if (logWriter) return logWriter;
2414
- if (own) return own;
2415
- if (!projectRoot || !platform) return null;
2416
- own = createNdjsonWriter(join(workspaceLogsDir$1(projectRoot), `build-${platform}.ndjson`));
2417
- return own;
2418
- },
2419
- close: () => {
2420
- own?.close?.();
2421
- own = null;
2422
- }
2423
- };
2424
- capturing.push(frame);
2425
- install();
2426
- return frame;
2427
- }
2428
- function endCapture(frame) {
2429
- if (!frame) return;
2430
- const at = capturing.lastIndexOf(frame);
2431
- if (at !== -1) capturing.splice(at, 1);
2432
- abandoned.delete(frame);
2433
- if (frame.pending !== "") {
2434
- const rest = frame.pending;
2435
- frame.pending = "";
2436
- emitLine(frame, rest);
2437
- }
2438
- frame.close();
2439
- uninstall();
2440
- }
2441
- function abandonCapture(frame, work) {
2442
- if (!frame) return;
2443
- const at = capturing.lastIndexOf(frame);
2444
- if (at !== -1) capturing.splice(at, 1);
2445
- abandoned.add(frame);
2446
- Promise.resolve(work).then(() => endCapture(frame), () => endCapture(frame));
2447
- }
2448
2419
  async function resolveRemote({ context, provider, platform, projectRoot, fingerprintHash, runOptions = null, timeoutMs = RESOLVE_TIMEOUT_MS, logWriter = null } = {}) {
2449
2420
  if (!provider || !fingerprintHash) return null;
2450
2421
  const opts = runOptions || runOptionsFor(platform);
2451
- if ("easCli" in provider) {
2452
- if (!projectRoot) return null;
2453
- return easCache("resolve", {
2454
- file: provider.easCli,
2455
- nodeEntry: provider.nodeEntry,
2456
- projectRoot,
2457
- platform,
2458
- fingerprintHash,
2459
- runOptions: opts,
2460
- timeoutMs,
2461
- context,
2462
- logWriter
2463
- });
2464
- }
2465
- const outcome = await withBudget(async () => {
2466
- const props = {
2467
- fingerprintHash: await providerFingerprint({
2468
- provider,
2469
- platform,
2470
- projectRoot,
2471
- runOptions: opts
2472
- }) ?? fingerprintHash,
2473
- platform,
2474
- runOptions: opts,
2475
- projectRoot
2476
- };
2477
- return typeof provider.plugin.resolveBuildCache === "function" ? provider.plugin.resolveBuildCache(props, provider.options) : provider.plugin.resolveRemoteBuildCache(props, provider.options);
2478
- }, timeoutMs, {
2479
- logWriter,
2422
+ if (!projectRoot) return null;
2423
+ return easCache("resolve", {
2424
+ file: provider.easCli,
2425
+ nodeEntry: provider.nodeEntry,
2480
2426
  projectRoot,
2481
- platform
2427
+ platform,
2428
+ fingerprintHash,
2429
+ runOptions: opts,
2430
+ timeoutMs,
2431
+ context,
2432
+ logWriter
2482
2433
  });
2483
- if (outcome.timedOut) return { timedOut: true };
2484
- if (outcome.error) return { failed: firstLine(outcome.error) };
2485
- const appPath = typeof outcome.value === "string" ? outcome.value.trim() : "";
2486
- if (!appPath) return null;
2487
- if (!existsSync(appPath)) return { failed: `returned ${appPath}, which does not exist` };
2488
- return { appPath };
2489
2434
  }
2490
2435
  async function uploadRemote({ context, provider, platform, projectRoot, fingerprintHash, buildPath, runOptions = null, timeoutMs = UPLOAD_TIMEOUT_MS, logWriter = null, note = (line) => context ? context.output("stderr", `${line}\n`) : console.error(line) } = {}) {
2491
2436
  if (!provider || !fingerprintHash || !buildPath) return { skipped: true };
2492
2437
  const opts = runOptions || runOptionsFor(platform);
2493
- if ("easCli" in provider) {
2494
- if (!projectRoot) return { skipped: true };
2495
- return await easCache("upload", {
2496
- file: provider.easCli,
2497
- nodeEntry: provider.nodeEntry,
2498
- projectRoot,
2499
- platform,
2500
- fingerprintHash,
2501
- runOptions: opts,
2502
- buildPath,
2503
- timeoutMs,
2504
- context,
2505
- logWriter,
2506
- note
2507
- }) ?? { skipped: true };
2508
- }
2509
- let announced = false;
2510
- const onLine = (line) => {
2511
- if (announced) return;
2512
- const dest = uploadDestination([line]);
2513
- if (!dest) return;
2514
- announced = true;
2515
- note(providerNote(`uploading to ${dest}`));
2516
- };
2517
- const outcome = await withBudget(async () => {
2518
- const props = {
2519
- projectRoot,
2520
- platform,
2521
- fingerprintHash: await providerFingerprint({
2522
- provider,
2523
- platform,
2524
- projectRoot,
2525
- runOptions: opts
2526
- }) ?? fingerprintHash,
2527
- buildPath,
2528
- runOptions: opts
2529
- };
2530
- return typeof provider.plugin.uploadBuildCache === "function" ? provider.plugin.uploadBuildCache(props, provider.options) : provider.plugin.uploadRemoteBuildCache(props, provider.options);
2531
- }, timeoutMs, {
2532
- logWriter,
2438
+ if (!projectRoot) return { skipped: true };
2439
+ return await easCache("upload", {
2440
+ file: provider.easCli,
2441
+ nodeEntry: provider.nodeEntry,
2533
2442
  projectRoot,
2534
2443
  platform,
2535
- onLine
2536
- });
2537
- if (outcome.timedOut) return { timedOut: true };
2538
- if (outcome.error) return { failed: firstLine(outcome.error) };
2539
- if (typeof outcome.value !== "string" || outcome.value.trim() === "") {
2540
- const authLine = outcome.lines.find((line) => isEasAuthFailureText(line));
2541
- return { failed: authLine ? firstLine(authLine) : "the provider returned no upload destination" };
2542
- }
2543
- const destination = uploadDestination(outcome.lines);
2544
- return destination ? {
2545
- uploaded: true,
2546
- destination
2547
- } : { uploaded: true };
2548
- }
2549
- async function providerFingerprint({ provider, platform, projectRoot, runOptions }) {
2550
- if (typeof provider.plugin.calculateFingerprintHash !== "function") return null;
2551
- try {
2552
- const hash = await provider.plugin.calculateFingerprintHash({
2553
- projectRoot,
2554
- platform,
2555
- runOptions
2556
- }, provider.options);
2557
- return typeof hash === "string" && hash.trim() !== "" ? hash.trim() : null;
2558
- } catch {
2559
- return null;
2560
- }
2561
- }
2562
- async function withBudget(factory, ms, capture) {
2563
- let timer = null;
2564
- const timeout = new Promise((resolve) => {
2565
- timer = setTimeout(() => resolve(TIMED_OUT), ms);
2566
- timer?.unref?.();
2567
- });
2568
- const frame = beginCapture(capture || {});
2569
- const work = Promise.resolve().then(() => PROVIDER_CONTEXT.run(frame, factory)).then((value) => ({ value }), (error) => ({ error }));
2570
- try {
2571
- const settled = await Promise.race([work, timeout]);
2572
- if (settled === TIMED_OUT) {
2573
- abandonCapture(frame, work);
2574
- return {
2575
- timedOut: true,
2576
- lines: frame.lines
2577
- };
2578
- }
2579
- endCapture(frame);
2580
- return {
2581
- ...settled,
2582
- lines: frame.lines
2583
- };
2584
- } catch (err) {
2585
- endCapture(frame);
2586
- throw err;
2587
- } finally {
2588
- if (timer) clearTimeout(timer);
2589
- }
2444
+ fingerprintHash,
2445
+ runOptions: opts,
2446
+ buildPath,
2447
+ timeoutMs,
2448
+ context,
2449
+ logWriter,
2450
+ note
2451
+ }) ?? { skipped: true };
2590
2452
  }
2591
2453
  function firstLine(err) {
2592
2454
  const line = (String(err?.message || err || "unknown error").trim().split("\n")[0] ?? "").trim();
@@ -5111,7 +4973,7 @@ async function collectEasSessionSweep(config, deps) {
5111
4973
  const recorded = readRecordedRemoteSessionIds([...Object.keys(config?.projects ?? {}), projectRoot]);
5112
4974
  const ledgerRoot = deps.easLedgerRoot ?? easMachineStateRoot$1();
5113
4975
  const ledger = readEasSessionLedger$1(ledgerRoot);
5114
- const run = deps.runEasFile ?? ((file, args, options) => getExecutor().runFileAsync(file, args, {
4976
+ const run = deps.runEasFile ?? ((file, args, options) => runOperationFile(deps.operation, getExecutor(), file, args, {
5115
4977
  ...options,
5116
4978
  killSignal: "SIGKILL"
5117
4979
  }));
@@ -5124,6 +4986,7 @@ async function collectEasSessionSweep(config, deps) {
5124
4986
  let pageCount = 0;
5125
4987
  let after = null;
5126
4988
  while (true) {
4989
+ deps.operation?.signal.throwIfAborted();
5127
4990
  if (pageCount >= maxPages) return empty([...recorded.notices, `EAS session list exceeded the maximum page limit of ${maxPages} for ${projectRoot}.`]);
5128
4991
  const remainingMs = deadline - now();
5129
4992
  if (remainingMs <= 0) return empty([...recorded.notices, `EAS session list exceeded its total time limit for ${projectRoot}.`]);
@@ -5135,6 +4998,8 @@ async function collectEasSessionSweep(config, deps) {
5135
4998
  omitEnv: PROXY_CREDENTIAL_ENV
5136
4999
  });
5137
5000
  } catch (error) {
5001
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
5002
+ deps.operation?.signal.throwIfAborted();
5138
5003
  return empty([...recorded.notices, `EAS session list failed for ${projectRoot}: ${describeError(error)}`]);
5139
5004
  }
5140
5005
  pageCount++;
@@ -5207,6 +5072,7 @@ async function withRemoteSessionGcLocks(projectRoot, deps, fn) {
5207
5072
  const waitMs = deps.easLockWaitMs ?? EAS_LOCK_WAIT_MS;
5208
5073
  const maxSnapshots = deps.easLockSnapshotRetries ?? EAS_LOCK_SNAPSHOT_RETRIES;
5209
5074
  for (let attempt = 0; attempt < maxSnapshots; attempt++) {
5075
+ deps.operation?.signal.throwIfAborted();
5210
5076
  const roots = easLockRoots(projectRoot, loadConfig$1());
5211
5077
  const unavailable = unavailableLockRootNotice(roots);
5212
5078
  if (unavailable) return fn({
@@ -5231,11 +5097,19 @@ async function withRemoteSessionGcLocks(projectRoot, deps, fn) {
5231
5097
  easSweepBlockedNotice: void 0,
5232
5098
  easLockedRoots: roots
5233
5099
  });
5234
- })() : withLock(roots[index], () => acquire(index + 1), { waitMs });
5100
+ })() : withLock(roots[index], () => acquire(index + 1), {
5101
+ waitMs,
5102
+ ...deps.operation ? {
5103
+ signal: deps.operation.signal,
5104
+ declareSpawnsFor: deps.operation
5105
+ } : {}
5106
+ });
5235
5107
  try {
5236
5108
  const result = await acquire(0);
5237
5109
  if (!expanded) return result;
5238
5110
  } catch (error) {
5111
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
5112
+ deps.operation?.signal.throwIfAborted();
5239
5113
  if (coreStarted) throw error;
5240
5114
  return fn({
5241
5115
  ...deps,
@@ -5248,11 +5122,12 @@ async function withRemoteSessionGcLocks(projectRoot, deps, fn) {
5248
5122
  easSweepBlockedNotice: `EAS session sweep skipped: registered workspace roots kept changing during remote-session lock acquisition.`
5249
5123
  });
5250
5124
  }
5251
- async function deleteEasSessions(easSessionSweep, deps) {
5125
+ async function deleteEasSessions(easSessionSweep, deps, output = GC_OUTPUT) {
5126
+ deps.operation?.signal.throwIfAborted();
5252
5127
  let deleteFailures = 0;
5253
5128
  const refuseAll = (detail) => {
5254
5129
  deleteFailures += easSessionSweep.orphaned.length;
5255
- for (const session of easSessionSweep.orphaned) recordGcResult("easSession", "failed", session.name, {
5130
+ for (const session of easSessionSweep.orphaned) output.record("easSession", "failed", session.name, {
5256
5131
  id: session.id,
5257
5132
  detail
5258
5133
  });
@@ -5267,7 +5142,7 @@ async function deleteEasSessions(easSessionSweep, deps) {
5267
5142
  return withRemoteSessionGcLocks(projectScope, deps, async (lockedDeps) => {
5268
5143
  if (lockedDeps.easSweepBlockedNotice) {
5269
5144
  refuseAll(`EAS session deletion refused: ${lockedDeps.easSweepBlockedNotice}`);
5270
- console.log(chalk.red(`EAS session deletion refused: ${lockedDeps.easSweepBlockedNotice}`));
5145
+ output.log(chalk.red(`EAS session deletion refused: ${lockedDeps.easSweepBlockedNotice}`));
5271
5146
  return;
5272
5147
  }
5273
5148
  const coordinatedDeps = lockedDeps;
@@ -5279,29 +5154,35 @@ async function deleteEasSessions(easSessionSweep, deps) {
5279
5154
  const ledger = readEasSessionLedger$1(ledgerRoot);
5280
5155
  const removeClaim = coordinatedDeps.removeEasSessionClaim ?? removeEasSessionClaim;
5281
5156
  const reconcileClaim = (sessionId) => {
5157
+ deps.operation?.signal.throwIfAborted();
5282
5158
  let detail = "the claim store did not remove it";
5283
5159
  try {
5284
5160
  if (removeClaim(sessionId, ledgerRoot)) return;
5285
5161
  } catch (error) {
5162
+ deps.operation?.signal.throwIfAborted();
5286
5163
  detail = describeError(error);
5287
5164
  }
5288
5165
  deleteFailures++;
5289
- console.log(chalk.red(`EAS session ${sessionId} is resolved, but its ownership claim could not be removed: ${detail}. The claim was retained for reconciliation.`));
5166
+ output.log(chalk.red(`EAS session ${sessionId} is resolved, but its ownership claim could not be removed: ${detail}. The claim was retained for reconciliation.`));
5290
5167
  };
5291
5168
  if (registryExpanded) {
5292
5169
  refuseAll("EAS session deletion refused: registered workspace roots changed after classification.");
5293
- console.log(chalk.red("EAS session deletion refused: registered workspace roots changed after classification."));
5170
+ output.log(chalk.red("EAS session deletion refused: registered workspace roots changed after classification."));
5294
5171
  } else if (!currentRecords?.safe || !ledger.safe) {
5295
5172
  refuseAll("EAS session deletion refused: the session records could not be read safely.");
5296
- for (const notice of [...currentRecords?.notices ?? [], ...ledger.notice ? [ledger.notice] : []]) console.log(chalk.red(`EAS session deletion refused: ${notice}`));
5173
+ for (const notice of [...currentRecords?.notices ?? [], ...ledger.notice ? [ledger.notice] : []]) output.log(chalk.red(`EAS session deletion refused: ${notice}`));
5297
5174
  } else {
5298
5175
  const recorded = new Set(currentRecords.ids);
5299
5176
  const eas = (coordinatedDeps.resolveEasCliBin ?? resolveEasCliBin)(projectScope);
5300
- const run = coordinatedDeps.runEasFile ?? ((file, args, options) => getExecutor().runFile(file, args, options));
5177
+ const run = coordinatedDeps.runEasFile ?? ((file, args, options) => runOperationFile(coordinatedDeps.operation, getExecutor(), file, args, {
5178
+ ...options,
5179
+ killSignal: "SIGKILL"
5180
+ }));
5301
5181
  for (const session of easSessionSweep.orphaned) {
5182
+ deps.operation?.signal.throwIfAborted();
5302
5183
  if (recorded.has(session.id)) {
5303
- console.log(chalk.dim(`EAS session ${session.id} has a workspace record and was left running.`));
5304
- recordGcResult("easSession", "kept", session.name, {
5184
+ output.log(chalk.dim(`EAS session ${session.id} has a workspace record and was left running.`));
5185
+ output.record("easSession", "kept", session.name, {
5305
5186
  id: session.id,
5306
5187
  detail: "it has a workspace record and was left running"
5307
5188
  });
@@ -5311,8 +5192,8 @@ async function deleteEasSessions(easSessionSweep, deps) {
5311
5192
  const claimState = claim ? claimStateAbsence(claim) : null;
5312
5193
  if (!claim || claim.name !== session.name || claim.platform !== session.platform || !claimState?.absent || claimState.notice) {
5313
5194
  deleteFailures++;
5314
- console.log(chalk.red(`Could not verify fixed ownership for EAS session ${session.id}: ${claimState?.notice ?? "the claim is missing, mismatched, or still recorded in workspace state"}.`));
5315
- recordGcResult("easSession", "failed", session.name, {
5195
+ output.log(chalk.red(`Could not verify fixed ownership for EAS session ${session.id}: ${claimState?.notice ?? "the claim is missing, mismatched, or still recorded in workspace state"}.`));
5196
+ output.record("easSession", "failed", session.name, {
5316
5197
  id: session.id,
5317
5198
  detail: `Could not verify fixed ownership for EAS session ${session.id}: ${claimState?.notice ?? "the claim is missing, mismatched, or still recorded in workspace state"}.`
5318
5199
  });
@@ -5320,8 +5201,8 @@ async function deleteEasSessions(easSessionSweep, deps) {
5320
5201
  }
5321
5202
  if (!eas) {
5322
5203
  deleteFailures++;
5323
- console.log(chalk.red(`Could not verify EAS session ${session.id}: eas-cli is not available.`));
5324
- recordGcResult("easSession", "failed", session.name, {
5204
+ output.log(chalk.red(`Could not verify EAS session ${session.id}: eas-cli is not available.`));
5205
+ output.record("easSession", "failed", session.name, {
5325
5206
  id: session.id,
5326
5207
  detail: `Could not verify EAS session ${session.id}: eas-cli is not available.`
5327
5208
  });
@@ -5335,14 +5216,17 @@ async function deleteEasSessions(easSessionSweep, deps) {
5335
5216
  let live;
5336
5217
  try {
5337
5218
  live = await run(eas.file, getSessionArgs(session.id), options);
5219
+ deps.operation?.signal.throwIfAborted();
5338
5220
  } catch (error) {
5221
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
5222
+ deps.operation?.signal.throwIfAborted();
5339
5223
  if (isDefinitiveMissingSessionError(error, session.id)) {
5340
- console.log(chalk.dim(`EAS session ${session.id} is already gone.`));
5224
+ output.log(chalk.dim(`EAS session ${session.id} is already gone.`));
5341
5225
  reconcileClaim(session.id);
5342
5226
  } else {
5343
5227
  deleteFailures++;
5344
- console.log(chalk.red(`Could not verify EAS session ${session.id}: ${describeError(error)}`));
5345
- recordGcResult("easSession", "failed", session.name, {
5228
+ output.log(chalk.red(`Could not verify EAS session ${session.id}: ${describeError(error)}`));
5229
+ output.record("easSession", "failed", session.name, {
5346
5230
  id: session.id,
5347
5231
  detail: `Could not verify EAS session ${session.id}: ${describeError(error)}`
5348
5232
  });
@@ -5351,14 +5235,14 @@ async function deleteEasSessions(easSessionSweep, deps) {
5351
5235
  }
5352
5236
  const inspection = inspectSessionForTeardown(live, session.id);
5353
5237
  if (inspection.action === "already-stopped") {
5354
- console.log(chalk.dim(`EAS session ${session.id} is already stopped (${inspection.status}).`));
5238
+ output.log(chalk.dim(`EAS session ${session.id} is already stopped (${inspection.status}).`));
5355
5239
  reconcileClaim(session.id);
5356
5240
  continue;
5357
5241
  }
5358
5242
  if (inspection.action === "refused") {
5359
5243
  deleteFailures++;
5360
- console.log(chalk.red(`Could not stop EAS session ${session.id}: ${inspection.reason}`));
5361
- recordGcResult("easSession", "failed", session.name, {
5244
+ output.log(chalk.red(`Could not stop EAS session ${session.id}: ${inspection.reason}`));
5245
+ output.record("easSession", "failed", session.name, {
5362
5246
  id: session.id,
5363
5247
  detail: `Could not stop EAS session ${session.id}: ${inspection.reason}`
5364
5248
  });
@@ -5366,11 +5250,15 @@ async function deleteEasSessions(easSessionSweep, deps) {
5366
5250
  }
5367
5251
  let stopped;
5368
5252
  try {
5253
+ deps.operation?.signal.throwIfAborted();
5369
5254
  stopped = await run(eas.file, stopSessionArgs(session.id), options);
5255
+ deps.operation?.signal.throwIfAborted();
5370
5256
  } catch (error) {
5257
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
5258
+ deps.operation?.signal.throwIfAborted();
5371
5259
  deleteFailures++;
5372
- console.log(chalk.red(`Could not stop EAS session ${session.id}: ${describeError(error)}`));
5373
- recordGcResult("easSession", "failed", session.name, {
5260
+ output.log(chalk.red(`Could not stop EAS session ${session.id}: ${describeError(error)}`));
5261
+ output.record("easSession", "failed", session.name, {
5374
5262
  id: session.id,
5375
5263
  detail: `Could not stop EAS session ${session.id}: ${describeError(error)}`
5376
5264
  });
@@ -5379,15 +5267,15 @@ async function deleteEasSessions(easSessionSweep, deps) {
5379
5267
  const verified = verifyStoppedSession(stopped, session.id);
5380
5268
  if (!verified.ok) {
5381
5269
  deleteFailures++;
5382
- console.log(chalk.red(`Could not verify EAS session ${session.id} stopped: ${verified.reason}`));
5383
- recordGcResult("easSession", "failed", session.name, {
5270
+ output.log(chalk.red(`Could not verify EAS session ${session.id} stopped: ${verified.reason}`));
5271
+ output.record("easSession", "failed", session.name, {
5384
5272
  id: session.id,
5385
5273
  detail: `Could not verify EAS session ${session.id} stopped: ${verified.reason}`
5386
5274
  });
5387
5275
  continue;
5388
5276
  }
5389
- console.log(chalk.green(`Stopped EAS session ${session.id} (${session.name})`));
5390
- recordGcResult("easSession", "done", session.name, { id: session.id });
5277
+ output.log(chalk.green(`Stopped EAS session ${session.id} (${session.name})`));
5278
+ output.record("easSession", "done", session.name, { id: session.id });
5391
5279
  reconcileClaim(session.id);
5392
5280
  }
5393
5281
  }
@@ -5395,12 +5283,18 @@ async function deleteEasSessions(easSessionSweep, deps) {
5395
5283
  }, {
5396
5284
  waitMs: 0,
5397
5285
  ownerPurpose: "EAS orphan deletion",
5398
- machineRoot: deps.easLedgerRoot
5286
+ machineRoot: deps.easLedgerRoot,
5287
+ ...deps.operation ? {
5288
+ signal: deps.operation.signal,
5289
+ declareSpawnsFor: deps.operation
5290
+ } : {}
5399
5291
  });
5400
5292
  } catch (error) {
5293
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
5294
+ deps.operation?.signal.throwIfAborted();
5401
5295
  const phase = finalSweepStarted ? "failed" : "lock acquisition failed";
5402
5296
  refuseAll(`EAS session deletion ${phase}: ${describeError(error)}`);
5403
- console.log(chalk.red(`EAS session deletion ${phase}: ${describeError(error)}`));
5297
+ output.log(chalk.red(`EAS session deletion ${phase}: ${describeError(error)}`));
5404
5298
  }
5405
5299
  }
5406
5300
  return deleteFailures;
@@ -5569,9 +5463,10 @@ function describeUnverifiableDevices(simNames = [], avdNames = [], { reason = "n
5569
5463
  if (ours.length === 0) return [`${reason}; device sweep skipped`];
5570
5464
  return [`${reason}, so ${ours.length} stim-created device(s) cannot be verified as orphaned: ${ours.join(", ")}`, "they were NOT touched. If they are stale, delete them with `xcrun simctl delete <udid>` or `avdmanager delete avd -n <name>`"];
5571
5465
  }
5572
- async function withAndroidAvdSizes(devices, { avdDirectory = ownedAvdDirectory, size = directorySizeAsync } = {}) {
5466
+ async function withAndroidAvdSizes(devices, { avdDirectory = ownedAvdDirectory, size = directorySizeAsync, operation } = {}) {
5573
5467
  const sized = [];
5574
5468
  for (const device of devices) {
5469
+ operation?.signal.throwIfAborted();
5575
5470
  if (device.kind !== "android") {
5576
5471
  sized.push(device);
5577
5472
  continue;
@@ -5582,12 +5477,18 @@ async function withAndroidAvdSizes(devices, { avdDirectory = ownedAvdDirectory,
5582
5477
  continue;
5583
5478
  }
5584
5479
  try {
5585
- const bytes = await size(dir, { timeoutMs: AVD_SIZE_TIMEOUT_MS });
5480
+ const bytes = await size(dir, {
5481
+ timeoutMs: AVD_SIZE_TIMEOUT_MS,
5482
+ operation,
5483
+ signal: operation?.signal
5484
+ });
5586
5485
  sized.push(bytes > 0 ? {
5587
5486
  ...device,
5588
5487
  bytes
5589
5488
  } : device);
5590
- } catch {
5489
+ } catch (error) {
5490
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
5491
+ operation?.signal.throwIfAborted();
5591
5492
  sized.push(device);
5592
5493
  }
5593
5494
  }
@@ -5615,6 +5516,8 @@ function parkedAtLeast(records, { olderThanDays = null, now = Date.now() }) {
5615
5516
  return records.filter((record) => Date.parse(record.parkedAt) <= cutoff);
5616
5517
  }
5617
5518
  async function collectParkedSims(deps, age = {}) {
5519
+ const operation = deps.operation;
5520
+ operation?.signal.throwIfAborted();
5618
5521
  const records = parkedAtLeast(readParked("ios"), age);
5619
5522
  if (records.length === 0) return [];
5620
5523
  let sims;
@@ -5622,14 +5525,24 @@ async function collectParkedSims(deps, age = {}) {
5622
5525
  try {
5623
5526
  sims = await (deps.listAllIosSims ?? listAllIosSimsAsync)({
5624
5527
  timeoutMs: DEVICE_LIST_TIMEOUT_MS$1,
5625
- includeUnavailable: true
5528
+ includeUnavailable: true,
5529
+ operation,
5530
+ signal: operation?.signal
5626
5531
  });
5627
- } catch {
5532
+ } catch (error) {
5533
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
5534
+ operation?.signal.throwIfAborted();
5628
5535
  return describeParkedSims(records, [], [], { simsChecked: false });
5629
5536
  }
5630
5537
  try {
5631
- deviceTypes = await (deps.listIosDeviceTypes ?? listIosDeviceTypesAsync)();
5632
- } catch {}
5538
+ deviceTypes = await (deps.listIosDeviceTypes ?? listIosDeviceTypesAsync)({
5539
+ operation,
5540
+ signal: operation?.signal
5541
+ });
5542
+ } catch (error) {
5543
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
5544
+ operation?.signal.throwIfAborted();
5545
+ }
5633
5546
  return describeParkedSims(records, sims, deviceTypes);
5634
5547
  }
5635
5548
  function poolConfigurationDeviceProfile(configuration) {
@@ -5642,19 +5555,35 @@ function poolConfigurationDeviceProfile(configuration) {
5642
5555
  }
5643
5556
  }
5644
5557
  async function collectParkedAvds(deps, age = {}) {
5558
+ const operation = deps.operation;
5559
+ operation?.signal.throwIfAborted();
5645
5560
  const records = parkedAtLeast(readParked("android"), age);
5646
5561
  if (!records.length) return [];
5647
5562
  let avds = null;
5648
5563
  try {
5649
- avds = await (deps.listAvds ?? listAvdsAsync)();
5650
- } catch {}
5564
+ avds = await (deps.listAvds ?? listAvdsAsync)({
5565
+ operation,
5566
+ signal: operation?.signal
5567
+ });
5568
+ } catch (error) {
5569
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
5570
+ operation?.signal.throwIfAborted();
5571
+ }
5651
5572
  const reports = [];
5652
5573
  for (const record of records) {
5574
+ operation?.signal.throwIfAborted();
5653
5575
  let bytes = null;
5654
5576
  try {
5655
5577
  const directory = (deps.avdDirectory ?? ownedAvdDirectory)(record.name);
5656
- if (directory) bytes = await (deps.directorySize ?? directorySizeAsync)(directory, { timeoutMs: AVD_SIZE_TIMEOUT_MS });
5657
- } catch {}
5578
+ if (directory) bytes = await (deps.directorySize ?? directorySizeAsync)(directory, {
5579
+ timeoutMs: AVD_SIZE_TIMEOUT_MS,
5580
+ operation,
5581
+ signal: operation?.signal
5582
+ });
5583
+ } catch (error) {
5584
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
5585
+ operation?.signal.throwIfAborted();
5586
+ }
5658
5587
  reports.push({
5659
5588
  name: record.name,
5660
5589
  systemImage: record.systemImage,
@@ -5667,7 +5596,7 @@ async function collectParkedAvds(deps, age = {}) {
5667
5596
  }
5668
5597
  return reports;
5669
5598
  }
5670
- async function eraseParkedDevices(sims, avds) {
5599
+ async function eraseParkedDevices(sims, avds, output = GC_OUTPUT, operation) {
5671
5600
  let failures = 0;
5672
5601
  const devices = [...sims.map((sim) => ({
5673
5602
  platform: "ios",
@@ -5683,74 +5612,82 @@ async function eraseParkedDevices(sims, avds) {
5683
5612
  bytes: avd.bytes
5684
5613
  }))];
5685
5614
  for (const device of devices) {
5615
+ operation?.signal.throwIfAborted();
5686
5616
  const what = `parked ${device.platform === "ios" ? "ios sim" : "android emulator"} ${device.name}`;
5687
5617
  if (device.listed !== true) {
5688
5618
  const unverified = device.listed === null;
5689
5619
  const detail = unverified ? "Stim could not list devices to verify it; it was kept. Retry stim gc --cache parked --delete." : "it is not on this machine; stim gc --delete drops its pool record.";
5690
5620
  if (unverified) failures++;
5691
- console.log((unverified ? chalk.red : chalk.dim)(`Did not erase ${what}: ${detail}`));
5692
- recordGcResult("parkedDevice", unverified ? "failed" : "kept", device.name, {
5621
+ output.log((unverified ? chalk.red : chalk.dim)(`Did not erase ${what}: ${detail}`));
5622
+ output.record("parkedDevice", unverified ? "failed" : "kept", device.name, {
5693
5623
  id: device.id,
5694
5624
  detail
5695
5625
  });
5696
5626
  continue;
5697
5627
  }
5698
- const result = device.platform === "ios" ? await eraseParkedIosSim(device.id, { label: device.name }) : await eraseParkedAvd(device.id);
5628
+ const result = device.platform === "ios" ? await eraseParkedIosSim(device.id, {
5629
+ label: device.name,
5630
+ operation
5631
+ }) : await eraseParkedAvd(device.id, { operation });
5632
+ operation?.signal.throwIfAborted();
5699
5633
  if (result.status === "torn-down") {
5700
- console.log(chalk.green(`Erased ${what}; it stays parked`));
5701
- recordGcResult("parkedDevice", "done", device.name, {
5634
+ output.log(chalk.green(`Erased ${what}; it stays parked`));
5635
+ output.record("parkedDevice", "done", device.name, {
5702
5636
  id: device.id,
5703
5637
  bytes: device.bytes
5704
5638
  });
5705
5639
  } else if (result.status === "failed") {
5706
5640
  failures++;
5707
- console.log(chalk.red(`Failed to erase ${what}: ${result.reason}`));
5708
- recordGcResult("parkedDevice", "failed", device.name, {
5641
+ output.log(chalk.red(`Failed to erase ${what}: ${result.reason}`));
5642
+ output.record("parkedDevice", "failed", device.name, {
5709
5643
  id: device.id,
5710
5644
  detail: result.reason
5711
5645
  });
5712
- } else console.log(chalk.dim(`Skipped ${device.name}; it is no longer parked.`));
5646
+ } else output.log(chalk.dim(`Skipped ${device.name}; it is no longer parked.`));
5713
5647
  }
5714
5648
  return failures;
5715
5649
  }
5716
- async function deleteParkedAvds(avds) {
5650
+ async function deleteParkedAvds(avds, output = GC_OUTPUT, operation) {
5717
5651
  let failures = 0;
5718
5652
  for (const avd of avds) {
5653
+ operation?.signal.throwIfAborted();
5719
5654
  if (avd.listed === null) {
5720
5655
  failures++;
5721
- console.log(chalk.red(`Could not verify parked android emulator ${avd.name}; its pool record was kept.`));
5722
- recordGcResult("parkedDevice", "failed", avd.name, {
5656
+ output.log(chalk.red(`Could not verify parked android emulator ${avd.name}; its pool record was kept.`));
5657
+ output.record("parkedDevice", "failed", avd.name, {
5723
5658
  id: avd.name,
5724
5659
  detail: "Stim could not list emulators to verify it; its pool record was kept. Retry stim gc --delete."
5725
5660
  });
5726
5661
  continue;
5727
5662
  }
5728
- const result = await teardownParkedAvd(avd.name);
5663
+ const result = await teardownParkedAvd(avd.name, { operation });
5664
+ operation?.signal.throwIfAborted();
5729
5665
  if (result.status === "failed") {
5730
5666
  failures++;
5731
- console.log(chalk.red(`Failed to delete parked android emulator ${avd.name}: ${result.reason}`));
5732
- recordGcResult("parkedDevice", "failed", avd.name, {
5667
+ output.log(chalk.red(`Failed to delete parked android emulator ${avd.name}: ${result.reason}`));
5668
+ output.record("parkedDevice", "failed", avd.name, {
5733
5669
  id: avd.name,
5734
5670
  detail: result.reason
5735
5671
  });
5736
5672
  } else if (result.status === "torn-down") {
5737
- console.log(chalk.green(`Deleted parked android emulator ${avd.name}`));
5738
- recordGcResult("parkedDevice", "done", avd.name, {
5673
+ output.log(chalk.green(`Deleted parked android emulator ${avd.name}`));
5674
+ output.record("parkedDevice", "done", avd.name, {
5739
5675
  id: avd.name,
5740
5676
  bytes: avd.bytes
5741
5677
  });
5742
- } else console.log(chalk.dim(`Skipped ${avd.name}; it is no longer parked.`));
5678
+ } else output.log(chalk.dim(`Skipped ${avd.name}; it is no longer parked.`));
5743
5679
  }
5744
5680
  return failures;
5745
5681
  }
5746
- async function deleteParkedSims(parkedSims, deps = {}) {
5682
+ async function deleteParkedSims(parkedSims, deps = {}, output = GC_OUTPUT) {
5747
5683
  let failures = 0;
5748
5684
  let emptied = 0;
5749
5685
  for (const sim of parkedSims) {
5686
+ deps.operation?.signal.throwIfAborted();
5750
5687
  if (sim.listed === null) {
5751
5688
  failures++;
5752
- console.log(chalk.red(`Could not verify parked ios sim ${sim.name} (${sim.udid}); its pool record was kept.`));
5753
- recordGcResult("parkedDevice", "failed", sim.name, {
5689
+ output.log(chalk.red(`Could not verify parked ios sim ${sim.name} (${sim.udid}); its pool record was kept.`));
5690
+ output.record("parkedDevice", "failed", sim.name, {
5754
5691
  id: sim.udid,
5755
5692
  detail: "Stim could not list simulators to verify it; its pool record was kept. Retry stim gc --delete."
5756
5693
  });
@@ -5759,35 +5696,38 @@ async function deleteParkedSims(parkedSims, deps = {}) {
5759
5696
  try {
5760
5697
  const teardown = sim.listed ? await teardownParkedIosSim(sim.udid, {
5761
5698
  label: sim.name,
5762
- deleteSim: deps.deleteParkedIosSim
5699
+ deleteSim: deps.deleteParkedIosSim,
5700
+ operation: deps.operation
5763
5701
  }) : null;
5764
- if (!(sim.listed ? teardown?.status === "torn-down" : await dropParked("ios", sim.udid))) {
5702
+ if (!(sim.listed ? teardown?.status === "torn-down" : await dropParked("ios", sim.udid, deps.operation))) {
5765
5703
  if (teardown?.status === "failed") {
5766
5704
  failures++;
5767
- console.log(chalk.red(`Failed to delete parked ios sim ${sim.name}: ${teardown.reason}`));
5768
- recordGcResult("parkedDevice", "failed", sim.name, {
5705
+ output.log(chalk.red(`Failed to delete parked ios sim ${sim.name}: ${teardown.reason}`));
5706
+ output.record("parkedDevice", "failed", sim.name, {
5769
5707
  id: sim.udid,
5770
5708
  detail: teardown.reason
5771
5709
  });
5772
- } else console.log(chalk.dim(`Skipped ${sim.name} (${sim.udid}); it is no longer parked.`));
5710
+ } else output.log(chalk.dim(`Skipped ${sim.name} (${sim.udid}); it is no longer parked.`));
5773
5711
  continue;
5774
5712
  }
5775
5713
  emptied++;
5776
- recordGcResult("parkedDevice", "done", sim.name, {
5714
+ output.record("parkedDevice", "done", sim.name, {
5777
5715
  id: sim.udid,
5778
5716
  bytes: sim.listed ? sim.bytes : null
5779
5717
  });
5780
- console.log(chalk.green(sim.listed ? `Deleted parked ios sim ${sim.name} (${sim.udid})` : `Dropped the parked record for ${sim.name} (${sim.udid}); it is not on this machine.`));
5718
+ output.log(chalk.green(sim.listed ? `Deleted parked ios sim ${sim.name} (${sim.udid})` : `Dropped the parked record for ${sim.name} (${sim.udid}); it is not on this machine.`));
5781
5719
  } catch (err) {
5720
+ if (err instanceof WorkspaceProcessCleanupError) throw err;
5721
+ deps.operation?.signal.throwIfAborted();
5782
5722
  failures++;
5783
- console.log(chalk.red(`Failed to delete parked ios sim ${sim.name}: ${err?.message || err}`));
5784
- recordGcResult("parkedDevice", "failed", sim.name, {
5723
+ output.log(chalk.red(`Failed to delete parked ios sim ${sim.name}: ${err?.message || err}`));
5724
+ output.record("parkedDevice", "failed", sim.name, {
5785
5725
  id: sim.udid,
5786
5726
  detail: err?.message || String(err)
5787
5727
  });
5788
5728
  }
5789
5729
  }
5790
- if (emptied) console.log(chalk.dim(` emptied ${plural(emptied, "parked simulator")} from the pool`));
5730
+ if (emptied) output.log(chalk.dim(` emptied ${plural(emptied, "parked simulator")} from the pool`));
5791
5731
  return failures;
5792
5732
  }
5793
5733
  function deviceSweepIsScoped(env = process.env) {
@@ -5821,13 +5761,16 @@ function collectDeviceLeases(now) {
5821
5761
  kept
5822
5762
  };
5823
5763
  }
5824
- async function deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords) {
5764
+ async function deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords, output = GC_OUTPUT, operation) {
5825
5765
  let deleteFailures = 0;
5826
5766
  async function reap(d, onlyIfMissing = false) {
5767
+ operation?.signal.throwIfAborted();
5827
5768
  const r = d.kind === "ios" ? await teardownOwnedIosSim(d.id, {
5828
5769
  del: true,
5829
- label: d.name
5770
+ label: d.name,
5771
+ operation
5830
5772
  }) : await teardownOwnedAvd(d.name, {
5773
+ operation,
5831
5774
  del: true,
5832
5775
  onlyIfMissing,
5833
5776
  ..."project" in d ? { owner: {
@@ -5835,7 +5778,7 @@ async function deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRe
5835
5778
  slot: d.slot
5836
5779
  } } : {},
5837
5780
  ..."project" in d ? { onRemoved: () => {
5838
- if (clearDevice(d.project, d.kind, d.slot, d.id)) console.log(chalk.dim(` cleared the ${d.kind} record for ${d.project}`));
5781
+ if (clearDevice(d.project, d.kind, d.slot, d.id)) output.log(chalk.dim(` cleared the ${d.kind} record for ${d.project}`));
5839
5782
  } } : {},
5840
5783
  ..."orphanedDirectory" in d ? { orphanedDirectory: d.orphanedDirectory } : {}
5841
5784
  });
@@ -5845,19 +5788,19 @@ async function deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRe
5845
5788
  bytes: d.bytes ?? null
5846
5789
  };
5847
5790
  if (r.status === "torn-down") {
5848
- console.log(chalk.green(`Deleted ${what}`));
5849
- recordGcResult("device", "done", d.name, result);
5850
- } else if (r.status === "missing") console.log(chalk.dim(`${what} is already gone; nothing to delete.`));
5791
+ output.log(chalk.green(`Deleted ${what}`));
5792
+ output.record("device", "done", d.name, result);
5793
+ } else if (r.status === "missing") output.log(chalk.dim(`${what} is already gone; nothing to delete.`));
5851
5794
  else if (r.status === "skipped") {
5852
- console.log(chalk.yellow(`Skipped ${what}: ${r.reason} -- left for a later gc`));
5853
- recordGcResult("device", "kept", d.name, {
5795
+ output.log(chalk.yellow(`Skipped ${what}: ${r.reason} -- left for a later gc`));
5796
+ output.record("device", "kept", d.name, {
5854
5797
  ...result,
5855
5798
  detail: `${r.reason}; left for a later gc`
5856
5799
  });
5857
5800
  } else {
5858
5801
  deleteFailures++;
5859
- console.log(chalk.red(`Failed to delete ${d.kind} device ${d.name}: ${r.reason}`));
5860
- recordGcResult("device", "failed", d.name, {
5802
+ output.log(chalk.red(`Failed to delete ${d.kind} device ${d.name}: ${r.reason}`));
5803
+ output.record("device", "failed", d.name, {
5861
5804
  ...result,
5862
5805
  detail: r.reason
5863
5806
  });
@@ -5869,20 +5812,24 @@ async function deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRe
5869
5812
  const status = await reap(d);
5870
5813
  if (d.kind === "ios" && (status === "torn-down" || status === "missing")) {
5871
5814
  clearDevice(d.project, d.kind, d.slot, d.id);
5872
- console.log(chalk.dim(` cleared the ${d.kind} record for ${d.project}`));
5815
+ output.log(chalk.dim(` cleared the ${d.kind} record for ${d.project}`));
5873
5816
  }
5874
5817
  }
5875
- for (const r of staleDeviceRecords) if (r.kind === "android" && r.owned) await reap({
5876
- ...r,
5877
- name: r.id,
5878
- idleDays: 0
5879
- }, true);
5880
- else if (await withConfigLockAsync$1(() => {
5881
- if (r.kind === "android" && deviceSlotPlatforms(getProject(r.project), r.slot)?.android?.owned) return false;
5882
- return clearDevice(r.project, r.kind, r.slot, r.id);
5883
- })) {
5884
- console.log(chalk.green(`Cleared the ${r.kind} record for ${r.project} (${r.id} is not on this machine)`));
5885
- recordGcResult("deviceRecord", "done", r.project, { id: r.id });
5818
+ for (const r of staleDeviceRecords) {
5819
+ operation?.signal.throwIfAborted();
5820
+ if (r.kind === "android" && r.owned) await reap({
5821
+ ...r,
5822
+ name: r.id,
5823
+ idleDays: 0
5824
+ }, true);
5825
+ else if (await withConfigLockAsync$1(() => {
5826
+ operation?.signal.throwIfAborted();
5827
+ if (r.kind === "android" && deviceSlotPlatforms(getProject(r.project), r.slot)?.android?.owned) return false;
5828
+ return clearDevice(r.project, r.kind, r.slot, r.id);
5829
+ }, operation?.signal)) {
5830
+ output.log(chalk.green(`Cleared the ${r.kind} record for ${r.project} (${r.id} is not on this machine)`));
5831
+ output.record("deviceRecord", "done", r.project, { id: r.id });
5832
+ }
5886
5833
  }
5887
5834
  return deleteFailures;
5888
5835
  }
@@ -6094,14 +6041,16 @@ function listAvdRecords(notices, env) {
6094
6041
  }
6095
6042
  return [...records.values()];
6096
6043
  }
6097
- function simctl(args, env) {
6098
- return getExecutor().runFileAsync("xcrun", ["simctl", ...args], {
6044
+ function simctl(args, env, operation) {
6045
+ return runOperationFile(operation, getExecutor(), "xcrun", ["simctl", ...args], {
6099
6046
  timeoutMs: DEVICE_LIST_TIMEOUT_MS$1,
6100
6047
  killSignal: "SIGKILL",
6101
- env
6048
+ env,
6049
+ cwd: operation?.cwd
6102
6050
  });
6103
6051
  }
6104
- async function collectInventory(config, deadProjects, env = process.env) {
6052
+ async function collectInventory(config, deadProjects, env = process.env, operation) {
6053
+ operation?.signal.throwIfAborted();
6105
6054
  const childEnv = Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== void 0));
6106
6055
  const notices = [];
6107
6056
  let sims = [];
@@ -6112,8 +6061,10 @@ async function collectInventory(config, deadProjects, env = process.env) {
6112
6061
  "list",
6113
6062
  "devices",
6114
6063
  "--json"
6115
- ], childEnv));
6064
+ ], childEnv, operation));
6116
6065
  } catch (error) {
6066
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
6067
+ operation?.signal.throwIfAborted();
6117
6068
  notices.push(`simulators not listed: ${describeError(error)}`);
6118
6069
  }
6119
6070
  try {
@@ -6121,8 +6072,10 @@ async function collectInventory(config, deadProjects, env = process.env) {
6121
6072
  "runtime",
6122
6073
  "list",
6123
6074
  "-j"
6124
- ], childEnv));
6075
+ ], childEnv, operation));
6125
6076
  } catch (error) {
6077
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
6078
+ operation?.signal.throwIfAborted();
6126
6079
  notices.push(`simulator runtime images not listed: ${describeError(error)}`);
6127
6080
  }
6128
6081
  try {
@@ -6130,8 +6083,10 @@ async function collectInventory(config, deadProjects, env = process.env) {
6130
6083
  "list",
6131
6084
  "runtimes",
6132
6085
  "-j"
6133
- ], childEnv), runtimes));
6086
+ ], childEnv, operation), runtimes));
6134
6087
  } catch (error) {
6088
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
6089
+ operation?.signal.throwIfAborted();
6135
6090
  notices.push(`simulator runtimes not listed: ${describeError(error)}`);
6136
6091
  }
6137
6092
  }
@@ -6225,29 +6180,37 @@ function readWrapper(path) {
6225
6180
  return missing$1(error) ? null : "unreadable";
6226
6181
  }
6227
6182
  }
6228
- async function projectWrapperRoots(project) {
6183
+ async function projectWrapperRoots(project, operation) {
6229
6184
  try {
6230
- const { context, settings } = await resolveProjectSettingsAsync(project);
6185
+ const { context, settings } = await resolveProjectSettingsAsync(project, {
6186
+ env: operation?.env,
6187
+ signal: operation?.signal,
6188
+ declareFor: operation
6189
+ });
6231
6190
  return {
6232
6191
  roots: [project, androidLayoutSetting(settings, project, context.repoRoot ?? project).gradleRoot],
6233
6192
  resolved: true
6234
6193
  };
6235
- } catch {
6194
+ } catch (error) {
6195
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
6196
+ operation?.signal.throwIfAborted();
6236
6197
  return {
6237
6198
  roots: [project],
6238
6199
  resolved: false
6239
6200
  };
6240
6201
  }
6241
6202
  }
6242
- async function projectPins(project) {
6203
+ async function projectPins(project, operation) {
6204
+ operation?.signal.throwIfAborted();
6243
6205
  if (!isDirectory(project)) return {
6244
6206
  versions: [],
6245
6207
  unreadable: true
6246
6208
  };
6247
- const { roots, resolved } = await projectWrapperRoots(project);
6209
+ const { roots, resolved } = await projectWrapperRoots(project, operation);
6248
6210
  let unreadable = !resolved;
6249
6211
  const versions = [];
6250
6212
  for (const root of roots) {
6213
+ operation?.signal.throwIfAborted();
6251
6214
  const text = readWrapper(join(root, "gradle", "wrapper", "gradle-wrapper.properties"));
6252
6215
  if (text === null) continue;
6253
6216
  const version = text === "unreadable" ? null : parseWrapperVersion(text);
@@ -6260,13 +6223,16 @@ async function projectPins(project) {
6260
6223
  };
6261
6224
  }
6262
6225
  /** The Gradle versions registered projects pin in their wrapper, and the projects whose pin cannot be read. */
6263
- async function projectGradleVersions(projects) {
6226
+ async function projectGradleVersions(projects, operation) {
6264
6227
  const used = /* @__PURE__ */ new Map();
6265
6228
  const unreadable = [];
6266
- const pins = await Promise.all(projects.map(async (project) => ({
6229
+ const read = async (project) => ({
6267
6230
  project,
6268
- ...await projectPins(project)
6269
- })));
6231
+ ...await projectPins(project, operation)
6232
+ });
6233
+ const pins = [];
6234
+ if (operation) for (const project of projects) pins.push(await read(project));
6235
+ else pins.push(...await Promise.all(projects.map(read)));
6270
6236
  for (const { project, versions, unreadable: partial } of pins) {
6271
6237
  if (partial) unreadable.push(project);
6272
6238
  for (const version of new Set(versions)) used.set(version, [...used.get(version) ?? [], project]);
@@ -6293,40 +6259,61 @@ function parseSimRuntimes(runtimeList, deviceList) {
6293
6259
  function parseXcodeBuildVersion(output) {
6294
6260
  return output ? /^Build version (\S+)/m.exec(output)?.[1] ?? null : null;
6295
6261
  }
6296
- async function quiet(file, args) {
6262
+ async function quiet(file, args, operation) {
6297
6263
  try {
6298
- return await getExecutor().runFileAsync(file, args, {
6264
+ return await runOperationFile(operation, getExecutor(), file, args, {
6299
6265
  timeoutMs: TOOL_TIMEOUT_MS,
6300
- killSignal: "SIGKILL"
6266
+ killSignal: "SIGKILL",
6267
+ cwd: operation?.cwd,
6268
+ signal: operation?.signal
6301
6269
  });
6302
- } catch {
6270
+ } catch (error) {
6271
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
6272
+ operation?.signal.throwIfAborted();
6303
6273
  return null;
6304
6274
  }
6305
6275
  }
6306
- async function measure(dirs) {
6276
+ async function measure(dirs, operation) {
6307
6277
  const darwin = process.platform === "darwin";
6308
- const [sizes, runtimeList, deviceList, agentDevice, xcode] = await Promise.all([
6309
- Promise.all(dirs.map(async (dir) => {
6310
- const result = await measureDu(["-sk", dir]);
6311
- return {
6312
- dir,
6313
- bytes: result.complete ? result.sizes.get(dir) ?? null : null
6314
- };
6315
- })),
6316
- darwin ? quiet("xcrun", [
6317
- "simctl",
6318
- "runtime",
6319
- "list",
6320
- "-j"
6321
- ]) : null,
6322
- darwin ? quiet("xcrun", [
6323
- "simctl",
6324
- "list",
6325
- "devices",
6326
- "--json"
6327
- ]) : null,
6328
- quiet("agent-device", ["--version"]),
6329
- darwin ? quiet("xcodebuild", ["-version"]) : null
6278
+ const size = async (dir) => {
6279
+ const result = await measureDu(["-sk", dir], operation);
6280
+ return {
6281
+ dir,
6282
+ bytes: result.complete ? result.sizes.get(dir) ?? null : null
6283
+ };
6284
+ };
6285
+ const measureSizes = async () => {
6286
+ if (!operation) return Promise.all(dirs.map(size));
6287
+ const sizes = [];
6288
+ for (const dir of dirs) sizes.push(await size(dir));
6289
+ return sizes;
6290
+ };
6291
+ const runtimes = () => darwin ? quiet("xcrun", [
6292
+ "simctl",
6293
+ "runtime",
6294
+ "list",
6295
+ "-j"
6296
+ ], operation) : null;
6297
+ const devices = () => darwin ? quiet("xcrun", [
6298
+ "simctl",
6299
+ "list",
6300
+ "devices",
6301
+ "--json"
6302
+ ], operation) : null;
6303
+ const agent = () => quiet("agent-device", ["--version"], operation);
6304
+ const version = () => darwin ? quiet("xcodebuild", ["-version"], operation) : null;
6305
+ const [sizes, runtimeList, deviceList, agentDevice, xcode] = operation ? [
6306
+ await measureSizes(),
6307
+ await runtimes(),
6308
+ await devices(),
6309
+ await agent(),
6310
+ await version()
6311
+ ] : await Promise.all([
6312
+ measureSizes(),
6313
+ runtimes(),
6314
+ devices(),
6315
+ agent(),
6316
+ version()
6330
6317
  ]);
6331
6318
  return {
6332
6319
  version: 1,
@@ -6426,7 +6413,7 @@ function assembleMachineCacheReport(usage, discovery, gradleUse, agentDeviceUsag
6426
6413
  }
6427
6414
  async function getMachineCacheReport(agentDeviceUsage, { maxAgeMs = MAX_AGE_MS, operation } = {}) {
6428
6415
  const discovery = discover(operation?.env, operation?.cwd);
6429
- const gradleUse = await projectGradleVersions(Object.keys(loadConfig$1()?.projects ?? {}));
6416
+ const gradleUse = await projectGradleVersions(Object.keys(loadConfig$1()?.projects ?? {}), operation);
6430
6417
  const dirs = [
6431
6418
  ...[...discovery.versionDirs].filter(([version]) => !gradleUse.used.has(version)).flatMap(([, list]) => list),
6432
6419
  ...discovery.modulesDir ? [discovery.modulesDir] : [],
@@ -6436,7 +6423,7 @@ async function getMachineCacheReport(agentDeviceUsage, { maxAgeMs = MAX_AGE_MS,
6436
6423
  const age = cached ? Date.now() - Date.parse(cached.measuredAt) : Infinity;
6437
6424
  let usage = cached;
6438
6425
  if (!usage || age < 0 || age >= maxAgeMs || JSON.stringify(usage.roots) !== JSON.stringify(dirs)) {
6439
- usage = await measure(dirs);
6426
+ usage = await measure(dirs, operation);
6440
6427
  writeUsageCache(machineCacheUsageFile(), usage, operation?.signal);
6441
6428
  }
6442
6429
  return assembleMachineCacheReport(usage, discovery, gradleUse, agentDeviceUsage, operation?.env);
@@ -6554,15 +6541,24 @@ function mayUseHome(command, homes) {
6554
6541
  return real !== null && !homes.includes(real);
6555
6542
  }));
6556
6543
  }
6557
- async function gradleProcesses(home, env = process.env, cwd = process.cwd()) {
6544
+ async function gradleProcesses(home, env = process.env, cwd = process.cwd(), signal, operation) {
6545
+ signal?.throwIfAborted();
6558
6546
  let table;
6559
6547
  try {
6560
- table = await getExecutor().runFileAsync("ps", [
6548
+ table = await runOperationFile(operation, getExecutor(), "ps", [
6561
6549
  "-axww",
6562
6550
  "-o",
6563
6551
  "pid=,command="
6564
- ], { timeoutMs: PS_TIMEOUT_MS });
6552
+ ], {
6553
+ timeoutMs: PS_TIMEOUT_MS,
6554
+ cwd,
6555
+ ...childEnvironment(env),
6556
+ signal
6557
+ });
6558
+ signal?.throwIfAborted();
6565
6559
  } catch (error) {
6560
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
6561
+ signal?.throwIfAborted();
6566
6562
  return describeError(error);
6567
6563
  }
6568
6564
  const homes = [.../* @__PURE__ */ new Set([home, gradleUserHomeSetting(env, cwd)])];
@@ -6576,8 +6572,8 @@ function insideConfigDir(dir) {
6576
6572
  const rel = relative(realpathOrNull(config) ?? config, dir);
6577
6573
  return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
6578
6574
  }
6579
- async function liveBlock(home, { pastRunning = false, env = process.env, cwd = process.cwd() } = {}) {
6580
- const processes = await gradleProcesses(home, env, cwd);
6575
+ async function liveBlock(home, { pastRunning = false, env = process.env, cwd = process.cwd(), operation } = {}) {
6576
+ const processes = await gradleProcesses(home, env, cwd, operation?.signal, operation);
6581
6577
  const config = loadConfig$1();
6582
6578
  if (config === null) return {
6583
6579
  blocked: {
@@ -6588,7 +6584,8 @@ async function liveBlock(home, { pastRunning = false, env = process.env, cwd = p
6588
6584
  running: null,
6589
6585
  runningPids: []
6590
6586
  };
6591
- const { used, unreadable } = await projectGradleVersions(Object.keys(config.projects ?? {}));
6587
+ const { used, unreadable } = await projectGradleVersions(Object.keys(config.projects ?? {}), operation);
6588
+ operation?.signal.throwIfAborted();
6592
6589
  const pinned = new Set(used.keys());
6593
6590
  if (typeof processes === "string") return {
6594
6591
  blocked: {
@@ -6635,7 +6632,8 @@ function stagedDirs(home) {
6635
6632
  }
6636
6633
  });
6637
6634
  }
6638
- async function sweepState(pastRunning = false, env = process.env, cwd = process.cwd()) {
6635
+ async function sweepState(pastRunning = false, env = process.env, cwd = process.cwd(), operation) {
6636
+ operation?.signal.throwIfAborted();
6639
6637
  const home = gradleUserHome(env, cwd);
6640
6638
  const empty = {
6641
6639
  home,
@@ -6679,7 +6677,8 @@ async function sweepState(pastRunning = false, env = process.env, cwd = process.
6679
6677
  ...await liveBlock(home, {
6680
6678
  pastRunning,
6681
6679
  env,
6682
- cwd
6680
+ cwd,
6681
+ operation
6683
6682
  })
6684
6683
  };
6685
6684
  }
@@ -6713,12 +6712,15 @@ function judgeVersion(version, dirs, state, olderThanDays, now, maxEntries = MAX
6713
6712
  detail: null
6714
6713
  };
6715
6714
  }
6716
- async function directoryBytes(dirs) {
6717
- const sizes = await Promise.all(dirs.map(async (dir) => {
6718
- const result = await measureDu(["-sk", dir]);
6715
+ async function directoryBytes(dirs, operation) {
6716
+ const size = async (dir) => {
6717
+ const result = await measureDu(["-sk", dir], operation);
6719
6718
  return result.complete ? result.sizes.get(dir) ?? null : null;
6720
- }));
6721
- return sizes.includes(null) ? null : sizes.reduce((sum, size) => sum + (size ?? 0), 0);
6719
+ };
6720
+ const sizes = [];
6721
+ if (operation) for (const dir of dirs) sizes.push(await size(dir));
6722
+ else sizes.push(...await Promise.all(dirs.map(size)));
6723
+ return sizes.includes(null) ? null : sizes.reduce((sum, bytes) => sum + (bytes ?? 0), 0);
6722
6724
  }
6723
6725
  /**
6724
6726
  * The Gradle versions under GRADLE_USER_HOME that no registered project pins, each with whether `--delete` removes
@@ -6726,8 +6728,8 @@ async function directoryBytes(dirs) {
6726
6728
  * removal left. Shared caches such as `modules-2` are never candidates.
6727
6729
  */
6728
6730
  async function collectGradleVersions({ olderThanDays = null, now = Date.now(), measure = true, maxWalkEntries = MAX_WALK_ENTRIES, pastRunningGradle = false, operation } = {}) {
6729
- const state = await sweepState(pastRunningGradle, operation?.env ?? process.env, operation?.cwd);
6730
- const versions = await Promise.all([...state.versionDirs].filter(([version]) => !state.pinned.has(version)).map(async ([version, dirs]) => {
6731
+ const state = await sweepState(pastRunningGradle, operation?.env ?? process.env, operation?.cwd, operation);
6732
+ const read = async ([version, dirs]) => {
6731
6733
  let tree;
6732
6734
  if (operation && !state.blocked && dirs.every((dir) => plainChild(dir, state.home))) {
6733
6735
  tree = {
@@ -6737,8 +6739,12 @@ async function collectGradleVersions({ olderThanDays = null, now = Date.now(), m
6737
6739
  };
6738
6740
  for (const dir of dirs) await walkAsync(dir, tree, maxWalkEntries, operation.signal);
6739
6741
  }
6740
- return Object.assign(judgeVersion(version, dirs, state, olderThanDays, now, maxWalkEntries, tree), { bytes: measure ? await directoryBytes(dirs) : null });
6741
- }));
6742
+ return Object.assign(judgeVersion(version, dirs, state, olderThanDays, now, maxWalkEntries, tree), { bytes: measure ? await directoryBytes(dirs, operation) : null });
6743
+ };
6744
+ const entries = [...state.versionDirs].filter(([version]) => !state.pinned.has(version));
6745
+ const versions = [];
6746
+ if (operation) for (const entry of entries) versions.push(await read(entry));
6747
+ else versions.push(...await Promise.all(entries.map(read)));
6742
6748
  return {
6743
6749
  home: state.home,
6744
6750
  olderThanDays,
@@ -6769,19 +6775,32 @@ function stage(dir) {
6769
6775
  * Checks one version again and removes its directories when it is still unpinned and unused: each directory is
6770
6776
  * renamed to a `.stim-delete-*` sibling right after the last process and pin check, then deleted.
6771
6777
  */
6772
- async function deleteGradleVersion(version, { olderThanDays = null, now = Date.now() } = {}) {
6773
- const state = await sweepState();
6778
+ async function deleteGradleVersion(version, { olderThanDays = null, now = Date.now(), operation } = {}) {
6779
+ const state = await sweepState(false, operation?.env, operation?.cwd, operation);
6774
6780
  const dirs = state.versionDirs.get(version) ?? [];
6775
6781
  if (dirs.length === 0 && !state.blocked) return {
6776
6782
  deleted: false,
6777
6783
  detail: "its directories are already gone"
6778
6784
  };
6779
- const judged = judgeVersion(version, dirs, state, olderThanDays, now);
6785
+ let tree;
6786
+ if (operation && !state.blocked && dirs.every((dir) => plainChild(dir, state.home))) {
6787
+ tree = {
6788
+ newestMs: 0,
6789
+ entries: 0,
6790
+ error: null
6791
+ };
6792
+ for (const dir of dirs) await walkAsync(dir, tree, MAX_WALK_ENTRIES, operation.signal);
6793
+ }
6794
+ const judged = judgeVersion(version, dirs, state, olderThanDays, now, MAX_WALK_ENTRIES, tree);
6780
6795
  if (!judged.willDelete) return {
6781
6796
  deleted: false,
6782
6797
  detail: judged.detail ?? "it is no longer eligible"
6783
6798
  };
6784
- const live = await liveBlock(state.home);
6799
+ const live = await liveBlock(state.home, {
6800
+ env: operation?.env,
6801
+ cwd: operation?.cwd,
6802
+ operation
6803
+ });
6785
6804
  if (live.blocked) return {
6786
6805
  deleted: false,
6787
6806
  detail: live.blocked.detail
@@ -6794,56 +6813,71 @@ async function deleteGradleVersion(version, { olderThanDays = null, now = Date.n
6794
6813
  deleted: false,
6795
6814
  detail: `one of its paths is no longer a plain directory inside ${state.home}`
6796
6815
  };
6816
+ operation?.signal.throwIfAborted();
6797
6817
  const staged = dirs.map(stage);
6798
- for (const dir of staged) rmSync(dir, {
6799
- recursive: true,
6800
- force: true
6801
- });
6818
+ for (const dir of staged) {
6819
+ operation?.signal.throwIfAborted();
6820
+ await rm(dir, {
6821
+ recursive: true,
6822
+ force: true
6823
+ });
6824
+ }
6802
6825
  return {
6803
6826
  deleted: true,
6804
6827
  detail: dirs.join(", ")
6805
6828
  };
6806
6829
  }
6807
- function deleteLeftovers(sweep) {
6830
+ async function deleteLeftovers(sweep, output, operation) {
6808
6831
  let failures = 0;
6809
- for (const dir of sweep.leftovers) try {
6810
- rmSync(dir, {
6811
- recursive: true,
6812
- force: true
6813
- });
6814
- console.log(chalk.green(`Finished deleting ${dir}`));
6815
- recordGcResult("gradleVersion", "done", basename(dir), { id: dir });
6816
- } catch (error) {
6817
- failures += 1;
6818
- console.log(chalk.red(`Failed to finish deleting ${dir}: ${describeError(error)}`));
6819
- recordGcResult("gradleVersion", "failed", basename(dir), {
6820
- id: dir,
6821
- detail: describeError(error)
6822
- });
6832
+ for (const dir of sweep.leftovers) {
6833
+ operation?.signal.throwIfAborted();
6834
+ try {
6835
+ await rm(dir, {
6836
+ recursive: true,
6837
+ force: true
6838
+ });
6839
+ output.log(chalk.green(`Finished deleting ${dir}`));
6840
+ output.record("gradleVersion", "done", basename(dir), { id: dir });
6841
+ } catch (error) {
6842
+ operation?.signal.throwIfAborted();
6843
+ failures += 1;
6844
+ output.log(chalk.red(`Failed to finish deleting ${dir}: ${describeError(error)}`));
6845
+ output.record("gradleVersion", "failed", basename(dir), {
6846
+ id: dir,
6847
+ detail: describeError(error)
6848
+ });
6849
+ }
6823
6850
  }
6824
6851
  return failures;
6825
6852
  }
6826
- async function deleteGradleVersions(sweep) {
6853
+ async function deleteGradleVersions(sweep, output = GC_OUTPUT, operation) {
6854
+ operation?.signal.throwIfAborted();
6827
6855
  if (!sweep) return 0;
6828
- let failures = deleteLeftovers(sweep);
6856
+ let failures = await deleteLeftovers(sweep, output, operation);
6829
6857
  for (const entry of sweep.versions) {
6858
+ operation?.signal.throwIfAborted();
6830
6859
  if (!entry.willDelete) continue;
6831
6860
  try {
6832
- const result = await deleteGradleVersion(entry.version, { olderThanDays: sweep.olderThanDays });
6861
+ const result = await deleteGradleVersion(entry.version, {
6862
+ olderThanDays: sweep.olderThanDays,
6863
+ operation
6864
+ });
6833
6865
  if (result.deleted) {
6834
- console.log(chalk.green(`Deleted Gradle ${entry.version}: ${result.detail}`));
6835
- recordGcResult("gradleVersion", "done", `Gradle ${entry.version}`, {
6866
+ output.log(chalk.green(`Deleted Gradle ${entry.version}: ${result.detail}`));
6867
+ output.record("gradleVersion", "done", `Gradle ${entry.version}`, {
6836
6868
  bytes: entry.bytes,
6837
6869
  id: entry.dirs[0]
6838
6870
  });
6839
6871
  } else {
6840
- console.log(chalk.yellow(`Kept Gradle ${entry.version}: ${result.detail}`));
6841
- recordGcResult("gradleVersion", "kept", `Gradle ${entry.version}`, { detail: result.detail });
6872
+ output.log(chalk.yellow(`Kept Gradle ${entry.version}: ${result.detail}`));
6873
+ output.record("gradleVersion", "kept", `Gradle ${entry.version}`, { detail: result.detail });
6842
6874
  }
6843
6875
  } catch (error) {
6876
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
6877
+ operation?.signal.throwIfAborted();
6844
6878
  failures += 1;
6845
- console.log(chalk.red(`Failed to delete Gradle ${entry.version}: ${describeError(error)}`));
6846
- recordGcResult("gradleVersion", "failed", `Gradle ${entry.version}`, { detail: describeError(error) });
6879
+ output.log(chalk.red(`Failed to delete Gradle ${entry.version}: ${describeError(error)}`));
6880
+ output.record("gradleVersion", "failed", `Gradle ${entry.version}`, { detail: describeError(error) });
6847
6881
  }
6848
6882
  }
6849
6883
  return failures;
@@ -6950,15 +6984,17 @@ function previewScopedEmptying(caches, env = process.env) {
6950
6984
  function ownsItsDirectory(cache) {
6951
6985
  return !Array.isArray(cache.files) && !cacheDirRefusal(cache.dir);
6952
6986
  }
6953
- function emptyCache(cache) {
6987
+ async function emptyCache(cache, signal, operation) {
6954
6988
  if (cache.prune === "report-only") return {
6955
6989
  removed: 0,
6956
6990
  bytes: 0,
6957
6991
  skipped: "report-only shared cache; Stim never deletes it"
6958
6992
  };
6959
- if (cache.prune !== "atomic") return pruneCache(cache, {
6993
+ if (cache.prune !== "atomic") return pruneCacheAsync(cache, {
6960
6994
  olderThanDays: 0,
6961
- now: Date.now() + DAY_MS
6995
+ now: Date.now() + DAY_MS,
6996
+ signal,
6997
+ operation
6962
6998
  });
6963
6999
  const claim = claimForEmptying(cache.dir);
6964
7000
  if (typeof claim === "string") return {
@@ -6969,8 +7005,9 @@ function emptyCache(cache) {
6969
7005
  try {
6970
7006
  let names;
6971
7007
  try {
6972
- names = readdirSync$1(cache.dir);
7008
+ names = await readdir(cache.dir);
6973
7009
  } catch (e) {
7010
+ signal?.throwIfAborted();
6974
7011
  return {
6975
7012
  removed: 0,
6976
7013
  bytes: 0,
@@ -6979,15 +7016,19 @@ function emptyCache(cache) {
6979
7016
  }
6980
7017
  let removed = 0;
6981
7018
  let failed = 0;
6982
- for (const name of names) try {
6983
- rmSync$1(join$1(cache.dir, name), {
6984
- recursive: true,
6985
- force: true
6986
- });
6987
- removed++;
6988
- } catch {
6989
- failed++;
7019
+ for (const name of names) {
7020
+ signal?.throwIfAborted();
7021
+ try {
7022
+ await rm(join$1(cache.dir, name), {
7023
+ recursive: true,
7024
+ force: true
7025
+ });
7026
+ removed++;
7027
+ } catch {
7028
+ failed++;
7029
+ }
6990
7030
  }
7031
+ signal?.throwIfAborted();
6991
7032
  return {
6992
7033
  removed,
6993
7034
  bytes: failed ? 0 : cache.bytes ?? 0,
@@ -7009,12 +7050,13 @@ function reportFailures(cache, failed, { log, record, fail }) {
7009
7050
  fail();
7010
7051
  return true;
7011
7052
  }
7012
- function trimCaches(caches, olderThan, output = GC_OUTPUT) {
7053
+ async function trimCaches(caches, olderThan, output = GC_OUTPUT, signal, operation) {
7013
7054
  const { log, record } = output;
7014
7055
  let cacheBytes = 0;
7015
7056
  let failures = 0;
7016
7057
  const trimmed = [];
7017
7058
  for (const c of caches) {
7059
+ signal?.throwIfAborted();
7018
7060
  if (c.machineGlobal) {
7019
7061
  log(chalk.yellow(`Left ${c.name} alone: ${c.machineGlobal}`));
7020
7062
  record("cache", "kept", c.name, {
@@ -7023,7 +7065,11 @@ function trimCaches(caches, olderThan, output = GC_OUTPUT) {
7023
7065
  });
7024
7066
  continue;
7025
7067
  }
7026
- const r = pruneCache(c, { olderThanDays: olderThan });
7068
+ const r = await pruneCacheAsync(c, {
7069
+ olderThanDays: olderThan,
7070
+ signal,
7071
+ operation
7072
+ });
7027
7073
  if (r.skipped) {
7028
7074
  log(chalk.yellow(`Left ${c.name} alone: ${r.skipped}`));
7029
7075
  record("cache", "kept", c.name, {
@@ -7047,10 +7093,11 @@ function trimCaches(caches, olderThan, output = GC_OUTPUT) {
7047
7093
  failures
7048
7094
  };
7049
7095
  }
7050
- function emptyCaches(caches, output = GC_OUTPUT) {
7096
+ async function emptyCaches(caches, output = GC_OUTPUT, signal, operation) {
7051
7097
  const { log, record } = output;
7052
7098
  let cacheBytes = 0;
7053
7099
  for (const c of caches) {
7100
+ signal?.throwIfAborted();
7054
7101
  if (!c.willEmpty) {
7055
7102
  log(chalk.yellow(`Left ${c.name} alone: ${c.emptySkipped}`));
7056
7103
  record("cache", "kept", c.name, {
@@ -7059,7 +7106,7 @@ function emptyCaches(caches, output = GC_OUTPUT) {
7059
7106
  });
7060
7107
  continue;
7061
7108
  }
7062
- const r = emptyCache(c);
7109
+ const r = await emptyCache(c, signal, operation);
7063
7110
  if (r.skipped) {
7064
7111
  log(chalk.yellow(`Left ${c.name} alone: ${r.skipped}`));
7065
7112
  record("cache", "kept", c.name, {
@@ -8285,17 +8332,24 @@ function collectIdleDevices(config, sims, deadProjects, now = Date.now(), env) {
8285
8332
  buildInProgress: workspaceBuildInProgress
8286
8333
  });
8287
8334
  }
8288
- async function deviceActivityInputsAsync(config, sims, now, env, deadProjects = []) {
8289
- const resolve = ownedAvdSerialResolverAsync({ timeoutMs: 5e3 });
8335
+ async function deviceActivityInputsAsync(config, sims, now, env, deadProjects = [], signal, operation) {
8336
+ const resolve = ownedAvdSerialResolverAsync({
8337
+ timeoutMs: 5e3,
8338
+ signal,
8339
+ operation
8340
+ });
8290
8341
  const serials = /* @__PURE__ */ new Map();
8291
8342
  for (const [project, record] of Object.entries(config?.projects ?? {})) {
8343
+ signal?.throwIfAborted();
8292
8344
  if (deadProjects.includes(project)) continue;
8293
8345
  for (const { platforms } of projectDeviceSlots(record)) {
8294
8346
  const android = platforms.android;
8295
8347
  if (!android?.owned || !android.avdName || serials.has(android.avdName)) continue;
8296
8348
  try {
8297
8349
  serials.set(android.avdName, (await resolve(android.avdName)).serial ?? null);
8298
- } catch {
8350
+ } catch (error) {
8351
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
8352
+ signal?.throwIfAborted();
8299
8353
  serials.set(android.avdName, null);
8300
8354
  }
8301
8355
  }
@@ -8305,18 +8359,20 @@ async function deviceActivityInputsAsync(config, sims, now, env, deadProjects =
8305
8359
  sims,
8306
8360
  readActivity: createActivityReaderAsync({
8307
8361
  now,
8308
- env
8362
+ env,
8363
+ signal,
8364
+ operation
8309
8365
  }),
8310
8366
  androidSerial: (name) => serials.get(name) ?? null
8311
8367
  };
8312
8368
  }
8313
- async function collectOwnedDeviceActivityAsync(config, sims, now = Date.now(), env) {
8314
- const devices = findOwnedDeviceActivity(await deviceActivityInputsAsync(config, sims, now, env));
8369
+ async function collectOwnedDeviceActivityAsync(config, sims, now = Date.now(), env, signal, operation) {
8370
+ const devices = findOwnedDeviceActivity(await deviceActivityInputsAsync(config, sims, now, env, [], signal, operation));
8315
8371
  return Promise.all(devices.map(async (device) => Object.assign({}, device, { activity: await device.activity })));
8316
8372
  }
8317
- async function collectIdleDevicesAsync(config, sims, deadProjects, now = Date.now(), env) {
8373
+ async function collectIdleDevicesAsync(config, sims, deadProjects, now = Date.now(), env, signal, operation) {
8318
8374
  const pending = findOwnedDeviceActivity({
8319
- ...await deviceActivityInputsAsync(config, sims, now, env, deadProjects),
8375
+ ...await deviceActivityInputsAsync(config, sims, now, env, deadProjects, signal, operation),
8320
8376
  deadProjects
8321
8377
  });
8322
8378
  return idleDevicesFromActivity(await Promise.all(pending.map(async (device) => Object.assign({}, device, { activity: await device.activity }))), workspaceBuildInProgress, now);
@@ -8330,7 +8386,8 @@ function idleDeviceLines(devices) {
8330
8386
  return ` ${d.kind}${slot} ${d.name} (${d.project}) -- ${idle}${build}`;
8331
8387
  })];
8332
8388
  }
8333
- async function shutDownIdleDevices(devices, idleMs, recollect, { log, record } = GC_OUTPUT) {
8389
+ async function shutDownIdleDevices(devices, idleMs, recollect, { log, record } = GC_OUTPUT, signal, operation) {
8390
+ signal?.throwIfAborted();
8334
8391
  const candidates = idleShutdownCandidates(devices, idleMs);
8335
8392
  const shutDown = [];
8336
8393
  if (candidates.length === 0) {
@@ -8341,8 +8398,10 @@ async function shutDownIdleDevices(devices, idleMs, recollect, { log, record } =
8341
8398
  };
8342
8399
  }
8343
8400
  const fresh = idleShutdownCandidates(await recollect(), idleMs);
8401
+ signal?.throwIfAborted();
8344
8402
  let failures = 0;
8345
8403
  for (const device of candidates) {
8404
+ signal?.throwIfAborted();
8346
8405
  const what = `${device.kind} ${device.name}`;
8347
8406
  const current = fresh.find((d) => d.kind === device.kind && d.id === device.id && d.project === device.project && d.slot === device.slot);
8348
8407
  if (!current) {
@@ -8353,10 +8412,16 @@ async function shutDownIdleDevices(devices, idleMs, recollect, { log, record } =
8353
8412
  });
8354
8413
  continue;
8355
8414
  }
8356
- const outcome = device.kind === "ios" ? await teardownOwnedIosSim(device.id, { label: device.name }) : await teardownOwnedAvd(device.id, { owner: {
8357
- projectPath: device.project,
8358
- slot: device.slot
8359
- } });
8415
+ const outcome = device.kind === "ios" ? await teardownOwnedIosSim(device.id, {
8416
+ label: device.name,
8417
+ operation
8418
+ }) : await teardownOwnedAvd(device.id, {
8419
+ owner: {
8420
+ projectPath: device.project,
8421
+ slot: device.slot
8422
+ },
8423
+ operation
8424
+ });
8360
8425
  if (outcome.status === "torn-down") {
8361
8426
  shutDown.push(device);
8362
8427
  log(chalk.green(`Shut down ${what}, idle ${formatLongDuration(current.idleForMs)}`));
@@ -8477,7 +8542,18 @@ function describeMetroLastStop(stop) {
8477
8542
  }
8478
8543
  //#endregion
8479
8544
  //#region src/supervisor/cache-reset.ts
8480
- async function stopOwnedMetro(root, requester) {
8545
+ async function stopOwnedMetro(root, requester, signal) {
8546
+ const resolve = async (port, probeSignal) => {
8547
+ throwIfCancelled(probeSignal, "Metro reset");
8548
+ const result = await resolveProjectMetro(port, root, {
8549
+ pidsOf: (candidate) => listeningPidsAsync(candidate, process.platform, probeSignal),
8550
+ cwdOf: async (pid) => (await processCwds([pid], probeSignal)).get(pid) ?? null,
8551
+ probe: (candidate) => isMetroRunning(candidate, probeSignal)
8552
+ });
8553
+ throwIfCancelled(probeSignal, "Metro reset");
8554
+ return result;
8555
+ };
8556
+ throwIfCancelled(signal, "Metro reset");
8481
8557
  const project = getProject(root);
8482
8558
  const port = project?.metroPort;
8483
8559
  const target = resolveSupervisorTarget({
@@ -8489,7 +8565,7 @@ async function stopOwnedMetro(root, requester) {
8489
8565
  status: "unverified",
8490
8566
  reason: target.reason ?? "unknown identity"
8491
8567
  };
8492
- if (port && target.status !== "ours" && !(await resolveProjectMetro(port, root)).missing) return {
8568
+ if (port && target.status !== "ours" && !(await resolve(port, signal)).missing) return {
8493
8569
  status: "external",
8494
8570
  reason: `a dev server Stim did not start answers on port ${port}`
8495
8571
  };
@@ -8498,23 +8574,24 @@ async function stopOwnedMetro(root, requester) {
8498
8574
  pid: target.pid,
8499
8575
  processToken: target.processToken
8500
8576
  };
8501
- requestDevServerStop(root, target.processToken, requester);
8502
- const signalled = killMetroTree(target.pid, target.processToken);
8503
- if (!signalled) withdrawDevServerStopRequest(root, target.processToken);
8577
+ await withWorkspaceStateLockAsync(root, () => requestDevServerStop(root, target.processToken, requester), signal);
8578
+ const signalled = await killMetroTreeAsync(target.pid, target.processToken);
8579
+ if (!signalled) await withWorkspaceStateLockAsync(root, () => withdrawDevServerStopRequest(root, target.processToken));
8504
8580
  if (!signalled || !await (0, process_identity_exports.waitForProcessExit)(expected, 1e4)) return {
8505
8581
  status: "stuck",
8506
8582
  reason: `supervisor pid ${target.pid} did not exit`
8507
8583
  };
8508
- if (port && !(await resolveProjectMetro(port, root)).missing) return {
8584
+ if (port && !(await resolve(port)).missing) return {
8509
8585
  status: "port-occupied",
8510
8586
  reason: `port ${port} is still occupied after stopping Metro`
8511
8587
  };
8512
- clearWorkspaceSupervisor(root, expected);
8513
- clearSupervisor(root, expected);
8588
+ await withWorkspaceStateLockAsync(root, () => clearWorkspaceSupervisor(root, expected));
8589
+ await withConfigLockAsync$1(() => clearSupervisor(root, expected));
8590
+ throwIfCancelled(signal, "Metro reset");
8514
8591
  return { status: "stopped" };
8515
8592
  }
8516
- async function stopOwnedMetroForReset(root) {
8517
- const stopped = await stopOwnedMetro(root, { by: "stim start --reset-cache" });
8593
+ async function stopOwnedMetroForReset(root, signal) {
8594
+ const stopped = await stopOwnedMetro(root, { by: "stim start --reset-cache" }, signal);
8518
8595
  switch (stopped.status) {
8519
8596
  case "unverified": throw supervisorError("STIM_SUPERVISOR_EXITED", `Cannot reset Metro: ${stopped.reason}.`, "Stop it with the tool that started it, then retry. Stim leaves unverified processes alone.");
8520
8597
  case "external": throw supervisorError("STIM_SUPERVISOR_EXITED", "Cannot reset an externally started dev server.", "Stop it with the tool that started it, then retry `stim start --reset-cache`.");
@@ -8667,10 +8744,16 @@ function estimateMemoryUse({ config, sims, androidSerial, supervisorRunning }) {
8667
8744
  liveWorkspaces: liveCount
8668
8745
  };
8669
8746
  }
8670
- async function listSims() {
8747
+ async function listSims(signal, operation) {
8671
8748
  try {
8672
- return await listAllIosSimsAsync({ timeoutMs: DEVICE_LIST_TIMEOUT_MS });
8673
- } catch {
8749
+ return await listAllIosSimsAsync({
8750
+ timeoutMs: DEVICE_LIST_TIMEOUT_MS,
8751
+ signal,
8752
+ operation
8753
+ });
8754
+ } catch (error) {
8755
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
8756
+ signal?.throwIfAborted();
8674
8757
  return null;
8675
8758
  }
8676
8759
  }
@@ -8689,20 +8772,30 @@ function runningSupervisorPid(path, project) {
8689
8772
  });
8690
8773
  return target.status === "ours" ? target.pid ?? null : null;
8691
8774
  }
8692
- async function readMemoryUse(simSnapshot) {
8693
- const resolve = ownedAvdSerialResolverAsync({ timeoutMs: 5e3 });
8775
+ async function readMemoryUse(simSnapshot, signal, operation) {
8776
+ signal?.throwIfAborted();
8777
+ const resolve = ownedAvdSerialResolverAsync({
8778
+ timeoutMs: 5e3,
8779
+ signal,
8780
+ operation
8781
+ });
8694
8782
  const config = loadConfig$1();
8695
- const sims = simSnapshot ? await snapshotSims(simSnapshot) : await listSims();
8783
+ const sims = simSnapshot && !signal ? await snapshotSims(simSnapshot) : await listSims(signal, operation);
8784
+ signal?.throwIfAborted();
8696
8785
  const serials = /* @__PURE__ */ new Map();
8697
8786
  for (const record of Object.values(config?.projects ?? {})) for (const { platforms } of projectDeviceSlots(record)) {
8787
+ signal?.throwIfAborted();
8698
8788
  const android = platforms.android;
8699
8789
  if (!android?.owned || !android.avdName || serials.has(android.avdName)) continue;
8700
8790
  try {
8701
8791
  serials.set(android.avdName, (await resolve(android.avdName)).serial ?? null);
8702
- } catch {
8792
+ } catch (error) {
8793
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
8794
+ signal?.throwIfAborted();
8703
8795
  serials.set(android.avdName, null);
8704
8796
  }
8705
8797
  }
8798
+ signal?.throwIfAborted();
8706
8799
  return estimateMemoryUse({
8707
8800
  config,
8708
8801
  sims,
@@ -8734,7 +8827,7 @@ function isOtherWorkspace(root) {
8734
8827
  function deviceLabel(device) {
8735
8828
  return `${device.kind} ${device.name} in ${device.project}`;
8736
8829
  }
8737
- async function reclaimIdleDevices({ root, dryRun, note, now, env }) {
8830
+ async function reclaimIdleDevices({ root, dryRun, note, now, env, signal, operation }) {
8738
8831
  if (deviceSweepIsScoped(env)) {
8739
8832
  note(chalk.dim(phaseLine("budget", "idle devices left alone: STIM_HOME scopes this config, devices are global")));
8740
8833
  return {
@@ -8743,13 +8836,18 @@ async function reclaimIdleDevices({ root, dryRun, note, now, env }) {
8743
8836
  };
8744
8837
  }
8745
8838
  const other = isOtherWorkspace(root);
8746
- const collect = async (at) => idleShutdownCandidates((await collectIdleDevicesAsync(loadConfig$1(), await listSims() ?? [], [], at, env)).filter((device) => other(device.project)), 0);
8839
+ const collect = async (at) => {
8840
+ signal?.throwIfAborted();
8841
+ const devices = await collectIdleDevicesAsync(loadConfig$1(), await listSims(signal, operation) ?? [], [], at, env, signal, operation);
8842
+ signal?.throwIfAborted();
8843
+ return idleShutdownCandidates(devices.filter((device) => other(device.project)), 0);
8844
+ };
8747
8845
  const idle = await collect(now);
8748
8846
  if (dryRun || idle.length === 0) return {
8749
8847
  targets: idle.map(deviceLabel),
8750
8848
  failures: 0
8751
8849
  };
8752
- const { failures, shutDown } = await shutDownIdleDevices(idle, 0, () => collect(Date.now()), stepOutput(note));
8850
+ const { failures, shutDown } = await shutDownIdleDevices(idle, 0, () => collect(Date.now()), stepOutput(note), signal, operation);
8753
8851
  return {
8754
8852
  targets: shutDown.map(deviceLabel),
8755
8853
  failures
@@ -8760,7 +8858,7 @@ function idleSince(path, now) {
8760
8858
  const at = Date.parse(activity.lastActivityAt ?? "");
8761
8859
  return activity.state === "idle" && Number.isFinite(at) ? at : null;
8762
8860
  }
8763
- async function reclaimIdleDevServers({ root, dryRun, note, now, env }) {
8861
+ async function reclaimIdleDevServers({ root, dryRun, note, now, env, signal, operation }) {
8764
8862
  const config = loadConfig$1();
8765
8863
  const other = isOtherWorkspace(root);
8766
8864
  const serving = Object.entries(config?.projects ?? {}).flatMap(([path, project]) => {
@@ -8774,7 +8872,8 @@ async function reclaimIdleDevServers({ root, dryRun, note, now, env }) {
8774
8872
  targets: [],
8775
8873
  failures: 0
8776
8874
  };
8777
- const sims = await listSims();
8875
+ const sims = await listSims(signal, operation);
8876
+ signal?.throwIfAborted();
8778
8877
  if (sims === null) {
8779
8878
  note(chalk.dim(phaseLine("budget", "dev servers left alone: simulators could not be listed to prove them idle")));
8780
8879
  return {
@@ -8782,7 +8881,8 @@ async function reclaimIdleDevServers({ root, dryRun, note, now, env }) {
8782
8881
  failures: 0
8783
8882
  };
8784
8883
  }
8785
- const busy = new Set((await collectOwnedDeviceActivityAsync(config, sims, now, env)).filter((device) => device.activity.state !== "idle").map((device) => device.project));
8884
+ const busy = new Set((await collectOwnedDeviceActivityAsync(config, sims, now, env, signal, operation)).filter((device) => device.activity.state !== "idle").map((device) => device.project));
8885
+ signal?.throwIfAborted();
8786
8886
  const idle = serving.filter(({ path }) => !busy.has(path));
8787
8887
  if (dryRun) return {
8788
8888
  targets: idle.map(({ path }) => path),
@@ -8795,6 +8895,7 @@ async function reclaimIdleDevServers({ root, dryRun, note, now, env }) {
8795
8895
  return Boolean(project && runningSupervisorPid(path, project) === pid && !workspaceBuildInProgress(path) && idleSince(path, Date.now()) !== null);
8796
8896
  };
8797
8897
  for (const { path, pid } of idle) {
8898
+ signal?.throwIfAborted();
8798
8899
  let run;
8799
8900
  try {
8800
8901
  run = await withIdleWorkspace(path, async () => {
@@ -8802,20 +8903,28 @@ async function reclaimIdleDevServers({ root, dryRun, note, now, env }) {
8802
8903
  return await withWorkspaceProcessLock(workspaceDir(path), "metro-start", async () => stillIdle(path, pid) ? stopOwnedMetro(path, {
8803
8904
  by: "budget reclaim",
8804
8905
  workspace: root
8805
- }) : "busy", {
8906
+ }, signal) : "busy", {
8806
8907
  external: true,
8807
8908
  waitMs: 0,
8808
- ownerPurpose: "budget reclaim"
8909
+ ownerPurpose: "budget reclaim",
8910
+ signal,
8911
+ declareSpawnsFor: operation
8809
8912
  });
8810
8913
  } catch (error) {
8914
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
8915
+ signal?.throwIfAborted();
8811
8916
  if (workspaceProcessLockError(error)) return "starting";
8812
8917
  throw error;
8813
8918
  }
8814
8919
  }, {
8815
8920
  purpose: "budget reclaim",
8816
- supervisor: false
8921
+ supervisor: false,
8922
+ signal,
8923
+ operation
8817
8924
  });
8818
8925
  } catch (error) {
8926
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
8927
+ signal?.throwIfAborted();
8819
8928
  failures++;
8820
8929
  note(chalk.red(`Could not stop the dev server of ${path}: ${error?.message || error}`));
8821
8930
  continue;
@@ -8841,12 +8950,14 @@ async function reclaimIdleDevServers({ root, dryRun, note, now, env }) {
8841
8950
  failures
8842
8951
  };
8843
8952
  }
8844
- async function reclaimWorkspaceOutputs({ root, dryRun, now, note, diskRecovered }) {
8953
+ async function reclaimWorkspaceOutputs({ root, dryRun, now, note, diskRecovered, signal, operation }) {
8845
8954
  const other = isOtherWorkspace(root);
8846
- const report = collectWorkspaceOutputs({
8955
+ const report = await collectWorkspaceOutputs({
8847
8956
  olderThan: null,
8848
8957
  now,
8849
- measure: false
8958
+ measure: false,
8959
+ signal,
8960
+ operation
8850
8961
  });
8851
8962
  const clearable = report.workspaces.flatMap((entry) => {
8852
8963
  const project = entry.projectRoot;
@@ -8868,11 +8979,13 @@ async function reclaimWorkspaceOutputs({ root, dryRun, now, note, diskRecovered
8868
8979
  workspaces: [entry]
8869
8980
  }, {
8870
8981
  olderThan: null,
8871
- now
8982
+ now,
8983
+ signal,
8984
+ operation
8872
8985
  }, stepOutput(note));
8873
8986
  failures += result.failures;
8874
8987
  targets.push(...result.cleared);
8875
- if (diskRecovered()) break;
8988
+ if (await diskRecovered()) break;
8876
8989
  }
8877
8990
  return {
8878
8991
  targets,
@@ -8885,13 +8998,13 @@ function cacheLabel(cache) {
8885
8998
  function trimmableCaches(env = process.env, cwd) {
8886
8999
  return planCacheEmptying(discoverCaches(env, cwd), false, env).filter((cache) => cache.prune !== "report-only" && cache.prune !== "atomic" && !cache.machineGlobal);
8887
9000
  }
8888
- function reclaimStaleCacheEntries({ dryRun, note, env, cwd }) {
9001
+ async function reclaimStaleCacheEntries({ dryRun, note, env, cwd, signal }) {
8889
9002
  const caches = trimmableCaches(env, cwd);
8890
9003
  if (dryRun) return {
8891
9004
  targets: caches.map(cacheLabel),
8892
9005
  failures: 0
8893
9006
  };
8894
- const { trimmed, failures } = trimCaches(caches, 14, stepOutput(note));
9007
+ const { trimmed, failures } = await trimCaches(caches, 14, stepOutput(note), signal);
8895
9008
  return {
8896
9009
  targets: trimmed.map(cacheLabel),
8897
9010
  failures
@@ -8900,6 +9013,7 @@ function reclaimStaleCacheEntries({ dryRun, note, env, cwd }) {
8900
9013
  function stepOutput(note) {
8901
9014
  return {
8902
9015
  log: note,
9016
+ error: note,
8903
9017
  record: () => {},
8904
9018
  fail: () => {}
8905
9019
  };
@@ -8920,6 +9034,8 @@ async function runStep(step, context) {
8920
9034
  try {
8921
9035
  return await STEPS[step](context);
8922
9036
  } catch (error) {
9037
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
9038
+ context.signal?.throwIfAborted();
8923
9039
  context.note(chalk.red(phaseLine("budget", `${STEP_ACTIONS[step]} failed: ${error?.message || error}`)));
8924
9040
  return {
8925
9041
  targets: [],
@@ -8927,8 +9043,8 @@ async function runStep(step, context) {
8927
9043
  };
8928
9044
  }
8929
9045
  }
8930
- function spaceUsers() {
8931
- const home = homedir();
9046
+ async function spaceUsers(env = process.env, signal, operation) {
9047
+ const home = env.HOME || env.USERPROFILE || homedir();
8932
9048
  const candidates = [
8933
9049
  ["Stim state", getConfigDir()],
8934
9050
  ["shared build cache", sharedBuildCache()],
@@ -8942,7 +9058,11 @@ function spaceUsers() {
8942
9058
  if (measured.has(canonical) || [...measured].some((dir) => canonical.startsWith(`${dir}${sep}`))) continue;
8943
9059
  measured.add(canonical);
8944
9060
  if (!existsSync(path)) continue;
8945
- const bytes = measuredDirectorySize(path, { timeoutMs: USAGE_TIMEOUT_MS });
9061
+ const bytes = await measuredDirectorySizeAsync(path, {
9062
+ timeoutMs: USAGE_TIMEOUT_MS,
9063
+ signal,
9064
+ operation
9065
+ });
8946
9066
  if (bytes) users.push({
8947
9067
  label: `${label} ${path}`,
8948
9068
  bytes
@@ -8966,7 +9086,7 @@ function describeShortfall(budget, measure, short) {
8966
9086
  ...short.workspaces && measure.memory ? [`${measure.memory.liveWorkspaces} active workspaces, over the limit of ${budget.maxLiveWorkspaces}`] : []
8967
9087
  ];
8968
9088
  }
8969
- async function enforceBudget({ root, note, dryRun = false, budget: resolved, env = process.env, simSnapshot, cwd }, overrides = {}) {
9089
+ async function enforceBudget({ root, note, dryRun = false, budget: resolved, env = process.env, simSnapshot, cwd, signal, operation }, overrides = {}) {
8970
9090
  const deps = {
8971
9091
  ...DEFAULT_DEPS,
8972
9092
  ...overrides
@@ -8987,7 +9107,7 @@ async function enforceBudget({ root, note, dryRun = false, budget: resolved, env
8987
9107
  const paths = [root, getConfigDir()];
8988
9108
  const measureNow = async () => ({
8989
9109
  volumes: budget.minFreeDiskMb > 0 || budget.hardFloorDiskMb > 0 ? await (deps.volumesAsync?.(paths) ?? deps.volumes(paths)) : [],
8990
- memory: budget.maxCommittedMemoryMb > 0 || budget.maxLiveWorkspaces > 0 ? await deps.memory(simSnapshot) : null
9110
+ memory: budget.maxCommittedMemoryMb > 0 || budget.maxLiveWorkspaces > 0 ? await deps.memory(simSnapshot, signal, operation) : null
8991
9111
  });
8992
9112
  let measure = await measureNow();
8993
9113
  let short = budgetShortfalls(budget, measure);
@@ -8995,6 +9115,7 @@ async function enforceBudget({ root, note, dryRun = false, budget: resolved, env
8995
9115
  const plan = reclaimPlan(short);
8996
9116
  if (plan.length) note(chalk.yellow(phaseLine("budget", `${describeShortfall(budget, measure, short).join("; ")}; ${dryRun ? "would reclaim" : "reclaiming"}`)));
8997
9117
  for (const step of plan) {
9118
+ signal?.throwIfAborted();
8998
9119
  if (!dryRun && (!overBudget(short) || DISK_ONLY.includes(step) && short.disk.length === 0)) break;
8999
9120
  const freeBefore = measure.volumes.reduce((sum, v) => sum + v.freeMb, 0);
9000
9121
  const result = await deps.step(step, {
@@ -9004,11 +9125,14 @@ async function enforceBudget({ root, note, dryRun = false, budget: resolved, env
9004
9125
  dryRun,
9005
9126
  note,
9006
9127
  now: deps.now(),
9007
- diskRecovered: () => budgetShortfalls(budget, {
9008
- volumes: deps.volumes(paths),
9128
+ diskRecovered: async () => budgetShortfalls(budget, {
9129
+ volumes: await (deps.volumesAsync?.(paths) ?? deps.volumes(paths)),
9009
9130
  memory: null
9010
- }).disk.length === 0
9131
+ }).disk.length === 0,
9132
+ signal,
9133
+ operation
9011
9134
  });
9135
+ signal?.throwIfAborted();
9012
9136
  if (!dryRun) {
9013
9137
  simSnapshot?.invalidate();
9014
9138
  measure = await measureNow();
@@ -9026,7 +9150,7 @@ async function enforceBudget({ root, note, dryRun = false, budget: resolved, env
9026
9150
  }
9027
9151
  if (!dryRun && short.hardFloor.length) {
9028
9152
  const tight = short.hardFloor.map((v) => `${formatSpace(v.freeMb)} on ${v.volume}`).join(" and ");
9029
- const users = deps.usage();
9153
+ const users = await deps.usage(env, signal, operation);
9030
9154
  return {
9031
9155
  status: "refused",
9032
9156
  reclaimed,
@@ -9046,13 +9170,15 @@ async function enforceBudget({ root, note, dryRun = false, budget: resolved, env
9046
9170
  short
9047
9171
  };
9048
9172
  }
9049
- async function peekBudget(root, overrides = {}, env = process.env) {
9173
+ async function peekBudget(root, overrides = {}, env = process.env, signal) {
9050
9174
  const outcome = await enforceBudget({
9051
9175
  root,
9052
9176
  note: () => {},
9053
9177
  dryRun: true,
9054
- env
9178
+ env,
9179
+ signal
9055
9180
  }, {
9181
+ ...signal ? { volumesAsync: (paths) => readVolumeSpaceAsync(paths, signal) } : {},
9056
9182
  ...overrides,
9057
9183
  step: async () => ({
9058
9184
  targets: [],
@@ -9064,7 +9190,10 @@ async function peekBudget(root, overrides = {}, env = process.env) {
9064
9190
  return shortfall.length ? shortfall.join("; ") : null;
9065
9191
  }
9066
9192
  async function budgetGate(args, overrides = {}) {
9067
- const outcome = await enforceBudget(args, overrides);
9193
+ const outcome = await enforceBudget(args, {
9194
+ ...args.signal ? { volumesAsync: (paths) => readVolumeSpaceAsync(paths, args.signal) } : {},
9195
+ ...overrides
9196
+ });
9068
9197
  return {
9069
9198
  reclaimed: "reclaimed" in outcome ? outcome.reclaimed : [],
9070
9199
  refusal: outcome.status === "ok" ? null : outcome.refusal
@@ -9115,7 +9244,9 @@ async function inspectBudget(root, deps = {}, env = process.env, operation) {
9115
9244
  dryRun: true,
9116
9245
  budget: read.budget,
9117
9246
  env,
9118
- cwd: operation?.cwd
9247
+ cwd: operation?.cwd,
9248
+ signal: operation?.signal,
9249
+ operation
9119
9250
  }, {
9120
9251
  ...operation ? { volumesAsync: (paths) => readVolumeSpaceAsync(paths, operation.signal) } : {},
9121
9252
  ...deps