stim 1.11.0 → 1.12.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 (71) hide show
  1. package/dist/{activity-BtG4Va9l.mjs → activity-CCZJs__P.mjs} +4 -3
  2. package/dist/{android-C4zG2Tuc.mjs → android-75Sk2rIR.mjs} +25 -8
  3. package/dist/{android-C1q4l523.mjs → android-B7X-8H7h.mjs} +75 -32
  4. package/dist/{android-BA2i_KAV.mjs → android-BQiKJYK0.mjs} +1 -1
  5. package/dist/{android-cas-DfJkwCXW.mjs → android-cas-DzLPXiEn.mjs} +7 -7
  6. package/dist/{app-install-DHut6JnV.mjs → app-install-CsZi0SAT.mjs} +1 -1
  7. package/dist/{workspaces-COD2xCz-.mjs → budget-DbzW5VTD.mjs} +641 -32
  8. package/dist/{build-plan-CeAYgKAx.mjs → build-plan-CqntbR7b.mjs} +120 -35
  9. package/dist/{build-progress-CDwogz6R.mjs → build-progress-h60GPV27.mjs} +3 -3
  10. package/dist/{cache-manifest-Pubfp8wV.mjs → cache-manifest-DWPF4RKJ.mjs} +1 -1
  11. package/dist/cache-manifest.mjs +1 -1
  12. package/dist/cli.mjs +16 -16
  13. package/dist/collector-run.mjs +4 -4
  14. package/dist/{command-output-D5z2_bcF.mjs → command-output-BWCj5Vh-.mjs} +1 -1
  15. package/dist/{config-BEq0Qw_a.mjs → config-0jCcMM0E.mjs} +10 -1
  16. package/dist/{dependency-state-r9UesA9a.mjs → dependency-state-DNN0yfx6.mjs} +1 -1
  17. package/dist/{deps-OF7CD5Dx.mjs → deps-CO-_7UOm.mjs} +2 -2
  18. package/dist/{device-BwH2iI3O.mjs → device-9r9xRYDa.mjs} +7 -7
  19. package/dist/{device-lease-D83nqeMX.mjs → device-lease-B9LAXIUH.mjs} +5 -5
  20. package/dist/{device-pool-DHj7cdPy.mjs → device-pool-BoezJd-c.mjs} +4 -4
  21. package/dist/{device-remote-BQZSnnmE.mjs → device-remote-C0_o-tJH.mjs} +254 -579
  22. package/dist/{doctor-D8QdMRMf.mjs → doctor-LfDlbR-N.mjs} +19 -18
  23. package/dist/{error-diagnostics-UYFSQwRy.mjs → error-diagnostics-CCxjn31V.mjs} +7 -7
  24. package/dist/{gc-COZteyiq.mjs → gc-C4nQFcTX.mjs} +203 -39
  25. package/dist/{guide-NDvTmMi-.mjs → guide-BPBlh9ML.mjs} +189 -38
  26. package/dist/{guide-status-CWvti2Lg.mjs → guide-status-i9ogHt4v.mjs} +2 -2
  27. package/dist/{idle-NETbeNiQ.mjs → idle-S3tFZ53_.mjs} +7 -7
  28. package/dist/{in-use-DVhlSoyn.mjs → in-use-D9XYEgFD.mjs} +7 -7
  29. package/dist/{ios-CQLKpNfr.mjs → ios-CDzWL1mj.mjs} +30 -29
  30. package/dist/{ios-DArbmwer.mjs → ios-CZWOHTTc.mjs} +23 -19
  31. package/dist/{ios-device-CwDZo0X4.mjs → ios-device-D9cLxVr5.mjs} +1 -1
  32. package/dist/{launch-verify-CuPDGXdr.mjs → launch-verify-B2oR_kFN.mjs} +2 -2
  33. package/dist/{logs-B44a1UeU.mjs → logs-SzuYudOj.mjs} +6 -6
  34. package/dist/{merge-state-DDxS-XIZ.mjs → merge-state-ytFQCeh4.mjs} +38 -8
  35. package/dist/{metro-BWLOJ_Wr.mjs → metro-Dzo30hvJ.mjs} +1 -1
  36. package/dist/{named-ports-Dc7neAlc.mjs → named-ports-Dn1o61D9.mjs} +3 -3
  37. package/dist/{ownership-BiojdtAp.mjs → ownership-B8d4kXBx.mjs} +49 -16
  38. package/dist/{ownership-D2xbLVoz.mjs → ownership-Dqnrjrio.mjs} +2 -2
  39. package/dist/{ports-DwBaRFJs.mjs → ports-Dp_QTwTC.mjs} +3 -3
  40. package/dist/{project-BXZzOABt.mjs → project-B8rbutVC.mjs} +4 -72
  41. package/dist/{reload-By9Dwsrt.mjs → reload-nocmx8KB.mjs} +9 -9
  42. package/dist/remote-cache-BRsRZru5.mjs +526 -0
  43. package/dist/{server-bare-D_RK1RCj.mjs → server-bare-Gn3ssSdK.mjs} +1 -1
  44. package/dist/{server-expo-o6H5gcBc.mjs → server-expo-CeT1nIIk.mjs} +2 -2
  45. package/dist/server-expo-eWi9Coof.mjs +2 -0
  46. package/dist/{settings-B5oFZEri.mjs → settings-BlyDn2Oz.mjs} +2 -2
  47. package/dist/{settings-C0ugALSC.mjs → settings-bE-Gu3Ti.mjs} +23 -11
  48. package/dist/settings.schema.json +22 -2
  49. package/dist/{simslim-CIR3IYJO.mjs → simslim-COtEEP7x.mjs} +6 -5
  50. package/dist/{slot-launch-rggO1Esw.mjs → slot-launch-DIACGEfH.mjs} +4 -4
  51. package/dist/{spawn-claims-BJwXTMPt.mjs → spawn-claims-PT8aQpCa.mjs} +1 -1
  52. package/dist/{start-i8zc9VmS.mjs → start-BFstVBlx.mjs} +1 -1
  53. package/dist/{start-BSsf2tUs.mjs → start-BboE1HIS.mjs} +108 -17
  54. package/dist/{state-WGdIbi_d.mjs → state-DcDgzfu3.mjs} +15 -3
  55. package/dist/{stats-BAgJaLA4.mjs → stats-D8rUTPHd.mjs} +1 -1
  56. package/dist/{stats-DBYJ5bxL.mjs → stats-DiCtmluY.mjs} +4 -4
  57. package/dist/{status-Q-WDEGCB.mjs → status-BUASYyy_.mjs} +73 -28
  58. package/dist/{status-CitcBRsC.mjs → status-DI9Tjo7T.mjs} +41 -29
  59. package/dist/stim-desktop-D0xhpZt8.mjs +29 -0
  60. package/dist/{stop-rst5mL14.mjs → stop-BI7QY76M.mjs} +2 -2
  61. package/dist/{stop-DIhrvu0B.mjs → stop-Bn6v-GvS.mjs} +23 -16
  62. package/dist/supervisor-run.mjs +11 -11
  63. package/dist/{workspace-process-lock-Cx74RSDZ.mjs → workspace-process-lock-BvvJTfo_.mjs} +2 -2
  64. package/dist/{worktree-DhxDTK44.mjs → worktree-B5sTRskL.mjs} +1 -1
  65. package/dist/{worktree-KOjMX3zA.mjs → worktree-ChDL5fO1.mjs} +17 -16
  66. package/dist/{worktree-BlizKDpZ.mjs → worktree-DeBQSVtR.mjs} +3 -1
  67. package/dist/{xcode-B5SPD--o.mjs → xcode-BzplpQzc.mjs} +7 -7
  68. package/package.json +5 -5
  69. package/dist/budget-De3P2rq_.mjs +0 -620
  70. package/dist/native-run-D7yB_AsB.mjs +0 -143
  71. package/dist/server-expo-B-CCUJZj.mjs +0 -2
@@ -1,538 +1,20 @@
1
- import { u as getConfigDir } from "./config-BEq0Qw_a.mjs";
1
+ import { d as getConfigDir } from "./config-0jCcMM0E.mjs";
2
2
  import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
3
- import { r as createNdjsonWriter } from "./ndjson-CDY9IkVC.mjs";
4
- import { N as workspaceStateFile, O as workspaceDir, P as withDirLock, i as pidExists, j as workspaceLogsDir, k as workspaceId } from "./metro-BWLOJ_Wr.mjs";
5
- import { c as ownedDeviceLabel, u as resolvePackageJson } from "./project-BXZzOABt.mjs";
6
- import { a as clearRemoteSession, l as readRemoteSession, s as readMetroTunnel } from "./state-WGdIbi_d.mjs";
7
- import { t as withWorkspaceProcessLock } from "./workspace-process-lock-Cx74RSDZ.mjs";
8
- import { n as stripAnsi } from "./process-output-Fpl8PYi8.mjs";
9
- import { B as planMetroReach } from "./settings-B5oFZEri.mjs";
10
- import { u as probePublicHttp } from "./ownership-BiojdtAp.mjs";
11
- import { C as sanitizeDeviceLabel } from "./ios-DArbmwer.mjs";
12
- import { n as expoBinFromPackage, r as expoBinPath, s as findBinUpward } from "./server-expo-o6H5gcBc.mjs";
13
- import { a as LAUNCH_ERROR, i as INSTALL_ERROR, l as devClientDeepLink } from "./app-install-DHut6JnV.mjs";
14
- import { c as readMetroRecords, o as isBundleProof } from "./launch-verify-CuPDGXdr.mjs";
3
+ import { N as workspaceStateFile, O as workspaceDir, P as withDirLock, f as clearWorkspaceStateKey, g as updateWorkspaceState, i as pidExists, j as workspaceLogsDir, k as workspaceId, m as readWorkspaceState$1 } from "./metro-Dzo30hvJ.mjs";
4
+ import { o as ownedDeviceLabel } from "./project-B8rbutVC.mjs";
5
+ import { a as clearRemoteSession, c as readMetroTunnel, u as readRemoteSession } from "./state-DcDgzfu3.mjs";
6
+ import { i as signalDeclaredSpawns, r as refuseDeclaredSpawns, t as allowDeclaredSpawns } from "./spawn-claims-PT8aQpCa.mjs";
7
+ import { t as withWorkspaceProcessLock } from "./workspace-process-lock-BvvJTfo_.mjs";
8
+ import { a as formatElapsed } from "./command-output-BWCj5Vh-.mjs";
9
+ import { B as planMetroReach } from "./settings-BlyDn2Oz.mjs";
10
+ import { u as probePublicHttp } from "./ownership-B8d4kXBx.mjs";
11
+ import { C as sanitizeDeviceLabel } from "./ios-CZWOHTTc.mjs";
12
+ import { a as LAUNCH_ERROR, i as INSTALL_ERROR, l as devClientDeepLink } from "./app-install-CsZi0SAT.mjs";
13
+ import { c as readMetroRecords, o as isBundleProof } from "./launch-verify-B2oR_kFN.mjs";
15
14
  import { t as compareStimVersions } from "./stim-installations-CINXy_su.mjs";
16
- import { createRequire } from "node:module";
17
- import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
15
+ import { existsSync, mkdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
18
16
  import { easMachineStateRoot, easMachineStateRoot as easMachineStateRoot$1, easSessionLedgerFile, easSessionLedgerLock, readEasSessionLedger, readEasSessionLedger as readEasSessionLedger$1, readJsonObject, validEasSessionClaim } from "@stim-cli/core/state";
19
17
  import { dirname, join, resolve } from "node:path";
20
- import { pathToFileURL } from "node:url";
21
- import { format } from "node:util";
22
- import { AsyncLocalStorage } from "node:async_hooks";
23
- //#region src/engine/remote-cache.ts
24
- const EAS_PROVIDER_PACKAGE = "eas-build-cache-provider";
25
- const RESOLVE_TIMEOUT_MS = 3e4;
26
- const UPLOAD_TIMEOUT_MS = 6e4;
27
- const CONFIG_TIMEOUT_MS = 3e4;
28
- const DYNAMIC_CONFIG_FILES = [
29
- "app.config.ts",
30
- "app.config.js",
31
- "app.config.mjs",
32
- "app.config.cjs"
33
- ];
34
- const TIMED_OUT = Symbol("timed-out");
35
- function dynamicConfigFile(root) {
36
- for (const name of DYNAMIC_CONFIG_FILES) if (existsSync(join(root, name))) return name;
37
- return null;
38
- }
39
- function providerFromConfig(config) {
40
- if (!config || typeof config !== "object") return null;
41
- const expoField = config.expo;
42
- const exp = expoField && typeof expoField === "object" ? expoField : config;
43
- return exp.buildCacheProvider ?? exp.experiments?.buildCacheProvider ?? null;
44
- }
45
- function ownerFromConfig(config) {
46
- if (!config || typeof config !== "object") return null;
47
- const expoField = config.expo;
48
- const owner = (expoField && typeof expoField === "object" ? expoField : config).owner;
49
- return typeof owner === "string" && owner.trim() !== "" ? owner.trim() : null;
50
- }
51
- function normalizeProvider(raw) {
52
- if (raw === null || raw === void 0 || raw === "") return null;
53
- if (raw === "eas") return {
54
- name: "eas",
55
- reference: EAS_PROVIDER_PACKAGE,
56
- options: {}
57
- };
58
- if (typeof raw === "object") {
59
- const obj = raw;
60
- if (typeof obj.plugin === "string" && obj.plugin.trim() !== "") {
61
- const reference = obj.plugin.trim();
62
- return {
63
- name: reference,
64
- reference,
65
- options: obj.options && typeof obj.options === "object" ? obj.options : {}
66
- };
67
- }
68
- }
69
- return { invalid: typeof raw === "string" ? `"${raw}"` : JSON.stringify(raw) };
70
- }
71
- function runOptionsFor(platform) {
72
- return platform === "android" ? { variant: "debug" } : { configuration: "Debug" };
73
- }
74
- function isProviderPlugin(plugin) {
75
- if (!plugin || typeof plugin !== "object") return false;
76
- const p = plugin;
77
- const resolves = typeof p.resolveBuildCache === "function" || typeof p.resolveRemoteBuildCache === "function";
78
- const uploads = typeof p.uploadBuildCache === "function" || typeof p.uploadRemoteBuildCache === "function";
79
- return resolves && uploads;
80
- }
81
- function cacheLevel(value) {
82
- return value === "local" || value === "remote" ? value : false;
83
- }
84
- function exitAfterFlush(code = 0, { exit = (c) => process.exit(c), stream = process.stdout } = {}) {
85
- try {
86
- stream.write("", () => exit(code));
87
- } catch {
88
- exit(code);
89
- }
90
- }
91
- function readProjectConfig(root, { run = null, timeoutMs = CONFIG_TIMEOUT_MS } = {}) {
92
- const dynamic = dynamicConfigFile(root);
93
- if (!dynamic) {
94
- const file = join(root, "app.json");
95
- if (!existsSync(file)) return {
96
- config: null,
97
- source: null
98
- };
99
- try {
100
- return {
101
- config: JSON.parse(readFileSync(file, "utf-8")),
102
- source: "app.json"
103
- };
104
- } catch (err) {
105
- return { unavailable: `app.json could not be parsed: ${firstLine(err)}` };
106
- }
107
- }
108
- const bin = expoBinPath(root);
109
- if (!bin) return { unavailable: `${dynamic} is code, so it has to be evaluated to read its buildCacheProvider, and the \`expo\` package is not resolvable from this project (no expo/package.json to read a bin from, and no node_modules/.bin/expo here or in any parent)` };
110
- const exec = run || ((file, args, opts) => getExecutor().runFile(file, args, opts));
111
- let stdout;
112
- try {
113
- stdout = exec(bin, [
114
- "config",
115
- "--json",
116
- root
117
- ], { timeoutMs });
118
- } catch (err) {
119
- return { unavailable: `\`expo config --json\` failed: ${firstLine(err)}` };
120
- }
121
- try {
122
- return {
123
- config: JSON.parse(String(stdout)),
124
- source: dynamic
125
- };
126
- } catch (err) {
127
- return { unavailable: `\`expo config --json\` did not print JSON: ${firstLine(err)}` };
128
- }
129
- }
130
- async function loadPlugin(projectRoot, reference, { requireFrom = null } = {}) {
131
- const localRequire = requireFrom ? requireFrom(projectRoot) : createRequire(join(projectRoot, "package.json"));
132
- const file = localRequire.resolve(reference);
133
- let mod;
134
- try {
135
- mod = localRequire(file);
136
- } catch (err) {
137
- if (err?.code !== "ERR_REQUIRE_ESM") throw err;
138
- mod = await import(pathToFileURL(file).href);
139
- }
140
- const plugin = mod?.default ?? mod;
141
- if (!isProviderPlugin(plugin)) throw new Error(`"${reference}" does not export resolveBuildCache and uploadBuildCache functions`);
142
- return plugin;
143
- }
144
- async function loadProjectProvider(projectRoot, { isExpo = true, run = null, requireFrom = null, timeoutMs = CONFIG_TIMEOUT_MS } = {}) {
145
- if (!isExpo) return { none: true };
146
- const read = readProjectConfig(projectRoot, {
147
- run,
148
- timeoutMs
149
- });
150
- if (read.unavailable) return { unavailable: read.unavailable };
151
- const normalized = normalizeProvider(providerFromConfig(read.config));
152
- if (!normalized) return { none: true };
153
- if (normalized.invalid) return { unavailable: `buildCacheProvider is ${normalized.invalid}, which is not "eas" or { plugin: <module> }` };
154
- if (normalized.reference === "@stim-cli/expo-build-cache") return { none: true };
155
- let plugin;
156
- try {
157
- plugin = await loadPlugin(projectRoot, normalized.reference, { requireFrom });
158
- } catch (err) {
159
- const missing = err?.code === "MODULE_NOT_FOUND";
160
- return { unavailable: normalized.name === "eas" && missing ? `the EAS build cache needs the \`${EAS_PROVIDER_PACKAGE}\` package, which is not installed in this project` : `${normalized.name} could not be loaded: ${firstLine(err)}` };
161
- }
162
- return {
163
- provider: {
164
- plugin,
165
- options: normalized.options ?? {}
166
- },
167
- name: normalized.name,
168
- owner: ownerFromConfig(read.config)
169
- };
170
- }
171
- const WHOAMI_TIMEOUT_MS = 15e3;
172
- const EAS_CLI_PACKAGE = "eas-cli";
173
- const EAS_CLI_BIN = "eas";
174
- function parseWhoami({ stdout = "", stderr = "", exitCode = 0 } = {}) {
175
- const out = stripAnsi(stdout);
176
- const combined = `${out}\n${stripAnsi(stderr)}`;
177
- if (/^\s*Not logged in\s*$/m.test(combined)) return { loggedOut: true };
178
- if (exitCode !== 0) return { unknown: firstLine(combined.trim() || `eas whoami exited ${exitCode}`) };
179
- const lines = out.split("\n").map((line) => line.trim()).filter((line) => line !== "");
180
- if (lines.length === 0) return { unknown: "eas whoami printed nothing" };
181
- const raw = lines[0];
182
- if (raw === void 0) return { unknown: "eas whoami printed nothing" };
183
- const viaToken = /\(authenticated using EXPO_TOKEN\)\s*$/.test(raw);
184
- const display = raw.replace(/\s*\(authenticated using EXPO_TOKEN\)\s*$/, "").trim();
185
- const accounts = [];
186
- for (const line of lines) {
187
- const name = /^\u2022\s*(\S+)\s*\(Role:/.exec(line)?.[1];
188
- if (name !== void 0) accounts.push(name);
189
- }
190
- if (accounts.length === 0 && !display.endsWith("(robot)") && display !== "robot") accounts.push(display);
191
- return {
192
- loggedIn: true,
193
- account: display,
194
- accounts: accounts.length ? accounts : null,
195
- viaToken
196
- };
197
- }
198
- function isEasAuthFailureText(text) {
199
- const t = stripAnsi(text || "");
200
- if (t.trim() === "") return false;
201
- return /not logged in/i.test(t) || /\beas login\b/i.test(t) || /EXPO_TOKEN/.test(t) || /unauthorized/i.test(t) || /not authorized/i.test(t) || /authentication (failed|required)/i.test(t);
202
- }
203
- function easAuthNote(status) {
204
- const { code, account, owner, reason, phase = "resolve" } = status || {};
205
- const because = reason ? ` (${reason})` : "";
206
- if (code === "logged-out") return `eas is not authenticated${because} -- run \`eas login\` (or set EXPO_TOKEN); ` + (phase === "upload" ? "this build stayed in the local cache" : "building with the local cache only");
207
- if (code === "wrong-account") return `eas is authenticated as ${account}, and this project's owner is ${owner} -- run \`eas login\` as a member of ${owner} (or set EXPO_TOKEN); consulting the EAS cache anyway`;
208
- return null;
209
- }
210
- function resolveEasCliBin(projectRoot, { lookupPath = null } = {}) {
211
- const fromPackage = expoBinFromPackage(resolvePackageJson(projectRoot, EAS_CLI_PACKAGE), EAS_CLI_BIN);
212
- if (fromPackage) return {
213
- file: fromPackage,
214
- source: "project"
215
- };
216
- const shim = findBinUpward(projectRoot, EAS_CLI_BIN);
217
- if (shim) return {
218
- file: shim,
219
- source: "project"
220
- };
221
- const onPath = lookupPath ? lookupPath() : getExecutor().findExecutable(EAS_CLI_BIN);
222
- const file = (String(onPath || "").split("\n")[0] ?? "").trim();
223
- return file ? {
224
- file,
225
- source: "path"
226
- } : null;
227
- }
228
- const easAuthCache = /* @__PURE__ */ new Map();
229
- function checkEasAuth({ projectRoot, owner = null, run = null, resolveBin = null, timeoutMs = WHOAMI_TIMEOUT_MS, cache = easAuthCache }) {
230
- const key = `${projectRoot}::${owner || ""}`;
231
- if (cache?.has(key)) return cache.get(key);
232
- const answer = probeEasAuth({
233
- projectRoot,
234
- owner,
235
- run,
236
- resolveBin,
237
- timeoutMs
238
- });
239
- cache?.set(key, answer);
240
- return answer;
241
- }
242
- function probeEasAuth({ projectRoot, owner, run, resolveBin, timeoutMs }) {
243
- const bin = (resolveBin || resolveEasCliBin)(projectRoot);
244
- if (!bin) return {
245
- failed: true,
246
- code: "no-cli",
247
- reason: "no `eas` executable is resolvable from this project or on PATH",
248
- remedy: "Install eas-cli (`npm i -g eas-cli`, or as a project devDependency), then run `eas login`."
249
- };
250
- const exec = run || ((file, args, opts) => getExecutor().runFile(file, args, opts));
251
- let result;
252
- try {
253
- result = {
254
- stdout: String(exec(bin.file, ["whoami"], { timeoutMs }) ?? ""),
255
- exitCode: 0
256
- };
257
- } catch (e) {
258
- const err = e;
259
- if (err?.code === "ETIMEDOUT" || err?.signal === "SIGTERM" || err?.killed) return { unknown: `eas whoami timed out after ${timeoutMs}ms` };
260
- result = {
261
- stdout: String(err?.stdout ?? ""),
262
- stderr: String(err?.stderr ?? err?.message ?? ""),
263
- exitCode: typeof err?.status === "number" ? err.status : 1
264
- };
265
- }
266
- const parsed = parseWhoami(result);
267
- if (parsed.loggedOut) return {
268
- failed: true,
269
- code: "logged-out",
270
- reason: "eas whoami says \"Not logged in\"",
271
- remedy: "Run `eas login` (or set EXPO_TOKEN)."
272
- };
273
- if (parsed.unknown) return { unknown: parsed.unknown };
274
- if (owner && Array.isArray(parsed.accounts) && !parsed.accounts.includes(owner)) return {
275
- failed: true,
276
- code: "wrong-account",
277
- account: parsed.account,
278
- accounts: parsed.accounts,
279
- owner,
280
- reason: `eas is authenticated as ${parsed.account} (accounts: ${parsed.accounts.join(", ")}), which does not include this project's owner ${owner}`,
281
- remedy: `Run \`eas login\` as a member of ${owner}, or set EXPO_TOKEN to a token for it.`
282
- };
283
- return {
284
- ok: true,
285
- account: parsed.account,
286
- accounts: parsed.accounts,
287
- viaToken: parsed.viaToken,
288
- source: bin.source
289
- };
290
- }
291
- const capturing = [];
292
- const abandoned = /* @__PURE__ */ new Set();
293
- let patched = null;
294
- const PROVIDER_CONTEXT = new AsyncLocalStorage();
295
- function uploadDestination(lines) {
296
- const text = (lines || []).map(stripAnsi);
297
- for (const line of text) {
298
- const url = /(https?:\/\/[^\s'"]+)/.exec(line)?.[1];
299
- if (url !== void 0) return url.replace(/[.,)\]]+$/, "");
300
- }
301
- for (const line of text) {
302
- const slug = /\bto\s+(@?[\w.-]+\/[\w.-]+)/i.exec(line)?.[1];
303
- if (slug !== void 0) return slug.replace(/[.,)\]]+$/, "");
304
- }
305
- return null;
306
- }
307
- function providerNote(text) {
308
- return `${"cache".padEnd(11)} ${text}`;
309
- }
310
- function currentFrame() {
311
- const store = PROVIDER_CONTEXT.getStore();
312
- return store && (capturing.includes(store) || abandoned.has(store)) ? store : null;
313
- }
314
- function install() {
315
- if (patched && process.stdout.write === patched.patchedWrite) return;
316
- const stdout = process.stdout;
317
- const write = stdout.write.bind(stdout);
318
- const log = console.log;
319
- const passthrough = write;
320
- const patchedWrite = ((chunk, encoding, callback) => {
321
- const frame = currentFrame();
322
- if (!frame) return passthrough(chunk, encoding, callback);
323
- const cb = typeof encoding === "function" ? encoding : callback;
324
- absorb(frame, typeof chunk === "string" ? chunk : String(chunk));
325
- if (typeof cb === "function") cb();
326
- return true;
327
- });
328
- const patchedLog = (...args) => {
329
- const frame = currentFrame();
330
- if (!frame) return log(...args);
331
- absorb(frame, `${format(...args)}\n`);
332
- };
333
- stdout.write = patchedWrite;
334
- console.log = patchedLog;
335
- patched = {
336
- stdout,
337
- write,
338
- log,
339
- patchedWrite,
340
- patchedLog
341
- };
342
- }
343
- function uninstall() {
344
- if (!patched || capturing.length || abandoned.size) return;
345
- if (patched.stdout.write === patched.patchedWrite) patched.stdout.write = patched.write;
346
- if (console.log === patched.patchedLog) console.log = patched.log;
347
- patched = null;
348
- }
349
- function absorb(frame, chunk) {
350
- try {
351
- process.stderr.write(chunk);
352
- } catch {}
353
- frame.pending += chunk;
354
- let index = frame.pending.indexOf("\n");
355
- while (index !== -1) {
356
- emitLine(frame, frame.pending.slice(0, index));
357
- frame.pending = frame.pending.slice(index + 1);
358
- index = frame.pending.indexOf("\n");
359
- }
360
- }
361
- function emitLine(frame, raw) {
362
- const line = stripAnsi(raw).trim();
363
- if (line === "") return;
364
- frame.lines.push(line);
365
- const writer = frame.writer();
366
- try {
367
- writer?.write({
368
- src: "build",
369
- level: "debug",
370
- event: "provider",
371
- msg: line
372
- });
373
- } catch {}
374
- frame.onLine?.(line);
375
- }
376
- function beginCapture({ logWriter = null, projectRoot = null, platform = null, onLine = null } = {}) {
377
- let own = null;
378
- const frame = {
379
- lines: [],
380
- pending: "",
381
- onLine,
382
- writer: () => {
383
- if (logWriter) return logWriter;
384
- if (own) return own;
385
- if (!projectRoot || !platform) return null;
386
- own = createNdjsonWriter(join(workspaceLogsDir(projectRoot), `build-${platform}.ndjson`));
387
- return own;
388
- },
389
- close: () => {
390
- own?.close?.();
391
- own = null;
392
- }
393
- };
394
- capturing.push(frame);
395
- install();
396
- return frame;
397
- }
398
- function endCapture(frame) {
399
- if (!frame) return;
400
- const at = capturing.lastIndexOf(frame);
401
- if (at !== -1) capturing.splice(at, 1);
402
- abandoned.delete(frame);
403
- if (frame.pending !== "") {
404
- const rest = frame.pending;
405
- frame.pending = "";
406
- emitLine(frame, rest);
407
- }
408
- frame.close();
409
- uninstall();
410
- }
411
- function abandonCapture(frame, work) {
412
- if (!frame) return;
413
- const at = capturing.lastIndexOf(frame);
414
- if (at !== -1) capturing.splice(at, 1);
415
- abandoned.add(frame);
416
- Promise.resolve(work).then(() => endCapture(frame), () => endCapture(frame));
417
- }
418
- async function resolveRemote({ provider, platform, projectRoot, fingerprintHash, runOptions = null, timeoutMs = RESOLVE_TIMEOUT_MS, logWriter = null } = {}) {
419
- if (!provider?.plugin || !fingerprintHash) return null;
420
- const opts = runOptions || runOptionsFor(platform);
421
- const outcome = await withBudget(async () => {
422
- const props = {
423
- fingerprintHash: await providerFingerprint({
424
- provider,
425
- platform,
426
- projectRoot,
427
- runOptions: opts
428
- }) ?? fingerprintHash,
429
- platform,
430
- runOptions: opts,
431
- projectRoot
432
- };
433
- return typeof provider.plugin.resolveBuildCache === "function" ? provider.plugin.resolveBuildCache(props, provider.options) : provider.plugin.resolveRemoteBuildCache(props, provider.options);
434
- }, timeoutMs, {
435
- logWriter,
436
- projectRoot,
437
- platform
438
- });
439
- if (outcome.timedOut) return { timedOut: true };
440
- if (outcome.error) return { failed: firstLine(outcome.error) };
441
- const appPath = typeof outcome.value === "string" ? outcome.value.trim() : "";
442
- if (!appPath) return null;
443
- if (!existsSync(appPath)) return { failed: `returned ${appPath}, which does not exist` };
444
- return { appPath };
445
- }
446
- async function uploadRemote({ provider, platform, projectRoot, fingerprintHash, buildPath, runOptions = null, timeoutMs = UPLOAD_TIMEOUT_MS, logWriter = null, note = (line) => console.error(line) } = {}) {
447
- if (!provider?.plugin || !fingerprintHash || !buildPath) return { skipped: true };
448
- const opts = runOptions || runOptionsFor(platform);
449
- let announced = false;
450
- const onLine = (line) => {
451
- if (announced) return;
452
- const dest = uploadDestination([line]);
453
- if (!dest) return;
454
- announced = true;
455
- note(providerNote(`uploading to ${dest}`));
456
- };
457
- const outcome = await withBudget(async () => {
458
- const props = {
459
- projectRoot,
460
- platform,
461
- fingerprintHash: await providerFingerprint({
462
- provider,
463
- platform,
464
- projectRoot,
465
- runOptions: opts
466
- }) ?? fingerprintHash,
467
- buildPath,
468
- runOptions: opts
469
- };
470
- return typeof provider.plugin.uploadBuildCache === "function" ? provider.plugin.uploadBuildCache(props, provider.options) : provider.plugin.uploadRemoteBuildCache(props, provider.options);
471
- }, timeoutMs, {
472
- logWriter,
473
- projectRoot,
474
- platform,
475
- onLine
476
- });
477
- if (outcome.timedOut) return { timedOut: true };
478
- if (outcome.error) return { failed: firstLine(outcome.error) };
479
- if (typeof outcome.value !== "string" || outcome.value.trim() === "") {
480
- const authLine = outcome.lines.find((line) => isEasAuthFailureText(line));
481
- return { failed: authLine ? firstLine(authLine) : "the provider returned no upload destination" };
482
- }
483
- const destination = uploadDestination(outcome.lines);
484
- return destination ? {
485
- uploaded: true,
486
- destination
487
- } : { uploaded: true };
488
- }
489
- async function providerFingerprint({ provider, platform, projectRoot, runOptions }) {
490
- if (typeof provider.plugin.calculateFingerprintHash !== "function") return null;
491
- try {
492
- const hash = await provider.plugin.calculateFingerprintHash({
493
- projectRoot,
494
- platform,
495
- runOptions
496
- }, provider.options);
497
- return typeof hash === "string" && hash.trim() !== "" ? hash.trim() : null;
498
- } catch {
499
- return null;
500
- }
501
- }
502
- async function withBudget(factory, ms, capture) {
503
- let timer = null;
504
- const timeout = new Promise((resolve) => {
505
- timer = setTimeout(() => resolve(TIMED_OUT), ms);
506
- timer?.unref?.();
507
- });
508
- const frame = beginCapture(capture || {});
509
- const work = Promise.resolve().then(() => PROVIDER_CONTEXT.run(frame, factory)).then((value) => ({ value }), (error) => ({ error }));
510
- try {
511
- const settled = await Promise.race([work, timeout]);
512
- if (settled === TIMED_OUT) {
513
- abandonCapture(frame, work);
514
- return {
515
- timedOut: true,
516
- lines: frame.lines
517
- };
518
- }
519
- endCapture(frame);
520
- return {
521
- ...settled,
522
- lines: frame.lines
523
- };
524
- } catch (err) {
525
- endCapture(frame);
526
- throw err;
527
- } finally {
528
- if (timer) clearTimeout(timer);
529
- }
530
- }
531
- function firstLine(err) {
532
- const line = (String(err?.message || err || "unknown error").trim().split("\n")[0] ?? "").trim();
533
- return line.length > 200 ? `${line.slice(0, 197)}...` : line;
534
- }
535
- //#endregion
536
18
  //#region src/engine/metro-gate.ts
537
19
  const REMOTE_METRO_WRONG = "STIM_REMOTE_METRO_WRONG";
538
20
  const GATE_TIMEOUT_MS = 25e3;
@@ -1004,6 +486,14 @@ function removeEasSessionClaim(sessionId, root = easMachineStateRoot()) {
1004
486
  //#endregion
1005
487
  //#region src/engine/eas-project-lock.ts
1006
488
  const EAS_PROJECT_LOCK_WAIT_MS = 24e4;
489
+ function describeEasProjectLockHolder(holder, now) {
490
+ const { purpose, workspace } = holder.details;
491
+ const facts = [`pid ${holder.owner.pid}`];
492
+ if (typeof workspace === "string") facts.push(`in ${workspace}`);
493
+ const started = Date.parse(holder.startedAt);
494
+ if (Number.isFinite(started)) facts.push(`running for ${formatElapsed(now - started)}`);
495
+ return `${typeof purpose === "string" ? purpose : "another Stim EAS operation"} (${facts.join(", ")})`;
496
+ }
1007
497
  function withEasProjectLock(_root, fn, options = {}) {
1008
498
  const { machineRoot = easMachineStateRoot$1(), ...lockOptions } = options;
1009
499
  assertEasMachineRootWritable(machineRoot);
@@ -1014,6 +504,144 @@ function withEasProjectLock(_root, fn, options = {}) {
1014
504
  });
1015
505
  }
1016
506
  //#endregion
507
+ //#region src/engine/native-run.ts
508
+ const NATIVE_RUN_LOCK = "native-run";
509
+ const NATIVE_RUN_WAIT_MS = 18e5;
510
+ const WAIT_HEARTBEAT_MS = 3e4;
511
+ const CANCEL_REQUEST_KEY = "nativeRunCancel";
512
+ function nativeRunHolder(holder) {
513
+ const { command, platform, slot } = holder.details;
514
+ return {
515
+ command: typeof command === "string" ? command : null,
516
+ platform: typeof platform === "string" ? platform : null,
517
+ slot: typeof slot === "string" ? slot : "default"
518
+ };
519
+ }
520
+ function describeNativeRunHolder(holder, now) {
521
+ const { command, slot } = nativeRunHolder(holder);
522
+ const started = Date.parse(holder.startedAt);
523
+ const facts = [`pid ${holder.owner.pid}`];
524
+ if (Number.isFinite(started)) facts.push(`running for ${formatElapsed(now - started)}`);
525
+ if (command === null) return `another Stim run (${facts.join(", ")})`;
526
+ return `\`stim ${command}${slot === "default" ? "" : ` --slot ${slot}`}\` (${facts.join(", ")})`;
527
+ }
528
+ /**
529
+ * Report a wait on the native-run lock: one line as soon as a holder is seen, a line for each new
530
+ * holder, and a heartbeat every WAIT_HEARTBEAT_MS while the same holder keeps it.
531
+ */
532
+ function nativeRunWaitNotice({ write, now = Date.now, describe = describeNativeRunHolder }) {
533
+ let claimId = null;
534
+ let lastAt = 0;
535
+ return (holder, firstLine) => {
536
+ const at = now();
537
+ if (holder.claimId !== claimId) {
538
+ claimId = holder.claimId;
539
+ lastAt = at;
540
+ write(firstLine ?? `waiting for ${describe(holder, at)} in this workspace to finish`);
541
+ return;
542
+ }
543
+ if (at - lastAt < WAIT_HEARTBEAT_MS) return;
544
+ lastAt = at;
545
+ write(`still waiting for ${describe(holder, at)}`);
546
+ };
547
+ }
548
+ /**
549
+ * What `stop` does about the run holding the native-run lock. A build is interrupted only when the stop
550
+ * leaves it nothing to deploy to: the whole workspace stops, the build targets the stopped slot, or the
551
+ * stopped slot is the workspace's only device. A build for another slot that stays is left running and
552
+ * the stop proceeds without the lock. Only an owner whose identity is proven live is signalled.
553
+ */
554
+ function decideStopAction({ stopSlot, holder, ownerIdentity, deviceSlots }) {
555
+ if (holder.command !== "ios" && holder.command !== "android") return { action: "wait" };
556
+ if (!(stopSlot === void 0 || holder.slot === stopSlot || deviceSlots.includes(stopSlot) && deviceSlots.every((slot) => slot === stopSlot))) return { action: "proceed" };
557
+ return ownerIdentity === "same" ? { action: "interrupt" } : { action: "refuse" };
558
+ }
559
+ function requestNativeRunCancel(root, claimId, pid = process.pid) {
560
+ updateWorkspaceState(root, (state) => ({
561
+ ...state,
562
+ [CANCEL_REQUEST_KEY]: {
563
+ claimId,
564
+ pid,
565
+ at: (/* @__PURE__ */ new Date()).toISOString()
566
+ }
567
+ }));
568
+ }
569
+ function cancelRequestFor(root, claimId) {
570
+ const request = readWorkspaceState$1(root)?.[CANCEL_REQUEST_KEY];
571
+ if (request?.claimId !== claimId || typeof request.pid !== "number") return null;
572
+ return { pid: request.pid };
573
+ }
574
+ function clearNativeRunCancel(root, claimId) {
575
+ try {
576
+ clearWorkspaceStateKey(root, CANCEL_REQUEST_KEY, (value) => value?.claimId === claimId);
577
+ } catch {}
578
+ }
579
+ let cancelled = null;
580
+ /** Why this process's native run was cancelled, or null while it was not. */
581
+ function runCancellation() {
582
+ return cancelled;
583
+ }
584
+ function cancelledFailure(platform, { code, message }) {
585
+ if (cancelled === null) return null;
586
+ return {
587
+ code: "STIM_CANCELLED",
588
+ message: `The ${platform} run was ${cancelled} before it finished.`,
589
+ remedy: `Run \`stim ${platform}\` again when you want the app on a device.`,
590
+ lines: [`it stopped at ${code ?? "an unfinished step"}${message ? `: ${message}` : ""}`]
591
+ };
592
+ }
593
+ function cancelOnInterrupt({ root, claim, write, exit }) {
594
+ let signals = 0;
595
+ const onInterrupt = () => {
596
+ signals += 1;
597
+ if (signals > 1) {
598
+ exit(130);
599
+ return;
600
+ }
601
+ const request = cancelRequestFor(root, claim.claimId);
602
+ cancelled = request ? `cancelled by \`stim stop\` (pid ${request.pid})` : "cancelled by an interrupt";
603
+ refuseDeclaredSpawns(`the run was ${cancelled}`);
604
+ const signalled = signalDeclaredSpawns("SIGINT");
605
+ write(signalled > 0 ? `${cancelled}; stopping the running build tool` : request ? `${cancelled}; stopping before the next build tool or install` : cancelled);
606
+ if (signalled === 0 && !request) exit(130);
607
+ };
608
+ process.on("SIGINT", onInterrupt);
609
+ return () => {
610
+ process.off("SIGINT", onInterrupt);
611
+ allowDeclaredSpawns();
612
+ clearNativeRunCancel(root, claim.claimId);
613
+ };
614
+ }
615
+ /**
616
+ * Run an `ios` or `android` invocation under the workspace's native-run lock: the claim records the
617
+ * command, platform and slot, and a wait for another holder is reported on `write`. The first SIGINT
618
+ * forwards the interrupt to the running declared build tool; a run `stim stop` cancelled between build
619
+ * tools starts no further one; a terminal interrupt with no build tool running, or a second SIGINT, exits
620
+ * with 130 at once.
621
+ */
622
+ function withNativeBuildRun(root, details, fn, { write, exit = (code) => process.exit(code) }) {
623
+ const notice = nativeRunWaitNotice({ write });
624
+ return withWorkspaceProcessLock(workspaceDir(root), NATIVE_RUN_LOCK, async (claim) => {
625
+ const dispose = cancelOnInterrupt({
626
+ root,
627
+ claim,
628
+ write,
629
+ exit
630
+ });
631
+ try {
632
+ return await fn(claim);
633
+ } finally {
634
+ dispose();
635
+ }
636
+ }, {
637
+ external: true,
638
+ waitMs: NATIVE_RUN_WAIT_MS,
639
+ declareSpawns: true,
640
+ details: { ...details },
641
+ onHeld: (holder) => notice(holder)
642
+ });
643
+ }
644
+ //#endregion
1017
645
  //#region src/engine/device-remote.ts
1018
646
  const REMOTE_SESSION_ERROR = "STIM_NO_REMOTE_SESSION";
1019
647
  const REMOTE_METRO_ERROR = "STIM_REMOTE_METRO_UNREACHABLE";
@@ -1437,67 +1065,114 @@ function withRemoteSessionLock(root, fn, options = {}) {
1437
1065
  ...options
1438
1066
  });
1439
1067
  }
1440
- async function ensureRemoteBootOwned({ root, platform, sessionName, startedAt, boot, createdSessionId, abandonCreatedSession, webPreviewUrl = () => null, writeState, register = () => {}, withProjectLock = withEasProjectLock, withLock = withRemoteSessionLock, removeEasSessionClaim: removeClaim = removeEasSessionClaim, ledgerRoot = easMachineStateRoot$1() }) {
1068
+ const EAS_START_HOLD_MS = 108e4 + Math.ceil(DAEMON_WAIT_MS / DAEMON_POLL_MS) * 35e3;
1069
+ async function ensureRemoteBootOwned({ root, platform, sessionName, startedAt, boot, createdSessionId, abandonCreatedSession, webPreviewUrl = () => null, writeState, register = () => {}, withProjectLock = withEasProjectLock, withLock = withRemoteSessionLock, removeEasSessionClaim: removeClaim = removeEasSessionClaim, ledgerRoot = easMachineStateRoot$1(), notice = () => {}, now = Date.now }) {
1070
+ const waitNotice = nativeRunWaitNotice({
1071
+ write: notice,
1072
+ now,
1073
+ describe: describeEasProjectLockHolder
1074
+ });
1075
+ let stuck = null;
1076
+ let seen = null;
1077
+ let projectLocked = false;
1441
1078
  try {
1442
- return await withProjectLock(root, () => withLock(root, async () => {
1443
- register();
1444
- const booted = await boot();
1445
- if (booted.failed) return booted;
1446
- const sessionId = createdSessionId();
1447
- if (!sessionId) return booted;
1448
- try {
1449
- recordEasSessionClaim({
1450
- sessionId,
1451
- name: sessionName,
1452
- platform,
1453
- workspaceRoot: root,
1454
- workspaceHome: getConfigDir(),
1455
- stateFile: workspaceStateFile(root)
1456
- }, ledgerRoot);
1457
- const preview = webPreviewUrl();
1458
- writeState(root, { remoteDevice: {
1459
- platform,
1460
- sessionId,
1461
- startedAt,
1462
- ...preview ? { webPreviewUrl: preview } : {}
1463
- } });
1464
- return booted;
1465
- } catch (err) {
1466
- const cleanup = abandonCreatedSession();
1467
- if (cleanup.ok) {
1468
- let removalFailure = "the claim store did not remove it";
1469
- try {
1470
- if (removeClaim(sessionId, ledgerRoot)) return {
1079
+ return await withProjectLock(root, () => {
1080
+ projectLocked = true;
1081
+ return withLock(root, async () => {
1082
+ register();
1083
+ const booted = await boot();
1084
+ if (booted.failed) return booted;
1085
+ const sessionId = createdSessionId();
1086
+ if (!sessionId) return booted;
1087
+ try {
1088
+ recordEasSessionClaim({
1089
+ sessionId,
1090
+ name: sessionName,
1091
+ platform,
1092
+ workspaceRoot: root,
1093
+ workspaceHome: getConfigDir(),
1094
+ stateFile: workspaceStateFile(root)
1095
+ }, ledgerRoot);
1096
+ const preview = webPreviewUrl();
1097
+ writeState(root, { remoteDevice: {
1098
+ platform,
1099
+ sessionId,
1100
+ startedAt,
1101
+ ...preview ? { webPreviewUrl: preview } : {}
1102
+ } });
1103
+ return booted;
1104
+ } catch (err) {
1105
+ const cleanup = abandonCreatedSession();
1106
+ if (cleanup.ok) {
1107
+ let removalFailure = "the claim store did not remove it";
1108
+ try {
1109
+ if (removeClaim(sessionId, ledgerRoot)) return {
1110
+ failed: true,
1111
+ code: "STIM_REMOTE_SESSION_STATE",
1112
+ reason: `Could not record EAS session ${sessionId}: ${describe(err)}. The session was stopped.`,
1113
+ remedy: "Fix workspace state storage, then retry the remote command."
1114
+ };
1115
+ } catch (error) {
1116
+ removalFailure = describe(error);
1117
+ }
1118
+ return {
1471
1119
  failed: true,
1472
- code: "STIM_REMOTE_SESSION_STATE",
1473
- reason: `Could not record EAS session ${sessionId}: ${describe(err)}. The session was stopped.`,
1474
- remedy: "Fix workspace state storage, then retry the remote command."
1120
+ code: "STIM_REMOTE_SESSION_CLEANUP",
1121
+ reason: `Could not record EAS session ${sessionId}: ${describe(err)}. The session was stopped, but its ownership claim could not be removed: ${removalFailure}.`,
1122
+ remedy: `The stopped session claim remains under ${ledgerRoot}. Repair that ownership ledger before retrying the remote command.`
1475
1123
  };
1476
- } catch (error) {
1477
- removalFailure = describe(error);
1478
1124
  }
1479
1125
  return {
1480
1126
  failed: true,
1481
- code: "STIM_REMOTE_SESSION_CLEANUP",
1482
- reason: `Could not record EAS session ${sessionId}: ${describe(err)}. The session was stopped, but its ownership claim could not be removed: ${removalFailure}.`,
1483
- remedy: `The stopped session claim remains under ${ledgerRoot}. Repair that ownership ledger before retrying the remote command.`
1127
+ code: cleanup.code ?? "STIM_REMOTE_SESSION_CLEANUP",
1128
+ reason: `Could not record EAS session ${sessionId}: ${describe(err)}. ${cleanup.reason ?? ""}`.trim(),
1129
+ remedy: cleanup.remedy ?? `Run \`eas simulator:stop --id ${sessionId}\`.`
1484
1130
  };
1485
1131
  }
1486
- return {
1487
- failed: true,
1488
- code: cleanup.code ?? "STIM_REMOTE_SESSION_CLEANUP",
1489
- reason: `Could not record EAS session ${sessionId}: ${describe(err)}. ${cleanup.reason ?? ""}`.trim(),
1490
- remedy: cleanup.remedy ?? `Run \`eas simulator:stop --id ${sessionId}\`.`
1132
+ });
1133
+ }, {
1134
+ ownerPurpose: "EAS remote start",
1135
+ machineRoot: ledgerRoot,
1136
+ waitMs: Number.POSITIVE_INFINITY,
1137
+ details: {
1138
+ workspace: root,
1139
+ platform
1140
+ },
1141
+ now,
1142
+ onHeld: (holder) => {
1143
+ const at = now();
1144
+ if (seen?.claimId !== holder.claimId) seen = {
1145
+ claimId: holder.claimId,
1146
+ at
1491
1147
  };
1148
+ const claimedAt = Date.parse(holder.startedAt);
1149
+ const heldMs = at - (Number.isFinite(claimedAt) ? Math.min(claimedAt, seen.at) : seen.at);
1150
+ if (heldMs > EAS_START_HOLD_MS) {
1151
+ stuck = {
1152
+ holder,
1153
+ heldMs
1154
+ };
1155
+ const error = /* @__PURE__ */ new Error(`The eas-project lock was held past ${formatElapsed(EAS_START_HOLD_MS)}.`);
1156
+ error.code = "STIM_LOCK_TIMEOUT";
1157
+ throw error;
1158
+ }
1159
+ waitNotice(holder, `waiting for ${describeEasProjectLockHolder(holder, at)} to release the EAS project lock; one EAS session starts at a time on this machine`);
1492
1160
  }
1493
- }), {
1494
- ownerPurpose: "EAS remote start",
1495
- machineRoot: ledgerRoot
1496
1161
  });
1497
1162
  } catch (err) {
1163
+ const code = err.code ?? "STIM_NO_REMOTE_SESSION";
1164
+ if (stuck && !projectLocked) {
1165
+ const { holder, heldMs } = stuck;
1166
+ return {
1167
+ failed: true,
1168
+ code,
1169
+ reason: `${describeEasProjectLockHolder(holder, now())} has held the EAS project lock for ${formatElapsed(heldMs)}; an EAS session start holds it for at most ${formatElapsed(EAS_START_HOLD_MS)}.`,
1170
+ remedy: `If pid ${holder.owner.pid} is stuck, stop it, then run the remote command again.`
1171
+ };
1172
+ }
1498
1173
  return {
1499
1174
  failed: true,
1500
- code: err.code ?? "STIM_NO_REMOTE_SESSION",
1175
+ code,
1501
1176
  reason: describe(err),
1502
1177
  remedy: "Retry the remote command after the other workspace operation finishes."
1503
1178
  };
@@ -1740,4 +1415,4 @@ function bundleEntryPoint(root, isExpo) {
1740
1415
  return local ? normalized : `node_modules/${normalized}`;
1741
1416
  }
1742
1417
  //#endregion
1743
- export { UPLOAD_TIMEOUT_MS as A, resolveRemote as B, listOwnedSessionsArgs as C, verifyStoppedSession as D, stopSessionArgs as E, isEasAuthFailureText as F, loadProjectProvider as I, ownerFromConfig as L, checkEasAuth as M, easAuthNote as N, gateMetroOrigin as O, exitAfterFlush as P, providerFromConfig as R, isDefinitiveMissingSessionError as S, parseSessionListPage as T, uploadRemote as V, easCliSupport as _, ensureMetroReachable as a, getSessionArgs as b, remoteAndroidDeps as c, withRemoteSessionLock as d, withEasProjectLock as f, MIN_EAS_CLI_SIMULATOR_VERSION as g, removeEasSessionClaim as h, endRecordedSession as i, cacheLevel as j, RESOLVE_TIMEOUT_MS as k, remoteIosDeps as l, readEasSessionLedger$1 as m, binOnPath as n, ensureRemoteBootOwned as o, easMachineStateRoot$1 as p, bundleEntryPoint as r, readInstalledEasCliVersion as s, REMOTE_SESSION_ERROR as t, resolveRemoteContext as u, easCliUpgradeRemedy as v, ownedSessionName as w, inspectSessionForTeardown as x, findOrphanedOwnedSessions as y, resolveEasCliBin as z };
1418
+ export { findOrphanedOwnedSessions as A, withEasProjectLock as C, MIN_EAS_CLI_SIMULATOR_VERSION as D, removeEasSessionClaim as E, ownedSessionName as F, parseSessionListPage as I, stopSessionArgs as L, inspectSessionForTeardown as M, isDefinitiveMissingSessionError as N, easCliSupport as O, listOwnedSessionsArgs as P, verifyStoppedSession as R, withNativeBuildRun as S, readEasSessionLedger$1 as T, describeNativeRunHolder as _, ensureMetroReachable as a, requestNativeRunCancel as b, remoteAndroidDeps as c, withRemoteSessionLock as d, NATIVE_RUN_LOCK as f, decideStopAction as g, clearNativeRunCancel as h, endRecordedSession as i, getSessionArgs as j, easCliUpgradeRemedy as k, remoteIosDeps as l, cancelledFailure as m, binOnPath as n, ensureRemoteBootOwned as o, NATIVE_RUN_WAIT_MS as p, bundleEntryPoint as r, readInstalledEasCliVersion as s, REMOTE_SESSION_ERROR as t, resolveRemoteContext as u, nativeRunHolder as v, easMachineStateRoot$1 as w, runCancellation as x, nativeRunWaitNotice as y, gateMetroOrigin as z };