stim 1.19.0 → 1.20.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 (172) hide show
  1. package/README.md +20 -3
  2. package/dist/{activity-BSw0x84R.mjs → activity-BrGh9EAI.mjs} +3 -3
  3. package/dist/{agent-device-usage-output-Bjxu_VmX.mjs → agent-device-usage-output-DVyv_hWj.mjs} +7 -7
  4. package/dist/android-0vv-OruW.mjs +6 -0
  5. package/dist/android-Brx6EKvJ.mjs +1258 -0
  6. package/dist/{android-BdQC6zs1.mjs → android-BsYNR81W.mjs} +103 -45
  7. package/dist/{android-DE5g_5gv.mjs → android-CK3fpEyZ.mjs} +2 -2
  8. package/dist/{android-cas-C87geAg-.mjs → android-cas-DCAuXryb.mjs} +9 -5
  9. package/dist/android-cas-compiler.mjs +1 -1
  10. package/dist/api-run.mjs +159 -8
  11. package/dist/{api-DIf_s86s.mjs → api-zuX3ynYN.mjs} +5 -1
  12. package/dist/api.d.mts +98 -4
  13. package/dist/api.mjs +1 -1
  14. package/dist/{app-install-CFI4ptUL.mjs → app-install-0LgaQRT7.mjs} +9 -8
  15. package/dist/artifact-CnC0mfov.mjs +807 -0
  16. package/dist/artifact-DClXJNWx.mjs +772 -0
  17. package/dist/artifact-lifecycle-DzPpBYvK.mjs +30 -0
  18. package/dist/{browser-web-CDhqQxhv.mjs → browser-web-CzBmhmgs.mjs} +19 -19
  19. package/dist/{budget-Dz-kyXLv.mjs → budget-BONq9vXY.mjs} +3101 -3062
  20. package/dist/build-BixRI6sB.mjs +462 -0
  21. package/dist/{build-facts-B87vo-Im.d.mts → build-facts-DcLv2HSd.d.mts} +1 -32
  22. package/dist/build-plan-CP0tckNU.mjs +553 -0
  23. package/dist/{cache-manifest-DiwKSyv9.mjs → cache-manifest-qA3dT2x2.mjs} +1 -1
  24. package/dist/cache-manifest.mjs +1 -1
  25. package/dist/{manifest-CX0d1IkZ.mjs → ccache-CjHQ5F8g.mjs} +13 -19
  26. package/dist/{chrome-BTUOo8p2.mjs → chrome-Dp2lMLBe.mjs} +1 -1
  27. package/dist/{cli-C9Zvgbdc.mjs → cli-B2R8wH1F.mjs} +20 -20
  28. package/dist/cli.mjs +1 -1
  29. package/dist/{client-CuUo8Nvg.mjs → client-BLy1Pw2V.mjs} +89 -307
  30. package/dist/collector-run.d.mts +4 -0
  31. package/dist/collector-run.mjs +7 -7
  32. package/dist/{command-output-Bt8ygHE7.mjs → command-output-BjPN99ZB.mjs} +2 -2
  33. package/dist/{config-DC5jEtd1.mjs → config-D0LhTY3S.mjs} +8 -12
  34. package/dist/{created-devices-D0s26Hx5.mjs → created-devices-HU_DAb_Q.mjs} +7 -6
  35. package/dist/{dependency-state-DKaS1OM9.mjs → dependency-state-B2m8eqsS.mjs} +2 -2
  36. package/dist/{detached-entry-pB_IPfV4.mjs → detached-entry-BXq4fIl-.mjs} +1 -1
  37. package/dist/{dev-client-CDY_zAmD.mjs → dev-client-ClATaJoH.mjs} +2 -2
  38. package/dist/{device-BHw0WlHl.mjs → device-B7lsC0K1.mjs} +8 -8
  39. package/dist/device-DVCwAFlX.mjs +727 -0
  40. package/dist/{device-capacity-BoN1jQkO.mjs → device-capacity-Ca_ke1LE.mjs} +13 -14
  41. package/dist/device-host-worker.mjs +38 -18
  42. package/dist/device-ios-CRUqPxKw.mjs +413 -0
  43. package/dist/{device-lease-Cu7PtQpZ.mjs → device-lease-DHOBXGgS.mjs} +135 -22
  44. package/dist/{device-lease-run-D38SnZtt.mjs → device-lease-run-CvDeHTkG.mjs} +4 -4
  45. package/dist/{device-pool-CP4rAsHx.mjs → device-pool-L2-5GxMy.mjs} +3 -3
  46. package/dist/{device-remote-B5GiUifk.mjs → device-remote-Bmg42063.mjs} +17 -68
  47. package/dist/doctor-C2MSuMMg.mjs +515 -0
  48. package/dist/{doctor-PRJH94v6.mjs → doctor-DPAqhVCK.mjs} +333 -744
  49. package/dist/{doctor-watchman-wZhyTdp4.mjs → doctor-watchman-CB4NnVZm.mjs} +13 -12
  50. package/dist/{simslim-D9LkJxIO.mjs → eas-build-Dw_ec2Km.mjs} +6 -152
  51. package/dist/{stim-installations-DbmUjsLv.mjs → eas-session-ledger-DZlC-Fq3.mjs} +57 -4
  52. package/dist/{error-diagnostics--v5HGOU0.mjs → error-diagnostics-Du7I_GuY.mjs} +17 -12
  53. package/dist/errors-DzkMYh8Q.mjs +205 -0
  54. package/dist/{exec-i8GDPwq9.mjs → exec-B81Qodjn.mjs} +40 -10
  55. package/dist/{gc-CyHMcKcn.mjs → gc-PWp34zGb.mjs} +21 -20
  56. package/dist/{device-ios-B2ruohE-.mjs → gradle-C2OEKCCI.mjs} +21 -650
  57. package/dist/{guide-dRaQJJk9.mjs → guide-Dai9lphv.mjs} +504 -117
  58. package/dist/{hosted-android-Bs7fytj7.mjs → hosted-android-DQ-Agsd8.mjs} +5 -5
  59. package/dist/{hosted-client-C5G4h_OJ.mjs → hosted-client-Io01s5By.mjs} +9 -3
  60. package/dist/{hosted-ios-Bik035eL.mjs → hosted-ios-DIxFnYp-.mjs} +3 -3
  61. package/dist/{hosted-logs-CI9Az9a4.mjs → hosted-logs-DP-ABU_M.mjs} +8 -8
  62. package/dist/{hosted-macos-DABm3ScO.mjs → hosted-macos-Aziw9T_9.mjs} +7 -6
  63. package/dist/{hosted-native-DbV2aUVw.mjs → hosted-native-BdAQi21M.mjs} +17 -14
  64. package/dist/{idle-shutdown-DYiMBn34.mjs → idle-shutdown-DdMqoSUB.mjs} +7 -7
  65. package/dist/{ios-DYUr9LGc.mjs → ios-BZ9dg2Fr.mjs} +8 -7
  66. package/dist/{ios-BWbHFZVz.mjs → ios-CXLIUxXJ.mjs} +180 -24
  67. package/dist/ios-CZuv6gRv.mjs +1112 -0
  68. package/dist/{ios-DCrkgXEM.mjs → ios-VGvNdsv2.mjs} +1 -1
  69. package/dist/{ios-device-CrW8aMb5.mjs → ios-device-B1B14xND.mjs} +1 -1
  70. package/dist/{ios-device-CJ_ZOGky.mjs → ios-device-BreZOpM0.mjs} +3 -3
  71. package/dist/ios-project-B9drt_aY.mjs +197 -0
  72. package/dist/{ios-state-By0XuLTQ.mjs → ios-state-CjrL2qv-.mjs} +1 -1
  73. package/dist/js-swap-C8icNYIY.mjs +432 -0
  74. package/dist/launch-DxH0THLP.mjs +745 -0
  75. package/dist/{react-native-ios-CSpIxexZ.mjs → launch-YrbqaIFj.mjs} +59 -612
  76. package/dist/{launch-verify-DCveSE6d.mjs → launch-verify-BzRdbuO1.mjs} +26 -7
  77. package/dist/{logs-Bao1HOMy.mjs → logs-DHaRNfg7.mjs} +11 -11
  78. package/dist/macos-C0iljhsN.mjs +2 -0
  79. package/dist/{macos-ciyBdTY5.mjs → macos-V_3Ui7g4.mjs} +42 -50
  80. package/dist/macos-run.mjs +1 -1
  81. package/dist/maintenance-run.mjs +13 -10
  82. package/dist/{metro-DI2AjPF_.mjs → metro-D_j9sN-B.mjs} +18 -5
  83. package/dist/{metro-gateway-BxpJLIwh.mjs → metro-gateway-BBJldOdi.mjs} +2 -2
  84. package/dist/miss-reason-BPAzZS0r.mjs +238 -0
  85. package/dist/{named-ports-B4iWH4E-.mjs → named-ports-C2lvoPsb.mjs} +3 -3
  86. package/dist/native-android-lHzaew7r.mjs +226 -0
  87. package/dist/native-gradle-inputs-BQnIdAlL.mjs +356 -0
  88. package/dist/{native-runtime-BGxsbfny.mjs → native-runtime-BRmWL7ru.mjs} +9 -8
  89. package/dist/native-xcode-inputs-eutS19jU.mjs +307 -0
  90. package/dist/native-xcode-ios-DP3cHLwu.mjs +434 -0
  91. package/dist/{ndjson-LGZAMsqa.mjs → ndjson-GOnbyvso.mjs} +6 -6
  92. package/dist/offload-worker.d.mts +41 -7
  93. package/dist/offload-worker.mjs +409 -74
  94. package/dist/{ownership-EB0ySdGK.mjs → ownership-DXzMMSx6.mjs} +2 -2
  95. package/dist/{placement-log-Dxe34rJm.mjs → placement-log-reFbjqEr.mjs} +1 -1
  96. package/dist/plan-BDk2AeY_.mjs +260 -0
  97. package/dist/plan-placement-D_YYlE6N.mjs +73 -0
  98. package/dist/{ports-ByPc0_97.mjs → ports-jzvAJXXL.mjs} +3 -3
  99. package/dist/{prebuild-BKJ3wJI1.mjs → prebuild-Bw44QxVh.mjs} +4 -4
  100. package/dist/{preview-BbSottmO.mjs → preview-BaBJ72Al.mjs} +50 -32
  101. package/dist/project-B8_duAzW.mjs +1067 -0
  102. package/dist/project-plan-BPwbLwQu.mjs +10 -0
  103. package/dist/{pull-request-IHNdMnG8.mjs → pull-request-vOhLNmIb.mjs} +1 -1
  104. package/dist/pull-requests.mjs +1 -1
  105. package/dist/react-native-android-chm1DMlA.mjs +921 -0
  106. package/dist/react-native-build-oAw6gQeW.mjs +239 -0
  107. package/dist/react-native-doctor-DYXoh-6I.mjs +78 -0
  108. package/dist/react-native-ios-SCTK_CT5.mjs +595 -0
  109. package/dist/{recordings-Bv8McZN4.mjs → recordings-Bk-VivUV.mjs} +1 -1
  110. package/dist/{reload-rAPdqeRR.mjs → reload-D34GkY78.mjs} +17 -13
  111. package/dist/remote-BU9UyhCJ.mjs +192 -0
  112. package/dist/{remote-cache-N3hAEaAL.mjs → remote-cache-Bsts6f6Y.mjs} +5 -5
  113. package/dist/result-BDoRk1k3.mjs +186 -0
  114. package/dist/{run-lAkle1-c.mjs → run-qvff78hv.mjs} +3 -3
  115. package/dist/{server-bare-CkIjnegq.mjs → server-bare-dWjAarki.mjs} +5 -5
  116. package/dist/server-command-7XIKlWtO.mjs +69 -0
  117. package/dist/{server-expo-n3tUi3Em.mjs → server-expo-CsYu2aKL.mjs} +107 -82
  118. package/dist/server-expo-DV43vep4.mjs +2 -0
  119. package/dist/{settings-CxgngUZk.mjs → settings-Bbztm4oV.mjs} +179 -129
  120. package/dist/{settings-2am7TSxp.mjs → settings-CT3E71-i.mjs} +6 -6
  121. package/dist/{state-lKh8shBp.d.mts → settings-vk3mJGSm.d.mts} +4 -2
  122. package/dist/settings.schema.json +84 -0
  123. package/dist/simslim-ChMfe8rg.mjs +149 -0
  124. package/dist/{slot-launch-HV_KtPIw.mjs → slot-launch-B9hn96wd.mjs} +6 -6
  125. package/dist/{stage-BgaZJVZY.mjs → stage-BNKKdDNc.mjs} +6 -3
  126. package/dist/start-CbQCSnXQ.mjs +2 -0
  127. package/dist/{start-DtxthMfI.mjs → start-CdvcTaXr.mjs} +97 -68
  128. package/dist/{state-Tp4uImDS.mjs → state-Boo0IMJq.mjs} +1 -1
  129. package/dist/{state-D2KtnALG.mjs → state-DI84A-uu.mjs} +29 -10
  130. package/dist/{stats-8EJ5ivFT.mjs → stats-BIIDrjA8.mjs} +5 -5
  131. package/dist/{status-BEeL6IKz.mjs → status-BDGHwkCs.mjs} +3 -1
  132. package/dist/{status-BOyp0CUl.mjs → status-DARMkc9q.mjs} +81 -293
  133. package/dist/status-watch-DvmI3SmN.mjs +296 -0
  134. package/dist/{stim-desktop-C65jk_q2.mjs → stim-desktop-8GQX2OV8.mjs} +1 -1
  135. package/dist/{stop-DRipAg-e.mjs → stop-CCZS-C9m.mjs} +2 -2
  136. package/dist/{stop-D0UIpZuh.mjs → stop-WQPQHLuy.mjs} +5 -5
  137. package/dist/{stop-zDlknFoz.mjs → stop-iNemHsJC.mjs} +21 -20
  138. package/dist/supervisor-run.d.mts +7 -2
  139. package/dist/supervisor-run.mjs +32 -20
  140. package/dist/{support-DsNaFg6D.mjs → support-CnE_SIjt.mjs} +17 -7
  141. package/dist/{support-CNqYkV0y.mjs → support-DCco5fut.mjs} +61 -8
  142. package/dist/{swiftpm-macos-aJzPAj1t.mjs → swiftpm-macos-afeNUExW.mjs} +5 -5
  143. package/dist/tailnet-CoJUb2tB.mjs +265 -0
  144. package/dist/toolchain-BIupIro1.mjs +2 -0
  145. package/dist/{toolchain-C3xSoyUl.mjs → toolchain-DqyDXs30.mjs} +206 -53
  146. package/dist/{trigger-CAhmG-4s.mjs → trigger-C0MWBi8O.mjs} +2 -2
  147. package/dist/trigger-SHLOWJ4O.mjs +2 -0
  148. package/dist/{warm-progress-pFLB2e6P.mjs → warm-progress-C3ASD6z5.mjs} +6 -6
  149. package/dist/{watchman-BL1IdOtq.mjs → watchman-CQl51YZu.mjs} +6 -6
  150. package/dist/{web-DbbrPUn0.mjs → web-B3eldHFH.mjs} +9 -13
  151. package/dist/web-BLXpOe6b.mjs +2 -0
  152. package/dist/web-run.mjs +10 -10
  153. package/dist/{workspace-state-CR9X86xJ.mjs → workspace-state-CSIFuuDa.mjs} +33 -33
  154. package/dist/{workspaces-Djb2gfi9.mjs → workspaces-D2xSHBDY.mjs} +1260 -1258
  155. package/dist/{worktree-BL4M1rhB.mjs → worktree-BxDqaTev.mjs} +84 -48
  156. package/dist/worktree-q7_OZNY0.mjs +2 -0
  157. package/package.json +8 -6
  158. package/shim/native-android.gradle +101 -0
  159. package/dist/android-BXc6BmEi.mjs +0 -3732
  160. package/dist/android-BvV1gGdN.mjs +0 -4
  161. package/dist/ios-ITYno9rO.mjs +0 -2241
  162. package/dist/macos-nIOATJmR.mjs +0 -2
  163. package/dist/metro-store-BAHFcJm6.mjs +0 -106
  164. package/dist/miss-reason-CjQCOKqq.mjs +0 -1743
  165. package/dist/plan-placement-BVY59qa8.mjs +0 -290
  166. package/dist/project-B5eQ1USs.mjs +0 -612
  167. package/dist/react-native-ios-BMh2i25I.mjs +0 -2
  168. package/dist/server-expo-DGsdUGX2.mjs +0 -2
  169. package/dist/start-D8NbxNBb.mjs +0 -2
  170. package/dist/trigger-CoalsMTV.mjs +0 -2
  171. package/dist/web-DFpFeZu5.mjs +0 -2
  172. package/dist/worktree-DV-A0LNL.mjs +0 -2
@@ -0,0 +1,515 @@
1
+ import { _ as loadConfig$1, p as getProject } from "./config-D0LhTY3S.mjs";
2
+ import { Et as repoRoot, I as projectSettingsContext } from "./settings-Bbztm4oV.mjs";
3
+ import { f as refuseNoProject, l as phaseLine } from "./command-output-BjPN99ZB.mjs";
4
+ import { d as projectRegistry } from "./project-B8_duAzW.mjs";
5
+ import { J as inspectWatchmanMemory, Q as memorySweepIsScoped, a as inspectBudget, n as budgetLine } from "./budget-BONq9vXY.mjs";
6
+ import { C as inspectDeviceHostMachines, b as inspectBuildMachines, d as simulatorRuntime, s as offloadCheck } from "./client-BLy1Pw2V.mjs";
7
+ import { I as memoryCulpritFindings, L as readMemoryCulprits, h as listIosRuntimesAsync, l as iosRuntimeMatches, x as pickDefaultIosCreation } from "./ios-CXLIUxXJ.mjs";
8
+ import { T as detectXcodeMajor } from "./toolchain-DqyDXs30.mjs";
9
+ import { o as compareStimVersions, s as inspectStimVersions } from "./eas-session-ledger-DZlC-Fq3.mjs";
10
+ import { h as resolveDeviceType, v as resolveRuntime } from "./support-DCco5fut.mjs";
11
+ import { r as maintenanceStatus, t as maintenanceLine } from "./status-BDGHwkCs.mjs";
12
+ import { a as writeIgnoreDirs, i as watchedCheckouts, n as inspectNestedWorktrees, r as nestedWorktreeFinding, s as recordDoctorRun } from "./doctor-watchman-CB4NnVZm.mjs";
13
+ import { i as runDoctor, o as readMachineSettings } from "./doctor-DPAqhVCK.mjs";
14
+ import { dirname, join, sep } from "path";
15
+ import { isJsonObject, maintenanceNdjsonFile, readHostedAppMetadata, readHostedSessions } from "@stim-cli/core/state";
16
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
17
+ import { homedir } from "os";
18
+ import { InvalidArgumentError } from "commander";
19
+ import chalk from "chalk";
20
+ //#region src/diagnostics/sandbox.ts
21
+ /**
22
+ * What a sandboxed harness has to permit for Stim to work: its state
23
+ * directory, the XPC service simctl talks to, and the port adb's server binds.
24
+ */
25
+ function sandboxAllowance(stimHome = "~/.stim") {
26
+ return {
27
+ allowWrite: [stimHome],
28
+ allowMachLookup: ["com.apple.coresimulator.*"],
29
+ allowLocalBinding: true
30
+ };
31
+ }
32
+ function detectHarness(env = process.env) {
33
+ if (env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT) return "claude-code";
34
+ if (Object.keys(env).some((k) => k.startsWith("CODEX_"))) return "codex";
35
+ return null;
36
+ }
37
+ function expandHome(value, home) {
38
+ const v = value.trim();
39
+ if (v === "~" || v === "$HOME") return home;
40
+ if (v.startsWith("~/")) return join(home, v.slice(2));
41
+ if (v.startsWith("$HOME/")) return join(home, v.slice(6));
42
+ return v;
43
+ }
44
+ /**
45
+ * Whether the sandbox actually stops Stim, rather than whether a harness that
46
+ * can sandbox is present: a session with its sandbox off exports the same
47
+ * variables as one with it on, so only a write says which this is.
48
+ */
49
+ function sandboxBlocksStimHome(stimHome, home = homedir()) {
50
+ const dir = expandHome(stimHome, home);
51
+ const probe = join(dir, `.probe-${process.pid}-${Math.random().toString(36).slice(2)}`);
52
+ try {
53
+ mkdirSync(dir, { recursive: true });
54
+ writeFileSync(probe, "");
55
+ rmSync(probe, { force: true });
56
+ return false;
57
+ } catch (err) {
58
+ const code = err.code;
59
+ return code === "EPERM" || code === "EACCES";
60
+ }
61
+ }
62
+ /** Where a per-user, gitignored Claude Code settings file belongs for this project. */
63
+ function claudeLocalSettingsPath(root) {
64
+ return join(root, ".claude", "settings.local.json");
65
+ }
66
+ /** Every settings file that may already carry the allowance, most local first. */
67
+ function allowanceSearchPaths(settingsRoot, home = homedir()) {
68
+ return [
69
+ claudeLocalSettingsPath(settingsRoot),
70
+ join(settingsRoot, ".claude", "settings.json"),
71
+ join(home, ".claude", "settings.json")
72
+ ];
73
+ }
74
+ function isPlainObject(value) {
75
+ return !!value && typeof value === "object" && !Array.isArray(value);
76
+ }
77
+ /**
78
+ * Absent, unreadable and present-but-not-an-object are three different states.
79
+ * Collapsing them loses the one distinction that matters before a write: a
80
+ * file whose contents did not parse still holds settings we must not destroy.
81
+ */
82
+ function readSettings(path) {
83
+ let raw;
84
+ try {
85
+ raw = readFileSync(path, "utf-8");
86
+ } catch (err) {
87
+ return err.code === "ENOENT" ? { kind: "absent" } : { kind: "invalid" };
88
+ }
89
+ try {
90
+ const parsed = JSON.parse(raw);
91
+ return isPlainObject(parsed) ? {
92
+ kind: "object",
93
+ value: parsed
94
+ } : { kind: "invalid" };
95
+ } catch {
96
+ return { kind: "invalid" };
97
+ }
98
+ }
99
+ function nested(source, ...keys) {
100
+ let cursor = source;
101
+ for (const key of keys) {
102
+ if (!isPlainObject(cursor)) return void 0;
103
+ cursor = cursor[key];
104
+ }
105
+ return cursor;
106
+ }
107
+ /** Whether an allowWrite entry covers the Stim home, including as a parent. */
108
+ function covers(entry, target, home) {
109
+ const posix = (value) => {
110
+ const expanded = expandHome(value, home);
111
+ return sep === "/" ? expanded : expanded.replaceAll(sep, "/");
112
+ };
113
+ const e = posix(entry).replace(/\/+\*+$/, "").replace(/\/+$/, "");
114
+ const t = posix(target).replace(/\/+$/, "");
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(dirname(path), { recursive: true });
184
+ const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
185
+ writeFileSync(tmp, `${JSON.stringify(withAllowance(existing, allowance), null, 2)}\n`);
186
+ try {
187
+ renameSync(tmp, path);
188
+ } catch (err) {
189
+ rmSync(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
226
+ //#region src/device-host/agent-driver.ts
227
+ function hostedAgentDriverFinding(driver, apps) {
228
+ if (apps === 0 || driver !== void 0 && driver !== "none") return null;
229
+ return {
230
+ code: "hosted-agent-driver",
231
+ level: "note",
232
+ title: `${apps === 1 ? "A hosted app runs" : `${apps} hosted apps run`} with no agent driver`,
233
+ detail: "The client's coding agent cannot drive these apps because `hosting.agentDriver` is none, so stim-server starts no driver. agent-device confines macOS control to one macos-app lease and iOS or Android control to one device through its daemon policy.",
234
+ fix: "stim settings set hosting.agentDriver agent-device, with agent-device 0.21.22 or later"
235
+ };
236
+ }
237
+ function runningHostedApps() {
238
+ try {
239
+ return readHostedSessions().filter((session) => {
240
+ if (![
241
+ "macos",
242
+ "ios",
243
+ "android"
244
+ ].includes(session.platform) || session.state !== "ready" || !session.appAttempt) return false;
245
+ try {
246
+ return readHostedAppMetadata(session.id, session.appAttempt).state === "installed";
247
+ } catch {
248
+ return false;
249
+ }
250
+ }).length;
251
+ } catch {
252
+ return 0;
253
+ }
254
+ }
255
+ /** Notes installed hosted apps on this Mac while `hosting.agentDriver` starts no driver. */
256
+ function inspectHostedAgentDriver() {
257
+ const hosting = loadConfig$1()?.hosting;
258
+ return hostedAgentDriverFinding(isJsonObject(hosting) ? hosting.agentDriver : void 0, runningHostedApps());
259
+ }
260
+ //#endregion
261
+ //#region src/commands/doctor.ts
262
+ function parseDoctorPlatform(value) {
263
+ if (value === "ios" || value === "android") return value;
264
+ throw new InvalidArgumentError("expected one of: ios, android");
265
+ }
266
+ /**
267
+ * The simulator runtime `stim ios` would build for here: the one `ios.runtime` names, else the workspace's own
268
+ * simulator's, else the one it would create a simulator on.
269
+ */
270
+ async function iosTargetRuntime(root, settings) {
271
+ if (!settings) return null;
272
+ try {
273
+ const runtimes = await listIosRuntimesAsync();
274
+ const runtime = resolveRuntime(null, settings);
275
+ if (runtime) return runtimes.find((each) => iosRuntimeMatches(each, runtime))?.identifier ?? null;
276
+ const udid = getProject(root)?.platforms?.ios?.deviceUdid;
277
+ const own = typeof udid === "string" ? simulatorRuntime(udid) : null;
278
+ if (own) return own;
279
+ const deviceType = resolveDeviceType(null, settings) ?? void 0;
280
+ return pickDefaultIosCreation([], runtimes, { deviceType })?.runtimeId ?? null;
281
+ } catch {
282
+ return null;
283
+ }
284
+ }
285
+ function withoutUnhandledRejection(started) {
286
+ started.catch(() => {});
287
+ return started;
288
+ }
289
+ function doctorTarget(platform) {
290
+ if (platform === "ios") return "iOS";
291
+ if (platform === "android") return "Android";
292
+ return "all platforms";
293
+ }
294
+ function stimVersionLines(report) {
295
+ const resolved = report.resolved ? `${report.resolved.version ?? "unknown version"} at ${report.resolved.path}` : "not found on PATH";
296
+ const paths = `${report.installations.length} distinct PATH install${report.installations.length === 1 ? "" : "s"}`;
297
+ const pathVersions = [...new Set(report.installations.map((entry) => entry.version).filter(Boolean))];
298
+ const versions = pathVersions.length > 1 ? ` (${pathVersions.join(", ")})` : "";
299
+ return [
300
+ phaseLine("version", report.runningVersion),
301
+ phaseLine("resolved", resolved),
302
+ phaseLine("installs", paths + versions)
303
+ ];
304
+ }
305
+ function budgetLines(budget) {
306
+ return budget ? [phaseLine("budget", budgetLine(budget))] : [];
307
+ }
308
+ function doctorSuccessLines(platform, stim, budget = null, projectLines = []) {
309
+ return [
310
+ `Doctor (${doctorTarget(platform)})`,
311
+ phaseLine("result", "PASS"),
312
+ phaseLine("findings", "0"),
313
+ ...stimVersionLines(stim),
314
+ ...budgetLines(budget),
315
+ "",
316
+ "Shared",
317
+ phaseLine("settings", "every Stim setting type, machine config paths, companions and inert keys"),
318
+ phaseLine("storage", "temporary staging and build-cache volume placement"),
319
+ ...projectLines
320
+ ];
321
+ }
322
+ function shadowedStimFinding(report) {
323
+ if (!report.resolvedIsOlder || !report.resolved?.version || !report.highestVersion) return null;
324
+ const newer = [...(compareStimVersions(report.runningVersion, report.resolved.version) ?? 0) > 0 ? [{
325
+ path: report.runningPath,
326
+ version: report.runningVersion
327
+ }] : [], ...report.installations.filter((entry) => entry.version && (compareStimVersions(entry.version, report.resolved?.version ?? "") ?? 0) > 0)].find((entry) => entry.version === report.highestVersion);
328
+ const newerLocation = newer?.path ? ` at ${newer.path}` : "";
329
+ return {
330
+ level: "cost",
331
+ title: "The Stim resolved from PATH is older than another installation",
332
+ 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.`,
333
+ fix: `Update or remove ${report.resolved.path}, or put the ${report.highestVersion} installation earlier on PATH. Then run \`stim doctor\` again.`
334
+ };
335
+ }
336
+ /**
337
+ * Status goes to stderr so `--json --fix` still prints one parseable payload
338
+ * on stdout, and the report that follows shows what the write left.
339
+ */
340
+ function applySandboxFix(root, env = process.env) {
341
+ const harness = detectHarness(env);
342
+ if (harness === "codex") {
343
+ console.error(chalk.yellow("Nothing to apply under Codex."));
344
+ 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."));
345
+ process.exitCode = 1;
346
+ return;
347
+ }
348
+ if (harness !== "claude-code") {
349
+ console.error(chalk.dim("No sandboxing harness detected, so there is nothing to apply."));
350
+ return;
351
+ }
352
+ const settingsRoot = repoRoot(root) ?? root;
353
+ const stimHome = env.STIM_HOME || "~/.stim";
354
+ const target = claudeLocalSettingsPath(settingsRoot);
355
+ const missing = missingAllowance(allowanceSearchPaths(settingsRoot), stimHome);
356
+ if (missing.length === 0) {
357
+ console.error(chalk.green("Stim is already allowed through this sandbox. Nothing to apply."));
358
+ return;
359
+ }
360
+ const result = applyClaudeAllowance(target, sandboxAllowance(stimHome));
361
+ if (result.status === "refused") {
362
+ console.error(chalk.red(result.reason));
363
+ console.error(chalk.dim(`Nothing was written. Add ${missing.join(", ")} by hand.`));
364
+ process.exitCode = 1;
365
+ return;
366
+ }
367
+ console.error(chalk.green(`${result.status === "created" ? "Wrote" : "Updated"} ${target}`));
368
+ 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."));
369
+ }
370
+ function doctorCommand(program, version, inspectVersions = inspectStimVersions, host = process.platform, registry = projectRegistry) {
371
+ 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 supported native 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 a React Native or Expo checkout; add linked worktrees nested in a checkout to its .watchmanconfig ignore_dirs; ask each remote Mac in remote.machines for build and device-host approval. Stop native builds first. Generated CMake output must be ignored and untracked; custom launcher settings and source files are preserved.").action(async (opts) => {
372
+ const root = registry.findProjectRoot(process.cwd());
373
+ if (!root) {
374
+ refuseNoProject({ json: Boolean(opts.json) });
375
+ return;
376
+ }
377
+ const settingsContext = projectSettingsContext(root);
378
+ const machineSettings = readMachineSettings(settingsContext);
379
+ const settings = machineSettings.corrupt ? null : machineSettings.settings;
380
+ const selected = registry.selectDoctor(root, opts.platform);
381
+ const doctors = await selected.load();
382
+ if (opts.fix) {
383
+ if (sandboxFinding(repoRoot(root) ?? root)) applySandboxFix(root);
384
+ for (const doctor of doctors) {
385
+ if (!doctor.repair) continue;
386
+ try {
387
+ const repair = doctor.repair(opts.platform, settings, settingsContext.repoRoot);
388
+ for (const path of repair.removed) console.error(phaseLine("cache", `removed ${path}; next build reconfigures`));
389
+ for (const { path, reason } of repair.refused) console.error(phaseLine("cache", `kept ${path}: ${reason}`));
390
+ if (repair.refused.length > 0) process.exitCode = 1;
391
+ } catch (error) {
392
+ console.error(phaseLine("cache", `Project repair failed: ${error.message}`));
393
+ process.exitCode = 1;
394
+ }
395
+ }
396
+ const nestedToFix = inspectNestedWorktrees(root);
397
+ const watchedToFix = await watchedCheckouts(nestedToFix.map((entry) => entry.checkout));
398
+ for (const entry of nestedToFix) {
399
+ let result;
400
+ try {
401
+ result = writeIgnoreDirs(entry);
402
+ } catch (error) {
403
+ result = {
404
+ status: "refused",
405
+ reason: `${entry.configPath} could not be written: ${error.message}`
406
+ };
407
+ }
408
+ if (result.status === "refused") {
409
+ console.error(phaseLine("checkout", `kept ${result.reason}; add ${entry.add.join(", ")} to ignore_dirs by hand`));
410
+ process.exitCode = 1;
411
+ } else {
412
+ console.error(phaseLine("checkout", `${result.status} ${entry.configPath}: ignore_dirs ${entry.add.join(", ")}`));
413
+ if (watchedToFix.has(entry.checkout)) console.error(phaseLine("checkout", `Watchman watches ${entry.checkout} and reads the file only when it adds a root: run \`watchman watch-del ${entry.checkout}\`, then restart watchman to free its memory`));
414
+ }
415
+ }
416
+ }
417
+ const stim = await inspectVersions(version);
418
+ const { findings, context } = runDoctor(root, {
419
+ xcodeMajor: selected.platforms.includes("ios") && host === "darwin" ? detectXcodeMajor() : null,
420
+ platform: opts.platform,
421
+ platforms: selected.platforms,
422
+ host,
423
+ machineSettings,
424
+ repoRoot: settingsContext.repoRoot
425
+ }, doctors.map((doctor) => doctor.inspect));
426
+ if (selected.problem) findings.unshift({
427
+ level: "cost",
428
+ ...selected.problem.kind === "not-an-app" ? { code: "not-an-app" } : {},
429
+ title: selected.problem.kind === "unreadable" ? "The project could not be read" : "This directory has no supported app for this operation",
430
+ detail: selected.problem.message,
431
+ fix: selected.problem.remedy
432
+ });
433
+ for (const doctor of doctors) if (doctor.inspectAsync) findings.push(...await doctor.inspectAsync(context));
434
+ if (detectHarness()) {
435
+ const sandbox = sandboxFinding(repoRoot(root) ?? root);
436
+ if (sandbox) findings.push(sandbox);
437
+ }
438
+ const shadowed = shadowedStimFinding(stim);
439
+ if (shadowed) findings.push(shadowed);
440
+ const budget = await inspectBudget(root);
441
+ findings.push(...budget.findings);
442
+ const watchmanMemory = withoutUnhandledRejection(inspectWatchmanMemory());
443
+ const deviceHosts = withoutUnhandledRejection(inspectDeviceHostMachines({ fix: opts.fix === true }));
444
+ const nestedWorktrees = inspectNestedWorktrees(root);
445
+ const watched = withoutUnhandledRejection(watchedCheckouts(nestedWorktrees.map((entry) => entry.checkout)));
446
+ const targetInspectors = doctors.flatMap((doctor) => {
447
+ const inspect = doctor.offloadTargets?.(context, () => iosTargetRuntime(root, settings));
448
+ return inspect ? [inspect] : [];
449
+ });
450
+ const remoteMachinesInspection = withoutUnhandledRejection(inspectBuildMachines({
451
+ fix: opts.fix === true,
452
+ check: targetInspectors.length ? offloadCheck(root, async () => (await Promise.all(targetInspectors.map((inspect) => inspect()))).flat()) : null
453
+ }));
454
+ const watchman = await watchmanMemory;
455
+ if (watchman) findings.push(watchman);
456
+ if (!memorySweepIsScoped()) findings.push(...memoryCulpritFindings(readMemoryCulprits()));
457
+ const watchedRoots = await watched;
458
+ for (const entry of nestedWorktrees) findings.push(nestedWorktreeFinding(entry, watchedRoots.has(entry.checkout)));
459
+ const remoteMachines = await remoteMachinesInspection;
460
+ findings.push(...remoteMachines.findings);
461
+ const deviceHostMachines = await deviceHosts;
462
+ const reported = new Set(remoteMachines.findings.map((each) => each.title));
463
+ findings.push(...deviceHostMachines.findings.filter((each) => !reported.has(each.title)));
464
+ const agentDriver = inspectHostedAgentDriver();
465
+ if (agentDriver) findings.push(agentDriver);
466
+ const maintenance = maintenanceStatus();
467
+ const maintenanceLines = [
468
+ phaseLine("maintenance.mode", maintenance.mode),
469
+ phaseLine("maintenance log", maintenanceNdjsonFile()),
470
+ phaseLine("last pass", maintenanceLine(maintenance, false) ?? "no pass has run yet")
471
+ ];
472
+ if (opts.json) {
473
+ console.log(JSON.stringify({
474
+ project: root,
475
+ platform: opts.platform ?? null,
476
+ stim,
477
+ budget: budget.report,
478
+ maintenance: {
479
+ ...maintenance,
480
+ logPath: maintenanceNdjsonFile()
481
+ },
482
+ remoteMachines: remoteMachines.machines,
483
+ deviceHosts: deviceHostMachines.machines,
484
+ findings
485
+ }));
486
+ recordDoctorRun(root, opts.platform, version, void 0, selected.platforms);
487
+ return;
488
+ }
489
+ if (findings.length === 0) {
490
+ const lines = [...doctorSuccessLines(opts.platform, stim, budget.report, doctors.flatMap((doctor) => doctor.successLines?.(opts.platform) ?? [])), ...maintenanceLines];
491
+ for (const [index, line] of lines.entries()) if (index === 1) console.log(chalk.green(line));
492
+ else if (line && !line.startsWith(" ")) console.log(chalk.bold(line));
493
+ else console.log(chalk.dim(line));
494
+ recordDoctorRun(root, opts.platform, version, void 0, selected.platforms);
495
+ return;
496
+ }
497
+ const ordered = findings.toSorted((a, b) => a.level === b.level ? 0 : a.level === "cost" ? -1 : 1);
498
+ console.log(chalk.bold(`Doctor (${doctorTarget(opts.platform)})`));
499
+ for (const line of [
500
+ ...stimVersionLines(stim),
501
+ ...budgetLines(budget.report),
502
+ ...maintenanceLines
503
+ ]) console.log(chalk.dim(line));
504
+ for (const f of ordered) {
505
+ const tag = f.level === "cost" ? chalk.yellow("costs time") : chalk.dim("note");
506
+ console.log(`\n${tag} ${chalk.bold(f.title)}`);
507
+ console.log(` ${f.detail}`);
508
+ if (f.fix) console.log(chalk.dim(` -> ${f.fix}`));
509
+ }
510
+ console.log(chalk.dim(`\n${findings.length} finding(s). Fix relevant "costs time" findings before copying the source checkout into a native worktree.`));
511
+ recordDoctorRun(root, opts.platform, version, void 0, selected.platforms);
512
+ });
513
+ }
514
+ //#endregion
515
+ export { applySandboxFix, doctorCommand as default, doctorSuccessLines, parseDoctorPlatform, shadowedStimFinding };