stim 1.12.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +3 -1
  2. package/dist/{activity-CCZJs__P.mjs → activity-DfVKZTEB.mjs} +42 -25
  3. package/dist/{android-BQiKJYK0.mjs → android-BHfAu2mf.mjs} +1 -1
  4. package/dist/{android-B7X-8H7h.mjs → android-CD8pr8Q2.mjs} +62 -47
  5. package/dist/{android-cas-DzLPXiEn.mjs → android-cas-B9Kv9aIy.mjs} +9 -7
  6. package/dist/{android-75Sk2rIR.mjs → android-qx_31wJo.mjs} +395 -116
  7. package/dist/{app-install-CsZi0SAT.mjs → app-install-CDFY2nUL.mjs} +1 -1
  8. package/dist/{budget-DbzW5VTD.mjs → budget-E5o91NR1.mjs} +154 -37
  9. package/dist/{build-plan-CqntbR7b.mjs → build-plan-BTa93o_r.mjs} +21 -88
  10. package/dist/{build-progress-h60GPV27.mjs → build-progress-l7d-X-ZP.mjs} +75 -7
  11. package/dist/cdp-Dq_ypAlJ.mjs +115 -0
  12. package/dist/chrome-B5hWgQJS.mjs +61 -0
  13. package/dist/cli.mjs +16 -15
  14. package/dist/collector-run.mjs +14 -8
  15. package/dist/{command-output-BWCj5Vh-.mjs → command-output-BIL7rlX7.mjs} +1 -1
  16. package/dist/created-devices-Cd558h31.mjs +37 -0
  17. package/dist/{deps-CO-_7UOm.mjs → deps-2Wo81np9.mjs} +1 -1
  18. package/dist/{device-9r9xRYDa.mjs → device-Cf7vl-so.mjs} +6 -6
  19. package/dist/{device-lease-B9LAXIUH.mjs → device-lease-CL9dZf73.mjs} +52 -3
  20. package/dist/{device-pool-BoezJd-c.mjs → device-pool-MuoiyRcu.mjs} +3 -3
  21. package/dist/{device-remote-C0_o-tJH.mjs → device-remote-DT9ngZiD.mjs} +10 -10
  22. package/dist/{doctor-LfDlbR-N.mjs → doctor-D2iysNNH.mjs} +47 -18
  23. package/dist/{error-diagnostics-CCxjn31V.mjs → error-diagnostics-BoiOhck8.mjs} +6 -6
  24. package/dist/{gc-C4nQFcTX.mjs → gc-eA-PRBX8.mjs} +853 -126
  25. package/dist/{guide-BPBlh9ML.mjs → guide-BfZ0OmiP.mjs} +736 -110
  26. package/dist/{guide-status-i9ogHt4v.mjs → guide-status-BaMYu55M.mjs} +1 -1
  27. package/dist/{idle-S3tFZ53_.mjs → idle-DxLi5OkN.mjs} +42 -9
  28. package/dist/{in-use-D9XYEgFD.mjs → in-use-wYEaKQM9.mjs} +9 -5
  29. package/dist/{ios-CDzWL1mj.mjs → ios-CcfCeZ19.mjs} +98 -54
  30. package/dist/{ios-CZWOHTTc.mjs → ios-CmEzZiOj.mjs} +10 -5
  31. package/dist/{ios-device-D9cLxVr5.mjs → ios-device-BW-t_Lkv.mjs} +1 -1
  32. package/dist/{launch-verify-B2oR_kFN.mjs → launch-verify-CuJaBpEa.mjs} +33 -20
  33. package/dist/{logs-SzuYudOj.mjs → logs-DcSRbC2t.mjs} +11 -10
  34. package/dist/{metro-Dzo30hvJ.mjs → metro-BPeY1YMh.mjs} +4 -4
  35. package/dist/{named-ports-Dn1o61D9.mjs → named-ports-n7tnJsI8.mjs} +78 -6
  36. package/dist/native-runtime-CiiFHt0e.mjs +74 -0
  37. package/dist/{ownership-Dqnrjrio.mjs → ownership-Bz2kETAS.mjs} +1 -1
  38. package/dist/{ownership-B8d4kXBx.mjs → ownership-PUP4Dwai.mjs} +134 -209
  39. package/dist/page-BTu7I60K.mjs +29 -0
  40. package/dist/{ports-Dp_QTwTC.mjs → ports-C5UutqpJ.mjs} +4 -4
  41. package/dist/{project-B8rbutVC.mjs → project-DGeclhvO.mjs} +53 -17
  42. package/dist/{reload-nocmx8KB.mjs → reload-C-cV5yy_.mjs} +79 -20
  43. package/dist/{remote-cache-BRsRZru5.mjs → remote-cache--aehr4Pr.mjs} +3 -3
  44. package/dist/{server-bare-Gn3ssSdK.mjs → server-bare-Bq8BpSIo.mjs} +1 -1
  45. package/dist/server-expo-BeYwP_Xy.mjs +2 -0
  46. package/dist/{server-expo-CeT1nIIk.mjs → server-expo-DUCg52-K.mjs} +2 -2
  47. package/dist/{settings-bE-Gu3Ti.mjs → settings-ssVBToDR.mjs} +10 -10
  48. package/dist/{settings-BlyDn2Oz.mjs → settings-xB0Frkg6.mjs} +11 -2
  49. package/dist/settings.schema.json +53 -1
  50. package/dist/{simslim-COtEEP7x.mjs → simslim-Bbg9UWrt.mjs} +12 -6
  51. package/dist/{slot-launch-DIACGEfH.mjs → slot-launch-BIWEW-wI.mjs} +3 -3
  52. package/dist/{start-BboE1HIS.mjs → start-BZhItkiL.mjs} +11 -10
  53. package/dist/{start-BFstVBlx.mjs → start-Rw2GD3nV.mjs} +1 -1
  54. package/dist/{state-DcDgzfu3.mjs → state-DwAU2_-D.mjs} +1 -1
  55. package/dist/state-l1pYwOre.mjs +148 -0
  56. package/dist/{stats-DiCtmluY.mjs → stats-CaSwmsSZ.mjs} +2 -2
  57. package/dist/{status-DI9Tjo7T.mjs → status-TJlL0HWo.mjs} +624 -103
  58. package/dist/{status-BUASYyy_.mjs → status-sszP4XKU.mjs} +59 -2
  59. package/dist/{stim-desktop-D0xhpZt8.mjs → stim-desktop-CXt8-Z3J.mjs} +13 -2
  60. package/dist/{stop-Bn6v-GvS.mjs → stop-BZ2qMiBP.mjs} +37 -12
  61. package/dist/{stop-BI7QY76M.mjs → stop-DskDKNTU.mjs} +2 -2
  62. package/dist/supervisor-run.mjs +10 -10
  63. package/dist/web-run.d.mts +22 -0
  64. package/dist/web-run.mjs +481 -0
  65. package/dist/web-sWXd5iek.mjs +394 -0
  66. package/dist/{worktree-B5sTRskL.mjs → worktree-3cBa-bQt.mjs} +1 -1
  67. package/dist/{worktree-ChDL5fO1.mjs → worktree-D4Do_XiQ.mjs} +226 -19
  68. package/dist/{xcode-BzplpQzc.mjs → xcode-LM6A8cNZ.mjs} +5 -5
  69. package/package.json +5 -5
  70. package/dist/server-expo-eWi9Coof.mjs +0 -2
  71. package/dist/workspace-process-lock-BvvJTfo_.mjs +0 -57
@@ -0,0 +1,115 @@
1
+ import { WebSocket } from "ws";
2
+ //#region src/web/cdp.ts
3
+ const COMMAND_TIMEOUT_MS = 15e3;
4
+ function connectCdp(url, { timeoutMs = 5e3 } = {}) {
5
+ return new Promise((resolve, reject) => {
6
+ const socket = new WebSocket(url, {
7
+ perMessageDeflate: false,
8
+ maxPayload: 268435456
9
+ });
10
+ const pending = /* @__PURE__ */ new Map();
11
+ const eventListeners = [];
12
+ const closeListeners = [];
13
+ let nextId = 1;
14
+ let closed = false;
15
+ const timer = setTimeout(() => {
16
+ socket.terminate();
17
+ reject(/* @__PURE__ */ new Error(`DevTools did not accept a connection at ${url} within ${timeoutMs}ms`));
18
+ }, timeoutMs);
19
+ socket.on("message", (data) => {
20
+ let message;
21
+ try {
22
+ message = JSON.parse(String(data));
23
+ } catch {
24
+ return;
25
+ }
26
+ if (typeof message.id === "number") {
27
+ const waiter = pending.get(message.id);
28
+ if (!waiter) return;
29
+ pending.delete(message.id);
30
+ if (message.error) waiter.reject(new Error(message.error.message ?? "DevTools command failed"));
31
+ else waiter.resolve(message.result ?? {});
32
+ return;
33
+ }
34
+ if (typeof message.method === "string") {
35
+ const event = {
36
+ method: message.method,
37
+ params: message.params ?? {},
38
+ ...message.sessionId ? { sessionId: message.sessionId } : {}
39
+ };
40
+ for (const listener of eventListeners) listener(event);
41
+ }
42
+ });
43
+ socket.on("close", () => {
44
+ closed = true;
45
+ for (const waiter of pending.values()) waiter.reject(/* @__PURE__ */ new Error("DevTools connection closed"));
46
+ pending.clear();
47
+ for (const listener of closeListeners) listener();
48
+ });
49
+ socket.on("error", (error) => {
50
+ clearTimeout(timer);
51
+ reject(error);
52
+ });
53
+ socket.on("open", () => {
54
+ clearTimeout(timer);
55
+ resolve({
56
+ send(method, params = {}, sessionId) {
57
+ if (closed) return Promise.reject(/* @__PURE__ */ new Error("DevTools connection closed"));
58
+ const id = nextId++;
59
+ return new Promise((resolveCommand, rejectCommand) => {
60
+ const commandTimer = setTimeout(() => {
61
+ pending.delete(id);
62
+ rejectCommand(/* @__PURE__ */ new Error(`DevTools command ${method} timed out`));
63
+ }, COMMAND_TIMEOUT_MS);
64
+ pending.set(id, {
65
+ resolve: (value) => {
66
+ clearTimeout(commandTimer);
67
+ resolveCommand(value);
68
+ },
69
+ reject: (error) => {
70
+ clearTimeout(commandTimer);
71
+ rejectCommand(error);
72
+ }
73
+ });
74
+ socket.send(JSON.stringify({
75
+ id,
76
+ method,
77
+ params,
78
+ ...sessionId ? { sessionId } : {}
79
+ }));
80
+ });
81
+ },
82
+ onEvent(listener) {
83
+ eventListeners.push(listener);
84
+ },
85
+ onClose(listener) {
86
+ if (closed) listener();
87
+ else closeListeners.push(listener);
88
+ },
89
+ close() {
90
+ socket.close();
91
+ }
92
+ });
93
+ });
94
+ });
95
+ }
96
+ /**
97
+ * Connects to the browser DevTools endpoint on loopback `port` only when the process serving it is `chromePid`,
98
+ * the Chrome Stim started. Chrome reports its own browser process through `SystemInfo.getProcessInfo`.
99
+ */
100
+ async function connectOwnedBrowser(port, chromePid) {
101
+ const { webSocketDebuggerUrl } = await (await fetch(`http://127.0.0.1:${port}/json/version`, { signal: AbortSignal.timeout(2e3) })).json();
102
+ if (typeof webSocketDebuggerUrl !== "string" || !webSocketDebuggerUrl.startsWith(`ws://127.0.0.1:${port}/`)) throw new Error(`port ${port} does not serve a browser DevTools endpoint`);
103
+ const cdp = await connectCdp(webSocketDebuggerUrl);
104
+ try {
105
+ const { processInfo } = await cdp.send("SystemInfo.getProcessInfo");
106
+ const browser = processInfo?.find((entry) => entry.type === "browser")?.id;
107
+ if (browser !== chromePid) throw new Error(`port ${port} is served by browser pid ${browser ?? "unknown"}, not the owned Chrome ${chromePid}`);
108
+ return cdp;
109
+ } catch (error) {
110
+ cdp.close();
111
+ throw error;
112
+ }
113
+ }
114
+ //#endregion
115
+ export { connectOwnedBrowser as t };
@@ -0,0 +1,61 @@
1
+ import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ //#region src/web/chrome.ts
6
+ const CHROME_INSTALL_REMEDY = "Install Google Chrome from https://www.google.com/chrome/ (or Chromium), then run `stim doctor`.";
7
+ const MAC_APPS = [["Google Chrome.app", "Google Chrome"], ["Chromium.app", "Chromium"]];
8
+ const PATH_NAMES = [
9
+ "google-chrome",
10
+ "google-chrome-stable",
11
+ "chromium",
12
+ "chromium-browser"
13
+ ];
14
+ function chromeCandidates(host, { home = homedir(), env = process.env } = {}) {
15
+ if (host === "darwin") return ["/Applications", join(home, "Applications")].flatMap((dir) => MAC_APPS.map(([app, binary]) => join(dir, app, "Contents", "MacOS", binary)));
16
+ if (host === "win32") return [
17
+ env.PROGRAMFILES,
18
+ env["PROGRAMFILES(X86)"],
19
+ env.LOCALAPPDATA
20
+ ].filter((dir) => Boolean(dir)).map((dir) => join(dir, "Google", "Chrome", "Application", "chrome.exe"));
21
+ return [];
22
+ }
23
+ /** The installed Chrome or Chromium executable Stim launches, or null when none is installed. */
24
+ function findChrome({ host = process.platform, exists = existsSync, findExecutable = (name) => getExecutor().findExecutable(name) } = {}) {
25
+ const installed = chromeCandidates(host).find((path) => exists(path));
26
+ if (installed) return installed;
27
+ for (const name of PATH_NAMES) {
28
+ const found = findExecutable(name);
29
+ if (found) return found;
30
+ }
31
+ return null;
32
+ }
33
+ const DESKTOP_WINDOW = {
34
+ width: 1280,
35
+ height: 800
36
+ };
37
+ const PHONE_SCREEN = {
38
+ width: 390,
39
+ height: 844,
40
+ deviceScaleFactor: 3
41
+ };
42
+ function chromeArgs({ profile, port, headless, ignoreCertificateErrors, viewport }) {
43
+ const window = viewport === "phone" ? PHONE_SCREEN : DESKTOP_WINDOW;
44
+ return [
45
+ `--user-data-dir=${profile}`,
46
+ `--remote-debugging-port=${port}`,
47
+ ...headless ? ["--headless"] : [],
48
+ `--window-size=${window.width},${window.height}`,
49
+ "--no-first-run",
50
+ "--no-default-browser-check",
51
+ "--disable-search-engine-choice-screen",
52
+ "--disable-background-networking",
53
+ "--disable-sync",
54
+ "--password-store=basic",
55
+ "--use-mock-keychain",
56
+ ...ignoreCertificateErrors ? ["--ignore-certificate-errors"] : [],
57
+ "about:blank"
58
+ ];
59
+ }
60
+ //#endregion
61
+ export { findChrome as i, PHONE_SCREEN as n, chromeArgs as r, CHROME_INSTALL_REMEDY as t };
package/dist/cli.mjs CHANGED
@@ -4,21 +4,22 @@ import { readFileSync } from "node:fs";
4
4
  import { Command } from "commander";
5
5
  //#region bin/cli.ts
6
6
  const commands = /* @__PURE__ */ new Map([
7
- ["doctor", () => import("./doctor-LfDlbR-N.mjs")],
8
- ["worktree", () => import("./worktree-B5sTRskL.mjs")],
9
- ["start", () => import("./start-BFstVBlx.mjs")],
10
- ["ports", () => import("./ports-Dp_QTwTC.mjs")],
11
- ["stop", () => import("./stop-BI7QY76M.mjs")],
12
- ["ios", () => import("./ios-CDzWL1mj.mjs")],
13
- ["android", () => import("./android-B7X-8H7h.mjs")],
14
- ["reload", () => import("./reload-nocmx8KB.mjs")],
15
- ["device", () => import("./device-9r9xRYDa.mjs")],
16
- ["logs", () => import("./logs-SzuYudOj.mjs")],
17
- ["settings", () => import("./settings-bE-Gu3Ti.mjs")],
18
- ["status", () => import("./status-DI9Tjo7T.mjs")],
19
- ["stats", () => import("./stats-DiCtmluY.mjs")],
20
- ["gc", () => import("./gc-C4nQFcTX.mjs")],
21
- ["guide", () => import("./guide-BPBlh9ML.mjs")]
7
+ ["doctor", () => import("./doctor-D2iysNNH.mjs")],
8
+ ["worktree", () => import("./worktree-3cBa-bQt.mjs")],
9
+ ["start", () => import("./start-Rw2GD3nV.mjs")],
10
+ ["ports", () => import("./ports-C5UutqpJ.mjs")],
11
+ ["stop", () => import("./stop-DskDKNTU.mjs")],
12
+ ["ios", () => import("./ios-CcfCeZ19.mjs")],
13
+ ["android", () => import("./android-CD8pr8Q2.mjs")],
14
+ ["web", () => import("./web-sWXd5iek.mjs")],
15
+ ["reload", () => import("./reload-C-cV5yy_.mjs")],
16
+ ["device", () => import("./device-Cf7vl-so.mjs")],
17
+ ["logs", () => import("./logs-DcSRbC2t.mjs")],
18
+ ["settings", () => import("./settings-ssVBToDR.mjs")],
19
+ ["status", () => import("./status-TJlL0HWo.mjs")],
20
+ ["stats", () => import("./stats-CaSwmsSZ.mjs")],
21
+ ["gc", () => import("./gc-eA-PRBX8.mjs")],
22
+ ["guide", () => import("./guide-BfZ0OmiP.mjs")]
22
23
  ]);
23
24
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
24
25
  const program = new Command();
@@ -1,11 +1,11 @@
1
1
  import { G as validateDeviceSlot, O as process_identity_exports, z as deviceSlotKey } from "./config-0jCcMM0E.mjs";
2
2
  import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
3
3
  import { r as createNdjsonWriter } from "./ndjson-CDY9IkVC.mjs";
4
- import { j as workspaceLogsDir } from "./metro-Dzo30hvJ.mjs";
4
+ import { M as workspaceLogsDir } from "./metro-BPeY1YMh.mjs";
5
5
  import { t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
6
6
  import { a as startIosDeviceConsole, i as parseDeviceConsoleLine } from "./ios-device-BiOsSVzd.mjs";
7
- import { a as unregisterCollector, i as registerCollector, r as readCollectors, t as collectorProcessTitle } from "./ownership-Dqnrjrio.mjs";
8
- import { a as watchAppPid, i as waitForAppPid, n as parseLogcatLine, r as startAndroidLogcat, t as androidClockOffset } from "./android-BQiKJYK0.mjs";
7
+ import { a as unregisterCollector, i as registerCollector, r as readCollectors, t as collectorProcessTitle } from "./ownership-Bz2kETAS.mjs";
8
+ import { a as watchAppPid, i as waitForAppPid, n as parseLogcatLine, r as startAndroidLogcat, t as androidClockOffset } from "./android-BHfAu2mf.mjs";
9
9
  import { existsSync } from "node:fs";
10
10
  import { LOG_ROTATE_BYTES } from "@stim-cli/core";
11
11
  import { isAbsolute, join, resolve } from "node:path";
@@ -95,6 +95,12 @@ const NOISE_RULES = [
95
95
  exactSubsystem: "com.apple.UIKit",
96
96
  category: "UIFocus",
97
97
  messageEquals: "RCTScrollViewComponentView implements focusItemsInRect: - caching for linear focus movement is limited as long as this view is on screen."
98
+ },
99
+ {
100
+ id: "cfnetwork-performance-diagnostic",
101
+ exactSubsystem: "com.apple.runtime-issues",
102
+ category: "CFNetwork Performance Diagnostic",
103
+ level: "warn"
98
104
  }
99
105
  ];
100
106
  function ruleMatches(rule, { subsystem, category, message }) {
@@ -106,21 +112,21 @@ function ruleMatches(rule, { subsystem, category, message }) {
106
112
  if (rule.messageIncludes && !rule.messageIncludes.some((p) => message.includes(p))) return false;
107
113
  return true;
108
114
  }
109
- function noiseRuleId(event) {
115
+ function matchingNoiseRule(event) {
110
116
  const subsystem = typeof event?.subsystem === "string" ? event.subsystem : "";
111
117
  const category = typeof event?.category === "string" ? event.category : "";
112
118
  const message = typeof event?.eventMessage === "string" ? event.eventMessage : "";
113
- for (const rule of NOISE_RULES) if (ruleMatches(rule, {
119
+ return NOISE_RULES.find((rule) => ruleMatches(rule, {
114
120
  subsystem,
115
121
  category,
116
122
  message
117
- })) return rule.id;
118
- return null;
123
+ }));
119
124
  }
120
125
  function levelForEvent(event) {
121
126
  const level = levelFromMessageType(event?.messageType);
122
127
  if (level !== "error" && level !== "fatal") return level;
123
- return noiseRuleId(event) ? "info" : level;
128
+ const rule = matchingNoiseRule(event);
129
+ return rule ? rule.level ?? "info" : level;
124
130
  }
125
131
  function procFromImagePath(path) {
126
132
  if (typeof path !== "string" || !path) return null;
@@ -1,4 +1,4 @@
1
- import { t as NO_PROJECT_REFUSAL } from "./project-B8rbutVC.mjs";
1
+ import { t as NO_PROJECT_REFUSAL } from "./project-DGeclhvO.mjs";
2
2
  import { n as stripAnsi } from "./process-output-Fpl8PYi8.mjs";
3
3
  import { quotedPath } from "@stim-cli/core";
4
4
  import chalk from "chalk";
@@ -0,0 +1,37 @@
1
+ import { d as getConfigDir } from "./config-0jCcMM0E.mjs";
2
+ import { F as withDirLock } from "./metro-BPeY1YMh.mjs";
3
+ import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
+ import { createdDevicesFile, createdDevicesLock, readCreatedDevices, readCreatedDevices as readCreatedDevices$1 } from "@stim-cli/core/state";
5
+ //#region src/devices/created-devices.ts
6
+ function update(platform, change) {
7
+ const dir = getConfigDir();
8
+ withDirLock(createdDevicesLock(), () => {
9
+ const current = readCreatedDevices();
10
+ const entries = new Set(current[platform]);
11
+ if (!change(entries)) return;
12
+ const next = {
13
+ version: 1,
14
+ ios: [...current.ios],
15
+ android: [...current.android],
16
+ web: [...current.web],
17
+ [platform]: [...entries]
18
+ };
19
+ const file = createdDevicesFile();
20
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
21
+ writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`);
22
+ try {
23
+ renameSync(tmp, file);
24
+ } catch (error) {
25
+ rmSync(tmp, { force: true });
26
+ throw error;
27
+ }
28
+ }, { ensureParent: () => mkdirSync(dir, { recursive: true }) });
29
+ }
30
+ function recordCreatedDevice(platform, id) {
31
+ update(platform, (entries) => entries.size !== entries.add(id).size);
32
+ }
33
+ function forgetCreatedDevice(platform, id) {
34
+ update(platform, (entries) => entries.delete(id));
35
+ }
36
+ //#endregion
37
+ export { readCreatedDevices$1 as n, recordCreatedDevice as r, forgetCreatedDevice as t };
@@ -1,7 +1,7 @@
1
1
  import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
2
2
  import { a as spawnDeclared } from "./spawn-claims-PT8aQpCa.mjs";
3
3
  import { n as stripAnsi, r as waitForChild, t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
4
- import { d as startBuildHeartbeat, m as bundlerPin, n as HEARTBEAT_INTERVAL_MS } from "./xcode-BzplpQzc.mjs";
4
+ import { d as startBuildHeartbeat, m as bundlerPin, n as HEARTBEAT_INTERVAL_MS } from "./xcode-LM6A8cNZ.mjs";
5
5
  import { existsSync, readFileSync } from "node:fs";
6
6
  import { delimiter, dirname, join, relative, resolve, sep } from "node:path";
7
7
  import { homedir } from "node:os";
@@ -1,10 +1,10 @@
1
1
  import { H as parseDeviceSlotOption, U as projectDeviceSlots, p as getProject, z as deviceSlotKey } from "./config-0jCcMM0E.mjs";
2
- import { i as findProjectRoot, t as NO_PROJECT_REFUSAL } from "./project-B8rbutVC.mjs";
3
- import { r as clockTime } from "./command-output-BWCj5Vh-.mjs";
4
- import { n as fileLeaseIo, o as parseLeaseDuration, p as takeLease, u as releaseWorkspaceLeases } from "./device-lease-B9LAXIUH.mjs";
5
- import { C as getAvdNameForSerial, E as listAdbDevices, G as resolvePhysicalDevice, V as probeEmulatorSerial, j as memoizeEmulatorProbe, o as androidPoolCandidates, s as androidPoolNoCandidatesRefusal, z as physicalDeviceModel } from "./android-75Sk2rIR.mjs";
6
- import { d as waitForDevice, s as parseDeviceWait, t as selectFromPool } from "./device-pool-BoezJd-c.mjs";
7
- import { a as iosPoolCandidates, c as listIosDevices, o as iosPoolNoCandidatesRefusal, s as isWirelessIosDevice, u as resolveIosPhysicalDevice } from "./ios-device-D9cLxVr5.mjs";
2
+ import { a as findProjectRoot, t as NO_PROJECT_REFUSAL } from "./project-DGeclhvO.mjs";
3
+ import { r as clockTime } from "./command-output-BIL7rlX7.mjs";
4
+ import { n as fileLeaseIo, o as parseLeaseDuration, p as takeLease, u as releaseWorkspaceLeases } from "./device-lease-CL9dZf73.mjs";
5
+ import { D as listAdbDevices, H as physicalDeviceModel, J as resolvePhysicalDevice, M as memoizeEmulatorProbe, T as getAvdNameForSerial, W as probeEmulatorSerial, o as androidPoolCandidates, s as androidPoolNoCandidatesRefusal } from "./android-qx_31wJo.mjs";
6
+ import { d as waitForDevice, s as parseDeviceWait, t as selectFromPool } from "./device-pool-MuoiyRcu.mjs";
7
+ import { a as iosPoolCandidates, c as listIosDevices, o as iosPoolNoCandidatesRefusal, s as isWirelessIosDevice, u as resolveIosPhysicalDevice } from "./ios-device-BW-t_Lkv.mjs";
8
8
  import chalk from "chalk";
9
9
  //#region src/commands/device.ts
10
10
  const DEFAULT_FOR = "5m";
@@ -1,15 +1,16 @@
1
1
  import { A as ownership_claim_exports, G as validateDeviceSlot, V as parseDeviceSlotKey, d as getConfigDir, z as deviceSlotKey } from "./config-0jCcMM0E.mjs";
2
2
  import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
3
3
  import { r as register } from "./cache-manifest-DWPF4RKJ.mjs";
4
- import { P as withDirLock, S as sharedBuildCache, _ as withWorkspaceStateLock, m as readWorkspaceState, p as clearWorkspaceStateKeys, y as writeWorkspaceState } from "./metro-Dzo30hvJ.mjs";
4
+ import { C as sharedBuildCache, F as withDirLock, b as writeWorkspaceState, h as readWorkspaceState, m as clearWorkspaceStateKeys, v as withWorkspaceStateLock } from "./metro-BPeY1YMh.mjs";
5
5
  import { n as declareSpawnsOn, o as stopDeclaringSpawnsOn } from "./spawn-claims-PT8aQpCa.mjs";
6
- import { a as formatElapsed, i as formatDuration, m as shortHash } from "./command-output-BWCj5Vh-.mjs";
6
+ import { a as formatElapsed, i as formatDuration, m as shortHash } from "./command-output-BIL7rlX7.mjs";
7
7
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
8
8
  import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, rmSync as rmSync$1, writeFileSync as writeFileSync$1 } from "fs";
9
9
  import { join } from "path";
10
10
  import { artifactIn, buildCacheKey, resolveArtifact, storeArtifact } from "@stim-cli/core";
11
11
  import { join as join$1 } from "node:path";
12
12
  import { createHash, randomBytes } from "node:crypto";
13
+ import { isDeepStrictEqual } from "util";
13
14
  import * as expoFingerprint from "@expo/fingerprint";
14
15
  import { buildUploadTimeoutMs } from "@stim-cli/cache";
15
16
  const ASSET_MANIFEST_FILE = "assets-manifest.json";
@@ -388,6 +389,54 @@ async function refingerprintAfterMutation({ projectRoot, platform, previousHash,
388
389
  moved: computed.hash !== previousHash
389
390
  };
390
391
  }
392
+ const CONFIG_INPUT_REASONS = /* @__PURE__ */ new Set([
393
+ "expoConfig",
394
+ "expoConfigPlugins",
395
+ "expoConfigExternalFile"
396
+ ]);
397
+ function writtenByBuild(name, platform) {
398
+ return name === platform || name.startsWith(`${platform}/`) || name.startsWith("node_modules/") || name.includes("/node_modules/");
399
+ }
400
+ const PREBUILD_WRITTEN_IDS = [["ios", "bundleIdentifier"], ["android", "package"]];
401
+ function expoConfigContents(sources) {
402
+ const source = sources.find((s) => s.type === "contents" && s.id === "expoConfig");
403
+ if (source?.type !== "contents") return null;
404
+ try {
405
+ const parsed = JSON.parse(String(source.contents));
406
+ return parsed && typeof parsed === "object" ? parsed : null;
407
+ } catch {
408
+ return null;
409
+ }
410
+ }
411
+ function onlyPrebuildIdsAdded(before, after) {
412
+ const previous = expoConfigContents(before);
413
+ const current = expoConfigContents(after);
414
+ if (!previous || !current) return false;
415
+ const stripped = { ...current };
416
+ for (const [section, key] of PREBUILD_WRITTEN_IDS) {
417
+ const was = previous[section];
418
+ const now = stripped[section];
419
+ if (now?.[key] === void 0 || was?.[key] !== void 0) continue;
420
+ const rest = { ...now };
421
+ delete rest[key];
422
+ if (was === void 0 && Object.keys(rest).length === 0) delete stripped[section];
423
+ else stripped[section] = rest;
424
+ }
425
+ return isDeepStrictEqual(previous, stripped);
426
+ }
427
+ function configInputsChanged(before, after, { prebuildRan }) {
428
+ return compareSourceLists(before, after).filter((change) => change.reasons.some((reason) => CONFIG_INPUT_REASONS.has(reason))).filter((change) => !(prebuildRan && change.name === "expoConfig" && onlyPrebuildIdsAdded(before, after))).map((change) => change.name);
429
+ }
430
+ function inputsChangedDuringBuild({ platform, lookup, prebuildRan, compiled, current }) {
431
+ const names = new Set(configInputsChanged(lookup, current, { prebuildRan }));
432
+ for (const change of compareSourceLists(compiled, current)) if (!writtenByBuild(change.name, platform)) names.add(change.name);
433
+ return [...names];
434
+ }
435
+ function changedDuringBuildLine(changed) {
436
+ const shown = changed.slice(0, 3);
437
+ const more = changed.length > shown.length ? `, and ${changed.length - shown.length} more` : "";
438
+ return `${shown.join(", ")}${more} changed while the build ran, so the artifact may not match its key; the build will be installed but not cached`;
439
+ }
391
440
  function untrackedNativeFiles({ projectRoot, exec = getExecutor() }) {
392
441
  let out;
393
442
  try {
@@ -1055,4 +1104,4 @@ function withHolderLock(io, root, fn) {
1055
1104
  return io.withHolderLock ? io.withHolderLock(root, fn) : fn();
1056
1105
  }
1057
1106
  //#endregion
1058
- export { resolveBuild as A, filesystemBuildCapability as C, providerDownloadPath as D, prepareProviderDownloadDir as E, untrackedNativeFiles as F, assetDiffReason as I, captureAssetManifest as L, storedAssetManifest as M, storedSources as N, providerUploadOutcome as O, untrackedMissLine as P, compareAssetManifests as R, entryDir as S, fingerprintProject as T, waitForBuild as _, parseLease as a, buildCacheKey as b, raiseLease as c, removeExpiredLease as d, selectPoolDevice as f, releaseBuildLock as g, listBuildLocks as h, listLeaseFiles as i, storeBuild as j, refingerprintAfterMutation as k, releaseRunLease as l, acquireBuildLock as m, fileLeaseIo as n, parseLeaseDuration as o, takeLease as p, leaseIsExpired as r, parseWorkspaceLeases as s, deviceLeasePath as t, releaseWorkspaceLeases as u, waitForSharedBuild as v, fingerprintDiffRecord as w, diffFingerprintSources as x, artifactIn as y, readAssetManifest as z };
1107
+ export { providerDownloadPath as A, captureAssetManifest as B, diffFingerprintSources as C, fingerprintProject as D, fingerprintDiffRecord as E, storedAssetManifest as F, readAssetManifest as H, storedSources as I, untrackedMissLine as L, refingerprintAfterMutation as M, resolveBuild as N, inputsChangedDuringBuild as O, storeBuild as P, untrackedNativeFiles as R, configInputsChanged as S, filesystemBuildCapability as T, compareAssetManifests as V, waitForBuild as _, parseLease as a, buildCacheKey as b, raiseLease as c, removeExpiredLease as d, selectPoolDevice as f, releaseBuildLock as g, listBuildLocks as h, listLeaseFiles as i, providerUploadOutcome as j, prepareProviderDownloadDir as k, releaseRunLease as l, acquireBuildLock as m, fileLeaseIo as n, parseLeaseDuration as o, takeLease as p, leaseIsExpired as r, parseWorkspaceLeases as s, deviceLeasePath as t, releaseWorkspaceLeases as u, waitForSharedBuild as v, entryDir as w, changedDuringBuildLine as x, artifactIn as y, assetDiffReason as z };
@@ -1,7 +1,7 @@
1
1
  import { z as deviceSlotKey } from "./config-0jCcMM0E.mjs";
2
- import { a as formatElapsed, r as clockTime } from "./command-output-BWCj5Vh-.mjs";
3
- import { a as parseLease, c as raiseLease, f as selectPoolDevice, l as releaseRunLease, n as fileLeaseIo, p as takeLease, r as leaseIsExpired, t as deviceLeasePath } from "./device-lease-B9LAXIUH.mjs";
4
- import { a as VERIFY_TIMEOUT_MS, p as APP_READINESS_TIMEOUT_MS } from "./launch-verify-B2oR_kFN.mjs";
2
+ import { a as formatElapsed, r as clockTime } from "./command-output-BIL7rlX7.mjs";
3
+ import { a as parseLease, c as raiseLease, f as selectPoolDevice, l as releaseRunLease, n as fileLeaseIo, p as takeLease, r as leaseIsExpired, t as deviceLeasePath } from "./device-lease-CL9dZf73.mjs";
4
+ import { a as VERIFY_TIMEOUT_MS, p as APP_READINESS_TIMEOUT_MS } from "./launch-verify-CuJaBpEa.mjs";
5
5
  //#region src/engine/device-lease-run.ts
6
6
  const LEASE_STEP_FLOOR_MS = 6e4;
7
7
  const DEBUG_VERIFY_STEP_MS = VERIFY_TIMEOUT_MS + APP_READINESS_TIMEOUT_MS;
@@ -1,16 +1,16 @@
1
1
  import { d as getConfigDir } from "./config-0jCcMM0E.mjs";
2
2
  import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
3
- import { N as workspaceStateFile, O as workspaceDir, P as withDirLock, f as clearWorkspaceStateKey, g as updateWorkspaceState, i as pidExists, j as workspaceLogsDir, k as workspaceId, m as readWorkspaceState$1 } from "./metro-Dzo30hvJ.mjs";
4
- import { o as ownedDeviceLabel } from "./project-B8rbutVC.mjs";
5
- import { a as clearRemoteSession, c as readMetroTunnel, u as readRemoteSession } from "./state-DcDgzfu3.mjs";
3
+ import { A as workspaceId, F as withDirLock, M as workspaceLogsDir, P as workspaceStateFile, _ as updateWorkspaceState, a as pidExists, h as readWorkspaceState$1, k as workspaceDir, p as clearWorkspaceStateKey } from "./metro-BPeY1YMh.mjs";
4
+ import { c as ownedDeviceLabel } from "./project-DGeclhvO.mjs";
5
+ import { a as clearRemoteSession, c as readMetroTunnel, u as readRemoteSession } from "./state-DwAU2_-D.mjs";
6
6
  import { i as signalDeclaredSpawns, r as refuseDeclaredSpawns, t as allowDeclaredSpawns } from "./spawn-claims-PT8aQpCa.mjs";
7
- import { t as withWorkspaceProcessLock } from "./workspace-process-lock-BvvJTfo_.mjs";
8
- import { a as formatElapsed } from "./command-output-BWCj5Vh-.mjs";
9
- import { B as planMetroReach } from "./settings-BlyDn2Oz.mjs";
10
- import { u as probePublicHttp } from "./ownership-B8d4kXBx.mjs";
11
- import { C as sanitizeDeviceLabel } from "./ios-CZWOHTTc.mjs";
12
- import { a as LAUNCH_ERROR, i as INSTALL_ERROR, l as devClientDeepLink } from "./app-install-CsZi0SAT.mjs";
13
- import { c as readMetroRecords, o as isBundleProof } from "./launch-verify-B2oR_kFN.mjs";
7
+ import { a as withWorkspaceProcessLock } from "./named-ports-n7tnJsI8.mjs";
8
+ import { a as formatElapsed } from "./command-output-BIL7rlX7.mjs";
9
+ import { V as planMetroReach } from "./settings-xB0Frkg6.mjs";
10
+ import { u as probePublicHttp } from "./ownership-PUP4Dwai.mjs";
11
+ import { C as sanitizeDeviceLabel } from "./ios-CmEzZiOj.mjs";
12
+ import { a as LAUNCH_ERROR, i as INSTALL_ERROR, l as devClientDeepLink } from "./app-install-CDFY2nUL.mjs";
13
+ import { c as readMetroRecords, o as isBundleProof } from "./launch-verify-CuJaBpEa.mjs";
14
14
  import { t as compareStimVersions } from "./stim-installations-CINXy_su.mjs";
15
15
  import { existsSync, mkdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
16
16
  import { easMachineStateRoot, easMachineStateRoot as easMachineStateRoot$1, easSessionLedgerFile, easSessionLedgerLock, readEasSessionLedger, readEasSessionLedger as readEasSessionLedger$1, readJsonObject, validEasSessionClaim } from "@stim-cli/core/state";
@@ -1,23 +1,23 @@
1
1
  import { _ as loadConfig$1, s as configCorruptRepair, u as getConcurrencyLimits } from "./config-0jCcMM0E.mjs";
2
2
  import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
3
- import { D as workspaceDerivedData, S as sharedBuildCache } from "./metro-Dzo30hvJ.mjs";
3
+ import { C as sharedBuildCache, O as workspaceDerivedData } from "./metro-BPeY1YMh.mjs";
4
4
  import { D as makeTemporaryDirectory, E as filesystemDevice, a as dirtyFingerprintFiles, h as locallyKnownUpstream, k as temporaryRoot, m as listWorktrees, s as gitCommonDir, x as resolveSourceCheckout, y as repoRoot } from "./worktree-DeBQSVtR.mjs";
5
- import { i as findProjectRoot, n as appProjectProblem, r as detectIsExpo } from "./project-B8rbutVC.mjs";
6
- import { d as quotedPath, f as refuseNoProject, l as phaseLine, u as plural } from "./command-output-BWCj5Vh-.mjs";
7
- import { T as fingerprintProject, h as listBuildLocks, x as diffFingerprintSources } from "./device-lease-B9LAXIUH.mjs";
8
- import { E as listAdbDevices, w as hostSystemImageArch } from "./android-75Sk2rIR.mjs";
9
- import { F as settingsLayers, H as compilerCacheFallbackMessage, L as unknownSettingKeys, N as settingOrigin, O as remoteAndroidSetting, P as settingShapeErrors, T as projectOptimizations, W as resolveOptimizations, a as SETTING_SHAPE_REMEDY, i as SETTINGS_WITH_RESOLVE_TIME_FALLBACK, j as resolveSettings, k as remoteIosSetting, n as PATH_SETTINGS, v as iosSimSlimProfileSetting, y as mergeSettingsLayers } from "./settings-BlyDn2Oz.mjs";
10
- import { h as POOL_SETTING_REMEDY, y as parkedMaxSetting } from "./ownership-B8d4kXBx.mjs";
11
- import { O as hostMemoryPressureAdvice, k as readHostMemoryPressure, l as listAllIosSims } from "./ios-CZWOHTTc.mjs";
12
- import { o as listBuildSlots } from "./in-use-D9XYEgFD.mjs";
13
- import { f as resolveEasCliBin, i as checkEasAuth$1, l as ownerFromConfig, u as providerFromConfig } from "./remote-cache-BRsRZru5.mjs";
14
- import { D as MIN_EAS_CLI_SIMULATOR_VERSION, O as easCliSupport, k as easCliUpgradeRemedy, s as readInstalledEasCliVersion } from "./device-remote-C0_o-tJH.mjs";
5
+ import { a as findProjectRoot, n as appProjectProblem, r as detectIsExpo, s as isPackageResolvable } from "./project-DGeclhvO.mjs";
6
+ import { d as quotedPath, f as refuseNoProject, l as phaseLine, u as plural } from "./command-output-BIL7rlX7.mjs";
7
+ import { C as diffFingerprintSources, D as fingerprintProject, h as listBuildLocks } from "./device-lease-CL9dZf73.mjs";
8
+ import { D as listAdbDevices, E as hostSystemImageArch, a as androidHome, ct as parkedMaxSetting, rt as POOL_SETTING_REMEDY } from "./android-qx_31wJo.mjs";
9
+ import { F as settingsLayers, G as resolveOptimizations, L as unknownSettingKeys, N as settingOrigin, O as remoteAndroidSetting, P as settingShapeErrors, R as webSettings, T as projectOptimizations, U as compilerCacheFallbackMessage, a as SETTING_SHAPE_REMEDY, i as SETTINGS_WITH_RESOLVE_TIME_FALLBACK, j as resolveSettings, k as remoteIosSetting, n as PATH_SETTINGS, v as iosSimSlimProfileSetting, y as mergeSettingsLayers } from "./settings-xB0Frkg6.mjs";
10
+ import { O as hostMemoryPressureAdvice, k as readHostMemoryPressure, l as listAllIosSims } from "./ios-CmEzZiOj.mjs";
11
+ import { o as listBuildSlots } from "./in-use-wYEaKQM9.mjs";
12
+ import { i as findChrome, t as CHROME_INSTALL_REMEDY } from "./chrome-B5hWgQJS.mjs";
13
+ import { f as resolveEasCliBin, i as checkEasAuth$1, l as ownerFromConfig, u as providerFromConfig } from "./remote-cache--aehr4Pr.mjs";
14
+ import { D as MIN_EAS_CLI_SIMULATOR_VERSION, O as easCliSupport, k as easCliUpgradeRemedy, s as readInstalledEasCliVersion } from "./device-remote-DT9ngZiD.mjs";
15
15
  import { n as inspectStimVersions, t as compareStimVersions } from "./stim-installations-CINXy_su.mjs";
16
- import { a as liveOwnedDeviceCount, n as simslimIsOnPath, u as MIN_EAS_CLI_BUILD_DOWNLOAD_VERSION } from "./simslim-COtEEP7x.mjs";
17
- import { n as budgetLine, r as inspectBudget } from "./budget-DbzW5VTD.mjs";
18
- import { d as projectCmakeLauncher, g as androidPathRoomRemedy, h as androidPathRoomMessage, i as apkOutputsDir, m as androidPathRoom, r as resolveAndroidCompilerCache, t as readAndroidCasToolchain } from "./android-cas-DzLPXiEn.mjs";
19
- import { h as podInstallCommand, i as ccacheEnabled, o as detectXcodeMajor } from "./xcode-BzplpQzc.mjs";
20
- import { n as recordDoctorRun } from "./guide-status-i9ogHt4v.mjs";
16
+ import { a as liveOwnedDeviceCount, d as MIN_EAS_CLI_BUILD_DOWNLOAD_VERSION, n as simslimIsOnPath } from "./simslim-Bbg9UWrt.mjs";
17
+ import { n as budgetLine, r as inspectBudget } from "./budget-E5o91NR1.mjs";
18
+ import { _ as androidPathRoomRemedy, a as apkOutputsDir, f as projectCmakeLauncher, g as androidPathRoomMessage, h as androidPathRoom, i as androidSdkRefusal, r as resolveAndroidCompilerCache, t as readAndroidCasToolchain } from "./android-cas-B9Kv9aIy.mjs";
19
+ import { h as podInstallCommand, i as ccacheEnabled, o as detectXcodeMajor } from "./xcode-LM6A8cNZ.mjs";
20
+ import { n as recordDoctorRun } from "./guide-status-BaMYu55M.mjs";
21
21
  import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-DNN0yfx6.mjs";
22
22
  import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, rmSync } from "node:fs";
23
23
  import { InvalidArgumentError } from "commander";
@@ -846,6 +846,15 @@ function checkCcacheConflict(podfileSource, podfileProperties) {
846
846
  if (!ccacheEnabled(podfileProperties)) return null;
847
847
  return finding("cost", "ccache is enabled, so Stim leaves Xcode compilation caching off", "The ccache launcher script is what disables explicitly built modules, which compilation caching requires -- so enabling both tends to mean neither works. Stim will not add its compilation-cache settings to a build whose project has apple.ccacheEnabled=true, and in its default configuration ccache hashes the working directory and every absolute include path, so it misses across worktrees. (Stim relocates ccache itself on Android, where it drives the compile and can set CCACHE_BASEDIR and CCACHE_NOHASHDIR; the Podfile launcher script here is the project's, not Stim's.)", "Pick one, and on Xcode 26 the compilation cache is the one that survives a different workspace path -- Stim supplies it on its own builds as soon as ccache is off. Turn it off where the value comes FROM: on Expo that is the expo-build-properties plugin in the app config (ios.ccacheEnabled), because prebuild rewrites ios/Podfile.properties.json from it; on a bare project edit ios/Podfile.properties.json directly. Then re-run pod install (or let `stim ios` do it).");
848
848
  }
849
+ function checkChrome({ usesWeb, chrome = findChrome }) {
850
+ if (!usesWeb() || chrome()) return null;
851
+ return finding("cost", "No Chrome is installed, so `stim web` cannot open this app", "`stim web` drives the installed Google Chrome or Chromium with a Stim-owned profile, and found neither in /Applications, ~/Applications, Program Files or on PATH. This project renders on the web (web.url is set, app.json lists the web platform, or react-native-web is installed).", CHROME_INSTALL_REMEDY);
852
+ }
853
+ function projectUsesWeb(projectRoot, settings, appConfig, platform) {
854
+ if (platform) return false;
855
+ const platforms = (appConfig?.expo ?? appConfig)?.platforms;
856
+ return webSettings(settings).url !== null || Array.isArray(platforms) && platforms.includes("web") || isPackageResolvable(projectRoot, "react-native-web");
857
+ }
849
858
  function checkCcacheInstalled(onPath) {
850
859
  if (onPath) return null;
851
860
  return finding("cost", "ccache is not on PATH, so Android C++ recompiles in every worktree", "Stim resolves the launcher with a PATH lookup for `ccache` -- the same lookup the build itself uses -- and found nothing, so it passes no CMAKE_C_COMPILER_LAUNCHER / CMAKE_CXX_COMPILER_LAUNCHER to Gradle and every AGP CMake task compiles without one. Those tasks are uncacheable by Gradle, so not one C++ object crosses a worktree. Measured on trailhead (arm64, fresh worktree): 49.6s without the launcher against 34.2s with it.", "brew install ccache, then delete android/app/.cxx and node_modules/**/android/.cxx once so CMake reconfigures with the launcher. The shell that runs Stim must have the install location on PATH -- agent shells often lack /opt/homebrew/bin.");
@@ -955,7 +964,7 @@ function checkXcodeEnvLineEndings(projectRoot, platform, host) {
955
964
  if (!content.includes("\r")) return null;
956
965
  return finding("cost", "ios/.xcode.env has CRLF line endings", "An EAS build uploads the working-tree bytes of ios/.xcode.env, and Xcode sources that file with sh on the build worker, where each carriage return becomes part of the line and the build fails with `: command not found`. On Windows the usual cause is core.autocrlf=true rewriting the file at checkout.", "Run `git config core.autocrlf input` then `git checkout -- ios/.xcode.env`, or add `ios/.xcode.env text eol=lf` to .gitattributes and check the file out again.");
957
966
  }
958
- function runDoctor(projectRoot, { readFile = readFileSync$1, xcodeMajor = null, easAuth = checkEasAuth$1, concurrency = getConcurrencyLimits, liveDevices = null, activeBuilds = null, remoteEnv = process.env, lookupAgentDevice = null, lookupEasCli = null, lookupSimSlim = null, memoryPressure = readHostMemoryPressure, lookupCcache = null, platform, host = process.platform } = {}) {
967
+ function runDoctor(projectRoot, { readFile = readFileSync$1, xcodeMajor = null, easAuth = checkEasAuth$1, concurrency = getConcurrencyLimits, liveDevices = null, activeBuilds = null, remoteEnv = process.env, lookupAgentDevice = null, lookupEasCli = null, lookupSimSlim = null, memoryPressure = readHostMemoryPressure, lookupCcache = null, lookupChrome, platform, host = process.platform } = {}) {
959
968
  const localIos = platform !== "android" && host === "darwin";
960
969
  const read = (rel) => {
961
970
  const p = join(projectRoot, rel);
@@ -1062,6 +1071,11 @@ function runDoctor(projectRoot, { readFile = readFileSync$1, xcodeMajor = null,
1062
1071
  localIos && optimizations?.ios.compilationCache ? checkCcacheConflict(podfile, podfileProperties) : null,
1063
1072
  ...platform === "ios" || optimizations?.android.compilerCache !== "ccache" ? [] : androidCcacheFindings(projectRoot, platform, lookupCcache),
1064
1073
  checkAndroidPathRoom(projectRoot, platform, host),
1074
+ checkAndroidSdk(projectRoot, platform),
1075
+ checkChrome({
1076
+ usesWeb: () => projectUsesWeb(projectRoot, projectSettings, appConfig, platform),
1077
+ chrome: lookupChrome
1078
+ }),
1065
1079
  remoteBuildCache ? checkBuildCacheProvider(appConfig, sdkMajor, isExpo, dynamicConfig) : null,
1066
1080
  easFinding,
1067
1081
  easBuildDownloadFinding,
@@ -1090,6 +1104,21 @@ function checkAndroidPathRoom(projectRoot, platform, host, hostArch = process.ar
1090
1104
  ...finding("cost", "The project path leaves no room for Android native object paths", `${androidPathRoomMessage(room)} \`stim android\` refuses to build here with STIM_PATH_TOO_LONG, because Gradle would fail deep inside ninja instead.`, androidPathRoomRemedy(room))
1091
1105
  };
1092
1106
  }
1107
+ function checkAndroidSdk(projectRoot, platform) {
1108
+ const androidDir = join(projectRoot, "android");
1109
+ if (platform === "ios" || platform !== "android" && !existsSync$1(androidDir)) return null;
1110
+ const sdkPath = androidHome();
1111
+ const refusal = androidSdkRefusal({
1112
+ sdkPath,
1113
+ sdkExists: existsSync$1(sdkPath),
1114
+ hasLocalProperties: existsSync$1(join(androidDir, "local.properties"))
1115
+ });
1116
+ if (!refusal) return null;
1117
+ return {
1118
+ code: "android-sdk-missing",
1119
+ ...finding("cost", "No Android SDK was found", `${refusal.reason} \`stim android\` refuses with ${refusal.code} before Gradle runs.`, refusal.remedy)
1120
+ };
1121
+ }
1093
1122
  function androidCcacheFindings(projectRoot, platform, lookupCcache) {
1094
1123
  if (platform !== "android" && !existsSync$1(join(projectRoot, "android"))) return [];
1095
1124
  const onPath = lookupCcache ? lookupCcache() : ccacheIsOnPath();
@@ -1342,7 +1371,7 @@ function doctorSuccessLines(platform, stim, budget = null) {
1342
1371
  }
1343
1372
  if (platform !== "ios") {
1344
1373
  lines.push("", "Android");
1345
- lines.push(phaseLine("setup", "warm state"));
1374
+ lines.push(phaseLine("setup", "Android SDK, warm state"));
1346
1375
  lines.push(phaseLine("caches", "Metro, Gradle, ccache, build provider"));
1347
1376
  lines.push(phaseLine("devices", "remote device"));
1348
1377
  }
@@ -1,12 +1,12 @@
1
1
  import { B as deviceSlotPlatforms, p as getProject, z as deviceSlotKey } from "./config-0jCcMM0E.mjs";
2
2
  import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
3
- import { P as withDirLock, m as readWorkspaceState$1, s as resolveProjectMetro } from "./metro-Dzo30hvJ.mjs";
4
- import { f as readWorkspaceLaunches } from "./state-DcDgzfu3.mjs";
5
- import { v as launchErrorPreview } from "./command-output-BWCj5Vh-.mjs";
6
- import { a as parseLease, n as fileLeaseIo, t as deviceLeasePath } from "./device-lease-B9LAXIUH.mjs";
7
- import { a as androidHome, l as androidToolCwd } from "./android-75Sk2rIR.mjs";
3
+ import { F as withDirLock, c as resolveProjectMetro, h as readWorkspaceState$1 } from "./metro-BPeY1YMh.mjs";
4
+ import { f as readWorkspaceLaunches } from "./state-DwAU2_-D.mjs";
5
+ import { v as launchErrorPreview } from "./command-output-BIL7rlX7.mjs";
6
+ import { a as parseLease, n as fileLeaseIo, t as deviceLeasePath } from "./device-lease-CL9dZf73.mjs";
7
+ import { a as androidHome, l as androidToolCwd } from "./android-qx_31wJo.mjs";
8
8
  import { r as deviceConsoleLevel } from "./ios-device-BiOsSVzd.mjs";
9
- import { n as parseLogcatLine, t as androidClockOffset } from "./android-BQiKJYK0.mjs";
9
+ import { n as parseLogcatLine, t as androidClockOffset } from "./android-BHfAu2mf.mjs";
10
10
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
11
11
  import { attachExpoErrorContext, readLogRecords, sortByTs } from "@stim-cli/core/state";
12
12
  import { basename, dirname, join, resolve, sep } from "node:path";