stim 1.26.0 → 1.28.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 (172) hide show
  1. package/dist/{acquire-artifact-DVsvSwf7.mjs → acquire-artifact-CKByBnTD.mjs} +2 -2
  2. package/dist/{act-C-BqJEpu.mjs → act-8iswyfRa.mjs} +319 -126
  3. package/dist/{activity-Lnb_EoRY.mjs → activity-DRMg4_mD.mjs} +6 -6
  4. package/dist/{agent-device-usage-output-DbXy5tg8.mjs → agent-device-usage-output-tE90fDdo.mjs} +5 -5
  5. package/dist/{android-CiNQw3kq.mjs → android-CHmSU4B4.mjs} +101 -45
  6. package/dist/{android-DTAcRwBC.mjs → android-CsPs-7Mg.mjs} +23 -18
  7. package/dist/{android-B5P81yI-.mjs → android-D577ApKG.mjs} +37 -15
  8. package/dist/{android-CSbLRD53.mjs → android-YWCbmkVK.mjs} +5 -5
  9. package/dist/{android-cas-BTVhZ0Qs.mjs → android-cas-_aDifWW0.mjs} +4 -4
  10. package/dist/android-cas-compiler.mjs +1 -1
  11. package/dist/api.d.mts +14 -4
  12. package/dist/api.mjs +39 -29
  13. package/dist/{apk-signed-tail-B46U9hXc.mjs → apk-signed-tail-CQbwM0qO.mjs} +2 -2
  14. package/dist/{artifact-CsAP6AGK.mjs → artifact-DYtHjxsn.mjs} +19 -24
  15. package/dist/{artifact-BxREhAaV.mjs → artifact-bFkfD3vf.mjs} +18 -23
  16. package/dist/{artifact-platform-CMRc0M_r.mjs → artifact-platform-BO2Uvk_A.mjs} +1 -1
  17. package/dist/auto-placement-BVqs8AJi.mjs +350 -0
  18. package/dist/{browser-web-rYPyqhTf.mjs → browser-web-BUaLcafY.mjs} +16 -16
  19. package/dist/{build-DPhApsjV.mjs → build-cQURy_KW.mjs} +35 -31
  20. package/dist/{build-cache-DrXNCy3T.mjs → build-cache-CZULpXVl.mjs} +43 -21
  21. package/dist/{build-plan-DrMMdayj.mjs → build-plan-CxoYfxsc.mjs} +7 -6
  22. package/dist/{cache-manifest-BEZ9Bpl1.mjs → cache-manifest-DEjqGGat.mjs} +1 -1
  23. package/dist/cache-manifest.mjs +1 -1
  24. package/dist/{budget-k2mZGvvk.mjs → ccache-Bm_A1xX9.mjs} +562 -1604
  25. package/dist/{cdp-BG3FJCCx.mjs → cdp-C3svvXiE.mjs} +1 -1
  26. package/dist/{chrome-BvV8ssI3.mjs → chrome-DyDWurWG.mjs} +1 -1
  27. package/dist/{cli-BBKFGOFl.mjs → cli-CpbaLDa7.mjs} +24 -23
  28. package/dist/cli.mjs +1 -1
  29. package/dist/{client-D9CaRks-.mjs → client-YxCer6ab.mjs} +84 -65
  30. package/dist/collector-run.d.mts +10 -9
  31. package/dist/collector-run.mjs +42 -27
  32. package/dist/{command-output-BF5AR2dL.mjs → command-output-C8R1KIvi.mjs} +1 -1
  33. package/dist/{config-BktkY_zC.mjs → config-Cn94Jc0g.mjs} +23 -4
  34. package/dist/{dependencies-Dw6wfQdv.mjs → dependencies-DKeMzwXY.mjs} +108 -60
  35. package/dist/{dependency-state-DFB41vfr.mjs → dependency-state-CojA2o0t.mjs} +14 -7
  36. package/dist/{detached-entry-KmieOKgN.mjs → detached-entry-BPlSaCwC.mjs} +1 -1
  37. package/dist/{dev-client-DXR_tPVY.mjs → dev-client-D-BW5uiq.mjs} +2 -2
  38. package/dist/{device-BK-zwyd_.mjs → device-CgXfbcz5.mjs} +25 -24
  39. package/dist/{device-capacity-DCSvUF0l.mjs → device-capacity-Cd8lMSn3.mjs} +175 -22
  40. package/dist/{device-BcYeHSFu.mjs → device-eeK95Lir.mjs} +16 -16
  41. package/dist/device-host-worker.mjs +22 -20
  42. package/dist/{device-ios-BLGfXQeK.mjs → device-ios-Cc9ulihb.mjs} +251 -23
  43. package/dist/{device-lease-run-BmvIl7bI.mjs → device-lease-run-Mtzp4THX.mjs} +5 -5
  44. package/dist/{device-pool-Dluix4H3.mjs → device-pool-eRz6DtCg.mjs} +3 -3
  45. package/dist/{doctor-Bhg1ii70.mjs → doctor-Bz_ouYAW.mjs} +28 -16
  46. package/dist/{doctor-Cp-BhzTS.mjs → doctor-C2QyhejJ.mjs} +1 -1
  47. package/dist/{doctor-fingerprint-D3a04Mhu.mjs → doctor-fingerprint-o7ZI9YLn.mjs} +1 -1
  48. package/dist/doctor-fingerprint.mjs +1 -1
  49. package/dist/{doctor-watchman-BStK05nt.mjs → doctor-watchman-BTW82PDC.mjs} +5 -5
  50. package/dist/{doctor-OnwP94ne.mjs → doctor-xf7ht-5n.mjs} +76 -32
  51. package/dist/{eas-build-Dsj93iM9.mjs → eas-build-Dm6q68QY.mjs} +4 -4
  52. package/dist/eas-fallback-DajNpxf-.mjs +87 -0
  53. package/dist/{eas-session-ledger-B5vygTiD.mjs → eas-session-ledger-CBFo_Uo1.mjs} +37 -8
  54. package/dist/{error-diagnostics-BOOm9Db7.mjs → error-diagnostics-D1QfyJzQ.mjs} +51 -25
  55. package/dist/{exec-VFRpu1iA.mjs → exec-aULwtNly.mjs} +57 -6
  56. package/dist/{gc-ldgr-hXU.mjs → gc-DM5oMm-J.mjs} +161 -45
  57. package/dist/{gc-C_79zvz4.mjs → gc-PLFwb0k4.mjs} +2 -2
  58. package/dist/{guide-D7K0X6KG.mjs → guide-C4I46XAk.mjs} +674 -213
  59. package/dist/{hosted-android-Bk54ej6n.mjs → hosted-android-DlaKLbW0.mjs} +6 -7
  60. package/dist/{hosted-client-fHO8yqzc.mjs → hosted-client-DDRm9hPJ.mjs} +51 -20
  61. package/dist/{hosted-logs-BfbmDM4M.mjs → hosted-logs-bSebOKbz.mjs} +8 -8
  62. package/dist/{hosted-logs-sync-B0pvmOk-.mjs → hosted-logs-sync-B0c53W_c.mjs} +5 -5
  63. package/dist/{hosted-macos-CPl0OZ9h.mjs → hosted-macos-B7-8Pv4H.mjs} +6 -6
  64. package/dist/{hosted-native-CpDB3-PO.mjs → hosted-native-CDe33CSr.mjs} +22 -18
  65. package/dist/{idle-shutdown-FfxL1KdG.mjs → idle-shutdown-CQ0QwU9J.mjs} +7 -8
  66. package/dist/{ios-WxpDJ5T1.mjs → ios-B1DYuOjh.mjs} +27 -11
  67. package/dist/{ios-B7-lToDu.mjs → ios-B1HvafEG.mjs} +8 -8
  68. package/dist/{ios-ENC4sRPA.mjs → ios-CsRVik64.mjs} +7 -1
  69. package/dist/{ios-device-CsyryvpT.mjs → ios-device-B4elVFKi.mjs} +3 -3
  70. package/dist/{ios-device-B6HK951C.mjs → ios-device-CiBA-er9.mjs} +1 -1
  71. package/dist/{ios-project-Ty5pHl9O.mjs → ios-project-CR4mZ7CH.mjs} +22 -9
  72. package/dist/{ios-signing-4c_qXvQm.mjs → ios-signing-BJzvDPfU.mjs} +2 -2
  73. package/dist/{ios-state-Cw7-WQf2.mjs → ios-state-BPHqMDdZ.mjs} +1 -1
  74. package/dist/{ios-ftxdQbJS.mjs → ios-tg4tp3q1.mjs} +94 -37
  75. package/dist/{js-swap-BMhXYVDS.mjs → js-swap-CVpSW1fn.mjs} +8 -7
  76. package/dist/{launch-DOp8RjDz.mjs → launch-DoySmhjz.mjs} +42 -38
  77. package/dist/lightened-DFAJgfIk.mjs +171 -0
  78. package/dist/{logs-s1dRIG1E.mjs → logs-BGsW7iPe.mjs} +46 -21
  79. package/dist/{logs-dQDki6Tt.mjs → logs-DI5ILkYB.mjs} +7 -7
  80. package/dist/{macos-DBBPNnIA.mjs → macos-Bo9st9Di.mjs} +73 -38
  81. package/dist/{macos-B0VEV3Gf.mjs → macos-C12RMJQF.mjs} +1 -1
  82. package/dist/macos-run.mjs +1 -1
  83. package/dist/maintenance-run.mjs +7 -7
  84. package/dist/{metro-CjC10t6E.mjs → metro-BnhXV2Mj.mjs} +4 -4
  85. package/dist/{metro-gateway-B6_PgD0L.mjs → metro-request-DXyKjVDF.mjs} +26 -47
  86. package/dist/{named-ports-BG0j1X4g.mjs → named-ports-Phoi2POD.mjs} +3 -3
  87. package/dist/{native-android-CL2NGiik.mjs → native-android-vxgtLD7w.mjs} +17 -18
  88. package/dist/{native-gradle-artifact-inputs-btSUulme.mjs → native-gradle-artifact-inputs-BN96IVw8.mjs} +5 -5
  89. package/dist/{native-runtime-Dq3RBcON.mjs → native-runtime-DG4Rodzj.mjs} +9 -8
  90. package/dist/{native-source-D-4Dh4JU.mjs → native-source-D1AzWiuP.mjs} +1 -1
  91. package/dist/{native-xcode-inputs-BW7H8JGu.mjs → native-xcode-inputs-BKarCoKg.mjs} +3 -3
  92. package/dist/{native-xcode-ios-CBXJAxfC.mjs → native-xcode-ios-8A-efGa5.mjs} +17 -17
  93. package/dist/{ndjson-CgE_o3Dl.mjs → ndjson-Dy2W4vRa.mjs} +2 -2
  94. package/dist/offload-worker.mjs +23 -24
  95. package/dist/{operation-exec-CUZifp6Z.mjs → operation-exec-Dp4FuIGR.mjs} +1 -1
  96. package/dist/{ownership-BjYvMNX_.mjs → ownership-DJCq48IJ.mjs} +3 -3
  97. package/dist/{page-CzEA_-5_.mjs → page-CQJlcj3x.mjs} +2 -2
  98. package/dist/{phone-reverses-lmj6kcPm.mjs → phone-reverses-BzTa8g1F.mjs} +18 -4
  99. package/dist/{placement-log-DIFPQAXp.mjs → placement-log-Cvhta5ib.mjs} +3 -3
  100. package/dist/{plan-placement-IiiSQhtz.mjs → plan-placement-WkZdccuN.mjs} +36 -10
  101. package/dist/{ports-C2_w1Z0Z.mjs → ports-UV4mJgyC.mjs} +3 -3
  102. package/dist/{prebuild-gmouvmJZ.mjs → prebuild-k8d_tqOq.mjs} +16 -17
  103. package/dist/{project-BUu6Is4k.mjs → project-Sui52ywT.mjs} +17 -17
  104. package/dist/{pull-request-CbNlkFok.mjs → pull-request-BzLmzAIQ.mjs} +2 -2
  105. package/dist/pull-requests.mjs +1 -1
  106. package/dist/{react-native-android-Cgg99REN.mjs → react-native-android-DANlGGQf.mjs} +32 -26
  107. package/dist/{react-native-build-oNwqF5Wb.mjs → react-native-build-jUHnIfZ9.mjs} +9 -9
  108. package/dist/{react-native-doctor-BhA3KUte.mjs → react-native-doctor-DCWRjS7K.mjs} +6 -6
  109. package/dist/{react-native-ios-B-nf4rjN.mjs → react-native-ios-BwMsFMXi.mjs} +23 -17
  110. package/dist/{recordings-BldzVKq0.mjs → recordings-iy0mkm7b.mjs} +1 -1
  111. package/dist/{registration-CN6nlhcD.mjs → registration-BwCe64PH.mjs} +1 -1
  112. package/dist/{reload-C9OduiNc.mjs → reload-Bj7QyuDy.mjs} +1 -1
  113. package/dist/{reload-CCR7KC_l.mjs → reload-c_uEyc_X.mjs} +16 -16
  114. package/dist/{remote-C8S7FlPF.mjs → remote-DFlpAqdV.mjs} +23 -10
  115. package/dist/{result-7F5hEwQJ.mjs → result-BIcXREyC.mjs} +32 -21
  116. package/dist/{run-BqB0v3pt.mjs → run-y2HwyNvg.mjs} +3 -3
  117. package/dist/{selection-BNPBZ6T5.mjs → selection-CXgM_VK_.mjs} +37 -29
  118. package/dist/{server-bare-A75rDIdD.mjs → server-bare-2N4vSRZG.mjs} +2 -2
  119. package/dist/{server-command-CssKFOD7.mjs → server-command-qneHPcRI.mjs} +6 -6
  120. package/dist/{server-expo-eQfOdydb.mjs → server-expo-Ccm9zB0o.mjs} +4 -4
  121. package/dist/server-expo-WulkyAwT.mjs +2 -0
  122. package/dist/{settings-CosW5X-S.mjs → settings-CY5JZlNN.mjs} +375 -62
  123. package/dist/{settings-OaGQF-ri.mjs → settings-Cfd_Q6ys.mjs} +24 -3
  124. package/dist/settings-payload-DwQiEMCn.mjs +257 -0
  125. package/dist/settings-write-CJpjkeAd.mjs +185 -0
  126. package/dist/settings.schema.json +51 -33
  127. package/dist/{simslim-Bax1qcxu.mjs → simslim-DCQwzoPy.mjs} +2 -2
  128. package/dist/{slot-launch-CYCEhfBY.mjs → slot-launch-BpmanOnk.mjs} +6 -6
  129. package/dist/{stage-jKQ7CP_B.mjs → stage-CmugrSG5.mjs} +2 -2
  130. package/dist/{start-C_wefNKB.mjs → start-CHH3zgIU.mjs} +1 -1
  131. package/dist/{start-_BdJ32cZ.mjs → start-DYLk6GXn.mjs} +27 -22
  132. package/dist/{state-C1i2nMOP.mjs → state-B2QJbDwz.mjs} +4 -4
  133. package/dist/{state-C4OdpOTF.mjs → state-BXC1Lfxw.mjs} +2 -2
  134. package/dist/{stats-tEpEEH-u.mjs → stats-DxKqhcGp.mjs} +5 -5
  135. package/dist/{stats-report-n2U5plhk.mjs → stats-report-B86Cv7Uc.mjs} +1 -1
  136. package/dist/{status-CRXz-Gtj.mjs → status-7ULDEJ1e.mjs} +1 -1
  137. package/dist/{status-DQrqodLj.mjs → status-BHCkOZga.mjs} +51 -28
  138. package/dist/{status-B9M7x4CZ.mjs → status-D_Ljc89p.mjs} +1 -1
  139. package/dist/{status-watch-B5wDIH_o.mjs → status-watch-BmsPqYSU.mjs} +18 -7
  140. package/dist/status-worker.mjs +1 -1
  141. package/dist/{stim-desktop-6Dqp-gjX.mjs → stim-desktop-CbWo21kb.mjs} +2 -2
  142. package/dist/{stop-DRqQVqDJ.mjs → stop-D-dGE9gh.mjs} +21 -20
  143. package/dist/{stop-ySNQHXMG.mjs → stop-DQvE-P76.mjs} +10 -6
  144. package/dist/{stop-CmdNhGlC.mjs → stop-b2pouxAO.mjs} +2 -2
  145. package/dist/supervisor-run.mjs +237 -63
  146. package/dist/{support-BGSCBoib.mjs → support-04IhBUTT.mjs} +6 -6
  147. package/dist/{support-CVOCp0Ft.mjs → support-BtlJKjyQ.mjs} +12 -11
  148. package/dist/{swiftpm-artifact-inputs-CfUzt_0Z.mjs → swiftpm-artifact-inputs-Cj2UpmPN.mjs} +4 -4
  149. package/dist/{swiftpm-macos-x6HPpl_2.mjs → swiftpm-macos-BhF2stxY.mjs} +11 -12
  150. package/dist/{toolchain-J4J8MrAr.mjs → toolchain-mLA_km-b.mjs} +321 -15
  151. package/dist/{toolchain-DoavrvIA.mjs → toolchain-qktiV8xc.mjs} +1 -1
  152. package/dist/{trigger-KVaWWNyn.mjs → trigger-DMou-X7j.mjs} +2 -2
  153. package/dist/trigger-qp2ji5wE.mjs +2 -0
  154. package/dist/{warm-progress-DvhTX_eu.mjs → warm-progress-B0Vpuvpl.mjs} +731 -727
  155. package/dist/{web-Bihbf7-u.mjs → web-BYmK0TjL.mjs} +9 -9
  156. package/dist/web-CrVLVQZY.mjs +2 -0
  157. package/dist/web-run.mjs +9 -9
  158. package/dist/windows-report.mjs +1 -1
  159. package/dist/{workspace-state-pUg3_RRA.mjs → workspace-state-kIZrdlSg.mjs} +3 -3
  160. package/dist/{workspaces-g2lhdBXF.mjs → workspaces-CmOcceDN.mjs} +1879 -74
  161. package/dist/{worktree-BKuklFh_.mjs → worktree-2dk5edza.mjs} +933 -534
  162. package/dist/worktree-nliAoHxB.mjs +2 -0
  163. package/package.json +7 -7
  164. package/dist/ccache-Bx5TbxwG.mjs +0 -143
  165. package/dist/eas-fallback-FwkeqqGi.mjs +0 -381
  166. package/dist/hosted-ios-D55QhYaD.mjs +0 -15
  167. package/dist/native-run-BfIDFKLH.mjs +0 -275
  168. package/dist/server-expo-wvcj5tiZ.mjs +0 -2
  169. package/dist/settings-write-QjYfSrjF.mjs +0 -360
  170. package/dist/trigger-DzJu208Q.mjs +0 -2
  171. package/dist/web-BFLueSh-.mjs +0 -2
  172. package/dist/worktree-Bm4cb-nU.mjs +0 -2
@@ -1,209 +1,45 @@
1
- import { D as ownership_claim_exports, S as process_identity_exports, b as operationState, l as WorkspaceProcessCleanupError, m as declareSpawnsOn, n as isTimeoutError, t as getExecutor, v as stopDeclaringSpawnsOn } from "./exec-VFRpu1iA.mjs";
2
- import { E as workspaceDir, a as recordWorkspaceUseAsync, l as withWorkspaceStateLockAsync, r as readWorkspaceState$1, w as workspaceAgentDeviceDir } from "./workspace-state-pUg3_RRA.mjs";
3
- import { E as upsertProject, G as projectDeviceSlots, L as measuredDirectorySizeAsync, O as withConfigLockAsync$1, S as removeProject, _ as loadConfig$1, a as clearDevice, d as getConfigDir, g as isPathPrefix, j as acquireAvdClaim, p as getProject } from "./config-BktkY_zC.mjs";
4
- import { a as formatElapsed, h as shortUdid, i as formatDuration, l as phaseLine, p as releasedLeaseFact, u as plural } from "./command-output-BF5AR2dL.mjs";
5
- import { c as gitQuiet, o as git, r as childEnvironment, s as gitAsync, t as runOperationFile } from "./operation-exec-CUZifp6Z.mjs";
6
- import { At as podsOutOfSync, B as readCommittedSettings, Bt as unpushedCommitsAsync, Ct as hasUncommittedWorkAsync, Dt as listWorktreesAsync, Ft as resolveFullRef, Gt as removeTemporaryEntryAsync, Ht as filesystemDevice, It as resolveFullRefAsync, J as resolveProjectSettings, Mt as removeWorktreeAsync, Ot as locallyKnownUpstream, Rt as restoreFileAsync, St as hasRemoteAsync, Vt as warmWorktreePaths, X as resolveSettings, _t as dirtyPathsAsync, a as SETTING_SHAPE_REMEDY, at as unknownSettingKeys, bt as gitCommonDirOnDisk, ft as branchExistsAsync, ht as depsOutOfSync, jt as readWorktreeExclude, mt as deleteBranchAsync, pt as cloneIgnoredEntries, q as resolveIosProjectDir, tt as settingShapeErrors, wt as isPodInstallChurn, xt as hasPopulatedSubmodulesAsync, yt as gitCommonDirAsync, zt as sourceCheckoutOf } from "./settings-CosW5X-S.mjs";
7
- 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-BUu6Is4k.mjs";
8
- import { t as macosAppPresent } from "./state-C4OdpOTF.mjs";
9
- import { a as workspaceLinkLine, o as workspaceLinks } from "./stim-desktop-6Dqp-gjX.mjs";
10
- import { ft as POOL_SETTING_REMEDY, vt as parkedMaxSetting } from "./android-DTAcRwBC.mjs";
11
- import { c as pidExists, d as resolveProjectMetro, p as signalProcessTreeAsync, r as killMetroTreeAsync } from "./metro-CjC10t6E.mjs";
12
- import { o as readWebRecord } from "./state-C1i2nMOP.mjs";
13
- import { Dr as discoverCaches, Gt as endRecordedSession, S as withdrawDevServerStopRequest, Un as runProcessOperation, b as requestDevServerStop, c as readVolumeSpace, ct as planCacheEmptying, er as resolveEasCliBin, l as readVolumeSpaceAsync, o as machineNumber, u as resolveBudget } from "./budget-k2mZGvvk.mjs";
1
+ import { D as ownership_claim_exports, S as process_identity_exports, b as operationState, l as WorkspaceProcessCleanupError, m as declareSpawnsOn, n as isTimeoutError, t as getExecutor, v as stopDeclaringSpawnsOn } from "./exec-aULwtNly.mjs";
2
+ import { E as workspaceDir, a as recordWorkspaceUseAsync, l as withWorkspaceStateLockAsync, r as readWorkspaceState$1, w as workspaceAgentDeviceDir } from "./workspace-state-kIZrdlSg.mjs";
3
+ import { E as upsertProject, G as projectDeviceSlots, L as measuredDirectorySizeAsync, O as withConfigLockAsync$1, S as removeProject, _ as loadConfig$1, a as clearDevice, d as getConfigDir, g as isPathPrefix$1, j as acquireAvdClaim, p as getProject } from "./config-Cn94Jc0g.mjs";
4
+ import { a as formatElapsed, h as shortUdid, i as formatDuration, l as phaseLine, p as releasedLeaseFact, u as plural } from "./command-output-C8R1KIvi.mjs";
5
+ import { c as gitQuiet, l as gitQuietAsync, o as git, r as childEnvironment, s as gitAsync, t as runOperationFile } from "./operation-exec-Dp4FuIGR.mjs";
6
+ import { Ct as gitCommonDirOnDisk, Dt as isPodInstallChurn, Et as hasUncommittedWorkAsync, Ft as matchWorktreeEntry, Gt as restoreFileAsync, Ht as resolveFullRef, It as nativeCanonicalPath, Jt as warmWorktreePaths, Kt as sourceCheckoutOf, Lt as podsOutOfSync, Mt as locallyKnownUpstream, Q as resolveSettings, Rt as readWorktreeExclude, St as gitCommonDirAsync, Tt as hasRemoteAsync, Ut as resolveFullRefAsync, V as readCommittedSettings, X as resolveProjectSettings, Y as resolveIosProjectDir, _ as inertSettingWarning, _t as deleteBranchAsync, a as SETTING_SHAPE_REMEDY, bt as dirtyPathsAsync, gt as cloneIgnoredEntries, ht as branchExistsAsync, jt as listWorktreesAsync, nn as moveIgnoredTreesToTrash, qt as unpushedCommitsAsync, rn as restoreTrashedTrees, rt as settingShapeErrors, st as unknownSettingKeys, tn as disposeTrash, vt as depsOutOfSync, wt as hasPopulatedSubmodulesAsync, zt as removeWorktreeAsync } from "./settings-CY5JZlNN.mjs";
7
+ 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-Sui52ywT.mjs";
8
+ import { t as macosAppPresent } from "./state-BXC1Lfxw.mjs";
9
+ import { a as workspaceLinkLine, o as workspaceLinks } from "./stim-desktop-CbWo21kb.mjs";
10
+ import { ft as POOL_SETTING_REMEDY, vt as parkedMaxSetting } from "./android-CsPs-7Mg.mjs";
11
+ import { c as pidExists, d as resolveProjectMetro, p as signalProcessTreeAsync, r as killMetroTreeAsync } from "./metro-BnhXV2Mj.mjs";
12
+ import { A as diskPressureLevel, D as requestDevServerStop, En as runProcessOperation, G as planCacheEmptying, Ln as resolveEasCliBin, N as lowestFree, Ot as endRecordedSession, _ as resolveBudget, g as readVolumeSpaceAsync, h as readVolumeSpace, k as withdrawDevServerStopRequest, p as machineNumber, ur as discoverCaches } from "./ccache-Bm_A1xX9.mjs";
13
+ import { o as readWebRecord } from "./state-B2QJbDwz.mjs";
14
+ import { C as stopTunnel, E as withManagedRemoteWorktreeRemovalLock, O as withManagedTunnelRemovalLock, _ as emptyWorkspaceDir, ht as teardownOwnedIosSim, mt as teardownOwnedBrowser, o as collectWorkspaceOutputs, pt as teardownOwnedAvd, v as withIdleWorkspace, x as resolveSupervisorTarget } from "./workspaces-CmOcceDN.mjs";
14
15
  import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
15
- import { p as cacheUser } from "./native-run-BfIDFKLH.mjs";
16
- import { A as teardownOwnedIosSim, C as withManagedTunnelRemovalLock, O as teardownOwnedAvd, a as collectWorkspaceOutputs, f as emptyWorkspaceDir, g as resolveSupervisorTarget, k as teardownOwnedBrowser, p as withIdleWorkspace, v as stopTunnel, x as withManagedRemoteWorktreeRemovalLock } from "./workspaces-g2lhdBXF.mjs";
17
- import { D as releaseWorkspaceLeases, t as agentDeviceDaemon } from "./activity-Lnb_EoRY.mjs";
18
- import { L as readHostMemoryPressure, R as readHostMemoryPressureAsync } from "./ios-WxpDJ5T1.mjs";
19
- import { t as clearNamedPorts } from "./named-ports-BG0j1X4g.mjs";
16
+ import { D as runCaptured, O as runPodInstall, S as DEPS_ERROR, T as readPodState, Y as podInstallCommand, ct as cacheUser, j as HEARTBEAT_INTERVAL_MS, w as podsAreStale } from "./toolchain-mLA_km-b.mjs";
17
+ import { D as releaseWorkspaceLeases, t as agentDeviceDaemon } from "./activity-DRMg4_mD.mjs";
18
+ import { R as readHostMemoryPressure, z as readHostMemoryPressureAsync } from "./ios-B1DYuOjh.mjs";
19
+ import { t as clearNamedPorts } from "./named-ports-Phoi2POD.mjs";
20
20
  import { t as resolveMaintenanceSettings } from "./settings-Djl-mpjX.mjs";
21
- import { D as runCaptured, O as runPodInstall, S as DEPS_ERROR, T as readPodState, Y as podInstallCommand, j as HEARTBEAT_INTERVAL_MS, w as podsAreStale } from "./toolchain-J4J8MrAr.mjs";
22
- import { i as stopHostedAndroid } from "./hosted-android-Bk54ej6n.mjs";
23
- import { r as stopHostedIos } from "./hosted-ios-D55QhYaD.mjs";
24
- import { n as stopMacosApp } from "./stop-ySNQHXMG.mjs";
25
- import { n as verifyCollectorOwnership } from "./ownership-BjYvMNX_.mjs";
26
- import { n as removePhoneReverses } from "./phone-reverses-lmj6kcPm.mjs";
27
- import { a as discoverXcodeProject } from "./react-native-build-oNwqF5Wb.mjs";
28
- import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-DFB41vfr.mjs";
29
- import { a as forgetStatusMeasures, p as mergeStateAsync, t as startWarmProgress } from "./warm-progress-DvhTX_eu.mjs";
30
- import { o as pullRequestLookupAsync, r as endedPullRequest } from "./pull-request-CbNlkFok.mjs";
31
- import { r as workspaceRecordingEnabled } from "./recordings-BldzVKq0.mjs";
21
+ import { i as stopHostedAndroid } from "./hosted-android-DlaKLbW0.mjs";
22
+ import { a as stopHostedIos, n as removePhoneReverses } from "./phone-reverses-BzTa8g1F.mjs";
23
+ import { r as stopMacosApp } from "./stop-DQvE-P76.mjs";
24
+ import { n as verifyCollectorOwnership } from "./ownership-DJCq48IJ.mjs";
25
+ import { o as pullRequestLookupAsync, r as endedPullRequest } from "./pull-request-BzLmzAIQ.mjs";
26
+ import { c as mergeStateAsync, d as forgetStatusMeasures, t as startWarmProgress } from "./warm-progress-B0Vpuvpl.mjs";
27
+ import { r as workspaceRecordingEnabled } from "./recordings-iy0mkm7b.mjs";
28
+ import { i as installedNpmTreeProblems, n as hasInstalledDependencies, t as dependencyState } from "./dependency-state-CojA2o0t.mjs";
29
+ import { a as discoverXcodeProject } from "./react-native-build-jUHnIfZ9.mjs";
32
30
  import { claimRemoveCommand, clearFreeClaimSet, readClaimSet, releaseClaim, tryAcquireClaim } from "@stim-cli/core/ownership-claim";
33
31
  import { createHash, randomUUID } from "node:crypto";
34
32
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
35
33
  import { cpus, freemem, homedir, loadavg, totalmem } from "node:os";
36
34
  import { basename, dirname, join, relative } from "node:path";
37
- import { buildCacheRoot, configDir, metroCacheRoot, unitLines, unitWords, withDirLockTransaction, workspaceName } from "@stim-cli/core";
38
- 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, trashRoot } from "@stim-cli/core/state";
35
+ import { buildCacheRoot, configDir, metroCacheRoot, quotedPath, unitLines, unitWords, withDirLockTransaction, workspaceName } from "@stim-cli/core";
36
+ import { LAST_BUILD_KEYS, archiveDir, archiveEnabled, archiveLock, archiveRoot, archivedUsage, buildWorkerRoot, countErrorEntries, endedAgentSessionOf, isJsonObject, isPathPrefix, listRecordedDevicesAsync, loadConfig, queryLogs, readArchivesAsync, readBuildHistory, readJsonObject, readLastBuilds, readMacosRecord, readPullRequestCache, readWorkspaceState, recordingEnabled, sharedCcache, sharedCompilationCache, sortByTs, trace, worktreeArchiveRoot } from "@stim-cli/core/state";
39
37
  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
38
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
41
- import { captureProcessIdentity } from "@stim-cli/core/process-identity";
42
39
  import chalk from "chalk";
43
40
  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, mkdtemp, readdir, rename, rm, rmdir, stat, utimes, writeFile } from "node:fs/promises";
41
+ import { chmod, lstat, mkdir, readdir, rename, rm, rmdir, stat, utimes, writeFile } from "node:fs/promises";
45
42
  import { createHash as createHash$1 } from "crypto";
46
- //#region src/maintenance/pressure.ts
47
- /**
48
- * The free-disk floor of each level in MiB. Level 2 has no floor of its own: it runs when level 1 left free disk
49
- * under the budget floor, and stops once free disk is back above it. Its urgent threshold only sets the reported level.
50
- */
51
- function levelFloors(budget, settings) {
52
- const floor = Math.max(budget.minFreeDiskMb, budget.hardFloorDiskMb);
53
- return {
54
- 1: floor,
55
- 2: floor,
56
- 3: Math.max(settings.criticalFreeDiskGb * 1024, budget.hardFloorDiskMb),
57
- urgent: settings.urgentFreeDiskGb * 1024
58
- };
59
- }
60
- function diskPressureLevel(freeMb, budget, settings) {
61
- const floors = levelFloors(budget, settings);
62
- if (freeMb < floors[3]) return 3;
63
- if (freeMb < floors.urgent) return 2;
64
- return freeMb < floors[1] ? 1 : 0;
65
- }
66
- function lowestFree(disk) {
67
- return disk.length ? Math.min(...disk.map((volume) => volume.freeMb)) : Infinity;
68
- }
69
- /** Whether an action planned at a pressure level may still run with this much free disk. */
70
- function levelStillShort(action, freeMb, budget, settings) {
71
- return action.level === void 0 || freeMb < levelFloors(budget, settings)[action.level];
72
- }
73
- /** The protection window in milliseconds: protectRecentHours, or maintenance.pressureProtectMinutes for actions of level 2 and 3. */
74
- function protectWindowMs(action, settings) {
75
- return action.level === void 0 ? settings.protectRecentHours * 36e5 : settings.pressureProtectMinutes * 6e4;
76
- }
77
- //#endregion
78
- //#region src/maintenance/measure.ts
79
- function measureDisk() {
80
- return readVolumeSpace([
81
- configDir(),
82
- ...Object.keys(loadConfig()?.projects ?? {}),
83
- buildWorkerRoot(loadConfig())
84
- ]);
85
- }
86
- function measureDiskAsync(operation) {
87
- return readVolumeSpaceAsync([
88
- configDir(),
89
- ...Object.keys(loadConfig()?.projects ?? {}),
90
- buildWorkerRoot(loadConfig())
91
- ], operation.signal);
92
- }
93
- /** The disk pressure level of the fullest volume Stim writes to, measured now. */
94
- function currentDiskLevel(disk = measureDisk()) {
95
- return diskPressureLevel(lowestFree(disk), resolveBudget().budget, resolveMaintenanceSettings());
96
- }
97
- function measurePressure(settings, previous, now) {
98
- return pressureFromObservations(settings, previous, now, readHostMemoryPressure(), measureDisk());
99
- }
100
- async function measurePressureAsync(settings, previous, now, operation) {
101
- return pressureFromObservations(settings, previous, now, await readHostMemoryPressureAsync(void 0, void 0, operation.signal, operation), await readVolumeSpaceAsync([
102
- configDir(),
103
- ...Object.keys(loadConfig()?.projects ?? {}),
104
- buildWorkerRoot(loadConfig())
105
- ], operation.signal));
106
- }
107
- function pressureFromObservations(settings, previous, now, level, disk) {
108
- const availableBytes = process.platform === "darwin" ? null : freemem();
109
- const warningSince = level === "warning" ? previous?.warningSince ?? now : null;
110
- return {
111
- disk,
112
- memory: {
113
- level,
114
- availableBytes,
115
- pressured: settings.memoryPressureLevel !== "off" && (level === "critical" || settings.memoryPressureLevel === "warning" && warningSince !== null && now - warningSince >= settings.memoryWarningMinutes * 6e4 || availableBytes !== null && availableBytes < (settings.minAvailableMemoryGb === void 0 ? totalmem() * .1 : settings.minAvailableMemoryGb * 1024 ** 3))
116
- },
117
- warningSince
118
- };
119
- }
120
- function sizeScanDeferred(settings) {
121
- return loadavg()[0] / Math.max(1, cpus().length) > settings.maxLoadPerCore;
122
- }
123
- function cacheBlocked(entry, discovered = discoverCaches(), env = process.env) {
124
- return planCacheEmptying([{
125
- ...entry,
126
- note: "",
127
- prune: "entries",
128
- ...discovered.some((cache) => canonicalPath(cache.dir) === canonicalPath(entry.dir) && cache.source === "registered") ? { source: "registered" } : {}
129
- }], false, env)[0]?.machineGlobal ?? (entry.category === "compilation-cache" ? cacheUser(entry.dir) : void 0);
130
- }
131
- async function measureSizes(now, failure) {
132
- const sizes = [];
133
- const outputs = await collectWorkspaceOutputs({
134
- olderThan: null,
135
- now,
136
- sizeTimeoutMs: 12e4
137
- });
138
- for (const entry of outputs.workspaces) {
139
- if (entry.bytes === null) {
140
- failure(entry.dir, entry.projectRoot ?? void 0);
141
- continue;
142
- }
143
- sizes.push({
144
- name: "workspace build outputs",
145
- category: "workspace-outputs",
146
- dir: entry.dir,
147
- bytes: entry.bytes,
148
- measuredAt: now,
149
- ...entry.projectRoot ? { workspace: canonicalPath(entry.projectRoot) } : {},
150
- idleDays: entry.idleDays,
151
- ...!entry.willClear ? { blocked: entry.keptReason ?? "workspace ownership is unresolved" } : {}
152
- });
153
- }
154
- const known = [
155
- {
156
- name: "shared build cache",
157
- dir: buildCacheRoot(),
158
- category: "build-cache"
159
- },
160
- {
161
- name: "Metro transform caches",
162
- dir: metroCacheRoot(),
163
- category: "metro-cache"
164
- },
165
- {
166
- name: "ccache",
167
- dir: sharedCcache(),
168
- category: "ccache"
169
- },
170
- {
171
- name: "Swift compilation cache",
172
- dir: sharedCompilationCache(),
173
- category: "compilation-cache"
174
- }
175
- ];
176
- const discovered = discoverCaches();
177
- for (const cache of discovered) {
178
- if (cache.source !== "registered" && !canonicalPath(cache.dir).startsWith(`${canonicalPath(configDir())}/`)) continue;
179
- if (cache.prune === "report-only") continue;
180
- const dir = canonicalPath(cache.dir);
181
- if (known.some((item) => dir === canonicalPath(item.dir) || dir.startsWith(`${canonicalPath(item.dir)}/`))) continue;
182
- const category = cache.name.startsWith("Metro transform cache") ? "metro-cache" : /^(Expo build cache|native build cache|Build cache)$/.test(cache.name) ? "build-cache" : "other";
183
- known.push({
184
- name: cache.name,
185
- dir: cache.dir,
186
- category
187
- });
188
- }
189
- for (const entry of known) {
190
- if (!existsSync(entry.dir)) continue;
191
- const bytes = await measuredDirectorySizeAsync(entry.dir, { timeoutMs: 12e4 });
192
- if (bytes === null) {
193
- failure(entry.dir);
194
- continue;
195
- }
196
- const blocked = cacheBlocked(entry, discovered);
197
- sizes.push({
198
- ...entry,
199
- bytes,
200
- measuredAt: now,
201
- ...blocked ? { blocked } : {}
202
- });
203
- }
204
- return sizes;
205
- }
206
- //#endregion
207
43
  //#region src/workspace/pods-relocate.ts
208
44
  const CHECKSUMS_HEADER = "SPEC CHECKSUMS:\n";
209
45
  const GENERATED_DIRS = [
@@ -828,6 +664,95 @@ async function removeKind(record, kind) {
828
664
  function logRemovalOrder(path) {
829
665
  return /\.ndjson\.\d+$/.test(path) ? 0 : /^build-.*\.ndjson$/.test(basename(path)) ? 1 : 2;
830
666
  }
667
+ /**
668
+ * How long and how large worktree archives of `root` are kept, from `archive.maxAgeDays` and `archive.maxTotalGb`, or
669
+ * why its uncommitted changes cannot be archived: archives are disabled for it or for a project registered inside it,
670
+ * a limit is 0, or a setting is invalid.
671
+ */
672
+ function worktreeArchiveLimits(root, env = process.env) {
673
+ if (!enabled(root, env)) return { refusal: "archive.enabled is false for it" };
674
+ const nested = Object.keys(loadConfig()?.projects ?? {}).find((key) => key !== root && isPathPrefix(root, key) && !enabled(key, env));
675
+ if (nested) return { refusal: `archive.enabled is false for ${nested}, inside it` };
676
+ let limits;
677
+ try {
678
+ limits = archiveSettings(env);
679
+ } catch (error) {
680
+ return { refusal: (error instanceof Error ? error.message : String(error)).split("\n")[0] ?? "" };
681
+ }
682
+ if (!limits.maxAgeDays) return { refusal: "archive.maxAgeDays is 0, so no archive would be kept" };
683
+ if (!limits.maxTotalGb) return { refusal: "archive.maxTotalGb is 0, so no archive would be kept" };
684
+ return {
685
+ maxAgeDays: limits.maxAgeDays,
686
+ maxBytes: limits.maxTotalGb * 1024 ** 3
687
+ };
688
+ }
689
+ async function listWorktreeArchives(signal) {
690
+ const root = worktreeArchiveRoot();
691
+ let repositories;
692
+ try {
693
+ repositories = await readdir(root);
694
+ } catch {
695
+ return {
696
+ repositories: [],
697
+ entries: []
698
+ };
699
+ }
700
+ const entries = [];
701
+ for (const repository of repositories) {
702
+ signal?.throwIfAborted();
703
+ const dir = join(root, repository);
704
+ if (!(await lstat(dir)).isDirectory()) continue;
705
+ for (const name of await readdir(dir)) {
706
+ if (name.endsWith(".claims")) continue;
707
+ const path = join(dir, name);
708
+ const entry = await lstat(path);
709
+ if (!entry.isDirectory()) continue;
710
+ const created = Date.parse(String(readJsonObject(join(path, "manifest.json"))?.createdAt ?? ""));
711
+ const bytes = (await files(path, signal)).reduce((sum, file) => sum + file.bytes, 0);
712
+ entries.push({
713
+ path,
714
+ at: Number.isFinite(created) ? created : entry.mtimeMs,
715
+ bytes
716
+ });
717
+ }
718
+ }
719
+ return {
720
+ repositories,
721
+ entries
722
+ };
723
+ }
724
+ /** Bytes every worktree archive holds, the one being written included. */
725
+ async function worktreeArchiveBytes(signal) {
726
+ return (await listWorktreeArchives(signal)).entries.reduce((sum, entry) => sum + entry.bytes, 0);
727
+ }
728
+ async function pruneWorktreeArchives(now, limits, signal) {
729
+ const listed = await listWorktreeArchives(signal);
730
+ const { repositories } = listed;
731
+ const entries = listed.entries.filter((entry) => {
732
+ const claims = readClaimSet(`${entry.path}.claims`);
733
+ return !claims.live.length && !claims.unresolved.length;
734
+ });
735
+ entries.sort((a, b) => a.at - b.at);
736
+ let total = entries.reduce((sum, entry) => sum + entry.bytes, 0);
737
+ const cap = (limits.maxTotalGb ?? 0) * 1024 ** 3;
738
+ const maxAgeMs = (limits.maxAgeDays ?? 0) * DAY;
739
+ for (const entry of entries) {
740
+ signal?.throwIfAborted();
741
+ if (now - entry.at < maxAgeMs && total <= cap) continue;
742
+ if (clearFreeClaimSet({
743
+ root: `${entry.path}.claims`,
744
+ label: "worktree archive"
745
+ }).status !== "cleared") continue;
746
+ await rm(entry.path, {
747
+ recursive: true,
748
+ force: true
749
+ });
750
+ total -= entry.bytes;
751
+ }
752
+ for (const repository of repositories) try {
753
+ await rmdir(join(worktreeArchiveRoot(), repository));
754
+ } catch {}
755
+ }
831
756
  async function enforceArchiveRetention(now = Date.now(), operation) {
832
757
  const signal = operation?.signal;
833
758
  const env = operation?.env ?? process.env;
@@ -901,6 +826,7 @@ async function enforceArchiveRetention(now = Date.now(), operation) {
901
826
  record.expires.record = new Date(Date.parse(record.removedAt) + limits.maxAgeDays * DAY).toISOString();
902
827
  if (JSON.stringify(record.expires) !== before) await writeRecord(archiveDir(record.id), record);
903
828
  }
829
+ await pruneWorktreeArchives(now, limits, signal);
904
830
  }, {
905
831
  waitMs: 3e4,
906
832
  signal
@@ -1456,7 +1382,7 @@ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwned
1456
1382
  //#region src/workspace/service-guard.ts
1457
1383
  const SERVICE_IN_USE = { code: "STIM_WORKTREE_SERVICE" };
1458
1384
  function inside(folder, canonical) {
1459
- return (value) => typeof value === "string" && value.startsWith("/") && isPathPrefix(folder, canonical(value));
1385
+ return (value) => typeof value === "string" && value.startsWith("/") && isPathPrefix$1(folder, canonical(value));
1460
1386
  }
1461
1387
  function serviceUseOf(plist, file, folder, canonical) {
1462
1388
  if (typeof plist !== "object" || plist === null) return null;
@@ -1535,37 +1461,561 @@ function serviceRunningFrom(folder, canonical, agentsDir = join(homedir(), "Libr
1535
1461
  argument: null
1536
1462
  };
1537
1463
  }
1538
- for (const name of names) {
1539
- const file = join(agentsDir, name);
1540
- const label = name.replace(/\.plist$/, "");
1541
- try {
1542
- if (!readFileSync(file).includes("StimService")) continue;
1543
- } catch {
1544
- return {
1545
- label,
1546
- plist: file,
1547
- argument: null
1548
- };
1464
+ for (const name of names) {
1465
+ const file = join(agentsDir, name);
1466
+ const label = name.replace(/\.plist$/, "");
1467
+ try {
1468
+ if (!readFileSync(file).includes("StimService")) continue;
1469
+ } catch {
1470
+ return {
1471
+ label,
1472
+ plist: file,
1473
+ argument: null
1474
+ };
1475
+ }
1476
+ try {
1477
+ const json = getExecutor().runFile("plutil", [
1478
+ "-convert",
1479
+ "json",
1480
+ "-o",
1481
+ "-",
1482
+ file
1483
+ ], { timeoutMs: 5e3 });
1484
+ const use = serviceUseOf(JSON.parse(json), file, folder, canonical);
1485
+ if (use) return use;
1486
+ } catch {
1487
+ return {
1488
+ label,
1489
+ plist: file,
1490
+ argument: null
1491
+ };
1492
+ }
1493
+ }
1494
+ return null;
1495
+ }
1496
+ /** The Stim CLI entry points removal must not delete: the running one and the `stim` on PATH. */
1497
+ function stimCliEntries(env) {
1498
+ const entries = [];
1499
+ if (process.argv[1]) entries.push({
1500
+ label: "the running stim",
1501
+ path: process.argv[1]
1502
+ });
1503
+ const onPath = getExecutor().findExecutable("stim", env ? { env } : void 0);
1504
+ if (onPath) entries.push({
1505
+ label: "the stim on PATH",
1506
+ path: onPath
1507
+ });
1508
+ return entries;
1509
+ }
1510
+ /** Why removing `folder` would delete a Stim CLI in `entries`, resolved through symlinks; empty when it would not. */
1511
+ function stimCliUseOf(folder, canonical, entries) {
1512
+ return entries.flatMap(({ label, path }) => {
1513
+ const resolved = canonical(path);
1514
+ return isPathPrefix$1(folder, resolved) ? [`it holds ${label} (${resolved})`] : [];
1515
+ });
1516
+ }
1517
+ //#endregion
1518
+ //#region src/workspace/worktree-archive.ts
1519
+ const WATCHMAN_COOKIE = /(?:^|\/)\.watchman-cookie-[^/]*$/;
1520
+ const ARCHIVE_TIMEOUT_MS = 6e5;
1521
+ function execution(operation) {
1522
+ return {
1523
+ ...operation ? childEnvironment(operation.env) : {},
1524
+ signal: operation?.signal,
1525
+ declareFor: operation,
1526
+ killSignal: "SIGKILL"
1527
+ };
1528
+ }
1529
+ function nulList(text) {
1530
+ return text.split("\0").filter(Boolean);
1531
+ }
1532
+ async function changesOf(path, operation) {
1533
+ const read = {
1534
+ ...execution(operation),
1535
+ untrimmed: true,
1536
+ timeoutMs: 6e4
1537
+ };
1538
+ const tracked = nulList(await gitAsync(path, [
1539
+ "diff",
1540
+ "--name-only",
1541
+ "--no-renames",
1542
+ "-z",
1543
+ "HEAD",
1544
+ "--"
1545
+ ], read));
1546
+ const untracked = nulList(await gitAsync(path, [
1547
+ "ls-files",
1548
+ "-z",
1549
+ "--others",
1550
+ "--exclude-standard",
1551
+ "--"
1552
+ ], read)).filter((file) => !WATCHMAN_COOKIE.test(file));
1553
+ return {
1554
+ tracked: tracked.toSorted(),
1555
+ untracked: untracked.toSorted()
1556
+ };
1557
+ }
1558
+ /**
1559
+ * Paths whose staged content differs from the working tree. `git diff HEAD` saves only the working tree, so a staged
1560
+ * version that the working tree no longer holds would be lost.
1561
+ */
1562
+ async function stagedOnly(path, operation) {
1563
+ const read = {
1564
+ ...execution(operation),
1565
+ untrimmed: true,
1566
+ timeoutMs: 6e4
1567
+ };
1568
+ const staged = new Set(nulList(await gitAsync(path, [
1569
+ "diff",
1570
+ "--cached",
1571
+ "--name-only",
1572
+ "--no-renames",
1573
+ "-z",
1574
+ "--"
1575
+ ], read)));
1576
+ return nulList(await gitAsync(path, [
1577
+ "diff",
1578
+ "--name-only",
1579
+ "--no-renames",
1580
+ "-z",
1581
+ "--"
1582
+ ], read)).filter((file) => staged.has(file));
1583
+ }
1584
+ async function headSizes(path, files, operation) {
1585
+ if (!files.length) return 0;
1586
+ if (files.some((file) => file.includes("\n"))) throw new Error("a changed path contains a newline");
1587
+ return (await gitAsync(path, ["cat-file", "--batch-check=%(objectsize)"], {
1588
+ ...execution(operation),
1589
+ timeoutMs: 6e4,
1590
+ input: `${files.map((file) => `HEAD:${file}`).join("\n")}\n`
1591
+ })).split("\n").reduce((sum, line) => sum + (/^\d+$/.test(line.trim()) ? Number(line.trim()) : 0), 0);
1592
+ }
1593
+ async function changedAt(file, deleted) {
1594
+ for (let current = file;; current = dirname(current)) try {
1595
+ const { mtimeMs, ctimeMs } = await lstat(current);
1596
+ return Math.max(mtimeMs, ctimeMs);
1597
+ } catch (error) {
1598
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
1599
+ if (deleted === "skip" || dirname(current) === current) return null;
1600
+ }
1601
+ }
1602
+ /** With `parent`, a deleted file counts its nearest existing parent directory's change, since unlinking changes it. */
1603
+ async function newestChange(path, changes, deleted = "skip") {
1604
+ let newest = null;
1605
+ for (const file of [...changes.tracked, ...changes.untracked]) {
1606
+ const changed = await changedAt(join(path, file), deleted);
1607
+ if (changed !== null && (newest === null || changed > newest)) newest = changed;
1608
+ }
1609
+ return newest;
1610
+ }
1611
+ /**
1612
+ * Plans a bundle of HEAD's commits that the default branch lacks, all of HEAD's history without `origin/HEAD`, so the
1613
+ * restore can recreate HEAD after its branch is deleted and git collects the commits; null when the default branch
1614
+ * already contains HEAD. `bytes` is their size on disk.
1615
+ */
1616
+ async function headBundlePlan(path, head, operation) {
1617
+ const run = {
1618
+ ...execution(operation),
1619
+ timeoutMs: ARCHIVE_TIMEOUT_MS
1620
+ };
1621
+ const base = (await gitQuietAsync(path, [
1622
+ "symbolic-ref",
1623
+ "--quiet",
1624
+ "refs/remotes/origin/HEAD"
1625
+ ], run))?.trim();
1626
+ if (base && await gitQuietAsync(path, [
1627
+ "merge-base",
1628
+ "--is-ancestor",
1629
+ head,
1630
+ base
1631
+ ], run) !== null) return null;
1632
+ const exclude = base ? [`^${base}`] : [];
1633
+ const bytes = Number(await gitAsync(path, [
1634
+ "rev-list",
1635
+ "--disk-usage",
1636
+ "--objects",
1637
+ "HEAD",
1638
+ ...exclude
1639
+ ], run));
1640
+ if (!Number.isFinite(bytes)) throw new Error("git did not report the size of the commits to bundle");
1641
+ return {
1642
+ exclude,
1643
+ bytes
1644
+ };
1645
+ }
1646
+ async function bundleHead(path, plan, dir, operation) {
1647
+ const run = {
1648
+ ...execution(operation),
1649
+ timeoutMs: ARCHIVE_TIMEOUT_MS
1650
+ };
1651
+ const bundle = join(dir, "head.bundle");
1652
+ await gitAsync(path, [
1653
+ "bundle",
1654
+ "create",
1655
+ bundle,
1656
+ "HEAD",
1657
+ ...plan.exclude
1658
+ ], run);
1659
+ await chmod(bundle, 384);
1660
+ await gitAsync(path, [
1661
+ "bundle",
1662
+ "verify",
1663
+ bundle
1664
+ ], run);
1665
+ return bundle;
1666
+ }
1667
+ /**
1668
+ * When a worktree's uncommitted or untracked, non-ignored files last changed, a deleted file by its nearest existing
1669
+ * parent directory: null when it has none, and `unreadable` when git or the files cannot be read. git records no time
1670
+ * for these edits, so the idle checks read the files themselves.
1671
+ */
1672
+ async function uncommittedActivityOf(path, operation) {
1673
+ try {
1674
+ const at = await newestChange(path, await changesOf(path, operation), "parent");
1675
+ return at === null ? null : {
1676
+ at,
1677
+ basis: "an uncommitted file change"
1678
+ };
1679
+ } catch (error) {
1680
+ operation?.signal.throwIfAborted();
1681
+ if (error instanceof Error && "reportCleanup" in error) throw error;
1682
+ return "unreadable";
1683
+ }
1684
+ }
1685
+ function sameList(a, b) {
1686
+ return a.length === b.length && a.every((value, index) => value === b[index]);
1687
+ }
1688
+ function safeName(value) {
1689
+ return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[.-]+/, "") || "worktree";
1690
+ }
1691
+ function stamp(at) {
1692
+ return new Date(at).toISOString().replace(/[-:]/g, "").replace(".", "-");
1693
+ }
1694
+ /**
1695
+ * Why the archive no longer describes the worktree, or null when it still does: the same changed and untracked files,
1696
+ * none modified since the archive started, and the patch still reverses cleanly against the working tree.
1697
+ */
1698
+ async function worktreeArchiveMismatch(path, archive, operation) {
1699
+ if (archive.tracked.length) try {
1700
+ await gitAsync(path, [
1701
+ "apply",
1702
+ "--check",
1703
+ "-R",
1704
+ join(archive.dir, "tracked.patch")
1705
+ ], {
1706
+ ...execution(operation),
1707
+ timeoutMs: ARCHIVE_TIMEOUT_MS
1708
+ });
1709
+ } catch (error) {
1710
+ if (error instanceof Error && "reportCleanup" in error) throw error;
1711
+ operation?.signal.throwIfAborted();
1712
+ return `the archived patch does not reverse against the worktree: ${(error instanceof Error ? error.message : String(error)).split("\n")[0]}`;
1713
+ }
1714
+ const staged = await stagedOnly(path, operation);
1715
+ if (staged.length) return `the staged content of ${staged.join(", ")} differs from its working tree`;
1716
+ const now = await changesOf(path, operation);
1717
+ if (!sameList(now.tracked, archive.tracked) || !sameList(now.untracked, archive.untracked)) return "its uncommitted or untracked files changed while they were archived";
1718
+ const newest = await newestChange(path, now);
1719
+ if (newest !== null && newest >= archive.at) return "an uncommitted file changed while it was archived";
1720
+ for (const file of [
1721
+ ...archive.bundled ? ["head.bundle"] : [],
1722
+ ...archive.tracked.length ? ["tracked.patch"] : [],
1723
+ ...archive.untracked.length ? ["untracked.tar"] : []
1724
+ ]) if (!await lstat(join(archive.dir, file)).catch(() => null)) return `its archive lost ${file}`;
1725
+ return null;
1726
+ }
1727
+ /**
1728
+ * Saves a worktree's uncommitted work under `$STIM_HOME/archive/worktrees/<repository>/<branch-or-dir>-<timestamp>/`:
1729
+ * `tracked.patch` (`git diff --binary HEAD`, staged and unstaged changes together), `untracked.tar` (untracked files
1730
+ * git does not ignore) and `manifest.json` naming the branch, HEAD, pull request, files and restore commands. Ignored
1731
+ * files are not saved. Throws, leaving no archive, when archives are disabled or limited to 0, the files exceed
1732
+ * `archive.maxTotalGb` or the disk floor, an untracked nested repository is present, a staged version differs from
1733
+ * the working tree, or the written archive does not verify.
1734
+ */
1735
+ async function archiveWorktreeChanges({ path, repository, branch, head, pullRequest, operation }) {
1736
+ const limits = worktreeArchiveLimits(path, operation?.env);
1737
+ if ("refusal" in limits) throw new Error(limits.refusal);
1738
+ const at = Date.now();
1739
+ const changes = await changesOf(path, operation);
1740
+ const nested = changes.untracked.find((file) => file.endsWith("/"));
1741
+ if (nested) throw new Error(`${nested} is an untracked nested repository`);
1742
+ const staged = await stagedOnly(path, operation);
1743
+ if (staged.length) throw new Error(`the staged content of ${staged.join(", ")} differs from its working tree`);
1744
+ if (!changes.tracked.length && !changes.untracked.length) throw new Error("git reported no change it could save");
1745
+ const retention = async () => {
1746
+ try {
1747
+ await enforceArchiveRetention(void 0, operation);
1748
+ } catch (error) {
1749
+ if (error instanceof Error && "reportCleanup" in error) throw error;
1750
+ operation?.signal.throwIfAborted();
1751
+ }
1752
+ };
1753
+ await retention();
1754
+ const existing = await worktreeArchiveBytes(operation?.signal);
1755
+ let bytes = 0;
1756
+ for (const file of changes.untracked) bytes += (await lstat(join(path, file))).size;
1757
+ let tracked = await headSizes(path, changes.tracked, operation);
1758
+ for (const file of changes.tracked) tracked += (await lstat(join(path, file)).catch(() => null))?.size ?? 0;
1759
+ const plan = await headBundlePlan(path, head, operation);
1760
+ if (existing + bytes + (plan?.bytes ?? 0) > limits.maxBytes) throw new Error(`its untracked files and unmerged commits (${bytes + (plan?.bytes ?? 0)} bytes) do not fit in archive.maxTotalGb beside the other archives`);
1761
+ const [volume] = await readVolumeSpaceAsync([worktreeArchiveRoot()], operation?.signal);
1762
+ const floorMb = resolveBudget({ env: operation?.env }).budget.hardFloorDiskMb;
1763
+ const neededMb = (bytes + 2 * tracked + 2 * (plan?.bytes ?? 0)) / 1024 ** 2;
1764
+ if (!volume || volume.freeMb - neededMb < floorMb) throw new Error(volume ? `writing it would leave less free disk than budget.hardFloorDiskGb on ${volume.volume}` : "free disk for it could not be read");
1765
+ const dir = join(worktreeArchiveRoot(), safeName(basename(repository)), `${safeName(branch?.replace(/^refs\/heads\//, "") ?? basename(path))}-${stamp(at)}-${randomUUID().slice(0, 8)}`);
1766
+ await mkdir(dirname(dir), {
1767
+ recursive: true,
1768
+ mode: 448
1769
+ });
1770
+ const attempt = tryAcquireClaim({
1771
+ root: `${dir}.claims`,
1772
+ mode: "exclusive",
1773
+ label: "worktree archive"
1774
+ });
1775
+ const claim = attempt.acquired;
1776
+ if (!claim) {
1777
+ releaseClaim(attempt.pending);
1778
+ throw new Error("its archive claim is held");
1779
+ }
1780
+ let created = false;
1781
+ const restore = [`git -C ${quotedPath(repository)} worktree add --detach ${quotedPath(path)} ${head}`];
1782
+ try {
1783
+ await mkdir(dir, { mode: 448 });
1784
+ created = true;
1785
+ const bundle = plan ? await bundleHead(path, plan, dir, operation) : null;
1786
+ if (bundle) restore.unshift(`git -C ${quotedPath(repository)} bundle unbundle ${quotedPath(bundle)}`);
1787
+ if (changes.tracked.length) {
1788
+ const patch = join(dir, "tracked.patch");
1789
+ await gitAsync(path, [
1790
+ "diff",
1791
+ "--binary",
1792
+ "--no-color",
1793
+ "--no-ext-diff",
1794
+ "--no-textconv",
1795
+ "--no-relative",
1796
+ "--src-prefix=a/",
1797
+ "--dst-prefix=b/",
1798
+ `--output=${patch}`,
1799
+ "HEAD",
1800
+ "--"
1801
+ ], {
1802
+ ...execution(operation),
1803
+ timeoutMs: ARCHIVE_TIMEOUT_MS
1804
+ });
1805
+ await chmod(patch, 384);
1806
+ restore.push(`git -C ${quotedPath(path)} apply ${quotedPath(patch)}`);
1807
+ }
1808
+ if (changes.untracked.length) {
1809
+ const tarball = join(dir, "untracked.tar");
1810
+ const exec = {
1811
+ ...execution(operation),
1812
+ cwd: path,
1813
+ timeoutMs: ARCHIVE_TIMEOUT_MS
1814
+ };
1815
+ await getExecutor().runFileAsync("tar", [
1816
+ "-cf",
1817
+ tarball,
1818
+ "--null",
1819
+ "-T",
1820
+ "-"
1821
+ ], {
1822
+ ...exec,
1823
+ input: `${changes.untracked.join("\0")}\0`
1824
+ });
1825
+ await chmod(tarball, 384);
1826
+ if (!sameList((await getExecutor().runFileAsync("tar", ["-tf", tarball], {
1827
+ ...exec,
1828
+ untrimmed: true
1829
+ })).split(/\r?\n/).filter(Boolean).toSorted(), changes.untracked)) throw new Error("the untracked tarball does not list every file");
1830
+ restore.push(`tar -xf ${quotedPath(tarball)} -C ${quotedPath(path)}`);
1831
+ }
1832
+ const archive = {
1833
+ dir,
1834
+ at,
1835
+ ...changes,
1836
+ restore,
1837
+ bundled: bundle !== null,
1838
+ claim
1839
+ };
1840
+ const mismatch = await worktreeArchiveMismatch(path, archive, operation);
1841
+ if (mismatch) throw new Error(mismatch);
1842
+ const manifest = {
1843
+ version: 1,
1844
+ createdAt: new Date(at).toISOString(),
1845
+ worktree: path,
1846
+ repository,
1847
+ branch,
1848
+ head,
1849
+ pullRequest,
1850
+ headBundle: bundle ? "head.bundle" : null,
1851
+ trackedPatch: changes.tracked.length ? "tracked.patch" : null,
1852
+ untrackedTarball: changes.untracked.length ? "untracked.tar" : null,
1853
+ tracked: changes.tracked,
1854
+ untracked: changes.untracked,
1855
+ restore
1856
+ };
1857
+ const tmp = join(dir, ".manifest.json.tmp");
1858
+ await writeFile(tmp, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 384 });
1859
+ await rename(tmp, join(dir, "manifest.json"));
1860
+ const total = await worktreeArchiveBytes(operation?.signal);
1861
+ if (total > limits.maxBytes) throw new Error(`the archive (${total - existing} bytes) does not fit in archive.maxTotalGb beside the other archives`);
1862
+ const [after] = await readVolumeSpaceAsync([dir], operation?.signal);
1863
+ if (!after || after.freeMb < floorMb) throw new Error(`it left less free disk than budget.hardFloorDiskGb on ${after?.volume ?? "its volume"}`);
1864
+ await retention();
1865
+ if (!await lstat(join(dir, "manifest.json")).catch(() => null)) throw new Error("archive retention removed it");
1866
+ return archive;
1867
+ } catch (error) {
1868
+ try {
1869
+ if (created) await rm(dir, {
1870
+ recursive: true,
1871
+ force: true
1872
+ });
1873
+ } finally {
1874
+ releaseClaim(claim);
1875
+ }
1876
+ throw error;
1877
+ }
1878
+ }
1879
+ /** Deletes an archive whose worktree was kept intact, so a retried removal does not pile up copies. */
1880
+ async function discardWorktreeArchive(archive) {
1881
+ await rm(archive.dir, {
1882
+ recursive: true,
1883
+ force: true
1884
+ });
1885
+ releaseWorktreeArchive(archive);
1886
+ }
1887
+ /** Lets retention manage the archive once the removal that needed it has ended. */
1888
+ function releaseWorktreeArchive(archive) {
1889
+ releaseClaim(archive.claim);
1890
+ }
1891
+ //#endregion
1892
+ //#region src/maintenance/measure.ts
1893
+ function measureDisk() {
1894
+ return readVolumeSpace([
1895
+ configDir(),
1896
+ ...Object.keys(loadConfig()?.projects ?? {}),
1897
+ buildWorkerRoot(loadConfig())
1898
+ ]);
1899
+ }
1900
+ function measureDiskAsync(operation) {
1901
+ return readVolumeSpaceAsync([
1902
+ configDir(),
1903
+ ...Object.keys(loadConfig()?.projects ?? {}),
1904
+ buildWorkerRoot(loadConfig())
1905
+ ], operation.signal);
1906
+ }
1907
+ /** The disk pressure level of the fullest volume Stim writes to, measured now. */
1908
+ function currentDiskLevel(disk = measureDisk()) {
1909
+ return diskPressureLevel(lowestFree(disk), resolveBudget().budget, resolveMaintenanceSettings());
1910
+ }
1911
+ function measurePressure(settings, previous, now) {
1912
+ return pressureFromObservations(settings, previous, now, readHostMemoryPressure(), measureDisk());
1913
+ }
1914
+ async function measurePressureAsync(settings, previous, now, operation) {
1915
+ return pressureFromObservations(settings, previous, now, await readHostMemoryPressureAsync(void 0, void 0, operation.signal, operation), await readVolumeSpaceAsync([
1916
+ configDir(),
1917
+ ...Object.keys(loadConfig()?.projects ?? {}),
1918
+ buildWorkerRoot(loadConfig())
1919
+ ], operation.signal));
1920
+ }
1921
+ function pressureFromObservations(settings, previous, now, level, disk) {
1922
+ const availableBytes = process.platform === "darwin" ? null : freemem();
1923
+ const warningSince = level === "warning" ? previous?.warningSince ?? now : null;
1924
+ return {
1925
+ disk,
1926
+ memory: {
1927
+ level,
1928
+ availableBytes,
1929
+ pressured: settings.memoryPressureLevel !== "off" && (level === "critical" || settings.memoryPressureLevel === "warning" && warningSince !== null && now - warningSince >= settings.memoryWarningMinutes * 6e4 || availableBytes !== null && availableBytes < (settings.minAvailableMemoryGb === void 0 ? totalmem() * .1 : settings.minAvailableMemoryGb * 1024 ** 3))
1930
+ },
1931
+ warningSince
1932
+ };
1933
+ }
1934
+ function sizeScanDeferred(settings) {
1935
+ return loadavg()[0] / Math.max(1, cpus().length) > settings.maxLoadPerCore;
1936
+ }
1937
+ function cacheBlocked(entry, discovered = discoverCaches(), env = process.env) {
1938
+ return planCacheEmptying([{
1939
+ ...entry,
1940
+ note: "",
1941
+ prune: "entries",
1942
+ ...discovered.some((cache) => canonicalPath(cache.dir) === canonicalPath(entry.dir) && cache.source === "registered") ? { source: "registered" } : {}
1943
+ }], false, env)[0]?.machineGlobal ?? (entry.category === "compilation-cache" ? cacheUser(entry.dir) : void 0);
1944
+ }
1945
+ async function measureSizes(now, failure) {
1946
+ const sizes = [];
1947
+ const outputs = await collectWorkspaceOutputs({
1948
+ olderThan: null,
1949
+ now,
1950
+ sizeTimeoutMs: 12e4
1951
+ });
1952
+ for (const entry of outputs.workspaces) {
1953
+ if (entry.bytes === null) {
1954
+ failure(entry.dir, entry.projectRoot ?? void 0);
1955
+ continue;
1956
+ }
1957
+ sizes.push({
1958
+ name: "workspace build outputs",
1959
+ category: "workspace-outputs",
1960
+ dir: entry.dir,
1961
+ bytes: entry.bytes,
1962
+ measuredAt: now,
1963
+ ...entry.projectRoot ? { workspace: canonicalPath(entry.projectRoot) } : {},
1964
+ idleDays: entry.idleDays,
1965
+ ...!entry.willClear ? { blocked: entry.keptReason ?? "workspace ownership is unresolved" } : {}
1966
+ });
1967
+ }
1968
+ const known = [
1969
+ {
1970
+ name: "shared build cache",
1971
+ dir: buildCacheRoot(),
1972
+ category: "build-cache"
1973
+ },
1974
+ {
1975
+ name: "Metro transform caches",
1976
+ dir: metroCacheRoot(),
1977
+ category: "metro-cache"
1978
+ },
1979
+ {
1980
+ name: "ccache",
1981
+ dir: sharedCcache(),
1982
+ category: "ccache"
1983
+ },
1984
+ {
1985
+ name: "Swift compilation cache",
1986
+ dir: sharedCompilationCache(),
1987
+ category: "compilation-cache"
1549
1988
  }
1550
- try {
1551
- const json = getExecutor().runFile("plutil", [
1552
- "-convert",
1553
- "json",
1554
- "-o",
1555
- "-",
1556
- file
1557
- ], { timeoutMs: 5e3 });
1558
- const use = serviceUseOf(JSON.parse(json), file, folder, canonical);
1559
- if (use) return use;
1560
- } catch {
1561
- return {
1562
- label,
1563
- plist: file,
1564
- argument: null
1565
- };
1989
+ ];
1990
+ const discovered = discoverCaches();
1991
+ for (const cache of discovered) {
1992
+ if (cache.source !== "registered" && !canonicalPath(cache.dir).startsWith(`${canonicalPath(configDir())}/`)) continue;
1993
+ if (cache.prune === "report-only") continue;
1994
+ const dir = canonicalPath(cache.dir);
1995
+ if (known.some((item) => dir === canonicalPath(item.dir) || dir.startsWith(`${canonicalPath(item.dir)}/`))) continue;
1996
+ const category = cache.name.startsWith("Metro transform cache") ? "metro-cache" : /^(Expo build cache|native build cache|Build cache)$/.test(cache.name) ? "build-cache" : "other";
1997
+ known.push({
1998
+ name: cache.name,
1999
+ dir: cache.dir,
2000
+ category
2001
+ });
2002
+ }
2003
+ for (const entry of known) {
2004
+ if (!existsSync(entry.dir)) continue;
2005
+ const bytes = await measuredDirectorySizeAsync(entry.dir, { timeoutMs: 12e4 });
2006
+ if (bytes === null) {
2007
+ failure(entry.dir);
2008
+ continue;
1566
2009
  }
2010
+ const blocked = cacheBlocked(entry, discovered);
2011
+ sizes.push({
2012
+ ...entry,
2013
+ bytes,
2014
+ measuredAt: now,
2015
+ ...blocked ? { blocked } : {}
2016
+ });
1567
2017
  }
1568
- return null;
2018
+ return sizes;
1569
2019
  }
1570
2020
  //#endregion
1571
2021
  //#region src/engine/warm-claim.ts
@@ -1825,9 +2275,11 @@ function checkoutFactLine(branch, plan, head) {
1825
2275
  /**
1826
2276
  * Whether to install, decided against the lockfile as it stands now rather than against what HEAD did.
1827
2277
  * A lockfile that did not move is not evidence that its dependencies are installed: an install that was
1828
- * interrupted leaves a present node_modules behind and moves HEAD no further.
2278
+ * interrupted leaves a present node_modules behind and moves HEAD no further. A `patches/` change counts
2279
+ * as a dependency change because patch-package applies it only on a fresh install. A tree `npm ls` already
2280
+ * rejected right after the last completed install, with the same problems, is not one another install would fix.
1829
2281
  */
1830
- function depsPlan({ lockfile, lockfileChanged, installed, treeValid, lastInstall, lockHash }) {
2282
+ function depsPlan({ lockfile, lockfileChanged, patchesChanged, installed, treeProblems, lastInstall, lockHash, patchesHash }) {
1831
2283
  if (!lockfile) return {
1832
2284
  run: false,
1833
2285
  reason: "no lockfile in this repository"
@@ -1836,7 +2288,7 @@ function depsPlan({ lockfile, lockfileChanged, installed, treeValid, lastInstall
1836
2288
  run: true,
1837
2289
  reason: "no installed dependencies"
1838
2290
  };
1839
- if (treeValid === false) return {
2291
+ if (treeProblems && lastInstall?.treeProblems !== treeProblems) return {
1840
2292
  run: true,
1841
2293
  reason: `the installed tree does not match ${lockfile}`
1842
2294
  };
@@ -1853,6 +2305,10 @@ function depsPlan({ lockfile, lockfileChanged, installed, treeValid, lastInstall
1853
2305
  run: true,
1854
2306
  reason: `${lockfile} does not match the last completed install`
1855
2307
  };
2308
+ if (lastInstall.patches !== patchesHash) return {
2309
+ run: true,
2310
+ reason: "patches/ does not match the last completed install"
2311
+ };
1856
2312
  return {
1857
2313
  run: false,
1858
2314
  reason: `${lockfile} matches the last completed install`
@@ -1862,6 +2318,10 @@ function depsPlan({ lockfile, lockfileChanged, installed, treeValid, lastInstall
1862
2318
  run: true,
1863
2319
  reason: `${lockfile} changed`
1864
2320
  };
2321
+ if (patchesChanged) return {
2322
+ run: true,
2323
+ reason: "patches/ changed"
2324
+ };
1865
2325
  return {
1866
2326
  run: false,
1867
2327
  reason: `${lockfile} unchanged`
@@ -1873,18 +2333,51 @@ function depsPlan({ lockfile, lockfileChanged, installed, treeValid, lastInstall
1873
2333
  * nothing about this one, and no record at all is the state of every repository before its first refresh.
1874
2334
  */
1875
2335
  function incompleteInstallRefusal(root, appDir) {
1876
- const dependencies = dependencyState(existsSync(appDir) ? appDir : root);
1877
- if (!dependencies) return null;
1878
- const last = readInstallEvidence(dependencies.root);
1879
- if (!last || last.completed || last.lock !== dependencies.lock) return null;
1880
- const hash = lockfileHash(dependencies.root, dependencies.lock);
1881
- if (hash === null || last.hash !== hash) return null;
1882
- return {
1883
- code: "STIM_DEPS_INCOMPLETE",
1884
- message: `Refusing to copy from ${dependencies.root}: the last install of ${dependencies.lock} there did not finish, so its dependencies are partial.`,
1885
- lines: [],
1886
- remedy: `Run \`stim worktree warm --refresh\` to install them in the source checkout -- it reinstalls rather than skipping, because no completed install of this ${dependencies.lock} is recorded -- then warm again.`
1887
- };
2336
+ for (const dependencies of dependencyStates(root, appDir)) {
2337
+ const last = readInstallEvidence(dependencies.root);
2338
+ if (!last || last.completed || last.lock !== dependencies.lock) continue;
2339
+ const hash = lockfileHash(dependencies.root, dependencies.lock);
2340
+ if (hash === null || last.hash !== hash) continue;
2341
+ return {
2342
+ code: "STIM_DEPS_INCOMPLETE",
2343
+ message: `Refusing to copy from ${dependencies.root}: the last install of ${dependencies.lock} there did not finish, so its dependencies are partial.`,
2344
+ lines: [],
2345
+ remedy: `Run \`stim worktree warm --refresh\` to install them in the source checkout -- it reinstalls rather than skipping, because no completed install of this ${dependencies.lock} is recorded -- then warm again.`
2346
+ };
2347
+ }
2348
+ return null;
2349
+ }
2350
+ function dependencyStates(root, appDir) {
2351
+ const app = dependencyState(existsSync(appDir) ? appDir : root);
2352
+ const top = dependencyState(root, root);
2353
+ return [app, top && top.root !== app?.root && (hasInstalledDependencies(top.root, top.installed) || readInstallEvidence(top.root)) ? top : null].filter((state) => state !== null);
2354
+ }
2355
+ /**
2356
+ * An npm project whose own `.npmrc` sets `ignore-scripts` gets no lifecycle scripts from `npm ci`, its own
2357
+ * postinstall included. `npm run` still runs a named script under that setting, so the project's postinstall
2358
+ * (usually patch-package) runs explicitly; dependencies' install scripts stay blocked.
2359
+ */
2360
+ function skippedPostinstall(dependencies) {
2361
+ if (dependencies.lock !== "package-lock.json") return false;
2362
+ let postinstall;
2363
+ try {
2364
+ postinstall = JSON.parse(readFileSync(join(dependencies.root, "package.json"), "utf-8"))?.scripts?.postinstall;
2365
+ } catch {
2366
+ return false;
2367
+ }
2368
+ if (typeof postinstall !== "string" || !postinstall.trim()) return false;
2369
+ try {
2370
+ return getExecutor().runFile("npm", [
2371
+ "config",
2372
+ "get",
2373
+ "ignore-scripts"
2374
+ ], {
2375
+ cwd: dependencies.root,
2376
+ timeoutMs: 3e4
2377
+ }) === "true";
2378
+ } catch {
2379
+ return false;
2380
+ }
1888
2381
  }
1889
2382
  function podsPlan({ hasIos, hasPodfile, podfileLockChanged, stale, dir = "ios/" }) {
1890
2383
  if (!hasIos) return {
@@ -1936,6 +2429,22 @@ function lockfileHash(depsRoot, lockfile) {
1936
2429
  return null;
1937
2430
  }
1938
2431
  }
2432
+ function hashPatches(depsRoot) {
2433
+ const dir = join(depsRoot, "patches");
2434
+ let names;
2435
+ try {
2436
+ names = readdirSync(dir, {
2437
+ recursive: true,
2438
+ withFileTypes: true
2439
+ }).filter((entry) => entry.isFile() && entry.name.endsWith(".patch")).map((entry) => relative(dir, join(entry.parentPath, entry.name))).toSorted();
2440
+ } catch {
2441
+ return "";
2442
+ }
2443
+ if (!names.length) return "";
2444
+ const hash = createHash("sha256");
2445
+ for (const name of names) hash.update(name).update("\0").update(readFileSync(join(dir, name))).update("\0");
2446
+ return hash.digest("hex");
2447
+ }
1939
2448
  function readInstallEvidence(depsRoot) {
1940
2449
  try {
1941
2450
  const parsed = JSON.parse(readFileSync(installRecordFile(depsRoot), "utf-8"));
@@ -1944,21 +2453,19 @@ function readInstallEvidence(depsRoot) {
1944
2453
  return {
1945
2454
  lock: parsed.lock,
1946
2455
  hash: parsed.hash,
1947
- completed: parsed.completed === true
2456
+ patches: typeof parsed.patches === "string" ? parsed.patches : "",
2457
+ completed: parsed.completed === true,
2458
+ treeProblems: typeof parsed.treeProblems === "string" ? parsed.treeProblems : null
1948
2459
  };
1949
2460
  } catch {
1950
2461
  return null;
1951
2462
  }
1952
2463
  }
1953
- function writeInstallEvidence(depsRoot, lockfile, hash, completed) {
2464
+ function writeInstallEvidence(depsRoot, evidence) {
1954
2465
  const file = installRecordFile(depsRoot);
1955
2466
  const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
1956
2467
  mkdirSync(join(getConfigDir(), "warm-installs"), { recursive: true });
1957
- writeFileSync(tmp, JSON.stringify({
1958
- lock: lockfile,
1959
- hash: hash ?? "",
1960
- completed
1961
- }));
2468
+ writeFileSync(tmp, JSON.stringify(evidence));
1962
2469
  try {
1963
2470
  renameSync(tmp, file);
1964
2471
  } catch (error) {
@@ -1966,8 +2473,17 @@ function writeInstallEvidence(depsRoot, lockfile, hash, completed) {
1966
2473
  throw error;
1967
2474
  }
1968
2475
  }
2476
+ function pendingInstallEvidence(dependencies) {
2477
+ return {
2478
+ lock: dependencies.lock,
2479
+ hash: lockfileHash(dependencies.root, dependencies.lock) ?? "",
2480
+ patches: dependencies.lock === "package-lock.json" ? hashPatches(dependencies.root) : "",
2481
+ completed: false,
2482
+ treeProblems: null
2483
+ };
2484
+ }
1969
2485
  function restoreInstallEvidence(depsRoot, prior) {
1970
- if (prior) writeInstallEvidence(depsRoot, prior.lock, prior.hash, prior.completed);
2486
+ if (prior) writeInstallEvidence(depsRoot, prior);
1971
2487
  else rmSync(installRecordFile(depsRoot), { force: true });
1972
2488
  }
1973
2489
  function inProgressOperation(gitDir) {
@@ -2181,10 +2697,13 @@ async function refreshMainCheckout({ root, appDir, settings, iosProject, emit, s
2181
2697
  const plan = checkoutPlan(locallyKnownUpstream(root));
2182
2698
  if (plan.kind === "diverged") return divergedRefusal(root, branch, plan);
2183
2699
  if (inspectOnly && plan.kind === "fast-forward") return { mutation: `fast-forward ${plan.behind} commit${plan.behind === 1 ? "" : "s"}` };
2184
- const depsBefore = dependencyState(existsSync(appDir) ? appDir : root);
2185
- const priorInstall = depsBefore ? readInstallEvidence(depsBefore.root) : null;
2186
- const marked = plan.kind === "fast-forward" ? depsBefore : null;
2187
- if (marked) writeInstallEvidence(marked.root, marked.lock, lockfileHash(marked.root, marked.lock), false);
2700
+ const depsBefore = dependencyStates(root, appDir);
2701
+ const priorInstalls = new Map(depsBefore.map((entry) => [entry.root, readInstallEvidence(entry.root)]));
2702
+ const marked = plan.kind === "fast-forward" ? depsBefore : [];
2703
+ for (const entry of marked) writeInstallEvidence(entry.root, pendingInstallEvidence(entry));
2704
+ const restoreMarked = (depsRoot) => {
2705
+ if (marked.some((entry) => entry.root === depsRoot)) restoreInstallEvidence(depsRoot, priorInstalls.get(depsRoot) ?? null);
2706
+ };
2188
2707
  let head = before;
2189
2708
  if (plan.kind === "fast-forward") {
2190
2709
  const merged = fastForward(root, plan.upstream);
@@ -2199,43 +2718,71 @@ async function refreshMainCheckout({ root, appDir, settings, iosProject, emit, s
2199
2718
  emit(checkoutFactLine(branch, plan, head));
2200
2719
  const note = defaultBranchNote(branch, resolveDefaultBranch(root, settings));
2201
2720
  if (note.kind !== "match") for (const line of note.lines) emit(phaseLine("", line));
2202
- const dependencies = dependencyState(existsSync(appDir) ? appDir : root);
2203
- const installed = dependencies ? hasInstalledDependencies(dependencies.root, dependencies.installed) : false;
2204
- const consumed = dependencies ? lockfileHash(dependencies.root, dependencies.lock) : null;
2205
- const deps = depsPlan({
2206
- lockfile: dependencies?.lock ?? null,
2207
- lockfileChanged: dependencies ? pathChangedBetween(root, before, head, gitPath(relative(root, dependencies.root), dependencies.lock)) : false,
2208
- installed,
2209
- treeValid: dependencies?.lock === "package-lock.json" && installed ? installedNpmTreeIsValid(dependencies.root) : null,
2210
- lastInstall: priorInstall,
2211
- lockHash: consumed
2212
- });
2213
- const depsSubject = `source ${dependencies?.root ?? root}: ${deps.reason}`;
2214
- if (deps.run && dependencies) {
2215
- if (inspectOnly) return { mutation: deps.reason };
2216
- writeInstallEvidence(dependencies.root, dependencies.lock, consumed, false);
2217
- const install = await runInstallCommand({
2218
- command: dependencies.command,
2219
- cwd: dependencies.root,
2220
- spawnFn: spawn,
2221
- now,
2222
- heartbeatMs,
2223
- onHeartbeat: emit
2721
+ const dependencyRoots = dependencyStates(root, appDir);
2722
+ for (const entry of marked) if (!dependencyRoots.some((other) => other.root === entry.root)) restoreMarked(entry.root);
2723
+ if (!dependencyRoots.length) emit(stepLine("deps", `source ${root}: no lockfile in this repository`, "skipped"));
2724
+ const owed = [];
2725
+ for (const dependencies of dependencyRoots) {
2726
+ const installed = hasInstalledDependencies(dependencies.root, dependencies.installed);
2727
+ const depsDir = relative(root, dependencies.root);
2728
+ const consumed = pendingInstallEvidence(dependencies);
2729
+ const deps = depsPlan({
2730
+ lockfile: dependencies.lock,
2731
+ lockfileChanged: pathChangedBetween(root, before, head, gitPath(depsDir, dependencies.lock)),
2732
+ patchesChanged: dependencies.lock === "package-lock.json" && pathChangedBetween(root, before, head, gitPath(depsDir, "patches/*.patch")),
2733
+ installed,
2734
+ treeProblems: dependencies.lock === "package-lock.json" && installed ? installedNpmTreeProblems(dependencies.root) : null,
2735
+ lastInstall: priorInstalls.has(dependencies.root) ? priorInstalls.get(dependencies.root) ?? null : readInstallEvidence(dependencies.root),
2736
+ lockHash: consumed.hash || null,
2737
+ patchesHash: consumed.patches
2224
2738
  });
2225
- emit(stepLine("deps", depsSubject, `${dependencies.command} (${formatDuration(install.durationMs)})`));
2226
- const running = await awaitSpawnedWork(`\`${dependencies.command}\``);
2227
- if (!install.ok) return {
2228
- code: DEPS_ERROR,
2229
- message: String(install.reason),
2230
- lines: install.lastLines,
2231
- remedy: `Run \`cd ${dependencies.root} && ${dependencies.command}\` and fix what it reports, then run warm again.`
2232
- };
2233
- if (running) return running;
2234
- writeInstallEvidence(dependencies.root, dependencies.lock, consumed, true);
2235
- } else {
2236
- if (marked) restoreInstallEvidence(marked.root, priorInstall);
2739
+ const depsSubject = `source ${dependencies.root}: ${deps.reason}`;
2740
+ if (deps.run) {
2741
+ if (inspectOnly) return { mutation: deps.reason };
2742
+ owed.push({
2743
+ dependencies,
2744
+ subject: depsSubject
2745
+ });
2746
+ continue;
2747
+ }
2748
+ restoreMarked(dependencies.root);
2237
2749
  emit(stepLine("deps", depsSubject, "skipped"));
2238
2750
  }
2751
+ for (const { dependencies, subject: depsSubject } of owed) {
2752
+ const consumed = pendingInstallEvidence(dependencies);
2753
+ writeInstallEvidence(dependencies.root, consumed);
2754
+ const runStep = async (command, subject, remedy) => {
2755
+ const install = await runInstallCommand({
2756
+ command,
2757
+ cwd: dependencies.root,
2758
+ spawnFn: spawn,
2759
+ now,
2760
+ heartbeatMs,
2761
+ onHeartbeat: emit
2762
+ });
2763
+ emit(stepLine("deps", subject, `${command} (${formatDuration(install.durationMs)})`));
2764
+ const running = await awaitSpawnedWork(`\`${command}\``);
2765
+ if (!install.ok) return {
2766
+ code: DEPS_ERROR,
2767
+ message: String(install.reason),
2768
+ lines: install.lastLines,
2769
+ remedy: `Run \`cd ${dependencies.root} && ${remedy}\` and fix what it reports, then run warm again.`
2770
+ };
2771
+ return running;
2772
+ };
2773
+ const postinstall = skippedPostinstall(dependencies) ? "npm run postinstall" : null;
2774
+ const installFailure = await runStep(dependencies.command, depsSubject, postinstall ? `${dependencies.command} && ${postinstall}` : dependencies.command);
2775
+ if (installFailure) return installFailure;
2776
+ if (postinstall) {
2777
+ const postinstallFailure = await runStep(postinstall, `source ${dependencies.root}: ignore-scripts skipped postinstall`, postinstall);
2778
+ if (postinstallFailure) return postinstallFailure;
2779
+ }
2780
+ writeInstallEvidence(dependencies.root, {
2781
+ ...consumed,
2782
+ completed: true,
2783
+ treeProblems: dependencies.lock === "package-lock.json" ? installedNpmTreeProblems(dependencies.root) : null
2784
+ });
2785
+ }
2239
2786
  const app = relative(root, appDir) || ".";
2240
2787
  const where = `source ${appDir}: `;
2241
2788
  const ios = iosProject;
@@ -2272,210 +2819,6 @@ async function refreshMainCheckout({ root, appDir, settings, iosProject, emit, s
2272
2819
  return podsRunning;
2273
2820
  }
2274
2821
  //#endregion
2275
- //#region src/workspace/worktree-trash.ts
2276
- const TRASH_MANIFEST = "trash.json";
2277
- const TRASH_ENTRY_PREFIX = "remove-";
2278
- const TRASH_ENTRIES = "entries";
2279
- const TRASH_CLAIMS = "claims";
2280
- const LIST_TIMEOUT_MS = 3e4;
2281
- const DELETE_NICENESS = "10";
2282
- function rethrowCancellation(error, signal) {
2283
- if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
2284
- signal?.throwIfAborted();
2285
- }
2286
- async function isPlainDirectory(path) {
2287
- const stat = await lstat(path).catch(() => null);
2288
- return Boolean(stat?.isDirectory());
2289
- }
2290
- async function ignoredDirectories(worktree, options) {
2291
- const out = await gitAsync(worktree, [
2292
- "ls-files",
2293
- "--others",
2294
- "--ignored",
2295
- "--exclude-standard",
2296
- "--directory",
2297
- "--no-empty-directory",
2298
- "-z"
2299
- ], {
2300
- ...options.env ? childEnvironment(options.env) : {},
2301
- signal: options.signal,
2302
- declareFor: options.declareFor,
2303
- timeoutMs: LIST_TIMEOUT_MS,
2304
- untrimmed: true
2305
- });
2306
- const found = [];
2307
- for (const entry of out.split("\0")) {
2308
- if (!entry.endsWith("/")) continue;
2309
- const rel = entry.slice(0, -1);
2310
- const parts = rel.split("/");
2311
- let plain = true;
2312
- for (let depth = 1; depth <= parts.length && plain; depth++) plain = await isPlainDirectory(join(worktree, ...parts.slice(0, depth)));
2313
- if (plain) found.push(rel);
2314
- }
2315
- return found;
2316
- }
2317
- /**
2318
- * Moves the worktree's ignored directories, which `git worktree remove` would delete, into a new entry under
2319
- * `$STIM_HOME/trash` held by an exclusive ownership claim. Null, with nothing moved, when the trash is on another
2320
- * filesystem, the platform is Windows, git cannot list the entries, or the claim cannot be taken. A directory that
2321
- * does not rename stays in the worktree for git to delete.
2322
- */
2323
- async function moveIgnoredTreesToTrash(worktree, options = {}) {
2324
- if ((options.platform ?? process.platform) === "win32") return null;
2325
- let listed;
2326
- try {
2327
- listed = await ignoredDirectories(worktree, options);
2328
- } catch (error) {
2329
- rethrowCancellation(error, options.signal);
2330
- return null;
2331
- }
2332
- if (!listed.length) return null;
2333
- const root = trashRoot();
2334
- let dir = null;
2335
- let claim = null;
2336
- const entries = listed.map((from, index) => ({
2337
- from,
2338
- to: String(index)
2339
- }));
2340
- try {
2341
- await mkdir(root, { recursive: true });
2342
- if (filesystemDevice(root) !== filesystemDevice(worktree)) return null;
2343
- dir = await mkdtemp(join(root, TRASH_ENTRY_PREFIX));
2344
- claim = (0, ownership_claim_exports.tryAcquireClaim)({
2345
- root: join(dir, "claims"),
2346
- mode: "exclusive",
2347
- label: "worktree trash"
2348
- }).acquired ?? null;
2349
- if (!claim) throw new Error("the trash entry claim was not acquired");
2350
- const manifest = {
2351
- worktree,
2352
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
2353
- };
2354
- await writeFile(join(dir, TRASH_MANIFEST), `${JSON.stringify(manifest)}\n`);
2355
- await mkdir(join(dir, TRASH_ENTRIES));
2356
- } catch (error) {
2357
- if (claim) (0, ownership_claim_exports.releaseClaim)(claim);
2358
- if (dir) await rm(dir, {
2359
- recursive: true,
2360
- force: true
2361
- }).catch(() => {});
2362
- rethrowCancellation(error, options.signal);
2363
- return null;
2364
- }
2365
- const moved = [];
2366
- for (const entry of entries) try {
2367
- await rename(join(worktree, entry.from), join(dir, TRASH_ENTRIES, entry.to));
2368
- moved.push(entry);
2369
- } catch {}
2370
- const trash = {
2371
- dir,
2372
- worktree,
2373
- moved,
2374
- claim
2375
- };
2376
- if (moved.length) return trash;
2377
- await discardTrash(trash);
2378
- return null;
2379
- }
2380
- async function discardTrash(trash) {
2381
- await rmdir(join(trash.dir, TRASH_ENTRIES)).catch(() => {});
2382
- await rm(join(trash.dir, TRASH_MANIFEST), { force: true }).catch(() => {});
2383
- (0, ownership_claim_exports.releaseClaim)(trash.claim);
2384
- await rmdir(trash.dir).catch(() => {});
2385
- }
2386
- /** Moves every tree back into the worktree. Returns the relative paths that stay in the trash for gc. */
2387
- async function restoreTrashedTrees(trash) {
2388
- const kept = [];
2389
- for (const entry of trash.moved.toReversed()) {
2390
- const target = join(trash.worktree, entry.from);
2391
- try {
2392
- if (await lstat(target).catch(() => null) || !await isPlainDirectory(dirname(target))) {
2393
- kept.push(entry.from);
2394
- continue;
2395
- }
2396
- await rename(join(trash.dir, TRASH_ENTRIES, entry.to), target);
2397
- } catch {
2398
- kept.push(entry.from);
2399
- }
2400
- }
2401
- if (kept.length) (0, ownership_claim_exports.releaseClaim)(trash.claim);
2402
- else await discardTrash(trash);
2403
- return kept;
2404
- }
2405
- /** Deletes one trash entry: `rm -rf` for the bulk, then a walk that also clears read-only directories. */
2406
- async function deleteTrashEntry(dir, operation) {
2407
- if (process.platform !== "win32") try {
2408
- await runOperationFile(operation, getExecutor(), "rm", [
2409
- "-rf",
2410
- "--",
2411
- join(dir, TRASH_ENTRIES)
2412
- ], {
2413
- timeoutMs: "unbounded",
2414
- signal: operation?.signal,
2415
- cwd: dirname(dir)
2416
- });
2417
- } catch (error) {
2418
- rethrowCancellation(error, operation?.signal);
2419
- }
2420
- await removeTemporaryEntryAsync(dir, operation?.signal);
2421
- }
2422
- function deleteInBackground(trash) {
2423
- try {
2424
- (0, ownership_claim_exports.markClaimChildPending)(trash.claim);
2425
- const child = getExecutor().spawn("nice", [
2426
- "-n",
2427
- DELETE_NICENESS,
2428
- "rm",
2429
- "-rf",
2430
- "--",
2431
- join(trash.dir, TRASH_ENTRIES),
2432
- trash.dir
2433
- ], {
2434
- cwd: dirname(trash.dir),
2435
- detached: true,
2436
- stdio: "ignore"
2437
- });
2438
- child.on?.("error", () => {});
2439
- child.unref?.();
2440
- const captured = child.pid === void 0 ? null : captureProcessIdentity(child.pid);
2441
- if (captured?.ok === false && captured.reason.startsWith("NOT_FOUND")) {
2442
- (0, ownership_claim_exports.clearClaimChild)(trash.claim);
2443
- (0, ownership_claim_exports.releaseClaim)(trash.claim);
2444
- return existsSync(trash.dir) ? "left" : "deleted";
2445
- }
2446
- if (!captured?.ok || child.pid === void 0) throw new Error("the trash deletion process was not recorded");
2447
- (0, ownership_claim_exports.setClaimChild)(trash.claim, {
2448
- pid: child.pid,
2449
- processToken: captured.token
2450
- });
2451
- return "background";
2452
- } catch {
2453
- try {
2454
- (0, ownership_claim_exports.clearClaimChild)(trash.claim);
2455
- } catch {}
2456
- (0, ownership_claim_exports.releaseClaim)(trash.claim);
2457
- return "left";
2458
- }
2459
- }
2460
- /**
2461
- * Deletes the trash entry once git removed the worktree. A CLI command starts a detached, low-priority `rm` recorded
2462
- * as the claim's child, so gc reports the entry as in use until `rm` exits. Inside a bounded report, such as the
2463
- * programmatic API, every child is stopped when the report settles, so the entry is deleted before returning.
2464
- * `left` means the entry waits for gc with its claim released.
2465
- */
2466
- async function disposeTrash(trash, operation) {
2467
- if (!getExecutor().reportSignal?.()) return deleteInBackground(trash);
2468
- try {
2469
- await deleteTrashEntry(trash.dir, operation);
2470
- return "deleted";
2471
- } catch (error) {
2472
- rethrowCancellation(error, operation?.signal);
2473
- return "left";
2474
- } finally {
2475
- (0, ownership_claim_exports.releaseClaim)(trash.claim);
2476
- }
2477
- }
2478
- //#endregion
2479
2822
  //#region src/commands/worktree.ts
2480
2823
  function dependencyInstallCommand(target, dir = ".") {
2481
2824
  const installRoot = resolve$1(target, dir);
@@ -2532,7 +2875,7 @@ function reportRefreshFailure(failure, out) {
2532
2875
  out(chalk.red(`failed: ${failure.code}`));
2533
2876
  }
2534
2877
  function mainCheckoutAppDir(root, target, cwd) {
2535
- const app = findProjectRoot$1(cwd);
2878
+ const app = warmedWorkspace(root, target, cwd) ?? findProjectRoot$1(cwd);
2536
2879
  if (!app) return root;
2537
2880
  const rel = relative$1(target, app);
2538
2881
  if (!rel || rel.startsWith("..") || isAbsolute$1(rel)) return root;
@@ -2540,20 +2883,26 @@ function mainCheckoutAppDir(root, target, cwd) {
2540
2883
  }
2541
2884
  /**
2542
2885
  * The app workspace a warm prepares: the nearest app below the worktree root, or else the one app registered in
2543
- * the source checkout, at the same place in this worktree, or else the worktree root when it is the app. A monorepo
2544
- * root can declare react-native without being the app.
2886
+ * the source checkout, at the same place in this worktree, or else the one registered in this worktree, or else
2887
+ * the worktree root when it is the app. A monorepo root can declare react-native without being the app.
2545
2888
  */
2546
2889
  function warmedWorkspace(root, target, cwd) {
2547
2890
  const nearest = findProjectRoot$1(cwd);
2548
- const app = nearest && isPathPrefix(target, nearest) && isMobileProject(nearest) ? nearest : null;
2891
+ const app = nearest && isPathPrefix$1(target, nearest) && isMobileProject(nearest) ? nearest : null;
2549
2892
  if (app && app !== target) return app;
2550
- const apps = /* @__PURE__ */ new Set();
2551
- for (const path of Object.keys(loadConfig$1()?.projects ?? {})) {
2552
- const rel = relative$1(root, path);
2553
- if (rel.startsWith("..") || isAbsolute$1(rel)) continue;
2554
- const mapped = resolve$1(target, rel);
2555
- if (isMobileProject(mapped)) apps.add(mapped);
2556
- }
2893
+ const registered = Object.keys(loadConfig$1()?.projects ?? {});
2894
+ const appsUnder = (checkout) => {
2895
+ const apps = /* @__PURE__ */ new Set();
2896
+ for (const path of registered) {
2897
+ const rel = relative$1(checkout, path);
2898
+ if (rel.startsWith("..") || isAbsolute$1(rel)) continue;
2899
+ const mapped = resolve$1(target, rel);
2900
+ if (isMobileProject(mapped)) apps.add(mapped);
2901
+ }
2902
+ return apps;
2903
+ };
2904
+ const source = appsUnder(root);
2905
+ const apps = source.size ? source : appsUnder(target);
2557
2906
  return apps.size === 1 ? [...apps][0] : app;
2558
2907
  }
2559
2908
  async function registerWarmedWorkspace(workspace, out) {
@@ -2608,7 +2957,7 @@ async function warmWorktreeTarget(opts) {
2608
2957
  for (const key of scope) opts.beforeWarm?.(nativeCanonicalPath(key));
2609
2958
  if (app) await recordWorkspaceUseAsync(app, /* @__PURE__ */ new Date(), operation.env, opts.cwd, operation.signal);
2610
2959
  if (workspace && workspace !== app) await recordWorkspaceUseAsync(workspace, /* @__PURE__ */ new Date(), operation.env, opts.cwd, operation.signal);
2611
- 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")));
2960
+ if (!workspace) out(chalk.dim(phaseLine("project", opts.refresh ? "no single app to register here; --refresh updates only the repository root. Run warm --refresh from the app directory to refresh that app and list it in status" : "no single app to register here; run warm from the app directory to list it in status while it warms")));
2612
2961
  else {
2613
2962
  await registerWarmedWorkspace(workspace, out);
2614
2963
  progress = startWarmProgress(workspace, opts.refresh ? "refresh" : "copy", { note: (line) => out(chalk.yellow(line)) });
@@ -2624,7 +2973,7 @@ async function warmWorktreeTarget(opts) {
2624
2973
  out(chalk.dim(SETTING_SHAPE_REMEDY));
2625
2974
  return null;
2626
2975
  }
2627
- for (const key of unknownSettingKeys(settings)) out(chalk.yellow(`Warning: setting "${key}" is not read by Stim and will be ignored.`));
2976
+ for (const key of unknownSettingKeys(settings)) out(chalk.yellow(inertSettingWarning(settings, key)));
2628
2977
  return settings;
2629
2978
  };
2630
2979
  const copy = async (wait) => {
@@ -2657,12 +3006,12 @@ async function warmWorktreeTarget(opts) {
2657
3006
  }
2658
3007
  const excluded = readWorktreeExclude(root);
2659
3008
  const patterns = excluded?.length ? excluded : settings.worktree?.exclude || [];
2660
- const result = await cloneIgnoredEntries({
3009
+ const result = await trace.spanAsync("carry ignored entries", () => cloneIgnoredEntries({
2661
3010
  root,
2662
3011
  target,
2663
3012
  patterns,
2664
3013
  operation
2665
- });
3014
+ }));
2666
3015
  for (const entry of result.skipped) out(chalk.dim(phaseLine("carry", `kept ${entry.file} (${entry.reason})`)));
2667
3016
  for (const entry of result.failed) out(chalk.yellow(phaseLine("carry", `could not copy ${entry.file}: ${entry.error}`)));
2668
3017
  if (result.copied.length) {
@@ -2783,7 +3132,7 @@ function customIosDirs(worktree) {
2783
3132
  } catch {
2784
3133
  return [];
2785
3134
  }
2786
- return Object.keys(loadConfig$1()?.projects ?? {}).filter((path) => isPathPrefix(worktree, path)).map((path) => ({
3135
+ return Object.keys(loadConfig$1()?.projects ?? {}).filter((path) => isPathPrefix$1(worktree, path)).map((path) => ({
2787
3136
  path,
2788
3137
  ios: appIosProjectDir(path)
2789
3138
  })).filter(({ ios }) => ios.custom && existsSync$1(ios.dir)).map(({ path, ios }) => ({
@@ -2832,31 +3181,10 @@ function excludePodChurn(lines, iosDirs = []) {
2832
3181
  restore
2833
3182
  };
2834
3183
  }
2835
- function nativeCanonicalPath(path) {
2836
- const missing = [];
2837
- for (let existing = resolve$1(path);; existing = dirname$1(existing)) try {
2838
- return resolve$1(realpathSync$1.native(existing), ...missing);
2839
- } catch {
2840
- if (dirname$1(existing) === existing) return resolve$1(path);
2841
- missing.unshift(basename$1(existing));
2842
- }
2843
- }
2844
3184
  /** Other worktrees git lists inside `path`, which removing `path` would delete with it. */
2845
3185
  function nestedWorktrees(entries, path) {
2846
3186
  const root = nativeCanonicalPath(path);
2847
- return entries.filter((candidate) => !candidate.bare && nativeCanonicalPath(candidate.path) !== root).filter((candidate) => isPathPrefix(root, nativeCanonicalPath(candidate.path))).map((candidate) => candidate.path);
2848
- }
2849
- function matchWorktreeEntry(entries, path) {
2850
- let best = null;
2851
- const target = nativeCanonicalPath(path);
2852
- (entries || []).forEach((entry, index) => {
2853
- if (!entry?.path || !isPathPrefix(nativeCanonicalPath(entry.path), target)) return;
2854
- if (!best || entry.path.length > best.path.length) best = {
2855
- ...entry,
2856
- index
2857
- };
2858
- });
2859
- return best;
3187
+ return entries.filter((candidate) => !candidate.bare && nativeCanonicalPath(candidate.path) !== root).filter((candidate) => isPathPrefix$1(root, nativeCanonicalPath(candidate.path))).map((candidate) => candidate.path);
2860
3188
  }
2861
3189
  function removalRemedy(dirtyLines, { worktree = "<worktree>" } = {}) {
2862
3190
  const tracked = [];
@@ -2886,10 +3214,11 @@ function pathArgs(lines, limit = 5) {
2886
3214
  const shown = paths.slice(0, limit).map((p) => /[\s"'\\$`]/.test(p) ? JSON.stringify(p) : p);
2887
3215
  return `${shown.join(" ")}${paths.length > shown.length ? " ..." : ""}`;
2888
3216
  }
3217
+ const DIRTY_BLOCKER = "uncommitted changes or untracked files";
2889
3218
  function removalBlockers({ dirty, unpushed }) {
2890
3219
  const blockers = [];
2891
3220
  if (dirty === null || unpushed === null) blockers.push("could not determine git status; re-run with --force to override");
2892
- if (dirty) blockers.push("uncommitted changes or untracked files");
3221
+ if (dirty) blockers.push(DIRTY_BLOCKER);
2893
3222
  if (unpushed && unpushed.length) blockers.push(`${plural(unpushed.length, "commit")} not on any remote or any other local branch`);
2894
3223
  return blockers;
2895
3224
  }
@@ -2901,7 +3230,7 @@ function reclaimKeys(rootPath) {
2901
3230
  const keys = /* @__PURE__ */ new Set([rootPath]);
2902
3231
  const root = nativeCanonicalPath(rootPath);
2903
3232
  if (cfg?.projects) {
2904
- for (const key of Object.keys(cfg.projects)) if (isPathPrefix(rootPath, key) || isPathPrefix(root, nativeCanonicalPath(key))) keys.add(key);
3233
+ for (const key of Object.keys(cfg.projects)) if (isPathPrefix$1(rootPath, key) || isPathPrefix$1(root, nativeCanonicalPath(key))) keys.add(key);
2905
3234
  }
2906
3235
  return [...keys].toSorted();
2907
3236
  }
@@ -3042,11 +3371,11 @@ function reportRetainedResources(root, result, output) {
3042
3371
  }
3043
3372
  function hasRegisteredProjectUnder(rootPath) {
3044
3373
  const cfg = loadConfig$1();
3045
- return Object.keys(cfg?.projects ?? {}).some((key) => isPathPrefix(rootPath, key));
3374
+ return Object.keys(cfg?.projects ?? {}).some((key) => isPathPrefix$1(rootPath, key));
3046
3375
  }
3047
3376
  function projectAtOrAbove(root, path) {
3048
3377
  const here = nativeCanonicalPath(path);
3049
- 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;
3378
+ return Object.keys(loadConfig$1()?.projects ?? {}).filter((key) => isPathPrefix$1(nativeCanonicalPath(root), nativeCanonicalPath(key)) && isPathPrefix$1(nativeCanonicalPath(key), here)).toSorted((a, b) => b.length - a.length)[0] ?? null;
3050
3379
  }
3051
3380
  async function reclaimEnvironment(root, why, output, operation, beforeRemove) {
3052
3381
  await withManagedRemoteWorktreeRemovalLock(root, () => withReclaimLocks(root, async (lockedKeys) => {
@@ -3080,6 +3409,9 @@ const REMOVAL_OUTPUT = {
3080
3409
  process.exitCode = 1;
3081
3410
  }
3082
3411
  };
3412
+ function blockersOf(inspection, opts) {
3413
+ return opts.archiveDirty ? inspection.blockers.filter((blocker) => blocker !== DIRTY_BLOCKER) : inspection.blockers;
3414
+ }
3083
3415
  function removalPath(target, cwd = process.cwd()) {
3084
3416
  return canonicalExistingPath(target ?? cwd);
3085
3417
  }
@@ -3186,6 +3518,25 @@ function printRemovalCleanup(result, failed, output) {
3186
3518
  if (result.dereferenced.length) output.error(chalk.dim(phaseLine("workspace", `no longer referenced: ${result.dereferenced.join(", ")}`)));
3187
3519
  for (const skipped of result.skippedDevices) output.error((failed ? chalk.dim : chalk.yellow)(phaseLine("device", `kept ${describeKeptDevice(skipped)} (${skipped.reason})`)));
3188
3520
  }
3521
+ async function archiveBeforeRemoval(path, repository, branch, head, opts, output) {
3522
+ try {
3523
+ if (!head) throw new Error("its HEAD could not be read");
3524
+ return await archiveWorktreeChanges({
3525
+ path,
3526
+ repository,
3527
+ branch,
3528
+ head,
3529
+ pullRequest: opts.archiveDirty?.pullRequest ?? null,
3530
+ operation: opts.operation
3531
+ });
3532
+ } catch (error) {
3533
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
3534
+ opts.operation?.signal.throwIfAborted();
3535
+ output.error(chalk.red(`Refusing to remove ${path}: could not archive its uncommitted changes: ${(error instanceof Error ? error.message : String(error)).split("\n")[0]}`));
3536
+ output.fail();
3537
+ return null;
3538
+ }
3539
+ }
3189
3540
  async function removeWorktreeTarget(target, opts = {}) {
3190
3541
  let removed = false;
3191
3542
  await runRemove(target, opts, () => {
@@ -3337,6 +3688,13 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
3337
3688
  output.fail();
3338
3689
  return;
3339
3690
  }
3691
+ const cli = stimCliUseOf(path, nativeCanonicalPath, stimCliEntries(opts.operation?.env));
3692
+ if (cli.length) {
3693
+ output.error(chalk.red(`Refusing to remove ${path}: ${cli.join("; ")}.`));
3694
+ output.error(chalk.dim("Run stim from another checkout or install, or point PATH elsewhere, then retry."));
3695
+ output.fail();
3696
+ return;
3697
+ }
3340
3698
  const project = getProject(path);
3341
3699
  const branch = entry.branch;
3342
3700
  const ownsBranch = Boolean(branch && project?.worktreeBranchOwned === true && project.worktreeBranch === branch);
@@ -3352,13 +3710,15 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
3352
3710
  output.error(chalk.dim(`HEAD is in the merged PR #${pr.number} (${pr.url}); its local-only commits stay on GitHub.`));
3353
3711
  }
3354
3712
  }
3355
- if (inspection.blockers.length && !opts.force) {
3713
+ if (blockersOf(inspection, opts).length && !opts.force) {
3356
3714
  await printRemovalRefusal(path, inspection, output, opts.operation);
3357
3715
  return;
3358
3716
  }
3359
3717
  const deleteOwnedBranch = Boolean(ownsBranch && approvedBranchSha && inspection.unpushed?.length === 0);
3360
3718
  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;
3361
3719
  const branchDeleteCwd = source.path;
3720
+ let archive = null;
3721
+ let gitRemoving = false;
3362
3722
  await withManagedRemoteWorktreeRemovalLock(path, () => withReclaimLocks(path, async (lockedKeys) => {
3363
3723
  options.signal?.throwIfAborted();
3364
3724
  if ((await opts.guard?.(lockedKeys))?.length) return;
@@ -3370,16 +3730,25 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
3370
3730
  return;
3371
3731
  }
3372
3732
  const current = await inspectRemoval(path, mergedHead, opts.operation);
3373
- if (current.blockers.length && !opts.force) {
3733
+ if (blockersOf(current, opts).length && !opts.force) {
3374
3734
  await printRemovalRefusal(path, current, output, opts.operation);
3375
3735
  return;
3376
3736
  }
3737
+ if (opts.archiveDirty && !opts.force && current.dirtyLines.length) {
3738
+ const saved = await archiveBeforeRemoval(path, source.path, branch ?? null, inspectedHead, opts, output);
3739
+ if (!saved) return;
3740
+ archive = saved;
3741
+ }
3742
+ const discardArchive = async () => {
3743
+ if (archive) await discardWorktreeArchive(archive);
3744
+ };
3377
3745
  const result = await reclaimAll(path, lockedKeys, {
3378
3746
  preserveRootProject: true,
3379
3747
  removedBy: opts.removedBy ?? "worktree-remove",
3380
3748
  operation: opts.operation
3381
3749
  });
3382
3750
  if (result.keptEntries.length) {
3751
+ await discardArchive();
3383
3752
  reportRetainedResources(path, result, output);
3384
3753
  return;
3385
3754
  }
@@ -3390,12 +3759,14 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
3390
3759
  if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
3391
3760
  opts.operation?.signal.throwIfAborted();
3392
3761
  if (!isTimeoutError(error)) throw error;
3762
+ await discardArchive();
3393
3763
  await printRemovalRefusal(path, timedOutInspection(error), output, opts.operation);
3394
3764
  output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
3395
3765
  printRemovalCleanup(result, true, output);
3396
3766
  return;
3397
3767
  }
3398
3768
  if (headMoved) {
3769
+ await discardArchive();
3399
3770
  output.error(chalk.red(`Refusing to remove ${path}: its HEAD moved while its environment was reclaimed.`));
3400
3771
  output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
3401
3772
  printRemovalCleanup(result, true, output);
@@ -3410,10 +3781,28 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
3410
3781
  signal: options.signal,
3411
3782
  declareFor: opts.operation
3412
3783
  });
3784
+ let stale = null;
3785
+ if (archive) try {
3786
+ stale = await worktreeArchiveMismatch(path, archive, opts.operation);
3787
+ } catch (error) {
3788
+ if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
3789
+ stale = `its archive could not be checked: ${error instanceof Error ? error.message : String(error)}`;
3790
+ }
3791
+ if (stale) {
3792
+ const unrestored = trash ? await restoreTrashedTrees(trash) : [];
3793
+ if (trash && unrestored.length) output.error(chalk.yellow(`Could not move ${unrestored.join(", ")} back into ${path}; ${unrestored.length === 1 ? "it stays" : "they stay"} in ${trash.dir}, which stim gc keeps while ${path} exists.`));
3794
+ await discardArchive();
3795
+ output.error(chalk.red(`Refusing to remove ${path}: ${stale}.`));
3796
+ output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
3797
+ printRemovalCleanup(result, true, output);
3798
+ output.fail();
3799
+ return;
3800
+ }
3801
+ gitRemoving = true;
3413
3802
  try {
3414
3803
  await removeWorktreeAsync(path, {
3415
3804
  from: source.path,
3416
- force: opts.force,
3805
+ force: opts.force || archive !== null,
3417
3806
  env: options.env,
3418
3807
  signal: options.signal,
3419
3808
  declareFor: opts.operation
@@ -3426,10 +3815,15 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
3426
3815
  output.error(chalk.red(`git worktree remove failed: ${message}`));
3427
3816
  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."));
3428
3817
  output.error(chalk.dim(`The directory and Stim ownership record for ${path} were kept.`));
3818
+ if (archive) output.error(chalk.dim(`Its uncommitted changes stay archived in ${archive.dir}.`));
3429
3819
  printRemovalCleanup(result, true, output);
3430
3820
  output.fail();
3431
3821
  return;
3432
3822
  }
3823
+ if (archive) {
3824
+ opts.archiveDirty?.onArchived(archive);
3825
+ output.error(chalk.dim(phaseLine("archive", `saved ${plural(archive.tracked.length + archive.untracked.length, "uncommitted file")} to ${archive.dir}; restore: ${archive.restore.join(" && ")}`)));
3826
+ }
3433
3827
  onRemoved();
3434
3828
  try {
3435
3829
  forgetStatusMeasures(path, result.reclaimedKeys);
@@ -3492,6 +3886,11 @@ async function removeOrRefuse(target, opts, onRemoved, output) {
3492
3886
  }, opts.operation, opts.beforeRemove), {
3493
3887
  signal: opts.operation?.signal,
3494
3888
  declareSpawnsFor: opts.operation
3889
+ }).catch(async (error) => {
3890
+ if (archive && !gitRemoving) await discardWorktreeArchive(archive);
3891
+ throw error;
3892
+ }).finally(() => {
3893
+ if (archive) releaseWorktreeArchive(archive);
3495
3894
  });
3496
3895
  }
3497
3896
  function registerRemove(worktree) {
@@ -3505,4 +3904,4 @@ function worktreeCommand(program) {
3505
3904
  registerRemove(worktree);
3506
3905
  }
3507
3906
  //#endregion
3508
- export { measurePressureAsync as A, reclaimProject as C, measureDisk as D, cacheBlocked as E, levelStillShort as F, lowestFree as I, protectWindowMs as L, sizeScanDeferred as M, diskPressureLevel as N, measureDiskAsync as O, levelFloors as P, describeDereferenced as S, sweepArchiveStaging as T, worktreeCommand as _, matchWorktreeEntry as a, TRASH_MANIFEST as b, reclaimKeys as c, removalBlockers as d, removalPath as f, warmedWorkspace as g, warmWorktreeTarget as h, excludeWatchmanCookies as i, measureSizes as j, measurePressure 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, TRASH_CLAIMS as v, deleteSelectedArchives as w, deleteTrashEntry as x, TRASH_ENTRY_PREFIX as y };
3907
+ export { deleteSelectedArchives as A, sizeScanDeferred as C, stimCliUseOf as D, stimCliEntries as E, worktreeArchiveLimits as M, describeDereferenced as O, measureSizes as S, serviceRunningFrom as T, cacheBlocked as _, nestedWorktrees as a, measurePressure as b, registerRemove as c, removalPath as d, removalRemedy as f, worktreeCommand as g, warmedWorkspace as h, excludeWatchmanCookies as i, sweepArchiveStaging as j, reclaimProject as k, registerWarm as l, warmWorktreeTarget as m, dependencyInstallCommand as n, porcelainPath as o, removeWorktreeTarget as p, excludePodChurn as r, reclaimKeys as s, customIosDirs as t, removalBlockers as u, measureDisk as v, uncommittedActivityOf as w, measurePressureAsync as x, measureDiskAsync as y };