stim 1.26.0 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/dist/{acquire-artifact-DVsvSwf7.mjs → acquire-artifact-CKByBnTD.mjs} +2 -2
  2. package/dist/{act-C-BqJEpu.mjs → act-8iswyfRa.mjs} +319 -126
  3. package/dist/{activity-Lnb_EoRY.mjs → activity-DRMg4_mD.mjs} +6 -6
  4. package/dist/{agent-device-usage-output-DbXy5tg8.mjs → agent-device-usage-output-tE90fDdo.mjs} +5 -5
  5. package/dist/{android-CiNQw3kq.mjs → android-CHmSU4B4.mjs} +101 -45
  6. package/dist/{android-DTAcRwBC.mjs → android-CsPs-7Mg.mjs} +23 -18
  7. package/dist/{android-B5P81yI-.mjs → android-D577ApKG.mjs} +37 -15
  8. package/dist/{android-CSbLRD53.mjs → android-YWCbmkVK.mjs} +5 -5
  9. package/dist/{android-cas-BTVhZ0Qs.mjs → android-cas-_aDifWW0.mjs} +4 -4
  10. package/dist/android-cas-compiler.mjs +1 -1
  11. package/dist/api.d.mts +14 -4
  12. package/dist/api.mjs +39 -29
  13. package/dist/{apk-signed-tail-B46U9hXc.mjs → apk-signed-tail-CQbwM0qO.mjs} +2 -2
  14. package/dist/{artifact-CsAP6AGK.mjs → artifact-DYtHjxsn.mjs} +19 -24
  15. package/dist/{artifact-BxREhAaV.mjs → artifact-bFkfD3vf.mjs} +18 -23
  16. package/dist/{artifact-platform-CMRc0M_r.mjs → artifact-platform-BO2Uvk_A.mjs} +1 -1
  17. package/dist/auto-placement-BVqs8AJi.mjs +350 -0
  18. package/dist/{browser-web-rYPyqhTf.mjs → browser-web-BUaLcafY.mjs} +16 -16
  19. package/dist/{build-DPhApsjV.mjs → build-cQURy_KW.mjs} +35 -31
  20. package/dist/{build-cache-DrXNCy3T.mjs → build-cache-CZULpXVl.mjs} +43 -21
  21. package/dist/{build-plan-DrMMdayj.mjs → build-plan-CxoYfxsc.mjs} +7 -6
  22. package/dist/{cache-manifest-BEZ9Bpl1.mjs → cache-manifest-DEjqGGat.mjs} +1 -1
  23. package/dist/cache-manifest.mjs +1 -1
  24. package/dist/{budget-k2mZGvvk.mjs → ccache-Bm_A1xX9.mjs} +562 -1604
  25. package/dist/{cdp-BG3FJCCx.mjs → cdp-C3svvXiE.mjs} +1 -1
  26. package/dist/{chrome-BvV8ssI3.mjs → chrome-DyDWurWG.mjs} +1 -1
  27. package/dist/{cli-BBKFGOFl.mjs → cli-CpbaLDa7.mjs} +24 -23
  28. package/dist/cli.mjs +1 -1
  29. package/dist/{client-D9CaRks-.mjs → client-YxCer6ab.mjs} +84 -65
  30. package/dist/collector-run.d.mts +10 -9
  31. package/dist/collector-run.mjs +42 -27
  32. package/dist/{command-output-BF5AR2dL.mjs → command-output-C8R1KIvi.mjs} +1 -1
  33. package/dist/{config-BktkY_zC.mjs → config-Cn94Jc0g.mjs} +23 -4
  34. package/dist/{dependencies-Dw6wfQdv.mjs → dependencies-DKeMzwXY.mjs} +108 -60
  35. package/dist/{dependency-state-DFB41vfr.mjs → dependency-state-CojA2o0t.mjs} +14 -7
  36. package/dist/{detached-entry-KmieOKgN.mjs → detached-entry-BPlSaCwC.mjs} +1 -1
  37. package/dist/{dev-client-DXR_tPVY.mjs → dev-client-D-BW5uiq.mjs} +2 -2
  38. package/dist/{device-BK-zwyd_.mjs → device-CgXfbcz5.mjs} +25 -24
  39. package/dist/{device-capacity-DCSvUF0l.mjs → device-capacity-Cd8lMSn3.mjs} +175 -22
  40. package/dist/{device-BcYeHSFu.mjs → device-eeK95Lir.mjs} +16 -16
  41. package/dist/device-host-worker.mjs +22 -20
  42. package/dist/{device-ios-BLGfXQeK.mjs → device-ios-Cc9ulihb.mjs} +251 -23
  43. package/dist/{device-lease-run-BmvIl7bI.mjs → device-lease-run-Mtzp4THX.mjs} +5 -5
  44. package/dist/{device-pool-Dluix4H3.mjs → device-pool-eRz6DtCg.mjs} +3 -3
  45. package/dist/{doctor-Bhg1ii70.mjs → doctor-Bz_ouYAW.mjs} +28 -16
  46. package/dist/{doctor-Cp-BhzTS.mjs → doctor-C2QyhejJ.mjs} +1 -1
  47. package/dist/{doctor-fingerprint-D3a04Mhu.mjs → doctor-fingerprint-o7ZI9YLn.mjs} +1 -1
  48. package/dist/doctor-fingerprint.mjs +1 -1
  49. package/dist/{doctor-watchman-BStK05nt.mjs → doctor-watchman-BTW82PDC.mjs} +5 -5
  50. package/dist/{doctor-OnwP94ne.mjs → doctor-xf7ht-5n.mjs} +76 -32
  51. package/dist/{eas-build-Dsj93iM9.mjs → eas-build-Dm6q68QY.mjs} +4 -4
  52. package/dist/eas-fallback-DajNpxf-.mjs +87 -0
  53. package/dist/{eas-session-ledger-B5vygTiD.mjs → eas-session-ledger-CBFo_Uo1.mjs} +37 -8
  54. package/dist/{error-diagnostics-BOOm9Db7.mjs → error-diagnostics-D1QfyJzQ.mjs} +51 -25
  55. package/dist/{exec-VFRpu1iA.mjs → exec-aULwtNly.mjs} +57 -6
  56. package/dist/{gc-ldgr-hXU.mjs → gc-DM5oMm-J.mjs} +161 -45
  57. package/dist/{gc-C_79zvz4.mjs → gc-PLFwb0k4.mjs} +2 -2
  58. package/dist/{guide-D7K0X6KG.mjs → guide-C4I46XAk.mjs} +674 -213
  59. package/dist/{hosted-android-Bk54ej6n.mjs → hosted-android-DlaKLbW0.mjs} +6 -7
  60. package/dist/{hosted-client-fHO8yqzc.mjs → hosted-client-DDRm9hPJ.mjs} +51 -20
  61. package/dist/{hosted-logs-BfbmDM4M.mjs → hosted-logs-bSebOKbz.mjs} +8 -8
  62. package/dist/{hosted-logs-sync-B0pvmOk-.mjs → hosted-logs-sync-B0c53W_c.mjs} +5 -5
  63. package/dist/{hosted-macos-CPl0OZ9h.mjs → hosted-macos-B7-8Pv4H.mjs} +6 -6
  64. package/dist/{hosted-native-CpDB3-PO.mjs → hosted-native-CDe33CSr.mjs} +22 -18
  65. package/dist/{idle-shutdown-FfxL1KdG.mjs → idle-shutdown-CQ0QwU9J.mjs} +7 -8
  66. package/dist/{ios-WxpDJ5T1.mjs → ios-B1DYuOjh.mjs} +27 -11
  67. package/dist/{ios-B7-lToDu.mjs → ios-B1HvafEG.mjs} +8 -8
  68. package/dist/{ios-ENC4sRPA.mjs → ios-CsRVik64.mjs} +7 -1
  69. package/dist/{ios-device-CsyryvpT.mjs → ios-device-B4elVFKi.mjs} +3 -3
  70. package/dist/{ios-device-B6HK951C.mjs → ios-device-CiBA-er9.mjs} +1 -1
  71. package/dist/{ios-project-Ty5pHl9O.mjs → ios-project-CR4mZ7CH.mjs} +22 -9
  72. package/dist/{ios-signing-4c_qXvQm.mjs → ios-signing-BJzvDPfU.mjs} +2 -2
  73. package/dist/{ios-state-Cw7-WQf2.mjs → ios-state-BPHqMDdZ.mjs} +1 -1
  74. package/dist/{ios-ftxdQbJS.mjs → ios-tg4tp3q1.mjs} +94 -37
  75. package/dist/{js-swap-BMhXYVDS.mjs → js-swap-CVpSW1fn.mjs} +8 -7
  76. package/dist/{launch-DOp8RjDz.mjs → launch-DoySmhjz.mjs} +42 -38
  77. package/dist/lightened-DFAJgfIk.mjs +171 -0
  78. package/dist/{logs-s1dRIG1E.mjs → logs-BGsW7iPe.mjs} +46 -21
  79. package/dist/{logs-dQDki6Tt.mjs → logs-DI5ILkYB.mjs} +7 -7
  80. package/dist/{macos-DBBPNnIA.mjs → macos-Bo9st9Di.mjs} +73 -38
  81. package/dist/{macos-B0VEV3Gf.mjs → macos-C12RMJQF.mjs} +1 -1
  82. package/dist/macos-run.mjs +1 -1
  83. package/dist/maintenance-run.mjs +7 -7
  84. package/dist/{metro-CjC10t6E.mjs → metro-BnhXV2Mj.mjs} +4 -4
  85. package/dist/{metro-gateway-B6_PgD0L.mjs → metro-request-DXyKjVDF.mjs} +26 -47
  86. package/dist/{named-ports-BG0j1X4g.mjs → named-ports-Phoi2POD.mjs} +3 -3
  87. package/dist/{native-android-CL2NGiik.mjs → native-android-vxgtLD7w.mjs} +17 -18
  88. package/dist/{native-gradle-artifact-inputs-btSUulme.mjs → native-gradle-artifact-inputs-BN96IVw8.mjs} +5 -5
  89. package/dist/{native-runtime-Dq3RBcON.mjs → native-runtime-DG4Rodzj.mjs} +9 -8
  90. package/dist/{native-source-D-4Dh4JU.mjs → native-source-D1AzWiuP.mjs} +1 -1
  91. package/dist/{native-xcode-inputs-BW7H8JGu.mjs → native-xcode-inputs-BKarCoKg.mjs} +3 -3
  92. package/dist/{native-xcode-ios-CBXJAxfC.mjs → native-xcode-ios-8A-efGa5.mjs} +17 -17
  93. package/dist/{ndjson-CgE_o3Dl.mjs → ndjson-Dy2W4vRa.mjs} +2 -2
  94. package/dist/offload-worker.mjs +23 -24
  95. package/dist/{operation-exec-CUZifp6Z.mjs → operation-exec-Dp4FuIGR.mjs} +1 -1
  96. package/dist/{ownership-BjYvMNX_.mjs → ownership-DJCq48IJ.mjs} +3 -3
  97. package/dist/{page-CzEA_-5_.mjs → page-CQJlcj3x.mjs} +2 -2
  98. package/dist/{phone-reverses-lmj6kcPm.mjs → phone-reverses-BzTa8g1F.mjs} +18 -4
  99. package/dist/{placement-log-DIFPQAXp.mjs → placement-log-Cvhta5ib.mjs} +3 -3
  100. package/dist/{plan-placement-IiiSQhtz.mjs → plan-placement-WkZdccuN.mjs} +36 -10
  101. package/dist/{ports-C2_w1Z0Z.mjs → ports-UV4mJgyC.mjs} +3 -3
  102. package/dist/{prebuild-gmouvmJZ.mjs → prebuild-k8d_tqOq.mjs} +16 -17
  103. package/dist/{project-BUu6Is4k.mjs → project-Sui52ywT.mjs} +17 -17
  104. package/dist/{pull-request-CbNlkFok.mjs → pull-request-BzLmzAIQ.mjs} +2 -2
  105. package/dist/pull-requests.mjs +1 -1
  106. package/dist/{react-native-android-Cgg99REN.mjs → react-native-android-DANlGGQf.mjs} +32 -26
  107. package/dist/{react-native-build-oNwqF5Wb.mjs → react-native-build-jUHnIfZ9.mjs} +9 -9
  108. package/dist/{react-native-doctor-BhA3KUte.mjs → react-native-doctor-DCWRjS7K.mjs} +6 -6
  109. package/dist/{react-native-ios-B-nf4rjN.mjs → react-native-ios-BwMsFMXi.mjs} +23 -17
  110. package/dist/{recordings-BldzVKq0.mjs → recordings-iy0mkm7b.mjs} +1 -1
  111. package/dist/{registration-CN6nlhcD.mjs → registration-BwCe64PH.mjs} +1 -1
  112. package/dist/{reload-C9OduiNc.mjs → reload-Bj7QyuDy.mjs} +1 -1
  113. package/dist/{reload-CCR7KC_l.mjs → reload-c_uEyc_X.mjs} +16 -16
  114. package/dist/{remote-C8S7FlPF.mjs → remote-DFlpAqdV.mjs} +23 -10
  115. package/dist/{result-7F5hEwQJ.mjs → result-BIcXREyC.mjs} +32 -21
  116. package/dist/{run-BqB0v3pt.mjs → run-y2HwyNvg.mjs} +3 -3
  117. package/dist/{selection-BNPBZ6T5.mjs → selection-CXgM_VK_.mjs} +37 -29
  118. package/dist/{server-bare-A75rDIdD.mjs → server-bare-2N4vSRZG.mjs} +2 -2
  119. package/dist/{server-command-CssKFOD7.mjs → server-command-qneHPcRI.mjs} +6 -6
  120. package/dist/{server-expo-eQfOdydb.mjs → server-expo-Ccm9zB0o.mjs} +4 -4
  121. package/dist/server-expo-WulkyAwT.mjs +2 -0
  122. package/dist/{settings-CosW5X-S.mjs → settings-CY5JZlNN.mjs} +375 -62
  123. package/dist/{settings-OaGQF-ri.mjs → settings-Cfd_Q6ys.mjs} +24 -3
  124. package/dist/settings-payload-DwQiEMCn.mjs +257 -0
  125. package/dist/settings-write-CJpjkeAd.mjs +185 -0
  126. package/dist/settings.schema.json +51 -33
  127. package/dist/{simslim-Bax1qcxu.mjs → simslim-DCQwzoPy.mjs} +2 -2
  128. package/dist/{slot-launch-CYCEhfBY.mjs → slot-launch-BpmanOnk.mjs} +6 -6
  129. package/dist/{stage-jKQ7CP_B.mjs → stage-CmugrSG5.mjs} +2 -2
  130. package/dist/{start-C_wefNKB.mjs → start-CHH3zgIU.mjs} +1 -1
  131. package/dist/{start-_BdJ32cZ.mjs → start-DYLk6GXn.mjs} +27 -22
  132. package/dist/{state-C1i2nMOP.mjs → state-B2QJbDwz.mjs} +4 -4
  133. package/dist/{state-C4OdpOTF.mjs → state-BXC1Lfxw.mjs} +2 -2
  134. package/dist/{stats-tEpEEH-u.mjs → stats-DxKqhcGp.mjs} +5 -5
  135. package/dist/{stats-report-n2U5plhk.mjs → stats-report-B86Cv7Uc.mjs} +1 -1
  136. package/dist/{status-CRXz-Gtj.mjs → status-7ULDEJ1e.mjs} +1 -1
  137. package/dist/{status-DQrqodLj.mjs → status-BHCkOZga.mjs} +51 -28
  138. package/dist/{status-B9M7x4CZ.mjs → status-D_Ljc89p.mjs} +1 -1
  139. package/dist/{status-watch-B5wDIH_o.mjs → status-watch-BmsPqYSU.mjs} +18 -7
  140. package/dist/status-worker.mjs +1 -1
  141. package/dist/{stim-desktop-6Dqp-gjX.mjs → stim-desktop-CbWo21kb.mjs} +2 -2
  142. package/dist/{stop-DRqQVqDJ.mjs → stop-D-dGE9gh.mjs} +21 -20
  143. package/dist/{stop-ySNQHXMG.mjs → stop-DQvE-P76.mjs} +10 -6
  144. package/dist/{stop-CmdNhGlC.mjs → stop-b2pouxAO.mjs} +2 -2
  145. package/dist/supervisor-run.mjs +237 -63
  146. package/dist/{support-BGSCBoib.mjs → support-04IhBUTT.mjs} +6 -6
  147. package/dist/{support-CVOCp0Ft.mjs → support-BtlJKjyQ.mjs} +12 -11
  148. package/dist/{swiftpm-artifact-inputs-CfUzt_0Z.mjs → swiftpm-artifact-inputs-Cj2UpmPN.mjs} +4 -4
  149. package/dist/{swiftpm-macos-x6HPpl_2.mjs → swiftpm-macos-BhF2stxY.mjs} +11 -12
  150. package/dist/{toolchain-J4J8MrAr.mjs → toolchain-mLA_km-b.mjs} +321 -15
  151. package/dist/{toolchain-DoavrvIA.mjs → toolchain-qktiV8xc.mjs} +1 -1
  152. package/dist/{trigger-KVaWWNyn.mjs → trigger-DMou-X7j.mjs} +2 -2
  153. package/dist/trigger-qp2ji5wE.mjs +2 -0
  154. package/dist/{warm-progress-DvhTX_eu.mjs → warm-progress-B0Vpuvpl.mjs} +731 -727
  155. package/dist/{web-Bihbf7-u.mjs → web-BYmK0TjL.mjs} +9 -9
  156. package/dist/web-CrVLVQZY.mjs +2 -0
  157. package/dist/web-run.mjs +9 -9
  158. package/dist/windows-report.mjs +1 -1
  159. package/dist/{workspace-state-pUg3_RRA.mjs → workspace-state-kIZrdlSg.mjs} +3 -3
  160. package/dist/{workspaces-g2lhdBXF.mjs → workspaces-CmOcceDN.mjs} +1879 -74
  161. package/dist/{worktree-BKuklFh_.mjs → worktree-2dk5edza.mjs} +933 -534
  162. package/dist/worktree-nliAoHxB.mjs +2 -0
  163. package/package.json +7 -7
  164. package/dist/ccache-Bx5TbxwG.mjs +0 -143
  165. package/dist/eas-fallback-FwkeqqGi.mjs +0 -381
  166. package/dist/hosted-ios-D55QhYaD.mjs +0 -15
  167. package/dist/native-run-BfIDFKLH.mjs +0 -275
  168. package/dist/server-expo-wvcj5tiZ.mjs +0 -2
  169. package/dist/settings-write-QjYfSrjF.mjs +0 -360
  170. package/dist/trigger-DzJu208Q.mjs +0 -2
  171. package/dist/web-BFLueSh-.mjs +0 -2
  172. package/dist/worktree-Bm4cb-nU.mjs +0 -2
@@ -1,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-kIZrdlSg.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-C8R1KIvi.mjs";
6
+ import { c as gitQuiet, l as gitQuietAsync, t as runOperationFile } from "./operation-exec-Dp4FuIGR.mjs";
7
+ import { Pt as lockReason, Vt as repoRootAsync, Xt as worktreeLockAsync, Yt as worktreeLock, pt as resolveTailscaleBinary } from "./settings-CY5JZlNN.mjs";
8
+ import { a as macosRuntimeClaim, n as macosDir } from "./state-BXC1Lfxw.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-CsPs-7Mg.mjs";
10
+ import { c as pidExists, f as signalProcessTree, p as signalProcessTreeAsync } from "./metro-BnhXV2Mj.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-B2QJbDwz.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-DRMg4_mD.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-B1DYuOjh.mjs";
15
+ import { r as releaseBrowserPort } from "./named-ports-Phoi2POD.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, hostedAppAttempt, hostedNativeRecords, isJsonObject, lastNativeUseFrom, loadPerCore, 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 = [];
@@ -71,7 +104,8 @@ function toRecord(holder) {
71
104
  index: typeof details.index === "number" ? details.index : null,
72
105
  projectRoot: typeof details.projectRoot === "string" ? details.projectRoot : null,
73
106
  startedAt: holder.startedAt || null,
74
- logFile: typeof details.logFile === "string" ? details.logFile : null
107
+ logFile: typeof details.logFile === "string" ? details.logFile : null,
108
+ client: details.offloaded === true && typeof details.client === "string" ? details.client : null
75
109
  };
76
110
  }
77
111
  function tryAcquireBuildSlot({ max, root = null, logFile = null }) {
@@ -97,7 +131,8 @@ function tryAcquireBuildSlot({ max, root = null, logFile = null }) {
97
131
  index: got.index,
98
132
  projectRoot: root,
99
133
  startedAt: got.claim.startedAt,
100
- logFile
134
+ logFile,
135
+ client: null
101
136
  },
102
137
  claim: got.claim
103
138
  };
@@ -191,12 +226,37 @@ function listBuildSlots() {
191
226
  projectRoot: info?.projectRoot ?? null,
192
227
  startedAt: info?.startedAt ?? null,
193
228
  logFile: info?.logFile ?? null,
229
+ client: info?.client ?? null,
194
230
  alive: survey.live.length > 0,
195
231
  unresolved: survey.unresolved.length > 0
196
232
  });
197
233
  }
198
234
  return slots;
199
235
  }
236
+ /** The `concurrency.maxBuilds` slots a new build could take, `0` to `max - 1`, and who holds each busy one. */
237
+ function buildSlotUsage(max, slots = listBuildSlots()) {
238
+ const holders = slots.flatMap(({ index, alive, unresolved, projectRoot, client, startedAt }) => {
239
+ if (index === null || index >= max) return [];
240
+ if (alive) return [{
241
+ slot: index,
242
+ workspace: projectRoot,
243
+ client,
244
+ since: startedAt
245
+ }];
246
+ return unresolved ? [{
247
+ slot: index,
248
+ workspace: null,
249
+ client: null,
250
+ since: null,
251
+ unresolved: true
252
+ }] : [];
253
+ }).toSorted((a, b) => a.slot - b.slot);
254
+ return {
255
+ max,
256
+ inUse: holders.length,
257
+ holders
258
+ };
259
+ }
200
260
  //#endregion
201
261
  //#region src/devices/report-only-usage.ts
202
262
  function canonical$1(path) {
@@ -349,6 +409,10 @@ function deviceClaims(device) {
349
409
  workspace: record.workspace && canonical(record.workspace)
350
410
  }));
351
411
  }
412
+ var AgentDeviceRefusal = class extends Error {
413
+ reason;
414
+ stateDir;
415
+ };
352
416
  function explainFailure(error) {
353
417
  const stdout = error?.stdout;
354
418
  let failure;
@@ -358,13 +422,17 @@ function explainFailure(error) {
358
422
  failure = void 0;
359
423
  }
360
424
  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}`);
425
+ const reason = typeof failure.details?.reason === "string" ? failure.details.reason : void 0;
426
+ const stateDir = typeof failure.details?.stateDir === "string" ? failure.details.stateDir : void 0;
427
+ const refusal = new AgentDeviceRefusal(`agent-device refused: ${failure.message}${reason ? ` (${reason})` : ""}`);
428
+ refusal.reason = reason;
429
+ refusal.stateDir = stateDir;
430
+ return refusal;
363
431
  }
364
432
  function report(message) {
365
433
  ambientWrite("stderr", `${phaseLine("device", message)}\n`);
366
434
  }
367
- async function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot, operation) {
435
+ async function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot, operation, reportedBusy = /* @__PURE__ */ new Set()) {
368
436
  const exec = getExecutor();
369
437
  try {
370
438
  operation?.signal.throwIfAborted();
@@ -378,6 +446,7 @@ async function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot,
378
446
  };
379
447
  const stateDir = root === void 0 ? void 0 : workspaceAgentDeviceDir(root);
380
448
  const stateDirs = [void 0, ...stateDir && !["none", "dead"].includes(agentDeviceDaemon(stateDir)) ? [stateDir] : []];
449
+ const busy = /* @__PURE__ */ new Set();
381
450
  for (const dir of stateDirs) {
382
451
  const deadline = Date.now() + 15e3;
383
452
  const run = async (args) => {
@@ -431,9 +500,16 @@ async function closeOwnedDeviceSessions(device, stillOwned, workspace, repoRoot,
431
500
  } catch (error) {
432
501
  if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
433
502
  operation?.signal.throwIfAborted();
434
- report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
503
+ if (error instanceof AgentDeviceRefusal && error.reason === "daemon-busy") busy.add(error.stateDir ?? dir ?? "");
504
+ else report(`could not list agent-device sessions for ${device.platform === "ios" ? device.id : device.id ?? device.avdName}: ${error.message}`);
435
505
  }
436
506
  }
507
+ const unreported = [...busy].filter((dir) => !reportedBusy.has(dir));
508
+ if (unreported.length > 0) {
509
+ for (const dir of unreported) reportedBusy.add(dir);
510
+ const stop = unreported.map((dir) => `agent-device daemon stop${dir ? ` --state-dir ${quotedPath$1(dir)}` : ""}`);
511
+ 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 ")}`);
512
+ }
437
513
  } catch (error) {
438
514
  if (error instanceof WorkspaceProcessCleanupError || error instanceof Error && "reportCleanup" in error) throw error;
439
515
  operation?.signal.throwIfAborted();
@@ -652,6 +728,7 @@ async function parkOwnedIosSim(settled, udid, park, operation) {
652
728
  }
653
729
  async function teardownOwnedIosSim(udid, { del = false, label, park, workspace, shutdownClock = {}, operation } = {}) {
654
730
  let parkFallback;
731
+ const reportedBusy = /* @__PURE__ */ new Set();
655
732
  try {
656
733
  const resolved = await resolveOwnedIosSimNow(udid, operation);
657
734
  if (resolved.notOwned) return {
@@ -663,7 +740,7 @@ async function teardownOwnedIosSim(udid, { del = false, label, park, workspace,
663
740
  await closeOwnedDeviceSessions({
664
741
  platform: "ios",
665
742
  id: udid
666
- }, async () => Boolean((await resolveOwnedIosSimNow(udid, operation)).sim), workspace, void 0, operation);
743
+ }, async () => Boolean((await resolveOwnedIosSimNow(udid, operation)).sim), workspace, void 0, operation, reportedBusy);
667
744
  const current = await resolveOwnedIosSimNow(udid, operation);
668
745
  if (current.missing) return { status: "missing" };
669
746
  if (current.notOwned) return {
@@ -721,7 +798,7 @@ async function teardownOwnedIosSim(udid, { del = false, label, park, workspace,
721
798
  if (root && settled.sim) await closeOwnedDeviceSessions({
722
799
  platform: "ios",
723
800
  id: udid
724
- }, async () => (await resolveOwnedIosSimNow(udid, operation)).sim?.state === "Shutdown", workspace, root, operation);
801
+ }, async () => (await resolveOwnedIosSimNow(udid, operation)).sim?.state === "Shutdown", workspace, root, operation, reportedBusy);
725
802
  return {
726
803
  status: "torn-down",
727
804
  label: label ?? sim.name ?? udid
@@ -849,6 +926,7 @@ async function teardownClaimedAvd(avdName, claim, { operation, del = false, wipe
849
926
  signal: operation?.signal
850
927
  }) }) {
851
928
  let parkFallback;
929
+ const reportedBusy = /* @__PURE__ */ new Set();
852
930
  const resolveAvd = async (name) => {
853
931
  operation?.signal.throwIfAborted();
854
932
  const result = await resolve(name);
@@ -890,7 +968,7 @@ async function teardownClaimedAvd(avdName, claim, { operation, del = false, wipe
890
968
  platform: "android",
891
969
  id: serial,
892
970
  avdName
893
- }, stillOwned, workspace, void 0, operation);
971
+ }, stillOwned, workspace, void 0, operation, reportedBusy);
894
972
  if (!await stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
895
973
  await waitForShutdown(avdName, async (timeoutMs) => {
896
974
  if (!operation) (0, ownership_claim_exports.markClaimChildPending)(claim);
@@ -911,7 +989,7 @@ async function teardownClaimedAvd(avdName, claim, { operation, del = false, wipe
911
989
  platform: "android",
912
990
  id: serial,
913
991
  avdName
914
- }, stillStopped, workspace, root, operation);
992
+ }, stillStopped, workspace, root, operation, reportedBusy);
915
993
  }
916
994
  } else {
917
995
  await closeOwnedDeviceSessions({
@@ -1149,6 +1227,1592 @@ async function teardownBrowserHeld(root, { deleteProfile = false, operation } =
1149
1227
  }
1150
1228
  }
1151
1229
  //#endregion
1230
+ //#region src/engine/stats.ts
1231
+ const PLACEMENT_MAX_AGE_MS = 6048e5;
1232
+ function emptyStats() {
1233
+ return {
1234
+ version: STATS_VERSION,
1235
+ machine: {},
1236
+ projects: {}
1237
+ };
1238
+ }
1239
+ function updateStats(record, run, now) {
1240
+ const at = new Date(now).toISOString();
1241
+ const durationMs = wholeMs$1(run.durationMs);
1242
+ const phases = {
1243
+ coldBuildMs: wholeMs$1(run.coldBuildMs),
1244
+ podsMs: wholeMs$1(run.podsMs)
1245
+ };
1246
+ const projects = { ...record.projects };
1247
+ const scope = { ...projects[run.projectKey] };
1248
+ const machine = { ...record.machine };
1249
+ const before = scope[run.platform] ?? null;
1250
+ const credit = creditMs(before, run, durationMs);
1251
+ scope[run.platform] = applyRun(before, run, {
1252
+ at,
1253
+ durationMs,
1254
+ credit,
1255
+ phases
1256
+ });
1257
+ projects[run.projectKey] = scope;
1258
+ machine[run.platform] = applyRun(machine[run.platform] ?? null, run, {
1259
+ at,
1260
+ durationMs,
1261
+ credit,
1262
+ phases
1263
+ });
1264
+ const history = { ...record.history };
1265
+ if (run.phases && !run.failed && !run.waitedForBuild && !run.offloadedTo) {
1266
+ const outcome = isHit(run.cacheHit) ? "hit" : "cold";
1267
+ const project = { ...history[run.projectKey] };
1268
+ const lists = { ...project[run.platform] };
1269
+ const sample = {
1270
+ at,
1271
+ durationMs,
1272
+ phases: wholePhases(run.phases),
1273
+ ...run.deviceSetup === void 0 ? {} : { deviceSetup: run.deviceSetup }
1274
+ };
1275
+ lists[outcome] = trimSamples([...lists[outcome] ?? [], sample]);
1276
+ project[run.platform] = lists;
1277
+ history[run.projectKey] = project;
1278
+ }
1279
+ let placements = record.placements;
1280
+ let buildMachines = record.buildMachines;
1281
+ if (run.placement) {
1282
+ const placement = buildPlacement(run, before, at);
1283
+ placements = trimPlacements([...placements ?? [], placement], now);
1284
+ if (placement.machine && placement.decision !== "here") buildMachines = {
1285
+ ...buildMachines,
1286
+ [placement.machine]: applyPlacement(buildMachines?.[placement.machine], placement)
1287
+ };
1288
+ }
1289
+ const capacityRefusals = trimCapacityRefusals(record.capacityRefusals ?? [], now);
1290
+ const capacityWaits = trimCapacityWaits(record.capacityWaits ?? [], now);
1291
+ return {
1292
+ version: STATS_VERSION,
1293
+ machine,
1294
+ projects,
1295
+ ...Object.keys(history).length ? { history } : {},
1296
+ ...placements?.length ? { placements } : {},
1297
+ ...capacityRefusals.length ? { capacityRefusals } : {},
1298
+ ...capacityWaits.length ? { capacityWaits } : {},
1299
+ ...buildMachines && Object.keys(buildMachines).length ? { buildMachines } : {}
1300
+ };
1301
+ }
1302
+ function buildPlacement(run, before, at) {
1303
+ const placement = run.placement;
1304
+ const buildMs = wholeMs$1(placement.decision === "offloaded" ? placement.buildMs : run.coldBuildMs);
1305
+ const localEstimateMs = wholeMs$1(before?.lastColdBuildMs);
1306
+ const slotWaitMs = wholeMs$1(placement.slotWaitMs);
1307
+ const deviceSlotWaitMs = wholeMs$1(placement.deviceSlotWaitMs);
1308
+ return {
1309
+ at,
1310
+ project: run.projectKey,
1311
+ platform: run.platform,
1312
+ decision: placement.decision,
1313
+ reason: placement.reason,
1314
+ ...placement.machine ? { machine: placement.machine } : {},
1315
+ ...buildMs > 0 ? { buildMs } : {},
1316
+ ...slotWaitMs > 0 ? { slotWaitMs } : {},
1317
+ ...deviceSlotWaitMs > 0 ? { deviceSlotWaitMs } : {},
1318
+ ...localEstimateMs > 0 ? { localEstimateMs } : {},
1319
+ ...run.failed ? { failed: true } : {}
1320
+ };
1321
+ }
1322
+ function applyPlacement(totals, placement) {
1323
+ const next = totals ? { ...totals } : {
1324
+ offloaded: 0,
1325
+ offloadedMs: 0,
1326
+ savedMs: 0,
1327
+ fallbacks: 0
1328
+ };
1329
+ if (placement.decision === "fell-back") {
1330
+ next.fallbacks += 1;
1331
+ next.lastFallbackAt = placement.at;
1332
+ return next;
1333
+ }
1334
+ next.offloaded += 1;
1335
+ next.offloadedMs += placement.buildMs ?? 0;
1336
+ if (placement.localEstimateMs && placement.buildMs) next.savedMs += placement.localEstimateMs - placement.buildMs;
1337
+ next.lastOffloadAt = placement.at;
1338
+ return next;
1339
+ }
1340
+ function trimPlacements(placements, now) {
1341
+ return placements.filter((each) => now - Date.parse(each.at) <= PLACEMENT_MAX_AGE_MS).slice(-100);
1342
+ }
1343
+ function recordRunStats(run, now) {
1344
+ return withConfigLock$1(() => {
1345
+ const path = statsFile();
1346
+ const loaded = loadForUpdate(path, now);
1347
+ if (loaded.newerVersion !== null) return {
1348
+ recorded: false,
1349
+ note: `Run statistics at ${path} are version ${loaded.newerVersion}, which this Stim does not understand, so this run was not recorded.`
1350
+ };
1351
+ writeStats(path, updateStats(loaded.record, run, now));
1352
+ return {
1353
+ recorded: true,
1354
+ note: loaded.note
1355
+ };
1356
+ });
1357
+ }
1358
+ function recordCapacityRefusal(event, now) {
1359
+ try {
1360
+ withConfigLock$1(() => {
1361
+ const path = statsFile();
1362
+ const loaded = loadForUpdate(path, now, false);
1363
+ if (!loaded.record || loaded.newerVersion !== null) return;
1364
+ const capacityRefusals = trimCapacityRefusals([...loaded.record.capacityRefusals ?? [], {
1365
+ ...event,
1366
+ at: new Date(now).toISOString(),
1367
+ kind: "device"
1368
+ }], now);
1369
+ writeStats(path, {
1370
+ ...loaded.record,
1371
+ capacityRefusals
1372
+ });
1373
+ });
1374
+ } catch {}
1375
+ }
1376
+ function recordCapacityWait(event, now) {
1377
+ try {
1378
+ withConfigLock$1(() => {
1379
+ const path = statsFile();
1380
+ const loaded = loadForUpdate(path, now, false);
1381
+ if (!loaded.record || loaded.newerVersion !== null) return;
1382
+ const capacityWaits = trimCapacityWaits([...loaded.record.capacityWaits ?? [], {
1383
+ platform: event.platform,
1384
+ max: event.max,
1385
+ workspace: event.workspace,
1386
+ ms: wholeMs$1(event.ms),
1387
+ ...event.reclaimed && Number.isInteger(event.reclaimed) && event.reclaimed > 0 ? { reclaimed: event.reclaimed } : {},
1388
+ at: new Date(now).toISOString(),
1389
+ kind: "device-wait"
1390
+ }], now);
1391
+ writeStats(path, {
1392
+ ...loaded.record,
1393
+ capacityWaits
1394
+ });
1395
+ });
1396
+ } catch {}
1397
+ }
1398
+ function createRunRecorder({ platform, write, now, note, phases, deviceSetup }) {
1399
+ let projectKey = null;
1400
+ let cacheKey = null;
1401
+ let coldBuildMs = 0;
1402
+ let podsMs = 0;
1403
+ let placement = null;
1404
+ let deviceSlotWaitMs = 0;
1405
+ let recorded = false;
1406
+ return {
1407
+ setProject(key) {
1408
+ projectKey = key;
1409
+ },
1410
+ setCacheKey(key) {
1411
+ cacheKey = key;
1412
+ },
1413
+ setBuildMs(ms) {
1414
+ coldBuildMs = wholeMs$1(ms);
1415
+ },
1416
+ setPodsMs(ms) {
1417
+ podsMs = wholeMs$1(ms);
1418
+ },
1419
+ addDeviceSlotWaitMs(ms) {
1420
+ deviceSlotWaitMs += wholeMs$1(ms);
1421
+ },
1422
+ deviceSlotWaitMs() {
1423
+ return deviceSlotWaitMs || void 0;
1424
+ },
1425
+ setPlacement(next) {
1426
+ placement = next;
1427
+ },
1428
+ record({ failed, cacheHit = false, waited = null, durationMs, offloadedTo = null }) {
1429
+ if (!projectKey || !cacheKey || recorded) return;
1430
+ recorded = true;
1431
+ const ran = failed ? {} : phases?.() ?? {};
1432
+ try {
1433
+ const outcome = write({
1434
+ platform,
1435
+ projectKey,
1436
+ failed,
1437
+ cacheHit,
1438
+ waitedForBuild: Boolean(waited),
1439
+ durationMs,
1440
+ ...offloadedTo ? { offloadedTo } : {},
1441
+ ...coldBuildMs > 0 ? { coldBuildMs } : {},
1442
+ ...podsMs > 0 ? { podsMs } : {},
1443
+ ...Object.keys(ran).length ? { phases: ran } : {},
1444
+ ...failed || deviceSetup?.() === void 0 ? {} : { deviceSetup: deviceSetup() },
1445
+ ...placement ? { placement: {
1446
+ ...placement,
1447
+ ...deviceSlotWaitMs > 0 ? { deviceSlotWaitMs } : {}
1448
+ } } : {}
1449
+ }, now());
1450
+ if (outcome?.note) note(outcome.note);
1451
+ } catch (error) {
1452
+ note(`Run statistics could not be recorded: ${error?.message || error}`);
1453
+ }
1454
+ }
1455
+ };
1456
+ }
1457
+ function loadForUpdate(path, now, recoverCorrupt = true) {
1458
+ let text;
1459
+ try {
1460
+ text = readFileSync(path, "utf-8");
1461
+ } catch {
1462
+ return {
1463
+ record: emptyStats(),
1464
+ note: null,
1465
+ newerVersion: null
1466
+ };
1467
+ }
1468
+ const parsed = decodeStats(text);
1469
+ if (!parsed.record && parsed.newerVersion === null) {
1470
+ if (!recoverCorrupt) return {
1471
+ record: null,
1472
+ note: null,
1473
+ newerVersion: null
1474
+ };
1475
+ const aside = `${path}.corrupt-${now}`;
1476
+ renameSync(path, aside);
1477
+ return {
1478
+ record: emptyStats(),
1479
+ note: `Run statistics at ${path} could not be read, so they were moved to ${aside} and a new file was started.`,
1480
+ newerVersion: null
1481
+ };
1482
+ }
1483
+ if (parsed.newerVersion !== null) return {
1484
+ record: emptyStats(),
1485
+ note: null,
1486
+ newerVersion: parsed.newerVersion
1487
+ };
1488
+ return {
1489
+ record: parsed.record,
1490
+ note: null,
1491
+ newerVersion: null
1492
+ };
1493
+ }
1494
+ function writeStats(path, record) {
1495
+ const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
1496
+ writeFileSync(tmp, `${JSON.stringify(record)}\n`);
1497
+ try {
1498
+ renameSync(tmp, path);
1499
+ } catch (error) {
1500
+ rmSync(tmp, { force: true });
1501
+ throw error;
1502
+ }
1503
+ }
1504
+ function applyRun(bucket, run, { at, durationMs, credit, phases }) {
1505
+ const next = bucket ? { ...bucket } : {
1506
+ runs: 0,
1507
+ failed: 0,
1508
+ hits: 0,
1509
+ misses: 0,
1510
+ coldRuns: 0,
1511
+ coldRunMs: 0,
1512
+ hitRuns: 0,
1513
+ hitRunMs: 0,
1514
+ timeSavedMs: 0,
1515
+ firstRunAt: at,
1516
+ lastRunAt: at
1517
+ };
1518
+ next.runs += 1;
1519
+ next.lastRunAt = at;
1520
+ if (phases.coldBuildMs > 0) next.lastColdBuildMs = phases.coldBuildMs;
1521
+ if (phases.podsMs > 0) next.lastPodsMs = phases.podsMs;
1522
+ if (run.failed) {
1523
+ next.failed += 1;
1524
+ return next;
1525
+ }
1526
+ if (isHit(run.cacheHit)) {
1527
+ next.hits += 1;
1528
+ if (!run.waitedForBuild) {
1529
+ next.hitRuns += 1;
1530
+ next.hitRunMs += durationMs;
1531
+ next.timeSavedMs += credit;
1532
+ }
1533
+ return next;
1534
+ }
1535
+ next.misses += 1;
1536
+ if (run.offloadedTo) {
1537
+ next.offloadedRuns = (next.offloadedRuns ?? 0) + 1;
1538
+ next.offloadedRunMs = (next.offloadedRunMs ?? 0) + durationMs;
1539
+ next.lastOffloadHost = run.offloadedTo;
1540
+ return next;
1541
+ }
1542
+ next.coldRuns += 1;
1543
+ next.coldRunMs += durationMs;
1544
+ return next;
1545
+ }
1546
+ function creditMs(bucket, run, durationMs) {
1547
+ if (run.failed || !isHit(run.cacheHit) || run.waitedForBuild) return 0;
1548
+ if (!bucket || bucket.coldRuns <= 0) return 0;
1549
+ return Math.max(0, Math.round(bucket.coldRunMs / bucket.coldRuns) - durationMs);
1550
+ }
1551
+ function isHit(cacheHit) {
1552
+ return cacheHit === "local" || cacheHit === "remote";
1553
+ }
1554
+ function wholeMs$1(value) {
1555
+ const ms = Number(value);
1556
+ return Number.isFinite(ms) && ms > 0 ? Math.round(ms) : 0;
1557
+ }
1558
+ //#endregion
1559
+ //#region src/engine/build-detail.ts
1560
+ const LINE_MAX = 160;
1561
+ const TARGET_GRAPH = /^note: Target dependency graph \((\d+) targets?\)/;
1562
+ const XCODE_ACTION = /^([A-Z][A-Za-z]+)\s(.*)\s\(in target '([^']+)' from project '([^']+)'\)$/;
1563
+ const XCODE_CONFIGURE = /^(ComputeTargetDependencyGraph|CreateBuildDescription|CreateBuildRequest|Resolve Package Graph|Prepare packages)$/;
1564
+ const GRADLE_TASK = /^> Task (:\S+)/;
1565
+ /**
1566
+ * xcodebuild actions that end a target's work: a clean build touches each product last, and an incremental build
1567
+ * that touches nothing still signs framework products last. Targets that end any other way are not counted.
1568
+ */
1569
+ const XCODE_TARGET_DONE = /^(Touch|RegisterExecutionPolicyException|CodeSign)$/;
1570
+ const GRADLE_CONFIGURE = /^> Configure project /;
1571
+ /**
1572
+ * SwiftPM prints `[done / total] name` once it plans the build (with U+2009 thin spaces around the slash in Swift 6.4
1573
+ * output), `[done/total] Compiling ...` in older toolchains; Swift 6.4 names the target being worked on, and
1574
+ * `<product>-product` is the link of a product.
1575
+ */
1576
+ const SWIFTPM_PROGRESS = /^\[(\d+)\s*\/\s*(\d+)\](?:\s+(.*))?$/;
1577
+ const SWIFTPM_CONFIGURE = /^(Fetching|Fetched|Computing version|Computed|Creating working copy|Working copy of)\b|^\[(Pre-planning|Planning|Using on-disk description)/;
1578
+ const XCODE_STEPS = [
1579
+ [/^(CpResource|CopyPNGFile|CopyStringsFile|CompileAssetCatalog\w*|CompileStoryboard|CompileXIB|LinkStoryboards|ProcessInfoPlistFile)$/, "resources"],
1580
+ [/^(Compile\w*|Swift\w*|ScanDependencies|Precompile\w*)$/, "compile"],
1581
+ [/^(Ld|Libtool|CreateUniversalBinary|GenerateDSYMFile)$/, "link"],
1582
+ [/^PhaseScriptExecution$/, "script"],
1583
+ [/^CodeSign$/, "sign"]
1584
+ ];
1585
+ const GRADLE_STEPS = [
1586
+ [/[Dd]ex/, "dex"],
1587
+ [/^(createBundle\w*JsAndAssets|generateCodegen\w*)$/, "script"],
1588
+ [/(Resources?|Assets|Manifest|ResValues|RFile|JavaRes)$/, "resources"],
1589
+ [/^(compile|kapt|ksp|javaPreCompile|buildCMake|configureCMake|externalNativeBuild)/, "compile"],
1590
+ [/^(package|assemble|install|zipalign|validateSigning|merge\w*NativeLibs|strip\w*Symbols)/, "package"]
1591
+ ];
1592
+ function stepOf(name, table) {
1593
+ return table.find(([pattern]) => pattern.test(name))?.[1] ?? null;
1594
+ }
1595
+ function clip(text) {
1596
+ return text.length > LINE_MAX ? `${text.slice(0, 157)}...` : text;
1597
+ }
1598
+ function unescape(token) {
1599
+ return token.replace(/\\(.)/g, "$1");
1600
+ }
1601
+ function baseName(path) {
1602
+ return path.split("/").findLast(Boolean) ?? path;
1603
+ }
1604
+ /**
1605
+ * An xcodebuild action line in short form: the action, the file it works on and its target, as in
1606
+ * `CompileC sqlite3.c (sqlite3)`. The file is the last absolute path in the arguments, which xcodebuild prints
1607
+ * as the source for compile, copy and plist steps and as the product for link and sign steps; a script phase
1608
+ * shows its name instead.
1609
+ */
1610
+ function shortXcodeAction(action, args, target) {
1611
+ const tokens = args.match(/(?:\\.|\S)+/g) ?? [];
1612
+ const subject = action === "PhaseScriptExecution" ? unescape(tokens[0] ?? "") : baseName(unescape(tokens.findLast((token) => token.startsWith("/")) ?? ""));
1613
+ return clip(`${action}${subject ? ` ${subject}` : ""} (${target})`);
1614
+ }
1615
+ function swiftpmStep(text) {
1616
+ return /^Linking\b|-product$/.test(text) ? "link" : "compile";
1617
+ }
1618
+ /** What one line of xcodebuild, Gradle or SwiftPM output says about the build's progress, or null for any other line. */
1619
+ function parseBuildToolLine(msg) {
1620
+ const counted = SWIFTPM_PROGRESS.exec(msg);
1621
+ if (counted) {
1622
+ const text = counted[3]?.trim() ?? "";
1623
+ return {
1624
+ kind: "count",
1625
+ done: Number(counted[1]),
1626
+ total: Number(counted[2]),
1627
+ step: swiftpmStep(text),
1628
+ line: text ? clip(text) : null
1629
+ };
1630
+ }
1631
+ if (SWIFTPM_CONFIGURE.test(msg)) return { kind: "configure" };
1632
+ const graph = TARGET_GRAPH.exec(msg);
1633
+ if (graph) return {
1634
+ kind: "total",
1635
+ unit: "targets",
1636
+ total: Number(graph[1])
1637
+ };
1638
+ if (XCODE_CONFIGURE.test(msg) || GRADLE_CONFIGURE.test(msg)) return { kind: "configure" };
1639
+ const action = XCODE_ACTION.exec(msg);
1640
+ if (action) {
1641
+ const [, name, args, target, project] = action;
1642
+ const step = stepOf(name, XCODE_STEPS);
1643
+ return {
1644
+ kind: "unit",
1645
+ unit: "targets",
1646
+ id: `${project}/${target}`,
1647
+ step,
1648
+ line: step ? shortXcodeAction(name, args, target) : null,
1649
+ finished: XCODE_TARGET_DONE.test(name)
1650
+ };
1651
+ }
1652
+ const task = GRADLE_TASK.exec(msg);
1653
+ if (task) {
1654
+ const path = task[1];
1655
+ return {
1656
+ kind: "unit",
1657
+ unit: "tasks",
1658
+ id: path,
1659
+ step: stepOf(path.slice(path.lastIndexOf(":") + 1), GRADLE_STEPS),
1660
+ line: clip(`> Task ${path}`),
1661
+ finished: false
1662
+ };
1663
+ }
1664
+ return null;
1665
+ }
1666
+ /**
1667
+ * Accumulates a build's tool output into its detail: xcodebuild counts the distinct targets it finished against the
1668
+ * total its dependency graph note gives; Gradle counts the task lines it printed and has no total; SwiftPM reports
1669
+ * its own `[done / total]` counts, which the parser passes through.
1670
+ */
1671
+ function createBuildDetailParser() {
1672
+ let seen = false;
1673
+ let step = null;
1674
+ let unit = null;
1675
+ let total = null;
1676
+ let line = null;
1677
+ let tasks = 0;
1678
+ let counted = null;
1679
+ const targets = /* @__PURE__ */ new Set();
1680
+ return {
1681
+ push(msg) {
1682
+ const parsed = parseBuildToolLine(msg);
1683
+ if (!parsed) return false;
1684
+ seen = true;
1685
+ if (parsed.kind === "total") {
1686
+ unit = "targets";
1687
+ total = parsed.total;
1688
+ return true;
1689
+ }
1690
+ if (parsed.kind === "configure") {
1691
+ step = "configure";
1692
+ return true;
1693
+ }
1694
+ if (parsed.kind === "count") {
1695
+ unit = "steps";
1696
+ counted = {
1697
+ done: parsed.done,
1698
+ total: parsed.total
1699
+ };
1700
+ step = parsed.step;
1701
+ if (parsed.line) line = parsed.line;
1702
+ return true;
1703
+ }
1704
+ unit = parsed.unit;
1705
+ if (parsed.unit === "tasks") tasks += 1;
1706
+ else if (parsed.finished) targets.add(parsed.id);
1707
+ if (parsed.step) step = parsed.step;
1708
+ if (parsed.line) line = parsed.line;
1709
+ return true;
1710
+ },
1711
+ detail(updatedAt) {
1712
+ if (!seen) return null;
1713
+ if (unit === "steps" && counted) return {
1714
+ step,
1715
+ unit,
1716
+ done: Math.min(counted.done, counted.total),
1717
+ total: counted.total,
1718
+ line,
1719
+ updatedAt
1720
+ };
1721
+ const done = unit === "tasks" ? tasks : unit === "targets" ? targets.size : null;
1722
+ return {
1723
+ step,
1724
+ unit,
1725
+ done: done !== null && total !== null ? Math.min(done, total) : done,
1726
+ total: unit === "tasks" ? null : total,
1727
+ line,
1728
+ updatedAt
1729
+ };
1730
+ }
1731
+ };
1732
+ }
1733
+ //#endregion
1734
+ //#region src/stim-version.ts
1735
+ const STIM_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
1736
+ //#endregion
1737
+ //#region src/engine/build-ledger.ts
1738
+ const LOCK_WAIT_MS = 2e3;
1739
+ const GIT_TIMEOUT_MS = 2e3;
1740
+ function text(value) {
1741
+ return typeof value === "string" && value !== "" ? value : null;
1742
+ }
1743
+ function wholeMs(value) {
1744
+ const ms = Number(value);
1745
+ return Number.isFinite(ms) && ms > 0 ? Math.round(ms) : 0;
1746
+ }
1747
+ function wholePhaseMs(phases) {
1748
+ const out = {};
1749
+ for (const [name, ms] of Object.entries(phases ?? {})) if (wholeMs(ms) > 0) out[name] = wholeMs(ms);
1750
+ return out;
1751
+ }
1752
+ /** The remedy a thrown error carries, when it carries one. */
1753
+ function errorRemedy(error) {
1754
+ const remedy = typeof error === "object" && error !== null && "remedy" in error ? error.remedy : void 0;
1755
+ return typeof remedy === "string" && remedy !== "" ? remedy : void 0;
1756
+ }
1757
+ function buildLedgerRecord(run) {
1758
+ const { record, extras, outcome } = run;
1759
+ const platform = record.platform === "ios" || record.platform === "android" ? record.platform : "macos";
1760
+ const configuration = text(record.configuration);
1761
+ const durationMs = wholeMs(record.durationMs);
1762
+ const startedAt = Date.parse(String(record.startedAt));
1763
+ const fallback = text(record.offloadFallback);
1764
+ const ranHere = fallback !== null || record.builtOn === "here";
1765
+ const machine = text(record.offloadedTo) ?? (ranHere ? null : run.offload?.host) ?? null;
1766
+ const placement = machine ? {
1767
+ where: "offloaded",
1768
+ machine
1769
+ } : fallback ? {
1770
+ where: "local",
1771
+ fellBack: true,
1772
+ reason: fallback
1773
+ } : { where: "local" };
1774
+ const hit = record.cacheHit === "local" || record.cacheHit === "remote" ? record.cacheHit : null;
1775
+ const missReason = text(record.missReason);
1776
+ const cache = {
1777
+ result: record.cacheSkipped === true ? "bypassed" : hit ? "hit" : "miss",
1778
+ ...hit && record.cacheSkipped !== true ? { source: hit } : {},
1779
+ key: text(record.cacheKey),
1780
+ fingerprint: text(record.fingerprint),
1781
+ ...missReason && !hit ? { missReason } : {}
1782
+ };
1783
+ const offloadPhases = wholePhaseMs(run.offload?.phaseMs);
1784
+ const scheme = text(extras.scheme);
1785
+ const device = text(extras.device?.name);
1786
+ const runtime = text(extras.device?.runtime);
1787
+ const failure = outcome === "succeeded" ? void 0 : {
1788
+ code: text(record.errorCode),
1789
+ message: text(extras.message),
1790
+ remedy: text(extras.remedy),
1791
+ phase: run.phase
1792
+ };
1793
+ const bootStartedAt = run.boot ? Date.parse(run.boot.startedAt) : NaN;
1794
+ const where = placement.machine ? `on ${placement.machine}` : "here";
1795
+ const cacheText = cache.result === "hit" ? `cache hit${cache.source === "remote" ? " (remote)" : ""}` : cache.result;
1796
+ const summary = `${platform} build ${outcome} ${where} in ${formatElapsed(durationMs)} (${cacheText})`;
1797
+ const why = failure ? `: ${[failure.code, failure.message].filter(Boolean).join(": ") || "no reason recorded"}` : "";
1798
+ return {
1799
+ ts: run.now,
1800
+ src: "builds",
1801
+ level: outcome === "succeeded" ? "info" : outcome === "cancelled" ? "warn" : "error",
1802
+ msg: `${summary}${why}`,
1803
+ event: "build",
1804
+ id: run.id,
1805
+ startedAt: Number.isFinite(startedAt) ? startedAt : run.now - durationMs,
1806
+ project: run.root,
1807
+ workspace: run.workspace,
1808
+ branch: run.branch,
1809
+ platform,
1810
+ ...scheme ? { scheme } : {},
1811
+ ...platform === "android" && configuration ? { variant: configuration } : {},
1812
+ configuration,
1813
+ placement,
1814
+ cache,
1815
+ durationMs,
1816
+ phases: wholePhaseMs(run.phases),
1817
+ ...Object.keys(offloadPhases).length ? { offloadPhases } : {},
1818
+ outcome,
1819
+ ...failure ? { failure } : {},
1820
+ ...device ? { device: {
1821
+ name: device,
1822
+ ...runtime ? { runtime } : {}
1823
+ } } : {},
1824
+ ...run.loadPerCore === void 0 ? {} : { loadPerCore: run.loadPerCore },
1825
+ ...run.boot && Number.isFinite(bootStartedAt) ? { boot: {
1826
+ startedAt: bootStartedAt,
1827
+ loadPerCore: run.boot.loadPerCore
1828
+ } } : {},
1829
+ stimVersion: run.version
1830
+ };
1831
+ }
1832
+ function lineId(line) {
1833
+ try {
1834
+ const parsed = JSON.parse(line);
1835
+ return typeof parsed === "object" && parsed !== null && "id" in parsed ? parsed.id : void 0;
1836
+ } catch {
1837
+ return;
1838
+ }
1839
+ }
1840
+ /**
1841
+ * The ledger file's text after appending `line`, keeping only the newest `cap` complete records. A run that records
1842
+ * twice (an Android launch that fails after the build was recorded) replaces its earlier record.
1843
+ */
1844
+ function appendCapped(existing, line, cap = BUILD_LEDGER_CAP) {
1845
+ const id = lineId(line);
1846
+ const lines = existing.split("\n").filter((each) => each.trim() !== "" && (id === void 0 || lineId(each) !== id));
1847
+ lines.push(line);
1848
+ return `${lines.slice(-cap).join("\n")}\n`;
1849
+ }
1850
+ function appendBuildLedger(record, file = buildLedgerFile()) {
1851
+ withDirLock(buildLedgerLock(), () => {
1852
+ let existing = "";
1853
+ try {
1854
+ existing = readFileSync(file, "utf-8");
1855
+ } catch {}
1856
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
1857
+ writeFileSync(tmp, appendCapped(existing, JSON.stringify(record)));
1858
+ try {
1859
+ renameSync(tmp, file);
1860
+ } catch (error) {
1861
+ rmSync(tmp, { force: true });
1862
+ throw error;
1863
+ }
1864
+ }, {
1865
+ waitMs: LOCK_WAIT_MS,
1866
+ ensureParent: () => {
1867
+ mkdirSync(dirname(file), { recursive: true });
1868
+ mkdirSync(dirname(buildLedgerLock()), { recursive: true });
1869
+ }
1870
+ });
1871
+ }
1872
+ /** Records one finished build in the ledger. A failure to do so never changes the run, so it is ignored. */
1873
+ function writeBuildLedger(root, run) {
1874
+ try {
1875
+ const branch = gitQuiet(root, [
1876
+ "symbolic-ref",
1877
+ "--quiet",
1878
+ "--short",
1879
+ "HEAD"
1880
+ ], { timeoutMs: GIT_TIMEOUT_MS })?.trim() || null;
1881
+ appendBuildLedger(buildLedgerRecord({
1882
+ ...run,
1883
+ id: run.id ?? randomUUID(),
1884
+ root,
1885
+ workspace: workspaceName(root),
1886
+ branch,
1887
+ version: STIM_VERSION
1888
+ }));
1889
+ } catch {}
1890
+ }
1891
+ //#endregion
1892
+ //#region src/engine/build-progress.ts
1893
+ const ACTIVE_BUILD_KEY = "activeBuild";
1894
+ const NO_BUILD_PROGRESS = {
1895
+ step: () => {},
1896
+ activity: () => {},
1897
+ estimate: () => {},
1898
+ miss: () => {},
1899
+ hit: () => {},
1900
+ deviceSetup: () => {},
1901
+ deviceSetupKnown: () => void 0,
1902
+ place: () => {},
1903
+ waitingOn: () => {},
1904
+ waitingFor: () => {},
1905
+ bootStarted: () => {},
1906
+ output: () => {},
1907
+ durations: () => ({}),
1908
+ phase: () => null,
1909
+ steps: () => null,
1910
+ clear: () => {}
1911
+ };
1912
+ /** `writer`, with each record it writes also passed to `progress.output`. */
1913
+ function tapBuildLog(writer, progress) {
1914
+ if (progress === NO_BUILD_PROGRESS) return writer;
1915
+ return {
1916
+ get file() {
1917
+ return writer.file;
1918
+ },
1919
+ write(record) {
1920
+ progress.output(record);
1921
+ return writer.write(record);
1922
+ },
1923
+ close: () => writer.close(),
1924
+ get written() {
1925
+ return writer.written;
1926
+ },
1927
+ get dropped() {
1928
+ return writer.dropped;
1929
+ },
1930
+ get lastError() {
1931
+ return writer.lastError;
1932
+ }
1933
+ };
1934
+ }
1935
+ function runFailureRecord({ code, message, phase }) {
1936
+ return {
1937
+ src: "build",
1938
+ level: "error",
1939
+ event: "run_failed",
1940
+ ...code ? { code } : {},
1941
+ ...phase ? { phase } : {},
1942
+ msg: [code, message].filter(Boolean).join(": ") || "the run failed"
1943
+ };
1944
+ }
1945
+ function thrownRunFailureRecord(error, phase) {
1946
+ return runFailureRecord({
1947
+ code: typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0,
1948
+ message: error instanceof Error ? error.message : String(error),
1949
+ phase
1950
+ });
1951
+ }
1952
+ const DETAIL_WRITE_MS = 2e3;
1953
+ function toolLine(record) {
1954
+ if (!record || typeof record !== "object") return null;
1955
+ const { src, level, msg } = record;
1956
+ return src === "build" && level === "debug" && typeof msg === "string" ? msg : null;
1957
+ }
1958
+ function writeBuildDetail(root, claimId, detail) {
1959
+ const file = workspaceBuildDetailFile(root);
1960
+ const tmp = `${file}.tmp-${process.pid}`;
1961
+ mkdirSync(dirname(file), { recursive: true });
1962
+ writeFileSync(tmp, JSON.stringify({
1963
+ claimId,
1964
+ detail
1965
+ }));
1966
+ renameSync(tmp, file);
1967
+ }
1968
+ const COLD_PHASES = [
1969
+ "prebuild",
1970
+ "pods",
1971
+ "compile"
1972
+ ];
1973
+ const HIT_PHASES = ["install", "launch"];
1974
+ function settledOutcome(phase, entered) {
1975
+ if (COLD_PHASES.includes(phase)) return "cold";
1976
+ if (phase === "device") return entered.some((entry) => entry.phase === "cache-lookup") ? "hit" : null;
1977
+ return HIT_PHASES.includes(phase) ? "hit" : null;
1978
+ }
1979
+ function projectHistory(projectKey) {
1980
+ try {
1981
+ return readStats$1().record?.history?.[projectKey];
1982
+ } catch {
1983
+ return;
1984
+ }
1985
+ }
1986
+ function startBuildProgress({ root, platform, slot, claim, now = Date.now, load = () => loadPerCore().loadPerCore, note = () => {} }) {
1987
+ const startedAt = new Date(now()).toISOString();
1988
+ const record = {
1989
+ platform,
1990
+ slot,
1991
+ startedAt,
1992
+ phase: "prepare",
1993
+ phaseStartedAt: startedAt,
1994
+ phases: [{
1995
+ phase: "prepare",
1996
+ startedAt
1997
+ }],
1998
+ claim: {
1999
+ root: claim.root,
2000
+ path: claim.path,
2001
+ claimId: claim.claimId,
2002
+ pid: claim.owner.pid
2003
+ },
2004
+ loadPerCore: load()
2005
+ };
2006
+ let warned = false;
2007
+ const guard = (fn) => {
2008
+ try {
2009
+ fn();
2010
+ } catch (error) {
2011
+ if (warned) return;
2012
+ warned = true;
2013
+ note(`Build progress could not be recorded in the workspace state: ${error?.message || error}`);
2014
+ }
2015
+ };
2016
+ const write = (before = (state) => state) => guard(() => updateWorkspaceState(root, (state) => ({
2017
+ ...before(state),
2018
+ [ACTIVE_BUILD_KEY]: record
2019
+ })));
2020
+ write(withInterruptedBuild);
2021
+ const phases = trace.phases(`${platform} build`);
2022
+ phases.enter("prepare");
2023
+ let projectKey = null;
2024
+ const parser = createBuildDetailParser();
2025
+ let detailWrittenAt = -Infinity;
2026
+ let detailTimer = null;
2027
+ const flushDetail = () => {
2028
+ detailTimer = null;
2029
+ detailWrittenAt = now();
2030
+ const detail = parser.detail(new Date(detailWrittenAt).toISOString());
2031
+ if (detail) guard(() => writeBuildDetail(root, record.claim.claimId, detail));
2032
+ };
2033
+ const settle = (outcome) => {
2034
+ record.outcome = outcome;
2035
+ if (projectKey && platform !== "macos") record.estimate = estimateBuild(projectHistory(projectKey), platform, outcome, record.deviceSetup);
2036
+ };
2037
+ const waits = /* @__PURE__ */ new Map();
2038
+ return {
2039
+ step(phase) {
2040
+ if (phase === record.phase) return;
2041
+ const at = new Date(now()).toISOString();
2042
+ record.phase = phase;
2043
+ record.phaseStartedAt = at;
2044
+ record.phases.push({
2045
+ phase,
2046
+ startedAt: at
2047
+ });
2048
+ phases.enter(phase);
2049
+ if (phase !== "wait") delete record.waitingOn;
2050
+ delete record.activity;
2051
+ const outcome = record.outcome || platform === "macos" ? null : settledOutcome(phase, record.phases);
2052
+ if (outcome) settle(outcome);
2053
+ write();
2054
+ },
2055
+ activity(name, at) {
2056
+ if ((record.activity?.name ?? null) === name) return;
2057
+ if (name) record.activity = {
2058
+ name,
2059
+ startedAt: new Date(at ?? now()).toISOString()
2060
+ };
2061
+ else delete record.activity;
2062
+ write();
2063
+ },
2064
+ estimate(key) {
2065
+ if (platform === "macos") return;
2066
+ projectKey = key;
2067
+ record.estimate = estimateBuild(projectHistory(key), platform, record.outcome ?? null, record.outcome ? record.deviceSetup : void 0);
2068
+ write();
2069
+ },
2070
+ miss(reason, provisional = false) {
2071
+ record.missReason = reason;
2072
+ settle("cold");
2073
+ if (provisional) record.missProvisional = true;
2074
+ else delete record.missProvisional;
2075
+ write();
2076
+ },
2077
+ hit() {
2078
+ delete record.missReason;
2079
+ delete record.missProvisional;
2080
+ settle("hit");
2081
+ write();
2082
+ },
2083
+ deviceSetup(setup) {
2084
+ if (setup === void 0) return;
2085
+ record.deviceSetup = setup;
2086
+ write();
2087
+ },
2088
+ deviceSetupKnown() {
2089
+ return record.deviceSetup;
2090
+ },
2091
+ place(remote) {
2092
+ const current = record.placement;
2093
+ if (remote && current?.host === remote.host && current.phase === remote.phase) return;
2094
+ if (current) record.offload = withOffloadPhase(record.offload, current, now());
2095
+ if (!remote) {
2096
+ if (!current) return;
2097
+ delete record.placement;
2098
+ return write();
2099
+ }
2100
+ const at = new Date(now()).toISOString();
2101
+ record.placement = {
2102
+ host: remote.host,
2103
+ phase: remote.phase,
2104
+ startedAt: current?.host === remote.host ? current.startedAt : at,
2105
+ phaseStartedAt: at
2106
+ };
2107
+ write();
2108
+ },
2109
+ waitingFor(info, kind) {
2110
+ if (info) waits.set(info.kind, info);
2111
+ else if (kind) waits.delete(kind);
2112
+ else waits.clear();
2113
+ const first = [...waits.values()].toSorted((a, b) => Date.parse(a.since) - Date.parse(b.since))[0];
2114
+ if (first) record.waitingFor = first;
2115
+ else delete record.waitingFor;
2116
+ write();
2117
+ },
2118
+ bootStarted() {
2119
+ if (record.boot) return;
2120
+ record.boot = {
2121
+ startedAt: new Date(now()).toISOString(),
2122
+ loadPerCore: load()
2123
+ };
2124
+ write();
2125
+ },
2126
+ waitingOn(holder) {
2127
+ if (holder === null) {
2128
+ if (!record.waitingOn) return;
2129
+ delete record.waitingOn;
2130
+ } else {
2131
+ if (record.waitingOn?.path === holder) return;
2132
+ record.waitingOn = { path: holder };
2133
+ }
2134
+ write();
2135
+ },
2136
+ output(line) {
2137
+ const msg = toolLine(line);
2138
+ if (msg === null || !parser.push(msg) || detailTimer) return;
2139
+ const wait = detailWrittenAt + DETAIL_WRITE_MS - now();
2140
+ if (wait <= 0) return flushDetail();
2141
+ detailTimer = setTimeout(flushDetail, wait);
2142
+ detailTimer.unref?.();
2143
+ },
2144
+ durations() {
2145
+ return phaseDurations(record.phases, now());
2146
+ },
2147
+ phase() {
2148
+ return record.phase;
2149
+ },
2150
+ steps() {
2151
+ const detail = parser.detail("");
2152
+ return detail?.unit === "steps" ? detail.total : null;
2153
+ },
2154
+ clear() {
2155
+ phases.end();
2156
+ if (detailTimer) clearTimeout(detailTimer);
2157
+ detailTimer = null;
2158
+ guard(() => rmSync(workspaceBuildDetailFile(root), { force: true }));
2159
+ guard(() => {
2160
+ clearWorkspaceStateKey(root, ACTIVE_BUILD_KEY, (value) => parseActiveBuild(value)?.claim.claimId === record.claim.claimId);
2161
+ });
2162
+ }
2163
+ };
2164
+ }
2165
+ /** `offload` with the time `placement` has spent in its current phase as of `now` added to that phase. */
2166
+ function withOffloadPhase(offload, placement, now) {
2167
+ const phaseMs = { ...offload?.phaseMs };
2168
+ const ms = Math.max(0, now - Date.parse(placement.phaseStartedAt));
2169
+ if (Number.isFinite(ms)) phaseMs[placement.phase] = (phaseMs[placement.phase] ?? 0) + ms;
2170
+ return {
2171
+ host: placement.host,
2172
+ phaseMs
2173
+ };
2174
+ }
2175
+ function phaseDurations(phases, now) {
2176
+ const out = {};
2177
+ phases.forEach(({ phase, startedAt }, index) => {
2178
+ const start = Date.parse(startedAt);
2179
+ const next = phases[index + 1];
2180
+ const end = next ? Date.parse(next.startedAt) : now;
2181
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return;
2182
+ out[phase] = (out[phase] ?? 0) + Math.max(0, end - start);
2183
+ });
2184
+ return out;
2185
+ }
2186
+ function completedPhaseDurations(phases) {
2187
+ if (phases.length < 2) return void 0;
2188
+ const durations = phaseDurations(phases.slice(0, -1), Date.parse(phases.at(-1).startedAt));
2189
+ return Object.keys(durations).length ? durations : void 0;
2190
+ }
2191
+ const HISTORY_FIELDS = [
2192
+ "devicePlacement",
2193
+ "platform",
2194
+ "status",
2195
+ "configuration",
2196
+ "fingerprint",
2197
+ "cacheKey",
2198
+ "cacheHit",
2199
+ "cacheSkipped",
2200
+ "durationMs",
2201
+ "startedAt",
2202
+ "errorCode",
2203
+ "missReason",
2204
+ "buildMachine",
2205
+ "builtOn",
2206
+ "offloadedTo",
2207
+ "offloadFallback",
2208
+ "diagnostics",
2209
+ "compileSteps"
2210
+ ];
2211
+ function withHistoryEntry(state, platform, entry) {
2212
+ const saved = state[BUILD_HISTORY_KEY];
2213
+ const history = saved && typeof saved === "object" && !Array.isArray(saved) ? saved : {};
2214
+ const list = Array.isArray(history[platform]) ? history[platform] : [];
2215
+ return {
2216
+ ...state,
2217
+ [BUILD_HISTORY_KEY]: {
2218
+ ...history,
2219
+ [platform]: [entry, ...list].slice(0, BUILD_HISTORY_LIMIT)
2220
+ }
2221
+ };
2222
+ }
2223
+ function finishedResult(record) {
2224
+ if (record.status === "ok") return "succeeded";
2225
+ return record.errorCode === "STIM_CANCELLED" ? "cancelled" : "failed";
2226
+ }
2227
+ function recordFinishedBuild(root, recorded, { update = updateWorkspaceState, now = Date.now, artifactOnly = false, ledger = writeBuildLedger } = {}) {
2228
+ const { ledger: extras, ...record } = recorded;
2229
+ const ran = {};
2230
+ try {
2231
+ updateFinishedBuild(root, record, ran, {
2232
+ update,
2233
+ now,
2234
+ artifactOnly
2235
+ });
2236
+ } finally {
2237
+ ledger(root, {
2238
+ now: now(),
2239
+ outcome: finishedResult(record),
2240
+ record,
2241
+ extras: isJsonObject(extras) ? extras : {},
2242
+ phases: {},
2243
+ phase: null,
2244
+ ...ran
2245
+ });
2246
+ }
2247
+ }
2248
+ function updateFinishedBuild(root, record, ran, { update, now, artifactOnly }) {
2249
+ const platform = record.platform;
2250
+ update(root, (state) => {
2251
+ const active = parseActiveBuild(state[ACTIVE_BUILD_KEY]);
2252
+ const own = active?.platform === platform ? active : null;
2253
+ const phases = own ? phaseDurations(own.phases, now()) : {};
2254
+ if (own) {
2255
+ const offload = own.placement ? withOffloadPhase(own.offload, own.placement, now()) : own.offload;
2256
+ Object.assign(ran, {
2257
+ id: own.claim.claimId,
2258
+ phases,
2259
+ phase: own.phase,
2260
+ ...offload ? { offload } : {},
2261
+ ...own.loadPerCore === void 0 ? {} : { loadPerCore: own.loadPerCore },
2262
+ ...own.boot ? { boot: own.boot } : {}
2263
+ });
2264
+ }
2265
+ const entry = {
2266
+ ...Object.fromEntries(HISTORY_FIELDS.filter((key) => key in record).map((key) => [key, record[key]])),
2267
+ result: finishedResult(record),
2268
+ slot: own?.slot ?? "default",
2269
+ phases
2270
+ };
2271
+ if (artifactOnly || platform === "macos") return withHistoryEntry(state, platform, entry);
2272
+ const slot = own?.slot ?? "default";
2273
+ const slots = isJsonObject(state.deviceSlots) ? state.deviceSlots : {};
2274
+ const savedSlot = isJsonObject(slots[slot]) ? slots[slot] : {};
2275
+ const savedDevice = slot === "default" ? state[platform] : savedSlot[platform];
2276
+ if (record.devicePlacement || isJsonObject(savedDevice)) {
2277
+ const device = isJsonObject(savedDevice) ? { ...savedDevice } : {};
2278
+ if (record.devicePlacement) device.devicePlacement = record.devicePlacement;
2279
+ else delete device.devicePlacement;
2280
+ state = slot === "default" ? {
2281
+ ...state,
2282
+ [platform]: device
2283
+ } : {
2284
+ ...state,
2285
+ deviceSlots: {
2286
+ ...slots,
2287
+ [slot]: {
2288
+ ...savedSlot,
2289
+ [platform]: device
2290
+ }
2291
+ }
2292
+ };
2293
+ }
2294
+ return withHistoryEntry({
2295
+ ...state,
2296
+ lastBuild: record,
2297
+ [LAST_BUILD_KEYS[platform]]: record
2298
+ }, platform, entry);
2299
+ });
2300
+ }
2301
+ /** Adds one phase's duration to the recorded run of `platform` that started at `startedAt`. */
2302
+ function recordBuildPhase(root, platform, startedAt, phase, ms) {
2303
+ updateWorkspaceState(root, (state) => {
2304
+ const saved = state[BUILD_HISTORY_KEY];
2305
+ const list = isJsonObject(saved) && Array.isArray(saved[platform]) ? saved[platform] : [];
2306
+ const index = list.findIndex((entry) => isJsonObject(entry) && entry.startedAt === startedAt);
2307
+ const entry = list[index];
2308
+ if (!isJsonObject(entry)) return state;
2309
+ const phases = isJsonObject(entry.phases) ? entry.phases : {};
2310
+ const next = [...list];
2311
+ next[index] = {
2312
+ ...entry,
2313
+ phases: {
2314
+ ...phases,
2315
+ [phase]: Math.round(ms)
2316
+ }
2317
+ };
2318
+ return {
2319
+ ...state,
2320
+ [BUILD_HISTORY_KEY]: {
2321
+ ...saved,
2322
+ [platform]: next
2323
+ }
2324
+ };
2325
+ });
2326
+ }
2327
+ function withInterruptedBuild(state) {
2328
+ const left = parseActiveBuild(state[ACTIVE_BUILD_KEY]);
2329
+ if (!left) return state;
2330
+ const last = state[BUILD_HISTORY_KEY]?.[left.platform]?.[0];
2331
+ if (Date.parse(String(last?.startedAt)) >= Date.parse(left.startedAt)) return state;
2332
+ return withHistoryEntry(state, left.platform, {
2333
+ platform: left.platform,
2334
+ status: "failed",
2335
+ result: "interrupted",
2336
+ slot: left.slot,
2337
+ configuration: null,
2338
+ fingerprint: null,
2339
+ cacheKey: null,
2340
+ cacheHit: false,
2341
+ cacheSkipped: false,
2342
+ durationMs: null,
2343
+ startedAt: left.startedAt,
2344
+ phases: {
2345
+ ...phaseDurations(left.phases, Date.parse(left.phaseStartedAt)),
2346
+ [left.phase]: 0
2347
+ }
2348
+ });
2349
+ }
2350
+ function parseActiveBuild(value) {
2351
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2352
+ const record = value;
2353
+ const claim = record.claim;
2354
+ if (record.platform !== "ios" && record.platform !== "android" && record.platform !== "macos") return null;
2355
+ if (!isPhase(record.phase)) return null;
2356
+ if (typeof record.startedAt !== "string" || typeof record.phaseStartedAt !== "string") return null;
2357
+ if (!claim || typeof claim.root !== "string" || typeof claim.claimId !== "string") return null;
2358
+ const phases = Array.isArray(record.phases) ? record.phases.filter((entry) => isPhase(entry?.phase) && typeof entry?.startedAt === "string") : [];
2359
+ const missReason = parseMissReason(record.missReason);
2360
+ const placement = parsePlacement(record.placement);
2361
+ const waitingOn = parseWaitingOn(record.waitingOn);
2362
+ const waitingFor = parseWaitingFor(record.waitingFor);
2363
+ const estimate = parseEstimate(record.estimate);
2364
+ const activity = parseActivity(record.activity);
2365
+ const offload = parseOffload(record.offload);
2366
+ const boot = parseBoot(record.boot);
2367
+ return {
2368
+ platform: record.platform,
2369
+ slot: typeof record.slot === "string" ? record.slot : "default",
2370
+ startedAt: record.startedAt,
2371
+ phase: record.phase,
2372
+ phaseStartedAt: record.phaseStartedAt,
2373
+ phases,
2374
+ ...missReason ? { missReason } : {},
2375
+ ...missReason && record.missProvisional === true ? { missProvisional: true } : {},
2376
+ ...placement ? { placement } : {},
2377
+ ...offload ? { offload } : {},
2378
+ ...waitingOn ? { waitingOn } : {},
2379
+ ...waitingFor ? { waitingFor } : {},
2380
+ ...typeof record.deviceSetup === "boolean" ? { deviceSetup: record.deviceSetup } : {},
2381
+ ...record.outcome === "hit" || record.outcome === "cold" ? { outcome: record.outcome } : {},
2382
+ ...estimate ? { estimate } : {},
2383
+ ...activity ? { activity } : {},
2384
+ ...isLoad(record.loadPerCore) ? { loadPerCore: record.loadPerCore } : {},
2385
+ ...boot ? { boot } : {},
2386
+ claim: {
2387
+ root: claim.root,
2388
+ path: typeof claim.path === "string" ? claim.path : "",
2389
+ claimId: claim.claimId,
2390
+ pid: typeof claim.pid === "number" ? claim.pid : 0
2391
+ }
2392
+ };
2393
+ }
2394
+ function isLoad(value) {
2395
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
2396
+ }
2397
+ function parseBoot(value) {
2398
+ if (!isJsonObject(value) || typeof value.startedAt !== "string" || !isLoad(value.loadPerCore)) return null;
2399
+ return {
2400
+ startedAt: value.startedAt,
2401
+ loadPerCore: value.loadPerCore
2402
+ };
2403
+ }
2404
+ function parseOffload(value) {
2405
+ if (!isJsonObject(value) || typeof value.host !== "string" || !isJsonObject(value.phaseMs)) return null;
2406
+ const phaseMs = {};
2407
+ for (const [phase, ms] of Object.entries(value.phaseMs)) if (typeof ms === "number" && Number.isFinite(ms) && ms >= 0) phaseMs[phase] = ms;
2408
+ return {
2409
+ host: value.host,
2410
+ phaseMs
2411
+ };
2412
+ }
2413
+ function parsePlacement(value) {
2414
+ if (!value || typeof value !== "object") return null;
2415
+ const { host, phase, startedAt, phaseStartedAt } = value;
2416
+ if ([
2417
+ host,
2418
+ phase,
2419
+ startedAt,
2420
+ phaseStartedAt
2421
+ ].some((field) => typeof field !== "string")) return null;
2422
+ return {
2423
+ host,
2424
+ phase,
2425
+ startedAt,
2426
+ phaseStartedAt
2427
+ };
2428
+ }
2429
+ function parseWaitingFor(value) {
2430
+ if (!value || typeof value !== "object") return null;
2431
+ const { kind, inUse, max, since } = value;
2432
+ if (kind !== "build-slot" && kind !== "device-slot") return null;
2433
+ if (!Number.isInteger(inUse) || inUse < 0 || !Number.isInteger(max) || max <= 0) return null;
2434
+ if (typeof since !== "string" || !Number.isFinite(Date.parse(since))) return null;
2435
+ return {
2436
+ kind,
2437
+ inUse,
2438
+ max,
2439
+ since
2440
+ };
2441
+ }
2442
+ function parseActivity(value) {
2443
+ if (!value || typeof value !== "object") return null;
2444
+ const { name, startedAt } = value;
2445
+ if (name !== "booting" && name !== "bundling" && name !== "waiting-ready") return null;
2446
+ return typeof startedAt === "string" ? {
2447
+ name,
2448
+ startedAt
2449
+ } : null;
2450
+ }
2451
+ function parseWaitingOn(value) {
2452
+ const path = value?.path;
2453
+ return typeof path === "string" && path ? { path } : null;
2454
+ }
2455
+ function parseEstimate(value) {
2456
+ if (!value || typeof value !== "object") return null;
2457
+ const { outcome, expectedMs, basis, phaseMs, planned } = value;
2458
+ if (outcome !== null && outcome !== "hit" && outcome !== "cold") return null;
2459
+ if (expectedMs !== null && typeof expectedMs !== "number") return null;
2460
+ if (typeof basis !== "number" || !Array.isArray(planned) || !phaseMs || typeof phaseMs !== "object") return null;
2461
+ const durations = Object.entries(phaseMs).filter((entry) => isPhase(entry[0]) && typeof entry[1] === "number");
2462
+ return {
2463
+ outcome,
2464
+ expectedMs,
2465
+ basis,
2466
+ phaseMs: Object.fromEntries(durations),
2467
+ planned: planned.filter(isPhase)
2468
+ };
2469
+ }
2470
+ function isPhase(value) {
2471
+ return BUILD_PHASES.includes(value);
2472
+ }
2473
+ function activeBuildState(claim, survey = (0, ownership_claim_exports.readClaimSet)(claim.root)) {
2474
+ if (survey.live.some((holder) => holder.claimId === claim.claimId)) return "running";
2475
+ if (survey.dead.some((holder) => holder.claimId === claim.claimId)) return "stale";
2476
+ return survey.unresolved.length ? "unknown" : "stale";
2477
+ }
2478
+ function median(values) {
2479
+ if (!values.length) return null;
2480
+ const sorted = values.toSorted((a, b) => a - b);
2481
+ const mid = Math.floor(sorted.length / 2);
2482
+ return sorted.length % 2 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2);
2483
+ }
2484
+ function estimateBuild(history, platform, known, deviceSetup) {
2485
+ const lists = platform === "macos" ? void 0 : history?.[platform];
2486
+ const outcome = known ?? latestOutcome(lists);
2487
+ const samples = matchDeviceSetup(outcome && lists?.[outcome] || [], deviceSetup);
2488
+ const phaseMs = {};
2489
+ const planned = [];
2490
+ for (const phase of BUILD_PHASES) {
2491
+ const durations = samples.flatMap((sample) => phase in sample.phases ? [sample.phases[phase]] : []);
2492
+ const ms = median(durations);
2493
+ if (ms === null) continue;
2494
+ phaseMs[phase] = ms;
2495
+ if (durations.length * 2 >= samples.length) planned.push(phase);
2496
+ }
2497
+ return {
2498
+ outcome,
2499
+ expectedMs: median(samples.map((sample) => sample.durationMs)),
2500
+ basis: samples.length,
2501
+ phaseMs,
2502
+ planned
2503
+ };
2504
+ }
2505
+ const MIN_TAGGED_SAMPLES = 3;
2506
+ function matchDeviceSetup(samples, deviceSetup) {
2507
+ if (deviceSetup === void 0) return samples;
2508
+ const tagged = samples.filter((sample) => sample.deviceSetup === deviceSetup);
2509
+ if (deviceSetup || tagged.length >= MIN_TAGGED_SAMPLES) return tagged;
2510
+ return samples.filter((sample) => sample.deviceSetup !== true);
2511
+ }
2512
+ function latestOutcome(lists) {
2513
+ let latest = null;
2514
+ for (const outcome of ["hit", "cold"]) {
2515
+ const last = lists?.[outcome]?.at(-1);
2516
+ const at = last ? Date.parse(last.at) : NaN;
2517
+ if (Number.isFinite(at) && (!latest || at > latest.at)) latest = {
2518
+ outcome,
2519
+ at
2520
+ };
2521
+ }
2522
+ return latest?.outcome ?? null;
2523
+ }
2524
+ function buildReport(record, { state, history }) {
2525
+ const estimate = record.estimate ?? estimateBuild(history, record.platform, record.outcome ?? null, record.outcome ? record.deviceSetup : void 0);
2526
+ const plannedPhases = estimate.planned.flatMap((phase) => {
2527
+ const expectedMs = estimate.phaseMs[phase];
2528
+ return expectedMs === void 0 ? [] : [{
2529
+ phase,
2530
+ expectedMs
2531
+ }];
2532
+ });
2533
+ const completedPhaseMs = completedPhaseDurations(record.phases);
2534
+ return {
2535
+ platform: record.platform,
2536
+ slot: record.slot,
2537
+ state,
2538
+ phase: record.phase,
2539
+ startedAt: record.startedAt,
2540
+ phaseStartedAt: record.phaseStartedAt,
2541
+ outcome: estimate.outcome,
2542
+ outcomeKnown: record.outcome !== void 0,
2543
+ ...record.outcome && record.phases.some(({ phase }) => phase === "cache-lookup") ? { cacheLookupOutcome: record.outcome === "hit" ? "hit" : "miss" } : {},
2544
+ expectedMs: estimate.expectedMs,
2545
+ expectedPhaseMs: estimate.phaseMs[record.phase] ?? null,
2546
+ ...completedPhaseMs ? { completedPhaseMs } : {},
2547
+ basis: estimate.basis,
2548
+ plannedPhases: plannedPhases.length ? plannedPhases : null,
2549
+ ...record.missReason ? { missReason: record.missReason } : {},
2550
+ ...record.missReason && record.missProvisional ? { missProvisional: true } : {},
2551
+ placement: record.placement ?? "local",
2552
+ ...record.waitingOn ? { waitingOn: record.waitingOn } : {},
2553
+ ...record.waitingFor ? { waitingFor: record.waitingFor } : {},
2554
+ ...record.activity ? { activity: { ...record.activity } } : {}
2555
+ };
2556
+ }
2557
+ /**
2558
+ * The run's `bundling` activity with Metro's percent, when the request Metro reports in flight is the one the activity
2559
+ * started on: same platform and same start, since both come from the request's `bundle_response_started` record.
2560
+ */
2561
+ function bundlingActivity(activity, platform, bundle) {
2562
+ if (activity?.name !== "bundling" || !bundle?.bundling || bundle.percent === void 0) return activity;
2563
+ if (bundle.platform !== platform || bundle.startedAt !== activity.startedAt) return activity;
2564
+ return {
2565
+ ...activity,
2566
+ percent: bundle.percent
2567
+ };
2568
+ }
2569
+ function remainingText(remainingMs) {
2570
+ return remainingMs < 6e4 ? "under a minute left" : `about ${Math.ceil(remainingMs / 6e4)} min left`;
2571
+ }
2572
+ function activityText({ name, percent, startedAt }, now) {
2573
+ 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)))}`;
2574
+ }
2575
+ function buildStatusLine(report, now) {
2576
+ const slot = report.slot === "default" ? "" : ` [${report.slot}]`;
2577
+ if (report.state === "stale") return `build: stale ${report.platform}${slot} record from ${report.startedAt} (its run is gone; the next run replaces it)`;
2578
+ const elapsedMs = Math.max(0, now - Date.parse(report.startedAt));
2579
+ const remote = typeof report.placement === "object" ? ` on ${report.placement.host} (${report.placement.phase}, ${formatElapsed(Math.max(0, now - Date.parse(report.placement.phaseStartedAt)))})` : "";
2580
+ const waiting = report.waitingOn ? ` on ${report.waitingOn.path}` : "";
2581
+ const activity = report.activity ? ` (${activityText(report.activity, now)})` : "";
2582
+ const head = `build: ${report.platform}${slot} ${report.phase}${activity}${waiting}${remote}, ${formatElapsed(elapsedMs)} elapsed`;
2583
+ if (report.state === "unknown") return `${head} (its native-run claim cannot be resolved, so it may not be running)`;
2584
+ if (report.expectedMs === null) return head;
2585
+ const basis = `median of ${plural(report.basis, `${report.outcome} run`)}`;
2586
+ const remainingMs = report.expectedMs - elapsedMs;
2587
+ return remainingMs > 0 ? `${head} -- ${remainingText(remainingMs)} (${basis})` : `${head} (usually ~${formatElapsed(report.expectedMs)}, ${basis})`;
2588
+ }
2589
+ //#endregion
2590
+ //#region src/commands/gc/idle.ts
2591
+ function findOwnedDeviceActivity({ config, sims, androidSerial, readActivity, deadProjects = [] }) {
2592
+ const booted = new Map(sims.filter((sim) => sim.state === "Booted").map((sim) => [sim.udid, sim.name]));
2593
+ const dead = new Set(deadProjects);
2594
+ const found = [];
2595
+ for (const [project, record] of Object.entries(config?.projects ?? {})) {
2596
+ if (dead.has(project)) continue;
2597
+ for (const { slot, platforms } of projectDeviceSlots(record)) {
2598
+ const ios = platforms.ios;
2599
+ if (ios?.owned && ios.deviceUdid && booted.has(ios.deviceUdid)) found.push({
2600
+ kind: "ios",
2601
+ id: ios.deviceUdid,
2602
+ name: booted.get(ios.deviceUdid) ?? ios.deviceUdid,
2603
+ project,
2604
+ slot,
2605
+ activity: readActivity({
2606
+ platform: "ios",
2607
+ id: ios.deviceUdid,
2608
+ slot,
2609
+ workspace: project
2610
+ })
2611
+ });
2612
+ const android = platforms.android;
2613
+ const serial = android?.owned && android.avdName ? androidSerial(android.avdName) : null;
2614
+ if (android?.avdName && serial) found.push({
2615
+ kind: "android",
2616
+ id: android.avdName,
2617
+ name: android.avdName,
2618
+ project,
2619
+ slot,
2620
+ activity: readActivity({
2621
+ platform: "android",
2622
+ id: serial,
2623
+ slot,
2624
+ workspace: project
2625
+ })
2626
+ });
2627
+ }
2628
+ }
2629
+ return found;
2630
+ }
2631
+ function findIdleDevices({ buildInProgress, now, ...inputs }) {
2632
+ return idleDevicesFromActivity(findOwnedDeviceActivity(inputs), buildInProgress, now);
2633
+ }
2634
+ function idleDevicesFromActivity(devices, buildInProgress, now) {
2635
+ return devices.filter((device) => device.activity.state === "idle").map(({ kind, id, name, project, slot, activity }) => ({
2636
+ kind,
2637
+ id,
2638
+ name,
2639
+ project,
2640
+ slot,
2641
+ lastActivityAt: activity.lastActivityAt ?? null,
2642
+ idleForMs: idleForMs(activity, now),
2643
+ buildInProgress: buildInProgress(project)
2644
+ }));
2645
+ }
2646
+ function idleShutdownCandidates(devices, idleMs) {
2647
+ return devices.filter((d) => !d.buildInProgress && d.idleForMs !== null && d.idleForMs >= idleMs);
2648
+ }
2649
+ function parseIdleDuration(value) {
2650
+ const match = /^([1-9][0-9]*)(m|h|d)$/.exec(value.trim());
2651
+ if (!match) return null;
2652
+ const unit = {
2653
+ m: 6e4,
2654
+ h: 36e5,
2655
+ d: 864e5
2656
+ }[match[2]];
2657
+ return Number(match[1]) * unit;
2658
+ }
2659
+ function workspaceBuildInProgress(project) {
2660
+ const record = parseActiveBuild(readWorkspaceState$1(project)?.[ACTIVE_BUILD_KEY]);
2661
+ if (!record) return false;
2662
+ try {
2663
+ return activeBuildState(record.claim) !== "stale";
2664
+ } catch {
2665
+ return true;
2666
+ }
2667
+ }
2668
+ function deviceActivityInputs(config, sims, now, env) {
2669
+ const resolve = ownedAvdSerialResolver({ timeoutMs: 5e3 });
2670
+ return {
2671
+ config,
2672
+ sims,
2673
+ readActivity: createActivityReader({
2674
+ now,
2675
+ env
2676
+ }),
2677
+ androidSerial: (avdName) => {
2678
+ try {
2679
+ return resolve(avdName).serial ?? null;
2680
+ } catch {
2681
+ return null;
2682
+ }
2683
+ }
2684
+ };
2685
+ }
2686
+ function collectOwnedDeviceActivity(config, sims, now = Date.now(), env) {
2687
+ return findOwnedDeviceActivity(deviceActivityInputs(config, sims, now, env));
2688
+ }
2689
+ function collectIdleDevices(config, sims, deadProjects, now = Date.now(), env) {
2690
+ return findIdleDevices({
2691
+ ...deviceActivityInputs(config, sims, now, env),
2692
+ deadProjects,
2693
+ now,
2694
+ buildInProgress: workspaceBuildInProgress
2695
+ });
2696
+ }
2697
+ async function deviceActivityInputsAsync(config, sims, now, env, deadProjects = [], signal, operation) {
2698
+ const resolve = ownedAvdSerialResolverAsync({
2699
+ timeoutMs: 5e3,
2700
+ signal,
2701
+ operation
2702
+ });
2703
+ const serials = /* @__PURE__ */ new Map();
2704
+ for (const [project, record] of Object.entries(config?.projects ?? {})) {
2705
+ signal?.throwIfAborted();
2706
+ if (deadProjects.includes(project)) continue;
2707
+ for (const { platforms } of projectDeviceSlots(record)) {
2708
+ const android = platforms.android;
2709
+ if (!android?.owned || !android.avdName || serials.has(android.avdName)) continue;
2710
+ try {
2711
+ serials.set(android.avdName, (await resolve(android.avdName)).serial ?? null);
2712
+ } catch (error) {
2713
+ if (error instanceof WorkspaceProcessCleanupError) throw error;
2714
+ signal?.throwIfAborted();
2715
+ serials.set(android.avdName, null);
2716
+ }
2717
+ }
2718
+ }
2719
+ return {
2720
+ config,
2721
+ sims,
2722
+ readActivity: createActivityReaderAsync({
2723
+ now,
2724
+ env,
2725
+ signal,
2726
+ operation
2727
+ }),
2728
+ androidSerial: (name) => serials.get(name) ?? null
2729
+ };
2730
+ }
2731
+ async function collectOwnedDeviceActivityAsync(config, sims, now = Date.now(), env, signal, operation) {
2732
+ const devices = findOwnedDeviceActivity(await deviceActivityInputsAsync(config, sims, now, env, [], signal, operation));
2733
+ return Promise.all(devices.map(async (device) => Object.assign({}, device, { activity: await device.activity })));
2734
+ }
2735
+ async function collectIdleDevicesAsync(config, sims, deadProjects, now = Date.now(), env, signal, operation) {
2736
+ const pending = findOwnedDeviceActivity({
2737
+ ...await deviceActivityInputsAsync(config, sims, now, env, deadProjects, signal, operation),
2738
+ deadProjects
2739
+ });
2740
+ return idleDevicesFromActivity(await Promise.all(pending.map(async (device) => Object.assign({}, device, { activity: await device.activity }))), workspaceBuildInProgress, now);
2741
+ }
2742
+ function idleDeviceLines(devices) {
2743
+ if (devices.length === 0) return [];
2744
+ return [`Idle owned devices (${devices.length}) - booted with no driver, claim, or recent activity:`, ...devices.map((d) => {
2745
+ const slot = d.slot === "default" ? "" : ` [${d.slot}]`;
2746
+ const idle = d.idleForMs === null ? "idle for an unknown time" : `idle ${formatLongDuration(d.idleForMs)}`;
2747
+ const build = d.buildInProgress ? ", build in progress" : "";
2748
+ return ` ${d.kind}${slot} ${d.name} (${d.project}) -- ${idle}${build}`;
2749
+ })];
2750
+ }
2751
+ async function shutDownIdleDevices(devices, idleMs, recollect, { log, record } = GC_OUTPUT, signal, operation) {
2752
+ signal?.throwIfAborted();
2753
+ const candidates = idleShutdownCandidates(devices, idleMs);
2754
+ const shutDown = [];
2755
+ if (candidates.length === 0) {
2756
+ log(chalk.dim(`No owned device has been idle for ${formatLongDuration(idleMs)} or more.`));
2757
+ return {
2758
+ failures: 0,
2759
+ shutDown
2760
+ };
2761
+ }
2762
+ const fresh = idleShutdownCandidates(await recollect(), idleMs);
2763
+ signal?.throwIfAborted();
2764
+ let failures = 0;
2765
+ for (const device of candidates) {
2766
+ signal?.throwIfAborted();
2767
+ const what = `${device.kind} ${device.name}`;
2768
+ const current = fresh.find((d) => d.kind === device.kind && d.id === device.id && d.project === device.project && d.slot === device.slot);
2769
+ if (!current) {
2770
+ log(chalk.dim(`Kept ${what}: it is no longer idle for ${formatLongDuration(idleMs)}.`));
2771
+ record("idleDevice", "kept", device.name, {
2772
+ id: device.id,
2773
+ detail: `it is no longer idle for ${formatLongDuration(idleMs)}`
2774
+ });
2775
+ continue;
2776
+ }
2777
+ const outcome = device.kind === "ios" ? await teardownOwnedIosSim(device.id, {
2778
+ label: device.name,
2779
+ operation
2780
+ }) : await teardownOwnedAvd(device.id, {
2781
+ owner: {
2782
+ projectPath: device.project,
2783
+ slot: device.slot
2784
+ },
2785
+ operation
2786
+ });
2787
+ if (outcome.status === "torn-down") {
2788
+ shutDown.push(device);
2789
+ log(chalk.green(`Shut down ${what}, idle ${formatLongDuration(current.idleForMs)}`));
2790
+ record("idleDevice", "done", device.name, {
2791
+ id: device.id,
2792
+ detail: `idle ${formatLongDuration(current.idleForMs)}`
2793
+ });
2794
+ } else if (outcome.status === "missing") log(chalk.dim(`${what} is already gone; nothing to shut down.`));
2795
+ else if (outcome.status === "skipped") {
2796
+ log(chalk.yellow(`Skipped ${what}: ${outcome.reason}`));
2797
+ record("idleDevice", "kept", device.name, {
2798
+ id: device.id,
2799
+ detail: outcome.reason
2800
+ });
2801
+ } else {
2802
+ failures++;
2803
+ log(chalk.red(`Failed to shut down ${what}: ${outcome.reason}`));
2804
+ record("idleDevice", "failed", device.name, {
2805
+ id: device.id,
2806
+ detail: outcome.reason
2807
+ });
2808
+ }
2809
+ }
2810
+ return {
2811
+ failures,
2812
+ shutDown
2813
+ };
2814
+ }
2815
+ //#endregion
1152
2816
  //#region src/macos/instances.ts
1153
2817
  const BUNDLE_EXECUTABLE = /^[^/]+\.app\/Contents\/MacOS\/[^/]+$/;
1154
2818
  function bundleDirs(root) {
@@ -1221,10 +2885,20 @@ function failedBundleListing(record, error) {
1221
2885
  if (!open.trim()) throw unverified(record.pid, `lsof failed (${error.message.split("\n")[0]})`);
1222
2886
  return open;
1223
2887
  }
2888
+ /** The attempt directories under a hosted session's `apps`, each of which holds one `App.app`. */
2889
+ function hostedBundleDirs(area) {
2890
+ const apps = join(area, "apps");
2891
+ if (process.platform !== "darwin" || !existsSync(apps)) return [];
2892
+ if (realpathSync(apps) !== apps) throw new Error("Hosted app data resolves outside its private area.");
2893
+ return readdirSync(apps).filter((attempt) => hostedAppAttempt(attempt)).map((attempt) => join(apps, attempt)).filter((dir) => lstatSync(dir, { throwIfNoEntry: false })?.isDirectory());
2894
+ }
1224
2895
  /** Verifies owned bundle instances without blocking on process and executable probes. */
1225
- async function runningBundleInstancesAsync(root, operation) {
2896
+ function runningBundleInstancesAsync(root, operation) {
2897
+ return runningInstancesInAsync(bundleDirs(root), operation);
2898
+ }
2899
+ /** Verifies the running copies of app bundles directly inside `dirs`. */
2900
+ async function runningInstancesInAsync(dirs, operation) {
1226
2901
  operation?.signal.throwIfAborted();
1227
- const dirs = bundleDirs(root);
1228
2902
  if (!dirs.length) return [];
1229
2903
  const listing = await runOperationFile(operation, getExecutor(), "ps", [
1230
2904
  "-axww",
@@ -1853,9 +3527,9 @@ function numberOrNull(v) {
1853
3527
  //#region src/workspace/in-use.ts
1854
3528
  const NATIVE_RUN = "native-run";
1855
3529
  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) {
3530
+ function workspaceInUse(root, { supervisor = true, devServer = true, managedLocks = true, hosted = true, nativeRun = true } = {}, bundleInstances = bundleInstancePids) {
1857
3531
  const reasons = [];
1858
- if (supervisor) {
3532
+ if (supervisor && devServer) {
1859
3533
  const project = getProject(root);
1860
3534
  const target = resolveSupervisorTarget({
1861
3535
  state: readWorkspaceState$1(root)?.supervisor,
@@ -1864,6 +3538,11 @@ function workspaceInUse(root, { supervisor = true, managedLocks = true, hosted =
1864
3538
  });
1865
3539
  if (target.status === "ours") reasons.push(`its dev server supervisor (pid ${target.pid}) is running`);
1866
3540
  else if (target.status === "unverified") reasons.push(`its dev server supervisor cannot be verified: ${target.reason ?? "unknown identity"}`);
3541
+ const browser = webFacts(readWebRecord(root));
3542
+ if (browser?.status === "running" || browser?.status === "orphaned") reasons.push(`its owned Chrome (pid ${browser.record.chromeProcess?.pid}) is running`);
3543
+ else if (browser?.status === "unverified") reasons.push("its owned Chrome cannot be verified");
3544
+ }
3545
+ if (supervisor) {
1867
3546
  const macos = macosAppState(readMacosRecord(root));
1868
3547
  if (macos?.host) reasons.push(`its macOS app runs on ${macos.host.machine}`);
1869
3548
  else if (macos?.state === "running" || macos?.state === "orphaned" || macos?.build.state === "running") reasons.push("its macOS app or build is running");
@@ -1877,9 +3556,6 @@ function workspaceInUse(root, { supervisor = true, managedLocks = true, hosted =
1877
3556
  const runtime = (0, ownership_claim_exports.readClaimSet)(macosRuntimeClaim(root));
1878
3557
  if (runtime.live.length) reasons.push("its macOS runtime claim is held");
1879
3558
  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
3559
  }
1884
3560
  if (nativeRun) {
1885
3561
  const claims = (0, ownership_claim_exports.readClaimSet)(workspaceProcessLockPath(workspaceDir(root), NATIVE_RUN, true));
@@ -1974,6 +3650,63 @@ async function withIdleWorkspace(root, fn, { purpose, operation, signal = operat
1974
3650
  }
1975
3651
  }
1976
3652
  //#endregion
3653
+ //#region src/workspace/regenerable-dirs.ts
3654
+ /**
3655
+ * Directories that hold only what a build or an install regenerates, relative to an app or package root. ios/build is
3656
+ * not one: `pod install` writes the codegen output (build/generated/ios) and, on React Native 0.88, the podspec
3657
+ * facades (scripts/cocoapods/rncore_facades.rb) there, the Pods project references them, and only `pod install`
3658
+ * recreates them.
3659
+ */
3660
+ const ARTIFACT_DIRS = {
3661
+ build: [
3662
+ "android/app/build",
3663
+ "android/build",
3664
+ "android/.gradle",
3665
+ ".build"
3666
+ ],
3667
+ dependencies: ["node_modules", "ios/Pods"]
3668
+ };
3669
+ /** The existing real directories among `dirs` under `root`, never through a link. */
3670
+ function existingDirsOf(root, dirs) {
3671
+ return dirs.map((dir) => join(root, dir)).filter((path) => {
3672
+ try {
3673
+ return lstatSync(path).isDirectory();
3674
+ } catch {
3675
+ return false;
3676
+ }
3677
+ });
3678
+ }
3679
+ /**
3680
+ * Why `path`, inside the checkout at `root`, must not be deleted as a regenerable directory, or null when it is a real
3681
+ * directory inside `root` whose every file git ignores.
3682
+ */
3683
+ async function regenerableDirProblem(root, path, signal) {
3684
+ const rel = relative(root, path);
3685
+ try {
3686
+ if (!lstatSync(path).isDirectory()) return "it is not a directory";
3687
+ } catch {
3688
+ return "it is gone";
3689
+ }
3690
+ if (canonicalPath(path) !== join(canonicalPath(root), rel)) return "it or a directory above it is a link, or it lies outside its project";
3691
+ const tracked = await gitQuietAsync(root, [
3692
+ "ls-files",
3693
+ "--",
3694
+ rel
3695
+ ], { signal });
3696
+ if (tracked === null) return "git cannot tell whether it holds tracked files";
3697
+ if (tracked.trim()) return "it holds files git tracks";
3698
+ const unignored = await gitQuietAsync(root, [
3699
+ "ls-files",
3700
+ "--others",
3701
+ "--exclude-standard",
3702
+ "--",
3703
+ rel
3704
+ ], { signal });
3705
+ if (unignored === null) return "git cannot tell whether it ignores every file in it";
3706
+ if (unignored.trim()) return "it holds files git does not ignore";
3707
+ return null;
3708
+ }
3709
+ //#endregion
1977
3710
  //#region src/commands/gc/workspaces.ts
1978
3711
  function workspacesRoot() {
1979
3712
  return join$1(getConfigDir(), "workspaces");
@@ -2091,7 +3824,7 @@ function classifyWorkspaceDirs(entries, { registryKeys, exists, isMounted, inUse
2091
3824
  continue;
2092
3825
  }
2093
3826
  if (present) continue;
2094
- if (registryKeys.some((key) => isPathPrefix(projectRoot, key))) continue;
3827
+ if (registryKeys.some((key) => isPathPrefix$1(projectRoot, key))) continue;
2095
3828
  if (!isMounted(projectRoot)) {
2096
3829
  skipped.push({
2097
3830
  dir,
@@ -2153,39 +3886,71 @@ async function collectOrphanedWorkspaces(registryKeys, mountedVolumes, { measure
2153
3886
  }
2154
3887
  }
2155
3888
  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
3889
  return {
2170
- orphaned,
3890
+ orphaned: await mapLimited(classified.orphaned, sizeConcurrency(operation), async (entry) => {
3891
+ operation?.signal.throwIfAborted();
3892
+ const bytes = await measuredDirectorySizeAsync(entry.dir, {
3893
+ timeoutMs: SIZE_TIMEOUT_MS,
3894
+ operation,
3895
+ signal: operation?.signal
3896
+ });
3897
+ return {
3898
+ ...entry,
3899
+ bytes
3900
+ };
3901
+ }),
2171
3902
  skipped: classified.skipped
2172
3903
  };
2173
3904
  }
2174
3905
  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.";
3906
+ /** How long after its last ios, android or macos run a workspace keeps its build outputs. */
3907
+ const LIGHTEN_AFTER_MS = 9e5;
3908
+ const BUILD_CHECKS = {
3909
+ devServer: false,
3910
+ hosted: false,
3911
+ managedLocks: false
3912
+ };
3913
+ const BUILD_IN_PROGRESS = "a build is in progress";
3914
+ /** Why the build outputs of the workspace at `root` are in use: it is building or running a macOS app. */
3915
+ function workspaceBuildReasons(root, { nativeRun = true } = {}) {
3916
+ const reasons = workspaceInUse(root, {
3917
+ ...BUILD_CHECKS,
3918
+ nativeRun
3919
+ });
3920
+ return workspaceBuildInProgress(root) ? [...reasons, BUILD_IN_PROGRESS] : reasons;
3921
+ }
3922
+ async function workspaceBuildReasonsAsync(root, signal, operation) {
3923
+ const reasons = await workspaceInUseAsync(root, BUILD_CHECKS, signal, operation);
3924
+ return workspaceBuildInProgress(root) ? [...reasons, BUILD_IN_PROGRESS] : reasons;
3925
+ }
3926
+ async function inTreeBuildDirs(root, signal) {
3927
+ const dirs = [];
3928
+ for (const path of existingDirsOf(root, ARTIFACT_DIRS.build)) if (!await regenerableDirProblem(root, path, signal)) dirs.push(path);
3929
+ return dirs;
3930
+ }
2175
3931
  const WORKSPACE_OUTPUT_DIRS = [
2176
3932
  "derived-data",
2177
3933
  "gradle-build",
2178
3934
  "android-cas",
2179
3935
  "cache-provider"
2180
3936
  ];
3937
+ /** Why a workspace whose last ios, android or macos run was at `nativeUsed` keeps its build outputs, or null. */
3938
+ function recentNativeUseReason(nativeUsed, now) {
3939
+ return now - nativeUsed < 9e5 ? `ran ios, android or macos within the last ${LIGHTEN_AFTER_MS / 6e4} minutes` : null;
3940
+ }
2181
3941
  const DAY_MS = 864e5;
3942
+ function keptByLock(entry) {
3943
+ return entry.keptCode === "locked" || entry.keptCode === "lock-unknown";
3944
+ }
2182
3945
  function planWorkspaceOutputs(entries, { olderThan, now }) {
2183
- return entries.map(({ dir, projectRoot, problem, bytes, lastUsed, inUse }) => {
3946
+ return entries.map(({ dir, projectRoot, problem, inTreeDirs = [], bytes, lastUsed, nativeUsed, inUse, lock }) => {
3947
+ const recentNative = recentNativeUseReason(nativeUsed, now);
2184
3948
  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];
3949
+ const [keptCode, keptReason] = projectRoot === null ? ["unresolved", `workspace directory not resolved: ${problem ?? "unknown project root"}`] : lock ? [lock === "locked" ? "locked" : "lock-unknown", lockReason(lock)] : 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
3950
  return {
2187
3951
  dir,
2188
3952
  projectRoot,
3953
+ inTreeDirs,
2189
3954
  bytes,
2190
3955
  idleDays,
2191
3956
  willClear: keptCode === null,
@@ -2195,6 +3960,11 @@ function planWorkspaceOutputs(entries, { olderThan, now }) {
2195
3960
  });
2196
3961
  }
2197
3962
  const SIZE_TIMEOUT_MS = 6e4;
3963
+ const gitRead = (operation, signal) => ({
3964
+ env: operation?.env,
3965
+ signal: signal ?? operation?.signal,
3966
+ declareFor: operation
3967
+ });
2198
3968
  function outputPaths(dir) {
2199
3969
  const paths = WORKSPACE_OUTPUT_DIRS.map((name) => join$1(dir, name)).filter((path) => existsSync$1(path));
2200
3970
  const macos = join$1(dir, "macos");
@@ -2204,48 +3974,71 @@ function outputPaths(dir) {
2204
3974
  } catch {}
2205
3975
  return paths;
2206
3976
  }
2207
- async function outputBytes(paths, timeoutMs, signal, operation) {
2208
- let total = 0;
2209
- for (const path of paths) {
2210
- const size = await measuredDirectorySizeAsync(path, {
3977
+ async function outputBytes(entries, timeoutMs, signal, operation) {
3978
+ const sizes = await mapLimited(entries.flatMap((paths, index) => paths.map((path) => ({
3979
+ path,
3980
+ index
3981
+ }))), sizeConcurrency(operation), async ({ path, index }) => ({
3982
+ index,
3983
+ size: await measuredDirectorySizeAsync(path, {
2211
3984
  timeoutMs,
2212
3985
  signal,
2213
3986
  operation
2214
- });
2215
- if (size === null) return null;
2216
- total += size;
2217
- }
2218
- return total;
3987
+ })
3988
+ }));
3989
+ return entries.map((_, index) => {
3990
+ const own = sizes.filter((size) => size.index === index).map((size) => size.size);
3991
+ return own.includes(null) ? null : own.reduce((total, size) => total + (size ?? 0), 0);
3992
+ });
2219
3993
  }
2220
- function collectWorkspaceOutputs({ olderThan, now, exclude = [], measure = true, sizeTimeoutMs = SIZE_TIMEOUT_MS, signal, operation }) {
3994
+ function collectWorkspaceOutputs({ olderThan, now, exclude = [], only, measure = true, sizeTimeoutMs = SIZE_TIMEOUT_MS, signal, operation, lighten = false }) {
2221
3995
  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);
3996
+ const outputEntries = (entries, inTree = /* @__PURE__ */ new Map()) => entries.filter((entry) => !exclude.includes(entry.dir)).filter((entry) => only === void 0 || entry.projectRoot !== null && canonicalPath(entry.projectRoot) === only).filter((entry) => entry.projectRoot === null || isOnMountedVolume(entry.projectRoot, mountedVolumes)).map((entry) => {
3997
+ const inTreeDirs = inTree.get(entry.dir) ?? [];
3998
+ return Object.assign({}, entry, {
3999
+ inTreeDirs,
4000
+ paths: [...outputPaths(entry.dir), ...inTreeDirs]
4001
+ });
4002
+ }).filter((entry) => entry.paths.length > 0);
2223
4003
  const report = (sizes) => ({
2224
4004
  root: workspacesRoot(),
2225
- workspaces: planWorkspaceOutputs(sizes.map((entry) => Object.assign({}, entry, { lastUsed: entry.projectRoot === null ? NaN : workspaceLastUsed(entry.projectRoot) })), {
4005
+ workspaces: planWorkspaceOutputs(sizes.map((entry) => Object.assign({}, entry, {
4006
+ lastUsed: entry.projectRoot === null ? NaN : workspaceLastUsed(entry.projectRoot),
4007
+ nativeUsed: lighten && entry.projectRoot !== null ? lastNativeUseFrom(readWorkspaceState$1(entry.projectRoot)) : NaN
4008
+ })), {
2226
4009
  olderThan,
2227
4010
  now
2228
4011
  })
2229
4012
  });
4013
+ const inUseOf = (root) => lighten ? workspaceBuildReasons(root) : workspaceInUse(root);
2230
4014
  if (!measure && !signal) return report(outputEntries(listWorkspaceDirs()).map(({ paths: _paths, ...entry }) => Object.assign({}, entry, {
2231
4015
  bytes: null,
2232
- inUse: entry.projectRoot === null ? [] : workspaceInUse(entry.projectRoot)
4016
+ inUse: entry.projectRoot === null ? [] : inUseOf(entry.projectRoot),
4017
+ lock: entry.projectRoot === null ? null : worktreeLock(entry.projectRoot)
2233
4018
  })));
2234
4019
  return (async () => {
2235
4020
  const sizes = [];
2236
- const entries = outputEntries(await listWorkspaceDirsAsync(signal));
2237
- for (const { paths, ...entry } of entries) {
4021
+ const listed = await listWorkspaceDirsAsync(signal);
4022
+ const inTree = /* @__PURE__ */ new Map();
4023
+ if (lighten) {
4024
+ for (const { dir, projectRoot } of listed) if (projectRoot !== null && !exclude.includes(dir)) inTree.set(dir, await inTreeBuildDirs(projectRoot, signal));
4025
+ }
4026
+ const entries = outputEntries(listed, inTree);
4027
+ const bytes = measure ? await outputBytes(entries.map((entry) => entry.paths), sizeTimeoutMs, signal, operation) : [];
4028
+ const locks = await mapLimited(entries, sizeConcurrency(operation), async ({ projectRoot }) => projectRoot === null ? null : worktreeLockAsync(projectRoot, gitRead(operation, signal)));
4029
+ for (const [index, { paths: _paths, ...entry }] of entries.entries()) {
2238
4030
  signal?.throwIfAborted();
2239
4031
  sizes.push({
2240
4032
  ...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)
4033
+ bytes: bytes[index] ?? null,
4034
+ lock: locks[index] ?? null,
4035
+ inUse: entry.projectRoot === null ? [] : !signal ? inUseOf(entry.projectRoot) : lighten ? await workspaceBuildReasonsAsync(entry.projectRoot, signal, operation) : await workspaceInUseAsync(entry.projectRoot, {}, signal, operation)
2243
4036
  });
2244
4037
  }
2245
4038
  return report(sizes);
2246
4039
  })();
2247
4040
  }
2248
- async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), operation, signal = operation?.signal }, { log, record } = GC_OUTPUT) {
4041
+ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), operation, signal = operation?.signal, lighten = false }, { log, record } = GC_OUTPUT) {
2249
4042
  let failures = 0;
2250
4043
  let cleared = 0;
2251
4044
  const clearedRoots = [];
@@ -2269,13 +4062,20 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), oper
2269
4062
  ...entry,
2270
4063
  problem: null,
2271
4064
  lastUsed: workspaceLastUsed(root),
2272
- inUse: []
4065
+ nativeUsed: lighten ? lastNativeUseFrom(readWorkspaceState$1(root)) : NaN,
4066
+ inUse: lighten && workspaceBuildInProgress(root) ? [BUILD_IN_PROGRESS] : [],
4067
+ lock: await worktreeLockAsync(root, gitRead(operation, signal))
2273
4068
  }], {
2274
4069
  olderThan,
2275
4070
  now
2276
4071
  });
2277
4072
  if (again?.keptReason) return again.keptReason;
2278
- for (const path of outputPaths(entry.dir)) {
4073
+ const inTree = [];
4074
+ for (const path of lighten ? existingDirsOf(root, ARTIFACT_DIRS.build) : []) inTree.push({
4075
+ path,
4076
+ problem: await regenerableDirProblem(root, path, signal)
4077
+ });
4078
+ for (const path of [...outputPaths(entry.dir), ...inTree.filter((dir) => !dir.problem).map((dir) => dir.path)]) {
2279
4079
  signal?.throwIfAborted();
2280
4080
  await rm(path, {
2281
4081
  recursive: true,
@@ -2283,11 +4083,13 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), oper
2283
4083
  });
2284
4084
  }
2285
4085
  signal?.throwIfAborted();
4086
+ for (const { path, problem } of inTree) if (problem) log(chalk.yellow(`Kept ${path}: ${problem}`));
2286
4087
  return null;
2287
4088
  }, {
2288
4089
  purpose: "gc",
2289
4090
  signal,
2290
- operation
4091
+ operation,
4092
+ ...lighten ? BUILD_CHECKS : {}
2291
4093
  });
2292
4094
  } catch (error) {
2293
4095
  if (error instanceof WorkspaceProcessCleanupError) throw error;
@@ -2311,10 +4113,13 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), oper
2311
4113
  }
2312
4114
  cleared += entry.bytes ?? 0;
2313
4115
  clearedRoots.push(root);
2314
- log(chalk.green(`Cleared the build outputs of ${root} (${sizeText(entry.bytes)})`));
4116
+ log(chalk.green(lighten ? `Lightened ${root}: freed ${sizeText(entry.bytes)} of build outputs` : `Cleared the build outputs of ${root} (${sizeText(entry.bytes)})`));
2315
4117
  record("workspaceOutputs", "done", root, { bytes: entry.bytes });
2316
4118
  }
2317
- if (cleared) log(chalk.dim(`Cleared ${formatBytes(cleared)} of workspace build outputs. ${REBUILD_COST}`));
4119
+ if (cleared) {
4120
+ const count = `${clearedRoots.length} workspace${clearedRoots.length === 1 ? "" : "s"}`;
4121
+ log(chalk.dim(`${lighten ? `Lightened ${count}, freeing` : "Cleared"} ${formatBytes(cleared)} of workspace build outputs. ${REBUILD_COST}`));
4122
+ }
2318
4123
  return {
2319
4124
  failures,
2320
4125
  cleared: clearedRoots
@@ -2322,7 +4127,7 @@ async function clearWorkspaceOutputs(report, { olderThan, now = Date.now(), oper
2322
4127
  }
2323
4128
  function stillOrphaned({ dir, projectRoot }) {
2324
4129
  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));
4130
+ return !Object.keys(loadConfig$1()?.projects ?? {}).some((key) => isPathPrefix$1(projectRoot, key));
2326
4131
  }
2327
4132
  async function deleteOrphanedWorkspaces(orphaned, output = GC_OUTPUT, operation) {
2328
4133
  let failures = 0;
@@ -2381,4 +4186,4 @@ function sizeText(bytes) {
2381
4186
  return bytes === null ? "size unknown" : formatBytes(bytes);
2382
4187
  }
2383
4188
  //#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 };
4189
+ export { tapBuildLog as $, runningBundleInstancesAsync as A, mapLimited as At, workspaceBuildInProgress as B, stopTunnel as C, buildSlotUsage as Ct, withManagedTunnelLock as D, createGcResults as Dt, withManagedRemoteWorktreeRemovalLock as E, GC_OUTPUT as Et, collectOwnedDeviceActivityAsync as F, buildStatusLine as G, NO_BUILD_PROGRESS as H, idleDeviceLines as I, parseActiveBuild as J, bundlingActivity as K, idleShutdownCandidates as L, collectIdleDevices as M, sizeConcurrency as Mt, collectIdleDevicesAsync as N, withManagedTunnelRemovalLock as O, takeGcResults as Ot, collectOwnedDeviceActivity as P, startBuildProgress as Q, parseIdleDuration as R, startTunnelSequence as S, acquireBuildSlot as St, withManagedRemoteWorktreeLock as T, releaseBuildSlot as Tt, activeBuildState as U, ACTIVE_BUILD_KEY as V, buildReport as W, recordFinishedBuild as X, recordBuildPhase as Y, runFailureRecord as Z, emptyWorkspaceDir as _, teardownParkedIosSim as _t, collectOrphanedWorkspaces as a, readStats$1 as at, workspaceInUseAsync as b, measureDu as bt, isInsideWorkspaces as c, recordRunStats as ct, recentNativeUseReason as d, eraseParkedIosSim as dt, thrownRunFailureRecord as et, rootPresence as f, teardownBrowserHeld as ft, ARTIFACT_DIRS as g, teardownParkedAvd as gt, workspaceDirRemoved as h, teardownOwnedIosSim as ht, clearWorkspaceOutputs as i, readRunEstimates as it, runningInstancesInAsync as j, readConcurrency as jt, hostedBundleDirs as k, eachLimited as kt, keptByLock as l, statsProjectKey$1 as lt, workspaceBuildReasons as m, teardownOwnedBrowser as mt, REBUILD_COST as n, STIM_VERSION as nt, collectWorkspaceOutputs as o, recordCapacityRefusal as ot, sizeText as p, teardownOwnedAvd as pt, estimateBuild as q, WORKSPACE_OUTPUT_DIRS as r, createRunRecorder as rt, deleteOrphanedWorkspaces as s, recordCapacityWait as st, LIGHTEN_AFTER_MS as t, errorRemedy as tt, listWorkspaceDirs as u, eraseParkedAvd as ut, withIdleWorkspace as v, probePublicHttp as vt, terminateChild as w, listBuildSlots as wt, resolveSupervisorTarget as x, writeUsageCache as xt, workspaceInUse as y, canonical$1 as yt, shutDownIdleDevices as z };