stim 1.21.0 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +60 -7
  2. package/dist/{acquire-artifact-CD4lbiWE.mjs → acquire-artifact-Co2k4f7O.mjs} +21 -3
  3. package/dist/{act-DHAw2A2S.mjs → act-DsuqEkiM.mjs} +298 -164
  4. package/dist/{activity-BzVfHSrY.mjs → activity-CMLmzp7o.mjs} +33 -12
  5. package/dist/{agent-device-usage-output-zh1vhbLp.mjs → agent-device-usage-output-DUFaTMdd.mjs} +7 -7
  6. package/dist/{android-BLgbNqox.mjs → android-ByRMUoiJ.mjs} +66 -52
  7. package/dist/{android-DUYkNF4u.mjs → android-C7sVmyI9.mjs} +160 -59
  8. package/dist/{android-CV_GCELZ.mjs → android-DGkNqa_c.mjs} +5 -5
  9. package/dist/{android-BEJ8Jpl7.mjs → android-DR5lZ2Az.mjs} +2 -2
  10. package/dist/android-cas-compiler.mjs +1 -1
  11. package/dist/{android-cas-Ct5b1xCK.mjs → android-cas-ngferVq2.mjs} +3 -3
  12. package/dist/{client-B7QVOlCr.d.mts → android-project-DVWyCbWU.d.mts} +145 -7
  13. package/dist/api.d.mts +132 -118
  14. package/dist/api.mjs +239 -42
  15. package/dist/{artifact-B8T3S5gi.mjs → artifact-8EY8SAPw.mjs} +40 -35
  16. package/dist/{artifact-DAuZ-FNU.mjs → artifact-DJPy518Y.mjs} +69 -40
  17. package/dist/artifact-platform-CXcmX_j3.mjs +27 -0
  18. package/dist/{browser-web-QQzVjz3O.mjs → browser-web-Bcv99PBM.mjs} +187 -59
  19. package/dist/{budget-Bs6SxxzO.mjs → budget-Cvwn54qV.mjs} +830 -699
  20. package/dist/{build-CoPzQRNw.mjs → build-CabC3G23.mjs} +34 -27
  21. package/dist/{build-cache-DmyR9Zzc.mjs → build-cache-Dv7RN200.mjs} +4 -4
  22. package/dist/{build-plan-CaFskB7_.mjs → build-plan-DUC8Ka9y.mjs} +74 -14
  23. package/dist/{cache-manifest-DkkqguoK.mjs → cache-manifest-DSeQjt-Q.mjs} +1 -1
  24. package/dist/cache-manifest.mjs +1 -1
  25. package/dist/{ccache-BJRZ9pG3.mjs → ccache-Btwc36-I.mjs} +6 -6
  26. package/dist/{cdp-CWmIUMbw.mjs → cdp-3fwICjzi.mjs} +1 -1
  27. package/dist/{chrome-B6tfCta1.mjs → chrome-Bbw689-6.mjs} +1 -1
  28. package/dist/{cli-0DYf8QhQ.mjs → cli-CHZa9Lvk.mjs} +20 -20
  29. package/dist/cli.mjs +1 -1
  30. package/dist/{client-UXD70ZbM.mjs → client-OyRoEuSr.mjs} +218 -47
  31. package/dist/collector-run.d.mts +1 -1
  32. package/dist/collector-run.mjs +6 -6
  33. package/dist/{command-output-B1UrjZHL.mjs → command-output-gwpm28fu.mjs} +1 -1
  34. package/dist/{config-CYcBF478.mjs → config-DlrsduwG.mjs} +17 -11
  35. package/dist/{created-devices-CqeBlrEn.mjs → created-devices-x9rN6bhs.mjs} +13 -3
  36. package/dist/{dependency-state-BbKQW83h.mjs → dependency-state-1I9KZT1Y.mjs} +2 -2
  37. package/dist/{detached-entry-DFRyG1_o.mjs → detached-entry-BwNEtIrP.mjs} +1 -1
  38. package/dist/{dev-client-UOeESKm6.mjs → dev-client-BSDR5h3F.mjs} +2 -2
  39. package/dist/{device-ExJq8Ei7.mjs → device-B-L3Fbxy.mjs} +71 -14
  40. package/dist/{device-DCxH7_By.mjs → device-DklWZLqb.mjs} +16 -16
  41. package/dist/{device-capacity-CMlqEcvw.mjs → device-capacity-DK5_EwRS.mjs} +9 -10
  42. package/dist/device-host-worker.mjs +25 -32
  43. package/dist/{device-ios-DRUeKaFV.mjs → device-ios-DoB6KFCO.mjs} +39 -16
  44. package/dist/{device-lease-run-C8raGFoP.mjs → device-lease-run-xJPQ_QQG.mjs} +5 -5
  45. package/dist/{device-pool-DMRpVX9a.mjs → device-pool-BMkdsefq.mjs} +3 -3
  46. package/dist/{doctor-DTvaDOda.mjs → doctor-Bo7BXLux.mjs} +24 -37
  47. package/dist/{doctor-Dhqebm0q.mjs → doctor-D5_EzwfA.mjs} +19 -16
  48. package/dist/doctor-fingerprint-DZLoYFVb.mjs +16 -0
  49. package/dist/doctor-fingerprint.mjs +1 -1
  50. package/dist/{doctor-BLHCYxzi.mjs → doctor-thjWAV0z.mjs} +1 -1
  51. package/dist/{doctor-watchman-Cv1F10Kj.mjs → doctor-watchman-N2wDotwQ.mjs} +5 -5
  52. package/dist/{eas-build-BomfRnFT.mjs → eas-build-CwknaQYj.mjs} +4 -4
  53. package/dist/{eas-fallback-DJwZpy0c.mjs → eas-fallback-CSgAulr5.mjs} +27 -18
  54. package/dist/{eas-session-ledger-op6A54yi.mjs → eas-session-ledger-DlY_serQ.mjs} +8 -7
  55. package/dist/{error-diagnostics-Bf0rZvG9.mjs → error-diagnostics-DBWO2rxE.mjs} +10 -10
  56. package/dist/{exec-_UbtCQcG.mjs → exec-DEoAhcze.mjs} +155 -47
  57. package/dist/{exec-H20xSg2T.d.mts → exec-DnYc4HGH.d.mts} +17 -4
  58. package/dist/{gc-_-dCXdyL.mjs → gc-CXxN-4SE.mjs} +587 -313
  59. package/dist/gc-DnNDQyaP.mjs +3 -0
  60. package/dist/{guide-Yqwt2bd6.mjs → guide-Br0LEfKy.mjs} +239 -43
  61. package/dist/{hosted-android-Bw-honsd.mjs → hosted-android-B-9f-jkd.mjs} +6 -6
  62. package/dist/{hosted-client-DB_gBPfY.mjs → hosted-client-DZAPrcro.mjs} +4 -4
  63. package/dist/{hosted-ios-CUvOf4Vr.mjs → hosted-ios-CsCS21hn.mjs} +1 -1
  64. package/dist/{hosted-logs-DL_P5AdN.mjs → hosted-logs-FIRx9X2f.mjs} +5 -5
  65. package/dist/{hosted-macos-QI141yKk.mjs → hosted-macos-_awYSpfv.mjs} +21 -12
  66. package/dist/{hosted-native-Ksyjgzq-.mjs → hosted-native-BdzE668-.mjs} +27 -14
  67. package/dist/{idle-BVeRGroT.d.mts → idle-C4cZ757K.d.mts} +3 -3
  68. package/dist/{idle-shutdown-DaKmZi0z.mjs → idle-shutdown-DDg3SdvY.mjs} +7 -7
  69. package/dist/ios-CDvlmUs7.d.mts +24 -0
  70. package/dist/{ios-CQ68hG9s.mjs → ios-Ds1clbIt.mjs} +8 -8
  71. package/dist/{ios-device-CwFMehUS.mjs → ios-device--h_-8ENa.mjs} +3 -3
  72. package/dist/{ios-device-CXGP1bsD.mjs → ios-device-BXZ0c7Kb.mjs} +1 -1
  73. package/dist/{ios-CpByZy12.mjs → ios-hcBpnEGk.mjs} +81 -34
  74. package/dist/{ios-ubFsBzwq.mjs → ios-pm4nL6By.mjs} +59 -50
  75. package/dist/{ios-project-WuwwBdvW.mjs → ios-project-CZhKODF3.mjs} +5 -5
  76. package/dist/{ios-signing-DTTIChGs.mjs → ios-signing-BtAffm0A.mjs} +2 -2
  77. package/dist/{ios-state-DvFX-K_l.mjs → ios-state-Dtw5K2aI.mjs} +1 -1
  78. package/dist/{ios-qNJt4dFm.mjs → ios-xtlRHaeB.mjs} +1 -1
  79. package/dist/{js-swap-Cc1dttps.mjs → js-swap-Bwhh4S7V.mjs} +7 -29
  80. package/dist/{launch-Zq2mOSDy.mjs → launch-BjxemNpK.mjs} +40 -39
  81. package/dist/{launch-CD1yVc7U.mjs → launch-CCzXlc8r.mjs} +16 -16
  82. package/dist/{logs-BS00Anxf.mjs → logs-BfwwadsZ.mjs} +151 -24
  83. package/dist/{logs-CMm05HxN.mjs → logs-jTS_lHSo.mjs} +10 -17
  84. package/dist/{macos-B65--pPd.mjs → macos-DVlSSyUg.mjs} +1 -1
  85. package/dist/{macos-C-0qJ6zq.mjs → macos-NfV2D7dO.mjs} +143 -46
  86. package/dist/macos-run.mjs +1 -1
  87. package/dist/maintenance-run.mjs +8 -8
  88. package/dist/{metro-DEXpIhHC.mjs → metro-DiXtA3Ty.mjs} +59 -37
  89. package/dist/{metro-gateway-BMJNlj3E.mjs → metro-gateway-CKZqKqx7.mjs} +120 -22
  90. package/dist/{named-ports-BWTzt6mu.mjs → named-ports-DkEzOfzO.mjs} +40 -33
  91. package/dist/{native-android-JACyjTZs.mjs → native-android-CqgnOgsT.mjs} +43 -242
  92. package/dist/native-gradle-artifact-inputs-DHFnmrVf.mjs +270 -0
  93. package/dist/{native-gradle-inputs-Bep9JcSF.mjs → native-gradle-inputs-Dnh84Zbd.mjs} +3 -129
  94. package/dist/{native-run-DY_RLx3C.mjs → native-run-NwUuxbkg.mjs} +4 -5
  95. package/dist/{native-runtime-Dg1Q7tfF.mjs → native-runtime-B7KIafpM.mjs} +8 -7
  96. package/dist/native-source-Y0yLhFyQ.mjs +133 -0
  97. package/dist/{native-xcode-inputs-CB2iQeWq.mjs → native-xcode-inputs-D_VOfFL7.mjs} +2 -3
  98. package/dist/{native-xcode-ios-CXX39F7o.mjs → native-xcode-ios-BYDOHWLN.mjs} +27 -32
  99. package/dist/offload-worker.d.mts +65 -2
  100. package/dist/offload-worker.mjs +305 -73
  101. package/dist/operation-exec-BW0em61G.mjs +171 -0
  102. package/dist/{ownership-DEmM5vhh.mjs → ownership-C5fo5Edb.mjs} +3 -3
  103. package/dist/{page-CPaHeYqj.mjs → page-DBL2PiUU.mjs} +8 -4
  104. package/dist/{placement-log-BqGbTa61.mjs → placement-log-BnGgJRRZ.mjs} +1 -1
  105. package/dist/{plan-placement-Bwj-IjY1.mjs → plan-placement-DIsMtN-d.mjs} +12 -8
  106. package/dist/{ports-D-MmCTdv.mjs → ports-Cy1RkmuQ.mjs} +3 -3
  107. package/dist/{prebuild-uD_ipCxF.mjs → prebuild-D4JoEAEs.mjs} +49 -14
  108. package/dist/{project-BZNjmW_8.mjs → project-wM_XFTvE.mjs} +16 -16
  109. package/dist/{pull-request-Lfetn8Kr.mjs → pull-request-W1IuLXDt.mjs} +102 -20
  110. package/dist/{pull-request-zjCSAryH.d.mts → pull-request-eODyIqZF.d.mts} +6 -2
  111. package/dist/pull-requests.d.mts +2 -2
  112. package/dist/pull-requests.mjs +2 -2
  113. package/dist/{react-native-android-DuzH0cn3.mjs → react-native-android-6OZ7MSM7.mjs} +71 -48
  114. package/dist/{react-native-build-rhk4smwQ.mjs → react-native-build-Dvd8hC21.mjs} +8 -8
  115. package/dist/{react-native-doctor-CQpuIMso.mjs → react-native-doctor-DsoSF1Y9.mjs} +7 -7
  116. package/dist/{react-native-ios-Bhq1rJeV.mjs → react-native-ios-4u1Rn-GR.mjs} +40 -26
  117. package/dist/{recordings-DYteQSu4.mjs → recordings-CQmfzAh7.mjs} +1 -1
  118. package/dist/registration-BcRgF6Ox.mjs +51 -0
  119. package/dist/{reload-CmTRFJaX.mjs → reload-BzkYabB-.mjs} +16 -15
  120. package/dist/{reload-DzjSl5P_.mjs → reload-D7AVlb8N.mjs} +1 -1
  121. package/dist/{remote-ByYkXdGD.mjs → remote-DjGDmlTZ.mjs} +18 -15
  122. package/dist/{result-DNiS-tTP.mjs → result-D2Ykz_iD.mjs} +24 -23
  123. package/dist/{run-D23HmJMs.mjs → run-DidHghD1.mjs} +3 -2
  124. package/dist/{selection-CSl9i5qU.mjs → selection-DSMbWEX2.mjs} +6 -5
  125. package/dist/{server-bare-CxTrvDg8.mjs → server-bare-CafzuyFB.mjs} +2 -2
  126. package/dist/{server-command-CtzloH3v.mjs → server-command-C6QjeupS.mjs} +5 -5
  127. package/dist/{server-expo-DT_F_uVI.mjs → server-expo-CrjB29pq.mjs} +7 -5
  128. package/dist/server-expo-D58c9L0M.mjs +2 -0
  129. package/dist/{settings-CsJDEXgK.mjs → settings-C7O8A6ZI.mjs} +223 -243
  130. package/dist/{settings-T1gtc3bf.mjs → settings-DpR-1Wnn.mjs} +1 -1
  131. package/dist/{settings-write-HyVZ8eQ8.mjs → settings-write-CzD60rOD.mjs} +7 -6
  132. package/dist/settings.schema.json +13 -0
  133. package/dist/{simslim-Cgk17SXV.mjs → simslim-BNckWpXi.mjs} +2 -2
  134. package/dist/{slot-launch-DF8VfT64.mjs → slot-launch-CbfmNgM6.mjs} +6 -6
  135. package/dist/{stage-BIS0ydsV.mjs → stage-Bps5oius.mjs} +21 -4
  136. package/dist/{start-RChynRDp.mjs → start-Br-oxqMD.mjs} +1 -1
  137. package/dist/{start-7lpH7hmN.mjs → start-EojN4WFz.mjs} +622 -393
  138. package/dist/{state-C4ZtUisW.mjs → state--smjFSTL.mjs} +22 -7
  139. package/dist/{state-XMziexv2.mjs → state-08I3hw3_.mjs} +2 -2
  140. package/dist/{stats-53T7OTb9.mjs → stats-D3GerlFW.mjs} +5 -5
  141. package/dist/{stats-report-BFfMpWWF.mjs → stats-report-BzaPrRm6.mjs} +1 -1
  142. package/dist/{status-DqEUSSxs.mjs → status-BRROykC8.mjs} +2 -2
  143. package/dist/{status-CSi6tlRq.mjs → status-EvrK72L2.mjs} +1 -1
  144. package/dist/{status-BL7jVyAB.mjs → status-WBDajew2.mjs} +43 -29
  145. package/dist/{status-watch-GDCfqpTE.mjs → status-watch-Bg_u-pwR.mjs} +48 -16
  146. package/dist/status-worker.mjs +1 -1
  147. package/dist/{stim-desktop-C12we7lz.mjs → stim-desktop-D5OGpYZM.mjs} +2 -2
  148. package/dist/{stop-Bk_1qiV6.mjs → stop-DTZ9uTw3.mjs} +2 -2
  149. package/dist/{stop-D7h6jAXQ.mjs → stop-Dky10AEJ.mjs} +31 -22
  150. package/dist/{stop-CPzlM1L-.mjs → stop-Mv6W9FAk.mjs} +15 -16
  151. package/dist/supervisor-run.d.mts +3 -3
  152. package/dist/supervisor-run.mjs +28 -36
  153. package/dist/{support-Dlu37g6H.mjs → support-CNioewsd.mjs} +10 -10
  154. package/dist/{support-67vraa1Q.mjs → support-yUiOjnVY.mjs} +8 -8
  155. package/dist/swiftpm-artifact-inputs-DFcGsXDP.mjs +153 -0
  156. package/dist/swiftpm-macos-BIak-Rxv.mjs +532 -0
  157. package/dist/toolchain-BY8X8HXx.mjs +2 -0
  158. package/dist/{toolchain-Hp9sCpK4.mjs → toolchain-DpJMHq0n.mjs} +9 -6
  159. package/dist/{trigger-BAahxUPq.mjs → trigger-Cbyc41d_.mjs} +3 -3
  160. package/dist/trigger-yb_nFzPF.mjs +2 -0
  161. package/dist/{warm-progress-BHX5r0Ei.mjs → warm-progress-CA3TgT3-.mjs} +29 -16
  162. package/dist/web-CZ07gd77.mjs +2 -0
  163. package/dist/{web-CJmIj6AI.mjs → web-DKx6yoFA.mjs} +11 -10
  164. package/dist/web-run.mjs +11 -9
  165. package/dist/windows-report.mjs +1 -1
  166. package/dist/{workspace-state-Cjuigeeu.mjs → workspace-state-U08dEzhG.mjs} +3 -3
  167. package/dist/{workspaces-BccMFmGQ.mjs → workspaces-CpANmBlq.mjs} +594 -266
  168. package/dist/{worktree-DQ6Zi2Mv.mjs → worktree-CHecZ4Oq.mjs} +850 -551
  169. package/dist/worktree-ZqZVjD9K.mjs +2 -0
  170. package/dist/{xcode-Zlx3qjEs.mjs → xcode-BXn9-xYx.mjs} +5 -5
  171. package/package.json +7 -7
  172. package/shim/android-audio/Main.java +24 -0
  173. package/shim/android-audio/README.md +17 -0
  174. package/shim/android-audio.jar +0 -0
  175. package/dist/gc-C-WwL0yi.mjs +0 -3
  176. package/dist/git-C3iTsJaZ.mjs +0 -73
  177. package/dist/server-expo-C-90EhcG.mjs +0 -2
  178. package/dist/settings-DkdmlelE.d.mts +0 -15
  179. package/dist/swiftpm-macos-p8WZbxKU.mjs +0 -130
  180. package/dist/toolchain-DhKwp9iI.mjs +0 -2
  181. package/dist/trigger-Bf3uDcpv.mjs +0 -2
  182. package/dist/web-DknQETPs.mjs +0 -2
  183. package/dist/workspace-process-lock-1_1OoVW5.mjs +0 -57
  184. package/dist/worktree-CK2MpJW5.mjs +0 -2
@@ -1,45 +1,47 @@
1
- import { f as ownership_claim_exports, n as isTimeoutError, p as process_identity_exports, t as getExecutor } from "./exec-_UbtCQcG.mjs";
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
2
  import { d as workspaceDir, l as workspaceAgentDeviceDir } from "./paths-DQtTgsBf.mjs";
3
- import { E as upsertProject, K as projectDeviceSlots, R 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-CYcBF478.mjs";
4
- import { a as formatElapsed, h as shortUdid, i as formatDuration, l as phaseLine, p as releasedLeaseFact, u as plural } from "./command-output-B1UrjZHL.mjs";
5
- import { i as recordWorkspaceUse, r as readWorkspaceState$1 } from "./workspace-state-Cjuigeeu.mjs";
6
- import { At as locallyKnownUpstream, Bt as restoreFile, Ct as hasRemote, Et as isPodInstallChurn, Ht as unpushedCommits, K as resolveIosProjectDir, Lt as resolveFullRef, Mt as podsOutOfSync, Nt as readWorktreeExclude, Ot as listWorktrees, Pt as removeWorktree, Vt as sourceCheckoutOf, Wt as warmWorktreePaths, Y as resolveSettings, a as SETTING_SHAPE_REMEDY, bt as gitCommonDirOnDisk, dt as branchExists, et as settingShapeErrors, ft as cloneIgnoredEntries, gt as dirtyPaths, it as unknownSettingKeys, mt as depsOutOfSync, pt as deleteBranch, q as resolveProjectSettings, vt as gitCommonDir, wt as hasUncommittedWork, xt as hasPopulatedSubmodules, z as readCommittedSettings } from "./settings-CsJDEXgK.mjs";
7
- import { r as gitQuiet, t as git } from "./git-C3iTsJaZ.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-BZNjmW_8.mjs";
9
- import { t as macosAppPresent } from "./state-XMziexv2.mjs";
10
- import { f as resolveProjectMetro, l as pidExists, p as signalProcessTree, r as killMetroTree } from "./metro-DEXpIhHC.mjs";
11
- import { o as readWebRecord } from "./state-C4ZtUisW.mjs";
12
- import { 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-Bs6SxxzO.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
13
  import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
14
- import { p as cacheUser } from "./native-run-DY_RLx3C.mjs";
15
- import { D as teardownOwnedBrowser, E as teardownOwnedAvd, O as teardownOwnedIosSim, a as collectWorkspaceOutputs, d as emptyWorkspaceDir, f as withIdleWorkspace, g as stopTunnel, m as resolveSupervisorTarget, x as withManagedTunnelRemovalLock, y as withManagedRemoteWorktreeRemovalLock } from "./workspaces-BccMFmGQ.mjs";
16
- import { D as releaseWorkspaceLeases, t as agentDeviceDaemon } from "./activity-BzVfHSrY.mjs";
17
- import { ht as parkedMaxSetting, lt as POOL_SETTING_REMEDY } from "./android-DUYkNF4u.mjs";
18
- import { F as readHostMemoryPressureAsync, P as readHostMemoryPressure } from "./ios-CpByZy12.mjs";
19
- import { a as workspaceLinkLine, o as workspaceLinks } from "./stim-desktop-C12we7lz.mjs";
20
- import { t as clearNamedPorts } from "./named-ports-BWTzt6mu.mjs";
21
- import { r as HEARTBEAT_INTERVAL_MS, x as podInstallCommand } from "./xcode-Zlx3qjEs.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
22
  import { t as resolveMaintenanceSettings } from "./settings-Djl-mpjX.mjs";
23
- import { _ as runPodInstall, f as DEPS_ERROR, g as runCaptured, m as readPodState, p as podsAreStale } from "./toolchain-Hp9sCpK4.mjs";
24
- import { i as stopHostedAndroid } from "./hosted-android-Bw-honsd.mjs";
25
- import { r as stopHostedIos } from "./hosted-ios-CUvOf4Vr.mjs";
26
- import { n as stopMacosApp } from "./stop-D7h6jAXQ.mjs";
27
- import { n as verifyCollectorOwnership } from "./ownership-DEmM5vhh.mjs";
28
- import { a as discoverXcodeProject } from "./react-native-build-rhk4smwQ.mjs";
29
- import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-BbKQW83h.mjs";
30
- import { a as forgetStatusMeasures, f as mergeState, t as startWarmProgress } from "./warm-progress-BHX5r0Ei.mjs";
31
- import { a as pullRequestLookup, r as endedPullRequest } from "./pull-request-Lfetn8Kr.mjs";
32
- import { r as workspaceRecordingEnabled } from "./recordings-DYteQSu4.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";
33
+ import { claimRemoveCommand, clearFreeClaimSet, readClaimSet, releaseClaim, tryAcquireClaim } from "@stim-cli/core/ownership-claim";
33
34
  import { createHash, randomUUID } from "node:crypto";
34
- import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
35
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
35
36
  import { cpus, freemem, homedir, loadavg, totalmem } from "node:os";
36
37
  import { basename, dirname, join, relative } from "node:path";
37
- import { buildCacheRoot, configDir, metroCacheRoot, unitLines, unitWords, withDirLock, workspaceName } from "@stim-cli/core";
38
- import { LAST_BUILD_KEYS, archiveDir, archiveEnabled, archiveLock, archiveRoot, archivedUsage, buildWorkerRoot, countErrorEntries, endedAgentSessionOf, isJsonObject, listRecordedDevices, loadConfig, queryLogs, readArchives, readBuildHistory, readJsonObject, readLastBuilds, readMacosRecord, readPullRequestCache, readWorkspaceState, recordingEnabled, sharedCcache, sharedCompilationCache, sortByTs } from "@stim-cli/core/state";
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";
39
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";
40
- import { claimRemoveCommand, clearFreeClaimSet, readClaimSet, releaseClaim, tryAcquireClaim } from "@stim-cli/core/ownership-claim";
41
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
41
42
  import chalk from "chalk";
42
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";
43
45
  import { createHash as createHash$1 } from "crypto";
44
46
  //#region src/maintenance/pressure.ts
45
47
  /**
@@ -81,15 +83,22 @@ function measureDisk() {
81
83
  buildWorkerRoot(loadConfig())
82
84
  ]);
83
85
  }
86
+ function measureDiskAsync(operation) {
87
+ return readVolumeSpaceAsync([
88
+ configDir(),
89
+ ...Object.keys(loadConfig()?.projects ?? {}),
90
+ buildWorkerRoot(loadConfig())
91
+ ], operation.signal);
92
+ }
84
93
  /** The disk pressure level of the fullest volume Stim writes to, measured now. */
85
- function currentDiskLevel() {
86
- return diskPressureLevel(lowestFree(measureDisk()), resolveBudget().budget, resolveMaintenanceSettings());
94
+ function currentDiskLevel(disk = measureDisk()) {
95
+ return diskPressureLevel(lowestFree(disk), resolveBudget().budget, resolveMaintenanceSettings());
87
96
  }
88
97
  function measurePressure(settings, previous, now) {
89
98
  return pressureFromObservations(settings, previous, now, readHostMemoryPressure(), measureDisk());
90
99
  }
91
100
  async function measurePressureAsync(settings, previous, now, operation) {
92
- return pressureFromObservations(settings, previous, now, await readHostMemoryPressureAsync(), await readVolumeSpaceAsync([
101
+ return pressureFromObservations(settings, previous, now, await readHostMemoryPressureAsync(void 0, void 0, operation.signal, operation), await readVolumeSpaceAsync([
93
102
  configDir(),
94
103
  ...Object.keys(loadConfig()?.projects ?? {}),
95
104
  buildWorkerRoot(loadConfig())
@@ -387,7 +396,7 @@ const KIND_PATH = {
387
396
  recordings: "recordings",
388
397
  agentActions: "agent-device/sessions"
389
398
  };
390
- function archiveSettings() {
399
+ function archiveSettings(env = process.env) {
391
400
  const config = loadConfig();
392
401
  return Object.fromEntries([
393
402
  "maxAgeDays",
@@ -399,57 +408,63 @@ function archiveSettings() {
399
408
  "recordings.maxTotalGb",
400
409
  "agentActions.maxAgeDays"
401
410
  ].map((key) => {
402
- const result = machineNumber(`archive.${key}`, config, process.env);
411
+ const result = machineNumber(`archive.${key}`, config, env);
403
412
  if (result.error) throw new Error(result.error);
404
413
  return [key, result.value];
405
414
  }));
406
415
  }
407
- function enabled(root) {
416
+ function enabled(root, env) {
408
417
  const config = loadConfig();
409
418
  const project = config?.projects?.[root];
410
419
  const present = existsSync(root);
411
420
  const common = present ? gitCommonDirOnDisk(root) : null;
412
421
  if (!present && Object.values(config?.repos ?? {}).some((repo) => repo.settings?.archive?.enabled === false)) return false;
413
- return archiveEnabled(process.env, [
422
+ return archiveEnabled(env, [
414
423
  project?.settings,
415
424
  common ? config?.repos?.[common]?.settings : void 0,
416
425
  present ? readCommittedSettings(root) : void 0,
417
426
  config
418
427
  ]);
419
428
  }
420
- function files(dir) {
429
+ async function files(dir, signal) {
430
+ signal?.throwIfAborted();
421
431
  if (!existsSync(dir)) return [];
422
- return readdirSync(dir).flatMap((name) => {
432
+ const found = [];
433
+ for (const name of await readdir(dir)) {
434
+ signal?.throwIfAborted();
423
435
  const path = join(dir, name);
424
- const stat = lstatSync(path);
425
- if (stat.isSymbolicLink()) throw new Error(`refusing symlink ${path}`);
426
- 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({
427
440
  path,
428
- bytes: stat.size,
429
- at: stat.mtimeMs
430
- }];
431
- });
441
+ bytes: entry.size,
442
+ at: entry.mtimeMs
443
+ });
444
+ }
445
+ return found;
432
446
  }
433
- function secureTree(dir) {
434
- if (lstatSync(dir).isSymbolicLink()) throw new Error(`refusing symlink ${dir}`);
435
- chmodSync(dir, 448);
436
- 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)) {
437
451
  const path = join(dir, name);
438
- const stat = lstatSync(path);
439
- if (stat.isSymbolicLink()) throw new Error(`refusing symlink ${path}`);
440
- if (stat.isDirectory()) secureTree(path);
441
- 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);
442
456
  }
443
457
  }
444
- function writeRecord(dir, record) {
445
- const small = [
458
+ async function writeRecord(dir, record) {
459
+ let small = 0;
460
+ for (const name of [
446
461
  "state.json",
447
462
  "ended-agents.json",
448
463
  "log-error-index.json"
449
- ].reduce((sum, name) => {
464
+ ]) {
450
465
  const path = join(dir, name);
451
- return sum + (existsSync(path) ? statSync(path).size : 0);
452
- }, 0);
466
+ if (existsSync(path)) small += (await stat(path)).size;
467
+ }
453
468
  for (let pass = 0; pass < 8; pass++) {
454
469
  record.bytes.total = KINDS.reduce((sum, kind) => sum + record.bytes[kind], record.bytes.record);
455
470
  const bytes = small + Buffer.byteLength(`${JSON.stringify(record)}\n`);
@@ -459,59 +474,70 @@ function writeRecord(dir, record) {
459
474
  record.bytes.total = KINDS.reduce((sum, kind) => sum + record.bytes[kind], record.bytes.record);
460
475
  const tmp = join(dir, `.archive-${randomUUID()}.tmp`);
461
476
  try {
462
- writeFileSync(tmp, `${JSON.stringify(record)}\n`, { mode: 384 });
463
- renameSync(tmp, join(dir, "archive.json"));
477
+ await writeFile(tmp, `${JSON.stringify(record)}\n`, { mode: 384 });
478
+ await rename(tmp, join(dir, "archive.json"));
464
479
  } finally {
465
- rmSync(tmp, { force: true });
480
+ await rm(tmp, { force: true });
466
481
  }
467
482
  const now = /* @__PURE__ */ new Date();
468
- utimesSync(archiveRoot(), now, now);
483
+ await utimes(archiveRoot(), now, now);
469
484
  }
470
- function removeRecord(id) {
485
+ async function removeRecord(id) {
471
486
  const dir = archiveDir(id);
472
487
  if (!existsSync(dir)) return;
473
488
  const removing = join(archiveRoot(), `.removing-${id}`);
474
- renameSync(dir, removing);
475
- rmSync(removing, {
489
+ await rename(dir, removing);
490
+ await rm(removing, {
476
491
  recursive: true,
477
492
  force: true
478
493
  });
479
494
  }
480
- function sweepArchiveStaging(remove, waitMs = 0) {
495
+ async function sweepArchiveStaging(remove, waitMs = 0, signal) {
496
+ signal?.throwIfAborted();
481
497
  if (!existsSync(archiveRoot())) return [];
482
- 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) => {
483
- const path = join(archiveRoot(), name.endsWith(".claims") ? name.slice(0, -7) : name);
484
- const root = `${path}.claims`;
485
- const survey = name.startsWith(".incoming-") ? readClaimSet(root) : null;
486
- const unresolved = survey?.unresolved[0];
487
- const kept = Boolean(survey && (survey.live.length || survey.unresolved.length));
488
- const entry = {
489
- path,
490
- kept,
491
- reason: unresolved?.reason ?? (kept ? "live archive writer" : null),
492
- removeCommand: unresolved ? claimRemoveCommand(root) : null
493
- };
494
- if (remove && !kept) {
495
- if (!survey || clearFreeClaimSet({
496
- root,
497
- label: "archive staging"
498
- }).status === "cleared") {
499
- rmSync(path, {
500
- recursive: true,
501
- force: true
502
- });
503
- 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
+ }
504
526
  }
527
+ entries.push(entry);
505
528
  }
506
- return [entry];
507
- }), { waitMs });
529
+ return entries;
530
+ }, {
531
+ waitMs,
532
+ signal
533
+ });
508
534
  }
509
535
  function stripPids(value) {
510
536
  if (Array.isArray(value)) return value.map(stripPids);
511
537
  if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).filter(([key]) => !/pids?$/i.test(key)).map(([key, child]) => [key, stripPids(child)]));
512
538
  return value;
513
539
  }
514
- function gitFacts(root) {
540
+ async function gitFacts(root, operation) {
515
541
  const empty = {
516
542
  repository: null,
517
543
  branch: null,
@@ -521,32 +547,55 @@ function gitFacts(root) {
521
547
  pullRequest: null
522
548
  };
523
549
  if (!existsSync(root)) return cachedFacts(root) ?? empty;
524
- 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
+ };
525
571
  const common = gitCommonDirOnDisk(root);
526
- const summary = run([
572
+ const summary = (await run([
527
573
  "show",
528
574
  "-s",
529
575
  "--format=%H%n%s",
530
576
  "HEAD"
531
- ])?.split("\n");
532
- const location = run([
577
+ ]))?.split("\n");
578
+ const location = (await run([
533
579
  "rev-parse",
534
580
  "--show-toplevel",
535
581
  "--abbrev-ref",
536
582
  "HEAD"
537
- ])?.split("\n");
583
+ ]))?.split("\n");
538
584
  const branch = location?.[1] && location[1] !== "HEAD" ? location[1] : null;
539
- const defaultRef = run([
585
+ const defaultRef = await run([
540
586
  "symbolic-ref",
541
587
  "--quiet",
542
588
  "refs/remotes/origin/HEAD"
543
589
  ]);
544
590
  let merged = null;
545
591
  if (defaultRef?.startsWith("refs/remotes/")) {
546
- const state = mergeState(root, {
592
+ const state = await mergeStateAsync(root, {
547
593
  ref: defaultRef,
548
594
  name: defaultRef.slice(13)
549
- }, { timeoutMs: 2e3 });
595
+ }, {
596
+ ...execution,
597
+ timeoutMs: 2e3
598
+ });
550
599
  merged = state.merged ? true : state.unknown ? null : false;
551
600
  }
552
601
  let prPath = root;
@@ -587,35 +636,48 @@ function cachedFacts(root) {
587
636
  }
588
637
  return null;
589
638
  }
590
- function archiveWorkspace(root, removedBy, state = readWorkspaceState(root)) {
591
- 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();
592
648
  try {
593
- enforceArchiveRetention();
649
+ await enforceArchiveRetention(void 0, operation);
594
650
  } catch (error) {
595
- 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);
596
654
  }
597
655
  }
598
- function writeArchive(root, removedBy, state) {
656
+ async function writeArchive(root, removedBy, state, operation) {
657
+ const env = operation?.env ?? process.env;
599
658
  let staging = null;
600
659
  let claim;
601
660
  try {
602
661
  const dir = workspaceDir(root);
603
- if (!existsSync(dir) || lstatSync(dir).isSymbolicLink()) return;
662
+ if (!existsSync(dir) || (await lstat(dir)).isSymbolicLink()) return;
604
663
  if (existsSync(root)) root = realpathSync(root);
605
664
  try {
606
- sweepArchiveStaging(true, 5e3);
607
- } catch {}
608
- if (!enabled(root)) return;
609
- 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);
610
671
  if (!limits.maxAgeDays || !limits.maxCount) return;
611
672
  const logs = join(dir, "logs");
612
- if (!existsSync(join(dir, "state.json")) && !Object.values(readBuildHistory(state)).some((entries) => entries?.length) && (!existsSync(logs) || readdirSync(logs).length === 0)) return;
613
- const worktree = gitFacts(root);
614
- 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(), {
615
677
  recursive: true,
616
678
  mode: 448
617
679
  });
618
- chmodSync(archiveRoot(), 448);
680
+ await chmod(archiveRoot(), 448);
619
681
  const token = randomUUID();
620
682
  staging = join(archiveRoot(), `.incoming-${token}`);
621
683
  const attempt = tryAcquireClaim({
@@ -628,49 +690,56 @@ function writeArchive(root, removedBy, state) {
628
690
  releaseClaim(attempt.pending);
629
691
  throw new Error("archive staging claim is held");
630
692
  }
631
- withDirLock(archiveLock(), () => {
632
- 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 });
633
702
  let at = Date.now();
634
703
  const workspace = workspaceName(root);
635
704
  while (existsSync(archiveDir(`${workspace}--${at}`))) at++;
636
705
  const id = `${workspace}--${at}`;
637
706
  if (state) {
638
707
  const { supervisor: _supervisor, collectors: _collectors, warm: _warm, ...stable } = state;
639
- 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 });
640
709
  }
641
710
  for (const name of ["ended-agents.json", "log-error-index.json"]) {
642
711
  const data = readJsonObject(join(dir, name));
643
- 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 });
644
713
  }
645
- const move = (kind) => {
714
+ const move = async (kind) => {
646
715
  const from = join(dir, KIND_PATH[kind]);
647
716
  if (!limits[`${kind}.maxAgeDays`] || !existsSync(from)) return;
648
717
  const to = join(staging, KIND_PATH[kind]);
649
- mkdirSync(join(to, ".."), {
718
+ await mkdir(join(to, ".."), {
650
719
  recursive: true,
651
720
  mode: 448
652
721
  });
653
- renameSync(from, to);
654
- secureTree(to);
722
+ await moveHistory(from, to);
723
+ await secureTree(to);
655
724
  };
656
- move("logs");
657
- move("agentActions");
725
+ await move("logs");
726
+ await move("agentActions");
658
727
  const config = loadConfig();
659
- if (limits["recordings.maxAgeDays"] && workspaceRecordingEnabled(root, config?.projects?.[root] ?? {}, config, process.env)) {
728
+ if (limits["recordings.maxAgeDays"] && workspaceRecordingEnabled(root, config?.projects?.[root] ?? {}, config, env)) {
660
729
  const recordingDir = join(dir, "recordings");
661
- if (existsSync(recordingDir) && lstatSync(recordingDir).isSymbolicLink()) throw new Error(`refusing symlink ${recordingDir}`);
662
- for (const device of listRecordedDevices(recordingDir)) {
663
- 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}`);
664
733
  const to = join(staging, "recordings", `${device.platform}-${device.slot}`);
665
734
  for (const segment of device.segments.filter((entry) => !entry.open)) {
666
- mkdirSync(to, {
735
+ await mkdir(to, {
667
736
  recursive: true,
668
737
  mode: 448
669
738
  });
670
739
  try {
671
- if (lstatSync(segment.file).isSymbolicLink()) throw new Error(`refusing symlink ${segment.file}`);
672
- renameSync(segment.file, join(to, basename(segment.file)));
673
- 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);
674
743
  } catch (error) {
675
744
  if (error.code !== "ENOENT") throw error;
676
745
  }
@@ -699,7 +768,7 @@ function writeArchive(root, removedBy, state) {
699
768
  record: new Date(at + limits.maxAgeDays * DAY).toISOString()
700
769
  };
701
770
  for (const kind of KINDS) {
702
- 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);
703
772
  if (bytes[kind]) expires[kind] = new Date(at + limits[`${kind}.maxAgeDays`] * DAY).toISOString();
704
773
  }
705
774
  const record = {
@@ -721,102 +790,144 @@ function writeArchive(root, removedBy, state) {
721
790
  expires,
722
791
  version: 1
723
792
  };
724
- writeRecord(staging, record);
725
- renameSync(staging, archiveDir(id));
726
- }, { 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
+ });
727
800
  releaseClaim(claim);
728
801
  claim = void 0;
729
802
  staging = null;
730
803
  } catch (error) {
804
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
731
805
  if (staging) try {
732
- rmSync(staging, {
806
+ await rm(staging, {
733
807
  recursive: true,
734
808
  force: true
735
809
  });
736
810
  } catch {}
737
- 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);
738
813
  } finally {
739
814
  releaseClaim(claim);
740
815
  }
741
816
  }
742
- function removeKind(record, kind) {
743
- rmSync(join(archiveDir(record.id), KIND_PATH[kind]), {
817
+ async function removeKind(record, kind) {
818
+ await rm(join(archiveDir(record.id), KIND_PATH[kind]), {
744
819
  recursive: true,
745
820
  force: true
746
821
  });
747
822
  record.bytes[kind] = 0;
748
823
  record.expires[kind] = null;
749
- writeRecord(archiveDir(record.id), record);
824
+ await writeRecord(archiveDir(record.id), record);
750
825
  }
751
826
  function logRemovalOrder(path) {
752
827
  return /\.ndjson\.\d+$/.test(path) ? 0 : /^build-.*\.ndjson$/.test(basename(path)) ? 1 : 2;
753
828
  }
754
- 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();
755
833
  if (!existsSync(archiveRoot())) return;
756
834
  let limits;
757
835
  try {
758
- limits = archiveSettings();
836
+ limits = archiveSettings(env);
759
837
  } catch (error) {
760
- 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);
761
839
  return;
762
840
  }
763
- withDirLock(archiveLock(), () => {
764
- const listed = readArchives().toReversed();
841
+ await withDirLockTransaction(archiveLock(), async () => {
842
+ const listed = (await readArchivesAsync(signal)).toReversed();
765
843
  const records = listed.filter((record, index) => limits.maxAgeDays > 0 && now - Date.parse(record.removedAt) < limits.maxAgeDays * DAY && index >= listed.length - limits.maxCount);
766
- for (const record of listed) if (!records.includes(record)) removeRecord(record.id);
767
- 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()]);
768
849
  for (const record of records) {
850
+ signal?.throwIfAborted();
769
851
  for (const kind of [
770
852
  "recordings",
771
853
  "agentActions",
772
854
  "logs"
773
855
  ]) {
856
+ signal?.throwIfAborted();
774
857
  const age = limits[`${kind}.maxAgeDays`];
775
- 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);
776
859
  }
777
860
  const cap = limits["logs.maxMbPerWorkspace"] * 1024 ** 2;
778
861
  if (record.bytes.logs > cap) {
779
- 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);
780
- for (const file of entries) {
781
- if (record.bytes.logs <= cap) break;
782
- rmSync(file.path);
783
- 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
+ }
784
877
  }
785
- if (!record.bytes.logs) record.expires.logs = null;
786
- writeRecord(archiveDir(record.id), record);
787
878
  }
788
879
  }
789
880
  const totalCap = limits.maxTotalGb * 1024 ** 3;
790
881
  const recordingCap = limits["recordings.maxTotalGb"] * 1024 ** 3;
791
- for (const record of records) for (;;) {
792
- const usage = archivedUsage(records);
793
- const totalOver = usage.bytes > totalCap;
794
- const recordingsOver = usage.byKind.recordings > recordingCap;
795
- if (!totalOver && !recordingsOver) break;
796
- const candidates = KINDS.filter((kind) => record.bytes[kind] > 0 && (totalOver || kind === "recordings")).toSorted((a, b) => record.bytes[b] - record.bytes[a]);
797
- if (!candidates.length) break;
798
- 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
+ }
799
894
  }
800
895
  for (const record of records) {
896
+ signal?.throwIfAborted();
801
897
  const before = JSON.stringify(record.expires);
802
898
  for (const kind of KINDS) record.expires[kind] = record.bytes[kind] ? new Date(Date.parse(record.removedAt) + limits[`${kind}.maxAgeDays`] * DAY).toISOString() : null;
803
899
  record.expires.record = new Date(Date.parse(record.removedAt) + limits.maxAgeDays * DAY).toISOString();
804
- if (JSON.stringify(record.expires) !== before) writeRecord(archiveDir(record.id), record);
900
+ if (JSON.stringify(record.expires) !== before) await writeRecord(archiveDir(record.id), record);
805
901
  }
806
- }, { waitMs: 3e4 });
902
+ }, {
903
+ waitMs: 3e4,
904
+ signal
905
+ });
807
906
  }
808
- function deleteSelectedArchives(ids, kind) {
809
- withDirLock(archiveLock(), () => {
810
- for (const record of readArchives().filter((entry) => ids.includes(entry.id))) if (kind) removeKind(record, kind);
811
- else removeRecord(record.id);
812
- }, { 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
+ });
813
919
  }
814
920
  //#endregion
815
921
  //#region src/devices/reclaim.ts
816
- 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 }) {
817
927
  const skippedDevices = [];
818
928
  const failedDevices = [];
819
929
  for (const [platform, record] of Object.entries(collectors)) {
930
+ operation?.signal.throwIfAborted();
820
931
  const rec = record;
821
932
  const pid = rec?.pid;
822
933
  if (typeof pid !== "number" || pid <= 0 || pid === process.pid || !pidExists(pid)) continue;
@@ -840,7 +951,7 @@ async function reapCollectors(root, { verify = verifyCollectorOwnership, collect
840
951
  }
841
952
  try {
842
953
  process.kill(pid, "SIGTERM");
843
- if (await (0, process_identity_exports.waitForProcessExit)(rec, 5e3)) continue;
954
+ if (await (0, process_identity_exports.waitForProcessExit)(rec, 5e3, operation?.signal)) continue;
844
955
  } catch (error) {
845
956
  if (error.code === "ESRCH") continue;
846
957
  }
@@ -857,7 +968,7 @@ async function reapCollectors(root, { verify = verifyCollectorOwnership, collect
857
968
  failedDevices
858
969
  };
859
970
  }
860
- async function reclaimRemoteSession(root, { stopSession = defaultStopSession } = {}) {
971
+ async function reclaimRemoteSession(root, { stopSession = defaultStopSession, operation } = {}) {
861
972
  const sessionId = readRemoteSessionId(root);
862
973
  if (!sessionId) return {
863
974
  stopped: null,
@@ -865,8 +976,10 @@ async function reclaimRemoteSession(root, { stopSession = defaultStopSession } =
865
976
  };
866
977
  let result;
867
978
  try {
868
- result = await stopSession(root, sessionId);
979
+ result = await stopSession(root, sessionId, operation);
980
+ operation?.signal.throwIfAborted();
869
981
  } catch (err) {
982
+ throwIfCleanupInterrupted(err, operation);
870
983
  result = {
871
984
  status: "failed",
872
985
  reason: String(err?.message ?? err)
@@ -896,17 +1009,18 @@ async function reclaimRemoteSession(root, { stopSession = defaultStopSession } =
896
1009
  }
897
1010
  };
898
1011
  }
899
- function defaultStopSession(root, sessionId) {
1012
+ function defaultStopSession(root, sessionId, operation) {
900
1013
  return endRecordedSession({
901
1014
  root,
902
1015
  sessionId,
903
- easBin: resolveEasCliBin(root)?.file ?? null
1016
+ easBin: resolveEasCliBin(root)?.file ?? null,
1017
+ operation
904
1018
  });
905
1019
  }
906
- function defaultStopMetroTunnel(record) {
907
- return stopTunnel(record);
1020
+ function defaultStopMetroTunnel(record, operation) {
1021
+ return stopTunnel(record, { operation });
908
1022
  }
909
- async function reclaimMetroTunnel(root, { stopMetroTunnel = defaultStopMetroTunnel } = {}) {
1023
+ async function reclaimMetroTunnel(root, { stopMetroTunnel = defaultStopMetroTunnel, operation } = {}) {
910
1024
  const record = readMetroTunnel(root);
911
1025
  if (record?.kind !== "managed") return {
912
1026
  stopped: null,
@@ -914,8 +1028,10 @@ async function reclaimMetroTunnel(root, { stopMetroTunnel = defaultStopMetroTunn
914
1028
  };
915
1029
  let result;
916
1030
  try {
917
- result = await stopMetroTunnel(record);
1031
+ result = await stopMetroTunnel(record, operation);
1032
+ operation?.signal.throwIfAborted();
918
1033
  } catch (err) {
1034
+ throwIfCleanupInterrupted(err, operation);
919
1035
  result = {
920
1036
  status: "failed",
921
1037
  reason: String(err?.message ?? err)
@@ -971,7 +1087,7 @@ function parkRequest(project, projectPath, slot, simslimManaged) {
971
1087
  cacheKey: lastBuildCacheKey(projectPath, "ios")
972
1088
  };
973
1089
  }
974
- async function reclaimOwnedDevices(project, projectPath, { park = false } = {}) {
1090
+ async function reclaimOwnedDevices(project, projectPath, { park = false, operation } = {}) {
975
1091
  const deletedDevices = [];
976
1092
  const parkedDevices = [];
977
1093
  const evictedDevices = [];
@@ -979,11 +1095,13 @@ async function reclaimOwnedDevices(project, projectPath, { park = false } = {})
979
1095
  const skippedDevices = [];
980
1096
  const failedDevices = [];
981
1097
  for (const { slot, platforms } of projectDeviceSlots(project)) {
1098
+ operation?.signal.throwIfAborted();
982
1099
  const ios = platforms.ios;
983
1100
  if (ios?.owned && ios.deviceUdid) {
984
1101
  const udid = ios.deviceUdid;
985
1102
  const label = ios.deviceName || udid;
986
1103
  const r = await teardownOwnedIosSim(udid, {
1104
+ operation,
987
1105
  del: true,
988
1106
  label,
989
1107
  ...park ? { park: parkRequest(project, projectPath, slot, Boolean(ios.simslimManaged)) } : {}
@@ -1013,9 +1131,11 @@ async function reclaimOwnedDevices(project, projectPath, { park = false } = {})
1013
1131
  }
1014
1132
  }
1015
1133
  const android = platforms.android;
1134
+ operation?.signal.throwIfAborted();
1016
1135
  if (android?.owned && android.avdName) {
1017
1136
  const bound = parkedMaxSetting("android");
1018
1137
  const r = await teardownOwnedAvd(android.avdName, {
1138
+ operation,
1019
1139
  del: true,
1020
1140
  owner: {
1021
1141
  projectPath,
@@ -1066,6 +1186,7 @@ async function reclaimProject(path, options = {}) {
1066
1186
  const dir = workspaceDir(path);
1067
1187
  const existed = existsSync(dir);
1068
1188
  const run = await withIdleWorkspace(path, () => reclaimIdleProject(path, existed, options), {
1189
+ operation: options.operation,
1069
1190
  purpose: "workspace removal",
1070
1191
  hosted: false,
1071
1192
  supervisor: false,
@@ -1096,10 +1217,52 @@ async function reclaimProject(path, options = {}) {
1096
1217
  failedWorkspaceDirs: []
1097
1218
  };
1098
1219
  }
1099
- 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" } }) {
1100
- 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();
1101
1264
  const project = getProject(path);
1102
- 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);
1103
1266
  const dereferenced = describeDereferenced(project);
1104
1267
  const initialState = readWorkspaceState$1(path);
1105
1268
  let killedPid = null;
@@ -1117,9 +1280,9 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
1117
1280
  const orphanIdentity = supervisor.status === "stale" && typeof orphanServer.processToken === "string" ? (0, process_identity_exports.inspectProcessIdentity)(orphanServer) : null;
1118
1281
  if (supervisor.status === "ours") {
1119
1282
  requestDevServerStop(path, supervisor.processToken, stopRequester);
1120
- const signalled = killMetroTree(supervisor.pid, supervisor.processToken);
1283
+ const signalled = await killMetroTreeAsync(supervisor.pid, supervisor.processToken, operation);
1121
1284
  if (!signalled) withdrawDevServerStopRequest(path, supervisor.processToken);
1122
- 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;
1123
1286
  else {
1124
1287
  skippedMetro = `could not confirm supervisor pid ${supervisor.pid} exited`;
1125
1288
  supervisorHeld = true;
@@ -1130,9 +1293,14 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
1130
1293
  } else if (orphanIdentity === "same") {
1131
1294
  let signalled = false;
1132
1295
  try {
1133
- signalled = signalProcessTree(orphanServer.pid, "SIGTERM", { group: initialState?.supervisor?.mode === MODE_COMMAND });
1134
- } catch {}
1135
- 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;
1136
1304
  else {
1137
1305
  skippedMetro = `could not confirm dev server pid ${orphanServer.pid} left by the supervisor exited`;
1138
1306
  supervisorHeld = true;
@@ -1145,9 +1313,15 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
1145
1313
  }
1146
1314
  const { skippedDevices: skippedCollectors, failedDevices: failedCollectors } = await reapCollectors(path, {
1147
1315
  verify: verifyCollector,
1148
- collectors: initialState?.collectors ?? {}
1316
+ collectors: initialState?.collectors ?? {},
1317
+ operation
1149
1318
  });
1150
- 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();
1151
1325
  if (browser.status === "failed" || browser.status === "skipped") {
1152
1326
  const entry = {
1153
1327
  name: "owned Chrome",
@@ -1165,7 +1339,10 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
1165
1339
  }
1166
1340
  }
1167
1341
  retainReplacementProcesses();
1168
- const { deletedDevices, parkedDevices, evictedDevices, poolNotes, skippedDevices, failedDevices } = deleteOwnedDevices && !supervisorHeld && failedCollectors.length === 0 ? await 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
+ }) : {
1169
1346
  deletedDevices: [],
1170
1347
  parkedDevices: [],
1171
1348
  evictedDevices: [],
@@ -1175,27 +1352,37 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
1175
1352
  };
1176
1353
  skippedDevices.push(...skippedCollectors);
1177
1354
  failedDevices.push(...failedCollectors);
1178
- for (const platform of ["ios", "android"]) try {
1179
- await (platform === "ios" ? stopHostedIos : stopHostedAndroid)(path);
1180
- } catch (error) {
1181
- const failed = {
1182
- platform,
1183
- name: `hosted ${platform} device`,
1184
- reason: error.message
1185
- };
1186
- failedDevices.push(failed);
1187
- 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
+ }
1188
1369
  }
1189
- const remote = await reclaimRemoteSession(path, { stopSession });
1370
+ signal?.throwIfAborted();
1371
+ const remote = await reclaimRemoteSession(path, {
1372
+ stopSession,
1373
+ operation
1374
+ });
1190
1375
  if (remote.failed) {
1191
1376
  skippedDevices.push(remote.failed);
1192
1377
  failedDevices.push(remote.failed);
1193
1378
  }
1194
1379
  if (macosAppPresent(path)) {
1380
+ signal?.throwIfAborted();
1195
1381
  const hosted = readMacosRecord(path)?.host;
1196
1382
  try {
1197
- await stopMacosApp(path);
1383
+ await stopMacosApp(path, operation);
1198
1384
  } catch (error) {
1385
+ throwIfCleanupInterrupted(error, operation);
1199
1386
  const failed = {
1200
1387
  name: hosted ? `hosted macOS session ${hosted.session} on ${hosted.machine}` : "macOS app",
1201
1388
  reason: String(error?.message ?? error)
@@ -1204,52 +1391,39 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
1204
1391
  failedDevices.push(failed);
1205
1392
  }
1206
1393
  }
1207
- const tunnel = await reclaimMetroTunnel(path, { stopMetroTunnel });
1394
+ signal?.throwIfAborted();
1395
+ const tunnel = await reclaimMetroTunnel(path, {
1396
+ stopMetroTunnel,
1397
+ operation
1398
+ });
1208
1399
  if (tunnel.failed) {
1209
1400
  skippedDevices.push(tunnel.failed);
1210
1401
  failedDevices.push(tunnel.failed);
1211
1402
  }
1212
1403
  let releasedLeases = [];
1404
+ signal?.throwIfAborted();
1213
1405
  try {
1214
1406
  releasedLeases = releaseLeases(path);
1215
- } catch {
1407
+ } catch (error) {
1408
+ throwIfCleanupInterrupted(error, operation);
1216
1409
  releasedLeases = [];
1217
1410
  }
1218
1411
  retainReplacementProcesses();
1219
1412
  const removedWorkspaceDirs = [];
1220
1413
  const failedWorkspaceDirs = [];
1221
1414
  if (failedDevices.length === 0 && !supervisorHeld) {
1415
+ signal?.throwIfAborted();
1222
1416
  const dir = workspaceDir(path);
1223
1417
  if (existsSync(dir)) try {
1224
- const stateDir = workspaceAgentDeviceDir(path);
1225
- if (!["none", "dead"].includes(agentDeviceDaemon(stateDir))) {
1226
- const exec = getExecutor();
1227
- try {
1228
- if (!exec.findExecutable("agent-device")) throw new Error("agent-device is not on PATH");
1229
- exec.runFile("agent-device", [
1230
- "daemon",
1231
- "stop",
1232
- "--state-dir",
1233
- stateDir,
1234
- "--clean"
1235
- ], {
1236
- timeoutMs: 2e4,
1237
- killSignal: "SIGKILL",
1238
- env: { AGENT_DEVICE_NO_UPDATE_NOTIFIER: "1" }
1239
- });
1240
- } catch (error) {
1241
- process.stderr.write(`${phaseLine("agent-device", `daemon could not be stopped: ${error.message}; run agent-device daemon stop --state-dir "${stateDir}" --clean`)}\n`);
1242
- throw error;
1243
- }
1244
- }
1245
- if (archive) archiveWorkspace(path, archive.removedBy, initialState);
1246
- emptyWorkspaceDir(dir);
1418
+ await removeWorkspaceOutput(path, dir, archive, initialState, operation);
1247
1419
  if (workspaceExisted) removedWorkspaceDirs.push(dir);
1248
- } catch {
1420
+ } catch (error) {
1421
+ throwIfCleanupInterrupted(error, operation);
1249
1422
  failedWorkspaceDirs.push(dir);
1250
1423
  }
1251
1424
  }
1252
1425
  const keptEntry = supervisorHeld || failedDevices.length > 0 || failedWorkspaceDirs.length > 0;
1426
+ signal?.throwIfAborted();
1253
1427
  if (project && !keptEntry && !preserveProjectRecord) removeProject(path);
1254
1428
  return {
1255
1429
  path,
@@ -1418,7 +1592,7 @@ function warmCommand(phase) {
1418
1592
  function asHolder(holder) {
1419
1593
  return {
1420
1594
  pid: holder.owner.pid,
1421
- phase: holder.mode === "exclusive" ? "refresh" : "copy"
1595
+ phase: holder.details.phase === "copy" ? "copy" : holder.mode === "exclusive" ? "refresh" : "copy"
1422
1596
  };
1423
1597
  }
1424
1598
  function waitingLine(holder, elapsedMs) {
@@ -1504,7 +1678,7 @@ function timeoutError(holder, elapsedMs, root) {
1504
1678
  error.code = "STIM_LOCK_TIMEOUT";
1505
1679
  return error;
1506
1680
  }
1507
- 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 = () => {} }) {
1508
1682
  const root = warmClaimPath(repositoryRoot);
1509
1683
  const started = now();
1510
1684
  let lastProgress = started;
@@ -1512,26 +1686,31 @@ async function acquireWarmClaim({ repositoryRoot, phase, mode = phase === "refre
1512
1686
  let pending = null;
1513
1687
  try {
1514
1688
  for (;;) {
1689
+ signal?.throwIfAborted();
1515
1690
  const attempt = pending ? (0, ownership_claim_exports.settleClaim)(pending) : (0, ownership_claim_exports.tryAcquireClaim)({
1516
1691
  root,
1517
1692
  mode,
1518
- label: WARM_CLAIM_LABEL
1693
+ label: WARM_CLAIM_LABEL,
1694
+ details: { phase }
1519
1695
  });
1520
1696
  const acquired = attempt.acquired;
1521
1697
  if (acquired) {
1698
+ if (declareSpawnsFor) declareSpawnsOn(declareSpawnsFor, acquired);
1522
1699
  const installer = installerClaim(acquired, {
1523
1700
  now,
1524
1701
  progressMs,
1525
1702
  out
1526
1703
  });
1527
1704
  return {
1705
+ claim: acquired,
1528
1706
  wait: {
1529
1707
  waitedMs: now() - started,
1530
1708
  holder: waitedOn
1531
1709
  },
1532
1710
  installer,
1533
- release: () => {
1534
- if (installer.holding()) return;
1711
+ release: (retained = false) => {
1712
+ if (declareSpawnsFor) stopDeclaringSpawnsOn(declareSpawnsFor, acquired);
1713
+ if (retained || installer.holding()) return;
1535
1714
  (0, ownership_claim_exports.releaseClaim)(acquired);
1536
1715
  }
1537
1716
  };
@@ -1560,10 +1739,14 @@ async function acquireWarmClaim({ repositoryRoot, phase, mode = phase === "refre
1560
1739
  }
1561
1740
  async function withWarmClaim(options, fn) {
1562
1741
  const { release, ...hold } = await acquireWarmClaim(options);
1742
+ let retained = false;
1563
1743
  try {
1564
1744
  return await fn(hold);
1745
+ } catch (cause) {
1746
+ retained = cause instanceof WorkspaceProcessCleanupError && cause.claims.includes(hold.claim);
1747
+ throw cause;
1565
1748
  } finally {
1566
- release();
1749
+ release(retained);
1567
1750
  }
1568
1751
  }
1569
1752
  //#endregion
@@ -2092,7 +2275,7 @@ function dependencyInstallCommand(target, dir = ".") {
2092
2275
  else if (existsSync$1(resolve$1(installRoot, "package-lock.json"))) command = "npm ci";
2093
2276
  return `cd '${installRoot.replaceAll("'", "'\\''")}' && ${command}`;
2094
2277
  }
2095
- function relocateCarriedPods(root, target, copied) {
2278
+ function relocateCarriedPods(root, target, copied, out) {
2096
2279
  const canRelocate = depsOutOfSync(root, target, copied).length === 0;
2097
2280
  for (const rel of copied) {
2098
2281
  if (rel !== "Pods" && !rel.endsWith("/Pods")) continue;
@@ -2105,41 +2288,40 @@ function relocateCarriedPods(root, target, copied) {
2105
2288
  targetRoot: target,
2106
2289
  canRelocate
2107
2290
  });
2108
- 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`)));
2109
- 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`)));
2110
2293
  } catch (error) {
2111
- 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`)));
2112
2295
  }
2113
2296
  }
2114
2297
  }
2115
- function reportCarriedStateHealth(root, target, copied) {
2298
+ function reportCarriedStateHealth(root, target, copied, out) {
2116
2299
  const outOfSync = depsOutOfSync(root, target, copied);
2117
2300
  for (const d of outOfSync.filter((dependency) => dependency.reason === "installed")) {
2118
2301
  const where = d.dir === "." ? d.lockfile : `${d.dir}/${d.lockfile}`;
2119
2302
  const modules = d.dir === "." ? "node_modules" : `${d.dir}/node_modules`;
2120
- 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.`)));
2121
2304
  }
2122
2305
  const staleDeps = outOfSync.filter((dependency) => dependency.reason === "lockfile");
2123
2306
  if (staleDeps.length) {
2124
2307
  const manifests = staleDeps.map((d) => d.dir === "." ? d.lockfile : `${d.dir}/${d.lockfile}`).join(", ");
2125
2308
  const remedies = [...new Set(staleDeps.map((dependency) => dependencyInstallCommand(target, dependency.dir)))];
2126
- 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.`)));
2127
2310
  }
2128
2311
  for (const p of podsOutOfSync(target, copied)) {
2129
2312
  const where = p.dir === "." ? "Podfile.lock" : `${p.dir}/Podfile.lock`;
2130
2313
  const pod = podInstallCommand(p.dir === "." ? target : resolve$1(target, p.dir, ".."));
2131
- 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.`)));
2132
2315
  }
2133
2316
  }
2134
- function reportRefreshFailure(failure) {
2135
- console.error(chalk.red(failure.message));
2136
- for (const line of failure.lines) console.error(chalk.dim(` ${line}`));
2137
- if (failure.remedy) console.error(chalk.dim(failure.remedy));
2138
- console.error(chalk.red(`failed: ${failure.code}`));
2139
- 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}`));
2140
2322
  }
2141
- function mainCheckoutAppDir(root, target) {
2142
- const app = findProjectRoot$1(process.cwd());
2323
+ function mainCheckoutAppDir(root, target, cwd) {
2324
+ const app = findProjectRoot$1(cwd);
2143
2325
  if (!app) return root;
2144
2326
  const rel = relative$1(target, app);
2145
2327
  if (!rel || rel.startsWith("..") || isAbsolute$1(rel)) return root;
@@ -2163,150 +2345,204 @@ function warmedWorkspace(root, target, cwd) {
2163
2345
  }
2164
2346
  return apps.size === 1 ? [...apps][0] : app;
2165
2347
  }
2166
- function registerWarmedWorkspace(workspace) {
2348
+ function registerWarmedWorkspace(workspace, out) {
2167
2349
  try {
2168
2350
  if (!getProject(workspace)) upsertProject(workspace, {});
2169
2351
  } catch (error) {
2170
- 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)}`));
2171
2353
  }
2172
2354
  }
2173
2355
  function registerWarm(worktree) {
2174
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) => {
2175
- let progress = null;
2176
- let workspace = null;
2177
- try {
2178
- const { root, target, common } = warmWorktreePaths(process.cwd());
2179
- const app = findProjectRoot$1(process.cwd());
2180
- if (app) recordWorkspaceUse(app);
2181
- workspace = warmedWorkspace(root, target, process.cwd());
2182
- if (workspace && workspace !== app) recordWorkspaceUse(workspace);
2183
- 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")));
2184
- else {
2185
- registerWarmedWorkspace(workspace);
2186
- progress = startWarmProgress(workspace, opts.refresh ? "refresh" : "copy", { note: (line) => console.error(chalk.yellow(line)) });
2187
- }
2188
- const readSettings = () => {
2189
- const settings = resolveSettings({
2190
- gitCommonDir: common,
2191
- repoRoot: root
2192
- });
2193
- const shapeErrors = settingShapeErrors(settings);
2194
- if (shapeErrors.length) {
2195
- for (const message of shapeErrors) console.error(chalk.red(message));
2196
- console.error(chalk.dim(SETTING_SHAPE_REMEDY));
2197
- return null;
2198
- }
2199
- for (const key of unknownSettingKeys(settings)) console.error(chalk.yellow(`Warning: setting "${key}" is not read by Stim and will be ignored.`));
2200
- return settings;
2201
- };
2202
- const copy = (wait) => {
2203
- progress?.step("copy");
2204
- if (wait?.holder) console.error(warmClaimAcquiredLine(wait));
2205
- const incomplete = incompleteInstallRefusal(root, mainCheckoutAppDir(root, target));
2206
- if (incomplete) {
2207
- reportRefreshFailure(incomplete);
2208
- return;
2209
- }
2210
- const settings = readSettings();
2211
- if (!settings) {
2212
- process.exitCode = 1;
2213
- return;
2214
- }
2215
- const excluded = readWorktreeExclude(root);
2216
- const patterns = excluded?.length ? excluded : settings.worktree?.exclude || [];
2217
- const result = cloneIgnoredEntries({
2218
- root,
2219
- target,
2220
- 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
2221
2363
  });
2222
- for (const entry of result.skipped) console.error(chalk.dim(phaseLine("carry", `kept ${entry.file} (${entry.reason})`)));
2223
- for (const entry of result.failed) console.error(chalk.yellow(phaseLine("carry", `could not copy ${entry.file}: ${entry.error}`)));
2224
- if (result.copied.length) {
2225
- console.error(chalk.dim(phaseLine("carry", `copied ${carriedFileList(result.copied)} from ${root}`)));
2226
- relocateCarriedPods(root, target, result.copied);
2227
- 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}`));
2228
2371
  }
2229
- console.error(phaseLine("carry", `${result.failed.length ? "incomplete" : "complete"}: ${result.copied.length} ignored entries copied, ${result.skipped.length} kept, ${result.failed.length} failed`));
2230
- if (result.failed.length) process.exitCode = 1;
2231
- };
2232
- if (opts.refresh) {
2233
- const inspect = await acquireWarmClaim({
2234
- repositoryRoot: root,
2235
- phase: "refresh",
2236
- mode: "shared",
2237
- out: console.error
2238
- });
2239
- let mutation;
2240
- 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
+ }
2241
2440
  const settings = readSettings();
2242
2441
  if (!settings) {
2243
- process.exitCode = 1;
2442
+ exitCode = 1;
2244
2443
  return;
2245
2444
  }
2246
- const lines = [];
2247
- const result = await refreshMainCheckout({
2445
+ const excluded = readWorktreeExclude(root);
2446
+ const patterns = excluded?.length ? excluded : settings.worktree?.exclude || [];
2447
+ const result = await cloneIgnoredEntries({
2248
2448
  root,
2249
- appDir: mainCheckoutAppDir(root, target),
2250
- settings,
2251
- iosProject: appIosProjectDir(mainCheckoutAppDir(root, target)),
2252
- emit: (line) => lines.push(line),
2253
- inspectOnly: true
2449
+ target,
2450
+ patterns,
2451
+ operation
2254
2452
  });
2255
- if (!result || !("mutation" in result)) {
2256
- console.error(warmClaimAcquiredLine(inspect.wait, result ? "shared" : "shared (seed current)"));
2257
- for (const line of lines) console.error(line);
2258
- if (result) reportRefreshFailure(result);
2259
- else copy(null);
2260
- 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);
2261
2459
  }
2262
- mutation = result.mutation;
2263
- } finally {
2264
- 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;
2265
2480
  }
2266
- const failure = await withWarmClaim({
2267
- repositoryRoot: root,
2268
- phase: "refresh",
2269
- out: console.error
2270
- }, async (hold) => {
2271
- console.error(warmClaimAcquiredLine(hold.wait, `exclusive (${mutation})`));
2272
- const settings = readSettings();
2273
- if (!settings) {
2274
- process.exitCode = 1;
2275
- return null;
2276
- }
2277
- return refreshMainCheckout({
2278
- root,
2279
- appDir: mainCheckoutAppDir(root, target),
2280
- settings,
2281
- iosProject: appIosProjectDir(mainCheckoutAppDir(root, target)),
2282
- emit: (line) => console.error(line),
2283
- installer: hold.installer
2284
- });
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
2285
2489
  });
2286
- if (failure) {
2287
- reportRefreshFailure(failure);
2288
- 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;
2289
2498
  }
2290
- if (process.exitCode) return;
2291
- }
2292
- await withWarmClaim({
2499
+ return result.mutation;
2500
+ });
2501
+ if (mutation === null) return { exitCode };
2502
+ const failure = await withWarmClaim({
2293
2503
  repositoryRoot: root,
2294
- phase: "copy",
2295
- out: console.error
2296
- }, (hold) => copy(hold.wait));
2297
- } catch (error) {
2298
- const code = error?.code;
2299
- console.error(chalk.red(`Could not warm this worktree: ${error.message}`));
2300
- const claim = (0, ownership_claim_exports.claimFailure)(error, `stim worktree warm${opts.refresh ? " --refresh" : ""}`);
2301
- if (claim) console.error(chalk.dim(claim.remedy));
2302
- if (typeof code === "string" && code.startsWith("STIM_")) console.error(chalk.red(`failed: ${code}`));
2303
- process.exitCode = 1;
2304
- } finally {
2305
- progress?.finish(!process.exitCode);
2306
- const links = workspace && !process.exitCode ? await workspaceLinks(workspace) : void 0;
2307
- 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 };
2308
2529
  }
2309
- });
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 };
2310
2546
  }
2311
2547
  function carriedFileList(files) {
2312
2548
  const shown = files.slice(0, 3).join(", ");
@@ -2456,12 +2692,16 @@ function reclaimKeys(rootPath) {
2456
2692
  }
2457
2693
  return [...keys].toSorted();
2458
2694
  }
2459
- async function withReclaimLocks(rootPath, fn) {
2695
+ async function withReclaimLocks(rootPath, fn, operation, beforeRemove) {
2460
2696
  const keys = reclaimKeys(rootPath);
2461
- 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
+ });
2462
2702
  return acquire(0);
2463
2703
  }
2464
- async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRootProject = false, removedBy } = {}) {
2704
+ async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRootProject = false, removedBy, operation } = {}) {
2465
2705
  const dereferenced = [];
2466
2706
  const killedPids = [];
2467
2707
  const deletedDevices = [];
@@ -2476,8 +2716,10 @@ async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRoot
2476
2716
  const releasedLeases = [];
2477
2717
  const removedWorkspaceDirs = [];
2478
2718
  const failedWorkspaceDirs = [];
2479
- const pressure = currentDiskLevel();
2719
+ operation?.signal.throwIfAborted();
2720
+ const pressure = currentDiskLevel(operation ? await measureDiskAsync(operation) : void 0);
2480
2721
  for (const key of keys) {
2722
+ operation?.signal.throwIfAborted();
2481
2723
  let r;
2482
2724
  try {
2483
2725
  r = await reclaimProject(key, {
@@ -2485,9 +2727,12 @@ async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRoot
2485
2727
  deleteOwnedDevices: true,
2486
2728
  parkOwnedDevices: pressure < 2,
2487
2729
  preserveProjectRecord: preserveRootProject && key === rootPath,
2488
- stopRequester: { by: "stim worktree remove" }
2730
+ stopRequester: { by: "stim worktree remove" },
2731
+ operation
2489
2732
  });
2490
2733
  } catch (error) {
2734
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
2735
+ operation?.signal.throwIfAborted();
2491
2736
  keptEntries.push(key);
2492
2737
  retainedResources.push({
2493
2738
  name: "ownership state",
@@ -2569,18 +2814,18 @@ function poolLines(result) {
2569
2814
  for (const note of result.poolNotes) lines.push(chalk.yellow(phaseLine("device", note)));
2570
2815
  return lines;
2571
2816
  }
2572
- function reportRetainedResources(root, result) {
2573
- 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.`));
2574
2819
  for (const resource of result.retainedResources) {
2575
- console.error(chalk.yellow(` - Stim still tracks ${describeKeptDevice(resource)} for ${resource.project}`));
2576
- 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}`));
2577
2822
  }
2578
2823
  for (const kept of result.keptEntries) {
2579
2824
  if (result.retainedResources.some((resource) => resource.project === kept)) continue;
2580
- console.error(chalk.yellow(` - retained Stim ownership state for ${kept}`));
2825
+ output.error(chalk.yellow(` - retained Stim ownership state for ${kept}`));
2581
2826
  }
2582
- console.error(chalk.dim("Fix the reported cause, then run `stim worktree remove` again."));
2583
- process.exitCode = 1;
2827
+ output.error(chalk.dim("Fix the reported cause, then run `stim worktree remove` again."));
2828
+ output.fail();
2584
2829
  }
2585
2830
  function hasRegisteredProjectUnder(rootPath) {
2586
2831
  const cfg = loadConfig$1();
@@ -2590,28 +2835,40 @@ function projectAtOrAbove(root, path) {
2590
2835
  const here = nativeCanonicalPath(path);
2591
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;
2592
2837
  }
2593
- async function reclaimEnvironment(root, why) {
2838
+ async function reclaimEnvironment(root, why, output, operation, beforeRemove) {
2594
2839
  await withManagedRemoteWorktreeRemovalLock(root, () => withReclaimLocks(root, async (lockedKeys) => {
2595
- const result = await reclaimAll(root, lockedKeys, { removedBy: "worktree-remove" });
2596
- for (const line of poolLines(result)) console.error(line);
2597
- for (const device of result.deletedDevices) console.error(chalk.dim(phaseLine("device", `deleted ${device}`)));
2598
- for (const session of result.stoppedSessions) console.error(chalk.dim(phaseLine("device", `stopped remote session ${session}`)));
2599
- for (const tunnel of result.stoppedTunnels) console.error(chalk.dim(phaseLine("lan", `stopped the ${tunnel} tunnel`)));
2600
- for (const lease of result.releasedLeases) console.error(chalk.dim(phaseLine("lease", releasedLeaseFact(lease))));
2601
- for (const pid of result.killedPids) console.error(chalk.dim(phaseLine("metro", `killed pid ${pid}`)));
2602
- for (const dir of result.removedWorkspaceDirs) console.error(chalk.dim(phaseLine("workspace", `removed ${dir}`)));
2603
- for (const dir of result.failedWorkspaceDirs) console.error(chalk.yellow(phaseLine("workspace", `could not remove ${dir}`)));
2604
- 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(", ")}`)));
2605
2853
  if (result.keptEntries.length) {
2606
- reportRetainedResources(root, result);
2854
+ reportRetainedResources(root, result, output);
2607
2855
  return;
2608
2856
  }
2609
- for (const s of result.skippedDevices) console.error(chalk.yellow(phaseLine("device", `kept ${describeKeptDevice(s)} (${s.reason})`)));
2610
- console.error(chalk.green(`Reclaimed the environment; the working tree stays (${why}).`));
2611
- }));
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
+ });
2612
2863
  }
2613
- function removalPath(target) {
2614
- 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);
2615
2872
  }
2616
2873
  function canonicalExistingPath(target) {
2617
2874
  const resolved = resolve$1(target);
@@ -2625,9 +2882,9 @@ function canonicalExistingPath(target) {
2625
2882
  }
2626
2883
  return resolve$1(realpathSync$1(existing), ...missing);
2627
2884
  }
2628
- function inspectRemoval(path, mergedHead) {
2885
+ async function inspectRemoval(path, mergedHead, operation) {
2629
2886
  try {
2630
- return inspectGit(path, mergedHead);
2887
+ return await inspectGit(path, mergedHead, operation);
2631
2888
  } catch (error) {
2632
2889
  if (!isTimeoutError(error)) throw error;
2633
2890
  return timedOutInspection(error);
@@ -2643,18 +2900,24 @@ function timedOutInspection(error) {
2643
2900
  timedOut: true
2644
2901
  };
2645
2902
  }
2646
- function inspectGit(path, mergedHead) {
2647
- const gitAnswered = hasUncommittedWork(path);
2648
- 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) : [];
2649
2912
  const { lines: withoutCookies, cookies } = excludeWatchmanCookies(allDirty ?? []);
2650
2913
  const { lines: dirtyLines, restore: podChurn } = excludePodChurn(withoutCookies, customIosDirs(path));
2651
2914
  const dirty = gitAnswered === null || allDirty === null ? null : dirtyLines.length > 0;
2652
- const unpushed = unpushedCommits(path);
2915
+ const unpushed = await unpushedCommitsAsync(path, options);
2653
2916
  const blockers = removalBlockers({
2654
2917
  dirty,
2655
- unpushed: Boolean(mergedHead) && resolveFullRef(path, "HEAD", { removal: true }) === mergedHead && unpushed ? [] : unpushed
2918
+ unpushed: Boolean(mergedHead) && await resolveFullRefAsync(path, "HEAD", options) === mergedHead && unpushed ? [] : unpushed
2656
2919
  });
2657
- const submodules = hasPopulatedSubmodules(path);
2920
+ const submodules = await hasPopulatedSubmodulesAsync(path, options);
2658
2921
  if (submodules === null) blockers.push("could not check for initialized submodules; re-run with --force to override");
2659
2922
  else if (submodules) blockers.push("initialized submodules, which git removes only with --force");
2660
2923
  return {
@@ -2665,43 +2928,50 @@ function inspectGit(path, mergedHead) {
2665
2928
  blockers
2666
2929
  };
2667
2930
  }
2668
- function printRemovalRefusal(path, inspection) {
2931
+ async function printRemovalRefusal(path, inspection, output, operation) {
2669
2932
  const { blockers, dirtyLines, unpushed } = inspection;
2670
- console.error(chalk.red(`Refusing to remove ${path}:`));
2671
- 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}`));
2672
2935
  if (inspection.timedOut) {
2673
- console.error(chalk.dim("Git did not answer in time, so Stim cannot tell whether the worktree holds unsaved work."));
2674
- console.error(chalk.dim(`Retry once git responds; check with: git -C ${path} status`));
2675
- 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();
2676
2939
  return;
2677
2940
  }
2678
- 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)"));
2679
- for (const line of dirtyLines.slice(0, 10)) console.error(chalk.dim(` ${line}`));
2680
- if (dirtyLines.length) console.error(chalk.dim(` (git -C ${path} status -s for the full list)`));
2681
- for (const line of removalRemedy(dirtyLines, { worktree: path })) console.error(chalk.dim(line));
2682
- console.error(chalk.dim("Otherwise: commit or push the branch (only commits found nowhere else are counted,"));
2683
- console.error(chalk.dim("so pushing publishes nothing but this worktree's own work). --force is a last"));
2684
- console.error(chalk.dim("resort -- it discards uncommitted changes and untracked files permanently; committed"));
2685
- console.error(chalk.dim("work stays on the branch."));
2686
- 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();
2687
2953
  }
2688
2954
  function deleteWatchmanCookies(path, cookies) {
2689
2955
  for (const cookie of cookies) rmSync$1(join$1(path, cookie), { force: true });
2690
2956
  }
2691
- function restorePodChurn(path, files) {
2692
- for (const file of files) if (restoreFile(path, file)) console.error(chalk.dim(` restored ${file} (pod install churn; the worktree is being removed)`));
2693
- else console.error(chalk.yellow(` could not restore ${file}; git may refuse to remove the worktree`));
2694
- }
2695
- function printRemovalCleanup(result, failed) {
2696
- for (const line of poolLines(result)) console.error(line);
2697
- for (const device of result.deletedDevices) console.error(chalk.dim(phaseLine("device", `deleted ${device}`)));
2698
- for (const session of result.stoppedSessions) console.error(chalk.dim(phaseLine("device", `stopped remote session ${session}`)));
2699
- for (const tunnel of result.stoppedTunnels) console.error(chalk.dim(phaseLine("lan", `stopped the ${tunnel} tunnel`)));
2700
- for (const lease of result.releasedLeases) console.error(chalk.dim(phaseLine("lease", releasedLeaseFact(lease))));
2701
- for (const pid of result.killedPids) console.error(chalk.dim(phaseLine("metro", `killed pid ${pid}`)));
2702
- if (!failed) for (const dir of result.removedWorkspaceDirs) console.error(chalk.dim(phaseLine("workspace", `removed ${dir}`)));
2703
- if (result.dereferenced.length) console.error(chalk.dim(phaseLine("workspace", `no longer referenced: ${result.dereferenced.join(", ")}`)));
2704
- 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})`)));
2705
2975
  }
2706
2976
  async function removeWorktreeTarget(target, opts = {}) {
2707
2977
  let removed = false;
@@ -2711,222 +2981,245 @@ async function removeWorktreeTarget(target, opts = {}) {
2711
2981
  return removed;
2712
2982
  }
2713
2983
  async function runRemove(target, opts, onRemoved) {
2984
+ const output = opts.output ?? REMOVAL_OUTPUT;
2714
2985
  try {
2715
- await removeOrRefuse(target, opts, onRemoved);
2986
+ await removeOrRefuse(target, opts, onRemoved, output);
2716
2987
  } catch (error) {
2988
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
2989
+ opts.operation?.signal.throwIfAborted();
2717
2990
  if (!isTimeoutError(error)) throw error;
2718
- printRemovalRefusal(removalPath(target), timedOutInspection(error));
2991
+ await printRemovalRefusal(removalPath(target, opts.operation?.cwd), timedOutInspection(error), output, opts.operation);
2719
2992
  }
2720
2993
  }
2721
- 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();
2722
3002
  const poolError = parkedMaxSetting("ios").error || parkedMaxSetting("android").error;
2723
3003
  if (poolError) {
2724
- console.error(chalk.red(poolError));
2725
- console.error(chalk.dim(POOL_SETTING_REMEDY));
2726
- process.exitCode = 1;
3004
+ output.error(chalk.red(poolError));
3005
+ output.error(chalk.dim(POOL_SETTING_REMEDY));
3006
+ output.fail();
2727
3007
  return;
2728
3008
  }
2729
- let path = removalPath(target);
3009
+ let path = removalPath(target, opts.operation?.cwd);
2730
3010
  if (!existsSync$1(path)) {
3011
+ opts.beforeRemove?.(path);
2731
3012
  const pending = getProject(path);
2732
3013
  if (pending?.worktreeRemovalComplete === true && pending.worktreeBranchOwned === true && pending.worktreeBranch && pending.worktreeMainRoot && pending.worktreePendingBranchSha) {
2733
3014
  const branch = pending.worktreeBranch;
2734
3015
  const mainRoot = pending.worktreeMainRoot;
2735
3016
  if (!existsSync$1(mainRoot)) {
2736
- console.error(chalk.red(`Cannot finish branch cleanup for ${path}: source checkout ${mainRoot} is missing.`));
2737
- console.error(chalk.dim(`Delete ${branch} from the repository, then run \`stim gc --delete\`.`));
2738
- 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();
2739
3020
  return;
2740
3021
  }
2741
- if (!branchExists(mainRoot, branch, { removal: true })) {
3022
+ opts.beforeRemove?.(removalPath(mainRoot));
3023
+ if (!await branchExistsAsync(mainRoot, branch, options)) {
2742
3024
  removeProject(path);
2743
- 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`)));
2744
3026
  return;
2745
3027
  }
2746
- 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;
2747
3029
  if (checkedOutAt) {
2748
- console.error(chalk.red(`Refusing pending cleanup for ${branch}: it is checked out at ${checkedOutAt}.`));
2749
- console.error(chalk.dim("Switch that worktree to another branch, then retry this command."));
2750
- 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();
2751
3033
  return;
2752
3034
  }
2753
- const currentSha = resolveFullRef(mainRoot, branch, { removal: true });
3035
+ const currentSha = await resolveFullRefAsync(mainRoot, branch, options);
2754
3036
  if (currentSha !== pending.worktreePendingBranchSha) {
2755
- console.error(chalk.red(`Refusing pending cleanup for ${branch}: its tip changed from ${pending.worktreePendingBranchSha} to ${currentSha || "an unknown commit"}.`));
2756
- console.error(chalk.dim("The branch can contain new work. Inspect it and delete it manually if appropriate."));
2757
- 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();
2758
3040
  return;
2759
3041
  }
2760
3042
  try {
2761
- deleteBranch(mainRoot, branch, pending.worktreePendingBranchSha);
3043
+ await deleteBranchAsync(mainRoot, branch, pending.worktreePendingBranchSha, options);
2762
3044
  removeProject(path);
2763
- 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`)));
2764
3046
  } catch (error) {
2765
- console.error(chalk.red(`Could not delete branch ${branch}: ${String(error?.message || error)}`));
2766
- console.error(chalk.dim(`Retry with: stim worktree remove ${path}`));
2767
- 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();
2768
3051
  }
2769
3052
  return;
2770
3053
  }
2771
- console.error(chalk.red(`No such worktree: ${path}`));
2772
- process.exitCode = 1;
3054
+ output.error(chalk.red(`No such worktree: ${path}`));
3055
+ output.fail();
2773
3056
  return;
2774
3057
  }
2775
- const worktrees = listWorktrees(path, { removal: true });
3058
+ const worktrees = await listWorktreesAsync(path, options);
2776
3059
  const entry = matchWorktreeEntry(worktrees, path);
2777
3060
  if (!entry) {
2778
- if (!opts.linkedOnly && gitCommonDir(path, { removal: true }) === null && hasRegisteredProjectUnder(path)) {
2779
- 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);
2780
3063
  return;
2781
3064
  }
2782
- console.error(chalk.red(`Refusing to remove ${path}: it is not inside any worktree known to git.`));
2783
- console.error(chalk.dim(" Run it from inside the worktree, or pass the worktree root path, e.g. as printed by `git worktree list`."));
2784
- 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();
2785
3068
  return;
2786
3069
  }
2787
3070
  if (entry.bare) {
2788
- console.error(chalk.red(`Refusing to remove ${path}: ${entry.path} is the bare repository, not a worktree.`));
2789
- console.error(chalk.dim(" Pass a worktree path, e.g. as printed by `git worktree list`."));
2790
- 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();
2791
3074
  return;
2792
3075
  }
2793
3076
  const source = sourceCheckoutOf(worktrees);
2794
3077
  if ("refusal" in source) {
2795
- console.error(chalk.red(`Refusing to remove ${path}: ${source.refusal}`));
2796
- process.exitCode = 1;
3078
+ output.error(chalk.red(`Refusing to remove ${path}: ${source.refusal}`));
3079
+ output.fail();
2797
3080
  return;
2798
3081
  }
2799
3082
  if (entry.path === source.path && opts.linkedOnly) {
2800
- console.error(chalk.red(`Refusing to remove ${path}: ${entry.path} is the source checkout.`));
2801
- process.exitCode = 1;
3083
+ output.error(chalk.red(`Refusing to remove ${path}: ${entry.path} is the source checkout.`));
3084
+ output.fail();
2802
3085
  return;
2803
3086
  }
2804
3087
  if (entry.path === source.path) {
2805
3088
  if (nativeCanonicalPath(entry.path) !== nativeCanonicalPath(path)) {
2806
3089
  const project = projectAtOrAbove(entry.path, path);
2807
3090
  if (!project) {
2808
- console.error(chalk.red(`Refusing to reclaim from ${path}: no Stim project is registered at or above it.`));
2809
- console.error(chalk.dim(` Run it from the project folder, or from ${entry.path} to reclaim every project under it.`));
2810
- 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();
2811
3094
  return;
2812
3095
  }
2813
- console.error(chalk.dim(`${path} is inside the source checkout ${entry.path}; reclaiming only the project ${project}.`));
2814
- 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);
2815
3098
  return;
2816
3099
  }
2817
- await reclaimEnvironment(entry.path, "it is the source checkout");
3100
+ await reclaimEnvironment(entry.path, "it is the source checkout", output, opts.operation, opts.beforeRemove);
2818
3101
  return;
2819
3102
  }
2820
3103
  if (entry.path !== path) {
2821
- 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.`));
2822
3105
  path = entry.path;
2823
3106
  }
2824
3107
  if (entry.locked) {
2825
- console.error(chalk.red(`Refusing to remove ${path}: git has it locked.`));
2826
- console.error(chalk.dim(`Unlock it, then retry: git -C ${source.path} worktree unlock ${path}`));
2827
- 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();
2828
3111
  return;
2829
3112
  }
2830
3113
  const nested = nestedWorktrees(worktrees, entry.path);
2831
3114
  if (nested.length) {
2832
- console.error(chalk.red(`Refusing to remove ${path}: it contains the worktree ${nested.join(", ")}.`));
2833
- console.error(chalk.dim("Remove the nested worktree first, then retry."));
2834
- process.exitCode = 1;
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();
2835
3118
  return;
2836
3119
  }
2837
3120
  const service = serviceRunningFrom(path, nativeCanonicalPath);
2838
3121
  if (service) {
2839
- 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}`));
2840
- 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.`));
2841
- 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();
2842
3125
  return;
2843
3126
  }
2844
3127
  const project = getProject(path);
2845
3128
  const branch = entry.branch;
2846
3129
  const ownsBranch = Boolean(branch && project?.worktreeBranchOwned === true && project.worktreeBranch === branch);
2847
- const approvedBranchSha = ownsBranch ? resolveFullRef(path, "HEAD", { removal: true }) : null;
3130
+ const approvedBranchSha = ownsBranch ? await resolveFullRefAsync(path, "HEAD", options) : null;
2848
3131
  let mergedHead = opts.mergedHead;
2849
- let inspection = inspectRemoval(path, mergedHead);
3132
+ let inspection = await inspectRemoval(path, mergedHead, opts.operation);
2850
3133
  if (inspection.blockers.length && !opts.force && !mergedHead && branch && inspection.unpushed?.length && !inspection.dirtyLines.length) {
2851
- const head = resolveFullRef(path, "HEAD", { removal: true });
2852
- 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;
2853
3136
  if (head && pr?.state === "merged") {
2854
3137
  mergedHead = head;
2855
- inspection = inspectRemoval(path, mergedHead);
2856
- 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.`));
2857
3140
  }
2858
3141
  }
2859
3142
  if (inspection.blockers.length && !opts.force) {
2860
- printRemovalRefusal(path, inspection);
3143
+ await printRemovalRefusal(path, inspection, output, opts.operation);
2861
3144
  return;
2862
3145
  }
2863
3146
  const deleteOwnedBranch = Boolean(ownsBranch && approvedBranchSha && inspection.unpushed?.length === 0);
2864
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;
2865
3148
  const branchDeleteCwd = source.path;
2866
3149
  await withManagedRemoteWorktreeRemovalLock(path, () => withReclaimLocks(path, async (lockedKeys) => {
2867
- if (opts.guard?.(lockedKeys).length) return;
2868
- const inspectedHead = resolveFullRef(path, "HEAD", { removal: true });
3150
+ options.signal?.throwIfAborted();
3151
+ if ((await opts.guard?.(lockedKeys))?.length) return;
3152
+ options.signal?.throwIfAborted();
3153
+ const inspectedHead = await resolveFullRefAsync(path, "HEAD", options);
2869
3154
  if (opts.expectedHead && inspectedHead !== opts.expectedHead) {
2870
- console.error(chalk.red(`Refusing to remove ${path}: its HEAD moved since it was checked.`));
2871
- process.exitCode = 1;
3155
+ output.error(chalk.red(`Refusing to remove ${path}: its HEAD moved since it was checked.`));
3156
+ output.fail();
2872
3157
  return;
2873
3158
  }
2874
- const current = inspectRemoval(path, mergedHead);
3159
+ const current = await inspectRemoval(path, mergedHead, opts.operation);
2875
3160
  if (current.blockers.length && !opts.force) {
2876
- printRemovalRefusal(path, current);
3161
+ await printRemovalRefusal(path, current, output, opts.operation);
2877
3162
  return;
2878
3163
  }
2879
3164
  const result = await reclaimAll(path, lockedKeys, {
2880
3165
  preserveRootProject: true,
2881
- removedBy: opts.removedBy ?? "worktree-remove"
3166
+ removedBy: opts.removedBy ?? "worktree-remove",
3167
+ operation: opts.operation
2882
3168
  });
2883
3169
  if (result.keptEntries.length) {
2884
- reportRetainedResources(path, result);
3170
+ reportRetainedResources(path, result, output);
2885
3171
  return;
2886
3172
  }
2887
3173
  let headMoved = false;
2888
3174
  if (!opts.force) try {
2889
- headMoved = resolveFullRef(path, "HEAD", { removal: true }) !== inspectedHead;
3175
+ headMoved = await resolveFullRefAsync(path, "HEAD", options) !== inspectedHead;
2890
3176
  } catch (error) {
3177
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
3178
+ opts.operation?.signal.throwIfAborted();
2891
3179
  if (!isTimeoutError(error)) throw error;
2892
- printRemovalRefusal(path, timedOutInspection(error));
2893
- console.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
2894
- printRemovalCleanup(result, true);
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);
2895
3183
  return;
2896
3184
  }
2897
3185
  if (headMoved) {
2898
- console.error(chalk.red(`Refusing to remove ${path}: its HEAD moved while its environment was reclaimed.`));
2899
- console.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
2900
- printRemovalCleanup(result, true);
2901
- process.exitCode = 1;
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();
2902
3190
  return;
2903
3191
  }
2904
- restorePodChurn(path, current.podChurn);
3192
+ await restorePodChurn(path, current.podChurn, output, opts.operation);
3193
+ options.signal?.throwIfAborted();
2905
3194
  deleteWatchmanCookies(path, current.cookies);
2906
3195
  try {
2907
- removeWorktree(path, {
3196
+ await removeWorktreeAsync(path, {
2908
3197
  from: source.path,
2909
- force: opts.force
3198
+ force: opts.force,
3199
+ env: options.env,
3200
+ signal: options.signal,
3201
+ declareFor: opts.operation
2910
3202
  });
2911
3203
  } catch (error) {
3204
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
2912
3205
  const message = String(error?.message || error);
2913
- console.error(chalk.red(`git worktree remove failed: ${message}`));
2914
- 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."));
2915
- console.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
2916
- printRemovalCleanup(result, true);
2917
- 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();
2918
3211
  return;
2919
3212
  }
2920
3213
  onRemoved();
2921
3214
  forgetStatusMeasures(path, result.reclaimedKeys);
2922
3215
  const finish = () => {
2923
- printRemovalCleanup(result, false);
2924
- console.error(chalk.dim(phaseLine("removed", path)));
3216
+ printRemovalCleanup(result, false, output);
3217
+ output.error(chalk.dim(phaseLine("removed", path)));
2925
3218
  };
2926
3219
  if (deleteOwnedBranch && branch) {
2927
3220
  if (!approvedBranchSha) {
2928
- console.error(chalk.yellow(phaseLine("branch", `kept ${branch} (Stim could not record its commit before removal)`)));
2929
- process.exitCode = 1;
3221
+ output.error(chalk.yellow(phaseLine("branch", `kept ${branch} (Stim could not record its commit before removal)`)));
3222
+ output.fail();
2930
3223
  finish();
2931
3224
  return;
2932
3225
  }
@@ -2936,36 +3229,42 @@ async function removeOrRefuse(target, opts, onRemoved) {
2936
3229
  });
2937
3230
  let checkedOutAt;
2938
3231
  try {
2939
- checkedOutAt = listWorktrees(branchDeleteCwd, { removal: true }).find((candidate) => candidate.branch === branch && resolve$1(candidate.path) !== resolve$1(path))?.path;
3232
+ checkedOutAt = (await listWorktreesAsync(branchDeleteCwd, options)).find((candidate) => candidate.branch === branch && resolve$1(candidate.path) !== resolve$1(path))?.path;
2940
3233
  } catch (error) {
3234
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
3235
+ opts.operation?.signal.throwIfAborted();
2941
3236
  if (!isTimeoutError(error)) throw error;
2942
- console.error(chalk.yellow(phaseLine("branch", `kept ${branch} (git did not list the worktrees in time to check it)`)));
2943
- console.error(chalk.dim(` Retry: stim worktree remove ${path}`));
2944
- process.exitCode = 1;
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();
2945
3240
  finish();
2946
3241
  return;
2947
3242
  }
2948
3243
  if (checkedOutAt) {
2949
- console.error(chalk.yellow(phaseLine("branch", `kept ${branch} (it is checked out at ${checkedOutAt})`)));
2950
- console.error(chalk.dim(` Switch that worktree to another branch, then retry: stim worktree remove ${path}`));
2951
- 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();
2952
3247
  finish();
2953
3248
  return;
2954
3249
  }
2955
3250
  try {
2956
- deleteBranch(branchDeleteCwd, branch, approvedBranchSha);
2957
- console.error(chalk.dim(phaseLine("branch", `deleted ${branch}`)));
3251
+ await deleteBranchAsync(branchDeleteCwd, branch, approvedBranchSha, options);
3252
+ output.error(chalk.dim(phaseLine("branch", `deleted ${branch}`)));
2958
3253
  } catch (error) {
2959
- console.error(chalk.yellow(phaseLine("branch", `kept ${branch} (${String(error?.message || error)})`)));
2960
- console.error(chalk.dim(` Retry with: git -C ${branchDeleteCwd} branch -D -- ${branch}`));
2961
- 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();
2962
3258
  finish();
2963
3259
  return;
2964
3260
  }
2965
- } 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})`)));
2966
3262
  removeProject(path);
2967
3263
  finish();
2968
- }));
3264
+ }, opts.operation, opts.beforeRemove), {
3265
+ signal: opts.operation?.signal,
3266
+ declareSpawnsFor: opts.operation
3267
+ });
2969
3268
  }
2970
3269
  function registerRemove(worktree) {
2971
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) => {
@@ -2978,4 +3277,4 @@ function worktreeCommand(program) {
2978
3277
  registerRemove(worktree);
2979
3278
  }
2980
3279
  //#endregion
2981
- export { lowestFree as A, measurePressure as C, diskPressureLevel as D, sizeScanDeferred as E, levelFloors as O, measureDisk as S, measureSizes as T, describeDereferenced as _, matchWorktreeEntry as a, sweepArchiveStaging as b, reclaimKeys as c, removalBlockers as d, removalPath as f, worktreeCommand as g, warmedWorkspace as h, excludeWatchmanCookies as i, protectWindowMs as j, levelStillShort 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, reclaimProject as v, measurePressureAsync as w, cacheBlocked as x, deleteSelectedArchives 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 };