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,318 +1,160 @@
1
- import { t as getExecutor } from "./exec-B29XNuhP.mjs";
2
- import { C as withConfigLock, E as ownership_claim_exports, I as deviceSlotPlatforms, R as projectDeviceSlots, T as acquireAvdClaim, _ as releaseAndroidConsolePort, b as setDevice, g as loadConfig, i as clearDevice, t as allConsolePortsAndSerials, u as getConfigDir, w as process_identity_exports, y as saveConfig } from "./config-B7iyKWkL.mjs";
3
- import { g as workspaceId } from "./workspace-state-BT5E3SA2.mjs";
4
- import { s as ownedDeviceLabel } from "./project-DDZWf_16.mjs";
5
- import { n as stripAnsi, r as waitForChild, t as createLineReader } from "./process-output-Dl5uKE03.mjs";
6
- import { i as pidExists } from "./metro-C7bcJ0uo.mjs";
7
- import { l as androidDataPartitionSizeGbSetting, o as androidAvdConfigSetting, v as iosSimSlimProfileSetting } from "./settings-BXXorVM2.mjs";
8
- import { A as ownedAvdName, B as waitForBoot, C as listAvds, D as nextConsolePort, I as resolveOwnedAvdSerial, N as pickDefaultSystemImage, S as listAdbDevices, b as getAvdNameForSerial, c as assertOwnedAvdStopped, f as bootAndroidEmulator, j as ownedAvdSystemImage, k as ownedAvdMatchesConfiguration, m as createOwnedAvd, p as configureNewOwnedAvd, u as avdPoolConfiguration, w as listInstalledSystemImages } from "./android-DWssqHQ8.mjs";
9
- import { l as phaseLine } from "./command-output-B3ycpsYv.mjs";
10
- import { c as isLegacyDeletionClaim, d as selectParked, i as teardownParkedIosSim, l as parkedMaxSetting, o as adoptParked, r as teardownParkedAvd, s as dropParked, t as teardownOwnedAvd, u as readParked } from "./teardown-BJqEzPo_.mjs";
11
- import { t as withWorkspaceProcessLock } from "./workspace-process-lock-BYfZz_S-.mjs";
12
- import { D as hostMemoryPressureAdvice, O as readHostMemoryPressure, T as configuredIosSimulatorViewer, _ as renameIosSim, b as resolveIosCreation, c as iosRuntimeMatches, d as listIosDeviceTypes, g as parseRuntimeVersion, i as createOwnedIosSim, l as iosSimulatorFailureAdvice, m as ownedSimName, n as bootIosSim, u as listAllIosSims, v as resetIosKeychain, x as resolveOwnedIosSim, y as resetIosPrivacy } from "./ios-BcCiI4TS.mjs";
13
- import { randomUUID } from "node:crypto";
14
- import { join } from "node:path";
1
+ import { H as projectDeviceSlots, O as acquireAvdClaim, S as setDevice, T as withConfigLock$1, g as loadConfig$1, i as clearDevice, t as allConsolePortsAndSerials, u as getConfigDir, v as releaseAndroidConsolePort, x as saveConfig, z as deviceSlotPlatforms } from "./config-BEq0Qw_a.mjs";
2
+ import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
3
+ import { g as updateWorkspaceState, i as pidExists, k as workspaceId, m as readWorkspaceState$1, s as resolveProjectMetro, x as ensureWorkspaceStorage } from "./metro-BWLOJ_Wr.mjs";
4
+ import { D as makeTemporaryDirectory, O as removeTemporaryEntry } from "./worktree-BlizKDpZ.mjs";
5
+ import { a as detectIsExpo, c as ownedDeviceLabel, s as isPackageResolvable } from "./project-BXZzOABt.mjs";
6
+ import { n as spawnDeclared } from "./spawn-claims-CUleC2cM.mjs";
7
+ import { t as withWorkspaceProcessLock } from "./workspace-process-lock-BXA96myD.mjs";
8
+ import { n as stripAnsi, r as waitForChild, t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
9
+ import { l as phaseLine } from "./command-output-D5z2_bcF.mjs";
10
+ import { E as listInstalledSystemImages, I as pickDefaultSystemImage, M as ownedAvdName, P as ownedAvdSystemImage, T as listAvds, W as waitForBoot, d as avdPoolConfiguration, h as createOwnedAvd, j as ownedAvdMatchesConfiguration, k as nextConsolePort, l as assertOwnedAvdStopped, m as configureNewOwnedAvd, p as bootAndroidEmulator, w as listAdbDevices, x as getAvdNameForSerial, z as resolveOwnedAvdSerial } from "./android-787HhP5w.mjs";
11
+ import { l as androidDataPartitionSizeGbSetting, o as androidAvdConfigSetting, v as iosSimSlimProfileSetting } from "./settings-DOugPlPz.mjs";
12
+ import { _ as dropParked, b as readParked, d as teardownOwnedAvd, g as adoptParked, m as teardownParkedIosSim, p as teardownParkedAvd, v as isLegacyDeletionClaim, x as selectParked, y as parkedMaxSetting } from "./ownership-DZjdx8Gq.mjs";
13
+ import { E as configuredIosSimulatorViewer, O as hostMemoryPressureAdvice, S as resolveOwnedIosSim, b as resetIosPrivacy, c as iosSimulatorFailureAdvice, d as listIosDeviceTypes, g as parseRuntimeVersion, k as readHostMemoryPressure, l as listAllIosSims, m as ownedSimName, n as bootIosSim, r as createOwnedIosSim, v as renameIosSim, x as resolveIosCreation, y as resetIosKeychain } from "./ios-B2Fpqvlb.mjs";
14
+ import { i as expoBinRefusal, r as expoBinPath, t as cleanLine } from "./server-expo-BvOCeakp.mjs";
15
+ import { r as LAUNCH_UNVERIFIED, t as LAUNCH_BUNDLING } from "./launch-verify-CQS_Kg-C.mjs";
16
+ import { a as liveOwnedDeviceCount, i as deviceTypeMismatch, t as reconcileSimSlim } from "./simslim-Ca3487H6.mjs";
17
+ import { s as startDevServer } from "./start-F9gv5dcC.mjs";
18
+ import { d as startBuildHeartbeat, f as tailLines, n as HEARTBEAT_INTERVAL_MS } from "./xcode-B2tBkf0m.mjs";
19
+ import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
20
+ import { isJsonObject, readIdleStop, readJsonObject } from "@stim-cli/core/state";
21
+ import { basename, join } from "node:path";
15
22
  import { claimRemoveCommand, clearClaimChild, markClaimChildPending, processGroupAlive, releaseClaim, setClaimChild } from "@stim-cli/core/ownership-claim";
16
23
  import { captureProcessIdentity } from "@stim-cli/core/process-identity";
17
- import { isDeepStrictEqual } from "node:util";
24
+ import { randomUUID } from "node:crypto";
18
25
  import chalk from "chalk";
19
- //#region src/engine/simslim.ts
20
- async function reconcileSimSlim({ udid, profile, previouslyManaged = false, out = () => {}, spawn, timeoutMs = 72e4, cleanupMs = 1e4 }) {
21
- if (!profile && !previouslyManaged) return {
22
- managed: false,
23
- profile: null
24
- };
25
- const action = profile ? "on" : "off";
26
- const args = profile ? [
27
- "on",
28
- udid,
29
- "--profile",
30
- profile
31
- ] : ["off", udid];
32
- const lines = [];
33
- const onLine = (raw) => {
34
- const line = stripAnsi(raw).trim();
35
- if (!line) return;
36
- lines.push(line);
37
- if (lines.length > 20) lines.shift();
38
- out(`SimSlim: ${line}`);
39
- };
40
- const stdout = createLineReader(onLine);
41
- const stderr = createLineReader(onLine);
42
- const root = join(getConfigDir(), "simslim-locks", `${encodeURIComponent(udid.toLowerCase())}.lock`);
43
- const label = `SimSlim for ${udid}`;
44
- const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
45
- root,
46
- mode: "exclusive",
47
- label
48
- });
49
- if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
50
- const claim = attempt.acquired;
51
- if (!claim) throw new ownership_claim_exports.ClaimRefusedError({
52
- root,
53
- claimPath: attempt.held?.path ?? root,
54
- label,
55
- reason: "another SimSlim operation is still using this simulator"
56
- });
57
- let child;
58
- let result;
59
- let identity;
60
- let cancellation;
61
- const stopGroup = () => {
62
- if (child?.pid !== void 0 && identity && (0, process_identity_exports.inspectProcessIdentity)(identity) === "same") try {
63
- process.kill(-child.pid, "SIGKILL");
64
- } catch {}
65
- };
66
- const cancel = (signal) => {
67
- cancellation ??= signal;
68
- stopGroup();
69
- };
70
- const onInterrupt = () => cancel("SIGINT");
71
- const onTerminate = () => cancel("SIGTERM");
72
- const groupAlive = () => child?.pid !== void 0 && (0, ownership_claim_exports.processGroupAlive)(child.pid);
73
- const settled = () => result !== void 0 && !groupAlive();
74
- const waitUntil = async (deadline, cancellable = false) => {
75
- while (!settled() && Date.now() < deadline) {
76
- if (cancellable && cancellation) break;
77
- await new Promise((resolve) => setTimeout(resolve, Math.min(50, deadline - Date.now())));
78
- }
79
- return settled();
80
- };
81
- process.on("SIGINT", onInterrupt);
82
- process.on("SIGTERM", onTerminate);
83
- process.on("exit", stopGroup);
84
- try {
85
- (0, ownership_claim_exports.markClaimChildPending)(claim);
86
- try {
87
- child = (spawn ?? ((cmd, childArgs, opts) => getExecutor().spawn(cmd, childArgs, opts)))("simslim", args, {
88
- stdio: [
89
- "ignore",
90
- "pipe",
91
- "pipe"
92
- ],
93
- detached: true
26
+ import { isDeepStrictEqual } from "node:util";
27
+ import { request } from "node:http";
28
+ //#region src/engine/metro-warmup.ts
29
+ const WARMUP_TIMEOUT_MS = 6e4;
30
+ async function requestLocal(url, platform, prefetch) {
31
+ return new Promise((resolve) => {
32
+ const req = request(url, { headers: {
33
+ accept: "application/json",
34
+ "expo-platform": platform,
35
+ ...prefetch ? { "x-stim-metro-warmup": "1" } : {}
36
+ } });
37
+ const timer = setTimeout(() => req.destroy(), WARMUP_TIMEOUT_MS);
38
+ timer.unref();
39
+ req.on("socket", (socket) => socket.unref());
40
+ req.on("error", () => resolve(null));
41
+ req.on("close", () => {
42
+ clearTimeout(timer);
43
+ resolve(null);
44
+ });
45
+ req.on("response", (res) => {
46
+ let body = "";
47
+ res.setEncoding("utf8");
48
+ res.on("data", (chunk) => {
49
+ if (!prefetch) body += chunk;
94
50
  });
95
- } catch (error) {
96
- throw simslimLaunchError(error);
97
- }
98
- child.stdout?.on("data", (chunk) => stdout.push(chunk));
99
- child.stderr?.on("data", (chunk) => stderr.push(chunk));
100
- waitForChild(child).then((value) => {
101
- result = value;
51
+ res.on("error", () => resolve(null));
52
+ res.on("end", () => resolve(res.statusCode === 200 ? body : null));
102
53
  });
103
- if (child.pid !== void 0) {
104
- const captured = (0, process_identity_exports.captureProcessIdentity)(child.pid);
105
- if (captured.ok) {
106
- identity = {
107
- pid: child.pid,
108
- processToken: captured.token
109
- };
110
- try {
111
- (0, ownership_claim_exports.setClaimChild)(claim, identity);
112
- } catch {}
113
- }
114
- }
115
- const completed = await waitUntil(Date.now() + timeoutMs, true);
116
- if (cancellation || !completed) {
117
- stopGroup();
118
- const stopped = await waitUntil(Date.now() + cleanupMs);
119
- const detail = lines.length ? ` ${lines.join(" | ")}` : "";
120
- const recovery = stopped ? "Its process group has stopped. Retry stim ios to reconcile the retained simulator settings." : `Its process group could not be confirmed stopped. The claim remains at ${claim.path}. Inspect the SimSlim process group ${child.pid ?? "unknown"} before retrying. Only when nothing is using it, remove the claim: ${(0, ownership_claim_exports.claimRemoveCommand)(claim.path)}`;
121
- const message = `SimSlim ${action} ${cancellation ? `interrupted by ${cancellation}` : `exceeded ${Math.round(timeoutMs / 1e3)}s`}. ${recovery}${detail}`;
122
- if (cancellation) out(message);
123
- throw Object.assign(new Error(message), { code: cancellation ? "EINTR" : "ETIMEDOUT" });
54
+ req.end();
55
+ });
56
+ }
57
+ async function warmMetro({ port, platform, isExpo, appId, bundleUrl }) {
58
+ try {
59
+ const origin = `http://127.0.0.1:${port}`;
60
+ if (await requestLocal(new URL("/_stim/metro-warmup", origin), platform, false) !== "ready") return;
61
+ const url = new URL("/index.bundle", origin);
62
+ url.search = new URLSearchParams({
63
+ platform,
64
+ dev: "true",
65
+ lazy: "true",
66
+ minify: "false",
67
+ ...platform === "ios" ? { inlineSourceMap: "false" } : {},
68
+ modulesOnly: "false",
69
+ runModule: "true",
70
+ excludeSource: "true",
71
+ sourcePaths: "url-server",
72
+ ...appId ? { app: appId } : {}
73
+ }).toString();
74
+ if (!bundleUrl && isExpo) {
75
+ const body = await requestLocal(new URL(origin), platform, false);
76
+ if (body === null) return;
77
+ const manifest = JSON.parse(body);
78
+ bundleUrl = manifest.launchAsset?.url ?? manifest.bundleUrl;
79
+ if (typeof bundleUrl !== "string") return;
124
80
  }
125
- } finally {
126
- if (!child || settled()) (0, ownership_claim_exports.releaseClaim)(claim);
127
- else {
128
- child.stdout?.destroy?.();
129
- child.stderr?.destroy?.();
130
- child.unref();
81
+ if (bundleUrl) {
82
+ const bundle = new URL(bundleUrl, origin);
83
+ url.pathname = bundle.pathname;
84
+ url.search = bundle.search;
131
85
  }
132
- process.off("SIGINT", onInterrupt);
133
- process.off("SIGTERM", onTerminate);
134
- process.off("exit", stopGroup);
135
- if (cancellation) process.exit(cancellation === "SIGINT" ? 130 : 143);
136
- }
137
- stdout.flush();
138
- stderr.flush();
139
- if (result?.error) throw simslimLaunchError(result.error);
140
- if (result?.code !== 0) {
141
- const detail = lines.length ? ` ${lines.join(" | ")}` : "";
142
- throw new Error(`SimSlim ${action} failed with exit code ${result?.code ?? "unknown"}.${detail}`);
143
- }
86
+ await requestLocal(url, platform, true);
87
+ } catch {}
88
+ }
89
+ //#endregion
90
+ //#region src/commands/native-runtime.ts
91
+ const sleep$1 = (ms) => new Promise((r) => setTimeout(r, ms));
92
+ async function ensureDevServer({ root, port, settings, remote, note, resolve = resolveProjectMetro, start = startDevServer, readState = readWorkspaceState$1 }) {
93
+ const held = port === null ? null : (await resolve(port, root)).metro;
94
+ if (port !== null && held) return {
95
+ ok: true,
96
+ port,
97
+ pid: held.pid ?? null,
98
+ devServer: null,
99
+ reclaimed: []
100
+ };
101
+ const reason = readIdleStop(readState(root)) ? "stopped (idle)" : "not running";
102
+ note(chalk.dim(phaseLine("metro", `dev server ${reason}; starting it${remote ? " for a remote device" : ""}`)));
103
+ const result = await start({
104
+ root,
105
+ settings,
106
+ remote,
107
+ out: note,
108
+ note
109
+ });
110
+ if (!result.ok) return {
111
+ ok: false,
112
+ code: result.error.code,
113
+ message: `Could not start this workspace's dev server: ${result.error.message}`,
114
+ remedy: result.error.remedy,
115
+ lines: result.lines,
116
+ reclaimed: result.reclaimed
117
+ };
118
+ const { facts } = result;
144
119
  return {
145
- managed: Boolean(profile),
146
- profile: profile ?? null
120
+ ok: true,
121
+ port: facts.port,
122
+ pid: facts.supervisorPid,
123
+ devServer: facts.alreadyRunning ? null : {
124
+ started: true,
125
+ reason
126
+ },
127
+ reclaimed: result.reclaimed
147
128
  };
148
129
  }
149
- function simslimLaunchError(error) {
150
- const cause = error;
151
- if (cause?.code === "ENOENT") return new Error("SimSlim is configured but the `simslim` command is not installed. Run `brew install mobai-app/tap/simslim`.", { cause });
152
- return new Error(`Could not start SimSlim: ${String(cause?.message || cause)}`, { cause });
153
- }
154
- function simslimIsOnPath() {
130
+ async function ensureWorkspaceStorageSafely(root, { note = (_line) => {} } = {}) {
155
131
  try {
156
- return Boolean(getExecutor().findExecutable("simslim"));
157
- } catch {
158
- return false;
159
- }
160
- }
161
- //#endregion
162
- //#region src/engine/android-avd-setup.ts
163
- var AvdBootError = class extends Error {
164
- remedy;
165
- constructor(message, remedy, cause) {
166
- super(message, { cause });
167
- this.remedy = remedy;
132
+ return ensureWorkspaceStorage(root);
133
+ } catch (err) {
134
+ note(chalk.dim(`Could not prepare this workspace's Stim state: ${err?.message || err}`));
135
+ throw err;
168
136
  }
169
- };
170
- var AvdRecoveryError = class extends AvdBootError {};
171
- function findOtherOwner(avdName, projectPath, selectedSlot = "default") {
172
- for (const [path, project] of Object.entries(loadConfig()?.projects ?? {})) if (projectDeviceSlots(project).some(({ slot, platforms }) => (path !== projectPath || slot !== selectedSlot) && platforms.android?.avdName?.toLowerCase() === avdName.toLowerCase())) return path;
173
- return null;
174
137
  }
175
- async function prepareOwnedAvd({ projectPath, slot, label, previousAvdName, systemImage, configuration, configure, teardown = teardownOwnedAvd }) {
176
- const { claim, reservation, previous, avdName } = withConfigLock(() => {
177
- const previous = deviceSlotPlatforms(loadConfig()?.projects?.[projectPath], slot)?.android;
178
- if (previous?.avdName && previous.avdName !== previousAvdName) throw new Error(`Another Stim run assigned AVD ${previous.avdName} to this workspace. Retry to use it.`);
179
- if (readParked("android").some((entry) => entry.name === ownedAvdName(label)) || findOtherOwner(ownedAvdName(label), projectPath, slot)) label = `${label}-${randomUUID().slice(0, 8)}`;
180
- const avdName = ownedAvdName(label);
181
- const claim = acquireAvdClaim(avdName);
182
- const reservation = {
183
- avdName,
184
- owned: true,
185
- deviceName: avdName,
186
- setupIncomplete: true,
187
- poolConfiguration: configuration
188
- };
189
- try {
190
- setDevice(projectPath, "android", reservation, slot);
191
- return {
192
- claim,
193
- reservation,
194
- previous,
195
- avdName
196
- };
197
- } catch (error) {
198
- releaseClaim(claim);
199
- throw error;
200
- }
201
- });
202
- const currentRecord = () => deviceSlotPlatforms(loadConfig()?.projects?.[projectPath], slot)?.android;
203
- const assertReservation = () => {
204
- if (readParked("android").some((entry) => entry.name === avdName)) throw new Error(`AVD ${avdName} was parked by another Stim run. Retry to adopt it safely.`);
205
- const owner = findOtherOwner(avdName, projectPath, slot);
206
- if (owner) throw new Error(`AVD ${avdName} already exists and is owned by another project (${owner}). Retry to allocate a distinct owned emulator.`);
207
- const current = currentRecord();
208
- if (!isDeepStrictEqual(current, reservation)) {
209
- const state = current?.setupIncomplete ? "has incomplete setup" : "was registered or removed";
210
- throw new Error(`AVD ${current?.avdName ?? avdName} ${state} by another concurrent Stim run. Retry after that run finishes so the recorded device is resolved safely.`);
211
- }
138
+ function launchOutcomeRecord({ launchState, release, bundleId, configuration, metroPort }) {
139
+ const unverified = launchState === LAUNCH_UNVERIFIED;
140
+ const bundling = launchState === LAUNCH_BUNDLING;
141
+ let msg;
142
+ if (release) msg = unverified ? `${bundleId} could not be verified as running after its ${configuration} launch` : `${bundleId} is running its embedded ${configuration} bundle`;
143
+ else if (unverified) msg = `no bundle request from ${bundleId} reached this workspace's Metro on port ${metroPort}`;
144
+ else if (bundling) msg = `${bundleId} requested a bundle from this workspace's Metro on port ${metroPort}; it was still being built when the launch check ended`;
145
+ else msg = `${bundleId} fetched a bundle from this workspace's Metro on port ${metroPort}`;
146
+ return {
147
+ src: "build",
148
+ level: unverified ? "warn" : "info",
149
+ event: unverified ? "launch_unverified" : bundling ? "launch_bundling" : "launch_verified",
150
+ msg
212
151
  };
213
- let prepared;
214
- let child;
215
- const creationRunning = () => child?.pid !== void 0 && processGroupAlive(child.pid);
216
- let configurationFailure;
217
- try {
218
- try {
219
- const creation = await createOwnedAvd(label, {
220
- systemImage,
221
- spawn: (...args) => {
222
- markClaimChildPending(claim);
223
- child = getExecutor().spawn(...args);
224
- if (child.pid !== void 0) {
225
- const captured = captureProcessIdentity(child.pid);
226
- if (captured.ok) try {
227
- setClaimChild(claim, {
228
- pid: child.pid,
229
- processToken: captured.token
230
- });
231
- } catch {}
232
- }
233
- return child;
234
- }
235
- }).then((result) => ({ result }), (error) => ({ error }));
236
- if (creationRunning()) throw new AvdRecoveryError(`AVD ${avdName} creation left its process group running (pid ${child.pid}); its reservation remains incomplete.`, `Wait for process group ${child.pid} to finish before retrying stim android. If its identity could not be recorded, inspect the group and remove the claim only after it stops: ${claimRemoveCommand(claim.path)}`);
237
- clearClaimChild(claim);
238
- if ("error" in creation) throw creation.error;
239
- prepared = {
240
- ...creation.result,
241
- created: true
242
- };
243
- } catch (error) {
244
- const message = String(error?.message || error);
245
- if (!message.includes("already exists")) throw error;
246
- try {
247
- if (!listAvds().includes(avdName)) throw new AvdRecoveryError(`AVD ${avdName} already exists on disk but is not listed by the emulator. ${message}`, "Run `npx stim gc` to inspect orphaned owned AVDs, then `npx stim gc --delete` to reclaim those safe to delete. Retry `stim android` after cleanup; keep any AVD that GC cannot verify.", error);
248
- withConfigLock(assertReservation);
249
- const resolved = resolveOwnedAvdSerial(avdName);
250
- if (resolved.missing || resolved.notOwned) throw new Error(`AVD ${avdName} could not be verified for recovery. Retry after checking its registration.`, { cause: error });
251
- if (!resolved.serial) assertOwnedAvdStopped(avdName);
252
- prepared = {
253
- avdName,
254
- created: false,
255
- systemImage: ownedAvdSystemImage(avdName),
256
- ...resolved.serial ? {
257
- serial: resolved.serial,
258
- consolePort: Number(resolved.serial.replace(/^emulator-/, ""))
259
- } : {}
260
- };
261
- } catch (recoveryError) {
262
- withConfigLock(() => {
263
- if (!isDeepStrictEqual(currentRecord(), reservation)) return;
264
- if (previous) setDevice(projectPath, "android", previous, slot);
265
- else clearDevice(projectPath, "android", slot, avdName);
266
- });
267
- if (recoveryError instanceof AvdRecoveryError) throw recoveryError;
268
- throw new AvdRecoveryError(`Could not recover owned AVD ${avdName}: ${String(recoveryError?.message || recoveryError)}`, "Inspect `npx stim status` and `adb devices`. Wait for any other Stim run using this AVD to finish, then retry `stim android`. Keep the AVD and its process locks while its state is unverified.", recoveryError);
269
- }
270
- }
271
- if (prepared.created) try {
272
- configure(avdName);
273
- } catch (error) {
274
- configurationFailure = { error };
275
- }
276
- if (!configurationFailure) withConfigLock(() => {
277
- assertReservation();
278
- setDevice(projectPath, "android", {
279
- avdName,
280
- owned: true,
281
- deviceName: avdName,
282
- ...prepared.created ? { poolConfiguration: configuration } : {},
283
- ...prepared.consolePort ? { consolePort: prepared.consolePort } : {}
284
- }, slot);
285
- });
286
- } finally {
287
- if (!creationRunning()) releaseClaim(claim);
288
- else {
289
- child?.stdout?.destroy();
290
- child?.stderr?.destroy();
291
- child?.unref();
292
- }
293
- }
294
- if (configurationFailure) {
295
- const { error } = configurationFailure;
296
- const cleanup = teardown(avdName, {
297
- del: true,
298
- owner: {
299
- projectPath,
300
- slot,
301
- expectedRecord: reservation
302
- },
303
- onRemoved: () => {
304
- clearDevice(projectPath, "android", slot, avdName);
305
- }
306
- });
307
- const orphan = cleanup.status === "failed" || cleanup.status === "skipped" ? ` The owned AVD remains tracked for cleanup (${cleanup.reason || cleanup.status}); fix the cause, then retry or run \`stim gc --delete\`.` : "";
308
- throw new Error(`Created owned AVD ${avdName}, but could not configure its AVD settings: ${String(error?.message || error)}${orphan}`, { cause: error });
309
- }
310
- return prepared;
152
+ }
153
+ function isPhysicalDeviceRequest(flag) {
154
+ return flag !== null && flag !== void 0 && flag !== false;
311
155
  }
312
156
  //#endregion
313
- //#region src/engine/device.ts
314
- const ANDROID_FRESH_BOOT_TIMEOUT_MS = process.platform === "win32" ? 12e5 : 12e4;
315
- const ANDROID_BOOT_TIMEOUT_MS = process.platform === "win32" ? 12e5 : 24e4;
157
+ //#region src/engine/device-ios.ts
316
158
  function startIosBoot(udid, configure, label, out, simulatorApp) {
317
159
  const done = (async () => {
318
160
  await bootIosSim(udid, {
@@ -328,37 +170,6 @@ function startIosBoot(udid, configure, label, out, simulatorApp) {
328
170
  done
329
171
  };
330
172
  }
331
- async function ensureOwnedDevice({ platform, project, projectPath, slot, settingsRoot = projectPath, label = ownedDeviceLabel(projectPath), settings, flags = {}, note = () => {}, out = () => {}, logFile = null, alive = pidExists, configureAvd = configureNewOwnedAvd, teardownAvd = teardownOwnedAvd, reconcileIosSimulator = reconcileSimSlim }) {
332
- if (slot && slot !== "default") label = `${label}-${slot}`;
333
- const record = deviceSlotPlatforms(project, slot)?.[platform] ?? null;
334
- if (platform === "ios") return ensureOwnedIosDevice({
335
- record,
336
- projectPath,
337
- slot,
338
- settingsRoot,
339
- label,
340
- settings,
341
- flags,
342
- note,
343
- out,
344
- reconcileIosSimulator
345
- });
346
- return ensureOwnedAndroidDevice({
347
- record,
348
- projectPath,
349
- slot,
350
- settingsRoot,
351
- label,
352
- settings,
353
- flags,
354
- note,
355
- out,
356
- logFile,
357
- alive,
358
- configureAvd,
359
- teardownAvd
360
- });
361
- }
362
173
  async function ensureOwnedIosDevice({ record, projectPath, slot, settingsRoot, label, settings, flags, note, out, reconcileIosSimulator }) {
363
174
  const memoryAdvice = hostMemoryPressureAdvice(readHostMemoryPressure());
364
175
  if (memoryAdvice) out(chalk.yellow(phaseLine("memory", memoryAdvice)));
@@ -425,8 +236,8 @@ async function ensureOwnedIosDevice({ record, projectPath, slot, settingsRoot, l
425
236
  return record;
426
237
  }
427
238
  }
428
- withConfigLock(() => {
429
- const current = deviceSlotPlatforms(loadConfig()?.projects?.[projectPath], slot)?.ios;
239
+ withConfigLock$1(() => {
240
+ const current = deviceSlotPlatforms(loadConfig$1()?.projects?.[projectPath], slot)?.ios;
430
241
  if (!current) return;
431
242
  if (current.deviceUdid !== record.deviceUdid || current.owned !== record.owned) throw new Error("The simulator assignment changed during recovery. Run `stim ios` again.");
432
243
  clearDevice(projectPath, "ios", slot);
@@ -502,7 +313,7 @@ function withIosDeviceNameLock(fn) {
502
313
  }
503
314
  function ownedIosNameSuffix(label, model, projectPath, udid) {
504
315
  const names = new Set(listAllIosSims({ includeUnavailable: true }).filter((sim) => sim.udid !== udid).map((sim) => sim.name));
505
- for (const project of Object.values(loadConfig()?.projects ?? {})) for (const { platforms } of projectDeviceSlots(project)) {
316
+ for (const project of Object.values(loadConfig$1()?.projects ?? {})) for (const { platforms } of projectDeviceSlots(project)) {
506
317
  const record = platforms.ios;
507
318
  if (record?.deviceUdid !== udid && record?.deviceName) names.add(record.deviceName);
508
319
  }
@@ -574,8 +385,8 @@ function takeParkedIosSim({ projectPath, slot, label, choice, out }) {
574
385
  return null;
575
386
  }
576
387
  function clearIosAdoptionPending(projectPath, slot = "default") {
577
- withConfigLock(() => {
578
- const cfg = loadConfig();
388
+ withConfigLock$1(() => {
389
+ const cfg = loadConfig$1();
579
390
  const ios = deviceSlotPlatforms(cfg?.projects?.[projectPath], slot)?.ios;
580
391
  if (!cfg || !ios?.adoptionPending) return;
581
392
  delete ios.adopted;
@@ -608,39 +419,287 @@ async function configureOwnedIosSim({ record, projectPath, slot, profile, out, r
608
419
  setDevice(projectPath, "ios", updated, slot);
609
420
  return updated;
610
421
  }
611
- async function ensureOwnedAndroidDevice({ record, projectPath, slot, settingsRoot, label, settings, flags, note, out, logFile, alive, configureAvd, teardownAvd }) {
612
- const avdConfig = androidAvdConfigSetting(settings, settingsRoot);
613
- const configuration = avdPoolConfiguration(androidDataPartitionSizeGbSetting(settings), avdConfig);
614
- if (record?.setupIncomplete && record.avdName) {
615
- const avdName = record.avdName;
616
- const cleanup = teardownAvd(avdName, {
617
- del: true,
618
- owner: {
619
- projectPath,
620
- slot,
621
- expectedRecord: record
622
- },
623
- onRemoved: () => {
624
- clearDevice(projectPath, "android", slot, avdName);
625
- }
626
- });
627
- if (cleanup.status === "failed" || cleanup.status === "skipped") throw new AvdRecoveryError(`Owned AVD ${record.avdName} has incomplete setup and could not be deleted (${cleanup.reason || cleanup.status}). Fix the cause, then retry; Stim kept the device record for cleanup.`, "Wait for any active AVD operation to finish. If the refusal names an unresolved claim, inspect its creator and native processes before removing only that claim. Retry `stim android` to reconcile the incomplete device.");
628
- record = null;
629
- }
630
- if (record?.avdName) {
631
- if (record.owned) {
632
- const resolved = resolveOwnedAvdSerial(record.avdName);
633
- if (resolved.notOwned) note(chalk.yellow(`Note: recorded AVD ${record.avdName} is not Stim-owned by name -- creating a fresh owned AVD instead of reusing it.`));
634
- else if (resolved.serial) {
635
- const consolePort = Number(resolved.serial.replace(/^emulator-/, ""));
636
- if (record.consolePort && record.consolePort !== consolePort) out(phaseLine("device", `${record.avdName} changed serial (emulator-${record.consolePort} -> ${resolved.serial}); reconnecting this run, reopen agent-device on ${resolved.serial}`));
637
- const updated = {
638
- ...record,
639
- avdName: record.avdName,
640
- consolePort,
641
- owned: true,
642
- deviceName: record.deviceName ?? record.avdName
643
- };
422
+ async function ensureIosBooted({ device, simulatorApp, timeoutMs, pollMs, out }) {
423
+ const udid = device?.deviceUdid;
424
+ if (!udid) return {
425
+ failed: true,
426
+ reason: "No iOS simulator is recorded for this project."
427
+ };
428
+ const ready = () => {
429
+ try {
430
+ getExecutor().runFile("xcrun", [
431
+ "simctl",
432
+ "spawn",
433
+ udid,
434
+ "launchctl",
435
+ "list"
436
+ ], { timeoutMs: 3e4 });
437
+ return {
438
+ ok: true,
439
+ udid
440
+ };
441
+ } catch (error) {
442
+ return {
443
+ failed: true,
444
+ reason: `Simulator ${udid} failed its process-spawn readiness check: ${error?.message || error}. ${iosSimulatorFailureAdvice()}`
445
+ };
446
+ }
447
+ };
448
+ const booting = device?.booting;
449
+ if (booting?.udid === udid) {
450
+ try {
451
+ await booting.done;
452
+ } catch (e) {
453
+ return {
454
+ failed: true,
455
+ reason: `Could not boot simulator ${udid}: ${e?.message || e}`
456
+ };
457
+ }
458
+ return ready();
459
+ }
460
+ let resolved;
461
+ try {
462
+ resolved = resolveOwnedIosSim(udid);
463
+ } catch (e) {
464
+ return {
465
+ failed: true,
466
+ reason: `Could not list simulators: ${e?.message || e}`
467
+ };
468
+ }
469
+ if (resolved.missing) return {
470
+ failed: true,
471
+ reason: `Simulator ${udid} no longer exists. Run \`stim ios\` again to create a fresh owned sim.`
472
+ };
473
+ if (resolved.notOwned) return {
474
+ failed: true,
475
+ reason: `Simulator ${udid} is now named "${resolved.notOwned}" and is not Stim-owned; refusing to boot it.`
476
+ };
477
+ const sim = resolved.sim;
478
+ if (sim.state === "Booted") {
479
+ const result = ready();
480
+ if (result.ok && simulatorApp !== void 0) configuredIosSimulatorViewer(simulatorApp).open(udid);
481
+ return result;
482
+ }
483
+ out(chalk.dim(phaseLine("device", `booting ${sim.name} (${udid})`)));
484
+ const bootDeadline = Date.now() + timeoutMs;
485
+ try {
486
+ await bootIosSim(udid, {
487
+ timeoutMs,
488
+ label: sim.name,
489
+ out,
490
+ simulatorApp
491
+ });
492
+ } catch (e) {
493
+ return {
494
+ failed: true,
495
+ reason: `Could not boot simulator ${udid}: ${e?.message || e}`
496
+ };
497
+ }
498
+ const deadline = Math.max(bootDeadline, Date.now() + 2 * pollMs);
499
+ while (Date.now() < deadline) {
500
+ await sleep(pollMs);
501
+ let state = null;
502
+ try {
503
+ state = listAllIosSims({ timeoutMs: 3e4 }).find((s) => s.udid === udid)?.state ?? null;
504
+ } catch {}
505
+ if (state === "Booted") return ready();
506
+ }
507
+ return {
508
+ failed: true,
509
+ reason: `Simulator ${udid} did not reach the Booted state within ${Math.round(timeoutMs / 1e3)}s.`
510
+ };
511
+ }
512
+ function sleep(ms) {
513
+ return new Promise((r) => setTimeout(r, ms));
514
+ }
515
+ //#endregion
516
+ //#region src/engine/android-avd-setup.ts
517
+ var AvdBootError = class extends Error {
518
+ remedy;
519
+ constructor(message, remedy, cause) {
520
+ super(message, { cause });
521
+ this.remedy = remedy;
522
+ }
523
+ };
524
+ var AvdRecoveryError = class extends AvdBootError {};
525
+ function findOtherOwner(avdName, projectPath, selectedSlot = "default") {
526
+ for (const [path, project] of Object.entries(loadConfig$1()?.projects ?? {})) if (projectDeviceSlots(project).some(({ slot, platforms }) => (path !== projectPath || slot !== selectedSlot) && platforms.android?.avdName?.toLowerCase() === avdName.toLowerCase())) return path;
527
+ return null;
528
+ }
529
+ async function prepareOwnedAvd({ projectPath, slot, label, previousAvdName, systemImage, configuration, configure, teardown = teardownOwnedAvd }) {
530
+ const { claim, reservation, previous, avdName } = withConfigLock$1(() => {
531
+ const previous = deviceSlotPlatforms(loadConfig$1()?.projects?.[projectPath], slot)?.android;
532
+ if (previous?.avdName && previous.avdName !== previousAvdName) throw new Error(`Another Stim run assigned AVD ${previous.avdName} to this workspace. Retry to use it.`);
533
+ if (readParked("android").some((entry) => entry.name === ownedAvdName(label)) || findOtherOwner(ownedAvdName(label), projectPath, slot)) label = `${label}-${randomUUID().slice(0, 8)}`;
534
+ const avdName = ownedAvdName(label);
535
+ const claim = acquireAvdClaim(avdName);
536
+ const reservation = {
537
+ avdName,
538
+ owned: true,
539
+ deviceName: avdName,
540
+ setupIncomplete: true,
541
+ poolConfiguration: configuration
542
+ };
543
+ try {
544
+ setDevice(projectPath, "android", reservation, slot);
545
+ return {
546
+ claim,
547
+ reservation,
548
+ previous,
549
+ avdName
550
+ };
551
+ } catch (error) {
552
+ releaseClaim(claim);
553
+ throw error;
554
+ }
555
+ });
556
+ const currentRecord = () => deviceSlotPlatforms(loadConfig$1()?.projects?.[projectPath], slot)?.android;
557
+ const assertReservation = () => {
558
+ if (readParked("android").some((entry) => entry.name === avdName)) throw new Error(`AVD ${avdName} was parked by another Stim run. Retry to adopt it safely.`);
559
+ const owner = findOtherOwner(avdName, projectPath, slot);
560
+ if (owner) throw new Error(`AVD ${avdName} already exists and is owned by another project (${owner}). Retry to allocate a distinct owned emulator.`);
561
+ const current = currentRecord();
562
+ if (!isDeepStrictEqual(current, reservation)) {
563
+ const state = current?.setupIncomplete ? "has incomplete setup" : "was registered or removed";
564
+ throw new Error(`AVD ${current?.avdName ?? avdName} ${state} by another concurrent Stim run. Retry after that run finishes so the recorded device is resolved safely.`);
565
+ }
566
+ };
567
+ let prepared;
568
+ let child;
569
+ const creationRunning = () => child?.pid !== void 0 && processGroupAlive(child.pid);
570
+ let configurationFailure;
571
+ try {
572
+ try {
573
+ const creation = await createOwnedAvd(label, {
574
+ systemImage,
575
+ spawn: (...args) => {
576
+ markClaimChildPending(claim);
577
+ child = getExecutor().spawn(...args);
578
+ if (child.pid !== void 0) {
579
+ const captured = captureProcessIdentity(child.pid);
580
+ if (captured.ok) try {
581
+ setClaimChild(claim, {
582
+ pid: child.pid,
583
+ processToken: captured.token
584
+ });
585
+ } catch {}
586
+ }
587
+ return child;
588
+ }
589
+ }).then((result) => ({ result }), (error) => ({ error }));
590
+ if (creationRunning()) throw new AvdRecoveryError(`AVD ${avdName} creation left its process group running (pid ${child.pid}); its reservation remains incomplete.`, `Wait for process group ${child.pid} to finish before retrying stim android. If its identity could not be recorded, inspect the group and remove the claim only after it stops: ${claimRemoveCommand(claim.path)}`);
591
+ clearClaimChild(claim);
592
+ if ("error" in creation) throw creation.error;
593
+ prepared = {
594
+ ...creation.result,
595
+ created: true
596
+ };
597
+ } catch (error) {
598
+ const message = String(error?.message || error);
599
+ if (!message.includes("already exists")) throw error;
600
+ try {
601
+ if (!listAvds().includes(avdName)) throw new AvdRecoveryError(`AVD ${avdName} already exists on disk but is not listed by the emulator. ${message}`, "Run `npx stim gc` to inspect orphaned owned AVDs, then `npx stim gc --delete` to reclaim those safe to delete. Retry `stim android` after cleanup; keep any AVD that GC cannot verify.", error);
602
+ withConfigLock$1(assertReservation);
603
+ const resolved = resolveOwnedAvdSerial(avdName);
604
+ if (resolved.missing || resolved.notOwned) throw new Error(`AVD ${avdName} could not be verified for recovery. Retry after checking its registration.`, { cause: error });
605
+ if (!resolved.serial) assertOwnedAvdStopped(avdName);
606
+ prepared = {
607
+ avdName,
608
+ created: false,
609
+ systemImage: ownedAvdSystemImage(avdName),
610
+ ...resolved.serial ? {
611
+ serial: resolved.serial,
612
+ consolePort: Number(resolved.serial.replace(/^emulator-/, ""))
613
+ } : {}
614
+ };
615
+ } catch (recoveryError) {
616
+ withConfigLock$1(() => {
617
+ if (!isDeepStrictEqual(currentRecord(), reservation)) return;
618
+ if (previous) setDevice(projectPath, "android", previous, slot);
619
+ else clearDevice(projectPath, "android", slot, avdName);
620
+ });
621
+ if (recoveryError instanceof AvdRecoveryError) throw recoveryError;
622
+ throw new AvdRecoveryError(`Could not recover owned AVD ${avdName}: ${String(recoveryError?.message || recoveryError)}`, "Inspect `npx stim status` and `adb devices`. Wait for any other Stim run using this AVD to finish, then retry `stim android`. Keep the AVD and its process locks while its state is unverified.", recoveryError);
623
+ }
624
+ }
625
+ if (prepared.created) try {
626
+ configure(avdName);
627
+ } catch (error) {
628
+ configurationFailure = { error };
629
+ }
630
+ if (!configurationFailure) withConfigLock$1(() => {
631
+ assertReservation();
632
+ setDevice(projectPath, "android", {
633
+ avdName,
634
+ owned: true,
635
+ deviceName: avdName,
636
+ ...prepared.created ? { poolConfiguration: configuration } : {},
637
+ ...prepared.consolePort ? { consolePort: prepared.consolePort } : {}
638
+ }, slot);
639
+ });
640
+ } finally {
641
+ if (!creationRunning()) releaseClaim(claim);
642
+ else {
643
+ child?.stdout?.destroy();
644
+ child?.stderr?.destroy();
645
+ child?.unref();
646
+ }
647
+ }
648
+ if (configurationFailure) {
649
+ const { error } = configurationFailure;
650
+ const cleanup = teardown(avdName, {
651
+ del: true,
652
+ owner: {
653
+ projectPath,
654
+ slot,
655
+ expectedRecord: reservation
656
+ },
657
+ onRemoved: () => {
658
+ clearDevice(projectPath, "android", slot, avdName);
659
+ }
660
+ });
661
+ const orphan = cleanup.status === "failed" || cleanup.status === "skipped" ? ` The owned AVD remains tracked for cleanup (${cleanup.reason || cleanup.status}); fix the cause, then retry or run \`stim gc --delete\`.` : "";
662
+ throw new Error(`Created owned AVD ${avdName}, but could not configure its AVD settings: ${String(error?.message || error)}${orphan}`, { cause: error });
663
+ }
664
+ return prepared;
665
+ }
666
+ //#endregion
667
+ //#region src/engine/device-android.ts
668
+ const ANDROID_FRESH_BOOT_TIMEOUT_MS = process.platform === "win32" ? 12e5 : 12e4;
669
+ const ANDROID_BOOT_TIMEOUT_MS = process.platform === "win32" ? 12e5 : 24e4;
670
+ async function ensureOwnedAndroidDevice({ record, projectPath, slot, settingsRoot, label, settings, flags, note, out, logFile, alive, configureAvd, teardownAvd }) {
671
+ const avdConfig = androidAvdConfigSetting(settings, settingsRoot);
672
+ const configuration = avdPoolConfiguration(androidDataPartitionSizeGbSetting(settings), avdConfig);
673
+ if (record?.setupIncomplete && record.avdName) {
674
+ const avdName = record.avdName;
675
+ const cleanup = teardownAvd(avdName, {
676
+ del: true,
677
+ owner: {
678
+ projectPath,
679
+ slot,
680
+ expectedRecord: record
681
+ },
682
+ onRemoved: () => {
683
+ clearDevice(projectPath, "android", slot, avdName);
684
+ }
685
+ });
686
+ if (cleanup.status === "failed" || cleanup.status === "skipped") throw new AvdRecoveryError(`Owned AVD ${record.avdName} has incomplete setup and could not be deleted (${cleanup.reason || cleanup.status}). Fix the cause, then retry; Stim kept the device record for cleanup.`, "Wait for any active AVD operation to finish. If the refusal names an unresolved claim, inspect its creator and native processes before removing only that claim. Retry `stim android` to reconcile the incomplete device.");
687
+ record = null;
688
+ }
689
+ if (record?.avdName) {
690
+ if (record.owned) {
691
+ const resolved = resolveOwnedAvdSerial(record.avdName);
692
+ if (resolved.notOwned) note(chalk.yellow(`Note: recorded AVD ${record.avdName} is not Stim-owned by name -- creating a fresh owned AVD instead of reusing it.`));
693
+ else if (resolved.serial) {
694
+ const consolePort = Number(resolved.serial.replace(/^emulator-/, ""));
695
+ if (record.consolePort && record.consolePort !== consolePort) out(phaseLine("device", `${record.avdName} changed serial (emulator-${record.consolePort} -> ${resolved.serial}); reconnecting this run, reopen agent-device on ${resolved.serial}`));
696
+ const updated = {
697
+ ...record,
698
+ avdName: record.avdName,
699
+ consolePort,
700
+ owned: true,
701
+ deviceName: record.deviceName ?? record.avdName
702
+ };
644
703
  setDevice(projectPath, "android", updated, slot);
645
704
  return {
646
705
  ...updated,
@@ -761,7 +820,7 @@ async function ensureOwnedAndroidDevice({ record, projectPath, slot, settingsRoo
761
820
  systemImage: created.systemImage
762
821
  };
763
822
  }
764
- function claimAndroidConsolePort({ projectPath, slot, avdName, deviceName, livePorts = [], metadata }, { lock = withConfigLock, recordedPorts = () => allConsolePortsAndSerials().androidConsolePorts, record = setDevice } = {}) {
823
+ function claimAndroidConsolePort({ projectPath, slot, avdName, deviceName, livePorts = [], metadata }, { lock = withConfigLock$1, recordedPorts = () => allConsolePortsAndSerials().androidConsolePorts, record = setDevice } = {}) {
765
824
  return lock(() => {
766
825
  const consolePort = nextConsolePort([...recordedPorts(), ...livePorts]);
767
826
  const claim = {
@@ -792,296 +851,78 @@ async function bootOwnedAvdOnFreshPort({ avdName, metadata, projectPath, slot, d
792
851
  const serial = `emulator-${claim.consolePort}`;
793
852
  try {
794
853
  reportAndroidMemoryPressure(out);
795
- const pid = bootAndroidEmulator(avdName, claim.consolePort, { logFile });
796
- out(chalk.dim(phaseLine("device", `waiting for ${serial} to finish booting`)));
797
- const result = await waitForAndroidBoot({
798
- serial,
799
- timeoutMs: ANDROID_FRESH_BOOT_TIMEOUT_MS,
800
- pid,
801
- alive,
802
- out
803
- });
804
- if (result.failed) throw new AvdBootError(result.reason, result.remedy);
805
- const running = getAvdNameForSerial(serial);
806
- if (running && running !== avdName) throw new Error(`${serial} is running AVD ${running}, not this workspace's owned AVD ${avdName}; refusing to use it.`);
807
- return {
808
- ...claim,
809
- serial
810
- };
811
- } catch (error) {
812
- releaseAndroidConsolePort(projectPath, claim.consolePort, slot);
813
- throw error;
814
- }
815
- }
816
- function reportAndroidMemoryPressure(out) {
817
- const pressure = readHostMemoryPressure();
818
- if (pressure === "warning" || pressure === "critical") out(chalk.dim(phaseLine("memory", `macOS reports ${pressure} host memory pressure; emulator boot may be slow. Free memory with \`stim stop\` only in workspaces you own; ask before closing other apps or devices.`)));
819
- }
820
- function androidBootRemedy(pressure) {
821
- let count = null;
822
- try {
823
- count = liveOwnedDeviceCount({
824
- sims: process.platform === "darwin" ? listAllIosSims({ timeoutMs: 2e3 }) : [],
825
- adbEmulators: listAdbDevices({ timeoutMs: 2e3 }).emulators,
826
- config: loadConfig()
827
- });
828
- } catch {}
829
- const observation = pressure === "warning" || pressure === "critical" ? `macOS reports ${pressure} host memory pressure. ` : "";
830
- const devices = count ? `${count} Stim-owned ${count === 1 ? "device is" : "devices are"} running. ` : "";
831
- if (observation || devices) return `${observation}${devices}Stop an unneeded device with \`stim stop\` only in a workspace you own, then run \`stim android\` again; ask before closing other apps or devices. These observations do not establish the cause of the timeout or an OOM crash.`;
832
- return "Inspect the emulator log and run `stim doctor` to check JAVA_HOME, ANDROID_HOME, and installed system images, then run `stim android` again.";
833
- }
834
- async function waitForAndroidBoot({ serial, timeoutMs, pid = null, alive = pidExists, out }) {
835
- const aborted = () => pid !== null && !alive(pid);
836
- const wait = (windowMs) => waitForBoot(serial, windowMs, {
837
- aborted,
838
- commandTimeoutMs: 5e3
839
- });
840
- let result = await wait(timeoutMs);
841
- if (result.ok) return {
842
- ok: true,
843
- serial
844
- };
845
- let pressure = readHostMemoryPressure();
846
- let elapsedMs = timeoutMs;
847
- if (!result.exited && pid !== null && !aborted() && (pressure === "warning" || pressure === "critical")) {
848
- const extensionMs = 24e4;
849
- out(chalk.dim(phaseLine("memory", `macOS reports ${pressure} host memory pressure; retrying the boot wait once for the same live emulator ${serial} (up to ${extensionMs / 1e3}s more).`)));
850
- result = await wait(extensionMs);
851
- if (result.ok) return {
852
- ok: true,
853
- serial
854
- };
855
- elapsedMs += extensionMs;
856
- pressure = readHostMemoryPressure();
857
- }
858
- const exited = result.exited || aborted();
859
- return {
860
- failed: true,
861
- reason: `${exited ? `The emulator process for ${serial} exited before the device finished booting.` : `Emulator ${serial} did not finish booting within ${Math.round(elapsedMs / 1e3)}s.`} Diagnostic: ${JSON.stringify(result.diagnostic)}`,
862
- remedy: exited ? "Inspect the emulator log for the process exit, fix what it reports, then run `stim android` again." : androidBootRemedy(pressure)
863
- };
864
- }
865
- function liveOwnedDeviceCount({ sims = [], adbEmulators = [], config = null } = {}) {
866
- let count = 0;
867
- for (const sim of sims) if (sim?.state === "Booted" && sim.name?.startsWith("stim-")) count++;
868
- const livePorts = new Set(adbEmulators.map((e) => e.consolePort));
869
- for (const proj of Object.values(config?.projects || {})) for (const { platforms } of projectDeviceSlots(proj)) {
870
- const android = platforms.android;
871
- if (android?.owned && android.avdName && typeof android.consolePort === "number" && livePorts.has(android.consolePort)) count++;
872
- }
873
- return count;
874
- }
875
- function workspaceHasLiveDevice({ platform, project, slot = "default", sims = [], adbEmulators = [] } = {}) {
876
- if (!platform) return false;
877
- const record = deviceSlotPlatforms(project, slot)?.[platform];
878
- if (!record) return false;
879
- if (platform === "ios") return sims.some((s) => s.udid === record.deviceUdid && s.state === "Booted");
880
- return typeof record.consolePort === "number" && adbEmulators.some((e) => e.consolePort === record.consolePort);
881
- }
882
- function deviceCapacityRefusal({ platform, project, slot = "default", max, sims = [], adb = null, config = null } = {}) {
883
- if (!max || max <= 0) return null;
884
- const adbEmulators = adb?.emulators || [];
885
- if (workspaceHasLiveDevice({
886
- platform,
887
- project,
888
- slot,
889
- sims,
890
- adbEmulators
891
- })) return null;
892
- const count = liveOwnedDeviceCount({
893
- sims,
894
- adbEmulators,
895
- config
896
- });
897
- if (count < max) return null;
898
- return {
899
- code: "STIM_AT_CAPACITY",
900
- message: `${count} Stim device(s) are already booted and concurrency.maxDevices is ${max}, so booting another would exceed the cap.`,
901
- remedy: "stop an environment (stim stop) or raise concurrency.maxDevices"
902
- };
903
- }
904
- function checkDeviceCapacity({ platform, project, slot = "default", max, sims = listAllIosSims, adb = listAdbDevices, config = loadConfig } = {}) {
905
- if (!max || max <= 0) return null;
906
- let simList = [];
907
- let adbRes = {
908
- emulators: [],
909
- physical: [],
910
- unhealthy: []
911
- };
912
- try {
913
- simList = typeof sims === "function" ? sims() || [] : sims || [];
914
- } catch {}
915
- try {
916
- adbRes = typeof adb === "function" ? adb() || adbRes : adb || adbRes;
917
- } catch {}
918
- let cfg = null;
919
- try {
920
- cfg = typeof config === "function" ? config() : config ?? null;
921
- } catch {}
922
- return deviceCapacityRefusal({
923
- platform,
924
- project,
925
- slot,
926
- max,
927
- sims: simList,
928
- adb: adbRes,
929
- config: cfg
930
- });
931
- }
932
- function deviceTypeMismatch(recordedTypeId, requestedName, deviceTypes) {
933
- if (!requestedName || !recordedTypeId) return null;
934
- const wanted = (deviceTypes || []).find((d) => d.name === requestedName);
935
- if (!wanted) return null;
936
- if (wanted.identifier === recordedTypeId) return null;
937
- const recorded = (deviceTypes || []).find((d) => d.identifier === recordedTypeId);
938
- return `this project's sim is ${recorded ? recorded.name : recordedTypeId}, but --device-type asked for ${requestedName}`;
939
- }
940
- function installedNames(names) {
941
- const unique = [...new Set(names.filter((n) => typeof n === "string" && n !== ""))];
942
- return unique.length > 0 ? unique.join(", ") : "none";
943
- }
944
- function unknownIosRuntimeRefusal(requested, runtimes) {
945
- if (!requested) return null;
946
- if ((runtimes || []).some((r) => iosRuntimeMatches(r, requested))) return null;
947
- return {
948
- message: `No installed simulator runtime matches "${requested}". Installed runtimes: ${installedNames((runtimes || []).map((r) => r.version))}.`,
949
- remedy: "Pass `--runtime` (or set ios.runtime) a version printed above (\"26.5\") or a runtime's full name (\"iOS 26.5\"); nothing else matches. Install more runtimes through Xcode."
950
- };
951
- }
952
- function creatableIosDeviceTypeNames(runtimes, runtime) {
953
- return (runtime ? (runtimes || []).filter((r) => iosRuntimeMatches(r, runtime)) : runtimes || []).flatMap((r) => (r.supportedDeviceTypes || []).map((d) => d.name));
954
- }
955
- function unknownIosDeviceTypeRefusal(requested, runtimes, runtime) {
956
- if (!requested) return null;
957
- const creatable = creatableIosDeviceTypeNames(runtimes, runtime);
958
- if (creatable.includes(requested)) return null;
959
- return {
960
- message: `No device type named "${requested}" can be created on ${runtime ? `runtime ${runtime}` : "any installed simulator runtime"}. ${runtime ? `Device types runtime ${runtime} supports` : "Device types the installed runtimes support"}: ${installedNames(creatable)}.`,
961
- remedy: "Pass `--device-type` (or set ios.deviceType) one of the names printed above, exactly as `xcrun simctl list devicetypes` spells it. `xcrun simctl list devicetypes` also lists watchOS, tvOS and visionOS models, which no iOS runtime can create. Install more models through Xcode."
962
- };
963
- }
964
- function unknownAndroidSystemImageRefusal(requested, images) {
965
- if (!requested) return null;
966
- if ((images || []).some((i) => i.pkg === requested)) return null;
967
- return {
968
- message: `No installed Android system image is named "${requested}". Installed system images: ${installedNames((images || []).map((i) => i.pkg))}.`,
969
- remedy: "Pass `--system-image` (or set android.systemImage) to one of the package ids printed above. Install more with `sdkmanager \"system-images;android-36;google_apis;arm64-v8a\"`."
970
- };
971
- }
972
- const BOOT_POLL_MS = 500;
973
- async function ensureBooted({ platform, device, simulatorApp, timeoutMs, pollMs = BOOT_POLL_MS, out = () => {}, logFile = null, alive = pidExists } = {}) {
974
- if (platform === "ios") return ensureIosBooted({
975
- device,
976
- simulatorApp,
977
- timeoutMs: timeoutMs ?? 6e5,
978
- pollMs,
979
- out
980
- });
981
- if (platform === "android") return ensureAndroidBooted({
982
- device,
983
- timeoutMs: timeoutMs ?? ANDROID_BOOT_TIMEOUT_MS,
984
- out,
985
- logFile,
986
- alive
987
- });
988
- return {
989
- failed: true,
990
- reason: `Unknown platform "${platform}".`
991
- };
992
- }
993
- async function ensureIosBooted({ device, simulatorApp, timeoutMs, pollMs, out }) {
994
- const udid = device?.deviceUdid;
995
- if (!udid) return {
996
- failed: true,
997
- reason: "No iOS simulator is recorded for this project."
998
- };
999
- const ready = () => {
1000
- try {
1001
- getExecutor().runFile("xcrun", [
1002
- "simctl",
1003
- "spawn",
1004
- udid,
1005
- "launchctl",
1006
- "list"
1007
- ], { timeoutMs: 3e4 });
1008
- return {
1009
- ok: true,
1010
- udid
1011
- };
1012
- } catch (error) {
1013
- return {
1014
- failed: true,
1015
- reason: `Simulator ${udid} failed its process-spawn readiness check: ${error?.message || error}. ${iosSimulatorFailureAdvice()}`
1016
- };
1017
- }
1018
- };
1019
- const booting = device?.booting;
1020
- if (booting?.udid === udid) {
1021
- try {
1022
- await booting.done;
1023
- } catch (e) {
1024
- return {
1025
- failed: true,
1026
- reason: `Could not boot simulator ${udid}: ${e?.message || e}`
1027
- };
1028
- }
1029
- return ready();
1030
- }
1031
- let resolved;
1032
- try {
1033
- resolved = resolveOwnedIosSim(udid);
1034
- } catch (e) {
1035
- return {
1036
- failed: true,
1037
- reason: `Could not list simulators: ${e?.message || e}`
1038
- };
1039
- }
1040
- if (resolved.missing) return {
1041
- failed: true,
1042
- reason: `Simulator ${udid} no longer exists. Run \`stim ios\` again to create a fresh owned sim.`
1043
- };
1044
- if (resolved.notOwned) return {
1045
- failed: true,
1046
- reason: `Simulator ${udid} is now named "${resolved.notOwned}" and is not Stim-owned; refusing to boot it.`
1047
- };
1048
- const sim = resolved.sim;
1049
- if (sim.state === "Booted") {
1050
- const result = ready();
1051
- if (result.ok && simulatorApp !== void 0) configuredIosSimulatorViewer(simulatorApp).open(udid);
1052
- return result;
1053
- }
1054
- out(chalk.dim(phaseLine("device", `booting ${sim.name} (${udid})`)));
1055
- const bootDeadline = Date.now() + timeoutMs;
1056
- try {
1057
- await bootIosSim(udid, {
1058
- timeoutMs,
1059
- label: sim.name,
1060
- out,
1061
- simulatorApp
854
+ const pid = bootAndroidEmulator(avdName, claim.consolePort, { logFile });
855
+ out(chalk.dim(phaseLine("device", `waiting for ${serial} to finish booting`)));
856
+ const result = await waitForAndroidBoot({
857
+ serial,
858
+ timeoutMs: ANDROID_FRESH_BOOT_TIMEOUT_MS,
859
+ pid,
860
+ alive,
861
+ out
1062
862
  });
1063
- } catch (e) {
863
+ if (result.failed) throw new AvdBootError(result.reason, result.remedy);
864
+ const running = getAvdNameForSerial(serial);
865
+ if (running && running !== avdName) throw new Error(`${serial} is running AVD ${running}, not this workspace's owned AVD ${avdName}; refusing to use it.`);
1064
866
  return {
1065
- failed: true,
1066
- reason: `Could not boot simulator ${udid}: ${e?.message || e}`
867
+ ...claim,
868
+ serial
1067
869
  };
870
+ } catch (error) {
871
+ releaseAndroidConsolePort(projectPath, claim.consolePort, slot);
872
+ throw error;
1068
873
  }
1069
- const deadline = Math.max(bootDeadline, Date.now() + 2 * pollMs);
1070
- while (Date.now() < deadline) {
1071
- await sleep(pollMs);
1072
- let state = null;
1073
- try {
1074
- state = listAllIosSims({ timeoutMs: 3e4 }).find((s) => s.udid === udid)?.state ?? null;
1075
- } catch {}
1076
- if (state === "Booted") return ready();
874
+ }
875
+ function reportAndroidMemoryPressure(out) {
876
+ const pressure = readHostMemoryPressure();
877
+ if (pressure === "warning" || pressure === "critical") out(chalk.dim(phaseLine("memory", `macOS reports ${pressure} host memory pressure; emulator boot may be slow. Free memory with \`stim stop\` only in workspaces you own; ask before closing other apps or devices.`)));
878
+ }
879
+ function androidBootRemedy(pressure) {
880
+ let count = null;
881
+ try {
882
+ count = liveOwnedDeviceCount({
883
+ sims: process.platform === "darwin" ? listAllIosSims({ timeoutMs: 2e3 }) : [],
884
+ adbEmulators: listAdbDevices({ timeoutMs: 2e3 }).emulators,
885
+ config: loadConfig$1()
886
+ });
887
+ } catch {}
888
+ const observation = pressure === "warning" || pressure === "critical" ? `macOS reports ${pressure} host memory pressure. ` : "";
889
+ const devices = count ? `${count} Stim-owned ${count === 1 ? "device is" : "devices are"} running. ` : "";
890
+ if (observation || devices) return `${observation}${devices}Stop an unneeded device with \`stim stop\` only in a workspace you own, then run \`stim android\` again; ask before closing other apps or devices. These observations do not establish the cause of the timeout or an OOM crash.`;
891
+ return "Inspect the emulator log and run `stim doctor` to check JAVA_HOME, ANDROID_HOME, and installed system images, then run `stim android` again.";
892
+ }
893
+ async function waitForAndroidBoot({ serial, timeoutMs, pid = null, alive = pidExists, out }) {
894
+ const aborted = () => pid !== null && !alive(pid);
895
+ const wait = (windowMs) => waitForBoot(serial, windowMs, {
896
+ aborted,
897
+ commandTimeoutMs: 5e3
898
+ });
899
+ let result = await wait(timeoutMs);
900
+ if (result.ok) return {
901
+ ok: true,
902
+ serial
903
+ };
904
+ let pressure = readHostMemoryPressure();
905
+ let elapsedMs = timeoutMs;
906
+ if (!result.exited && pid !== null && !aborted() && (pressure === "warning" || pressure === "critical")) {
907
+ const extensionMs = 24e4;
908
+ out(chalk.dim(phaseLine("memory", `macOS reports ${pressure} host memory pressure; retrying the boot wait once for the same live emulator ${serial} (up to ${extensionMs / 1e3}s more).`)));
909
+ result = await wait(extensionMs);
910
+ if (result.ok) return {
911
+ ok: true,
912
+ serial
913
+ };
914
+ elapsedMs += extensionMs;
915
+ pressure = readHostMemoryPressure();
1077
916
  }
917
+ const exited = result.exited || aborted();
1078
918
  return {
1079
919
  failed: true,
1080
- reason: `Simulator ${udid} did not reach the Booted state within ${Math.round(timeoutMs / 1e3)}s.`
920
+ reason: `${exited ? `The emulator process for ${serial} exited before the device finished booting.` : `Emulator ${serial} did not finish booting within ${Math.round(elapsedMs / 1e3)}s.`} Diagnostic: ${JSON.stringify(result.diagnostic)}`,
921
+ remedy: exited ? "Inspect the emulator log for the process exit, fix what it reports, then run `stim android` again." : androidBootRemedy(pressure)
1081
922
  };
1082
923
  }
1083
- async function ensureAndroidBooted({ device, timeoutMs, out, logFile = null, alive = pidExists }) {
1084
- if (!device?.avdName) return {
924
+ async function ensureAndroidBooted({ device, projectPath, slot, timeoutMs, out, logFile = null, alive = pidExists }) {
925
+ if (!device?.avdName || !projectPath) return {
1085
926
  failed: true,
1086
927
  reason: "No owned Android emulator is recorded for this project."
1087
928
  };
@@ -1113,33 +954,622 @@ async function ensureAndroidBooted({ device, timeoutMs, out, logFile = null, ali
1113
954
  timeoutMs,
1114
955
  out
1115
956
  });
1116
- const serial = `emulator-${pickConsolePort(device.consolePort)}`;
957
+ const claim = claimAndroidConsolePort({
958
+ projectPath,
959
+ slot,
960
+ avdName: device.avdName,
961
+ deviceName: device.deviceName,
962
+ livePorts: liveAndroidConsolePorts(),
963
+ metadata: device
964
+ });
965
+ const serial = `emulator-${claim.consolePort}`;
1117
966
  reportAndroidMemoryPressure(out);
1118
967
  out(chalk.dim(phaseLine("device", `booting ${device.avdName} as ${serial}`)));
1119
968
  let pid = null;
1120
969
  try {
1121
- pid = bootAndroidEmulator(device.avdName, Number(serial.replace(/^emulator-/, "")), { logFile });
970
+ pid = bootAndroidEmulator(device.avdName, claim.consolePort, { logFile });
1122
971
  } catch (e) {
972
+ releaseAndroidConsolePort(projectPath, claim.consolePort, slot);
1123
973
  return {
1124
974
  failed: true,
1125
975
  reason: `Could not start emulator for AVD ${device.avdName}: ${e?.message || e}`
1126
976
  };
1127
977
  }
1128
- return waitForAndroidBoot({
978
+ const result = await waitForAndroidBoot({
1129
979
  serial,
1130
980
  timeoutMs,
1131
981
  pid,
1132
982
  alive,
1133
983
  out
1134
984
  });
985
+ if (result.failed) releaseAndroidConsolePort(projectPath, claim.consolePort, slot);
986
+ return result;
1135
987
  }
1136
- function pickConsolePort(recorded) {
1137
- const live = liveAndroidConsolePorts();
1138
- if (recorded && !live.includes(Number(recorded))) return Number(recorded);
1139
- return nextConsolePort([...allConsolePortsAndSerials().androidConsolePorts, ...live]);
988
+ //#endregion
989
+ //#region src/engine/device.ts
990
+ async function ensureOwnedDevice({ platform, project, projectPath, slot, settingsRoot = projectPath, label = ownedDeviceLabel(projectPath), settings, flags = {}, note = () => {}, out = () => {}, logFile = null, alive = pidExists, configureAvd = configureNewOwnedAvd, teardownAvd = teardownOwnedAvd, reconcileIosSimulator = reconcileSimSlim }) {
991
+ if (slot && slot !== "default") label = `${label}-${slot}`;
992
+ const record = deviceSlotPlatforms(project, slot)?.[platform] ?? null;
993
+ if (platform === "ios") return ensureOwnedIosDevice({
994
+ record,
995
+ projectPath,
996
+ slot,
997
+ settingsRoot,
998
+ label,
999
+ settings,
1000
+ flags,
1001
+ note,
1002
+ out,
1003
+ reconcileIosSimulator
1004
+ });
1005
+ return ensureOwnedAndroidDevice({
1006
+ record,
1007
+ projectPath,
1008
+ slot,
1009
+ settingsRoot,
1010
+ label,
1011
+ settings,
1012
+ flags,
1013
+ note,
1014
+ out,
1015
+ logFile,
1016
+ alive,
1017
+ configureAvd,
1018
+ teardownAvd
1019
+ });
1140
1020
  }
1141
- function sleep(ms) {
1142
- return new Promise((r) => setTimeout(r, ms));
1021
+ const BOOT_POLL_MS = 500;
1022
+ async function ensureBooted({ platform, device, simulatorApp, timeoutMs, pollMs = BOOT_POLL_MS, out = () => {}, logFile = null, alive = pidExists, projectPath, slot } = {}) {
1023
+ if (platform === "ios") return ensureIosBooted({
1024
+ device,
1025
+ simulatorApp,
1026
+ timeoutMs: timeoutMs ?? 6e5,
1027
+ pollMs,
1028
+ out
1029
+ });
1030
+ if (platform === "android") return ensureAndroidBooted({
1031
+ device,
1032
+ projectPath,
1033
+ slot,
1034
+ timeoutMs: timeoutMs ?? ANDROID_BOOT_TIMEOUT_MS,
1035
+ out,
1036
+ logFile,
1037
+ alive
1038
+ });
1039
+ return {
1040
+ failed: true,
1041
+ reason: `Unknown platform "${platform}".`
1042
+ };
1043
+ }
1044
+ //#endregion
1045
+ //#region src/engine/prebuild.ts
1046
+ const PREBUILD_ERROR = "STIM_PREBUILD_FAILED";
1047
+ const LAST_LINES$1 = 20;
1048
+ function nativeDirName(platform) {
1049
+ return platform === "android" ? "android" : "ios";
1050
+ }
1051
+ function nativeDir(root, platform) {
1052
+ return join(root, nativeDirName(platform));
1053
+ }
1054
+ function nativeDirInFingerprint(sources, platform) {
1055
+ const dir = nativeDirName(platform);
1056
+ return sources.some((source) => source.type === "dir" && source.filePath === dir && source.hash != null);
1057
+ }
1058
+ /**
1059
+ * @expo/fingerprint leaves a native directory out of the hash when git ignores it (CNG), so the cache
1060
+ * key only covers that directory when this workspace's recorded prebuild ran on the same fingerprint.
1061
+ */
1062
+ function prebuildAction({ isExpo, nativeDirExists, nativeDirFingerprinted, nativeDirTracked, generatedFrom, fingerprint }) {
1063
+ if (!isExpo) return "none";
1064
+ if (!nativeDirExists) return "generate";
1065
+ if (nativeDirFingerprinted || generatedFrom === fingerprint) return "none";
1066
+ return nativeDirTracked ? "refuse" : "regenerate";
1067
+ }
1068
+ function prebuildRecord(state) {
1069
+ const record = state?.prebuild;
1070
+ return record && typeof record === "object" ? record : {};
1071
+ }
1072
+ function gitTracksNativeDir(root, platform) {
1073
+ try {
1074
+ return getExecutor().runFile("git", [
1075
+ "-C",
1076
+ root,
1077
+ "ls-files",
1078
+ "--",
1079
+ nativeDirName(platform)
1080
+ ], { env: {
1081
+ LC_ALL: "C",
1082
+ LANGUAGE: "C"
1083
+ } }).trim() !== "";
1084
+ } catch (error) {
1085
+ if (error?.code === "ENOENT") return false;
1086
+ return !/not a git repository \(or any (of the parent directories|parent up to mount point)/.test(String(error?.message));
1087
+ }
1088
+ }
1089
+ function planPrebuild(root, platform, { isExpo, fingerprint, sources }) {
1090
+ const nativeDirExists = existsSync(nativeDir(root, platform));
1091
+ const fingerprinted = nativeDirInFingerprint(sources, platform);
1092
+ const generatedFrom = prebuildRecord(readWorkspaceState$1(root))[platform];
1093
+ return prebuildAction({
1094
+ isExpo,
1095
+ nativeDirExists,
1096
+ nativeDirFingerprinted: fingerprinted,
1097
+ nativeDirTracked: nativeDirExists && !fingerprinted && gitTracksNativeDir(root, platform),
1098
+ generatedFrom: typeof generatedFrom === "string" ? generatedFrom : null,
1099
+ fingerprint
1100
+ });
1101
+ }
1102
+ function recordPrebuild(root, platform, fingerprint) {
1103
+ updateWorkspaceState(root, (state) => ({
1104
+ ...state,
1105
+ prebuild: {
1106
+ ...prebuildRecord(state),
1107
+ [platform]: fingerprint
1108
+ }
1109
+ }));
1110
+ }
1111
+ function staleNativeDirRefusal(platform) {
1112
+ const dir = nativeDirName(platform);
1113
+ return {
1114
+ code: PREBUILD_ERROR,
1115
+ message: `The fingerprint leaves ${dir}/ out of the cache key, and Stim cannot show that ${dir}/ was generated from the current app config. Git tracks files in it, or could not say whether it does, so Stim will not regenerate it with \`expo prebuild --clean\`.`,
1116
+ remedy: `Stop excluding ${dir}/ from the fingerprint (check .fingerprintignore) so the cache key covers its files, or add ${dir}/ to .gitignore and untrack it so Stim regenerates it from the app config.`
1117
+ };
1118
+ }
1119
+ function prebuildRefusal({ isExpo, platform, nativeDirExists }) {
1120
+ if (nativeDirExists || isExpo) return null;
1121
+ const dir = nativeDirName(platform);
1122
+ return {
1123
+ code: PREBUILD_ERROR,
1124
+ message: `This project has no ${dir}/ directory and is not an Expo (CNG) project, so Stim cannot generate one.`,
1125
+ remedy: `Check out or generate the native project (\`npx @react-native-community/cli init\` produced one originally), or add \`expo\` to the project so \`expo prebuild\` can create ${dir}/.`
1126
+ };
1127
+ }
1128
+ function prebuildFailure(logWriter, result) {
1129
+ logWriter?.write?.({
1130
+ src: "build",
1131
+ level: "error",
1132
+ msg: result.reason || "Expo prebuild failed.",
1133
+ event: "prebuild_failed"
1134
+ });
1135
+ return result;
1136
+ }
1137
+ async function runPrebuild(root, platform, logWriter, { spawnFn = null, now = Date.now, isExpo = null, clean = false } = {}) {
1138
+ const dirExists = existsSync(nativeDir(root, platform));
1139
+ const refusal = prebuildRefusal({
1140
+ isExpo: isExpo === null ? detectIsExpo(root) : isExpo,
1141
+ platform,
1142
+ nativeDirExists: dirExists
1143
+ });
1144
+ if (refusal) return prebuildFailure(logWriter, {
1145
+ failed: true,
1146
+ code: refusal.code,
1147
+ reason: refusal.message,
1148
+ remedy: refusal.remedy,
1149
+ error: refusal,
1150
+ lastLines: []
1151
+ });
1152
+ const bin = expoBinPath(root);
1153
+ if (!bin) {
1154
+ const binRefusal = expoBinRefusal(root, "prebuild");
1155
+ return prebuildFailure(logWriter, {
1156
+ failed: true,
1157
+ code: PREBUILD_ERROR,
1158
+ reason: binRefusal.message,
1159
+ remedy: binRefusal.remedy,
1160
+ lastLines: []
1161
+ });
1162
+ }
1163
+ const spawn = spawnFn || ((cmd, args, opts) => getExecutor().spawn(cmd, args, opts));
1164
+ const startedAt = now();
1165
+ const tail = [];
1166
+ const push = (line) => {
1167
+ const msg = stripAnsi(String(line)).trimEnd();
1168
+ if (!msg.trim()) return;
1169
+ tail.push(msg);
1170
+ if (tail.length > LAST_LINES$1) tail.shift();
1171
+ logWriter?.write?.({
1172
+ src: "build",
1173
+ level: "debug",
1174
+ msg,
1175
+ raw: true,
1176
+ event: "prebuild"
1177
+ });
1178
+ };
1179
+ let child;
1180
+ try {
1181
+ child = spawnDeclared(() => spawn(bin, [
1182
+ "prebuild",
1183
+ "-p",
1184
+ platform,
1185
+ "--no-install",
1186
+ ...clean ? ["--clean"] : []
1187
+ ], {
1188
+ cwd: root,
1189
+ stdio: [
1190
+ "ignore",
1191
+ "pipe",
1192
+ "pipe"
1193
+ ],
1194
+ env: {
1195
+ ...process.env,
1196
+ FORCE_COLOR: "0",
1197
+ CI: "1"
1198
+ }
1199
+ }));
1200
+ } catch (err) {
1201
+ return prebuildFailure(logWriter, {
1202
+ failed: true,
1203
+ code: PREBUILD_ERROR,
1204
+ reason: `Could not run \`expo prebuild\`: ${err?.message || err}`,
1205
+ lastLines: []
1206
+ });
1207
+ }
1208
+ const outReader = createLineReader(push);
1209
+ const errReader = createLineReader(push);
1210
+ child.stdout?.setEncoding?.("utf-8");
1211
+ child.stderr?.setEncoding?.("utf-8");
1212
+ child.stdout?.on("data", (chunk) => outReader.push(chunk));
1213
+ child.stderr?.on("data", (chunk) => errReader.push(chunk));
1214
+ const result = await waitForChild(child);
1215
+ outReader.flush();
1216
+ errReader.flush();
1217
+ const durationMs = now() - startedAt;
1218
+ if (result.error) return prebuildFailure(logWriter, {
1219
+ failed: true,
1220
+ code: PREBUILD_ERROR,
1221
+ reason: `Could not run \`expo prebuild\`: ${result.error?.message || result.error}`,
1222
+ lastLines: tail.slice(),
1223
+ durationMs
1224
+ });
1225
+ if (result.code !== 0) {
1226
+ const how = result.signal ? `signal ${result.signal}` : `exit code ${result.code}`;
1227
+ return prebuildFailure(logWriter, {
1228
+ failed: true,
1229
+ code: PREBUILD_ERROR,
1230
+ reason: `\`expo prebuild -p ${platform}\` failed (${how}).`,
1231
+ lastLines: tail.slice(),
1232
+ durationMs
1233
+ });
1234
+ }
1235
+ if (!existsSync(nativeDir(root, platform))) return prebuildFailure(logWriter, {
1236
+ failed: true,
1237
+ code: PREBUILD_ERROR,
1238
+ reason: `\`expo prebuild -p ${platform}\` succeeded but did not create ${nativeDirName(platform)}/.`,
1239
+ lastLines: tail.slice(),
1240
+ durationMs
1241
+ });
1242
+ return {
1243
+ ok: true,
1244
+ durationMs,
1245
+ nativeDir: nativeDir(root, platform)
1246
+ };
1247
+ }
1248
+ //#endregion
1249
+ //#region src/engine/js-swap.ts
1250
+ const JS_BUNDLE_NAME = "main.jsbundle";
1251
+ const LAST_LINES = 5;
1252
+ function hermesEnabledFromProperties(text) {
1253
+ if (typeof text !== "string") return true;
1254
+ let data;
1255
+ try {
1256
+ data = JSON.parse(text);
1257
+ } catch {
1258
+ return true;
1259
+ }
1260
+ if (!data || typeof data !== "object") return true;
1261
+ const raw = data.hermesEnabled;
1262
+ return raw !== "false" && raw !== false;
1263
+ }
1264
+ function readHermesEnabled(root) {
1265
+ try {
1266
+ return hermesEnabledFromProperties(readFileSync(join(root, "ios", "Podfile.properties.json"), "utf-8"));
1267
+ } catch {
1268
+ return true;
1269
+ }
1270
+ }
1271
+ const ENTRY_CANDIDATES = [
1272
+ "index.js",
1273
+ "index.ts",
1274
+ "index.tsx",
1275
+ "index.jsx"
1276
+ ];
1277
+ function pickEntryFile(entries) {
1278
+ const names = new Set((Array.isArray(entries) ? entries : []).filter((e) => typeof e === "string"));
1279
+ return ENTRY_CANDIDATES.find((c) => names.has(c)) ?? "index.js";
1280
+ }
1281
+ function detectEntryFile(root) {
1282
+ let entries;
1283
+ try {
1284
+ entries = readdirSync(root);
1285
+ } catch {
1286
+ return "index.js";
1287
+ }
1288
+ return pickEntryFile(entries);
1289
+ }
1290
+ function bundleCommand({ isExpo, entryFile, bundleOutput, assetsDest }) {
1291
+ if (isExpo) return {
1292
+ file: "npx",
1293
+ args: [
1294
+ "expo",
1295
+ "export:embed",
1296
+ "--platform",
1297
+ "ios",
1298
+ "--dev",
1299
+ "false",
1300
+ "--bundle-output",
1301
+ bundleOutput,
1302
+ "--assets-dest",
1303
+ assetsDest
1304
+ ]
1305
+ };
1306
+ return {
1307
+ file: "npx",
1308
+ args: [
1309
+ "react-native",
1310
+ "bundle",
1311
+ "--platform",
1312
+ "ios",
1313
+ "--dev",
1314
+ "false",
1315
+ "--entry-file",
1316
+ entryFile,
1317
+ "--bundle-output",
1318
+ bundleOutput,
1319
+ "--assets-dest",
1320
+ assetsDest
1321
+ ]
1322
+ };
1323
+ }
1324
+ function hermescPath(root, { exists = existsSync } = {}) {
1325
+ const candidates = [
1326
+ join(root, "node_modules", "hermes-compiler", "hermesc", "osx-bin", "hermesc"),
1327
+ join(root, "ios", "Pods", "hermes-engine", "destroot", "bin", "hermesc"),
1328
+ join(root, "node_modules", "react-native", "sdks", "hermesc", "osx-bin", "hermesc")
1329
+ ];
1330
+ return candidates.find(exists) ?? candidates[candidates.length - 1];
1331
+ }
1332
+ function hermescArgs({ bundle, out }) {
1333
+ return [
1334
+ "-emit-binary",
1335
+ "-out",
1336
+ out,
1337
+ bundle
1338
+ ];
1339
+ }
1340
+ async function swapJsBundle({ root, isExpo, cachedAppPath, logWriter = null, exec = null, spawnFn = null, mkdtemp = () => makeTemporaryDirectory(cachedAppPath, "stim-js-swap-"), exists = existsSync, hermesEnabled = null, now = Date.now, heartbeatMs = HEARTBEAT_INTERVAL_MS, onHeartbeat = (line) => console.error(line) }) {
1341
+ const e = exec || getExecutor();
1342
+ const startedAt = now();
1343
+ const elapsed = () => now() - startedAt;
1344
+ let tmp;
1345
+ const fail = (step, reason, lastLines = []) => {
1346
+ logWriter?.write?.({
1347
+ src: "build",
1348
+ level: "error",
1349
+ msg: `JS swap failed at ${step}: ${reason}`,
1350
+ event: "js_swap"
1351
+ });
1352
+ if (tmp) removeTemporaryEntry(tmp);
1353
+ return {
1354
+ failed: true,
1355
+ step,
1356
+ reason,
1357
+ lastLines,
1358
+ durationMs: elapsed()
1359
+ };
1360
+ };
1361
+ let appCopy;
1362
+ try {
1363
+ tmp = mkdtemp();
1364
+ appCopy = join(tmp, basename(cachedAppPath));
1365
+ try {
1366
+ e.runFile("cp", [
1367
+ "-c",
1368
+ "-R",
1369
+ cachedAppPath,
1370
+ appCopy
1371
+ ]);
1372
+ } catch {
1373
+ removeTemporaryEntry(appCopy);
1374
+ e.runFile("cp", [
1375
+ "-R",
1376
+ cachedAppPath,
1377
+ appCopy
1378
+ ]);
1379
+ }
1380
+ } catch (err) {
1381
+ return fail("copy", `could not copy ${cachedAppPath} aside: ${describe(err)}`);
1382
+ }
1383
+ const bundleOutput = join(tmp, JS_BUNDLE_NAME);
1384
+ const assetsDest = join(tmp, "assets");
1385
+ const command = bundleCommand({
1386
+ isExpo,
1387
+ entryFile: isExpo ? "index.js" : detectEntryFile(root),
1388
+ bundleOutput,
1389
+ assetsDest
1390
+ });
1391
+ try {
1392
+ mkdirSync(assetsDest, { recursive: true });
1393
+ } catch (err) {
1394
+ return fail("bundle", `could not create ${assetsDest}: ${describe(err)}`);
1395
+ }
1396
+ logWriter?.write?.({
1397
+ src: "build",
1398
+ level: "info",
1399
+ msg: `${command.file} ${command.args.join(" ")}`,
1400
+ event: "js_swap"
1401
+ });
1402
+ const spawn = spawnFn || ((cmd, args, opts) => e.spawn(cmd, args, opts));
1403
+ const transcript = [];
1404
+ const push = (line) => {
1405
+ const msg = cleanLine(line);
1406
+ if (msg.trim() === "") return;
1407
+ transcript.push(msg);
1408
+ logWriter?.write?.({
1409
+ src: "build",
1410
+ level: "debug",
1411
+ msg,
1412
+ raw: true,
1413
+ event: "js_swap"
1414
+ });
1415
+ };
1416
+ let child;
1417
+ try {
1418
+ child = spawn(command.file, command.args, {
1419
+ cwd: root,
1420
+ stdio: [
1421
+ "ignore",
1422
+ "pipe",
1423
+ "pipe"
1424
+ ],
1425
+ env: {
1426
+ ...process.env,
1427
+ FORCE_COLOR: "0"
1428
+ }
1429
+ });
1430
+ } catch (err) {
1431
+ return fail("bundle", `could not run ${command.file} ${command.args[0]}: ${describe(err)}`);
1432
+ }
1433
+ const reader = {
1434
+ out: createLineReader(push),
1435
+ err: createLineReader(push)
1436
+ };
1437
+ child.stdout?.setEncoding?.("utf-8");
1438
+ child.stderr?.setEncoding?.("utf-8");
1439
+ child.stdout?.on("data", (chunk) => reader.out.push(chunk));
1440
+ child.stderr?.on("data", (chunk) => reader.err.push(chunk));
1441
+ const stopHeartbeat = startBuildHeartbeat({
1442
+ intervalMs: heartbeatMs,
1443
+ elapsed,
1444
+ emit: onHeartbeat,
1445
+ label: "swap"
1446
+ });
1447
+ let outcome;
1448
+ try {
1449
+ outcome = await waitForChild(child);
1450
+ } finally {
1451
+ stopHeartbeat();
1452
+ }
1453
+ reader.out.flush();
1454
+ reader.err.flush();
1455
+ if (outcome.error) return fail("bundle", `could not run ${command.file} ${command.args[0]}: ${describe(outcome.error)}`);
1456
+ if (outcome.code !== 0) {
1457
+ const how = outcome.signal ? `signal ${outcome.signal}` : `exit code ${outcome.code}`;
1458
+ return fail("bundle", `\`${command.args.slice(0, 2).join(" ")}\` failed (${how})`, tailLines(transcript, LAST_LINES));
1459
+ }
1460
+ if (!exists(bundleOutput)) return fail("bundle", `the bundle command exited 0 but wrote no ${JS_BUNDLE_NAME} at ${bundleOutput}`);
1461
+ let hermes = false;
1462
+ let note;
1463
+ if (hermesEnabled ?? readHermesEnabled(root)) {
1464
+ const hermesc = hermescPath(root);
1465
+ if (!exists(hermesc)) note = `hermesc not found at ${hermesc}; embedding the plain JS bundle instead of Hermes bytecode`;
1466
+ else {
1467
+ const hbc = join(tmp, `${JS_BUNDLE_NAME}.hbc`);
1468
+ try {
1469
+ e.runFile(hermesc, hermescArgs({
1470
+ bundle: bundleOutput,
1471
+ out: hbc
1472
+ }));
1473
+ e.runFile("mv", [hbc, bundleOutput]);
1474
+ hermes = true;
1475
+ } catch (err) {
1476
+ return fail("hermesc", `hermesc failed on ${bundleOutput}: ${describe(err)}`);
1477
+ }
1478
+ }
1479
+ }
1480
+ try {
1481
+ e.runFile("cp", [bundleOutput, join(appCopy, JS_BUNDLE_NAME)]);
1482
+ e.runFile("cp", [
1483
+ "-R",
1484
+ `${assetsDest}/.`,
1485
+ `${appCopy}/`
1486
+ ]);
1487
+ } catch (err) {
1488
+ return fail("replace", `could not replace the JS bundle inside ${appCopy}: ${describe(err)}`);
1489
+ }
1490
+ try {
1491
+ e.runFile("codesign", [
1492
+ "--force",
1493
+ "--sign",
1494
+ "-",
1495
+ appCopy
1496
+ ]);
1497
+ } catch (err) {
1498
+ return fail("codesign", `codesign --force --sign - ${appCopy} failed: ${describe(err)}`);
1499
+ }
1500
+ logWriter?.write?.({
1501
+ src: "build",
1502
+ level: "info",
1503
+ msg: `JS swap done: ${hermes ? "hermes bytecode" : "plain JS"} into ${appCopy} in ${elapsed()}ms`,
1504
+ event: "js_swap"
1505
+ });
1506
+ const result = {
1507
+ ok: true,
1508
+ appPath: appCopy,
1509
+ tmpDir: tmp,
1510
+ hermes,
1511
+ durationMs: elapsed()
1512
+ };
1513
+ if (note) result.note = note;
1514
+ return result;
1515
+ }
1516
+ function describe(err) {
1517
+ return String(err?.message || err);
1518
+ }
1519
+ //#endregion
1520
+ //#region src/commands/dev-client.ts
1521
+ function devClientScheme(root, appPath = null, { exec = null } = {}) {
1522
+ if (!hasDevClient(root)) return void 0;
1523
+ const fromBundle = pickDevClientScheme(readBundleSchemes(appPath, { exec }));
1524
+ if (fromBundle) return fromBundle;
1525
+ const app = readJsonObject(join(root, "app.json"));
1526
+ const expo = app?.expo;
1527
+ const raw = (isJsonObject(expo) ? expo.scheme : void 0) ?? app?.scheme ?? null;
1528
+ const scheme = Array.isArray(raw) ? raw.find((s) => typeof s === "string" && s.trim() !== "") : raw;
1529
+ if (typeof scheme !== "string" || scheme.trim() === "") return void 0;
1530
+ return scheme.trim();
1531
+ }
1532
+ function schemesFromInfoPlist(plist) {
1533
+ const types = plist?.CFBundleURLTypes;
1534
+ if (!Array.isArray(types)) return [];
1535
+ const out = [];
1536
+ for (const type of types) {
1537
+ const schemes = type?.CFBundleURLSchemes;
1538
+ if (Array.isArray(schemes)) out.push(...schemes.filter((s) => typeof s === "string" && s.trim() !== ""));
1539
+ }
1540
+ return out;
1541
+ }
1542
+ function readBundleSchemes(appPath, { exec = null } = {}) {
1543
+ if (typeof appPath !== "string" || appPath.trim() === "") return [];
1544
+ const e = exec || getExecutor();
1545
+ let out;
1546
+ try {
1547
+ out = e.runFile("plutil", [
1548
+ "-convert",
1549
+ "json",
1550
+ "-o",
1551
+ "-",
1552
+ join(appPath, "Info.plist")
1553
+ ]);
1554
+ } catch {
1555
+ return [];
1556
+ }
1557
+ try {
1558
+ return schemesFromInfoPlist(JSON.parse(String(out)));
1559
+ } catch {
1560
+ return [];
1561
+ }
1562
+ }
1563
+ const THIRD_PARTY_SCHEME = /^(?:fb\d+|com\.googleusercontent\.apps\.|msauth\.|msauthv2|twitterkit-|db-[a-z0-9]+$|spotify|snapchat|com\.facebook)/i;
1564
+ function pickDevClientScheme(schemes) {
1565
+ const all = (Array.isArray(schemes) ? schemes : []).filter((s) => typeof s === "string" && s.trim() !== "").map((s) => s.trim()).filter((s) => !/^(?:https?|mailto|tel|sms|itms(?:-apps)?)$/i.test(s));
1566
+ const expo = all.filter((s) => s.startsWith("exp+"));
1567
+ return (expo.length ? expo : all.filter((s) => !THIRD_PARTY_SCHEME.test(s))).toSorted((a, b) => b.length - a.length)[0] ?? null;
1568
+ }
1569
+ function hasDevClient(root) {
1570
+ const pkg = readJsonObject(join(root, "package.json"));
1571
+ if ([pkg?.dependencies, pkg?.devDependencies].some((deps) => isJsonObject(deps) && "expo-dev-client" in deps)) return true;
1572
+ return isPackageResolvable(root, "expo-dev-client");
1143
1573
  }
1144
1574
  //#endregion
1145
- export { liveOwnedDeviceCount as a, unknownIosRuntimeRefusal as c, simslimIsOnPath as d, ensureOwnedDevice as i, AvdBootError as l, clearIosAdoptionPending as n, unknownAndroidSystemImageRefusal as o, ensureBooted as r, unknownIosDeviceTypeRefusal as s, checkDeviceCapacity as t, AvdRecoveryError as u };
1575
+ export { isPhysicalDeviceRequest as _, swapJsBundle as a, warmMetro as b, runPrebuild as c, ensureOwnedDevice as d, AvdBootError as f, ensureWorkspaceStorageSafely as g, ensureDevServer as h, detectEntryFile as i, staleNativeDirRefusal as l, clearIosAdoptionPending as m, pickDevClientScheme as n, planPrebuild as o, AvdRecoveryError as p, schemesFromInfoPlist as r, recordPrebuild as s, devClientScheme as t, ensureBooted as u, launchOutcomeRecord as v, sleep$1 as y };