stim 1.26.0 → 1.27.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 (171) hide show
  1. package/dist/{acquire-artifact-DVsvSwf7.mjs → acquire-artifact-BKN2Yuhg.mjs} +2 -2
  2. package/dist/{act-C-BqJEpu.mjs → act-ROviTF_t.mjs} +297 -122
  3. package/dist/{activity-Lnb_EoRY.mjs → activity-Cd-ysFes.mjs} +6 -6
  4. package/dist/{agent-device-usage-output-DbXy5tg8.mjs → agent-device-usage-output-C5nCKWp9.mjs} +5 -5
  5. package/dist/{android-CSbLRD53.mjs → android--HBgpvpW.mjs} +5 -5
  6. package/dist/{android-CiNQw3kq.mjs → android-Cd6w7_qw.mjs} +96 -43
  7. package/dist/{android-B5P81yI-.mjs → android-Deb9X3_Y.mjs} +37 -15
  8. package/dist/{android-DTAcRwBC.mjs → android-DriNoAVy.mjs} +23 -18
  9. package/dist/{android-cas-BTVhZ0Qs.mjs → android-cas-D5e6ecUg.mjs} +4 -4
  10. package/dist/android-cas-compiler.mjs +1 -1
  11. package/dist/api.d.mts +13 -3
  12. package/dist/api.mjs +39 -29
  13. package/dist/{apk-signed-tail-B46U9hXc.mjs → apk-signed-tail-BHQHx3Rm.mjs} +2 -2
  14. package/dist/{artifact-BxREhAaV.mjs → artifact-0AGJYB9O.mjs} +18 -23
  15. package/dist/{artifact-CsAP6AGK.mjs → artifact-faemIZv5.mjs} +19 -24
  16. package/dist/{artifact-platform-CMRc0M_r.mjs → artifact-platform-BO2Uvk_A.mjs} +1 -1
  17. package/dist/auto-placement-z-H5pJza.mjs +350 -0
  18. package/dist/{browser-web-rYPyqhTf.mjs → browser-web-slD8AvLp.mjs} +16 -16
  19. package/dist/{build-cache-DrXNCy3T.mjs → build-cache-gBJ-WKzA.mjs} +43 -21
  20. package/dist/{build-DPhApsjV.mjs → build-hS0QyFlQ.mjs} +35 -31
  21. package/dist/{build-plan-DrMMdayj.mjs → build-plan-CBqYbzYM.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-BWTze1Ls.mjs} +560 -1603
  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-ftXDLto1.mjs} +24 -23
  28. package/dist/cli.mjs +1 -1
  29. package/dist/{client-D9CaRks-.mjs → client-xsdxrhWA.mjs} +48 -62
  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-Dj-WT4BP.mjs} +1 -1
  33. package/dist/{config-BktkY_zC.mjs → config-Cn94Jc0g.mjs} +23 -4
  34. package/dist/{dependencies-Dw6wfQdv.mjs → dependencies-BGokZ-Ng.mjs} +108 -60
  35. package/dist/{dependency-state-DFB41vfr.mjs → dependency-state-C3OtgG94.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-BMca82xo.mjs} +2 -2
  38. package/dist/{device-BcYeHSFu.mjs → device-BJm0irrq.mjs} +16 -16
  39. package/dist/{device-capacity-DCSvUF0l.mjs → device-capacity-6LP7QsaA.mjs} +101 -16
  40. package/dist/device-host-worker.mjs +19 -19
  41. package/dist/{device-ios-BLGfXQeK.mjs → device-ios-534c14fz.mjs} +251 -23
  42. package/dist/{device-lease-run-BmvIl7bI.mjs → device-lease-run-DwsGfjUN.mjs} +5 -5
  43. package/dist/{device-pool-Dluix4H3.mjs → device-pool-BnQqvs6o.mjs} +3 -3
  44. package/dist/{device-BK-zwyd_.mjs → device-tM41-TUq.mjs} +25 -24
  45. package/dist/{doctor-Cp-BhzTS.mjs → doctor-Dbx1ZpD2.mjs} +1 -1
  46. package/dist/{doctor-Bhg1ii70.mjs → doctor-DyqE9n1J.mjs} +28 -16
  47. package/dist/{doctor-OnwP94ne.mjs → doctor-F-3VUUyN.mjs} +76 -32
  48. package/dist/{doctor-fingerprint-D3a04Mhu.mjs → doctor-fingerprint-o7ZI9YLn.mjs} +1 -1
  49. package/dist/doctor-fingerprint.mjs +1 -1
  50. package/dist/{doctor-watchman-BStK05nt.mjs → doctor-watchman-CxDbyrrt.mjs} +5 -5
  51. package/dist/{eas-build-Dsj93iM9.mjs → eas-build-CjNSWmjo.mjs} +4 -4
  52. package/dist/eas-fallback-9fqb46kW.mjs +87 -0
  53. package/dist/{eas-session-ledger-B5vygTiD.mjs → eas-session-ledger-DrP4OuuM.mjs} +6 -6
  54. package/dist/{error-diagnostics-BOOm9Db7.mjs → error-diagnostics-snej9k4j.mjs} +51 -25
  55. package/dist/{exec-VFRpu1iA.mjs → exec-aULwtNly.mjs} +57 -6
  56. package/dist/{gc-C_79zvz4.mjs → gc-BRwAY3qT.mjs} +2 -2
  57. package/dist/{gc-ldgr-hXU.mjs → gc-CGZ1Yg88.mjs} +160 -45
  58. package/dist/{guide-D7K0X6KG.mjs → guide-_rfEh_zM.mjs} +580 -183
  59. package/dist/{hosted-android-Bk54ej6n.mjs → hosted-android-BMKzUNnY.mjs} +4 -4
  60. package/dist/{hosted-client-fHO8yqzc.mjs → hosted-client-D7Pk_Oo1.mjs} +12 -8
  61. package/dist/{hosted-logs-BfbmDM4M.mjs → hosted-logs-B1gqQuva.mjs} +8 -8
  62. package/dist/{hosted-logs-sync-B0pvmOk-.mjs → hosted-logs-sync-Dg3qrZmV.mjs} +5 -5
  63. package/dist/{hosted-macos-CPl0OZ9h.mjs → hosted-macos-B9w4H8hc.mjs} +6 -6
  64. package/dist/{hosted-native-CpDB3-PO.mjs → hosted-native-CdgaiuyP.mjs} +17 -16
  65. package/dist/{idle-shutdown-FfxL1KdG.mjs → idle-shutdown-Cbvo5KEh.mjs} +7 -8
  66. package/dist/{ios-ftxdQbJS.mjs → ios-B5HlVMhQ.mjs} +92 -37
  67. package/dist/{ios-B7-lToDu.mjs → ios-Bk4mWbtS.mjs} +8 -8
  68. package/dist/{ios-ENC4sRPA.mjs → ios-CsRVik64.mjs} +7 -1
  69. package/dist/{ios-WxpDJ5T1.mjs → ios-E26qSpkb.mjs} +27 -11
  70. package/dist/{ios-device-CsyryvpT.mjs → ios-device-C_QkIf1Z.mjs} +3 -3
  71. package/dist/{ios-device-B6HK951C.mjs → ios-device-CiBA-er9.mjs} +1 -1
  72. package/dist/{ios-project-Ty5pHl9O.mjs → ios-project-D8Jcy_br.mjs} +22 -9
  73. package/dist/{ios-signing-4c_qXvQm.mjs → ios-signing-BJzvDPfU.mjs} +2 -2
  74. package/dist/{ios-state-Cw7-WQf2.mjs → ios-state-BZ03NFgF.mjs} +1 -1
  75. package/dist/{js-swap-BMhXYVDS.mjs → js-swap-Dg2a945i.mjs} +8 -7
  76. package/dist/{launch-DOp8RjDz.mjs → launch-CRyBeNsA.mjs} +42 -38
  77. package/dist/lightened-BCsdAGj_.mjs +171 -0
  78. package/dist/{logs-dQDki6Tt.mjs → logs-CC_-ZzEE.mjs} +7 -7
  79. package/dist/{logs-s1dRIG1E.mjs → logs-aJQb-RDg.mjs} +46 -21
  80. package/dist/{macos-B0VEV3Gf.mjs → macos-BGmTpnv9.mjs} +1 -1
  81. package/dist/{macos-DBBPNnIA.mjs → macos-DlCBhMw5.mjs} +73 -38
  82. package/dist/macos-run.mjs +1 -1
  83. package/dist/maintenance-run.mjs +7 -7
  84. package/dist/{metro-CjC10t6E.mjs → metro-CvyD-xwe.mjs} +4 -4
  85. package/dist/{metro-gateway-B6_PgD0L.mjs → metro-gateway-BnHdqMO_.mjs} +3 -3
  86. package/dist/{named-ports-BG0j1X4g.mjs → named-ports-BCyMWA6b.mjs} +3 -3
  87. package/dist/{native-android-CL2NGiik.mjs → native-android-DlpxCyOP.mjs} +17 -18
  88. package/dist/{native-gradle-artifact-inputs-btSUulme.mjs → native-gradle-artifact-inputs-C13qT7R_.mjs} +5 -5
  89. package/dist/{native-runtime-Dq3RBcON.mjs → native-runtime-DqaIBO5w.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-B1W8SrxB.mjs} +3 -3
  92. package/dist/{native-xcode-ios-CBXJAxfC.mjs → native-xcode-ios-QPP0Rm68.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-Bx-0cku-.mjs} +3 -3
  97. package/dist/{page-CzEA_-5_.mjs → page-CQJlcj3x.mjs} +2 -2
  98. package/dist/{phone-reverses-lmj6kcPm.mjs → phone-reverses-C9yAaKEv.mjs} +18 -4
  99. package/dist/{placement-log-DIFPQAXp.mjs → placement-log-BXUbiZM6.mjs} +3 -3
  100. package/dist/{plan-placement-IiiSQhtz.mjs → plan-placement-UxnQjv5Z.mjs} +36 -10
  101. package/dist/{ports-C2_w1Z0Z.mjs → ports-uUrghqWN.mjs} +3 -3
  102. package/dist/{prebuild-gmouvmJZ.mjs → prebuild-BDpToDgR.mjs} +16 -17
  103. package/dist/{project-BUu6Is4k.mjs → project-DYj-HVx6.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-DDBccgJw.mjs} +32 -26
  107. package/dist/{react-native-build-oNwqF5Wb.mjs → react-native-build-DaK2IFyP.mjs} +9 -9
  108. package/dist/{react-native-doctor-BhA3KUte.mjs → react-native-doctor-B-Mw6DcR.mjs} +6 -6
  109. package/dist/{react-native-ios-B-nf4rjN.mjs → react-native-ios-DjYwUF9U.mjs} +23 -17
  110. package/dist/{recordings-BldzVKq0.mjs → recordings-C20iUZfp.mjs} +1 -1
  111. package/dist/{registration-CN6nlhcD.mjs → registration-BwCe64PH.mjs} +1 -1
  112. package/dist/{reload-C9OduiNc.mjs → reload-DJBeF_rC.mjs} +1 -1
  113. package/dist/{reload-CCR7KC_l.mjs → reload-fUdkH2_p.mjs} +16 -16
  114. package/dist/{remote-C8S7FlPF.mjs → remote-y6n99WMW.mjs} +22 -9
  115. package/dist/{result-7F5hEwQJ.mjs → result-CTqBw2Te.mjs} +32 -21
  116. package/dist/{run-BqB0v3pt.mjs → run-DaCeNKRm.mjs} +3 -3
  117. package/dist/{selection-BNPBZ6T5.mjs → selection-Bs8phwhN.mjs} +37 -29
  118. package/dist/{server-bare-A75rDIdD.mjs → server-bare-BOh-iZEj.mjs} +2 -2
  119. package/dist/{server-command-CssKFOD7.mjs → server-command-8CuA51Cv.mjs} +6 -6
  120. package/dist/{server-expo-eQfOdydb.mjs → server-expo-ABQs2XFA.mjs} +4 -4
  121. package/dist/server-expo-Ba_Y21vB.mjs +2 -0
  122. package/dist/{settings-CosW5X-S.mjs → settings-BDVI8Ivc.mjs} +331 -62
  123. package/dist/{settings-OaGQF-ri.mjs → settings-CMHhQNwI.mjs} +24 -3
  124. package/dist/settings-payload-cm60Zhib.mjs +257 -0
  125. package/dist/settings-write-BdNRLlBn.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-0l7RqSpr.mjs} +6 -6
  129. package/dist/{stage-jKQ7CP_B.mjs → stage-F4cAfJGi.mjs} +2 -2
  130. package/dist/{start-C_wefNKB.mjs → start-DcqnF9p8.mjs} +1 -1
  131. package/dist/{start-_BdJ32cZ.mjs → start-DrPwER_2.mjs} +27 -22
  132. package/dist/{state-C1i2nMOP.mjs → state-BX1y5syV.mjs} +4 -4
  133. package/dist/{state-C4OdpOTF.mjs → state-CZxLyetD.mjs} +2 -2
  134. package/dist/{stats-tEpEEH-u.mjs → stats-GxuCbles.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-BfnK9pUd.mjs} +25 -25
  138. package/dist/{status-B9M7x4CZ.mjs → status-Bj7ZYvvc.mjs} +1 -1
  139. package/dist/{status-watch-B5wDIH_o.mjs → status-watch-BfabQn03.mjs} +5 -5
  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-ySNQHXMG.mjs → stop-CH79S3IZ.mjs} +5 -5
  143. package/dist/{stop-DRqQVqDJ.mjs → stop-D-EAr25B.mjs} +21 -20
  144. package/dist/{stop-CmdNhGlC.mjs → stop-x1Yu3He4.mjs} +2 -2
  145. package/dist/supervisor-run.mjs +17 -17
  146. package/dist/{support-CVOCp0Ft.mjs → support-Be0UeVZU.mjs} +10 -10
  147. package/dist/{support-BGSCBoib.mjs → support-CNg_YsOk.mjs} +6 -6
  148. package/dist/{swiftpm-artifact-inputs-CfUzt_0Z.mjs → swiftpm-artifact-inputs-uPqYvGBv.mjs} +4 -4
  149. package/dist/{swiftpm-macos-x6HPpl_2.mjs → swiftpm-macos-Ch4YjY_A.mjs} +11 -12
  150. package/dist/{toolchain-J4J8MrAr.mjs → toolchain-CbCG6C5Z.mjs} +321 -15
  151. package/dist/{toolchain-DoavrvIA.mjs → toolchain-JYAOquPg.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-C0RsbngN.mjs} +731 -727
  155. package/dist/{web-Bihbf7-u.mjs → web-Cq_AQ4DK.mjs} +9 -9
  156. package/dist/web-Je6abcpz.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-BjqdjnJR.mjs} +3 -3
  160. package/dist/{workspaces-g2lhdBXF.mjs → workspaces-DFbj07jp.mjs} +1795 -70
  161. package/dist/{worktree-BKuklFh_.mjs → worktree-COAJ3-OD.mjs} +934 -514
  162. package/dist/{worktree-Bm4cb-nU.mjs → worktree-JGUd_53L.mjs} +1 -1
  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
@@ -1,31 +1,64 @@
1
- import { D as ownership_claim_exports, S as process_identity_exports, c as throwIfCancelled, d as workspaceProcessLockError, f as workspaceProcessLockPath, l as WorkspaceProcessCleanupError, m as declareSpawnsOn, o as cancellableSleep, t as getExecutor, u as withWorkspaceProcessLock, v as stopDeclaringSpawnsOn } from "./exec-VFRpu1iA.mjs";
2
- import { A as workspaceName$1, E as workspaceDir, l as withWorkspaceStateLockAsync, r as readWorkspaceState$1, u as workspaceLastUsed, w as workspaceAgentDeviceDir } from "./workspace-state-pUg3_RRA.mjs";
3
- import { F as isOnMountedVolume, G as projectDeviceSlots, H as deviceSlotPlatforms, I as listMountedVolumes, L as measuredDirectorySizeAsync, O as withConfigLockAsync$1, P as formatBytes, _ as loadConfig$1, d as getConfigDir, g as isPathPrefix, j as acquireAvdClaim, p as getProject } from "./config-BktkY_zC.mjs";
1
+ import { D as ownership_claim_exports, S as process_identity_exports, b as operationState, c as throwIfCancelled, d as workspaceProcessLockError, f as workspaceProcessLockPath, l as WorkspaceProcessCleanupError, m as declareSpawnsOn, o as cancellableSleep, t as getExecutor, u as withWorkspaceProcessLock, v as stopDeclaringSpawnsOn } from "./exec-aULwtNly.mjs";
2
+ import { A as workspaceName$1, E as workspaceDir, l as withWorkspaceStateLockAsync, o as updateWorkspaceState, r as readWorkspaceState$1, t as clearWorkspaceStateKey, u as workspaceLastUsed, w as workspaceAgentDeviceDir } from "./workspace-state-BjqdjnJR.mjs";
3
+ import { D as withConfigLock$1, F as isOnMountedVolume, G as projectDeviceSlots, H as deviceSlotPlatforms, I as listMountedVolumes, L as measuredDirectorySizeAsync, O as withConfigLockAsync$1, P as formatBytes, _ as loadConfig$1, d as getConfigDir, g as isPathPrefix$1, j as acquireAvdClaim, p as getProject } from "./config-Cn94Jc0g.mjs";
4
4
  import { t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
5
- import { a as formatElapsed, l as phaseLine } from "./command-output-BF5AR2dL.mjs";
6
- import { t as runOperationFile } from "./operation-exec-CUZifp6Z.mjs";
7
- import { Pt as repoRootAsync, dt as resolveTailscaleBinary } from "./settings-CosW5X-S.mjs";
8
- import { a as macosRuntimeClaim, n as macosDir } from "./state-C4OdpOTF.mjs";
9
- import { Ct as forgetCreatedDeviceAsync, H as ownedAvdMatchesConfiguration, K as ownedAvdSystemImage, L as listOrphanedAvdDirectories, S as deleteAvd, St as forgetCreatedDevice, Tt as recordCreatedDevice, V as ownedAvdDirectory, _t as parkSim, bt as removeParkedAfter, g as avdPathExists, ht as eraseParkedAfter, it as shutdownAndroidEmulator, j as listAdbDevicesAsync, lt as wipeAvdUserData, nt as resolveOwnedAvdSerialAsync, p as assertOwnedAvdStoppedAsync, st as waitForAndroidEmulatorShutdown, ut as isStimOwnedAvd, v as avdStorageRoots, wt as readCreatedDevices$1, yt as readParked } from "./android-DTAcRwBC.mjs";
10
- import { c as pidExists, f as signalProcessTree, p as signalProcessTreeAsync } from "./metro-CjC10t6E.mjs";
11
- import { _ as chromeProcessStateAsync, c as webClaimRoot, i as clearWebRecord, o as readWebRecord, p as webProfileDir, t as CDP_PORT_LABEL, u as webFacts, v as liveProfileHolder, y as removeSingletonFiles } from "./state-C1i2nMOP.mjs";
5
+ import { a as formatElapsed, d as quotedPath$1, l as phaseLine, o as formatLongDuration, u as plural } from "./command-output-Dj-WT4BP.mjs";
6
+ import { c as gitQuiet, l as gitQuietAsync, t as runOperationFile } from "./operation-exec-Dp4FuIGR.mjs";
7
+ import { Lt as repoRootAsync, pt as resolveTailscaleBinary } from "./settings-BDVI8Ivc.mjs";
8
+ import { a as macosRuntimeClaim, n as macosDir } from "./state-CZxLyetD.mjs";
9
+ import { Ct as forgetCreatedDeviceAsync, G as ownedAvdSerialResolverAsync, H as ownedAvdMatchesConfiguration, K as ownedAvdSystemImage, L as listOrphanedAvdDirectories, S as deleteAvd, St as forgetCreatedDevice, Tt as recordCreatedDevice, V as ownedAvdDirectory, W as ownedAvdSerialResolver, _t as parkSim, bt as removeParkedAfter, g as avdPathExists, ht as eraseParkedAfter, it as shutdownAndroidEmulator, j as listAdbDevicesAsync, lt as wipeAvdUserData, nt as resolveOwnedAvdSerialAsync, p as assertOwnedAvdStoppedAsync, st as waitForAndroidEmulatorShutdown, ut as isStimOwnedAvd, v as avdStorageRoots, wt as readCreatedDevices$1, yt as readParked } from "./android-DriNoAVy.mjs";
10
+ import { c as pidExists, f as signalProcessTree, p as signalProcessTreeAsync } from "./metro-CvyD-xwe.mjs";
11
+ import { _ as chromeProcessStateAsync, c as webClaimRoot, i as clearWebRecord, o as readWebRecord, p as webProfileDir, t as CDP_PORT_LABEL, u as webFacts, v as liveProfileHolder, y as removeSingletonFiles } from "./state-BX1y5syV.mjs";
12
12
  import { t as canonicalPath } from "./paths-BsJMoPP2.mjs";
13
- import { N as listBuildLocks, d as readAgentDeviceRecords, t as agentDeviceDaemon } from "./activity-Lnb_EoRY.mjs";
14
- import { C as renameIosSim, D as resolveOwnedIosSim, c as eraseIosSim, f as listAllIosSimsAsync, k as shutdownIosSim, o as deleteIosSim, p as listIosDeviceTypesAsync, s as deleteParkedIosSim, v as parkedSimName, y as parseRuntimeVersion } from "./ios-WxpDJ5T1.mjs";
15
- import { r as releaseBrowserPort } from "./named-ports-BG0j1X4g.mjs";
13
+ import { N as listBuildLocks, d as readAgentDeviceRecords, i as createActivityReaderAsync, r as createActivityReader, s as idleForMs, t as agentDeviceDaemon } from "./activity-Cd-ysFes.mjs";
14
+ import { C as renameIosSim, D as resolveOwnedIosSim, c as eraseIosSim, f as listAllIosSimsAsync, k as shutdownIosSim, o as deleteIosSim, p as listIosDeviceTypesAsync, s as deleteParkedIosSim, v as parkedSimName, y as parseRuntimeVersion } from "./ios-E26qSpkb.mjs";
15
+ import { r as releaseBrowserPort } from "./named-ports-BCyMWA6b.mjs";
16
16
  import { createHash, randomUUID } from "node:crypto";
17
17
  import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
18
18
  import { isDeepStrictEqual } from "node:util";
19
19
  import { homedir } from "node:os";
20
20
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
21
- import { ambientContext, ambientEnv, ambientWrite } from "@stim-cli/core";
22
- import { buildSlotsDir, hostedNativeRecords, isJsonObject, macosAppState, parseHostedNativePlacement, readJsonFile, readMacosRecord, requireAutomaticMachine, tryAcquireBuildSlotClaim, unreadableHostedNative } from "@stim-cli/core/state";
21
+ import { ambientContext, ambientEnv, ambientWrite, withDirLock, workspaceName } from "@stim-cli/core";
22
+ import { BUILD_HISTORY_KEY, BUILD_HISTORY_LIMIT, BUILD_LEDGER_CAP, BUILD_PHASES, LAST_BUILD_KEYS, STATS_VERSION, buildLedgerFile, buildLedgerLock, buildSlotsDir, decodeStats, hostedNativeRecords, isJsonObject, lastNativeUseFrom, macosAppState, parseHostedNativePlacement, parseMissReason, readJsonFile, readMacosRecord, readRunEstimates, readStats as readStats$1, requireAutomaticMachine, statsFile, statsProjectKey as statsProjectKey$1, trace, trimCapacityRefusals, trimCapacityWaits, trimSamples, tryAcquireBuildSlotClaim, unreadableHostedNative, wholePhases, workspaceBuildDetailFile } from "@stim-cli/core/state";
23
23
  import { basename as basename$1, dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1 } from "path";
24
24
  import { request } from "node:https";
25
25
  import chalk from "chalk";
26
26
  import { existsSync as existsSync$1, lstatSync as lstatSync$1, readdirSync as readdirSync$1, statSync as statSync$1 } from "fs";
27
27
  import { lstat, readFile, readdir, rm, stat, unlink } from "node:fs/promises";
28
28
  import { resolve4 } from "node:dns/promises";
29
+ //#region src/commands/gc/limited.ts
30
+ const isCleanupFailure = (error) => error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error;
31
+ async function eachLimited(items, limit, run) {
32
+ const queue = items[Symbol.iterator]();
33
+ let failed = false;
34
+ const worker = async () => {
35
+ for (const item of queue) {
36
+ if (failed) return;
37
+ try {
38
+ await run(item);
39
+ } catch (error) {
40
+ failed = true;
41
+ throw error;
42
+ }
43
+ }
44
+ };
45
+ const errors = (await Promise.allSettled(Array.from({ length: Math.min(limit, items.length) }, worker))).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
46
+ if (errors.length) throw errors.find(isCleanupFailure) ?? errors[0];
47
+ }
48
+ function readConcurrency(operation) {
49
+ return operationState(operation).declaring.size ? 1 : 6;
50
+ }
51
+ function sizeConcurrency(operation) {
52
+ return operationState(operation).declaring.size ? 1 : 4;
53
+ }
54
+ async function mapLimited(items, limit, run) {
55
+ const done = [];
56
+ await eachLimited([...items.entries()], limit, async ([index, item]) => {
57
+ done.push([index, await run(item)]);
58
+ });
59
+ return done.toSorted(([a], [b]) => a - b).map(([, result]) => result);
60
+ }
61
+ //#endregion
29
62
  //#region src/commands/gc/results.ts
30
63
  function createGcResults(observe) {
31
64
  const results = [];
@@ -349,6 +382,10 @@ function deviceClaims(device) {
349
382
  workspace: record.workspace && canonical(record.workspace)
350
383
  }));
351
384
  }
385
+ var AgentDeviceRefusal = class extends Error {
386
+ reason;
387
+ stateDir;
388
+ };
352
389
  function explainFailure(error) {
353
390
  const stdout = error?.stdout;
354
391
  let failure;
@@ -358,13 +395,17 @@ function explainFailure(error) {
358
395
  failure = void 0;
359
396
  }
360
397
  if (typeof failure?.message !== "string" || !failure.message) return error;
361
- const reason = typeof failure.details?.reason === "string" ? ` (${failure.details.reason})` : "";
362
- return /* @__PURE__ */ new Error(`agent-device refused: ${failure.message}${reason}`);
398
+ const reason = typeof failure.details?.reason === "string" ? failure.details.reason : void 0;
399
+ const stateDir = typeof failure.details?.stateDir === "string" ? failure.details.stateDir : void 0;
400
+ const refusal = new AgentDeviceRefusal(`agent-device refused: ${failure.message}${reason ? ` (${reason})` : ""}`);
401
+ refusal.reason = reason;
402
+ refusal.stateDir = stateDir;
403
+ return refusal;
363
404
  }
364
405
  function report(message) {
365
406
  ambientWrite("stderr", `${phaseLine("device", message)}\n`);
366
407
  }
367
- async function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot, operation) {
408
+ async function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot, operation, reportedBusy = /* @__PURE__ */ new Set()) {
368
409
  const exec = getExecutor();
369
410
  try {
370
411
  operation?.signal.throwIfAborted();
@@ -378,6 +419,7 @@ async function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot,
378
419
  };
379
420
  const stateDir = root === void 0 ? void 0 : workspaceAgentDeviceDir(root);
380
421
  const stateDirs = [void 0, ...stateDir && !["none", "dead"].includes(agentDeviceDaemon(stateDir)) ? [stateDir] : []];
422
+ const busy = /* @__PURE__ */ new Set();
381
423
  for (const dir of stateDirs) {
382
424
  const deadline = Date.now() + 15e3;
383
425
  const run = async (args) => {
@@ -431,9 +473,16 @@ async function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot,
431
473
  } catch (error) {
432
474
  if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
433
475
  operation?.signal.throwIfAborted();
434
- report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
476
+ if (error instanceof AgentDeviceRefusal && error.reason === "daemon-busy") busy.add(error.stateDir ?? dir ?? "");
477
+ else report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
435
478
  }
436
479
  }
480
+ const unreported = [...busy].filter((dir) => !reportedBusy.has(dir));
481
+ if (unreported.length > 0) {
482
+ for (const dir of unreported) reportedBusy.add(dir);
483
+ const stop = unreported.map((dir) => `agent-device daemon stop${dir ? ` --state-dir ${quotedPath$1(dir)}` : ""}`);
484
+ report(`agent-device daemon is busy, so Stim did not check it for sessions on ${device.platform === "ios" ? device.id : device.id ?? device.avdName}; any that name the device stay open until the daemon is idle or, if it is yours, stopped with ${stop.join(" and ")}`);
485
+ }
437
486
  } catch (error) {
438
487
  if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
439
488
  operation?.signal.throwIfAborted();
@@ -652,6 +701,7 @@ async function parkOwnedIosSim(settled, udid, park, operation) {
652
701
  }
653
702
  async function teardownOwnedIosSim(udid, { del = false, label, park, workspace, shutdownClock = {}, operation } = {}) {
654
703
  let parkFallback;
704
+ const reportedBusy = /* @__PURE__ */ new Set();
655
705
  try {
656
706
  const resolved = await resolveOwnedIosSimNow(udid, operation);
657
707
  if (resolved.notOwned) return {
@@ -663,7 +713,7 @@ async function teardownOwnedIosSim(udid, { del = false, label, park, workspace,
663
713
  await closeOwnedDeviceSessions({
664
714
  platform: "ios",
665
715
  id: udid
666
- }, async () => Boolean((await resolveOwnedIosSimNow(udid, operation)).sim), workspace, void 0, operation);
716
+ }, async () => Boolean((await resolveOwnedIosSimNow(udid, operation)).sim), workspace, void 0, operation, reportedBusy);
667
717
  const current = await resolveOwnedIosSimNow(udid, operation);
668
718
  if (current.missing) return { status: "missing" };
669
719
  if (current.notOwned) return {
@@ -721,7 +771,7 @@ async function teardownOwnedIosSim(udid, { del = false, label, park, workspace,
721
771
  if (root && settled.sim) await closeOwnedDeviceSessions({
722
772
  platform: "ios",
723
773
  id: udid
724
- }, async () => (await resolveOwnedIosSimNow(udid, operation)).sim?.state === "Shutdown", workspace, root, operation);
774
+ }, async () => (await resolveOwnedIosSimNow(udid, operation)).sim?.state === "Shutdown", workspace, root, operation, reportedBusy);
725
775
  return {
726
776
  status: "torn-down",
727
777
  label: label ?? sim.name ?? udid
@@ -849,6 +899,7 @@ async function teardownClaimedAvd(avdName, claim, { operation, del = false, wipe
849
899
  signal: operation?.signal
850
900
  }) }) {
851
901
  let parkFallback;
902
+ const reportedBusy = /* @__PURE__ */ new Set();
852
903
  const resolveAvd = async (name) => {
853
904
  operation?.signal.throwIfAborted();
854
905
  const result = await resolve(name);
@@ -890,7 +941,7 @@ async function teardownClaimedAvd(avdName, claim, { operation, del = false, wipe
890
941
  platform: "android",
891
942
  id: serial,
892
943
  avdName
893
- }, stillOwned, workspace, void 0, operation);
944
+ }, stillOwned, workspace, void 0, operation, reportedBusy);
894
945
  if (!await stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
895
946
  await waitForShutdown(avdName, async (timeoutMs) => {
896
947
  if (!operation) (0, ownership_claim_exports.markClaimChildPending)(claim);
@@ -911,7 +962,7 @@ async function teardownClaimedAvd(avdName, claim, { operation, del = false, wipe
911
962
  platform: "android",
912
963
  id: serial,
913
964
  avdName
914
- }, stillStopped, workspace, root, operation);
965
+ }, stillStopped, workspace, root, operation, reportedBusy);
915
966
  }
916
967
  } else {
917
968
  await closeOwnedDeviceSessions({
@@ -1149,6 +1200,1561 @@ async function teardownBrowserHeld(root, { deleteProfile = false, operation } =
1149
1200
  }
1150
1201
  }
1151
1202
  //#endregion
1203
+ //#region src/engine/stats.ts
1204
+ const PLACEMENT_MAX_AGE_MS = 6048e5;
1205
+ function emptyStats() {
1206
+ return {
1207
+ version: STATS_VERSION,
1208
+ machine: {},
1209
+ projects: {}
1210
+ };
1211
+ }
1212
+ function updateStats(record, run, now) {
1213
+ const at = new Date(now).toISOString();
1214
+ const durationMs = wholeMs$1(run.durationMs);
1215
+ const phases = {
1216
+ coldBuildMs: wholeMs$1(run.coldBuildMs),
1217
+ podsMs: wholeMs$1(run.podsMs)
1218
+ };
1219
+ const projects = { ...record.projects };
1220
+ const scope = { ...projects[run.projectKey] };
1221
+ const machine = { ...record.machine };
1222
+ const before = scope[run.platform] ?? null;
1223
+ const credit = creditMs(before, run, durationMs);
1224
+ scope[run.platform] = applyRun(before, run, {
1225
+ at,
1226
+ durationMs,
1227
+ credit,
1228
+ phases
1229
+ });
1230
+ projects[run.projectKey] = scope;
1231
+ machine[run.platform] = applyRun(machine[run.platform] ?? null, run, {
1232
+ at,
1233
+ durationMs,
1234
+ credit,
1235
+ phases
1236
+ });
1237
+ const history = { ...record.history };
1238
+ if (run.phases && !run.failed && !run.waitedForBuild && !run.offloadedTo) {
1239
+ const outcome = isHit(run.cacheHit) ? "hit" : "cold";
1240
+ const project = { ...history[run.projectKey] };
1241
+ const lists = { ...project[run.platform] };
1242
+ const sample = {
1243
+ at,
1244
+ durationMs,
1245
+ phases: wholePhases(run.phases),
1246
+ ...run.deviceSetup === void 0 ? {} : { deviceSetup: run.deviceSetup }
1247
+ };
1248
+ lists[outcome] = trimSamples([...lists[outcome] ?? [], sample]);
1249
+ project[run.platform] = lists;
1250
+ history[run.projectKey] = project;
1251
+ }
1252
+ let placements = record.placements;
1253
+ let buildMachines = record.buildMachines;
1254
+ if (run.placement) {
1255
+ const placement = buildPlacement(run, before, at);
1256
+ placements = trimPlacements([...placements ?? [], placement], now);
1257
+ if (placement.machine && placement.decision !== "here") buildMachines = {
1258
+ ...buildMachines,
1259
+ [placement.machine]: applyPlacement(buildMachines?.[placement.machine], placement)
1260
+ };
1261
+ }
1262
+ const capacityRefusals = trimCapacityRefusals(record.capacityRefusals ?? [], now);
1263
+ const capacityWaits = trimCapacityWaits(record.capacityWaits ?? [], now);
1264
+ return {
1265
+ version: STATS_VERSION,
1266
+ machine,
1267
+ projects,
1268
+ ...Object.keys(history).length ? { history } : {},
1269
+ ...placements?.length ? { placements } : {},
1270
+ ...capacityRefusals.length ? { capacityRefusals } : {},
1271
+ ...capacityWaits.length ? { capacityWaits } : {},
1272
+ ...buildMachines && Object.keys(buildMachines).length ? { buildMachines } : {}
1273
+ };
1274
+ }
1275
+ function buildPlacement(run, before, at) {
1276
+ const placement = run.placement;
1277
+ const buildMs = wholeMs$1(placement.decision === "offloaded" ? placement.buildMs : run.coldBuildMs);
1278
+ const localEstimateMs = wholeMs$1(before?.lastColdBuildMs);
1279
+ const slotWaitMs = wholeMs$1(placement.slotWaitMs);
1280
+ const deviceSlotWaitMs = wholeMs$1(placement.deviceSlotWaitMs);
1281
+ return {
1282
+ at,
1283
+ project: run.projectKey,
1284
+ platform: run.platform,
1285
+ decision: placement.decision,
1286
+ reason: placement.reason,
1287
+ ...placement.machine ? { machine: placement.machine } : {},
1288
+ ...buildMs > 0 ? { buildMs } : {},
1289
+ ...slotWaitMs > 0 ? { slotWaitMs } : {},
1290
+ ...deviceSlotWaitMs > 0 ? { deviceSlotWaitMs } : {},
1291
+ ...localEstimateMs > 0 ? { localEstimateMs } : {},
1292
+ ...run.failed ? { failed: true } : {}
1293
+ };
1294
+ }
1295
+ function applyPlacement(totals, placement) {
1296
+ const next = totals ? { ...totals } : {
1297
+ offloaded: 0,
1298
+ offloadedMs: 0,
1299
+ savedMs: 0,
1300
+ fallbacks: 0
1301
+ };
1302
+ if (placement.decision === "fell-back") {
1303
+ next.fallbacks += 1;
1304
+ next.lastFallbackAt = placement.at;
1305
+ return next;
1306
+ }
1307
+ next.offloaded += 1;
1308
+ next.offloadedMs += placement.buildMs ?? 0;
1309
+ if (placement.localEstimateMs && placement.buildMs) next.savedMs += placement.localEstimateMs - placement.buildMs;
1310
+ next.lastOffloadAt = placement.at;
1311
+ return next;
1312
+ }
1313
+ function trimPlacements(placements, now) {
1314
+ return placements.filter((each) => now - Date.parse(each.at) <= PLACEMENT_MAX_AGE_MS).slice(-100);
1315
+ }
1316
+ function recordRunStats(run, now) {
1317
+ return withConfigLock$1(() => {
1318
+ const path = statsFile();
1319
+ const loaded = loadForUpdate(path, now);
1320
+ if (loaded.newerVersion !== null) return {
1321
+ recorded: false,
1322
+ note: `Run statistics at ${path} are version ${loaded.newerVersion}, which this Stim does not understand, so this run was not recorded.`
1323
+ };
1324
+ writeStats(path, updateStats(loaded.record, run, now));
1325
+ return {
1326
+ recorded: true,
1327
+ note: loaded.note
1328
+ };
1329
+ });
1330
+ }
1331
+ function recordCapacityRefusal(event, now) {
1332
+ try {
1333
+ withConfigLock$1(() => {
1334
+ const path = statsFile();
1335
+ const loaded = loadForUpdate(path, now, false);
1336
+ if (!loaded.record || loaded.newerVersion !== null) return;
1337
+ const capacityRefusals = trimCapacityRefusals([...loaded.record.capacityRefusals ?? [], {
1338
+ ...event,
1339
+ at: new Date(now).toISOString(),
1340
+ kind: "device"
1341
+ }], now);
1342
+ writeStats(path, {
1343
+ ...loaded.record,
1344
+ capacityRefusals
1345
+ });
1346
+ });
1347
+ } catch {}
1348
+ }
1349
+ function recordCapacityWait(event, now) {
1350
+ try {
1351
+ withConfigLock$1(() => {
1352
+ const path = statsFile();
1353
+ const loaded = loadForUpdate(path, now, false);
1354
+ if (!loaded.record || loaded.newerVersion !== null) return;
1355
+ const capacityWaits = trimCapacityWaits([...loaded.record.capacityWaits ?? [], {
1356
+ platform: event.platform,
1357
+ max: event.max,
1358
+ workspace: event.workspace,
1359
+ ms: wholeMs$1(event.ms),
1360
+ ...event.reclaimed && Number.isInteger(event.reclaimed) && event.reclaimed > 0 ? { reclaimed: event.reclaimed } : {},
1361
+ at: new Date(now).toISOString(),
1362
+ kind: "device-wait"
1363
+ }], now);
1364
+ writeStats(path, {
1365
+ ...loaded.record,
1366
+ capacityWaits
1367
+ });
1368
+ });
1369
+ } catch {}
1370
+ }
1371
+ function createRunRecorder({ platform, write, now, note, phases, deviceSetup }) {
1372
+ let projectKey = null;
1373
+ let cacheKey = null;
1374
+ let coldBuildMs = 0;
1375
+ let podsMs = 0;
1376
+ let placement = null;
1377
+ let deviceSlotWaitMs = 0;
1378
+ let recorded = false;
1379
+ return {
1380
+ setProject(key) {
1381
+ projectKey = key;
1382
+ },
1383
+ setCacheKey(key) {
1384
+ cacheKey = key;
1385
+ },
1386
+ setBuildMs(ms) {
1387
+ coldBuildMs = wholeMs$1(ms);
1388
+ },
1389
+ setPodsMs(ms) {
1390
+ podsMs = wholeMs$1(ms);
1391
+ },
1392
+ addDeviceSlotWaitMs(ms) {
1393
+ deviceSlotWaitMs += wholeMs$1(ms);
1394
+ },
1395
+ deviceSlotWaitMs() {
1396
+ return deviceSlotWaitMs || void 0;
1397
+ },
1398
+ setPlacement(next) {
1399
+ placement = next;
1400
+ },
1401
+ record({ failed, cacheHit = false, waited = null, durationMs, offloadedTo = null }) {
1402
+ if (!projectKey || !cacheKey || recorded) return;
1403
+ recorded = true;
1404
+ const ran = failed ? {} : phases?.() ?? {};
1405
+ try {
1406
+ const outcome = write({
1407
+ platform,
1408
+ projectKey,
1409
+ failed,
1410
+ cacheHit,
1411
+ waitedForBuild: Boolean(waited),
1412
+ durationMs,
1413
+ ...offloadedTo ? { offloadedTo } : {},
1414
+ ...coldBuildMs > 0 ? { coldBuildMs } : {},
1415
+ ...podsMs > 0 ? { podsMs } : {},
1416
+ ...Object.keys(ran).length ? { phases: ran } : {},
1417
+ ...failed || deviceSetup?.() === void 0 ? {} : { deviceSetup: deviceSetup() },
1418
+ ...placement ? { placement: {
1419
+ ...placement,
1420
+ ...deviceSlotWaitMs > 0 ? { deviceSlotWaitMs } : {}
1421
+ } } : {}
1422
+ }, now());
1423
+ if (outcome?.note) note(outcome.note);
1424
+ } catch (error) {
1425
+ note(`Run statistics could not be recorded: ${error?.message || error}`);
1426
+ }
1427
+ }
1428
+ };
1429
+ }
1430
+ function loadForUpdate(path, now, recoverCorrupt = true) {
1431
+ let text;
1432
+ try {
1433
+ text = readFileSync(path, "utf-8");
1434
+ } catch {
1435
+ return {
1436
+ record: emptyStats(),
1437
+ note: null,
1438
+ newerVersion: null
1439
+ };
1440
+ }
1441
+ const parsed = decodeStats(text);
1442
+ if (!parsed.record && parsed.newerVersion === null) {
1443
+ if (!recoverCorrupt) return {
1444
+ record: null,
1445
+ note: null,
1446
+ newerVersion: null
1447
+ };
1448
+ const aside = `${path}.corrupt-${now}`;
1449
+ renameSync(path, aside);
1450
+ return {
1451
+ record: emptyStats(),
1452
+ note: `Run statistics at ${path} could not be read, so they were moved to ${aside} and a new file was started.`,
1453
+ newerVersion: null
1454
+ };
1455
+ }
1456
+ if (parsed.newerVersion !== null) return {
1457
+ record: emptyStats(),
1458
+ note: null,
1459
+ newerVersion: parsed.newerVersion
1460
+ };
1461
+ return {
1462
+ record: parsed.record,
1463
+ note: null,
1464
+ newerVersion: null
1465
+ };
1466
+ }
1467
+ function writeStats(path, record) {
1468
+ const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
1469
+ writeFileSync(tmp, `${JSON.stringify(record)}\n`);
1470
+ try {
1471
+ renameSync(tmp, path);
1472
+ } catch (error) {
1473
+ rmSync(tmp, { force: true });
1474
+ throw error;
1475
+ }
1476
+ }
1477
+ function applyRun(bucket, run, { at, durationMs, credit, phases }) {
1478
+ const next = bucket ? { ...bucket } : {
1479
+ runs: 0,
1480
+ failed: 0,
1481
+ hits: 0,
1482
+ misses: 0,
1483
+ coldRuns: 0,
1484
+ coldRunMs: 0,
1485
+ hitRuns: 0,
1486
+ hitRunMs: 0,
1487
+ timeSavedMs: 0,
1488
+ firstRunAt: at,
1489
+ lastRunAt: at
1490
+ };
1491
+ next.runs += 1;
1492
+ next.lastRunAt = at;
1493
+ if (phases.coldBuildMs > 0) next.lastColdBuildMs = phases.coldBuildMs;
1494
+ if (phases.podsMs > 0) next.lastPodsMs = phases.podsMs;
1495
+ if (run.failed) {
1496
+ next.failed += 1;
1497
+ return next;
1498
+ }
1499
+ if (isHit(run.cacheHit)) {
1500
+ next.hits += 1;
1501
+ if (!run.waitedForBuild) {
1502
+ next.hitRuns += 1;
1503
+ next.hitRunMs += durationMs;
1504
+ next.timeSavedMs += credit;
1505
+ }
1506
+ return next;
1507
+ }
1508
+ next.misses += 1;
1509
+ if (run.offloadedTo) {
1510
+ next.offloadedRuns = (next.offloadedRuns ?? 0) + 1;
1511
+ next.offloadedRunMs = (next.offloadedRunMs ?? 0) + durationMs;
1512
+ next.lastOffloadHost = run.offloadedTo;
1513
+ return next;
1514
+ }
1515
+ next.coldRuns += 1;
1516
+ next.coldRunMs += durationMs;
1517
+ return next;
1518
+ }
1519
+ function creditMs(bucket, run, durationMs) {
1520
+ if (run.failed || !isHit(run.cacheHit) || run.waitedForBuild) return 0;
1521
+ if (!bucket || bucket.coldRuns <= 0) return 0;
1522
+ return Math.max(0, Math.round(bucket.coldRunMs / bucket.coldRuns) - durationMs);
1523
+ }
1524
+ function isHit(cacheHit) {
1525
+ return cacheHit === "local" || cacheHit === "remote";
1526
+ }
1527
+ function wholeMs$1(value) {
1528
+ const ms = Number(value);
1529
+ return Number.isFinite(ms) && ms > 0 ? Math.round(ms) : 0;
1530
+ }
1531
+ //#endregion
1532
+ //#region src/engine/build-detail.ts
1533
+ const LINE_MAX = 160;
1534
+ const TARGET_GRAPH = /^note: Target dependency graph \((\d+) targets?\)/;
1535
+ const XCODE_ACTION = /^([A-Z][A-Za-z]+)\s(.*)\s\(in target '([^']+)' from project '([^']+)'\)$/;
1536
+ const XCODE_CONFIGURE = /^(ComputeTargetDependencyGraph|CreateBuildDescription|CreateBuildRequest|Resolve Package Graph|Prepare packages)$/;
1537
+ const GRADLE_TASK = /^> Task (:\S+)/;
1538
+ /**
1539
+ * xcodebuild actions that end a target's work: a clean build touches each product last, and an incremental build
1540
+ * that touches nothing still signs framework products last. Targets that end any other way are not counted.
1541
+ */
1542
+ const XCODE_TARGET_DONE = /^(Touch|RegisterExecutionPolicyException|CodeSign)$/;
1543
+ const GRADLE_CONFIGURE = /^> Configure project /;
1544
+ /**
1545
+ * SwiftPM prints `[done / total] name` once it plans the build (with U+2009 thin spaces around the slash in Swift 6.4
1546
+ * output), `[done/total] Compiling ...` in older toolchains; Swift 6.4 names the target being worked on, and
1547
+ * `<product>-product` is the link of a product.
1548
+ */
1549
+ const SWIFTPM_PROGRESS = /^\[(\d+)\s*\/\s*(\d+)\](?:\s+(.*))?$/;
1550
+ const SWIFTPM_CONFIGURE = /^(Fetching|Fetched|Computing version|Computed|Creating working copy|Working copy of)\b|^\[(Pre-planning|Planning|Using on-disk description)/;
1551
+ const XCODE_STEPS = [
1552
+ [/^(CpResource|CopyPNGFile|CopyStringsFile|CompileAssetCatalog\w*|CompileStoryboard|CompileXIB|LinkStoryboards|ProcessInfoPlistFile)$/, "resources"],
1553
+ [/^(Compile\w*|Swift\w*|ScanDependencies|Precompile\w*)$/, "compile"],
1554
+ [/^(Ld|Libtool|CreateUniversalBinary|GenerateDSYMFile)$/, "link"],
1555
+ [/^PhaseScriptExecution$/, "script"],
1556
+ [/^CodeSign$/, "sign"]
1557
+ ];
1558
+ const GRADLE_STEPS = [
1559
+ [/[Dd]ex/, "dex"],
1560
+ [/^(createBundle\w*JsAndAssets|generateCodegen\w*)$/, "script"],
1561
+ [/(Resources?|Assets|Manifest|ResValues|RFile|JavaRes)$/, "resources"],
1562
+ [/^(compile|kapt|ksp|javaPreCompile|buildCMake|configureCMake|externalNativeBuild)/, "compile"],
1563
+ [/^(package|assemble|install|zipalign|validateSigning|merge\w*NativeLibs|strip\w*Symbols)/, "package"]
1564
+ ];
1565
+ function stepOf(name, table) {
1566
+ return table.find(([pattern]) => pattern.test(name))?.[1] ?? null;
1567
+ }
1568
+ function clip(text) {
1569
+ return text.length > LINE_MAX ? `${text.slice(0, 157)}...` : text;
1570
+ }
1571
+ function unescape(token) {
1572
+ return token.replace(/\\(.)/g, "$1");
1573
+ }
1574
+ function baseName(path) {
1575
+ return path.split("/").findLast(Boolean) ?? path;
1576
+ }
1577
+ /**
1578
+ * An xcodebuild action line in short form: the action, the file it works on and its target, as in
1579
+ * `CompileC sqlite3.c (sqlite3)`. The file is the last absolute path in the arguments, which xcodebuild prints
1580
+ * as the source for compile, copy and plist steps and as the product for link and sign steps; a script phase
1581
+ * shows its name instead.
1582
+ */
1583
+ function shortXcodeAction(action, args, target) {
1584
+ const tokens = args.match(/(?:\\.|\S)+/g) ?? [];
1585
+ const subject = action === "PhaseScriptExecution" ? unescape(tokens[0] ?? "") : baseName(unescape(tokens.findLast((token) => token.startsWith("/")) ?? ""));
1586
+ return clip(`${action}${subject ? ` ${subject}` : ""} (${target})`);
1587
+ }
1588
+ function swiftpmStep(text) {
1589
+ return /^Linking\b|-product$/.test(text) ? "link" : "compile";
1590
+ }
1591
+ /** What one line of xcodebuild, Gradle or SwiftPM output says about the build's progress, or null for any other line. */
1592
+ function parseBuildToolLine(msg) {
1593
+ const counted = SWIFTPM_PROGRESS.exec(msg);
1594
+ if (counted) {
1595
+ const text = counted[3]?.trim() ?? "";
1596
+ return {
1597
+ kind: "count",
1598
+ done: Number(counted[1]),
1599
+ total: Number(counted[2]),
1600
+ step: swiftpmStep(text),
1601
+ line: text ? clip(text) : null
1602
+ };
1603
+ }
1604
+ if (SWIFTPM_CONFIGURE.test(msg)) return { kind: "configure" };
1605
+ const graph = TARGET_GRAPH.exec(msg);
1606
+ if (graph) return {
1607
+ kind: "total",
1608
+ unit: "targets",
1609
+ total: Number(graph[1])
1610
+ };
1611
+ if (XCODE_CONFIGURE.test(msg) || GRADLE_CONFIGURE.test(msg)) return { kind: "configure" };
1612
+ const action = XCODE_ACTION.exec(msg);
1613
+ if (action) {
1614
+ const [, name, args, target, project] = action;
1615
+ const step = stepOf(name, XCODE_STEPS);
1616
+ return {
1617
+ kind: "unit",
1618
+ unit: "targets",
1619
+ id: `${project}/${target}`,
1620
+ step,
1621
+ line: step ? shortXcodeAction(name, args, target) : null,
1622
+ finished: XCODE_TARGET_DONE.test(name)
1623
+ };
1624
+ }
1625
+ const task = GRADLE_TASK.exec(msg);
1626
+ if (task) {
1627
+ const path = task[1];
1628
+ return {
1629
+ kind: "unit",
1630
+ unit: "tasks",
1631
+ id: path,
1632
+ step: stepOf(path.slice(path.lastIndexOf(":") + 1), GRADLE_STEPS),
1633
+ line: clip(`> Task ${path}`),
1634
+ finished: false
1635
+ };
1636
+ }
1637
+ return null;
1638
+ }
1639
+ /**
1640
+ * Accumulates a build's tool output into its detail: xcodebuild counts the distinct targets it finished against the
1641
+ * total its dependency graph note gives; Gradle counts the task lines it printed and has no total; SwiftPM reports
1642
+ * its own `[done / total]` counts, which the parser passes through.
1643
+ */
1644
+ function createBuildDetailParser() {
1645
+ let seen = false;
1646
+ let step = null;
1647
+ let unit = null;
1648
+ let total = null;
1649
+ let line = null;
1650
+ let tasks = 0;
1651
+ let counted = null;
1652
+ const targets = /* @__PURE__ */ new Set();
1653
+ return {
1654
+ push(msg) {
1655
+ const parsed = parseBuildToolLine(msg);
1656
+ if (!parsed) return false;
1657
+ seen = true;
1658
+ if (parsed.kind === "total") {
1659
+ unit = "targets";
1660
+ total = parsed.total;
1661
+ return true;
1662
+ }
1663
+ if (parsed.kind === "configure") {
1664
+ step = "configure";
1665
+ return true;
1666
+ }
1667
+ if (parsed.kind === "count") {
1668
+ unit = "steps";
1669
+ counted = {
1670
+ done: parsed.done,
1671
+ total: parsed.total
1672
+ };
1673
+ step = parsed.step;
1674
+ if (parsed.line) line = parsed.line;
1675
+ return true;
1676
+ }
1677
+ unit = parsed.unit;
1678
+ if (parsed.unit === "tasks") tasks += 1;
1679
+ else if (parsed.finished) targets.add(parsed.id);
1680
+ if (parsed.step) step = parsed.step;
1681
+ if (parsed.line) line = parsed.line;
1682
+ return true;
1683
+ },
1684
+ detail(updatedAt) {
1685
+ if (!seen) return null;
1686
+ if (unit === "steps" && counted) return {
1687
+ step,
1688
+ unit,
1689
+ done: Math.min(counted.done, counted.total),
1690
+ total: counted.total,
1691
+ line,
1692
+ updatedAt
1693
+ };
1694
+ const done = unit === "tasks" ? tasks : unit === "targets" ? targets.size : null;
1695
+ return {
1696
+ step,
1697
+ unit,
1698
+ done: done !== null && total !== null ? Math.min(done, total) : done,
1699
+ total: unit === "tasks" ? null : total,
1700
+ line,
1701
+ updatedAt
1702
+ };
1703
+ }
1704
+ };
1705
+ }
1706
+ //#endregion
1707
+ //#region src/stim-version.ts
1708
+ const STIM_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
1709
+ //#endregion
1710
+ //#region src/engine/build-ledger.ts
1711
+ const LOCK_WAIT_MS = 2e3;
1712
+ const GIT_TIMEOUT_MS = 2e3;
1713
+ function text(value) {
1714
+ return typeof value === "string" && value !== "" ? value : null;
1715
+ }
1716
+ function wholeMs(value) {
1717
+ const ms = Number(value);
1718
+ return Number.isFinite(ms) && ms > 0 ? Math.round(ms) : 0;
1719
+ }
1720
+ function wholePhaseMs(phases) {
1721
+ const out = {};
1722
+ for (const [name, ms] of Object.entries(phases ?? {})) if (wholeMs(ms) > 0) out[name] = wholeMs(ms);
1723
+ return out;
1724
+ }
1725
+ /** The remedy a thrown error carries, when it carries one. */
1726
+ function errorRemedy(error) {
1727
+ const remedy = typeof error === "object" && error !== null && "remedy" in error ? error.remedy : void 0;
1728
+ return typeof remedy === "string" && remedy !== "" ? remedy : void 0;
1729
+ }
1730
+ function buildLedgerRecord(run) {
1731
+ const { record, extras, outcome } = run;
1732
+ const platform = record.platform === "ios" || record.platform === "android" ? record.platform : "macos";
1733
+ const configuration = text(record.configuration);
1734
+ const durationMs = wholeMs(record.durationMs);
1735
+ const startedAt = Date.parse(String(record.startedAt));
1736
+ const fallback = text(record.offloadFallback);
1737
+ const ranHere = fallback !== null || record.builtOn === "here";
1738
+ const machine = text(record.offloadedTo) ?? (ranHere ? null : run.offload?.host) ?? null;
1739
+ const placement = machine ? {
1740
+ where: "offloaded",
1741
+ machine
1742
+ } : fallback ? {
1743
+ where: "local",
1744
+ fellBack: true,
1745
+ reason: fallback
1746
+ } : { where: "local" };
1747
+ const hit = record.cacheHit === "local" || record.cacheHit === "remote" ? record.cacheHit : null;
1748
+ const missReason = text(record.missReason);
1749
+ const cache = {
1750
+ result: record.cacheSkipped === true ? "bypassed" : hit ? "hit" : "miss",
1751
+ ...hit && record.cacheSkipped !== true ? { source: hit } : {},
1752
+ key: text(record.cacheKey),
1753
+ fingerprint: text(record.fingerprint),
1754
+ ...missReason && !hit ? { missReason } : {}
1755
+ };
1756
+ const offloadPhases = wholePhaseMs(run.offload?.phaseMs);
1757
+ const scheme = text(extras.scheme);
1758
+ const device = text(extras.device?.name);
1759
+ const runtime = text(extras.device?.runtime);
1760
+ const failure = outcome === "succeeded" ? void 0 : {
1761
+ code: text(record.errorCode),
1762
+ message: text(extras.message),
1763
+ remedy: text(extras.remedy),
1764
+ phase: run.phase
1765
+ };
1766
+ const where = placement.machine ? `on ${placement.machine}` : "here";
1767
+ const cacheText = cache.result === "hit" ? `cache hit${cache.source === "remote" ? " (remote)" : ""}` : cache.result;
1768
+ const summary = `${platform} build ${outcome} ${where} in ${formatElapsed(durationMs)} (${cacheText})`;
1769
+ const why = failure ? `: ${[failure.code, failure.message].filter(Boolean).join(": ") || "no reason recorded"}` : "";
1770
+ return {
1771
+ ts: run.now,
1772
+ src: "builds",
1773
+ level: outcome === "succeeded" ? "info" : outcome === "cancelled" ? "warn" : "error",
1774
+ msg: `${summary}${why}`,
1775
+ event: "build",
1776
+ id: run.id,
1777
+ startedAt: Number.isFinite(startedAt) ? startedAt : run.now - durationMs,
1778
+ project: run.root,
1779
+ workspace: run.workspace,
1780
+ branch: run.branch,
1781
+ platform,
1782
+ ...scheme ? { scheme } : {},
1783
+ ...platform === "android" && configuration ? { variant: configuration } : {},
1784
+ configuration,
1785
+ placement,
1786
+ cache,
1787
+ durationMs,
1788
+ phases: wholePhaseMs(run.phases),
1789
+ ...Object.keys(offloadPhases).length ? { offloadPhases } : {},
1790
+ outcome,
1791
+ ...failure ? { failure } : {},
1792
+ ...device ? { device: {
1793
+ name: device,
1794
+ ...runtime ? { runtime } : {}
1795
+ } } : {},
1796
+ stimVersion: run.version
1797
+ };
1798
+ }
1799
+ function lineId(line) {
1800
+ try {
1801
+ const parsed = JSON.parse(line);
1802
+ return typeof parsed === "object" && parsed !== null && "id" in parsed ? parsed.id : void 0;
1803
+ } catch {
1804
+ return;
1805
+ }
1806
+ }
1807
+ /**
1808
+ * The ledger file's text after appending `line`, keeping only the newest `cap` complete records. A run that records
1809
+ * twice (an Android launch that fails after the build was recorded) replaces its earlier record.
1810
+ */
1811
+ function appendCapped(existing, line, cap = BUILD_LEDGER_CAP) {
1812
+ const id = lineId(line);
1813
+ const lines = existing.split("\n").filter((each) => each.trim() !== "" && (id === void 0 || lineId(each) !== id));
1814
+ lines.push(line);
1815
+ return `${lines.slice(-cap).join("\n")}\n`;
1816
+ }
1817
+ function appendBuildLedger(record, file = buildLedgerFile()) {
1818
+ withDirLock(buildLedgerLock(), () => {
1819
+ let existing = "";
1820
+ try {
1821
+ existing = readFileSync(file, "utf-8");
1822
+ } catch {}
1823
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
1824
+ writeFileSync(tmp, appendCapped(existing, JSON.stringify(record)));
1825
+ try {
1826
+ renameSync(tmp, file);
1827
+ } catch (error) {
1828
+ rmSync(tmp, { force: true });
1829
+ throw error;
1830
+ }
1831
+ }, {
1832
+ waitMs: LOCK_WAIT_MS,
1833
+ ensureParent: () => {
1834
+ mkdirSync(dirname(file), { recursive: true });
1835
+ mkdirSync(dirname(buildLedgerLock()), { recursive: true });
1836
+ }
1837
+ });
1838
+ }
1839
+ /** Records one finished build in the ledger. A failure to do so never changes the run, so it is ignored. */
1840
+ function writeBuildLedger(root, run) {
1841
+ try {
1842
+ const branch = gitQuiet(root, [
1843
+ "symbolic-ref",
1844
+ "--quiet",
1845
+ "--short",
1846
+ "HEAD"
1847
+ ], { timeoutMs: GIT_TIMEOUT_MS })?.trim() || null;
1848
+ appendBuildLedger(buildLedgerRecord({
1849
+ ...run,
1850
+ id: run.id ?? randomUUID(),
1851
+ root,
1852
+ workspace: workspaceName(root),
1853
+ branch,
1854
+ version: STIM_VERSION
1855
+ }));
1856
+ } catch {}
1857
+ }
1858
+ //#endregion
1859
+ //#region src/engine/build-progress.ts
1860
+ const ACTIVE_BUILD_KEY = "activeBuild";
1861
+ const NO_BUILD_PROGRESS = {
1862
+ step: () => {},
1863
+ activity: () => {},
1864
+ estimate: () => {},
1865
+ miss: () => {},
1866
+ hit: () => {},
1867
+ deviceSetup: () => {},
1868
+ deviceSetupKnown: () => void 0,
1869
+ place: () => {},
1870
+ waitingOn: () => {},
1871
+ waitingFor: () => {},
1872
+ output: () => {},
1873
+ durations: () => ({}),
1874
+ phase: () => null,
1875
+ steps: () => null,
1876
+ clear: () => {}
1877
+ };
1878
+ /** `writer`, with each record it writes also passed to `progress.output`. */
1879
+ function tapBuildLog(writer, progress) {
1880
+ if (progress === NO_BUILD_PROGRESS) return writer;
1881
+ return {
1882
+ get file() {
1883
+ return writer.file;
1884
+ },
1885
+ write(record) {
1886
+ progress.output(record);
1887
+ return writer.write(record);
1888
+ },
1889
+ close: () => writer.close(),
1890
+ get written() {
1891
+ return writer.written;
1892
+ },
1893
+ get dropped() {
1894
+ return writer.dropped;
1895
+ },
1896
+ get lastError() {
1897
+ return writer.lastError;
1898
+ }
1899
+ };
1900
+ }
1901
+ function runFailureRecord({ code, message, phase }) {
1902
+ return {
1903
+ src: "build",
1904
+ level: "error",
1905
+ event: "run_failed",
1906
+ ...code ? { code } : {},
1907
+ ...phase ? { phase } : {},
1908
+ msg: [code, message].filter(Boolean).join(": ") || "the run failed"
1909
+ };
1910
+ }
1911
+ function thrownRunFailureRecord(error, phase) {
1912
+ return runFailureRecord({
1913
+ code: typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0,
1914
+ message: error instanceof Error ? error.message : String(error),
1915
+ phase
1916
+ });
1917
+ }
1918
+ const DETAIL_WRITE_MS = 2e3;
1919
+ function toolLine(record) {
1920
+ if (!record || typeof record !== "object") return null;
1921
+ const { src, level, msg } = record;
1922
+ return src === "build" && level === "debug" && typeof msg === "string" ? msg : null;
1923
+ }
1924
+ function writeBuildDetail(root, claimId, detail) {
1925
+ const file = workspaceBuildDetailFile(root);
1926
+ const tmp = `${file}.tmp-${process.pid}`;
1927
+ mkdirSync(dirname(file), { recursive: true });
1928
+ writeFileSync(tmp, JSON.stringify({
1929
+ claimId,
1930
+ detail
1931
+ }));
1932
+ renameSync(tmp, file);
1933
+ }
1934
+ const COLD_PHASES = [
1935
+ "prebuild",
1936
+ "pods",
1937
+ "compile"
1938
+ ];
1939
+ const HIT_PHASES = ["install", "launch"];
1940
+ function settledOutcome(phase, entered) {
1941
+ if (COLD_PHASES.includes(phase)) return "cold";
1942
+ if (phase === "device") return entered.some((entry) => entry.phase === "cache-lookup") ? "hit" : null;
1943
+ return HIT_PHASES.includes(phase) ? "hit" : null;
1944
+ }
1945
+ function projectHistory(projectKey) {
1946
+ try {
1947
+ return readStats$1().record?.history?.[projectKey];
1948
+ } catch {
1949
+ return;
1950
+ }
1951
+ }
1952
+ function startBuildProgress({ root, platform, slot, claim, now = Date.now, note = () => {} }) {
1953
+ const startedAt = new Date(now()).toISOString();
1954
+ const record = {
1955
+ platform,
1956
+ slot,
1957
+ startedAt,
1958
+ phase: "prepare",
1959
+ phaseStartedAt: startedAt,
1960
+ phases: [{
1961
+ phase: "prepare",
1962
+ startedAt
1963
+ }],
1964
+ claim: {
1965
+ root: claim.root,
1966
+ path: claim.path,
1967
+ claimId: claim.claimId,
1968
+ pid: claim.owner.pid
1969
+ }
1970
+ };
1971
+ let warned = false;
1972
+ const guard = (fn) => {
1973
+ try {
1974
+ fn();
1975
+ } catch (error) {
1976
+ if (warned) return;
1977
+ warned = true;
1978
+ note(`Build progress could not be recorded in the workspace state: ${error?.message || error}`);
1979
+ }
1980
+ };
1981
+ const write = (before = (state) => state) => guard(() => updateWorkspaceState(root, (state) => ({
1982
+ ...before(state),
1983
+ [ACTIVE_BUILD_KEY]: record
1984
+ })));
1985
+ write(withInterruptedBuild);
1986
+ const phases = trace.phases(`${platform} build`);
1987
+ phases.enter("prepare");
1988
+ let projectKey = null;
1989
+ const parser = createBuildDetailParser();
1990
+ let detailWrittenAt = -Infinity;
1991
+ let detailTimer = null;
1992
+ const flushDetail = () => {
1993
+ detailTimer = null;
1994
+ detailWrittenAt = now();
1995
+ const detail = parser.detail(new Date(detailWrittenAt).toISOString());
1996
+ if (detail) guard(() => writeBuildDetail(root, record.claim.claimId, detail));
1997
+ };
1998
+ const settle = (outcome) => {
1999
+ record.outcome = outcome;
2000
+ if (projectKey && platform !== "macos") record.estimate = estimateBuild(projectHistory(projectKey), platform, outcome, record.deviceSetup);
2001
+ };
2002
+ const waits = /* @__PURE__ */ new Map();
2003
+ return {
2004
+ step(phase) {
2005
+ if (phase === record.phase) return;
2006
+ const at = new Date(now()).toISOString();
2007
+ record.phase = phase;
2008
+ record.phaseStartedAt = at;
2009
+ record.phases.push({
2010
+ phase,
2011
+ startedAt: at
2012
+ });
2013
+ phases.enter(phase);
2014
+ if (phase !== "wait") delete record.waitingOn;
2015
+ delete record.activity;
2016
+ const outcome = record.outcome || platform === "macos" ? null : settledOutcome(phase, record.phases);
2017
+ if (outcome) settle(outcome);
2018
+ write();
2019
+ },
2020
+ activity(name, at) {
2021
+ if ((record.activity?.name ?? null) === name) return;
2022
+ if (name) record.activity = {
2023
+ name,
2024
+ startedAt: new Date(at ?? now()).toISOString()
2025
+ };
2026
+ else delete record.activity;
2027
+ write();
2028
+ },
2029
+ estimate(key) {
2030
+ if (platform === "macos") return;
2031
+ projectKey = key;
2032
+ record.estimate = estimateBuild(projectHistory(key), platform, record.outcome ?? null, record.outcome ? record.deviceSetup : void 0);
2033
+ write();
2034
+ },
2035
+ miss(reason, provisional = false) {
2036
+ record.missReason = reason;
2037
+ settle("cold");
2038
+ if (provisional) record.missProvisional = true;
2039
+ else delete record.missProvisional;
2040
+ write();
2041
+ },
2042
+ hit() {
2043
+ delete record.missReason;
2044
+ delete record.missProvisional;
2045
+ settle("hit");
2046
+ write();
2047
+ },
2048
+ deviceSetup(setup) {
2049
+ if (setup === void 0) return;
2050
+ record.deviceSetup = setup;
2051
+ write();
2052
+ },
2053
+ deviceSetupKnown() {
2054
+ return record.deviceSetup;
2055
+ },
2056
+ place(remote) {
2057
+ const current = record.placement;
2058
+ if (remote && current?.host === remote.host && current.phase === remote.phase) return;
2059
+ if (current) record.offload = withOffloadPhase(record.offload, current, now());
2060
+ if (!remote) {
2061
+ if (!current) return;
2062
+ delete record.placement;
2063
+ return write();
2064
+ }
2065
+ const at = new Date(now()).toISOString();
2066
+ record.placement = {
2067
+ host: remote.host,
2068
+ phase: remote.phase,
2069
+ startedAt: current?.host === remote.host ? current.startedAt : at,
2070
+ phaseStartedAt: at
2071
+ };
2072
+ write();
2073
+ },
2074
+ waitingFor(info, kind) {
2075
+ if (info) waits.set(info.kind, info);
2076
+ else if (kind) waits.delete(kind);
2077
+ else waits.clear();
2078
+ const first = [...waits.values()].toSorted((a, b) => Date.parse(a.since) - Date.parse(b.since))[0];
2079
+ if (first) record.waitingFor = first;
2080
+ else delete record.waitingFor;
2081
+ write();
2082
+ },
2083
+ waitingOn(holder) {
2084
+ if (holder === null) {
2085
+ if (!record.waitingOn) return;
2086
+ delete record.waitingOn;
2087
+ } else {
2088
+ if (record.waitingOn?.path === holder) return;
2089
+ record.waitingOn = { path: holder };
2090
+ }
2091
+ write();
2092
+ },
2093
+ output(line) {
2094
+ const msg = toolLine(line);
2095
+ if (msg === null || !parser.push(msg) || detailTimer) return;
2096
+ const wait = detailWrittenAt + DETAIL_WRITE_MS - now();
2097
+ if (wait <= 0) return flushDetail();
2098
+ detailTimer = setTimeout(flushDetail, wait);
2099
+ detailTimer.unref?.();
2100
+ },
2101
+ durations() {
2102
+ return phaseDurations(record.phases, now());
2103
+ },
2104
+ phase() {
2105
+ return record.phase;
2106
+ },
2107
+ steps() {
2108
+ const detail = parser.detail("");
2109
+ return detail?.unit === "steps" ? detail.total : null;
2110
+ },
2111
+ clear() {
2112
+ phases.end();
2113
+ if (detailTimer) clearTimeout(detailTimer);
2114
+ detailTimer = null;
2115
+ guard(() => rmSync(workspaceBuildDetailFile(root), { force: true }));
2116
+ guard(() => {
2117
+ clearWorkspaceStateKey(root, ACTIVE_BUILD_KEY, (value) => parseActiveBuild(value)?.claim.claimId === record.claim.claimId);
2118
+ });
2119
+ }
2120
+ };
2121
+ }
2122
+ /** `offload` with the time `placement` has spent in its current phase as of `now` added to that phase. */
2123
+ function withOffloadPhase(offload, placement, now) {
2124
+ const phaseMs = { ...offload?.phaseMs };
2125
+ const ms = Math.max(0, now - Date.parse(placement.phaseStartedAt));
2126
+ if (Number.isFinite(ms)) phaseMs[placement.phase] = (phaseMs[placement.phase] ?? 0) + ms;
2127
+ return {
2128
+ host: placement.host,
2129
+ phaseMs
2130
+ };
2131
+ }
2132
+ function phaseDurations(phases, now) {
2133
+ const out = {};
2134
+ phases.forEach(({ phase, startedAt }, index) => {
2135
+ const start = Date.parse(startedAt);
2136
+ const next = phases[index + 1];
2137
+ const end = next ? Date.parse(next.startedAt) : now;
2138
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return;
2139
+ out[phase] = (out[phase] ?? 0) + Math.max(0, end - start);
2140
+ });
2141
+ return out;
2142
+ }
2143
+ function completedPhaseDurations(phases) {
2144
+ if (phases.length < 2) return void 0;
2145
+ const durations = phaseDurations(phases.slice(0, -1), Date.parse(phases.at(-1).startedAt));
2146
+ return Object.keys(durations).length ? durations : void 0;
2147
+ }
2148
+ const HISTORY_FIELDS = [
2149
+ "devicePlacement",
2150
+ "platform",
2151
+ "status",
2152
+ "configuration",
2153
+ "fingerprint",
2154
+ "cacheKey",
2155
+ "cacheHit",
2156
+ "cacheSkipped",
2157
+ "durationMs",
2158
+ "startedAt",
2159
+ "errorCode",
2160
+ "missReason",
2161
+ "buildMachine",
2162
+ "builtOn",
2163
+ "offloadedTo",
2164
+ "offloadFallback",
2165
+ "diagnostics",
2166
+ "compileSteps"
2167
+ ];
2168
+ function withHistoryEntry(state, platform, entry) {
2169
+ const saved = state[BUILD_HISTORY_KEY];
2170
+ const history = saved && typeof saved === "object" && !Array.isArray(saved) ? saved : {};
2171
+ const list = Array.isArray(history[platform]) ? history[platform] : [];
2172
+ return {
2173
+ ...state,
2174
+ [BUILD_HISTORY_KEY]: {
2175
+ ...history,
2176
+ [platform]: [entry, ...list].slice(0, BUILD_HISTORY_LIMIT)
2177
+ }
2178
+ };
2179
+ }
2180
+ function finishedResult(record) {
2181
+ if (record.status === "ok") return "succeeded";
2182
+ return record.errorCode === "STIM_CANCELLED" ? "cancelled" : "failed";
2183
+ }
2184
+ function recordFinishedBuild(root, recorded, { update = updateWorkspaceState, now = Date.now, artifactOnly = false, ledger = writeBuildLedger } = {}) {
2185
+ const { ledger: extras, ...record } = recorded;
2186
+ const ran = {};
2187
+ try {
2188
+ updateFinishedBuild(root, record, ran, {
2189
+ update,
2190
+ now,
2191
+ artifactOnly
2192
+ });
2193
+ } finally {
2194
+ ledger(root, {
2195
+ now: now(),
2196
+ outcome: finishedResult(record),
2197
+ record,
2198
+ extras: isJsonObject(extras) ? extras : {},
2199
+ phases: {},
2200
+ phase: null,
2201
+ ...ran
2202
+ });
2203
+ }
2204
+ }
2205
+ function updateFinishedBuild(root, record, ran, { update, now, artifactOnly }) {
2206
+ const platform = record.platform;
2207
+ update(root, (state) => {
2208
+ const active = parseActiveBuild(state[ACTIVE_BUILD_KEY]);
2209
+ const own = active?.platform === platform ? active : null;
2210
+ const phases = own ? phaseDurations(own.phases, now()) : {};
2211
+ if (own) {
2212
+ const offload = own.placement ? withOffloadPhase(own.offload, own.placement, now()) : own.offload;
2213
+ Object.assign(ran, {
2214
+ id: own.claim.claimId,
2215
+ phases,
2216
+ phase: own.phase,
2217
+ ...offload ? { offload } : {}
2218
+ });
2219
+ }
2220
+ const entry = {
2221
+ ...Object.fromEntries(HISTORY_FIELDS.filter((key) => key in record).map((key) => [key, record[key]])),
2222
+ result: finishedResult(record),
2223
+ slot: own?.slot ?? "default",
2224
+ phases
2225
+ };
2226
+ if (artifactOnly || platform === "macos") return withHistoryEntry(state, platform, entry);
2227
+ const slot = own?.slot ?? "default";
2228
+ const slots = isJsonObject(state.deviceSlots) ? state.deviceSlots : {};
2229
+ const savedSlot = isJsonObject(slots[slot]) ? slots[slot] : {};
2230
+ const savedDevice = slot === "default" ? state[platform] : savedSlot[platform];
2231
+ if (record.devicePlacement || isJsonObject(savedDevice)) {
2232
+ const device = isJsonObject(savedDevice) ? { ...savedDevice } : {};
2233
+ if (record.devicePlacement) device.devicePlacement = record.devicePlacement;
2234
+ else delete device.devicePlacement;
2235
+ state = slot === "default" ? {
2236
+ ...state,
2237
+ [platform]: device
2238
+ } : {
2239
+ ...state,
2240
+ deviceSlots: {
2241
+ ...slots,
2242
+ [slot]: {
2243
+ ...savedSlot,
2244
+ [platform]: device
2245
+ }
2246
+ }
2247
+ };
2248
+ }
2249
+ return withHistoryEntry({
2250
+ ...state,
2251
+ lastBuild: record,
2252
+ [LAST_BUILD_KEYS[platform]]: record
2253
+ }, platform, entry);
2254
+ });
2255
+ }
2256
+ /** Adds one phase's duration to the recorded run of `platform` that started at `startedAt`. */
2257
+ function recordBuildPhase(root, platform, startedAt, phase, ms) {
2258
+ updateWorkspaceState(root, (state) => {
2259
+ const saved = state[BUILD_HISTORY_KEY];
2260
+ const list = isJsonObject(saved) && Array.isArray(saved[platform]) ? saved[platform] : [];
2261
+ const index = list.findIndex((entry) => isJsonObject(entry) && entry.startedAt === startedAt);
2262
+ const entry = list[index];
2263
+ if (!isJsonObject(entry)) return state;
2264
+ const phases = isJsonObject(entry.phases) ? entry.phases : {};
2265
+ const next = [...list];
2266
+ next[index] = {
2267
+ ...entry,
2268
+ phases: {
2269
+ ...phases,
2270
+ [phase]: Math.round(ms)
2271
+ }
2272
+ };
2273
+ return {
2274
+ ...state,
2275
+ [BUILD_HISTORY_KEY]: {
2276
+ ...saved,
2277
+ [platform]: next
2278
+ }
2279
+ };
2280
+ });
2281
+ }
2282
+ function withInterruptedBuild(state) {
2283
+ const left = parseActiveBuild(state[ACTIVE_BUILD_KEY]);
2284
+ if (!left) return state;
2285
+ const last = state[BUILD_HISTORY_KEY]?.[left.platform]?.[0];
2286
+ if (Date.parse(String(last?.startedAt)) >= Date.parse(left.startedAt)) return state;
2287
+ return withHistoryEntry(state, left.platform, {
2288
+ platform: left.platform,
2289
+ status: "failed",
2290
+ result: "interrupted",
2291
+ slot: left.slot,
2292
+ configuration: null,
2293
+ fingerprint: null,
2294
+ cacheKey: null,
2295
+ cacheHit: false,
2296
+ cacheSkipped: false,
2297
+ durationMs: null,
2298
+ startedAt: left.startedAt,
2299
+ phases: {
2300
+ ...phaseDurations(left.phases, Date.parse(left.phaseStartedAt)),
2301
+ [left.phase]: 0
2302
+ }
2303
+ });
2304
+ }
2305
+ function parseActiveBuild(value) {
2306
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2307
+ const record = value;
2308
+ const claim = record.claim;
2309
+ if (record.platform !== "ios" && record.platform !== "android" && record.platform !== "macos") return null;
2310
+ if (!isPhase(record.phase)) return null;
2311
+ if (typeof record.startedAt !== "string" || typeof record.phaseStartedAt !== "string") return null;
2312
+ if (!claim || typeof claim.root !== "string" || typeof claim.claimId !== "string") return null;
2313
+ const phases = Array.isArray(record.phases) ? record.phases.filter((entry) => isPhase(entry?.phase) && typeof entry?.startedAt === "string") : [];
2314
+ const missReason = parseMissReason(record.missReason);
2315
+ const placement = parsePlacement(record.placement);
2316
+ const waitingOn = parseWaitingOn(record.waitingOn);
2317
+ const waitingFor = parseWaitingFor(record.waitingFor);
2318
+ const estimate = parseEstimate(record.estimate);
2319
+ const activity = parseActivity(record.activity);
2320
+ const offload = parseOffload(record.offload);
2321
+ return {
2322
+ platform: record.platform,
2323
+ slot: typeof record.slot === "string" ? record.slot : "default",
2324
+ startedAt: record.startedAt,
2325
+ phase: record.phase,
2326
+ phaseStartedAt: record.phaseStartedAt,
2327
+ phases,
2328
+ ...missReason ? { missReason } : {},
2329
+ ...missReason && record.missProvisional === true ? { missProvisional: true } : {},
2330
+ ...placement ? { placement } : {},
2331
+ ...offload ? { offload } : {},
2332
+ ...waitingOn ? { waitingOn } : {},
2333
+ ...waitingFor ? { waitingFor } : {},
2334
+ ...typeof record.deviceSetup === "boolean" ? { deviceSetup: record.deviceSetup } : {},
2335
+ ...record.outcome === "hit" || record.outcome === "cold" ? { outcome: record.outcome } : {},
2336
+ ...estimate ? { estimate } : {},
2337
+ ...activity ? { activity } : {},
2338
+ claim: {
2339
+ root: claim.root,
2340
+ path: typeof claim.path === "string" ? claim.path : "",
2341
+ claimId: claim.claimId,
2342
+ pid: typeof claim.pid === "number" ? claim.pid : 0
2343
+ }
2344
+ };
2345
+ }
2346
+ function parseOffload(value) {
2347
+ if (!isJsonObject(value) || typeof value.host !== "string" || !isJsonObject(value.phaseMs)) return null;
2348
+ const phaseMs = {};
2349
+ for (const [phase, ms] of Object.entries(value.phaseMs)) if (typeof ms === "number" && Number.isFinite(ms) && ms >= 0) phaseMs[phase] = ms;
2350
+ return {
2351
+ host: value.host,
2352
+ phaseMs
2353
+ };
2354
+ }
2355
+ function parsePlacement(value) {
2356
+ if (!value || typeof value !== "object") return null;
2357
+ const { host, phase, startedAt, phaseStartedAt } = value;
2358
+ if ([
2359
+ host,
2360
+ phase,
2361
+ startedAt,
2362
+ phaseStartedAt
2363
+ ].some((field) => typeof field !== "string")) return null;
2364
+ return {
2365
+ host,
2366
+ phase,
2367
+ startedAt,
2368
+ phaseStartedAt
2369
+ };
2370
+ }
2371
+ function parseWaitingFor(value) {
2372
+ if (!value || typeof value !== "object") return null;
2373
+ const { kind, inUse, max, since } = value;
2374
+ if (kind !== "build-slot" && kind !== "device-slot") return null;
2375
+ if (!Number.isInteger(inUse) || inUse < 0 || !Number.isInteger(max) || max <= 0) return null;
2376
+ if (typeof since !== "string" || !Number.isFinite(Date.parse(since))) return null;
2377
+ return {
2378
+ kind,
2379
+ inUse,
2380
+ max,
2381
+ since
2382
+ };
2383
+ }
2384
+ function parseActivity(value) {
2385
+ if (!value || typeof value !== "object") return null;
2386
+ const { name, startedAt } = value;
2387
+ if (name !== "booting" && name !== "bundling" && name !== "waiting-ready") return null;
2388
+ return typeof startedAt === "string" ? {
2389
+ name,
2390
+ startedAt
2391
+ } : null;
2392
+ }
2393
+ function parseWaitingOn(value) {
2394
+ const path = value?.path;
2395
+ return typeof path === "string" && path ? { path } : null;
2396
+ }
2397
+ function parseEstimate(value) {
2398
+ if (!value || typeof value !== "object") return null;
2399
+ const { outcome, expectedMs, basis, phaseMs, planned } = value;
2400
+ if (outcome !== null && outcome !== "hit" && outcome !== "cold") return null;
2401
+ if (expectedMs !== null && typeof expectedMs !== "number") return null;
2402
+ if (typeof basis !== "number" || !Array.isArray(planned) || !phaseMs || typeof phaseMs !== "object") return null;
2403
+ const durations = Object.entries(phaseMs).filter((entry) => isPhase(entry[0]) && typeof entry[1] === "number");
2404
+ return {
2405
+ outcome,
2406
+ expectedMs,
2407
+ basis,
2408
+ phaseMs: Object.fromEntries(durations),
2409
+ planned: planned.filter(isPhase)
2410
+ };
2411
+ }
2412
+ function isPhase(value) {
2413
+ return BUILD_PHASES.includes(value);
2414
+ }
2415
+ function activeBuildState(claim, survey = (0, ownership_claim_exports.readClaimSet)(claim.root)) {
2416
+ if (survey.live.some((holder) => holder.claimId === claim.claimId)) return "running";
2417
+ if (survey.dead.some((holder) => holder.claimId === claim.claimId)) return "stale";
2418
+ return survey.unresolved.length ? "unknown" : "stale";
2419
+ }
2420
+ function median(values) {
2421
+ if (!values.length) return null;
2422
+ const sorted = values.toSorted((a, b) => a - b);
2423
+ const mid = Math.floor(sorted.length / 2);
2424
+ return sorted.length % 2 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2);
2425
+ }
2426
+ function estimateBuild(history, platform, known, deviceSetup) {
2427
+ const lists = platform === "macos" ? void 0 : history?.[platform];
2428
+ const outcome = known ?? latestOutcome(lists);
2429
+ const samples = matchDeviceSetup(outcome && lists?.[outcome] || [], deviceSetup);
2430
+ const phaseMs = {};
2431
+ const planned = [];
2432
+ for (const phase of BUILD_PHASES) {
2433
+ const durations = samples.flatMap((sample) => phase in sample.phases ? [sample.phases[phase]] : []);
2434
+ const ms = median(durations);
2435
+ if (ms === null) continue;
2436
+ phaseMs[phase] = ms;
2437
+ if (durations.length * 2 >= samples.length) planned.push(phase);
2438
+ }
2439
+ return {
2440
+ outcome,
2441
+ expectedMs: median(samples.map((sample) => sample.durationMs)),
2442
+ basis: samples.length,
2443
+ phaseMs,
2444
+ planned
2445
+ };
2446
+ }
2447
+ const MIN_TAGGED_SAMPLES = 3;
2448
+ function matchDeviceSetup(samples, deviceSetup) {
2449
+ if (deviceSetup === void 0) return samples;
2450
+ const tagged = samples.filter((sample) => sample.deviceSetup === deviceSetup);
2451
+ if (deviceSetup || tagged.length >= MIN_TAGGED_SAMPLES) return tagged;
2452
+ return samples.filter((sample) => sample.deviceSetup !== true);
2453
+ }
2454
+ function latestOutcome(lists) {
2455
+ let latest = null;
2456
+ for (const outcome of ["hit", "cold"]) {
2457
+ const last = lists?.[outcome]?.at(-1);
2458
+ const at = last ? Date.parse(last.at) : NaN;
2459
+ if (Number.isFinite(at) && (!latest || at > latest.at)) latest = {
2460
+ outcome,
2461
+ at
2462
+ };
2463
+ }
2464
+ return latest?.outcome ?? null;
2465
+ }
2466
+ function buildReport(record, { state, history }) {
2467
+ const estimate = record.estimate ?? estimateBuild(history, record.platform, record.outcome ?? null, record.outcome ? record.deviceSetup : void 0);
2468
+ const plannedPhases = estimate.planned.flatMap((phase) => {
2469
+ const expectedMs = estimate.phaseMs[phase];
2470
+ return expectedMs === void 0 ? [] : [{
2471
+ phase,
2472
+ expectedMs
2473
+ }];
2474
+ });
2475
+ const completedPhaseMs = completedPhaseDurations(record.phases);
2476
+ return {
2477
+ platform: record.platform,
2478
+ slot: record.slot,
2479
+ state,
2480
+ phase: record.phase,
2481
+ startedAt: record.startedAt,
2482
+ phaseStartedAt: record.phaseStartedAt,
2483
+ outcome: estimate.outcome,
2484
+ outcomeKnown: record.outcome !== void 0,
2485
+ ...record.outcome && record.phases.some(({ phase }) => phase === "cache-lookup") ? { cacheLookupOutcome: record.outcome === "hit" ? "hit" : "miss" } : {},
2486
+ expectedMs: estimate.expectedMs,
2487
+ expectedPhaseMs: estimate.phaseMs[record.phase] ?? null,
2488
+ ...completedPhaseMs ? { completedPhaseMs } : {},
2489
+ basis: estimate.basis,
2490
+ plannedPhases: plannedPhases.length ? plannedPhases : null,
2491
+ ...record.missReason ? { missReason: record.missReason } : {},
2492
+ ...record.missReason && record.missProvisional ? { missProvisional: true } : {},
2493
+ placement: record.placement ?? "local",
2494
+ ...record.waitingOn ? { waitingOn: record.waitingOn } : {},
2495
+ ...record.waitingFor ? { waitingFor: record.waitingFor } : {},
2496
+ ...record.activity ? { activity: { ...record.activity } } : {}
2497
+ };
2498
+ }
2499
+ /**
2500
+ * The run's `bundling` activity with Metro's percent, when the request Metro reports in flight is the one the activity
2501
+ * started on: same platform and same start, since both come from the request's `bundle_response_started` record.
2502
+ */
2503
+ function bundlingActivity(activity, platform, bundle) {
2504
+ if (activity?.name !== "bundling" || !bundle?.bundling || bundle.percent === void 0) return activity;
2505
+ if (bundle.platform !== platform || bundle.startedAt !== activity.startedAt) return activity;
2506
+ return {
2507
+ ...activity,
2508
+ percent: bundle.percent
2509
+ };
2510
+ }
2511
+ function remainingText(remainingMs) {
2512
+ return remainingMs < 6e4 ? "under a minute left" : `about ${Math.ceil(remainingMs / 6e4)} min left`;
2513
+ }
2514
+ function activityText({ name, percent, startedAt }, now) {
2515
+ return `${name === "booting" ? "booting" : name === "waiting-ready" ? "waiting for app ready" : percent === void 0 ? "bundling JS" : `bundling JS ${percent}%`}, ${formatElapsed(Math.max(0, now - Date.parse(startedAt)))}`;
2516
+ }
2517
+ function buildStatusLine(report, now) {
2518
+ const slot = report.slot === "default" ? "" : ` [${report.slot}]`;
2519
+ if (report.state === "stale") return `build: stale ${report.platform}${slot} record from ${report.startedAt} (its run is gone; the next run replaces it)`;
2520
+ const elapsedMs = Math.max(0, now - Date.parse(report.startedAt));
2521
+ const remote = typeof report.placement === "object" ? ` on ${report.placement.host} (${report.placement.phase}, ${formatElapsed(Math.max(0, now - Date.parse(report.placement.phaseStartedAt)))})` : "";
2522
+ const waiting = report.waitingOn ? ` on ${report.waitingOn.path}` : "";
2523
+ const activity = report.activity ? ` (${activityText(report.activity, now)})` : "";
2524
+ const head = `build: ${report.platform}${slot} ${report.phase}${activity}${waiting}${remote}, ${formatElapsed(elapsedMs)} elapsed`;
2525
+ if (report.state === "unknown") return `${head} (its native-run claim cannot be resolved, so it may not be running)`;
2526
+ if (report.expectedMs === null) return head;
2527
+ const basis = `median of ${plural(report.basis, `${report.outcome} run`)}`;
2528
+ const remainingMs = report.expectedMs - elapsedMs;
2529
+ return remainingMs > 0 ? `${head} -- ${remainingText(remainingMs)} (${basis})` : `${head} (usually ~${formatElapsed(report.expectedMs)}, ${basis})`;
2530
+ }
2531
+ //#endregion
2532
+ //#region src/commands/gc/idle.ts
2533
+ function findOwnedDeviceActivity({ config, sims, androidSerial, readActivity, deadProjects = [] }) {
2534
+ const booted = new Map(sims.filter((sim) => sim.state === "Booted").map((sim) => [sim.udid, sim.name]));
2535
+ const dead = new Set(deadProjects);
2536
+ const found = [];
2537
+ for (const [project, record] of Object.entries(config?.projects ?? {})) {
2538
+ if (dead.has(project)) continue;
2539
+ for (const { slot, platforms } of projectDeviceSlots(record)) {
2540
+ const ios = platforms.ios;
2541
+ if (ios?.owned && ios.deviceUdid && booted.has(ios.deviceUdid)) found.push({
2542
+ kind: "ios",
2543
+ id: ios.deviceUdid,
2544
+ name: booted.get(ios.deviceUdid) ?? ios.deviceUdid,
2545
+ project,
2546
+ slot,
2547
+ activity: readActivity({
2548
+ platform: "ios",
2549
+ id: ios.deviceUdid,
2550
+ slot,
2551
+ workspace: project
2552
+ })
2553
+ });
2554
+ const android = platforms.android;
2555
+ const serial = android?.owned && android.avdName ? androidSerial(android.avdName) : null;
2556
+ if (android?.avdName && serial) found.push({
2557
+ kind: "android",
2558
+ id: android.avdName,
2559
+ name: android.avdName,
2560
+ project,
2561
+ slot,
2562
+ activity: readActivity({
2563
+ platform: "android",
2564
+ id: serial,
2565
+ slot,
2566
+ workspace: project
2567
+ })
2568
+ });
2569
+ }
2570
+ }
2571
+ return found;
2572
+ }
2573
+ function findIdleDevices({ buildInProgress, now, ...inputs }) {
2574
+ return idleDevicesFromActivity(findOwnedDeviceActivity(inputs), buildInProgress, now);
2575
+ }
2576
+ function idleDevicesFromActivity(devices, buildInProgress, now) {
2577
+ return devices.filter((device) => device.activity.state === "idle").map(({ kind, id, name, project, slot, activity }) => ({
2578
+ kind,
2579
+ id,
2580
+ name,
2581
+ project,
2582
+ slot,
2583
+ lastActivityAt: activity.lastActivityAt ?? null,
2584
+ idleForMs: idleForMs(activity, now),
2585
+ buildInProgress: buildInProgress(project)
2586
+ }));
2587
+ }
2588
+ function idleShutdownCandidates(devices, idleMs) {
2589
+ return devices.filter((d) => !d.buildInProgress && d.idleForMs !== null && d.idleForMs >= idleMs);
2590
+ }
2591
+ function parseIdleDuration(value) {
2592
+ const match = /^([1-9][0-9]*)(m|h|d)$/.exec(value.trim());
2593
+ if (!match) return null;
2594
+ const unit = {
2595
+ m: 6e4,
2596
+ h: 36e5,
2597
+ d: 864e5
2598
+ }[match[2]];
2599
+ return Number(match[1]) * unit;
2600
+ }
2601
+ function workspaceBuildInProgress(project) {
2602
+ const record = parseActiveBuild(readWorkspaceState$1(project)?.[ACTIVE_BUILD_KEY]);
2603
+ if (!record) return false;
2604
+ try {
2605
+ return activeBuildState(record.claim) !== "stale";
2606
+ } catch {
2607
+ return true;
2608
+ }
2609
+ }
2610
+ function deviceActivityInputs(config, sims, now, env) {
2611
+ const resolve = ownedAvdSerialResolver({ timeoutMs: 5e3 });
2612
+ return {
2613
+ config,
2614
+ sims,
2615
+ readActivity: createActivityReader({
2616
+ now,
2617
+ env
2618
+ }),
2619
+ androidSerial: (avdName) => {
2620
+ try {
2621
+ return resolve(avdName).serial ?? null;
2622
+ } catch {
2623
+ return null;
2624
+ }
2625
+ }
2626
+ };
2627
+ }
2628
+ function collectOwnedDeviceActivity(config, sims, now = Date.now(), env) {
2629
+ return findOwnedDeviceActivity(deviceActivityInputs(config, sims, now, env));
2630
+ }
2631
+ function collectIdleDevices(config, sims, deadProjects, now = Date.now(), env) {
2632
+ return findIdleDevices({
2633
+ ...deviceActivityInputs(config, sims, now, env),
2634
+ deadProjects,
2635
+ now,
2636
+ buildInProgress: workspaceBuildInProgress
2637
+ });
2638
+ }
2639
+ async function deviceActivityInputsAsync(config, sims, now, env, deadProjects = [], signal, operation) {
2640
+ const resolve = ownedAvdSerialResolverAsync({
2641
+ timeoutMs: 5e3,
2642
+ signal,
2643
+ operation
2644
+ });
2645
+ const serials = /* @__PURE__ */ new Map();
2646
+ for (const [project, record] of Object.entries(config?.projects ?? {})) {
2647
+ signal?.throwIfAborted();
2648
+ if (deadProjects.includes(project)) continue;
2649
+ for (const { platforms } of projectDeviceSlots(record)) {
2650
+ const android = platforms.android;
2651
+ if (!android?.owned || !android.avdName || serials.has(android.avdName)) continue;
2652
+ try {
2653
+ serials.set(android.avdName, (await resolve(android.avdName)).serial ?? null);
2654
+ } catch (error) {
2655
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
2656
+ signal?.throwIfAborted();
2657
+ serials.set(android.avdName, null);
2658
+ }
2659
+ }
2660
+ }
2661
+ return {
2662
+ config,
2663
+ sims,
2664
+ readActivity: createActivityReaderAsync({
2665
+ now,
2666
+ env,
2667
+ signal,
2668
+ operation
2669
+ }),
2670
+ androidSerial: (name) => serials.get(name) ?? null
2671
+ };
2672
+ }
2673
+ async function collectOwnedDeviceActivityAsync(config, sims, now = Date.now(), env, signal, operation) {
2674
+ const devices = findOwnedDeviceActivity(await deviceActivityInputsAsync(config, sims, now, env, [], signal, operation));
2675
+ return Promise.all(devices.map(async (device) => Object.assign({}, device, { activity: await device.activity })));
2676
+ }
2677
+ async function collectIdleDevicesAsync(config, sims, deadProjects, now = Date.now(), env, signal, operation) {
2678
+ const pending = findOwnedDeviceActivity({
2679
+ ...await deviceActivityInputsAsync(config, sims, now, env, deadProjects, signal, operation),
2680
+ deadProjects
2681
+ });
2682
+ return idleDevicesFromActivity(await Promise.all(pending.map(async (device) => Object.assign({}, device, { activity: await device.activity }))), workspaceBuildInProgress, now);
2683
+ }
2684
+ function idleDeviceLines(devices) {
2685
+ if (devices.length === 0) return [];
2686
+ return [`Idle owned devices (${devices.length}) - booted with no driver, claim, or recent activity:`, ...devices.map((d) => {
2687
+ const slot = d.slot === "default" ? "" : ` [${d.slot}]`;
2688
+ const idle = d.idleForMs === null ? "idle for an unknown time" : `idle ${formatLongDuration(d.idleForMs)}`;
2689
+ const build = d.buildInProgress ? ", build in progress" : "";
2690
+ return ` ${d.kind}${slot} ${d.name} (${d.project}) -- ${idle}${build}`;
2691
+ })];
2692
+ }
2693
+ async function shutDownIdleDevices(devices, idleMs, recollect, { log, record } = GC_OUTPUT, signal, operation) {
2694
+ signal?.throwIfAborted();
2695
+ const candidates = idleShutdownCandidates(devices, idleMs);
2696
+ const shutDown = [];
2697
+ if (candidates.length === 0) {
2698
+ log(chalk.dim(`No owned device has been idle for ${formatLongDuration(idleMs)} or more.`));
2699
+ return {
2700
+ failures: 0,
2701
+ shutDown
2702
+ };
2703
+ }
2704
+ const fresh = idleShutdownCandidates(await recollect(), idleMs);
2705
+ signal?.throwIfAborted();
2706
+ let failures = 0;
2707
+ for (const device of candidates) {
2708
+ signal?.throwIfAborted();
2709
+ const what = `${device.kind} ${device.name}`;
2710
+ const current = fresh.find((d) => d.kind === device.kind && d.id === device.id && d.project === device.project && d.slot === device.slot);
2711
+ if (!current) {
2712
+ log(chalk.dim(`Kept ${what}: it is no longer idle for ${formatLongDuration(idleMs)}.`));
2713
+ record("idleDevice", "kept", device.name, {
2714
+ id: device.id,
2715
+ detail: `it is no longer idle for ${formatLongDuration(idleMs)}`
2716
+ });
2717
+ continue;
2718
+ }
2719
+ const outcome = device.kind === "ios" ? await teardownOwnedIosSim(device.id, {
2720
+ label: device.name,
2721
+ operation
2722
+ }) : await teardownOwnedAvd(device.id, {
2723
+ owner: {
2724
+ projectPath: device.project,
2725
+ slot: device.slot
2726
+ },
2727
+ operation
2728
+ });
2729
+ if (outcome.status === "torn-down") {
2730
+ shutDown.push(device);
2731
+ log(chalk.green(`Shut down ${what}, idle ${formatLongDuration(current.idleForMs)}`));
2732
+ record("idleDevice", "done", device.name, {
2733
+ id: device.id,
2734
+ detail: `idle ${formatLongDuration(current.idleForMs)}`
2735
+ });
2736
+ } else if (outcome.status === "missing") log(chalk.dim(`${what} is already gone; nothing to shut down.`));
2737
+ else if (outcome.status === "skipped") {
2738
+ log(chalk.yellow(`Skipped ${what}: ${outcome.reason}`));
2739
+ record("idleDevice", "kept", device.name, {
2740
+ id: device.id,
2741
+ detail: outcome.reason
2742
+ });
2743
+ } else {
2744
+ failures++;
2745
+ log(chalk.red(`Failed to shut down ${what}: ${outcome.reason}`));
2746
+ record("idleDevice", "failed", device.name, {
2747
+ id: device.id,
2748
+ detail: outcome.reason
2749
+ });
2750
+ }
2751
+ }
2752
+ return {
2753
+ failures,
2754
+ shutDown
2755
+ };
2756
+ }
2757
+ //#endregion
1152
2758
  //#region src/macos/instances.ts
1153
2759
  const BUNDLE_EXECUTABLE = /^[^/]+\.app\/Contents\/MacOS\/[^/]+$/;
1154
2760
  function bundleDirs(root) {
@@ -1853,9 +3459,9 @@ function numberOrNull(v) {
1853
3459
  //#region src/workspace/in-use.ts
1854
3460
  const NATIVE_RUN = "native-run";
1855
3461
  const NATIVE_RUN_HELD = "a stim ios, android or stop run holds its native-run.lock";
1856
- function workspaceInUse(root, { supervisor = true, managedLocks = true, hosted = true, nativeRun = true } = {}, bundleInstances = bundleInstancePids) {
3462
+ function workspaceInUse(root, { supervisor = true, devServer = true, managedLocks = true, hosted = true, nativeRun = true } = {}, bundleInstances = bundleInstancePids) {
1857
3463
  const reasons = [];
1858
- if (supervisor) {
3464
+ if (supervisor && devServer) {
1859
3465
  const project = getProject(root);
1860
3466
  const target = resolveSupervisorTarget({
1861
3467
  state: readWorkspaceState$1(root)?.supervisor,
@@ -1864,6 +3470,11 @@ function workspaceInUse(root, { supervisor = true, managedLocks = true, hosted =
1864
3470
  });
1865
3471
  if (target.status === "ours") reasons.push(`its dev server supervisor (pid ${target.pid}) is running`);
1866
3472
  else if (target.status === "unverified") reasons.push(`its dev server supervisor cannot be verified: ${target.reason ?? "unknown identity"}`);
3473
+ const browser = webFacts(readWebRecord(root));
3474
+ if (browser?.status === "running" || browser?.status === "orphaned") reasons.push(`its owned Chrome (pid ${browser.record.chromeProcess?.pid}) is running`);
3475
+ else if (browser?.status === "unverified") reasons.push("its owned Chrome cannot be verified");
3476
+ }
3477
+ if (supervisor) {
1867
3478
  const macos = macosAppState(readMacosRecord(root));
1868
3479
  if (macos?.host) reasons.push(`its macOS app runs on ${macos.host.machine}`);
1869
3480
  else if (macos?.state === "running" || macos?.state === "orphaned" || macos?.build.state === "running") reasons.push("its macOS app or build is running");
@@ -1877,9 +3488,6 @@ function workspaceInUse(root, { supervisor = true, managedLocks = true, hosted =
1877
3488
  const runtime = (0, ownership_claim_exports.readClaimSet)(macosRuntimeClaim(root));
1878
3489
  if (runtime.live.length) reasons.push("its macOS runtime claim is held");
1879
3490
  else if (runtime.unresolved[0]) reasons.push(`its macOS runtime cannot be verified: ${runtime.unresolved[0].reason}`);
1880
- const browser = webFacts(readWebRecord(root));
1881
- if (browser?.status === "running" || browser?.status === "orphaned") reasons.push(`its owned Chrome (pid ${browser.record.chromeProcess?.pid}) is running`);
1882
- else if (browser?.status === "unverified") reasons.push("its owned Chrome cannot be verified");
1883
3491
  }
1884
3492
  if (nativeRun) {
1885
3493
  const claims = (0, ownership_claim_exports.readClaimSet)(workspaceProcessLockPath(workspaceDir(root), NATIVE_RUN, true));
@@ -1974,6 +3582,63 @@ async function withIdleWorkspace(root, fn, { purpose, operation, signal = operat
1974
3582
  }
1975
3583
  }
1976
3584
  //#endregion
3585
+ //#region src/workspace/regenerable-dirs.ts
3586
+ /**
3587
+ * Directories that hold only what a build or an install regenerates, relative to an app or package root. ios/build is
3588
+ * not one: `pod install` writes the codegen output (build/generated/ios) and, on React Native 0.88, the podspec
3589
+ * facades (scripts/cocoapods/rncore_facades.rb) there, the Pods project references them, and only `pod install`
3590
+ * recreates them.
3591
+ */
3592
+ const ARTIFACT_DIRS = {
3593
+ build: [
3594
+ "android/app/build",
3595
+ "android/build",
3596
+ "android/.gradle",
3597
+ ".build"
3598
+ ],
3599
+ dependencies: ["node_modules", "ios/Pods"]
3600
+ };
3601
+ /** The existing real directories among `dirs` under `root`, never through a link. */
3602
+ function existingDirsOf(root, dirs) {
3603
+ return dirs.map((dir) => join(root, dir)).filter((path) => {
3604
+ try {
3605
+ return lstatSync(path).isDirectory();
3606
+ } catch {
3607
+ return false;
3608
+ }
3609
+ });
3610
+ }
3611
+ /**
3612
+ * Why `path`, inside the checkout at `root`, must not be deleted as a regenerable directory, or null when it is a real
3613
+ * directory inside `root` whose every file git ignores.
3614
+ */
3615
+ async function regenerableDirProblem(root, path, signal) {
3616
+ const rel = relative(root, path);
3617
+ try {
3618
+ if (!lstatSync(path).isDirectory()) return "it is not a directory";
3619
+ } catch {
3620
+ return "it is gone";
3621
+ }
3622
+ if (canonicalPath(path) !== join(canonicalPath(root), rel)) return "it or a directory above it is a link, or it lies outside its project";
3623
+ const tracked = await gitQuietAsync(root, [
3624
+ "ls-files",
3625
+ "--",
3626
+ rel
3627
+ ], { signal });
3628
+ if (tracked === null) return "git cannot tell whether it holds tracked files";
3629
+ if (tracked.trim()) return "it holds files git tracks";
3630
+ const unignored = await gitQuietAsync(root, [
3631
+ "ls-files",
3632
+ "--others",
3633
+ "--exclude-standard",
3634
+ "--",
3635
+ rel
3636
+ ], { signal });
3637
+ if (unignored === null) return "git cannot tell whether it ignores every file in it";
3638
+ if (unignored.trim()) return "it holds files git does not ignore";
3639
+ return null;
3640
+ }
3641
+ //#endregion
1977
3642
  //#region src/commands/gc/workspaces.ts
1978
3643
  function workspacesRoot() {
1979
3644
  return join$1(getConfigDir(), "workspaces");
@@ -2091,7 +3756,7 @@ function classifyWorkspaceDirs(entries, { registryKeys, exists, isMounted, inUse
2091
3756
  continue;
2092
3757
  }
2093
3758
  if (present) continue;
2094
- if (registryKeys.some((key) => isPathPrefix(projectRoot, key))) continue;
3759
+ if (registryKeys.some((key) => isPathPrefix$1(projectRoot, key))) continue;
2095
3760
  if (!isMounted(projectRoot)) {
2096
3761
  skipped.push({
2097
3762
  dir,
@@ -2153,39 +3818,68 @@ async function collectOrphanedWorkspaces(registryKeys, mountedVolumes, { measure
2153
3818
  }
2154
3819
  }
2155
3820
  if (!measure) return classified;
2156
- const orphaned = [];
2157
- for (const entry of classified.orphaned) {
2158
- operation?.signal.throwIfAborted();
2159
- const bytes = await measuredDirectorySizeAsync(entry.dir, {
2160
- timeoutMs: SIZE_TIMEOUT_MS,
2161
- operation,
2162
- signal: operation?.signal
2163
- });
2164
- orphaned.push({
2165
- ...entry,
2166
- bytes
2167
- });
2168
- }
2169
3821
  return {
2170
- orphaned,
3822
+ orphaned: await mapLimited(classified.orphaned, sizeConcurrency(operation), async (entry) => {
3823
+ operation?.signal.throwIfAborted();
3824
+ const bytes = await measuredDirectorySizeAsync(entry.dir, {
3825
+ timeoutMs: SIZE_TIMEOUT_MS,
3826
+ operation,
3827
+ signal: operation?.signal
3828
+ });
3829
+ return {
3830
+ ...entry,
3831
+ bytes
3832
+ };
3833
+ }),
2171
3834
  skipped: classified.skipped
2172
3835
  };
2173
3836
  }
2174
3837
  const REBUILD_COST = "The next build of an unchanged app installs from the shared build cache; after a native change the compilation cache speeds the rebuild, but on React Native 0.86 Swift does not use it (explicit modules are off), so that build recompiles Swift. The next stim macos in a cleared workspace is a full Swift build.";
3838
+ /** How long after its last ios, android or macos run a workspace keeps its build outputs. */
3839
+ const LIGHTEN_AFTER_MS = 9e5;
3840
+ const BUILD_CHECKS = {
3841
+ devServer: false,
3842
+ hosted: false,
3843
+ managedLocks: false
3844
+ };
3845
+ const BUILD_IN_PROGRESS = "a build is in progress";
3846
+ /** Why the build outputs of the workspace at `root` are in use: it is building or running a macOS app. */
3847
+ function workspaceBuildReasons(root, { nativeRun = true } = {}) {
3848
+ const reasons = workspaceInUse(root, {
3849
+ ...BUILD_CHECKS,
3850
+ nativeRun
3851
+ });
3852
+ return workspaceBuildInProgress(root) ? [...reasons, BUILD_IN_PROGRESS] : reasons;
3853
+ }
3854
+ async function workspaceBuildReasonsAsync(root, signal, operation) {
3855
+ const reasons = await workspaceInUseAsync(root, BUILD_CHECKS, signal, operation);
3856
+ return workspaceBuildInProgress(root) ? [...reasons, BUILD_IN_PROGRESS] : reasons;
3857
+ }
3858
+ async function inTreeBuildDirs(root, signal) {
3859
+ const dirs = [];
3860
+ for (const path of existingDirsOf(root, ARTIFACT_DIRS.build)) if (!await regenerableDirProblem(root, path, signal)) dirs.push(path);
3861
+ return dirs;
3862
+ }
2175
3863
  const WORKSPACE_OUTPUT_DIRS = [
2176
3864
  "derived-data",
2177
3865
  "gradle-build",
2178
3866
  "android-cas",
2179
3867
  "cache-provider"
2180
3868
  ];
3869
+ /** Why a workspace whose last ios, android or macos run was at `nativeUsed` keeps its build outputs, or null. */
3870
+ function recentNativeUseReason(nativeUsed, now) {
3871
+ return now - nativeUsed < 9e5 ? `ran ios, android or macos within the last ${LIGHTEN_AFTER_MS / 6e4} minutes` : null;
3872
+ }
2181
3873
  const DAY_MS = 864e5;
2182
3874
  function planWorkspaceOutputs(entries, { olderThan, now }) {
2183
- return entries.map(({ dir, projectRoot, problem, bytes, lastUsed, inUse }) => {
3875
+ return entries.map(({ dir, projectRoot, problem, inTreeDirs = [], bytes, lastUsed, nativeUsed, inUse }) => {
3876
+ const recentNative = recentNativeUseReason(nativeUsed, now);
2184
3877
  const idleDays = Number.isFinite(lastUsed) ? Math.max(0, Math.floor((now - lastUsed) / DAY_MS)) : null;
2185
- const [keptCode, keptReason] = projectRoot === null ? ["unresolved", `workspace directory not resolved: ${problem ?? "unknown project root"}`] : inUse.length ? ["in-use", `in use: ${inUse.join("; ")}`] : olderThan === null ? [null, null] : idleDays === null ? ["last-use-unknown", "its last use is unknown"] : idleDays < olderThan ? ["recently-used", `used ${idleDays}d ago, within --older-than ${olderThan}`] : [null, null];
3878
+ const [keptCode, keptReason] = projectRoot === null ? ["unresolved", `workspace directory not resolved: ${problem ?? "unknown project root"}`] : inUse.length ? ["in-use", `in use: ${inUse.join("; ")}`] : recentNative ? ["recently-used", recentNative] : olderThan === null ? [null, null] : idleDays === null ? ["last-use-unknown", "its last use is unknown"] : idleDays < olderThan ? ["recently-used", `used ${idleDays}d ago, within --older-than ${olderThan}`] : [null, null];
2186
3879
  return {
2187
3880
  dir,
2188
3881
  projectRoot,
3882
+ inTreeDirs,
2189
3883
  bytes,
2190
3884
  idleDays,
2191
3885
  willClear: keptCode === null,
@@ -2204,48 +3898,68 @@ function outputPaths(dir) {
2204
3898
  } catch {}
2205
3899
  return paths;
2206
3900
  }
2207
- async function outputBytes(paths, timeoutMs, signal, operation) {
2208
- let total = 0;
2209
- for (const path of paths) {
2210
- const size = await measuredDirectorySizeAsync(path, {
3901
+ async function outputBytes(entries, timeoutMs, signal, operation) {
3902
+ const sizes = await mapLimited(entries.flatMap((paths, index) => paths.map((path) => ({
3903
+ path,
3904
+ index
3905
+ }))), sizeConcurrency(operation), async ({ path, index }) => ({
3906
+ index,
3907
+ size: await measuredDirectorySizeAsync(path, {
2211
3908
  timeoutMs,
2212
3909
  signal,
2213
3910
  operation
2214
- });
2215
- if (size === null) return null;
2216
- total += size;
2217
- }
2218
- return total;
3911
+ })
3912
+ }));
3913
+ return entries.map((_, index) => {
3914
+ const own = sizes.filter((size) => size.index === index).map((size) => size.size);
3915
+ return own.includes(null) ? null : own.reduce((total, size) => total + (size ?? 0), 0);
3916
+ });
2219
3917
  }
2220
- function collectWorkspaceOutputs({ olderThan, now, exclude = [], measure = true, sizeTimeoutMs = SIZE_TIMEOUT_MS, signal, operation }) {
3918
+ function collectWorkspaceOutputs({ olderThan, now, exclude = [], measure = true, sizeTimeoutMs = SIZE_TIMEOUT_MS, signal, operation, lighten = false }) {
2221
3919
  const mountedVolumes = listMountedVolumes();
2222
- const outputEntries = (entries) => entries.filter((entry) => !exclude.includes(entry.dir)).filter((entry) => entry.projectRoot === null || isOnMountedVolume(entry.projectRoot, mountedVolumes)).map((entry) => Object.assign({}, entry, { paths: outputPaths(entry.dir) })).filter((entry) => entry.paths.length > 0);
3920
+ const outputEntries = (entries, inTree = /* @__PURE__ */ new Map()) => entries.filter((entry) => !exclude.includes(entry.dir)).filter((entry) => entry.projectRoot === null || isOnMountedVolume(entry.projectRoot, mountedVolumes)).map((entry) => {
3921
+ const inTreeDirs = inTree.get(entry.dir) ?? [];
3922
+ return Object.assign({}, entry, {
3923
+ inTreeDirs,
3924
+ paths: [...outputPaths(entry.dir), ...inTreeDirs]
3925
+ });
3926
+ }).filter((entry) => entry.paths.length > 0);
2223
3927
  const report = (sizes) => ({
2224
3928
  root: workspacesRoot(),
2225
- workspaces: planWorkspaceOutputs(sizes.map((entry) => Object.assign({}, entry, { lastUsed: entry.projectRoot === null ? NaN : workspaceLastUsed(entry.projectRoot) })), {
3929
+ workspaces: planWorkspaceOutputs(sizes.map((entry) => Object.assign({}, entry, {
3930
+ lastUsed: entry.projectRoot === null ? NaN : workspaceLastUsed(entry.projectRoot),
3931
+ nativeUsed: lighten && entry.projectRoot !== null ? lastNativeUseFrom(readWorkspaceState$1(entry.projectRoot)) : NaN
3932
+ })), {
2226
3933
  olderThan,
2227
3934
  now
2228
3935
  })
2229
3936
  });
3937
+ const inUseOf = (root) => lighten ? workspaceBuildReasons(root) : workspaceInUse(root);
2230
3938
  if (!measure && !signal) return report(outputEntries(listWorkspaceDirs()).map(({ paths: _paths, ...entry }) => Object.assign({}, entry, {
2231
3939
  bytes: null,
2232
- inUse: entry.projectRoot === null ? [] : workspaceInUse(entry.projectRoot)
3940
+ inUse: entry.projectRoot === null ? [] : inUseOf(entry.projectRoot)
2233
3941
  })));
2234
3942
  return (async () => {
2235
3943
  const sizes = [];
2236
- const entries = outputEntries(await listWorkspaceDirsAsync(signal));
2237
- for (const { paths, ...entry } of entries) {
3944
+ const listed = await listWorkspaceDirsAsync(signal);
3945
+ const inTree = /* @__PURE__ */ new Map();
3946
+ if (lighten) {
3947
+ for (const { dir, projectRoot } of listed) if (projectRoot !== null && !exclude.includes(dir)) inTree.set(dir, await inTreeBuildDirs(projectRoot, signal));
3948
+ }
3949
+ const entries = outputEntries(listed, inTree);
3950
+ const bytes = measure ? await outputBytes(entries.map((entry) => entry.paths), sizeTimeoutMs, signal, operation) : [];
3951
+ for (const [index, { paths: _paths, ...entry }] of entries.entries()) {
2238
3952
  signal?.throwIfAborted();
2239
3953
  sizes.push({
2240
3954
  ...entry,
2241
- bytes: measure ? await outputBytes(paths, sizeTimeoutMs, signal, operation) : null,
2242
- inUse: entry.projectRoot === null ? [] : signal ? await workspaceInUseAsync(entry.projectRoot, {}, signal, operation) : workspaceInUse(entry.projectRoot)
3955
+ bytes: bytes[index] ?? null,
3956
+ inUse: entry.projectRoot === null ? [] : !signal ? inUseOf(entry.projectRoot) : lighten ? await workspaceBuildReasonsAsync(entry.projectRoot, signal, operation) : await workspaceInUseAsync(entry.projectRoot, {}, signal, operation)
2243
3957
  });
2244
3958
  }
2245
3959
  return report(sizes);
2246
3960
  })();
2247
3961
  }
2248
- async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), operation, signal = operation?.signal }, { log, record } = GC_OUTPUT) {
3962
+ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), operation, signal = operation?.signal, lighten = false }, { log, record } = GC_OUTPUT) {
2249
3963
  let failures = 0;
2250
3964
  let cleared = 0;
2251
3965
  const clearedRoots = [];
@@ -2269,13 +3983,19 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), oper
2269
3983
  ...entry,
2270
3984
  problem: null,
2271
3985
  lastUsed: workspaceLastUsed(root),
2272
- inUse: []
3986
+ nativeUsed: lighten ? lastNativeUseFrom(readWorkspaceState$1(root)) : NaN,
3987
+ inUse: lighten && workspaceBuildInProgress(root) ? [BUILD_IN_PROGRESS] : []
2273
3988
  }], {
2274
3989
  olderThan,
2275
3990
  now
2276
3991
  });
2277
3992
  if (again?.keptReason) return again.keptReason;
2278
- for (const path of outputPaths(entry.dir)) {
3993
+ const inTree = [];
3994
+ for (const path of lighten ? existingDirsOf(root, ARTIFACT_DIRS.build) : []) inTree.push({
3995
+ path,
3996
+ problem: await regenerableDirProblem(root, path, signal)
3997
+ });
3998
+ for (const path of [...outputPaths(entry.dir), ...inTree.filter((dir) => !dir.problem).map((dir) => dir.path)]) {
2279
3999
  signal?.throwIfAborted();
2280
4000
  await rm(path, {
2281
4001
  recursive: true,
@@ -2283,11 +4003,13 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), oper
2283
4003
  });
2284
4004
  }
2285
4005
  signal?.throwIfAborted();
4006
+ for (const { path, problem } of inTree) if (problem) log(chalk.yellow(`Kept ${path}: ${problem}`));
2286
4007
  return null;
2287
4008
  }, {
2288
4009
  purpose: "gc",
2289
4010
  signal,
2290
- operation
4011
+ operation,
4012
+ ...lighten ? BUILD_CHECKS : {}
2291
4013
  });
2292
4014
  } catch (error) {
2293
4015
  if (error instanceof WorkspaceProcessCleanupError) throw error;
@@ -2311,10 +4033,13 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), oper
2311
4033
  }
2312
4034
  cleared += entry.bytes ?? 0;
2313
4035
  clearedRoots.push(root);
2314
- log(chalk.green(`Cleared the build outputs of ${root} (${sizeText(entry.bytes)})`));
4036
+ log(chalk.green(lighten ? `Lightened ${root}: freed ${sizeText(entry.bytes)} of build outputs` : `Cleared the build outputs of ${root} (${sizeText(entry.bytes)})`));
2315
4037
  record("workspaceOutputs", "done", root, { bytes: entry.bytes });
2316
4038
  }
2317
- if (cleared) log(chalk.dim(`Cleared ${formatBytes(cleared)} of workspace build outputs. ${REBUILD_COST}`));
4039
+ if (cleared) {
4040
+ const count = `${clearedRoots.length} workspace${clearedRoots.length === 1 ? "" : "s"}`;
4041
+ log(chalk.dim(`${lighten ? `Lightened ${count}, freeing` : "Cleared"} ${formatBytes(cleared)} of workspace build outputs. ${REBUILD_COST}`));
4042
+ }
2318
4043
  return {
2319
4044
  failures,
2320
4045
  cleared: clearedRoots
@@ -2322,7 +4047,7 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), oper
2322
4047
  }
2323
4048
  function stillOrphaned({ dir, projectRoot }) {
2324
4049
  if (readRecordedRoot(dir, basename$1(dir)).projectRoot !== projectRoot || rootPresence(projectRoot) !== false || !isOnMountedVolume(projectRoot)) return false;
2325
- return !Object.keys(loadConfig$1()?.projects ?? {}).some((key) => isPathPrefix(projectRoot, key));
4050
+ return !Object.keys(loadConfig$1()?.projects ?? {}).some((key) => isPathPrefix$1(projectRoot, key));
2326
4051
  }
2327
4052
  async function deleteOrphanedWorkspaces(orphaned, output = GC_OUTPUT, operation) {
2328
4053
  let failures = 0;
@@ -2381,4 +4106,4 @@ function sizeText(bytes) {
2381
4106
  return bytes === null ? "size unknown" : formatBytes(bytes);
2382
4107
  }
2383
4108
  //#endregion
2384
- export { teardownOwnedIosSim as A, GC_OUTPUT as B, withManagedTunnelRemovalLock as C, teardownBrowserHeld as D, eraseParkedIosSim as E, measureDu as F, takeGcResults as H, writeUsageCache as I, acquireBuildSlot as L, teardownParkedIosSim as M, probePublicHttp as N, teardownOwnedAvd as O, canonical$1 as P, listBuildSlots as R, withManagedTunnelLock as S, eraseParkedAvd as T, createGcResults as V, startTunnelSequence as _, collectWorkspaceOutputs as a, withManagedRemoteWorktreeLock as b, listWorkspaceDirs as c, workspaceDirRemoved as d, emptyWorkspaceDir as f, resolveSupervisorTarget as g, workspaceInUseAsync as h, collectOrphanedWorkspaces as i, teardownParkedAvd as j, teardownOwnedBrowser as k, rootPresence as l, workspaceInUse as m, WORKSPACE_OUTPUT_DIRS as n, deleteOrphanedWorkspaces as o, withIdleWorkspace as p, clearWorkspaceOutputs as r, isInsideWorkspaces as s, REBUILD_COST as t, sizeText as u, stopTunnel as v, runningBundleInstancesAsync as w, withManagedRemoteWorktreeRemovalLock as x, terminateChild as y, releaseBuildSlot as z };
4109
+ export { STIM_VERSION as $, collectIdleDevicesAsync as A, activeBuildState as B, terminateChild as C, createGcResults as Ct, withManagedTunnelRemovalLock as D, readConcurrency as Dt, withManagedTunnelLock as E, mapLimited as Et, parseIdleDuration as F, parseActiveBuild as G, buildStatusLine as H, shutDownIdleDevices as I, runFailureRecord as J, recordBuildPhase as K, workspaceBuildInProgress as L, collectOwnedDeviceActivityAsync as M, idleDeviceLines as N, runningBundleInstancesAsync as O, sizeConcurrency as Ot, idleShutdownCandidates as P, errorRemedy as Q, ACTIVE_BUILD_KEY as R, stopTunnel as S, GC_OUTPUT as St, withManagedRemoteWorktreeRemovalLock as T, eachLimited as Tt, bundlingActivity as U, buildReport as V, estimateBuild as W, tapBuildLog as X, startBuildProgress as Y, thrownRunFailureRecord as Z, withIdleWorkspace as _, measureDu as _t, collectOrphanedWorkspaces as a, recordRunStats as at, resolveSupervisorTarget as b, listBuildSlots as bt, isInsideWorkspaces as c, eraseParkedIosSim as ct, rootPresence as d, teardownOwnedBrowser as dt, createRunRecorder as et, sizeText as f, teardownOwnedIosSim as ft, emptyWorkspaceDir as g, canonical$1 as gt, ARTIFACT_DIRS as h, probePublicHttp as ht, clearWorkspaceOutputs as i, recordCapacityWait as it, collectOwnedDeviceActivity as j, collectIdleDevices as k, listWorkspaceDirs as l, teardownBrowserHeld as lt, workspaceDirRemoved as m, teardownParkedIosSim as mt, REBUILD_COST as n, readStats$1 as nt, collectWorkspaceOutputs as o, statsProjectKey$1 as ot, workspaceBuildReasons as p, teardownParkedAvd as pt, recordFinishedBuild as q, WORKSPACE_OUTPUT_DIRS as r, recordCapacityRefusal as rt, deleteOrphanedWorkspaces as s, eraseParkedAvd as st, LIGHTEN_AFTER_MS as t, readRunEstimates as tt, recentNativeUseReason as u, teardownOwnedAvd as ut, workspaceInUse as v, writeUsageCache as vt, withManagedRemoteWorktreeLock as w, takeGcResults as wt, startTunnelSequence as x, releaseBuildSlot as xt, workspaceInUseAsync as y, acquireBuildSlot as yt, NO_BUILD_PROGRESS as z };