stim 1.14.0 → 1.15.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 (130) hide show
  1. package/README.md +2 -0
  2. package/dist/{activity-CYVDcrt3.mjs → activity-B2-kb7k0.mjs} +281 -41
  3. package/dist/{android-XpvCrV7q.mjs → android-CYmq0jNi.mjs} +2 -2
  4. package/dist/{android-LbPFRWVL.mjs → android-Cu_L4RpL.mjs} +502 -407
  5. package/dist/{android-d6rGq1pA.mjs → android-Db2eEMaf.mjs} +152 -65
  6. package/dist/android-cas-PnKfAdB2.mjs +167 -0
  7. package/dist/android-cas-compiler.mjs +2 -2
  8. package/dist/{app-install-BpgSgkJj.mjs → app-install-CAAplXAt.mjs} +95 -161
  9. package/dist/{budget-CGDkhvv_.mjs → budget-CMiqghig.mjs} +1038 -73
  10. package/dist/{build-plan-Dcr-3ie0.mjs → build-plan-aeGMFX7e.mjs} +193 -653
  11. package/dist/build-progress-C2Jv1AtA.mjs +872 -0
  12. package/dist/build-slots-Df1ApSuH.mjs +127 -0
  13. package/dist/{cache-manifest-C68QQ0ua.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
  14. package/dist/cache-manifest.mjs +1 -1
  15. package/dist/{chrome-BPCiC3ZX.mjs → chrome-BrcVWQyD.mjs} +1 -1
  16. package/dist/cli-0cX5s4NM.mjs +45 -0
  17. package/dist/cli.mjs +6 -42
  18. package/dist/client-C2IS36Qf.mjs +1333 -0
  19. package/dist/collector-run.mjs +8 -7
  20. package/dist/{command-output-DVcpw6eE.mjs → command-output-CliHHn7a.mjs} +1 -1
  21. package/dist/{config-lvc7d3C2.mjs → config-D0DbpZtM.mjs} +28 -29
  22. package/dist/{created-devices-CllH_8eb.mjs → created-devices-BwVb3liH.mjs} +2 -2
  23. package/dist/{dependency-state-DsT7aKeo.mjs → dependency-state-BmsjKZ2H.mjs} +2 -2
  24. package/dist/{deps-D9IcKqXS.mjs → deps-BFlKbOIp.mjs} +3 -3
  25. package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
  26. package/dist/dev-client-CjzLoNs0.mjs +80 -0
  27. package/dist/{device-CjuuAJjQ.mjs → device-BZ1IyS5A.mjs} +7 -7
  28. package/dist/device-capacity-DMuKC7nx.mjs +136 -0
  29. package/dist/device-host-worker.d.mts +1 -0
  30. package/dist/device-host-worker.mjs +769 -0
  31. package/dist/device-ios-DMQBUPus.mjs +381 -0
  32. package/dist/{device-lease-5c3WrVRm.mjs → device-lease-W5Pi1q38.mjs} +43 -36
  33. package/dist/{device-pool-BiyDyPP-.mjs → device-pool-B2r38YjS.mjs} +4 -4
  34. package/dist/{device-remote-D87zEzaq.mjs → device-remote-9ulPsdhY.mjs} +68 -162
  35. package/dist/{doctor-x9pnU9fq.mjs → doctor-BcE5xsBn.mjs} +191 -99
  36. package/dist/{error-diagnostics-C5HzcwCW.mjs → error-diagnostics-Cb0P1WLS.mjs} +12 -11
  37. package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
  38. package/dist/{gc-ko6mUAYD.mjs → gc-BuL1Genr.mjs} +429 -101
  39. package/dist/{android-cas-iZK3DOCr.mjs → gradle-CmdOBq1d.mjs} +8 -294
  40. package/dist/{guide-6R0VNFqI.mjs → guide-cCSGRFd3.mjs} +1959 -222
  41. package/dist/{guide-status-BZrFPIA_.mjs → guide-status-CIoWK6mG.mjs} +3 -3
  42. package/dist/hosted-macos-DwowNU7W.mjs +447 -0
  43. package/dist/idle-shutdown-B5LPaNyY.mjs +122 -0
  44. package/dist/in-use-CndPMilN.mjs +110 -0
  45. package/dist/{ios-BwDPrnYQ.mjs → ios-3CtCRhwr.mjs} +201 -10
  46. package/dist/{ios-P1f5nm2n.mjs → ios-Chob8ny7.mjs} +512 -263
  47. package/dist/{ios-device-DGglU3E_.mjs → ios-device-DERmMmk-.mjs} +12 -7
  48. package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
  49. package/dist/{launch-verify-Deg4tB3Z.mjs → launch-verify-CLlofxRt.mjs} +4 -4
  50. package/dist/{logs-Civ4gbBH.mjs → logs-h9h6MUvE.mjs} +95 -13
  51. package/dist/machine-usage-D_YYMzfR.mjs +283 -0
  52. package/dist/machines-CYtOCqcj.mjs +259 -0
  53. package/dist/macos-BwtbUtTN.mjs +468 -0
  54. package/dist/macos-run.d.mts +5 -0
  55. package/dist/macos-run.mjs +2 -0
  56. package/dist/{merge-state-ytFQCeh4.mjs → merge-state-BnxLNQiM.mjs} +22 -12
  57. package/dist/{metro-CcEo8Fl8.mjs → metro-Di8EN2Tb.mjs} +20 -120
  58. package/dist/{named-ports-Blg8IxOh.mjs → named-ports-Dj_XBNaL.mjs} +6 -59
  59. package/dist/native-run-DFDoIRbY.mjs +147 -0
  60. package/dist/{native-runtime-DjBHRg5m.mjs → native-runtime-i6OkVg-w.mjs} +9 -5
  61. package/dist/offload-worker.d.mts +121 -0
  62. package/dist/offload-worker.mjs +601 -0
  63. package/dist/{ownership-DyYDF5ww.mjs → ownership-BkYxUtuj.mjs} +3 -2
  64. package/dist/{page-BHUzTDFt.mjs → page-QF_Ke10s.mjs} +7 -2
  65. package/dist/{ports-BguS83ex.mjs → ports-DddwttKM.mjs} +3 -3
  66. package/dist/prebuild-BvxDlD6C.mjs +213 -0
  67. package/dist/process-identity-BmRm1qV0.mjs +12 -0
  68. package/dist/{project-CWlvEaeN.mjs → project-CV3tDFHl.mjs} +9 -18
  69. package/dist/pull-request-pxnDCQzn.mjs +322 -0
  70. package/dist/pull-requests.d.mts +120 -0
  71. package/dist/pull-requests.mjs +2 -0
  72. package/dist/recordings-GEXp4iBT.mjs +37 -0
  73. package/dist/{reload-BiniwsSZ.mjs → reload-ChPiwvot.mjs} +70 -22
  74. package/dist/{remote-cache-CcR6o6lI.mjs → remote-cache-CO2elm_b.mjs} +4 -4
  75. package/dist/run-B-xqRrsg.mjs +119 -0
  76. package/dist/{server-bare-DGfgyJ5J.mjs → server-bare-X7PUgizL.mjs} +33 -15
  77. package/dist/{server-expo-BVuwQ65v.mjs → server-expo-DhtX6bom.mjs} +6 -10
  78. package/dist/server-expo-KJeD109I.mjs +2 -0
  79. package/dist/{settings-D61iNOsB.mjs → settings-CduVcoxj.mjs} +77 -26
  80. package/dist/{settings-B-fsC-Om.mjs → settings-DbL6uhH8.mjs} +16 -10
  81. package/dist/settings.schema.json +357 -2
  82. package/dist/{simslim-DrDN4WuV.mjs → simslim-Ciluxlq4.mjs} +8 -132
  83. package/dist/{slot-launch-D5t8EiMf.mjs → slot-launch-BY0BrOLh.mjs} +6 -6
  84. package/dist/{spawn-claims-Drw_o-Fb.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
  85. package/dist/spawn-entry-DncpJvWL.mjs +16 -0
  86. package/dist/stage-CzaiwFsI.mjs +143 -0
  87. package/dist/start-CPO4wRg7.mjs +2 -0
  88. package/dist/{start-s4RXp1wy.mjs → start-CUD9nasA.mjs} +108 -61
  89. package/dist/state-B4PQFXMW.d.mts +12 -0
  90. package/dist/state-BxCgeTQu.mjs +41 -0
  91. package/dist/{state-DHqvsbqC.mjs → state-CaeaHbDA.mjs} +7 -4
  92. package/dist/{state-Dk9RVXI7.mjs → state-DFm77wRn.mjs} +67 -5
  93. package/dist/stats-DBLLFmKd.mjs +66 -0
  94. package/dist/{status-BsPAnfOI.mjs → status-DYXxNOT0.mjs} +423 -268
  95. package/dist/{status-BgAuxESg.mjs → status-l84Gbvp2.mjs} +116 -20
  96. package/dist/stdout-reader-C8yjre1x.mjs +23 -0
  97. package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
  98. package/dist/stim-footprint.swift +32 -0
  99. package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
  100. package/dist/{stop-BzreOWJp.mjs → stop-Bad61z_2.mjs} +38 -12
  101. package/dist/stop-Bvf1MflS.mjs +48 -0
  102. package/dist/{stop-BWIF_6BK.mjs → stop-CceYh1yG.mjs} +2 -2
  103. package/dist/stop-cause-CZ_Mr-v9.mjs +665 -0
  104. package/dist/supervisor-run.d.mts +43 -11
  105. package/dist/supervisor-run.mjs +227 -19
  106. package/dist/support-CDQbDkL5.mjs +315 -0
  107. package/dist/support-WLOzVKUv.mjs +176 -0
  108. package/dist/teardown-BYuQafiA.mjs +718 -0
  109. package/dist/{xcode-Cp5KvK9x.mjs → toolchain-DmeYjXD9.mjs} +363 -13
  110. package/dist/warm-progress-DVHLN4uC.mjs +463 -0
  111. package/dist/{idle-DXtx3RbX.mjs → watchman-Bx0wceoF.mjs} +55 -8
  112. package/dist/{web-BsQs1WlO.mjs → web-k6w8-HnE.mjs} +37 -20
  113. package/dist/web-run.mjs +309 -12
  114. package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
  115. package/dist/workspace-state-CL2V2Ons.mjs +371 -0
  116. package/dist/worktree-BT_uULFx.mjs +2 -0
  117. package/dist/{worktree-BVrHsYZG.mjs → worktree-C0c5Udwa.mjs} +55 -54
  118. package/dist/{worktree-UrQWXSeb.mjs → worktree-qXKzADSC.mjs} +154 -252
  119. package/package.json +10 -5
  120. package/shim/bundle-response.cjs +28 -12
  121. package/dist/build-progress-DxKpwnaS.mjs +0 -247
  122. package/dist/in-use-DlqBDKqY.mjs +0 -249
  123. package/dist/server-expo-DIHDhvGg.mjs +0 -2
  124. package/dist/start-Cetfvy8l.mjs +0 -2
  125. package/dist/stats-CFpFGlJV.mjs +0 -62
  126. package/dist/stats-CX1Z8Tca.mjs +0 -368
  127. package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
  128. package/dist/stop-cause-hjGBOZTG.mjs +0 -1231
  129. package/dist/worktree-CHWWvm5v.mjs +0 -2
  130. package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
@@ -1,32 +1,64 @@
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";
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 } from "./config-D0DbpZtM.mjs";
3
+ import { t as process_identity_exports } from "./process-identity-BmRm1qV0.mjs";
3
4
  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";
5
+ import { C as workspaceLogsDir, S as workspaceLogErrorIndex, r as readWorkspaceState$1 } from "./workspace-state-CL2V2Ons.mjs";
6
+ import { c as gitCommonDirOnDisk, p as linkedWorktreesOnDisk, s as gitCommonDir, y as repoRoot } from "./worktree-C0c5Udwa.mjs";
7
+ import { l as projectShortcut, o as findServerWorkspace, r as detectIsExpo } from "./project-CV3tDFHl.mjs";
8
+ import { c as readMetroTunnel, f as readWorkspaceLaunches, u as readRemoteSession } from "./state-CaeaHbDA.mjs";
9
+ import { c as tailLines, n as createActivityReader, r as createDeviceProcessTables, s as readWebActivity } from "./activity-B2-kb7k0.mjs";
10
+ import { a as formatElapsed, i as formatDuration, r as clockTime } from "./command-output-CliHHn7a.mjs";
11
+ import { i as listLeaseFiles, r as leaseIsExpired, s as parseWorkspaceLeases } from "./device-lease-W5Pi1q38.mjs";
12
+ import { F as ownedAvdDeviceProfile, O as listAdbDevices, at as POOL_SETTING_REMEDY, d as androidToolPath, dt as parkedMaxSetting, ft as readParked, z as ownedAvdSerialResolver } from "./android-Db2eEMaf.mjs";
13
+ import { a as listeningPidsByPort, c as processCwds, d as isMetroRunning, i as listeningPids, l as resolveProjectMetro, o as pidExists, s as processCwd } from "./metro-Di8EN2Tb.mjs";
14
+ import { a as readWebPage, o as readWebRecord, u as webFacts } from "./state-DFm77wRn.mjs";
15
+ import { d as listAllIosSimsAsync, v as parseSimctlList } from "./ios-3CtCRhwr.mjs";
16
+ import { a as buildStatusLine, h as statsProjectKey$1, i as buildReport, p as readStats, r as activeBuildState, s as parseActiveBuild, t as ACTIVE_BUILD_KEY } from "./build-progress-C2Jv1AtA.mjs";
17
+ import { n as describeMetroLastStop, o as metroLastStop, u as resolveSupervisorTarget } from "./stop-cause-CZ_Mr-v9.mjs";
18
+ import { m as hasMetroReverse } from "./app-install-CAAplXAt.mjs";
19
+ import { r as probeHostedMacos } from "./hosted-macos-DwowNU7W.mjs";
20
+ import { m as readEasSessionLedger$1, p as easMachineStateRoot$1 } from "./device-remote-9ulPsdhY.mjs";
21
+ 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-l84Gbvp2.mjs";
22
+ import { n as attributeMachineUsage, r as readFootprints } from "./machine-usage-D_YYMzfR.mjs";
23
+ import { d as readIosDevices, s as isReachable } from "./ios-device-DERmMmk-.mjs";
24
+ import { n as mergeState } from "./merge-state-BnxLNQiM.mjs";
25
+ import { i as createStatusMeasurer, n as workspacePhase, r as applyStatusMeasures } from "./warm-progress-DVHLN4uC.mjs";
26
+ import { t as watchStdoutReader } from "./stdout-reader-C8yjre1x.mjs";
27
+ import { r as workspaceRecordingEnabled } from "./recordings-GEXp4iBT.mjs";
28
+ import { i as readSupervisorState } from "./stop-Bad61z_2.mjs";
29
+ import { createHash, randomUUID } from "node:crypto";
21
30
  import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
31
+ import { join, posix, resolve } from "node:path";
22
32
  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";
27
- import { createHash, randomUUID } from "node:crypto";
28
33
  import { homedir, totalmem } from "os";
34
+ import { basename as basename$1, dirname as dirname$1, join as join$1 } from "path";
35
+ import { rotatedLogPath } from "@stim-cli/core";
36
+ import { ERROR_SOURCES, bundleMarkerPlatform, countErrorEntries, gitMergeCacheDir, logFiles, macosAppState, queryLogs, readBuildDetail, readBuildHistory, readDeviceIdleShutdowns, readIdleStop, readLastBuilds, readMacosRecord, recordMatches, sortByTs, withDerivedFacts, withGitChip, withStateReadCache } from "@stim-cli/core/state";
29
37
  import chalk from "chalk";
38
+ //#region src/device-host/hosted-macos-status.ts
39
+ function applyHostedMacosProbe(record, probe) {
40
+ if (!record.host || probe.state === "ready") return { record };
41
+ const { machine, session } = record.host;
42
+ if (probe.state === "stopped") return {
43
+ record: {
44
+ ...record,
45
+ hostLaunched: false
46
+ },
47
+ 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.`
48
+ };
49
+ return {
50
+ record: {
51
+ ...record,
52
+ hostLaunched: "unverified"
53
+ },
54
+ 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.`
55
+ };
56
+ }
57
+ async function readHostedMacosStatus(record, options) {
58
+ if (!record?.host || record.supervisor || !record.hostLaunched) return { record };
59
+ return applyHostedMacosProbe(record, await probeHostedMacos(record.host, options));
60
+ }
61
+ //#endregion
30
62
  //#region src/status-watch.ts
31
63
  const WATCH_FALLBACK_MS = 3e4;
32
64
  const WATCH_LIGHT_INTERVAL_MS = 15e3;
@@ -88,9 +120,9 @@ function createRefreshScheduler({ debounceMs, lightIntervalMs = 0, run }) {
88
120
  };
89
121
  }
90
122
  /**
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.
123
+ * Which refresh a change to `name` in a watched `$STIM_HOME` directory needs: `light` for a log append or a running
124
+ * build's detail, which can change only the log-derived fields, `full` for anything else that can change the payload,
125
+ * null for none. A null name means the platform did not report one.
94
126
  */
95
127
  function statusChange(dir, name) {
96
128
  if (name?.includes(".lock")) return null;
@@ -98,7 +130,9 @@ function statusChange(dir, name) {
98
130
  if (name === null) return "full";
99
131
  switch (dir) {
100
132
  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;
133
+ case "workspace":
134
+ if (name.startsWith("build-detail.json")) return "light";
135
+ return name.startsWith("state.json") || name.startsWith("ended-agents.json") || name === "logs" ? "full" : null;
102
136
  case "eas": return name.startsWith("sessions.json") ? "full" : null;
103
137
  default: return "full";
104
138
  }
@@ -122,18 +156,18 @@ function watchStatusSources({ home, onChange, platform = process.platform, simct
122
156
  const desiredDirs = () => {
123
157
  const dirs = /* @__PURE__ */ new Map([
124
158
  [home, "home"],
125
- [join$1(home, "workspaces"), "workspaces"],
126
- [join$1(home, "device-locks"), "leases"],
159
+ [join(home, "workspaces"), "workspaces"],
160
+ [join(home, "device-locks"), "leases"],
127
161
  [easMachineStateRoot$1(), "eas"]
128
162
  ]);
129
163
  let names = [];
130
164
  try {
131
- names = readdirSync(join$1(home, "workspaces"));
165
+ names = readdirSync(join(home, "workspaces"));
132
166
  } catch {}
133
167
  for (const name of names) {
134
- const dir = join$1(home, "workspaces", name);
168
+ const dir = join(home, "workspaces", name);
135
169
  dirs.set(dir, "workspace");
136
- dirs.set(join$1(dir, "logs"), "logs");
170
+ dirs.set(join(dir, "logs"), "logs");
137
171
  }
138
172
  return dirs;
139
173
  };
@@ -266,7 +300,7 @@ function trackAdbDevices(onChange) {
266
300
  }
267
301
  //#endregion
268
302
  //#region src/diagnostics/error-index.ts
269
- const INDEX_VERSION = 3;
303
+ const INDEX_VERSION = 7;
270
304
  const HEAD_BYTES = 1024;
271
305
  const TAIL_BYTES = 256;
272
306
  const CHUNK_BYTES = 4194304;
@@ -282,7 +316,7 @@ const ERROR_CRITERIA = {
282
316
  includeAppDeviceErrors: true
283
317
  };
284
318
  /**
285
- * The number of records `queryLogs({ dir, errorsOnly: true })` returns, without parsing every
319
+ * The number of rows the apps list for `queryLogs({ dir, errorsOnly: true })`, one per failure, without parsing every
286
320
  * line on every call. `indexFile` keeps, per log file generation, the markers and error records
287
321
  * already read and the byte offset they were read through; a later call reads only what was
288
322
  * appended since. The index is a cache: losing it or a concurrent writer only costs a rescan.
@@ -292,7 +326,7 @@ function countErrorsSinceMarker(dir, indexFile) {
292
326
  const files = {};
293
327
  let changed = false;
294
328
  for (const name of logFiles(dir)) {
295
- const current = join(dir, name);
329
+ const current = join$1(dir, name);
296
330
  for (const path of [rotatedLogPath(current), current]) {
297
331
  const result = summarize(path, previous.files);
298
332
  if (!result) continue;
@@ -306,10 +340,10 @@ function countErrorsSinceMarker(dir, indexFile) {
306
340
  });
307
341
  const records = [];
308
342
  for (const summary of Object.values(files)) records.push(...Object.values(summary.markers), ...summary.errors);
309
- return queryLogs({
343
+ return countErrorEntries(sortByTs(queryLogs({
310
344
  records,
311
345
  errorsOnly: true
312
- }).length;
346
+ })));
313
347
  }
314
348
  function summarize(path, known) {
315
349
  let fd;
@@ -401,11 +435,17 @@ function collect(record, markers, errors) {
401
435
  const metro = record.src === "metro";
402
436
  const web = !metro && record.platform === "web";
403
437
  const slot = record.slot ?? "default";
404
- const key = metro ? "metro" : web ? "web" : JSON.stringify(["launch", slot]);
438
+ const platform = metro ? bundleMarkerPlatform(record) : record.platform ?? null;
439
+ const key = web ? "web" : JSON.stringify([
440
+ metro ? "metro" : "launch",
441
+ slot,
442
+ platform
443
+ ]);
405
444
  if ((markers[key]?.ts ?? -Infinity) < ts) markers[key] = metro ? {
406
445
  src: "metro",
407
446
  marker: true,
408
- ts
447
+ ts,
448
+ ...platform === null ? {} : { platform }
409
449
  } : web ? {
410
450
  platform: "web",
411
451
  marker: true,
@@ -413,16 +453,20 @@ function collect(record, markers, errors) {
413
453
  } : {
414
454
  marker: true,
415
455
  ts,
416
- slot
456
+ slot,
457
+ ...platform === null ? {} : { platform }
417
458
  };
418
459
  }
419
460
  if (recordMatches(record, ERROR_CRITERIA)) errors.push({
420
461
  src: record.src,
421
462
  level: record.level,
422
463
  event: record.event,
423
- platform: record.platform,
464
+ platform: record.src === "metro" && record.marker === true ? bundleMarkerPlatform(record) : record.platform,
424
465
  ts: ts ?? void 0,
425
- slot: record.slot
466
+ slot: record.slot,
467
+ raw: record.raw,
468
+ marker: record.marker,
469
+ msg: record.marker === true ? record.msg : void 0
426
470
  });
427
471
  }
428
472
  function readIndex(file) {
@@ -513,7 +557,7 @@ async function defaultBranchOf(repository) {
513
557
  };
514
558
  }
515
559
  function mergeCacheFile(path) {
516
- return join$1(gitMergeCacheDir(), `${createHash("sha256").update(path).digest("hex").slice(0, 32)}.json`);
560
+ return join(gitMergeCacheDir(), `${createHash("sha256").update(path).digest("hex").slice(0, 32)}.json`);
517
561
  }
518
562
  function readMergeCache(path) {
519
563
  try {
@@ -587,14 +631,14 @@ const mergeTimeouts = /* @__PURE__ */ new Map();
587
631
  const recent = /* @__PURE__ */ new Map();
588
632
  function gitDirsOf(worktree) {
589
633
  try {
590
- const dotGit = join$1(worktree, ".git");
634
+ const dotGit = join(worktree, ".git");
591
635
  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;
636
+ const gitDir = pointer ? resolve(worktree, pointer) : pointer === null ? dotGit : null;
593
637
  if (!gitDir) return null;
594
638
  try {
595
639
  return {
596
640
  gitDir,
597
- commonDir: resolve$1(gitDir, readFileSync(join$1(gitDir, "commondir"), "utf-8").trim())
641
+ commonDir: resolve(gitDir, readFileSync(join(gitDir, "commondir"), "utf-8").trim())
598
642
  };
599
643
  } catch {
600
644
  return {
@@ -623,21 +667,21 @@ function fileStamp(path) {
623
667
  function gitFilesStamp(worktree, refs) {
624
668
  const dirs = gitDirsOf(worktree.path);
625
669
  if (!dirs) return {
626
- stamp: fileStamp(join$1(worktree.path, ".git")),
670
+ stamp: fileStamp(join(worktree.path, ".git")),
627
671
  missing: true
628
672
  };
629
673
  const { gitDir, commonDir } = dirs;
630
674
  return {
631
675
  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)] : []
676
+ join(gitDir, "index"),
677
+ join(gitDir, "HEAD"),
678
+ join(gitDir, "logs", "HEAD"),
679
+ join(commonDir, "packed-refs"),
680
+ join(commonDir, "FETCH_HEAD"),
681
+ join(commonDir, "refs", "remotes", "origin", "HEAD"),
682
+ ...refs.target ? [join(commonDir, refs.target)] : [],
683
+ ...worktree.branch ? [join(commonDir, "refs", "heads", worktree.branch)] : [],
684
+ ...refs.upstream ? [join(commonDir, "refs", "remotes", refs.upstream), join(commonDir, "refs", "heads", refs.upstream)] : []
641
685
  ].map(fileStamp).join(" "),
642
686
  missing: false
643
687
  };
@@ -753,7 +797,7 @@ function plistBundleId(appPath) {
753
797
  "raw",
754
798
  "-o",
755
799
  "-",
756
- join$1(appPath, "Info.plist")
800
+ join(appPath, "Info.plist")
757
801
  ], { timeoutMs: 2e3 })?.trim() || null;
758
802
  if (id) bundleIdsByApp.set(appPath, id);
759
803
  return id;
@@ -777,196 +821,155 @@ function createAppProcessReader(tables, bundleIdOf = plistBundleId) {
777
821
  };
778
822
  }
779
823
  //#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|$)/
824
+ //#region src/metro-bundle.ts
825
+ const IN_FLIGHT_MAX_MS = 6e5;
826
+ const RESTART_EVENTS = /* @__PURE__ */ new Set(["supervisor_started", "server_started"]);
827
+ const BUNDLE_EVENT = /^bundle_(?:response|prefetch)_(started|progress|finished|failed)$/;
828
+ function parseRecord(line) {
829
+ if (!line.includes("\"bundle_") && !line.includes("_started\"")) return null;
830
+ try {
831
+ const value = JSON.parse(line);
832
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
833
+ } catch {
834
+ return null;
805
835
  }
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
836
  }
823
- function rank(owner) {
824
- return MACHINE_OWNER_KINDS.indexOf(owner.kind);
837
+ function platformOf(value) {
838
+ return value === "ios" || value === "android" ? value : null;
825
839
  }
826
840
  /**
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.
841
+ * The bundle state of a workspace's Metro from the tail of its `metro.ndjson`: the records Stim's middleware writes
842
+ * for each native bundle request, an app's (`bundle_response_*`) or Stim's own prefetch (`bundle_prefetch_*`),
843
+ * matched by `requestId`. A request counts as in flight only while
844
+ * Metro runs, when it started under the current dev server and less than 10 minutes ago, since a dev server that
845
+ * died mid-request never records its end. Null when the tail holds no bundle request.
832
846
  */
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);
847
+ function metroBundleState(lines, { running, now }) {
848
+ let open = /* @__PURE__ */ new Map();
849
+ let last;
850
+ let seen = false;
851
+ for (const line of lines) {
852
+ const record = parseRecord(line);
853
+ if (!record) continue;
854
+ const { event, requestId, ts } = record;
855
+ if (typeof event !== "string") continue;
856
+ if (RESTART_EVENTS.has(event)) {
857
+ open = /* @__PURE__ */ new Map();
877
858
  continue;
878
859
  }
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
860
+ const platform = platformOf(record.platform);
861
+ const stage = BUNDLE_EVENT.exec(event)?.[1];
862
+ if (!stage || typeof requestId !== "string" || typeof ts !== "number" || !platform) continue;
863
+ seen = true;
864
+ if (stage === "started") open.set(requestId, {
865
+ platform,
866
+ startedAt: ts
867
+ });
868
+ else if (stage === "progress") {
869
+ const request = open.get(requestId);
870
+ const percent = record.percent;
871
+ if (request && typeof percent === "number" && percent >= 0 && percent <= 100) request.percent = percent;
872
+ } else {
873
+ const request = open.get(requestId);
874
+ open.delete(requestId);
875
+ if (!request) continue;
876
+ last = {
877
+ platform,
878
+ status: stage === "finished" ? "ok" : "failed",
879
+ durationMs: Math.max(0, ts - request.startedAt),
880
+ finishedAt: new Date(ts).toISOString()
909
881
  };
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
- }
927
- for (const pid of root.browserPids) if (present.has(pid)) claim(pid, `browser:${env.path}`, workspaceOwner("browser", env, "Chrome", null));
928
- }
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
882
  }
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
883
  }
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
884
+ if (!seen) return null;
885
+ const active = running ? [...open.values()].filter((request) => now - request.startedAt < IN_FLIGHT_MAX_MS).toSorted((a, b) => b.startedAt - a.startedAt)[0] : void 0;
886
+ return {
887
+ bundling: Boolean(active),
888
+ ...active ? {
889
+ platform: active.platform,
890
+ startedAt: new Date(active.startedAt).toISOString(),
891
+ ...active.percent === void 0 ? {} : { percent: active.percent }
892
+ } : {},
893
+ ...last ? { last } : {}
894
+ };
895
+ }
896
+ //#endregion
897
+ //#region src/physical-devices.ts
898
+ const EMULATOR_SERIAL = /^emulator-\d+$/;
899
+ /**
900
+ * The physical devices a workspace holds: each lease its state records whose file still carries the same token,
901
+ * names this workspace as holder, and has not expired. A lease on a simulator (a UDID in `simulatorUdids`) or an
902
+ * emulator (an `emulator-<port>` serial), which `device lock` also takes, is not a physical device. `read` is
903
+ * called once per device.
904
+ */
905
+ function physicalDeviceStates(entries, held, { root, now, simulatorUdids, read }) {
906
+ const out = [];
907
+ for (const [key, record] of Object.entries(held)) {
908
+ const parsed = parseDeviceSlotKey(key);
909
+ const lease = entries.find((entry) => entry.lease?.token === record.token)?.lease;
910
+ if (!parsed || !lease || lease.platform !== parsed.platform || lease.id !== record.id) continue;
911
+ if (lease.holder !== root || leaseIsExpired(lease, now)) continue;
912
+ if (parsed.platform === "ios" ? simulatorUdids.has(lease.id.toUpperCase()) : EMULATOR_SERIAL.test(lease.id)) continue;
913
+ const reading = read(parsed.platform, lease.id);
914
+ out.push({
915
+ platform: parsed.platform,
916
+ slot: parsed.slot,
917
+ id: lease.id,
918
+ name: reading.name ?? lease.deviceName,
919
+ model: reading.model ?? (parsed.platform === "android" && lease.deviceName !== lease.id ? lease.deviceName : null),
920
+ owned: false,
921
+ physical: true,
922
+ connection: reading.connection,
923
+ lease: {
924
+ holder: lease.holder,
925
+ kind: record.kind,
926
+ grantedAt: lease.grantedAt,
927
+ expiresAt: lease.expiresAt
928
+ }
961
929
  });
962
930
  }
963
- list.sort((x, y) => rank(x) - rank(y) || (x.workspace ?? "~").localeCompare(y.workspace ?? "~") || x.name.localeCompare(y.name));
964
- return { owners: list };
931
+ return out;
932
+ }
933
+ /** A phone's reading from devicectl's device list; null `devices` means devicectl could not be read. */
934
+ function iosPhysicalReading(devices, udid) {
935
+ if (!devices) return {
936
+ name: null,
937
+ model: null,
938
+ connection: "unknown"
939
+ };
940
+ const device = devices.find((entry) => entry.udid.toUpperCase() === udid.toUpperCase());
941
+ if (!device) return {
942
+ name: null,
943
+ model: null,
944
+ connection: "disconnected"
945
+ };
946
+ return {
947
+ name: device.name,
948
+ model: device.model ?? null,
949
+ connection: isReachable(device) ? "connected" : "disconnected"
950
+ };
951
+ }
952
+ /**
953
+ * A phone's reading from `adb devices`; null `devices` means adb could not be read. `android --device` and `device lock`
954
+ * record the phone's model as the lease's device name, so the model comes from there.
955
+ */
956
+ function androidPhysicalReading(devices, serial) {
957
+ return {
958
+ name: null,
959
+ model: null,
960
+ connection: !devices ? "unknown" : devices.physical.some((entry) => entry.serial === serial) ? "connected" : "disconnected"
961
+ };
962
+ }
963
+ function physicalDeviceLine(device, now) {
964
+ const slot = device.slot === "default" ? "" : ` [${device.slot}]`;
965
+ const name = device.name ?? device.id;
966
+ const model = device.model && device.model !== name ? `, ${device.model}` : "";
967
+ const remaining = formatElapsed(Date.parse(device.lease.expiresAt) - now);
968
+ return `${device.platform}${slot}: ${name} (physical${model}) ${device.connection} -- leased until ${clockTime(device.lease.expiresAt)} (${remaining} left)`;
965
969
  }
966
970
  //#endregion
967
971
  //#region src/commands/status.ts
968
972
  const WATCH_GIT_MAX_AGE_MS = 6e4;
969
- const WATCH_READER_CHECK_MS = 2e3;
970
973
  function formatGb(mb) {
971
974
  return `${(mb / 1024).toFixed(1)} GB`;
972
975
  }
@@ -983,6 +986,7 @@ function readStatus(gitMaxAgeMs, simctlListing = null) {
983
986
  async function readStatusFacts(gitMaxAgeMs, simctlListing) {
984
987
  const cfg = loadConfig$1();
985
988
  const projects = Object.entries(cfg?.projects || {});
989
+ const macosReads = projects.map(([path]) => readHostedMacosStatus(readMacosRecord(path)));
986
990
  const cwdRoot = findServerWorkspace(process.cwd())?.root ?? null;
987
991
  const worktrees = linkedWorktrees([process.cwd(), ...projects.map(([path]) => path)]);
988
992
  const orphanWorktrees = unprovisionedWorktrees(worktrees, projects.map(([p]) => p));
@@ -1029,11 +1033,15 @@ async function readStatusFacts(gitMaxAgeMs, simctlListing) {
1029
1033
  now: leaseNow
1030
1034
  });
1031
1035
  const roots = [];
1036
+ const readPhysicalDevice = physicalDeviceReader();
1037
+ const simulatorUdids = new Set([...Object.keys(simsByUdid), ...projects.flatMap(([, proj]) => projectDeviceSlots(proj).flatMap(({ platforms }) => platforms.ios?.deviceUdid ?? []))].map((udid) => udid.toUpperCase()));
1032
1038
  for (const [i, [path, proj]] of projects.entries()) {
1033
1039
  const { metro, supervisor } = running[i];
1034
1040
  const saved = readWorkspaceState$1(path);
1035
1041
  const builds = workspaceBuilds(path, saved, history);
1036
1042
  const web = webFacts(readWebRecord(path));
1043
+ const macosFacts = await macosReads[i];
1044
+ const macos = macosAppState(macosFacts.record);
1037
1045
  const activeBuild = parseActiveBuild(saved?.[ACTIVE_BUILD_KEY]);
1038
1046
  roots.push({
1039
1047
  path,
@@ -1042,7 +1050,8 @@ async function readStatusFacts(gitMaxAgeMs, simctlListing) {
1042
1050
  platform: activeBuild.platform,
1043
1051
  pid: activeBuild.claim.pid
1044
1052
  } : null,
1045
- browserPids: browserPids(web)
1053
+ browserPids: browserPids(web),
1054
+ macosPids: [macos?.supervisor, macos?.app].flatMap((owner) => owner && (0, process_identity_exports.inspectProcessIdentity)(owner) === "same" ? [owner.pid] : [])
1046
1055
  });
1047
1056
  const launches = readWorkspaceLaunches(path);
1048
1057
  launchesByState.push(launches);
@@ -1060,25 +1069,49 @@ async function readStatusFacts(gitMaxAgeMs, simctlListing) {
1060
1069
  remote: remoteDeviceState(readRemoteSession(path), easLedger, path),
1061
1070
  idleStop: readIdleStop(saved),
1062
1071
  lastStop: metroLastStop(saved, supervisor),
1072
+ idleShutdowns: readDeviceIdleShutdowns(saved),
1063
1073
  launches,
1064
1074
  leasedIds: new Set(deviceLeaseStates(leaseFiles, {
1065
1075
  root: path,
1066
1076
  now: leaseNow
1067
1077
  }).flatMap((lease) => lease.mine && !lease.expired && lease.id ? [lease.id] : [])),
1068
- now: leaseNow
1078
+ now: leaseNow,
1079
+ workspaces: projects.map(([other]) => other)
1069
1080
  }), web));
1070
1081
  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));
1082
+ if (state) {
1083
+ const tunnel = readMetroTunnel(path);
1084
+ if (state.metro && tunnel?.kind === "managed" && tunnel.port === state.metro.port && pidExists(tunnel.pid)) state.metro.tunnel = {
1085
+ provider: tunnel.provider,
1086
+ url: tunnel.url
1087
+ };
1088
+ if (macos) {
1089
+ state.macos = macos;
1090
+ if (macosFacts.warning) state.warnings.push(macosFacts.warning);
1091
+ state.live ||= macos.state === "running" || macos.state === "orphaned" || macos.build.state === "running";
1092
+ }
1093
+ Object.assign(state, builds, workspacePhase(state.live, saved, { now: leaseNow }), { recording: { enabled: workspaceRecordingEnabled(path, proj, cfg, process.env) } });
1094
+ }
1095
+ const physicalDevices = physicalDeviceStates(leaseFiles, parseWorkspaceLeases(saved?.deviceLeases), {
1096
+ root: path,
1097
+ now: leaseNow,
1098
+ simulatorUdids,
1099
+ read: readPhysicalDevice
1100
+ });
1101
+ if (state && physicalDevices.length) state.physicalDevices = physicalDevices;
1102
+ labelOnlyRoots.push(Boolean(proj.worktreeRoot && !proj.bundleId && !state?.metro && !state?.ios && !state?.android && !state?.web && !state?.macos));
1073
1103
  }
1104
+ applyStatusMeasures(states, worktrees);
1105
+ readLogDerivedFacts(states);
1074
1106
  const tables = createDeviceProcessTables();
1075
1107
  readDeviceProcesses(states, tables, {
1076
1108
  projects,
1077
1109
  launchesByState
1078
1110
  });
1111
+ readMetroReverses(states, launchesByState);
1079
1112
  const sims = Object.values(simsByUdid);
1080
1113
  const simNames = Object.fromEntries(sims.map((sim) => [sim.udid.toUpperCase(), sim.name]));
1081
- const machine = readMachineUsage({
1114
+ const machine = await readMachineUsage({
1082
1115
  states,
1083
1116
  roots,
1084
1117
  simNames,
@@ -1092,6 +1125,7 @@ async function readStatusFacts(gitMaxAgeMs, simctlListing) {
1092
1125
  leases,
1093
1126
  leaseNow,
1094
1127
  orphanWorktrees,
1128
+ worktrees,
1095
1129
  simsAvailable,
1096
1130
  simctlError,
1097
1131
  cwdRoot,
@@ -1101,6 +1135,48 @@ async function readStatusFacts(gitMaxAgeMs, simctlListing) {
1101
1135
  simNames
1102
1136
  };
1103
1137
  }
1138
+ /** Sets the facts status derives from workspace logs, the running build's record and its detail file. */
1139
+ function readLogDerivedFacts(states) {
1140
+ const now = Date.now();
1141
+ for (const state of states) {
1142
+ if (state.metro) {
1143
+ const bundle = metroBundleState(tailLines(join$1(workspaceLogsDir(state.path), "metro.ndjson")) ?? [], {
1144
+ running: state.metro.running,
1145
+ now
1146
+ });
1147
+ if (bundle) state.metro.bundle = bundle;
1148
+ else delete state.metro.bundle;
1149
+ }
1150
+ if (state.build?.state === "running") {
1151
+ const record = parseActiveBuild(readWorkspaceState$1(state.path)?.[ACTIVE_BUILD_KEY]);
1152
+ if (!record) {
1153
+ state.build = null;
1154
+ continue;
1155
+ }
1156
+ const report = buildReport(record, {
1157
+ state: activeBuildState(record.claim),
1158
+ history: void 0
1159
+ });
1160
+ const { outcome, expectedMs, expectedPhaseMs, basis, plannedPhases } = state.build;
1161
+ state.build = record.estimate || record.startedAt !== state.build.startedAt ? report : {
1162
+ ...report,
1163
+ outcome,
1164
+ expectedMs,
1165
+ expectedPhaseMs: report.phase === state.build.phase ? expectedPhaseMs : null,
1166
+ basis,
1167
+ plannedPhases
1168
+ };
1169
+ const detail = record.phase === "compile" ? readBuildDetail(state.path, record.claim.claimId) : null;
1170
+ if (detail) state.build.detail = detail;
1171
+ else delete state.build.detail;
1172
+ }
1173
+ }
1174
+ }
1175
+ function phaseMarker({ phase, live, warmStep }) {
1176
+ if (phase === "warming") return chalk.cyan(` [warming: ${warmStep}]`);
1177
+ if (phase === "ready") return chalk.green(" [ready]");
1178
+ return live ? "" : chalk.dim(" [idle]");
1179
+ }
1104
1180
  function browserPids(web) {
1105
1181
  if (!web) return [];
1106
1182
  const chrome = web.record.chromeProcess?.pid;
@@ -1108,32 +1184,43 @@ function browserPids(web) {
1108
1184
  return web.status === "orphaned" && chrome ? [chrome] : [];
1109
1185
  }
1110
1186
  /**
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`.
1187
+ * Attributes the host process table and the processes' footprints to their owners, and sets each environment's
1188
+ * `memoryMb` from them. Reads both only when a simulator is booted, a workspace is live or a build runs, so an idle
1189
+ * machine runs no `ps` and keeps the estimate.
1113
1190
  */
1114
- function readMachineUsage({ states, roots, simNames, tables, busy }) {
1191
+ async function readMachineUsage({ states, roots, simNames, tables, busy }) {
1115
1192
  const processes = busy || states.some((state) => state.live || state.build?.state === "running") ? tables.host() : null;
1116
- return processes ? attributeMachineUsage({
1193
+ if (!processes) return null;
1194
+ const footprints = await readFootprints();
1195
+ const machine = attributeMachineUsage({
1117
1196
  processes,
1197
+ footprints,
1118
1198
  environments: states,
1119
1199
  roots,
1120
1200
  simNames
1121
- }) : null;
1201
+ });
1202
+ applyMachineMemory(states, machine);
1203
+ return machine;
1122
1204
  }
1123
1205
  /**
1124
1206
  * Rereads only the error counts, device activity and machine usage. With `machine`, and when the snapshot measured
1125
1207
  * machine usage, it reads the host process table again; it reuses every other subprocess fact of the full read.
1126
1208
  */
1127
- function refreshLightFacts(snapshot, machine) {
1128
- if (machine && snapshot.machine) snapshot.tables = {
1129
- host: createDeviceProcessTables().host,
1130
- android: snapshot.tables.android
1209
+ async function refreshLightFacts(snapshot, machine) {
1210
+ const fresh = createDeviceProcessTables();
1211
+ snapshot.tables = {
1212
+ host: machine && snapshot.machine ? fresh.host : snapshot.tables.host,
1213
+ android: snapshot.tables.android,
1214
+ tcpConnections: fresh.tcpConnections,
1215
+ clients: fresh.host
1131
1216
  };
1132
1217
  withStateReadCache(() => {
1133
1218
  for (const state of snapshot.states) state.logs = logFacts(state.path);
1219
+ applyStatusMeasures(snapshot.states, snapshot.worktrees);
1220
+ readLogDerivedFacts(snapshot.states);
1134
1221
  readDeviceProcesses(snapshot.states, snapshot.tables, null);
1135
1222
  });
1136
- if (machine && snapshot.machine) snapshot.machine = readMachineUsage({
1223
+ if (machine && snapshot.machine) snapshot.machine = await readMachineUsage({
1137
1224
  ...snapshot,
1138
1225
  busy: true
1139
1226
  });
@@ -1155,13 +1242,13 @@ function renderStatus({ projects, states, labelOnlyRoots, leases, leaseNow, orph
1155
1242
  });
1156
1243
  if (json) {
1157
1244
  out.push(JSON.stringify({
1158
- environments: states.map((state, i) => labelOnlyRoots[i] ? {
1245
+ environments: states.map((state, i) => withDerivedFacts(labelOnlyRoots[i] ? {
1159
1246
  ...state,
1160
1247
  labelOnly: true
1161
- } : state),
1248
+ } : state)),
1162
1249
  capacity: cap,
1163
1250
  deviceLeases: leases,
1164
- unprovisionedWorktrees: orphanWorktrees,
1251
+ unprovisionedWorktrees: orphanWorktrees.map(withGitChip),
1165
1252
  simctlAvailable: simsAvailable,
1166
1253
  machine
1167
1254
  }));
@@ -1182,12 +1269,16 @@ function renderStatus({ projects, states, labelOnlyRoots, leases, leaseNow, orph
1182
1269
  if (!state) continue;
1183
1270
  const shortcut = projectShortcut(path, proj);
1184
1271
  const marker = path === cwdRoot ? chalk.bold.cyan(`* ${shortcut}`) : shortcut;
1185
- const idle = state.live ? "" : chalk.dim(" [idle]");
1272
+ const idle = phaseMarker(state);
1186
1273
  out.push(`\n${marker}${idle} ${chalk.dim(`(${path})`)}`);
1187
1274
  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
1275
  if (state.metro) {
1189
1276
  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
1277
  out.push(` metro: port ${state.metro.port} ${label}`);
1278
+ if (state.metro.tunnel) {
1279
+ const { provider, url } = state.metro.tunnel;
1280
+ out.push(` tunnel: ${provider}${provider === "tailscale" ? " (tailnet-only)" : ""} ${url}`);
1281
+ }
1191
1282
  }
1192
1283
  if (state.supervisor) {
1193
1284
  const health = state.supervisor.healthy ? chalk.green("healthy") : chalk.yellow("not answering");
@@ -1200,6 +1291,7 @@ function renderStatus({ projects, states, labelOnlyRoots, leases, leaseNow, orph
1200
1291
  }
1201
1292
  out.push(...lastBuildsLines(state.lastBuilds));
1202
1293
  if (state.worktree?.git) out.push(chalk.dim(` git: ${gitSummaryText(state.worktree.git)}`));
1294
+ for (const agent of state.agents ?? []) out.push(chalk.dim(` agent: ${agentLine(agent)}`));
1203
1295
  if (state.logs) {
1204
1296
  const n = state.logs.errorsSinceMarker;
1205
1297
  const errs = n > 0 ? chalk.yellow(` (${n} error${n === 1 ? "" : "s"} since the last marker)`) : "";
@@ -1214,20 +1306,32 @@ function renderStatus({ projects, states, labelOnlyRoots, leases, leaseNow, orph
1214
1306
  if (deviceState.ios) {
1215
1307
  const booted = deviceState.ios.state === "Booted" ? chalk.green("booted") : chalk.dim(deviceState.ios.state.toLowerCase());
1216
1308
  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)}`);
1309
+ out.push(` ios${slotLabel}: ${chalk.cyan(deviceState.ios.name ?? deviceState.ios.udid)} ${booted}${owned}${activitySuffix(deviceState.ios.activity)}${appSuffix(deviceState.ios.app)}${idleShutdownSuffix(deviceState.ios.idleShutdown)}`);
1218
1310
  }
1219
1311
  if (deviceState.android) {
1220
1312
  const kind = deviceState.android.physical ? chalk.dim("(physical)") : chalk.dim("(emulator)");
1221
1313
  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)}`);
1314
+ 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
1315
  }
1224
1316
  }
1317
+ if (state.macos) {
1318
+ const host = state.macos.host;
1319
+ const where = host ? ` on ${host.machine} as ${host.bundleId}${chalk.dim(` (session ${host.session})`)}` : "";
1320
+ const build = state.macos.build;
1321
+ const placement = build.buildMachine === void 0 ? "" : `; build machine ${build.buildMachine}, built on ${build.builtOn ?? "none"}`;
1322
+ out.push(` macOS ${state.macos.product}: ${state.macos.state}${where}; build ${build.state}${placement}`);
1323
+ if (host?.agent.driver === "agent-device") out.push(chalk.dim(` agent: ${host.agent.command}`));
1324
+ }
1225
1325
  if (state.web) {
1226
1326
  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)")}`);
1327
+ const page = state.web.page;
1328
+ const failed = page?.state === "failed" ? ` -- ${chalk.yellow(`page failed: ${page.error}`)}` : "";
1329
+ out.push(` web: ${chalk.cyan(page?.url ?? state.web.url)} ${browser}${state.web.headless ? "" : chalk.dim(" (headed)")}${activitySuffix(state.web.activity)}${failed}`);
1228
1330
  }
1331
+ for (const device of state.physicalDevices ?? []) out.push(` ${physicalDeviceLine(device, leaseNow)}`);
1229
1332
  for (const remote of state.remoteDevices ?? []) out.push(` ${remoteDeviceLine(remote)}`);
1230
1333
  for (const w of state.warnings) out.push(chalk.yellow(` ! ${w}`));
1334
+ for (const note of state.issues.filter((issue) => issue.severity === "info")) out.push(chalk.dim(` - ${note.message}`));
1231
1335
  }
1232
1336
  for (const pool of pools) {
1233
1337
  if (pool.error) out.push(chalk.yellow(`\n${pool.error} ${POOL_SETTING_REMEDY}`));
@@ -1242,7 +1346,7 @@ function renderStatus({ projects, states, labelOnlyRoots, leases, leaseNow, orph
1242
1346
  out.push(chalk.dim(`\nWorktrees with no environment (${orphanWorktrees.length}):`));
1243
1347
  for (const w of orphanWorktrees) out.push(chalk.dim(` ${orphanWorktreeLine(w)}`));
1244
1348
  }
1245
- out.push(chalk.dim(`\n${cap.liveCount} live environment(s), roughly ${formatGb(cap.committedMb)} of ${formatGb(cap.totalMemoryMb)} committed.`));
1349
+ 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
1350
  const volumes = readVolumes(cwdRoot || process.cwd());
1247
1351
  const line = diskLine(volumes);
1248
1352
  if (line) {
@@ -1259,14 +1363,16 @@ async function watchStatus(json) {
1259
1363
  let last = null;
1260
1364
  let snapshot = null;
1261
1365
  let sources = null;
1366
+ const measurer = createStatusMeasurer({ updated: () => scheduler.trigger("light") });
1262
1367
  const scheduler = createRefreshScheduler({
1263
1368
  debounceMs: 250,
1264
1369
  lightIntervalMs: WATCH_LIGHT_INTERVAL_MS,
1265
1370
  run: async (kind) => {
1266
1371
  let text;
1267
1372
  try {
1268
- if (kind === "light" && snapshot) refreshLightFacts(snapshot, json);
1373
+ if (kind === "light" && snapshot) await refreshLightFacts(snapshot, json);
1269
1374
  else snapshot = await readStatus(WATCH_GIT_MAX_AGE_MS, sources?.simulatorListing());
1375
+ measurer.schedule(snapshot.states, snapshot.worktrees);
1270
1376
  text = renderStatus(snapshot, json).join("\n");
1271
1377
  } catch (error) {
1272
1378
  console.error(chalk.red(String(error?.message || error)));
@@ -1281,24 +1387,17 @@ async function watchStatus(json) {
1281
1387
  });
1282
1388
  const fallback = setInterval(() => scheduler.trigger("full"), WATCH_FALLBACK_MS);
1283
1389
  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
1390
  const finish = () => {
1292
1391
  clearInterval(fallback);
1293
1392
  clearInterval(machine);
1294
- clearInterval(reader);
1393
+ stopReader();
1295
1394
  scheduler.stop();
1296
1395
  sources?.stop();
1297
1396
  process.exit(0);
1298
1397
  };
1398
+ const stopReader = watchStdoutReader(finish);
1299
1399
  process.on("SIGINT", finish);
1300
1400
  process.on("SIGTERM", finish);
1301
- process.stdout.on("error", finish);
1302
1401
  sources = watchStatusSources({
1303
1402
  home: getConfigDir(),
1304
1403
  onChange: (kind) => scheduler.trigger(kind)
@@ -1314,6 +1413,10 @@ async function readGitInto(worktrees, orphans, maxAgeMs) {
1314
1413
  });
1315
1414
  for (const worktree of worktrees) worktree.git = byPath.get(worktree.path) ?? null;
1316
1415
  }
1416
+ function agentLine({ tool, title, sessionId, lastActiveAt }) {
1417
+ const at = lastActiveAt ? `, active ${lastActiveAt}` : "";
1418
+ return `${tool === "codex" ? "Codex" : "Claude Code"} ${title ? `"${title}"` : sessionId}${at}`;
1419
+ }
1317
1420
  function orphanWorktreeLine(w) {
1318
1421
  return `${w.path}${w.branch ? ` [${w.branch}]` : ""}${w.git ? ` -- ${gitSummaryText(w.git)}` : ""}`;
1319
1422
  }
@@ -1324,7 +1427,11 @@ function lastBuildsLines(reports) {
1324
1427
  function lastBuildText(report) {
1325
1428
  const source = report.cacheHit ? `${report.cacheHit} cache` : report.status === "ok" ? "compiled" : "no cache hit";
1326
1429
  const took = report.durationMs === null ? "" : ` in ${formatDuration(report.durationMs)}`;
1327
- return `${report.platform} ${report.status === "ok" ? source : `failed (${report.errorCode ?? "error"}), ${source}`}${took}`;
1430
+ const placement = report.buildMachine === void 0 ? "" : `; build machine ${report.buildMachine}, built on ${report.builtOn ?? (report.cacheHit ? "none (cache)" : "none")}`;
1431
+ return `${report.platform} ${report.status === "ok" ? source : `failed (${report.errorCode ?? "error"}), ${source}`}${took}${placement}`;
1432
+ }
1433
+ function idleShutdownSuffix(record) {
1434
+ return record ? chalk.dim(` -- shut down after ${record.idleMinutes}m idle at ${record.at} (devices.idleShutdownMinutes)`) : "";
1328
1435
  }
1329
1436
  function activitySuffix(activity) {
1330
1437
  const label = activityLabel(activity, Date.now());
@@ -1378,6 +1485,28 @@ function readDeviceProcesses(states, tables, apps) {
1378
1485
  });
1379
1486
  }
1380
1487
  }
1488
+ const web = state.web;
1489
+ const port = web?.cdpEndpoint ? Number(new URL(web.cdpEndpoint).port) : null;
1490
+ if (web?.running && port) {
1491
+ web.page = readWebPage(state.path);
1492
+ const ownPids = [web.pid, web.supervisorPid].filter((pid) => pid !== null);
1493
+ web.activity = statusActivity(readWebActivity({
1494
+ port,
1495
+ ownPids,
1496
+ workspace: state.path
1497
+ }, tables));
1498
+ }
1499
+ }
1500
+ }
1501
+ function readMetroReverses(states, launchesByState) {
1502
+ for (const [i, state] of states.entries()) for (const target of metroReverseTargets(state, launchesByState[i])) {
1503
+ const listing = getExecutor().runFileQuiet("adb", [
1504
+ "-s",
1505
+ target.serial,
1506
+ "reverse",
1507
+ "--list"
1508
+ ], { timeoutMs: 5e3 });
1509
+ if (listing !== null && !hasMetroReverse(listing, target.metroPort)) addReverseMissingIssue(state, target);
1381
1510
  }
1382
1511
  }
1383
1512
  function appSuffix(app) {
@@ -1388,7 +1517,7 @@ function appSuffix(app) {
1388
1517
  }
1389
1518
  function linkedWorktrees(paths) {
1390
1519
  return [...new Set(paths.flatMap((path) => gitCommonDirOnDisk(path) ?? []))].flatMap((common) => {
1391
- const repository = basename(common) === ".git" ? dirname(common) : common;
1520
+ const repository = basename$1(common) === ".git" ? dirname$1(common) : common;
1392
1521
  return linkedWorktreesOnDisk(common).map((entry) => Object.assign(entry, { repository }));
1393
1522
  });
1394
1523
  }
@@ -1404,7 +1533,7 @@ function workspaceBuilds(path, saved, history) {
1404
1533
  function workspaceBuild(path, saved, history) {
1405
1534
  const record = parseActiveBuild(saved?.[ACTIVE_BUILD_KEY]);
1406
1535
  if (!record) return null;
1407
- const projectKey = statsProjectKey({
1536
+ const projectKey = statsProjectKey$1({
1408
1537
  root: path,
1409
1538
  commonDir: gitCommonDir(path),
1410
1539
  repoRoot: repoRoot(path)
@@ -1414,6 +1543,32 @@ function workspaceBuild(path, saved, history) {
1414
1543
  history: history?.[projectKey]
1415
1544
  });
1416
1545
  }
1546
+ const PHYSICAL_DEVICE_LIST_TIMEOUT_MS = 5e3;
1547
+ const PHYSICAL_DEVICE_LIST_MAX_AGE_MS = 3e4;
1548
+ const physicalDeviceLists = {};
1549
+ function freshList(entry, read) {
1550
+ const now = Date.now();
1551
+ return entry && now - entry.at < PHYSICAL_DEVICE_LIST_MAX_AGE_MS ? entry : {
1552
+ at: now,
1553
+ devices: read()
1554
+ };
1555
+ }
1556
+ function physicalDeviceReader() {
1557
+ return (platform, id) => {
1558
+ if (platform === "ios") {
1559
+ physicalDeviceLists.ios = freshList(physicalDeviceLists.ios, () => readIosDevices({ timeoutMs: PHYSICAL_DEVICE_LIST_TIMEOUT_MS }));
1560
+ return iosPhysicalReading(physicalDeviceLists.ios.devices, id);
1561
+ }
1562
+ physicalDeviceLists.android = freshList(physicalDeviceLists.android, () => {
1563
+ try {
1564
+ return listAdbDevices({ timeoutMs: PHYSICAL_DEVICE_LIST_TIMEOUT_MS });
1565
+ } catch {
1566
+ return null;
1567
+ }
1568
+ });
1569
+ return androidPhysicalReading(physicalDeviceLists.android.devices, id);
1570
+ };
1571
+ }
1417
1572
  function androidRuntimeReader() {
1418
1573
  const resolve = ownedAvdSerialResolver({ timeoutMs: 5e3 });
1419
1574
  return (avdName) => readAndroidRuntime(() => resolve(avdName));