stim 1.3.1 → 1.5.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 (67) hide show
  1. package/README.md +16 -3
  2. package/dist/{android-hMj83VpN.mjs → android-DJWiTBkz.mjs} +65 -72
  3. package/dist/{android-cas-DXorPCi_.mjs → android-cas-CSS8gjR7.mjs} +15 -12
  4. package/dist/{android-CT-PkXUh.mjs → android-sobvO9ia.mjs} +197 -114
  5. package/dist/{app-install-DIp9riNx.mjs → app-install--PAocWvq.mjs} +2 -2
  6. package/dist/{build-slots-C4AouyNz.mjs → build-slots-CuPszJDu.mjs} +7 -8
  7. package/dist/{cache-manifest-CXR-Y8pH.mjs → cache-manifest-uoOKhgUD.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-DMP7L7H6.mjs} +52 -52
  13. package/dist/dependency-state-CTL8A9p3.mjs +76 -0
  14. package/dist/{deps-0Yt0Inly.mjs → deps-BAGOnrc7.mjs} +1 -1
  15. package/dist/{dev-client-VfSZNLT4.mjs → dev-client-BoifdqkU.mjs} +12 -12
  16. package/dist/{device-Bbv45Z9Z.mjs → device-BUw4JBuH.mjs} +7 -7
  17. package/dist/{xcode-xnxyvJkD.mjs → device-DvKJ454A.mjs} +208 -967
  18. package/dist/{device-lease-CJwK7OZr.mjs → device-lease-r7yEQ1R8.mjs} +3 -3
  19. package/dist/{device-pool-CcBNTvM7.mjs → device-pool-DU4FQKp5.mjs} +4 -4
  20. package/dist/{device-remote-DAA0Zm6Z.mjs → device-remote-BTUqbl0i.mjs} +7 -6
  21. package/dist/{doctor-B4EITgQB.mjs → doctor-Di_B26uh.mjs} +396 -117
  22. package/dist/{error-diagnostics-DwhNBFDU.mjs → error-diagnostics-CTePWAzW.mjs} +7 -6
  23. package/dist/{gc-BRu62v4o.mjs → gc-C20dWw_8.mjs} +61 -27
  24. package/dist/{guide-D95E0u3i.mjs → guide-sAnoty2y.mjs} +151 -20
  25. package/dist/guide-status-BF6RsJJp.mjs +271 -0
  26. package/dist/{ios-DAju3TVy.mjs → ios-BdpkncL6.mjs} +2667 -2607
  27. package/dist/{ios-device-CWOM_S44.mjs → ios-device-DJOi9FR5.mjs} +1 -1
  28. package/dist/{logs-CAdwW6c2.mjs → logs-BfQdIhiF.mjs} +5 -5
  29. package/dist/metro-ato6ef3J.mjs +99 -0
  30. package/dist/named-ports-DH9SXd0f.mjs +137 -0
  31. package/dist/{ownership-8O073GIC.mjs → ownership-CD_XnpDu.mjs} +5 -5
  32. package/dist/{ownership-DXcol4oU.mjs → ownership-CVY9cqCr.mjs} +6 -6
  33. package/dist/ports-A6uTJSPj.mjs +39 -0
  34. package/dist/ports-BZprTJRa.mjs +90 -0
  35. package/dist/{project-fKmuIaMI.mjs → project-B_SDt4ip.mjs} +2 -2
  36. package/dist/{reclaim-CUmwucWG.mjs → reclaim-C_rB25f4.mjs} +20 -13
  37. package/dist/{reload-CWpXoOgN.mjs → reload-CYBJbJfI.mjs} +8 -8
  38. package/dist/rolldown-runtime-BsOwBuO_.mjs +30 -0
  39. package/dist/{server-bare-DEzPZLvf.mjs → server-bare-H3pAhbMs.mjs} +11 -35
  40. package/dist/server-expo-CSwUVYOa.mjs +2 -0
  41. package/dist/{server-expo-DizT4OMk.mjs → server-expo-Ds8_mfng.mjs} +6 -14
  42. package/dist/{settings-DEJf6_kJ.mjs → settings-kO2ZqIKy.mjs} +2 -2
  43. package/dist/{slot-launch-CZklomj6.mjs → slot-launch-BpHiHH6A.mjs} +4 -4
  44. package/dist/{start-B5G4NjiA.mjs → start-DiEfqEWk.mjs} +20 -22
  45. package/dist/state-CnWzvCWP.mjs +131 -0
  46. package/dist/{stats-CaaQr4yN.mjs → stats-DHrO0frH.mjs} +4 -4
  47. package/dist/{stats-BR-kwj0B.mjs → stats-Fo9ACIv7.mjs} +1 -1
  48. package/dist/{status-CI9clRSb.mjs → status-J5LAXpag.mjs} +15 -12
  49. package/dist/{stop-CjlgDk0a.mjs → stop-0jJNahwN.mjs} +15 -14
  50. package/dist/{stop-CQhffpmX.mjs → stop-CM1fejKW.mjs} +2 -2
  51. package/dist/supervisor-run.d.mts +7 -28
  52. package/dist/supervisor-run.mjs +20 -11
  53. package/dist/{teardown--gESs9ck.mjs → teardown-CAVcWeRD.mjs} +148 -109
  54. package/dist/{workspace-process-lock-IBj1Towr.mjs → workspace-process-lock-Bz19_R9R.mjs} +4 -4
  55. package/dist/workspace-state-WVwWw_hB.mjs +146 -0
  56. package/dist/{worktree-DtQGO9-s.mjs → worktree-CeUKDAQw.mjs} +38 -34
  57. package/dist/{worktree-Eq6TLH8N.mjs → worktree-DJnBbQZ9.mjs} +79 -22
  58. package/dist/xcode-BqDeRkpT.mjs +860 -0
  59. package/package.json +7 -7
  60. package/shim/expo-metro-config.cjs +11 -17
  61. package/dist/doctor-BueDfLYG.mjs +0 -530
  62. package/dist/metro-CORqXyTL.mjs +0 -184
  63. package/dist/ownership-claim-BtPtJVmq.mjs +0 -584
  64. package/dist/server-expo-44em5Pmg.mjs +0 -2
  65. package/dist/state-B3XA2wp2.mjs +0 -268
  66. package/shim/metro-cache-generation.cjs +0 -20
  67. package/shim/metro-cache-generation.d.cts +0 -5
@@ -1,6 +1,6 @@
1
- import { I as deviceSlotKey, R as parseDeviceSlotKey, V as validateDeviceSlot, k as withDirLock, u as getConfigDir } from "./config-CcefMY2Q.mjs";
2
- import { _ as writeWorkspaceState, m as withWorkspaceStateLock, o as clearWorkspaceStateKeys, p as readWorkspaceState } from "./state-B3XA2wp2.mjs";
3
- import { H as segment } from "./android-hMj83VpN.mjs";
1
+ import { D as withDirLock, I as parseDeviceSlotKey, P as deviceSlotKey, u as getConfigDir, z as validateDeviceSlot } from "./config-DMP7L7H6.mjs";
2
+ import { a as withWorkspaceStateLock, n as clearWorkspaceStateKeys, o as writeWorkspaceState, r as readWorkspaceState } from "./workspace-state-WVwWw_hB.mjs";
3
+ import { H as segment } from "./android-DJWiTBkz.mjs";
4
4
  import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { randomBytes } from "node:crypto";
6
6
  import { join } from "node:path";
@@ -1,7 +1,7 @@
1
- import { I as deviceSlotKey } from "./config-CcefMY2Q.mjs";
2
- import { a as formatElapsed, r as clockTime } from "./command-output-8wHMEde0.mjs";
3
- import { A as APP_READINESS_TIMEOUT_MS, l as VERIFY_TIMEOUT_MS } from "./app-install-DIp9riNx.mjs";
4
- import { a as parseLease, c as releaseRunLease, d as selectPoolDevice, f as takeLease, n as fileLeaseIo, r as leaseIsExpired, s as raiseLease, t as deviceLeasePath } from "./device-lease-CJwK7OZr.mjs";
1
+ import { P as deviceSlotKey } from "./config-DMP7L7H6.mjs";
2
+ import { a as formatElapsed, r as clockTime } from "./command-output-BuIb_ZOI.mjs";
3
+ import { A as APP_READINESS_TIMEOUT_MS, l as VERIFY_TIMEOUT_MS } from "./app-install--PAocWvq.mjs";
4
+ import { a as parseLease, c as releaseRunLease, d as selectPoolDevice, f as takeLease, n as fileLeaseIo, r as leaseIsExpired, s as raiseLease, t as deviceLeasePath } from "./device-lease-r7yEQ1R8.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,10 +1,11 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { k as withDirLock, u as getConfigDir } from "./config-CcefMY2Q.mjs";
3
- import { D as workspaceLogsDir, O as workspaceName, T as workspaceDir, a as clearRemoteSession, c as readMetroTunnel, k as workspaceStateFile, u as readRemoteSession } from "./state-B3XA2wp2.mjs";
4
- import { r as pidExists } from "./metro-CORqXyTL.mjs";
5
- import { z as planMetroReach } from "./settings-DEJf6_kJ.mjs";
6
- import { t as withWorkspaceProcessLock } from "./workspace-process-lock-IBj1Towr.mjs";
7
- import { T as readMetroRecords, a as LAUNCH_ERROR, b as isBundleProof, p as devClientDeepLink, r as INSTALL_ERROR } from "./app-install-DIp9riNx.mjs";
2
+ import { D as withDirLock, u as getConfigDir } from "./config-DMP7L7H6.mjs";
3
+ import { _ as workspaceLogsDir, h as workspaceDir, v as workspaceName, y as workspaceStateFile } from "./workspace-state-WVwWw_hB.mjs";
4
+ import { a as clearRemoteSession, l as readRemoteSession, s as readMetroTunnel } from "./state-CnWzvCWP.mjs";
5
+ import { r as pidExists } from "./metro-ato6ef3J.mjs";
6
+ import { z as planMetroReach } from "./settings-kO2ZqIKy.mjs";
7
+ import { t as withWorkspaceProcessLock } from "./workspace-process-lock-Bz19_R9R.mjs";
8
+ import { T as readMetroRecords, a as LAUNCH_ERROR, b as isBundleProof, p as devClientDeepLink, r as INSTALL_ERROR } from "./app-install--PAocWvq.mjs";
8
9
  import { S as sanitizeDeviceLabel } from "./ios-Deh5js5V.mjs";
9
10
  import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
10
11
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
@@ -1,21 +1,229 @@
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-DMP7L7H6.mjs";
3
+ import { l as sharedBuildCache, m as workspaceDerivedData } from "./workspace-state-WVwWw_hB.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-DJnBbQZ9.mjs";
5
+ import { a as findProjectRoot, i as detectIsExpo, t as appProjectProblem } from "./project-B_SDt4ip.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-DJWiTBkz.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-kO2ZqIKy.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-CAVcWeRD.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";
11
+ import { n as listBuildSlots } from "./build-slots-CuPszJDu.mjs";
12
+ import { a as liveOwnedDeviceCount, d as simslimIsOnPath } from "./device-DvKJ454A.mjs";
13
+ import { d as projectCmakeLauncher, i as apkOutputsDir, r as resolveAndroidCompilerCache, t as readAndroidCasToolchain } from "./android-cas-CSS8gjR7.mjs";
14
+ import { h as podInstallCommand, i as ccacheEnabled, o as detectXcodeMajor } from "./xcode-BqDeRkpT.mjs";
15
+ import { i as inspectStimVersions, n as recordDoctorRun, r as compareStimVersions } from "./guide-status-BF6RsJJp.mjs";
16
+ import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-CTL8A9p3.mjs";
14
17
  import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, rmSync } from "node:fs";
18
+ import { InvalidArgumentError } from "commander";
15
19
  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";
20
+ 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";
21
+ import { homedir } from "os";
17
22
  import { dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1, resolve as resolve$1 } from "path";
18
23
  import * as expoFingerprint from "@expo/fingerprint";
24
+ import chalk from "chalk";
25
+ //#region src/sandbox.ts
26
+ /**
27
+ * What a sandboxed harness has to permit for Stim to work: its state
28
+ * directory, the XPC service simctl talks to, and the port adb's server binds.
29
+ */
30
+ function sandboxAllowance(stimHome = "~/.stim") {
31
+ return {
32
+ allowWrite: [stimHome],
33
+ allowMachLookup: ["com.apple.coresimulator.*"],
34
+ allowLocalBinding: true
35
+ };
36
+ }
37
+ function detectHarness(env = process.env) {
38
+ if (env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT) return "claude-code";
39
+ if (Object.keys(env).some((k) => k.startsWith("CODEX_"))) return "codex";
40
+ return null;
41
+ }
42
+ function expandHome(value, home) {
43
+ const v = value.trim();
44
+ if (v === "~" || v === "$HOME") return home;
45
+ if (v.startsWith("~/")) return join$1(home, v.slice(2));
46
+ if (v.startsWith("$HOME/")) return join$1(home, v.slice(6));
47
+ return v;
48
+ }
49
+ /**
50
+ * Whether the sandbox actually stops Stim, rather than whether a harness that
51
+ * can sandbox is present: a session with its sandbox off exports the same
52
+ * variables as one with it on, so only a write says which this is.
53
+ */
54
+ function sandboxBlocksStimHome(stimHome, home = homedir()) {
55
+ const dir = expandHome(stimHome, home);
56
+ const probe = join$1(dir, `.probe-${process.pid}-${Math.random().toString(36).slice(2)}`);
57
+ try {
58
+ mkdirSync$1(dir, { recursive: true });
59
+ writeFileSync$1(probe, "");
60
+ rmSync$1(probe, { force: true });
61
+ return false;
62
+ } catch (err) {
63
+ const code = err.code;
64
+ return code === "EPERM" || code === "EACCES";
65
+ }
66
+ }
67
+ /** Where a per-user, gitignored Claude Code settings file belongs for this project. */
68
+ function claudeLocalSettingsPath(root) {
69
+ return join$1(root, ".claude", "settings.local.json");
70
+ }
71
+ /** Every settings file that may already carry the allowance, most local first. */
72
+ function allowanceSearchPaths(settingsRoot, home = homedir()) {
73
+ return [
74
+ claudeLocalSettingsPath(settingsRoot),
75
+ join$1(settingsRoot, ".claude", "settings.json"),
76
+ join$1(home, ".claude", "settings.json")
77
+ ];
78
+ }
79
+ function isPlainObject(value) {
80
+ return !!value && typeof value === "object" && !Array.isArray(value);
81
+ }
82
+ /**
83
+ * Absent, unreadable and present-but-not-an-object are three different states.
84
+ * Collapsing them loses the one distinction that matters before a write: a
85
+ * file whose contents did not parse still holds settings we must not destroy.
86
+ */
87
+ function readSettings(path) {
88
+ let raw;
89
+ try {
90
+ raw = readFileSync$1(path, "utf-8");
91
+ } catch (err) {
92
+ return err.code === "ENOENT" ? { kind: "absent" } : { kind: "invalid" };
93
+ }
94
+ try {
95
+ const parsed = JSON.parse(raw);
96
+ return isPlainObject(parsed) ? {
97
+ kind: "object",
98
+ value: parsed
99
+ } : { kind: "invalid" };
100
+ } catch {
101
+ return { kind: "invalid" };
102
+ }
103
+ }
104
+ function nested(source, ...keys) {
105
+ let cursor = source;
106
+ for (const key of keys) {
107
+ if (!isPlainObject(cursor)) return void 0;
108
+ cursor = cursor[key];
109
+ }
110
+ return cursor;
111
+ }
112
+ /** Whether an allowWrite entry covers the Stim home, including as a parent. */
113
+ function covers(entry, target, home) {
114
+ const e = expandHome(entry.replace(/\/+\*+$/, "").replace(/\/+$/, ""), home);
115
+ const t = expandHome(target.replace(/\/+$/, ""), home);
116
+ return e === t || t.startsWith(`${e}/`);
117
+ }
118
+ /**
119
+ * Which parts of the allowance the settings files already carry. Any file may
120
+ * supply any part, so a project that solved this in one place is not asked to
121
+ * repeat it in another.
122
+ */
123
+ function missingAllowance(paths, stimHome = "~/.stim", home = homedir()) {
124
+ const files = paths.map((p) => {
125
+ const read = readSettings(p);
126
+ return read.kind === "object" ? read.value : null;
127
+ });
128
+ const list = (...keys) => files.flatMap((f) => {
129
+ const value = nested(f, ...keys);
130
+ return Array.isArray(value) ? value.map(String) : [];
131
+ });
132
+ const missing = [];
133
+ if (!list("sandbox", "filesystem", "allowWrite").some((w) => covers(w, stimHome, home))) missing.push("sandbox.filesystem.allowWrite");
134
+ if (!list("sandbox", "network", "allowMachLookup").some((l) => l.toLowerCase().startsWith("com.apple.coresimulator"))) missing.push("sandbox.network.allowMachLookup");
135
+ if (!files.some((f) => nested(f, "sandbox", "network", "allowLocalBinding") === true)) missing.push("sandbox.network.allowLocalBinding");
136
+ return missing;
137
+ }
138
+ /**
139
+ * The key that stops a merge, or null when the settings can take the
140
+ * allowance. A value of an unexpected shape is someone else's, and rewriting
141
+ * it would lose whatever it means.
142
+ */
143
+ function unmergeableKey(settings) {
144
+ if (!("sandbox" in settings)) return null;
145
+ if (!isPlainObject(settings.sandbox)) return "sandbox";
146
+ const sandbox = settings.sandbox;
147
+ if ("filesystem" in sandbox && !isPlainObject(sandbox.filesystem)) return "sandbox.filesystem";
148
+ if ("network" in sandbox && !isPlainObject(sandbox.network)) return "sandbox.network";
149
+ return null;
150
+ }
151
+ /** Merge the allowance into a settings object, leaving everything else as it is. */
152
+ function withAllowance(settings, allowance) {
153
+ const next = { ...settings };
154
+ const sandbox = isPlainObject(next.sandbox) ? { ...next.sandbox } : {};
155
+ const filesystem = isPlainObject(sandbox.filesystem) ? { ...sandbox.filesystem } : {};
156
+ const network = isPlainObject(sandbox.network) ? { ...sandbox.network } : {};
157
+ const writes = Array.isArray(filesystem.allowWrite) ? filesystem.allowWrite.map(String) : [];
158
+ const lookups = Array.isArray(network.allowMachLookup) ? network.allowMachLookup.map(String) : [];
159
+ filesystem.allowWrite = [.../* @__PURE__ */ new Set([...writes, ...allowance.allowWrite])];
160
+ network.allowMachLookup = [.../* @__PURE__ */ new Set([...lookups, ...allowance.allowMachLookup])];
161
+ network.allowLocalBinding = true;
162
+ sandbox.filesystem = filesystem;
163
+ sandbox.network = network;
164
+ next.sandbox = sandbox;
165
+ return next;
166
+ }
167
+ /**
168
+ * Write the allowance into a Claude Code local settings file, creating it if
169
+ * needed. Refuses whenever the current contents cannot be read back, because
170
+ * the merge would replace settings rather than extend them.
171
+ */
172
+ function applyClaudeAllowance(path, allowance) {
173
+ const read = readSettings(path);
174
+ if (read.kind === "invalid") return {
175
+ status: "refused",
176
+ 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.`
177
+ };
178
+ const existing = read.kind === "object" ? read.value : {};
179
+ const blocker = unmergeableKey(existing);
180
+ if (blocker) return {
181
+ status: "refused",
182
+ reason: `${path} holds a "${blocker}" that is not an object, so Stim cannot merge into it without discarding that value.`
183
+ };
184
+ mkdirSync$1(dirname$1(path), { recursive: true });
185
+ const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
186
+ writeFileSync$1(tmp, `${JSON.stringify(withAllowance(existing, allowance), null, 2)}\n`);
187
+ try {
188
+ renameSync$1(tmp, path);
189
+ } catch (err) {
190
+ rmSync$1(tmp, { force: true });
191
+ throw err;
192
+ }
193
+ return { status: read.kind === "absent" ? "created" : "updated" };
194
+ }
195
+ /**
196
+ * The finding a sandboxed session should see: what fails, and the one command
197
+ * that fixes it where the harness has a per-path allowance.
198
+ */
199
+ function sandboxFinding(settingsRoot, { env = process.env, home = homedir(), stimHome = process.env.STIM_HOME || "~/.stim", blocked } = {}) {
200
+ const harness = detectHarness(env);
201
+ if (!harness) return null;
202
+ if (!(blocked ?? sandboxBlocksStimHome(stimHome, home))) return null;
203
+ const symptoms = `Writes to ${stimHome} raise EPERM on a directory you can write, the simulator service looks dead, and the adb server looks unreachable.`;
204
+ const title = "This session sandboxes shell commands, and Stim is not allowed through";
205
+ if (harness === "codex") return {
206
+ level: "cost",
207
+ title,
208
+ detail: `${symptoms} Codex has one sandbox setting and no per-path allowance, so there is nothing to add.`,
209
+ 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`."
210
+ };
211
+ const paths = allowanceSearchPaths(settingsRoot, home);
212
+ const missing = missingAllowance(paths, stimHome, home);
213
+ if (missing.length === 0) return {
214
+ level: "cost",
215
+ title: "Stim is allowed through this sandbox, but the session has not picked it up",
216
+ detail: `${symptoms} ${paths[0]} already grants all three, so the settings were read before they were written.`,
217
+ fix: "Restart the session for the allowance to take effect."
218
+ };
219
+ return {
220
+ level: "cost",
221
+ title,
222
+ detail: `${symptoms} Missing: ${missing.join(", ")}.`,
223
+ 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\`.`
224
+ };
225
+ }
226
+ //#endregion
19
227
  //#region src/doctor-storage.ts
20
228
  function checkStorageLayout(projectRoot, { platform, device = filesystemDevice, stagingRoot = temporaryRoot } = {}) {
21
229
  const findings = [];
@@ -31,7 +239,8 @@ function checkStorageLayout(projectRoot, { platform, device = filesystemDevice,
31
239
  };
32
240
  try {
33
241
  const target = repoRoot(projectRoot) ?? projectRoot;
34
- check("Worktree copy", [listWorktrees(target)[0]?.path ?? target, target], "Keep the source checkout and linked worktree on the same volume to share file blocks when warming.");
242
+ const source = resolveSourceCheckout(target);
243
+ 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.");
35
244
  const cache = sharedBuildCache();
36
245
  check("Cached app/APK staging", [cache, stagingRoot(join(cache, "artifact.app"))], temporaryFix);
37
246
  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.";
@@ -450,114 +659,15 @@ function mainCheckoutProjectRoot(projectRoot) {
450
659
  const currentRepoRoot = repoRoot(projectRoot);
451
660
  if (!currentRepoRoot) return projectRoot;
452
661
  try {
453
- const main = listWorktrees(currentRepoRoot)[0];
454
- if (!main) return projectRoot;
662
+ const source = resolveSourceCheckout(currentRepoRoot);
663
+ if ("refusal" in source) return projectRoot;
455
664
  const projectRel = relative$1(currentRepoRoot, realpathSync$1(projectRoot));
456
665
  if (projectRel.startsWith("..")) return projectRoot;
457
- return resolve$1(main.path, projectRel);
666
+ return resolve$1(source.path, projectRel);
458
667
  } catch {
459
668
  return projectRoot;
460
669
  }
461
670
  }
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
671
  function brokenPodLinks(podsRoot) {
562
672
  try {
563
673
  return getExecutor().runFile("find", [
@@ -584,7 +694,7 @@ function hasIosWarmOutput(root) {
584
694
  function hasLinkedWorktree(projectRoot) {
585
695
  const root = repoRoot(projectRoot);
586
696
  if (!root) return false;
587
- return listWorktrees(root).filter((entry) => !entry.prunable).length > 1;
697
+ return listWorktrees(root).filter((entry) => !entry.prunable && !entry.bare).length > 1;
588
698
  }
589
699
  function headBranch(root) {
590
700
  const ref = getExecutor().runFileQuiet("git", [
@@ -1126,4 +1236,173 @@ async function detectFingerprintParity(projectRoot, { createFingerprint = expoFi
1126
1236
  }
1127
1237
  }
1128
1238
  //#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 };
1239
+ //#region src/commands/doctor.ts
1240
+ function parseDoctorPlatform(value) {
1241
+ if (value === "ios" || value === "android") return value;
1242
+ throw new InvalidArgumentError("expected one of: ios, android");
1243
+ }
1244
+ function doctorTarget(platform) {
1245
+ if (platform === "ios") return "iOS";
1246
+ if (platform === "android") return "Android";
1247
+ return "all platforms";
1248
+ }
1249
+ function stimVersionLines(report) {
1250
+ const resolved = report.resolved ? `${report.resolved.version ?? "unknown version"} at ${report.resolved.path}` : "not found on PATH";
1251
+ const paths = `${report.installations.length} distinct PATH install${report.installations.length === 1 ? "" : "s"}`;
1252
+ const pathVersions = [...new Set(report.installations.map((entry) => entry.version).filter(Boolean))];
1253
+ const versions = pathVersions.length > 1 ? ` (${pathVersions.join(", ")})` : "";
1254
+ return [
1255
+ phaseLine("version", report.runningVersion),
1256
+ phaseLine("resolved", resolved),
1257
+ phaseLine("installs", paths + versions)
1258
+ ];
1259
+ }
1260
+ function doctorSuccessLines(platform, stim) {
1261
+ const lines = [
1262
+ `Doctor (${doctorTarget(platform)})`,
1263
+ phaseLine("result", "PASS"),
1264
+ phaseLine("findings", "0"),
1265
+ ...stimVersionLines(stim),
1266
+ "",
1267
+ "Project",
1268
+ phaseLine("project", "source checkout, dependencies, local upstream"),
1269
+ phaseLine("settings", "every Stim setting type, machine config paths, companions and inert keys")
1270
+ ];
1271
+ if (platform !== "android") {
1272
+ lines.push("", "iOS");
1273
+ lines.push(phaseLine("setup", "CocoaPods, warm state, effective Debug simulator architectures"));
1274
+ lines.push(phaseLine("caches", "Metro, Xcode compilation, ccache, build provider"));
1275
+ lines.push(phaseLine("devices", "remote device, SimSlim profile"));
1276
+ }
1277
+ if (platform !== "ios") {
1278
+ lines.push("", "Android");
1279
+ lines.push(phaseLine("setup", "warm state"));
1280
+ lines.push(phaseLine("caches", "Metro, Gradle, ccache, build provider"));
1281
+ lines.push(phaseLine("devices", "remote device"));
1282
+ }
1283
+ lines.push("", "Shared");
1284
+ lines.push(phaseLine("services", "EAS session"));
1285
+ lines.push(phaseLine("storage", "temporary staging and build-cache volume placement"));
1286
+ lines.push(phaseLine("fingerprint", "parity when dependencies are absent"));
1287
+ lines.push("", "Handled automatically");
1288
+ 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";
1289
+ lines.push(phaseLine("caches", suppliedCaches));
1290
+ lines.push(phaseLine("meaning", "missing project cache settings are healthy"));
1291
+ return lines;
1292
+ }
1293
+ function shadowedStimFinding(report) {
1294
+ if (!report.resolvedIsOlder || !report.resolved?.version || !report.highestVersion) return null;
1295
+ const newer = [...(compareStimVersions(report.runningVersion, report.resolved.version) ?? 0) > 0 ? [{
1296
+ path: report.runningPath,
1297
+ version: report.runningVersion
1298
+ }] : [], ...report.installations.filter((entry) => entry.version && (compareStimVersions(entry.version, report.resolved?.version ?? "") ?? 0) > 0)].find((entry) => entry.version === report.highestVersion);
1299
+ const newerLocation = newer?.path ? ` at ${newer.path}` : "";
1300
+ return {
1301
+ level: "cost",
1302
+ title: "The Stim resolved from PATH is older than another installation",
1303
+ 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.`,
1304
+ fix: `Update or remove ${report.resolved.path}, or put the ${report.highestVersion} installation earlier on PATH. Then run \`stim doctor\` again.`
1305
+ };
1306
+ }
1307
+ /**
1308
+ * Status goes to stderr so `--json --fix` still prints one parseable payload
1309
+ * on stdout, and the report that follows shows what the write left.
1310
+ */
1311
+ function applySandboxFix(root, env = process.env) {
1312
+ const harness = detectHarness(env);
1313
+ if (harness === "codex") {
1314
+ console.error(chalk.yellow("Nothing to apply under Codex."));
1315
+ 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."));
1316
+ process.exitCode = 1;
1317
+ return;
1318
+ }
1319
+ if (harness !== "claude-code") {
1320
+ console.error(chalk.dim("No sandboxing harness detected, so there is nothing to apply."));
1321
+ return;
1322
+ }
1323
+ const settingsRoot = repoRoot(root) ?? root;
1324
+ const stimHome = env.STIM_HOME || "~/.stim";
1325
+ const target = claudeLocalSettingsPath(settingsRoot);
1326
+ const missing = missingAllowance(allowanceSearchPaths(settingsRoot), stimHome);
1327
+ if (missing.length === 0) {
1328
+ console.error(chalk.green("Stim is already allowed through this sandbox. Nothing to apply."));
1329
+ return;
1330
+ }
1331
+ const result = applyClaudeAllowance(target, sandboxAllowance(stimHome));
1332
+ if (result.status === "refused") {
1333
+ console.error(chalk.red(result.reason));
1334
+ console.error(chalk.dim(`Nothing was written. Add ${missing.join(", ")} by hand.`));
1335
+ process.exitCode = 1;
1336
+ return;
1337
+ }
1338
+ console.error(chalk.green(`${result.status === "created" ? "Wrote" : "Updated"} ${target}`));
1339
+ 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."));
1340
+ }
1341
+ function doctorCommand(program, version, inspectVersions = inspectStimVersions) {
1342
+ 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) => {
1343
+ const root = findProjectRoot(process.cwd());
1344
+ if (!root) {
1345
+ console.error(chalk.red("Not in a React Native project (no package.json found)."));
1346
+ process.exitCode = 1;
1347
+ return;
1348
+ }
1349
+ if (opts.fix) {
1350
+ if (sandboxFinding(repoRoot(root) ?? root)) applySandboxFix(root);
1351
+ if (opts.platform !== "ios") try {
1352
+ const repair = repairCxxLauncherState(root);
1353
+ for (const path of repair.removed) console.error(phaseLine("cache", `removed ${path}; next build reconfigures`));
1354
+ for (const { path, reason } of repair.refused) console.error(phaseLine("cache", `kept ${path}: ${reason}`));
1355
+ if (repair.refused.length > 0) process.exitCode = 1;
1356
+ } catch (error) {
1357
+ console.error(phaseLine("cache", `CMake cleanup failed: ${error.message}`));
1358
+ process.exitCode = 1;
1359
+ }
1360
+ }
1361
+ const stim = await inspectVersions(version);
1362
+ const findings = runDoctor(root, {
1363
+ xcodeMajor: opts.platform === "android" ? null : detectXcodeMajor(),
1364
+ platform: opts.platform
1365
+ });
1366
+ const parity = await detectFingerprintParity(root, { platform: opts.platform });
1367
+ if (parity) findings.push(parity);
1368
+ const linkedGit = await detectLinkedLibraryGitMetadata(root, { platform: opts.platform });
1369
+ if (linkedGit) findings.push(linkedGit);
1370
+ if (detectHarness()) {
1371
+ const sandbox = sandboxFinding(repoRoot(root) ?? root);
1372
+ if (sandbox) findings.push(sandbox);
1373
+ }
1374
+ const shadowed = shadowedStimFinding(stim);
1375
+ if (shadowed) findings.push(shadowed);
1376
+ if (opts.json) {
1377
+ console.log(JSON.stringify({
1378
+ project: root,
1379
+ platform: opts.platform ?? null,
1380
+ stim,
1381
+ findings
1382
+ }));
1383
+ recordDoctorRun(root, opts.platform, version);
1384
+ return;
1385
+ }
1386
+ if (findings.length === 0) {
1387
+ const lines = doctorSuccessLines(opts.platform, stim);
1388
+ for (const [index, line] of lines.entries()) if (index === 1) console.log(chalk.green(line));
1389
+ else if (line && !line.startsWith(" ")) console.log(chalk.bold(line));
1390
+ else console.log(chalk.dim(line));
1391
+ recordDoctorRun(root, opts.platform, version);
1392
+ return;
1393
+ }
1394
+ const ordered = findings.toSorted((a, b) => a.level === b.level ? 0 : a.level === "cost" ? -1 : 1);
1395
+ console.log(chalk.bold(`Doctor (${doctorTarget(opts.platform)})`));
1396
+ for (const line of stimVersionLines(stim)) console.log(chalk.dim(line));
1397
+ for (const f of ordered) {
1398
+ const tag = f.level === "cost" ? chalk.yellow("costs time") : chalk.dim("note");
1399
+ console.log(`\n${tag} ${chalk.bold(f.title)}`);
1400
+ console.log(` ${f.detail}`);
1401
+ if (f.fix) console.log(chalk.dim(` -> ${f.fix}`));
1402
+ }
1403
+ console.log(chalk.dim(`\n${findings.length} finding(s). Fix relevant "costs time" findings before copying the source checkout into a native worktree.`));
1404
+ recordDoctorRun(root, opts.platform, version);
1405
+ });
1406
+ }
1407
+ //#endregion
1408
+ export { applySandboxFix, doctorCommand as default, doctorSuccessLines, parseDoctorPlatform, shadowedStimFinding };
@@ -1,12 +1,13 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { I as deviceSlotKey, L as deviceSlotPlatforms, f as getProject, k as withDirLock } from "./config-CcefMY2Q.mjs";
3
- import { f as readWorkspaceLaunches, p as readWorkspaceState } from "./state-B3XA2wp2.mjs";
2
+ import { D as withDirLock, F as deviceSlotPlatforms, P as deviceSlotKey, f as getProject } from "./config-DMP7L7H6.mjs";
3
+ import { r as readWorkspaceState } from "./workspace-state-WVwWw_hB.mjs";
4
+ import { d as readWorkspaceLaunches } from "./state-CnWzvCWP.mjs";
4
5
  import { n as deviceConsoleLevel } from "./ios-device-D3pzTKQE.mjs";
5
- import { i as resolveProjectMetro } from "./metro-CORqXyTL.mjs";
6
+ import { i as resolveProjectMetro } from "./metro-ato6ef3J.mjs";
6
7
  import { n as parseLogcatLine, t as androidClockOffset } from "./android-BW8i_YxU.mjs";
7
- import { _ as launchErrorPreview } from "./command-output-8wHMEde0.mjs";
8
- import { n as androidHome } from "./android-hMj83VpN.mjs";
9
- import { a as parseLease, n as fileLeaseIo, t as deviceLeasePath } from "./device-lease-CJwK7OZr.mjs";
8
+ import { _ as launchErrorPreview } from "./command-output-BuIb_ZOI.mjs";
9
+ import { n as androidHome } from "./android-DJWiTBkz.mjs";
10
+ import { a as parseLease, n as fileLeaseIo, t as deviceLeasePath } from "./device-lease-r7yEQ1R8.mjs";
10
11
  import { l as readLogRecords, t as attachExpoErrorContext } from "./logs-query-Cdp5aaCH.mjs";
11
12
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
12
13
  import { createHash, randomUUID } from "node:crypto";