stim 1.13.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 (133) hide show
  1. package/README.md +2 -0
  2. package/dist/{activity-DfVKZTEB.mjs → activity-B2-kb7k0.mjs} +281 -41
  3. package/dist/{android-BHfAu2mf.mjs → android-CYmq0jNi.mjs} +2 -2
  4. package/dist/{android-CD8pr8Q2.mjs → android-Cu_L4RpL.mjs} +524 -412
  5. package/dist/{android-qx_31wJo.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-CDFY2nUL.mjs → app-install-CAAplXAt.mjs} +162 -173
  9. package/dist/{budget-E5o91NR1.mjs → budget-CMiqghig.mjs} +1048 -77
  10. package/dist/{build-plan-BTa93o_r.mjs → build-plan-aeGMFX7e.mjs} +193 -650
  11. package/dist/build-progress-C2Jv1AtA.mjs +872 -0
  12. package/dist/build-slots-Df1ApSuH.mjs +127 -0
  13. package/dist/{cache-manifest-DWPF4RKJ.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
  14. package/dist/cache-manifest.mjs +1 -1
  15. package/dist/{chrome-B5hWgQJS.mjs → chrome-BrcVWQyD.mjs} +4 -4
  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-BIL7rlX7.mjs → command-output-CliHHn7a.mjs} +1 -1
  21. package/dist/{config-0jCcMM0E.mjs → config-D0DbpZtM.mjs} +29 -29
  22. package/dist/{created-devices-Cd558h31.mjs → created-devices-BwVb3liH.mjs} +2 -2
  23. package/dist/{dependency-state-DNN0yfx6.mjs → dependency-state-BmsjKZ2H.mjs} +2 -2
  24. package/dist/{deps-2Wo81np9.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-Cf7vl-so.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-CL9dZf73.mjs → device-lease-W5Pi1q38.mjs} +43 -36
  33. package/dist/{device-pool-MuoiyRcu.mjs → device-pool-B2r38YjS.mjs} +4 -4
  34. package/dist/{device-remote-DT9ngZiD.mjs → device-remote-9ulPsdhY.mjs} +68 -162
  35. package/dist/{doctor-D2iysNNH.mjs → doctor-BcE5xsBn.mjs} +191 -99
  36. package/dist/{error-diagnostics-BoiOhck8.mjs → error-diagnostics-Cb0P1WLS.mjs} +12 -11
  37. package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
  38. package/dist/{gc-eA-PRBX8.mjs → gc-BuL1Genr.mjs} +430 -102
  39. package/dist/{android-cas-B9Kv9aIy.mjs → gradle-CmdOBq1d.mjs} +8 -294
  40. package/dist/{guide-BfZ0OmiP.mjs → guide-cCSGRFd3.mjs} +2052 -240
  41. package/dist/{guide-status-BaMYu55M.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-CmEzZiOj.mjs → ios-3CtCRhwr.mjs} +201 -10
  46. package/dist/{ios-CcfCeZ19.mjs → ios-Chob8ny7.mjs} +560 -259
  47. package/dist/{ios-device-BW-t_Lkv.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-CuJaBpEa.mjs → launch-verify-CLlofxRt.mjs} +42 -11
  50. package/dist/{logs-DcSRbC2t.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-BPeY1YMh.mjs → metro-Di8EN2Tb.mjs} +20 -120
  58. package/dist/{named-ports-n7tnJsI8.mjs → named-ports-Dj_XBNaL.mjs} +8 -64
  59. package/dist/native-run-DFDoIRbY.mjs +147 -0
  60. package/dist/{native-runtime-CiiFHt0e.mjs → native-runtime-i6OkVg-w.mjs} +11 -6
  61. package/dist/offload-worker.d.mts +121 -0
  62. package/dist/offload-worker.mjs +601 -0
  63. package/dist/{ownership-Bz2kETAS.mjs → ownership-BkYxUtuj.mjs} +3 -2
  64. package/dist/{page-BTu7I60K.mjs → page-QF_Ke10s.mjs} +7 -2
  65. package/dist/{ports-C5UutqpJ.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-DGeclhvO.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-C-cV5yy_.mjs → reload-ChPiwvot.mjs} +70 -22
  74. package/dist/{remote-cache--aehr4Pr.mjs → remote-cache-CO2elm_b.mjs} +4 -4
  75. package/dist/run-B-xqRrsg.mjs +119 -0
  76. package/dist/{server-bare-Bq8BpSIo.mjs → server-bare-X7PUgizL.mjs} +60 -13
  77. package/dist/{server-expo-DUCg52-K.mjs → server-expo-DhtX6bom.mjs} +6 -10
  78. package/dist/server-expo-KJeD109I.mjs +2 -0
  79. package/dist/{settings-xB0Frkg6.mjs → settings-CduVcoxj.mjs} +77 -26
  80. package/dist/{settings-ssVBToDR.mjs → settings-DbL6uhH8.mjs} +16 -10
  81. package/dist/settings.schema.json +357 -2
  82. package/dist/{simslim-Bbg9UWrt.mjs → simslim-Ciluxlq4.mjs} +8 -132
  83. package/dist/slot-launch-BY0BrOLh.mjs +65 -0
  84. package/dist/{spawn-claims-PT8aQpCa.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-BZhItkiL.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-DwAU2_-D.mjs → state-CaeaHbDA.mjs} +7 -4
  92. package/dist/{state-l1pYwOre.mjs → state-DFm77wRn.mjs} +67 -5
  93. package/dist/stats-DBLLFmKd.mjs +66 -0
  94. package/dist/{status-TJlL0HWo.mjs → status-DYXxNOT0.mjs} +425 -260
  95. package/dist/{status-sszP4XKU.mjs → status-l84Gbvp2.mjs} +122 -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-BZ2qMiBP.mjs → stop-Bad61z_2.mjs} +128 -19
  101. package/dist/stop-Bvf1MflS.mjs +48 -0
  102. package/dist/{stop-DskDKNTU.mjs → stop-CceYh1yG.mjs} +2 -2
  103. package/dist/stop-cause-CZ_Mr-v9.mjs +665 -0
  104. package/dist/supervisor-run.d.mts +44 -12
  105. package/dist/supervisor-run.mjs +276 -33
  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-LM6A8cNZ.mjs → toolchain-DmeYjXD9.mjs} +363 -13
  110. package/dist/warm-progress-DVHLN4uC.mjs +463 -0
  111. package/dist/{idle-DxLi5OkN.mjs → watchman-Bx0wceoF.mjs} +55 -8
  112. package/dist/{web-sWXd5iek.mjs → web-k6w8-HnE.mjs} +39 -22
  113. package/dist/web-run.mjs +326 -13
  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-DeBQSVtR.mjs → worktree-C0c5Udwa.mjs} +55 -54
  118. package/dist/{worktree-D4Do_XiQ.mjs → worktree-qXKzADSC.mjs} +161 -255
  119. package/package.json +10 -5
  120. package/shim/bundle-response.cjs +55 -10
  121. package/shim/bundle-response.d.cts +1 -0
  122. package/shim/expo-metro-config.cjs +10 -2
  123. package/dist/build-progress-l7d-X-ZP.mjs +0 -247
  124. package/dist/in-use-wYEaKQM9.mjs +0 -249
  125. package/dist/ownership-PUP4Dwai.mjs +0 -1138
  126. package/dist/server-expo-BeYwP_Xy.mjs +0 -2
  127. package/dist/slot-launch-BIWEW-wI.mjs +0 -19
  128. package/dist/start-Rw2GD3nV.mjs +0 -2
  129. package/dist/stats-CaSwmsSZ.mjs +0 -62
  130. package/dist/stats-D8rUTPHd.mjs +0 -368
  131. package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
  132. package/dist/worktree-3cBa-bQt.mjs +0 -2
  133. package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
@@ -0,0 +1,463 @@
1
+ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
+ import { n as ownership_claim_exports } from "./process-identity-BmRm1qV0.mjs";
3
+ import { o as pullRequestLookups } from "./pull-request-pxnDCQzn.mjs";
4
+ import { E as withDirLock, a as updateWorkspaceState, b as workspaceDir, d as discoverAgentSessions, l as agentKey, p as ensureWorkspaceStorage, u as attributeAgentSessions } from "./workspace-state-CL2V2Ons.mjs";
5
+ import { c as gitCommonDirOnDisk } from "./worktree-C0c5Udwa.mjs";
6
+ import { I as ownedAvdDirectory } from "./android-Db2eEMaf.mjs";
7
+ import { randomUUID } from "node:crypto";
8
+ import { existsSync, mkdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
9
+ import { basename, dirname, join } from "node:path";
10
+ import { ENDED_AGENT_RETENTION_MS, READY_PHASE_MS, WARM_KEY, agentSessionsCacheFile, diskUsageCacheFile, pullRequestCacheFile, readAgentSessionsCache, readDiskUsage, readEndedAgentSessions, readPullRequestCache, readWarmRecord, readWorkspaceAgent, readWorkspaceState, workspaceEndedAgentsFile, workspaceEndedAgentsLock } from "@stim-cli/core/state";
11
+ import { homedir } from "node:os";
12
+ //#region src/status-measures.ts
13
+ const LIVE_DISK_MAX_AGE_MS = 3e5;
14
+ const IDLE_DISK_MAX_AGE_MS = 36e5;
15
+ const DU_TIMEOUT_MS = 12e4;
16
+ const PULL_REQUEST_MAX_AGE_MS = 3e5;
17
+ const PULL_REQUEST_CHECK_MS = 6e4;
18
+ const GIT_TIMEOUT_MS = 5e3;
19
+ const AGENT_DISCOVERY_MS = 15e3;
20
+ const AGENT_CACHE_MAX_AGE_MS = 12e4;
21
+ function writeCacheFile(file, entry) {
22
+ const temporary = `${file}.${randomUUID()}.tmp`;
23
+ try {
24
+ mkdirSync(dirname(file), { recursive: true });
25
+ writeFileSync(temporary, JSON.stringify(entry));
26
+ renameSync(temporary, file);
27
+ } catch {
28
+ rmSync(temporary, { force: true });
29
+ }
30
+ }
31
+ function forgetStatusMeasures(worktree, workspaces) {
32
+ const roots = [.../* @__PURE__ */ new Set([worktree, ...workspaces])];
33
+ const files = [pullRequestCacheFile(worktree), ...roots.flatMap((root) => [
34
+ diskUsageCacheFile(root),
35
+ diskUsageCacheFile(join(root, "node_modules")),
36
+ diskUsageCacheFile(workspaceDir(root))
37
+ ])];
38
+ for (const file of files) try {
39
+ rmSync(file, { force: true });
40
+ } catch {}
41
+ }
42
+ function simulatorDataDir(udid) {
43
+ return join(homedir(), "Library", "Developer", "CoreSimulator", "Devices", udid);
44
+ }
45
+ function checkoutDir(state) {
46
+ if (state.worktree?.path) return state.worktree.path;
47
+ const common = gitCommonDirOnDisk(state.path);
48
+ return common && basename(common) === ".git" ? dirname(common) : state.path;
49
+ }
50
+ function deviceFolders(state) {
51
+ const folders = [];
52
+ for (const { ios, android } of [state, ...state.slots ?? []]) {
53
+ if (ios?.owned) folders.push({
54
+ device: ios,
55
+ path: simulatorDataDir(ios.udid)
56
+ });
57
+ if (android?.owned && !android.physical && android.name) {
58
+ const path = ownedAvdDirectory(android.name);
59
+ if (path) folders.push({
60
+ device: android,
61
+ path
62
+ });
63
+ }
64
+ }
65
+ return folders;
66
+ }
67
+ /** The folders an environment's disk use sums: its checkout, its node_modules folders and Stim's workspace folder. */
68
+ function environmentFolders(state) {
69
+ const worktree = checkoutDir(state);
70
+ return {
71
+ worktree,
72
+ nodeModules: [.../* @__PURE__ */ new Set([join(worktree, "node_modules"), join(state.path, "node_modules")])],
73
+ build: workspaceDir(state.path)
74
+ };
75
+ }
76
+ /** The environment's disk use from the cached measurements; null before any folder was measured. */
77
+ function environmentDisk(folders) {
78
+ const worktree = readDiskUsage(folders.worktree);
79
+ const modules = folders.nodeModules.map((path) => readDiskUsage(path));
80
+ const build = readDiskUsage(folders.build);
81
+ const measured = [
82
+ worktree,
83
+ ...modules,
84
+ build
85
+ ].filter((entry) => entry !== null);
86
+ if (!measured.length) return null;
87
+ return {
88
+ worktreeBytes: worktree?.bytes ?? null,
89
+ nodeModulesBytes: modules.every((entry) => entry !== null) ? modules.reduce((sum, entry) => sum + entry.bytes, 0) : null,
90
+ buildBytes: build?.bytes ?? null,
91
+ measuredAt: measured.map((entry) => entry.measuredAt).toSorted()[0]
92
+ };
93
+ }
94
+ function statusPullRequest(fact, checkedAt) {
95
+ return {
96
+ number: fact.number,
97
+ url: fact.url,
98
+ title: fact.title ?? "",
99
+ state: fact.state === "open" && fact.draft ? "draft" : fact.state,
100
+ checks: fact.checks ?? null,
101
+ reviewDecision: fact.reviewDecision ?? null,
102
+ checkedAt
103
+ };
104
+ }
105
+ function canonicalPath(path) {
106
+ try {
107
+ return realpathSync(path);
108
+ } catch {
109
+ return path;
110
+ }
111
+ }
112
+ function agentWorkspaces(states) {
113
+ return states.map((state) => ({
114
+ path: canonicalPath(state.path),
115
+ root: canonicalPath(checkoutDir(state)),
116
+ recorded: readWorkspaceAgent(readWorkspaceState(state.path))
117
+ }));
118
+ }
119
+ function applyAgentSessions(states, now) {
120
+ const cache = readAgentSessionsCache();
121
+ const sessions = cache && now - Date.parse(cache.discoveredAt) <= AGENT_CACHE_MAX_AGE_MS ? cache.sessions : [];
122
+ const discovered = new Set(sessions.map(agentKey));
123
+ const ended = states.map((state) => readEndedAgentSessions(state.path, now));
124
+ const workspaces = agentWorkspaces(states);
125
+ workspaces.forEach((workspace, i) => {
126
+ const { recorded } = workspace;
127
+ if (!recorded || discovered.has(agentKey(recorded))) return;
128
+ if (ended[i].some((session) => agentKey(session) === agentKey(recorded))) workspace.recorded = null;
129
+ });
130
+ attributeAgentSessions(workspaces, sessions, now).forEach((agents, i) => {
131
+ if (agents.length) states[i].agents = agents;
132
+ const running = new Set(agents.map(agentKey));
133
+ const endedAgents = ended[i].filter((session) => !running.has(agentKey(session)));
134
+ if (endedAgents.length) states[i].endedAgents = endedAgents;
135
+ });
136
+ }
137
+ function writeFileAtomic(file, entry) {
138
+ const temporary = `${file}.${randomUUID()}.tmp`;
139
+ try {
140
+ writeFileSync(temporary, JSON.stringify(entry));
141
+ renameSync(temporary, file);
142
+ } catch (error) {
143
+ rmSync(temporary, { force: true });
144
+ throw error;
145
+ }
146
+ }
147
+ function recordEndedAgents(states, gone, endedAt, now) {
148
+ const goneKeys = new Set(gone.map(agentKey));
149
+ const result = {
150
+ changed: false,
151
+ failed: false
152
+ };
153
+ attributeAgentSessions(agentWorkspaces(states), gone, now).forEach((sessions, i) => {
154
+ const root = states[i].path;
155
+ const ended = sessions.filter((session) => goneKeys.has(agentKey(session)));
156
+ if (!ended.length || !existsSync(workspaceDir(root))) return;
157
+ try {
158
+ withDirLock(workspaceEndedAgentsLock(root), () => {
159
+ const kept = new Map(readEndedAgentSessions(root, now).map((session) => [agentKey(session), session]));
160
+ for (const { pid: _pid, ...session } of ended) kept.set(agentKey(session), {
161
+ ...session,
162
+ endedAt
163
+ });
164
+ writeFileAtomic(workspaceEndedAgentsFile(root), { sessions: [...kept.values()] });
165
+ });
166
+ result.changed = true;
167
+ } catch {
168
+ result.failed = true;
169
+ }
170
+ });
171
+ return result;
172
+ }
173
+ /**
174
+ * Adds the cached measurements to a status read: each environment's `disk`, `agents` and `endedAgents`, each owned
175
+ * device's `disk`, and each linked worktree's `pullRequest` when the cache holds a lookup for its current branch.
176
+ * Reads files only.
177
+ */
178
+ function applyStatusMeasures(states, worktrees) {
179
+ applyAgentSessions(states, Date.now());
180
+ for (const state of states) {
181
+ const disk = environmentDisk(environmentFolders(state));
182
+ if (disk) state.disk = disk;
183
+ for (const { device, path } of deviceFolders(state)) {
184
+ const measure = readDiskUsage(path);
185
+ if (measure) device.disk = measure;
186
+ }
187
+ }
188
+ for (const worktree of worktrees) {
189
+ const cached = worktree.branch ? readPullRequestCache(worktree.path) : null;
190
+ if (cached && cached.branch === worktree.branch) worktree.pullRequest = cached.pullRequest;
191
+ }
192
+ }
193
+ /** Kilobytes from `du -sk` output, or null when it printed none. */
194
+ function parseDuKilobytes(output) {
195
+ const kb = Number(/^(\d+)\s/.exec(output)?.[1]);
196
+ return Number.isFinite(kb) ? kb : null;
197
+ }
198
+ function isStale(measure, maxAgeMs, now) {
199
+ return !measure || now - Date.parse(measure.measuredAt) >= maxAgeMs;
200
+ }
201
+ /**
202
+ * Keeps the disk use, pull request and agent session caches fresh for `status --watch`, off its refresh path: one
203
+ * agent session discovery at most every 15 seconds, which status ignores once it is over 2 minutes old, one `du -sk` at
204
+ * a time, live environments first, each folder at most every 5 minutes while its environment is live and every hour otherwise, and one `gh api
205
+ * graphql` per repository for worktrees whose lookup is over 5 minutes old or whose branch or HEAD moved. It
206
+ * runs git only in the repository, never in a worktree, and only for worktrees whose git state status read. It rechecks
207
+ * a folder's cache right before measuring, so two watchers rarely measure the same folder. A session the previous
208
+ * discovery found and this one does not is recorded as ended in the workspaces it worked in. `updated` runs after each
209
+ * write.
210
+ */
211
+ function createStatusMeasurer({ updated, now = Date.now }) {
212
+ const exec = getExecutor();
213
+ let measuring = false;
214
+ let duMissing = false;
215
+ let checking = false;
216
+ let checkedAt = -Infinity;
217
+ let discovering = false;
218
+ let discoveredAt = -Infinity;
219
+ const failedAt = /* @__PURE__ */ new Map();
220
+ async function du(path) {
221
+ try {
222
+ return parseDuKilobytes(await exec.runFileAsync("du", ["-sk", path], { timeoutMs: DU_TIMEOUT_MS }));
223
+ } catch (error) {
224
+ const { code, status, stdout } = error;
225
+ if (code === "ENOENT") duMissing = true;
226
+ return status === 1 && typeof stdout === "string" ? parseDuKilobytes(stdout) : null;
227
+ }
228
+ }
229
+ async function measure(folders) {
230
+ for (const { path, maxAgeMs } of folders) {
231
+ if (duMissing || !isStale(readDiskUsage(path), maxAgeMs, now())) continue;
232
+ if (now() - (failedAt.get(path) ?? -Infinity) < maxAgeMs) continue;
233
+ let bytes = 0;
234
+ if (existsSync(path)) {
235
+ const kb = await du(path);
236
+ if (kb === null) {
237
+ failedAt.set(path, now());
238
+ continue;
239
+ }
240
+ bytes = kb * 1024;
241
+ } else if (!basename(path).startsWith("node_modules")) continue;
242
+ writeCacheFile(diskUsageCacheFile(path), {
243
+ path,
244
+ bytes,
245
+ measuredAt: new Date(now()).toISOString()
246
+ });
247
+ updated();
248
+ }
249
+ }
250
+ async function branchHead(repository, branch) {
251
+ try {
252
+ return (await exec.runFileAsync("git", [
253
+ "-C",
254
+ repository,
255
+ "rev-parse",
256
+ "--verify",
257
+ `refs/heads/${branch}`
258
+ ], { timeoutMs: GIT_TIMEOUT_MS })).trim() || null;
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
263
+ async function checkPullRequests(worktrees) {
264
+ const lookup = pullRequestLookups({ detail: true });
265
+ const byRepository = /* @__PURE__ */ new Map();
266
+ for (const { path, branch, repository, git } of worktrees) {
267
+ if (!branch || !repository || !git) continue;
268
+ const head = await branchHead(repository, branch);
269
+ if (!head) continue;
270
+ const cached = readPullRequestCache(path);
271
+ if (!(cached?.branch === branch && cached.head === head && now() - Date.parse(cached.checkedAt) < PULL_REQUEST_MAX_AGE_MS)) byRepository.set(repository, [...byRepository.get(repository) ?? [], {
272
+ path,
273
+ cwd: repository,
274
+ branch,
275
+ head
276
+ }]);
277
+ }
278
+ for (const [repository, queries] of byRepository) {
279
+ const results = await lookup(repository, queries);
280
+ const at = new Date(now()).toISOString();
281
+ results.forEach((result, i) => {
282
+ if (!("pullRequest" in result)) return;
283
+ const { path, branch, head } = queries[i];
284
+ const entry = {
285
+ path,
286
+ branch,
287
+ head,
288
+ checkedAt: at,
289
+ pullRequest: result.pullRequest ? statusPullRequest(result.pullRequest, at) : null
290
+ };
291
+ writeCacheFile(pullRequestCacheFile(path), entry);
292
+ });
293
+ if (results.some((result) => "pullRequest" in result)) updated();
294
+ }
295
+ }
296
+ async function discoverAgents(states) {
297
+ const at = now();
298
+ const sessions = await discoverAgentSessions({ now: at });
299
+ const previous = readAgentSessionsCache();
300
+ const running = new Set(sessions.map(agentKey));
301
+ const lastFound = previous ? Date.parse(previous.discoveredAt) : -Infinity;
302
+ const gone = at - lastFound <= ENDED_AGENT_RETENTION_MS ? previous.sessions.filter((session) => !running.has(agentKey(session))) : [];
303
+ const ended = gone.length ? recordEndedAgents(states, gone, previous.discoveredAt, at) : {
304
+ changed: false,
305
+ failed: false
306
+ };
307
+ if (ended.failed && at - lastFound <= AGENT_CACHE_MAX_AGE_MS) return;
308
+ writeCacheFile(agentSessionsCacheFile(), {
309
+ discoveredAt: new Date(at).toISOString(),
310
+ sessions
311
+ });
312
+ const shown = previous && at - Date.parse(previous.discoveredAt) <= AGENT_CACHE_MAX_AGE_MS;
313
+ if (ended.changed || !shown || JSON.stringify(previous.sessions) !== JSON.stringify(sessions)) updated();
314
+ }
315
+ return { schedule(states, worktrees) {
316
+ if (!discovering && now() - discoveredAt >= AGENT_DISCOVERY_MS) {
317
+ discovering = true;
318
+ discoveredAt = now();
319
+ discoverAgents(states).catch(() => {}).finally(() => {
320
+ discovering = false;
321
+ });
322
+ }
323
+ if (!measuring) {
324
+ const folders = /* @__PURE__ */ new Map();
325
+ for (const state of states.toSorted((a, b) => Number(b.live) - Number(a.live))) {
326
+ if (!existsSync(state.path)) continue;
327
+ const maxAgeMs = state.live ? LIVE_DISK_MAX_AGE_MS : IDLE_DISK_MAX_AGE_MS;
328
+ const { worktree, nodeModules, build } = environmentFolders(state);
329
+ for (const path of [
330
+ worktree,
331
+ ...nodeModules,
332
+ build,
333
+ ...deviceFolders(state).map((folder) => folder.path)
334
+ ]) folders.set(path, Math.min(folders.get(path) ?? Infinity, maxAgeMs));
335
+ }
336
+ measuring = true;
337
+ measure([...folders].map(([path, maxAgeMs]) => ({
338
+ path,
339
+ maxAgeMs
340
+ }))).catch(() => {}).finally(() => {
341
+ measuring = false;
342
+ });
343
+ }
344
+ if (!checking && now() - checkedAt >= PULL_REQUEST_CHECK_MS) {
345
+ checking = true;
346
+ checkedAt = now();
347
+ checkPullRequests(worktrees).catch(() => {}).finally(() => {
348
+ checking = false;
349
+ });
350
+ }
351
+ } };
352
+ }
353
+ //#endregion
354
+ //#region src/engine/warm-progress.ts
355
+ const NO_WARM_PROGRESS = {
356
+ step: () => {},
357
+ finish: () => {}
358
+ };
359
+ function warmProgressClaimRoot(root) {
360
+ return join(workspaceDir(root), "warm.lock");
361
+ }
362
+ /**
363
+ * Records that `stim worktree warm` runs in the workspace at `root`, under an ownership claim this process holds
364
+ * until `finish`, so a warm that was killed reads as over. A process that cannot take the claim records nothing.
365
+ */
366
+ function startWarmProgress(root, step, { note, now = Date.now }) {
367
+ let claim;
368
+ try {
369
+ ensureWorkspaceStorage(root);
370
+ const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
371
+ root: warmProgressClaimRoot(root),
372
+ mode: "shared",
373
+ label: "worktree warm"
374
+ });
375
+ if (!attempt.acquired) throw new Error("another process holds its claim exclusively");
376
+ claim = attempt.acquired;
377
+ } catch (error) {
378
+ note(`Warm progress is not recorded for status: ${error?.message || error}`);
379
+ return NO_WARM_PROGRESS;
380
+ }
381
+ const startedAt = new Date(now()).toISOString();
382
+ const ours = (state) => {
383
+ const record = readWarmRecord(state);
384
+ return record?.phase === "warming" && record.claim.claimId === claim.claimId;
385
+ };
386
+ let warned = false;
387
+ const write = (update) => {
388
+ try {
389
+ updateWorkspaceState(root, update);
390
+ } catch (error) {
391
+ if (warned) return;
392
+ warned = true;
393
+ note(`Warm progress could not be recorded in the workspace state: ${error?.message || error}`);
394
+ }
395
+ };
396
+ const warming = (current) => ({
397
+ phase: "warming",
398
+ step: current,
399
+ startedAt,
400
+ claim: {
401
+ root: claim.root,
402
+ claimId: claim.claimId
403
+ }
404
+ });
405
+ write((state) => ({
406
+ ...state,
407
+ [WARM_KEY]: warming(step)
408
+ }));
409
+ return {
410
+ step(next) {
411
+ write((state) => ours(state) ? {
412
+ ...state,
413
+ [WARM_KEY]: warming(next)
414
+ } : state);
415
+ },
416
+ finish(succeeded) {
417
+ write((state) => {
418
+ if (!ours(state)) return state;
419
+ const { [WARM_KEY]: _, ...rest } = state;
420
+ return succeeded ? {
421
+ ...rest,
422
+ [WARM_KEY]: {
423
+ phase: "ready",
424
+ at: new Date(now()).toISOString()
425
+ }
426
+ } : rest;
427
+ });
428
+ (0, ownership_claim_exports.releaseClaim)(claim);
429
+ }
430
+ };
431
+ }
432
+ /**
433
+ * The lifecycle phase status reports. A warming record counts only while a warm holds the workspace's claim, and a ready
434
+ * record only until the workspace is next used or `READY_PHASE_MS` passes.
435
+ */
436
+ function workspacePhase(live, state, { now = Date.now(), survey = ownership_claim_exports.readClaimSet } = {}) {
437
+ if (live) return {
438
+ phase: "live",
439
+ phaseSince: null
440
+ };
441
+ const record = readWarmRecord(state);
442
+ if (record?.phase === "warming") {
443
+ if (survey(record.claim.root).live.length) return {
444
+ phase: "warming",
445
+ phaseSince: record.startedAt,
446
+ warmStep: record.step
447
+ };
448
+ }
449
+ if (record?.phase === "ready") {
450
+ const at = Date.parse(record.at);
451
+ const used = Date.parse(String(state?.lastUsedAt ?? ""));
452
+ if (now - at < READY_PHASE_MS && !(used > at)) return {
453
+ phase: "ready",
454
+ phaseSince: record.at
455
+ };
456
+ }
457
+ return {
458
+ phase: "idle",
459
+ phaseSince: null
460
+ };
461
+ }
462
+ //#endregion
463
+ export { forgetStatusMeasures as a, createStatusMeasurer as i, workspacePhase as n, applyStatusMeasures as r, startWarmProgress as t };
@@ -1,10 +1,11 @@
1
- import { U as projectDeviceSlots } from "./config-0jCcMM0E.mjs";
2
- import { h as readWorkspaceState } from "./metro-BPeY1YMh.mjs";
3
- import { o as formatLongDuration } from "./command-output-BIL7rlX7.mjs";
4
- import { i as idleForMs, n as createActivityReader } from "./activity-DfVKZTEB.mjs";
5
- import { R as ownedAvdSerialResolver } from "./android-qx_31wJo.mjs";
6
- import { f as teardownOwnedAvd, m as teardownOwnedIosSim } from "./ownership-PUP4Dwai.mjs";
7
- import { r as activeBuildState, s as parseActiveBuild, t as ACTIVE_BUILD_KEY } from "./build-progress-l7d-X-ZP.mjs";
1
+ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
+ import { H as projectDeviceSlots } from "./config-D0DbpZtM.mjs";
3
+ import { r as readWorkspaceState } from "./workspace-state-CL2V2Ons.mjs";
4
+ import { i as idleForMs, n as createActivityReader } from "./activity-B2-kb7k0.mjs";
5
+ import { o as formatLongDuration } from "./command-output-CliHHn7a.mjs";
6
+ import { z as ownedAvdSerialResolver } from "./android-Db2eEMaf.mjs";
7
+ import { i as teardownOwnedAvd, o as teardownOwnedIosSim } from "./teardown-BYuQafiA.mjs";
8
+ import { r as activeBuildState, s as parseActiveBuild, t as ACTIVE_BUILD_KEY } from "./build-progress-C2Jv1AtA.mjs";
8
9
  import chalk from "chalk";
9
10
  //#region src/commands/gc/results.ts
10
11
  const results = [];
@@ -188,4 +189,50 @@ function shutDownIdleDevices(devices, idleMs, recollect) {
188
189
  };
189
190
  }
190
191
  //#endregion
191
- export { parseIdleDuration as a, recordGcResult as c, idleShutdownCandidates as i, takeGcResults as l, collectOwnedDeviceActivity as n, shutDownIdleDevices as o, idleDeviceLines as r, workspaceBuildInProgress as s, collectIdleDevices as t };
192
+ //#region src/watchman.ts
193
+ /** Runs one watchman command with `--no-spawn`, so Stim never starts a daemon, and parses its JSON answer. */
194
+ const runWatchman = async (args, timeoutMs) => JSON.parse(await getExecutor().runFileAsync("watchman", ["--no-spawn", ...args], { timeoutMs }));
195
+ function parseWatchRoots(payload) {
196
+ const roots = payload?.roots;
197
+ if (!Array.isArray(roots)) return null;
198
+ return roots.filter((root) => typeof root === "string");
199
+ }
200
+ function parseSubscriberNames(payload) {
201
+ if (!payload || typeof payload !== "object" || "error" in payload) return null;
202
+ const subscribers = payload.subscribers;
203
+ if (subscribers === void 0) return [];
204
+ if (!Array.isArray(subscribers)) return null;
205
+ const names = [];
206
+ for (const subscriber of subscribers) {
207
+ const name = subscriber?.info?.name;
208
+ if (typeof name !== "string") return null;
209
+ names.push(name);
210
+ }
211
+ return names;
212
+ }
213
+ function parseTriggerCount(payload) {
214
+ const triggers = payload?.triggers;
215
+ return Array.isArray(triggers) ? triggers.length : null;
216
+ }
217
+ function parseWatchmanPid(payload) {
218
+ const pid = payload?.pid;
219
+ return typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? pid : null;
220
+ }
221
+ /** The connected clients in a `debug-status` answer, or null when the answer does not list them. */
222
+ function parseWatchmanClients(payload) {
223
+ if (!payload || typeof payload !== "object" || "error" in payload) return null;
224
+ const clients = payload.clients;
225
+ if (!Array.isArray(clients)) return null;
226
+ const parsed = [];
227
+ for (const client of clients) {
228
+ const peer = client?.peer;
229
+ if (typeof peer?.pid !== "number") return null;
230
+ parsed.push({
231
+ pid: peer.pid,
232
+ name: typeof peer.name === "string" ? peer.name : null
233
+ });
234
+ }
235
+ return parsed;
236
+ }
237
+ //#endregion
238
+ export { parseWatchmanPid as a, collectOwnedDeviceActivity as c, parseIdleDuration as d, shutDownIdleDevices as f, takeGcResults as h, parseWatchmanClients as i, idleDeviceLines as l, recordGcResult as m, parseTriggerCount as n, runWatchman as o, workspaceBuildInProgress as p, parseWatchRoots as r, collectIdleDevices as s, parseSubscriberNames as t, idleShutdownCandidates as u };
@@ -1,22 +1,27 @@
1
- import { T as upsertProject, p as getProject } from "./config-0jCcMM0E.mjs";
2
- import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
1
+ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
+ import { E as upsertProject, p as getProject } from "./config-D0DbpZtM.mjs";
3
3
  import { o as readNdjsonGenerations } from "./ndjson-CDY9IkVC.mjs";
4
- import { M as workspaceLogsDir, c as resolveProjectMetro, f as reserveMetroPort, g as recordWorkspaceUse, h as readWorkspaceState, k as workspaceDir } from "./metro-BPeY1YMh.mjs";
5
- import { s as gitCommonDir, y as repoRoot } from "./worktree-DeBQSVtR.mjs";
6
- import { i as findCommandWorkspace, r as detectIsExpo, s as isPackageResolvable } from "./project-DGeclhvO.mjs";
7
- import { n as windowsLauncherArgs } from "./detached-entry-DJHoca49.mjs";
8
- import { a as withWorkspaceProcessLock, i as reserveBrowserPort, n as getNamedPort } from "./named-ports-n7tnJsI8.mjs";
9
- import { f as refuseNoProject, l as phaseLine } from "./command-output-BIL7rlX7.mjs";
10
- import { P as settingShapeErrors, R as webSettings, a as SETTING_SHAPE_REMEDY, j as resolveSettings } from "./settings-xB0Frkg6.mjs";
11
- import { d as teardownBrowserHeld, t as resolveSupervisorTarget } from "./ownership-PUP4Dwai.mjs";
12
- import { a as readWebRecord, f as webSupervisorLogFile, o as updateWebRecord, r as cdpEndpoint, u as webLogFile } from "./state-l1pYwOre.mjs";
13
- import { i as findChrome, t as CHROME_INSTALL_REMEDY } from "./chrome-B5hWgQJS.mjs";
14
- import { c as readMetroRecords } from "./launch-verify-CuJaBpEa.mjs";
15
- import { f as spawnEntry } from "./start-BZhItkiL.mjs";
16
- import { a as sleep, n as ensureWorkspaceStorageSafely } from "./native-runtime-CiiFHt0e.mjs";
17
- import { n as sendToOwnedPage, t as liveWebRecord } from "./page-BTu7I60K.mjs";
18
- import { mkdirSync, openSync } from "node:fs";
4
+ import { C as workspaceLogsDir, b as workspaceDir, i as recordWorkspaceUse, r as readWorkspaceState } from "./workspace-state-CL2V2Ons.mjs";
5
+ import { s as gitCommonDir, y as repoRoot } from "./worktree-C0c5Udwa.mjs";
6
+ import { i as findCommandWorkspace, r as detectIsExpo, s as isPackageResolvable } from "./project-CV3tDFHl.mjs";
7
+ import { n as windowsLauncherArgs } from "./detached-entry-CiTEgMR1.mjs";
8
+ import { t as withWorkspaceProcessLock } from "./workspace-process-lock-BL7Q0HCO.mjs";
9
+ import { f as refuseNoProject, l as phaseLine } from "./command-output-CliHHn7a.mjs";
10
+ import { a as workspaceLinks, i as workspaceLinkLine } from "./stim-desktop-Cnz8CPnl.mjs";
11
+ import { l as resolveProjectMetro } from "./metro-Di8EN2Tb.mjs";
12
+ import { L as settingShapeErrors, N as resolveSettings, S as metroPortSetting, V as webSettings, a as SETTING_SHAPE_REMEDY } from "./settings-CduVcoxj.mjs";
13
+ import { r as teardownBrowserHeld } from "./teardown-BYuQafiA.mjs";
14
+ import { i as reserveBrowserPort, n as getNamedPort } from "./named-ports-Dj_XBNaL.mjs";
15
+ import { d as webLogFile, o as readWebRecord, p as webSupervisorLogFile, r as cdpEndpoint, s as updateWebRecord } from "./state-DFm77wRn.mjs";
16
+ import { u as resolveSupervisorTarget } from "./stop-cause-CZ_Mr-v9.mjs";
17
+ import { a as findChrome, t as CHROME_INSTALL_REMEDY } from "./chrome-BrcVWQyD.mjs";
18
+ import { t as spawnEntry } from "./spawn-entry-DncpJvWL.mjs";
19
+ import { c as readMetroRecords } from "./launch-verify-CLlofxRt.mjs";
20
+ import { o as resolveWorkspaceMetroPort } from "./start-CUD9nasA.mjs";
21
+ import { a as sleep, n as ensureWorkspaceStorageSafely } from "./native-runtime-i6OkVg-w.mjs";
22
+ import { n as sendToOwnedPage, t as liveWebRecord } from "./page-QF_Ke10s.mjs";
19
23
  import { randomUUID } from "node:crypto";
24
+ import { mkdirSync, openSync } from "node:fs";
20
25
  import chalk from "chalk";
21
26
  //#region src/web/launch.ts
22
27
  const VERIFY_WAIT_MS = 2e4;
@@ -243,13 +248,19 @@ async function runWeb({ root, headed, note }) {
243
248
  const [shapeError] = settingShapeErrors(settings);
244
249
  if (shapeError) return failure("STIM_BAD_ARG", shapeError, SETTING_SHAPE_REMEDY);
245
250
  const web = webSettings(settings);
251
+ const usesMetro = web.url === null || web.url.includes("{port:metro}");
252
+ const pin = usesMetro ? metroPortSetting(root) : null;
253
+ if (pin?.error) return failure("STIM_BAD_ARG", pin.error, SETTING_SHAPE_REMEDY);
246
254
  const chrome = findChrome();
247
255
  if (!chrome) return failure("STIM_WEB_NO_CHROME", "Google Chrome or Chromium is not installed.", CHROME_INSTALL_REMEDY);
248
- const usesMetro = web.url === null || web.url.includes("{port:metro}");
249
256
  if (web.url === null && !detectIsExpo(root)) return failure("STIM_WEB_NO_URL", "This is not an Expo app, so Stim does not know which page to open.", "Start your web dev server on a named port, then set the page: `stim settings set web.url 'http://localhost:{port:web}/' --scope workspace`. See `stim guide web`.");
250
257
  if (web.url === null && !isPackageResolvable(root, "react-native-web")) return failure("STIM_WEB_DEPS_MISSING", "This Expo app cannot render on the web: react-native-web is not installed.", `Run \`${EXPO_WEB_DEPENDENCIES}\` and \`stim start\`, then run \`stim web\` again.`);
251
258
  let metroPort = getProject(root)?.metroPort ?? null;
252
- if (usesMetro && metroPort === null) metroPort = await reserveMetroPort(root);
259
+ if (usesMetro && (pin?.port !== null || metroPort === null)) {
260
+ const result = await resolveWorkspaceMetroPort(root, note, "web");
261
+ if (typeof result !== "number") return failure(result.code, result.message, result.remedy);
262
+ metroPort = result;
263
+ }
253
264
  const metro = usesMetro && metroPort !== null ? await resolveProjectMetro(metroPort, root) : null;
254
265
  const supervisor = resolveSupervisorTarget({
255
266
  state: readWorkspaceState(root)?.supervisor,
@@ -266,7 +277,7 @@ async function runWeb({ root, headed, note }) {
266
277
  try {
267
278
  url = await resolveWebUrl(web.url ?? "http://localhost:{port:metro}/", {
268
279
  metroPort,
269
- namedPort: (label) => getNamedPort(root, label, { log: note })
280
+ namedPort: (label) => getNamedPort(root, label)
270
281
  });
271
282
  } catch (error) {
272
283
  return failure("STIM_BAD_ARG", `web.url: ${error.message}`, SETTING_SHAPE_REMEDY);
@@ -300,7 +311,7 @@ async function runWeb({ root, headed, note }) {
300
311
  }
301
312
  const stopped = await teardownBrowserHeld(root);
302
313
  if (stopped.status === "failed" || stopped.status === "skipped") return failure("STIM_WEB_BROWSER_HELD", `The previous owned Chrome could not be stopped: ${stopped.reason ?? "unknown reason"}.`, "Run `stim status` and `stim guide errors teardown`.");
303
- const port = await reserveBrowserPort(root, { log: note });
314
+ const port = await reserveBrowserPort(root);
304
315
  const since = Date.now();
305
316
  note(chalk.dim(phaseLine("device", `starting ${config.headless ? "headless " : ""}Chrome on DevTools port ${port}`)));
306
317
  const launchId = randomUUID();
@@ -351,6 +362,7 @@ async function runWeb({ root, headed, note }) {
351
362
  viewport: record.viewport,
352
363
  profile: record.profile,
353
364
  cdpEndpoint: live ? cdpEndpoint(live.cdpPort) : null,
365
+ targetId: live?.targetId ?? null,
354
366
  reused: launch.reused,
355
367
  launched: verdict.launched,
356
368
  metroPort: usesMetro ? metroPort : null,
@@ -382,12 +394,17 @@ function webCommand(program) {
382
394
  }
383
395
  const { facts, remedy } = result;
384
396
  if (remedy) printNote(chalk.yellow(phaseLine("launch", remedy)));
397
+ const links = workspaceLinks(root, { platform: "web" });
385
398
  if (json) {
386
- console.log(JSON.stringify(facts));
399
+ console.log(JSON.stringify(links ? {
400
+ ...facts,
401
+ links
402
+ } : facts));
387
403
  return;
388
404
  }
389
405
  const launched = facts.launched === true ? chalk.green("loaded") : chalk.yellow(facts.launched === "bundling" ? "bundling" : "unverified");
390
406
  console.log(facts.running ? `${facts.url} ${launched} in ${facts.version ?? "Chrome"} (pid ${facts.pid}, ${facts.headless ? "headless" : "headed"}, ${facts.viewport}). DevTools: ${facts.cdpEndpoint}. Logs: stim logs --errors.` : `${facts.url} ${launched}, and the owned Chrome is no longer running. Run stim logs --errors, then stim web again.`);
407
+ if (links) console.error(chalk.dim(workspaceLinkLine(links)));
391
408
  });
392
409
  }
393
410
  //#endregion