stim 1.14.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +2 -0
  2. package/dist/activity-BxwdD81S.mjs +609 -0
  3. package/dist/agent-device-usage-output-Dpih1Tfa.mjs +411 -0
  4. package/dist/{android-d6rGq1pA.mjs → android-B_NumVyQ.mjs} +169 -81
  5. package/dist/{android-LbPFRWVL.mjs → android-Ift4kVvt.mjs} +517 -413
  6. package/dist/android-cas-8U4DPqeR.mjs +167 -0
  7. package/dist/android-cas-compiler.mjs +2 -2
  8. package/dist/{android-XpvCrV7q.mjs → android-r2rBL8kV.mjs} +2 -2
  9. package/dist/{app-install-BpgSgkJj.mjs → app-install-CKWdbXIX.mjs} +128 -151
  10. package/dist/attempt-Bk2ouQVL.mjs +39 -0
  11. package/dist/budget-DjrURfQn.mjs +2700 -0
  12. package/dist/{build-plan-Dcr-3ie0.mjs → build-plan-DjeSb2Xl.mjs} +192 -653
  13. package/dist/build-slots-rUk77xqh.mjs +132 -0
  14. package/dist/{cache-manifest-C68QQ0ua.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
  15. package/dist/cache-manifest.mjs +1 -1
  16. package/dist/{chrome-BPCiC3ZX.mjs → chrome-BrcVWQyD.mjs} +1 -1
  17. package/dist/cli-BkYZ_6hy.mjs +51 -0
  18. package/dist/cli.mjs +6 -42
  19. package/dist/client-BTY686R6.mjs +1368 -0
  20. package/dist/collector-run.mjs +9 -195
  21. package/dist/{command-output-DVcpw6eE.mjs → command-output-DnJMdEY-.mjs} +1 -1
  22. package/dist/{config-lvc7d3C2.mjs → config-D0DbpZtM.mjs} +28 -29
  23. package/dist/{created-devices-CllH_8eb.mjs → created-devices-D5owjN4C.mjs} +2 -2
  24. package/dist/{dependency-state-DsT7aKeo.mjs → dependency-state-B6cMXZab.mjs} +2 -2
  25. package/dist/{deps-D9IcKqXS.mjs → deps-urbmfXig.mjs} +3 -3
  26. package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
  27. package/dist/dev-client-CroAIjjj.mjs +43 -0
  28. package/dist/{device-CjuuAJjQ.mjs → device-DZQjF7H5.mjs} +8 -7
  29. package/dist/device-capacity-BHwxNBlu.mjs +136 -0
  30. package/dist/device-host-worker.d.mts +1 -0
  31. package/dist/device-host-worker.mjs +964 -0
  32. package/dist/device-ios-BadmeL2V.mjs +381 -0
  33. package/dist/{device-lease-5c3WrVRm.mjs → device-lease-D-hLqx1b.mjs} +43 -36
  34. package/dist/{device-pool-BiyDyPP-.mjs → device-lease-run-CqjShqWW.mjs} +6 -126
  35. package/dist/device-pool-CUaEWMMm.mjs +125 -0
  36. package/dist/{device-remote-D87zEzaq.mjs → device-remote-D5WVWqOq.mjs} +69 -163
  37. package/dist/{doctor-x9pnU9fq.mjs → doctor-Cimf-gGB.mjs} +241 -103
  38. package/dist/{error-diagnostics-C5HzcwCW.mjs → error-diagnostics-B9L4Ae0x.mjs} +15 -18
  39. package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
  40. package/dist/{gc-ko6mUAYD.mjs → gc-Bqy4bjEu.mjs} +599 -108
  41. package/dist/{android-cas-iZK3DOCr.mjs → gradle-BsO1MdsH.mjs} +8 -294
  42. package/dist/{guide-6R0VNFqI.mjs → guide-B_uyKTxW.mjs} +2511 -239
  43. package/dist/{guide-status-BZrFPIA_.mjs → guide-status-CjEZMAD4.mjs} +3 -3
  44. package/dist/hosted-client-a-WmqE2L.mjs +197 -0
  45. package/dist/hosted-ios-CIWelIqf.mjs +220 -0
  46. package/dist/hosted-logs-D5g041Q3.mjs +187 -0
  47. package/dist/hosted-macos-BSZyIb3y.mjs +135 -0
  48. package/dist/idle-DYVymG92.mjs +1099 -0
  49. package/dist/idle-shutdown-CTsDnAde.mjs +122 -0
  50. package/dist/in-use-DvBw0Kxo.mjs +130 -0
  51. package/dist/{ios-BwDPrnYQ.mjs → ios-BaInn3sW.mjs} +201 -10
  52. package/dist/ios-Cr1h5gnO.mjs +189 -0
  53. package/dist/{ios-P1f5nm2n.mjs → ios-aq3QoGI3.mjs} +1159 -633
  54. package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
  55. package/dist/{ios-device-DGglU3E_.mjs → ios-device-FfCN_C8p.mjs} +12 -7
  56. package/dist/ios-state-DS5UpYTH.mjs +48 -0
  57. package/dist/{launch-verify-Deg4tB3Z.mjs → launch-verify-2DBRizjo.mjs} +5 -5
  58. package/dist/logs-CHEVK8zD.mjs +371 -0
  59. package/dist/machines-BIBUdhpj.mjs +265 -0
  60. package/dist/macos-CSYEAFmF.mjs +473 -0
  61. package/dist/macos-run.d.mts +5 -0
  62. package/dist/macos-run.mjs +2 -0
  63. package/dist/maintenance-run.d.mts +4 -0
  64. package/dist/maintenance-run.mjs +254 -0
  65. package/dist/{metro-CcEo8Fl8.mjs → metro-BeK1ZPz1.mjs} +20 -120
  66. package/dist/metro-gateway-BJ-rxQAB.mjs +84 -0
  67. package/dist/{named-ports-Blg8IxOh.mjs → named-ports-Bk_YXEmE.mjs} +6 -59
  68. package/dist/native-run-NaBrRFtQ.mjs +147 -0
  69. package/dist/{native-runtime-DjBHRg5m.mjs → native-runtime-DBzRnEKS.mjs} +9 -5
  70. package/dist/{ndjson-CDY9IkVC.mjs → ndjson-PclOIwVY.mjs} +2 -2
  71. package/dist/offload-worker.d.mts +122 -0
  72. package/dist/offload-worker.mjs +606 -0
  73. package/dist/{ownership-DyYDF5ww.mjs → ownership-DiA16AsN.mjs} +3 -2
  74. package/dist/ownership-hCkPzGmr.mjs +571 -0
  75. package/dist/{page-BHUzTDFt.mjs → page-QF_Ke10s.mjs} +7 -2
  76. package/dist/{ports-BguS83ex.mjs → ports-R2k4Knz3.mjs} +3 -3
  77. package/dist/prebuild-D2fsca_Z.mjs +213 -0
  78. package/dist/preview-DhVbvPAZ.mjs +318 -0
  79. package/dist/process-identity-BmRm1qV0.mjs +12 -0
  80. package/dist/{project-CWlvEaeN.mjs → project-Du91Ilvc.mjs} +74 -19
  81. package/dist/pull-request-pxnDCQzn.mjs +322 -0
  82. package/dist/pull-requests.d.mts +120 -0
  83. package/dist/pull-requests.mjs +2 -0
  84. package/dist/recordings-BIXOzVnr.mjs +37 -0
  85. package/dist/{reload-BiniwsSZ.mjs → reload-rM9jV1fz.mjs} +109 -24
  86. package/dist/{remote-cache-CcR6o6lI.mjs → remote-cache-C8-V13XN.mjs} +5 -5
  87. package/dist/run-f4vI6EnK.mjs +119 -0
  88. package/dist/{server-bare-DGfgyJ5J.mjs → server-bare-CpPGmnmO.mjs} +33 -15
  89. package/dist/server-expo-CVqO2c35.mjs +2 -0
  90. package/dist/{server-expo-BVuwQ65v.mjs → server-expo-MC0rHlOC.mjs} +8 -12
  91. package/dist/settings-BBpMcDbD.mjs +25 -0
  92. package/dist/{settings-B-fsC-Om.mjs → settings-ZkDzkzvB.mjs} +22 -10
  93. package/dist/settings-oTSkkNrM.mjs +1439 -0
  94. package/dist/settings.schema.json +845 -97
  95. package/dist/{simslim-DrDN4WuV.mjs → simslim-B8-7AoTI.mjs} +8 -132
  96. package/dist/{slot-launch-D5t8EiMf.mjs → slot-launch-B8VrHxn1.mjs} +11 -8
  97. package/dist/{spawn-claims-Drw_o-Fb.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
  98. package/dist/spawn-entry-jw19m8Nl.mjs +17 -0
  99. package/dist/stage-CzaiwFsI.mjs +143 -0
  100. package/dist/start-CX8Ig-U2.mjs +2 -0
  101. package/dist/{start-s4RXp1wy.mjs → start-ClDfwXa4.mjs} +116 -65
  102. package/dist/state-3DwSQIBK.mjs +41 -0
  103. package/dist/state-B4PQFXMW.d.mts +12 -0
  104. package/dist/{state-Dk9RVXI7.mjs → state-C1jB7Q8q.mjs} +70 -8
  105. package/dist/{state-DHqvsbqC.mjs → state-DcW6vOd_.mjs} +7 -4
  106. package/dist/stats-CEZ_i5je.mjs +77 -0
  107. package/dist/{status-BgAuxESg.mjs → status-BuvBFeII.mjs} +118 -20
  108. package/dist/{status-BsPAnfOI.mjs → status-Cs7VLg9D.mjs} +542 -283
  109. package/dist/status-DlKvVJBE.mjs +49 -0
  110. package/dist/stdout-reader-C8yjre1x.mjs +23 -0
  111. package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
  112. package/dist/stim-footprint.swift +32 -0
  113. package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
  114. package/dist/stop-B0Yxtmiy.mjs +48 -0
  115. package/dist/{stop-BzreOWJp.mjs → stop-Bffozsac.mjs} +83 -16
  116. package/dist/{stop-BWIF_6BK.mjs → stop-Dt_dM16A.mjs} +2 -2
  117. package/dist/stop-cause-CidxIqup.mjs +97 -0
  118. package/dist/supervisor-run.d.mts +43 -11
  119. package/dist/supervisor-run.mjs +363 -24
  120. package/dist/support-CAP8qDUF.mjs +316 -0
  121. package/dist/support-D4aYJniO.mjs +201 -0
  122. package/dist/teardown-CL6vDs2Y.mjs +727 -0
  123. package/dist/{xcode-Cp5KvK9x.mjs → toolchain-DkUflduf.mjs} +364 -14
  124. package/dist/trigger-Bemu8ytw.mjs +65 -0
  125. package/dist/trigger-CXts6qt1.mjs +2 -0
  126. package/dist/warm-progress-CpGv3Wb6.mjs +732 -0
  127. package/dist/{web-BsQs1WlO.mjs → web-DWUaMp8P.mjs} +37 -21
  128. package/dist/web-run.mjs +310 -13
  129. package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
  130. package/dist/workspace-state-B8-aJ1_5.mjs +371 -0
  131. package/dist/workspaces-BV4pdHFz.mjs +306 -0
  132. package/dist/{worktree-UrQWXSeb.mjs → worktree-K-xj9Db0.mjs} +625 -256
  133. package/dist/worktree-jnG8Iph5.mjs +2 -0
  134. package/package.json +10 -5
  135. package/shim/bundle-response.cjs +28 -12
  136. package/dist/activity-CYVDcrt3.mjs +0 -356
  137. package/dist/budget-CGDkhvv_.mjs +0 -1745
  138. package/dist/build-progress-DxKpwnaS.mjs +0 -247
  139. package/dist/idle-DXtx3RbX.mjs +0 -191
  140. package/dist/in-use-DlqBDKqY.mjs +0 -249
  141. package/dist/logs-Civ4gbBH.mjs +0 -486
  142. package/dist/merge-state-ytFQCeh4.mjs +0 -262
  143. package/dist/server-expo-DIHDhvGg.mjs +0 -2
  144. package/dist/settings-D61iNOsB.mjs +0 -629
  145. package/dist/start-Cetfvy8l.mjs +0 -2
  146. package/dist/stats-CFpFGlJV.mjs +0 -62
  147. package/dist/stats-CX1Z8Tca.mjs +0 -368
  148. package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
  149. package/dist/stop-cause-hjGBOZTG.mjs +0 -1231
  150. package/dist/worktree-BVrHsYZG.mjs +0 -752
  151. package/dist/worktree-CHWWvm5v.mjs +0 -2
  152. package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
@@ -0,0 +1,732 @@
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 { at as gitCommonDirOnDisk } from "./settings-oTSkkNrM.mjs";
5
+ import { D as discoverAgentSessions, E as attributeAgentSessions, T as agentKey, a as updateWorkspaceState, u as ensureWorkspaceStorage, v as workspaceDir, w as withDirLock } from "./workspace-state-B8-aJ1_5.mjs";
6
+ import { I as ownedAvdDirectory } from "./android-B_NumVyQ.mjs";
7
+ import { randomUUID } from "node:crypto";
8
+ import { existsSync, mkdirSync, realpathSync, renameSync, rmSync, statSync, 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/workspace/merge-state.ts
13
+ const FETCH_TIMEOUT_MS = 3e4;
14
+ const FETCH_FRESH_MS = 6e5;
15
+ const GIT_TIMEOUT_MS$1 = 6e4;
16
+ const REMOTE_PREFIX = "refs/remotes/origin/";
17
+ const MAX_MERGE_STATE_COMMITS = 2e3;
18
+ function failure(error) {
19
+ const { stderr, message } = error;
20
+ const text = String(stderr ?? "").trim() || String(message ?? error);
21
+ return text.split("\n")[0] ?? text;
22
+ }
23
+ function fetchedRecently(repo, now) {
24
+ const path = getExecutor().runFileQuiet("git", [
25
+ "-C",
26
+ repo,
27
+ "rev-parse",
28
+ "--path-format=absolute",
29
+ "--git-path",
30
+ "FETCH_HEAD"
31
+ ])?.trim();
32
+ try {
33
+ return Boolean(path) && now - statSync(path).mtimeMs < FETCH_FRESH_MS;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+ /**
39
+ * Fetches the default branch that `origin/HEAD` names into its remote-tracking ref, bounded by a timeout and never
40
+ * prompting for credentials. It skips the fetch when the checkout fetched anything in the last 10 minutes.
41
+ */
42
+ function fetchDefaultBranch(repo, now = Date.now()) {
43
+ const exec = getExecutor();
44
+ const ref = exec.runFileQuiet("git", [
45
+ "-C",
46
+ repo,
47
+ "symbolic-ref",
48
+ "--quiet",
49
+ "refs/remotes/origin/HEAD"
50
+ ])?.trim();
51
+ if (!ref?.startsWith(REMOTE_PREFIX)) return { error: `origin/HEAD is not set; run \`git -C ${repo} remote set-head origin --auto\`` };
52
+ const branch = ref.slice(20);
53
+ if (fetchedRecently(repo, now)) return {
54
+ ref,
55
+ name: `origin/${branch}`
56
+ };
57
+ try {
58
+ exec.runFile("git", [
59
+ "-C",
60
+ repo,
61
+ "fetch",
62
+ "--quiet",
63
+ "--no-tags",
64
+ "--no-recurse-submodules",
65
+ "origin",
66
+ `+refs/heads/${branch}:${ref}`
67
+ ], {
68
+ timeoutMs: FETCH_TIMEOUT_MS,
69
+ env: { GIT_TERMINAL_PROMPT: "0" }
70
+ });
71
+ } catch (error) {
72
+ return { error: `git fetch origin ${branch} failed: ${failure(error)}` };
73
+ }
74
+ return {
75
+ ref,
76
+ name: `origin/${branch}`
77
+ };
78
+ }
79
+ function notMerged(detail) {
80
+ return {
81
+ merged: false,
82
+ unknown: false,
83
+ detail
84
+ };
85
+ }
86
+ function currentBranch(path) {
87
+ const ref = getExecutor().runFileQuiet("git", [
88
+ "-C",
89
+ path,
90
+ "symbolic-ref",
91
+ "--quiet",
92
+ "HEAD"
93
+ ])?.trim();
94
+ return ref?.startsWith("refs/heads/") ? ref : null;
95
+ }
96
+ function upstreamGone(path, branch) {
97
+ if (!branch) return false;
98
+ const [upstream, state] = (getExecutor().runFileQuiet("git", [
99
+ "-C",
100
+ path,
101
+ "for-each-ref",
102
+ "--format=%(upstream)%00%(upstream:track)",
103
+ branch
104
+ ]) ?? "").trim().split("\0");
105
+ return Boolean(upstream) && state === "[gone]";
106
+ }
107
+ function committedOn(path, branch, head) {
108
+ if (!branch) return false;
109
+ const exec = getExecutor();
110
+ return (exec.runFileQuiet("git", [
111
+ "-C",
112
+ path,
113
+ "reflog",
114
+ "show",
115
+ "--format=%H %gs",
116
+ branch
117
+ ]) ?? "").split("\n").some((entry) => {
118
+ const [sha = "", ...subject] = entry.split(" ");
119
+ return /^(commit|cherry-pick|rebase|revert)\b/.test(subject.join(" ")) && exec.runFileQuiet("git", [
120
+ "-C",
121
+ path,
122
+ "merge-base",
123
+ "--is-ancestor",
124
+ sha,
125
+ head
126
+ ]) !== null;
127
+ });
128
+ }
129
+ /**
130
+ * Whether the worktree's HEAD is merged into `target`, and when: `mergedAt` is the committer date, in epoch
131
+ * milliseconds, of the commit on `target` that brought the work in, the latest one for a rebase merge.
132
+ * The signals, all local git:
133
+ * - HEAD is an ancestor of the default branch, off its first-parent line, and the branch's reflog shows a commit made
134
+ * on it that HEAD contains, so a merge commit brought the branch's own work in. A branch with no commit of its own
135
+ * is not merged.
136
+ * - The branch changes the tree, has no merge commits, and every commit since the merge base has the same
137
+ * `git patch-id --verbatim` as a commit on the default branch (a rebase merge).
138
+ * - The branch changes the tree and its whole diff since the merge base has the same verbatim patch id as a commit on
139
+ * the default branch, limited to the files the branch changes (a squash merge).
140
+ * Anything git cannot answer is unknown, never merged. `coversUnpushed` is true for a patch-equivalent HEAD whose
141
+ * upstream branch was deleted: its commits exist only locally, but their change is on the default branch.
142
+ */
143
+ function mergeState(path, { ref, name }, { timeoutMs = GIT_TIMEOUT_MS$1, maxCommits = MAX_MERGE_STATE_COMMITS } = {}) {
144
+ const git = (args, input) => getExecutor().runFile("git", [
145
+ "--literal-pathspecs",
146
+ "-C",
147
+ path,
148
+ ...args
149
+ ], {
150
+ timeoutMs,
151
+ input
152
+ });
153
+ const patch = (args, input) => getExecutor().runFile("git", [
154
+ "--literal-pathspecs",
155
+ "-C",
156
+ path,
157
+ ...args
158
+ ], {
159
+ timeoutMs,
160
+ input,
161
+ untrimmed: true
162
+ });
163
+ const patchIdCommits = (text) => new Map(text ? git(["patch-id", "--verbatim"], text).split("\n").flatMap((line) => {
164
+ const [id, commit] = line.split(" ");
165
+ return id && commit ? [[id, commit]] : [];
166
+ }) : []);
167
+ const patchIds = (text) => [...patchIdCommits(text).keys()];
168
+ const committedAt = (commits) => {
169
+ const times = commits.length ? git([
170
+ "log",
171
+ "--no-walk",
172
+ "--stdin",
173
+ "--format=%ct"
174
+ ], commits.join("\n")).split("\n").map(Number) : [];
175
+ if (!times.length || !times.every(Number.isFinite)) throw new Error(`no committer date for ${commits.join(" ")}`);
176
+ return Math.max(...times) * 1e3;
177
+ };
178
+ const diffOptions = ["--no-color", "--no-ext-diff"];
179
+ const noOwnCommits = notMerged(`no commits of its own beyond ${name}`);
180
+ try {
181
+ const head = git([
182
+ "rev-parse",
183
+ "--verify",
184
+ "HEAD^{commit}"
185
+ ]);
186
+ const base = git([
187
+ "merge-base",
188
+ head,
189
+ ref
190
+ ]);
191
+ const branch = currentBranch(path);
192
+ if (base === head) {
193
+ if (git(["rev-parse", ref]) === head || git([
194
+ "rev-list",
195
+ "--first-parent",
196
+ "--parents",
197
+ `${head}..${ref}`
198
+ ]).split("\n").some((line) => line.split(" ")[1] === head) || !committedOn(path, branch, head)) return noOwnCommits;
199
+ const descendants = new Set(git([
200
+ "rev-list",
201
+ "--ancestry-path",
202
+ `${head}..${ref}`
203
+ ]).split("\n"));
204
+ const landed = git([
205
+ "rev-list",
206
+ "--first-parent",
207
+ "--reverse",
208
+ `${head}..${ref}`
209
+ ]).split("\n").find((commit) => descendants.has(commit));
210
+ if (!landed) throw new Error(`no commit on ${name} merges ${head}`);
211
+ return {
212
+ merged: true,
213
+ into: name,
214
+ head,
215
+ coversUnpushed: false,
216
+ mergedAt: committedAt([landed])
217
+ };
218
+ }
219
+ const aheadCount = Number(git([
220
+ "rev-list",
221
+ "--count",
222
+ `${base}..${head}`
223
+ ]));
224
+ if (aheadCount > maxCommits) return {
225
+ merged: false,
226
+ unknown: true,
227
+ detail: `merge state unknown: ${aheadCount} commits of its own ahead of the merge base with ${name} exceeds the ${maxCommits}-commit limit`
228
+ };
229
+ const files = git([
230
+ "diff",
231
+ "--name-only",
232
+ "--no-renames",
233
+ "-z",
234
+ base,
235
+ head
236
+ ]).split("\0").filter(Boolean);
237
+ if (!files.length) return notMerged(`no net change beyond ${name}`);
238
+ const log = (range, pathspec = []) => patch([
239
+ "log",
240
+ "-p",
241
+ "--no-merges",
242
+ ...diffOptions,
243
+ "--format=commit %H",
244
+ "--stdin"
245
+ ], pathspec.length ? `${range}\n--\n${pathspec.join("\n")}\n` : `${range}\n`);
246
+ const upstream = patchIdCommits(log(`${base}..${ref}`, files));
247
+ const patchEquivalent = (ids) => ({
248
+ merged: true,
249
+ into: name,
250
+ head,
251
+ coversUnpushed: upstreamGone(path, branch),
252
+ mergedAt: committedAt(ids.map((id) => upstream.get(id)))
253
+ });
254
+ const squash = patchIds(patch([
255
+ "diff",
256
+ ...diffOptions,
257
+ base,
258
+ head
259
+ ]));
260
+ if (squash.length === 1 && upstream.has(squash[0])) return patchEquivalent(squash);
261
+ if (!git([
262
+ "rev-list",
263
+ "--merges",
264
+ `${base}..${head}`
265
+ ])) {
266
+ const own = patchIds(log(`${base}..${head}`));
267
+ if (own.length && own.every((id) => upstream.has(id))) return patchEquivalent(own);
268
+ }
269
+ return notMerged(`not merged into ${name}`);
270
+ } catch (error) {
271
+ const timedOut = error?.code === "ETIMEDOUT" ? { timedOut: true } : {};
272
+ return {
273
+ merged: false,
274
+ unknown: true,
275
+ detail: `merge state unknown: ${failure(error)}`,
276
+ ...timedOut
277
+ };
278
+ }
279
+ }
280
+ //#endregion
281
+ //#region src/status-measures.ts
282
+ const LIVE_DISK_MAX_AGE_MS = 3e5;
283
+ const IDLE_DISK_MAX_AGE_MS = 36e5;
284
+ const DU_TIMEOUT_MS = 12e4;
285
+ const PULL_REQUEST_MAX_AGE_MS = 3e5;
286
+ const PULL_REQUEST_CHECK_MS = 6e4;
287
+ const GIT_TIMEOUT_MS = 5e3;
288
+ const AGENT_DISCOVERY_MS = 15e3;
289
+ const AGENT_CACHE_MAX_AGE_MS = 12e4;
290
+ function writeCacheFile(file, entry) {
291
+ const temporary = `${file}.${randomUUID()}.tmp`;
292
+ try {
293
+ mkdirSync(dirname(file), { recursive: true });
294
+ writeFileSync(temporary, JSON.stringify(entry));
295
+ renameSync(temporary, file);
296
+ } catch {
297
+ rmSync(temporary, { force: true });
298
+ }
299
+ }
300
+ function forgetStatusMeasures(worktree, workspaces) {
301
+ const roots = [.../* @__PURE__ */ new Set([worktree, ...workspaces])];
302
+ const files = [pullRequestCacheFile(worktree), ...roots.flatMap((root) => [
303
+ diskUsageCacheFile(root),
304
+ diskUsageCacheFile(join(root, "node_modules")),
305
+ diskUsageCacheFile(workspaceDir(root))
306
+ ])];
307
+ for (const file of files) try {
308
+ rmSync(file, { force: true });
309
+ } catch {}
310
+ }
311
+ function simulatorDataDir(udid) {
312
+ return join(homedir(), "Library", "Developer", "CoreSimulator", "Devices", udid);
313
+ }
314
+ function checkoutDir(state) {
315
+ if (state.worktree?.path) return state.worktree.path;
316
+ const common = gitCommonDirOnDisk(state.path);
317
+ return common && basename(common) === ".git" ? dirname(common) : state.path;
318
+ }
319
+ function deviceFolders(state) {
320
+ const folders = [];
321
+ for (const { ios, android } of [state, ...state.slots ?? []]) {
322
+ if (ios?.owned) folders.push({
323
+ device: ios,
324
+ path: simulatorDataDir(ios.udid)
325
+ });
326
+ if (android?.owned && !android.physical && android.name) {
327
+ const path = ownedAvdDirectory(android.name);
328
+ if (path) folders.push({
329
+ device: android,
330
+ path
331
+ });
332
+ }
333
+ }
334
+ return folders;
335
+ }
336
+ /** The folders an environment's disk use sums: its checkout, its node_modules folders and Stim's workspace folder. */
337
+ function environmentFolders(state) {
338
+ const worktree = checkoutDir(state);
339
+ return {
340
+ worktree,
341
+ nodeModules: [.../* @__PURE__ */ new Set([join(worktree, "node_modules"), join(state.path, "node_modules")])],
342
+ build: workspaceDir(state.path)
343
+ };
344
+ }
345
+ /** The environment's disk use from the cached measurements; null before any folder was measured. */
346
+ function environmentDisk(folders) {
347
+ const worktree = readDiskUsage(folders.worktree);
348
+ const modules = folders.nodeModules.map((path) => readDiskUsage(path));
349
+ const build = readDiskUsage(folders.build);
350
+ const measured = [
351
+ worktree,
352
+ ...modules,
353
+ build
354
+ ].filter((entry) => entry !== null);
355
+ if (!measured.length) return null;
356
+ return {
357
+ worktreeBytes: worktree?.bytes ?? null,
358
+ nodeModulesBytes: modules.every((entry) => entry !== null) ? modules.reduce((sum, entry) => sum + entry.bytes, 0) : null,
359
+ buildBytes: build?.bytes ?? null,
360
+ measuredAt: measured.map((entry) => entry.measuredAt).toSorted()[0]
361
+ };
362
+ }
363
+ function statusPullRequest(fact, checkedAt) {
364
+ return {
365
+ number: fact.number,
366
+ url: fact.url,
367
+ title: fact.title ?? "",
368
+ state: fact.state === "open" && fact.draft ? "draft" : fact.state,
369
+ checks: fact.checks ?? null,
370
+ reviewDecision: fact.reviewDecision ?? null,
371
+ checkedAt
372
+ };
373
+ }
374
+ function canonicalPath(path) {
375
+ try {
376
+ return realpathSync(path);
377
+ } catch {
378
+ return path;
379
+ }
380
+ }
381
+ function agentWorkspaces(states) {
382
+ return states.map((state) => ({
383
+ path: canonicalPath(state.path),
384
+ root: canonicalPath(checkoutDir(state)),
385
+ recorded: readWorkspaceAgent(readWorkspaceState(state.path))
386
+ }));
387
+ }
388
+ function applyAgentSessions(states, now) {
389
+ const cache = readAgentSessionsCache();
390
+ const sessions = cache && now - Date.parse(cache.discoveredAt) <= AGENT_CACHE_MAX_AGE_MS ? cache.sessions : [];
391
+ const discovered = new Set(sessions.map(agentKey));
392
+ const ended = states.map((state) => readEndedAgentSessions(state.path, now));
393
+ const workspaces = agentWorkspaces(states);
394
+ workspaces.forEach((workspace, i) => {
395
+ const { recorded } = workspace;
396
+ if (!recorded || discovered.has(agentKey(recorded))) return;
397
+ if (ended[i].some((session) => agentKey(session) === agentKey(recorded))) workspace.recorded = null;
398
+ });
399
+ attributeAgentSessions(workspaces, sessions, now).forEach((agents, i) => {
400
+ if (agents.length) states[i].agents = agents;
401
+ const running = new Set(agents.map(agentKey));
402
+ const endedAgents = ended[i].filter((session) => !running.has(agentKey(session)));
403
+ if (endedAgents.length) states[i].endedAgents = endedAgents;
404
+ });
405
+ }
406
+ function writeFileAtomic(file, entry) {
407
+ const temporary = `${file}.${randomUUID()}.tmp`;
408
+ try {
409
+ writeFileSync(temporary, JSON.stringify(entry));
410
+ renameSync(temporary, file);
411
+ } catch (error) {
412
+ rmSync(temporary, { force: true });
413
+ throw error;
414
+ }
415
+ }
416
+ function recordEndedAgents(states, gone, endedAt, now) {
417
+ const goneKeys = new Set(gone.map(agentKey));
418
+ const result = {
419
+ changed: false,
420
+ failed: false
421
+ };
422
+ attributeAgentSessions(agentWorkspaces(states), gone, now).forEach((sessions, i) => {
423
+ const root = states[i].path;
424
+ const ended = sessions.filter((session) => goneKeys.has(agentKey(session)));
425
+ if (!ended.length || !existsSync(workspaceDir(root))) return;
426
+ try {
427
+ withDirLock(workspaceEndedAgentsLock(root), () => {
428
+ const kept = new Map(readEndedAgentSessions(root, now).map((session) => [agentKey(session), session]));
429
+ for (const { pid: _pid, ...session } of ended) kept.set(agentKey(session), {
430
+ ...session,
431
+ endedAt
432
+ });
433
+ writeFileAtomic(workspaceEndedAgentsFile(root), { sessions: [...kept.values()] });
434
+ });
435
+ result.changed = true;
436
+ } catch {
437
+ result.failed = true;
438
+ }
439
+ });
440
+ return result;
441
+ }
442
+ /**
443
+ * Adds the cached measurements to a status read: each environment's `disk`, `agents` and `endedAgents`, each owned
444
+ * device's `disk`, and each linked worktree's `pullRequest` when the cache holds a lookup for its current branch.
445
+ * Reads files only.
446
+ */
447
+ function applyStatusMeasures(states, worktrees) {
448
+ applyAgentSessions(states, Date.now());
449
+ for (const state of states) {
450
+ const disk = environmentDisk(environmentFolders(state));
451
+ if (disk) state.disk = disk;
452
+ for (const { device, path } of deviceFolders(state)) {
453
+ const measure = readDiskUsage(path);
454
+ if (measure) device.disk = measure;
455
+ }
456
+ }
457
+ for (const worktree of worktrees) {
458
+ const cached = worktree.branch ? readPullRequestCache(worktree.path) : null;
459
+ if (cached && cached.branch === worktree.branch) worktree.pullRequest = cached.pullRequest;
460
+ }
461
+ }
462
+ /** Kilobytes from `du -sk` output, or null when it printed none. */
463
+ function parseDuKilobytes(output) {
464
+ const kb = Number(/^(\d+)\s/.exec(output)?.[1]);
465
+ return Number.isFinite(kb) ? kb : null;
466
+ }
467
+ function isStale(measure, maxAgeMs, now) {
468
+ return !measure || now - Date.parse(measure.measuredAt) >= maxAgeMs;
469
+ }
470
+ /**
471
+ * Keeps the disk use, pull request and agent session caches fresh for `status --watch`, off its refresh path: one
472
+ * agent session discovery at most every 15 seconds, which status ignores once it is over 2 minutes old, one `du -sk` at
473
+ * 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
474
+ * graphql` per repository for worktrees whose lookup is over 5 minutes old or whose branch or HEAD moved. It
475
+ * runs git only in the repository, never in a worktree, and only for worktrees whose git state status read. It rechecks
476
+ * a folder's cache right before measuring, so two watchers rarely measure the same folder. A session the previous
477
+ * discovery found and this one does not is recorded as ended in the workspaces it worked in. `updated` runs after each
478
+ * write.
479
+ */
480
+ function createStatusMeasurer({ updated, now = Date.now }) {
481
+ const exec = getExecutor();
482
+ let measuring = false;
483
+ let duMissing = false;
484
+ let checking = false;
485
+ let checkedAt = -Infinity;
486
+ let discovering = false;
487
+ let discoveredAt = -Infinity;
488
+ const failedAt = /* @__PURE__ */ new Map();
489
+ async function du(path) {
490
+ try {
491
+ return parseDuKilobytes(await exec.runFileAsync("du", ["-sk", path], { timeoutMs: DU_TIMEOUT_MS }));
492
+ } catch (error) {
493
+ const { code, status, stdout } = error;
494
+ if (code === "ENOENT") duMissing = true;
495
+ return status === 1 && typeof stdout === "string" ? parseDuKilobytes(stdout) : null;
496
+ }
497
+ }
498
+ async function measure(folders) {
499
+ for (const { path, maxAgeMs } of folders) {
500
+ if (duMissing || !isStale(readDiskUsage(path), maxAgeMs, now())) continue;
501
+ if (now() - (failedAt.get(path) ?? -Infinity) < maxAgeMs) continue;
502
+ let bytes = 0;
503
+ if (existsSync(path)) {
504
+ const kb = await du(path);
505
+ if (kb === null) {
506
+ failedAt.set(path, now());
507
+ continue;
508
+ }
509
+ bytes = kb * 1024;
510
+ } else if (!basename(path).startsWith("node_modules")) continue;
511
+ writeCacheFile(diskUsageCacheFile(path), {
512
+ path,
513
+ bytes,
514
+ measuredAt: new Date(now()).toISOString()
515
+ });
516
+ updated();
517
+ }
518
+ }
519
+ async function branchHead(repository, branch) {
520
+ try {
521
+ return (await exec.runFileAsync("git", [
522
+ "-C",
523
+ repository,
524
+ "rev-parse",
525
+ "--verify",
526
+ `refs/heads/${branch}`
527
+ ], { timeoutMs: GIT_TIMEOUT_MS })).trim() || null;
528
+ } catch {
529
+ return null;
530
+ }
531
+ }
532
+ async function checkPullRequests(worktrees) {
533
+ const lookup = pullRequestLookups({ detail: true });
534
+ const byRepository = /* @__PURE__ */ new Map();
535
+ for (const { path, branch, repository, git } of worktrees) {
536
+ if (!branch || !repository || !git) continue;
537
+ const head = await branchHead(repository, branch);
538
+ if (!head) continue;
539
+ const cached = readPullRequestCache(path);
540
+ if (!(cached?.branch === branch && cached.head === head && now() - Date.parse(cached.checkedAt) < PULL_REQUEST_MAX_AGE_MS)) byRepository.set(repository, [...byRepository.get(repository) ?? [], {
541
+ path,
542
+ cwd: repository,
543
+ branch,
544
+ head
545
+ }]);
546
+ }
547
+ for (const [repository, queries] of byRepository) {
548
+ const results = await lookup(repository, queries);
549
+ const at = new Date(now()).toISOString();
550
+ results.forEach((result, i) => {
551
+ if (!("pullRequest" in result)) return;
552
+ const { path, branch, head } = queries[i];
553
+ const entry = {
554
+ path,
555
+ branch,
556
+ head,
557
+ checkedAt: at,
558
+ pullRequest: result.pullRequest ? statusPullRequest(result.pullRequest, at) : null
559
+ };
560
+ writeCacheFile(pullRequestCacheFile(path), entry);
561
+ });
562
+ if (results.some((result) => "pullRequest" in result)) updated();
563
+ }
564
+ }
565
+ async function discoverAgents(states) {
566
+ const at = now();
567
+ const sessions = await discoverAgentSessions({ now: at });
568
+ const previous = readAgentSessionsCache();
569
+ const running = new Set(sessions.map(agentKey));
570
+ const lastFound = previous ? Date.parse(previous.discoveredAt) : -Infinity;
571
+ const gone = at - lastFound <= ENDED_AGENT_RETENTION_MS ? previous.sessions.filter((session) => !running.has(agentKey(session))) : [];
572
+ const ended = gone.length ? recordEndedAgents(states, gone, previous.discoveredAt, at) : {
573
+ changed: false,
574
+ failed: false
575
+ };
576
+ if (ended.failed && at - lastFound <= AGENT_CACHE_MAX_AGE_MS) return;
577
+ writeCacheFile(agentSessionsCacheFile(), {
578
+ discoveredAt: new Date(at).toISOString(),
579
+ sessions
580
+ });
581
+ const shown = previous && at - Date.parse(previous.discoveredAt) <= AGENT_CACHE_MAX_AGE_MS;
582
+ if (ended.changed || !shown || JSON.stringify(previous.sessions) !== JSON.stringify(sessions)) updated();
583
+ }
584
+ return { schedule(states, worktrees) {
585
+ if (!discovering && now() - discoveredAt >= AGENT_DISCOVERY_MS) {
586
+ discovering = true;
587
+ discoveredAt = now();
588
+ discoverAgents(states).catch(() => {}).finally(() => {
589
+ discovering = false;
590
+ });
591
+ }
592
+ if (!measuring) {
593
+ const folders = /* @__PURE__ */ new Map();
594
+ for (const state of states.toSorted((a, b) => Number(b.live) - Number(a.live))) {
595
+ if (!existsSync(state.path)) continue;
596
+ const maxAgeMs = state.live ? LIVE_DISK_MAX_AGE_MS : IDLE_DISK_MAX_AGE_MS;
597
+ const { worktree, nodeModules, build } = environmentFolders(state);
598
+ for (const path of [
599
+ worktree,
600
+ ...nodeModules,
601
+ build,
602
+ ...deviceFolders(state).map((folder) => folder.path)
603
+ ]) folders.set(path, Math.min(folders.get(path) ?? Infinity, maxAgeMs));
604
+ }
605
+ measuring = true;
606
+ measure([...folders].map(([path, maxAgeMs]) => ({
607
+ path,
608
+ maxAgeMs
609
+ }))).catch(() => {}).finally(() => {
610
+ measuring = false;
611
+ });
612
+ }
613
+ if (!checking && now() - checkedAt >= PULL_REQUEST_CHECK_MS) {
614
+ checking = true;
615
+ checkedAt = now();
616
+ checkPullRequests(worktrees).catch(() => {}).finally(() => {
617
+ checking = false;
618
+ });
619
+ }
620
+ } };
621
+ }
622
+ //#endregion
623
+ //#region src/engine/warm-progress.ts
624
+ const NO_WARM_PROGRESS = {
625
+ step: () => {},
626
+ finish: () => {}
627
+ };
628
+ function warmProgressClaimRoot(root) {
629
+ return join(workspaceDir(root), "warm.lock");
630
+ }
631
+ /**
632
+ * Records that `stim worktree warm` runs in the workspace at `root`, under an ownership claim this process holds
633
+ * until `finish`, so a warm that was killed reads as over. A process that cannot take the claim records nothing.
634
+ */
635
+ function startWarmProgress(root, step, { note, now = Date.now }) {
636
+ let claim;
637
+ try {
638
+ ensureWorkspaceStorage(root);
639
+ const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
640
+ root: warmProgressClaimRoot(root),
641
+ mode: "shared",
642
+ label: "worktree warm"
643
+ });
644
+ if (!attempt.acquired) throw new Error("another process holds its claim exclusively");
645
+ claim = attempt.acquired;
646
+ } catch (error) {
647
+ note(`Warm progress is not recorded for status: ${error?.message || error}`);
648
+ return NO_WARM_PROGRESS;
649
+ }
650
+ const startedAt = new Date(now()).toISOString();
651
+ const ours = (state) => {
652
+ const record = readWarmRecord(state);
653
+ return record?.phase === "warming" && record.claim.claimId === claim.claimId;
654
+ };
655
+ let warned = false;
656
+ const write = (update) => {
657
+ try {
658
+ updateWorkspaceState(root, update);
659
+ } catch (error) {
660
+ if (warned) return;
661
+ warned = true;
662
+ note(`Warm progress could not be recorded in the workspace state: ${error?.message || error}`);
663
+ }
664
+ };
665
+ const warming = (current) => ({
666
+ phase: "warming",
667
+ step: current,
668
+ startedAt,
669
+ claim: {
670
+ root: claim.root,
671
+ claimId: claim.claimId
672
+ }
673
+ });
674
+ write((state) => ({
675
+ ...state,
676
+ [WARM_KEY]: warming(step)
677
+ }));
678
+ return {
679
+ step(next) {
680
+ write((state) => ours(state) ? {
681
+ ...state,
682
+ [WARM_KEY]: warming(next)
683
+ } : state);
684
+ },
685
+ finish(succeeded) {
686
+ write((state) => {
687
+ if (!ours(state)) return state;
688
+ const { [WARM_KEY]: _, ...rest } = state;
689
+ return succeeded ? {
690
+ ...rest,
691
+ [WARM_KEY]: {
692
+ phase: "ready",
693
+ at: new Date(now()).toISOString()
694
+ }
695
+ } : rest;
696
+ });
697
+ (0, ownership_claim_exports.releaseClaim)(claim);
698
+ }
699
+ };
700
+ }
701
+ /**
702
+ * The lifecycle phase status reports. A warming record counts only while a warm holds the workspace's claim, and a ready
703
+ * record only until the workspace is next used or `READY_PHASE_MS` passes.
704
+ */
705
+ function workspacePhase(live, state, { now = Date.now(), survey = ownership_claim_exports.readClaimSet } = {}) {
706
+ if (live) return {
707
+ phase: "live",
708
+ phaseSince: null
709
+ };
710
+ const record = readWarmRecord(state);
711
+ if (record?.phase === "warming") {
712
+ if (survey(record.claim.root).live.length) return {
713
+ phase: "warming",
714
+ phaseSince: record.startedAt,
715
+ warmStep: record.step
716
+ };
717
+ }
718
+ if (record?.phase === "ready") {
719
+ const at = Date.parse(record.at);
720
+ const used = Date.parse(String(state?.lastUsedAt ?? ""));
721
+ if (now - at < READY_PHASE_MS && !(used > at)) return {
722
+ phase: "ready",
723
+ phaseSince: record.at
724
+ };
725
+ }
726
+ return {
727
+ phase: "idle",
728
+ phaseSince: null
729
+ };
730
+ }
731
+ //#endregion
732
+ export { forgetStatusMeasures as a, createStatusMeasurer as i, workspacePhase as n, fetchDefaultBranch as o, applyStatusMeasures as r, mergeState as s, startWarmProgress as t };