stim 1.14.0 → 1.16.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 (152) hide show
  1. package/README.md +2 -0
  2. package/dist/activity-BxwdD81S.mjs +609 -0
  3. package/dist/agent-device-usage-output-Dpih1Tfa.mjs +411 -0
  4. package/dist/{android-d6rGq1pA.mjs → android-B_NumVyQ.mjs} +169 -81
  5. package/dist/{android-LbPFRWVL.mjs → android-Ift4kVvt.mjs} +517 -413
  6. package/dist/android-cas-8U4DPqeR.mjs +167 -0
  7. package/dist/android-cas-compiler.mjs +2 -2
  8. package/dist/{android-XpvCrV7q.mjs → android-r2rBL8kV.mjs} +2 -2
  9. package/dist/{app-install-BpgSgkJj.mjs → app-install-CKWdbXIX.mjs} +128 -151
  10. package/dist/attempt-Bk2ouQVL.mjs +39 -0
  11. package/dist/budget-DjrURfQn.mjs +2700 -0
  12. package/dist/{build-plan-Dcr-3ie0.mjs → build-plan-DjeSb2Xl.mjs} +192 -653
  13. package/dist/build-slots-rUk77xqh.mjs +132 -0
  14. package/dist/{cache-manifest-C68QQ0ua.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
  15. package/dist/cache-manifest.mjs +1 -1
  16. package/dist/{chrome-BPCiC3ZX.mjs → chrome-BrcVWQyD.mjs} +1 -1
  17. package/dist/cli-BkYZ_6hy.mjs +51 -0
  18. package/dist/cli.mjs +6 -42
  19. package/dist/client-BTY686R6.mjs +1368 -0
  20. package/dist/collector-run.mjs +9 -195
  21. package/dist/{command-output-DVcpw6eE.mjs → command-output-DnJMdEY-.mjs} +1 -1
  22. package/dist/{config-lvc7d3C2.mjs → config-D0DbpZtM.mjs} +28 -29
  23. package/dist/{created-devices-CllH_8eb.mjs → created-devices-D5owjN4C.mjs} +2 -2
  24. package/dist/{dependency-state-DsT7aKeo.mjs → dependency-state-B6cMXZab.mjs} +2 -2
  25. package/dist/{deps-D9IcKqXS.mjs → deps-urbmfXig.mjs} +3 -3
  26. package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
  27. package/dist/dev-client-CroAIjjj.mjs +43 -0
  28. package/dist/{device-CjuuAJjQ.mjs → device-DZQjF7H5.mjs} +8 -7
  29. package/dist/device-capacity-BHwxNBlu.mjs +136 -0
  30. package/dist/device-host-worker.d.mts +1 -0
  31. package/dist/device-host-worker.mjs +964 -0
  32. package/dist/device-ios-BadmeL2V.mjs +381 -0
  33. package/dist/{device-lease-5c3WrVRm.mjs → device-lease-D-hLqx1b.mjs} +43 -36
  34. package/dist/{device-pool-BiyDyPP-.mjs → device-lease-run-CqjShqWW.mjs} +6 -126
  35. package/dist/device-pool-CUaEWMMm.mjs +125 -0
  36. package/dist/{device-remote-D87zEzaq.mjs → device-remote-D5WVWqOq.mjs} +69 -163
  37. package/dist/{doctor-x9pnU9fq.mjs → doctor-Cimf-gGB.mjs} +241 -103
  38. package/dist/{error-diagnostics-C5HzcwCW.mjs → error-diagnostics-B9L4Ae0x.mjs} +15 -18
  39. package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
  40. package/dist/{gc-ko6mUAYD.mjs → gc-Bqy4bjEu.mjs} +599 -108
  41. package/dist/{android-cas-iZK3DOCr.mjs → gradle-BsO1MdsH.mjs} +8 -294
  42. package/dist/{guide-6R0VNFqI.mjs → guide-B_uyKTxW.mjs} +2511 -239
  43. package/dist/{guide-status-BZrFPIA_.mjs → guide-status-CjEZMAD4.mjs} +3 -3
  44. package/dist/hosted-client-a-WmqE2L.mjs +197 -0
  45. package/dist/hosted-ios-CIWelIqf.mjs +220 -0
  46. package/dist/hosted-logs-D5g041Q3.mjs +187 -0
  47. package/dist/hosted-macos-BSZyIb3y.mjs +135 -0
  48. package/dist/idle-DYVymG92.mjs +1099 -0
  49. package/dist/idle-shutdown-CTsDnAde.mjs +122 -0
  50. package/dist/in-use-DvBw0Kxo.mjs +130 -0
  51. package/dist/{ios-BwDPrnYQ.mjs → ios-BaInn3sW.mjs} +201 -10
  52. package/dist/ios-Cr1h5gnO.mjs +189 -0
  53. package/dist/{ios-P1f5nm2n.mjs → ios-aq3QoGI3.mjs} +1159 -633
  54. package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
  55. package/dist/{ios-device-DGglU3E_.mjs → ios-device-FfCN_C8p.mjs} +12 -7
  56. package/dist/ios-state-DS5UpYTH.mjs +48 -0
  57. package/dist/{launch-verify-Deg4tB3Z.mjs → launch-verify-2DBRizjo.mjs} +5 -5
  58. package/dist/logs-CHEVK8zD.mjs +371 -0
  59. package/dist/machines-BIBUdhpj.mjs +265 -0
  60. package/dist/macos-CSYEAFmF.mjs +473 -0
  61. package/dist/macos-run.d.mts +5 -0
  62. package/dist/macos-run.mjs +2 -0
  63. package/dist/maintenance-run.d.mts +4 -0
  64. package/dist/maintenance-run.mjs +254 -0
  65. package/dist/{metro-CcEo8Fl8.mjs → metro-BeK1ZPz1.mjs} +20 -120
  66. package/dist/metro-gateway-BJ-rxQAB.mjs +84 -0
  67. package/dist/{named-ports-Blg8IxOh.mjs → named-ports-Bk_YXEmE.mjs} +6 -59
  68. package/dist/native-run-NaBrRFtQ.mjs +147 -0
  69. package/dist/{native-runtime-DjBHRg5m.mjs → native-runtime-DBzRnEKS.mjs} +9 -5
  70. package/dist/{ndjson-CDY9IkVC.mjs → ndjson-PclOIwVY.mjs} +2 -2
  71. package/dist/offload-worker.d.mts +122 -0
  72. package/dist/offload-worker.mjs +606 -0
  73. package/dist/{ownership-DyYDF5ww.mjs → ownership-DiA16AsN.mjs} +3 -2
  74. package/dist/ownership-hCkPzGmr.mjs +571 -0
  75. package/dist/{page-BHUzTDFt.mjs → page-QF_Ke10s.mjs} +7 -2
  76. package/dist/{ports-BguS83ex.mjs → ports-R2k4Knz3.mjs} +3 -3
  77. package/dist/prebuild-D2fsca_Z.mjs +213 -0
  78. package/dist/preview-DhVbvPAZ.mjs +318 -0
  79. package/dist/process-identity-BmRm1qV0.mjs +12 -0
  80. package/dist/{project-CWlvEaeN.mjs → project-Du91Ilvc.mjs} +74 -19
  81. package/dist/pull-request-pxnDCQzn.mjs +322 -0
  82. package/dist/pull-requests.d.mts +120 -0
  83. package/dist/pull-requests.mjs +2 -0
  84. package/dist/recordings-BIXOzVnr.mjs +37 -0
  85. package/dist/{reload-BiniwsSZ.mjs → reload-rM9jV1fz.mjs} +109 -24
  86. package/dist/{remote-cache-CcR6o6lI.mjs → remote-cache-C8-V13XN.mjs} +5 -5
  87. package/dist/run-f4vI6EnK.mjs +119 -0
  88. package/dist/{server-bare-DGfgyJ5J.mjs → server-bare-CpPGmnmO.mjs} +33 -15
  89. package/dist/server-expo-CVqO2c35.mjs +2 -0
  90. package/dist/{server-expo-BVuwQ65v.mjs → server-expo-MC0rHlOC.mjs} +8 -12
  91. package/dist/settings-BBpMcDbD.mjs +25 -0
  92. package/dist/{settings-B-fsC-Om.mjs → settings-ZkDzkzvB.mjs} +22 -10
  93. package/dist/settings-oTSkkNrM.mjs +1439 -0
  94. package/dist/settings.schema.json +845 -97
  95. package/dist/{simslim-DrDN4WuV.mjs → simslim-B8-7AoTI.mjs} +8 -132
  96. package/dist/{slot-launch-D5t8EiMf.mjs → slot-launch-B8VrHxn1.mjs} +11 -8
  97. package/dist/{spawn-claims-Drw_o-Fb.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
  98. package/dist/spawn-entry-jw19m8Nl.mjs +17 -0
  99. package/dist/stage-CzaiwFsI.mjs +143 -0
  100. package/dist/start-CX8Ig-U2.mjs +2 -0
  101. package/dist/{start-s4RXp1wy.mjs → start-ClDfwXa4.mjs} +116 -65
  102. package/dist/state-3DwSQIBK.mjs +41 -0
  103. package/dist/state-B4PQFXMW.d.mts +12 -0
  104. package/dist/{state-Dk9RVXI7.mjs → state-C1jB7Q8q.mjs} +70 -8
  105. package/dist/{state-DHqvsbqC.mjs → state-DcW6vOd_.mjs} +7 -4
  106. package/dist/stats-CEZ_i5je.mjs +77 -0
  107. package/dist/{status-BgAuxESg.mjs → status-BuvBFeII.mjs} +118 -20
  108. package/dist/{status-BsPAnfOI.mjs → status-Cs7VLg9D.mjs} +542 -283
  109. package/dist/status-DlKvVJBE.mjs +49 -0
  110. package/dist/stdout-reader-C8yjre1x.mjs +23 -0
  111. package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
  112. package/dist/stim-footprint.swift +32 -0
  113. package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
  114. package/dist/stop-B0Yxtmiy.mjs +48 -0
  115. package/dist/{stop-BzreOWJp.mjs → stop-Bffozsac.mjs} +83 -16
  116. package/dist/{stop-BWIF_6BK.mjs → stop-Dt_dM16A.mjs} +2 -2
  117. package/dist/stop-cause-CidxIqup.mjs +97 -0
  118. package/dist/supervisor-run.d.mts +43 -11
  119. package/dist/supervisor-run.mjs +363 -24
  120. package/dist/support-CAP8qDUF.mjs +316 -0
  121. package/dist/support-D4aYJniO.mjs +201 -0
  122. package/dist/teardown-CL6vDs2Y.mjs +727 -0
  123. package/dist/{xcode-Cp5KvK9x.mjs → toolchain-DkUflduf.mjs} +364 -14
  124. package/dist/trigger-Bemu8ytw.mjs +65 -0
  125. package/dist/trigger-CXts6qt1.mjs +2 -0
  126. package/dist/warm-progress-CpGv3Wb6.mjs +732 -0
  127. package/dist/{web-BsQs1WlO.mjs → web-DWUaMp8P.mjs} +37 -21
  128. package/dist/web-run.mjs +310 -13
  129. package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
  130. package/dist/workspace-state-B8-aJ1_5.mjs +371 -0
  131. package/dist/workspaces-BV4pdHFz.mjs +306 -0
  132. package/dist/{worktree-UrQWXSeb.mjs → worktree-K-xj9Db0.mjs} +625 -256
  133. package/dist/worktree-jnG8Iph5.mjs +2 -0
  134. package/package.json +10 -5
  135. package/shim/bundle-response.cjs +28 -12
  136. package/dist/activity-CYVDcrt3.mjs +0 -356
  137. package/dist/budget-CGDkhvv_.mjs +0 -1745
  138. package/dist/build-progress-DxKpwnaS.mjs +0 -247
  139. package/dist/idle-DXtx3RbX.mjs +0 -191
  140. package/dist/in-use-DlqBDKqY.mjs +0 -249
  141. package/dist/logs-Civ4gbBH.mjs +0 -486
  142. package/dist/merge-state-ytFQCeh4.mjs +0 -262
  143. package/dist/server-expo-DIHDhvGg.mjs +0 -2
  144. package/dist/settings-D61iNOsB.mjs +0 -629
  145. package/dist/start-Cetfvy8l.mjs +0 -2
  146. package/dist/stats-CFpFGlJV.mjs +0 -62
  147. package/dist/stats-CX1Z8Tca.mjs +0 -368
  148. package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
  149. package/dist/stop-cause-hjGBOZTG.mjs +0 -1231
  150. package/dist/worktree-BVrHsYZG.mjs +0 -752
  151. package/dist/worktree-CHWWvm5v.mjs +0 -2
  152. package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
@@ -1,30 +1,467 @@
1
- import { A as ownership_claim_exports, O as process_identity_exports, T as upsertProject, U as projectDeviceSlots, _ as loadConfig, a as clearDevice, d as getConfigDir, g as isPathPrefix, k as acquireAvdClaim, p as getProject, x as removeProject } from "./config-lvc7d3C2.mjs";
2
- import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
3
- import { a as pidExists, c as resolveProjectMetro, g as recordWorkspaceUse, h as readWorkspaceState, k as workspaceDir, l as signalProcessTree, n as killMetroTree } from "./metro-CcEo8Fl8.mjs";
4
- import { C as sourceCheckoutOf, S as restoreFile, T as warmWorktreePaths, _ as readWorktreeExclude, b as resolveFullRef, d as hasUncommittedWork, f as isPodInstallChurn, g as podsOutOfSync, h as locallyKnownUpstream, i as depsOutOfSync, l as hasPopulatedSubmodules, m as listWorktrees, n as cloneIgnoredEntries, o as dirtyPaths, r as deleteBranch, s as gitCommonDir, t as branchExists, u as hasRemote, v as removeWorktree, w as unpushedCommits } from "./worktree-BVrHsYZG.mjs";
5
- import { a as findProjectRoot } from "./project-CWlvEaeN.mjs";
6
- import { a as clearRemoteSession, c as readMetroTunnel, d as readRemoteSessionId, i as clearManagedMetroTunnel, u as readRemoteSession } from "./state-DHqvsbqC.mjs";
7
- import { t as clearNamedPorts } from "./named-ports-Blg8IxOh.mjs";
8
- import { a as formatElapsed, h as shortUdid, i as formatDuration, l as phaseLine, p as releasedLeaseFact, u as plural } from "./command-output-DVcpw6eE.mjs";
9
- import { u as releaseWorkspaceLeases } from "./device-lease-5c3WrVRm.mjs";
10
- import { ct as parkedMaxSetting, rt as POOL_SETTING_REMEDY } from "./android-d6rGq1pA.mjs";
11
- import { L as unknownSettingKeys, P as settingShapeErrors, a as SETTING_SHAPE_REMEDY, j as resolveSettings } from "./settings-D61iNOsB.mjs";
12
- import { C as teardownOwnedIosSim, S as teardownOwnedBrowser, g as withManagedRemoteWorktreeRemovalLock, l as withdrawDevServerStopRequest, p as stopTunnel, s as requestDevServerStop, u as resolveSupervisorTarget, v as withManagedTunnelRemovalLock, x as teardownOwnedAvd } from "./stop-cause-hjGBOZTG.mjs";
13
- import { a as readWebRecord } from "./state-Dk9RVXI7.mjs";
14
- import { n as withIdleWorkspace, t as emptyWorkspaceDir } from "./in-use-DlqBDKqY.mjs";
15
- import { n as verifyCollectorOwnership } from "./ownership-DyYDF5ww.mjs";
16
- import { f as resolveEasCliBin } from "./remote-cache-CcR6o6lI.mjs";
17
- import { i as endRecordedSession } from "./device-remote-D87zEzaq.mjs";
18
- import { h as podInstallCommand, n as HEARTBEAT_INTERVAL_MS } from "./xcode-Cp5KvK9x.mjs";
19
- import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-DsT7aKeo.mjs";
20
- import { a as runPodInstall, i as runCaptured, n as podsAreStale, r as readPodState, t as DEPS_ERROR } from "./deps-D9IcKqXS.mjs";
21
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
22
- import { existsSync as existsSync$1, realpathSync as realpathSync$1 } from "fs";
23
- import { basename, dirname, isAbsolute, relative, resolve } from "path";
24
- import { workspaceName } from "@stim-cli/core";
25
- import { join as join$1, relative as relative$1 } from "node:path";
1
+ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
+ import { E as upsertProject, H as projectDeviceSlots, S as removeProject, _ as loadConfig$1, a as clearDevice, d as getConfigDir, g as isPathPrefix, k as acquireAvdClaim, p as getProject } from "./config-D0DbpZtM.mjs";
3
+ import { n as ownership_claim_exports, t as process_identity_exports } from "./process-identity-BmRm1qV0.mjs";
4
+ import { a as pullRequestLookup, r as endedPullRequest } from "./pull-request-pxnDCQzn.mjs";
5
+ import { a as formatElapsed, h as shortUdid, i as formatDuration, l as phaseLine, p as releasedLeaseFact, u as plural } from "./command-output-DnJMdEY-.mjs";
6
+ import { $ as cloneIgnoredEntries, A as readCommittedSettings, H as unknownSettingKeys, P as resolveSettings, Q as branchExists, R as settingShapeErrors, St as warmWorktreePaths, _t as resolveFullRef, a as SETTING_SHAPE_REMEDY, at as gitCommonDirOnDisk, bt as sourceCheckoutOf, ct as hasUncommittedWork, dt as listWorktrees, et as deleteBranch, ft as locallyKnownUpstream, ht as removeWorktree, it as gitCommonDir, lt as isPodInstallChurn, mt as readWorktreeExclude, ot as hasPopulatedSubmodules, pt as podsOutOfSync, rt as dirtyPaths, st as hasRemote, tt as depsOutOfSync, xt as unpushedCommits, yt as restoreFile } from "./settings-oTSkkNrM.mjs";
7
+ import { n as appProjectProblem, o as findProjectRoot$1 } from "./project-Du91Ilvc.mjs";
8
+ import { l as resolveProjectMetro, o as pidExists, r as killMetroTree, u as signalProcessTree } from "./metro-BeK1ZPz1.mjs";
9
+ import { g as workspaceAgentDeviceDir, i as recordWorkspaceUse, r as readWorkspaceState$1, v as workspaceDir } from "./workspace-state-B8-aJ1_5.mjs";
10
+ import { o as readWebRecord } from "./state-C1jB7Q8q.mjs";
11
+ import { a as machineNumber } from "./budget-DjrURfQn.mjs";
12
+ import { n as withIdleWorkspace, t as emptyWorkspaceDir } from "./in-use-DvBw0Kxo.mjs";
13
+ import { t as agentDeviceDaemon } from "./activity-BxwdD81S.mjs";
14
+ import { u as releaseWorkspaceLeases } from "./device-lease-D-hLqx1b.mjs";
15
+ import { a as clearRemoteSession, c as readMetroTunnel, d as readRemoteSessionId, i as clearManagedMetroTunnel, u as readRemoteSession } from "./state-DcW6vOd_.mjs";
16
+ import { ft as parkedMaxSetting, ot as POOL_SETTING_REMEDY } from "./android-B_NumVyQ.mjs";
17
+ import { a as workspaceLinks, i as workspaceLinkLine } from "./stim-desktop-Cnz8CPnl.mjs";
18
+ import { a as teardownOwnedBrowser, i as teardownOwnedAvd, o as teardownOwnedIosSim } from "./teardown-CL6vDs2Y.mjs";
19
+ import { t as clearNamedPorts } from "./named-ports-Bk_YXEmE.mjs";
20
+ import { i as stopTunnel, l as withManagedTunnelRemovalLock, s as withManagedRemoteWorktreeRemovalLock, t as resolveSupervisorTarget } from "./ownership-hCkPzGmr.mjs";
21
+ import { l as withdrawDevServerStopRequest, s as requestDevServerStop } from "./stop-cause-CidxIqup.mjs";
22
+ import { C as podInstallCommand, u as HEARTBEAT_INTERVAL_MS } from "./toolchain-DkUflduf.mjs";
23
+ import { n as verifyCollectorOwnership } from "./ownership-DiA16AsN.mjs";
24
+ import { t as stopMacosApp } from "./stop-B0Yxtmiy.mjs";
25
+ import { a as runPodInstall, i as runCaptured, n as podsAreStale, r as readPodState, t as DEPS_ERROR } from "./deps-urbmfXig.mjs";
26
+ import { f as resolveEasCliBin } from "./remote-cache-C8-V13XN.mjs";
27
+ import { i as endRecordedSession } from "./device-remote-D5WVWqOq.mjs";
28
+ import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-B6cMXZab.mjs";
29
+ import { a as forgetStatusMeasures, s as mergeState, t as startWarmProgress } from "./warm-progress-CpGv3Wb6.mjs";
30
+ import { r as workspaceRecordingEnabled } from "./recordings-BIXOzVnr.mjs";
31
+ import { r as stopHostedIos } from "./hosted-ios-CIWelIqf.mjs";
26
32
  import { createHash, randomUUID } from "node:crypto";
33
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs";
34
+ import { basename, dirname, join, relative } from "node:path";
35
+ import { existsSync as existsSync$1, realpathSync as realpathSync$1, rmSync as rmSync$1 } from "fs";
36
+ 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";
37
+ import { withDirLock, workspaceName } from "@stim-cli/core";
38
+ import { LAST_BUILD_KEYS, archiveDir, archiveEnabled, archiveLock, archiveRoot, archivedUsage, countErrorEntries, endedAgentSessionOf, listRecordedDevices, loadConfig, queryLogs, readArchives, readBuildHistory, readJsonObject, readLastBuilds, readMacosRecord, readPullRequestCache, readWorkspaceState, recordingEnabled, sortByTs } from "@stim-cli/core/state";
39
+ import { claimRemoveCommand, clearFreeClaimSet, readClaimSet, releaseClaim, tryAcquireClaim } from "@stim-cli/core/ownership-claim";
27
40
  import chalk from "chalk";
41
+ //#region src/archive.ts
42
+ const DAY = 864e5;
43
+ const KINDS = [
44
+ "logs",
45
+ "recordings",
46
+ "agentActions"
47
+ ];
48
+ const KIND_PATH = {
49
+ logs: "logs",
50
+ recordings: "recordings",
51
+ agentActions: "agent-device/sessions"
52
+ };
53
+ function archiveSettings() {
54
+ const config = loadConfig();
55
+ return Object.fromEntries([
56
+ "maxAgeDays",
57
+ "maxCount",
58
+ "maxTotalGb",
59
+ "logs.maxAgeDays",
60
+ "logs.maxMbPerWorkspace",
61
+ "recordings.maxAgeDays",
62
+ "recordings.maxTotalGb",
63
+ "agentActions.maxAgeDays"
64
+ ].map((key) => {
65
+ const result = machineNumber(`archive.${key}`, config, process.env);
66
+ if (result.error) throw new Error(result.error);
67
+ return [key, result.value];
68
+ }));
69
+ }
70
+ function enabled(root) {
71
+ const config = loadConfig();
72
+ const project = config?.projects?.[root];
73
+ const present = existsSync(root);
74
+ const common = present ? gitCommonDirOnDisk(root) : null;
75
+ if (!present && Object.values(config?.repos ?? {}).some((repo) => repo.settings?.archive?.enabled === false)) return false;
76
+ return archiveEnabled(process.env, [
77
+ project?.settings,
78
+ common ? config?.repos?.[common]?.settings : void 0,
79
+ present ? readCommittedSettings(root) : void 0,
80
+ config
81
+ ]);
82
+ }
83
+ function files(dir) {
84
+ if (!existsSync(dir)) return [];
85
+ return readdirSync(dir).flatMap((name) => {
86
+ const path = join(dir, name);
87
+ const stat = lstatSync(path);
88
+ if (stat.isSymbolicLink()) throw new Error(`refusing symlink ${path}`);
89
+ return stat.isDirectory() ? files(path) : [{
90
+ path,
91
+ bytes: stat.size,
92
+ at: stat.mtimeMs
93
+ }];
94
+ });
95
+ }
96
+ function secureTree(dir) {
97
+ if (lstatSync(dir).isSymbolicLink()) throw new Error(`refusing symlink ${dir}`);
98
+ chmodSync(dir, 448);
99
+ for (const name of readdirSync(dir)) {
100
+ const path = join(dir, name);
101
+ const stat = lstatSync(path);
102
+ if (stat.isSymbolicLink()) throw new Error(`refusing symlink ${path}`);
103
+ if (stat.isDirectory()) secureTree(path);
104
+ else chmodSync(path, 384);
105
+ }
106
+ }
107
+ function writeRecord(dir, record) {
108
+ const small = [
109
+ "state.json",
110
+ "ended-agents.json",
111
+ "log-error-index.json"
112
+ ].reduce((sum, name) => {
113
+ const path = join(dir, name);
114
+ return sum + (existsSync(path) ? statSync(path).size : 0);
115
+ }, 0);
116
+ for (let pass = 0; pass < 8; pass++) {
117
+ record.bytes.total = KINDS.reduce((sum, kind) => sum + record.bytes[kind], record.bytes.record);
118
+ const bytes = small + Buffer.byteLength(`${JSON.stringify(record)}\n`);
119
+ if (bytes === record.bytes.record) break;
120
+ record.bytes.record = bytes;
121
+ }
122
+ record.bytes.total = KINDS.reduce((sum, kind) => sum + record.bytes[kind], record.bytes.record);
123
+ const tmp = join(dir, `.archive-${randomUUID()}.tmp`);
124
+ try {
125
+ writeFileSync(tmp, `${JSON.stringify(record)}\n`, { mode: 384 });
126
+ renameSync(tmp, join(dir, "archive.json"));
127
+ } finally {
128
+ rmSync(tmp, { force: true });
129
+ }
130
+ const now = /* @__PURE__ */ new Date();
131
+ utimesSync(archiveRoot(), now, now);
132
+ }
133
+ function removeRecord(id) {
134
+ const dir = archiveDir(id);
135
+ if (!existsSync(dir)) return;
136
+ const removing = join(archiveRoot(), `.removing-${id}`);
137
+ renameSync(dir, removing);
138
+ rmSync(removing, {
139
+ recursive: true,
140
+ force: true
141
+ });
142
+ }
143
+ function sweepArchiveStaging(remove, waitMs = 0) {
144
+ if (!existsSync(archiveRoot())) return [];
145
+ return withDirLock(archiveLock(), () => readdirSync(archiveRoot()).filter((name) => name.startsWith(".incoming-") && (!name.endsWith(".claims") || !existsSync(join(archiveRoot(), name.slice(0, -7)))) || name.startsWith(".removing-")).flatMap((name) => {
146
+ const path = join(archiveRoot(), name.endsWith(".claims") ? name.slice(0, -7) : name);
147
+ const root = `${path}.claims`;
148
+ const survey = name.startsWith(".incoming-") ? readClaimSet(root) : null;
149
+ const unresolved = survey?.unresolved[0];
150
+ const kept = Boolean(survey && (survey.live.length || survey.unresolved.length));
151
+ const entry = {
152
+ path,
153
+ kept,
154
+ reason: unresolved?.reason ?? (kept ? "live archive writer" : null),
155
+ removeCommand: unresolved ? claimRemoveCommand(root) : null
156
+ };
157
+ if (remove && !kept) {
158
+ if (!survey || clearFreeClaimSet({
159
+ root,
160
+ label: "archive staging"
161
+ }).status === "cleared") {
162
+ rmSync(path, {
163
+ recursive: true,
164
+ force: true
165
+ });
166
+ return [];
167
+ }
168
+ }
169
+ return [entry];
170
+ }), { waitMs });
171
+ }
172
+ function stripPids(value) {
173
+ if (Array.isArray(value)) return value.map(stripPids);
174
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).filter(([key]) => !/pids?$/i.test(key)).map(([key, child]) => [key, stripPids(child)]));
175
+ return value;
176
+ }
177
+ function gitFacts(root) {
178
+ const empty = {
179
+ repository: null,
180
+ branch: null,
181
+ head: null,
182
+ subject: null,
183
+ merged: null,
184
+ pullRequest: null
185
+ };
186
+ if (!existsSync(root)) return empty;
187
+ const exec = getExecutor();
188
+ const run = (args) => exec.runFileQuiet("git", [
189
+ "-C",
190
+ root,
191
+ ...args
192
+ ], { timeoutMs: 1e3 });
193
+ const common = gitCommonDirOnDisk(root);
194
+ const summary = run([
195
+ "show",
196
+ "-s",
197
+ "--format=%H%n%s",
198
+ "HEAD"
199
+ ])?.split("\n");
200
+ const location = run([
201
+ "rev-parse",
202
+ "--show-toplevel",
203
+ "--abbrev-ref",
204
+ "HEAD"
205
+ ])?.split("\n");
206
+ const branch = location?.[1] && location[1] !== "HEAD" ? location[1] : null;
207
+ const defaultRef = run([
208
+ "symbolic-ref",
209
+ "--quiet",
210
+ "refs/remotes/origin/HEAD"
211
+ ]);
212
+ let merged = null;
213
+ if (defaultRef?.startsWith("refs/remotes/")) {
214
+ const state = mergeState(root, {
215
+ ref: defaultRef,
216
+ name: defaultRef.slice(13)
217
+ }, { timeoutMs: 2e3 });
218
+ merged = state.merged ? true : state.unknown ? null : false;
219
+ }
220
+ let prPath = root;
221
+ try {
222
+ if (location?.[0]) prPath = realpathSync(location[0]);
223
+ } catch {}
224
+ const pr = readPullRequestCache(prPath)?.pullRequest;
225
+ if (pr?.state === "merged") merged = true;
226
+ return {
227
+ repository: common ? basename(common) === ".git" ? dirname(common) : common : null,
228
+ branch,
229
+ head: summary?.[0] ?? null,
230
+ subject: summary?.[1] ?? null,
231
+ merged,
232
+ pullRequest: pr ? {
233
+ number: pr.number,
234
+ state: pr.state,
235
+ title: pr.title,
236
+ url: pr.url
237
+ } : null
238
+ };
239
+ }
240
+ function archiveWorkspace(root, removedBy, state = readWorkspaceState(root)) {
241
+ writeArchive(root, removedBy, state);
242
+ try {
243
+ enforceArchiveRetention();
244
+ } catch (error) {
245
+ console.error(`could not enforce archive retention: ${String(error?.message ?? error).split("\n")[0]}`);
246
+ }
247
+ }
248
+ function writeArchive(root, removedBy, state) {
249
+ let staging = null;
250
+ let claim;
251
+ try {
252
+ const dir = workspaceDir(root);
253
+ if (!existsSync(dir) || lstatSync(dir).isSymbolicLink()) return;
254
+ if (existsSync(root)) root = realpathSync(root);
255
+ try {
256
+ sweepArchiveStaging(true, 5e3);
257
+ } catch {}
258
+ if (!enabled(root)) return;
259
+ const limits = archiveSettings();
260
+ if (!limits.maxAgeDays || !limits.maxCount) return;
261
+ const logs = join(dir, "logs");
262
+ if (!existsSync(join(dir, "state.json")) && !Object.values(readBuildHistory(state)).some((entries) => entries?.length) && (!existsSync(logs) || readdirSync(logs).length === 0)) return;
263
+ const worktree = gitFacts(root);
264
+ mkdirSync(archiveRoot(), {
265
+ recursive: true,
266
+ mode: 448
267
+ });
268
+ chmodSync(archiveRoot(), 448);
269
+ const token = randomUUID();
270
+ staging = join(archiveRoot(), `.incoming-${token}`);
271
+ const attempt = tryAcquireClaim({
272
+ root: `${staging}.claims`,
273
+ mode: "exclusive",
274
+ label: "archive staging"
275
+ });
276
+ claim = attempt.acquired;
277
+ if (!claim) {
278
+ releaseClaim(attempt.pending);
279
+ throw new Error("archive staging claim is held");
280
+ }
281
+ withDirLock(archiveLock(), () => {
282
+ mkdirSync(staging, { mode: 448 });
283
+ let at = Date.now();
284
+ const workspace = workspaceName(root);
285
+ while (existsSync(archiveDir(`${workspace}--${at}`))) at++;
286
+ const id = `${workspace}--${at}`;
287
+ if (state) {
288
+ const { supervisor: _supervisor, collectors: _collectors, warm: _warm, ...stable } = state;
289
+ writeFileSync(join(staging, "state.json"), `${JSON.stringify(stripPids(stable))}\n`, { mode: 384 });
290
+ }
291
+ for (const name of ["ended-agents.json", "log-error-index.json"]) {
292
+ const data = readJsonObject(join(dir, name));
293
+ if (data) writeFileSync(join(staging, name), `${JSON.stringify(stripPids(data))}\n`, { mode: 384 });
294
+ }
295
+ const move = (kind) => {
296
+ const from = join(dir, KIND_PATH[kind]);
297
+ if (!limits[`${kind}.maxAgeDays`] || !existsSync(from)) return;
298
+ const to = join(staging, KIND_PATH[kind]);
299
+ mkdirSync(join(to, ".."), {
300
+ recursive: true,
301
+ mode: 448
302
+ });
303
+ renameSync(from, to);
304
+ secureTree(to);
305
+ };
306
+ move("logs");
307
+ move("agentActions");
308
+ const config = loadConfig();
309
+ if (limits["recordings.maxAgeDays"] && workspaceRecordingEnabled(root, config?.projects?.[root] ?? {}, config, process.env)) {
310
+ const recordingDir = join(dir, "recordings");
311
+ if (existsSync(recordingDir) && lstatSync(recordingDir).isSymbolicLink()) throw new Error(`refusing symlink ${recordingDir}`);
312
+ for (const device of listRecordedDevices(recordingDir)) {
313
+ if (lstatSync(device.dir).isSymbolicLink()) throw new Error(`refusing symlink ${device.dir}`);
314
+ const to = join(staging, "recordings", `${device.platform}-${device.slot}`);
315
+ for (const segment of device.segments.filter((entry) => !entry.open)) {
316
+ mkdirSync(to, {
317
+ recursive: true,
318
+ mode: 448
319
+ });
320
+ try {
321
+ if (lstatSync(segment.file).isSymbolicLink()) throw new Error(`refusing symlink ${segment.file}`);
322
+ renameSync(segment.file, join(to, basename(segment.file)));
323
+ chmodSync(join(to, basename(segment.file)), 384);
324
+ } catch (error) {
325
+ if (error.code !== "ENOENT") throw error;
326
+ }
327
+ }
328
+ }
329
+ }
330
+ const index = readJsonObject(join(staging, "log-error-index.json"));
331
+ const indexed = Object.values(index?.files ?? {}).flatMap((file) => Object.values(file.markers ?? {}).concat(file.errors ?? []));
332
+ const lastErrorCount = countErrorEntries(sortByTs(queryLogs({
333
+ records: indexed,
334
+ errorsOnly: true
335
+ })));
336
+ const history = Object.values(readBuildHistory(state)).flat();
337
+ const last = Object.values(readLastBuilds(state)).toSorted((a, b) => Date.parse(b.startedAt) - Date.parse(a.startedAt))[0] ?? null;
338
+ const bytes = {
339
+ logs: 0,
340
+ recordings: 0,
341
+ agentActions: 0,
342
+ record: 0,
343
+ total: 0
344
+ };
345
+ const expires = {
346
+ logs: null,
347
+ recordings: null,
348
+ agentActions: null,
349
+ record: new Date(at + limits.maxAgeDays * DAY).toISOString()
350
+ };
351
+ for (const kind of KINDS) {
352
+ bytes[kind] = files(join(staging, KIND_PATH[kind])).reduce((sum, file) => sum + file.bytes, 0);
353
+ if (bytes[kind]) expires[kind] = new Date(at + limits[`${kind}.maxAgeDays`] * DAY).toISOString();
354
+ }
355
+ const record = {
356
+ id,
357
+ projectRoot: root,
358
+ project: basename(root),
359
+ workspace,
360
+ worktree,
361
+ removedAt: new Date(at).toISOString(),
362
+ removedBy,
363
+ lastUsedAt: typeof state?.lastUsedAt === "string" ? state.lastUsedAt : null,
364
+ builds: {
365
+ count: history.length,
366
+ last,
367
+ lastErrorCount
368
+ },
369
+ agents: (readJsonObject(join(dir, "ended-agents.json"))?.sessions ?? []).map(endedAgentSessionOf).filter((session) => session !== null),
370
+ bytes,
371
+ expires,
372
+ version: 1
373
+ };
374
+ writeRecord(staging, record);
375
+ renameSync(staging, archiveDir(id));
376
+ }, { waitMs: 3e4 });
377
+ releaseClaim(claim);
378
+ claim = void 0;
379
+ staging = null;
380
+ } catch (error) {
381
+ if (staging) try {
382
+ rmSync(staging, {
383
+ recursive: true,
384
+ force: true
385
+ });
386
+ } catch {}
387
+ console.error(`could not archive: ${String(error?.message ?? error).split("\n")[0]}`);
388
+ } finally {
389
+ releaseClaim(claim);
390
+ }
391
+ }
392
+ function removeKind(record, kind) {
393
+ rmSync(join(archiveDir(record.id), KIND_PATH[kind]), {
394
+ recursive: true,
395
+ force: true
396
+ });
397
+ record.bytes[kind] = 0;
398
+ record.expires[kind] = null;
399
+ writeRecord(archiveDir(record.id), record);
400
+ }
401
+ function logRemovalOrder(path) {
402
+ return /\.ndjson\.\d+$/.test(path) ? 0 : /^build-.*\.ndjson$/.test(basename(path)) ? 1 : 2;
403
+ }
404
+ function enforceArchiveRetention(now = Date.now()) {
405
+ if (!existsSync(archiveRoot())) return;
406
+ let limits;
407
+ try {
408
+ limits = archiveSettings();
409
+ } catch (error) {
410
+ console.error(`could not enforce archive retention: ${String(error.message).split("\n")[0]}`);
411
+ return;
412
+ }
413
+ withDirLock(archiveLock(), () => {
414
+ const listed = readArchives().toReversed();
415
+ const records = listed.filter((record, index) => limits.maxAgeDays > 0 && now - Date.parse(record.removedAt) < limits.maxAgeDays * DAY && index >= listed.length - limits.maxCount);
416
+ for (const record of listed) if (!records.includes(record)) removeRecord(record.id);
417
+ const recordings = recordingEnabled(process.env, [loadConfig()]);
418
+ for (const record of records) {
419
+ for (const kind of [
420
+ "recordings",
421
+ "agentActions",
422
+ "logs"
423
+ ]) {
424
+ const age = limits[`${kind}.maxAgeDays`];
425
+ if (record.bytes[kind] && (!age || now - Date.parse(record.removedAt) >= age * DAY || kind === "recordings" && !recordings)) removeKind(record, kind);
426
+ }
427
+ const cap = limits["logs.maxMbPerWorkspace"] * 1024 ** 2;
428
+ if (record.bytes.logs > cap) {
429
+ const entries = files(join(archiveDir(record.id), "logs")).toSorted((a, b) => logRemovalOrder(a.path) - logRemovalOrder(b.path) || (logRemovalOrder(a.path) === 0 ? Number(b.path.split(".").at(-1)) - Number(a.path.split(".").at(-1)) : 0) || a.at - b.at);
430
+ for (const file of entries) {
431
+ if (record.bytes.logs <= cap) break;
432
+ rmSync(file.path);
433
+ record.bytes.logs = Math.max(0, record.bytes.logs - file.bytes);
434
+ }
435
+ if (!record.bytes.logs) record.expires.logs = null;
436
+ writeRecord(archiveDir(record.id), record);
437
+ }
438
+ }
439
+ const totalCap = limits.maxTotalGb * 1024 ** 3;
440
+ const recordingCap = limits["recordings.maxTotalGb"] * 1024 ** 3;
441
+ for (const record of records) for (;;) {
442
+ const usage = archivedUsage(records);
443
+ const totalOver = usage.bytes > totalCap;
444
+ const recordingsOver = usage.byKind.recordings > recordingCap;
445
+ if (!totalOver && !recordingsOver) break;
446
+ const candidates = KINDS.filter((kind) => record.bytes[kind] > 0 && (totalOver || kind === "recordings")).toSorted((a, b) => record.bytes[b] - record.bytes[a]);
447
+ if (!candidates.length) break;
448
+ removeKind(record, candidates[0]);
449
+ }
450
+ for (const record of records) {
451
+ const before = JSON.stringify(record.expires);
452
+ for (const kind of KINDS) record.expires[kind] = record.bytes[kind] ? new Date(Date.parse(record.removedAt) + limits[`${kind}.maxAgeDays`] * DAY).toISOString() : null;
453
+ record.expires.record = new Date(Date.parse(record.removedAt) + limits.maxAgeDays * DAY).toISOString();
454
+ if (JSON.stringify(record.expires) !== before) writeRecord(archiveDir(record.id), record);
455
+ }
456
+ }, { waitMs: 3e4 });
457
+ }
458
+ function deleteSelectedArchives(ids, kind) {
459
+ withDirLock(archiveLock(), () => {
460
+ for (const record of readArchives().filter((entry) => ids.includes(entry.id))) if (kind) removeKind(record, kind);
461
+ else removeRecord(record.id);
462
+ }, { waitMs: 3e4 });
463
+ }
464
+ //#endregion
28
465
  //#region src/devices/reclaim.ts
29
466
  async function reapCollectors(root, { verify = verifyCollectorOwnership, collectors }) {
30
467
  const skippedDevices = [];
@@ -166,14 +603,22 @@ function describeDereferenced(project) {
166
603
  }
167
604
  return devices;
168
605
  }
169
- function parkRequest(projectPath, slot, simslimManaged) {
606
+ function lastBuildCacheKey(projectPath, platform) {
607
+ const build = readWorkspaceState$1(projectPath)?.[LAST_BUILD_KEYS[platform]];
608
+ if (build === null || typeof build !== "object" || Array.isArray(build)) return null;
609
+ const { cacheKey } = build;
610
+ return typeof cacheKey === "string" ? cacheKey : null;
611
+ }
612
+ function parkRequest(project, projectPath, slot, simslimManaged) {
170
613
  const { max, error } = parkedMaxSetting("ios");
171
614
  if (error || max <= 0) return void 0;
172
615
  return {
173
616
  projectPath,
174
617
  max,
175
618
  simslimManaged,
176
- slot
619
+ slot,
620
+ appId: typeof project?.bundleId === "string" ? project.bundleId : null,
621
+ cacheKey: lastBuildCacheKey(projectPath, "ios")
177
622
  };
178
623
  }
179
624
  function reclaimOwnedDevices(project, projectPath, { park = false } = {}) {
@@ -191,7 +636,7 @@ function reclaimOwnedDevices(project, projectPath, { park = false } = {}) {
191
636
  const r = teardownOwnedIosSim(udid, {
192
637
  del: true,
193
638
  label,
194
- ...park ? { park: parkRequest(projectPath, slot, Boolean(ios.simslimManaged)) } : {}
639
+ ...park ? { park: parkRequest(project, projectPath, slot, Boolean(ios.simslimManaged)) } : {}
195
640
  });
196
641
  if (r.parkFallback) poolNotes.push(`could not park ${label}: ${r.parkFallback} -- deleted it instead`);
197
642
  for (const failure of r.evictionFailures ?? []) poolNotes.push(failure);
@@ -230,7 +675,9 @@ function reclaimOwnedDevices(project, projectPath, { park = false } = {}) {
230
675
  projectPath,
231
676
  slot,
232
677
  max: bound.max,
233
- configuration: typeof android.poolConfiguration === "string" ? android.poolConfiguration : void 0
678
+ configuration: typeof android.poolConfiguration === "string" ? android.poolConfiguration : void 0,
679
+ appId: typeof project?.androidPackage === "string" ? project.androidPackage : null,
680
+ cacheKey: lastBuildCacheKey(projectPath, "android")
234
681
  } } : {}
235
682
  });
236
683
  if (r.parkFallback) poolNotes.push(`could not park ${android.avdName}: ${r.parkFallback} -- deleted it instead`);
@@ -270,6 +717,7 @@ async function reclaimProject(path, options = {}) {
270
717
  const existed = existsSync(dir);
271
718
  const run = await withIdleWorkspace(path, () => reclaimIdleProject(path, existed, options), {
272
719
  purpose: "workspace removal",
720
+ hosted: false,
273
721
  supervisor: false,
274
722
  managedLocks: false
275
723
  });
@@ -298,12 +746,12 @@ async function reclaimProject(path, options = {}) {
298
746
  failedWorkspaceDirs: []
299
747
  };
300
748
  }
301
- async function reclaimIdleProject(path, workspaceExisted, { deleteOwnedDevices = false, parkOwnedDevices = false, preserveProjectRecord = false, stopSession = defaultStopSession, stopMetroTunnel = defaultStopMetroTunnel, releaseLeases = releaseWorkspaceLeases, verifyCollector = verifyCollectorOwnership, teardownBrowser = teardownOwnedBrowser, stopRequester = { by: "stim gc --delete" } }) {
749
+ async function reclaimIdleProject(path, workspaceExisted, { archive, deleteOwnedDevices = false, parkOwnedDevices = false, preserveProjectRecord = false, stopSession = defaultStopSession, stopMetroTunnel = defaultStopMetroTunnel, releaseLeases = releaseWorkspaceLeases, verifyCollector = verifyCollectorOwnership, teardownBrowser = teardownOwnedBrowser, stopRequester = { by: "stim gc --delete" } }) {
302
750
  await clearNamedPorts(path, { stop: true });
303
751
  const project = getProject(path);
304
752
  for (const { platforms } of projectDeviceSlots(project)) if (platforms.android?.owned && platforms.android.avdName) (0, ownership_claim_exports.releaseClaim)(acquireAvdClaim(platforms.android.avdName));
305
753
  const dereferenced = describeDereferenced(project);
306
- const initialState = readWorkspaceState(path);
754
+ const initialState = readWorkspaceState$1(path);
307
755
  let killedPid = null;
308
756
  let skippedMetro = null;
309
757
  let supervisorHeld = false;
@@ -359,7 +807,7 @@ async function reclaimIdleProject(path, workspaceExisted, { deleteOwnedDevices =
359
807
  failedCollectors.push(entry);
360
808
  }
361
809
  function retainReplacementProcesses() {
362
- const currentState = readWorkspaceState(path);
810
+ const currentState = readWorkspaceState$1(path);
363
811
  const currentProject = getProject(path);
364
812
  if (Boolean(currentState?.supervisor && !(0, process_identity_exports.sameProcessRecord)(currentState.supervisor, initialState?.supervisor) || currentProject?.supervisor && !(0, process_identity_exports.sameProcessRecord)(currentProject.supervisor, project?.supervisor) || readWebRecord(path) !== null || Object.entries(currentState?.collectors ?? {}).some(([platform, record]) => !(0, process_identity_exports.sameProcessRecord)(record, initialState?.collectors?.[platform])))) {
365
813
  supervisorHeld = true;
@@ -377,11 +825,33 @@ async function reclaimIdleProject(path, workspaceExisted, { deleteOwnedDevices =
377
825
  };
378
826
  skippedDevices.push(...skippedCollectors);
379
827
  failedDevices.push(...failedCollectors);
828
+ try {
829
+ await stopHostedIos(path);
830
+ } catch (error) {
831
+ const failed = {
832
+ platform: "ios",
833
+ name: "hosted iOS simulator",
834
+ reason: error.message
835
+ };
836
+ failedDevices.push(failed);
837
+ skippedDevices.push(failed);
838
+ }
380
839
  const remote = reclaimRemoteSession(path, { stopSession });
381
840
  if (remote.failed) {
382
841
  skippedDevices.push(remote.failed);
383
842
  failedDevices.push(remote.failed);
384
843
  }
844
+ const hosted = readMacosRecord(path)?.host;
845
+ if (hosted) try {
846
+ await stopMacosApp(path);
847
+ } catch (error) {
848
+ const failed = {
849
+ name: `hosted macOS session ${hosted.session} on ${hosted.machine}`,
850
+ reason: String(error?.message ?? error)
851
+ };
852
+ skippedDevices.push(failed);
853
+ failedDevices.push(failed);
854
+ }
385
855
  const tunnel = await reclaimMetroTunnel(path, { stopMetroTunnel });
386
856
  if (tunnel.failed) {
387
857
  skippedDevices.push(tunnel.failed);
@@ -399,6 +869,28 @@ async function reclaimIdleProject(path, workspaceExisted, { deleteOwnedDevices =
399
869
  if (failedDevices.length === 0 && !supervisorHeld) {
400
870
  const dir = workspaceDir(path);
401
871
  if (existsSync(dir)) try {
872
+ const stateDir = workspaceAgentDeviceDir(path);
873
+ if (!["none", "dead"].includes(agentDeviceDaemon(stateDir))) {
874
+ const exec = getExecutor();
875
+ try {
876
+ if (!exec.findExecutable("agent-device")) throw new Error("agent-device is not on PATH");
877
+ exec.runFile("agent-device", [
878
+ "daemon",
879
+ "stop",
880
+ "--state-dir",
881
+ stateDir,
882
+ "--clean"
883
+ ], {
884
+ timeoutMs: 2e4,
885
+ killSignal: "SIGKILL",
886
+ env: { AGENT_DEVICE_NO_UPDATE_NOTIFIER: "1" }
887
+ });
888
+ } catch (error) {
889
+ process.stderr.write(`${phaseLine("agent-device", `daemon could not be stopped: ${error.message}; run agent-device daemon stop --state-dir "${stateDir}" --clean`)}\n`);
890
+ throw error;
891
+ }
892
+ }
893
+ if (archive) archiveWorkspace(path, archive.removedBy, initialState);
402
894
  emptyWorkspaceDir(dir);
403
895
  if (workspaceExisted) removedWorkspaceDirs.push(dir);
404
896
  } catch {
@@ -428,192 +920,6 @@ async function reclaimIdleProject(path, workspaceExisted, { deleteOwnedDevices =
428
920
  };
429
921
  }
430
922
  //#endregion
431
- //#region src/workspace/pull-request.ts
432
- const GH_TIMEOUT_MS = 2e4;
433
- const GH_ENV = {
434
- GH_PROMPT_DISABLED: "1",
435
- GH_NO_UPDATE_NOTIFIER: "1",
436
- NO_COLOR: "1"
437
- };
438
- const GH_FIELDS = "number,state,url,headRefOid,mergedAt,closedAt,isCrossRepository";
439
- const SIGNED_OUT_EXIT = 4;
440
- function stateOf(state) {
441
- if (state === "OPEN") return "open";
442
- if (state === "MERGED") return "merged";
443
- if (state === "CLOSED") return "closed";
444
- return null;
445
- }
446
- function epoch(text) {
447
- const at = text ? Date.parse(text) : NaN;
448
- return Number.isFinite(at) ? at : null;
449
- }
450
- /**
451
- * Picks the pull request that describes `head` among those `gh pr list --head <branch>` returned. One whose head is
452
- * `head` wins, then one whose head contains `head`, then one whose head `head` contains. An open one wins a tier,
453
- * then the newest. A pull request unrelated to `head` is from an earlier use of the branch name, and one from a fork
454
- * only shares the branch name; both are ignored.
455
- * `isAncestor(a, b)` answers whether commit `a` is an ancestor of commit `b`, and false when git cannot tell.
456
- */
457
- function selectPullRequest(pulls, head, isAncestor) {
458
- const known = pulls.flatMap((pull) => {
459
- const state = stateOf(pull.state);
460
- return state && pull.headRefOid && !pull.isCrossRepository ? [{
461
- pull,
462
- state
463
- }] : [];
464
- });
465
- const tiers = [
466
- known.filter(({ pull }) => pull.headRefOid === head),
467
- known.filter(({ pull }) => pull.headRefOid !== head && isAncestor(head, pull.headRefOid)),
468
- known.filter(({ pull }) => pull.headRefOid !== head && isAncestor(pull.headRefOid, head))
469
- ];
470
- const tier = tiers.findIndex((entries) => entries.length > 0);
471
- if (tier < 0) return null;
472
- const { pull, state } = tiers[tier].toSorted((a, b) => Number(b.state === "open") - Number(a.state === "open") || b.pull.number - a.pull.number)[0];
473
- return {
474
- number: pull.number,
475
- state,
476
- url: pull.url,
477
- head: pull.headRefOid,
478
- containsHead: tier < 2,
479
- endedAt: state === "merged" ? epoch(pull.mergedAt) : state === "closed" ? epoch(pull.closedAt) : null
480
- };
481
- }
482
- function firstLine(error) {
483
- const { stderr, message } = error;
484
- const text = String(stderr ?? "").trim() || String(message ?? error);
485
- return text.split("\n")[0] ?? text;
486
- }
487
- function ghFailure(error, command) {
488
- if (error.status === SIGNED_OUT_EXIT) return {
489
- unavailable: "gh is not signed in; run `gh auth login`",
490
- sticky: true
491
- };
492
- if (error.code === "ETIMEDOUT") return {
493
- unavailable: `${command} did not answer within ${GH_TIMEOUT_MS / 1e3}s`,
494
- sticky: true
495
- };
496
- return {
497
- unavailable: `${command} failed: ${firstLine(error)}`,
498
- sticky: false
499
- };
500
- }
501
- function ancestry(cwd) {
502
- const exec = getExecutor();
503
- return (ancestor, descendant) => exec.runFileQuiet("git", [
504
- "-C",
505
- cwd,
506
- "merge-base",
507
- "--is-ancestor",
508
- ancestor,
509
- descendant
510
- ]) !== null;
511
- }
512
- /**
513
- * Asks GitHub, through `gh`, which pull requests have `branch` as their head, run from `cwd` so `gh` picks the
514
- * repository from its remotes. Each call is one fixed `gh pr list` invocation. Once `gh` is missing, signed out or
515
- * times out, every later lookup through the same function answers `unavailable` without running it again.
516
- */
517
- function pullRequestLookup() {
518
- let unavailable;
519
- return (cwd, branch, head) => {
520
- const exec = getExecutor();
521
- if (unavailable === void 0) unavailable = exec.findExecutable("gh") ? null : "gh is not installed";
522
- if (unavailable) return { unavailable };
523
- let out;
524
- try {
525
- out = exec.runFile("gh", [
526
- "pr",
527
- "list",
528
- "--head",
529
- branch,
530
- "--state",
531
- "all",
532
- "--limit",
533
- "20",
534
- "--json",
535
- GH_FIELDS
536
- ], {
537
- cwd,
538
- timeoutMs: GH_TIMEOUT_MS,
539
- env: GH_ENV
540
- });
541
- } catch (error) {
542
- const failure = ghFailure(error, "gh pr list");
543
- if (failure.sticky) unavailable = failure.unavailable;
544
- return { unavailable: failure.unavailable };
545
- }
546
- let pulls;
547
- try {
548
- pulls = JSON.parse(out);
549
- } catch {
550
- return { unavailable: "gh pr list printed no JSON" };
551
- }
552
- return { pullRequest: Array.isArray(pulls) ? selectPullRequest(pulls, head, ancestry(cwd)) : null };
553
- };
554
- }
555
- function branchesQuery(count) {
556
- return `query($owner: String!, $repo: String!${Array.from({ length: count }, (_, i) => `, $b${i}: String!`).join("")}) { repository(owner: $owner, name: $repo) {${Array.from({ length: count }, (_, i) => ` b${i}: pullRequests(headRefName: $b${i}, states: [OPEN, CLOSED, MERGED], first: 20, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { ${GH_FIELDS.replaceAll(",", " ")} } }`).join("")} } }`;
557
- }
558
- /**
559
- * {@link pullRequestLookup} for every branch of one repository in one fixed `gh api graphql` call run from `repo`,
560
- * which asks for the same 20 newest pull requests of each head branch that `gh pr list --head` returns. `gh` fills
561
- * `{owner}` and `{repo}` from the remotes of `repo`; branch names travel as variables. Once `gh` is missing, signed
562
- * out or times out, every later call through the same function answers `unavailable` without running it again.
563
- */
564
- function pullRequestLookups() {
565
- let unavailable;
566
- return async (repo, queries) => {
567
- const exec = getExecutor();
568
- if (unavailable === void 0) unavailable = exec.findExecutable("gh") ? null : "gh is not installed";
569
- const all = (lookup) => queries.map(() => lookup);
570
- if (unavailable) return all({ unavailable });
571
- if (!queries.length) return [];
572
- const args = [
573
- "api",
574
- "graphql",
575
- "-F",
576
- "owner={owner}",
577
- "-F",
578
- "repo={repo}"
579
- ];
580
- queries.forEach(({ branch }, i) => args.push("-f", `b${i}=${branch}`));
581
- args.push("-f", `query=${branchesQuery(queries.length)}`);
582
- let out;
583
- try {
584
- out = await exec.runFileAsync("gh", args, {
585
- cwd: repo,
586
- timeoutMs: GH_TIMEOUT_MS,
587
- env: GH_ENV
588
- });
589
- } catch (error) {
590
- const failure = ghFailure(error, "gh api graphql");
591
- if (failure.sticky) unavailable = failure.unavailable;
592
- return all({ unavailable: failure.unavailable });
593
- }
594
- let found;
595
- try {
596
- found = JSON.parse(out).data?.repository;
597
- } catch {
598
- return all({ unavailable: "gh api graphql printed no JSON" });
599
- }
600
- if (!found) return all({ unavailable: "gh api graphql printed no repository" });
601
- return queries.map(({ cwd, head }, i) => {
602
- const pulls = found[`b${i}`]?.nodes;
603
- return { pullRequest: Array.isArray(pulls) ? selectPullRequest(pulls, head, ancestry(cwd)) : null };
604
- });
605
- };
606
- }
607
- /** The merged or closed pull request whose commits include HEAD, which makes the worktree removable. */
608
- function endedPullRequest(lookup) {
609
- const pr = lookup && "pullRequest" in lookup ? lookup.pullRequest : null;
610
- return pr && pr.state !== "open" && pr.containsHead ? pr : null;
611
- }
612
- /** "PR #123 merged", for reports. */
613
- function describePullRequest(pr) {
614
- return `PR #${pr.number} ${pr.state}`;
615
- }
616
- //#endregion
617
923
  //#region src/engine/warm-claim.ts
618
924
  const WARM_CLAIM_LABEL = "worktree warm";
619
925
  const WARM_CLAIM_POLL_MS = 250;
@@ -634,10 +940,10 @@ function defaultSleep(ms) {
634
940
  return new Promise((resolve) => setTimeout(resolve, ms));
635
941
  }
636
942
  function warmClaimsDir() {
637
- return join$1(getConfigDir(), "warm-locks");
943
+ return join(getConfigDir(), "warm-locks");
638
944
  }
639
945
  function warmClaimPath(repositoryRoot) {
640
- return join$1(warmClaimsDir(), `${workspaceName(repositoryRoot)}.lock`);
946
+ return join(warmClaimsDir(), `${workspaceName(repositoryRoot)}.lock`);
641
947
  }
642
948
  function warmCommand(phase) {
643
949
  return phase === "refresh" ? "stim worktree warm --refresh" : "stim worktree warm";
@@ -964,11 +1270,11 @@ function stepLine(label, reason, action) {
964
1270
  return phaseLine(label, `${reason} -> ${action}`);
965
1271
  }
966
1272
  function installRecordFile(depsRoot) {
967
- return join$1(getConfigDir(), "warm-installs", `${workspaceName(depsRoot)}.json`);
1273
+ return join(getConfigDir(), "warm-installs", `${workspaceName(depsRoot)}.json`);
968
1274
  }
969
1275
  function lockfileHash(depsRoot, lockfile) {
970
1276
  try {
971
- return createHash("sha256").update(readFileSync(join$1(depsRoot, lockfile))).digest("hex");
1277
+ return createHash("sha256").update(readFileSync(join(depsRoot, lockfile))).digest("hex");
972
1278
  } catch {
973
1279
  return null;
974
1280
  }
@@ -990,7 +1296,7 @@ function readInstallEvidence(depsRoot) {
990
1296
  function writeInstallEvidence(depsRoot, lockfile, hash, completed) {
991
1297
  const file = installRecordFile(depsRoot);
992
1298
  const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
993
- mkdirSync(join$1(getConfigDir(), "warm-installs"), { recursive: true });
1299
+ mkdirSync(join(getConfigDir(), "warm-installs"), { recursive: true });
994
1300
  writeFileSync(tmp, JSON.stringify({
995
1301
  lock: lockfile,
996
1302
  hash: hash ?? "",
@@ -1008,9 +1314,9 @@ function restoreInstallEvidence(depsRoot, prior) {
1008
1314
  else rmSync(installRecordFile(depsRoot), { force: true });
1009
1315
  }
1010
1316
  function inProgressOperation(gitDir) {
1011
- if (existsSync(join$1(gitDir, "rebase-merge"))) return "rebase";
1012
- if (existsSync(join$1(gitDir, "rebase-apply"))) return existsSync(join$1(gitDir, "rebase-apply", "applying")) ? "am" : "rebase";
1013
- return existsSync(join$1(gitDir, "MERGE_HEAD")) ? "merge" : null;
1317
+ if (existsSync(join(gitDir, "rebase-merge"))) return "rebase";
1318
+ if (existsSync(join(gitDir, "rebase-apply"))) return existsSync(join(gitDir, "rebase-apply", "applying")) ? "am" : "rebase";
1319
+ return existsSync(join(gitDir, "MERGE_HEAD")) ? "merge" : null;
1014
1320
  }
1015
1321
  function readMainCheckoutState(root) {
1016
1322
  const exec = getExecutor();
@@ -1264,7 +1570,7 @@ async function refreshMainCheckout({ root, appDir, settings, emit, spawnFn = nul
1264
1570
  const consumed = dependencies ? lockfileHash(dependencies.root, dependencies.lock) : null;
1265
1571
  const deps = depsPlan({
1266
1572
  lockfile: dependencies?.lock ?? null,
1267
- lockfileChanged: dependencies ? pathChangedBetween(root, before, head, gitPath(relative$1(root, dependencies.root), dependencies.lock)) : false,
1573
+ lockfileChanged: dependencies ? pathChangedBetween(root, before, head, gitPath(relative(root, dependencies.root), dependencies.lock)) : false,
1268
1574
  installed,
1269
1575
  treeValid: dependencies?.lock === "package-lock.json" && installed ? installedNpmTreeIsValid(dependencies.root) : null,
1270
1576
  lastInstall: priorInstall,
@@ -1296,11 +1602,11 @@ async function refreshMainCheckout({ root, appDir, settings, emit, spawnFn = nul
1296
1602
  if (marked) restoreInstallEvidence(marked.root, priorInstall);
1297
1603
  emit(stepLine("deps", depsSubject, "skipped"));
1298
1604
  }
1299
- const app = relative$1(root, appDir) || ".";
1605
+ const app = relative(root, appDir) || ".";
1300
1606
  const where = `source ${appDir}: `;
1301
1607
  const podState = readPodState(appDir);
1302
1608
  const pods = podsPlan({
1303
- hasIos: existsSync(join$1(appDir, "ios")),
1609
+ hasIos: existsSync(join(appDir, "ios")),
1304
1610
  hasPodfile: podState.hasPodfile,
1305
1611
  podfileLockChanged: pathChangedBetween(root, before, head, gitPath(app, "ios", "Podfile.lock")),
1306
1612
  stale: podsAreStale(podState.lockText, podState.manifestText)
@@ -1331,12 +1637,12 @@ async function refreshMainCheckout({ root, appDir, settings, emit, spawnFn = nul
1331
1637
  //#endregion
1332
1638
  //#region src/commands/worktree.ts
1333
1639
  function dependencyInstallCommand(target, dir = ".") {
1334
- const installRoot = resolve(target, dir);
1640
+ const installRoot = resolve$1(target, dir);
1335
1641
  let command = "npm install";
1336
- if (existsSync$1(resolve(installRoot, "pnpm-lock.yaml"))) command = "pnpm install";
1337
- else if (existsSync$1(resolve(installRoot, "yarn.lock"))) command = "yarn install";
1338
- else if (existsSync$1(resolve(installRoot, "bun.lock")) || existsSync$1(resolve(installRoot, "bun.lockb"))) command = "bun install";
1339
- else if (existsSync$1(resolve(installRoot, "package-lock.json"))) command = "npm ci";
1642
+ if (existsSync$1(resolve$1(installRoot, "pnpm-lock.yaml"))) command = "pnpm install";
1643
+ else if (existsSync$1(resolve$1(installRoot, "yarn.lock"))) command = "yarn install";
1644
+ else if (existsSync$1(resolve$1(installRoot, "bun.lock")) || existsSync$1(resolve$1(installRoot, "bun.lockb"))) command = "bun install";
1645
+ else if (existsSync$1(resolve$1(installRoot, "package-lock.json"))) command = "npm ci";
1340
1646
  return `cd '${installRoot.replaceAll("'", "'\\''")}' && ${command}`;
1341
1647
  }
1342
1648
  function reportCarriedStateHealth(root, target, copied) {
@@ -1354,7 +1660,7 @@ function reportCarriedStateHealth(root, target, copied) {
1354
1660
  }
1355
1661
  for (const p of podsOutOfSync(target, copied)) {
1356
1662
  const where = p.dir === "." ? "Podfile.lock" : `${p.dir}/Podfile.lock`;
1357
- const pod = podInstallCommand(p.dir === "." ? target : resolve(target, p.dir, ".."));
1663
+ const pod = podInstallCommand(p.dir === "." ? target : resolve$1(target, p.dir, ".."));
1358
1664
  console.error(chalk.yellow(phaseLine("carry", p.reason === "missing" ? `carried ${p.dir === "." ? "Pods" : `${p.dir}/Pods`} but there is no ${where}. Run \`${pod}\` before building.` : `carried ${p.dir === "." ? "Pods" : `${p.dir}/Pods`} does not match the ${where} on disk here. Pods are gitignored and cloned; Podfile.lock is tracked, so the two can disagree. Run \`${pod}\` before building, or xcodebuild fails with "sandbox is not in sync" only after every pod has compiled.`)));
1359
1665
  }
1360
1666
  }
@@ -1366,18 +1672,51 @@ function reportRefreshFailure(failure) {
1366
1672
  process.exitCode = 1;
1367
1673
  }
1368
1674
  function mainCheckoutAppDir(root, target) {
1369
- const app = findProjectRoot(process.cwd());
1675
+ const app = findProjectRoot$1(process.cwd());
1370
1676
  if (!app) return root;
1371
- const rel = relative(target, app);
1372
- if (!rel || rel.startsWith("..") || isAbsolute(rel)) return root;
1373
- return resolve(root, rel);
1677
+ const rel = relative$1(target, app);
1678
+ if (!rel || rel.startsWith("..") || isAbsolute$1(rel)) return root;
1679
+ return resolve$1(root, rel);
1680
+ }
1681
+ /**
1682
+ * The app workspace a warm prepares: the nearest app below the worktree root, or else the one app registered in
1683
+ * the source checkout, at the same place in this worktree, or else the worktree root when it is the app. A monorepo
1684
+ * root can declare react-native without being the app.
1685
+ */
1686
+ function warmedWorkspace(root, target, cwd) {
1687
+ const nearest = findProjectRoot$1(cwd);
1688
+ const app = nearest && isPathPrefix(target, nearest) && appProjectProblem(nearest) === null ? nearest : null;
1689
+ if (app && app !== target) return app;
1690
+ const apps = /* @__PURE__ */ new Set();
1691
+ for (const path of Object.keys(loadConfig$1()?.projects ?? {})) {
1692
+ const rel = relative$1(root, path);
1693
+ if (rel.startsWith("..") || isAbsolute$1(rel)) continue;
1694
+ const mapped = resolve$1(target, rel);
1695
+ if (appProjectProblem(mapped) === null) apps.add(mapped);
1696
+ }
1697
+ return apps.size === 1 ? [...apps][0] : app;
1698
+ }
1699
+ function registerWarmedWorkspace(workspace) {
1700
+ try {
1701
+ if (!getProject(workspace)) upsertProject(workspace, {});
1702
+ } catch (error) {
1703
+ console.error(chalk.yellow(`Could not register ${workspace} for status: ${error?.message || String(error)}`));
1704
+ }
1374
1705
  }
1375
1706
  function registerWarm(worktree) {
1376
1707
  worktree.command("warm").description("Copy missing ignored paths from the source checkout into the current linked worktree.").option("--refresh", "Before copying, fast-forward the source checkout to its upstream and install what the new commits moved.").action(async (opts) => {
1708
+ let progress = null;
1709
+ let workspace = null;
1377
1710
  try {
1378
1711
  const { root, target, common } = warmWorktreePaths(process.cwd());
1379
- const app = findProjectRoot(process.cwd());
1712
+ const app = findProjectRoot$1(process.cwd());
1380
1713
  if (app) recordWorkspaceUse(app);
1714
+ workspace = warmedWorkspace(root, target, process.cwd());
1715
+ if (!workspace) console.error(chalk.dim(phaseLine("project", "no single app to register here; run warm from the app directory to list it in status while it warms")));
1716
+ else {
1717
+ registerWarmedWorkspace(workspace);
1718
+ progress = startWarmProgress(workspace, opts.refresh ? "refresh" : "copy", { note: (line) => console.error(chalk.yellow(line)) });
1719
+ }
1381
1720
  const readSettings = () => {
1382
1721
  const settings = resolveSettings({
1383
1722
  gitCommonDir: common,
@@ -1393,6 +1732,7 @@ function registerWarm(worktree) {
1393
1732
  return settings;
1394
1733
  };
1395
1734
  const copy = (wait) => {
1735
+ progress?.step("copy");
1396
1736
  if (wait?.holder) console.error(warmClaimAcquiredLine(wait));
1397
1737
  const incomplete = incompleteInstallRefusal(root, mainCheckoutAppDir(root, target));
1398
1738
  if (incomplete) {
@@ -1490,6 +1830,10 @@ function registerWarm(worktree) {
1490
1830
  if (claim) console.error(chalk.dim(claim.remedy));
1491
1831
  if (typeof code === "string" && code.startsWith("STIM_")) console.error(chalk.red(`failed: ${code}`));
1492
1832
  process.exitCode = 1;
1833
+ } finally {
1834
+ progress?.finish(!process.exitCode);
1835
+ const links = workspace && !process.exitCode ? workspaceLinks(workspace) : void 0;
1836
+ if (links) console.error(chalk.dim(workspaceLinkLine(links)));
1493
1837
  }
1494
1838
  });
1495
1839
  }
@@ -1504,6 +1848,20 @@ function porcelainPath(line) {
1504
1848
  }
1505
1849
  const SAFE_DIFF_PATH = /^[A-Za-z0-9._/-]+$/;
1506
1850
  const POD_CHURN_PATH = /(?:^|\/)ios\/(?:Podfile\.lock|[^/]+\.xcodeproj\/project\.pbxproj)$/;
1851
+ const WATCHMAN_COOKIE_PATH = /(?:^|\/)\.watchman-cookie-[A-Za-z0-9._-]+$/;
1852
+ function excludeWatchmanCookies(lines) {
1853
+ const kept = [];
1854
+ const cookies = [];
1855
+ for (const line of lines || []) {
1856
+ const path = String(line).startsWith("?? ") ? porcelainPath(line) : null;
1857
+ if (path && SAFE_DIFF_PATH.test(path) && WATCHMAN_COOKIE_PATH.test(path)) cookies.push(path);
1858
+ else kept.push(line);
1859
+ }
1860
+ return {
1861
+ lines: kept,
1862
+ cookies
1863
+ };
1864
+ }
1507
1865
  function excludePodChurn(lines) {
1508
1866
  const kept = [];
1509
1867
  const restore = [];
@@ -1526,11 +1884,11 @@ function excludePodChurn(lines) {
1526
1884
  }
1527
1885
  function nativeCanonicalPath(path) {
1528
1886
  const missing = [];
1529
- for (let existing = resolve(path);; existing = dirname(existing)) try {
1530
- return resolve(realpathSync$1.native(existing), ...missing);
1887
+ for (let existing = resolve$1(path);; existing = dirname$1(existing)) try {
1888
+ return resolve$1(realpathSync$1.native(existing), ...missing);
1531
1889
  } catch {
1532
- if (dirname(existing) === existing) return resolve(path);
1533
- missing.unshift(basename(existing));
1890
+ if (dirname$1(existing) === existing) return resolve$1(path);
1891
+ missing.unshift(basename$1(existing));
1534
1892
  }
1535
1893
  }
1536
1894
  function matchWorktreeEntry(entries, path) {
@@ -1584,7 +1942,7 @@ function describeKeptDevice(s) {
1584
1942
  return s.udid && s.udid !== s.name ? `${s.name} (${s.udid})` : s.name;
1585
1943
  }
1586
1944
  function reclaimKeys(rootPath) {
1587
- const cfg = loadConfig();
1945
+ const cfg = loadConfig$1();
1588
1946
  const keys = /* @__PURE__ */ new Set([rootPath]);
1589
1947
  const root = nativeCanonicalPath(rootPath);
1590
1948
  if (cfg?.projects) {
@@ -1597,7 +1955,7 @@ async function withReclaimLocks(rootPath, fn) {
1597
1955
  const acquire = (index) => index === keys.length ? fn(keys) : withManagedTunnelRemovalLock(keys[index], () => acquire(index + 1));
1598
1956
  return acquire(0);
1599
1957
  }
1600
- async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRootProject = false } = {}) {
1958
+ async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRootProject = false, removedBy } = {}) {
1601
1959
  const dereferenced = [];
1602
1960
  const killedPids = [];
1603
1961
  const deletedDevices = [];
@@ -1616,6 +1974,7 @@ async function reclaimAll(rootPath, keys = reclaimKeys(rootPath), { preserveRoot
1616
1974
  let r;
1617
1975
  try {
1618
1976
  r = await reclaimProject(key, {
1977
+ archive: removedBy ? { removedBy } : void 0,
1619
1978
  deleteOwnedDevices: true,
1620
1979
  parkOwnedDevices: true,
1621
1980
  preserveProjectRecord: preserveRootProject && key === rootPath,
@@ -1716,7 +2075,7 @@ function reportRetainedResources(root, result) {
1716
2075
  process.exitCode = 1;
1717
2076
  }
1718
2077
  function hasRegisteredProjectUnder(rootPath) {
1719
- const cfg = loadConfig();
2078
+ const cfg = loadConfig$1();
1720
2079
  return Object.keys(cfg?.projects ?? {}).some((key) => isPathPrefix(rootPath, key));
1721
2080
  }
1722
2081
  async function reclaimEnvironment(root, why) {
@@ -1743,20 +2102,21 @@ function removalPath(target) {
1743
2102
  return canonicalExistingPath(target ?? process.cwd());
1744
2103
  }
1745
2104
  function canonicalExistingPath(target) {
1746
- const resolved = resolve(target);
2105
+ const resolved = resolve$1(target);
1747
2106
  let existing = resolved;
1748
2107
  const missing = [];
1749
2108
  while (!existsSync$1(existing)) {
1750
- const parent = dirname(existing);
2109
+ const parent = dirname$1(existing);
1751
2110
  if (parent === existing) return resolved;
1752
- missing.unshift(basename(existing));
2111
+ missing.unshift(basename$1(existing));
1753
2112
  existing = parent;
1754
2113
  }
1755
- return resolve(realpathSync$1(existing), ...missing);
2114
+ return resolve$1(realpathSync$1(existing), ...missing);
1756
2115
  }
1757
2116
  function inspectRemoval(path, mergedHead) {
1758
2117
  const gitAnswered = hasUncommittedWork(path);
1759
- const { lines: dirtyLines, restore: podChurn } = excludePodChurn(gitAnswered ? dirtyPaths(path, { limit: Infinity }) : []);
2118
+ const { lines: withoutCookies, cookies } = excludeWatchmanCookies(gitAnswered ? dirtyPaths(path, { limit: Infinity }) : []);
2119
+ const { lines: dirtyLines, restore: podChurn } = excludePodChurn(withoutCookies);
1760
2120
  const dirty = gitAnswered === null ? null : dirtyLines.length > 0;
1761
2121
  const unpushed = unpushedCommits(path);
1762
2122
  const blockers = removalBlockers({
@@ -1767,6 +2127,7 @@ function inspectRemoval(path, mergedHead) {
1767
2127
  return {
1768
2128
  dirtyLines,
1769
2129
  podChurn,
2130
+ cookies,
1770
2131
  unpushed,
1771
2132
  blockers
1772
2133
  };
@@ -1785,6 +2146,9 @@ function printRemovalRefusal(path, inspection) {
1785
2146
  console.error(chalk.dim("work stays on the branch."));
1786
2147
  process.exitCode = 1;
1787
2148
  }
2149
+ function deleteWatchmanCookies(path, cookies) {
2150
+ for (const cookie of cookies) rmSync$1(join$1(path, cookie), { force: true });
2151
+ }
1788
2152
  function restorePodChurn(path, files) {
1789
2153
  for (const file of files) if (restoreFile(path, file)) console.error(chalk.dim(` restored ${file} (pod install churn; the worktree is being removed)`));
1790
2154
  else console.error(chalk.yellow(` could not restore ${file}; git may refuse to remove the worktree`));
@@ -1832,7 +2196,7 @@ async function runRemove(target, opts, onRemoved) {
1832
2196
  console.error(chalk.dim(phaseLine("branch", `${branch} is already absent; cleared its pending cleanup record`)));
1833
2197
  return;
1834
2198
  }
1835
- const checkedOutAt = listWorktrees(mainRoot).find((candidate) => candidate.branch === branch && resolve(candidate.path) !== resolve(path))?.path;
2199
+ const checkedOutAt = listWorktrees(mainRoot).find((candidate) => candidate.branch === branch && resolve$1(candidate.path) !== resolve$1(path))?.path;
1836
2200
  if (checkedOutAt) {
1837
2201
  console.error(chalk.red(`Refusing pending cleanup for ${branch}: it is checked out at ${checkedOutAt}.`));
1838
2202
  console.error(chalk.dim("Switch that worktree to another branch, then retry this command."));
@@ -1935,7 +2299,10 @@ async function runRemove(target, opts, onRemoved) {
1935
2299
  printRemovalRefusal(path, current);
1936
2300
  return;
1937
2301
  }
1938
- const result = await reclaimAll(path, lockedKeys, { preserveRootProject: true });
2302
+ const result = await reclaimAll(path, lockedKeys, {
2303
+ preserveRootProject: true,
2304
+ removedBy: opts.removedBy ?? "worktree-remove"
2305
+ });
1939
2306
  if (result.keptEntries.length) {
1940
2307
  reportRetainedResources(path, result);
1941
2308
  return;
@@ -1948,6 +2315,7 @@ async function runRemove(target, opts, onRemoved) {
1948
2315
  return;
1949
2316
  }
1950
2317
  restorePodChurn(path, current.podChurn);
2318
+ deleteWatchmanCookies(path, current.cookies);
1951
2319
  try {
1952
2320
  removeWorktree(path, {
1953
2321
  from: source.path,
@@ -1963,6 +2331,7 @@ async function runRemove(target, opts, onRemoved) {
1963
2331
  return;
1964
2332
  }
1965
2333
  onRemoved();
2334
+ forgetStatusMeasures(path, result.reclaimedKeys);
1966
2335
  const finish = () => {
1967
2336
  printRemovalCleanup(result, false);
1968
2337
  console.error(chalk.dim(phaseLine("removed", path)));
@@ -1978,7 +2347,7 @@ async function runRemove(target, opts, onRemoved) {
1978
2347
  worktreeRemovalComplete: true,
1979
2348
  worktreePendingBranchSha: approvedBranchSha
1980
2349
  });
1981
- const checkedOutAt = listWorktrees(branchDeleteCwd).find((candidate) => candidate.branch === branch && resolve(candidate.path) !== resolve(path))?.path;
2350
+ const checkedOutAt = listWorktrees(branchDeleteCwd).find((candidate) => candidate.branch === branch && resolve$1(candidate.path) !== resolve$1(path))?.path;
1982
2351
  if (checkedOutAt) {
1983
2352
  console.error(chalk.yellow(phaseLine("branch", `kept ${branch} (it is checked out at ${checkedOutAt})`)));
1984
2353
  console.error(chalk.dim(` Switch that worktree to another branch, then retry: stim worktree remove ${path}`));
@@ -2012,4 +2381,4 @@ function worktreeCommand(program) {
2012
2381
  registerRemove(worktree);
2013
2382
  }
2014
2383
  //#endregion
2015
- export { reclaimProject as _, reclaimKeys as a, removalBlockers as c, removeWorktreeTarget as d, worktreeCommand as f, describeDereferenced as g, pullRequestLookups as h, porcelainPath as i, removalPath as l, endedPullRequest as m, excludePodChurn as n, registerRemove as o, describePullRequest as p, matchWorktreeEntry as r, registerWarm as s, dependencyInstallCommand as t, removalRemedy as u };
2384
+ export { deleteSelectedArchives as _, porcelainPath as a, registerWarm as c, removalRemedy as d, removeWorktreeTarget as f, reclaimProject as g, describeDereferenced as h, matchWorktreeEntry as i, removalBlockers as l, worktreeCommand as m, excludePodChurn as n, reclaimKeys as o, warmedWorkspace as p, excludeWatchmanCookies as r, registerRemove as s, dependencyInstallCommand as t, removalPath as u, sweepArchiveStaging as v };