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,213 @@
1
+ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
+ import { a as updateWorkspaceState, r as readWorkspaceState } from "./workspace-state-CL2V2Ons.mjs";
3
+ import { r as detectIsExpo } from "./project-CV3tDFHl.mjs";
4
+ import { a as spawnDeclared } from "./spawn-claims-D0XfSe8u.mjs";
5
+ import { n as stripAnsi, r as waitForChild, t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
6
+ import { i as expoBinRefusal, r as expoBinPath } from "./server-expo-DhtX6bom.mjs";
7
+ import { existsSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ //#region src/engine/prebuild.ts
10
+ const PREBUILD_ERROR = "STIM_PREBUILD_FAILED";
11
+ const LAST_LINES = 20;
12
+ function nativeDirName(platform) {
13
+ return platform === "android" ? "android" : "ios";
14
+ }
15
+ function nativeDir(root, platform) {
16
+ return join(root, nativeDirName(platform));
17
+ }
18
+ function nativeDirInFingerprint(sources, platform) {
19
+ const dir = nativeDirName(platform);
20
+ return sources.some((source) => source.type === "dir" && source.filePath === dir && source.hash != null);
21
+ }
22
+ /**
23
+ * @expo/fingerprint leaves a native directory out of the hash when git ignores it (CNG), so the cache
24
+ * key only covers that directory when this workspace's recorded prebuild ran on the same fingerprint.
25
+ */
26
+ function prebuildAction({ isExpo, nativeDirExists, nativeDirFingerprinted, nativeDirTracked, generatedFrom, fingerprint }) {
27
+ if (!isExpo) return "none";
28
+ if (!nativeDirExists) return "generate";
29
+ if (nativeDirFingerprinted || generatedFrom === fingerprint) return "none";
30
+ return nativeDirTracked ? "refuse" : "regenerate";
31
+ }
32
+ function prebuildRecord(state) {
33
+ const record = state?.prebuild;
34
+ return record && typeof record === "object" ? record : {};
35
+ }
36
+ function gitTracksNativeDir(root, platform) {
37
+ try {
38
+ return getExecutor().runFile("git", [
39
+ "-C",
40
+ root,
41
+ "ls-files",
42
+ "--",
43
+ nativeDirName(platform)
44
+ ], { env: {
45
+ LC_ALL: "C",
46
+ LANGUAGE: "C"
47
+ } }).trim() !== "";
48
+ } catch (error) {
49
+ if (error?.code === "ENOENT") return false;
50
+ return !/not a git repository \(or any (of the parent directories|parent up to mount point)/.test(String(error?.message));
51
+ }
52
+ }
53
+ function planPrebuild(root, platform, { isExpo, fingerprint, sources }) {
54
+ const nativeDirExists = existsSync(nativeDir(root, platform));
55
+ const fingerprinted = nativeDirInFingerprint(sources, platform);
56
+ const generatedFrom = prebuildRecord(readWorkspaceState(root))[platform];
57
+ return prebuildAction({
58
+ isExpo,
59
+ nativeDirExists,
60
+ nativeDirFingerprinted: fingerprinted,
61
+ nativeDirTracked: nativeDirExists && !fingerprinted && gitTracksNativeDir(root, platform),
62
+ generatedFrom: typeof generatedFrom === "string" ? generatedFrom : null,
63
+ fingerprint
64
+ });
65
+ }
66
+ function recordPrebuild(root, platform, fingerprint) {
67
+ updateWorkspaceState(root, (state) => ({
68
+ ...state,
69
+ prebuild: {
70
+ ...prebuildRecord(state),
71
+ [platform]: fingerprint
72
+ }
73
+ }));
74
+ }
75
+ function staleNativeDirRefusal(platform) {
76
+ const dir = nativeDirName(platform);
77
+ return {
78
+ code: PREBUILD_ERROR,
79
+ message: `The fingerprint leaves ${dir}/ out of the cache key, and Stim cannot show that ${dir}/ was generated from the current app config. Git tracks files in it, or could not say whether it does, so Stim will not regenerate it with \`expo prebuild --clean\`.`,
80
+ remedy: `Stop excluding ${dir}/ from the fingerprint (check .fingerprintignore) so the cache key covers its files, or add ${dir}/ to .gitignore and untrack it so Stim regenerates it from the app config.`
81
+ };
82
+ }
83
+ function prebuildRefusal({ isExpo, platform, nativeDirExists }) {
84
+ if (nativeDirExists || isExpo) return null;
85
+ const dir = nativeDirName(platform);
86
+ return {
87
+ code: PREBUILD_ERROR,
88
+ message: `This project has no ${dir}/ directory and is not an Expo (CNG) project, so Stim cannot generate one.`,
89
+ remedy: `Check out or generate the native project (\`npx @react-native-community/cli init\` produced one originally), or add \`expo\` to the project so \`expo prebuild\` can create ${dir}/.`
90
+ };
91
+ }
92
+ function prebuildFailure(logWriter, result) {
93
+ logWriter?.write?.({
94
+ src: "build",
95
+ level: "error",
96
+ msg: result.reason || "Expo prebuild failed.",
97
+ event: "prebuild_failed"
98
+ });
99
+ return result;
100
+ }
101
+ async function runPrebuild(root, platform, logWriter, { spawnFn = null, now = Date.now, isExpo = null, clean = false } = {}) {
102
+ const dirExists = existsSync(nativeDir(root, platform));
103
+ const refusal = prebuildRefusal({
104
+ isExpo: isExpo === null ? detectIsExpo(root) : isExpo,
105
+ platform,
106
+ nativeDirExists: dirExists
107
+ });
108
+ if (refusal) return prebuildFailure(logWriter, {
109
+ failed: true,
110
+ code: refusal.code,
111
+ reason: refusal.message,
112
+ remedy: refusal.remedy,
113
+ error: refusal,
114
+ lastLines: []
115
+ });
116
+ const bin = expoBinPath(root);
117
+ if (!bin) {
118
+ const binRefusal = expoBinRefusal(root, "prebuild");
119
+ return prebuildFailure(logWriter, {
120
+ failed: true,
121
+ code: PREBUILD_ERROR,
122
+ reason: binRefusal.message,
123
+ remedy: binRefusal.remedy,
124
+ lastLines: []
125
+ });
126
+ }
127
+ const spawn = spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts));
128
+ const startedAt = now();
129
+ const tail = [];
130
+ const push = (line) => {
131
+ const msg = stripAnsi(String(line)).trimEnd();
132
+ if (!msg.trim()) return;
133
+ tail.push(msg);
134
+ if (tail.length > LAST_LINES) tail.shift();
135
+ logWriter?.write?.({
136
+ src: "build",
137
+ level: "debug",
138
+ msg,
139
+ raw: true,
140
+ event: "prebuild"
141
+ });
142
+ };
143
+ let child;
144
+ try {
145
+ child = spawnDeclared(() => spawn(bin, [
146
+ "prebuild",
147
+ "-p",
148
+ platform,
149
+ "--no-install",
150
+ ...clean ? ["--clean"] : []
151
+ ], {
152
+ cwd: root,
153
+ stdio: [
154
+ "ignore",
155
+ "pipe",
156
+ "pipe"
157
+ ],
158
+ env: {
159
+ ...process.env,
160
+ FORCE_COLOR: "0",
161
+ CI: "1"
162
+ }
163
+ }));
164
+ } catch (err) {
165
+ return prebuildFailure(logWriter, {
166
+ failed: true,
167
+ code: PREBUILD_ERROR,
168
+ reason: `Could not run \`expo prebuild\`: ${err?.message || err}`,
169
+ lastLines: []
170
+ });
171
+ }
172
+ const outReader = createLineReader(push);
173
+ const errReader = createLineReader(push);
174
+ child.stdout?.setEncoding?.("utf-8");
175
+ child.stderr?.setEncoding?.("utf-8");
176
+ child.stdout?.on("data", (chunk) => outReader.push(chunk));
177
+ child.stderr?.on("data", (chunk) => errReader.push(chunk));
178
+ const result = await waitForChild(child);
179
+ outReader.flush();
180
+ errReader.flush();
181
+ const durationMs = now() - startedAt;
182
+ if (result.error) return prebuildFailure(logWriter, {
183
+ failed: true,
184
+ code: PREBUILD_ERROR,
185
+ reason: `Could not run \`expo prebuild\`: ${result.error?.message || result.error}`,
186
+ lastLines: tail.slice(),
187
+ durationMs
188
+ });
189
+ if (result.code !== 0) {
190
+ const how = result.signal ? `signal ${result.signal}` : `exit code ${result.code}`;
191
+ return prebuildFailure(logWriter, {
192
+ failed: true,
193
+ code: PREBUILD_ERROR,
194
+ reason: `\`expo prebuild -p ${platform}\` failed (${how}).`,
195
+ lastLines: tail.slice(),
196
+ durationMs
197
+ });
198
+ }
199
+ if (!existsSync(nativeDir(root, platform))) return prebuildFailure(logWriter, {
200
+ failed: true,
201
+ code: PREBUILD_ERROR,
202
+ reason: `\`expo prebuild -p ${platform}\` succeeded but did not create ${nativeDirName(platform)}/.`,
203
+ lastLines: tail.slice(),
204
+ durationMs
205
+ });
206
+ return {
207
+ ok: true,
208
+ durationMs,
209
+ nativeDir: nativeDir(root, platform)
210
+ };
211
+ }
212
+ //#endregion
213
+ export { staleNativeDirRefusal as i, recordPrebuild as n, runPrebuild as r, planPrebuild as t };
@@ -0,0 +1,12 @@
1
+ import { n as __exportAll, r as __reExport } from "./rolldown-runtime-BsOwBuO_.mjs";
2
+ //#region src/ownership-claim.ts
3
+ var ownership_claim_exports = /* @__PURE__ */ __exportAll({});
4
+ import * as import__stim_cli_core_ownership_claim from "@stim-cli/core/ownership-claim";
5
+ __reExport(ownership_claim_exports, import__stim_cli_core_ownership_claim);
6
+ //#endregion
7
+ //#region src/process-identity.ts
8
+ var process_identity_exports = /* @__PURE__ */ __exportAll({});
9
+ import * as import__stim_cli_core_process_identity from "@stim-cli/core/process-identity";
10
+ __reExport(process_identity_exports, import__stim_cli_core_process_identity);
11
+ //#endregion
12
+ export { ownership_claim_exports as n, process_identity_exports as t };
@@ -1,7 +1,8 @@
1
- import { _ as loadConfig, g as isPathPrefix, l as findEnclosingWorktreeRoot, p as getProject } from "./config-0jCcMM0E.mjs";
2
- import { y as repoRoot } from "./worktree-DeBQSVtR.mjs";
1
+ import { _ as loadConfig$1, g as isPathPrefix, l as findEnclosingWorktreeRoot, p as getProject } from "./config-D0DbpZtM.mjs";
2
+ import { y as repoRoot } from "./worktree-C0c5Udwa.mjs";
3
3
  import { existsSync, readFileSync, realpathSync } from "fs";
4
4
  import { basename, dirname, join, resolve } from "path";
5
+ import { findProjectRoot, findProjectRoot as findProjectRoot$1 } from "@stim-cli/core/state";
5
6
  //#region src/workspace/project.ts
6
7
  /**
7
8
  * The realpath of `name`'s package.json in the nearest `node_modules` at or above `projectRoot`.
@@ -37,23 +38,13 @@ const NO_PROJECT_REFUSAL = {
37
38
  message: "Not in a React Native project (no package.json found).",
38
39
  remedy: "Run this from the app directory -- the one holding package.json."
39
40
  };
40
- function findProjectRoot(startDir) {
41
- let dir;
42
- try {
43
- dir = realpathSync(resolve(startDir));
44
- } catch {
45
- dir = resolve(startDir);
46
- }
47
- while (true) {
48
- if (existsSync(join(dir, "package.json"))) return dir;
49
- const parent = dirname(dir);
50
- if (parent === dir) return null;
51
- dir = parent;
52
- }
53
- }
54
41
  function findServerWorkspace(startDir) {
55
42
  const nearest = findProjectRoot(startDir);
56
43
  if (!nearest) return null;
44
+ if (existsSync(join(nearest, "Package.swift"))) return {
45
+ root: nearest,
46
+ from: null
47
+ };
57
48
  if (appProjectProblem(nearest)?.kind !== "not-an-app") return {
58
49
  root: nearest,
59
50
  from: null
@@ -68,7 +59,7 @@ function findServerWorkspace(startDir) {
68
59
  from: null
69
60
  };
70
61
  const worktree = expandedRealpath(top);
71
- const apps = Object.keys(loadConfig()?.projects ?? {}).filter((path) => {
62
+ const apps = Object.keys(loadConfig$1()?.projects ?? {}).filter((path) => {
72
63
  if (path === nearest || !isPathPrefix(worktree, expandedRealpath(path)) || appProjectProblem(path) !== null) return false;
73
64
  const owner = repoRoot(path);
74
65
  return owner !== null && expandedRealpath(owner) === worktree;
@@ -213,4 +204,4 @@ function readAppConfigText(projectRoot) {
213
204
  return null;
214
205
  }
215
206
  //#endregion
216
- export { findProjectRoot as a, ownedDeviceLabel as c, readAppJson as d, resolvePackageJson as f, findCommandWorkspace as i, projectShortcut as l, appProjectProblem as n, findServerWorkspace as o, detectIsExpo as r, isPackageResolvable as s, NO_PROJECT_REFUSAL as t, readAppConfigText as u };
207
+ export { findProjectRoot$1 as a, ownedDeviceLabel as c, readAppJson as d, resolvePackageJson as f, findCommandWorkspace as i, projectShortcut as l, appProjectProblem as n, findServerWorkspace as o, detectIsExpo as r, isPackageResolvable as s, NO_PROJECT_REFUSAL as t, readAppConfigText as u };
@@ -0,0 +1,322 @@
1
+ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
+ //#region src/workspace/pull-request.ts
3
+ const GH_TIMEOUT_MS = 2e4;
4
+ const GH_ENV = {
5
+ GH_PROMPT_DISABLED: "1",
6
+ GH_NO_UPDATE_NOTIFIER: "1",
7
+ NO_COLOR: "1"
8
+ };
9
+ const GH_SCALAR_FIELDS = "number,state,url,headRefOid,mergedAt,closedAt,isCrossRepository,isDraft";
10
+ const GH_FIELDS = `${GH_SCALAR_FIELDS},headRepository,headRepositoryOwner`;
11
+ const GH_GRAPHQL_REPO_SELECTION = "headRepository { name } headRepositoryOwner { login }";
12
+ const GH_DETAIL_FIELDS = "title reviewDecision commits(last: 1) { nodes { commit { statusCheckRollup { contexts(first: 0) { checkRunCountsByState { state count } statusContextCountsByState { state count } } } } } }";
13
+ const SIGNED_OUT_EXIT = 4;
14
+ const PASSING = /* @__PURE__ */ new Set([
15
+ "SUCCESS",
16
+ "NEUTRAL",
17
+ "SKIPPED",
18
+ "COMPLETED"
19
+ ]);
20
+ const FAILING = /* @__PURE__ */ new Set([
21
+ "FAILURE",
22
+ "ERROR",
23
+ "TIMED_OUT",
24
+ "CANCELLED",
25
+ "ACTION_REQUIRED",
26
+ "STARTUP_FAILURE",
27
+ "STALE"
28
+ ]);
29
+ const PENDING = /* @__PURE__ */ new Set([
30
+ "QUEUED",
31
+ "IN_PROGRESS",
32
+ "WAITING",
33
+ "PENDING",
34
+ "REQUESTED",
35
+ "EXPECTED"
36
+ ]);
37
+ /** Sums a head commit's check runs and commit statuses by outcome; null when the commit has no checks. */
38
+ function checkCounts(rollup) {
39
+ if (!rollup) return null;
40
+ const counts = {
41
+ passing: 0,
42
+ failing: 0,
43
+ pending: 0
44
+ };
45
+ const contexts = rollup.contexts;
46
+ for (const { state, count } of [...contexts?.checkRunCountsByState ?? [], ...contexts?.statusContextCountsByState ?? []]) {
47
+ if (!Number.isInteger(count) || count <= 0) continue;
48
+ if (PASSING.has(state)) counts.passing += count;
49
+ else if (FAILING.has(state)) counts.failing += count;
50
+ else if (PENDING.has(state)) counts.pending += count;
51
+ }
52
+ return counts;
53
+ }
54
+ function reviewDecisionOf(value) {
55
+ if (value === "APPROVED") return "approved";
56
+ if (value === "CHANGES_REQUESTED") return "changes-requested";
57
+ if (value === "REVIEW_REQUIRED") return "review-required";
58
+ return null;
59
+ }
60
+ function detailOf(pull) {
61
+ if (typeof pull.title !== "string") return {};
62
+ return {
63
+ title: pull.title,
64
+ reviewDecision: reviewDecisionOf(pull.reviewDecision),
65
+ checks: checkCounts(pull.commits?.nodes?.[0]?.commit?.statusCheckRollup)
66
+ };
67
+ }
68
+ function stateOf(state) {
69
+ if (state === "OPEN") return "open";
70
+ if (state === "MERGED") return "merged";
71
+ if (state === "CLOSED") return "closed";
72
+ return null;
73
+ }
74
+ function epoch(text) {
75
+ const at = text ? Date.parse(text) : NaN;
76
+ return Number.isFinite(at) ? at : null;
77
+ }
78
+ function headRepoOf(pull) {
79
+ const owner = pull.headRepositoryOwner?.login;
80
+ const name = pull.headRepository?.name;
81
+ return owner && name ? `${owner.toLowerCase()}/${name.toLowerCase()}` : null;
82
+ }
83
+ /**
84
+ * Picks the pull request that describes `head` among those `gh pr list --head <branch>` returned. One whose head is
85
+ * `head` wins, then one whose head contains `head`, then one whose head `head` contains. An open one wins a tier,
86
+ * then the newest. A pull request unrelated to `head` is from an earlier use of the branch name and is ignored. A
87
+ * cross-repository pull request is kept only when its head repository is one of `ownRepos` (this checkout's own git
88
+ * remotes, lowercased "owner/name"); otherwise it only shares the branch name with a stranger's fork and is ignored.
89
+ * `isAncestor(a, b)` answers whether commit `a` is an ancestor of commit `b`, and false when git cannot tell.
90
+ */
91
+ function selectPullRequest(pulls, head, isAncestor, ownRepos = /* @__PURE__ */ new Set()) {
92
+ const known = pulls.flatMap((pull) => {
93
+ if (!pull || typeof pull !== "object") return [];
94
+ const state = stateOf(pull.state);
95
+ if (!state || !pull.headRefOid) return [];
96
+ if (pull.isCrossRepository) {
97
+ const repo = headRepoOf(pull);
98
+ if (!repo || !ownRepos.has(repo)) return [];
99
+ }
100
+ return [{
101
+ pull,
102
+ state
103
+ }];
104
+ });
105
+ const tiers = [
106
+ known.filter(({ pull }) => pull.headRefOid === head),
107
+ known.filter(({ pull }) => pull.headRefOid !== head && isAncestor(head, pull.headRefOid)),
108
+ known.filter(({ pull }) => pull.headRefOid !== head && isAncestor(pull.headRefOid, head))
109
+ ];
110
+ const tier = tiers.findIndex((entries) => entries.length > 0);
111
+ if (tier < 0) return null;
112
+ const { pull, state } = tiers[tier].toSorted((a, b) => Number(b.state === "open") - Number(a.state === "open") || b.pull.number - a.pull.number)[0];
113
+ return {
114
+ number: pull.number,
115
+ state,
116
+ draft: pull.isDraft === true,
117
+ url: pull.url,
118
+ head: pull.headRefOid,
119
+ containsHead: tier < 2,
120
+ endedAt: state === "merged" ? epoch(pull.mergedAt) : state === "closed" ? epoch(pull.closedAt) : null,
121
+ ...detailOf(pull)
122
+ };
123
+ }
124
+ function firstLine(error) {
125
+ const { stderr, message } = error;
126
+ const text = String(stderr ?? "").trim() || String(message ?? error);
127
+ return text.split("\n")[0] ?? text;
128
+ }
129
+ function ghFailure(error, command) {
130
+ if (error.status === SIGNED_OUT_EXIT) return {
131
+ unavailable: "gh is not signed in; run `gh auth login`",
132
+ sticky: true
133
+ };
134
+ if (error.code === "ETIMEDOUT") return {
135
+ unavailable: `${command} did not answer within ${GH_TIMEOUT_MS / 1e3}s`,
136
+ sticky: true
137
+ };
138
+ return {
139
+ unavailable: `${command} failed: ${firstLine(error)}`,
140
+ sticky: false
141
+ };
142
+ }
143
+ function ancestry(cwd) {
144
+ const exec = getExecutor();
145
+ return (ancestor, descendant) => exec.runFileQuiet("git", [
146
+ "-C",
147
+ cwd,
148
+ "merge-base",
149
+ "--is-ancestor",
150
+ ancestor,
151
+ descendant
152
+ ]) !== null;
153
+ }
154
+ const REMOTE_URL_RE = /^(?:https?:\/\/(?:[^/@]+@)?[^/]+\/|ssh:\/\/(?:[^/@]+@)?[^/]+\/|(?:[^/@:]+@)?[^/:]+:)([^/]+)\/([^/]+?)(?:\.git)?\/?$/i;
155
+ /**
156
+ * "owner/name", lowercased, parsed out of a git remote URL, or null when it doesn't match: https, `ssh://`, or
157
+ * scp-like (`[user@]host:owner/name`, which also covers an `~/.ssh/config` host alias in place of `user@host`).
158
+ */
159
+ function parseRemoteRepo(url) {
160
+ const [, owner, name] = REMOTE_URL_RE.exec(url.trim()) ?? [];
161
+ return owner && name ? `${owner.toLowerCase()}/${name.toLowerCase()}` : null;
162
+ }
163
+ function localRemoteRepos(cwd) {
164
+ const out = getExecutor().runFileQuiet("git", [
165
+ "-C",
166
+ cwd,
167
+ "remote",
168
+ "-v"
169
+ ]);
170
+ const repos = /* @__PURE__ */ new Set();
171
+ for (const line of out ? out.split("\n") : []) {
172
+ const url = /^\S+\s+(\S+)\s+\((?:fetch|push)\)$/.exec(line)?.[1];
173
+ const repo = url ? parseRemoteRepo(url) : null;
174
+ if (repo) repos.add(repo);
175
+ }
176
+ return repos;
177
+ }
178
+ /**
179
+ * Asks GitHub, through `gh`, which pull requests have `branch` as their head, run from `cwd` so `gh` picks the
180
+ * repository from its remotes. Each call is one fixed `gh pr list` invocation. Once `gh` is missing, signed out or
181
+ * times out, every later lookup through the same function answers `unavailable` without running it again.
182
+ */
183
+ function pullRequestLookup() {
184
+ let unavailable;
185
+ return (cwd, branch, head) => {
186
+ const exec = getExecutor();
187
+ if (unavailable === void 0) unavailable = exec.findExecutable("gh") ? null : "gh is not installed";
188
+ if (unavailable) return { unavailable };
189
+ let out;
190
+ try {
191
+ out = exec.runFile("gh", [
192
+ "pr",
193
+ "list",
194
+ "--head",
195
+ branch,
196
+ "--state",
197
+ "all",
198
+ "--limit",
199
+ "20",
200
+ "--json",
201
+ GH_FIELDS
202
+ ], {
203
+ cwd,
204
+ timeoutMs: GH_TIMEOUT_MS,
205
+ env: GH_ENV
206
+ });
207
+ } catch (error) {
208
+ const failure = ghFailure(error, "gh pr list");
209
+ if (failure.sticky) unavailable = failure.unavailable;
210
+ return { unavailable: failure.unavailable };
211
+ }
212
+ let pulls;
213
+ try {
214
+ pulls = JSON.parse(out);
215
+ } catch {
216
+ return { unavailable: "gh pr list printed no JSON" };
217
+ }
218
+ return { pullRequest: Array.isArray(pulls) ? selectPullRequest(pulls, head, ancestry(cwd), localRemoteRepos(cwd)) : null };
219
+ };
220
+ }
221
+ function branchesQuery(count, detail) {
222
+ const variables = Array.from({ length: count }, (_, i) => `, $b${i}: String!`).join("");
223
+ const nodes = `${GH_SCALAR_FIELDS.replaceAll(",", " ")} ${GH_GRAPHQL_REPO_SELECTION}` + (detail ? ` ${GH_DETAIL_FIELDS}` : "");
224
+ return `query($owner: String!, $repo: String!${variables}) { repository(owner: $owner, name: $repo) {${Array.from({ length: count }, (_, i) => ` b${i}: pullRequests(headRefName: $b${i}, states: [OPEN, CLOSED, MERGED], first: 20, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { ${nodes} } }`).join("")} } }`;
225
+ }
226
+ /**
227
+ * {@link pullRequestLookup} for every branch of one repository in one fixed `gh api graphql` call run from `repo`,
228
+ * which asks for the same 20 newest pull requests of each head branch that `gh pr list --head` returns. `gh` fills
229
+ * `{owner}` and `{repo}` from the remotes of `repo`; branch names travel as variables. Once `gh` is missing, signed
230
+ * out or times out, every later call through the same function answers `unavailable` without running it again.
231
+ * With `detail`, it also asks for each pull request's title, review decision and head commit check counts.
232
+ */
233
+ function pullRequestLookups({ detail = false } = {}) {
234
+ let unavailable;
235
+ return async (repo, queries) => {
236
+ const exec = getExecutor();
237
+ if (unavailable === void 0) unavailable = exec.findExecutable("gh") ? null : "gh is not installed";
238
+ const all = (lookup) => queries.map(() => lookup);
239
+ if (unavailable) return all({ unavailable });
240
+ if (!queries.length) return [];
241
+ const args = [
242
+ "api",
243
+ "graphql",
244
+ "-F",
245
+ "owner={owner}",
246
+ "-F",
247
+ "repo={repo}"
248
+ ];
249
+ queries.forEach(({ branch }, i) => args.push("-f", `b${i}=${branch}`));
250
+ args.push("-f", `query=${branchesQuery(queries.length, detail)}`);
251
+ let out;
252
+ try {
253
+ out = await exec.runFileAsync("gh", args, {
254
+ cwd: repo,
255
+ timeoutMs: GH_TIMEOUT_MS,
256
+ env: GH_ENV
257
+ });
258
+ } catch (error) {
259
+ const failure = ghFailure(error, "gh api graphql");
260
+ if (failure.sticky) unavailable = failure.unavailable;
261
+ return all({ unavailable: failure.unavailable });
262
+ }
263
+ let found;
264
+ try {
265
+ found = JSON.parse(out).data?.repository;
266
+ } catch {
267
+ return all({ unavailable: "gh api graphql printed no JSON" });
268
+ }
269
+ if (!found) return all({ unavailable: "gh api graphql printed no repository" });
270
+ const ownRepos = localRemoteRepos(repo);
271
+ return queries.map(({ cwd, head }, i) => {
272
+ const pulls = found[`b${i}`]?.nodes;
273
+ return { pullRequest: Array.isArray(pulls) ? selectPullRequest(pulls, head, ancestry(cwd), ownRepos) : null };
274
+ });
275
+ };
276
+ }
277
+ /** The merged or closed pull request whose commits include HEAD, which makes the worktree removable. */
278
+ function endedPullRequest(lookup) {
279
+ const pr = lookup && "pullRequest" in lookup ? lookup.pullRequest : null;
280
+ return pr && pr.state !== "open" && pr.containsHead ? pr : null;
281
+ }
282
+ /** "PR #123 merged", for reports. */
283
+ function describePullRequest(pr) {
284
+ return `PR #${pr.number} ${pr.state}`;
285
+ }
286
+ /**
287
+ * Looks up the pull request of each worktree's branch and HEAD, with one {@link pullRequestLookups} call per
288
+ * repository. The map holds null for a worktree without one, and leaves out a worktree whose HEAD git cannot read
289
+ * or whose lookup was unavailable.
290
+ */
291
+ function worktreePullRequests() {
292
+ const lookup = pullRequestLookups();
293
+ return async (worktrees) => {
294
+ const exec = getExecutor();
295
+ const repos = /* @__PURE__ */ new Map();
296
+ for (const { path, branch, repository } of worktrees) {
297
+ let head;
298
+ try {
299
+ head = (await exec.runFileAsync("git", [
300
+ "-C",
301
+ path,
302
+ "rev-parse",
303
+ "HEAD"
304
+ ], { timeoutMs: 5e3 })).trim();
305
+ } catch {
306
+ continue;
307
+ }
308
+ repos.set(repository, [...repos.get(repository) ?? [], {
309
+ cwd: path,
310
+ branch,
311
+ head
312
+ }]);
313
+ }
314
+ const found = /* @__PURE__ */ new Map();
315
+ for (const [repo, queries] of repos) (await lookup(repo, queries)).forEach((result, i) => {
316
+ if ("pullRequest" in result) found.set(queries[i].cwd, result.pullRequest);
317
+ });
318
+ return found;
319
+ };
320
+ }
321
+ //#endregion
322
+ export { pullRequestLookup as a, worktreePullRequests as c, parseRemoteRepo as i, describePullRequest as n, pullRequestLookups as o, endedPullRequest as r, selectPullRequest as s, checkCounts as t };