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,28 +1,32 @@
1
- import { t as getExecutor } from "./exec-B29XNuhP.mjs";
2
- import { g as loadConfig, l as getConcurrencyLimits, o as configCorruptRepair } from "./config-B7iyKWkL.mjs";
3
- import { l as sharedBuildCache, m as workspaceDerivedData } from "./workspace-state-BT5E3SA2.mjs";
4
- import { C as filesystemDevice, E as temporaryRoot, a as dirtyFingerprintFiles, d as listWorktrees, f as locallyKnownUpstream, g as repoRoot, s as gitCommonDir, v as resolveSourceCheckout, w as makeTemporaryDirectory } from "./worktree-FG_ZKKdb.mjs";
5
- import { a as findProjectRoot, i as detectIsExpo, t as appProjectProblem } from "./project-DDZWf_16.mjs";
6
- import { A as resolveSettings, E as remoteAndroidSetting, I as unknownSettingKeys, M as settingOrigin, N as settingShapeErrors, O as remoteIosSetting, P as settingsLayers, U as resolveOptimizations, V as compilerCacheFallbackMessage, a as SETTING_SHAPE_REMEDY, i as SETTINGS_WITH_RESOLVE_TIME_FALLBACK, n as PATH_SETTINGS, v as iosSimSlimProfileSetting, w as projectOptimizations, y as mergeSettingsLayers } from "./settings-BXXorVM2.mjs";
7
- import { S as listAdbDevices, x as hostSystemImageArch } from "./android-DWssqHQ8.mjs";
8
- import { d as quotedPath, l as phaseLine, u as plural } from "./command-output-B3ycpsYv.mjs";
9
- import { m as fingerprintProject, r as listBuildLocks, u as diffFingerprintSources } from "./build-lock-CgtsB6TV.mjs";
10
- import { S as resolveEasCliBin, a as POOL_SETTING_REMEDY, b as ownerFromConfig, h as checkEasAuth$1, l as parkedMaxSetting, x as providerFromConfig } from "./teardown-BJqEzPo_.mjs";
11
- import { D as hostMemoryPressureAdvice, O as readHostMemoryPressure, u as listAllIosSims } from "./ios-BcCiI4TS.mjs";
12
- import { n as listBuildSlots } from "./build-slots-C-oAHRT1.mjs";
13
- import { a as liveOwnedDeviceCount, d as simslimIsOnPath } from "./device-BUoZyMoR.mjs";
14
- 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-SWIjzeuL.mjs";
15
- import { h as podInstallCommand, i as ccacheEnabled, o as detectXcodeMajor } from "./xcode-B6YaWeg0.mjs";
16
- import { i as inspectStimVersions, n as recordDoctorRun, r as compareStimVersions } from "./guide-status-Craq9z0t.mjs";
17
- import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-D0M7oKA7.mjs";
1
+ import { g as loadConfig$1, l as getConcurrencyLimits, o as configCorruptRepair } from "./config-BEq0Qw_a.mjs";
2
+ import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
3
+ import { D as workspaceDerivedData, S as sharedBuildCache } from "./metro-BWLOJ_Wr.mjs";
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-BlizKDpZ.mjs";
5
+ import { a as detectIsExpo, n as appProjectProblem, o as findProjectRoot } from "./project-BXZzOABt.mjs";
6
+ import { d as quotedPath, f as refuseNoProject, l as phaseLine, u as plural } from "./command-output-D5z2_bcF.mjs";
7
+ import { T as fingerprintProject, h as listBuildLocks, x as diffFingerprintSources } from "./device-lease-CbEaIk7b.mjs";
8
+ import { S as hostSystemImageArch, w as listAdbDevices } from "./android-787HhP5w.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-DOugPlPz.mjs";
10
+ import { h as POOL_SETTING_REMEDY, y as parkedMaxSetting } from "./ownership-DZjdx8Gq.mjs";
11
+ import { O as hostMemoryPressureAdvice, k as readHostMemoryPressure, l as listAllIosSims } from "./ios-B2Fpqvlb.mjs";
12
+ import { o as listBuildSlots } from "./in-use-CjUluqIX.mjs";
13
+ import { L as ownerFromConfig, M as checkEasAuth$1, R as providerFromConfig, _ as easCliSupport, g as MIN_EAS_CLI_SIMULATOR_VERSION, s as readInstalledEasCliVersion, v as easCliUpgradeRemedy, z as resolveEasCliBin } from "./device-remote-Dy1jGj3U.mjs";
14
+ import { n as inspectStimVersions, t as compareStimVersions } from "./stim-installations-CINXy_su.mjs";
15
+ import { a as liveOwnedDeviceCount, l as MIN_EAS_CLI_BUILD_DOWNLOAD_VERSION, n as simslimIsOnPath } from "./simslim-Ca3487H6.mjs";
16
+ import { n as budgetLine, r as inspectBudget } from "./budget-BP4jZZL4.mjs";
17
+ 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-CITmIeWY.mjs";
18
+ import { h as podInstallCommand, i as ccacheEnabled, o as detectXcodeMajor } from "./xcode-B2tBkf0m.mjs";
19
+ import { n as recordDoctorRun } from "./guide-status-CWvti2Lg.mjs";
20
+ import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-r9UesA9a.mjs";
18
21
  import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, rmSync } from "node:fs";
19
22
  import { InvalidArgumentError } from "commander";
20
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
21
23
  import { existsSync as existsSync$1, lstatSync as lstatSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, renameSync as renameSync$1, rmSync as rmSync$1, writeFileSync as writeFileSync$1 } from "fs";
24
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
25
+ import { readJsonObject, settingDefinition } from "@stim-cli/core/state";
26
+ import { basename as basename$1, dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1, resolve as resolve$1, sep as sep$1 } from "node:path";
22
27
  import { homedir } from "os";
23
- import { dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1, resolve as resolve$1, sep as sep$1 } from "path";
24
- import * as expoFingerprint from "@expo/fingerprint";
25
28
  import chalk from "chalk";
29
+ import * as expoFingerprint from "@expo/fingerprint";
26
30
  //#region src/diagnostics/sandbox.ts
27
31
  /**
28
32
  * What a sandboxed harness has to permit for Stim to work: its state
@@ -43,8 +47,8 @@ function detectHarness(env = process.env) {
43
47
  function expandHome(value, home) {
44
48
  const v = value.trim();
45
49
  if (v === "~" || v === "$HOME") return home;
46
- if (v.startsWith("~/")) return join$1(home, v.slice(2));
47
- if (v.startsWith("$HOME/")) return join$1(home, v.slice(6));
50
+ if (v.startsWith("~/")) return join(home, v.slice(2));
51
+ if (v.startsWith("$HOME/")) return join(home, v.slice(6));
48
52
  return v;
49
53
  }
50
54
  /**
@@ -54,7 +58,7 @@ function expandHome(value, home) {
54
58
  */
55
59
  function sandboxBlocksStimHome(stimHome, home = homedir()) {
56
60
  const dir = expandHome(stimHome, home);
57
- const probe = join$1(dir, `.probe-${process.pid}-${Math.random().toString(36).slice(2)}`);
61
+ const probe = join(dir, `.probe-${process.pid}-${Math.random().toString(36).slice(2)}`);
58
62
  try {
59
63
  mkdirSync$1(dir, { recursive: true });
60
64
  writeFileSync$1(probe, "");
@@ -67,14 +71,14 @@ function sandboxBlocksStimHome(stimHome, home = homedir()) {
67
71
  }
68
72
  /** Where a per-user, gitignored Claude Code settings file belongs for this project. */
69
73
  function claudeLocalSettingsPath(root) {
70
- return join$1(root, ".claude", "settings.local.json");
74
+ return join(root, ".claude", "settings.local.json");
71
75
  }
72
76
  /** Every settings file that may already carry the allowance, most local first. */
73
77
  function allowanceSearchPaths(settingsRoot, home = homedir()) {
74
78
  return [
75
79
  claudeLocalSettingsPath(settingsRoot),
76
- join$1(settingsRoot, ".claude", "settings.json"),
77
- join$1(home, ".claude", "settings.json")
80
+ join(settingsRoot, ".claude", "settings.json"),
81
+ join(home, ".claude", "settings.json")
78
82
  ];
79
83
  }
80
84
  function isPlainObject(value) {
@@ -114,10 +118,10 @@ function nested(source, ...keys) {
114
118
  function covers(entry, target, home) {
115
119
  const posix = (value) => {
116
120
  const expanded = expandHome(value, home);
117
- return sep$1 === "/" ? expanded : expanded.replaceAll(sep$1, "/");
121
+ return sep === "/" ? expanded : expanded.replaceAll(sep, "/");
118
122
  };
119
- const e = posix(entry.replace(/\/+\*+$/, "").replace(/\/+$/, ""));
120
- const t = posix(target.replace(/\/+$/, ""));
123
+ const e = posix(entry).replace(/\/+\*+$/, "").replace(/\/+$/, "");
124
+ const t = posix(target).replace(/\/+$/, "");
121
125
  return e === t || t.startsWith(`${e}/`);
122
126
  }
123
127
  /**
@@ -186,7 +190,7 @@ function applyClaudeAllowance(path, allowance) {
186
190
  status: "refused",
187
191
  reason: `${path} holds a "${blocker}" that is not an object, so Stim cannot merge into it without discarding that value.`
188
192
  };
189
- mkdirSync$1(dirname$1(path), { recursive: true });
193
+ mkdirSync$1(dirname(path), { recursive: true });
190
194
  const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
191
195
  writeFileSync$1(tmp, `${JSON.stringify(withAllowance(existing, allowance), null, 2)}\n`);
192
196
  try {
@@ -247,14 +251,14 @@ function checkStorageLayout(projectRoot, { platform, host = process.platform, de
247
251
  const source = resolveSourceCheckout(target);
248
252
  check("Worktree copy", ["path" in source ? source.path : target, target], "Keep the source checkout and linked worktree on the same volume to share file blocks when warming.");
249
253
  const cache = sharedBuildCache();
250
- check("Cached app/APK staging", [cache, stagingRoot(join(cache, "artifact.app"))], temporaryFix);
254
+ check("Cached app/APK staging", [cache, stagingRoot(join$1(cache, "artifact.app"))], temporaryFix);
251
255
  const cacheFix = "Place STIM_BUILD_CACHE / machine caches.buildCache on the build-output volume, or accept the full-copy cost of keeping the cache on a separate volume.";
252
- if ((platform === "ios" || platform !== "android" && existsSync(join(projectRoot, "ios"))) && host === "darwin") {
253
- const output = join(workspaceDerivedData(projectRoot), "Build", "Products");
256
+ if ((platform === "ios" || platform !== "android" && existsSync(join$1(projectRoot, "ios"))) && host === "darwin") {
257
+ const output = join$1(workspaceDerivedData(projectRoot), "Build", "Products");
254
258
  check("iOS build-cache storage", [output, cache], cacheFix);
255
- check("iOS device app staging", [output, stagingRoot(join(output, "artifact.app"))], temporaryFix);
259
+ check("iOS device app staging", [output, stagingRoot(join$1(output, "artifact.app"))], temporaryFix);
256
260
  }
257
- if (platform === "android" || platform !== "ios" && existsSync(join(projectRoot, "android"))) check("Android build-cache storage", [apkOutputsDir(projectRoot), cache], cacheFix);
261
+ if (platform === "android" || platform !== "ios" && existsSync(join$1(projectRoot, "android"))) check("Android build-cache storage", [apkOutputsDir(projectRoot), cache], cacheFix);
258
262
  } catch (error) {
259
263
  findings.push({
260
264
  level: "cost",
@@ -338,23 +342,23 @@ function parseIosDebugArchitectures(output) {
338
342
  };
339
343
  }
340
344
  function inspectIosDebugArchitectures(projectRoot) {
341
- const iosRoot = join(projectRoot, "ios");
345
+ const iosRoot = join$1(projectRoot, "ios");
342
346
  if (!existsSync(iosRoot)) return [];
343
347
  const findings = [];
344
348
  const unknown = [];
345
349
  let projects;
346
350
  try {
347
- projects = readdirSync(iosRoot).filter((name) => name.endsWith(".xcodeproj")).toSorted().map((name) => join(iosRoot, name));
351
+ projects = readdirSync(iosRoot).filter((name) => name.endsWith(".xcodeproj")).toSorted().map((name) => join$1(iosRoot, name));
348
352
  } catch {
349
353
  projects = [];
350
354
  }
351
355
  if (!projects.length) unknown.push("app project");
352
- const pods = join(iosRoot, "Pods", "Pods.xcodeproj");
356
+ const pods = join$1(iosRoot, "Pods", "Pods.xcodeproj");
353
357
  if (existsSync(pods)) projects.push(pods);
354
- else if (existsSync(join(iosRoot, "Podfile"))) unknown.push("Pods project (not generated)");
358
+ else if (existsSync(join$1(iosRoot, "Podfile"))) unknown.push("Pods project (not generated)");
355
359
  const deadline = Date.now() + 6e4;
356
360
  for (const project of projects) {
357
- const label = relative(projectRoot, project);
361
+ const label = relative$1(projectRoot, project);
358
362
  const timeoutMs = Math.min(3e4, deadline - Date.now());
359
363
  if (timeoutMs <= 0) {
360
364
  unknown.push(label);
@@ -416,13 +420,13 @@ function children(path) {
416
420
  }
417
421
  }
418
422
  function moduleRoots(root) {
419
- const modules = [join(root, "android", "app")];
420
- const dependencies = join(root, "node_modules");
423
+ const modules = [join$1(root, "android", "app")];
424
+ const dependencies = join$1(root, "node_modules");
421
425
  for (const name of children(dependencies)) {
422
426
  if (name.startsWith(".")) continue;
423
- const entry = join(dependencies, name);
424
- const packages = name.startsWith("@") ? children(entry).map((child) => join(entry, child)) : [entry];
425
- modules.push(...packages.map((pkg) => join(pkg, "android")));
427
+ const entry = join$1(dependencies, name);
428
+ const packages = name.startsWith("@") ? children(entry).map((child) => join$1(entry, child)) : [entry];
429
+ modules.push(...packages.map((pkg) => join$1(pkg, "android")));
426
430
  }
427
431
  return modules;
428
432
  }
@@ -430,7 +434,7 @@ function cacheFiles(base, depth = 0) {
430
434
  const files = [];
431
435
  for (const name of children(base)) {
432
436
  if (depth === 0 && /^stim-[a-f0-9]{16}$/.test(name)) continue;
433
- const path = join(base, name);
437
+ const path = join$1(base, name);
434
438
  const entry = lstatSync(path);
435
439
  if (entry.isSymbolicLink()) continue;
436
440
  if (name === "CMakeCache.txt" && entry.isFile()) files.push(path);
@@ -439,8 +443,8 @@ function cacheFiles(base, depth = 0) {
439
443
  return files;
440
444
  }
441
445
  function readCxxLauncherStates(root) {
442
- return moduleRoots(root).flatMap((module) => cacheFiles(join(module, ".cxx")).map((path) => ({
443
- path: relative(root, path),
446
+ return moduleRoots(root).flatMap((module) => cacheFiles(join$1(module, ".cxx")).map((path) => ({
447
+ path: relative$1(root, path),
444
448
  launcher: parseCmakeCacheLauncher(readFileSync(path, "utf8"))
445
449
  })));
446
450
  }
@@ -450,13 +454,13 @@ function declaredLauncher(module) {
450
454
  "build.gradle.kts",
451
455
  "CMakeLists.txt"
452
456
  ].some((name) => {
453
- const path = join(module, name);
457
+ const path = join$1(module, name);
454
458
  return existsSync(path) && Boolean(projectCmakeLauncher(readFileSync(path, "utf8")));
455
459
  });
456
460
  }
457
461
  function contained(root, path) {
458
- const rel = relative(root, path);
459
- return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
462
+ const rel = relative$1(root, path);
463
+ return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep$1}`) && !isAbsolute$1(rel);
460
464
  }
461
465
  function selectedCompilerCache(root) {
462
466
  try {
@@ -480,24 +484,24 @@ function repairCxxLauncherState(root) {
480
484
  };
481
485
  if (selectedCompilerCache(root) !== "ccache") return result;
482
486
  if (!getExecutor().findExecutable("ccache")) return result;
483
- const appOverrides = declaredLauncher(join(root, "android", "app"));
487
+ const appOverrides = declaredLauncher(join$1(root, "android", "app"));
484
488
  const targets = readCxxLauncherStates(root).filter(({ launcher }) => {
485
489
  if (!launcher) return true;
486
490
  const command = launcher.split(";")[0]?.trim();
487
- return Boolean(command && isAbsolute(command) && basename(command) === "ccache" && !existsSync(command));
491
+ return Boolean(command && isAbsolute$1(command) && basename$1(command) === "ccache" && !existsSync(command));
488
492
  });
489
493
  for (const state of targets) {
490
- const target = dirname(join(root, state.path));
491
- const label = relative(root, target);
494
+ const target = dirname$1(join$1(root, state.path));
495
+ const label = relative$1(root, target);
492
496
  try {
493
497
  let cxx = target;
494
- while (contained(root, cxx) && basename(cxx) !== ".cxx") cxx = dirname(cxx);
495
- if (basename(cxx) !== ".cxx" || target === cxx) throw new Error("unrecognized generated CMake layout");
496
- if (appOverrides || declaredLauncher(dirname(cxx))) throw new Error("project configures its own compiler launcher");
498
+ while (contained(root, cxx) && basename$1(cxx) !== ".cxx") cxx = dirname$1(cxx);
499
+ if (basename$1(cxx) !== ".cxx" || target === cxx) throw new Error("unrecognized generated CMake layout");
500
+ if (appOverrides || declaredLauncher(dirname$1(cxx))) throw new Error("project configures its own compiler launcher");
497
501
  const canonicalTarget = realpathSync(target);
498
502
  if (!contained(canonicalRoot, canonicalTarget)) throw new Error("generated directory resolves outside this checkout");
499
- if (realpathSync(cxx) !== join(realpathSync(dirname(cxx)), ".cxx")) throw new Error(".cxx is a symbolic link");
500
- const gitPath = relative(canonicalRoot, canonicalTarget);
503
+ if (realpathSync(cxx) !== join$1(realpathSync(dirname$1(cxx)), ".cxx")) throw new Error(".cxx is a symbolic link");
504
+ const gitPath = relative$1(canonicalRoot, canonicalTarget);
501
505
  if (getExecutor().runFile("git", [
502
506
  "ls-files",
503
507
  "-z",
@@ -551,14 +555,13 @@ function readMachineSettings(context) {
551
555
  layers: [],
552
556
  corrupt: {
553
557
  level: "note",
554
- title: "The Stim config is not valid JSON",
558
+ title: "The Stim config is not a valid JSON object",
555
559
  detail: `${String(error.message).split("\n")[0]}. Every setting and device record lives in that file, so no other machine-level check can run until it parses.`,
556
560
  fix: configCorruptRepair()
557
561
  }
558
562
  };
559
563
  }
560
564
  }
561
- const PATH_SETTING_ENVIRONMENT = { "optimizations.android.casToolchain": "STIM_ANDROID_CAS_TOOLCHAIN" };
562
565
  const PATH_SETTINGS_TRIMMED_BY_THEIR_CONSUMER = /* @__PURE__ */ new Set(["android.keystore"]);
563
566
  function checkMachineSettings({ settings, layers, projectRoot, optimizations = null, reportedElsewhere = [], exists = existsSync, env = process.env }) {
564
567
  const findings = [];
@@ -570,7 +573,7 @@ function checkMachineSettings({ settings, layers, projectRoot, optimizations = n
570
573
  if (reported) skip.add(reported.key);
571
574
  if (!optimizations) for (const key of SETTINGS_WITH_RESOLVE_TIME_FALLBACK) skip.add(key);
572
575
  for (const key of PATH_SETTINGS) {
573
- const variable = PATH_SETTING_ENVIRONMENT[key];
576
+ const variable = settingDefinition(key)?.env;
574
577
  if (skip.has(key) || variable && skip.has(variable)) continue;
575
578
  const override = variable ? env[variable] : void 0;
576
579
  if (variable && override) {
@@ -639,7 +642,7 @@ function compilerCacheFinding({ fallback, optimizations, file }) {
639
642
  function missingPath(value, projectRoot, exists, trim = false) {
640
643
  const opened = trim ? value.trim() : value;
641
644
  if (opened.trim() === "") return null;
642
- const path = isAbsolute(opened) ? opened : resolve(projectRoot, opened);
645
+ const path = isAbsolute$1(opened) ? opened : resolve$1(projectRoot, opened);
643
646
  if (exists(path)) return null;
644
647
  return opened === opened.trim() ? path : JSON.stringify(path);
645
648
  }
@@ -653,22 +656,15 @@ function finding(level, title, detail, fix) {
653
656
  fix
654
657
  };
655
658
  }
656
- function readJson(path) {
657
- try {
658
- return JSON.parse(readFileSync$1(path, "utf-8"));
659
- } catch {
660
- return null;
661
- }
662
- }
663
659
  function mainCheckoutProjectRoot(projectRoot) {
664
660
  const currentRepoRoot = repoRoot(projectRoot);
665
661
  if (!currentRepoRoot) return projectRoot;
666
662
  try {
667
663
  const source = resolveSourceCheckout(currentRepoRoot);
668
664
  if ("refusal" in source) return projectRoot;
669
- const projectRel = relative$1(currentRepoRoot, realpathSync$1(projectRoot));
665
+ const projectRel = relative(currentRepoRoot, realpathSync$1(projectRoot));
670
666
  if (projectRel.startsWith("..")) return projectRoot;
671
- return resolve$1(source.path, projectRel);
667
+ return resolve(source.path, projectRel);
672
668
  } catch {
673
669
  return projectRoot;
674
670
  }
@@ -687,11 +683,11 @@ function brokenPodLinks(podsRoot) {
687
683
  }
688
684
  }
689
685
  function hasIosWarmOutput(root) {
690
- if (existsSync$1(join$1(root, "ios", "build"))) return true;
686
+ if (existsSync$1(join(root, "ios", "build"))) return true;
691
687
  const derivedData = workspaceDerivedData(root);
692
- if (existsSync$1(join$1(derivedData, "Build", "Products"))) return true;
688
+ if (existsSync$1(join(derivedData, "Build", "Products"))) return true;
693
689
  try {
694
- return readdirSync$1(derivedData).some((entry) => /^scheme-[a-f0-9]{64}$/.test(entry) && existsSync$1(join$1(derivedData, entry, "Build", "Products")));
690
+ return readdirSync$1(derivedData).some((entry) => /^scheme-[a-f0-9]{64}$/.test(entry) && existsSync$1(join(derivedData, entry, "Build", "Products")));
695
691
  } catch {
696
692
  return false;
697
693
  }
@@ -720,8 +716,8 @@ function mainOperation(root) {
720
716
  "--git-dir"
721
717
  ])?.trim();
722
718
  if (!gitDir) return null;
723
- if (existsSync$1(join$1(gitDir, "rebase-merge")) || existsSync$1(join$1(gitDir, "rebase-apply"))) return "rebase";
724
- return existsSync$1(join$1(gitDir, "MERGE_HEAD")) ? "merge" : null;
719
+ if (existsSync$1(join(gitDir, "rebase-merge")) || existsSync$1(join(gitDir, "rebase-apply"))) return "rebase";
720
+ return existsSync$1(join(gitDir, "MERGE_HEAD")) ? "merge" : null;
725
721
  }
726
722
  function dirtyTrackedPaths(root) {
727
723
  return (getExecutor().runFileQuiet("git", [
@@ -785,9 +781,9 @@ function checkMainCheckout(projectRoot, { npmTreeValid, brokenPods = void 0, ups
785
781
  if ((npmTreeValid === void 0 ? installedNpmTreeIsValid(dependencies.root) : npmTreeValid) === false) findings.push(finding("cost", "The source checkout dependency tree is stale", `npm reports that ${dependencies.root}/node_modules does not match the project dependency graph. Copying it makes each worktree start from the same invalid state.`, `Run \`${installCommand}\` before creating native worktrees, or let \`stim worktree warm --refresh\` run it from a linked worktree.`));
786
782
  }
787
783
  }
788
- const podfileLock = join$1(mainRoot, "ios", "Podfile.lock");
789
- const podManifest = join$1(mainRoot, "ios", "Pods", "Manifest.lock");
790
- const podsRoot = join$1(mainRoot, "ios", "Pods");
784
+ const podfileLock = join(mainRoot, "ios", "Podfile.lock");
785
+ const podManifest = join(mainRoot, "ios", "Pods", "Manifest.lock");
786
+ const podsRoot = join(mainRoot, "ios", "Pods");
791
787
  if (localIos && existsSync$1(podfileLock)) {
792
788
  let podsState = null;
793
789
  if (!existsSync$1(podManifest)) podsState = "missing";
@@ -797,18 +793,18 @@ function checkMainCheckout(projectRoot, { npmTreeValid, brokenPods = void 0, ups
797
793
  podsState = "stale";
798
794
  }
799
795
  if (podsState) {
800
- const iosRoot = join$1(mainRoot, "ios");
796
+ const iosRoot = join(mainRoot, "ios");
801
797
  const podCommand = `cd ${quotedPath(iosRoot)} && ${podInstallCommand(mainRoot)}`;
802
798
  findings.push(finding("cost", `The source checkout CocoaPods state is ${podsState}`, `ios/Pods cannot be reused safely because its Manifest.lock ${podsState === "missing" ? "is absent" : "does not match ios/Podfile.lock"}.`, `Run \`${podCommand}\` before creating native worktrees, or let \`stim worktree warm --refresh\` run it from a linked worktree.`));
803
799
  }
804
800
  const broken = brokenPods === void 0 && existsSync$1(podsRoot) ? brokenPodLinks(podsRoot) : brokenPods || [];
805
801
  if (broken.length) {
806
- const iosRoot = join$1(mainRoot, "ios");
802
+ const iosRoot = join(mainRoot, "ios");
807
803
  const podCommand = `cd ${quotedPath(iosRoot)} && ${podInstallCommand(mainRoot, "--clean-install")}`;
808
804
  findings.push(finding("cost", "The source checkout CocoaPods state has broken links", `${broken.length} symlink${broken.length === 1 ? "" : "s"} under ios/Pods point to missing files. Worktrees copy these broken links and can fail during compilation. First: ${broken[0]}.`, `Run \`${podCommand}\` before creating native worktrees.`));
809
805
  }
810
806
  }
811
- const coldPlatforms = [localIos && existsSync$1(join$1(mainRoot, "ios")) && !hasIosWarmOutput(mainRoot) ? "iOS" : null, platform !== "ios" && existsSync$1(join$1(mainRoot, "android")) && !existsSync$1(join$1(mainRoot, "android", "build")) && !existsSync$1(join$1(mainRoot, "android", "app", "build")) ? "Android" : null].filter((coldPlatform) => coldPlatform !== null);
807
+ const coldPlatforms = [localIos && existsSync$1(join(mainRoot, "ios")) && !hasIosWarmOutput(mainRoot) ? "iOS" : null, platform !== "ios" && existsSync$1(join(mainRoot, "android")) && !existsSync$1(join(mainRoot, "android", "build")) && !existsSync$1(join(mainRoot, "android", "app", "build")) ? "Android" : null].filter((coldPlatform) => coldPlatform !== null);
812
808
  if (coldPlatforms.length) findings.push(finding("note", `The source checkout has no ${coldPlatforms.join(" or ")} warm build output`, "The shared Stim artifact or compilation cache can still be warm. Building the source checkout once gives later native worktrees the strongest warm starting point.", `When more native worktrees are expected, run \`stim start\`, \`stim ${coldPlatforms[0] === "iOS" ? "ios" : "android"}\`, and \`stim stop\` from ${mainRoot}.`));
813
809
  if (linkedWorktrees === void 0 ? hasLinkedWorktree(projectRoot) : linkedWorktrees) findings.push(...seedFindings(mainRoot, upstream === void 0 ? locallyKnownUpstream(mainRoot) : upstream));
814
810
  return findings;
@@ -855,7 +851,7 @@ function checkCcacheInstalled(onPath) {
855
851
  }
856
852
  function launcherPath(launcher) {
857
853
  const command = launcher?.split(";")[0]?.trim();
858
- return command && isAbsolute$1(command) ? command : null;
854
+ return command && isAbsolute(command) ? command : null;
859
855
  }
860
856
  function checkCxxCompilerLauncher({ states, ccacheOnPath, launcherExists = existsSync$1 }) {
861
857
  const missing = states.map((state) => ({
@@ -904,7 +900,11 @@ function checkConcurrency({ maxBuilds = 0, maxDevices = 0, liveDevices = 0, acti
904
900
  if (!maxBuilds && !maxDevices) return null;
905
901
  return finding("note", "Concurrency limits are set", `${`maxBuilds ${maxBuilds || "unlimited"}, maxDevices ${maxDevices || "unlimited"}`}. Right now ${liveDevices} Stim device(s) are booted and ${activeBuilds} build slot(s) are in use on this machine. At the device cap a new \`stim ios\`/\`android\` is refused with STIM_AT_CAPACITY (stop an environment or raise it); at the build cap a compile waits for a free slot.`, null);
906
902
  }
907
- function checkRemoteDevice({ configured = null, daemonInEnv = false, agentDeviceOnPath = false, easCliResolvable = false } = {}) {
903
+ function readProjectEasCliVersion(projectRoot) {
904
+ const bin = resolveEasCliBin(projectRoot);
905
+ return bin ? readInstalledEasCliVersion(bin.file, projectRoot) : null;
906
+ }
907
+ function checkRemoteDevice({ configured = null, daemonInEnv = false, agentDeviceOnPath = false, easCliResolvable = false, readEasCliVersion = () => null } = {}) {
908
908
  if (!configured) return null;
909
909
  if (!agentDeviceOnPath) return finding("cost", "A remote device is configured, but agent-device is missing", `agent-device drives the selected remote backend. Without it, \`stim ios --remote ${configured}\` and \`stim android --remote ${configured}\` refuse before device work.`, "npm i -g agent-device");
910
910
  if (configured === "proxy") {
@@ -912,8 +912,25 @@ function checkRemoteDevice({ configured = null, daemonInEnv = false, agentDevice
912
912
  return finding("cost", "The remote proxy credentials are missing", "The proxy backend requires AGENT_DEVICE_DAEMON_BASE_URL and AGENT_DEVICE_DAEMON_AUTH_TOKEN.", "Export AGENT_DEVICE_DAEMON_BASE_URL and AGENT_DEVICE_DAEMON_AUTH_TOKEN.");
913
913
  }
914
914
  if (!easCliResolvable) return finding("cost", "A remote device is configured, but there is no eas-cli to create a session with", "The eas backend creates an EAS Simulator session. It needs eas-cli and an account with EAS Simulator access. Neither a project copy nor one on PATH was found.", "Install eas-cli.");
915
+ const easCli = easCliSupport(readEasCliVersion(), MIN_EAS_CLI_SIMULATOR_VERSION);
916
+ if (!easCli.supported) return finding("cost", easCli.version ? `A remote device is configured, but eas-cli ${easCli.version} has no EAS Simulator commands` : "A remote device is configured, but the eas-cli version could not be read", `The eas backend runs \`eas simulator:*\` commands, which need eas-cli ${MIN_EAS_CLI_SIMULATOR_VERSION} or later. \`stim ios --remote eas\` and \`stim android --remote eas\` refuse with STIM_REMOTE_EAS_UNAVAILABLE before device work.`, `${easCliUpgradeRemedy(MIN_EAS_CLI_SIMULATOR_VERSION)}.`);
915
917
  return finding("note", "This project uses a remote device", "`ios --remote eas` / `android --remote eas` create an EAS Simulator session named stim-<label> and end it on `stop` and `worktree remove`. The build still runs on this machine; only the device is elsewhere. Native device logs are not captured on a remote device -- the Metro half of the timeline is unaffected.", null);
916
918
  }
919
+ function checkEasBuildDownload({ easJson = false, remoteEas = false, easCliResolvable = false, readEasCliVersion = () => null } = {}) {
920
+ if (!easJson || remoteEas || !easCliResolvable) return null;
921
+ const easCli = easCliSupport(readEasCliVersion(), MIN_EAS_CLI_BUILD_DOWNLOAD_VERSION);
922
+ if (easCli.supported) return null;
923
+ return finding("cost", easCli.version ? `eas-cli ${easCli.version} cannot download EAS builds for --eas-profile` : "The eas-cli version could not be read for --eas-profile", `\`stim ios --eas-profile\` and \`stim android --eas-profile\` download a finished EAS development build with \`eas build:download --build-id\`, which needs eas-cli ${MIN_EAS_CLI_BUILD_DOWNLOAD_VERSION} or later. Older versions refuse with STIM_EAS_UNAVAILABLE before any download.`, `${easCliUpgradeRemedy(MIN_EAS_CLI_BUILD_DOWNLOAD_VERSION)}.`);
924
+ }
925
+ function projectEasBuildDownloadFinding(projectRoot, remoteBackends, lookupEasCli) {
926
+ const easJson = existsSync$1(join(projectRoot, "eas.json"));
927
+ return checkEasBuildDownload({
928
+ easJson,
929
+ remoteEas: remoteBackends.includes("eas"),
930
+ easCliResolvable: easJson && (lookupEasCli ? lookupEasCli() : Boolean(resolveEasCliBin(projectRoot))),
931
+ readEasCliVersion: () => readProjectEasCliVersion(projectRoot)
932
+ });
933
+ }
917
934
  function checkSimSlim({ configured = false, profileError = null, onPath = false } = {}) {
918
935
  if (profileError) return finding("cost", "The SimSlim profile is invalid", profileError, "Set ios.simslimProfile to a readable JSON profile inside the repository.");
919
936
  if (!configured) return finding("note", "SimSlim is recommended for parallel iOS simulator work", "A reviewed profile can reduce background simulator services and memory use. It is optional: disabling services can affect tests that depend on them, and installing the binary alone does not apply a profile.", "Run `stim guide lifecycle simslim` to install SimSlim and configure ios.simslimProfile. Stim does not enable a profile through doctor --fix.");
@@ -926,7 +943,7 @@ function checkIosHost(platform, host) {
926
943
  }
927
944
  function checkXcodeEnvLineEndings(projectRoot, platform, host) {
928
945
  if (platform === "android" || host !== "win32") return null;
929
- const xcodeEnv = join$1(projectRoot, "ios", ".xcode.env");
946
+ const xcodeEnv = join(projectRoot, "ios", ".xcode.env");
930
947
  if (!existsSync$1(xcodeEnv)) return null;
931
948
  let content;
932
949
  try {
@@ -940,7 +957,7 @@ function checkXcodeEnvLineEndings(projectRoot, platform, host) {
940
957
  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 } = {}) {
941
958
  const localIos = platform !== "android" && host === "darwin";
942
959
  const read = (rel) => {
943
- const p = join$1(projectRoot, rel);
960
+ const p = join(projectRoot, rel);
944
961
  if (!existsSync$1(p)) return null;
945
962
  try {
946
963
  return readFile(p, "utf-8");
@@ -956,15 +973,15 @@ function runDoctor(projectRoot, { readFile = readFileSync$1, xcodeMajor = null,
956
973
  });
957
974
  if (machineSettings.corrupt) return [machineSettings.corrupt];
958
975
  const projectSettings = machineSettings.settings;
959
- const pkg = readJson(join$1(projectRoot, "package.json"));
960
- const appConfig = readJson(join$1(projectRoot, "app.json"));
976
+ const pkg = readJsonObject(join(projectRoot, "package.json"));
977
+ const appConfig = readJsonObject(join(projectRoot, "app.json"));
961
978
  const dynamicConfig = appConfig ? null : [
962
979
  "app.config.ts",
963
980
  "app.config.js",
964
981
  "app.config.mjs"
965
- ].find((f) => existsSync$1(join$1(projectRoot, f))) || null;
966
- const podfileProperties = readJson(join$1(projectRoot, "ios", "Podfile.properties.json"));
967
- const podfile = read(join$1("ios", "Podfile"));
982
+ ].find((f) => existsSync$1(join(projectRoot, f))) || null;
983
+ const podfileProperties = readJsonObject(join(projectRoot, "ios", "Podfile.properties.json"));
984
+ const podfile = read(join("ios", "Podfile"));
968
985
  const metroConfig = read("metro.config.js") ?? read("metro.config.cjs");
969
986
  const isExpo = detectIsExpo(projectRoot);
970
987
  const expoRange = (pkg?.dependencies)?.expo || "";
@@ -986,9 +1003,7 @@ function runDoctor(projectRoot, { readFile = readFileSync$1, xcodeMajor = null,
986
1003
  optimizations: resolveOptimizations(projectSettings),
987
1004
  use: readAndroidCasToolchain
988
1005
  }).optimizations;
989
- } catch (error) {
990
- settingShapeFindings.push(finding("cost", "Invalid optimization setting", error.message, SETTING_SHAPE_REMEDY));
991
- }
1006
+ } catch {}
992
1007
  const remoteBuildCache = optimizations?.buildCache && optimizations.remoteBuildCache;
993
1008
  const easFinding = remoteBuildCache && provider === "eas" ? checkEasAuth({
994
1009
  provider,
@@ -1023,8 +1038,10 @@ function runDoctor(projectRoot, { readFile = readFileSync$1, xcodeMajor = null,
1023
1038
  configured: backend,
1024
1039
  daemonInEnv,
1025
1040
  agentDeviceOnPath,
1026
- easCliResolvable
1041
+ easCliResolvable,
1042
+ readEasCliVersion: () => readProjectEasCliVersion(projectRoot)
1027
1043
  })).filter((remoteFinding) => remoteFinding !== null);
1044
+ const easBuildDownloadFinding = projectEasBuildDownloadFinding(projectRoot, remoteBackends, lookupEasCli);
1028
1045
  const memoryAdvice = localIos && !remoteIosSetting(projectSettings) ? hostMemoryPressureAdvice(memoryPressure()) : null;
1029
1046
  return [
1030
1047
  checkAppProject(projectRoot),
@@ -1046,6 +1063,7 @@ function runDoctor(projectRoot, { readFile = readFileSync$1, xcodeMajor = null,
1046
1063
  checkAndroidPathRoom(projectRoot, platform, host),
1047
1064
  remoteBuildCache ? checkBuildCacheProvider(appConfig, sdkMajor, isExpo, dynamicConfig) : null,
1048
1065
  easFinding,
1066
+ easBuildDownloadFinding,
1049
1067
  concurrencyFinding,
1050
1068
  memoryAdvice ? finding("cost", "Host memory pressure can stall the iOS simulator", memoryAdvice, "Free host memory, then retry. Run `stim guide lifecycle simslim` for the optional memory reduction setup.") : null,
1051
1069
  simslimFinding,
@@ -1072,7 +1090,7 @@ function checkAndroidPathRoom(projectRoot, platform, host, hostArch = process.ar
1072
1090
  };
1073
1091
  }
1074
1092
  function androidCcacheFindings(projectRoot, platform, lookupCcache) {
1075
- if (platform !== "android" && !existsSync$1(join$1(projectRoot, "android"))) return [];
1093
+ if (platform !== "android" && !existsSync$1(join(projectRoot, "android"))) return [];
1076
1094
  const onPath = lookupCcache ? lookupCcache() : ccacheIsOnPath();
1077
1095
  let cxx;
1078
1096
  try {
@@ -1113,7 +1131,7 @@ function countLiveDevices() {
1113
1131
  adb = listAdbDevices() || adb;
1114
1132
  } catch {}
1115
1133
  try {
1116
- config = loadConfig();
1134
+ config = loadConfig$1();
1117
1135
  } catch {}
1118
1136
  return liveOwnedDeviceCount({
1119
1137
  sims,
@@ -1135,7 +1153,7 @@ function checkFingerprintParity({ projectHash, worktreeHash, changed = [], dirty
1135
1153
  }
1136
1154
  function gitMetadataAt(path) {
1137
1155
  try {
1138
- const git = lstatSync$1(join$1(path, ".git"));
1156
+ const git = lstatSync$1(join(path, ".git"));
1139
1157
  return git.isFile() || git.isDirectory();
1140
1158
  } catch {
1141
1159
  return false;
@@ -1150,14 +1168,14 @@ function listDirectory(dir) {
1150
1168
  }
1151
1169
  function hasLinkedPackageWithGit(projectRoot) {
1152
1170
  const root = repoRoot(projectRoot) ?? projectRoot;
1153
- for (let dir = projectRoot;; dir = dirname$1(dir)) {
1154
- if (linkedPackageWithGitIn(join$1(dir, "node_modules"))) return true;
1155
- if (dir === root || dirname$1(dir) === dir || relative$1(root, dirname$1(dir)).startsWith("..")) return false;
1171
+ for (let dir = projectRoot;; dir = dirname(dir)) {
1172
+ if (linkedPackageWithGitIn(join(dir, "node_modules"))) return true;
1173
+ if (dir === root || dirname(dir) === dir || relative(root, dirname(dir)).startsWith("..")) return false;
1156
1174
  }
1157
1175
  }
1158
1176
  function linkedPackageWithGitIn(nodeModules) {
1159
- return listDirectory(nodeModules).filter((entry) => !entry.startsWith(".")).flatMap((entry) => entry.startsWith("@") ? listDirectory(join$1(nodeModules, entry)).map((child) => join$1(entry, child)) : [entry]).some((name) => {
1160
- const root = join$1(nodeModules, name);
1177
+ return listDirectory(nodeModules).filter((entry) => !entry.startsWith(".")).flatMap((entry) => entry.startsWith("@") ? listDirectory(join(nodeModules, entry)).map((child) => join(entry, child)) : [entry]).some((name) => {
1178
+ const root = join(nodeModules, name);
1161
1179
  try {
1162
1180
  return lstatSync$1(root).isSymbolicLink() && gitMetadataAt(root);
1163
1181
  } catch {
@@ -1209,14 +1227,14 @@ async function detectFingerprintParity(projectRoot, { createFingerprint = expoFi
1209
1227
  "--git-dir"
1210
1228
  ], { timeoutMs: 1e4 }) == null) return null;
1211
1229
  if (hasInstalledDependencies(projectRoot)) return null;
1212
- const platform = selectedPlatform ?? (existsSync$1(join$1(projectRoot, "ios")) ? "ios" : existsSync$1(join$1(projectRoot, "android")) ? "android" : void 0);
1230
+ const platform = selectedPlatform ?? (existsSync$1(join(projectRoot, "ios")) ? "ios" : existsSync$1(join(projectRoot, "android")) ? "android" : void 0);
1213
1231
  let base;
1214
1232
  try {
1215
1233
  base = makeTemporaryDirectory(projectRoot, "stim-parity-");
1216
1234
  } catch {
1217
1235
  return null;
1218
1236
  }
1219
- const worktree = join$1(base, "head");
1237
+ const worktree = join(base, "head");
1220
1238
  if (exec.runFileQuiet("git", [
1221
1239
  "-C",
1222
1240
  projectRoot,
@@ -1300,12 +1318,16 @@ function stimVersionLines(report) {
1300
1318
  phaseLine("installs", paths + versions)
1301
1319
  ];
1302
1320
  }
1303
- function doctorSuccessLines(platform, stim) {
1321
+ function budgetLines(budget) {
1322
+ return budget ? [phaseLine("budget", budgetLine(budget))] : [];
1323
+ }
1324
+ function doctorSuccessLines(platform, stim, budget = null) {
1304
1325
  const lines = [
1305
1326
  `Doctor (${doctorTarget(platform)})`,
1306
1327
  phaseLine("result", "PASS"),
1307
1328
  phaseLine("findings", "0"),
1308
1329
  ...stimVersionLines(stim),
1330
+ ...budgetLines(budget),
1309
1331
  "",
1310
1332
  "Project",
1311
1333
  phaseLine("project", "source checkout, dependencies, local upstream"),
@@ -1382,11 +1404,10 @@ function applySandboxFix(root, env = process.env) {
1382
1404
  console.error(chalk.dim("Added writes to Stim's state directory, the simulator XPC service, and local port binding. Claude Code reads project settings from the directory a session starts in, so this file only counts for sessions rooted there. Restart the session for it to take effect."));
1383
1405
  }
1384
1406
  function doctorCommand(program, version, inspectVersions = inspectStimVersions, host = process.platform) {
1385
- program.command("doctor").description("Inspect the source checkout and report project state that can make native worktrees slow or invalid. The checkout is left untouched unless --fix is passed; --platform filters native findings. Each run is recorded in Stim state so guide can say when doctor is due.").option("--json", "print the findings as JSON").option("--platform <platform>", "report shared findings plus only this native platform: ios or android", parseDoctorPlatform).option("--fix", "repair the sandbox allowance when the report names it, and stale Android .cxx configurations in this checkout. Stop native builds first. Generated CMake output must be ignored and untracked; custom launcher settings and source files are preserved.").action(async (opts) => {
1407
+ program.command("doctor").description("Inspect the source checkout and report project state that can make native worktrees slow or invalid. The checkout is left untouched unless --fix is passed; --platform filters native findings. Each run in a React Native or Expo app is recorded in Stim state so guide can say when doctor is due.").option("--json", "print the findings as JSON").option("--platform <platform>", "report shared findings plus only this native platform: ios or android", parseDoctorPlatform).option("--fix", "repair the sandbox allowance when the report names it, and stale Android .cxx configurations in this checkout. Stop native builds first. Generated CMake output must be ignored and untracked; custom launcher settings and source files are preserved.").action(async (opts) => {
1386
1408
  const root = findProjectRoot(process.cwd());
1387
1409
  if (!root) {
1388
- console.error(chalk.red("Not in a React Native project (no package.json found)."));
1389
- process.exitCode = 1;
1410
+ refuseNoProject({ json: Boolean(opts.json) });
1390
1411
  return;
1391
1412
  }
1392
1413
  if (opts.fix) {
@@ -1417,18 +1438,21 @@ function doctorCommand(program, version, inspectVersions = inspectStimVersions,
1417
1438
  }
1418
1439
  const shadowed = shadowedStimFinding(stim);
1419
1440
  if (shadowed) findings.push(shadowed);
1441
+ const budget = await inspectBudget(root);
1442
+ findings.push(...budget.findings);
1420
1443
  if (opts.json) {
1421
1444
  console.log(JSON.stringify({
1422
1445
  project: root,
1423
1446
  platform: opts.platform ?? null,
1424
1447
  stim,
1448
+ budget: budget.report,
1425
1449
  findings
1426
1450
  }));
1427
1451
  recordDoctorRun(root, opts.platform, version);
1428
1452
  return;
1429
1453
  }
1430
1454
  if (findings.length === 0) {
1431
- const lines = doctorSuccessLines(opts.platform, stim);
1455
+ const lines = doctorSuccessLines(opts.platform, stim, budget.report);
1432
1456
  for (const [index, line] of lines.entries()) if (index === 1) console.log(chalk.green(line));
1433
1457
  else if (line && !line.startsWith(" ")) console.log(chalk.bold(line));
1434
1458
  else console.log(chalk.dim(line));
@@ -1437,7 +1461,7 @@ function doctorCommand(program, version, inspectVersions = inspectStimVersions,
1437
1461
  }
1438
1462
  const ordered = findings.toSorted((a, b) => a.level === b.level ? 0 : a.level === "cost" ? -1 : 1);
1439
1463
  console.log(chalk.bold(`Doctor (${doctorTarget(opts.platform)})`));
1440
- for (const line of stimVersionLines(stim)) console.log(chalk.dim(line));
1464
+ for (const line of [...stimVersionLines(stim), ...budgetLines(budget.report)]) console.log(chalk.dim(line));
1441
1465
  for (const f of ordered) {
1442
1466
  const tag = f.level === "cost" ? chalk.yellow("costs time") : chalk.dim("note");
1443
1467
  console.log(`\n${tag} ${chalk.bold(f.title)}`);