stim 1.9.0 → 1.10.1

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 (83) hide show
  1. package/dist/{android-_y4Ohi5z.mjs → android-787HhP5w.mjs} +119 -29
  2. package/dist/{android-BPGizezh.mjs → android-Bu4mbDzc.mjs} +168 -95
  3. package/dist/{android-DEgo4oCo.mjs → android-DPov-fv2.mjs} +2 -2
  4. package/dist/{android-cas-eiExNLf7.mjs → android-cas-CITmIeWY.mjs} +9 -8
  5. package/dist/android-cas-compiler.mjs +1 -1
  6. package/dist/{app-install-Uf3a83Tv.mjs → app-install-Cfmqr3TQ.mjs} +2 -2
  7. package/dist/budget-nDCEwtJv.mjs +620 -0
  8. package/dist/build-progress-BXx-ZBe4.mjs +512 -0
  9. package/dist/{cache-manifest-Dg7OnMrp.mjs → cache-manifest-Pubfp8wV.mjs} +1 -1
  10. package/dist/cache-manifest.mjs +1 -1
  11. package/dist/cli.mjs +16 -16
  12. package/dist/collector-run.d.mts +5 -1
  13. package/dist/collector-run.mjs +7 -7
  14. package/dist/{command-output-gByVmIjy.mjs → command-output-D5z2_bcF.mjs} +1 -1
  15. package/dist/{config-Cp9lM3wX.mjs → config-BEq0Qw_a.mjs} +14 -131
  16. package/dist/{dependency-state-Bq5DcLGn.mjs → dependency-state-r9UesA9a.mjs} +2 -2
  17. package/dist/{deps-Cip5vUm1.mjs → deps-DP4eWqea.mjs} +3 -3
  18. package/dist/{detached-entry-KEu0_AXL.mjs → detached-entry-DJHoca49.mjs} +1 -1
  19. package/dist/{dev-client-HyqISM0h.mjs → dev-client-DExzPiOa.mjs} +137 -250
  20. package/dist/{device-D4U-xLty.mjs → device-Czh5kO8A.mjs} +7 -7
  21. package/dist/{build-lock-DPXPLgTc.mjs → device-lease-CbEaIk7b.mjs} +370 -12
  22. package/dist/{device-pool-CnTAz8X1.mjs → device-pool-BBWLAr55.mjs} +4 -4
  23. package/dist/{device-remote-AQ5rQPqr.mjs → device-remote-powUHdbK.mjs} +547 -468
  24. package/dist/{doctor-B_Ei3bZu.mjs → doctor-CmqK1LcQ.mjs} +58 -22
  25. package/dist/{error-diagnostics-Vfz83GCF.mjs → error-diagnostics-xxWKNO3-.mjs} +11 -12
  26. package/dist/{exec-jIAlgilk.mjs → exec-DK8KaVHK.mjs} +3 -2
  27. package/dist/{gc-Ckw8RQHi.mjs → gc-zCDpmdHU.mjs} +500 -1114
  28. package/dist/{guide-BiAbhs1J.mjs → guide-DwmaD_Wa.mjs} +882 -528
  29. package/dist/guide-status-CWvti2Lg.mjs +125 -0
  30. package/dist/idle--jn7pBNe.mjs +157 -0
  31. package/dist/in-use-CNIWU-r-.mjs +245 -0
  32. package/dist/{ios-BL2NFckr.mjs → ios-COGcGEcN.mjs} +39 -65
  33. package/dist/{ios-BzUJWk1K.mjs → ios-CbdqTqOc.mjs} +130 -89
  34. package/dist/{ios-device-DPsSm0v_.mjs → ios-device-BiOsSVzd.mjs} +1 -1
  35. package/dist/{ios-device-D972i9wf.mjs → ios-device-DNjPXBnb.mjs} +3 -3
  36. package/dist/{launch-verify-Q_OtW1Rf.mjs → launch-verify-BhgkYAws.mjs} +5 -5
  37. package/dist/{logs-CI9NQSK7.mjs → logs-MARIy6m9.mjs} +9 -9
  38. package/dist/{metro-DrLHaMn2.mjs → metro-BWLOJ_Wr.mjs} +116 -10
  39. package/dist/{named-ports-CmLQSIRj.mjs → named-ports-DiTfSz6p.mjs} +4 -4
  40. package/dist/{ndjson-9uFNRwfe.mjs → ndjson-CDY9IkVC.mjs} +3 -56
  41. package/dist/{ndjson-DvHSsNU9.d.mts → ndjson-Du3j0AQ9.d.mts} +1 -0
  42. package/dist/ownership-BfPNRf3-.mjs +1180 -0
  43. package/dist/{ownership-D15Op8vN.mjs → ownership-D2xbLVoz.mjs} +2 -3
  44. package/dist/{ports-BNKoWVWZ.mjs → ports-CLSSFOuz.mjs} +3 -3
  45. package/dist/{project-DS112DYB.mjs → project-BXZzOABt.mjs} +2 -2
  46. package/dist/{reload-ZNJUo1pl.mjs → reload-CymyF6Kk.mjs} +9 -10
  47. package/dist/{server-bare-DWkkcIxV.mjs → server-bare-DVbb7kXH.mjs} +1 -1
  48. package/dist/{server-expo-B_lctBR3.mjs → server-expo-BvOCeakp.mjs} +4 -4
  49. package/dist/server-expo-DBVTdult.mjs +2 -0
  50. package/dist/{settings-D030k0t4.mjs → settings-DOugPlPz.mjs} +16 -428
  51. package/dist/{settings-BKNh5rl4.mjs → settings-EWYNwe72.mjs} +16 -14
  52. package/dist/settings.schema.json +75 -0
  53. package/dist/{simslim-B_LsGLuJ.mjs → simslim-DZUxzsjq.mjs} +182 -5
  54. package/dist/{slot-launch-DU9A-gYw.mjs → slot-launch-Dk4oTREM.mjs} +4 -4
  55. package/dist/{spawn-claims-Dn3C_N6w.mjs → spawn-claims-CUleC2cM.mjs} +1 -1
  56. package/dist/{start-DYkkKF0s.mjs → start-RbmFPeZ7.mjs} +147 -98
  57. package/dist/start-qWapeB4W.mjs +2 -0
  58. package/dist/{state-NcJzMEBF.mjs → state-WGdIbi_d.mjs} +2 -2
  59. package/dist/{stats-DG9e66Rz.mjs → stats-BAgJaLA4.mjs} +1 -1
  60. package/dist/{stats-B63-JVAY.mjs → stats-DBYJ5bxL.mjs} +4 -4
  61. package/dist/status-Dr4fRkym.mjs +216 -0
  62. package/dist/{status-S6ehL4Ui.mjs → status-Zp8kvexe.mjs} +63 -231
  63. package/dist/{guide-status-BAMVU0ZA.mjs → stim-installations-CINXy_su.mjs} +3 -124
  64. package/dist/{stop-T1R-cZRz.mjs → stop-DI4BG4Os.mjs} +2 -2
  65. package/dist/{stop-BO28HGqF.mjs → stop-DcEv5sk-.mjs} +22 -18
  66. package/dist/supervisor-run.d.mts +11 -2
  67. package/dist/supervisor-run.mjs +185 -17
  68. package/dist/{workspace-process-lock-DWaiRD0e.mjs → workspace-process-lock-BXA96myD.mjs} +2 -2
  69. package/dist/workspaces-wrH6dXfu.mjs +1003 -0
  70. package/dist/{worktree-LGB-UwgG.mjs → worktree-Bd4dRvx7.mjs} +40 -122
  71. package/dist/{worktree-R_cUlE7s.mjs → worktree-BlizKDpZ.mjs} +143 -18
  72. package/dist/{worktree-DKhSF0UX.mjs → worktree-DEG2o9F8.mjs} +1 -1
  73. package/dist/{xcode-BZqyxh3e.mjs → xcode-B2tBkf0m.mjs} +9 -158
  74. package/package.json +6 -5
  75. package/dist/build-progress-CVqkfPC6.mjs +0 -188
  76. package/dist/device-lease-Crs-HkJY.mjs +0 -366
  77. package/dist/logs-query-Cqma3WIu.mjs +0 -298
  78. package/dist/ownership-DCQPuWEc.mjs +0 -489
  79. package/dist/public-http-probe-CyMDrQ3c.mjs +0 -61
  80. package/dist/server-expo-CeILEu86.mjs +0 -2
  81. package/dist/sim-pool-KaIsp2Ft.mjs +0 -723
  82. package/dist/spawn-entry-nCE6m770.mjs +0 -14
  83. package/dist/workspace-state-DF2gCFhx.mjs +0 -169
@@ -1,427 +1,537 @@
1
- import { A as withDirLock, K as projectDeviceSlots, N as readJsonObject, O as acquireAvdClaim, T as withConfigLock, U as deviceSlotPlatforms, g as loadConfig, k as ownership_claim_exports, u as getConfigDir } from "./config-Cp9lM3wX.mjs";
2
- import { t as getExecutor } from "./exec-jIAlgilk.mjs";
3
- import { S as workspaceStateFile, _ as workspaceDir, b as workspaceLogsDir, v as workspaceId, x as workspaceName } from "./workspace-state-DF2gCFhx.mjs";
4
- import { c as ownedDeviceLabel } from "./project-DS112DYB.mjs";
5
- import { a as clearRemoteSession, l as readRemoteSession, s as readMetroTunnel } from "./state-NcJzMEBF.mjs";
6
- import { a as pidExists } from "./metro-DrLHaMn2.mjs";
7
- import { G as planMetroReach } from "./settings-D030k0t4.mjs";
8
- import { A as ownedAvdMatchesConfiguration, B as shutdownAndroidEmulator, E as listOrphanedAvdDirectories, N as ownedAvdSystemImage, R as resolveOwnedAvdSerial, V as waitForAndroidEmulatorShutdown, f as avdStorageRoots, g as deleteAvd, k as ownedAvdDirectory, l as assertOwnedAvdStopped, u as avdPathExists } from "./android-_y4Ohi5z.mjs";
9
- import { l as phaseLine } from "./command-output-gByVmIjy.mjs";
10
- import { a as parkSim, c as removeParkedAfter, s as readParked } from "./sim-pool-KaIsp2Ft.mjs";
11
- import { t as withWorkspaceProcessLock } from "./workspace-process-lock-DWaiRD0e.mjs";
12
- import { c as readMetroRecords, o as isBundleProof } from "./launch-verify-Q_OtW1Rf.mjs";
13
- import { a as LAUNCH_ERROR, i as INSTALL_ERROR, l as devClientDeepLink } from "./app-install-Uf3a83Tv.mjs";
14
- import { C as resolveOwnedIosSim, T as shutdownIosSim, _ as parseRuntimeVersion, a as deleteIosSim, f as listIosDeviceTypes, g as parkedSimName, o as deleteParkedIosSim, r as clearAppDataContainer, s as findAppDataContainer, w as sanitizeDeviceLabel, y as renameIosSim } from "./ios-BL2NFckr.mjs";
15
- import { t as probePublicHttp } from "./public-http-probe-CyMDrQ3c.mjs";
16
- import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
17
- import { dirname, isAbsolute, join, relative, resolve } from "node:path";
18
- import { randomUUID } from "node:crypto";
19
- import { homedir } from "node:os";
20
- import { isDeepStrictEqual } from "node:util";
21
- //#region src/devices/agent-device-cleanup.ts
22
- function parseAgentDeviceSessions(output) {
23
- const result = JSON.parse(output);
24
- if (result?.success !== true || !Array.isArray(result.data?.sessions)) throw new Error("agent-device did not return a successful session list");
25
- return result.data.sessions.flatMap((entry) => {
26
- if (!entry || typeof entry !== "object") return [];
27
- const row = entry;
28
- const id = row.platform === "ios" ? row.device_udid : row.id;
29
- if (row.platform !== "ios" && row.platform !== "android" || typeof row.name !== "string" || !row.name.trim() || typeof id !== "string" || !id || typeof row.createdAt !== "number" || !Number.isFinite(row.createdAt) || row.platform === "ios" && row.id !== void 0 && row.id !== id) return [];
30
- return [{
31
- name: row.name,
32
- platform: row.platform,
33
- id,
34
- createdAt: row.createdAt
35
- }];
36
- });
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-BfPNRf3-.mjs";
11
+ import { C as sanitizeDeviceLabel } from "./ios-COGcGEcN.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-Cfmqr3TQ.mjs";
14
+ import { c as readMetroRecords, o as isBundleProof } from "./launch-verify-BhgkYAws.mjs";
15
+ 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";
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;
37
38
  }
38
- function report(message) {
39
- process.stderr.write(`${phaseLine("device", message)}\n`);
40
- }
41
- function closeOwnedDeviceSessions(device, stillOwned) {
42
- const exec = getExecutor();
43
- const deadline = Date.now() + 15e3;
44
- const run = (args) => {
45
- const remaining = deadline - Date.now();
46
- if (remaining <= 0) throw new Error("agent-device cleanup timed out");
47
- return exec.runFile("agent-device", [
48
- ...args,
49
- "--json",
50
- "--daemon-transport",
51
- "socket"
52
- ], {
53
- timeoutMs: Math.min(5e3, remaining),
54
- killSignal: "SIGKILL"
55
- });
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: {}
56
57
  };
57
- const list = () => parseAgentDeviceSessions(run([
58
- "session",
59
- "list",
60
- "--session",
61
- "stim-teardown-inventory"
62
- ]));
63
- try {
64
- if (!exec.findExecutable("agent-device")) return;
65
- const sessions = list().filter((session) => session.platform === device.platform && session.id === device.id);
66
- for (const session of sessions) try {
67
- if (!list().some((current) => current.name === session.name && current.platform === session.platform && current.id === session.id && current.createdAt === session.createdAt)) continue;
68
- if (!stillOwned()) return;
69
- if (JSON.parse(run([
70
- "close",
71
- "--session",
72
- session.name,
73
- "--session-lock",
74
- "reject",
75
- "--platform",
76
- device.platform,
77
- device.platform === "ios" ? "--udid" : "--serial",
78
- device.id
79
- ]))?.success !== true) throw new Error("agent-device did not confirm session close");
80
- report(`closed agent-device session ${session.name} on ${device.id}`);
81
- } catch (error) {
82
- report(`could not close agent-device session ${session.name} on ${device.id}: ${error.message}`);
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
+ };
83
67
  }
84
- } catch (error) {
85
- report(`could not list agent-device sessions for ${device.id}: ${error.message}`);
86
68
  }
69
+ return { invalid: typeof raw === "string" ? `"${raw}"` : JSON.stringify(raw) };
87
70
  }
88
- //#endregion
89
- //#region src/devices/teardown.ts
90
- function teardownParkedAvd(avdName) {
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 } = {}) {
91
85
  try {
92
- return removeParkedAfter("android", avdName, () => {
93
- const result = teardownOwnedAvd(avdName, {
94
- del: true,
95
- owner: { pool: true }
96
- });
97
- if (result.status !== "torn-down" && result.status !== "missing") throw new Error(result.reason);
98
- }) ? {
99
- status: "torn-down",
100
- label: avdName
101
- } : {
102
- status: "skipped",
103
- kind: "not-parked",
104
- reason: "emulator is no longer parked"
105
- };
106
- } catch (error) {
107
- return {
108
- status: "failed",
109
- reason: String(error?.message || error)
110
- };
86
+ stream.write("", () => exit(code));
87
+ } catch {
88
+ exit(code);
111
89
  }
112
90
  }
113
- function teardownParkedIosSim(udid, { label, deleteSim = deleteParkedIosSim } = {}) {
114
- try {
115
- const removed = removeParkedAfter("ios", udid, () => {
116
- closeOwnedDeviceSessions({
117
- platform: "ios",
118
- id: udid
119
- }, () => Boolean(resolveOwnedIosSim(udid).sim));
120
- deleteSim(udid);
121
- });
122
- if (!removed) return {
123
- status: "skipped",
124
- kind: "not-parked",
125
- reason: "simulator is no longer parked"
126
- };
127
- return {
128
- status: "torn-down",
129
- label: label ?? removed.name
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
130
98
  };
131
- } catch (error) {
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 {
132
122
  return {
133
- status: "failed",
134
- reason: String(error?.message || error)
123
+ config: JSON.parse(String(stdout)),
124
+ source: dynamic
135
125
  };
126
+ } catch (err) {
127
+ return { unavailable: `\`expo config --json\` did not print JSON: ${firstLine(err)}` };
136
128
  }
137
129
  }
138
- function parkOwnedIosSim(udid, park) {
139
- const resolved = resolveOwnedIosSim(udid);
140
- if (resolved.missing) throw new Error(`simulator ${udid} disappeared after shutdown`);
141
- if (resolved.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(resolved.notOwned)}`);
142
- const sim = resolved.sim;
143
- if (sim.state !== "Shutdown") throw new Error(`simulator ${udid} is still ${sim.state} after shutdown`);
144
- const model = listIosDeviceTypes().find((d) => d.identifier === sim.deviceTypeIdentifier)?.name ?? null;
145
- const runtime = parseRuntimeVersion(sim.runtime);
146
- if (park.bundleId) {
147
- if (!sim.dataPath) throw new Error(`simulator ${udid} did not report a data path for app cleanup`);
148
- const container = findAppDataContainer(sim.dataPath, park.bundleId);
149
- if (container) clearAppDataContainer(container);
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);
150
139
  }
151
- const name = parkedSimName(sim.udid, {
152
- model,
153
- runtime
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
154
149
  });
155
- renameIosSim(sim.udid, name);
156
- const record = {
157
- udid: sim.udid,
158
- name,
159
- deviceTypeIdentifier: sim.deviceTypeIdentifier,
160
- runtimeIdentifier: sim.runtime,
161
- parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
162
- simslimManaged: Boolean(park.simslimManaged),
163
- ...park.bundleId ? { bundleId: park.bundleId } : {},
164
- ...park.cacheKey ? { cacheKey: park.cacheKey } : {}
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)
165
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);
166
191
  return {
167
- record,
168
- evicted: parkSim({
169
- platform: "ios",
170
- projectPath: park.projectPath,
171
- slot: park.slot,
172
- record,
173
- max: park.max
174
- })
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"
175
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;
176
227
  }
177
- function teardownOwnedIosSim(udid, { del = false, label, park } = {}) {
178
- let parkFallback;
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;
179
252
  try {
180
- const resolved = resolveOwnedIosSim(udid);
181
- if (resolved.notOwned) return {
182
- status: "skipped",
183
- kind: "not-owned",
184
- reason: `sim is now named "${resolved.notOwned}", not Stim-owned by name`
185
- };
186
- if (resolved.missing) return { status: "missing" };
187
- closeOwnedDeviceSessions({
188
- platform: "ios",
189
- id: udid
190
- }, () => Boolean(resolveOwnedIosSim(udid).sim));
191
- const current = resolveOwnedIosSim(udid);
192
- if (current.missing) return { status: "missing" };
193
- if (current.notOwned) return {
194
- status: "skipped",
195
- kind: "not-owned",
196
- reason: "simulator ownership changed before shutdown"
197
- };
198
- shutdownIosSim(udid);
199
- const sim = resolved.sim;
200
- if (del && park && park.max > 0) try {
201
- const { record, evicted } = parkOwnedIosSim(udid, park);
202
- const removed = [];
203
- const failures = [];
204
- for (const entry of evicted) {
205
- const result = teardownParkedIosSim(entry.udid, { label: entry.name });
206
- if (result.status === "torn-down") removed.push({
207
- udid: entry.udid,
208
- name: entry.name
209
- });
210
- else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name} (${entry.udid}): ${result.reason}`);
211
- }
212
- return {
213
- status: "torn-down",
214
- label: label ?? sim.name ?? udid,
215
- parked: {
216
- udid: record.udid,
217
- name: record.name
218
- },
219
- evicted: removed,
220
- ...failures.length ? { evictionFailures: failures } : {}
221
- };
222
- } catch (e) {
223
- parkFallback = String(e?.message || e);
224
- }
225
- if (del) deleteIosSim(udid);
226
- return {
227
- status: "torn-down",
228
- label: label ?? sim.name ?? udid,
229
- ...parkFallback ? { parkFallback } : {}
253
+ result = {
254
+ stdout: String(exec(bin.file, ["whoami"], { timeoutMs }) ?? ""),
255
+ exitCode: 0
230
256
  };
231
257
  } catch (e) {
232
- return {
233
- status: "failed",
234
- reason: String(e?.message || e),
235
- ...parkFallback ? { parkFallback } : {}
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
236
264
  };
237
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
+ };
238
290
  }
239
- function assertAvdReferences(avdName, owner) {
240
- const config = loadConfig();
241
- if (!config) throw new Error("Cannot verify AVD references without a Stim config; the data was kept.");
242
- if (owner && "projectPath" in owner && owner.expectedRecord && !isDeepStrictEqual(deviceSlotPlatforms(config.projects[owner.projectPath], owner.slot)?.android, owner.expectedRecord)) throw new Error(`AVD ${avdName} has a changed workspace record; its data was kept.`);
243
- for (const [projectPath, project] of Object.entries(config.projects)) for (const { slot, platforms } of projectDeviceSlots(project)) {
244
- if (platforms.android?.avdName?.toLowerCase() !== avdName.toLowerCase()) continue;
245
- if (!owner || !("projectPath" in owner) || owner.projectPath !== projectPath || (owner.slot ?? "default") !== slot || !platforms.android.owned) throw new Error(`AVD ${avdName} is referenced by ${projectPath} (${slot}); its data was kept.`);
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(/[.,)\]]+$/, "");
246
300
  }
247
- if (readParked("android", { config }).some((entry) => entry.name.toLowerCase() === avdName.toLowerCase()) && !(owner && "pool" in owner)) throw new Error(`AVD ${avdName} is in the emulator pool; its data was kept.`);
248
- }
249
- function removeOrphanedAvdDirectory(candidate, owner) {
250
- const surveyed = listOrphanedAvdDirectories(candidate.name).find((entry) => entry.directory === candidate.directory);
251
- if (!surveyed || surveyed.name !== candidate.name || surveyed.dev !== candidate.dev || surveyed.ino !== candidate.ino) throw new Error(`AVD data at ${candidate.directory} changed or is registered; it was kept.`);
252
- assertOwnedAvdStopped(candidate.name, { resolveDirectory: () => candidate.directory });
253
- const detached = withConfigLock(() => {
254
- const current = lstatSync(candidate.directory);
255
- if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`AVD data at ${candidate.directory} changed or is registered; it was kept.`);
256
- assertAvdReferences(candidate.name, owner);
257
- const destination = join(dirname(candidate.directory), `stim-gc-${randomUUID()}.avd`);
258
- renameSync(candidate.directory, destination);
259
- return destination;
260
- });
261
- const current = lstatSync(detached);
262
- if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`Detached AVD data changed at ${detached}; it was kept.`);
263
- try {
264
- rmSync(detached, { recursive: true });
265
- } catch (error) {
266
- throw new Error(`Could not remove AVD data at ${detached}; retry gc --delete: ${error.message}`, { cause: error });
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(/[.,)\]]+$/, "");
267
304
  }
305
+ return null;
268
306
  }
269
- function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
270
- const candidates = surveyed ? [surveyed] : listOrphanedAvdDirectories(avdName);
271
- if (candidates.length === 0) {
272
- if (avdStorageRoots().some((root) => avdPathExists(join(root, `${avdName}.ini`)) || avdPathExists(join(root, `${avdName}.avd`)))) throw new Error(`AVD ${avdName} is not listed but its storage remains; it was kept.`);
273
- return { status: "missing" };
274
- }
275
- if (!del) return {
276
- status: "skipped",
277
- reason: "AVD registration is missing; its data was kept."
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`);
278
332
  };
279
- for (const candidate of candidates) {
280
- if (candidate.name !== avdName) throw new Error(`AVD data does not match ${avdName}; it was kept.`);
281
- removeOrphanedAvdDirectory(candidate, owner);
282
- }
283
- return {
284
- status: "torn-down",
285
- label: avdName
333
+ stdout.write = patchedWrite;
334
+ console.log = patchedLog;
335
+ patched = {
336
+ stdout,
337
+ write,
338
+ log,
339
+ patchedWrite,
340
+ patchedLog
286
341
  };
287
342
  }
288
- function teardownOwnedAvd(avdName, options = {}) {
289
- if (!/^stim-[A-Za-z0-9._-]+$/.test(avdName)) return {
290
- status: "skipped",
291
- kind: "not-owned",
292
- reason: `AVD ${avdName} is not Stim-owned by name`
293
- };
294
- let claim;
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) {
295
350
  try {
296
- claim = acquireAvdClaim(avdName);
297
- const result = teardownClaimedAvd(avdName, claim, options);
298
- if (result.status === "torn-down" || result.status === "missing") options.onRemoved?.();
299
- return result;
300
- } catch (error) {
301
- return {
302
- status: "failed",
303
- reason: String(error?.message || error)
304
- };
305
- } finally {
306
- (0, ownership_claim_exports.releaseClaim)(claim);
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");
307
359
  }
308
360
  }
309
- function teardownClaimedAvd(avdName, claim, { del = false, park, owner, orphanedDirectory, onlyIfMissing = false, waitForShutdown = waitForAndroidEmulatorShutdown, assertStopped = assertOwnedAvdStopped, resolveAvd = resolveOwnedAvdSerial }) {
310
- let parkFallback;
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();
311
366
  try {
312
- if (del) withConfigLock(() => assertAvdReferences(avdName, owner));
313
- const resolved = resolveAvd(avdName);
314
- if (resolved.notOwned) return {
315
- status: "skipped",
316
- kind: "not-owned",
317
- reason: `AVD ${avdName} is not Stim-owned by name`
318
- };
319
- if (resolved.missing) return teardownUnregisteredAvd(avdName, del, owner, orphanedDirectory);
320
- if (onlyIfMissing) return {
321
- status: "skipped",
322
- reason: "AVD registration appeared; its record was kept."
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
323
432
  };
324
- if (orphanedDirectory) return {
325
- status: "skipped",
326
- reason: "AVD registration appeared; its data was kept."
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
327
469
  };
328
- const serial = resolved.serial;
329
- if (serial) {
330
- const stillOwned = () => {
331
- const current = resolveAvd(avdName);
332
- return !current.notOwned && !current.missing && current.serial === serial;
333
- };
334
- closeOwnedDeviceSessions({
335
- platform: "android",
336
- id: serial
337
- }, stillOwned);
338
- if (!stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
339
- waitForShutdown(avdName, (timeoutMs) => {
340
- (0, ownership_claim_exports.markClaimChildPending)(claim);
341
- try {
342
- shutdownAndroidEmulator(serial, timeoutMs);
343
- } finally {
344
- (0, ownership_claim_exports.clearClaimChild)(claim);
345
- }
346
- });
347
- } else assertStopped(avdName);
348
- if (del) {
349
- const current = resolveAvd(avdName);
350
- if (current.notOwned) return {
351
- status: "skipped",
352
- kind: "not-owned",
353
- reason: `AVD ${avdName} is not Stim-owned by name`
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
354
517
  };
355
- if (current.missing) return teardownUnregisteredAvd(avdName, del, owner);
356
- if (current.serial) throw new Error(`Owned AVD ${avdName} started again before deletion.`);
357
- assertStopped(avdName);
358
- withConfigLock(() => assertAvdReferences(avdName, owner));
359
- if (park && park.max > 0) try {
360
- const systemImage = ownedAvdSystemImage(avdName);
361
- if (!systemImage || !park.configuration || !ownedAvdMatchesConfiguration(avdName, park.configuration)) throw new Error("the AVD has no verified creation configuration");
362
- const evicted = parkSim({
363
- platform: "android",
364
- projectPath: park.projectPath,
365
- slot: park.slot,
366
- max: park.max,
367
- record: {
368
- udid: avdName,
369
- name: avdName,
370
- systemImage,
371
- configuration: park.configuration,
372
- parkedAt: (/* @__PURE__ */ new Date()).toISOString()
373
- }
374
- });
375
- const removed = [];
376
- const failures = [];
377
- for (const entry of evicted) {
378
- const result = teardownParkedAvd(entry.name);
379
- if (result.status === "torn-down") removed.push({
380
- udid: entry.name,
381
- name: entry.name,
382
- platform: "android"
383
- });
384
- else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name}: ${result.reason}`);
385
- }
386
- return {
387
- status: "torn-down",
388
- label: avdName,
389
- parked: {
390
- udid: avdName,
391
- name: avdName,
392
- platform: "android"
393
- },
394
- evicted: removed,
395
- ...failures.length ? { evictionFailures: failures } : {}
396
- };
397
- } catch (error) {
398
- parkFallback = String(error?.message || error);
399
- }
400
- const directory = ownedAvdDirectory(avdName);
401
- const paths = avdStorageRoots().map((root) => join(root, `${avdName}.ini`));
402
- if (directory) paths.push(directory);
403
- (0, ownership_claim_exports.markClaimChildPending)(claim);
404
- try {
405
- deleteAvd(avdName);
406
- } finally {
407
- (0, ownership_claim_exports.clearClaimChild)(claim);
408
- }
409
- const remaining = paths.filter(avdPathExists);
410
- if (remaining.length) throw new Error(`AVD ${avdName} was only partially deleted; data remains at ${remaining.join(", ")}.`);
411
518
  }
519
+ endCapture(frame);
412
520
  return {
413
- status: "torn-down",
414
- label: avdName,
415
- serial: resolved.serial ?? null,
416
- ...parkFallback ? { parkFallback } : {}
417
- };
418
- } catch (e) {
419
- return {
420
- status: "failed",
421
- reason: String(e?.message || e)
521
+ ...settled,
522
+ lines: frame.lines
422
523
  };
524
+ } catch (err) {
525
+ endCapture(frame);
526
+ throw err;
527
+ } finally {
528
+ if (timer) clearTimeout(timer);
423
529
  }
424
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
+ }
425
535
  //#endregion
426
536
  //#region src/engine/metro-gate.ts
427
537
  const REMOTE_METRO_WRONG = "STIM_REMOTE_METRO_WRONG";
@@ -494,8 +604,10 @@ function remoteProfile({ daemon, platform, label }) {
494
604
  tenant: scope,
495
605
  runId: scope,
496
606
  sessionIsolation: "tenant",
497
- leaseProvider: "proxy",
498
- clientId: scope
607
+ ...platform === "android" ? {
608
+ leaseProvider: "proxy",
609
+ clientId: scope
610
+ } : {}
499
611
  };
500
612
  }
501
613
  function daemonEnv(daemon) {
@@ -558,6 +670,18 @@ const LIVE_STATUSES$1 = ["new", "in-progress"];
558
670
  const LIVE_SESSION_STATUSES = /* @__PURE__ */ new Set(["NEW", "IN_PROGRESS"]);
559
671
  const TERMINAL_SESSION_STATUSES = /* @__PURE__ */ new Set(["STOPPED", "ERRORED"]);
560
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
+ }
561
685
  function ownedSessionName(label) {
562
686
  const clean = sanitizeDeviceLabel(label);
563
687
  return `${OWNED_PREFIX}${clean.startsWith(OWNED_PREFIX) ? clean.slice(5) : clean}`;
@@ -827,80 +951,14 @@ function parseStoppedSession(stdout) {
827
951
  };
828
952
  }
829
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
830
957
  //#region src/engine/eas-session-ledger.ts
831
- function easMachineStateRoot() {
832
- return join(homedir(), ".stim", "machine", "eas");
833
- }
834
- function easSessionLedgerFile(root = easMachineStateRoot()) {
835
- return join(root, "sessions.json");
836
- }
837
- function validClaim(id, value) {
838
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
839
- const claim = value;
840
- if (claim.sessionId !== id) return null;
841
- if (typeof claim.name !== "string" || !claim.name.startsWith("stim-")) return null;
842
- if (claim.platform !== "ios" && claim.platform !== "android") return null;
843
- if (typeof claim.workspaceRoot !== "string" || !isAbsolute(claim.workspaceRoot)) return null;
844
- if (typeof claim.workspaceHome !== "string" || !isAbsolute(claim.workspaceHome)) return null;
845
- if (typeof claim.stateFile !== "string" || !isAbsolute(claim.stateFile)) return null;
846
- const workspaceRoot = resolve(claim.workspaceRoot);
847
- const workspaceHome = resolve(claim.workspaceHome);
848
- const stateFile = resolve(claim.stateFile);
849
- const stateRelative = relative(workspaceHome, stateFile);
850
- if (!stateRelative || stateRelative.startsWith("..") || isAbsolute(stateRelative)) return null;
851
- if (stateFile !== join(workspaceHome, "workspaces", workspaceName(workspaceRoot), "state.json")) return null;
852
- return {
853
- sessionId: id,
854
- name: claim.name,
855
- platform: claim.platform,
856
- workspaceRoot,
857
- workspaceHome,
858
- stateFile
859
- };
860
- }
861
- function readEasSessionLedger(root = easMachineStateRoot()) {
862
- const file = easSessionLedgerFile(root);
863
- if (!existsSync(file)) return {
864
- claims: /* @__PURE__ */ new Map(),
865
- notice: null,
866
- safe: true
867
- };
868
- let parsed;
869
- try {
870
- parsed = JSON.parse(readFileSync(file, "utf-8"));
871
- } catch (error) {
872
- return {
873
- claims: /* @__PURE__ */ new Map(),
874
- notice: `EAS ownership ledger ${file} is unreadable: ${error.message}`,
875
- safe: false
876
- };
877
- }
878
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {
879
- claims: /* @__PURE__ */ new Map(),
880
- notice: `EAS ownership ledger ${file} is malformed.`,
881
- safe: false
882
- };
883
- const ledger = parsed;
884
- if (ledger.version !== 1 || !ledger.claims || typeof ledger.claims !== "object" || Array.isArray(ledger.claims)) return {
885
- claims: /* @__PURE__ */ new Map(),
886
- notice: `EAS ownership ledger ${file} is malformed.`,
887
- safe: false
888
- };
889
- const claims = /* @__PURE__ */ new Map();
890
- for (const [id, value] of Object.entries(ledger.claims)) {
891
- const claim = validClaim(id, value);
892
- if (!claim) return {
893
- claims: /* @__PURE__ */ new Map(),
894
- notice: `EAS ownership ledger ${file} is malformed.`,
895
- safe: false
896
- };
897
- claims.set(id, claim);
898
- }
899
- return {
900
- claims,
901
- notice: null,
902
- safe: true
903
- };
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.`);
904
962
  }
905
963
  function writeLedger(root, ledger) {
906
964
  mkdirSync(root, { recursive: true });
@@ -915,9 +973,10 @@ function writeLedger(root, ledger) {
915
973
  }
916
974
  }
917
975
  function recordEasSessionClaim(claim, root = easMachineStateRoot()) {
918
- const normalized = validClaim(claim.sessionId, claim);
976
+ assertEasMachineRootWritable(root);
977
+ const normalized = validEasSessionClaim(claim.sessionId, claim);
919
978
  if (!normalized) throw new Error(`Invalid EAS session claim for ${claim.sessionId}.`);
920
- withDirLock(join(root, "ledger.lock"), () => {
979
+ withDirLock(easSessionLedgerLock(root), () => {
921
980
  const current = readEasSessionLedger(root);
922
981
  if (!current.safe) throw new Error(current.notice ?? "EAS ownership ledger is unreadable.");
923
982
  current.claims.set(normalized.sessionId, normalized);
@@ -925,12 +984,13 @@ function recordEasSessionClaim(claim, root = easMachineStateRoot()) {
925
984
  version: 1,
926
985
  claims: Object.fromEntries(current.claims)
927
986
  });
928
- }, { ensureParent: () => mkdirSync(dirname(join(root, "ledger.lock")), { recursive: true }) });
987
+ }, { ensureParent: () => mkdirSync(dirname(easSessionLedgerLock(root)), { recursive: true }) });
929
988
  }
930
989
  function removeEasSessionClaim(sessionId, root = easMachineStateRoot()) {
931
990
  const file = easSessionLedgerFile(root);
932
991
  if (!existsSync(file)) return false;
933
- return withDirLock(join(root, "ledger.lock"), () => {
992
+ assertEasMachineRootWritable(root);
993
+ return withDirLock(easSessionLedgerLock(root), () => {
934
994
  const current = readEasSessionLedger(root);
935
995
  if (!current.safe) return false;
936
996
  if (!current.claims.delete(sessionId)) return false;
@@ -945,7 +1005,8 @@ function removeEasSessionClaim(sessionId, root = easMachineStateRoot()) {
945
1005
  //#region src/engine/eas-project-lock.ts
946
1006
  const EAS_PROJECT_LOCK_WAIT_MS = 24e4;
947
1007
  function withEasProjectLock(_root, fn, options = {}) {
948
- const { machineRoot = easMachineStateRoot(), ...lockOptions } = options;
1008
+ const { machineRoot = easMachineStateRoot$1(), ...lockOptions } = options;
1009
+ assertEasMachineRootWritable(machineRoot);
949
1010
  return withWorkspaceProcessLock(machineRoot, "eas-project", fn, {
950
1011
  waitMs: EAS_PROJECT_LOCK_WAIT_MS,
951
1012
  ...lockOptions,
@@ -1233,10 +1294,11 @@ function remoteDeviceDeps(ctx) {
1233
1294
  }
1234
1295
  return stopped;
1235
1296
  },
1236
- 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."
1237
1299
  };
1238
1300
  }
1239
- async function resolveRemoteContext({ root, 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 }) {
1240
1302
  const agentDeviceBin = lookupAgentDevice();
1241
1303
  if (!agentDeviceBin) return {
1242
1304
  failed: "agent-device is not on PATH, and it is what drives a remote device.",
@@ -1270,6 +1332,14 @@ async function resolveRemoteContext({ root, backend, platform = "ios", easBin, e
1270
1332
  remedy: "Install eas-cli, then run the device command with `--remote eas` again.",
1271
1333
  code: "STIM_REMOTE_EAS_UNAVAILABLE"
1272
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
+ }
1273
1343
  return { ctx: {
1274
1344
  root,
1275
1345
  label: `${ownedDeviceLabel(root)}-${workspaceId(root)}`,
@@ -1282,6 +1352,13 @@ async function resolveRemoteContext({ root, backend, platform = "ios", easBin, e
1282
1352
  existingDaemon
1283
1353
  } };
1284
1354
  }
1355
+ function readInstalledEasCliVersion(easBin, root) {
1356
+ try {
1357
+ return getExecutor().runFile(easBin, ["--version"], easBoundedExecOptions(root));
1358
+ } catch {
1359
+ return null;
1360
+ }
1361
+ }
1285
1362
  function binOnPath(bin) {
1286
1363
  try {
1287
1364
  return Boolean(getExecutor().findExecutable(bin));
@@ -1360,7 +1437,7 @@ function withRemoteSessionLock(root, fn, options = {}) {
1360
1437
  ...options
1361
1438
  });
1362
1439
  }
1363
- async function ensureRemoteBootOwned({ root, platform, sessionName, startedAt, boot, createdSessionId, abandonCreatedSession, webPreviewUrl = () => null, 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() }) {
1364
1441
  try {
1365
1442
  return await withProjectLock(root, () => withLock(root, async () => {
1366
1443
  register();
@@ -1488,7 +1565,8 @@ function remoteIosDeps(ctx) {
1488
1565
  }),
1489
1566
  createdSessionId: core.createdSessionId,
1490
1567
  abandonCreatedSession: core.abandonCreatedSession,
1491
- webPreviewUrl: core.webPreviewUrl
1568
+ webPreviewUrl: core.webPreviewUrl,
1569
+ failureRemedy: core.failureRemedy
1492
1570
  };
1493
1571
  }
1494
1572
  function remoteAndroidDeps(ctx) {
@@ -1516,7 +1594,8 @@ function remoteAndroidDeps(ctx) {
1516
1594
  }),
1517
1595
  createdSessionId: core.createdSessionId,
1518
1596
  abandonCreatedSession: core.abandonCreatedSession,
1519
- webPreviewUrl: core.webPreviewUrl
1597
+ webPreviewUrl: core.webPreviewUrl,
1598
+ failureRemedy: core.failureRemedy
1520
1599
  };
1521
1600
  }
1522
1601
  function endRecordedSession({ root, sessionId, easBin, lookupAgentDevice, ledgerRoot }) {
@@ -1545,7 +1624,7 @@ function teardownRemote(ctx, { sessionId }) {
1545
1624
  });
1546
1625
  } catch {}
1547
1626
  if (!sessionId) return { status: "torn-down" };
1548
- const ledger = readEasSessionLedger(ctx.easLedgerRoot);
1627
+ const ledger = readEasSessionLedger$1(ctx.easLedgerRoot);
1549
1628
  const claimNeedsRemoval = !ledger.safe || ledger.claims.has(sessionId);
1550
1629
  const removeClaim = () => {
1551
1630
  try {
@@ -1661,4 +1740,4 @@ function bundleEntryPoint(root, isExpo) {
1661
1740
  return local ? normalized : `node_modules/${normalized}`;
1662
1741
  }
1663
1742
  //#endregion
1664
- export { verifyStoppedSession as C, teardownParkedAvd as D, teardownOwnedIosSim as E, teardownParkedIosSim as O, stopSessionArgs as S, teardownOwnedAvd as T, 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 };