stim 1.20.2 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/README.md +141 -8
  2. package/dist/acquire-artifact-Co2k4f7O.mjs +171 -0
  3. package/dist/act-DsuqEkiM.mjs +2801 -0
  4. package/dist/activity-CMLmzp7o.mjs +1397 -0
  5. package/dist/{agent-device-usage-output-Dzgv6lgR.mjs → agent-device-usage-output-DUFaTMdd.mjs} +23 -67
  6. package/dist/{android-DCKZrNvV.mjs → android-ByRMUoiJ.mjs} +157 -93
  7. package/dist/{android-Mz_Inn3w.mjs → android-C7sVmyI9.mjs} +299 -116
  8. package/dist/{android-Dlzxshgp.mjs → android-DGkNqa_c.mjs} +5 -5
  9. package/dist/{android-HqP1_vB_.mjs → android-DR5lZ2Az.mjs} +2 -2
  10. package/dist/android-cas-compiler.mjs +2 -2
  11. package/dist/{android-cas-Bl-cLJBk.mjs → android-cas-ngferVq2.mjs} +7 -6
  12. package/dist/android-project-BCAAmMwz.mjs +12 -0
  13. package/dist/android-project-DVWyCbWU.d.mts +391 -0
  14. package/dist/api.d.mts +810 -64
  15. package/dist/api.mjs +782 -1
  16. package/dist/{artifact-HSwkIWEQ.mjs → artifact-8EY8SAPw.mjs} +240 -213
  17. package/dist/{artifact-D3czlUvT.mjs → artifact-DJPy518Y.mjs} +203 -164
  18. package/dist/artifact-platform-CXcmX_j3.mjs +27 -0
  19. package/dist/{browser-web-CA-F1GQo.mjs → browser-web-Bcv99PBM.mjs} +194 -64
  20. package/dist/budget-Cvwn54qV.mjs +9285 -0
  21. package/dist/{build-DYDbfkk5.mjs → build-CabC3G23.mjs} +153 -98
  22. package/dist/build-cache-Dv7RN200.mjs +602 -0
  23. package/dist/build-plan-DUC8Ka9y.mjs +248 -0
  24. package/dist/{cache-manifest-XH-fvDUj.mjs → cache-manifest-DSeQjt-Q.mjs} +2 -2
  25. package/dist/cache-manifest.mjs +1 -1
  26. package/dist/{ccache-zdKiYQ3E.mjs → ccache-Btwc36-I.mjs} +10 -9
  27. package/dist/{cdp-Dq_ypAlJ.mjs → cdp-3fwICjzi.mjs} +18 -5
  28. package/dist/{chrome-BnzZMqsR.mjs → chrome-Bbw689-6.mjs} +7 -4
  29. package/dist/{cli-CLP3diBm.mjs → cli-CHZa9Lvk.mjs} +20 -20
  30. package/dist/cli.mjs +1 -1
  31. package/dist/client-OyRoEuSr.mjs +1577 -0
  32. package/dist/collector-run.d.mts +4 -40
  33. package/dist/collector-run.mjs +9 -8
  34. package/dist/{command-output-BTy8hZ0F.mjs → command-output-gwpm28fu.mjs} +1 -1
  35. package/dist/config-DlrsduwG.mjs +955 -0
  36. package/dist/{created-devices-Dzd5C2LF.mjs → created-devices-x9rN6bhs.mjs} +14 -4
  37. package/dist/{dependency-state-DG0p31-y.mjs → dependency-state-1I9KZT1Y.mjs} +20 -5
  38. package/dist/{detached-entry-B9C_zeQC.mjs → detached-entry-BwNEtIrP.mjs} +1 -1
  39. package/dist/{dev-client-DAWVIGBu.mjs → dev-client-BSDR5h3F.mjs} +2 -2
  40. package/dist/{device-BECOiufl.mjs → device-B-L3Fbxy.mjs} +91 -34
  41. package/dist/device-DklWZLqb.mjs +1068 -0
  42. package/dist/{device-capacity-CBUAcRyQ.mjs → device-capacity-DK5_EwRS.mjs} +42 -13
  43. package/dist/device-host-worker.mjs +76 -79
  44. package/dist/{device-ios-COSi6oS1.mjs → device-ios-DoB6KFCO.mjs} +77 -51
  45. package/dist/{device-lease-run-Cw2M4z51.mjs → device-lease-run-xJPQ_QQG.mjs} +14 -26
  46. package/dist/{device-pool-Bf-pOhSw.mjs → device-pool-BMkdsefq.mjs} +5 -5
  47. package/dist/{doctor-kfMBnUw6.mjs → doctor-Bo7BXLux.mjs} +271 -320
  48. package/dist/{doctor-CB8oZKCk.mjs → doctor-D5_EzwfA.mjs} +186 -91
  49. package/dist/doctor-fingerprint-DZLoYFVb.mjs +16 -0
  50. package/dist/doctor-fingerprint.mjs +19 -0
  51. package/dist/doctor-thjWAV0z.mjs +2 -0
  52. package/dist/{doctor-watchman-BRyV2adA.mjs → doctor-watchman-N2wDotwQ.mjs} +40 -21
  53. package/dist/{eas-build-JyfqyHfC.mjs → eas-build-CwknaQYj.mjs} +53 -20
  54. package/dist/{build-plan-B7SZsc2N.mjs → eas-fallback-CSgAulr5.mjs} +41 -214
  55. package/dist/{watchman-CPQFZLXY.mjs → eas-session-ledger-DlY_serQ.mjs} +230 -11
  56. package/dist/{error-diagnostics-BR677IDM.mjs → error-diagnostics-DBWO2rxE.mjs} +172 -82
  57. package/dist/exec-DEoAhcze.mjs +858 -0
  58. package/dist/exec-DnYc4HGH.d.mts +78 -0
  59. package/dist/{gc-qWOMdnER.mjs → gc-CXxN-4SE.mjs} +1091 -1029
  60. package/dist/gc-DnNDQyaP.mjs +3 -0
  61. package/dist/{guide-G9cAbVAc.mjs → guide-Br0LEfKy.mjs} +1165 -297
  62. package/dist/{hosted-android-C3MHGld1.mjs → hosted-android-B-9f-jkd.mjs} +10 -8
  63. package/dist/{hosted-client-ZEKpS9vw.mjs → hosted-client-DZAPrcro.mjs} +34 -19
  64. package/dist/{hosted-ios-l-o3iM9J.mjs → hosted-ios-CsCS21hn.mjs} +1 -1
  65. package/dist/{hosted-logs-EzQM7-3Z.mjs → hosted-logs-FIRx9X2f.mjs} +77 -43
  66. package/dist/{hosted-macos-BACVAVqE.mjs → hosted-macos-_awYSpfv.mjs} +35 -22
  67. package/dist/{hosted-native-A4s1QPXe.mjs → hosted-native-BdzE668-.mjs} +49 -27
  68. package/dist/idle-C4cZ757K.d.mts +24 -0
  69. package/dist/{idle-shutdown-DtxhPTKe.mjs → idle-shutdown-DDg3SdvY.mjs} +11 -10
  70. package/dist/ios-CDvlmUs7.d.mts +24 -0
  71. package/dist/{ios-BwVCF7oA.mjs → ios-Ds1clbIt.mjs} +8 -8
  72. package/dist/{ios-device-D7pi2Sca.mjs → ios-device--h_-8ENa.mjs} +51 -26
  73. package/dist/{ios-device-B5veSDDQ.mjs → ios-device-BXZ0c7Kb.mjs} +1 -1
  74. package/dist/{ios-RkW4pnQf.mjs → ios-hcBpnEGk.mjs} +200 -93
  75. package/dist/{ios-Ce7bc7br.mjs → ios-pm4nL6By.mjs} +160 -108
  76. package/dist/{ios-project-K9hY_Jab.mjs → ios-project-CZhKODF3.mjs} +13 -13
  77. package/dist/ios-signing-BtAffm0A.mjs +489 -0
  78. package/dist/{ios-state-CBLe1xYw.mjs → ios-state-Dtw5K2aI.mjs} +1 -1
  79. package/dist/{ios-nOLJGVdy.mjs → ios-xtlRHaeB.mjs} +1 -1
  80. package/dist/{js-swap-Bb3nsS07.mjs → js-swap-Bwhh4S7V.mjs} +30 -25
  81. package/dist/{launch-C3qJ6Agb.mjs → launch-BjxemNpK.mjs} +85 -543
  82. package/dist/{launch-F4sOqAhB.mjs → launch-CCzXlc8r.mjs} +91 -90
  83. package/dist/logs-BfwwadsZ.mjs +479 -0
  84. package/dist/logs-jTS_lHSo.mjs +248 -0
  85. package/dist/{macos-D7uDvO-z.mjs → macos-DVlSSyUg.mjs} +1 -1
  86. package/dist/macos-NfV2D7dO.mjs +694 -0
  87. package/dist/macos-run.mjs +1 -1
  88. package/dist/maintenance-run.mjs +123 -248
  89. package/dist/{metro-DnCEjCxs.mjs → metro-DiXtA3Ty.mjs} +107 -30
  90. package/dist/metro-gateway-CKZqKqx7.mjs +209 -0
  91. package/dist/{named-ports-4WopRPg-.mjs → named-ports-DkEzOfzO.mjs} +42 -90
  92. package/dist/native-android-CqgnOgsT.mjs +645 -0
  93. package/dist/native-gradle-artifact-inputs-DHFnmrVf.mjs +270 -0
  94. package/dist/native-gradle-inputs-Dnh84Zbd.mjs +211 -0
  95. package/dist/native-inputs-TRTTTRxt.mjs +91 -0
  96. package/dist/native-run-NwUuxbkg.mjs +276 -0
  97. package/dist/{native-runtime-k8dTIrbK.mjs → native-runtime-B7KIafpM.mjs} +11 -9
  98. package/dist/native-source-Y0yLhFyQ.mjs +133 -0
  99. package/dist/{native-xcode-inputs-CykopJdM.mjs → native-xcode-inputs-D_VOfFL7.mjs} +26 -18
  100. package/dist/{native-xcode-ios-Bbf30Gm0.mjs → native-xcode-ios-BYDOHWLN.mjs} +104 -60
  101. package/dist/{ndjson-GOnbyvso.mjs → ndjson-CgE_o3Dl.mjs} +3 -3
  102. package/dist/offload-worker.d.mts +59 -41
  103. package/dist/offload-worker.mjs +478 -115
  104. package/dist/operation-exec-BW0em61G.mjs +171 -0
  105. package/dist/{ownership-DCtw_2IA.mjs → ownership-C5fo5Edb.mjs} +3 -3
  106. package/dist/{page-DRHy329N.mjs → page-DBL2PiUU.mjs} +12 -5
  107. package/dist/paths-BsJMoPP2.mjs +12 -0
  108. package/dist/paths-DQtTgsBf.mjs +42 -0
  109. package/dist/{placement-log-MOfPVgaM.mjs → placement-log-BnGgJRRZ.mjs} +5 -5
  110. package/dist/{plan-placement-V0TdzYXp.mjs → plan-placement-DIsMtN-d.mjs} +15 -11
  111. package/dist/{ports-DdSUicVo.mjs → ports-Cy1RkmuQ.mjs} +3 -3
  112. package/dist/prebuild-D4JoEAEs.mjs +484 -0
  113. package/dist/{project-_phNIJfs.mjs → project-wM_XFTvE.mjs} +65 -37
  114. package/dist/{pull-request-CQ8sPbZa.mjs → pull-request-W1IuLXDt.mjs} +102 -20
  115. package/dist/pull-request-eODyIqZF.d.mts +124 -0
  116. package/dist/pull-requests.d.mts +2 -120
  117. package/dist/pull-requests.mjs +2 -2
  118. package/dist/{react-native-android-CM1XRDwv.mjs → react-native-android-6OZ7MSM7.mjs} +121 -72
  119. package/dist/{gradle-DSrXb6lG.mjs → react-native-build-Dvd8hC21.mjs} +292 -15
  120. package/dist/react-native-doctor-DsoSF1Y9.mjs +234 -0
  121. package/dist/{react-native-ios-Dhq_eiYQ.mjs → react-native-ios-4u1Rn-GR.mjs} +71 -39
  122. package/dist/{recordings-D0d5aQt0.mjs → recordings-CQmfzAh7.mjs} +12 -9
  123. package/dist/registration-BcRgF6Ox.mjs +51 -0
  124. package/dist/{reload-C8JhxjSh.mjs → reload-BzkYabB-.mjs} +94 -47
  125. package/dist/reload-D7AVlb8N.mjs +2 -0
  126. package/dist/{remote-BiR6aYjy.mjs → remote-DjGDmlTZ.mjs} +26 -18
  127. package/dist/result-D2Ykz_iD.mjs +435 -0
  128. package/dist/{run-BtPgpKks.mjs → run-DidHghD1.mjs} +5 -5
  129. package/dist/selection-DSMbWEX2.mjs +620 -0
  130. package/dist/{server-bare-R2m_gsH8.mjs → server-bare-CafzuyFB.mjs} +3 -4
  131. package/dist/{server-command-CTtCkmcQ.mjs → server-command-C6QjeupS.mjs} +14 -7
  132. package/dist/{server-expo-DsguMGUF.mjs → server-expo-CrjB29pq.mjs} +54 -11
  133. package/dist/server-expo-D58c9L0M.mjs +2 -0
  134. package/dist/{settings-B717odTD.mjs → settings-C7O8A6ZI.mjs} +309 -119
  135. package/dist/settings-DpR-1Wnn.mjs +114 -0
  136. package/dist/settings-write-CzD60rOD.mjs +360 -0
  137. package/dist/settings.schema.json +176 -16
  138. package/dist/{simslim-CCr3rE-A.mjs → simslim-BNckWpXi.mjs} +14 -12
  139. package/dist/{slot-launch-DPt0m9Nd.mjs → slot-launch-CbfmNgM6.mjs} +15 -20
  140. package/dist/{spawn-entry-Dcb7sDUz.mjs → spawn-entry-jw19m8Nl.mjs} +0 -1
  141. package/dist/stage-Bps5oius.mjs +200 -0
  142. package/dist/{start-TXeMmmO2.mjs → start-Br-oxqMD.mjs} +1 -1
  143. package/dist/start-EojN4WFz.mjs +1364 -0
  144. package/dist/{state-D1Icyjlu.mjs → state--smjFSTL.mjs} +55 -13
  145. package/dist/{state-CG_OFoaA.mjs → state-08I3hw3_.mjs} +3 -2
  146. package/dist/{stats-BoY5sEPU.mjs → stats-D3GerlFW.mjs} +7 -13
  147. package/dist/stats-report-BzaPrRm6.mjs +16 -0
  148. package/dist/{status-BTA7kKiQ.mjs → status-BRROykC8.mjs} +7 -5
  149. package/dist/status-EvrK72L2.mjs +2 -0
  150. package/dist/{status-WgFbkRzj.mjs → status-WBDajew2.mjs} +554 -452
  151. package/dist/{status-watch-C23ILtf9.mjs → status-watch-Bg_u-pwR.mjs} +222 -25
  152. package/dist/status-worker.d.mts +1 -0
  153. package/dist/status-worker.mjs +19 -0
  154. package/dist/stim-desktop-D5OGpYZM.mjs +112 -0
  155. package/dist/{stop-DR6VoDIk.mjs → stop-DTZ9uTw3.mjs} +2 -2
  156. package/dist/{stop-DPEhunlw.mjs → stop-Dky10AEJ.mjs} +33 -23
  157. package/dist/{stop-BNmBMi1F.mjs → stop-Mv6W9FAk.mjs} +137 -98
  158. package/dist/supervisor-run.d.mts +8 -17
  159. package/dist/supervisor-run.mjs +56 -28
  160. package/dist/{support-CwjbFs9H.mjs → support-CNioewsd.mjs} +14 -14
  161. package/dist/{support-DMTqj3Hm.mjs → support-yUiOjnVY.mjs} +20 -13
  162. package/dist/swiftpm-artifact-inputs-DFcGsXDP.mjs +153 -0
  163. package/dist/swiftpm-macos-BIak-Rxv.mjs +532 -0
  164. package/dist/toolchain-BY8X8HXx.mjs +2 -0
  165. package/dist/toolchain-DpJMHq0n.mjs +876 -0
  166. package/dist/{trigger-CigAYjeL.mjs → trigger-Cbyc41d_.mjs} +21 -6
  167. package/dist/trigger-yb_nFzPF.mjs +2 -0
  168. package/dist/{warm-progress-BuY-Fvfn.mjs → warm-progress-CA3TgT3-.mjs} +263 -156
  169. package/dist/web-CZ07gd77.mjs +2 -0
  170. package/dist/{web-BDBMxDpx.mjs → web-DKx6yoFA.mjs} +20 -14
  171. package/dist/web-run.mjs +13 -12
  172. package/dist/windows-report.d.mts +1 -0
  173. package/dist/windows-report.mjs +18 -0
  174. package/dist/windows-report.ps1 +43 -0
  175. package/dist/{workspace-state-DJt_MMvf.mjs → workspace-state-U08dEzhG.mjs} +62 -58
  176. package/dist/{workspaces-C36s2jb9.mjs → workspaces-CpANmBlq.mjs} +1723 -1286
  177. package/dist/{worktree-CgvJYJ-Q.mjs → worktree-CHecZ4Oq.mjs} +1097 -547
  178. package/dist/worktree-ZqZVjD9K.mjs +2 -0
  179. package/dist/xcode-BXn9-xYx.mjs +896 -0
  180. package/package.json +7 -5
  181. package/shim/android-audio/Main.java +24 -0
  182. package/shim/android-audio/README.md +17 -0
  183. package/shim/android-audio.jar +0 -0
  184. package/shim/android-optimizations.gradle +7 -5
  185. package/dist/activity-BSV745Kz.mjs +0 -629
  186. package/dist/api-qvOzWyfO.mjs +0 -154
  187. package/dist/api-run.mjs +0 -294
  188. package/dist/app-install-BIKtMfPq.mjs +0 -896
  189. package/dist/artifact-lifecycle-DzPpBYvK.mjs +0 -30
  190. package/dist/budget-f7516Zk0.mjs +0 -4106
  191. package/dist/build-facts-DcLv2HSd.d.mts +0 -111
  192. package/dist/cancellation-C_22VQTH.mjs +0 -24
  193. package/dist/client-qQAaXSUs.mjs +0 -1771
  194. package/dist/config-BYDxlMY3.mjs +0 -562
  195. package/dist/device-DuiPJQSn.mjs +0 -290
  196. package/dist/device-lease-BX1sJQ2E.mjs +0 -1224
  197. package/dist/device-remote-xRT0Vmo9.mjs +0 -1272
  198. package/dist/eas-session-ledger-Cm0Gwq2F.mjs +0 -207
  199. package/dist/exec-DqKo9GKJ.mjs +0 -268
  200. package/dist/git-DDfFzkMm.mjs +0 -45
  201. package/dist/launch-verify-B6Q8hZP-.mjs +0 -428
  202. package/dist/logs-BGrEUaEs.mjs +0 -383
  203. package/dist/macos-Ch5VJmcS.mjs +0 -584
  204. package/dist/metro-gateway-CX9Xlkq2.mjs +0 -103
  205. package/dist/miss-reason-mKzZ4VvN.mjs +0 -238
  206. package/dist/native-android-D5qc3-_v.mjs +0 -226
  207. package/dist/native-gradle-inputs-BsLRrl7u.mjs +0 -349
  208. package/dist/ndjson-Du3j0AQ9.d.mts +0 -17
  209. package/dist/plan-C4TjdPsR.mjs +0 -260
  210. package/dist/prebuild-DYbucPXy.mjs +0 -212
  211. package/dist/preview-78a9kR_-.mjs +0 -1011
  212. package/dist/react-native-build-CtCccJtb.mjs +0 -239
  213. package/dist/react-native-doctor-CfRnFfjs.mjs +0 -78
  214. package/dist/remote-cache-OyOyVIS3.mjs +0 -526
  215. package/dist/result-CaJg5Iat.mjs +0 -186
  216. package/dist/server-expo-Bigriskl.mjs +0 -2
  217. package/dist/settings-BORNaaCO.mjs +0 -402
  218. package/dist/settings-vk3mJGSm.d.mts +0 -13
  219. package/dist/spawn-claims-DVS5RkMS.mjs +0 -72
  220. package/dist/stage-KNTQopqv.mjs +0 -157
  221. package/dist/start-BWqzODpb.mjs +0 -1116
  222. package/dist/stim-desktop-BzBN2Em2.mjs +0 -63
  223. package/dist/swiftpm-macos-DRNVJWlb.mjs +0 -119
  224. package/dist/tailnet-Dh-qiYzG.mjs +0 -265
  225. package/dist/toolchain-BFkAcstd.mjs +0 -2
  226. package/dist/toolchain-DxSsNssu.mjs +0 -1657
  227. package/dist/trigger-9_USgmxz.mjs +0 -2
  228. package/dist/web-Dq-i3wzJ.mjs +0 -2
  229. package/dist/worktree-DpLdCHga.mjs +0 -2
  230. package/dist/{api-run.d.mts → doctor-fingerprint.d.mts} +0 -0
  231. package/dist/{errors-DzkMYh8Q.mjs → errors-ccaPc4ox.mjs} +2 -2
@@ -1,43 +1,209 @@
1
- import { A as acquireAvdClaim, E as upsertProject, S as removeProject, W as projectDeviceSlots, _ as loadConfig$1, a as clearDevice, d as getConfigDir, g as isPathPrefix, j as ownership_claim_exports, k as process_identity_exports, p as getProject } from "./config-BYDxlMY3.mjs";
2
- import { n as isTimeoutError, t as getExecutor } from "./exec-DqKo9GKJ.mjs";
3
- import { r as gitQuiet, t as git } from "./git-DDfFzkMm.mjs";
4
- import { At as restoreFile, Ct as locallyKnownUpstream, Et as removeWorktree, G as resolveIosProjectDir, K as resolveProjectSettings, Mt as unpushedCommits, Nt as warmWorktreePaths, Ot as resolveFullRef, Q as settingShapeErrors, R as readCommittedSettings, St as listWorktrees, Tt as readWorktreeExclude, _t as hasPopulatedSubmodules, a as SETTING_SHAPE_REMEDY, bt as isPodInstallChurn, dt as deleteBranch, ft as depsOutOfSync, gt as gitCommonDirOnDisk, ht as gitCommonDir, jt as sourceCheckoutOf, lt as branchExists, mt as dirtyPaths, nt as unknownSettingKeys, q as resolveSettings, ut as cloneIgnoredEntries, vt as hasRemote, wt as podsOutOfSync, yt as hasUncommittedWork } from "./settings-B717odTD.mjs";
5
- import { u as releaseWorkspaceLeases } from "./device-lease-BX1sJQ2E.mjs";
6
- import { i as recordWorkspaceUse, r as readWorkspaceState$1, x as workspaceDir, y as workspaceAgentDeviceDir } from "./workspace-state-DJt_MMvf.mjs";
7
- import { a as formatElapsed, h as shortUdid, i as formatDuration, l as phaseLine, p as releasedLeaseFact, u as plural } from "./command-output-BTy8hZ0F.mjs";
8
- import { D as clearManagedMetroTunnel, N as readRemoteSession, O as clearRemoteSession, P as readRemoteSessionId, a as isMobileProject, j as readMetroTunnel, r as findProjectRoot, w as MODE_COMMAND } from "./project-_phNIJfs.mjs";
9
- import { a as workspaceLinks, i as workspaceLinkLine } from "./stim-desktop-BzBN2Em2.mjs";
10
- import { at as POOL_SETTING_REMEDY, dt as parkedMaxSetting } from "./android-Mz_Inn3w.mjs";
11
- import { d as signalProcessTree, r as killMetroTree, s as pidExists, u as resolveProjectMetro } from "./metro-DnCEjCxs.mjs";
12
- import { A as withManagedRemoteWorktreeRemovalLock, D as stopTunnel, M as withManagedTunnelRemovalLock, P as resolveSupervisorTarget, b as teardownOwnedIosSim, d as emptyWorkspaceDir, f as withIdleWorkspace, v as teardownOwnedAvd, y as teardownOwnedBrowser } from "./workspaces-C36s2jb9.mjs";
13
- import { _t as withdrawDevServerStopRequest, ht as requestDevServerStop, o as machineNumber } from "./budget-f7516Zk0.mjs";
14
- import { t as clearNamedPorts } from "./named-ports-4WopRPg-.mjs";
15
- import { f as resolveEasCliBin } from "./remote-cache-OyOyVIS3.mjs";
16
- import { o as readWebRecord } from "./state-D1Icyjlu.mjs";
17
- import { t as agentDeviceDaemon } from "./activity-BSV745Kz.mjs";
18
- import { t as macosAppPresent } from "./state-CG_OFoaA.mjs";
19
- import { L as podInstallCommand, d as DEPS_ERROR, f as podsAreStale, g as runPodInstall, h as runCaptured, p as readPodState, y as HEARTBEAT_INTERVAL_MS } from "./toolchain-DxSsNssu.mjs";
20
- import { a as discoverXcodeProject } from "./react-native-build-CtCccJtb.mjs";
21
- import { i as stopHostedAndroid } from "./hosted-android-C3MHGld1.mjs";
22
- import { i as endRecordedSession } from "./device-remote-xRT0Vmo9.mjs";
23
- import { n as verifyCollectorOwnership } from "./ownership-DCtw_2IA.mjs";
24
- import { r as stopHostedIos } from "./hosted-ios-l-o3iM9J.mjs";
25
- import { n as stopMacosApp } from "./stop-DPEhunlw.mjs";
26
- import { a as pullRequestLookup, r as endedPullRequest } from "./pull-request-CQ8sPbZa.mjs";
27
- import { a as forgetStatusMeasures, s as mergeState, t as startWarmProgress } from "./warm-progress-BuY-Fvfn.mjs";
28
- import { r as workspaceRecordingEnabled } from "./recordings-D0d5aQt0.mjs";
29
- import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-DG0p31-y.mjs";
30
- import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
31
- import { basename, dirname, join, relative } from "node:path";
32
- 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";
33
- import { LAST_BUILD_KEYS, archiveDir, archiveEnabled, archiveLock, archiveRoot, archivedUsage, countErrorEntries, endedAgentSessionOf, isJsonObject, listRecordedDevices, loadConfig, queryLogs, readArchives, readBuildHistory, readJsonObject, readLastBuilds, readMacosRecord, readPullRequestCache, readWorkspaceState, recordingEnabled, sortByTs } from "@stim-cli/core/state";
34
- import { existsSync as existsSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, readlinkSync as readlinkSync$1, realpathSync as realpathSync$1, renameSync as renameSync$1, rmSync as rmSync$1, symlinkSync as symlinkSync$1, unlinkSync as unlinkSync$1, writeFileSync as writeFileSync$1 } from "fs";
35
- import { withDirLock, workspaceName } from "@stim-cli/core";
1
+ import { c as declareSpawnsOn, f as stopDeclaringSpawnsOn, g as process_identity_exports, m as operationState, n as isTimeoutError, r as WorkspaceProcessCleanupError, t as getExecutor, x as ownership_claim_exports } from "./exec-DEoAhcze.mjs";
2
+ import { d as workspaceDir, l as workspaceAgentDeviceDir } from "./paths-DQtTgsBf.mjs";
3
+ import { E as upsertProject, G as projectDeviceSlots, L as measuredDirectorySizeAsync, S as removeProject, _ as loadConfig$1, a as clearDevice, d as getConfigDir, g as isPathPrefix, j as acquireAvdClaim, p as getProject } from "./config-DlrsduwG.mjs";
4
+ import { a as formatElapsed, h as shortUdid, i as formatDuration, l as phaseLine, p as releasedLeaseFact, u as plural } from "./command-output-gwpm28fu.mjs";
5
+ import { c as gitQuiet, o as git, r as childEnvironment, s as gitAsync, t as runOperationFile } from "./operation-exec-BW0em61G.mjs";
6
+ import { i as recordWorkspaceUse, r as readWorkspaceState$1 } from "./workspace-state-U08dEzhG.mjs";
7
+ import { At as readWorktreeExclude, Bt as warmWorktreePaths, Ct as isPodInstallChurn, Dt as locallyKnownUpstream, Et as listWorktreesAsync, Ft as resolveFullRefAsync, K as resolveIosProjectDir, Lt as restoreFileAsync, Pt as resolveFullRef, Rt as sourceCheckoutOf, St as hasUncommittedWorkAsync, Y as resolveSettings, a as SETTING_SHAPE_REMEDY, bt as hasPopulatedSubmodulesAsync, dt as branchExistsAsync, et as settingShapeErrors, ft as cloneIgnoredEntries, gt as dirtyPathsAsync, it as unknownSettingKeys, jt as removeWorktreeAsync, kt as podsOutOfSync, mt as depsOutOfSync, pt as deleteBranchAsync, q as resolveProjectSettings, vt as gitCommonDirAsync, xt as hasRemoteAsync, yt as gitCommonDirOnDisk, z as readCommittedSettings, zt as unpushedCommitsAsync } from "./settings-C7O8A6ZI.mjs";
8
+ import { F as readRemoteSessionId, M as readMetroTunnel, O as clearManagedMetroTunnel, P as readRemoteSession, T as MODE_COMMAND, i as findProjectRoot$1, k as clearRemoteSession, o as isMobileProject } from "./project-wM_XFTvE.mjs";
9
+ import { t as macosAppPresent } from "./state-08I3hw3_.mjs";
10
+ import { c as pidExists, d as resolveProjectMetro, p as signalProcessTreeAsync, r as killMetroTreeAsync } from "./metro-DiXtA3Ty.mjs";
11
+ import { o as readWebRecord } from "./state--smjFSTL.mjs";
12
+ import { In as runProcessOperation, S as withdrawDevServerStopRequest, b as requestDevServerStop, br as discoverCaches, c as readVolumeSpace, l as readVolumeSpaceAsync, nt as planCacheEmptying, o as machineNumber, qn as resolveEasCliBin, u as resolveBudget, zt as endRecordedSession } from "./budget-Cvwn54qV.mjs";
13
+ import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
14
+ import { p as cacheUser } from "./native-run-NwUuxbkg.mjs";
15
+ import { D as teardownOwnedAvd, O as teardownOwnedBrowser, S as withManagedTunnelRemovalLock, _ as stopTunnel, a as collectWorkspaceOutputs, b as withManagedRemoteWorktreeRemovalLock, d as emptyWorkspaceDir, f as withIdleWorkspace, h as resolveSupervisorTarget, k as teardownOwnedIosSim } from "./workspaces-CpANmBlq.mjs";
16
+ import { D as releaseWorkspaceLeases, t as agentDeviceDaemon } from "./activity-CMLmzp7o.mjs";
17
+ import { _t as parkedMaxSetting, dt as POOL_SETTING_REMEDY } from "./android-C7sVmyI9.mjs";
18
+ import { F as readHostMemoryPressureAsync, P as readHostMemoryPressure } from "./ios-hcBpnEGk.mjs";
19
+ import { a as workspaceLinkLine, o as workspaceLinks } from "./stim-desktop-D5OGpYZM.mjs";
20
+ import { t as clearNamedPorts } from "./named-ports-DkEzOfzO.mjs";
21
+ import { r as HEARTBEAT_INTERVAL_MS, x as podInstallCommand } from "./xcode-BXn9-xYx.mjs";
22
+ import { t as resolveMaintenanceSettings } from "./settings-Djl-mpjX.mjs";
23
+ import { _ as runCaptured, h as readPodState, m as podsAreStale, p as DEPS_ERROR, v as runPodInstall } from "./toolchain-DpJMHq0n.mjs";
24
+ import { i as stopHostedAndroid } from "./hosted-android-B-9f-jkd.mjs";
25
+ import { r as stopHostedIos } from "./hosted-ios-CsCS21hn.mjs";
26
+ import { n as stopMacosApp } from "./stop-Dky10AEJ.mjs";
27
+ import { n as verifyCollectorOwnership } from "./ownership-C5fo5Edb.mjs";
28
+ import { a as discoverXcodeProject } from "./react-native-build-Dvd8hC21.mjs";
29
+ import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-1I9KZT1Y.mjs";
30
+ import { a as forgetStatusMeasures, p as mergeStateAsync, t as startWarmProgress } from "./warm-progress-CA3TgT3-.mjs";
31
+ import { o as pullRequestLookupAsync, r as endedPullRequest } from "./pull-request-W1IuLXDt.mjs";
32
+ import { r as workspaceRecordingEnabled } from "./recordings-CQmfzAh7.mjs";
36
33
  import { claimRemoveCommand, clearFreeClaimSet, readClaimSet, releaseClaim, tryAcquireClaim } from "@stim-cli/core/ownership-claim";
37
34
  import { createHash, randomUUID } from "node:crypto";
38
- import { homedir } from "node:os";
35
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
36
+ import { cpus, freemem, homedir, loadavg, totalmem } from "node:os";
37
+ import { basename, dirname, join, relative } from "node:path";
38
+ import { buildCacheRoot, configDir, metroCacheRoot, unitLines, unitWords, withDirLockTransaction, workspaceName } from "@stim-cli/core";
39
+ import { LAST_BUILD_KEYS, archiveDir, archiveEnabled, archiveLock, archiveRoot, archivedUsage, buildWorkerRoot, countErrorEntries, endedAgentSessionOf, isJsonObject, listRecordedDevicesAsync, loadConfig, queryLogs, readArchivesAsync, readBuildHistory, readJsonObject, readLastBuilds, readMacosRecord, readPullRequestCache, readWorkspaceState, recordingEnabled, sharedCcache, sharedCompilationCache, sortByTs } from "@stim-cli/core/state";
40
+ 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";
41
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
39
42
  import chalk from "chalk";
43
+ import { existsSync as existsSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, readlinkSync as readlinkSync$1, realpathSync as realpathSync$1, renameSync as renameSync$1, rmSync as rmSync$1, symlinkSync as symlinkSync$1, unlinkSync as unlinkSync$1, writeFileSync as writeFileSync$1 } from "fs";
44
+ import { chmod, lstat, mkdir, readdir, rename, rm, stat, utimes, writeFile } from "node:fs/promises";
40
45
  import { createHash as createHash$1 } from "crypto";
46
+ //#region src/maintenance/pressure.ts
47
+ /**
48
+ * The free-disk floor of each level in MiB. Level 2 has no floor of its own: it runs when level 1 left free disk
49
+ * under the budget floor, and stops once free disk is back above it. Its urgent threshold only sets the reported level.
50
+ */
51
+ function levelFloors(budget, settings) {
52
+ const floor = Math.max(budget.minFreeDiskMb, budget.hardFloorDiskMb);
53
+ return {
54
+ 1: floor,
55
+ 2: floor,
56
+ 3: Math.max(settings.criticalFreeDiskGb * 1024, budget.hardFloorDiskMb),
57
+ urgent: settings.urgentFreeDiskGb * 1024
58
+ };
59
+ }
60
+ function diskPressureLevel(freeMb, budget, settings) {
61
+ const floors = levelFloors(budget, settings);
62
+ if (freeMb < floors[3]) return 3;
63
+ if (freeMb < floors.urgent) return 2;
64
+ return freeMb < floors[1] ? 1 : 0;
65
+ }
66
+ function lowestFree(disk) {
67
+ return disk.length ? Math.min(...disk.map((volume) => volume.freeMb)) : Infinity;
68
+ }
69
+ /** Whether an action planned at a pressure level may still run with this much free disk. */
70
+ function levelStillShort(action, freeMb, budget, settings) {
71
+ return action.level === void 0 || freeMb < levelFloors(budget, settings)[action.level];
72
+ }
73
+ /** The protection window in milliseconds: protectRecentHours, or maintenance.pressureProtectMinutes for actions of level 2 and 3. */
74
+ function protectWindowMs(action, settings) {
75
+ return action.level === void 0 ? settings.protectRecentHours * 36e5 : settings.pressureProtectMinutes * 6e4;
76
+ }
77
+ //#endregion
78
+ //#region src/maintenance/measure.ts
79
+ function measureDisk() {
80
+ return readVolumeSpace([
81
+ configDir(),
82
+ ...Object.keys(loadConfig()?.projects ?? {}),
83
+ buildWorkerRoot(loadConfig())
84
+ ]);
85
+ }
86
+ function measureDiskAsync(operation) {
87
+ return readVolumeSpaceAsync([
88
+ configDir(),
89
+ ...Object.keys(loadConfig()?.projects ?? {}),
90
+ buildWorkerRoot(loadConfig())
91
+ ], operation.signal);
92
+ }
93
+ /** The disk pressure level of the fullest volume Stim writes to, measured now. */
94
+ function currentDiskLevel(disk = measureDisk()) {
95
+ return diskPressureLevel(lowestFree(disk), resolveBudget().budget, resolveMaintenanceSettings());
96
+ }
97
+ function measurePressure(settings, previous, now) {
98
+ return pressureFromObservations(settings, previous, now, readHostMemoryPressure(), measureDisk());
99
+ }
100
+ async function measurePressureAsync(settings, previous, now, operation) {
101
+ return pressureFromObservations(settings, previous, now, await readHostMemoryPressureAsync(void 0, void 0, operation.signal, operation), await readVolumeSpaceAsync([
102
+ configDir(),
103
+ ...Object.keys(loadConfig()?.projects ?? {}),
104
+ buildWorkerRoot(loadConfig())
105
+ ], operation.signal));
106
+ }
107
+ function pressureFromObservations(settings, previous, now, level, disk) {
108
+ const availableBytes = process.platform === "darwin" ? null : freemem();
109
+ const warningSince = level === "warning" ? previous?.warningSince ?? now : null;
110
+ return {
111
+ disk,
112
+ memory: {
113
+ level,
114
+ availableBytes,
115
+ pressured: settings.memoryPressureLevel !== "off" && (level === "critical" || settings.memoryPressureLevel === "warning" && warningSince !== null && now - warningSince >= settings.memoryWarningMinutes * 6e4 || availableBytes !== null && availableBytes < (settings.minAvailableMemoryGb === void 0 ? totalmem() * .1 : settings.minAvailableMemoryGb * 1024 ** 3))
116
+ },
117
+ warningSince
118
+ };
119
+ }
120
+ function sizeScanDeferred(settings) {
121
+ return loadavg()[0] / Math.max(1, cpus().length) > settings.maxLoadPerCore;
122
+ }
123
+ function cacheBlocked(entry, discovered = discoverCaches(), env = process.env) {
124
+ return planCacheEmptying([{
125
+ ...entry,
126
+ note: "",
127
+ prune: "entries",
128
+ ...discovered.some((cache) => canonicalPath(cache.dir) === canonicalPath(entry.dir) && cache.source === "registered") ? { source: "registered" } : {}
129
+ }], false, env)[0]?.machineGlobal ?? (entry.category === "compilation-cache" ? cacheUser(entry.dir) : void 0);
130
+ }
131
+ async function measureSizes(now, failure) {
132
+ const sizes = [];
133
+ const outputs = await collectWorkspaceOutputs({
134
+ olderThan: null,
135
+ now,
136
+ sizeTimeoutMs: 12e4
137
+ });
138
+ for (const entry of outputs.workspaces) {
139
+ if (entry.bytes === null) {
140
+ failure(entry.dir, entry.projectRoot ?? void 0);
141
+ continue;
142
+ }
143
+ sizes.push({
144
+ name: "workspace build outputs",
145
+ category: "workspace-outputs",
146
+ dir: entry.dir,
147
+ bytes: entry.bytes,
148
+ measuredAt: now,
149
+ ...entry.projectRoot ? { workspace: canonicalPath(entry.projectRoot) } : {},
150
+ idleDays: entry.idleDays,
151
+ ...!entry.willClear ? { blocked: entry.keptReason ?? "workspace ownership is unresolved" } : {}
152
+ });
153
+ }
154
+ const known = [
155
+ {
156
+ name: "shared build cache",
157
+ dir: buildCacheRoot(),
158
+ category: "build-cache"
159
+ },
160
+ {
161
+ name: "Metro transform caches",
162
+ dir: metroCacheRoot(),
163
+ category: "metro-cache"
164
+ },
165
+ {
166
+ name: "ccache",
167
+ dir: sharedCcache(),
168
+ category: "ccache"
169
+ },
170
+ {
171
+ name: "Swift compilation cache",
172
+ dir: sharedCompilationCache(),
173
+ category: "compilation-cache"
174
+ }
175
+ ];
176
+ const discovered = discoverCaches();
177
+ for (const cache of discovered) {
178
+ if (cache.source !== "registered" && !canonicalPath(cache.dir).startsWith(`${canonicalPath(configDir())}/`)) continue;
179
+ if (cache.prune === "report-only") continue;
180
+ const dir = canonicalPath(cache.dir);
181
+ if (known.some((item) => dir === canonicalPath(item.dir) || dir.startsWith(`${canonicalPath(item.dir)}/`))) continue;
182
+ const category = cache.name.startsWith("Metro transform cache") ? "metro-cache" : /^(Expo build cache|native build cache|Build cache)$/.test(cache.name) ? "build-cache" : "other";
183
+ known.push({
184
+ name: cache.name,
185
+ dir: cache.dir,
186
+ category
187
+ });
188
+ }
189
+ for (const entry of known) {
190
+ if (!existsSync(entry.dir)) continue;
191
+ const bytes = await measuredDirectorySizeAsync(entry.dir, { timeoutMs: 12e4 });
192
+ if (bytes === null) {
193
+ failure(entry.dir);
194
+ continue;
195
+ }
196
+ const blocked = cacheBlocked(entry, discovered);
197
+ sizes.push({
198
+ ...entry,
199
+ bytes,
200
+ measuredAt: now,
201
+ ...blocked ? { blocked } : {}
202
+ });
203
+ }
204
+ return sizes;
205
+ }
206
+ //#endregion
41
207
  //#region src/workspace/pods-relocate.ts
42
208
  const CHECKSUMS_HEADER = "SPEC CHECKSUMS:\n";
43
209
  const GENERATED_DIRS = [
@@ -230,7 +396,7 @@ const KIND_PATH = {
230
396
  recordings: "recordings",
231
397
  agentActions: "agent-device/sessions"
232
398
  };
233
- function archiveSettings() {
399
+ function archiveSettings(env = process.env) {
234
400
  const config = loadConfig();
235
401
  return Object.fromEntries([
236
402
  "maxAgeDays",
@@ -242,57 +408,63 @@ function archiveSettings() {
242
408
  "recordings.maxTotalGb",
243
409
  "agentActions.maxAgeDays"
244
410
  ].map((key) => {
245
- const result = machineNumber(`archive.${key}`, config, process.env);
411
+ const result = machineNumber(`archive.${key}`, config, env);
246
412
  if (result.error) throw new Error(result.error);
247
413
  return [key, result.value];
248
414
  }));
249
415
  }
250
- function enabled(root) {
416
+ function enabled(root, env) {
251
417
  const config = loadConfig();
252
418
  const project = config?.projects?.[root];
253
419
  const present = existsSync(root);
254
420
  const common = present ? gitCommonDirOnDisk(root) : null;
255
421
  if (!present && Object.values(config?.repos ?? {}).some((repo) => repo.settings?.archive?.enabled === false)) return false;
256
- return archiveEnabled(process.env, [
422
+ return archiveEnabled(env, [
257
423
  project?.settings,
258
424
  common ? config?.repos?.[common]?.settings : void 0,
259
425
  present ? readCommittedSettings(root) : void 0,
260
426
  config
261
427
  ]);
262
428
  }
263
- function files(dir) {
429
+ async function files(dir, signal) {
430
+ signal?.throwIfAborted();
264
431
  if (!existsSync(dir)) return [];
265
- return readdirSync(dir).flatMap((name) => {
432
+ const found = [];
433
+ for (const name of await readdir(dir)) {
434
+ signal?.throwIfAborted();
266
435
  const path = join(dir, name);
267
- const stat = lstatSync(path);
268
- if (stat.isSymbolicLink()) throw new Error(`refusing symlink ${path}`);
269
- return stat.isDirectory() ? files(path) : [{
436
+ const entry = await lstat(path);
437
+ if (entry.isSymbolicLink()) throw new Error(`refusing symlink ${path}`);
438
+ if (entry.isDirectory()) for (const file of await files(path, signal)) found.push(file);
439
+ else found.push({
270
440
  path,
271
- bytes: stat.size,
272
- at: stat.mtimeMs
273
- }];
274
- });
441
+ bytes: entry.size,
442
+ at: entry.mtimeMs
443
+ });
444
+ }
445
+ return found;
275
446
  }
276
- function secureTree(dir) {
277
- if (lstatSync(dir).isSymbolicLink()) throw new Error(`refusing symlink ${dir}`);
278
- chmodSync(dir, 448);
279
- for (const name of readdirSync(dir)) {
447
+ async function secureTree(dir) {
448
+ if ((await lstat(dir)).isSymbolicLink()) throw new Error(`refusing symlink ${dir}`);
449
+ await chmod(dir, 448);
450
+ for (const name of await readdir(dir)) {
280
451
  const path = join(dir, name);
281
- const stat = lstatSync(path);
282
- if (stat.isSymbolicLink()) throw new Error(`refusing symlink ${path}`);
283
- if (stat.isDirectory()) secureTree(path);
284
- else chmodSync(path, 384);
452
+ const entry = await lstat(path);
453
+ if (entry.isSymbolicLink()) throw new Error(`refusing symlink ${path}`);
454
+ if (entry.isDirectory()) await secureTree(path);
455
+ else await chmod(path, 384);
285
456
  }
286
457
  }
287
- function writeRecord(dir, record) {
288
- const small = [
458
+ async function writeRecord(dir, record) {
459
+ let small = 0;
460
+ for (const name of [
289
461
  "state.json",
290
462
  "ended-agents.json",
291
463
  "log-error-index.json"
292
- ].reduce((sum, name) => {
464
+ ]) {
293
465
  const path = join(dir, name);
294
- return sum + (existsSync(path) ? statSync(path).size : 0);
295
- }, 0);
466
+ if (existsSync(path)) small += (await stat(path)).size;
467
+ }
296
468
  for (let pass = 0; pass < 8; pass++) {
297
469
  record.bytes.total = KINDS.reduce((sum, kind) => sum + record.bytes[kind], record.bytes.record);
298
470
  const bytes = small + Buffer.byteLength(`${JSON.stringify(record)}\n`);
@@ -302,59 +474,70 @@ function writeRecord(dir, record) {
302
474
  record.bytes.total = KINDS.reduce((sum, kind) => sum + record.bytes[kind], record.bytes.record);
303
475
  const tmp = join(dir, `.archive-${randomUUID()}.tmp`);
304
476
  try {
305
- writeFileSync(tmp, `${JSON.stringify(record)}\n`, { mode: 384 });
306
- renameSync(tmp, join(dir, "archive.json"));
477
+ await writeFile(tmp, `${JSON.stringify(record)}\n`, { mode: 384 });
478
+ await rename(tmp, join(dir, "archive.json"));
307
479
  } finally {
308
- rmSync(tmp, { force: true });
480
+ await rm(tmp, { force: true });
309
481
  }
310
482
  const now = /* @__PURE__ */ new Date();
311
- utimesSync(archiveRoot(), now, now);
483
+ await utimes(archiveRoot(), now, now);
312
484
  }
313
- function removeRecord(id) {
485
+ async function removeRecord(id) {
314
486
  const dir = archiveDir(id);
315
487
  if (!existsSync(dir)) return;
316
488
  const removing = join(archiveRoot(), `.removing-${id}`);
317
- renameSync(dir, removing);
318
- rmSync(removing, {
489
+ await rename(dir, removing);
490
+ await rm(removing, {
319
491
  recursive: true,
320
492
  force: true
321
493
  });
322
494
  }
323
- function sweepArchiveStaging(remove, waitMs = 0) {
495
+ async function sweepArchiveStaging(remove, waitMs = 0, signal) {
496
+ signal?.throwIfAborted();
324
497
  if (!existsSync(archiveRoot())) return [];
325
- return withDirLock(archiveLock(), () => readdirSync(archiveRoot()).filter((name) => name.startsWith(".incoming-") && (!name.endsWith(".claims") || !existsSync(join(archiveRoot(), name.slice(0, -7)))) || name.startsWith(".removing-")).flatMap((name) => {
326
- const path = join(archiveRoot(), name.endsWith(".claims") ? name.slice(0, -7) : name);
327
- const root = `${path}.claims`;
328
- const survey = name.startsWith(".incoming-") ? readClaimSet(root) : null;
329
- const unresolved = survey?.unresolved[0];
330
- const kept = Boolean(survey && (survey.live.length || survey.unresolved.length));
331
- const entry = {
332
- path,
333
- kept,
334
- reason: unresolved?.reason ?? (kept ? "live archive writer" : null),
335
- removeCommand: unresolved ? claimRemoveCommand(root) : null
336
- };
337
- if (remove && !kept) {
338
- if (!survey || clearFreeClaimSet({
339
- root,
340
- label: "archive staging"
341
- }).status === "cleared") {
342
- rmSync(path, {
343
- recursive: true,
344
- force: true
345
- });
346
- return [];
498
+ return withDirLockTransaction(archiveLock(), async () => {
499
+ const entries = [];
500
+ for (const name of await readdir(archiveRoot())) {
501
+ signal?.throwIfAborted();
502
+ if (!(name.startsWith(".incoming-") && (!name.endsWith(".claims") || !existsSync(join(archiveRoot(), name.slice(0, -7)))) || name.startsWith(".removing-"))) continue;
503
+ const path = join(archiveRoot(), name.endsWith(".claims") ? name.slice(0, -7) : name);
504
+ const root = `${path}.claims`;
505
+ const survey = name.startsWith(".incoming-") ? readClaimSet(root) : null;
506
+ const unresolved = survey?.unresolved[0];
507
+ const kept = Boolean(survey && (survey.live.length || survey.unresolved.length));
508
+ const entry = {
509
+ path,
510
+ kept,
511
+ reason: unresolved?.reason ?? (kept ? "live archive writer" : null),
512
+ removeCommand: unresolved ? claimRemoveCommand(root) : null
513
+ };
514
+ if (remove && !kept) {
515
+ if (!survey || clearFreeClaimSet({
516
+ root,
517
+ label: "archive staging"
518
+ }).status === "cleared") {
519
+ signal?.throwIfAborted();
520
+ await rm(path, {
521
+ recursive: true,
522
+ force: true
523
+ });
524
+ continue;
525
+ }
347
526
  }
527
+ entries.push(entry);
348
528
  }
349
- return [entry];
350
- }), { waitMs });
529
+ return entries;
530
+ }, {
531
+ waitMs,
532
+ signal
533
+ });
351
534
  }
352
535
  function stripPids(value) {
353
536
  if (Array.isArray(value)) return value.map(stripPids);
354
537
  if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).filter(([key]) => !/pids?$/i.test(key)).map(([key, child]) => [key, stripPids(child)]));
355
538
  return value;
356
539
  }
357
- function gitFacts(root) {
540
+ async function gitFacts(root, operation) {
358
541
  const empty = {
359
542
  repository: null,
360
543
  branch: null,
@@ -364,32 +547,55 @@ function gitFacts(root) {
364
547
  pullRequest: null
365
548
  };
366
549
  if (!existsSync(root)) return cachedFacts(root) ?? empty;
367
- const run = (args) => gitQuiet(root, args, { timeoutMs: 1e3 });
550
+ const execution = {
551
+ ...childEnvironment(operation?.env ?? process.env),
552
+ signal: operation?.signal,
553
+ declareFor: operation,
554
+ cwd: operation?.cwd,
555
+ killSignal: "SIGKILL"
556
+ };
557
+ const run = async (args) => {
558
+ try {
559
+ const result = await gitAsync(root, args, {
560
+ ...execution,
561
+ timeoutMs: 1e3
562
+ });
563
+ operation?.signal.throwIfAborted();
564
+ return result;
565
+ } catch (error) {
566
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
567
+ operation?.signal.throwIfAborted();
568
+ return null;
569
+ }
570
+ };
368
571
  const common = gitCommonDirOnDisk(root);
369
- const summary = run([
572
+ const summary = (await run([
370
573
  "show",
371
574
  "-s",
372
575
  "--format=%H%n%s",
373
576
  "HEAD"
374
- ])?.split("\n");
375
- const location = run([
577
+ ]))?.split("\n");
578
+ const location = (await run([
376
579
  "rev-parse",
377
580
  "--show-toplevel",
378
581
  "--abbrev-ref",
379
582
  "HEAD"
380
- ])?.split("\n");
583
+ ]))?.split("\n");
381
584
  const branch = location?.[1] && location[1] !== "HEAD" ? location[1] : null;
382
- const defaultRef = run([
585
+ const defaultRef = await run([
383
586
  "symbolic-ref",
384
587
  "--quiet",
385
588
  "refs/remotes/origin/HEAD"
386
589
  ]);
387
590
  let merged = null;
388
591
  if (defaultRef?.startsWith("refs/remotes/")) {
389
- const state = mergeState(root, {
592
+ const state = await mergeStateAsync(root, {
390
593
  ref: defaultRef,
391
594
  name: defaultRef.slice(13)
392
- }, { timeoutMs: 2e3 });
595
+ }, {
596
+ ...execution,
597
+ timeoutMs: 2e3
598
+ });
393
599
  merged = state.merged ? true : state.unknown ? null : false;
394
600
  }
395
601
  let prPath = root;
@@ -430,35 +636,48 @@ function cachedFacts(root) {
430
636
  }
431
637
  return null;
432
638
  }
433
- function archiveWorkspace(root, removedBy, state = readWorkspaceState(root)) {
434
- writeArchive(root, removedBy, state);
639
+ function archiveWarning(message, operation) {
640
+ if (operation) operation.output("stderr", `${message}\n`);
641
+ else console.error(message);
642
+ }
643
+ /** Publishes history once its first move starts; cancellation then skips retention and further cleanup. */
644
+ async function archiveWorkspace(root, removedBy, state = readWorkspaceState(root), operation) {
645
+ operation?.signal.throwIfAborted();
646
+ await writeArchive(root, removedBy, state, operation);
647
+ operation?.signal.throwIfAborted();
435
648
  try {
436
- enforceArchiveRetention();
649
+ await enforceArchiveRetention(void 0, operation);
437
650
  } catch (error) {
438
- console.error(`could not enforce archive retention: ${String(error?.message ?? error).split("\n")[0]}`);
651
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
652
+ operation?.signal.throwIfAborted();
653
+ archiveWarning(`could not enforce archive retention: ${String(error?.message ?? error).split("\n")[0]}`, operation);
439
654
  }
440
655
  }
441
- function writeArchive(root, removedBy, state) {
656
+ async function writeArchive(root, removedBy, state, operation) {
657
+ const env = operation?.env ?? process.env;
442
658
  let staging = null;
443
659
  let claim;
444
660
  try {
445
661
  const dir = workspaceDir(root);
446
- if (!existsSync(dir) || lstatSync(dir).isSymbolicLink()) return;
662
+ if (!existsSync(dir) || (await lstat(dir)).isSymbolicLink()) return;
447
663
  if (existsSync(root)) root = realpathSync(root);
448
664
  try {
449
- sweepArchiveStaging(true, 5e3);
450
- } catch {}
451
- if (!enabled(root)) return;
452
- const limits = archiveSettings();
665
+ await sweepArchiveStaging(true, 5e3, operation?.signal);
666
+ } catch {
667
+ operation?.signal.throwIfAborted();
668
+ }
669
+ if (!enabled(root, env)) return;
670
+ const limits = archiveSettings(env);
453
671
  if (!limits.maxAgeDays || !limits.maxCount) return;
454
672
  const logs = join(dir, "logs");
455
- if (!existsSync(join(dir, "state.json")) && !Object.values(readBuildHistory(state)).some((entries) => entries?.length) && (!existsSync(logs) || readdirSync(logs).length === 0)) return;
456
- const worktree = gitFacts(root);
457
- mkdirSync(archiveRoot(), {
673
+ if (!existsSync(join(dir, "state.json")) && !Object.values(readBuildHistory(state)).some((entries) => entries?.length) && (!existsSync(logs) || (await readdir(logs)).length === 0)) return;
674
+ const worktree = await gitFacts(root, operation);
675
+ operation?.signal.throwIfAborted();
676
+ await mkdir(archiveRoot(), {
458
677
  recursive: true,
459
678
  mode: 448
460
679
  });
461
- chmodSync(archiveRoot(), 448);
680
+ await chmod(archiveRoot(), 448);
462
681
  const token = randomUUID();
463
682
  staging = join(archiveRoot(), `.incoming-${token}`);
464
683
  const attempt = tryAcquireClaim({
@@ -471,49 +690,56 @@ function writeArchive(root, removedBy, state) {
471
690
  releaseClaim(attempt.pending);
472
691
  throw new Error("archive staging claim is held");
473
692
  }
474
- withDirLock(archiveLock(), () => {
475
- mkdirSync(staging, { mode: 448 });
693
+ await withDirLockTransaction(archiveLock(), async () => {
694
+ operation?.signal.throwIfAborted();
695
+ let movedHistory = false;
696
+ const moveHistory = async (from, to) => {
697
+ if (!movedHistory) operation?.signal.throwIfAborted();
698
+ await rename(from, to);
699
+ movedHistory = true;
700
+ };
701
+ await mkdir(staging, { mode: 448 });
476
702
  let at = Date.now();
477
703
  const workspace = workspaceName(root);
478
704
  while (existsSync(archiveDir(`${workspace}--${at}`))) at++;
479
705
  const id = `${workspace}--${at}`;
480
706
  if (state) {
481
707
  const { supervisor: _supervisor, collectors: _collectors, warm: _warm, ...stable } = state;
482
- writeFileSync(join(staging, "state.json"), `${JSON.stringify(stripPids(stable))}\n`, { mode: 384 });
708
+ await writeFile(join(staging, "state.json"), `${JSON.stringify(stripPids(stable))}\n`, { mode: 384 });
483
709
  }
484
710
  for (const name of ["ended-agents.json", "log-error-index.json"]) {
485
711
  const data = readJsonObject(join(dir, name));
486
- if (data) writeFileSync(join(staging, name), `${JSON.stringify(stripPids(data))}\n`, { mode: 384 });
712
+ if (data) await writeFile(join(staging, name), `${JSON.stringify(stripPids(data))}\n`, { mode: 384 });
487
713
  }
488
- const move = (kind) => {
714
+ const move = async (kind) => {
489
715
  const from = join(dir, KIND_PATH[kind]);
490
716
  if (!limits[`${kind}.maxAgeDays`] || !existsSync(from)) return;
491
717
  const to = join(staging, KIND_PATH[kind]);
492
- mkdirSync(join(to, ".."), {
718
+ await mkdir(join(to, ".."), {
493
719
  recursive: true,
494
720
  mode: 448
495
721
  });
496
- renameSync(from, to);
497
- secureTree(to);
722
+ await moveHistory(from, to);
723
+ await secureTree(to);
498
724
  };
499
- move("logs");
500
- move("agentActions");
725
+ await move("logs");
726
+ await move("agentActions");
501
727
  const config = loadConfig();
502
- if (limits["recordings.maxAgeDays"] && workspaceRecordingEnabled(root, config?.projects?.[root] ?? {}, config, process.env)) {
728
+ if (limits["recordings.maxAgeDays"] && workspaceRecordingEnabled(root, config?.projects?.[root] ?? {}, config, env)) {
503
729
  const recordingDir = join(dir, "recordings");
504
- if (existsSync(recordingDir) && lstatSync(recordingDir).isSymbolicLink()) throw new Error(`refusing symlink ${recordingDir}`);
505
- for (const device of listRecordedDevices(recordingDir)) {
506
- if (lstatSync(device.dir).isSymbolicLink()) throw new Error(`refusing symlink ${device.dir}`);
730
+ if (existsSync(recordingDir) && (await lstat(recordingDir)).isSymbolicLink()) throw new Error(`refusing symlink ${recordingDir}`);
731
+ for (const device of await listRecordedDevicesAsync(recordingDir, false, movedHistory ? void 0 : operation?.signal)) {
732
+ if ((await lstat(device.dir)).isSymbolicLink()) throw new Error(`refusing symlink ${device.dir}`);
507
733
  const to = join(staging, "recordings", `${device.platform}-${device.slot}`);
508
734
  for (const segment of device.segments.filter((entry) => !entry.open)) {
509
- mkdirSync(to, {
735
+ await mkdir(to, {
510
736
  recursive: true,
511
737
  mode: 448
512
738
  });
513
739
  try {
514
- if (lstatSync(segment.file).isSymbolicLink()) throw new Error(`refusing symlink ${segment.file}`);
515
- renameSync(segment.file, join(to, basename(segment.file)));
516
- chmodSync(join(to, basename(segment.file)), 384);
740
+ if ((await lstat(segment.file)).isSymbolicLink()) throw new Error(`refusing symlink ${segment.file}`);
741
+ await moveHistory(segment.file, join(to, basename(segment.file)));
742
+ await chmod(join(to, basename(segment.file)), 384);
517
743
  } catch (error) {
518
744
  if (error.code !== "ENOENT") throw error;
519
745
  }
@@ -542,7 +768,7 @@ function writeArchive(root, removedBy, state) {
542
768
  record: new Date(at + limits.maxAgeDays * DAY).toISOString()
543
769
  };
544
770
  for (const kind of KINDS) {
545
- bytes[kind] = files(join(staging, KIND_PATH[kind])).reduce((sum, file) => sum + file.bytes, 0);
771
+ bytes[kind] = (await files(join(staging, KIND_PATH[kind]))).reduce((sum, file) => sum + file.bytes, 0);
546
772
  if (bytes[kind]) expires[kind] = new Date(at + limits[`${kind}.maxAgeDays`] * DAY).toISOString();
547
773
  }
548
774
  const record = {
@@ -564,102 +790,144 @@ function writeArchive(root, removedBy, state) {
564
790
  expires,
565
791
  version: 1
566
792
  };
567
- writeRecord(staging, record);
568
- renameSync(staging, archiveDir(id));
569
- }, { waitMs: 3e4 });
793
+ if (!movedHistory) operation?.signal.throwIfAborted();
794
+ await writeRecord(staging, record);
795
+ await rename(staging, archiveDir(id));
796
+ }, {
797
+ waitMs: 3e4,
798
+ signal: operation?.signal
799
+ });
570
800
  releaseClaim(claim);
571
801
  claim = void 0;
572
802
  staging = null;
573
803
  } catch (error) {
804
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
574
805
  if (staging) try {
575
- rmSync(staging, {
806
+ await rm(staging, {
576
807
  recursive: true,
577
808
  force: true
578
809
  });
579
810
  } catch {}
580
- console.error(`could not archive: ${String(error?.message ?? error).split("\n")[0]}`);
811
+ operation?.signal.throwIfAborted();
812
+ archiveWarning(`could not archive: ${String(error?.message ?? error).split("\n")[0]}`, operation);
581
813
  } finally {
582
814
  releaseClaim(claim);
583
815
  }
584
816
  }
585
- function removeKind(record, kind) {
586
- rmSync(join(archiveDir(record.id), KIND_PATH[kind]), {
817
+ async function removeKind(record, kind) {
818
+ await rm(join(archiveDir(record.id), KIND_PATH[kind]), {
587
819
  recursive: true,
588
820
  force: true
589
821
  });
590
822
  record.bytes[kind] = 0;
591
823
  record.expires[kind] = null;
592
- writeRecord(archiveDir(record.id), record);
824
+ await writeRecord(archiveDir(record.id), record);
593
825
  }
594
826
  function logRemovalOrder(path) {
595
827
  return /\.ndjson\.\d+$/.test(path) ? 0 : /^build-.*\.ndjson$/.test(basename(path)) ? 1 : 2;
596
828
  }
597
- function enforceArchiveRetention(now = Date.now()) {
829
+ async function enforceArchiveRetention(now = Date.now(), operation) {
830
+ const signal = operation?.signal;
831
+ const env = operation?.env ?? process.env;
832
+ signal?.throwIfAborted();
598
833
  if (!existsSync(archiveRoot())) return;
599
834
  let limits;
600
835
  try {
601
- limits = archiveSettings();
836
+ limits = archiveSettings(env);
602
837
  } catch (error) {
603
- console.error(`could not enforce archive retention: ${String(error.message).split("\n")[0]}`);
838
+ archiveWarning(`could not enforce archive retention: ${String(error.message).split("\n")[0]}`, operation);
604
839
  return;
605
840
  }
606
- withDirLock(archiveLock(), () => {
607
- const listed = readArchives().toReversed();
841
+ await withDirLockTransaction(archiveLock(), async () => {
842
+ const listed = (await readArchivesAsync(signal)).toReversed();
608
843
  const records = listed.filter((record, index) => limits.maxAgeDays > 0 && now - Date.parse(record.removedAt) < limits.maxAgeDays * DAY && index >= listed.length - limits.maxCount);
609
- for (const record of listed) if (!records.includes(record)) removeRecord(record.id);
610
- const recordings = recordingEnabled(process.env, [loadConfig()]);
844
+ for (const record of listed) {
845
+ signal?.throwIfAborted();
846
+ if (!records.includes(record)) await removeRecord(record.id);
847
+ }
848
+ const recordings = recordingEnabled(env, [loadConfig()]);
611
849
  for (const record of records) {
850
+ signal?.throwIfAborted();
612
851
  for (const kind of [
613
852
  "recordings",
614
853
  "agentActions",
615
854
  "logs"
616
855
  ]) {
856
+ signal?.throwIfAborted();
617
857
  const age = limits[`${kind}.maxAgeDays`];
618
- if (record.bytes[kind] && (!age || now - Date.parse(record.removedAt) >= age * DAY || kind === "recordings" && !recordings)) removeKind(record, kind);
858
+ if (record.bytes[kind] && (!age || now - Date.parse(record.removedAt) >= age * DAY || kind === "recordings" && !recordings)) await removeKind(record, kind);
619
859
  }
620
860
  const cap = limits["logs.maxMbPerWorkspace"] * 1024 ** 2;
621
861
  if (record.bytes.logs > cap) {
622
- const entries = files(join(archiveDir(record.id), "logs")).toSorted((a, b) => logRemovalOrder(a.path) - logRemovalOrder(b.path) || (logRemovalOrder(a.path) === 0 ? Number(b.path.split(".").at(-1)) - Number(a.path.split(".").at(-1)) : 0) || a.at - b.at);
623
- for (const file of entries) {
624
- if (record.bytes.logs <= cap) break;
625
- rmSync(file.path);
626
- record.bytes.logs = Math.max(0, record.bytes.logs - file.bytes);
862
+ const entries = (await files(join(archiveDir(record.id), "logs"), signal)).toSorted((a, b) => logRemovalOrder(a.path) - logRemovalOrder(b.path) || (logRemovalOrder(a.path) === 0 ? Number(b.path.split(".").at(-1)) - Number(a.path.split(".").at(-1)) : 0) || a.at - b.at);
863
+ let removed = false;
864
+ try {
865
+ for (const file of entries) {
866
+ signal?.throwIfAborted();
867
+ if (record.bytes.logs <= cap) break;
868
+ await rm(file.path);
869
+ record.bytes.logs = Math.max(0, record.bytes.logs - file.bytes);
870
+ removed = true;
871
+ }
872
+ } finally {
873
+ if (removed) {
874
+ if (!record.bytes.logs) record.expires.logs = null;
875
+ await writeRecord(archiveDir(record.id), record);
876
+ }
627
877
  }
628
- if (!record.bytes.logs) record.expires.logs = null;
629
- writeRecord(archiveDir(record.id), record);
630
878
  }
631
879
  }
632
880
  const totalCap = limits.maxTotalGb * 1024 ** 3;
633
881
  const recordingCap = limits["recordings.maxTotalGb"] * 1024 ** 3;
634
- for (const record of records) for (;;) {
635
- const usage = archivedUsage(records);
636
- const totalOver = usage.bytes > totalCap;
637
- const recordingsOver = usage.byKind.recordings > recordingCap;
638
- if (!totalOver && !recordingsOver) break;
639
- const candidates = KINDS.filter((kind) => record.bytes[kind] > 0 && (totalOver || kind === "recordings")).toSorted((a, b) => record.bytes[b] - record.bytes[a]);
640
- if (!candidates.length) break;
641
- removeKind(record, candidates[0]);
882
+ for (const record of records) {
883
+ signal?.throwIfAborted();
884
+ for (;;) {
885
+ signal?.throwIfAborted();
886
+ const usage = archivedUsage(records);
887
+ const totalOver = usage.bytes > totalCap;
888
+ const recordingsOver = usage.byKind.recordings > recordingCap;
889
+ if (!totalOver && !recordingsOver) break;
890
+ const candidates = KINDS.filter((kind) => record.bytes[kind] > 0 && (totalOver || kind === "recordings")).toSorted((a, b) => record.bytes[b] - record.bytes[a]);
891
+ if (!candidates.length) break;
892
+ await removeKind(record, candidates[0]);
893
+ }
642
894
  }
643
895
  for (const record of records) {
896
+ signal?.throwIfAborted();
644
897
  const before = JSON.stringify(record.expires);
645
898
  for (const kind of KINDS) record.expires[kind] = record.bytes[kind] ? new Date(Date.parse(record.removedAt) + limits[`${kind}.maxAgeDays`] * DAY).toISOString() : null;
646
899
  record.expires.record = new Date(Date.parse(record.removedAt) + limits.maxAgeDays * DAY).toISOString();
647
- if (JSON.stringify(record.expires) !== before) writeRecord(archiveDir(record.id), record);
900
+ if (JSON.stringify(record.expires) !== before) await writeRecord(archiveDir(record.id), record);
648
901
  }
649
- }, { waitMs: 3e4 });
902
+ }, {
903
+ waitMs: 3e4,
904
+ signal
905
+ });
650
906
  }
651
- function deleteSelectedArchives(ids, kind) {
652
- withDirLock(archiveLock(), () => {
653
- for (const record of readArchives().filter((entry) => ids.includes(entry.id))) if (kind) removeKind(record, kind);
654
- else removeRecord(record.id);
655
- }, { waitMs: 3e4 });
907
+ async function deleteSelectedArchives(ids, kind, signal) {
908
+ signal?.throwIfAborted();
909
+ await withDirLockTransaction(archiveLock(), async () => {
910
+ for (const record of (await readArchivesAsync(signal)).filter((entry) => ids.includes(entry.id))) {
911
+ signal?.throwIfAborted();
912
+ if (kind) await removeKind(record, kind);
913
+ else await removeRecord(record.id);
914
+ }
915
+ }, {
916
+ waitMs: 3e4,
917
+ signal
918
+ });
656
919
  }
657
920
  //#endregion
658
921
  //#region src/devices/reclaim.ts
659
- async function reapCollectors(root, { verify = verifyCollectorOwnership, collectors }) {
922
+ function throwIfCleanupInterrupted(error, operation) {
923
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
924
+ operation?.signal.throwIfAborted();
925
+ }
926
+ async function reapCollectors(root, { verify = verifyCollectorOwnership, collectors, operation }) {
660
927
  const skippedDevices = [];
661
928
  const failedDevices = [];
662
929
  for (const [platform, record] of Object.entries(collectors)) {
930
+ operation?.signal.throwIfAborted();
663
931
  const rec = record;
664
932
  const pid = rec?.pid;
665
933
  if (typeof pid !== "number" || pid <= 0 || pid === process.pid || !pidExists(pid)) continue;
@@ -683,7 +951,7 @@ async function reapCollectors(root, { verify = verifyCollectorOwnership, collect
683
951
  }
684
952
  try {
685
953
  process.kill(pid, "SIGTERM");
686
- if (await (0, process_identity_exports.waitForProcessExit)(rec, 5e3)) continue;
954
+ if (await (0, process_identity_exports.waitForProcessExit)(rec, 5e3, operation?.signal)) continue;
687
955
  } catch (error) {
688
956
  if (error.code === "ESRCH") continue;
689
957
  }
@@ -700,7 +968,7 @@ async function reapCollectors(root, { verify = verifyCollectorOwnership, collect
700
968
  failedDevices
701
969
  };
702
970
  }
703
- function reclaimRemoteSession(root, { stopSession = defaultStopSession } = {}) {
971
+ async function reclaimRemoteSession(root, { stopSession = defaultStopSession, operation } = {}) {
704
972
  const sessionId = readRemoteSessionId(root);
705
973
  if (!sessionId) return {
706
974
  stopped: null,
@@ -708,8 +976,10 @@ function reclaimRemoteSession(root, { stopSession = defaultStopSession } = {}) {
708
976
  };
709
977
  let result;
710
978
  try {
711
- result = stopSession(root, sessionId);
979
+ result = await stopSession(root, sessionId, operation);
980
+ operation?.signal.throwIfAborted();
712
981
  } catch (err) {
982
+ throwIfCleanupInterrupted(err, operation);
713
983
  result = {
714
984
  status: "failed",
715
985
  reason: String(err?.message ?? err)
@@ -739,17 +1009,18 @@ function reclaimRemoteSession(root, { stopSession = defaultStopSession } = {}) {
739
1009
  }
740
1010
  };
741
1011
  }
742
- function defaultStopSession(root, sessionId) {
1012
+ function defaultStopSession(root, sessionId, operation) {
743
1013
  return endRecordedSession({
744
1014
  root,
745
1015
  sessionId,
746
- easBin: resolveEasCliBin(root)?.file ?? null
1016
+ easBin: resolveEasCliBin(root)?.file ?? null,
1017
+ operation
747
1018
  });
748
1019
  }
749
- function defaultStopMetroTunnel(record) {
750
- return stopTunnel(record);
1020
+ function defaultStopMetroTunnel(record, operation) {
1021
+ return stopTunnel(record, { operation });
751
1022
  }
752
- async function reclaimMetroTunnel(root, { stopMetroTunnel = defaultStopMetroTunnel } = {}) {
1023
+ async function reclaimMetroTunnel(root, { stopMetroTunnel = defaultStopMetroTunnel, operation } = {}) {
753
1024
  const record = readMetroTunnel(root);
754
1025
  if (record?.kind !== "managed") return {
755
1026
  stopped: null,
@@ -757,8 +1028,10 @@ async function reclaimMetroTunnel(root, { stopMetroTunnel = defaultStopMetroTunn
757
1028
  };
758
1029
  let result;
759
1030
  try {
760
- result = await stopMetroTunnel(record);
1031
+ result = await stopMetroTunnel(record, operation);
1032
+ operation?.signal.throwIfAborted();
761
1033
  } catch (err) {
1034
+ throwIfCleanupInterrupted(err, operation);
762
1035
  result = {
763
1036
  status: "failed",
764
1037
  reason: String(err?.message ?? err)
@@ -814,7 +1087,7 @@ function parkRequest(project, projectPath, slot, simslimManaged) {
814
1087
  cacheKey: lastBuildCacheKey(projectPath, "ios")
815
1088
  };
816
1089
  }
817
- function reclaimOwnedDevices(project, projectPath, { park = false } = {}) {
1090
+ async function reclaimOwnedDevices(project, projectPath, { park = false, operation } = {}) {
818
1091
  const deletedDevices = [];
819
1092
  const parkedDevices = [];
820
1093
  const evictedDevices = [];
@@ -822,11 +1095,13 @@ function reclaimOwnedDevices(project, projectPath, { park = false } = {}) {
822
1095
  const skippedDevices = [];
823
1096
  const failedDevices = [];
824
1097
  for (const { slot, platforms } of projectDeviceSlots(project)) {
1098
+ operation?.signal.throwIfAborted();
825
1099
  const ios = platforms.ios;
826
1100
  if (ios?.owned && ios.deviceUdid) {
827
1101
  const udid = ios.deviceUdid;
828
1102
  const label = ios.deviceName || udid;
829
- const r = teardownOwnedIosSim(udid, {
1103
+ const r = await teardownOwnedIosSim(udid, {
1104
+ operation,
830
1105
  del: true,
831
1106
  label,
832
1107
  ...park ? { park: parkRequest(project, projectPath, slot, Boolean(ios.simslimManaged)) } : {}
@@ -856,9 +1131,11 @@ function reclaimOwnedDevices(project, projectPath, { park = false } = {}) {
856
1131
  }
857
1132
  }
858
1133
  const android = platforms.android;
1134
+ operation?.signal.throwIfAborted();
859
1135
  if (android?.owned && android.avdName) {
860
1136
  const bound = parkedMaxSetting("android");
861
- const r = teardownOwnedAvd(android.avdName, {
1137
+ const r = await teardownOwnedAvd(android.avdName, {
1138
+ operation,
862
1139
  del: true,
863
1140
  owner: {
864
1141
  projectPath,
@@ -909,6 +1186,7 @@ async function reclaimProject(path, options = {}) {
909
1186
  const dir = workspaceDir(path);
910
1187
  const existed = existsSync(dir);
911
1188
  const run = await withIdleWorkspace(path, () => reclaimIdleProject(path, existed, options), {
1189
+ operation: options.operation,
912
1190
  purpose: "workspace removal",
913
1191
  hosted: false,
914
1192
  supervisor: false,
@@ -939,10 +1217,52 @@ async function reclaimProject(path, options = {}) {
939
1217
  failedWorkspaceDirs: []
940
1218
  };
941
1219
  }
942
- async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwnedDevices = false, parkOwnedDevices = false, preserveProjectRecord = false, stopSession = defaultStopSession, stopMetroTunnel = defaultStopMetroTunnel, releaseLeases = releaseWorkspaceLeases, verifyCollector = verifyCollectorOwnership, teardownBrowser = teardownOwnedBrowser, stopRequester = { by: "stim gc --delete" } }) {
943
- await clearNamedPorts(path);
1220
+ function verifyAvdClaims(project, operation) {
1221
+ for (const { platforms } of projectDeviceSlots(project)) {
1222
+ operation?.signal.throwIfAborted();
1223
+ if (platforms.android?.owned && platforms.android.avdName) (0, ownership_claim_exports.releaseClaim)(acquireAvdClaim(platforms.android.avdName));
1224
+ }
1225
+ }
1226
+ async function removeWorkspaceOutput(path, dir, archive, state, operation) {
1227
+ const stateDir = workspaceAgentDeviceDir(path);
1228
+ if (!["none", "dead"].includes(agentDeviceDaemon(stateDir))) {
1229
+ const exec = getExecutor();
1230
+ try {
1231
+ if (!exec.findExecutable("agent-device")) throw new Error("agent-device is not on PATH");
1232
+ await runOperationFile(operation, exec, "agent-device", [
1233
+ "daemon",
1234
+ "stop",
1235
+ "--state-dir",
1236
+ stateDir,
1237
+ "--clean"
1238
+ ], {
1239
+ cwd: operation?.cwd,
1240
+ timeoutMs: 2e4,
1241
+ killSignal: "SIGKILL",
1242
+ env: { AGENT_DEVICE_NO_UPDATE_NOTIFIER: "1" }
1243
+ });
1244
+ } catch (error) {
1245
+ throwIfCleanupInterrupted(error, operation);
1246
+ const message = `${phaseLine("agent-device", `daemon could not be stopped: ${error.message}; run agent-device daemon stop --state-dir "${stateDir}" --clean`)}\n`;
1247
+ if (operation) operation.output("stderr", message);
1248
+ else process.stderr.write(message);
1249
+ throw error;
1250
+ }
1251
+ }
1252
+ operation?.signal.throwIfAborted();
1253
+ if (archive) await archiveWorkspace(path, archive.removedBy, state, operation);
1254
+ await emptyWorkspaceDir(dir, operation?.signal);
1255
+ }
1256
+ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwnedDevices = false, parkOwnedDevices = false, preserveProjectRecord = false, stopSession = defaultStopSession, stopMetroTunnel = defaultStopMetroTunnel, releaseLeases = releaseWorkspaceLeases, verifyCollector = verifyCollectorOwnership, teardownBrowser = teardownOwnedBrowser, stopRequester = { by: "stim gc --delete" }, operation }) {
1257
+ const signal = operation?.signal;
1258
+ signal?.throwIfAborted();
1259
+ await clearNamedPorts(path, operation ? {
1260
+ signal,
1261
+ log: (line) => operation.output("stderr", `${line}\n`)
1262
+ } : void 0);
1263
+ signal?.throwIfAborted();
944
1264
  const project = getProject(path);
945
- for (const { platforms } of projectDeviceSlots(project)) if (platforms.android?.owned && platforms.android.avdName) (0, ownership_claim_exports.releaseClaim)(acquireAvdClaim(platforms.android.avdName));
1265
+ verifyAvdClaims(project, operation);
946
1266
  const dereferenced = describeDereferenced(project);
947
1267
  const initialState = readWorkspaceState$1(path);
948
1268
  let killedPid = null;
@@ -960,9 +1280,9 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
960
1280
  const orphanIdentity = supervisor.status === "stale" && typeof orphanServer.processToken === "string" ? (0, process_identity_exports.inspectProcessIdentity)(orphanServer) : null;
961
1281
  if (supervisor.status === "ours") {
962
1282
  requestDevServerStop(path, supervisor.processToken, stopRequester);
963
- const signalled = killMetroTree(supervisor.pid, supervisor.processToken);
1283
+ const signalled = await killMetroTreeAsync(supervisor.pid, supervisor.processToken, operation);
964
1284
  if (!signalled) withdrawDevServerStopRequest(path, supervisor.processToken);
965
- if (signalled && await (0, process_identity_exports.waitForProcessExit)(supervisor, 1e4)) killedPid = supervisor.pid;
1285
+ if (signalled && await (0, process_identity_exports.waitForProcessExit)(supervisor, 1e4, signal)) killedPid = supervisor.pid;
966
1286
  else {
967
1287
  skippedMetro = `could not confirm supervisor pid ${supervisor.pid} exited`;
968
1288
  supervisorHeld = true;
@@ -973,9 +1293,14 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
973
1293
  } else if (orphanIdentity === "same") {
974
1294
  let signalled = false;
975
1295
  try {
976
- signalled = signalProcessTree(orphanServer.pid, "SIGTERM", { group: initialState?.supervisor?.mode === MODE_COMMAND });
977
- } catch {}
978
- if (signalled ? await (0, process_identity_exports.waitForProcessExit)(orphanServer, 1e4) : ["gone", "different"].includes((0, process_identity_exports.inspectProcessIdentity)(orphanServer))) killedPid = orphanServer.pid;
1296
+ signalled = await signalProcessTreeAsync(orphanServer.pid, "SIGTERM", {
1297
+ group: initialState?.supervisor?.mode === MODE_COMMAND,
1298
+ operation
1299
+ });
1300
+ } catch (error) {
1301
+ throwIfCleanupInterrupted(error, operation);
1302
+ }
1303
+ if (signalled ? await (0, process_identity_exports.waitForProcessExit)(orphanServer, 1e4, signal) : ["gone", "different"].includes((0, process_identity_exports.inspectProcessIdentity)(orphanServer))) killedPid = orphanServer.pid;
979
1304
  else {
980
1305
  skippedMetro = `could not confirm dev server pid ${orphanServer.pid} left by the supervisor exited`;
981
1306
  supervisorHeld = true;
@@ -988,9 +1313,15 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
988
1313
  }
989
1314
  const { skippedDevices: skippedCollectors, failedDevices: failedCollectors } = await reapCollectors(path, {
990
1315
  verify: verifyCollector,
991
- collectors: initialState?.collectors ?? {}
1316
+ collectors: initialState?.collectors ?? {},
1317
+ operation
992
1318
  });
993
- const browser = await teardownBrowser(path, { deleteProfile: true });
1319
+ signal?.throwIfAborted();
1320
+ const browser = await teardownBrowser(path, {
1321
+ deleteProfile: true,
1322
+ operation
1323
+ });
1324
+ signal?.throwIfAborted();
994
1325
  if (browser.status === "failed" || browser.status === "skipped") {
995
1326
  const entry = {
996
1327
  name: "owned Chrome",
@@ -1008,7 +1339,10 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
1008
1339
  }
1009
1340
  }
1010
1341
  retainReplacementProcesses();
1011
- const { deletedDevices, parkedDevices, evictedDevices, poolNotes, skippedDevices, failedDevices } = deleteOwnedDevices && !supervisorHeld && failedCollectors.length === 0 ? reclaimOwnedDevices(project, path, { park: parkOwnedDevices }) : {
1342
+ const { deletedDevices, parkedDevices, evictedDevices, poolNotes, skippedDevices, failedDevices } = deleteOwnedDevices && !supervisorHeld && failedCollectors.length === 0 ? await reclaimOwnedDevices(project, path, {
1343
+ park: parkOwnedDevices,
1344
+ operation
1345
+ }) : {
1012
1346
  deletedDevices: [],
1013
1347
  parkedDevices: [],
1014
1348
  evictedDevices: [],
@@ -1018,27 +1352,37 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
1018
1352
  };
1019
1353
  skippedDevices.push(...skippedCollectors);
1020
1354
  failedDevices.push(...failedCollectors);
1021
- for (const platform of ["ios", "android"]) try {
1022
- await (platform === "ios" ? stopHostedIos : stopHostedAndroid)(path);
1023
- } catch (error) {
1024
- const failed = {
1025
- platform,
1026
- name: `hosted ${platform} device`,
1027
- reason: error.message
1028
- };
1029
- failedDevices.push(failed);
1030
- skippedDevices.push(failed);
1355
+ for (const platform of ["ios", "android"]) {
1356
+ signal?.throwIfAborted();
1357
+ try {
1358
+ await (platform === "ios" ? stopHostedIos : stopHostedAndroid)(path, void 0, signal);
1359
+ } catch (error) {
1360
+ throwIfCleanupInterrupted(error, operation);
1361
+ const failed = {
1362
+ platform,
1363
+ name: `hosted ${platform} device`,
1364
+ reason: error.message
1365
+ };
1366
+ failedDevices.push(failed);
1367
+ skippedDevices.push(failed);
1368
+ }
1031
1369
  }
1032
- const remote = reclaimRemoteSession(path, { stopSession });
1370
+ signal?.throwIfAborted();
1371
+ const remote = await reclaimRemoteSession(path, {
1372
+ stopSession,
1373
+ operation
1374
+ });
1033
1375
  if (remote.failed) {
1034
1376
  skippedDevices.push(remote.failed);
1035
1377
  failedDevices.push(remote.failed);
1036
1378
  }
1037
1379
  if (macosAppPresent(path)) {
1380
+ signal?.throwIfAborted();
1038
1381
  const hosted = readMacosRecord(path)?.host;
1039
1382
  try {
1040
- await stopMacosApp(path);
1383
+ await stopMacosApp(path, operation);
1041
1384
  } catch (error) {
1385
+ throwIfCleanupInterrupted(error, operation);
1042
1386
  const failed = {
1043
1387
  name: hosted ? `hosted macOS session ${hosted.session} on ${hosted.machine}` : "macOS app",
1044
1388
  reason: String(error?.message ?? error)
@@ -1047,52 +1391,39 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
1047
1391
  failedDevices.push(failed);
1048
1392
  }
1049
1393
  }
1050
- const tunnel = await reclaimMetroTunnel(path, { stopMetroTunnel });
1394
+ signal?.throwIfAborted();
1395
+ const tunnel = await reclaimMetroTunnel(path, {
1396
+ stopMetroTunnel,
1397
+ operation
1398
+ });
1051
1399
  if (tunnel.failed) {
1052
1400
  skippedDevices.push(tunnel.failed);
1053
1401
  failedDevices.push(tunnel.failed);
1054
1402
  }
1055
1403
  let releasedLeases = [];
1404
+ signal?.throwIfAborted();
1056
1405
  try {
1057
1406
  releasedLeases = releaseLeases(path);
1058
- } catch {
1407
+ } catch (error) {
1408
+ throwIfCleanupInterrupted(error, operation);
1059
1409
  releasedLeases = [];
1060
1410
  }
1061
1411
  retainReplacementProcesses();
1062
1412
  const removedWorkspaceDirs = [];
1063
1413
  const failedWorkspaceDirs = [];
1064
1414
  if (failedDevices.length === 0 && !supervisorHeld) {
1415
+ signal?.throwIfAborted();
1065
1416
  const dir = workspaceDir(path);
1066
1417
  if (existsSync(dir)) try {
1067
- const stateDir = workspaceAgentDeviceDir(path);
1068
- if (!["none", "dead"].includes(agentDeviceDaemon(stateDir))) {
1069
- const exec = getExecutor();
1070
- try {
1071
- if (!exec.findExecutable("agent-device")) throw new Error("agent-device is not on PATH");
1072
- exec.runFile("agent-device", [
1073
- "daemon",
1074
- "stop",
1075
- "--state-dir",
1076
- stateDir,
1077
- "--clean"
1078
- ], {
1079
- timeoutMs: 2e4,
1080
- killSignal: "SIGKILL",
1081
- env: { AGENT_DEVICE_NO_UPDATE_NOTIFIER: "1" }
1082
- });
1083
- } catch (error) {
1084
- process.stderr.write(`${phaseLine("agent-device", `daemon could not be stopped: ${error.message}; run agent-device daemon stop --state-dir "${stateDir}" --clean`)}\n`);
1085
- throw error;
1086
- }
1087
- }
1088
- if (archive) archiveWorkspace(path, archive.removedBy, initialState);
1089
- emptyWorkspaceDir(dir);
1418
+ await removeWorkspaceOutput(path, dir, archive, initialState, operation);
1090
1419
  if (workspaceExisted) removedWorkspaceDirs.push(dir);
1091
- } catch {
1420
+ } catch (error) {
1421
+ throwIfCleanupInterrupted(error, operation);
1092
1422
  failedWorkspaceDirs.push(dir);
1093
1423
  }
1094
1424
  }
1095
1425
  const keptEntry = supervisorHeld || failedDevices.length > 0 || failedWorkspaceDirs.length > 0;
1426
+ signal?.throwIfAborted();
1096
1427
  if (project && !keptEntry && !preserveProjectRecord) removeProject(path);
1097
1428
  return {
1098
1429
  path,
@@ -1117,11 +1448,14 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
1117
1448
  //#endregion
1118
1449
  //#region src/workspace/service-guard.ts
1119
1450
  const SERVICE_IN_USE = { code: "STIM_WORKTREE_SERVICE" };
1451
+ function inside(folder, canonical) {
1452
+ return (value) => typeof value === "string" && value.startsWith("/") && isPathPrefix(folder, canonical(value));
1453
+ }
1120
1454
  function serviceUseOf(plist, file, folder, canonical) {
1121
1455
  if (typeof plist !== "object" || plist === null) return null;
1122
1456
  const job = plist;
1123
1457
  if (typeof job.StimService !== "object" || job.StimService === null || !Array.isArray(job.ProgramArguments)) return null;
1124
- const argument = job.ProgramArguments.find((value) => typeof value === "string" && value.startsWith("/") && isPathPrefix(folder, canonical(value)));
1458
+ const argument = job.ProgramArguments.find(inside(folder, canonical));
1125
1459
  if (argument === void 0) return null;
1126
1460
  return {
1127
1461
  label: typeof job.Label === "string" ? job.Label : file,
@@ -1129,9 +1463,61 @@ function serviceUseOf(plist, file, folder, canonical) {
1129
1463
  argument
1130
1464
  };
1131
1465
  }
1132
- /** The first installed stim-server service that runs from inside `folder`, or null. macOS only. */
1133
- function serviceRunningFrom(folder, canonical, agentsDir = join(homedir(), "Library", "LaunchAgents")) {
1134
- if (process.platform !== "darwin") return null;
1466
+ /** The launch argument inside `folder` of a unit `stim-server service install` wrote, or null when it has none. */
1467
+ function unitArgument(text, folder, canonical) {
1468
+ const lines = unitLines(text);
1469
+ if (!lines.some((line) => line.section === "X-Stim" && line.key === "Managed" && line.value === "true")) return null;
1470
+ const exec = lines.findLast((line) => line.section === "Service" && line.key === "ExecStart")?.value;
1471
+ const args = exec === void 0 ? null : unitWords(exec, true);
1472
+ if (!args) return void 0;
1473
+ return args.find(inside(folder, canonical)) ?? null;
1474
+ }
1475
+ function unitRunningFrom(folder, canonical, unitsDir) {
1476
+ let names;
1477
+ try {
1478
+ names = readdirSync(unitsDir).filter((name) => name.endsWith(".service"));
1479
+ } catch (error) {
1480
+ return error instanceof Error && "code" in error && error.code === "ENOENT" ? null : {
1481
+ label: "unknown",
1482
+ plist: unitsDir,
1483
+ argument: null
1484
+ };
1485
+ }
1486
+ for (const name of names) {
1487
+ const file = join(unitsDir, name);
1488
+ const label = name.replace(/\.service$/, "");
1489
+ let text;
1490
+ try {
1491
+ text = readFileSync(file, "utf8");
1492
+ } catch {
1493
+ return {
1494
+ label,
1495
+ plist: file,
1496
+ argument: null
1497
+ };
1498
+ }
1499
+ if (!text.includes("[X-Stim]")) continue;
1500
+ const argument = unitArgument(text, folder, canonical);
1501
+ if (argument === void 0) return {
1502
+ label,
1503
+ plist: file,
1504
+ argument: null
1505
+ };
1506
+ if (argument) return {
1507
+ label,
1508
+ plist: file,
1509
+ argument
1510
+ };
1511
+ }
1512
+ return null;
1513
+ }
1514
+ /**
1515
+ * The first installed stim-server service that runs from inside `folder`, or null: a LaunchAgent on macOS, a systemd
1516
+ * user unit on Linux. A service file that cannot be read counts as one, with a null `argument`.
1517
+ */
1518
+ function serviceRunningFrom(folder, canonical, agentsDir = join(homedir(), "Library", "LaunchAgents"), unitsDir = join(homedir(), ".config", "systemd", "user"), platform = process.platform) {
1519
+ if (platform === "linux") return unitRunningFrom(folder, canonical, unitsDir);
1520
+ if (platform !== "darwin") return null;
1135
1521
  let names;
1136
1522
  try {
1137
1523
  names = readdirSync(agentsDir).filter((name) => name.endsWith(".plist"));
@@ -1206,7 +1592,7 @@ function warmCommand(phase) {
1206
1592
  function asHolder(holder) {
1207
1593
  return {
1208
1594
  pid: holder.owner.pid,
1209
- phase: holder.mode === "exclusive" ? "refresh" : "copy"
1595
+ phase: holder.details.phase === "copy" ? "copy" : holder.mode === "exclusive" ? "refresh" : "copy"
1210
1596
  };
1211
1597
  }
1212
1598
  function waitingLine(holder, elapsedMs) {
@@ -1292,7 +1678,7 @@ function timeoutError(holder, elapsedMs, root) {
1292
1678
  error.code = "STIM_LOCK_TIMEOUT";
1293
1679
  return error;
1294
1680
  }
1295
- async function acquireWarmClaim({ repositoryRoot, phase, mode = phase === "refresh" ? "exclusive" : "shared", now = Date.now, sleep = defaultSleep, pollMs = WARM_CLAIM_POLL_MS, progressMs = WARM_CLAIM_PROGRESS_MS, ceilingMs = WARM_CLAIM_CEILING_MS, out = () => {} }) {
1681
+ async function acquireWarmClaim({ signal, declareSpawnsFor, repositoryRoot, phase, mode = phase === "refresh" ? "exclusive" : "shared", now = Date.now, sleep = (ms) => setTimeout$1(ms, void 0, { signal }), pollMs = WARM_CLAIM_POLL_MS, progressMs = WARM_CLAIM_PROGRESS_MS, ceilingMs = WARM_CLAIM_CEILING_MS, out = () => {} }) {
1296
1682
  const root = warmClaimPath(repositoryRoot);
1297
1683
  const started = now();
1298
1684
  let lastProgress = started;
@@ -1300,26 +1686,31 @@ async function acquireWarmClaim({ repositoryRoot, phase, mode = phase === "refre
1300
1686
  let pending = null;
1301
1687
  try {
1302
1688
  for (;;) {
1689
+ signal?.throwIfAborted();
1303
1690
  const attempt = pending ? (0, ownership_claim_exports.settleClaim)(pending) : (0, ownership_claim_exports.tryAcquireClaim)({
1304
1691
  root,
1305
1692
  mode,
1306
- label: WARM_CLAIM_LABEL
1693
+ label: WARM_CLAIM_LABEL,
1694
+ details: { phase }
1307
1695
  });
1308
1696
  const acquired = attempt.acquired;
1309
1697
  if (acquired) {
1698
+ if (declareSpawnsFor) declareSpawnsOn(declareSpawnsFor, acquired);
1310
1699
  const installer = installerClaim(acquired, {
1311
1700
  now,
1312
1701
  progressMs,
1313
1702
  out
1314
1703
  });
1315
1704
  return {
1705
+ claim: acquired,
1316
1706
  wait: {
1317
1707
  waitedMs: now() - started,
1318
1708
  holder: waitedOn
1319
1709
  },
1320
1710
  installer,
1321
- release: () => {
1322
- if (installer.holding()) return;
1711
+ release: (retained = false) => {
1712
+ if (declareSpawnsFor) stopDeclaringSpawnsOn(declareSpawnsFor, acquired);
1713
+ if (retained || installer.holding()) return;
1323
1714
  (0, ownership_claim_exports.releaseClaim)(acquired);
1324
1715
  }
1325
1716
  };
@@ -1348,10 +1739,14 @@ async function acquireWarmClaim({ repositoryRoot, phase, mode = phase === "refre
1348
1739
  }
1349
1740
  async function withWarmClaim(options, fn) {
1350
1741
  const { release, ...hold } = await acquireWarmClaim(options);
1742
+ let retained = false;
1351
1743
  try {
1352
1744
  return await fn(hold);
1745
+ } catch (cause) {
1746
+ retained = cause instanceof WorkspaceProcessCleanupError && cause.claims.includes(hold.claim);
1747
+ throw cause;
1353
1748
  } finally {
1354
- release();
1749
+ release(retained);
1355
1750
  }
1356
1751
  }
1357
1752
  //#endregion
@@ -1880,7 +2275,7 @@ function dependencyInstallCommand(target, dir = ".") {
1880
2275
  else if (existsSync$1(resolve$1(installRoot, "package-lock.json"))) command = "npm ci";
1881
2276
  return `cd '${installRoot.replaceAll("'", "'\\''")}' && ${command}`;
1882
2277
  }
1883
- function relocateCarriedPods(root, target, copied) {
2278
+ function relocateCarriedPods(root, target, copied, out) {
1884
2279
  const canRelocate = depsOutOfSync(root, target, copied).length === 0;
1885
2280
  for (const rel of copied) {
1886
2281
  if (rel !== "Pods" && !rel.endsWith("/Pods")) continue;
@@ -1893,41 +2288,40 @@ function relocateCarriedPods(root, target, copied) {
1893
2288
  targetRoot: target,
1894
2289
  canRelocate
1895
2290
  });
1896
- if (result.ok) console.error(chalk.dim(phaseLine("carry", result.pods.length > 0 ? `moved ${rel} to this checkout's path; ${result.pods.join(", ")} checksum depends on the checkout path, so pod install is not needed` : `moved ${rel} to this checkout's path; the locks match, so pod install is not needed`)));
1897
- else if (result.withheld) console.error(chalk.yellow(phaseLine("carry", `${rel} names the source checkout's path; removed ${rel}/Manifest.lock so pod install runs`)));
2291
+ if (result.ok) out(chalk.dim(phaseLine("carry", result.pods.length > 0 ? `moved ${rel} to this checkout's path; ${result.pods.join(", ")} checksum depends on the checkout path, so pod install is not needed` : `moved ${rel} to this checkout's path; the locks match, so pod install is not needed`)));
2292
+ else if (result.withheld) out(chalk.yellow(phaseLine("carry", `${rel} names the source checkout's path; removed ${rel}/Manifest.lock so pod install runs`)));
1898
2293
  } catch (error) {
1899
- console.error(chalk.yellow(phaseLine("carry", `could not move ${rel} to this checkout's path: ${error.message}. pod install will run`)));
2294
+ out(chalk.yellow(phaseLine("carry", `could not move ${rel} to this checkout's path: ${error.message}. pod install will run`)));
1900
2295
  }
1901
2296
  }
1902
2297
  }
1903
- function reportCarriedStateHealth(root, target, copied) {
2298
+ function reportCarriedStateHealth(root, target, copied, out) {
1904
2299
  const outOfSync = depsOutOfSync(root, target, copied);
1905
2300
  for (const d of outOfSync.filter((dependency) => dependency.reason === "installed")) {
1906
2301
  const where = d.dir === "." ? d.lockfile : `${d.dir}/${d.lockfile}`;
1907
2302
  const modules = d.dir === "." ? "node_modules" : `${d.dir}/node_modules`;
1908
- console.error(chalk.yellow(phaseLine("carry", `carried ${modules} was installed from a different ${d.lockfile} than the ${where} on disk here. node_modules is gitignored and cloned from the source checkout; ${d.lockfile} is tracked, so the two can disagree. Run \`${dependencyInstallCommand(target, d.dir)}\` before building, or pod install and native builds see the wrong package versions.`)));
2303
+ out(chalk.yellow(phaseLine("carry", `carried ${modules} was installed from a different ${d.lockfile} than the ${where} on disk here. node_modules is gitignored and cloned from the source checkout; ${d.lockfile} is tracked, so the two can disagree. Run \`${dependencyInstallCommand(target, d.dir)}\` before building, or pod install and native builds see the wrong package versions.`)));
1909
2304
  }
1910
2305
  const staleDeps = outOfSync.filter((dependency) => dependency.reason === "lockfile");
1911
2306
  if (staleDeps.length) {
1912
2307
  const manifests = staleDeps.map((d) => d.dir === "." ? d.lockfile : `${d.dir}/${d.lockfile}`).join(", ");
1913
2308
  const remedies = [...new Set(staleDeps.map((dependency) => dependencyInstallCommand(target, dependency.dir)))];
1914
- console.error(chalk.yellow(phaseLine("carry", `carried dependencies may be stale: they do not match ${manifests}. Run ${remedies.map((command) => `\`${command}\``).join(" and ")} before building.`)));
2309
+ out(chalk.yellow(phaseLine("carry", `carried dependencies may be stale: they do not match ${manifests}. Run ${remedies.map((command) => `\`${command}\``).join(" and ")} before building.`)));
1915
2310
  }
1916
2311
  for (const p of podsOutOfSync(target, copied)) {
1917
2312
  const where = p.dir === "." ? "Podfile.lock" : `${p.dir}/Podfile.lock`;
1918
2313
  const pod = podInstallCommand(p.dir === "." ? target : resolve$1(target, p.dir, ".."));
1919
- console.error(chalk.yellow(phaseLine("carry", p.reason === "missing" ? `carried ${p.dir === "." ? "Pods" : `${p.dir}/Pods`} but there is no ${where}. Run \`${pod}\` before building.` : `carried ${p.dir === "." ? "Pods" : `${p.dir}/Pods`} does not match the ${where} on disk here. Pods are gitignored and cloned; Podfile.lock is tracked, so the two can disagree. Run \`${pod}\` before building, or xcodebuild fails with "sandbox is not in sync" only after every pod has compiled.`)));
2314
+ out(chalk.yellow(phaseLine("carry", p.reason === "missing" ? `carried ${p.dir === "." ? "Pods" : `${p.dir}/Pods`} but there is no ${where}. Run \`${pod}\` before building.` : `carried ${p.dir === "." ? "Pods" : `${p.dir}/Pods`} does not match the ${where} on disk here. Pods are gitignored and cloned; Podfile.lock is tracked, so the two can disagree. Run \`${pod}\` before building, or xcodebuild fails with "sandbox is not in sync" only after every pod has compiled.`)));
1920
2315
  }
1921
2316
  }
1922
- function reportRefreshFailure(failure) {
1923
- console.error(chalk.red(failure.message));
1924
- for (const line of failure.lines) console.error(chalk.dim(` ${line}`));
1925
- if (failure.remedy) console.error(chalk.dim(failure.remedy));
1926
- console.error(chalk.red(`failed: ${failure.code}`));
1927
- process.exitCode = 1;
2317
+ function reportRefreshFailure(failure, out) {
2318
+ out(chalk.red(failure.message));
2319
+ for (const line of failure.lines) out(chalk.dim(` ${line}`));
2320
+ if (failure.remedy) out(chalk.dim(failure.remedy));
2321
+ out(chalk.red(`failed: ${failure.code}`));
1928
2322
  }
1929
- function mainCheckoutAppDir(root, target) {
1930
- const app = findProjectRoot(process.cwd());
2323
+ function mainCheckoutAppDir(root, target, cwd) {
2324
+ const app = findProjectRoot$1(cwd);
1931
2325
  if (!app) return root;
1932
2326
  const rel = relative$1(target, app);
1933
2327
  if (!rel || rel.startsWith("..") || isAbsolute$1(rel)) return root;
@@ -1939,7 +2333,7 @@ function mainCheckoutAppDir(root, target) {
1939
2333
  * root can declare react-native without being the app.
1940
2334
  */
1941
2335
  function warmedWorkspace(root, target, cwd) {
1942
- const nearest = findProjectRoot(cwd);
2336
+ const nearest = findProjectRoot$1(cwd);
1943
2337
  const app = nearest && isPathPrefix(target, nearest) && isMobileProject(nearest) ? nearest : null;
1944
2338
  if (app && app !== target) return app;
1945
2339
  const apps = /* @__PURE__ */ new Set();
@@ -1951,150 +2345,204 @@ function warmedWorkspace(root, target, cwd) {
1951
2345
  }
1952
2346
  return apps.size === 1 ? [...apps][0] : app;
1953
2347
  }
1954
- function registerWarmedWorkspace(workspace) {
2348
+ function registerWarmedWorkspace(workspace, out) {
1955
2349
  try {
1956
2350
  if (!getProject(workspace)) upsertProject(workspace, {});
1957
2351
  } catch (error) {
1958
- console.error(chalk.yellow(`Could not register ${workspace} for status: ${error?.message || String(error)}`));
2352
+ out(chalk.yellow(`Could not register ${workspace} for status: ${error?.message || String(error)}`));
1959
2353
  }
1960
2354
  }
1961
2355
  function registerWarm(worktree) {
1962
2356
  worktree.command("warm").description("Copy missing ignored paths from the source checkout into the current linked worktree.").option("--refresh", "Before copying, fast-forward the source checkout to its upstream and install what the new commits moved.").action(async (opts) => {
1963
- let progress = null;
1964
- let workspace = null;
1965
- try {
1966
- const { root, target, common } = warmWorktreePaths(process.cwd());
1967
- const app = findProjectRoot(process.cwd());
1968
- if (app) recordWorkspaceUse(app);
1969
- workspace = warmedWorkspace(root, target, process.cwd());
1970
- if (workspace && workspace !== app) recordWorkspaceUse(workspace);
1971
- if (!workspace) console.error(chalk.dim(phaseLine("project", "no single app to register here; run warm from the app directory to list it in status while it warms")));
1972
- else {
1973
- registerWarmedWorkspace(workspace);
1974
- progress = startWarmProgress(workspace, opts.refresh ? "refresh" : "copy", { note: (line) => console.error(chalk.yellow(line)) });
1975
- }
1976
- const readSettings = () => {
1977
- const settings = resolveSettings({
1978
- gitCommonDir: common,
1979
- repoRoot: root
1980
- });
1981
- const shapeErrors = settingShapeErrors(settings);
1982
- if (shapeErrors.length) {
1983
- for (const message of shapeErrors) console.error(chalk.red(message));
1984
- console.error(chalk.dim(SETTING_SHAPE_REMEDY));
1985
- return null;
1986
- }
1987
- for (const key of unknownSettingKeys(settings)) console.error(chalk.yellow(`Warning: setting "${key}" is not read by Stim and will be ignored.`));
1988
- return settings;
1989
- };
1990
- const copy = (wait) => {
1991
- progress?.step("copy");
1992
- if (wait?.holder) console.error(warmClaimAcquiredLine(wait));
1993
- const incomplete = incompleteInstallRefusal(root, mainCheckoutAppDir(root, target));
1994
- if (incomplete) {
1995
- reportRefreshFailure(incomplete);
1996
- return;
1997
- }
1998
- const settings = readSettings();
1999
- if (!settings) {
2000
- process.exitCode = 1;
2001
- return;
2002
- }
2003
- const excluded = readWorktreeExclude(root);
2004
- const patterns = excluded?.length ? excluded : settings.worktree?.exclude || [];
2005
- const result = cloneIgnoredEntries({
2006
- root,
2007
- target,
2008
- patterns
2357
+ const { value, interrupted } = await runProcessOperation(process.cwd(), async (operation) => {
2358
+ try {
2359
+ return await warmWorktreeTarget({
2360
+ cwd: operation.cwd,
2361
+ refresh: opts.refresh,
2362
+ operation
2009
2363
  });
2010
- for (const entry of result.skipped) console.error(chalk.dim(phaseLine("carry", `kept ${entry.file} (${entry.reason})`)));
2011
- for (const entry of result.failed) console.error(chalk.yellow(phaseLine("carry", `could not copy ${entry.file}: ${entry.error}`)));
2012
- if (result.copied.length) {
2013
- console.error(chalk.dim(phaseLine("carry", `copied ${carriedFileList(result.copied)} from ${root}`)));
2014
- relocateCarriedPods(root, target, result.copied);
2015
- reportCarriedStateHealth(root, target, result.copied);
2364
+ } catch (error) {
2365
+ if (!operation.signal.aborted || error instanceof WorkspaceProcessCleanupError) {
2366
+ const code = error?.code;
2367
+ console.error(chalk.red(`Could not warm this worktree: ${error.message}`));
2368
+ const claim = (0, ownership_claim_exports.claimFailure)(error, `stim worktree warm${opts.refresh ? " --refresh" : ""}`);
2369
+ if (claim) console.error(chalk.dim(claim.remedy));
2370
+ if (typeof code === "string" && code.startsWith("STIM_")) console.error(chalk.red(`failed: ${code}`));
2016
2371
  }
2017
- console.error(phaseLine("carry", `${result.failed.length ? "incomplete" : "complete"}: ${result.copied.length} ignored entries copied, ${result.skipped.length} kept, ${result.failed.length} failed`));
2018
- if (result.failed.length) process.exitCode = 1;
2019
- };
2020
- if (opts.refresh) {
2021
- const inspect = await acquireWarmClaim({
2022
- repositoryRoot: root,
2023
- phase: "refresh",
2024
- mode: "shared",
2025
- out: console.error
2026
- });
2027
- let mutation;
2028
- try {
2372
+ return { exitCode: 1 };
2373
+ }
2374
+ });
2375
+ if (interrupted !== null) process.exitCode = interrupted;
2376
+ else if (value.exitCode) process.exitCode = 1;
2377
+ });
2378
+ }
2379
+ async function warmWorktreeTarget(opts) {
2380
+ let exitCode = 0;
2381
+ const operation = opts.operation;
2382
+ const out = (line) => operation.output("stderr", `${line}\n`);
2383
+ let progress = null;
2384
+ let workspace = null;
2385
+ try {
2386
+ const { root, target, common } = await warmWorktreePaths(opts.cwd, operation);
2387
+ operation.signal.throwIfAborted();
2388
+ const app = findProjectRoot$1(opts.cwd);
2389
+ workspace = warmedWorkspace(root, target, opts.cwd);
2390
+ const scope = /* @__PURE__ */ new Set([
2391
+ ...reclaimKeys(target),
2392
+ ...app ? [app] : [],
2393
+ ...workspace ? [workspace] : []
2394
+ ]);
2395
+ for (const key of scope) opts.beforeWarm?.(nativeCanonicalPath(key));
2396
+ if (app) recordWorkspaceUse(app, /* @__PURE__ */ new Date(), operation.env, opts.cwd);
2397
+ if (workspace && workspace !== app) recordWorkspaceUse(workspace, /* @__PURE__ */ new Date(), operation.env, opts.cwd);
2398
+ if (!workspace) out(chalk.dim(phaseLine("project", "no single app to register here; run warm from the app directory to list it in status while it warms")));
2399
+ else {
2400
+ registerWarmedWorkspace(workspace, out);
2401
+ progress = startWarmProgress(workspace, opts.refresh ? "refresh" : "copy", { note: (line) => out(chalk.yellow(line)) });
2402
+ }
2403
+ const readSettings = () => {
2404
+ const settings = resolveSettings({
2405
+ gitCommonDir: common,
2406
+ repoRoot: root
2407
+ });
2408
+ const shapeErrors = settingShapeErrors(settings);
2409
+ if (shapeErrors.length) {
2410
+ for (const message of shapeErrors) out(chalk.red(message));
2411
+ out(chalk.dim(SETTING_SHAPE_REMEDY));
2412
+ return null;
2413
+ }
2414
+ for (const key of unknownSettingKeys(settings)) out(chalk.yellow(`Warning: setting "${key}" is not read by Stim and will be ignored.`));
2415
+ return settings;
2416
+ };
2417
+ const copy = async (wait) => {
2418
+ const state = operationState(operation);
2419
+ const wasDraining = state.draining;
2420
+ state.draining = true;
2421
+ try {
2422
+ await withWarmClaim({
2423
+ repositoryRoot: target,
2424
+ phase: "copy",
2425
+ mode: "exclusive",
2426
+ out,
2427
+ signal: operation.signal,
2428
+ declareSpawnsFor: operation
2429
+ }, async (hold) => {
2430
+ if (hold.wait.holder) out(warmClaimAcquiredLine(hold.wait));
2431
+ operation.signal.throwIfAborted();
2432
+ progress?.step("copy");
2433
+ if (wait?.holder) out(warmClaimAcquiredLine(wait));
2434
+ const incomplete = incompleteInstallRefusal(root, mainCheckoutAppDir(root, target, opts.cwd));
2435
+ if (incomplete) {
2436
+ reportRefreshFailure(incomplete, out);
2437
+ exitCode = 1;
2438
+ return;
2439
+ }
2029
2440
  const settings = readSettings();
2030
2441
  if (!settings) {
2031
- process.exitCode = 1;
2442
+ exitCode = 1;
2032
2443
  return;
2033
2444
  }
2034
- const lines = [];
2035
- const result = await refreshMainCheckout({
2445
+ const excluded = readWorktreeExclude(root);
2446
+ const patterns = excluded?.length ? excluded : settings.worktree?.exclude || [];
2447
+ const result = await cloneIgnoredEntries({
2036
2448
  root,
2037
- appDir: mainCheckoutAppDir(root, target),
2038
- settings,
2039
- iosProject: appIosProjectDir(mainCheckoutAppDir(root, target)),
2040
- emit: (line) => lines.push(line),
2041
- inspectOnly: true
2449
+ target,
2450
+ patterns,
2451
+ operation
2042
2452
  });
2043
- if (!result || !("mutation" in result)) {
2044
- console.error(warmClaimAcquiredLine(inspect.wait, result ? "shared" : "shared (seed current)"));
2045
- for (const line of lines) console.error(line);
2046
- if (result) reportRefreshFailure(result);
2047
- else copy(null);
2048
- return;
2453
+ for (const entry of result.skipped) out(chalk.dim(phaseLine("carry", `kept ${entry.file} (${entry.reason})`)));
2454
+ for (const entry of result.failed) out(chalk.yellow(phaseLine("carry", `could not copy ${entry.file}: ${entry.error}`)));
2455
+ if (result.copied.length) {
2456
+ out(chalk.dim(phaseLine("carry", `copied ${carriedFileList(result.copied)} from ${root}`)));
2457
+ relocateCarriedPods(root, target, result.copied, out);
2458
+ reportCarriedStateHealth(root, target, result.copied, out);
2049
2459
  }
2050
- mutation = result.mutation;
2051
- } finally {
2052
- inspect.release();
2460
+ out(phaseLine("carry", `${result.failed.length ? "incomplete" : "complete"}: ${result.copied.length} ignored entries copied, ${result.skipped.length} kept, ${result.failed.length} failed`));
2461
+ if (result.failed.length) exitCode = 1;
2462
+ });
2463
+ } finally {
2464
+ state.draining = wasDraining;
2465
+ }
2466
+ };
2467
+ if (opts.refresh) {
2468
+ const mutation = await withWarmClaim({
2469
+ repositoryRoot: root,
2470
+ phase: "refresh",
2471
+ mode: "shared",
2472
+ signal: operation.signal,
2473
+ declareSpawnsFor: operation,
2474
+ out
2475
+ }, async (hold) => {
2476
+ const settings = readSettings();
2477
+ if (!settings) {
2478
+ exitCode = 1;
2479
+ return null;
2053
2480
  }
2054
- const failure = await withWarmClaim({
2055
- repositoryRoot: root,
2056
- phase: "refresh",
2057
- out: console.error
2058
- }, async (hold) => {
2059
- console.error(warmClaimAcquiredLine(hold.wait, `exclusive (${mutation})`));
2060
- const settings = readSettings();
2061
- if (!settings) {
2062
- process.exitCode = 1;
2063
- return null;
2064
- }
2065
- return refreshMainCheckout({
2066
- root,
2067
- appDir: mainCheckoutAppDir(root, target),
2068
- settings,
2069
- iosProject: appIosProjectDir(mainCheckoutAppDir(root, target)),
2070
- emit: (line) => console.error(line),
2071
- installer: hold.installer
2072
- });
2481
+ const lines = [];
2482
+ const result = await refreshMainCheckout({
2483
+ root,
2484
+ appDir: mainCheckoutAppDir(root, target, opts.cwd),
2485
+ settings,
2486
+ iosProject: appIosProjectDir(mainCheckoutAppDir(root, target, opts.cwd)),
2487
+ emit: (line) => lines.push(line),
2488
+ inspectOnly: true
2073
2489
  });
2074
- if (failure) {
2075
- reportRefreshFailure(failure);
2076
- return;
2490
+ if (!result || !("mutation" in result)) {
2491
+ out(warmClaimAcquiredLine(hold.wait, result ? "shared" : "shared (seed current)"));
2492
+ for (const line of lines) out(line);
2493
+ if (result) {
2494
+ reportRefreshFailure(result, out);
2495
+ exitCode = 1;
2496
+ } else await copy(null);
2497
+ return null;
2077
2498
  }
2078
- if (process.exitCode) return;
2079
- }
2080
- await withWarmClaim({
2499
+ return result.mutation;
2500
+ });
2501
+ if (mutation === null) return { exitCode };
2502
+ const failure = await withWarmClaim({
2081
2503
  repositoryRoot: root,
2082
- phase: "copy",
2083
- out: console.error
2084
- }, (hold) => copy(hold.wait));
2085
- } catch (error) {
2086
- const code = error?.code;
2087
- console.error(chalk.red(`Could not warm this worktree: ${error.message}`));
2088
- const claim = (0, ownership_claim_exports.claimFailure)(error, `stim worktree warm${opts.refresh ? " --refresh" : ""}`);
2089
- if (claim) console.error(chalk.dim(claim.remedy));
2090
- if (typeof code === "string" && code.startsWith("STIM_")) console.error(chalk.red(`failed: ${code}`));
2091
- process.exitCode = 1;
2092
- } finally {
2093
- progress?.finish(!process.exitCode);
2094
- const links = workspace && !process.exitCode ? workspaceLinks(workspace) : void 0;
2095
- if (links) console.error(chalk.dim(workspaceLinkLine(links)));
2504
+ phase: "refresh",
2505
+ out,
2506
+ signal: operation.signal
2507
+ }, async (hold) => {
2508
+ out(warmClaimAcquiredLine(hold.wait, `exclusive (${mutation})`));
2509
+ const settings = readSettings();
2510
+ if (!settings) {
2511
+ exitCode = 1;
2512
+ return null;
2513
+ }
2514
+ return refreshMainCheckout({
2515
+ root,
2516
+ appDir: mainCheckoutAppDir(root, target, opts.cwd),
2517
+ settings,
2518
+ iosProject: appIosProjectDir(mainCheckoutAppDir(root, target, opts.cwd)),
2519
+ emit: (line) => out(line),
2520
+ installer: hold.installer
2521
+ });
2522
+ });
2523
+ if (failure) {
2524
+ reportRefreshFailure(failure, out);
2525
+ exitCode = 1;
2526
+ return { exitCode };
2527
+ }
2528
+ if (exitCode) return { exitCode };
2096
2529
  }
2097
- });
2530
+ await withWarmClaim({
2531
+ repositoryRoot: root,
2532
+ phase: "copy",
2533
+ out,
2534
+ signal: operation.signal,
2535
+ declareSpawnsFor: operation
2536
+ }, (hold) => copy(hold.wait));
2537
+ } catch (error) {
2538
+ exitCode = 1;
2539
+ throw error;
2540
+ } finally {
2541
+ progress?.finish(!exitCode && !operation.signal.aborted);
2542
+ const links = workspace && !exitCode && !operation.signal.aborted ? await workspaceLinks(workspace) : void 0;
2543
+ if (links) out(chalk.dim(workspaceLinkLine(links)));
2544
+ }
2545
+ return { exitCode };
2098
2546
  }
2099
2547
  function carriedFileList(files) {
2100
2548
  const shown = files.slice(0, 3).join(", ");
@@ -2180,6 +2628,11 @@ function nativeCanonicalPath(path) {
2180
2628
  missing.unshift(basename$1(existing));
2181
2629
  }
2182
2630
  }
2631
+ /** Other worktrees git lists inside `path`, which removing `path` would delete with it. */
2632
+ function nestedWorktrees(entries, path) {
2633
+ const root = nativeCanonicalPath(path);
2634
+ return entries.filter((candidate) => !candidate.bare && nativeCanonicalPath(candidate.path) !== root).filter((candidate) => isPathPrefix(root, nativeCanonicalPath(candidate.path))).map((candidate) => candidate.path);
2635
+ }
2183
2636
  function matchWorktreeEntry(entries, path) {
2184
2637
  let best = null;
2185
2638
  const target = nativeCanonicalPath(path);
@@ -2239,12 +2692,16 @@ function reclaimKeys(rootPath) {
2239
2692
  }
2240
2693
  return [...keys].toSorted();
2241
2694
  }
2242
- async function withReclaimLocks(rootPath, fn) {
2695
+ async function withReclaimLocks(rootPath, fn, operation, beforeRemove) {
2243
2696
  const keys = reclaimKeys(rootPath);
2244
- const acquire = (index) => index === keys.length ? fn(keys) : withManagedTunnelRemovalLock(keys[index], () => acquire(index + 1));
2697
+ for (const key of keys) beforeRemove?.(nativeCanonicalPath(key));
2698
+ const acquire = (index) => index === keys.length ? fn(keys) : withManagedTunnelRemovalLock(keys[index], () => acquire(index + 1), {
2699
+ signal: operation?.signal,
2700
+ declareSpawnsFor: operation
2701
+ });
2245
2702
  return acquire(0);
2246
2703
  }
2247
- async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRootProject = false, removedBy } = {}) {
2704
+ async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRootProject = false, removedBy, operation } = {}) {
2248
2705
  const dereferenced = [];
2249
2706
  const killedPids = [];
2250
2707
  const deletedDevices = [];
@@ -2259,17 +2716,23 @@ async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRoot
2259
2716
  const releasedLeases = [];
2260
2717
  const removedWorkspaceDirs = [];
2261
2718
  const failedWorkspaceDirs = [];
2719
+ operation?.signal.throwIfAborted();
2720
+ const pressure = currentDiskLevel(operation ? await measureDiskAsync(operation) : void 0);
2262
2721
  for (const key of keys) {
2722
+ operation?.signal.throwIfAborted();
2263
2723
  let r;
2264
2724
  try {
2265
2725
  r = await reclaimProject(key, {
2266
2726
  archive: removedBy ? { removedBy } : void 0,
2267
2727
  deleteOwnedDevices: true,
2268
- parkOwnedDevices: true,
2728
+ parkOwnedDevices: pressure < 2,
2269
2729
  preserveProjectRecord: preserveRootProject && key === rootPath,
2270
- stopRequester: { by: "stim worktree remove" }
2730
+ stopRequester: { by: "stim worktree remove" },
2731
+ operation
2271
2732
  });
2272
2733
  } catch (error) {
2734
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
2735
+ operation?.signal.throwIfAborted();
2273
2736
  keptEntries.push(key);
2274
2737
  retainedResources.push({
2275
2738
  name: "ownership state",
@@ -2298,6 +2761,7 @@ async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRoot
2298
2761
  });
2299
2762
  }
2300
2763
  }
2764
+ if (pressure >= 2 && deletedDevices.length) poolNotes.push(`disk pressure level ${pressure}: deleted the owned devices instead of parking them`);
2301
2765
  for (const key of keys) {
2302
2766
  if (keptEntries.includes(key)) continue;
2303
2767
  const tunnel = readMetroTunnel(key);
@@ -2350,18 +2814,18 @@ function poolLines(result) {
2350
2814
  for (const note of result.poolNotes) lines.push(chalk.yellow(phaseLine("device", note)));
2351
2815
  return lines;
2352
2816
  }
2353
- function reportRetainedResources(root, result) {
2354
- console.error(chalk.red(`Refusing to remove ${root}: Stim could not release owned resources.`));
2817
+ function reportRetainedResources(root, result, output) {
2818
+ output.error(chalk.red(`Refusing to remove ${root}: Stim could not release owned resources.`));
2355
2819
  for (const resource of result.retainedResources) {
2356
- console.error(chalk.yellow(` - Stim still tracks ${describeKeptDevice(resource)} for ${resource.project}`));
2357
- console.error(chalk.dim(` ${resource.reason}`));
2820
+ output.error(chalk.yellow(` - Stim still tracks ${describeKeptDevice(resource)} for ${resource.project}`));
2821
+ output.error(chalk.dim(` ${resource.reason}`));
2358
2822
  }
2359
2823
  for (const kept of result.keptEntries) {
2360
2824
  if (result.retainedResources.some((resource) => resource.project === kept)) continue;
2361
- console.error(chalk.yellow(` - retained Stim ownership state for ${kept}`));
2825
+ output.error(chalk.yellow(` - retained Stim ownership state for ${kept}`));
2362
2826
  }
2363
- console.error(chalk.dim("Fix the reported cause, then run `stim worktree remove` again."));
2364
- process.exitCode = 1;
2827
+ output.error(chalk.dim("Fix the reported cause, then run `stim worktree remove` again."));
2828
+ output.fail();
2365
2829
  }
2366
2830
  function hasRegisteredProjectUnder(rootPath) {
2367
2831
  const cfg = loadConfig$1();
@@ -2371,28 +2835,40 @@ function projectAtOrAbove(root, path) {
2371
2835
  const here = nativeCanonicalPath(path);
2372
2836
  return Object.keys(loadConfig$1()?.projects ?? {}).filter((key) => isPathPrefix(nativeCanonicalPath(root), nativeCanonicalPath(key)) && isPathPrefix(nativeCanonicalPath(key), here)).toSorted((a, b) => b.length - a.length)[0] ?? null;
2373
2837
  }
2374
- async function reclaimEnvironment(root, why) {
2838
+ async function reclaimEnvironment(root, why, output, operation, beforeRemove) {
2375
2839
  await withManagedRemoteWorktreeRemovalLock(root, () => withReclaimLocks(root, async (lockedKeys) => {
2376
- const result = await reclaimAll(root, lockedKeys, { removedBy: "worktree-remove" });
2377
- for (const line of poolLines(result)) console.error(line);
2378
- for (const device of result.deletedDevices) console.error(chalk.dim(phaseLine("device", `deleted ${device}`)));
2379
- for (const session of result.stoppedSessions) console.error(chalk.dim(phaseLine("device", `stopped remote session ${session}`)));
2380
- for (const tunnel of result.stoppedTunnels) console.error(chalk.dim(phaseLine("lan", `stopped the ${tunnel} tunnel`)));
2381
- for (const lease of result.releasedLeases) console.error(chalk.dim(phaseLine("lease", releasedLeaseFact(lease))));
2382
- for (const pid of result.killedPids) console.error(chalk.dim(phaseLine("metro", `killed pid ${pid}`)));
2383
- for (const dir of result.removedWorkspaceDirs) console.error(chalk.dim(phaseLine("workspace", `removed ${dir}`)));
2384
- for (const dir of result.failedWorkspaceDirs) console.error(chalk.yellow(phaseLine("workspace", `could not remove ${dir}`)));
2385
- if (result.dereferenced.length) console.error(chalk.dim(phaseLine("workspace", `no longer referenced: ${result.dereferenced.join(", ")}`)));
2840
+ const result = await reclaimAll(root, lockedKeys, {
2841
+ removedBy: "worktree-remove",
2842
+ operation
2843
+ });
2844
+ for (const line of poolLines(result)) output.error(line);
2845
+ for (const device of result.deletedDevices) output.error(chalk.dim(phaseLine("device", `deleted ${device}`)));
2846
+ for (const session of result.stoppedSessions) output.error(chalk.dim(phaseLine("device", `stopped remote session ${session}`)));
2847
+ for (const tunnel of result.stoppedTunnels) output.error(chalk.dim(phaseLine("lan", `stopped the ${tunnel} tunnel`)));
2848
+ for (const lease of result.releasedLeases) output.error(chalk.dim(phaseLine("lease", releasedLeaseFact(lease))));
2849
+ for (const pid of result.killedPids) output.error(chalk.dim(phaseLine("metro", `killed pid ${pid}`)));
2850
+ for (const dir of result.removedWorkspaceDirs) output.error(chalk.dim(phaseLine("workspace", `removed ${dir}`)));
2851
+ for (const dir of result.failedWorkspaceDirs) output.error(chalk.yellow(phaseLine("workspace", `could not remove ${dir}`)));
2852
+ if (result.dereferenced.length) output.error(chalk.dim(phaseLine("workspace", `no longer referenced: ${result.dereferenced.join(", ")}`)));
2386
2853
  if (result.keptEntries.length) {
2387
- reportRetainedResources(root, result);
2854
+ reportRetainedResources(root, result, output);
2388
2855
  return;
2389
2856
  }
2390
- for (const s of result.skippedDevices) console.error(chalk.yellow(phaseLine("device", `kept ${describeKeptDevice(s)} (${s.reason})`)));
2391
- console.error(chalk.green(`Reclaimed the environment; the working tree stays (${why}).`));
2392
- }));
2857
+ for (const s of result.skippedDevices) output.error(chalk.yellow(phaseLine("device", `kept ${describeKeptDevice(s)} (${s.reason})`)));
2858
+ output.error(chalk.green(`Reclaimed the environment; the working tree stays (${why}).`));
2859
+ }, operation, beforeRemove), {
2860
+ signal: operation?.signal,
2861
+ declareSpawnsFor: operation
2862
+ });
2393
2863
  }
2394
- function removalPath(target) {
2395
- return canonicalExistingPath(target ?? process.cwd());
2864
+ const REMOVAL_OUTPUT = {
2865
+ error: (line) => console.error(line),
2866
+ fail: () => {
2867
+ process.exitCode = 1;
2868
+ }
2869
+ };
2870
+ function removalPath(target, cwd = process.cwd()) {
2871
+ return canonicalExistingPath(target ?? cwd);
2396
2872
  }
2397
2873
  function canonicalExistingPath(target) {
2398
2874
  const resolved = resolve$1(target);
@@ -2406,9 +2882,9 @@ function canonicalExistingPath(target) {
2406
2882
  }
2407
2883
  return resolve$1(realpathSync$1(existing), ...missing);
2408
2884
  }
2409
- function inspectRemoval(path, mergedHead) {
2885
+ async function inspectRemoval(path, mergedHead, operation) {
2410
2886
  try {
2411
- return inspectGit(path, mergedHead);
2887
+ return await inspectGit(path, mergedHead, operation);
2412
2888
  } catch (error) {
2413
2889
  if (!isTimeoutError(error)) throw error;
2414
2890
  return timedOutInspection(error);
@@ -2424,18 +2900,24 @@ function timedOutInspection(error) {
2424
2900
  timedOut: true
2425
2901
  };
2426
2902
  }
2427
- function inspectGit(path, mergedHead) {
2428
- const gitAnswered = hasUncommittedWork(path);
2429
- const allDirty = gitAnswered ? dirtyPaths(path, { limit: Infinity }) : [];
2903
+ async function inspectGit(path, mergedHead, operation) {
2904
+ const options = {
2905
+ removal: true,
2906
+ env: operation?.env,
2907
+ signal: operation?.signal,
2908
+ declareFor: operation
2909
+ };
2910
+ const gitAnswered = await hasUncommittedWorkAsync(path, options);
2911
+ const allDirty = gitAnswered ? await dirtyPathsAsync(path, { limit: Infinity }, options) : [];
2430
2912
  const { lines: withoutCookies, cookies } = excludeWatchmanCookies(allDirty ?? []);
2431
2913
  const { lines: dirtyLines, restore: podChurn } = excludePodChurn(withoutCookies, customIosDirs(path));
2432
2914
  const dirty = gitAnswered === null || allDirty === null ? null : dirtyLines.length > 0;
2433
- const unpushed = unpushedCommits(path);
2915
+ const unpushed = await unpushedCommitsAsync(path, options);
2434
2916
  const blockers = removalBlockers({
2435
2917
  dirty,
2436
- unpushed: Boolean(mergedHead) && resolveFullRef(path, "HEAD", { removal: true }) === mergedHead && unpushed ? [] : unpushed
2918
+ unpushed: Boolean(mergedHead) && await resolveFullRefAsync(path, "HEAD", options) === mergedHead && unpushed ? [] : unpushed
2437
2919
  });
2438
- const submodules = hasPopulatedSubmodules(path);
2920
+ const submodules = await hasPopulatedSubmodulesAsync(path, options);
2439
2921
  if (submodules === null) blockers.push("could not check for initialized submodules; re-run with --force to override");
2440
2922
  else if (submodules) blockers.push("initialized submodules, which git removes only with --force");
2441
2923
  return {
@@ -2446,43 +2928,50 @@ function inspectGit(path, mergedHead) {
2446
2928
  blockers
2447
2929
  };
2448
2930
  }
2449
- function printRemovalRefusal(path, inspection) {
2931
+ async function printRemovalRefusal(path, inspection, output, operation) {
2450
2932
  const { blockers, dirtyLines, unpushed } = inspection;
2451
- console.error(chalk.red(`Refusing to remove ${path}:`));
2452
- for (const blocker of blockers) console.error(chalk.red(` - ${blocker}`));
2933
+ output.error(chalk.red(`Refusing to remove ${path}:`));
2934
+ for (const blocker of blockers) output.error(chalk.red(` - ${blocker}`));
2453
2935
  if (inspection.timedOut) {
2454
- console.error(chalk.dim("Git did not answer in time, so Stim cannot tell whether the worktree holds unsaved work."));
2455
- console.error(chalk.dim(`Retry once git responds; check with: git -C ${path} status`));
2456
- process.exitCode = 1;
2936
+ output.error(chalk.dim("Git did not answer in time, so Stim cannot tell whether the worktree holds unsaved work."));
2937
+ output.error(chalk.dim(`Retry once git responds; check with: git -C ${path} status`));
2938
+ output.fail();
2457
2939
  return;
2458
2940
  }
2459
- if (unpushed && unpushed.length && !hasRemote(path)) console.error(chalk.dim(" (no remote is configured for this worktree, so every commit no other local branch reaches counts as unpushed)"));
2460
- for (const line of dirtyLines.slice(0, 10)) console.error(chalk.dim(` ${line}`));
2461
- if (dirtyLines.length) console.error(chalk.dim(` (git -C ${path} status -s for the full list)`));
2462
- for (const line of removalRemedy(dirtyLines, { worktree: path })) console.error(chalk.dim(line));
2463
- console.error(chalk.dim("Otherwise: commit or push the branch (only commits found nowhere else are counted,"));
2464
- console.error(chalk.dim("so pushing publishes nothing but this worktree's own work). --force is a last"));
2465
- console.error(chalk.dim("resort -- it discards uncommitted changes and untracked files permanently; committed"));
2466
- console.error(chalk.dim("work stays on the branch."));
2467
- process.exitCode = 1;
2941
+ if (unpushed && unpushed.length && !await hasRemoteAsync(path, {
2942
+ env: operation?.env,
2943
+ signal: operation?.signal
2944
+ })) output.error(chalk.dim(" (no remote is configured for this worktree, so every commit no other local branch reaches counts as unpushed)"));
2945
+ for (const line of dirtyLines.slice(0, 10)) output.error(chalk.dim(` ${line}`));
2946
+ if (dirtyLines.length) output.error(chalk.dim(` (git -C ${path} status -s for the full list)`));
2947
+ for (const line of removalRemedy(dirtyLines, { worktree: path })) output.error(chalk.dim(line));
2948
+ output.error(chalk.dim("Otherwise: commit or push the branch (only commits found nowhere else are counted,"));
2949
+ output.error(chalk.dim("so pushing publishes nothing but this worktree's own work). --force is a last"));
2950
+ output.error(chalk.dim("resort -- it discards uncommitted changes and untracked files permanently; committed"));
2951
+ output.error(chalk.dim("work stays on the branch."));
2952
+ output.fail();
2468
2953
  }
2469
2954
  function deleteWatchmanCookies(path, cookies) {
2470
2955
  for (const cookie of cookies) rmSync$1(join$1(path, cookie), { force: true });
2471
2956
  }
2472
- function restorePodChurn(path, files) {
2473
- for (const file of files) if (restoreFile(path, file)) console.error(chalk.dim(` restored ${file} (pod install churn; the worktree is being removed)`));
2474
- else console.error(chalk.yellow(` could not restore ${file}; git may refuse to remove the worktree`));
2475
- }
2476
- function printRemovalCleanup(result, failed) {
2477
- for (const line of poolLines(result)) console.error(line);
2478
- for (const device of result.deletedDevices) console.error(chalk.dim(phaseLine("device", `deleted ${device}`)));
2479
- for (const session of result.stoppedSessions) console.error(chalk.dim(phaseLine("device", `stopped remote session ${session}`)));
2480
- for (const tunnel of result.stoppedTunnels) console.error(chalk.dim(phaseLine("lan", `stopped the ${tunnel} tunnel`)));
2481
- for (const lease of result.releasedLeases) console.error(chalk.dim(phaseLine("lease", releasedLeaseFact(lease))));
2482
- for (const pid of result.killedPids) console.error(chalk.dim(phaseLine("metro", `killed pid ${pid}`)));
2483
- if (!failed) for (const dir of result.removedWorkspaceDirs) console.error(chalk.dim(phaseLine("workspace", `removed ${dir}`)));
2484
- if (result.dereferenced.length) console.error(chalk.dim(phaseLine("workspace", `no longer referenced: ${result.dereferenced.join(", ")}`)));
2485
- for (const skipped of result.skippedDevices) console.error((failed ? chalk.dim : chalk.yellow)(phaseLine("device", `kept ${describeKeptDevice(skipped)} (${skipped.reason})`)));
2957
+ async function restorePodChurn(path, files, output, operation) {
2958
+ for (const file of files) if (await restoreFileAsync(path, file, {
2959
+ env: operation?.env,
2960
+ signal: operation?.signal,
2961
+ declareFor: operation
2962
+ })) output.error(chalk.dim(` restored ${file} (pod install churn; the worktree is being removed)`));
2963
+ else output.error(chalk.yellow(` could not restore ${file}; git may refuse to remove the worktree`));
2964
+ }
2965
+ function printRemovalCleanup(result, failed, output) {
2966
+ for (const line of poolLines(result)) output.error(line);
2967
+ for (const device of result.deletedDevices) output.error(chalk.dim(phaseLine("device", `deleted ${device}`)));
2968
+ for (const session of result.stoppedSessions) output.error(chalk.dim(phaseLine("device", `stopped remote session ${session}`)));
2969
+ for (const tunnel of result.stoppedTunnels) output.error(chalk.dim(phaseLine("lan", `stopped the ${tunnel} tunnel`)));
2970
+ for (const lease of result.releasedLeases) output.error(chalk.dim(phaseLine("lease", releasedLeaseFact(lease))));
2971
+ for (const pid of result.killedPids) output.error(chalk.dim(phaseLine("metro", `killed pid ${pid}`)));
2972
+ if (!failed) for (const dir of result.removedWorkspaceDirs) output.error(chalk.dim(phaseLine("workspace", `removed ${dir}`)));
2973
+ if (result.dereferenced.length) output.error(chalk.dim(phaseLine("workspace", `no longer referenced: ${result.dereferenced.join(", ")}`)));
2974
+ for (const skipped of result.skippedDevices) output.error((failed ? chalk.dim : chalk.yellow)(phaseLine("device", `kept ${describeKeptDevice(skipped)} (${skipped.reason})`)));
2486
2975
  }
2487
2976
  async function removeWorktreeTarget(target, opts = {}) {
2488
2977
  let removed = false;
@@ -2492,200 +2981,245 @@ async function removeWorktreeTarget(target, opts = {}) {
2492
2981
  return removed;
2493
2982
  }
2494
2983
  async function runRemove(target, opts, onRemoved) {
2984
+ const output = opts.output ?? REMOVAL_OUTPUT;
2495
2985
  try {
2496
- await removeOrRefuse(target, opts, onRemoved);
2986
+ await removeOrRefuse(target, opts, onRemoved, output);
2497
2987
  } catch (error) {
2988
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
2989
+ opts.operation?.signal.throwIfAborted();
2498
2990
  if (!isTimeoutError(error)) throw error;
2499
- printRemovalRefusal(removalPath(target), timedOutInspection(error));
2991
+ await printRemovalRefusal(removalPath(target, opts.operation?.cwd), timedOutInspection(error), output, opts.operation);
2500
2992
  }
2501
2993
  }
2502
- async function removeOrRefuse(target, opts, onRemoved) {
2994
+ async function removeOrRefuse(target, opts, onRemoved, output) {
2995
+ const options = {
2996
+ removal: true,
2997
+ env: opts.operation?.env,
2998
+ signal: opts.operation?.signal,
2999
+ declareFor: opts.operation
3000
+ };
3001
+ options.signal?.throwIfAborted();
2503
3002
  const poolError = parkedMaxSetting("ios").error || parkedMaxSetting("android").error;
2504
3003
  if (poolError) {
2505
- console.error(chalk.red(poolError));
2506
- console.error(chalk.dim(POOL_SETTING_REMEDY));
2507
- process.exitCode = 1;
3004
+ output.error(chalk.red(poolError));
3005
+ output.error(chalk.dim(POOL_SETTING_REMEDY));
3006
+ output.fail();
2508
3007
  return;
2509
3008
  }
2510
- let path = removalPath(target);
3009
+ let path = removalPath(target, opts.operation?.cwd);
2511
3010
  if (!existsSync$1(path)) {
3011
+ opts.beforeRemove?.(path);
2512
3012
  const pending = getProject(path);
2513
3013
  if (pending?.worktreeRemovalComplete === true && pending.worktreeBranchOwned === true && pending.worktreeBranch && pending.worktreeMainRoot && pending.worktreePendingBranchSha) {
2514
3014
  const branch = pending.worktreeBranch;
2515
3015
  const mainRoot = pending.worktreeMainRoot;
2516
3016
  if (!existsSync$1(mainRoot)) {
2517
- console.error(chalk.red(`Cannot finish branch cleanup for ${path}: source checkout ${mainRoot} is missing.`));
2518
- console.error(chalk.dim(`Delete ${branch} from the repository, then run \`stim gc --delete\`.`));
2519
- process.exitCode = 1;
3017
+ output.error(chalk.red(`Cannot finish branch cleanup for ${path}: source checkout ${mainRoot} is missing.`));
3018
+ output.error(chalk.dim(`Delete ${branch} from the repository, then run \`stim gc --delete\`.`));
3019
+ output.fail();
2520
3020
  return;
2521
3021
  }
2522
- if (!branchExists(mainRoot, branch, { removal: true })) {
3022
+ opts.beforeRemove?.(removalPath(mainRoot));
3023
+ if (!await branchExistsAsync(mainRoot, branch, options)) {
2523
3024
  removeProject(path);
2524
- console.error(chalk.dim(phaseLine("branch", `${branch} is already absent; cleared its pending cleanup record`)));
3025
+ output.error(chalk.dim(phaseLine("branch", `${branch} is already absent; cleared its pending cleanup record`)));
2525
3026
  return;
2526
3027
  }
2527
- const checkedOutAt = listWorktrees(mainRoot, { removal: true }).find((candidate) => candidate.branch === branch && resolve$1(candidate.path) !== resolve$1(path))?.path;
3028
+ const checkedOutAt = (await listWorktreesAsync(mainRoot, options)).find((candidate) => candidate.branch === branch && resolve$1(candidate.path) !== resolve$1(path))?.path;
2528
3029
  if (checkedOutAt) {
2529
- console.error(chalk.red(`Refusing pending cleanup for ${branch}: it is checked out at ${checkedOutAt}.`));
2530
- console.error(chalk.dim("Switch that worktree to another branch, then retry this command."));
2531
- process.exitCode = 1;
3030
+ output.error(chalk.red(`Refusing pending cleanup for ${branch}: it is checked out at ${checkedOutAt}.`));
3031
+ output.error(chalk.dim("Switch that worktree to another branch, then retry this command."));
3032
+ output.fail();
2532
3033
  return;
2533
3034
  }
2534
- const currentSha = resolveFullRef(mainRoot, branch, { removal: true });
3035
+ const currentSha = await resolveFullRefAsync(mainRoot, branch, options);
2535
3036
  if (currentSha !== pending.worktreePendingBranchSha) {
2536
- console.error(chalk.red(`Refusing pending cleanup for ${branch}: its tip changed from ${pending.worktreePendingBranchSha} to ${currentSha || "an unknown commit"}.`));
2537
- console.error(chalk.dim("The branch can contain new work. Inspect it and delete it manually if appropriate."));
2538
- process.exitCode = 1;
3037
+ output.error(chalk.red(`Refusing pending cleanup for ${branch}: its tip changed from ${pending.worktreePendingBranchSha} to ${currentSha || "an unknown commit"}.`));
3038
+ output.error(chalk.dim("The branch can contain new work. Inspect it and delete it manually if appropriate."));
3039
+ output.fail();
2539
3040
  return;
2540
3041
  }
2541
3042
  try {
2542
- deleteBranch(mainRoot, branch, pending.worktreePendingBranchSha);
3043
+ await deleteBranchAsync(mainRoot, branch, pending.worktreePendingBranchSha, options);
2543
3044
  removeProject(path);
2544
- console.error(chalk.dim(phaseLine("branch", `deleted ${branch}; cleared its pending cleanup record`)));
3045
+ output.error(chalk.dim(phaseLine("branch", `deleted ${branch}; cleared its pending cleanup record`)));
2545
3046
  } catch (error) {
2546
- console.error(chalk.red(`Could not delete branch ${branch}: ${String(error?.message || error)}`));
2547
- console.error(chalk.dim(`Retry with: stim worktree remove ${path}`));
2548
- process.exitCode = 1;
3047
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
3048
+ output.error(chalk.red(`Could not delete branch ${branch}: ${String(error?.message || error)}`));
3049
+ output.error(chalk.dim(`Retry with: stim worktree remove ${path}`));
3050
+ output.fail();
2549
3051
  }
2550
3052
  return;
2551
3053
  }
2552
- console.error(chalk.red(`No such worktree: ${path}`));
2553
- process.exitCode = 1;
3054
+ output.error(chalk.red(`No such worktree: ${path}`));
3055
+ output.fail();
2554
3056
  return;
2555
3057
  }
2556
- const worktrees = listWorktrees(path, { removal: true });
3058
+ const worktrees = await listWorktreesAsync(path, options);
2557
3059
  const entry = matchWorktreeEntry(worktrees, path);
2558
3060
  if (!entry) {
2559
- if (!opts.linkedOnly && gitCommonDir(path, { removal: true }) === null && hasRegisteredProjectUnder(path)) {
2560
- await reclaimEnvironment(path, "it is not a git repository");
3061
+ if (!opts.linkedOnly && await gitCommonDirAsync(path, options) === null && hasRegisteredProjectUnder(path)) {
3062
+ await reclaimEnvironment(path, "it is not a git repository", output, opts.operation, opts.beforeRemove);
2561
3063
  return;
2562
3064
  }
2563
- console.error(chalk.red(`Refusing to remove ${path}: it is not inside any worktree known to git.`));
2564
- console.error(chalk.dim(" Run it from inside the worktree, or pass the worktree root path, e.g. as printed by `git worktree list`."));
2565
- process.exitCode = 1;
3065
+ output.error(chalk.red(`Refusing to remove ${path}: it is not inside any worktree known to git.`));
3066
+ output.error(chalk.dim(" Run it from inside the worktree, or pass the worktree root path, e.g. as printed by `git worktree list`."));
3067
+ output.fail();
2566
3068
  return;
2567
3069
  }
2568
3070
  if (entry.bare) {
2569
- console.error(chalk.red(`Refusing to remove ${path}: ${entry.path} is the bare repository, not a worktree.`));
2570
- console.error(chalk.dim(" Pass a worktree path, e.g. as printed by `git worktree list`."));
2571
- process.exitCode = 1;
3071
+ output.error(chalk.red(`Refusing to remove ${path}: ${entry.path} is the bare repository, not a worktree.`));
3072
+ output.error(chalk.dim(" Pass a worktree path, e.g. as printed by `git worktree list`."));
3073
+ output.fail();
2572
3074
  return;
2573
3075
  }
2574
3076
  const source = sourceCheckoutOf(worktrees);
2575
3077
  if ("refusal" in source) {
2576
- console.error(chalk.red(`Refusing to remove ${path}: ${source.refusal}`));
2577
- process.exitCode = 1;
3078
+ output.error(chalk.red(`Refusing to remove ${path}: ${source.refusal}`));
3079
+ output.fail();
2578
3080
  return;
2579
3081
  }
2580
3082
  if (entry.path === source.path && opts.linkedOnly) {
2581
- console.error(chalk.red(`Refusing to remove ${path}: ${entry.path} is the source checkout.`));
2582
- process.exitCode = 1;
3083
+ output.error(chalk.red(`Refusing to remove ${path}: ${entry.path} is the source checkout.`));
3084
+ output.fail();
2583
3085
  return;
2584
3086
  }
2585
3087
  if (entry.path === source.path) {
2586
3088
  if (nativeCanonicalPath(entry.path) !== nativeCanonicalPath(path)) {
2587
3089
  const project = projectAtOrAbove(entry.path, path);
2588
3090
  if (!project) {
2589
- console.error(chalk.red(`Refusing to reclaim from ${path}: no Stim project is registered at or above it.`));
2590
- console.error(chalk.dim(` Run it from the project folder, or from ${entry.path} to reclaim every project under it.`));
2591
- process.exitCode = 1;
3091
+ output.error(chalk.red(`Refusing to reclaim from ${path}: no Stim project is registered at or above it.`));
3092
+ output.error(chalk.dim(` Run it from the project folder, or from ${entry.path} to reclaim every project under it.`));
3093
+ output.fail();
2592
3094
  return;
2593
3095
  }
2594
- console.error(chalk.dim(`${path} is inside the source checkout ${entry.path}; reclaiming only the project ${project}.`));
2595
- await reclaimEnvironment(project, "it is the source checkout");
3096
+ output.error(chalk.dim(`${path} is inside the source checkout ${entry.path}; reclaiming only the project ${project}.`));
3097
+ await reclaimEnvironment(project, "it is the source checkout", output, opts.operation, opts.beforeRemove);
2596
3098
  return;
2597
3099
  }
2598
- await reclaimEnvironment(entry.path, "it is the source checkout");
3100
+ await reclaimEnvironment(entry.path, "it is the source checkout", output, opts.operation, opts.beforeRemove);
2599
3101
  return;
2600
3102
  }
2601
3103
  if (entry.path !== path) {
2602
- console.error(chalk.dim(`${path} is inside the worktree ${entry.path}; removing that.`));
3104
+ output.error(chalk.dim(`${path} is inside the worktree ${entry.path}; removing that.`));
2603
3105
  path = entry.path;
2604
3106
  }
2605
3107
  if (entry.locked) {
2606
- console.error(chalk.red(`Refusing to remove ${path}: git has it locked.`));
2607
- console.error(chalk.dim(`Unlock it, then retry: git -C ${source.path} worktree unlock ${path}`));
2608
- process.exitCode = 1;
3108
+ output.error(chalk.red(`Refusing to remove ${path}: git has it locked.`));
3109
+ output.error(chalk.dim(`Unlock it, then retry: git -C ${source.path} worktree unlock ${path}`));
3110
+ output.fail();
3111
+ return;
3112
+ }
3113
+ const nested = nestedWorktrees(worktrees, entry.path);
3114
+ if (nested.length) {
3115
+ output.error(chalk.red(`Refusing to remove ${path}: it contains the worktree ${nested.join(", ")}.`));
3116
+ output.error(chalk.dim("Remove the nested worktree first, then retry."));
3117
+ output.fail();
2609
3118
  return;
2610
3119
  }
2611
3120
  const service = serviceRunningFrom(path, nativeCanonicalPath);
2612
3121
  if (service) {
2613
- console.error(chalk.red(service.argument ? `Refusing to remove ${path}: it runs the stim-server service ${service.label} (${service.argument}). ${SERVICE_IN_USE.code}` : `Refusing to remove ${path}: could not read ${service.plist} to check whether the stim-server service ${service.label} runs from it. ${SERVICE_IN_USE.code}`));
2614
- console.error(chalk.dim(`Reinstall the service from another build with \`stim-server service install --label ${service.label}\`, or remove it with \`stim-server service uninstall --label ${service.label}\`, then retry.`));
2615
- process.exitCode = 1;
3122
+ output.error(chalk.red(service.argument ? `Refusing to remove ${path}: it runs the stim-server service ${service.label} (${service.argument}). ${SERVICE_IN_USE.code}` : `Refusing to remove ${path}: could not read ${service.plist} to check whether the stim-server service ${service.label} runs from it. ${SERVICE_IN_USE.code}`));
3123
+ output.error(chalk.dim(`Reinstall the service from another build with \`stim-server service install --label ${service.label}\`, or remove it with \`stim-server service uninstall --label ${service.label}\`, then retry.`));
3124
+ output.fail();
2616
3125
  return;
2617
3126
  }
2618
3127
  const project = getProject(path);
2619
3128
  const branch = entry.branch;
2620
3129
  const ownsBranch = Boolean(branch && project?.worktreeBranchOwned === true && project.worktreeBranch === branch);
2621
- const approvedBranchSha = ownsBranch ? resolveFullRef(path, "HEAD", { removal: true }) : null;
3130
+ const approvedBranchSha = ownsBranch ? await resolveFullRefAsync(path, "HEAD", options) : null;
2622
3131
  let mergedHead = opts.mergedHead;
2623
- let inspection = inspectRemoval(path, mergedHead);
3132
+ let inspection = await inspectRemoval(path, mergedHead, opts.operation);
2624
3133
  if (inspection.blockers.length && !opts.force && !mergedHead && branch && inspection.unpushed?.length && !inspection.dirtyLines.length) {
2625
- const head = resolveFullRef(path, "HEAD", { removal: true });
2626
- const pr = head ? endedPullRequest(pullRequestLookup()(path, branch, head)) : null;
3134
+ const head = await resolveFullRefAsync(path, "HEAD", options);
3135
+ const pr = head ? endedPullRequest(await pullRequestLookupAsync(opts.operation)(path, branch, head)) : null;
2627
3136
  if (head && pr?.state === "merged") {
2628
3137
  mergedHead = head;
2629
- inspection = inspectRemoval(path, mergedHead);
2630
- console.error(chalk.dim(`HEAD is in the merged PR #${pr.number} (${pr.url}); its local-only commits stay on GitHub.`));
3138
+ inspection = await inspectRemoval(path, mergedHead, opts.operation);
3139
+ output.error(chalk.dim(`HEAD is in the merged PR #${pr.number} (${pr.url}); its local-only commits stay on GitHub.`));
2631
3140
  }
2632
3141
  }
2633
3142
  if (inspection.blockers.length && !opts.force) {
2634
- printRemovalRefusal(path, inspection);
3143
+ await printRemovalRefusal(path, inspection, output, opts.operation);
2635
3144
  return;
2636
3145
  }
2637
3146
  const deleteOwnedBranch = Boolean(ownsBranch && approvedBranchSha && inspection.unpushed?.length === 0);
2638
3147
  const retainedBranchReason = !branch ? null : !ownsBranch ? "Stim did not create it" : inspection.unpushed === null ? "Stim could not verify whether it has unique commits" : inspection.unpushed.length > 0 ? `it has ${plural(inspection.unpushed.length, "unique commit")}` : null;
2639
3148
  const branchDeleteCwd = source.path;
2640
3149
  await withManagedRemoteWorktreeRemovalLock(path, () => withReclaimLocks(path, async (lockedKeys) => {
2641
- if (opts.guard?.(lockedKeys).length) return;
2642
- const inspectedHead = resolveFullRef(path, "HEAD", { removal: true });
2643
- const current = inspectRemoval(path, mergedHead);
3150
+ options.signal?.throwIfAborted();
3151
+ if ((await opts.guard?.(lockedKeys))?.length) return;
3152
+ options.signal?.throwIfAborted();
3153
+ const inspectedHead = await resolveFullRefAsync(path, "HEAD", options);
3154
+ if (opts.expectedHead && inspectedHead !== opts.expectedHead) {
3155
+ output.error(chalk.red(`Refusing to remove ${path}: its HEAD moved since it was checked.`));
3156
+ output.fail();
3157
+ return;
3158
+ }
3159
+ const current = await inspectRemoval(path, mergedHead, opts.operation);
2644
3160
  if (current.blockers.length && !opts.force) {
2645
- printRemovalRefusal(path, current);
3161
+ await printRemovalRefusal(path, current, output, opts.operation);
2646
3162
  return;
2647
3163
  }
2648
3164
  const result = await reclaimAll(path, lockedKeys, {
2649
3165
  preserveRootProject: true,
2650
- removedBy: opts.removedBy ?? "worktree-remove"
3166
+ removedBy: opts.removedBy ?? "worktree-remove",
3167
+ operation: opts.operation
2651
3168
  });
2652
3169
  if (result.keptEntries.length) {
2653
- reportRetainedResources(path, result);
3170
+ reportRetainedResources(path, result, output);
3171
+ return;
3172
+ }
3173
+ let headMoved = false;
3174
+ if (!opts.force) try {
3175
+ headMoved = await resolveFullRefAsync(path, "HEAD", options) !== inspectedHead;
3176
+ } catch (error) {
3177
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
3178
+ opts.operation?.signal.throwIfAborted();
3179
+ if (!isTimeoutError(error)) throw error;
3180
+ await printRemovalRefusal(path, timedOutInspection(error), output, opts.operation);
3181
+ output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
3182
+ printRemovalCleanup(result, true, output);
2654
3183
  return;
2655
3184
  }
2656
- if (!opts.force && resolveFullRef(path, "HEAD", { removal: true }) !== inspectedHead) {
2657
- console.error(chalk.red(`Refusing to remove ${path}: its HEAD moved while its environment was reclaimed.`));
2658
- console.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
2659
- printRemovalCleanup(result, true);
2660
- process.exitCode = 1;
3185
+ if (headMoved) {
3186
+ output.error(chalk.red(`Refusing to remove ${path}: its HEAD moved while its environment was reclaimed.`));
3187
+ output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
3188
+ printRemovalCleanup(result, true, output);
3189
+ output.fail();
2661
3190
  return;
2662
3191
  }
2663
- restorePodChurn(path, current.podChurn);
3192
+ await restorePodChurn(path, current.podChurn, output, opts.operation);
3193
+ options.signal?.throwIfAborted();
2664
3194
  deleteWatchmanCookies(path, current.cookies);
2665
3195
  try {
2666
- removeWorktree(path, {
3196
+ await removeWorktreeAsync(path, {
2667
3197
  from: source.path,
2668
- force: opts.force
3198
+ force: opts.force,
3199
+ env: options.env,
3200
+ signal: options.signal,
3201
+ declareFor: opts.operation
2669
3202
  });
2670
3203
  } catch (error) {
3204
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
2671
3205
  const message = String(error?.message || error);
2672
- console.error(chalk.red(`git worktree remove failed: ${message}`));
2673
- if (process.platform === "win32" && /Permission denied/i.test(message)) console.error(chalk.dim("Another process holds a file or directory inside the worktree. An adb server or emulator started from it keeps its working directory open; `adb kill-server` releases the server, and Sysinternals handle.exe lists other holders."));
2674
- console.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
2675
- printRemovalCleanup(result, true);
2676
- process.exitCode = 1;
3206
+ output.error(chalk.red(`git worktree remove failed: ${message}`));
3207
+ if (process.platform === "win32" && /Permission denied/i.test(message)) output.error(chalk.dim("Another process holds a file or directory inside the worktree. An adb server or emulator started from it keeps its working directory open; `adb kill-server` releases the server, and Sysinternals handle.exe lists other holders."));
3208
+ output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
3209
+ printRemovalCleanup(result, true, output);
3210
+ output.fail();
2677
3211
  return;
2678
3212
  }
2679
3213
  onRemoved();
2680
3214
  forgetStatusMeasures(path, result.reclaimedKeys);
2681
3215
  const finish = () => {
2682
- printRemovalCleanup(result, false);
2683
- console.error(chalk.dim(phaseLine("removed", path)));
3216
+ printRemovalCleanup(result, false, output);
3217
+ output.error(chalk.dim(phaseLine("removed", path)));
2684
3218
  };
2685
3219
  if (deleteOwnedBranch && branch) {
2686
3220
  if (!approvedBranchSha) {
2687
- console.error(chalk.yellow(phaseLine("branch", `kept ${branch} (Stim could not record its commit before removal)`)));
2688
- process.exitCode = 1;
3221
+ output.error(chalk.yellow(phaseLine("branch", `kept ${branch} (Stim could not record its commit before removal)`)));
3222
+ output.fail();
2689
3223
  finish();
2690
3224
  return;
2691
3225
  }
@@ -2693,28 +3227,44 @@ async function removeOrRefuse(target, opts, onRemoved) {
2693
3227
  worktreeRemovalComplete: true,
2694
3228
  worktreePendingBranchSha: approvedBranchSha
2695
3229
  });
2696
- const checkedOutAt = listWorktrees(branchDeleteCwd).find((candidate) => candidate.branch === branch && resolve$1(candidate.path) !== resolve$1(path))?.path;
3230
+ let checkedOutAt;
3231
+ try {
3232
+ checkedOutAt = (await listWorktreesAsync(branchDeleteCwd, options)).find((candidate) => candidate.branch === branch && resolve$1(candidate.path) !== resolve$1(path))?.path;
3233
+ } catch (error) {
3234
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
3235
+ opts.operation?.signal.throwIfAborted();
3236
+ if (!isTimeoutError(error)) throw error;
3237
+ output.error(chalk.yellow(phaseLine("branch", `kept ${branch} (git did not list the worktrees in time to check it)`)));
3238
+ output.error(chalk.dim(` Retry: stim worktree remove ${path}`));
3239
+ output.fail();
3240
+ finish();
3241
+ return;
3242
+ }
2697
3243
  if (checkedOutAt) {
2698
- console.error(chalk.yellow(phaseLine("branch", `kept ${branch} (it is checked out at ${checkedOutAt})`)));
2699
- console.error(chalk.dim(` Switch that worktree to another branch, then retry: stim worktree remove ${path}`));
2700
- process.exitCode = 1;
3244
+ output.error(chalk.yellow(phaseLine("branch", `kept ${branch} (it is checked out at ${checkedOutAt})`)));
3245
+ output.error(chalk.dim(` Switch that worktree to another branch, then retry: stim worktree remove ${path}`));
3246
+ output.fail();
2701
3247
  finish();
2702
3248
  return;
2703
3249
  }
2704
3250
  try {
2705
- deleteBranch(branchDeleteCwd, branch, approvedBranchSha);
2706
- console.error(chalk.dim(phaseLine("branch", `deleted ${branch}`)));
3251
+ await deleteBranchAsync(branchDeleteCwd, branch, approvedBranchSha, options);
3252
+ output.error(chalk.dim(phaseLine("branch", `deleted ${branch}`)));
2707
3253
  } catch (error) {
2708
- console.error(chalk.yellow(phaseLine("branch", `kept ${branch} (${String(error?.message || error)})`)));
2709
- console.error(chalk.dim(` Retry with: git -C ${branchDeleteCwd} branch -D -- ${branch}`));
2710
- process.exitCode = 1;
3254
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
3255
+ output.error(chalk.yellow(phaseLine("branch", `kept ${branch} (${String(error?.message || error)})`)));
3256
+ output.error(chalk.dim(` Retry with: git -C ${branchDeleteCwd} branch -D -- ${branch}`));
3257
+ output.fail();
2711
3258
  finish();
2712
3259
  return;
2713
3260
  }
2714
- } else if (branch && retainedBranchReason) console.error(chalk.dim(phaseLine("branch", `kept ${branch} (${retainedBranchReason})`)));
3261
+ } else if (branch && retainedBranchReason) output.error(chalk.dim(phaseLine("branch", `kept ${branch} (${retainedBranchReason})`)));
2715
3262
  removeProject(path);
2716
3263
  finish();
2717
- }));
3264
+ }, opts.operation, opts.beforeRemove), {
3265
+ signal: opts.operation?.signal,
3266
+ declareSpawnsFor: opts.operation
3267
+ });
2718
3268
  }
2719
3269
  function registerRemove(worktree) {
2720
3270
  worktree.command("remove [target]").description("Remove a worktree, its unused Stim-created branch, build artifacts, owned devices, and Metro port. Defaults to the current workspace. On the source checkout it reclaims the environment only and leaves the tree in place; from a subfolder it reclaims only the project at or above that folder, with the projects nested under it.").option("--force", "remove even when the worktree holds uncommitted or unpushed work or initialized submodules").action(async (target, opts) => {
@@ -2727,4 +3277,4 @@ function worktreeCommand(program) {
2727
3277
  registerRemove(worktree);
2728
3278
  }
2729
3279
  //#endregion
2730
- export { reclaimProject as _, matchWorktreeEntry as a, registerRemove as c, removalPath as d, removalRemedy as f, describeDereferenced as g, worktreeCommand as h, excludeWatchmanCookies as i, registerWarm as l, warmedWorkspace as m, dependencyInstallCommand as n, porcelainPath as o, removeWorktreeTarget as p, excludePodChurn as r, reclaimKeys as s, customIosDirs as t, removalBlockers as u, deleteSelectedArchives as v, sweepArchiveStaging as y };
3280
+ export { levelFloors as A, measureDisk as C, measureSizes as D, measurePressureAsync as E, lowestFree as M, protectWindowMs as N, sizeScanDeferred as O, cacheBlocked as S, measurePressure as T, worktreeCommand as _, matchWorktreeEntry as a, deleteSelectedArchives as b, reclaimKeys as c, removalBlockers as d, removalPath as f, warmedWorkspace as g, warmWorktreeTarget as h, excludeWatchmanCookies as i, levelStillShort as j, diskPressureLevel as k, registerRemove as l, removeWorktreeTarget as m, dependencyInstallCommand as n, nestedWorktrees as o, removalRemedy as p, excludePodChurn as r, porcelainPath as s, customIosDirs as t, registerWarm as u, describeDereferenced as v, measureDiskAsync as w, sweepArchiveStaging as x, reclaimProject as y };