stim 1.8.0 → 1.10.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 (91) hide show
  1. package/README.md +23 -155
  2. package/dist/{android-DWssqHQ8.mjs → android-787HhP5w.mjs} +168 -39
  3. package/dist/{android-BRgBYzU0.mjs → android-DBkeHwpm.mjs} +250 -200
  4. package/dist/{android-DeYdjjBP.mjs → android-DPov-fv2.mjs} +2 -2
  5. package/dist/{android-cas-SWIjzeuL.mjs → android-cas-CITmIeWY.mjs} +22 -14
  6. package/dist/android-cas-compiler.mjs +2 -2
  7. package/dist/app-install-CrJ6t_bv.mjs +819 -0
  8. package/dist/budget-BP4jZZL4.mjs +620 -0
  9. package/dist/build-progress-BXx-ZBe4.mjs +512 -0
  10. package/dist/{cache-manifest-CYJi8-Hx.mjs → cache-manifest-Pubfp8wV.mjs} +4 -4
  11. package/dist/cache-manifest.mjs +1 -1
  12. package/dist/cli.mjs +22 -19
  13. package/dist/collector-run.d.mts +7 -1
  14. package/dist/collector-run.mjs +10 -9
  15. package/dist/{command-output-B3ycpsYv.mjs → command-output-D5z2_bcF.mjs} +11 -2
  16. package/dist/{config-B7iyKWkL.mjs → config-BEq0Qw_a.mjs} +134 -121
  17. package/dist/{dependency-state-D0M7oKA7.mjs → dependency-state-r9UesA9a.mjs} +2 -2
  18. package/dist/{deps-oCXVNEqQ.mjs → deps-DP4eWqea.mjs} +8 -5
  19. package/dist/{detached-entry-Bpo6ZY_I.mjs → detached-entry-DJHoca49.mjs} +1 -1
  20. package/dist/{device-BUoZyMoR.mjs → dev-client-92U593KT.mjs} +1090 -660
  21. package/dist/{device-RdujjHLB.mjs → device-DhTZe7L4.mjs} +13 -20
  22. package/dist/{build-lock-CgtsB6TV.mjs → device-lease-CbEaIk7b.mjs} +481 -27
  23. package/dist/{device-pool-BxRwdufO.mjs → device-pool-Di8Q-9Ro.mjs} +7 -7
  24. package/dist/{device-remote-ByN8P6CB.mjs → device-remote-Dy1jGj3U.mjs} +645 -119
  25. package/dist/{doctor-BMt156qe.mjs → doctor-B-inAJmt.mjs} +137 -113
  26. package/dist/{error-diagnostics-D1Kaf-eT.mjs → error-diagnostics-xxWKNO3-.mjs} +22 -17
  27. package/dist/exec-DK8KaVHK.mjs +133 -0
  28. package/dist/{gc-v2YcoJvm.mjs → gc-ChPFZMRC.mjs} +836 -813
  29. package/dist/{guide-BTAlD1_y.mjs → guide-DwmaD_Wa.mjs} +953 -209
  30. package/dist/guide-status-CWvti2Lg.mjs +125 -0
  31. package/dist/idle-DpRLp5lj.mjs +157 -0
  32. package/dist/{build-slots-C-oAHRT1.mjs → in-use-CjUluqIX.mjs} +102 -8
  33. package/dist/{ios-BcCiI4TS.mjs → ios-B2Fpqvlb.mjs} +60 -71
  34. package/dist/{ios-BP04pY5f.mjs → ios-Dd-hYJBh.mjs} +241 -234
  35. package/dist/{ios-device-CFXA46ez.mjs → ios-device-BiOsSVzd.mjs} +3 -2
  36. package/dist/{ios-device-CauFJiYx.mjs → ios-device-CtRdEVjx.mjs} +149 -52
  37. package/dist/launch-verify-CQS_Kg-C.mjs +365 -0
  38. package/dist/{logs-CakdBTtm.mjs → logs-MARIy6m9.mjs} +20 -15
  39. package/dist/metro-BWLOJ_Wr.mjs +409 -0
  40. package/dist/{named-ports-DzMOlmaX.mjs → named-ports-DiTfSz6p.mjs} +4 -4
  41. package/dist/{ndjson-BZwI1NBv.mjs → ndjson-CDY9IkVC.mjs} +23 -43
  42. package/dist/{ndjson-DvHSsNU9.d.mts → ndjson-Du3j0AQ9.d.mts} +1 -0
  43. package/dist/{ownership-Cp52QAK1.mjs → ownership-D2xbLVoz.mjs} +2 -3
  44. package/dist/{teardown-BJqEzPo_.mjs → ownership-DZjdx8Gq.mjs} +664 -612
  45. package/dist/{ports-BIbJaKNW.mjs → ports-CLSSFOuz.mjs} +3 -3
  46. package/dist/{process-output-Dl5uKE03.mjs → process-output-Fpl8PYi8.mjs} +1 -1
  47. package/dist/{project-DDZWf_16.mjs → project-BXZzOABt.mjs} +8 -3
  48. package/dist/{reload-BdypPbjM.mjs → reload-DoETJ3rl.mjs} +11 -13
  49. package/dist/{server-bare-BQkyxa_b.mjs → server-bare-DVbb7kXH.mjs} +2 -2
  50. package/dist/{server-expo-BVAA1OqJ.mjs → server-expo-BvOCeakp.mjs} +8 -7
  51. package/dist/server-expo-DBVTdult.mjs +2 -0
  52. package/dist/{settings-BXXorVM2.mjs → settings-DOugPlPz.mjs} +59 -159
  53. package/dist/settings-EWYNwe72.mjs +378 -0
  54. package/dist/settings.schema.json +1064 -0
  55. package/dist/simslim-Ca3487H6.mjs +436 -0
  56. package/dist/{slot-launch-ZUUe0TQP.mjs → slot-launch-C5cg0fmk.mjs} +4 -4
  57. package/dist/spawn-claims-CUleC2cM.mjs +49 -0
  58. package/dist/{start-CmxtOKjU.mjs → start-F9gv5dcC.mjs} +159 -114
  59. package/dist/start-VoFsPSes.mjs +2 -0
  60. package/dist/{state-DZobzxLW.mjs → state-WGdIbi_d.mjs} +5 -3
  61. package/dist/{stats-BYcZ6n1h.mjs → stats-BAgJaLA4.mjs} +66 -5
  62. package/dist/{stats-CLg5RuDS.mjs → stats-DBYJ5bxL.mjs} +4 -4
  63. package/dist/status-DLCD-o3s.mjs +764 -0
  64. package/dist/status-Dr4fRkym.mjs +216 -0
  65. package/dist/{guide-status-Craq9z0t.mjs → stim-installations-CINXy_su.mjs} +3 -122
  66. package/dist/{stop-C8JW6kKp.mjs → stop-Dj4pfmyv.mjs} +111 -31
  67. package/dist/{stop-B1kOpYei.mjs → stop-ZvOeTmKb.mjs} +2 -2
  68. package/dist/supervisor-run.d.mts +11 -2
  69. package/dist/supervisor-run.mjs +228 -42
  70. package/dist/{workspace-process-lock-BYfZz_S-.mjs → workspace-process-lock-BXA96myD.mjs} +14 -9
  71. package/dist/workspaces-BTUyFfkv.mjs +1003 -0
  72. package/dist/worktree-4aZg2NnI.mjs +2 -0
  73. package/dist/{worktree-CFLA-6MX.mjs → worktree-BMS7GeqB.mjs} +523 -61
  74. package/dist/{worktree-FG_ZKKdb.mjs → worktree-BlizKDpZ.mjs} +200 -54
  75. package/dist/{xcode-B6YaWeg0.mjs → xcode-B2tBkf0m.mjs} +66 -57
  76. package/package.json +6 -6
  77. package/skill/SKILL.md +2 -0
  78. package/dist/app-install-BDbsSgCl.mjs +0 -1175
  79. package/dist/dev-client-BmT-ioFI.mjs +0 -797
  80. package/dist/device-lease-Ffz_M5t0.mjs +0 -359
  81. package/dist/exec-B29XNuhP.mjs +0 -77
  82. package/dist/logs-query-BGL-Pl12.mjs +0 -290
  83. package/dist/metro-C7bcJ0uo.mjs +0 -233
  84. package/dist/ownership-wolE-0Do.mjs +0 -470
  85. package/dist/public-http-probe-CyMDrQ3c.mjs +0 -61
  86. package/dist/reclaim-KJ-0uWEF.mjs +0 -361
  87. package/dist/server-expo-YDDG3eP7.mjs +0 -2
  88. package/dist/spawn-entry-nCE6m770.mjs +0 -14
  89. package/dist/status-DdkLki47.mjs +0 -426
  90. package/dist/workspace-state-BT5E3SA2.mjs +0 -146
  91. package/dist/{metro-store-CDDZHtPl.mjs → metro-store-DzeWkfqW.mjs} +1 -1
@@ -1,16 +1,538 @@
1
- import { t as getExecutor } from "./exec-B29XNuhP.mjs";
2
- import { D as withDirLock, u as getConfigDir } from "./config-B7iyKWkL.mjs";
3
- import { _ as workspaceLogsDir, h as workspaceDir, v as workspaceName, y as workspaceStateFile } from "./workspace-state-BT5E3SA2.mjs";
4
- import { a as clearRemoteSession, l as readRemoteSession, s as readMetroTunnel } from "./state-DZobzxLW.mjs";
5
- import { i as pidExists } from "./metro-C7bcJ0uo.mjs";
6
- import { z as planMetroReach } from "./settings-BXXorVM2.mjs";
7
- import { t as withWorkspaceProcessLock } from "./workspace-process-lock-BYfZz_S-.mjs";
8
- import { T as readMetroRecords, a as LAUNCH_ERROR, b as isBundleProof, p as devClientDeepLink, r as INSTALL_ERROR } from "./app-install-BDbsSgCl.mjs";
9
- import { S as sanitizeDeviceLabel } from "./ios-BcCiI4TS.mjs";
10
- import { t as probePublicHttp } from "./public-http-probe-CyMDrQ3c.mjs";
1
+ import { u as getConfigDir } from "./config-BEq0Qw_a.mjs";
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-BXA96myD.mjs";
8
+ import { n as stripAnsi } from "./process-output-Fpl8PYi8.mjs";
9
+ import { B as planMetroReach } from "./settings-DOugPlPz.mjs";
10
+ import { u as probePublicHttp } from "./ownership-DZjdx8Gq.mjs";
11
+ import { C as sanitizeDeviceLabel } from "./ios-B2Fpqvlb.mjs";
12
+ import { n as expoBinFromPackage, r as expoBinPath, s as findBinUpward } from "./server-expo-BvOCeakp.mjs";
13
+ import { a as LAUNCH_ERROR, i as INSTALL_ERROR, l as devClientDeepLink } from "./app-install-CrJ6t_bv.mjs";
14
+ import { c as readMetroRecords, o as isBundleProof } from "./launch-verify-CQS_Kg-C.mjs";
15
+ import { t as compareStimVersions } from "./stim-installations-CINXy_su.mjs";
16
+ import { createRequire } from "node:module";
11
17
  import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
12
- import { dirname, isAbsolute, join, relative, resolve } from "node:path";
13
- import { homedir } from "node:os";
18
+ import { easMachineStateRoot, easMachineStateRoot as easMachineStateRoot$1, easSessionLedgerFile, easSessionLedgerLock, readEasSessionLedger, readEasSessionLedger as readEasSessionLedger$1, readJsonObject, validEasSessionClaim } from "@stim-cli/core/state";
19
+ 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
14
536
  //#region src/engine/metro-gate.ts
15
537
  const REMOTE_METRO_WRONG = "STIM_REMOTE_METRO_WRONG";
16
538
  const GATE_TIMEOUT_MS = 25e3;
@@ -81,7 +603,11 @@ function remoteProfile({ daemon, platform, label }) {
81
603
  session: scope,
82
604
  tenant: scope,
83
605
  runId: scope,
84
- sessionIsolation: "tenant"
606
+ sessionIsolation: "tenant",
607
+ ...platform === "android" ? {
608
+ leaseProvider: "proxy",
609
+ clientId: scope
610
+ } : {}
85
611
  };
86
612
  }
87
613
  function daemonEnv(daemon) {
@@ -144,6 +670,18 @@ const LIVE_STATUSES$1 = ["new", "in-progress"];
144
670
  const LIVE_SESSION_STATUSES = /* @__PURE__ */ new Set(["NEW", "IN_PROGRESS"]);
145
671
  const TERMINAL_SESSION_STATUSES = /* @__PURE__ */ new Set(["STOPPED", "ERRORED"]);
146
672
  const SESSION_PLATFORMS = /* @__PURE__ */ new Set(["ios", "android"]);
673
+ const MIN_EAS_CLI_SIMULATOR_VERSION = "21.6.0";
674
+ function easCliUpgradeRemedy(minimum) {
675
+ return `Upgrade eas-cli to ${minimum} or later (\`npm install --global eas-cli@latest\`, or the project's eas-cli dependency)`;
676
+ }
677
+ function easCliSupport(versionOutput, minimum) {
678
+ const version = /\beas-cli\/(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/.exec(versionOutput ?? "")?.[1] ?? null;
679
+ const order = version ? compareStimVersions(version, minimum) : null;
680
+ return {
681
+ supported: order !== null && order >= 0,
682
+ version
683
+ };
684
+ }
147
685
  function ownedSessionName(label) {
148
686
  const clean = sanitizeDeviceLabel(label);
149
687
  return `${OWNED_PREFIX}${clean.startsWith(OWNED_PREFIX) ? clean.slice(5) : clean}`;
@@ -413,80 +951,14 @@ function parseStoppedSession(stdout) {
413
951
  };
414
952
  }
415
953
  //#endregion
954
+ //#region src/engine/eas-machine-root-guard-env.ts
955
+ const EAS_TEST_GUARD_ROOT_ENV = "STIM_TEST_GUARD_REAL_EAS_ROOT";
956
+ //#endregion
416
957
  //#region src/engine/eas-session-ledger.ts
417
- function easMachineStateRoot() {
418
- return join(homedir(), ".stim", "machine", "eas");
419
- }
420
- function easSessionLedgerFile(root = easMachineStateRoot()) {
421
- return join(root, "sessions.json");
422
- }
423
- function validClaim(id, value) {
424
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
425
- const claim = value;
426
- if (claim.sessionId !== id) return null;
427
- if (typeof claim.name !== "string" || !claim.name.startsWith("stim-")) return null;
428
- if (claim.platform !== "ios" && claim.platform !== "android") return null;
429
- if (typeof claim.workspaceRoot !== "string" || !isAbsolute(claim.workspaceRoot)) return null;
430
- if (typeof claim.workspaceHome !== "string" || !isAbsolute(claim.workspaceHome)) return null;
431
- if (typeof claim.stateFile !== "string" || !isAbsolute(claim.stateFile)) return null;
432
- const workspaceRoot = resolve(claim.workspaceRoot);
433
- const workspaceHome = resolve(claim.workspaceHome);
434
- const stateFile = resolve(claim.stateFile);
435
- const stateRelative = relative(workspaceHome, stateFile);
436
- if (!stateRelative || stateRelative.startsWith("..") || isAbsolute(stateRelative)) return null;
437
- if (stateFile !== join(workspaceHome, "workspaces", workspaceName(workspaceRoot), "state.json")) return null;
438
- return {
439
- sessionId: id,
440
- name: claim.name,
441
- platform: claim.platform,
442
- workspaceRoot,
443
- workspaceHome,
444
- stateFile
445
- };
446
- }
447
- function readEasSessionLedger(root = easMachineStateRoot()) {
448
- const file = easSessionLedgerFile(root);
449
- if (!existsSync(file)) return {
450
- claims: /* @__PURE__ */ new Map(),
451
- notice: null,
452
- safe: true
453
- };
454
- let parsed;
455
- try {
456
- parsed = JSON.parse(readFileSync(file, "utf-8"));
457
- } catch (error) {
458
- return {
459
- claims: /* @__PURE__ */ new Map(),
460
- notice: `EAS ownership ledger ${file} is unreadable: ${error.message}`,
461
- safe: false
462
- };
463
- }
464
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {
465
- claims: /* @__PURE__ */ new Map(),
466
- notice: `EAS ownership ledger ${file} is malformed.`,
467
- safe: false
468
- };
469
- const ledger = parsed;
470
- if (ledger.version !== 1 || !ledger.claims || typeof ledger.claims !== "object" || Array.isArray(ledger.claims)) return {
471
- claims: /* @__PURE__ */ new Map(),
472
- notice: `EAS ownership ledger ${file} is malformed.`,
473
- safe: false
474
- };
475
- const claims = /* @__PURE__ */ new Map();
476
- for (const [id, value] of Object.entries(ledger.claims)) {
477
- const claim = validClaim(id, value);
478
- if (!claim) return {
479
- claims: /* @__PURE__ */ new Map(),
480
- notice: `EAS ownership ledger ${file} is malformed.`,
481
- safe: false
482
- };
483
- claims.set(id, claim);
484
- }
485
- return {
486
- claims,
487
- notice: null,
488
- safe: true
489
- };
958
+ function assertEasMachineRootWritable(root) {
959
+ const guardedRoot = process.env[EAS_TEST_GUARD_ROOT_ENV];
960
+ if (!guardedRoot || resolve(root) !== resolve(guardedRoot)) return;
961
+ throw new Error(`Refusing to write the real EAS machine root ${root} from a test process. Pass a temporary ledgerRoot, machineRoot, or easLedgerRoot, or point HOME (and USERPROFILE on Windows) at a temporary directory.`);
490
962
  }
491
963
  function writeLedger(root, ledger) {
492
964
  mkdirSync(root, { recursive: true });
@@ -501,9 +973,10 @@ function writeLedger(root, ledger) {
501
973
  }
502
974
  }
503
975
  function recordEasSessionClaim(claim, root = easMachineStateRoot()) {
504
- const normalized = validClaim(claim.sessionId, claim);
976
+ assertEasMachineRootWritable(root);
977
+ const normalized = validEasSessionClaim(claim.sessionId, claim);
505
978
  if (!normalized) throw new Error(`Invalid EAS session claim for ${claim.sessionId}.`);
506
- withDirLock(join(root, "ledger.lock"), () => {
979
+ withDirLock(easSessionLedgerLock(root), () => {
507
980
  const current = readEasSessionLedger(root);
508
981
  if (!current.safe) throw new Error(current.notice ?? "EAS ownership ledger is unreadable.");
509
982
  current.claims.set(normalized.sessionId, normalized);
@@ -511,12 +984,13 @@ function recordEasSessionClaim(claim, root = easMachineStateRoot()) {
511
984
  version: 1,
512
985
  claims: Object.fromEntries(current.claims)
513
986
  });
514
- }, { ensureParent: () => mkdirSync(dirname(join(root, "ledger.lock")), { recursive: true }) });
987
+ }, { ensureParent: () => mkdirSync(dirname(easSessionLedgerLock(root)), { recursive: true }) });
515
988
  }
516
989
  function removeEasSessionClaim(sessionId, root = easMachineStateRoot()) {
517
990
  const file = easSessionLedgerFile(root);
518
991
  if (!existsSync(file)) return false;
519
- return withDirLock(join(root, "ledger.lock"), () => {
992
+ assertEasMachineRootWritable(root);
993
+ return withDirLock(easSessionLedgerLock(root), () => {
520
994
  const current = readEasSessionLedger(root);
521
995
  if (!current.safe) return false;
522
996
  if (!current.claims.delete(sessionId)) return false;
@@ -531,7 +1005,8 @@ function removeEasSessionClaim(sessionId, root = easMachineStateRoot()) {
531
1005
  //#region src/engine/eas-project-lock.ts
532
1006
  const EAS_PROJECT_LOCK_WAIT_MS = 24e4;
533
1007
  function withEasProjectLock(_root, fn, options = {}) {
534
- const { machineRoot = easMachineStateRoot(), ...lockOptions } = options;
1008
+ const { machineRoot = easMachineStateRoot$1(), ...lockOptions } = options;
1009
+ assertEasMachineRootWritable(machineRoot);
535
1010
  return withWorkspaceProcessLock(machineRoot, "eas-project", fn, {
536
1011
  waitMs: EAS_PROJECT_LOCK_WAIT_MS,
537
1012
  ...lockOptions,
@@ -559,7 +1034,9 @@ function resolveMetroOrigin({ metroPort, publicUrl = null, mode = "auto", isExpo
559
1034
  }
560
1035
  function describe(err) {
561
1036
  const e = err;
562
- return (typeof e?.stderr === "string" ? e.stderr.trim() : "") || String(e?.message ?? err);
1037
+ const stderr = typeof e?.stderr === "string" ? e.stderr.trim() : "";
1038
+ if (err?.code === "ETIMEDOUT") return String(e?.message ?? err);
1039
+ return stderr || String(e?.message ?? err);
563
1040
  }
564
1041
  const PROXY_CREDENTIAL_ENV = ["AGENT_DEVICE_DAEMON_BASE_URL", "AGENT_DEVICE_DAEMON_AUTH_TOKEN"];
565
1042
  function easExecOptions(root) {
@@ -569,6 +1046,9 @@ function easExecOptions(root) {
569
1046
  };
570
1047
  }
571
1048
  const EAS_OPERATION_TIMEOUT_MS = 3e4;
1049
+ const EAS_SESSION_CREATE_TIMEOUT_MS = 6e5;
1050
+ const AGENT_DEVICE_TIMEOUT_MS = 12e4;
1051
+ const AGENT_DEVICE_INSTALL_TIMEOUT_MS = 6e5;
572
1052
  function easBoundedExecOptions(root) {
573
1053
  return {
574
1054
  ...easExecOptions(root),
@@ -643,8 +1123,22 @@ function remoteDeviceDeps(ctx) {
643
1123
  label: ctx.label,
644
1124
  platform: ctx.platform ?? "ios",
645
1125
  maxDurationMinutes: ctx.maxDurationMinutes ?? null
646
- }), easEnv);
1126
+ }), {
1127
+ ...easEnv,
1128
+ timeoutMs: EAS_SESSION_CREATE_TIMEOUT_MS,
1129
+ killSignal: "SIGKILL"
1130
+ });
647
1131
  } catch (err) {
1132
+ if (err?.code === "ETIMEDOUT") {
1133
+ const partial = parseCreatedSession(String(err.stdout ?? ""));
1134
+ if (partial) return abandonSession(ctx, partial.id, `eas sim did not finish within ${EAS_SESSION_CREATE_TIMEOUT_MS / 6e4} minutes after creating session ${partial.id}.`);
1135
+ const name = ownedSessionName(ctx.label);
1136
+ return {
1137
+ failed: true,
1138
+ reason: `eas sim did not finish within ${EAS_SESSION_CREATE_TIMEOUT_MS / 6e4} minutes and the eas process was killed. EAS may still have started session ${name}; it has no Stim ownership claim, so \`stim gc\` will not stop it.`,
1139
+ remedy: `Run \`eas simulator:list --name ${name}\` and stop any running session with \`eas simulator:stop --id <id>\`, then retry.`
1140
+ };
1141
+ }
648
1142
  return {
649
1143
  failed: true,
650
1144
  reason: `eas sim failed: ${describe(err)}`
@@ -674,13 +1168,17 @@ function remoteDeviceDeps(ctx) {
674
1168
  try {
675
1169
  getExecutor().runFile(ctx.agentDeviceBin, closeArgs(profilePath), {
676
1170
  cwd: ctx.root,
677
- env: daemonEnv(daemon)
1171
+ env: daemonEnv(daemon),
1172
+ timeoutMs: AGENT_DEVICE_TIMEOUT_MS,
1173
+ killSignal: "SIGKILL"
678
1174
  });
679
1175
  } catch {}
680
1176
  try {
681
1177
  getExecutor().runFile(ctx.agentDeviceBin, connectArgs(profilePath), {
682
1178
  cwd: ctx.root,
683
- env: daemonEnv(daemon)
1179
+ env: daemonEnv(daemon),
1180
+ timeoutMs: AGENT_DEVICE_TIMEOUT_MS,
1181
+ killSignal: "SIGKILL"
684
1182
  });
685
1183
  } catch (err) {
686
1184
  if (createdHere) return abandonSession(ctx, createdHere, `agent-device connect failed: ${describe(err)}`);
@@ -706,7 +1204,9 @@ function remoteDeviceDeps(ctx) {
706
1204
  try {
707
1205
  getExecutor().runFile(ctx.agentDeviceBin, installArgs(session.profilePath, artifactPath), {
708
1206
  cwd: ctx.root,
709
- env: daemonEnv(session.daemon)
1207
+ env: daemonEnv(session.daemon),
1208
+ timeoutMs: AGENT_DEVICE_INSTALL_TIMEOUT_MS,
1209
+ killSignal: "SIGKILL"
710
1210
  });
711
1211
  return {
712
1212
  ok: true,
@@ -725,7 +1225,9 @@ function remoteDeviceDeps(ctx) {
725
1225
  if (metroPort === null) try {
726
1226
  getExecutor().runFile(ctx.agentDeviceBin, openArgs(session.profilePath, appId, null, null), {
727
1227
  cwd: ctx.root,
728
- env: daemonEnv(session.daemon)
1228
+ env: daemonEnv(session.daemon),
1229
+ timeoutMs: AGENT_DEVICE_TIMEOUT_MS,
1230
+ killSignal: "SIGKILL"
729
1231
  });
730
1232
  return {
731
1233
  ok: true,
@@ -753,12 +1255,16 @@ function remoteDeviceDeps(ctx) {
753
1255
  try {
754
1256
  getExecutor().runFile(ctx.agentDeviceBin, openArgs(session.profilePath, appId, url, metroHintFrom(origin.origin)), {
755
1257
  cwd: ctx.root,
756
- env: daemonEnv(session.daemon)
1258
+ env: daemonEnv(session.daemon),
1259
+ timeoutMs: AGENT_DEVICE_TIMEOUT_MS,
1260
+ killSignal: "SIGKILL"
757
1261
  });
758
1262
  if (url) try {
759
1263
  getExecutor().runFile(ctx.agentDeviceBin, acceptAlertArgs(session.profilePath), {
760
1264
  cwd: ctx.root,
761
- env: daemonEnv(session.daemon)
1265
+ env: daemonEnv(session.daemon),
1266
+ timeoutMs: AGENT_DEVICE_TIMEOUT_MS,
1267
+ killSignal: "SIGKILL"
762
1268
  });
763
1269
  } catch {}
764
1270
  return {
@@ -788,10 +1294,11 @@ function remoteDeviceDeps(ctx) {
788
1294
  }
789
1295
  return stopped;
790
1296
  },
791
- webPreviewUrl: () => session?.daemon.webPreviewUrl ?? null
1297
+ webPreviewUrl: () => session?.daemon.webPreviewUrl ?? null,
1298
+ failureRemedy: () => ctx.backend === "eas" && session?.id ? `EAS Simulator session ${session.id} is still running and billed until it reaches its time limit or \`stim stop\` ends it. Fix the error above and run this command again to reuse the session, or run \`stim stop\` to end it.` : "Check that the remote daemon is reachable and still has the device, then run this command again."
792
1299
  };
793
1300
  }
794
- async function resolveRemoteContext({ root, label, backend, platform = "ios", easBin, env = process.env, lookupAgentDevice = defaultLookupAgentDevice, maxDurationMinutes = null }) {
1301
+ async function resolveRemoteContext({ root, backend, platform = "ios", easBin, env = process.env, lookupAgentDevice = defaultLookupAgentDevice, readEasCliVersion = readInstalledEasCliVersion, maxDurationMinutes = null }) {
795
1302
  const agentDeviceBin = lookupAgentDevice();
796
1303
  if (!agentDeviceBin) return {
797
1304
  failed: "agent-device is not on PATH, and it is what drives a remote device.",
@@ -825,9 +1332,17 @@ async function resolveRemoteContext({ root, label, backend, platform = "ios", ea
825
1332
  remedy: "Install eas-cli, then run the device command with `--remote eas` again.",
826
1333
  code: "STIM_REMOTE_EAS_UNAVAILABLE"
827
1334
  };
1335
+ else {
1336
+ const { supported, version } = easCliSupport(readEasCliVersion(easBin, root), MIN_EAS_CLI_SIMULATOR_VERSION);
1337
+ if (!supported) return {
1338
+ failed: version ? `eas-cli ${version} (${easBin}) has no EAS Simulator commands; the eas backend needs eas-cli ${MIN_EAS_CLI_SIMULATOR_VERSION} or later.` : `Could not read an eas-cli version from \`${easBin} --version\`; the eas backend needs eas-cli ${MIN_EAS_CLI_SIMULATOR_VERSION} or later.`,
1339
+ remedy: `${easCliUpgradeRemedy(MIN_EAS_CLI_SIMULATOR_VERSION)}, then run the device command with \`--remote eas\` again.`,
1340
+ code: "STIM_REMOTE_EAS_UNAVAILABLE"
1341
+ };
1342
+ }
828
1343
  return { ctx: {
829
1344
  root,
830
- label,
1345
+ label: `${ownedDeviceLabel(root)}-${workspaceId(root)}`,
831
1346
  backend,
832
1347
  platform,
833
1348
  easBin: easBin ?? "",
@@ -837,6 +1352,13 @@ async function resolveRemoteContext({ root, label, backend, platform = "ios", ea
837
1352
  existingDaemon
838
1353
  } };
839
1354
  }
1355
+ function readInstalledEasCliVersion(easBin, root) {
1356
+ try {
1357
+ return getExecutor().runFile(easBin, ["--version"], easBoundedExecOptions(root));
1358
+ } catch {
1359
+ return null;
1360
+ }
1361
+ }
840
1362
  function binOnPath(bin) {
841
1363
  try {
842
1364
  return Boolean(getExecutor().findExecutable(bin));
@@ -915,7 +1437,7 @@ function withRemoteSessionLock(root, fn, options = {}) {
915
1437
  ...options
916
1438
  });
917
1439
  }
918
- async function ensureRemoteBootOwned({ root, platform, sessionName, startedAt, boot, createdSessionId, abandonCreatedSession, writeState, register = () => {}, withProjectLock = withEasProjectLock, withLock = withRemoteSessionLock, removeEasSessionClaim: removeClaim = removeEasSessionClaim, ledgerRoot = easMachineStateRoot() }) {
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() }) {
919
1441
  try {
920
1442
  return await withProjectLock(root, () => withLock(root, async () => {
921
1443
  register();
@@ -932,10 +1454,12 @@ async function ensureRemoteBootOwned({ root, platform, sessionName, startedAt, b
932
1454
  workspaceHome: getConfigDir(),
933
1455
  stateFile: workspaceStateFile(root)
934
1456
  }, ledgerRoot);
1457
+ const preview = webPreviewUrl();
935
1458
  writeState(root, { remoteDevice: {
936
1459
  platform,
937
1460
  sessionId,
938
- startedAt
1461
+ startedAt,
1462
+ ...preview ? { webPreviewUrl: preview } : {}
939
1463
  } });
940
1464
  return booted;
941
1465
  } catch (err) {
@@ -1041,7 +1565,8 @@ function remoteIosDeps(ctx) {
1041
1565
  }),
1042
1566
  createdSessionId: core.createdSessionId,
1043
1567
  abandonCreatedSession: core.abandonCreatedSession,
1044
- webPreviewUrl: core.webPreviewUrl
1568
+ webPreviewUrl: core.webPreviewUrl,
1569
+ failureRemedy: core.failureRemedy
1045
1570
  };
1046
1571
  }
1047
1572
  function remoteAndroidDeps(ctx) {
@@ -1069,7 +1594,8 @@ function remoteAndroidDeps(ctx) {
1069
1594
  }),
1070
1595
  createdSessionId: core.createdSessionId,
1071
1596
  abandonCreatedSession: core.abandonCreatedSession,
1072
- webPreviewUrl: core.webPreviewUrl
1597
+ webPreviewUrl: core.webPreviewUrl,
1598
+ failureRemedy: core.failureRemedy
1073
1599
  };
1074
1600
  }
1075
1601
  function endRecordedSession({ root, sessionId, easBin, lookupAgentDevice, ledgerRoot }) {
@@ -1091,10 +1617,14 @@ function endRecordedSession({ root, sessionId, easBin, lookupAgentDevice, ledger
1091
1617
  function teardownRemote(ctx, { sessionId }) {
1092
1618
  const profilePath = remoteProfilePath(ctx.root);
1093
1619
  try {
1094
- getExecutor().runFile(ctx.agentDeviceBin, disconnectArgs(profilePath), { cwd: ctx.root });
1620
+ getExecutor().runFile(ctx.agentDeviceBin, disconnectArgs(profilePath), {
1621
+ cwd: ctx.root,
1622
+ timeoutMs: AGENT_DEVICE_TIMEOUT_MS,
1623
+ killSignal: "SIGKILL"
1624
+ });
1095
1625
  } catch {}
1096
1626
  if (!sessionId) return { status: "torn-down" };
1097
- const ledger = readEasSessionLedger(ctx.easLedgerRoot);
1627
+ const ledger = readEasSessionLedger$1(ctx.easLedgerRoot);
1098
1628
  const claimNeedsRemoval = !ledger.safe || ledger.claims.has(sessionId);
1099
1629
  const removeClaim = () => {
1100
1630
  try {
@@ -1202,16 +1732,12 @@ async function ensureMetroReachable({ ctx, metroPort, isExpo, tunnelMode = "auto
1202
1732
  }
1203
1733
  function bundleEntryPoint(root, isExpo) {
1204
1734
  if (!isExpo) return "index";
1205
- try {
1206
- const pkg = JSON.parse(readFileSync(resolve(root, "package.json"), "utf-8"));
1207
- if (typeof pkg.main !== "string" || !pkg.main.trim()) return "index";
1208
- const main = pkg.main.trim();
1209
- const local = main.startsWith(".") || /\.[cm]?[jt]sx?$/.test(main) || existsSync(resolve(root, main));
1210
- const normalized = main.replace(/^\.\//, "").replace(/\.(?:[cm]?[jt]sx?)$/, "");
1211
- return local ? normalized : `node_modules/${normalized}`;
1212
- } catch {
1213
- return "index";
1214
- }
1735
+ const declared = readJsonObject(resolve(root, "package.json"))?.main;
1736
+ if (typeof declared !== "string" || !declared.trim()) return "index";
1737
+ const main = declared.trim();
1738
+ const local = main.startsWith(".") || /\.[cm]?[jt]sx?$/.test(main) || existsSync(resolve(root, main));
1739
+ const normalized = main.replace(/^\.\//, "").replace(/\.(?:[cm]?[jt]sx?)$/, "");
1740
+ return local ? normalized : `node_modules/${normalized}`;
1215
1741
  }
1216
1742
  //#endregion
1217
- export { verifyStoppedSession as C, stopSessionArgs as S, inspectSessionForTeardown as _, ensureMetroReachable as a, ownedSessionName as b, remoteIosDeps as c, withEasProjectLock as d, easMachineStateRoot as f, getSessionArgs as g, findOrphanedOwnedSessions as h, endRecordedSession as i, resolveRemoteContext as l, removeEasSessionClaim as m, binOnPath as n, ensureRemoteBootOwned as o, readEasSessionLedger as p, bundleEntryPoint as r, remoteAndroidDeps as s, REMOTE_SESSION_ERROR as t, withRemoteSessionLock as u, isDefinitiveMissingSessionError as v, gateMetroOrigin as w, parseSessionListPage as x, listOwnedSessionsArgs as y };
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 };