stim 1.14.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +2 -0
  2. package/dist/activity-BxwdD81S.mjs +609 -0
  3. package/dist/agent-device-usage-output-Dpih1Tfa.mjs +411 -0
  4. package/dist/{android-d6rGq1pA.mjs → android-B_NumVyQ.mjs} +169 -81
  5. package/dist/{android-LbPFRWVL.mjs → android-Ift4kVvt.mjs} +517 -413
  6. package/dist/android-cas-8U4DPqeR.mjs +167 -0
  7. package/dist/android-cas-compiler.mjs +2 -2
  8. package/dist/{android-XpvCrV7q.mjs → android-r2rBL8kV.mjs} +2 -2
  9. package/dist/{app-install-BpgSgkJj.mjs → app-install-CKWdbXIX.mjs} +128 -151
  10. package/dist/attempt-Bk2ouQVL.mjs +39 -0
  11. package/dist/budget-DjrURfQn.mjs +2700 -0
  12. package/dist/{build-plan-Dcr-3ie0.mjs → build-plan-DjeSb2Xl.mjs} +192 -653
  13. package/dist/build-slots-rUk77xqh.mjs +132 -0
  14. package/dist/{cache-manifest-C68QQ0ua.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
  15. package/dist/cache-manifest.mjs +1 -1
  16. package/dist/{chrome-BPCiC3ZX.mjs → chrome-BrcVWQyD.mjs} +1 -1
  17. package/dist/cli-BkYZ_6hy.mjs +51 -0
  18. package/dist/cli.mjs +6 -42
  19. package/dist/client-BTY686R6.mjs +1368 -0
  20. package/dist/collector-run.mjs +9 -195
  21. package/dist/{command-output-DVcpw6eE.mjs → command-output-DnJMdEY-.mjs} +1 -1
  22. package/dist/{config-lvc7d3C2.mjs → config-D0DbpZtM.mjs} +28 -29
  23. package/dist/{created-devices-CllH_8eb.mjs → created-devices-D5owjN4C.mjs} +2 -2
  24. package/dist/{dependency-state-DsT7aKeo.mjs → dependency-state-B6cMXZab.mjs} +2 -2
  25. package/dist/{deps-D9IcKqXS.mjs → deps-urbmfXig.mjs} +3 -3
  26. package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
  27. package/dist/dev-client-CroAIjjj.mjs +43 -0
  28. package/dist/{device-CjuuAJjQ.mjs → device-DZQjF7H5.mjs} +8 -7
  29. package/dist/device-capacity-BHwxNBlu.mjs +136 -0
  30. package/dist/device-host-worker.d.mts +1 -0
  31. package/dist/device-host-worker.mjs +964 -0
  32. package/dist/device-ios-BadmeL2V.mjs +381 -0
  33. package/dist/{device-lease-5c3WrVRm.mjs → device-lease-D-hLqx1b.mjs} +43 -36
  34. package/dist/{device-pool-BiyDyPP-.mjs → device-lease-run-CqjShqWW.mjs} +6 -126
  35. package/dist/device-pool-CUaEWMMm.mjs +125 -0
  36. package/dist/{device-remote-D87zEzaq.mjs → device-remote-D5WVWqOq.mjs} +69 -163
  37. package/dist/{doctor-x9pnU9fq.mjs → doctor-Cimf-gGB.mjs} +241 -103
  38. package/dist/{error-diagnostics-C5HzcwCW.mjs → error-diagnostics-B9L4Ae0x.mjs} +15 -18
  39. package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
  40. package/dist/{gc-ko6mUAYD.mjs → gc-Bqy4bjEu.mjs} +599 -108
  41. package/dist/{android-cas-iZK3DOCr.mjs → gradle-BsO1MdsH.mjs} +8 -294
  42. package/dist/{guide-6R0VNFqI.mjs → guide-B_uyKTxW.mjs} +2511 -239
  43. package/dist/{guide-status-BZrFPIA_.mjs → guide-status-CjEZMAD4.mjs} +3 -3
  44. package/dist/hosted-client-a-WmqE2L.mjs +197 -0
  45. package/dist/hosted-ios-CIWelIqf.mjs +220 -0
  46. package/dist/hosted-logs-D5g041Q3.mjs +187 -0
  47. package/dist/hosted-macos-BSZyIb3y.mjs +135 -0
  48. package/dist/idle-DYVymG92.mjs +1099 -0
  49. package/dist/idle-shutdown-CTsDnAde.mjs +122 -0
  50. package/dist/in-use-DvBw0Kxo.mjs +130 -0
  51. package/dist/{ios-BwDPrnYQ.mjs → ios-BaInn3sW.mjs} +201 -10
  52. package/dist/ios-Cr1h5gnO.mjs +189 -0
  53. package/dist/{ios-P1f5nm2n.mjs → ios-aq3QoGI3.mjs} +1159 -633
  54. package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
  55. package/dist/{ios-device-DGglU3E_.mjs → ios-device-FfCN_C8p.mjs} +12 -7
  56. package/dist/ios-state-DS5UpYTH.mjs +48 -0
  57. package/dist/{launch-verify-Deg4tB3Z.mjs → launch-verify-2DBRizjo.mjs} +5 -5
  58. package/dist/logs-CHEVK8zD.mjs +371 -0
  59. package/dist/machines-BIBUdhpj.mjs +265 -0
  60. package/dist/macos-CSYEAFmF.mjs +473 -0
  61. package/dist/macos-run.d.mts +5 -0
  62. package/dist/macos-run.mjs +2 -0
  63. package/dist/maintenance-run.d.mts +4 -0
  64. package/dist/maintenance-run.mjs +254 -0
  65. package/dist/{metro-CcEo8Fl8.mjs → metro-BeK1ZPz1.mjs} +20 -120
  66. package/dist/metro-gateway-BJ-rxQAB.mjs +84 -0
  67. package/dist/{named-ports-Blg8IxOh.mjs → named-ports-Bk_YXEmE.mjs} +6 -59
  68. package/dist/native-run-NaBrRFtQ.mjs +147 -0
  69. package/dist/{native-runtime-DjBHRg5m.mjs → native-runtime-DBzRnEKS.mjs} +9 -5
  70. package/dist/{ndjson-CDY9IkVC.mjs → ndjson-PclOIwVY.mjs} +2 -2
  71. package/dist/offload-worker.d.mts +122 -0
  72. package/dist/offload-worker.mjs +606 -0
  73. package/dist/{ownership-DyYDF5ww.mjs → ownership-DiA16AsN.mjs} +3 -2
  74. package/dist/ownership-hCkPzGmr.mjs +571 -0
  75. package/dist/{page-BHUzTDFt.mjs → page-QF_Ke10s.mjs} +7 -2
  76. package/dist/{ports-BguS83ex.mjs → ports-R2k4Knz3.mjs} +3 -3
  77. package/dist/prebuild-D2fsca_Z.mjs +213 -0
  78. package/dist/preview-DhVbvPAZ.mjs +318 -0
  79. package/dist/process-identity-BmRm1qV0.mjs +12 -0
  80. package/dist/{project-CWlvEaeN.mjs → project-Du91Ilvc.mjs} +74 -19
  81. package/dist/pull-request-pxnDCQzn.mjs +322 -0
  82. package/dist/pull-requests.d.mts +120 -0
  83. package/dist/pull-requests.mjs +2 -0
  84. package/dist/recordings-BIXOzVnr.mjs +37 -0
  85. package/dist/{reload-BiniwsSZ.mjs → reload-rM9jV1fz.mjs} +109 -24
  86. package/dist/{remote-cache-CcR6o6lI.mjs → remote-cache-C8-V13XN.mjs} +5 -5
  87. package/dist/run-f4vI6EnK.mjs +119 -0
  88. package/dist/{server-bare-DGfgyJ5J.mjs → server-bare-CpPGmnmO.mjs} +33 -15
  89. package/dist/server-expo-CVqO2c35.mjs +2 -0
  90. package/dist/{server-expo-BVuwQ65v.mjs → server-expo-MC0rHlOC.mjs} +8 -12
  91. package/dist/settings-BBpMcDbD.mjs +25 -0
  92. package/dist/{settings-B-fsC-Om.mjs → settings-ZkDzkzvB.mjs} +22 -10
  93. package/dist/settings-oTSkkNrM.mjs +1439 -0
  94. package/dist/settings.schema.json +845 -97
  95. package/dist/{simslim-DrDN4WuV.mjs → simslim-B8-7AoTI.mjs} +8 -132
  96. package/dist/{slot-launch-D5t8EiMf.mjs → slot-launch-B8VrHxn1.mjs} +11 -8
  97. package/dist/{spawn-claims-Drw_o-Fb.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
  98. package/dist/spawn-entry-jw19m8Nl.mjs +17 -0
  99. package/dist/stage-CzaiwFsI.mjs +143 -0
  100. package/dist/start-CX8Ig-U2.mjs +2 -0
  101. package/dist/{start-s4RXp1wy.mjs → start-ClDfwXa4.mjs} +116 -65
  102. package/dist/state-3DwSQIBK.mjs +41 -0
  103. package/dist/state-B4PQFXMW.d.mts +12 -0
  104. package/dist/{state-Dk9RVXI7.mjs → state-C1jB7Q8q.mjs} +70 -8
  105. package/dist/{state-DHqvsbqC.mjs → state-DcW6vOd_.mjs} +7 -4
  106. package/dist/stats-CEZ_i5je.mjs +77 -0
  107. package/dist/{status-BgAuxESg.mjs → status-BuvBFeII.mjs} +118 -20
  108. package/dist/{status-BsPAnfOI.mjs → status-Cs7VLg9D.mjs} +542 -283
  109. package/dist/status-DlKvVJBE.mjs +49 -0
  110. package/dist/stdout-reader-C8yjre1x.mjs +23 -0
  111. package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
  112. package/dist/stim-footprint.swift +32 -0
  113. package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
  114. package/dist/stop-B0Yxtmiy.mjs +48 -0
  115. package/dist/{stop-BzreOWJp.mjs → stop-Bffozsac.mjs} +83 -16
  116. package/dist/{stop-BWIF_6BK.mjs → stop-Dt_dM16A.mjs} +2 -2
  117. package/dist/stop-cause-CidxIqup.mjs +97 -0
  118. package/dist/supervisor-run.d.mts +43 -11
  119. package/dist/supervisor-run.mjs +363 -24
  120. package/dist/support-CAP8qDUF.mjs +316 -0
  121. package/dist/support-D4aYJniO.mjs +201 -0
  122. package/dist/teardown-CL6vDs2Y.mjs +727 -0
  123. package/dist/{xcode-Cp5KvK9x.mjs → toolchain-DkUflduf.mjs} +364 -14
  124. package/dist/trigger-Bemu8ytw.mjs +65 -0
  125. package/dist/trigger-CXts6qt1.mjs +2 -0
  126. package/dist/warm-progress-CpGv3Wb6.mjs +732 -0
  127. package/dist/{web-BsQs1WlO.mjs → web-DWUaMp8P.mjs} +37 -21
  128. package/dist/web-run.mjs +310 -13
  129. package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
  130. package/dist/workspace-state-B8-aJ1_5.mjs +371 -0
  131. package/dist/workspaces-BV4pdHFz.mjs +306 -0
  132. package/dist/{worktree-UrQWXSeb.mjs → worktree-K-xj9Db0.mjs} +625 -256
  133. package/dist/worktree-jnG8Iph5.mjs +2 -0
  134. package/package.json +10 -5
  135. package/shim/bundle-response.cjs +28 -12
  136. package/dist/activity-CYVDcrt3.mjs +0 -356
  137. package/dist/budget-CGDkhvv_.mjs +0 -1745
  138. package/dist/build-progress-DxKpwnaS.mjs +0 -247
  139. package/dist/idle-DXtx3RbX.mjs +0 -191
  140. package/dist/in-use-DlqBDKqY.mjs +0 -249
  141. package/dist/logs-Civ4gbBH.mjs +0 -486
  142. package/dist/merge-state-ytFQCeh4.mjs +0 -262
  143. package/dist/server-expo-DIHDhvGg.mjs +0 -2
  144. package/dist/settings-D61iNOsB.mjs +0 -629
  145. package/dist/start-Cetfvy8l.mjs +0 -2
  146. package/dist/stats-CFpFGlJV.mjs +0 -62
  147. package/dist/stats-CX1Z8Tca.mjs +0 -368
  148. package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
  149. package/dist/stop-cause-hjGBOZTG.mjs +0 -1231
  150. package/dist/worktree-BVrHsYZG.mjs +0 -752
  151. package/dist/worktree-CHWWvm5v.mjs +0 -2
  152. package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
@@ -1,32 +1,89 @@
1
- import { I as volumeRootFor, U as projectDeviceSlots, _ as loadConfig$1, d as getConfigDir, z as deviceSlotKey } from "./config-lvc7d3C2.mjs";
2
- import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
3
- import { a as parseNdjsonLine } from "./ndjson-CDY9IkVC.mjs";
4
- import { M as workspaceLogsDir, c as resolveProjectMetro, h as readWorkspaceState$1, i as listeningPidsByPort, j as workspaceLogErrorIndex, o as processCwd, r as listeningPids, s as processCwds, u as isMetroRunning } from "./metro-CcEo8Fl8.mjs";
5
- import { c as gitCommonDirOnDisk, p as linkedWorktreesOnDisk, s as gitCommonDir, y as repoRoot } from "./worktree-BVrHsYZG.mjs";
6
- import { l as projectShortcut, o as findServerWorkspace, r as detectIsExpo } from "./project-CWlvEaeN.mjs";
7
- import { f as readWorkspaceLaunches, u as readRemoteSession } from "./state-DHqvsbqC.mjs";
8
- import { i as formatDuration } from "./command-output-DVcpw6eE.mjs";
9
- import { i as listLeaseFiles } from "./device-lease-5c3WrVRm.mjs";
10
- import { n as createActivityReader, r as createDeviceProcessTables } from "./activity-CYVDcrt3.mjs";
11
- import { P as ownedAvdDeviceProfile, R as ownedAvdSerialResolver, ct as parkedMaxSetting, lt as readParked, rt as POOL_SETTING_REMEDY, u as androidToolPath } from "./android-d6rGq1pA.mjs";
12
- import { n as describeMetroLastStop, o as metroLastStop, u as resolveSupervisorTarget } from "./stop-cause-hjGBOZTG.mjs";
13
- import { a as readWebRecord, l as webFacts } from "./state-Dk9RVXI7.mjs";
14
- import { _ as parseSimctlList, u as listAllIosSimsAsync } from "./ios-BwDPrnYQ.mjs";
15
- import { a as buildStatusLine, i as buildReport, r as activeBuildState, s as parseActiveBuild, t as ACTIVE_BUILD_KEY } from "./build-progress-DxKpwnaS.mjs";
16
- import { T as readEasSessionLedger$1, w as easMachineStateRoot$1 } from "./device-remote-D87zEzaq.mjs";
17
- import { _ as withWebFacts, a as deviceLeaseStates, d as poolLine, f as remoteDeviceLine, g as unprovisionedWorktrees, h as tightVolumes, i as deviceLeaseLines, l as gitSummaryText, m as statusActivity, n as activityLabel, o as diskLine, p as remoteDeviceState, r as capacity, s as environmentState, u as parseDfFree } from "./status-BgAuxESg.mjs";
18
- import { a as statsProjectKey, r as readStats } from "./stats-CX1Z8Tca.mjs";
19
- import { n as mergeState } from "./merge-state-ytFQCeh4.mjs";
20
- import { i as readSupervisorState } from "./stop-BzreOWJp.mjs";
21
- import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
22
- import { closeSync as closeSync$1, existsSync as existsSync$1, fstatSync as fstatSync$1, openSync as openSync$1, readFileSync as readFileSync$1, readSync as readSync$1, renameSync as renameSync$1, rmSync as rmSync$1, writeFileSync as writeFileSync$1 } from "fs";
23
- import { basename, dirname, join } from "path";
24
- import { rotatedLogPath } from "@stim-cli/core";
25
- import { ERROR_SOURCES, MACHINE_OWNER_KINDS, gitMergeCacheDir, logFiles, queryLogs, readBuildHistory, readIdleStop, readLastBuilds, recordMatches, withStateReadCache } from "@stim-cli/core/state";
26
- import { join as join$1, posix, resolve as resolve$1 } from "node:path";
1
+ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
+ import { B as parseDeviceSlotKey, F as volumeRootFor, H as projectDeviceSlots, R as deviceSlotKey, _ as loadConfig$1, d as getConfigDir, j as formatBytes } from "./config-D0DbpZtM.mjs";
3
+ import { t as process_identity_exports } from "./process-identity-BmRm1qV0.mjs";
4
+ import { a as formatElapsed, i as formatDuration, r as clockTime, u as plural } from "./command-output-DnJMdEY-.mjs";
5
+ import { P as resolveSettings, at as gitCommonDirOnDisk, gt as repoRoot, it as gitCommonDir, ut as linkedWorktreesOnDisk } from "./settings-oTSkkNrM.mjs";
6
+ import { i as detectPlatforms, r as detectIsExpo, s as findServerWorkspace, u as projectShortcut } from "./project-Du91Ilvc.mjs";
7
+ import { a as listeningPidsByPort, c as processCwds, d as isMetroRunning, i as listeningPids, l as resolveProjectMetro, o as pidExists, s as processCwd } from "./metro-BeK1ZPz1.mjs";
8
+ import { b as workspaceLogErrorIndex, r as readWorkspaceState$1, x as workspaceLogsDir$1 } from "./workspace-state-B8-aJ1_5.mjs";
9
+ import { a as readWebPage, o as readWebRecord, u as webFacts } from "./state-C1jB7Q8q.mjs";
10
+ import { _ as statusActivity, a as capacity, b as withWebFacts, c as diskLine, d as gitSummaryText, f as metroReverseTargets, g as remoteDeviceState, h as remoteDeviceLine, i as applyMachineMemory, l as environmentState, m as poolLine, n as activityLabel, o as deviceLeaseLines, p as parseDfFree, r as addReverseMissingIssue, s as deviceLeaseStates, v as tightVolumes, y as unprovisionedWorktrees } from "./status-BuvBFeII.mjs";
11
+ import { J as attributeMachineUsage, Y as readFootprints, a as machineNumber } from "./budget-DjrURfQn.mjs";
12
+ import { d as readWebActivity, f as tailLines, i as createDeviceProcessTables, r as createActivityReader } from "./activity-BxwdD81S.mjs";
13
+ import { i as listLeaseFiles, r as leaseIsExpired, s as parseWorkspaceLeases } from "./device-lease-D-hLqx1b.mjs";
14
+ import { c as readMetroTunnel, f as readWorkspaceLaunches, u as readRemoteSession } from "./state-DcW6vOd_.mjs";
15
+ import { S as statsProjectKey$1, b as readStats$1, c as ACTIVE_BUILD_KEY, d as buildReport, f as buildStatusLine, m as parseActiveBuild, u as activeBuildState } from "./idle-DYVymG92.mjs";
16
+ import { F as ownedAvdDeviceProfile, O as listAdbDevices, d as androidToolPath, ft as parkedMaxSetting, ot as POOL_SETTING_REMEDY, pt as readParked, z as ownedAvdSerialResolver } from "./android-B_NumVyQ.mjs";
17
+ import { d as listAllIosSimsAsync, v as parseSimctlList } from "./ios-BaInn3sW.mjs";
18
+ import { t as resolveSupervisorTarget } from "./ownership-hCkPzGmr.mjs";
19
+ import { a as parseNdjsonLine } from "./ndjson-PclOIwVY.mjs";
20
+ import { n as describeMetroLastStop, o as metroLastStop } from "./stop-cause-CidxIqup.mjs";
21
+ import { l as probeHostedSession } from "./hosted-client-a-WmqE2L.mjs";
22
+ import { m as hasMetroReverse } from "./app-install-CKWdbXIX.mjs";
23
+ import "./hosted-macos-BSZyIb3y.mjs";
24
+ import { m as readEasSessionLedger$1, p as easMachineStateRoot$1 } from "./device-remote-D5WVWqOq.mjs";
25
+ import { d as readIosDevices, s as isReachable } from "./ios-device-FfCN_C8p.mjs";
26
+ import { n as maintenanceLines, r as maintenanceStatus } from "./status-DlKvVJBE.mjs";
27
+ import { i as createStatusMeasurer, n as workspacePhase, r as applyStatusMeasures, s as mergeState } from "./warm-progress-CpGv3Wb6.mjs";
28
+ import { r as workspaceRecordingEnabled } from "./recordings-BIXOzVnr.mjs";
29
+ import { t as watchStdoutReader } from "./stdout-reader-C8yjre1x.mjs";
30
+ import { t as triggerMaintenance } from "./trigger-Bemu8ytw.mjs";
31
+ import { i as readSupervisorState } from "./stop-Bffozsac.mjs";
27
32
  import { createHash, randomUUID } from "node:crypto";
33
+ import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
34
+ import { join, posix, resolve } from "node:path";
35
+ import { closeSync as closeSync$1, existsSync as existsSync$1, fstatSync as fstatSync$1, openSync as openSync$1, readFileSync as readFileSync$1, readSync as readSync$1, renameSync as renameSync$1, rmSync as rmSync$1, statSync as statSync$1, writeFileSync as writeFileSync$1 } from "fs";
28
36
  import { homedir, totalmem } from "os";
37
+ import { basename as basename$1, dirname as dirname$1, join as join$1 } from "path";
38
+ import { rotatedLogPath } from "@stim-cli/core";
39
+ import { ERROR_SOURCES, archiveRoot, archivedUsage, bundleMarkerPlatform, countErrorEntries, gitMergeCacheDir, hostedIosPlacements, hostedIosRecords, hostedIosStatus, linkReplacedArchives, logFiles, macosAppState, parseHostedIosPlacement, queryLogs, readArchives, readBuildDetail, readBuildHistory, readDeviceIdleShutdowns, readIdleStop, readLastBuilds, readMacosRecord, recordMatches, sortByTs, unreadableHostedIos, withDerivedFacts, withGitChip, withStateReadCache } from "@stim-cli/core/state";
29
40
  import chalk from "chalk";
41
+ //#region src/device-host/hosted-ios-status.ts
42
+ function applyHostedIosProbe(placement, probe, slot = "default") {
43
+ const rerun = `stim ios --remote ${placement.machine}${slot === "default" ? "" : ` --slot ${slot}`}`;
44
+ const state = probe.state === "ready" ? "ready" : probe.state === "stopped" ? "stopped" : "unverified";
45
+ return {
46
+ ios: {
47
+ host: {
48
+ ...hostedIosStatus(placement),
49
+ state
50
+ },
51
+ name: placement.device?.name ?? null,
52
+ udid: "",
53
+ owned: false,
54
+ state
55
+ },
56
+ ...probe.state === "ready" ? {} : { warning: probe.state === "stopped" ? `The iOS session on ${placement.machine} stopped or no longer exists. Run ${rerun} to launch it again, or stim stop to clear the placement.` : `${placement.machine} could not confirm its iOS session${probe.state === "unknown" ? probe.notice ? `: ${probe.notice}` : "" : `: ${probe.reason.replace(/\.+$/, "")}`}. The placement stays recorded; run stim stop when the host answers.` }
57
+ };
58
+ }
59
+ async function readHostedIosStatus(placements) {
60
+ return Object.fromEntries(await Promise.all(Object.entries(placements).map(async ([slot, placement]) => [slot, applyHostedIosProbe(placement, await probeHostedSession(placement), slot)])));
61
+ }
62
+ //#endregion
63
+ //#region src/device-host/hosted-macos-status.ts
64
+ function applyHostedMacosProbe(record, probe) {
65
+ if (!record.host || probe.state === "ready") return { record };
66
+ const { machine, session } = record.host;
67
+ if (probe.state === "stopped") return {
68
+ record: {
69
+ ...record,
70
+ hostLaunched: false
71
+ },
72
+ warning: `The macOS app on ${machine} is no longer running (the host reports session ${session} stopped or no longer holds it, for example after a stim-server restart there). Run stim macos --remote ${machine} to launch it again, or stim stop to clear the placement.`
73
+ };
74
+ return {
75
+ record: {
76
+ ...record,
77
+ hostLaunched: "unverified"
78
+ },
79
+ warning: probe.state === "unknown" ? `${probe.notice ?? `The host cannot confirm session ${session} on ${machine}.`} Run stim stop to reconcile it.` : `${machine} could not be checked (${probe.reason}), so the macOS app there is unverified. The placement stays recorded; run stim stop when the host answers.`
80
+ };
81
+ }
82
+ async function readHostedMacosStatus(record, options) {
83
+ if (!record?.host || record.supervisor || !record.hostLaunched) return { record };
84
+ return applyHostedMacosProbe(record, await probeHostedSession(record.host, options));
85
+ }
86
+ //#endregion
30
87
  //#region src/status-watch.ts
31
88
  const WATCH_FALLBACK_MS = 3e4;
32
89
  const WATCH_LIGHT_INTERVAL_MS = 15e3;
@@ -88,17 +145,23 @@ function createRefreshScheduler({ debounceMs, lightIntervalMs = 0, run }) {
88
145
  };
89
146
  }
90
147
  /**
91
- * Which refresh a change to `name` in a watched `$STIM_HOME` directory needs: `light` for a log append, which can
92
- * change only the log-derived fields, `full` for anything else that can change the payload, null for none. A null
93
- * name means the platform did not report one.
148
+ * Which refresh a change to `name` in a watched `$STIM_HOME` directory needs: `light` for a log append or a running
149
+ * build's detail, which can change only the log-derived fields, `full` for anything else that can change the payload,
150
+ * null for none. A null name means the platform did not report one.
94
151
  */
95
152
  function statusChange(dir, name) {
153
+ if (dir === "archive" && (name?.startsWith(".incoming-") || name?.startsWith(".removing-") || name?.startsWith(".lock"))) return null;
96
154
  if (name?.includes(".lock")) return null;
97
155
  if (dir === "logs") return "light";
98
156
  if (name === null) return "full";
99
157
  switch (dir) {
100
- case "home": return name.startsWith("config.json") || name === "workspaces" || name === "device-locks" ? "full" : null;
101
- case "workspace": return name.startsWith("state.json") || name === "logs" ? "full" : null;
158
+ case "home":
159
+ if (name === "maintenance") return "light";
160
+ return name.startsWith("config.json") || name === "archive" || name === "workspaces" || name === "device-locks" ? "full" : null;
161
+ case "workspace":
162
+ if (name.startsWith("build-detail.json")) return "light";
163
+ return name.startsWith("state.json") || name.startsWith("ended-agents.json") || name === "logs" ? "full" : null;
164
+ case "maintenance": return name === "state.json" ? "light" : null;
102
165
  case "eas": return name.startsWith("sessions.json") ? "full" : null;
103
166
  default: return "full";
104
167
  }
@@ -122,18 +185,20 @@ function watchStatusSources({ home, onChange, platform = process.platform, simct
122
185
  const desiredDirs = () => {
123
186
  const dirs = /* @__PURE__ */ new Map([
124
187
  [home, "home"],
125
- [join$1(home, "workspaces"), "workspaces"],
126
- [join$1(home, "device-locks"), "leases"],
188
+ [join(home, "archive"), "archive"],
189
+ [join(home, "workspaces"), "workspaces"],
190
+ [join(home, "maintenance"), "maintenance"],
191
+ [join(home, "device-locks"), "leases"],
127
192
  [easMachineStateRoot$1(), "eas"]
128
193
  ]);
129
194
  let names = [];
130
195
  try {
131
- names = readdirSync(join$1(home, "workspaces"));
196
+ names = readdirSync(join(home, "workspaces"));
132
197
  } catch {}
133
198
  for (const name of names) {
134
- const dir = join$1(home, "workspaces", name);
199
+ const dir = join(home, "workspaces", name);
135
200
  dirs.set(dir, "workspace");
136
- dirs.set(join$1(dir, "logs"), "logs");
201
+ dirs.set(join(dir, "logs"), "logs");
137
202
  }
138
203
  return dirs;
139
204
  };
@@ -266,7 +331,7 @@ function trackAdbDevices(onChange) {
266
331
  }
267
332
  //#endregion
268
333
  //#region src/diagnostics/error-index.ts
269
- const INDEX_VERSION = 3;
334
+ const INDEX_VERSION = 7;
270
335
  const HEAD_BYTES = 1024;
271
336
  const TAIL_BYTES = 256;
272
337
  const CHUNK_BYTES = 4194304;
@@ -282,7 +347,7 @@ const ERROR_CRITERIA = {
282
347
  includeAppDeviceErrors: true
283
348
  };
284
349
  /**
285
- * The number of records `queryLogs({ dir, errorsOnly: true })` returns, without parsing every
350
+ * The number of rows the apps list for `queryLogs({ dir, errorsOnly: true })`, one per failure, without parsing every
286
351
  * line on every call. `indexFile` keeps, per log file generation, the markers and error records
287
352
  * already read and the byte offset they were read through; a later call reads only what was
288
353
  * appended since. The index is a cache: losing it or a concurrent writer only costs a rescan.
@@ -292,7 +357,7 @@ function countErrorsSinceMarker(dir, indexFile) {
292
357
  const files = {};
293
358
  let changed = false;
294
359
  for (const name of logFiles(dir)) {
295
- const current = join(dir, name);
360
+ const current = join$1(dir, name);
296
361
  for (const path of [rotatedLogPath(current), current]) {
297
362
  const result = summarize(path, previous.files);
298
363
  if (!result) continue;
@@ -306,10 +371,10 @@ function countErrorsSinceMarker(dir, indexFile) {
306
371
  });
307
372
  const records = [];
308
373
  for (const summary of Object.values(files)) records.push(...Object.values(summary.markers), ...summary.errors);
309
- return queryLogs({
374
+ return countErrorEntries(sortByTs(queryLogs({
310
375
  records,
311
376
  errorsOnly: true
312
- }).length;
377
+ })));
313
378
  }
314
379
  function summarize(path, known) {
315
380
  let fd;
@@ -401,11 +466,17 @@ function collect(record, markers, errors) {
401
466
  const metro = record.src === "metro";
402
467
  const web = !metro && record.platform === "web";
403
468
  const slot = record.slot ?? "default";
404
- const key = metro ? "metro" : web ? "web" : JSON.stringify(["launch", slot]);
469
+ const platform = metro ? bundleMarkerPlatform(record) : record.platform ?? null;
470
+ const key = web ? "web" : JSON.stringify([
471
+ metro ? "metro" : "launch",
472
+ slot,
473
+ platform
474
+ ]);
405
475
  if ((markers[key]?.ts ?? -Infinity) < ts) markers[key] = metro ? {
406
476
  src: "metro",
407
477
  marker: true,
408
- ts
478
+ ts,
479
+ ...platform === null ? {} : { platform }
409
480
  } : web ? {
410
481
  platform: "web",
411
482
  marker: true,
@@ -413,16 +484,20 @@ function collect(record, markers, errors) {
413
484
  } : {
414
485
  marker: true,
415
486
  ts,
416
- slot
487
+ slot,
488
+ ...platform === null ? {} : { platform }
417
489
  };
418
490
  }
419
491
  if (recordMatches(record, ERROR_CRITERIA)) errors.push({
420
492
  src: record.src,
421
493
  level: record.level,
422
494
  event: record.event,
423
- platform: record.platform,
495
+ platform: record.src === "metro" && record.marker === true ? bundleMarkerPlatform(record) : record.platform,
424
496
  ts: ts ?? void 0,
425
- slot: record.slot
497
+ slot: record.slot,
498
+ raw: record.raw,
499
+ marker: record.marker,
500
+ msg: record.marker === true ? record.msg : void 0
426
501
  });
427
502
  }
428
503
  function readIndex(file) {
@@ -513,7 +588,7 @@ async function defaultBranchOf(repository) {
513
588
  };
514
589
  }
515
590
  function mergeCacheFile(path) {
516
- return join$1(gitMergeCacheDir(), `${createHash("sha256").update(path).digest("hex").slice(0, 32)}.json`);
591
+ return join(gitMergeCacheDir(), `${createHash("sha256").update(path).digest("hex").slice(0, 32)}.json`);
517
592
  }
518
593
  function readMergeCache(path) {
519
594
  try {
@@ -587,14 +662,14 @@ const mergeTimeouts = /* @__PURE__ */ new Map();
587
662
  const recent = /* @__PURE__ */ new Map();
588
663
  function gitDirsOf(worktree) {
589
664
  try {
590
- const dotGit = join$1(worktree, ".git");
665
+ const dotGit = join(worktree, ".git");
591
666
  const pointer = statSync(dotGit).isDirectory() ? null : /^gitdir: (.+)$/m.exec(readFileSync(dotGit, "utf-8"))?.[1]?.trim();
592
- const gitDir = pointer ? resolve$1(worktree, pointer) : pointer === null ? dotGit : null;
667
+ const gitDir = pointer ? resolve(worktree, pointer) : pointer === null ? dotGit : null;
593
668
  if (!gitDir) return null;
594
669
  try {
595
670
  return {
596
671
  gitDir,
597
- commonDir: resolve$1(gitDir, readFileSync(join$1(gitDir, "commondir"), "utf-8").trim())
672
+ commonDir: resolve(gitDir, readFileSync(join(gitDir, "commondir"), "utf-8").trim())
598
673
  };
599
674
  } catch {
600
675
  return {
@@ -623,21 +698,21 @@ function fileStamp(path) {
623
698
  function gitFilesStamp(worktree, refs) {
624
699
  const dirs = gitDirsOf(worktree.path);
625
700
  if (!dirs) return {
626
- stamp: fileStamp(join$1(worktree.path, ".git")),
701
+ stamp: fileStamp(join(worktree.path, ".git")),
627
702
  missing: true
628
703
  };
629
704
  const { gitDir, commonDir } = dirs;
630
705
  return {
631
706
  stamp: [
632
- join$1(gitDir, "index"),
633
- join$1(gitDir, "HEAD"),
634
- join$1(gitDir, "logs", "HEAD"),
635
- join$1(commonDir, "packed-refs"),
636
- join$1(commonDir, "FETCH_HEAD"),
637
- join$1(commonDir, "refs", "remotes", "origin", "HEAD"),
638
- ...refs.target ? [join$1(commonDir, refs.target)] : [],
639
- ...worktree.branch ? [join$1(commonDir, "refs", "heads", worktree.branch)] : [],
640
- ...refs.upstream ? [join$1(commonDir, "refs", "remotes", refs.upstream), join$1(commonDir, "refs", "heads", refs.upstream)] : []
707
+ join(gitDir, "index"),
708
+ join(gitDir, "HEAD"),
709
+ join(gitDir, "logs", "HEAD"),
710
+ join(commonDir, "packed-refs"),
711
+ join(commonDir, "FETCH_HEAD"),
712
+ join(commonDir, "refs", "remotes", "origin", "HEAD"),
713
+ ...refs.target ? [join(commonDir, refs.target)] : [],
714
+ ...worktree.branch ? [join(commonDir, "refs", "heads", worktree.branch)] : [],
715
+ ...refs.upstream ? [join(commonDir, "refs", "remotes", refs.upstream), join(commonDir, "refs", "heads", refs.upstream)] : []
641
716
  ].map(fileStamp).join(" "),
642
717
  missing: false
643
718
  };
@@ -753,7 +828,7 @@ function plistBundleId(appPath) {
753
828
  "raw",
754
829
  "-o",
755
830
  "-",
756
- join$1(appPath, "Info.plist")
831
+ join(appPath, "Info.plist")
757
832
  ], { timeoutMs: 2e3 })?.trim() || null;
758
833
  if (id) bundleIdsByApp.set(appPath, id);
759
834
  return id;
@@ -777,196 +852,178 @@ function createAppProcessReader(tables, bundleIdOf = plistBundleId) {
777
852
  };
778
853
  }
779
854
  //#endregion
780
- //#region src/machine-usage.ts
781
- const SHARED = [
782
- {
783
- name: "CoreSimulator services",
784
- pattern: /\/CoreSimulator\.framework\/|\/Simulator\.app\/Contents\/MacOS\//
785
- },
786
- {
787
- name: "adb server",
788
- pattern: /(^|\/)adb\s.*\bfork-server\b/
789
- },
790
- {
791
- name: "Android emulator services",
792
- pattern: /\/emulator\/netsimd(\s|$)/
793
- },
794
- {
795
- name: "Gradle daemon",
796
- pattern: /\borg\.gradle\.launcher\.daemon\.bootstrap\.GradleDaemon\b/
797
- },
798
- {
799
- name: "Kotlin daemon",
800
- pattern: /\bKotlinCompileDaemon\b/
801
- },
802
- {
803
- name: "Watchman",
804
- pattern: /(^|\/)watchman(\s|$)/
855
+ //#region src/metro-bundle.ts
856
+ const IN_FLIGHT_MAX_MS = 6e5;
857
+ const RESTART_EVENTS = /* @__PURE__ */ new Set(["supervisor_started", "server_started"]);
858
+ const BUNDLE_EVENT = /^bundle_(?:response|prefetch)_(started|progress|finished|failed)$/;
859
+ function parseRecord(line) {
860
+ if (!line.includes("\"bundle_") && !line.includes("_started\"")) return null;
861
+ try {
862
+ const value = JSON.parse(line);
863
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
864
+ } catch {
865
+ return null;
805
866
  }
806
- ];
807
- const SERVER = /(^|[\s/])stim-server(\.mjs)?(\s|$)/;
808
- const LAUNCHD_SIM = /^launchd_sim\s.*\/Devices\/([^/\s]+)\//;
809
- const MAX_DEPTH = 64;
810
- const EMULATOR = /(?:^|\/)(?:qemu-system-[^\s/]+|emulator)(?:\s.*)?\s(?:-avd\s+|@)(\S+)/;
811
- function emulatorAvd(command) {
812
- return EMULATOR.exec(command)?.[1] ?? null;
813
- }
814
- function workspaceOwner(kind, env, name, id) {
815
- return {
816
- kind,
817
- name,
818
- workspace: env.path,
819
- id,
820
- owned: true
821
- };
822
867
  }
823
- function rank(owner) {
824
- return MACHINE_OWNER_KINDS.indexOf(owner.kind);
868
+ function platformOf(value) {
869
+ return value === "ios" || value === "android" ? value : null;
825
870
  }
826
871
  /**
827
- * Attributes every process in `processes` to at most one owner: the owner of its nearest ancestor (itself included)
828
- * that is a root. Roots are each booted simulator's `launchd_sim`, each emulator launcher and qemu process, and each
829
- * workspace's verified supervisor and Metro, running build and Chrome. Only a process under none of those falls back
830
- * to stim-server or a machine-wide service in `SHARED`, so a `simctl` a workspace's log collector runs stays with the
831
- * workspace. A process with no root ancestor is left out.
872
+ * The bundle state of a workspace's Metro from the tail of its `metro.ndjson`: the records Stim's middleware writes
873
+ * for each native bundle request, an app's (`bundle_response_*`) or Stim's own prefetch (`bundle_prefetch_*`),
874
+ * matched by `requestId`. A request counts as in flight only while
875
+ * Metro runs, when it started under the current dev server and less than 10 minutes ago, since a dev server that
876
+ * died mid-request never records its end. Null when the tail holds no bundle request.
832
877
  */
833
- function attributeMachineUsage({ processes, environments, roots, simNames }) {
834
- const owners = /* @__PURE__ */ new Map();
835
- const rootOf = /* @__PURE__ */ new Map();
836
- const fallbackOf = /* @__PURE__ */ new Map();
837
- const claim = (pid, key, owner, into = rootOf) => {
838
- if (!owners.has(key)) owners.set(key, owner);
839
- into.set(pid, key);
840
- };
841
- const sims = /* @__PURE__ */ new Map();
842
- const emulators = /* @__PURE__ */ new Map();
843
- for (const env of environments) for (const device of [{
844
- slot: "default",
845
- ios: env.ios,
846
- android: env.android
847
- }, ...env.slots ?? []]) {
848
- const slot = device.slot === "default" ? {} : { slot: device.slot };
849
- if (device.ios?.state === "Booted") sims.set(device.ios.udid.toUpperCase(), {
850
- kind: "simulator",
851
- name: device.ios.name ?? device.ios.udid,
852
- workspace: env.path,
853
- ...slot,
854
- id: device.ios.udid,
855
- owned: device.ios.owned
856
- });
857
- if (device.android?.name && !device.android.physical && device.android.serial) emulators.set(device.android.name, {
858
- kind: "emulator",
859
- name: device.android.name,
860
- workspace: env.path,
861
- ...slot,
862
- id: device.android.name,
863
- owned: device.android.owned
864
- });
865
- }
866
- for (const p of processes) {
867
- const shared = SHARED.find(({ pattern }) => pattern.test(p.command));
868
- if (shared) {
869
- const owner = {
870
- kind: "shared",
871
- name: shared.name,
872
- workspace: null,
873
- id: null,
874
- owned: false
875
- };
876
- claim(p.pid, `shared:${shared.name}`, owner, fallbackOf);
878
+ function metroBundleState(lines, { running, now }) {
879
+ let open = /* @__PURE__ */ new Map();
880
+ let last;
881
+ let seen = false;
882
+ for (const line of lines) {
883
+ const record = parseRecord(line);
884
+ if (!record) continue;
885
+ const { event, requestId, ts } = record;
886
+ if (typeof event !== "string") continue;
887
+ if (RESTART_EVENTS.has(event)) {
888
+ open = /* @__PURE__ */ new Map();
877
889
  continue;
878
890
  }
879
- if (SERVER.test(p.command)) {
880
- claim(p.pid, "server", {
881
- kind: "server",
882
- name: "stim-server",
883
- workspace: null,
884
- id: null,
885
- owned: false
886
- }, fallbackOf);
887
- continue;
888
- }
889
- const udid = LAUNCHD_SIM.exec(p.command)?.[1]?.toUpperCase();
890
- if (udid) {
891
- const owner = sims.get(udid) ?? {
892
- kind: "simulator",
893
- name: simNames[udid] ?? udid,
894
- workspace: null,
895
- id: udid,
896
- owned: false
897
- };
898
- claim(p.pid, `simulator:${udid}`, owner);
899
- continue;
900
- }
901
- const avd = emulatorAvd(p.command);
902
- if (avd) {
903
- const owner = emulators.get(avd) ?? {
904
- kind: "emulator",
905
- name: avd,
906
- workspace: null,
907
- id: avd,
908
- owned: false
891
+ const platform = platformOf(record.platform);
892
+ const stage = BUNDLE_EVENT.exec(event)?.[1];
893
+ if (!stage || typeof requestId !== "string" || typeof ts !== "number" || !platform) continue;
894
+ seen = true;
895
+ if (stage === "started") open.set(requestId, {
896
+ platform,
897
+ startedAt: ts
898
+ });
899
+ else if (stage === "progress") {
900
+ const request = open.get(requestId);
901
+ const percent = record.percent;
902
+ if (request && typeof percent === "number" && percent >= 0 && percent <= 100) request.percent = percent;
903
+ } else {
904
+ const request = open.get(requestId);
905
+ open.delete(requestId);
906
+ if (!request) continue;
907
+ last = {
908
+ platform,
909
+ status: stage === "finished" ? "ok" : "failed",
910
+ durationMs: Math.max(0, ts - request.startedAt),
911
+ finishedAt: new Date(ts).toISOString()
909
912
  };
910
- claim(p.pid, `emulator:${avd}`, owner);
911
- }
912
- }
913
- const present = new Set(processes.map((p) => p.pid));
914
- const byPath = new Map(environments.map((env) => [env.path, env]));
915
- for (const root of roots) {
916
- const env = byPath.get(root.path);
917
- if (!env) continue;
918
- const metroPids = [root.supervisorPid, env.metro?.running ? env.metro.pid : null];
919
- for (const pid of metroPids) if (pid && present.has(pid)) {
920
- const port = env.metro?.port ?? null;
921
- claim(pid, `metro:${env.path}`, workspaceOwner("metro", env, port ? `Metro :${port}` : "Metro", port ? String(port) : null));
922
- }
923
- if (root.build && present.has(root.build.pid)) {
924
- const label = root.build.platform === "ios" ? "iOS build" : "Android build";
925
- claim(root.build.pid, `build:${env.path}`, workspaceOwner("build", env, label, root.build.platform));
926
913
  }
927
- for (const pid of root.browserPids) if (present.has(pid)) claim(pid, `browser:${env.path}`, workspaceOwner("browser", env, "Chrome", null));
928
914
  }
929
- const parentOf = new Map(processes.map((p) => [p.pid, p.ppid]));
930
- const totals = /* @__PURE__ */ new Map();
931
- for (const p of processes) {
932
- let pid = p.pid;
933
- let key;
934
- let fallback;
935
- for (let depth = 0; pid !== void 0 && pid > 1 && depth < MAX_DEPTH; depth++) {
936
- key = rootOf.get(pid);
937
- if (key) break;
938
- fallback ??= fallbackOf.get(pid);
939
- pid = parentOf.get(pid);
940
- }
941
- key ??= fallback;
942
- if (!key) continue;
943
- const total = totals.get(key) ?? {
944
- rssKb: 0,
945
- cpu: 0,
946
- processes: 0
947
- };
948
- total.rssKb += p.rssKb;
949
- total.cpu += p.cpuPercent;
950
- total.processes += 1;
951
- totals.set(key, total);
952
- }
953
- const list = [];
954
- for (const [key, owner] of owners) {
955
- const total = totals.get(key);
956
- if (total) list.push({
957
- ...owner,
958
- cpuPercent: Math.round(total.cpu),
959
- residentMb: Math.round(total.rssKb / 1024),
960
- processes: total.processes
915
+ if (!seen) return null;
916
+ const active = running ? [...open.values()].filter((request) => now - request.startedAt < IN_FLIGHT_MAX_MS).toSorted((a, b) => b.startedAt - a.startedAt)[0] : void 0;
917
+ return {
918
+ bundling: Boolean(active),
919
+ ...active ? {
920
+ platform: active.platform,
921
+ startedAt: new Date(active.startedAt).toISOString(),
922
+ ...active.percent === void 0 ? {} : { percent: active.percent }
923
+ } : {},
924
+ ...last ? { last } : {}
925
+ };
926
+ }
927
+ //#endregion
928
+ //#region src/physical-devices.ts
929
+ const EMULATOR_SERIAL = /^emulator-\d+$/;
930
+ /**
931
+ * The physical devices a workspace holds: each lease its state records whose file still carries the same token,
932
+ * names this workspace as holder, and has not expired. A lease on a simulator (a UDID in `simulatorUdids`) or an
933
+ * emulator (an `emulator-<port>` serial), which `device lock` also takes, is not a physical device. `read` is
934
+ * called once per device.
935
+ */
936
+ function physicalDeviceStates(entries, held, { root, now, simulatorUdids, read }) {
937
+ const out = [];
938
+ for (const [key, record] of Object.entries(held)) {
939
+ const parsed = parseDeviceSlotKey(key);
940
+ const lease = entries.find((entry) => entry.lease?.token === record.token)?.lease;
941
+ if (!parsed || !lease || lease.platform !== parsed.platform || lease.id !== record.id) continue;
942
+ if (lease.holder !== root || leaseIsExpired(lease, now)) continue;
943
+ if (parsed.platform === "ios" ? simulatorUdids.has(lease.id.toUpperCase()) : EMULATOR_SERIAL.test(lease.id)) continue;
944
+ const reading = read(parsed.platform, lease.id);
945
+ out.push({
946
+ platform: parsed.platform,
947
+ slot: parsed.slot,
948
+ id: lease.id,
949
+ name: reading.name ?? lease.deviceName,
950
+ model: reading.model ?? (parsed.platform === "android" && lease.deviceName !== lease.id ? lease.deviceName : null),
951
+ owned: false,
952
+ physical: true,
953
+ connection: reading.connection,
954
+ lease: {
955
+ holder: lease.holder,
956
+ kind: record.kind,
957
+ grantedAt: lease.grantedAt,
958
+ expiresAt: lease.expiresAt
959
+ }
961
960
  });
962
961
  }
963
- list.sort((x, y) => rank(x) - rank(y) || (x.workspace ?? "~").localeCompare(y.workspace ?? "~") || x.name.localeCompare(y.name));
964
- return { owners: list };
962
+ return out;
963
+ }
964
+ /** A phone's reading from devicectl's device list; null `devices` means devicectl could not be read. */
965
+ function iosPhysicalReading(devices, udid) {
966
+ if (!devices) return {
967
+ name: null,
968
+ model: null,
969
+ connection: "unknown"
970
+ };
971
+ const device = devices.find((entry) => entry.udid.toUpperCase() === udid.toUpperCase());
972
+ if (!device) return {
973
+ name: null,
974
+ model: null,
975
+ connection: "disconnected"
976
+ };
977
+ return {
978
+ name: device.name,
979
+ model: device.model ?? null,
980
+ connection: isReachable(device) ? "connected" : "disconnected"
981
+ };
982
+ }
983
+ /**
984
+ * A phone's reading from `adb devices`; null `devices` means adb could not be read. `android --device` and `device lock`
985
+ * record the phone's model as the lease's device name, so the model comes from there.
986
+ */
987
+ function androidPhysicalReading(devices, serial) {
988
+ return {
989
+ name: null,
990
+ model: null,
991
+ connection: !devices ? "unknown" : devices.physical.some((entry) => entry.serial === serial) ? "connected" : "disconnected"
992
+ };
993
+ }
994
+ function physicalDeviceLine(device, now) {
995
+ const slot = device.slot === "default" ? "" : ` [${device.slot}]`;
996
+ const name = device.name ?? device.id;
997
+ const model = device.model && device.model !== name ? `, ${device.model}` : "";
998
+ const remaining = formatElapsed(Date.parse(device.lease.expiresAt) - now);
999
+ return `${device.platform}${slot}: ${name} (physical${model}) ${device.connection} -- leased until ${clockTime(device.lease.expiresAt)} (${remaining} left)`;
965
1000
  }
966
1001
  //#endregion
967
1002
  //#region src/commands/status.ts
968
1003
  const WATCH_GIT_MAX_AGE_MS = 6e4;
969
- const WATCH_READER_CHECK_MS = 2e3;
1004
+ function iosStatusName(ios) {
1005
+ const host = ios.host;
1006
+ return host && !ios.udid ? `${host.device?.name ?? "iOS simulator"} (${host.device ? `iOS ${host.device.runtime.replace(/^iOS /, "")}` : "runtime pending"}) on ${host.machine}` : ios.name ?? ios.udid;
1007
+ }
1008
+ function iosStatusLines(record, slotLabel) {
1009
+ const out = [];
1010
+ const host = record?.udid ? record.host : void 0;
1011
+ const iosDevices = [record, ...host ? [{
1012
+ host,
1013
+ name: host.device?.name ?? null,
1014
+ udid: "",
1015
+ owned: false,
1016
+ state: host.state ?? "unverified"
1017
+ }] : []];
1018
+ for (const ios of iosDevices) {
1019
+ if (!ios) continue;
1020
+ const booted = ios.state === "Booted" ? chalk.green("booted") : chalk.dim(ios.state.toLowerCase());
1021
+ const owned = ios.owned ? chalk.dim(" (owned)") : "";
1022
+ out.push(` ios${slotLabel}: ${chalk.cyan(iosStatusName(ios))} ${booted}${owned}${activitySuffix(ios.activity)}${appSuffix(ios.app)}${idleShutdownSuffix(ios.idleShutdown)}`);
1023
+ if (!ios.udid && ios.host?.agent.driver === "agent-device") out.push(chalk.dim(` agent: ${ios.host.agent.command}`));
1024
+ }
1025
+ return out;
1026
+ }
970
1027
  function formatGb(mb) {
971
1028
  return `${(mb / 1024).toFixed(1)} GB`;
972
1029
  }
@@ -977,12 +1034,14 @@ function statusCommand(program) {
977
1034
  for (const line of renderStatus(await readStatus(0), json)) console.log(line);
978
1035
  });
979
1036
  }
980
- function readStatus(gitMaxAgeMs, simctlListing = null) {
981
- return withStateReadCache(() => readStatusFacts(gitMaxAgeMs, simctlListing));
1037
+ function readStatus(gitMaxAgeMs, simctlListing = null, archives) {
1038
+ return withStateReadCache(() => readStatusFacts(gitMaxAgeMs, simctlListing, archives));
982
1039
  }
983
- async function readStatusFacts(gitMaxAgeMs, simctlListing) {
1040
+ async function readStatusFacts(gitMaxAgeMs, simctlListing, cachedArchives) {
984
1041
  const cfg = loadConfig$1();
985
1042
  const projects = Object.entries(cfg?.projects || {});
1043
+ const iosReads = projects.map(([path]) => readHostedIosStatus(hostedIosPlacements(readWorkspaceState$1(path))));
1044
+ const macosReads = projects.map(([path]) => readHostedMacosStatus(readMacosRecord(path)));
986
1045
  const cwdRoot = findServerWorkspace(process.cwd())?.root ?? null;
987
1046
  const worktrees = linkedWorktrees([process.cwd(), ...projects.map(([path]) => path)]);
988
1047
  const orphanWorktrees = unprovisionedWorktrees(worktrees, projects.map(([p]) => p));
@@ -1017,7 +1076,7 @@ async function readStatusFacts(gitMaxAgeMs, simctlListing) {
1017
1076
  }
1018
1077
  const running = await processes;
1019
1078
  await gitRead;
1020
- const history = readStats().record?.history;
1079
+ const history = readStats$1().record?.history;
1021
1080
  const states = [];
1022
1081
  const labelOnlyRoots = [];
1023
1082
  const launchesByState = [];
@@ -1029,11 +1088,15 @@ async function readStatusFacts(gitMaxAgeMs, simctlListing) {
1029
1088
  now: leaseNow
1030
1089
  });
1031
1090
  const roots = [];
1091
+ const readPhysicalDevice = physicalDeviceReader();
1092
+ const simulatorUdids = new Set([...Object.keys(simsByUdid), ...projects.flatMap(([, proj]) => projectDeviceSlots(proj).flatMap(({ platforms }) => platforms.ios?.deviceUdid ?? []))].map((udid) => udid.toUpperCase()));
1032
1093
  for (const [i, [path, proj]] of projects.entries()) {
1033
1094
  const { metro, supervisor } = running[i];
1034
1095
  const saved = readWorkspaceState$1(path);
1035
1096
  const builds = workspaceBuilds(path, saved, history);
1036
1097
  const web = webFacts(readWebRecord(path));
1098
+ const macosFacts = await macosReads[i];
1099
+ const macos = macosAppState(macosFacts.record);
1037
1100
  const activeBuild = parseActiveBuild(saved?.[ACTIVE_BUILD_KEY]);
1038
1101
  roots.push({
1039
1102
  path,
@@ -1042,7 +1105,8 @@ async function readStatusFacts(gitMaxAgeMs, simctlListing) {
1042
1105
  platform: activeBuild.platform,
1043
1106
  pid: activeBuild.claim.pid
1044
1107
  } : null,
1045
- browserPids: browserPids(web)
1108
+ browserPids: browserPids(web),
1109
+ macosPids: [macos?.supervisor, macos?.app].flatMap((owner) => owner && (0, process_identity_exports.inspectProcessIdentity)(owner) === "same" ? [owner.pid] : [])
1046
1110
  });
1047
1111
  const launches = readWorkspaceLaunches(path);
1048
1112
  launchesByState.push(launches);
@@ -1060,38 +1124,97 @@ async function readStatusFacts(gitMaxAgeMs, simctlListing) {
1060
1124
  remote: remoteDeviceState(readRemoteSession(path), easLedger, path),
1061
1125
  idleStop: readIdleStop(saved),
1062
1126
  lastStop: metroLastStop(saved, supervisor),
1127
+ idleShutdowns: readDeviceIdleShutdowns(saved),
1063
1128
  launches,
1064
1129
  leasedIds: new Set(deviceLeaseStates(leaseFiles, {
1065
1130
  root: path,
1066
1131
  now: leaseNow
1067
1132
  }).flatMap((lease) => lease.mine && !lease.expired && lease.id ? [lease.id] : [])),
1068
- now: leaseNow
1133
+ now: leaseNow,
1134
+ workspaces: projects.map(([other]) => other)
1069
1135
  }), web));
1070
1136
  const state = states[states.length - 1];
1071
- if (state) Object.assign(state, builds);
1072
- labelOnlyRoots.push(Boolean(proj.worktreeRoot && !proj.bundleId && !state?.metro && !state?.ios && !state?.android && !state?.web));
1137
+ if (state) {
1138
+ for (const [slot, record] of Object.entries(hostedIosRecords(saved))) if (!parseHostedIosPlacement(record)) state.warnings.push(unreadableHostedIos(slot));
1139
+ for (const [slot, facts] of Object.entries(await iosReads[i])) {
1140
+ const local = slot === "default" ? state.ios : state.slots?.find((entry) => entry.slot === slot)?.ios;
1141
+ const conflict = local?.udid && (local.state === "Booted" || local.state === "unknown");
1142
+ const ios = conflict ? {
1143
+ ...local,
1144
+ host: facts.ios.host
1145
+ } : facts.ios;
1146
+ if (conflict) state.warnings.push(`Slot ${slot} records both a local and hosted iOS simulator (${iosStatusName(facts.ios)}); run stim stop to reconcile them.`);
1147
+ if (slot === "default") state.ios = ios;
1148
+ else {
1149
+ state.slots ??= [];
1150
+ const savedSlot = state.slots.find((entry) => entry.slot === slot);
1151
+ if (savedSlot) savedSlot.ios = ios;
1152
+ else state.slots.push({
1153
+ slot,
1154
+ ios,
1155
+ android: null
1156
+ });
1157
+ }
1158
+ if (facts.warning) state.warnings.push(facts.warning);
1159
+ state.live ||= facts.ios.state === "ready";
1160
+ }
1161
+ const tunnel = readMetroTunnel(path);
1162
+ if (state.metro && tunnel?.kind === "managed" && tunnel.port === state.metro.port && pidExists(tunnel.pid)) state.metro.tunnel = {
1163
+ provider: tunnel.provider,
1164
+ url: tunnel.url
1165
+ };
1166
+ if (macos) {
1167
+ state.macos = macos;
1168
+ if (macosFacts.warning) state.warnings.push(macosFacts.warning);
1169
+ state.live ||= macos.state === "running" || macos.state === "orphaned" || macos.build.state === "running";
1170
+ }
1171
+ Object.assign(state, builds, workspacePhase(state.live, saved, { now: leaseNow }), {
1172
+ platforms: detectPlatforms(path, resolveSettings({
1173
+ projectPath: path,
1174
+ gitCommonDir: gitCommonDirOnDisk(path),
1175
+ repoRoot: state.worktree?.path ?? path
1176
+ })),
1177
+ recording: { enabled: workspaceRecordingEnabled(path, proj, cfg, process.env) }
1178
+ });
1179
+ }
1180
+ const physicalDevices = physicalDeviceStates(leaseFiles, parseWorkspaceLeases(saved?.deviceLeases), {
1181
+ root: path,
1182
+ now: leaseNow,
1183
+ simulatorUdids,
1184
+ read: readPhysicalDevice
1185
+ });
1186
+ if (state && physicalDevices.length) state.physicalDevices = physicalDevices;
1187
+ labelOnlyRoots.push(Boolean(proj.worktreeRoot && !proj.bundleId && !state?.metro && !state?.ios && !state?.android && !state?.web && !state?.macos));
1073
1188
  }
1189
+ applyStatusMeasures(states, worktrees);
1190
+ readLogDerivedFacts(states);
1074
1191
  const tables = createDeviceProcessTables();
1075
1192
  readDeviceProcesses(states, tables, {
1076
1193
  projects,
1077
1194
  launchesByState
1078
1195
  });
1196
+ readMetroReverses(states, launchesByState);
1079
1197
  const sims = Object.values(simsByUdid);
1080
1198
  const simNames = Object.fromEntries(sims.map((sim) => [sim.udid.toUpperCase(), sim.name]));
1081
- const machine = readMachineUsage({
1199
+ const machine = await readMachineUsage({
1082
1200
  states,
1083
1201
  roots,
1084
1202
  simNames,
1085
1203
  tables,
1086
1204
  busy: sims.some((sim) => sim.state === "Booted")
1087
1205
  });
1206
+ const archives = cachedArchives ?? readArchives();
1207
+ const maxCount = machineNumber("archive.maxCount", cfg, process.env);
1088
1208
  return {
1209
+ archived: linkReplacedArchives(archives.slice(0, maxCount.value ?? 200), states.map((state) => state.path)),
1210
+ archivedUsage: archivedUsage(archives),
1089
1211
  projects,
1090
1212
  states,
1091
1213
  labelOnlyRoots,
1092
1214
  leases,
1093
1215
  leaseNow,
1094
1216
  orphanWorktrees,
1217
+ worktrees,
1095
1218
  simsAvailable,
1096
1219
  simctlError,
1097
1220
  cwdRoot,
@@ -1101,6 +1224,48 @@ async function readStatusFacts(gitMaxAgeMs, simctlListing) {
1101
1224
  simNames
1102
1225
  };
1103
1226
  }
1227
+ /** Sets the facts status derives from workspace logs, the running build's record and its detail file. */
1228
+ function readLogDerivedFacts(states) {
1229
+ const now = Date.now();
1230
+ for (const state of states) {
1231
+ if (state.metro) {
1232
+ const bundle = metroBundleState(tailLines(join$1(workspaceLogsDir$1(state.path), "metro.ndjson")) ?? [], {
1233
+ running: state.metro.running,
1234
+ now
1235
+ });
1236
+ if (bundle) state.metro.bundle = bundle;
1237
+ else delete state.metro.bundle;
1238
+ }
1239
+ if (state.build?.state === "running") {
1240
+ const record = parseActiveBuild(readWorkspaceState$1(state.path)?.[ACTIVE_BUILD_KEY]);
1241
+ if (!record) {
1242
+ state.build = null;
1243
+ continue;
1244
+ }
1245
+ const report = buildReport(record, {
1246
+ state: activeBuildState(record.claim),
1247
+ history: void 0
1248
+ });
1249
+ const { outcome, expectedMs, expectedPhaseMs, basis, plannedPhases } = state.build;
1250
+ state.build = record.estimate || record.startedAt !== state.build.startedAt ? report : {
1251
+ ...report,
1252
+ outcome,
1253
+ expectedMs,
1254
+ expectedPhaseMs: report.phase === state.build.phase ? expectedPhaseMs : null,
1255
+ basis,
1256
+ plannedPhases
1257
+ };
1258
+ const detail = record.phase === "compile" ? readBuildDetail(state.path, record.claim.claimId) : null;
1259
+ if (detail) state.build.detail = detail;
1260
+ else delete state.build.detail;
1261
+ }
1262
+ }
1263
+ }
1264
+ function phaseMarker({ phase, live, warmStep }) {
1265
+ if (phase === "warming") return chalk.cyan(` [warming: ${warmStep}]`);
1266
+ if (phase === "ready") return chalk.green(" [ready]");
1267
+ return live ? "" : chalk.dim(" [idle]");
1268
+ }
1104
1269
  function browserPids(web) {
1105
1270
  if (!web) return [];
1106
1271
  const chrome = web.record.chromeProcess?.pid;
@@ -1108,37 +1273,48 @@ function browserPids(web) {
1108
1273
  return web.status === "orphaned" && chrome ? [chrome] : [];
1109
1274
  }
1110
1275
  /**
1111
- * Attributes the host process table to its owners. Reads the table only when a simulator is booted, a workspace is
1112
- * live or a build runs, so an idle machine runs no `ps`.
1276
+ * Attributes the host process table and the processes' footprints to their owners, and sets each environment's
1277
+ * `memoryMb` from them. Reads both only when a simulator is booted, a workspace is live or a build runs, so an idle
1278
+ * machine runs no `ps` and keeps the estimate.
1113
1279
  */
1114
- function readMachineUsage({ states, roots, simNames, tables, busy }) {
1280
+ async function readMachineUsage({ states, roots, simNames, tables, busy }) {
1115
1281
  const processes = busy || states.some((state) => state.live || state.build?.state === "running") ? tables.host() : null;
1116
- return processes ? attributeMachineUsage({
1282
+ if (!processes) return null;
1283
+ const footprints = await readFootprints();
1284
+ const machine = attributeMachineUsage({
1117
1285
  processes,
1286
+ footprints,
1118
1287
  environments: states,
1119
1288
  roots,
1120
1289
  simNames
1121
- }) : null;
1290
+ });
1291
+ applyMachineMemory(states, machine);
1292
+ return machine;
1122
1293
  }
1123
1294
  /**
1124
1295
  * Rereads only the error counts, device activity and machine usage. With `machine`, and when the snapshot measured
1125
1296
  * machine usage, it reads the host process table again; it reuses every other subprocess fact of the full read.
1126
1297
  */
1127
- function refreshLightFacts(snapshot, machine) {
1128
- if (machine && snapshot.machine) snapshot.tables = {
1129
- host: createDeviceProcessTables().host,
1130
- android: snapshot.tables.android
1298
+ async function refreshLightFacts(snapshot, machine) {
1299
+ const fresh = createDeviceProcessTables();
1300
+ snapshot.tables = {
1301
+ host: machine && snapshot.machine ? fresh.host : snapshot.tables.host,
1302
+ android: snapshot.tables.android,
1303
+ tcpConnections: fresh.tcpConnections,
1304
+ clients: fresh.host
1131
1305
  };
1132
1306
  withStateReadCache(() => {
1133
1307
  for (const state of snapshot.states) state.logs = logFacts(state.path);
1308
+ applyStatusMeasures(snapshot.states, snapshot.worktrees);
1309
+ readLogDerivedFacts(snapshot.states);
1134
1310
  readDeviceProcesses(snapshot.states, snapshot.tables, null);
1135
1311
  });
1136
- if (machine && snapshot.machine) snapshot.machine = readMachineUsage({
1312
+ if (machine && snapshot.machine) snapshot.machine = await readMachineUsage({
1137
1313
  ...snapshot,
1138
1314
  busy: true
1139
1315
  });
1140
1316
  }
1141
- function renderStatus({ projects, states, labelOnlyRoots, leases, leaseNow, orphanWorktrees, simsAvailable, simctlError, cwdRoot, machine }, json) {
1317
+ function renderStatus({ archived, archivedUsage: usage, projects, states, labelOnlyRoots, leases, leaseNow, orphanWorktrees, simsAvailable, simctlError, cwdRoot, machine }, json) {
1142
1318
  const out = [];
1143
1319
  const totalMemoryMb = Math.round(totalmem() / 1048576);
1144
1320
  const cap = capacity(states, totalMemoryMb);
@@ -1153,20 +1329,26 @@ function renderStatus({ projects, states, labelOnlyRoots, leases, leaseNow, orph
1153
1329
  })
1154
1330
  };
1155
1331
  });
1332
+ const maintenance = maintenanceStatus();
1156
1333
  if (json) {
1157
1334
  out.push(JSON.stringify({
1158
- environments: states.map((state, i) => labelOnlyRoots[i] ? {
1335
+ environments: states.map((state, i) => withDerivedFacts(labelOnlyRoots[i] ? {
1159
1336
  ...state,
1160
1337
  labelOnly: true
1161
- } : state),
1338
+ } : state)),
1339
+ archived,
1340
+ archivedUsage: usage,
1162
1341
  capacity: cap,
1163
1342
  deviceLeases: leases,
1164
- unprovisionedWorktrees: orphanWorktrees,
1343
+ unprovisionedWorktrees: orphanWorktrees.map(withGitChip),
1165
1344
  simctlAvailable: simsAvailable,
1166
- machine
1345
+ machine,
1346
+ maintenance
1167
1347
  }));
1168
1348
  return out;
1169
1349
  }
1350
+ out.push(...maintenanceLines(maintenance).map((line) => chalk.dim(line)));
1351
+ if (usage.count) out.push(`Archived: ${plural(usage.count, "workspace")}, ${formatBytes(usage.bytes)}`);
1170
1352
  if (projects.length === 0 && orphanWorktrees.length === 0) {
1171
1353
  out.push(chalk.dim("No projects registered."));
1172
1354
  for (const pool of pools) {
@@ -1182,12 +1364,16 @@ function renderStatus({ projects, states, labelOnlyRoots, leases, leaseNow, orph
1182
1364
  if (!state) continue;
1183
1365
  const shortcut = projectShortcut(path, proj);
1184
1366
  const marker = path === cwdRoot ? chalk.bold.cyan(`* ${shortcut}`) : shortcut;
1185
- const idle = state.live ? "" : chalk.dim(" [idle]");
1367
+ const idle = phaseMarker(state);
1186
1368
  out.push(`\n${marker}${idle} ${chalk.dim(`(${path})`)}`);
1187
1369
  out.push(labelOnlyRoots[i] ? chalk.dim(" worktree root (holds the label; the app registers its own entry)") : chalk.dim(` app: ${proj.bundleId ?? "?"} (${proj.isExpo ?? detectIsExpo(path) ? "expo" : "bare"})`));
1188
1370
  if (state.metro) {
1189
1371
  const label = state.metro.running ? chalk.green(`running (pid ${state.metro.pid})`) : state.metro.idleStop ? chalk.dim(`stopped (idle) after ${state.metro.idleStop.idleMinutes}m; \`stim start\`, \`ios\` or \`android\` restarts it`) : state.metro.lastStop ? chalk.dim(`not running${state.metro.lastStop.reason === "vanished" ? "" : ` since ${state.metro.lastStop.at}`}; ${describeMetroLastStop(state.metro.lastStop)}`) : chalk.dim("not running");
1190
1372
  out.push(` metro: port ${state.metro.port} ${label}`);
1373
+ if (state.metro.tunnel) {
1374
+ const { provider, url } = state.metro.tunnel;
1375
+ out.push(` tunnel: ${provider}${provider === "tailscale" ? " (tailnet-only)" : ""} ${url}`);
1376
+ }
1191
1377
  }
1192
1378
  if (state.supervisor) {
1193
1379
  const health = state.supervisor.healthy ? chalk.green("healthy") : chalk.yellow("not answering");
@@ -1200,6 +1386,7 @@ function renderStatus({ projects, states, labelOnlyRoots, leases, leaseNow, orph
1200
1386
  }
1201
1387
  out.push(...lastBuildsLines(state.lastBuilds));
1202
1388
  if (state.worktree?.git) out.push(chalk.dim(` git: ${gitSummaryText(state.worktree.git)}`));
1389
+ for (const agent of state.agents ?? []) out.push(chalk.dim(` agent: ${agentLine(agent)}`));
1203
1390
  if (state.logs) {
1204
1391
  const n = state.logs.errorsSinceMarker;
1205
1392
  const errs = n > 0 ? chalk.yellow(` (${n} error${n === 1 ? "" : "s"} since the last marker)`) : "";
@@ -1211,23 +1398,31 @@ function renderStatus({ projects, states, labelOnlyRoots, leases, leaseNow, orph
1211
1398
  android: state.android
1212
1399
  }, ...state.slots ?? []]) {
1213
1400
  const slotLabel = deviceState.slot === "default" ? "" : ` [${deviceState.slot}]`;
1214
- if (deviceState.ios) {
1215
- const booted = deviceState.ios.state === "Booted" ? chalk.green("booted") : chalk.dim(deviceState.ios.state.toLowerCase());
1216
- const owned = deviceState.ios.owned ? chalk.dim(" (owned)") : "";
1217
- out.push(` ios${slotLabel}: ${chalk.cyan(deviceState.ios.name ?? deviceState.ios.udid)} ${booted}${owned}${activitySuffix(deviceState.ios.activity)}${appSuffix(deviceState.ios.app)}`);
1218
- }
1401
+ out.push(...iosStatusLines(deviceState.ios, slotLabel));
1219
1402
  if (deviceState.android) {
1220
1403
  const kind = deviceState.android.physical ? chalk.dim("(physical)") : chalk.dim("(emulator)");
1221
1404
  const observed = deviceState.android.state ? ` ${deviceState.android.state}${deviceState.android.serial ? ` (${deviceState.android.serial})` : ""}` : "";
1222
- out.push(` android${slotLabel}: ${chalk.cyan(deviceState.android.name)} ${kind}${observed}${deviceState.android.owned ? chalk.dim(" (owned)") : ""}${activitySuffix(deviceState.android.activity)}${appSuffix(deviceState.android.app)}`);
1405
+ out.push(` android${slotLabel}: ${chalk.cyan(deviceState.android.name)} ${kind}${observed}${deviceState.android.owned ? chalk.dim(" (owned)") : ""}${activitySuffix(deviceState.android.activity)}${appSuffix(deviceState.android.app)}${idleShutdownSuffix(deviceState.android.idleShutdown)}`);
1223
1406
  }
1224
1407
  }
1408
+ if (state.macos) {
1409
+ const host = state.macos.host;
1410
+ const where = host ? ` on ${host.machine} as ${host.bundleId}${chalk.dim(` (session ${host.session})`)}` : "";
1411
+ const build = state.macos.build;
1412
+ const placement = build.buildMachine === void 0 ? "" : `; build machine ${build.buildMachine}, built on ${build.builtOn ?? "none"}`;
1413
+ out.push(` macOS ${state.macos.product}: ${state.macos.state}${where}; build ${build.state}${placement}`);
1414
+ if (host?.agent.driver === "agent-device") out.push(chalk.dim(` agent: ${host.agent.command}`));
1415
+ }
1225
1416
  if (state.web) {
1226
1417
  const browser = state.web.running ? chalk.green(`running (pid ${state.web.pid}, DevTools ${state.web.cdpEndpoint})`) : chalk.dim("not running");
1227
- out.push(` web: ${chalk.cyan(state.web.url)} ${browser}${state.web.headless ? "" : chalk.dim(" (headed)")}`);
1418
+ const page = state.web.page;
1419
+ const failed = page?.state === "failed" ? ` -- ${chalk.yellow(`page failed: ${page.error}`)}` : "";
1420
+ out.push(` web: ${chalk.cyan(page?.url ?? state.web.url)} ${browser}${state.web.headless ? "" : chalk.dim(" (headed)")}${activitySuffix(state.web.activity)}${failed}`);
1228
1421
  }
1422
+ for (const device of state.physicalDevices ?? []) out.push(` ${physicalDeviceLine(device, leaseNow)}`);
1229
1423
  for (const remote of state.remoteDevices ?? []) out.push(` ${remoteDeviceLine(remote)}`);
1230
1424
  for (const w of state.warnings) out.push(chalk.yellow(` ! ${w}`));
1425
+ for (const note of state.issues.filter((issue) => issue.severity === "info")) out.push(chalk.dim(` - ${note.message}`));
1231
1426
  }
1232
1427
  for (const pool of pools) {
1233
1428
  if (pool.error) out.push(chalk.yellow(`\n${pool.error} ${POOL_SETTING_REMEDY}`));
@@ -1242,7 +1437,7 @@ function renderStatus({ projects, states, labelOnlyRoots, leases, leaseNow, orph
1242
1437
  out.push(chalk.dim(`\nWorktrees with no environment (${orphanWorktrees.length}):`));
1243
1438
  for (const w of orphanWorktrees) out.push(chalk.dim(` ${orphanWorktreeLine(w)}`));
1244
1439
  }
1245
- out.push(chalk.dim(`\n${cap.liveCount} live environment(s), roughly ${formatGb(cap.committedMb)} of ${formatGb(cap.totalMemoryMb)} committed.`));
1440
+ out.push(chalk.dim(machine?.memorySource === "footprint" ? `\n${cap.liveCount} live environment(s) use ${formatGb(cap.committedMb)} of ${formatGb(cap.totalMemoryMb)}.` : `\n${cap.liveCount} live environment(s), roughly ${formatGb(cap.committedMb)} of ${formatGb(cap.totalMemoryMb)} committed.`));
1246
1441
  const volumes = readVolumes(cwdRoot || process.cwd());
1247
1442
  const line = diskLine(volumes);
1248
1443
  if (line) {
@@ -1257,16 +1452,31 @@ function renderStatus({ projects, states, labelOnlyRoots, leases, leaseNow, orph
1257
1452
  }
1258
1453
  async function watchStatus(json) {
1259
1454
  let last = null;
1455
+ let archiveStamp = null;
1456
+ let archives;
1260
1457
  let snapshot = null;
1261
1458
  let sources = null;
1459
+ const measurer = createStatusMeasurer({ updated: () => scheduler.trigger("light") });
1262
1460
  const scheduler = createRefreshScheduler({
1263
1461
  debounceMs: 250,
1264
1462
  lightIntervalMs: WATCH_LIGHT_INTERVAL_MS,
1265
1463
  run: async (kind) => {
1464
+ triggerMaintenance("status-watch");
1266
1465
  let text;
1267
1466
  try {
1268
- if (kind === "light" && snapshot) refreshLightFacts(snapshot, json);
1269
- else snapshot = await readStatus(WATCH_GIT_MAX_AGE_MS, sources?.simulatorListing());
1467
+ if (kind === "light" && snapshot) await refreshLightFacts(snapshot, json);
1468
+ else {
1469
+ let stamp = null;
1470
+ try {
1471
+ stamp = statSync$1(archiveRoot()).mtimeMs;
1472
+ } catch {}
1473
+ if (archives === void 0 || stamp !== archiveStamp) {
1474
+ archives = readArchives();
1475
+ archiveStamp = stamp;
1476
+ }
1477
+ snapshot = await readStatus(WATCH_GIT_MAX_AGE_MS, sources?.simulatorListing(), archives);
1478
+ }
1479
+ measurer.schedule(snapshot.states, snapshot.worktrees);
1270
1480
  text = renderStatus(snapshot, json).join("\n");
1271
1481
  } catch (error) {
1272
1482
  console.error(chalk.red(String(error?.message || error)));
@@ -1281,24 +1491,17 @@ async function watchStatus(json) {
1281
1491
  });
1282
1492
  const fallback = setInterval(() => scheduler.trigger("full"), WATCH_FALLBACK_MS);
1283
1493
  const machine = setInterval(() => json && snapshot?.machine && scheduler.trigger("light"), WATCH_LIGHT_INTERVAL_MS);
1284
- const parent = process.ppid;
1285
- const stdout = fstatSync$1(1);
1286
- const linuxPipe = process.platform === "linux" && (stdout.isFIFO() || stdout.isSocket());
1287
- const reader = setInterval(() => {
1288
- if (linuxPipe && process.ppid !== parent) finish();
1289
- else process.stdout.write("");
1290
- }, WATCH_READER_CHECK_MS);
1291
1494
  const finish = () => {
1292
1495
  clearInterval(fallback);
1293
1496
  clearInterval(machine);
1294
- clearInterval(reader);
1497
+ stopReader();
1295
1498
  scheduler.stop();
1296
1499
  sources?.stop();
1297
1500
  process.exit(0);
1298
1501
  };
1502
+ const stopReader = watchStdoutReader(finish);
1299
1503
  process.on("SIGINT", finish);
1300
1504
  process.on("SIGTERM", finish);
1301
- process.stdout.on("error", finish);
1302
1505
  sources = watchStatusSources({
1303
1506
  home: getConfigDir(),
1304
1507
  onChange: (kind) => scheduler.trigger(kind)
@@ -1314,6 +1517,10 @@ async function readGitInto(worktrees, orphans, maxAgeMs) {
1314
1517
  });
1315
1518
  for (const worktree of worktrees) worktree.git = byPath.get(worktree.path) ?? null;
1316
1519
  }
1520
+ function agentLine({ tool, title, sessionId, lastActiveAt }) {
1521
+ const at = lastActiveAt ? `, active ${lastActiveAt}` : "";
1522
+ return `${tool === "codex" ? "Codex" : "Claude Code"} ${title ? `"${title}"` : sessionId}${at}`;
1523
+ }
1317
1524
  function orphanWorktreeLine(w) {
1318
1525
  return `${w.path}${w.branch ? ` [${w.branch}]` : ""}${w.git ? ` -- ${gitSummaryText(w.git)}` : ""}`;
1319
1526
  }
@@ -1324,7 +1531,11 @@ function lastBuildsLines(reports) {
1324
1531
  function lastBuildText(report) {
1325
1532
  const source = report.cacheHit ? `${report.cacheHit} cache` : report.status === "ok" ? "compiled" : "no cache hit";
1326
1533
  const took = report.durationMs === null ? "" : ` in ${formatDuration(report.durationMs)}`;
1327
- return `${report.platform} ${report.status === "ok" ? source : `failed (${report.errorCode ?? "error"}), ${source}`}${took}`;
1534
+ const placement = report.buildMachine === void 0 ? "" : `; build machine ${report.buildMachine}, built on ${report.builtOn ?? (report.cacheHit ? "none (cache)" : "none")}`;
1535
+ return `${report.platform} ${report.status === "ok" ? source : `failed (${report.errorCode ?? "error"}), ${source}`}${took}${placement}`;
1536
+ }
1537
+ function idleShutdownSuffix(record) {
1538
+ return record ? chalk.dim(` -- shut down after ${record.idleMinutes}m idle at ${record.at} (devices.idleShutdownMinutes)`) : "";
1328
1539
  }
1329
1540
  function activitySuffix(activity) {
1330
1541
  const label = activityLabel(activity, Date.now());
@@ -1378,6 +1589,28 @@ function readDeviceProcesses(states, tables, apps) {
1378
1589
  });
1379
1590
  }
1380
1591
  }
1592
+ const web = state.web;
1593
+ const port = web?.cdpEndpoint ? Number(new URL(web.cdpEndpoint).port) : null;
1594
+ if (web?.running && port) {
1595
+ web.page = readWebPage(state.path);
1596
+ const ownPids = [web.pid, web.supervisorPid].filter((pid) => pid !== null);
1597
+ web.activity = statusActivity(readWebActivity({
1598
+ port,
1599
+ ownPids,
1600
+ workspace: state.path
1601
+ }, tables));
1602
+ }
1603
+ }
1604
+ }
1605
+ function readMetroReverses(states, launchesByState) {
1606
+ for (const [i, state] of states.entries()) for (const target of metroReverseTargets(state, launchesByState[i])) {
1607
+ const listing = getExecutor().runFileQuiet("adb", [
1608
+ "-s",
1609
+ target.serial,
1610
+ "reverse",
1611
+ "--list"
1612
+ ], { timeoutMs: 5e3 });
1613
+ if (listing !== null && !hasMetroReverse(listing, target.metroPort)) addReverseMissingIssue(state, target);
1381
1614
  }
1382
1615
  }
1383
1616
  function appSuffix(app) {
@@ -1388,7 +1621,7 @@ function appSuffix(app) {
1388
1621
  }
1389
1622
  function linkedWorktrees(paths) {
1390
1623
  return [...new Set(paths.flatMap((path) => gitCommonDirOnDisk(path) ?? []))].flatMap((common) => {
1391
- const repository = basename(common) === ".git" ? dirname(common) : common;
1624
+ const repository = basename$1(common) === ".git" ? dirname$1(common) : common;
1392
1625
  return linkedWorktreesOnDisk(common).map((entry) => Object.assign(entry, { repository }));
1393
1626
  });
1394
1627
  }
@@ -1404,7 +1637,7 @@ function workspaceBuilds(path, saved, history) {
1404
1637
  function workspaceBuild(path, saved, history) {
1405
1638
  const record = parseActiveBuild(saved?.[ACTIVE_BUILD_KEY]);
1406
1639
  if (!record) return null;
1407
- const projectKey = statsProjectKey({
1640
+ const projectKey = statsProjectKey$1({
1408
1641
  root: path,
1409
1642
  commonDir: gitCommonDir(path),
1410
1643
  repoRoot: repoRoot(path)
@@ -1414,6 +1647,32 @@ function workspaceBuild(path, saved, history) {
1414
1647
  history: history?.[projectKey]
1415
1648
  });
1416
1649
  }
1650
+ const PHYSICAL_DEVICE_LIST_TIMEOUT_MS = 5e3;
1651
+ const PHYSICAL_DEVICE_LIST_MAX_AGE_MS = 3e4;
1652
+ const physicalDeviceLists = {};
1653
+ function freshList(entry, read) {
1654
+ const now = Date.now();
1655
+ return entry && now - entry.at < PHYSICAL_DEVICE_LIST_MAX_AGE_MS ? entry : {
1656
+ at: now,
1657
+ devices: read()
1658
+ };
1659
+ }
1660
+ function physicalDeviceReader() {
1661
+ return (platform, id) => {
1662
+ if (platform === "ios") {
1663
+ physicalDeviceLists.ios = freshList(physicalDeviceLists.ios, () => readIosDevices({ timeoutMs: PHYSICAL_DEVICE_LIST_TIMEOUT_MS }));
1664
+ return iosPhysicalReading(physicalDeviceLists.ios.devices, id);
1665
+ }
1666
+ physicalDeviceLists.android = freshList(physicalDeviceLists.android, () => {
1667
+ try {
1668
+ return listAdbDevices({ timeoutMs: PHYSICAL_DEVICE_LIST_TIMEOUT_MS });
1669
+ } catch {
1670
+ return null;
1671
+ }
1672
+ });
1673
+ return androidPhysicalReading(physicalDeviceLists.android.devices, id);
1674
+ };
1675
+ }
1417
1676
  function androidRuntimeReader() {
1418
1677
  const resolve = ownedAvdSerialResolver({ timeoutMs: 5e3 });
1419
1678
  return (avdName) => readAndroidRuntime(() => resolve(avdName));
@@ -1494,7 +1753,7 @@ async function supervisorFacts(path, proj, metroResolution, lookup) {
1494
1753
  };
1495
1754
  }
1496
1755
  function logFacts(path) {
1497
- const dir = workspaceLogsDir(path);
1756
+ const dir = workspaceLogsDir$1(path);
1498
1757
  if (!existsSync$1(dir)) return null;
1499
1758
  try {
1500
1759
  return {