stim 1.3.1 → 1.4.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 (63) hide show
  1. package/README.md +13 -0
  2. package/dist/{android-CT-PkXUh.mjs → android-BTR1KTMP.mjs} +197 -114
  3. package/dist/{android-hMj83VpN.mjs → android-BkM4qTaN.mjs} +65 -72
  4. package/dist/{android-cas-DXorPCi_.mjs → android-cas-BjLRZyks.mjs} +15 -12
  5. package/dist/{app-install-DIp9riNx.mjs → app-install--PAocWvq.mjs} +2 -2
  6. package/dist/{build-slots-C4AouyNz.mjs → build-slots-D6sDzOe-.mjs} +7 -8
  7. package/dist/{cache-manifest-CXR-Y8pH.mjs → cache-manifest-D02TpYjA.mjs} +1 -1
  8. package/dist/cache-manifest.mjs +1 -1
  9. package/dist/cli.mjs +14 -13
  10. package/dist/collector-run.mjs +4 -4
  11. package/dist/{command-output-8wHMEde0.mjs → command-output-BuIb_ZOI.mjs} +1 -4
  12. package/dist/{config-CcefMY2Q.mjs → config-6vdGngKa.mjs} +51 -51
  13. package/dist/dependency-state-BMirHEPD.mjs +76 -0
  14. package/dist/{deps-0Yt0Inly.mjs → deps-Ck4wuTwa.mjs} +1 -1
  15. package/dist/{dev-client-VfSZNLT4.mjs → dev-client-Cd0v7iZk.mjs} +12 -12
  16. package/dist/{device-Bbv45Z9Z.mjs → device-CKXLnRRb.mjs} +7 -7
  17. package/dist/{xcode-xnxyvJkD.mjs → device-CMDFzx7C.mjs} +208 -967
  18. package/dist/{device-lease-CJwK7OZr.mjs → device-lease-DDJr85OK.mjs} +3 -3
  19. package/dist/{device-pool-CcBNTvM7.mjs → device-pool-DJvbIBGW.mjs} +4 -4
  20. package/dist/{device-remote-DAA0Zm6Z.mjs → device-remote-CMfHhoc8.mjs} +7 -6
  21. package/dist/{doctor-B4EITgQB.mjs → doctor-B_mfURdt.mjs} +536 -114
  22. package/dist/{error-diagnostics-DwhNBFDU.mjs → error-diagnostics-_VmkqRZo.mjs} +7 -6
  23. package/dist/{gc-BRu62v4o.mjs → gc-0AyxmECv.mjs} +61 -27
  24. package/dist/{guide-D95E0u3i.mjs → guide-CGxdmQ0K.mjs} +126 -7
  25. package/dist/{ios-DAju3TVy.mjs → ios-CtmGLx3X.mjs} +2667 -2607
  26. package/dist/{ios-device-CWOM_S44.mjs → ios-device-DJOi9FR5.mjs} +1 -1
  27. package/dist/{logs-CAdwW6c2.mjs → logs-BF3qPA7w.mjs} +5 -5
  28. package/dist/metro-DFLmaVz8.mjs +99 -0
  29. package/dist/named-ports-BO7Zpjp0.mjs +137 -0
  30. package/dist/{ownership-8O073GIC.mjs → ownership-BvSpZVBk.mjs} +5 -5
  31. package/dist/{ownership-DXcol4oU.mjs → ownership-C08Yzrp7.mjs} +6 -6
  32. package/dist/ports-BiKqP92J.mjs +39 -0
  33. package/dist/ports-ClqG8GPt.mjs +90 -0
  34. package/dist/{project-fKmuIaMI.mjs → project-CBFCwnN5.mjs} +2 -2
  35. package/dist/{reclaim-CUmwucWG.mjs → reclaim-DpDVo-AP.mjs} +20 -13
  36. package/dist/{reload-CWpXoOgN.mjs → reload-ByTqg6ZI.mjs} +8 -8
  37. package/dist/rolldown-runtime-BsOwBuO_.mjs +30 -0
  38. package/dist/{server-bare-DEzPZLvf.mjs → server-bare-Buk-k8tI.mjs} +3 -6
  39. package/dist/{server-expo-DizT4OMk.mjs → server-expo-D4P7iTvf.mjs} +3 -3
  40. package/dist/server-expo-Dho5Raiw.mjs +2 -0
  41. package/dist/{settings-DEJf6_kJ.mjs → settings-DwfltIXQ.mjs} +2 -2
  42. package/dist/{slot-launch-CZklomj6.mjs → slot-launch-DeKen9e5.mjs} +4 -4
  43. package/dist/{start-B5G4NjiA.mjs → start-BpKUHcU_.mjs} +15 -13
  44. package/dist/state-CDbvmZdt.mjs +131 -0
  45. package/dist/{stats-BR-kwj0B.mjs → stats-C59QLKm2.mjs} +1 -1
  46. package/dist/{stats-CaaQr4yN.mjs → stats-k5qMjyxo.mjs} +4 -4
  47. package/dist/{status-CI9clRSb.mjs → status-Y2ij3dm6.mjs} +12 -11
  48. package/dist/{stop-CjlgDk0a.mjs → stop-_P2OQwmZ.mjs} +15 -14
  49. package/dist/{stop-CQhffpmX.mjs → stop-vKmmxhea.mjs} +2 -2
  50. package/dist/supervisor-run.d.mts +3 -27
  51. package/dist/supervisor-run.mjs +7 -6
  52. package/dist/{teardown--gESs9ck.mjs → teardown-KsPpvldq.mjs} +148 -109
  53. package/dist/{workspace-process-lock-IBj1Towr.mjs → workspace-process-lock-UEIrvZMJ.mjs} +4 -4
  54. package/dist/workspace-state-Do1e2mOH.mjs +146 -0
  55. package/dist/{worktree-DtQGO9-s.mjs → worktree-BvcAIMdJ.mjs} +24 -25
  56. package/dist/{worktree-Eq6TLH8N.mjs → worktree-DWNx4tUg.mjs} +32 -2
  57. package/dist/xcode-D9tVItl-.mjs +860 -0
  58. package/package.json +7 -7
  59. package/dist/doctor-BueDfLYG.mjs +0 -530
  60. package/dist/metro-CORqXyTL.mjs +0 -184
  61. package/dist/ownership-claim-BtPtJVmq.mjs +0 -584
  62. package/dist/server-expo-44em5Pmg.mjs +0 -2
  63. package/dist/state-B3XA2wp2.mjs +0 -268
@@ -1,21 +1,228 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { g as loadConfig, l as getConcurrencyLimits, o as configCorruptRepair } from "./config-CcefMY2Q.mjs";
3
- import { b as sharedBuildCache, w as workspaceDerivedData } from "./state-B3XA2wp2.mjs";
4
- import { S as makeTemporaryDirectory, a as dirtyFingerprintFiles, f as listWorktrees, g as repoRoot, s as gitCommonDir, w as temporaryRoot, x as filesystemDevice } from "./worktree-Eq6TLH8N.mjs";
5
- import { i as detectIsExpo, t as appProjectProblem } from "./project-fKmuIaMI.mjs";
6
- import { d as quotedPath, u as plural } from "./command-output-8wHMEde0.mjs";
7
- import { B as listBuildLocks, Z as fingerprintProject, q as diffFingerprintSources, y as listAdbDevices } from "./android-hMj83VpN.mjs";
8
- 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-DEJf6_kJ.mjs";
9
- import { a as POOL_SETTING_REMEDY, b as providerFromConfig, c as parkedMaxSetting, m as checkEasAuth$1, x as resolveEasCliBin, y as ownerFromConfig } from "./teardown--gESs9ck.mjs";
2
+ import { g as loadConfig, l as getConcurrencyLimits, o as configCorruptRepair } from "./config-6vdGngKa.mjs";
3
+ import { l as sharedBuildCache, m as workspaceDerivedData } from "./workspace-state-Do1e2mOH.mjs";
4
+ import { C as makeTemporaryDirectory, S as filesystemDevice, T as temporaryRoot, _ as repoRoot, a as dirtyFingerprintFiles, f as listWorktrees, p as locallyKnownUpstream, s as gitCommonDir } from "./worktree-DWNx4tUg.mjs";
5
+ import { a as findProjectRoot, i as detectIsExpo, t as appProjectProblem } from "./project-CBFCwnN5.mjs";
6
+ import { d as quotedPath, l as phaseLine, u as plural } from "./command-output-BuIb_ZOI.mjs";
7
+ import { B as listBuildLocks, Z as fingerprintProject, q as diffFingerprintSources, y as listAdbDevices } from "./android-BkM4qTaN.mjs";
8
+ 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-DwfltIXQ.mjs";
9
+ import { S as resolveEasCliBin, a as POOL_SETTING_REMEDY, b as ownerFromConfig, h as checkEasAuth$1, l as parkedMaxSetting, x as providerFromConfig } from "./teardown-KsPpvldq.mjs";
10
10
  import { E as readHostMemoryPressure, T as hostMemoryPressureAdvice, u as listAllIosSims } from "./ios-Deh5js5V.mjs";
11
- import { n as listBuildSlots } from "./build-slots-C4AouyNz.mjs";
12
- import { C as liveOwnedDeviceCount, D as simslimIsOnPath, a as ccacheEnabled, g as podInstallCommand } from "./xcode-xnxyvJkD.mjs";
13
- import { d as projectCmakeLauncher, i as apkOutputsDir, r as resolveAndroidCompilerCache, t as readAndroidCasToolchain } from "./android-cas-DXorPCi_.mjs";
14
- import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, rmSync } from "node:fs";
15
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
16
- import { existsSync as existsSync$1, lstatSync as lstatSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, rmSync as rmSync$1 } from "fs";
11
+ import { n as listBuildSlots } from "./build-slots-D6sDzOe-.mjs";
12
+ import { a as liveOwnedDeviceCount, d as simslimIsOnPath } from "./device-CMDFzx7C.mjs";
13
+ import { d as projectCmakeLauncher, i as apkOutputsDir, r as resolveAndroidCompilerCache, t as readAndroidCasToolchain } from "./android-cas-BjLRZyks.mjs";
14
+ import { h as podInstallCommand, i as ccacheEnabled, o as detectXcodeMajor } from "./xcode-D9tVItl-.mjs";
15
+ import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-BMirHEPD.mjs";
16
+ import { accessSync, constants, existsSync, lstatSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
17
+ import { InvalidArgumentError } from "commander";
18
+ import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
19
+ 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";
20
+ import { homedir } from "os";
17
21
  import { dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1, resolve as resolve$1 } from "path";
18
22
  import * as expoFingerprint from "@expo/fingerprint";
23
+ import chalk from "chalk";
24
+ //#region src/sandbox.ts
25
+ /**
26
+ * What a sandboxed harness has to permit for Stim to work: its state
27
+ * directory, the XPC service simctl talks to, and the port adb's server binds.
28
+ */
29
+ function sandboxAllowance(stimHome = "~/.stim") {
30
+ return {
31
+ allowWrite: [stimHome],
32
+ allowMachLookup: ["com.apple.coresimulator.*"],
33
+ allowLocalBinding: true
34
+ };
35
+ }
36
+ function detectHarness(env = process.env) {
37
+ if (env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT) return "claude-code";
38
+ if (Object.keys(env).some((k) => k.startsWith("CODEX_"))) return "codex";
39
+ return null;
40
+ }
41
+ function expandHome(value, home) {
42
+ const v = value.trim();
43
+ if (v === "~" || v === "$HOME") return home;
44
+ if (v.startsWith("~/")) return join$1(home, v.slice(2));
45
+ if (v.startsWith("$HOME/")) return join$1(home, v.slice(6));
46
+ return v;
47
+ }
48
+ /**
49
+ * Whether the sandbox actually stops Stim, rather than whether a harness that
50
+ * can sandbox is present: a session with its sandbox off exports the same
51
+ * variables as one with it on, so only a write says which this is.
52
+ */
53
+ function sandboxBlocksStimHome(stimHome, home = homedir()) {
54
+ const dir = expandHome(stimHome, home);
55
+ const probe = join$1(dir, `.probe-${process.pid}-${Math.random().toString(36).slice(2)}`);
56
+ try {
57
+ mkdirSync$1(dir, { recursive: true });
58
+ writeFileSync$1(probe, "");
59
+ rmSync$1(probe, { force: true });
60
+ return false;
61
+ } catch (err) {
62
+ const code = err.code;
63
+ return code === "EPERM" || code === "EACCES";
64
+ }
65
+ }
66
+ /** Where a per-user, gitignored Claude Code settings file belongs for this project. */
67
+ function claudeLocalSettingsPath(root) {
68
+ return join$1(root, ".claude", "settings.local.json");
69
+ }
70
+ /** Every settings file that may already carry the allowance, most local first. */
71
+ function allowanceSearchPaths(settingsRoot, home = homedir()) {
72
+ return [
73
+ claudeLocalSettingsPath(settingsRoot),
74
+ join$1(settingsRoot, ".claude", "settings.json"),
75
+ join$1(home, ".claude", "settings.json")
76
+ ];
77
+ }
78
+ function isPlainObject(value) {
79
+ return !!value && typeof value === "object" && !Array.isArray(value);
80
+ }
81
+ /**
82
+ * Absent, unreadable and present-but-not-an-object are three different states.
83
+ * Collapsing them loses the one distinction that matters before a write: a
84
+ * file whose contents did not parse still holds settings we must not destroy.
85
+ */
86
+ function readSettings(path) {
87
+ let raw;
88
+ try {
89
+ raw = readFileSync$1(path, "utf-8");
90
+ } catch (err) {
91
+ return err.code === "ENOENT" ? { kind: "absent" } : { kind: "invalid" };
92
+ }
93
+ try {
94
+ const parsed = JSON.parse(raw);
95
+ return isPlainObject(parsed) ? {
96
+ kind: "object",
97
+ value: parsed
98
+ } : { kind: "invalid" };
99
+ } catch {
100
+ return { kind: "invalid" };
101
+ }
102
+ }
103
+ function nested(source, ...keys) {
104
+ let cursor = source;
105
+ for (const key of keys) {
106
+ if (!isPlainObject(cursor)) return void 0;
107
+ cursor = cursor[key];
108
+ }
109
+ return cursor;
110
+ }
111
+ /** Whether an allowWrite entry covers the Stim home, including as a parent. */
112
+ function covers(entry, target, home) {
113
+ const e = expandHome(entry.replace(/\/+\*+$/, "").replace(/\/+$/, ""), home);
114
+ const t = expandHome(target.replace(/\/+$/, ""), home);
115
+ return e === t || t.startsWith(`${e}/`);
116
+ }
117
+ /**
118
+ * Which parts of the allowance the settings files already carry. Any file may
119
+ * supply any part, so a project that solved this in one place is not asked to
120
+ * repeat it in another.
121
+ */
122
+ function missingAllowance(paths, stimHome = "~/.stim", home = homedir()) {
123
+ const files = paths.map((p) => {
124
+ const read = readSettings(p);
125
+ return read.kind === "object" ? read.value : null;
126
+ });
127
+ const list = (...keys) => files.flatMap((f) => {
128
+ const value = nested(f, ...keys);
129
+ return Array.isArray(value) ? value.map(String) : [];
130
+ });
131
+ const missing = [];
132
+ if (!list("sandbox", "filesystem", "allowWrite").some((w) => covers(w, stimHome, home))) missing.push("sandbox.filesystem.allowWrite");
133
+ if (!list("sandbox", "network", "allowMachLookup").some((l) => l.toLowerCase().startsWith("com.apple.coresimulator"))) missing.push("sandbox.network.allowMachLookup");
134
+ if (!files.some((f) => nested(f, "sandbox", "network", "allowLocalBinding") === true)) missing.push("sandbox.network.allowLocalBinding");
135
+ return missing;
136
+ }
137
+ /**
138
+ * The key that stops a merge, or null when the settings can take the
139
+ * allowance. A value of an unexpected shape is someone else's, and rewriting
140
+ * it would lose whatever it means.
141
+ */
142
+ function unmergeableKey(settings) {
143
+ if (!("sandbox" in settings)) return null;
144
+ if (!isPlainObject(settings.sandbox)) return "sandbox";
145
+ const sandbox = settings.sandbox;
146
+ if ("filesystem" in sandbox && !isPlainObject(sandbox.filesystem)) return "sandbox.filesystem";
147
+ if ("network" in sandbox && !isPlainObject(sandbox.network)) return "sandbox.network";
148
+ return null;
149
+ }
150
+ /** Merge the allowance into a settings object, leaving everything else as it is. */
151
+ function withAllowance(settings, allowance) {
152
+ const next = { ...settings };
153
+ const sandbox = isPlainObject(next.sandbox) ? { ...next.sandbox } : {};
154
+ const filesystem = isPlainObject(sandbox.filesystem) ? { ...sandbox.filesystem } : {};
155
+ const network = isPlainObject(sandbox.network) ? { ...sandbox.network } : {};
156
+ const writes = Array.isArray(filesystem.allowWrite) ? filesystem.allowWrite.map(String) : [];
157
+ const lookups = Array.isArray(network.allowMachLookup) ? network.allowMachLookup.map(String) : [];
158
+ filesystem.allowWrite = [.../* @__PURE__ */ new Set([...writes, ...allowance.allowWrite])];
159
+ network.allowMachLookup = [.../* @__PURE__ */ new Set([...lookups, ...allowance.allowMachLookup])];
160
+ network.allowLocalBinding = true;
161
+ sandbox.filesystem = filesystem;
162
+ sandbox.network = network;
163
+ next.sandbox = sandbox;
164
+ return next;
165
+ }
166
+ /**
167
+ * Write the allowance into a Claude Code local settings file, creating it if
168
+ * needed. Refuses whenever the current contents cannot be read back, because
169
+ * the merge would replace settings rather than extend them.
170
+ */
171
+ function applyClaudeAllowance(path, allowance) {
172
+ const read = readSettings(path);
173
+ if (read.kind === "invalid") return {
174
+ status: "refused",
175
+ reason: `${path} is not a plain JSON object, so Stim cannot merge into it without losing what is there. Comments make a settings file unparseable here even though Claude Code accepts them.`
176
+ };
177
+ const existing = read.kind === "object" ? read.value : {};
178
+ const blocker = unmergeableKey(existing);
179
+ if (blocker) return {
180
+ status: "refused",
181
+ reason: `${path} holds a "${blocker}" that is not an object, so Stim cannot merge into it without discarding that value.`
182
+ };
183
+ mkdirSync$1(dirname$1(path), { recursive: true });
184
+ const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
185
+ writeFileSync$1(tmp, `${JSON.stringify(withAllowance(existing, allowance), null, 2)}\n`);
186
+ try {
187
+ renameSync$1(tmp, path);
188
+ } catch (err) {
189
+ rmSync$1(tmp, { force: true });
190
+ throw err;
191
+ }
192
+ return { status: read.kind === "absent" ? "created" : "updated" };
193
+ }
194
+ /**
195
+ * The finding a sandboxed session should see: what fails, and the one command
196
+ * that fixes it where the harness has a per-path allowance.
197
+ */
198
+ function sandboxFinding(settingsRoot, { env = process.env, home = homedir(), stimHome = process.env.STIM_HOME || "~/.stim", blocked } = {}) {
199
+ const harness = detectHarness(env);
200
+ if (!harness) return null;
201
+ if (!(blocked ?? sandboxBlocksStimHome(stimHome, home))) return null;
202
+ const symptoms = `Writes to ${stimHome} raise EPERM on a directory you can write, the simulator service looks dead, and the adb server looks unreachable.`;
203
+ const title = "This session sandboxes shell commands, and Stim is not allowed through";
204
+ if (harness === "codex") return {
205
+ level: "cost",
206
+ title,
207
+ detail: `${symptoms} Codex has one sandbox setting and no per-path allowance, so there is nothing to add.`,
208
+ fix: "Run Stim with the sandbox off, or start Codex with `codex -s danger-full-access`, which turns the whole sandbox off rather than allowing these three. See `stim guide errors sandbox`."
209
+ };
210
+ const paths = allowanceSearchPaths(settingsRoot, home);
211
+ const missing = missingAllowance(paths, stimHome, home);
212
+ if (missing.length === 0) return {
213
+ level: "cost",
214
+ title: "Stim is allowed through this sandbox, but the session has not picked it up",
215
+ detail: `${symptoms} ${paths[0]} already grants all three, so the settings were read before they were written.`,
216
+ fix: "Restart the session for the allowance to take effect."
217
+ };
218
+ return {
219
+ level: "cost",
220
+ title,
221
+ detail: `${symptoms} Missing: ${missing.join(", ")}.`,
222
+ fix: `Run \`stim doctor --fix\` to write sandbox.filesystem.allowWrite, sandbox.network.allowMachLookup, and sandbox.network.allowLocalBinding into ${paths[0]}, merged with what is there, or add the missing keys by hand. See \`stim guide errors sandbox\`.`
223
+ };
224
+ }
225
+ //#endregion
19
226
  //#region src/doctor-storage.ts
20
227
  function checkStorageLayout(projectRoot, { platform, device = filesystemDevice, stagingRoot = temporaryRoot } = {}) {
21
228
  const findings = [];
@@ -459,105 +666,6 @@ function mainCheckoutProjectRoot(projectRoot) {
459
666
  return projectRoot;
460
667
  }
461
668
  }
462
- function installedNpmTreeIsValid(projectRoot) {
463
- try {
464
- getExecutor().runFile("npm", [
465
- "ls",
466
- "--all",
467
- "--json",
468
- "--silent"
469
- ], {
470
- cwd: projectRoot,
471
- timeoutMs: 3e4
472
- });
473
- return true;
474
- } catch {
475
- return false;
476
- }
477
- }
478
- const DEPENDENCY_STATES = [
479
- {
480
- lock: "pnpm-lock.yaml",
481
- installed: ["node_modules"],
482
- command: "pnpm install"
483
- },
484
- {
485
- lock: "yarn.lock",
486
- installed: [
487
- "node_modules",
488
- ".pnp.cjs",
489
- ".pnp.js"
490
- ],
491
- command: "yarn install"
492
- },
493
- {
494
- lock: "bun.lock",
495
- installed: ["node_modules"],
496
- command: "bun install"
497
- },
498
- {
499
- lock: "bun.lockb",
500
- installed: ["node_modules"],
501
- command: "bun install"
502
- },
503
- {
504
- lock: "package-lock.json",
505
- installed: ["node_modules"],
506
- command: "npm ci"
507
- }
508
- ];
509
- function dependencyState(projectRoot) {
510
- const root = repoRoot(projectRoot) ?? projectRoot;
511
- let dir = projectRoot;
512
- while (true) {
513
- const state = DEPENDENCY_STATES.find((candidate) => existsSync$1(join$1(dir, candidate.lock)));
514
- if (state) return {
515
- ...state,
516
- root: dir
517
- };
518
- if (dir === root) return null;
519
- const parent = dirname$1(dir);
520
- if (parent === dir || relative$1(root, parent).startsWith("..")) return null;
521
- dir = parent;
522
- }
523
- }
524
- function hasInstalledDependencies(projectRoot, markers = [
525
- "node_modules",
526
- ".pnp.cjs",
527
- ".pnp.js"
528
- ]) {
529
- return markers.some((entry) => existsSync$1(join$1(projectRoot, entry)));
530
- }
531
- function locallyKnownUpstream(projectRoot) {
532
- try {
533
- const name = getExecutor().runFile("git", [
534
- "-C",
535
- projectRoot,
536
- "rev-parse",
537
- "--abbrev-ref",
538
- "--symbolic-full-name",
539
- "@{upstream}"
540
- ], { timeoutMs: 5e3 });
541
- const counts = getExecutor().runFile("git", [
542
- "-C",
543
- projectRoot,
544
- "rev-list",
545
- "--left-right",
546
- "--count",
547
- "HEAD...@{upstream}"
548
- ], { timeoutMs: 5e3 }).trim().split(/\s+/).map(Number);
549
- const ahead = counts[0] ?? NaN;
550
- const behind = counts[1] ?? NaN;
551
- if (!name || !Number.isInteger(ahead) || !Number.isInteger(behind)) return null;
552
- return {
553
- name,
554
- ahead,
555
- behind
556
- };
557
- } catch {
558
- return null;
559
- }
560
- }
561
669
  function brokenPodLinks(podsRoot) {
562
670
  try {
563
671
  return getExecutor().runFile("find", [
@@ -1126,4 +1234,318 @@ async function detectFingerprintParity(projectRoot, { createFingerprint = expoFi
1126
1234
  }
1127
1235
  }
1128
1236
  //#endregion
1129
- export { installedNpmTreeIsValid as a, repairCxxLauncherState as c, hasInstalledDependencies as i, detectFingerprintParity as n, locallyKnownUpstream as o, detectLinkedLibraryGitMetadata as r, runDoctor as s, dependencyState as t };
1237
+ //#region src/stim-installations.ts
1238
+ const VERSION_OUTPUT_LIMIT = 4096;
1239
+ function parsedVersion(version) {
1240
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version);
1241
+ if (!match) return null;
1242
+ const core = [
1243
+ match[1],
1244
+ match[2],
1245
+ match[3]
1246
+ ];
1247
+ if (core.some((part) => part.length > 1 && part.startsWith("0"))) return null;
1248
+ const prereleaseParts = match[4]?.split(".");
1249
+ if (prereleaseParts?.some((part) => !part || /^\d+$/.test(part) && part.length > 1 && part.startsWith("0"))) return null;
1250
+ const prerelease = prereleaseParts?.map((part) => /^\d+$/.test(part) ? Number(part) : part);
1251
+ return {
1252
+ core: core.map(Number),
1253
+ prerelease: prerelease ?? null
1254
+ };
1255
+ }
1256
+ function compareStimVersions(left, right) {
1257
+ const a = parsedVersion(left);
1258
+ const b = parsedVersion(right);
1259
+ if (!a || !b) return null;
1260
+ for (let index = 0; index < a.core.length; index += 1) {
1261
+ const difference = (a.core[index] ?? 0) - (b.core[index] ?? 0);
1262
+ if (difference !== 0) return difference;
1263
+ }
1264
+ if (!a.prerelease && !b.prerelease) return 0;
1265
+ if (!a.prerelease) return 1;
1266
+ if (!b.prerelease) return -1;
1267
+ for (let index = 0; index < Math.max(a.prerelease.length, b.prerelease.length); index += 1) {
1268
+ const x = a.prerelease[index];
1269
+ const y = b.prerelease[index];
1270
+ if (x === void 0) return -1;
1271
+ if (y === void 0) return 1;
1272
+ if (x === y) continue;
1273
+ if (typeof x === "number" && typeof y === "number") return x - y;
1274
+ if (typeof x === "number") return -1;
1275
+ if (typeof y === "number") return 1;
1276
+ return x < y ? -1 : 1;
1277
+ }
1278
+ return 0;
1279
+ }
1280
+ function parseStimVersionOutput(output) {
1281
+ const version = output?.split("\n")[0]?.trim() ?? "";
1282
+ return parsedVersion(version) ? version.replace(/^v/, "") : null;
1283
+ }
1284
+ function executableNames(platform) {
1285
+ return platform === "win32" ? [
1286
+ "stim.cmd",
1287
+ "stim.exe",
1288
+ "stim"
1289
+ ] : ["stim"];
1290
+ }
1291
+ function stimExecutablePaths(pathValue, { platform = process.platform, cwd = process.cwd() } = {}) {
1292
+ const found = [];
1293
+ const seen = /* @__PURE__ */ new Set();
1294
+ for (const entry of pathValue.split(delimiter)) {
1295
+ const directory = resolve(cwd, entry || ".");
1296
+ for (const name of executableNames(platform)) {
1297
+ const path = join(directory, name);
1298
+ try {
1299
+ accessSync(path, platform === "win32" ? constants.F_OK : constants.X_OK);
1300
+ if (!statSync(path).isFile()) continue;
1301
+ const realPath = realpathSync(path);
1302
+ if (seen.has(realPath)) continue;
1303
+ seen.add(realPath);
1304
+ found.push({
1305
+ path,
1306
+ realPath
1307
+ });
1308
+ } catch {}
1309
+ }
1310
+ }
1311
+ return found;
1312
+ }
1313
+ function canonicalPath(path) {
1314
+ if (!path) return null;
1315
+ try {
1316
+ return realpathSync(resolve(path));
1317
+ } catch {
1318
+ return resolve(path);
1319
+ }
1320
+ }
1321
+ function analyzeStimVersions(runningVersion, runningPath, installations) {
1322
+ const candidates = [runningVersion, ...installations.map((entry) => entry.version)];
1323
+ const versions = [...new Set(candidates)].filter((version) => version !== null && compareStimVersions(version, version) !== null).toSorted((left, right) => -(compareStimVersions(left, right) ?? 0));
1324
+ const highestVersion = versions[0] ?? null;
1325
+ const resolved = installations[0] ?? null;
1326
+ return {
1327
+ runningVersion,
1328
+ runningPath,
1329
+ resolved,
1330
+ installations,
1331
+ versions,
1332
+ highestVersion,
1333
+ resolvedIsOlder: Boolean(resolved?.version && highestVersion && (compareStimVersions(resolved.version, highestVersion) ?? 0) < 0)
1334
+ };
1335
+ }
1336
+ function probeStimVersion(path, timeoutMs) {
1337
+ return new Promise((resolveProbe) => {
1338
+ let settled = false;
1339
+ let output = "";
1340
+ const finish = (version) => {
1341
+ if (settled) return;
1342
+ settled = true;
1343
+ clearTimeout(timer);
1344
+ resolveProbe(version);
1345
+ };
1346
+ let child;
1347
+ try {
1348
+ child = getExecutor().spawn(path, ["--version"], {
1349
+ stdio: [
1350
+ "ignore",
1351
+ "pipe",
1352
+ "ignore"
1353
+ ],
1354
+ detached: process.platform !== "win32"
1355
+ });
1356
+ } catch {
1357
+ resolveProbe(null);
1358
+ return;
1359
+ }
1360
+ const timer = setTimeout(() => {
1361
+ try {
1362
+ if (process.platform !== "win32" && child.pid) process.kill(-child.pid, "SIGKILL");
1363
+ else child.kill("SIGKILL");
1364
+ } catch {}
1365
+ finish(null);
1366
+ }, timeoutMs);
1367
+ child.stdout?.on("data", (chunk) => {
1368
+ if (output.length < VERSION_OUTPUT_LIMIT) output += String(chunk).slice(0, VERSION_OUTPUT_LIMIT - output.length);
1369
+ });
1370
+ child.on("error", () => finish(null));
1371
+ child.on("close", (code) => finish(code === 0 ? parseStimVersionOutput(output) : null));
1372
+ });
1373
+ }
1374
+ async function inspectStimVersions(runningVersion, { pathValue = process.env.PATH ?? "", runningPath = process.argv[1], probeTimeoutMs = 5e3 } = {}) {
1375
+ const paths = stimExecutablePaths(pathValue);
1376
+ const versions = await Promise.all(paths.map((entry) => probeStimVersion(entry.path, probeTimeoutMs)));
1377
+ const installations = paths.map((entry, index) => ({
1378
+ path: entry.path,
1379
+ realPath: entry.realPath,
1380
+ version: versions[index] ?? null
1381
+ }));
1382
+ return analyzeStimVersions(runningVersion, canonicalPath(runningPath), installations);
1383
+ }
1384
+ //#endregion
1385
+ //#region src/commands/doctor.ts
1386
+ function parseDoctorPlatform(value) {
1387
+ if (value === "ios" || value === "android") return value;
1388
+ throw new InvalidArgumentError("expected one of: ios, android");
1389
+ }
1390
+ function doctorTarget(platform) {
1391
+ if (platform === "ios") return "iOS";
1392
+ if (platform === "android") return "Android";
1393
+ return "all platforms";
1394
+ }
1395
+ function stimVersionLines(report) {
1396
+ const resolved = report.resolved ? `${report.resolved.version ?? "unknown version"} at ${report.resolved.path}` : "not found on PATH";
1397
+ const paths = `${report.installations.length} distinct PATH install${report.installations.length === 1 ? "" : "s"}`;
1398
+ const pathVersions = [...new Set(report.installations.map((entry) => entry.version).filter(Boolean))];
1399
+ const versions = pathVersions.length > 1 ? ` (${pathVersions.join(", ")})` : "";
1400
+ return [
1401
+ phaseLine("version", report.runningVersion),
1402
+ phaseLine("resolved", resolved),
1403
+ phaseLine("installs", paths + versions)
1404
+ ];
1405
+ }
1406
+ function doctorSuccessLines(platform, stim) {
1407
+ const lines = [
1408
+ `Doctor (${doctorTarget(platform)})`,
1409
+ phaseLine("result", "PASS"),
1410
+ phaseLine("findings", "0"),
1411
+ ...stimVersionLines(stim),
1412
+ "",
1413
+ "Project",
1414
+ phaseLine("project", "source checkout, dependencies, local upstream"),
1415
+ phaseLine("settings", "every Stim setting type, machine config paths, companions and inert keys")
1416
+ ];
1417
+ if (platform !== "android") {
1418
+ lines.push("", "iOS");
1419
+ lines.push(phaseLine("setup", "CocoaPods, warm state, effective Debug simulator architectures"));
1420
+ lines.push(phaseLine("caches", "Metro, Xcode compilation, ccache, build provider"));
1421
+ lines.push(phaseLine("devices", "remote device, SimSlim profile"));
1422
+ }
1423
+ if (platform !== "ios") {
1424
+ lines.push("", "Android");
1425
+ lines.push(phaseLine("setup", "warm state"));
1426
+ lines.push(phaseLine("caches", "Metro, Gradle, ccache, build provider"));
1427
+ lines.push(phaseLine("devices", "remote device"));
1428
+ }
1429
+ lines.push("", "Shared");
1430
+ lines.push(phaseLine("services", "EAS session"));
1431
+ lines.push(phaseLine("storage", "temporary staging and build-cache volume placement"));
1432
+ lines.push(phaseLine("fingerprint", "parity when dependencies are absent"));
1433
+ lines.push("", "Handled automatically");
1434
+ const suppliedCaches = platform === "ios" ? "Metro transform store, Xcode compilation cache" : platform === "android" ? "Metro transform store, Gradle build cache, ccache" : "Metro transform store, Xcode compilation cache, Gradle build cache, ccache";
1435
+ lines.push(phaseLine("caches", suppliedCaches));
1436
+ lines.push(phaseLine("meaning", "missing project cache settings are healthy"));
1437
+ return lines;
1438
+ }
1439
+ function shadowedStimFinding(report) {
1440
+ if (!report.resolvedIsOlder || !report.resolved?.version || !report.highestVersion) return null;
1441
+ const newer = [...(compareStimVersions(report.runningVersion, report.resolved.version) ?? 0) > 0 ? [{
1442
+ path: report.runningPath,
1443
+ version: report.runningVersion
1444
+ }] : [], ...report.installations.filter((entry) => entry.version && (compareStimVersions(entry.version, report.resolved?.version ?? "") ?? 0) > 0)].find((entry) => entry.version === report.highestVersion);
1445
+ const newerLocation = newer?.path ? ` at ${newer.path}` : "";
1446
+ return {
1447
+ level: "cost",
1448
+ title: "The Stim resolved from PATH is older than another installation",
1449
+ detail: `${report.resolved.path} reports ${report.resolved.version}, while ${report.highestVersion} is also installed${newerLocation}. A shell command named stim uses the first executable on PATH, so newer commands and fixes can appear to be missing.`,
1450
+ fix: `Update or remove ${report.resolved.path}, or put the ${report.highestVersion} installation earlier on PATH. Then run \`stim doctor\` again.`
1451
+ };
1452
+ }
1453
+ /**
1454
+ * Status goes to stderr so `--json --fix` still prints one parseable payload
1455
+ * on stdout, and the report that follows shows what the write left.
1456
+ */
1457
+ function applySandboxFix(root, env = process.env) {
1458
+ const harness = detectHarness(env);
1459
+ if (harness === "codex") {
1460
+ console.error(chalk.yellow("Nothing to apply under Codex."));
1461
+ console.error(chalk.dim("Its sandbox is one setting, `sandbox_mode`, with no per-path allowance: the only value that clears Stim is `danger-full-access`, which turns the sandbox off rather than allowing these three. Run Stim with the sandbox off instead, or set it yourself."));
1462
+ process.exitCode = 1;
1463
+ return;
1464
+ }
1465
+ if (harness !== "claude-code") {
1466
+ console.error(chalk.dim("No sandboxing harness detected, so there is nothing to apply."));
1467
+ return;
1468
+ }
1469
+ const settingsRoot = repoRoot(root) ?? root;
1470
+ const stimHome = env.STIM_HOME || "~/.stim";
1471
+ const target = claudeLocalSettingsPath(settingsRoot);
1472
+ const missing = missingAllowance(allowanceSearchPaths(settingsRoot), stimHome);
1473
+ if (missing.length === 0) {
1474
+ console.error(chalk.green("Stim is already allowed through this sandbox. Nothing to apply."));
1475
+ return;
1476
+ }
1477
+ const result = applyClaudeAllowance(target, sandboxAllowance(stimHome));
1478
+ if (result.status === "refused") {
1479
+ console.error(chalk.red(result.reason));
1480
+ console.error(chalk.dim(`Nothing was written. Add ${missing.join(", ")} by hand.`));
1481
+ process.exitCode = 1;
1482
+ return;
1483
+ }
1484
+ console.error(chalk.green(`${result.status === "created" ? "Wrote" : "Updated"} ${target}`));
1485
+ 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."));
1486
+ }
1487
+ function doctorCommand(program, version, inspectVersions = inspectStimVersions) {
1488
+ program.command("doctor").description("Inspect the source checkout and report project state that can make native worktrees slow or invalid. Read-only unless --fix is passed; --platform filters native findings.").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) => {
1489
+ const root = findProjectRoot(process.cwd());
1490
+ if (!root) {
1491
+ console.error(chalk.red("Not in a React Native project (no package.json found)."));
1492
+ process.exitCode = 1;
1493
+ return;
1494
+ }
1495
+ if (opts.fix) {
1496
+ if (sandboxFinding(repoRoot(root) ?? root)) applySandboxFix(root);
1497
+ if (opts.platform !== "ios") try {
1498
+ const repair = repairCxxLauncherState(root);
1499
+ for (const path of repair.removed) console.error(phaseLine("cache", `removed ${path}; next build reconfigures`));
1500
+ for (const { path, reason } of repair.refused) console.error(phaseLine("cache", `kept ${path}: ${reason}`));
1501
+ if (repair.refused.length > 0) process.exitCode = 1;
1502
+ } catch (error) {
1503
+ console.error(phaseLine("cache", `CMake cleanup failed: ${error.message}`));
1504
+ process.exitCode = 1;
1505
+ }
1506
+ }
1507
+ const stim = await inspectVersions(version);
1508
+ const findings = runDoctor(root, {
1509
+ xcodeMajor: opts.platform === "android" ? null : detectXcodeMajor(),
1510
+ platform: opts.platform
1511
+ });
1512
+ const parity = await detectFingerprintParity(root, { platform: opts.platform });
1513
+ if (parity) findings.push(parity);
1514
+ const linkedGit = await detectLinkedLibraryGitMetadata(root, { platform: opts.platform });
1515
+ if (linkedGit) findings.push(linkedGit);
1516
+ if (detectHarness()) {
1517
+ const sandbox = sandboxFinding(repoRoot(root) ?? root);
1518
+ if (sandbox) findings.push(sandbox);
1519
+ }
1520
+ const shadowed = shadowedStimFinding(stim);
1521
+ if (shadowed) findings.push(shadowed);
1522
+ if (opts.json) {
1523
+ console.log(JSON.stringify({
1524
+ project: root,
1525
+ platform: opts.platform ?? null,
1526
+ stim,
1527
+ findings
1528
+ }));
1529
+ return;
1530
+ }
1531
+ if (findings.length === 0) {
1532
+ const lines = doctorSuccessLines(opts.platform, stim);
1533
+ for (const [index, line] of lines.entries()) if (index === 1) console.log(chalk.green(line));
1534
+ else if (line && !line.startsWith(" ")) console.log(chalk.bold(line));
1535
+ else console.log(chalk.dim(line));
1536
+ return;
1537
+ }
1538
+ const ordered = findings.toSorted((a, b) => a.level === b.level ? 0 : a.level === "cost" ? -1 : 1);
1539
+ console.log(chalk.bold(`Doctor (${doctorTarget(opts.platform)})`));
1540
+ for (const line of stimVersionLines(stim)) console.log(chalk.dim(line));
1541
+ for (const f of ordered) {
1542
+ const tag = f.level === "cost" ? chalk.yellow("costs time") : chalk.dim("note");
1543
+ console.log(`\n${tag} ${chalk.bold(f.title)}`);
1544
+ console.log(` ${f.detail}`);
1545
+ if (f.fix) console.log(chalk.dim(` -> ${f.fix}`));
1546
+ }
1547
+ console.log(chalk.dim(`\n${findings.length} finding(s). Fix relevant "costs time" findings before copying the source checkout into a native worktree.`));
1548
+ });
1549
+ }
1550
+ //#endregion
1551
+ export { applySandboxFix, doctorCommand as default, doctorSuccessLines, parseDoctorPlatform, shadowedStimFinding };