stim 1.14.0 → 1.16.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 (152) hide show
  1. package/README.md +2 -0
  2. package/dist/activity-BxwdD81S.mjs +609 -0
  3. package/dist/agent-device-usage-output-Dpih1Tfa.mjs +411 -0
  4. package/dist/{android-d6rGq1pA.mjs → android-B_NumVyQ.mjs} +169 -81
  5. package/dist/{android-LbPFRWVL.mjs → android-Ift4kVvt.mjs} +517 -413
  6. package/dist/android-cas-8U4DPqeR.mjs +167 -0
  7. package/dist/android-cas-compiler.mjs +2 -2
  8. package/dist/{android-XpvCrV7q.mjs → android-r2rBL8kV.mjs} +2 -2
  9. package/dist/{app-install-BpgSgkJj.mjs → app-install-CKWdbXIX.mjs} +128 -151
  10. package/dist/attempt-Bk2ouQVL.mjs +39 -0
  11. package/dist/budget-DjrURfQn.mjs +2700 -0
  12. package/dist/{build-plan-Dcr-3ie0.mjs → build-plan-DjeSb2Xl.mjs} +192 -653
  13. package/dist/build-slots-rUk77xqh.mjs +132 -0
  14. package/dist/{cache-manifest-C68QQ0ua.mjs → cache-manifest-DnUHBJaU.mjs} +2 -2
  15. package/dist/cache-manifest.mjs +1 -1
  16. package/dist/{chrome-BPCiC3ZX.mjs → chrome-BrcVWQyD.mjs} +1 -1
  17. package/dist/cli-BkYZ_6hy.mjs +51 -0
  18. package/dist/cli.mjs +6 -42
  19. package/dist/client-BTY686R6.mjs +1368 -0
  20. package/dist/collector-run.mjs +9 -195
  21. package/dist/{command-output-DVcpw6eE.mjs → command-output-DnJMdEY-.mjs} +1 -1
  22. package/dist/{config-lvc7d3C2.mjs → config-D0DbpZtM.mjs} +28 -29
  23. package/dist/{created-devices-CllH_8eb.mjs → created-devices-D5owjN4C.mjs} +2 -2
  24. package/dist/{dependency-state-DsT7aKeo.mjs → dependency-state-B6cMXZab.mjs} +2 -2
  25. package/dist/{deps-D9IcKqXS.mjs → deps-urbmfXig.mjs} +3 -3
  26. package/dist/{detached-entry-DJHoca49.mjs → detached-entry-CiTEgMR1.mjs} +1 -1
  27. package/dist/dev-client-CroAIjjj.mjs +43 -0
  28. package/dist/{device-CjuuAJjQ.mjs → device-DZQjF7H5.mjs} +8 -7
  29. package/dist/device-capacity-BHwxNBlu.mjs +136 -0
  30. package/dist/device-host-worker.d.mts +1 -0
  31. package/dist/device-host-worker.mjs +964 -0
  32. package/dist/device-ios-BadmeL2V.mjs +381 -0
  33. package/dist/{device-lease-5c3WrVRm.mjs → device-lease-D-hLqx1b.mjs} +43 -36
  34. package/dist/{device-pool-BiyDyPP-.mjs → device-lease-run-CqjShqWW.mjs} +6 -126
  35. package/dist/device-pool-CUaEWMMm.mjs +125 -0
  36. package/dist/{device-remote-D87zEzaq.mjs → device-remote-D5WVWqOq.mjs} +69 -163
  37. package/dist/{doctor-x9pnU9fq.mjs → doctor-Cimf-gGB.mjs} +241 -103
  38. package/dist/{error-diagnostics-C5HzcwCW.mjs → error-diagnostics-B9L4Ae0x.mjs} +15 -18
  39. package/dist/{exec-DK8KaVHK.mjs → exec-CaErVv7t.mjs} +5 -1
  40. package/dist/{gc-ko6mUAYD.mjs → gc-Bqy4bjEu.mjs} +599 -108
  41. package/dist/{android-cas-iZK3DOCr.mjs → gradle-BsO1MdsH.mjs} +8 -294
  42. package/dist/{guide-6R0VNFqI.mjs → guide-B_uyKTxW.mjs} +2511 -239
  43. package/dist/{guide-status-BZrFPIA_.mjs → guide-status-CjEZMAD4.mjs} +3 -3
  44. package/dist/hosted-client-a-WmqE2L.mjs +197 -0
  45. package/dist/hosted-ios-CIWelIqf.mjs +220 -0
  46. package/dist/hosted-logs-D5g041Q3.mjs +187 -0
  47. package/dist/hosted-macos-BSZyIb3y.mjs +135 -0
  48. package/dist/idle-DYVymG92.mjs +1099 -0
  49. package/dist/idle-shutdown-CTsDnAde.mjs +122 -0
  50. package/dist/in-use-DvBw0Kxo.mjs +130 -0
  51. package/dist/{ios-BwDPrnYQ.mjs → ios-BaInn3sW.mjs} +201 -10
  52. package/dist/ios-Cr1h5gnO.mjs +189 -0
  53. package/dist/{ios-P1f5nm2n.mjs → ios-aq3QoGI3.mjs} +1159 -633
  54. package/dist/{ios-device-BiOsSVzd.mjs → ios-device-Ee7sDXfp.mjs} +1 -1
  55. package/dist/{ios-device-DGglU3E_.mjs → ios-device-FfCN_C8p.mjs} +12 -7
  56. package/dist/ios-state-DS5UpYTH.mjs +48 -0
  57. package/dist/{launch-verify-Deg4tB3Z.mjs → launch-verify-2DBRizjo.mjs} +5 -5
  58. package/dist/logs-CHEVK8zD.mjs +371 -0
  59. package/dist/machines-BIBUdhpj.mjs +265 -0
  60. package/dist/macos-CSYEAFmF.mjs +473 -0
  61. package/dist/macos-run.d.mts +5 -0
  62. package/dist/macos-run.mjs +2 -0
  63. package/dist/maintenance-run.d.mts +4 -0
  64. package/dist/maintenance-run.mjs +254 -0
  65. package/dist/{metro-CcEo8Fl8.mjs → metro-BeK1ZPz1.mjs} +20 -120
  66. package/dist/metro-gateway-BJ-rxQAB.mjs +84 -0
  67. package/dist/{named-ports-Blg8IxOh.mjs → named-ports-Bk_YXEmE.mjs} +6 -59
  68. package/dist/native-run-NaBrRFtQ.mjs +147 -0
  69. package/dist/{native-runtime-DjBHRg5m.mjs → native-runtime-DBzRnEKS.mjs} +9 -5
  70. package/dist/{ndjson-CDY9IkVC.mjs → ndjson-PclOIwVY.mjs} +2 -2
  71. package/dist/offload-worker.d.mts +122 -0
  72. package/dist/offload-worker.mjs +606 -0
  73. package/dist/{ownership-DyYDF5ww.mjs → ownership-DiA16AsN.mjs} +3 -2
  74. package/dist/ownership-hCkPzGmr.mjs +571 -0
  75. package/dist/{page-BHUzTDFt.mjs → page-QF_Ke10s.mjs} +7 -2
  76. package/dist/{ports-BguS83ex.mjs → ports-R2k4Knz3.mjs} +3 -3
  77. package/dist/prebuild-D2fsca_Z.mjs +213 -0
  78. package/dist/preview-DhVbvPAZ.mjs +318 -0
  79. package/dist/process-identity-BmRm1qV0.mjs +12 -0
  80. package/dist/{project-CWlvEaeN.mjs → project-Du91Ilvc.mjs} +74 -19
  81. package/dist/pull-request-pxnDCQzn.mjs +322 -0
  82. package/dist/pull-requests.d.mts +120 -0
  83. package/dist/pull-requests.mjs +2 -0
  84. package/dist/recordings-BIXOzVnr.mjs +37 -0
  85. package/dist/{reload-BiniwsSZ.mjs → reload-rM9jV1fz.mjs} +109 -24
  86. package/dist/{remote-cache-CcR6o6lI.mjs → remote-cache-C8-V13XN.mjs} +5 -5
  87. package/dist/run-f4vI6EnK.mjs +119 -0
  88. package/dist/{server-bare-DGfgyJ5J.mjs → server-bare-CpPGmnmO.mjs} +33 -15
  89. package/dist/server-expo-CVqO2c35.mjs +2 -0
  90. package/dist/{server-expo-BVuwQ65v.mjs → server-expo-MC0rHlOC.mjs} +8 -12
  91. package/dist/settings-BBpMcDbD.mjs +25 -0
  92. package/dist/{settings-B-fsC-Om.mjs → settings-ZkDzkzvB.mjs} +22 -10
  93. package/dist/settings-oTSkkNrM.mjs +1439 -0
  94. package/dist/settings.schema.json +845 -97
  95. package/dist/{simslim-DrDN4WuV.mjs → simslim-B8-7AoTI.mjs} +8 -132
  96. package/dist/{slot-launch-D5t8EiMf.mjs → slot-launch-B8VrHxn1.mjs} +11 -8
  97. package/dist/{spawn-claims-Drw_o-Fb.mjs → spawn-claims-D0XfSe8u.mjs} +1 -1
  98. package/dist/spawn-entry-jw19m8Nl.mjs +17 -0
  99. package/dist/stage-CzaiwFsI.mjs +143 -0
  100. package/dist/start-CX8Ig-U2.mjs +2 -0
  101. package/dist/{start-s4RXp1wy.mjs → start-ClDfwXa4.mjs} +116 -65
  102. package/dist/state-3DwSQIBK.mjs +41 -0
  103. package/dist/state-B4PQFXMW.d.mts +12 -0
  104. package/dist/{state-Dk9RVXI7.mjs → state-C1jB7Q8q.mjs} +70 -8
  105. package/dist/{state-DHqvsbqC.mjs → state-DcW6vOd_.mjs} +7 -4
  106. package/dist/stats-CEZ_i5je.mjs +77 -0
  107. package/dist/{status-BgAuxESg.mjs → status-BuvBFeII.mjs} +118 -20
  108. package/dist/{status-BsPAnfOI.mjs → status-Cs7VLg9D.mjs} +542 -283
  109. package/dist/status-DlKvVJBE.mjs +49 -0
  110. package/dist/stdout-reader-C8yjre1x.mjs +23 -0
  111. package/dist/stim-desktop-Cnz8CPnl.mjs +63 -0
  112. package/dist/stim-footprint.swift +32 -0
  113. package/dist/{stim-installations-CINXy_su.mjs → stim-installations-QMEI32v1.mjs} +1 -1
  114. package/dist/stop-B0Yxtmiy.mjs +48 -0
  115. package/dist/{stop-BzreOWJp.mjs → stop-Bffozsac.mjs} +83 -16
  116. package/dist/{stop-BWIF_6BK.mjs → stop-Dt_dM16A.mjs} +2 -2
  117. package/dist/stop-cause-CidxIqup.mjs +97 -0
  118. package/dist/supervisor-run.d.mts +43 -11
  119. package/dist/supervisor-run.mjs +363 -24
  120. package/dist/support-CAP8qDUF.mjs +316 -0
  121. package/dist/support-D4aYJniO.mjs +201 -0
  122. package/dist/teardown-CL6vDs2Y.mjs +727 -0
  123. package/dist/{xcode-Cp5KvK9x.mjs → toolchain-DkUflduf.mjs} +364 -14
  124. package/dist/trigger-Bemu8ytw.mjs +65 -0
  125. package/dist/trigger-CXts6qt1.mjs +2 -0
  126. package/dist/warm-progress-CpGv3Wb6.mjs +732 -0
  127. package/dist/{web-BsQs1WlO.mjs → web-DWUaMp8P.mjs} +37 -21
  128. package/dist/web-run.mjs +310 -13
  129. package/dist/workspace-process-lock-BL7Q0HCO.mjs +57 -0
  130. package/dist/workspace-state-B8-aJ1_5.mjs +371 -0
  131. package/dist/workspaces-BV4pdHFz.mjs +306 -0
  132. package/dist/{worktree-UrQWXSeb.mjs → worktree-K-xj9Db0.mjs} +625 -256
  133. package/dist/worktree-jnG8Iph5.mjs +2 -0
  134. package/package.json +10 -5
  135. package/shim/bundle-response.cjs +28 -12
  136. package/dist/activity-CYVDcrt3.mjs +0 -356
  137. package/dist/budget-CGDkhvv_.mjs +0 -1745
  138. package/dist/build-progress-DxKpwnaS.mjs +0 -247
  139. package/dist/idle-DXtx3RbX.mjs +0 -191
  140. package/dist/in-use-DlqBDKqY.mjs +0 -249
  141. package/dist/logs-Civ4gbBH.mjs +0 -486
  142. package/dist/merge-state-ytFQCeh4.mjs +0 -262
  143. package/dist/server-expo-DIHDhvGg.mjs +0 -2
  144. package/dist/settings-D61iNOsB.mjs +0 -629
  145. package/dist/start-Cetfvy8l.mjs +0 -2
  146. package/dist/stats-CFpFGlJV.mjs +0 -62
  147. package/dist/stats-CX1Z8Tca.mjs +0 -368
  148. package/dist/stim-desktop-CXt8-Z3J.mjs +0 -40
  149. package/dist/stop-cause-hjGBOZTG.mjs +0 -1231
  150. package/dist/worktree-BVrHsYZG.mjs +0 -752
  151. package/dist/worktree-CHWWvm5v.mjs +0 -2
  152. package/dist/{metro-store-DzeWkfqW.mjs → metro-store-CDDZHtPl.mjs} +1 -1
@@ -0,0 +1,1439 @@
1
+ import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
+ import { _ as loadConfig$1, d as getConfigDir, f as getConfigPath$1, h as getRepoSettings, m as getProjectSettings } from "./config-D0DbpZtM.mjs";
3
+ import { createHash } from "node:crypto";
4
+ import { accessSync, chmodSync, constants, lstatSync, mkdirSync, mkdtempSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
5
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
+ import { chmodSync as chmodSync$1, existsSync as existsSync$1, lstatSync as lstatSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, statSync as statSync$1, utimesSync as utimesSync$1 } from "fs";
7
+ import { dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, relative as relative$1, resolve as resolve$1, sep as sep$1 } from "path";
8
+ import { buildCacheRoot } from "@stim-cli/core";
9
+ import { ANDROID_COMPILER_CACHE_CHOICES, ANDROID_PCH_CHOICES, REMOTE_DEVICE_BACKENDS, SETTINGS, SETTING_GROUPS, TUNNEL_MODES, WEB_VIEWPORTS, acceptsShape, coerceSettingText, expectedShape, isLayeredSetting, settingDefinition, settingValueError } from "@stim-cli/core/state";
10
+ import { tmpdir } from "node:os";
11
+ //#region src/temporary.ts
12
+ function existingPath(path) {
13
+ const missing = [];
14
+ let current = resolve(path);
15
+ while (!lstatSync(current, { throwIfNoEntry: false })) {
16
+ missing.unshift(basename(current));
17
+ current = dirname(current);
18
+ }
19
+ const existing = realpathSync(current);
20
+ return {
21
+ existing,
22
+ resolved: join(existing, ...missing)
23
+ };
24
+ }
25
+ function filesystemDevice(path) {
26
+ return statSync(existingPath(path).existing).dev;
27
+ }
28
+ function outsideWorkingTree(path) {
29
+ let current = path;
30
+ while (true) {
31
+ if (lstatSync(join(current, ".git"), { throwIfNoEntry: false })) return false;
32
+ const parent = dirname(current);
33
+ if (parent === current) return true;
34
+ current = parent;
35
+ }
36
+ }
37
+ function inside(parent, path) {
38
+ const rel = relative(parent, path);
39
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
40
+ }
41
+ function temporaryRoot(near) {
42
+ const reference = existingPath(near);
43
+ const insideReference = (path) => inside(reference.resolved, path);
44
+ const override = process.env.STIM_TMPDIR ?? loadConfig$1()?.tempDir;
45
+ if (override !== void 0) {
46
+ if (typeof override !== "string" || !isAbsolute(override)) throw new Error("STIM_TMPDIR / tempDir must be an absolute directory path.");
47
+ const { existing, resolved } = existingPath(override);
48
+ if (!statSync(existing).isDirectory() || !outsideWorkingTree(existing)) throw new Error(`Temporary directory ${override} must be outside Git working trees.`);
49
+ if (insideReference(resolved)) throw new Error(`Temporary directory ${override} must not be inside ${near}.`);
50
+ accessSync(existing, constants.W_OK);
51
+ return resolved;
52
+ }
53
+ const { existing } = reference;
54
+ const cache = existingPath(buildCacheRoot()).resolved;
55
+ const device = statSync(existing).dev;
56
+ const candidates = [tmpdir()];
57
+ let current = existing === reference.resolved || !statSync(existing).isDirectory() ? dirname(existing) : existing;
58
+ while (statSync(current).dev === device) {
59
+ candidates.push(current);
60
+ const parent = dirname(current);
61
+ if (parent === current) break;
62
+ current = parent;
63
+ }
64
+ for (const candidate of candidates) try {
65
+ const path = realpathSync(candidate);
66
+ if (statSync(path).dev !== device || insideReference(path) || inside(cache, path) || !outsideWorkingTree(path)) continue;
67
+ accessSync(path, constants.W_OK);
68
+ return path;
69
+ } catch {}
70
+ throw new Error(`No writable temporary directory on the filesystem containing ${near}. Set STIM_TMPDIR or tempDir in ${join(getConfigDir(), "config.json")} to a directory outside Git working trees.`);
71
+ }
72
+ function makeTemporaryDirectory(near, prefix) {
73
+ const root = temporaryRoot(near);
74
+ mkdirSync(root, {
75
+ recursive: true,
76
+ mode: 448
77
+ });
78
+ return mkdtempSync(join(root, prefix));
79
+ }
80
+ function removeTemporaryEntry(path) {
81
+ const stat = lstatSync(path, { throwIfNoEntry: false });
82
+ if (stat?.isDirectory()) {
83
+ chmodSync(path, stat.mode | 448);
84
+ for (const entry of readdirSync(path, { withFileTypes: true })) if (entry.isDirectory()) removeTemporaryEntry(join(path, entry.name));
85
+ }
86
+ rmSync(path, {
87
+ recursive: true,
88
+ force: true
89
+ });
90
+ }
91
+ //#endregion
92
+ //#region src/workspace/worktree.ts
93
+ const CARRY_SKIP_BASENAMES = /* @__PURE__ */ new Set([
94
+ ".DerivedData",
95
+ ".DS_Store",
96
+ ".idea",
97
+ ".jj"
98
+ ]);
99
+ function isCarrySkipped(rel) {
100
+ return /(^|\/)android\/build\/generated\/autolinking(\/|$)/.test(rel) || String(rel).split("/").some((seg) => CARRY_SKIP_BASENAMES.has(seg));
101
+ }
102
+ function nativePath(path) {
103
+ return sep$1 === "/" ? path : path.replaceAll("/", sep$1);
104
+ }
105
+ function gitCommonDir(cwd) {
106
+ const out = getExecutor().runFileQuiet("git", [
107
+ "-C",
108
+ cwd,
109
+ "rev-parse",
110
+ "--path-format=absolute",
111
+ "--git-common-dir"
112
+ ]);
113
+ return out ? nativePath(out.trim()) : null;
114
+ }
115
+ function repoRoot(cwd) {
116
+ const out = getExecutor().runFileQuiet("git", [
117
+ "-C",
118
+ cwd,
119
+ "rev-parse",
120
+ "--show-toplevel"
121
+ ]);
122
+ return out ? nativePath(out.trim()) : null;
123
+ }
124
+ function locallyKnownUpstream(projectRoot) {
125
+ try {
126
+ const name = getExecutor().runFile("git", [
127
+ "-C",
128
+ projectRoot,
129
+ "rev-parse",
130
+ "--abbrev-ref",
131
+ "--symbolic-full-name",
132
+ "@{upstream}"
133
+ ], { timeoutMs: 5e3 });
134
+ const counts = getExecutor().runFile("git", [
135
+ "-C",
136
+ projectRoot,
137
+ "rev-list",
138
+ "--left-right",
139
+ "--count",
140
+ "HEAD...@{upstream}"
141
+ ], { timeoutMs: 5e3 }).trim().split(/\s+/).map(Number);
142
+ const ahead = counts[0] ?? NaN;
143
+ const behind = counts[1] ?? NaN;
144
+ if (!name || !Number.isInteger(ahead) || !Number.isInteger(behind)) return null;
145
+ return {
146
+ name,
147
+ ahead,
148
+ behind
149
+ };
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+ function warmWorktreePaths(cwd) {
155
+ const currentRoot = repoRoot(cwd);
156
+ if (!currentRoot) throw new Error("Not a git repository.");
157
+ const target = realpathSync$1(currentRoot);
158
+ const source = resolveSourceCheckout(target);
159
+ if ("refusal" in source) throw new Error(source.refusal);
160
+ if (!source.entries.find((entry) => canonicalPath(entry.path) === target)) throw new Error("Could not identify the linked worktree and its source checkout.");
161
+ const root = realpathSync$1(source.path);
162
+ if (root === target) throw new Error("Run stim worktree warm from a linked worktree, not the source checkout.");
163
+ const sourceRoot = repoRoot(root);
164
+ const sourceCommon = gitCommonDir(root);
165
+ const targetCommon = gitCommonDir(target);
166
+ if (!sourceRoot || realpathSync$1(sourceRoot) !== root || !sourceCommon || !targetCommon || realpathSync$1(sourceCommon) !== realpathSync$1(targetCommon)) throw new Error("Could not verify that the source checkout belongs to this linked worktree.");
167
+ return {
168
+ root,
169
+ target,
170
+ common: realpathSync$1(targetCommon)
171
+ };
172
+ }
173
+ function matchesInclude(path, patterns) {
174
+ for (const pattern of patterns || []) {
175
+ const rooted = pattern.startsWith("/");
176
+ const escaped = (rooted ? pattern.slice(1) : pattern).replace(/[.+^${}()|[\]\\?]/g, "\\$&").replace(/\*\*\//g, "::GLOBSTAR::").replace(/\*/g, "[^/]+").replace(/::GLOBSTAR::/g, "(?:.*/)?").replace(/\\\?/g, "[^/]");
177
+ if (new RegExp(`${rooted ? "^" : "(^|/)"}${escaped}$`).test(path)) return true;
178
+ }
179
+ return false;
180
+ }
181
+ function readWorktreeExclude(root) {
182
+ return readPatternFile(join$1(root, ".worktreeexclude"));
183
+ }
184
+ function readPatternFile(p) {
185
+ if (!existsSync$1(p)) return null;
186
+ return readFileSync$1(p, "utf-8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
187
+ }
188
+ function canonicalPath(path) {
189
+ try {
190
+ return realpathSync$1(path);
191
+ } catch {
192
+ return resolve$1(path);
193
+ }
194
+ }
195
+ function nestedWorktreePaths(root) {
196
+ const source = canonicalPath(root);
197
+ const paths = /* @__PURE__ */ new Set();
198
+ const out = getExecutor().runFileQuiet("git", [
199
+ "-C",
200
+ root,
201
+ "worktree",
202
+ "list",
203
+ "--porcelain"
204
+ ]);
205
+ if (out === null) throw new Error("Could not list Git worktrees. Refusing to carry ignored files.");
206
+ for (const entry of parseWorktrees(out)) {
207
+ const rel = relative$1(source, canonicalPath(entry.path));
208
+ if (!rel || rel === ".." || rel.startsWith(`..${sep$1}`) || isAbsolute$1(rel)) continue;
209
+ paths.add(rel.split(sep$1).join("/"));
210
+ }
211
+ return [...paths];
212
+ }
213
+ function overlapsNestedWorktree(rel, nestedPaths) {
214
+ const path = String(rel).replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, "");
215
+ return nestedPaths.some((nested) => path === nested || path.startsWith(`${nested}/`) || nested.startsWith(`${path}/`));
216
+ }
217
+ function listTrackedPaths(dir) {
218
+ const out = getExecutor().runFileQuiet("git", [
219
+ "-C",
220
+ dir,
221
+ "ls-files",
222
+ "-z"
223
+ ]);
224
+ if (out === null) return null;
225
+ return out.split("\0").filter(Boolean);
226
+ }
227
+ function trackedGuard(dir) {
228
+ const paths = listTrackedPaths(dir);
229
+ if (paths === null) return {
230
+ known: false,
231
+ covers: () => false
232
+ };
233
+ const entries = new Set(paths);
234
+ const covered = new Set(paths);
235
+ for (const p of paths) for (let i = p.indexOf("/"); i !== -1; i = p.indexOf("/", i + 1)) covered.add(p.slice(0, i));
236
+ return {
237
+ known: true,
238
+ covers: (rel) => {
239
+ if (covered.has(rel)) return true;
240
+ for (let i = rel.indexOf("/"); i !== -1; i = rel.indexOf("/", i + 1)) if (entries.has(rel.slice(0, i))) return true;
241
+ return false;
242
+ }
243
+ };
244
+ }
245
+ function listGitignoredEntries(root) {
246
+ const args = [
247
+ "-C",
248
+ root,
249
+ "ls-files",
250
+ "--others",
251
+ "--ignored",
252
+ "--exclude-standard",
253
+ "--directory",
254
+ "--no-empty-directory",
255
+ "-z"
256
+ ];
257
+ const out = getExecutor().runFile("git", args);
258
+ if (!out) return [];
259
+ return out.split("\0").filter(Boolean).map((e) => e.endsWith("/") ? e.slice(0, -1) : e);
260
+ }
261
+ function listCarryableIgnoredEntries(root, patterns) {
262
+ const nested = nestedWorktreePaths(root);
263
+ return listGitignoredEntries(root).filter((rel) => !isCarrySkipped(rel) && !overlapsNestedWorktree(rel, nested) && !matchesInclude(rel, patterns));
264
+ }
265
+ function missingDestinationReason(target, rel) {
266
+ const parts = rel.split("/");
267
+ for (let i = 1; i < parts.length; i++) {
268
+ const parent = parts.slice(0, i).join("/");
269
+ const stat = lstatSync$1(join$1(target, parent), { throwIfNoEntry: false });
270
+ if (stat?.isSymbolicLink()) return `symlink ancestor: ${parent}`;
271
+ if (stat && !stat.isDirectory()) return `non-directory ancestor: ${parent}`;
272
+ }
273
+ return lstatSync$1(join$1(target, rel), { throwIfNoEntry: false }) ? "exists" : null;
274
+ }
275
+ function removeCopiedExclusions(path, rel) {
276
+ const entries = readdirSync$1(path, { withFileTypes: true });
277
+ const excluded = entries.filter((entry) => isCarrySkipped(`${rel}/${entry.name}`));
278
+ if (excluded.length > 0) {
279
+ const stat = lstatSync$1(path);
280
+ chmodSync$1(path, stat.mode | 448);
281
+ try {
282
+ for (const entry of excluded) removeTemporaryEntry(join$1(path, entry.name));
283
+ } finally {
284
+ utimesSync$1(path, stat.atime, stat.mtime);
285
+ chmodSync$1(path, stat.mode);
286
+ }
287
+ }
288
+ for (const entry of entries) {
289
+ const child = `${rel}/${entry.name}`;
290
+ if (entry.isDirectory() && !isCarrySkipped(child)) removeCopiedExclusions(join$1(path, entry.name), child);
291
+ }
292
+ }
293
+ function cloneIgnoredEntries({ root, target, patterns }) {
294
+ const copied = [];
295
+ const skipped = [];
296
+ const failed = [];
297
+ let cloned = true;
298
+ const guard = trackedGuard(target);
299
+ if (!guard.known) throw new Error("Could not list the destination's tracked files.");
300
+ const nested = nestedWorktreePaths(target);
301
+ for (const rel of listCarryableIgnoredEntries(root, patterns)) {
302
+ const from = join$1(root, rel);
303
+ const to = join$1(target, rel);
304
+ try {
305
+ const reason = guard.covers(rel) ? "tracked" : overlapsNestedWorktree(rel, nested) ? "nested worktree" : missingDestinationReason(target, rel);
306
+ if (reason) {
307
+ skipped.push({
308
+ file: rel,
309
+ reason
310
+ });
311
+ continue;
312
+ }
313
+ mkdirSync$1(dirname$1(to), { recursive: true });
314
+ try {
315
+ getExecutor().runFile("cp", [
316
+ "-Rc",
317
+ from,
318
+ to
319
+ ]);
320
+ } catch {
321
+ removeTemporaryEntry(to);
322
+ getExecutor().runFile("cp", [
323
+ "-R",
324
+ from,
325
+ to
326
+ ]);
327
+ cloned = false;
328
+ }
329
+ if (lstatSync$1(to).isDirectory()) removeCopiedExclusions(to, rel);
330
+ copied.push(rel);
331
+ } catch (e) {
332
+ failed.push({
333
+ file: rel,
334
+ error: String(e?.message || e)
335
+ });
336
+ }
337
+ }
338
+ return {
339
+ copied,
340
+ skipped,
341
+ failed,
342
+ cloned
343
+ };
344
+ }
345
+ function podsOutOfSync(target, copiedEntries, { read = readFileSync$1 } = {}) {
346
+ const problems = [];
347
+ for (const rel of copiedEntries || []) {
348
+ if (rel !== "Pods" && !rel.endsWith("/Pods")) continue;
349
+ const iosDir = rel === "Pods" ? "" : rel.slice(0, -5);
350
+ const manifest = join$1(target, rel, "Manifest.lock");
351
+ const podfileLock = join$1(target, iosDir, "Podfile.lock");
352
+ if (!existsSync$1(manifest)) continue;
353
+ if (!existsSync$1(podfileLock)) {
354
+ problems.push({
355
+ dir: iosDir || ".",
356
+ reason: "missing"
357
+ });
358
+ continue;
359
+ }
360
+ try {
361
+ if (read(manifest, "utf-8") !== read(podfileLock, "utf-8")) problems.push({
362
+ dir: iosDir || ".",
363
+ reason: "mismatch"
364
+ });
365
+ } catch {}
366
+ }
367
+ return problems;
368
+ }
369
+ const LOCKFILE_NAMES = [
370
+ "pnpm-lock.yaml",
371
+ "yarn.lock",
372
+ "bun.lock",
373
+ "bun.lockb",
374
+ "package-lock.json"
375
+ ];
376
+ function npmInstallMatches(wanted, installed) {
377
+ if (!wanted.packages || !installed.packages) return null;
378
+ for (const [key, entry] of Object.entries(wanted.packages)) {
379
+ if (!key.includes("node_modules/") || entry.link) continue;
380
+ const present = installed.packages[key];
381
+ if (!present && (entry.optional || entry.devOptional)) continue;
382
+ if (present?.version !== entry.version) return false;
383
+ }
384
+ return true;
385
+ }
386
+ function lastYamlDocument(text) {
387
+ return (text.split(/^---$/m).at(-1) ?? "").trim();
388
+ }
389
+ function yarnClassicInstallMatches(lockfile, integrity) {
390
+ if (!integrity.lockfileEntries) return null;
391
+ const wanted = [...lockfile.matchAll(/^ {2}resolved "([^"]+)"$/gm)].flatMap((m) => m[1] ?? []);
392
+ if (wanted.length === 0) return null;
393
+ const installed = new Set(Object.values(integrity.lockfileEntries));
394
+ return wanted.every((url) => installed.has(url));
395
+ }
396
+ function yarnBerryInstallMatches(lockfile, state) {
397
+ const installed = new Set([...state.matchAll(/^"?([^\s"#][^"]*?)"?:$/gm)].flatMap((m) => m[1]?.replace(/@virtual:[^#]+#/, "@") ?? []));
398
+ let entries = 0;
399
+ for (const entry of lockfile.split(/\n(?=\S)/)) {
400
+ const resolution = /^ {2}resolution: "([^"]+)"$/m.exec(entry)?.[1];
401
+ if (!resolution) continue;
402
+ entries += 1;
403
+ if (installed.has(resolution) || /^ {2}conditions: /m.test(entry)) continue;
404
+ return false;
405
+ }
406
+ return entries === 0 ? null : true;
407
+ }
408
+ function installedMatchesLockfile(dir, lockfile, { read = readFileSync$1 } = {}) {
409
+ const text = (rel) => {
410
+ try {
411
+ return read(join$1(dir, rel), "utf-8").replaceAll("\r\n", "\n");
412
+ } catch {
413
+ return null;
414
+ }
415
+ };
416
+ const wanted = text(lockfile);
417
+ if (wanted === null) return null;
418
+ try {
419
+ if (lockfile === "package-lock.json") {
420
+ const installed = text("node_modules/.package-lock.json");
421
+ return installed === null ? null : npmInstallMatches(JSON.parse(wanted), JSON.parse(installed));
422
+ }
423
+ if (lockfile === "pnpm-lock.yaml") {
424
+ const installed = text("node_modules/.pnpm/lock.yaml");
425
+ return installed === null ? null : lastYamlDocument(wanted) === lastYamlDocument(installed);
426
+ }
427
+ if (lockfile === "yarn.lock") {
428
+ if (/^__metadata:$/m.test(wanted)) {
429
+ const berry = text("node_modules/.yarn-state.yml");
430
+ return berry === null ? null : yarnBerryInstallMatches(wanted, berry);
431
+ }
432
+ const classic = text("node_modules/.yarn-integrity");
433
+ return classic === null ? null : yarnClassicInstallMatches(wanted, JSON.parse(classic));
434
+ }
435
+ } catch {}
436
+ return null;
437
+ }
438
+ function depsOutOfSync(root, target, copiedEntries, { read = readFileSync$1 } = {}) {
439
+ const problems = [];
440
+ for (const rel of copiedEntries || []) {
441
+ if (rel !== "node_modules" && !rel.endsWith("/node_modules")) continue;
442
+ const dir = rel === "node_modules" ? "" : rel.slice(0, -13);
443
+ const name = LOCKFILE_NAMES.find((candidate) => existsSync$1(join$1(target, dir, candidate)));
444
+ if (!name) continue;
445
+ const installed = installedMatchesLockfile(join$1(target, dir), name, { read });
446
+ if (installed === false) {
447
+ problems.push({
448
+ dir: dir || ".",
449
+ lockfile: name,
450
+ reason: "installed"
451
+ });
452
+ continue;
453
+ }
454
+ const source = join$1(root, dir, name);
455
+ if (installed === true || !existsSync$1(source)) continue;
456
+ try {
457
+ if (read(source, "utf-8") !== read(join$1(target, dir, name), "utf-8")) problems.push({
458
+ dir: dir || ".",
459
+ lockfile: name,
460
+ reason: "lockfile"
461
+ });
462
+ } catch {}
463
+ }
464
+ return problems;
465
+ }
466
+ const FINGERPRINT_INPUT_FILES = [
467
+ "app.json",
468
+ "app.config.ts",
469
+ "app.config.js",
470
+ "app.config.mjs",
471
+ "package.json"
472
+ ];
473
+ function dirtyFingerprintFiles(root) {
474
+ const out = getExecutor().runFileQuiet("git", [
475
+ "-C",
476
+ root,
477
+ "status",
478
+ "--porcelain",
479
+ "--",
480
+ ...FINGERPRINT_INPUT_FILES
481
+ ]);
482
+ if (out === null || out.trim() === "") return [];
483
+ return out.split("\n").map((line) => normalizePorcelainLine(line.trimEnd())).filter((line) => line !== "").map((line) => line.slice(3).trim()).filter((path) => path !== "");
484
+ }
485
+ function hasUncommittedWork(dir) {
486
+ const out = getExecutor().runFileQuiet("git", [
487
+ "--no-optional-locks",
488
+ "-C",
489
+ dir,
490
+ "status",
491
+ "--porcelain"
492
+ ]);
493
+ if (out === null) return null;
494
+ return out.trim().length > 0;
495
+ }
496
+ function dirtyPaths(dir, { limit = 10 } = {}) {
497
+ const out = getExecutor().runFileQuiet("git", [
498
+ "--no-optional-locks",
499
+ "-C",
500
+ dir,
501
+ "status",
502
+ "--porcelain"
503
+ ]);
504
+ if (out === null) return [];
505
+ return out.split("\n").map((l) => normalizePorcelainLine(l.trimEnd())).filter(Boolean).slice(0, limit);
506
+ }
507
+ function normalizePorcelainLine(line) {
508
+ if (line === "" || line[2] === " ") return line;
509
+ return ` ${line}`;
510
+ }
511
+ function restoreFile(dir, file) {
512
+ return getExecutor().runFileQuiet("git", [
513
+ "-C",
514
+ dir,
515
+ "checkout",
516
+ "--",
517
+ file
518
+ ]) !== null;
519
+ }
520
+ function isPodInstallChurn(paths) {
521
+ if (!paths || paths.length === 0) return false;
522
+ return paths.every((line) => /(?:^|\/)(?:Podfile\.lock|project\.pbxproj)$/.test(line.slice(3).trim()));
523
+ }
524
+ const SAFE_BRANCH_NAME = /^[A-Za-z0-9@._/-]+$/;
525
+ function unpushedCommits(dir) {
526
+ const exec = getExecutor();
527
+ const branch = exec.runFileQuiet("git", [
528
+ "-C",
529
+ dir,
530
+ "symbolic-ref",
531
+ "--quiet",
532
+ "--short",
533
+ "HEAD"
534
+ ]);
535
+ const own = branch === null ? "" : branch.trim();
536
+ const protection = own && SAFE_BRANCH_NAME.test(own) ? [
537
+ "--remotes",
538
+ `--exclude=${own}`,
539
+ "--branches"
540
+ ] : ["--remotes"];
541
+ if (!own && exec.runFileQuiet("git", [
542
+ "-C",
543
+ dir,
544
+ "rev-parse",
545
+ "--abbrev-ref",
546
+ "HEAD"
547
+ ]) === "HEAD") protection.push("--branches");
548
+ const out = exec.runFileQuiet("git", [
549
+ "-C",
550
+ dir,
551
+ "log",
552
+ "--oneline",
553
+ "HEAD",
554
+ "--not",
555
+ ...protection
556
+ ]);
557
+ if (out === null) return null;
558
+ return out.split("\n").map((l) => l.trim()).filter(Boolean);
559
+ }
560
+ function hasRemote(dir) {
561
+ const out = getExecutor().runFileQuiet("git", [
562
+ "-C",
563
+ dir,
564
+ "remote"
565
+ ]);
566
+ return Boolean(out && out.trim().length > 0);
567
+ }
568
+ function branchExists(cwd, branch) {
569
+ const out = getExecutor().runFileQuiet("git", [
570
+ "-C",
571
+ cwd,
572
+ "rev-parse",
573
+ "--verify",
574
+ "--quiet",
575
+ `refs/heads/${branch}`
576
+ ]);
577
+ return Boolean(out);
578
+ }
579
+ function resolveFullRef(cwd, ref) {
580
+ try {
581
+ const out = getExecutor().runFile("git", [
582
+ "-C",
583
+ cwd,
584
+ "rev-parse",
585
+ "--verify",
586
+ "--quiet",
587
+ "--end-of-options",
588
+ `${ref}^{commit}`
589
+ ]);
590
+ return out && out.trim() ? out.trim() : null;
591
+ } catch {
592
+ return null;
593
+ }
594
+ }
595
+ function removeWorktree(path, { from, force = false, platform = process.platform }) {
596
+ const args = [
597
+ ...platform === "win32" ? ["-c", "core.longpaths=true"] : [],
598
+ "-C",
599
+ from,
600
+ "worktree",
601
+ "remove",
602
+ ...force ? ["--force"] : [],
603
+ "--",
604
+ path
605
+ ];
606
+ getExecutor().runFile("git", args);
607
+ }
608
+ function deleteBranch(cwd, branch, expectedSha) {
609
+ if (!SAFE_BRANCH_NAME.test(branch) || branch.startsWith("-")) throw new Error(`Refusing branch ${JSON.stringify(branch)}: it is not a safe local branch name.`);
610
+ getExecutor().runFile("git", [
611
+ "-C",
612
+ cwd,
613
+ "update-ref",
614
+ "-d",
615
+ `refs/heads/${branch}`,
616
+ expectedSha
617
+ ]);
618
+ }
619
+ function parseWorktrees(out) {
620
+ const entries = [];
621
+ let current = {};
622
+ for (const line of out.split("\n")) if (line.startsWith("worktree ")) {
623
+ if (current.path) entries.push(current);
624
+ current = { path: nativePath(line.slice(9)) };
625
+ } else if (line.startsWith("branch ")) current.branch = line.slice(7).replace("refs/heads/", "");
626
+ else if (line === "prunable" || line.startsWith("prunable ")) current.prunable = true;
627
+ else if (line === "locked" || line.startsWith("locked ")) current.locked = true;
628
+ else if (line === "bare") current.bare = true;
629
+ if (current.path) entries.push(current);
630
+ return entries;
631
+ }
632
+ function hasPopulatedSubmodules(worktree) {
633
+ const modules = getExecutor().runFileQuiet("git", [
634
+ "-C",
635
+ worktree,
636
+ "rev-parse",
637
+ "--git-path",
638
+ "modules"
639
+ ]);
640
+ if (modules && existsSync$1(resolve$1(worktree, nativePath(modules)))) return true;
641
+ return (getExecutor().runFileQuiet("git", [
642
+ "-C",
643
+ worktree,
644
+ "ls-files",
645
+ "--stage",
646
+ "-z"
647
+ ]) ?? "").split("\0").some((line) => line.startsWith("160000 ") && existsSync$1(join$1(worktree, line.slice(line.indexOf(" ") + 1), ".git")));
648
+ }
649
+ function listWorktrees(cwd) {
650
+ const out = getExecutor().runFileQuiet("git", [
651
+ "-C",
652
+ cwd,
653
+ "worktree",
654
+ "list",
655
+ "--porcelain"
656
+ ]);
657
+ return out ? parseWorktrees(out) : [];
658
+ }
659
+ function branchOf(head) {
660
+ return head.startsWith("ref: refs/heads/") ? head.slice(16) : void 0;
661
+ }
662
+ /**
663
+ * The linked worktrees recorded under a git common dir, read from `worktrees/<id>/gitdir` and `HEAD` without running
664
+ * git or touching the worktree directories, sorted by path. The worktree with the branch the common dir's own `HEAD`
665
+ * names (the main checkout's branch, or a bare repository's source checkout) is left out.
666
+ */
667
+ function linkedWorktreesOnDisk(commonDir) {
668
+ const admin = join$1(commonDir, "worktrees");
669
+ let ids;
670
+ let sourceBranch;
671
+ try {
672
+ ids = readdirSync$1(admin);
673
+ sourceBranch = branchOf(readFileSync$1(join$1(commonDir, "HEAD"), "utf-8").trim());
674
+ } catch {
675
+ return [];
676
+ }
677
+ return ids.flatMap((id) => {
678
+ try {
679
+ const gitdir = readFileSync$1(join$1(admin, id, "gitdir"), "utf-8").trim();
680
+ const branch = branchOf(readFileSync$1(join$1(admin, id, "HEAD"), "utf-8").trim());
681
+ if (branch && branch === sourceBranch) return [];
682
+ const path = dirname$1(resolve$1(admin, id, nativePath(gitdir)));
683
+ return [branch ? {
684
+ path,
685
+ branch
686
+ } : { path }];
687
+ } catch {
688
+ return [];
689
+ }
690
+ }).toSorted((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
691
+ }
692
+ /**
693
+ * The git common dir of the repository containing `start`, read from `.git` and `commondir` files without running
694
+ * git. Returns null when `start` is missing, unreadable, or not inside a repository.
695
+ */
696
+ function gitCommonDirOnDisk(start) {
697
+ try {
698
+ for (let dir = realpathSync$1.native(start);; dir = dirname$1(dir)) {
699
+ const dotGit = join$1(dir, ".git");
700
+ if (existsSync$1(dotGit)) {
701
+ if (statSync$1(dotGit).isDirectory()) return realpathSync$1.native(dotGit);
702
+ const gitdir = /^gitdir: (.+)$/m.exec(readFileSync$1(dotGit, "utf-8"))?.[1]?.trim();
703
+ if (!gitdir) return null;
704
+ const linked = resolve$1(dir, gitdir);
705
+ const commondir = join$1(linked, "commondir");
706
+ return realpathSync$1.native(existsSync$1(commondir) ? resolve$1(linked, readFileSync$1(commondir, "utf-8").trim()) : linked);
707
+ }
708
+ if (dirname$1(dir) === dir) return null;
709
+ }
710
+ } catch {
711
+ return null;
712
+ }
713
+ }
714
+ function selectSourceCheckout(entries, bareHead) {
715
+ const first = entries[0];
716
+ if (!first) return { refusal: "Git lists no worktrees for this repository." };
717
+ if (!first.bare) return { path: first.path };
718
+ const bare = first.path;
719
+ const checkouts = entries.filter((entry) => !entry.bare && !entry.prunable);
720
+ const listed = checkouts.map((entry) => `${entry.branch ?? "detached"} (${entry.path})`).join(", ") || "none";
721
+ const pointHead = ` git -C ${bare} symbolic-ref HEAD refs/heads/<branch>`;
722
+ if (!bareHead) return { refusal: `The bare repository at ${bare} has a detached HEAD, so Stim cannot tell which worktree is the source checkout. Point HEAD at the source branch, then retry:\n${pointHead}\nChecked-out branches: ${listed}` };
723
+ const matches = checkouts.filter((entry) => entry.branch === bareHead);
724
+ const [only] = matches;
725
+ if (only && matches.length === 1) return { path: only.path };
726
+ if (!only) {
727
+ const stale = entries.find((entry) => entry.prunable && entry.branch === bareHead);
728
+ const beside = checkouts[0] ? dirname$1(checkouts[0].path) : bare;
729
+ return { refusal: `The bare repository at ${bare} points HEAD at ${bareHead}, but no worktree has ${bareHead} checked out, so Stim has no source checkout to copy from. ${stale ? `Its worktree at ${stale.path} is missing. Recreate it, then retry:\n git -C ${bare} worktree prune\n git -C ${bare} worktree add ${stale.path} ${bareHead}\n` : `Create one, then retry:\n git -C ${bare} worktree add ${join$1(beside, bareHead)} ${bareHead}\n`}Or point HEAD at a branch that is checked out (${listed}):\n${pointHead}` };
730
+ }
731
+ return { refusal: `The bare repository at ${bare} points HEAD at ${bareHead}, which is checked out in more than one worktree: ${matches.map((entry) => entry.path).join(", ")}. Remove the extra worktrees, or point HEAD at a branch checked out once, then retry:\n${pointHead}` };
732
+ }
733
+ function bareHeadBranch(bare) {
734
+ const out = getExecutor().runFileQuiet("git", [
735
+ "-C",
736
+ bare,
737
+ "symbolic-ref",
738
+ "--quiet",
739
+ "HEAD"
740
+ ])?.trim();
741
+ return out?.startsWith("refs/heads/") ? out.slice(11) : null;
742
+ }
743
+ function sourceCheckoutOf(entries) {
744
+ const first = entries[0];
745
+ return selectSourceCheckout(entries, first?.bare ? bareHeadBranch(first.path) : null);
746
+ }
747
+ function resolveSourceCheckout(cwd) {
748
+ const entries = listWorktrees(cwd);
749
+ return {
750
+ entries,
751
+ ...sourceCheckoutOf(entries)
752
+ };
753
+ }
754
+ //#endregion
755
+ //#region src/optimizations.ts
756
+ function compilerCacheFallbackMessage({ fallback, compilerCache, file = null }) {
757
+ const origin = fallback.fromEnvironment ? "the environment" : file;
758
+ const backend = compilerCache === "none" ? "Android builds use no compiler cache, because optimizations.android.compilerCache is \"none\"." : "Android builds fall back to ccache when it is available.";
759
+ return `${fallback.key}${origin ? ` in ${origin}` : ""} ${fallback.reason}. ${backend}`;
760
+ }
761
+ function optimizationBuildProfile(platform, options) {
762
+ const selected = platform === "ios" ? {
763
+ ...options.ios,
764
+ swiftCompilationCache: options.ios.swiftCompilationCache ?? false
765
+ } : { pch: options.android.pch };
766
+ const defaults = platform === "ios" ? {
767
+ compilationCache: true,
768
+ swiftCompilationCache: false,
769
+ prefixMapping: true
770
+ } : { pch: "auto" };
771
+ if (JSON.stringify(selected) === JSON.stringify(defaults)) return void 0;
772
+ return `opt-${createHash("sha256").update(JSON.stringify(selected)).digest("hex").slice(0, 16)}`;
773
+ }
774
+ function resolveOptimizations(settings = {}, env = process.env) {
775
+ function choice(path, choices, fallback) {
776
+ const raw = optimizationValue(settings, path);
777
+ if (raw === void 0) return fallback;
778
+ if (typeof raw !== "string" || !choices.includes(raw)) throw new Error(`Invalid optimizations.${path}. Expected ${choices.join(", ")}.`);
779
+ return raw;
780
+ }
781
+ const compiler = choice("android.compilerCache", ANDROID_COMPILER_CACHE_CHOICES, "auto");
782
+ const fromEnvironment = Boolean(env.STIM_ANDROID_CAS_TOOLCHAIN);
783
+ const manifest = env.STIM_ANDROID_CAS_TOOLCHAIN || optimizationValue(settings, "android.casToolchain");
784
+ let fallback = null;
785
+ let toolchain = null;
786
+ if (manifest !== void 0) {
787
+ if (typeof manifest === "string" && isAbsolute(manifest) && !/[\r\n\0]/.test(manifest)) toolchain = manifest;
788
+ else fallback = {
789
+ key: fromEnvironment ? "STIM_ANDROID_CAS_TOOLCHAIN" : "optimizations.android.casToolchain",
790
+ reason: `is ${JSON.stringify(manifest)}, which is not an absolute path to a toolchain JSON manifest`,
791
+ fromEnvironment,
792
+ manifest: null
793
+ };
794
+ }
795
+ let compilerCache = compiler === "auto" ? toolchain ? "cas" : "ccache" : compiler;
796
+ if (compilerCache === "cas" && !toolchain) {
797
+ fallback ??= {
798
+ key: "optimizations.android.compilerCache",
799
+ reason: "is \"cas\", but no optimizations.android.casToolchain or STIM_ANDROID_CAS_TOOLCHAIN names the toolchain manifest",
800
+ fromEnvironment: false,
801
+ manifest: null
802
+ };
803
+ compilerCache = "ccache";
804
+ }
805
+ return {
806
+ buildCache: optimizationBoolean(settings, "buildCache"),
807
+ remoteBuildCache: optimizationBoolean(settings, "remoteBuildCache"),
808
+ releaseBundleSwap: optimizationBoolean(settings, "releaseBundleSwap"),
809
+ metroSharedCache: optimizationBoolean(settings, "metroSharedCache"),
810
+ metroWarmup: optimizationBoolean(settings, "metroWarmup"),
811
+ ios: {
812
+ compilationCache: optimizationBoolean(settings, "ios.compilationCache"),
813
+ swiftCompilationCache: optimizationValue(settings, "ios.swiftCompilationCache") === void 0 ? null : optimizationBoolean(settings, "ios.swiftCompilationCache"),
814
+ prefixMapping: optimizationBoolean(settings, "ios.prefixMapping")
815
+ },
816
+ android: {
817
+ compilerCache,
818
+ casToolchain: toolchain,
819
+ compilerCacheFallback: fallback,
820
+ pch: choice("android.pch", ANDROID_PCH_CHOICES, "auto"),
821
+ gradleBuildCache: optimizationBoolean(settings, "android.gradleBuildCache"),
822
+ targetAbiOnly: optimizationBoolean(settings, "android.targetAbiOnly")
823
+ }
824
+ };
825
+ }
826
+ function artifactCachePolicy(options, reuse, release) {
827
+ return {
828
+ read: reuse && options.buildCache && (!release || options.releaseBundleSwap),
829
+ write: options.buildCache,
830
+ remote: options.buildCache && options.remoteBuildCache
831
+ };
832
+ }
833
+ function optimizationValue(settings, path) {
834
+ let node = settings;
835
+ for (const key of ["optimizations", ...path.split(".")]) {
836
+ if (node === void 0) return void 0;
837
+ if (!node || typeof node !== "object" || Array.isArray(node)) throw new Error("Invalid optimizations setting. Expected nested objects.");
838
+ node = node[key];
839
+ }
840
+ return node;
841
+ }
842
+ function optimizationBoolean(settings, path, fallback = true) {
843
+ const raw = optimizationValue(settings, path);
844
+ if (raw === void 0) return fallback;
845
+ if (typeof raw !== "boolean") throw new Error(`Invalid optimizations.${path}. Expected true or false.`);
846
+ return raw;
847
+ }
848
+ function resolveMetroSharedCache(settings) {
849
+ return optimizationBoolean(settings, "metroSharedCache");
850
+ }
851
+ //#endregion
852
+ //#region src/engine/metro-reach.ts
853
+ const PUBLIC_METRO_ENV = "STIM_METRO_PUBLIC_URL";
854
+ const MANAGED_PROVIDERS = ["ngrok", "cloudflared"];
855
+ const NAMED = {
856
+ cloudflared: "`cloudflared` (brew install cloudflared)",
857
+ ngrok: "`ngrok` (brew install ngrok, then `ngrok config add-authtoken <token>`)",
858
+ tailscale: "Tailscale and sign in to your tailnet"
859
+ };
860
+ function planMetroReach({ mode, metroPort, publicUrl = null, isExpo, available = [] }) {
861
+ const named = publicUrl?.trim().replace(/\/+$/, "") || null;
862
+ if (mode === "off") return {
863
+ origin: `http://localhost:${metroPort}`,
864
+ gate: false
865
+ };
866
+ if (named) return {
867
+ origin: named,
868
+ gate: true
869
+ };
870
+ if (mode === "expo") {
871
+ if (!isExpo) return {
872
+ failed: "metro.tunnel is \"expo\", but this workspace does not run an Expo dev server.",
873
+ remedy: "Use \"auto\" to let stim start a tunnel, or \"off\" if the device shares this machine."
874
+ };
875
+ return { expoTunnel: true };
876
+ }
877
+ if (mode === "cloudflared" || mode === "ngrok" || mode === "tailscale") {
878
+ if (!available.includes(mode)) return {
879
+ failed: `metro.tunnel is "${mode}", but ${mode} is not on PATH.`,
880
+ remedy: `Install ${NAMED[mode] ?? mode}, or set metro.tunnel to "auto".`
881
+ };
882
+ return { start: mode };
883
+ }
884
+ const provider = available.find((candidate) => candidate !== "tailscale");
885
+ if (provider) return { start: provider };
886
+ return {
887
+ failed: `A remote device cannot reach this workspace's Metro on port ${metroPort}, and no tunnel is available to give it one.`,
888
+ remedy: `Install ${NAMED.ngrok} or ${NAMED.cloudflared} and Stim will manage the tunnel for you. If you already have one, set metro.publicUrl to its URL. If the device shares this machine (a local \`agent-device proxy\`), set metro.tunnel to "off".`
889
+ };
890
+ }
891
+ function resolveTailscaleBinary(findExecutable) {
892
+ return findExecutable("tailscale") ?? findExecutable("/Applications/Tailscale.app/Contents/MacOS/Tailscale");
893
+ }
894
+ function detectProviders(onPath, mode = "auto") {
895
+ if (mode === "tailscale") return resolveTailscaleBinary((bin) => onPath(bin) ? bin : null) ? ["tailscale"] : [];
896
+ return MANAGED_PROVIDERS.filter((p) => onPath(p));
897
+ }
898
+ //#endregion
899
+ //#region src/workspace/settings.ts
900
+ function isPlainObject(v) {
901
+ return v !== null && typeof v === "object" && !Array.isArray(v);
902
+ }
903
+ function mergeSettingsLayers(layers) {
904
+ const out = {};
905
+ for (const layer of layers) {
906
+ if (!isPlainObject(layer)) continue;
907
+ for (const [key, value] of Object.entries(layer)) if (isPlainObject(value) && isPlainObject(out[key])) out[key] = mergeSettingsLayers([out[key], value]);
908
+ else if (!(key in out)) out[key] = value;
909
+ }
910
+ return out;
911
+ }
912
+ const LAYERED_SETTINGS = SETTINGS.filter(isLayeredSetting);
913
+ const KNOWN_SETTINGS = /* @__PURE__ */ new Set([...LAYERED_SETTINGS.map((setting) => setting.key), ...SETTING_GROUPS]);
914
+ const BOUNDS_CHECKED_WHEN_USED = /* @__PURE__ */ new Set(["android.dataPartitionSizeGb"]);
915
+ const SETTINGS_WITH_RESOLVE_TIME_FALLBACK = /* @__PURE__ */ new Set(["optimizations.android.casToolchain"]);
916
+ const PATH_SETTINGS = Object.freeze(LAYERED_SETTINGS.filter((setting) => setting.type.kind === "path").map((setting) => setting.key));
917
+ function settingValueAt(settings, path) {
918
+ let node = settings;
919
+ for (const segment of path.split(".")) {
920
+ if (!isPlainObject(node)) return void 0;
921
+ node = node[segment];
922
+ }
923
+ return node;
924
+ }
925
+ function settingShapeErrors(settings) {
926
+ const errors = [];
927
+ for (const group of SETTING_GROUPS) {
928
+ const value = settingValueAt(settings, group);
929
+ if (value === void 0 || isPlainObject(value)) continue;
930
+ errors.push(`Invalid ${group} setting ${JSON.stringify(value)}. Expected an object.`);
931
+ }
932
+ for (const setting of LAYERED_SETTINGS) {
933
+ if (SETTINGS_WITH_RESOLVE_TIME_FALLBACK.has(setting.key)) continue;
934
+ const value = settingValueAt(settings, setting.key);
935
+ if (value === void 0) continue;
936
+ const expected = setting.key === "ios.remote" || setting.type.kind === "number" && !BOUNDS_CHECKED_WHEN_USED.has(setting.key) ? settingValueError(setting, value) : acceptsShape(setting, value) ? null : expectedShape(setting.type);
937
+ if (expected) errors.push(`Invalid ${setting.key} setting ${JSON.stringify(value)}. Expected ${expected}.`);
938
+ }
939
+ return errors;
940
+ }
941
+ const SETTING_SHAPE_REMEDY = "Run `stim guide settings` for the shape each setting takes.";
942
+ const MIN_ANDROID_DATA_PARTITION_SIZE_GB = 6;
943
+ const DEFAULT_ANDROID_DATA_PARTITION_SIZE_GB = 8;
944
+ const MAX_ANDROID_DATA_PARTITION_SIZE_GB = 16384;
945
+ function validateAndroidDataPartitionSizeGb(raw) {
946
+ if (typeof raw !== "number" || !Number.isSafeInteger(raw) || raw < MIN_ANDROID_DATA_PARTITION_SIZE_GB || raw > MAX_ANDROID_DATA_PARTITION_SIZE_GB) throw new Error(`Invalid android.dataPartitionSizeGb setting ${JSON.stringify(raw)}. Expected an integer from ${MIN_ANDROID_DATA_PARTITION_SIZE_GB} through ${MAX_ANDROID_DATA_PARTITION_SIZE_GB} GiB.`);
947
+ return raw;
948
+ }
949
+ function androidDataPartitionSizeBytes(sizeGb) {
950
+ return validateAndroidDataPartitionSizeGb(sizeGb) * 1024 ** 3;
951
+ }
952
+ function androidDataPartitionSizeGbRaw(settings) {
953
+ if (!isPlainObject(settings) || !isPlainObject(settings.android)) return void 0;
954
+ return settings.android.dataPartitionSizeGb;
955
+ }
956
+ function androidDataPartitionSizeGbSetting(settings) {
957
+ const raw = androidDataPartitionSizeGbRaw(settings);
958
+ if (raw === void 0) return DEFAULT_ANDROID_DATA_PARTITION_SIZE_GB;
959
+ return validateAndroidDataPartitionSizeGb(raw);
960
+ }
961
+ function androidDataPartitionSizeGbSettingError(settings) {
962
+ try {
963
+ androidDataPartitionSizeGbSetting(settings);
964
+ return null;
965
+ } catch (error) {
966
+ return String(error?.message || error);
967
+ }
968
+ }
969
+ function yesNoAvdValue(value) {
970
+ if (value === true || value === "true" || value === "yes") return "yes";
971
+ if (value === false || value === "false" || value === "no") return "no";
972
+ throw new Error("expected true, false, \"yes\", or \"no\"");
973
+ }
974
+ function boundedIntegerAvdRule(min, max, unit) {
975
+ return {
976
+ help: `integer ${min}-${max}${unit ? ` ${unit}` : ""}`,
977
+ parse: (value) => {
978
+ const parsed = typeof value === "number" ? value : typeof value === "string" && /^(0|[1-9]\d*)$/.test(value) ? Number(value) : NaN;
979
+ if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) throw new Error(`expected an integer from ${min} through ${max}${unit ? ` ${unit}` : ""}`);
980
+ return String(parsed);
981
+ }
982
+ };
983
+ }
984
+ function enumAvdRule(values) {
985
+ return {
986
+ help: values.join("|"),
987
+ parse: (value) => {
988
+ if (typeof value !== "string" || !values.includes(value)) throw new Error(`expected one of: ${values.join(", ")}`);
989
+ return value;
990
+ }
991
+ };
992
+ }
993
+ const ANDROID_AVD_CONFIG_RULES = {
994
+ "hw.cpu.ncore": boundedIntegerAvdRule(1, 64),
995
+ "hw.gpu.mode": enumAvdRule([
996
+ "auto",
997
+ "host",
998
+ "software",
999
+ "lavapipe",
1000
+ "swiftshader",
1001
+ "swangle"
1002
+ ]),
1003
+ "hw.initialOrientation": enumAvdRule(["portrait", "landscape"]),
1004
+ "hw.lcd.density": boundedIntegerAvdRule(72, 1e3, "dpi"),
1005
+ "hw.lcd.vsync": boundedIntegerAvdRule(1, 1e3, "Hz"),
1006
+ "hw.ramSize": boundedIntegerAvdRule(1536, 8192, "MB"),
1007
+ "hw.screen": enumAvdRule([
1008
+ "no-touch",
1009
+ "touch",
1010
+ "multi-touch"
1011
+ ]),
1012
+ "runtime.network.latency": enumAvdRule([
1013
+ "none",
1014
+ "gsm",
1015
+ "hscsd",
1016
+ "gprs",
1017
+ "edge",
1018
+ "umts",
1019
+ "hsdpa",
1020
+ "lte",
1021
+ "evdo",
1022
+ "5g"
1023
+ ]),
1024
+ "runtime.network.speed": enumAvdRule([
1025
+ "gsm",
1026
+ "hscsd",
1027
+ "gprs",
1028
+ "edge",
1029
+ "umts",
1030
+ "hsdpa",
1031
+ "lte",
1032
+ "evdo",
1033
+ "5g",
1034
+ "full"
1035
+ ]),
1036
+ "vm.heapSize": boundedIntegerAvdRule(16, 4096, "MB")
1037
+ };
1038
+ const YES_NO_AVD_RULE = {
1039
+ help: "true|false|yes|no",
1040
+ parse: yesNoAvdValue
1041
+ };
1042
+ for (const key of [
1043
+ "hw.accelerometer",
1044
+ "hw.accelerometer_uncalibrated",
1045
+ "hw.audioInput",
1046
+ "hw.audioOutput",
1047
+ "hw.battery",
1048
+ "hw.dPad",
1049
+ "hw.gps",
1050
+ "hw.gpu.enabled",
1051
+ "hw.gyroscope",
1052
+ "hw.keyboard",
1053
+ "hw.mainKeys",
1054
+ "hw.rotaryInput",
1055
+ "hw.trackBall",
1056
+ "showDeviceFrame"
1057
+ ]) ANDROID_AVD_CONFIG_RULES[key] = YES_NO_AVD_RULE;
1058
+ const ANDROID_AVD_CONFIG_KEYS = Object.freeze(Object.keys(ANDROID_AVD_CONFIG_RULES).toSorted());
1059
+ const ANDROID_AVD_CONFIG_HELP = Object.freeze(ANDROID_AVD_CONFIG_KEYS.map((key) => `${key}: ${ANDROID_AVD_CONFIG_RULES[key].help}`));
1060
+ function isSafeAndroidAvdConfigKey(key) {
1061
+ return Object.hasOwn(ANDROID_AVD_CONFIG_RULES, key);
1062
+ }
1063
+ function normalizedAndroidAvdConfigEntry(key, value) {
1064
+ if (typeof key !== "string" || key !== key.trim() || !isSafeAndroidAvdConfigKey(key)) throw new Error(`Unsupported android.avdConfig key ${JSON.stringify(key)}. Use a documented safe hardware override; identity, path, storage, image, snapshot, and boot-lifecycle keys are protected.`);
1065
+ if (typeof value === "string" && /[\r\n\0]/.test(value)) throw new Error(`Invalid android.avdConfig value for ${key}: expected one line.`);
1066
+ try {
1067
+ return [key, ANDROID_AVD_CONFIG_RULES[key].parse(value)];
1068
+ } catch (error) {
1069
+ throw new Error(`Invalid android.avdConfig value for ${key}: ${String(error?.message || error)}.`, { cause: error });
1070
+ }
1071
+ }
1072
+ function parseAndroidAvdConfigIni(contents) {
1073
+ const entries = {};
1074
+ const lines = contents.replace(/^\uFEFF/, "").split(/\r?\n/);
1075
+ for (let index = 0; index < lines.length; index++) {
1076
+ const line = String(lines[index] ?? "").trim();
1077
+ if (!line || line.startsWith("#") || line.startsWith(";")) continue;
1078
+ const separator = line.indexOf("=");
1079
+ if (separator <= 0) throw new Error(`Invalid android.avdConfigFile line ${index + 1}: expected key=value.`);
1080
+ const [key, value] = normalizedAndroidAvdConfigEntry(line.slice(0, separator), line.slice(separator + 1).trim());
1081
+ if (Object.hasOwn(entries, key)) throw new Error(`Invalid android.avdConfigFile line ${index + 1}: duplicate key ${key}.`);
1082
+ entries[key] = value;
1083
+ }
1084
+ return entries;
1085
+ }
1086
+ function pathEscapesRoot(root, path) {
1087
+ const child = relative$1(root, path);
1088
+ return child === ".." || child.startsWith(`..${sep$1}`) || isAbsolute$1(child);
1089
+ }
1090
+ function androidAvdConfigSetting(settings, settingsRoot) {
1091
+ if (!isPlainObject(settings) || !isPlainObject(settings.android)) return {};
1092
+ const android = settings.android;
1093
+ let entries = {};
1094
+ if ("avdConfigFile" in android) {
1095
+ if (typeof android.avdConfigFile !== "string" || !android.avdConfigFile.trim() || android.avdConfigFile !== android.avdConfigFile.trim() || /[\r\n\0]/.test(android.avdConfigFile) || isAbsolute$1(android.avdConfigFile)) throw new Error("Invalid android.avdConfigFile setting. Expected a relative file path inside the app directory.");
1096
+ try {
1097
+ const root = realpathSync$1(settingsRoot);
1098
+ const candidate = resolve$1(root, android.avdConfigFile);
1099
+ if (pathEscapesRoot(root, candidate)) throw new Error("path escapes the settings root");
1100
+ const path = realpathSync$1(candidate);
1101
+ if (pathEscapesRoot(root, path)) throw new Error("symlink target escapes the settings root");
1102
+ const stat = statSync$1(path);
1103
+ if (!stat.isFile()) throw new Error("path is not a regular file");
1104
+ if (stat.size > 65536) throw new Error("file exceeds 64 KiB");
1105
+ const contents = readFileSync$1(path, "utf8");
1106
+ if (Buffer.byteLength(contents) > 65536) throw new Error("file exceeds 64 KiB");
1107
+ entries = parseAndroidAvdConfigIni(contents);
1108
+ } catch (error) {
1109
+ throw new Error(`Could not read valid android.avdConfigFile ${android.avdConfigFile}: ${String(error?.message || error)}`, { cause: error });
1110
+ }
1111
+ }
1112
+ if ("avdConfig" in android) {
1113
+ if (!isPlainObject(android.avdConfig)) throw new Error("Invalid android.avdConfig setting. Expected an object of native AVD key/value pairs.");
1114
+ for (const [key, value] of Object.entries(android.avdConfig)) {
1115
+ const [normalizedKey, normalizedValue] = normalizedAndroidAvdConfigEntry(key, value);
1116
+ entries[normalizedKey] = normalizedValue;
1117
+ }
1118
+ }
1119
+ return entries;
1120
+ }
1121
+ function androidAvdConfigSettingError(settings, projectPath) {
1122
+ try {
1123
+ androidAvdConfigSetting(settings, projectPath);
1124
+ return null;
1125
+ } catch (error) {
1126
+ return String(error?.message || error);
1127
+ }
1128
+ }
1129
+ function iosSimSlimProfileSetting(settings, settingsRoot) {
1130
+ if (!isPlainObject(settings) || !isPlainObject(settings.ios) || !("simslimProfile" in settings.ios)) return null;
1131
+ const value = settings.ios.simslimProfile;
1132
+ if (typeof value !== "string" || !value.trim() || value !== value.trim() || /[\r\n\0]/.test(value) || isAbsolute$1(value)) throw new Error("Invalid ios.simslimProfile setting. Expected a relative JSON file path inside the app directory.");
1133
+ try {
1134
+ const root = realpathSync$1(settingsRoot);
1135
+ const candidate = resolve$1(root, value);
1136
+ if (pathEscapesRoot(root, candidate)) throw new Error("path escapes the settings root");
1137
+ const path = realpathSync$1(candidate);
1138
+ if (pathEscapesRoot(root, path)) throw new Error("symlink target escapes the settings root");
1139
+ const stat = statSync$1(path);
1140
+ if (!stat.isFile()) throw new Error("path is not a regular file");
1141
+ if (stat.size > 65536) throw new Error("file exceeds 64 KiB");
1142
+ return path;
1143
+ } catch (error) {
1144
+ throw new Error(`Could not read ios.simslimProfile ${value}: ${String(error?.message || error)}`, { cause: error });
1145
+ }
1146
+ }
1147
+ function iosString(settings, key) {
1148
+ if (!isPlainObject(settings) || !isPlainObject(settings.ios)) return void 0;
1149
+ return settings.ios[key];
1150
+ }
1151
+ function iosSigningIdentitySetting(settings) {
1152
+ const raw = iosString(settings, "signingIdentity");
1153
+ return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : null;
1154
+ }
1155
+ function iosSigningIdentitySettingError(settings) {
1156
+ const raw = iosString(settings, "signingIdentity");
1157
+ if (raw === void 0) return null;
1158
+ if (typeof raw !== "string" || raw.trim() === "" || /[\r\n\0]/.test(raw)) return `Invalid ios.signingIdentity setting ${JSON.stringify(raw)}. Expected the identity name \`security find-identity -v -p codesigning\` prints, such as "Apple Development: Jane (TEAMID5678)".`;
1159
+ return null;
1160
+ }
1161
+ const SIGNING_IDENTITY_SHA1 = /^[0-9A-Fa-f]{40}$/;
1162
+ function iosSigningIdentitySha1Setting(settings) {
1163
+ const raw = iosString(settings, "signingIdentitySha1");
1164
+ return typeof raw === "string" && SIGNING_IDENTITY_SHA1.test(raw.trim()) ? raw.trim().toUpperCase() : null;
1165
+ }
1166
+ function iosSigningIdentitySha1SettingError(settings) {
1167
+ const raw = iosString(settings, "signingIdentitySha1");
1168
+ if (raw === void 0) return null;
1169
+ if (typeof raw !== "string" || !SIGNING_IDENTITY_SHA1.test(raw.trim())) return `Invalid ios.signingIdentitySha1 setting ${JSON.stringify(raw)}. Expected the 40-character hex SHA-1 hash \`security find-identity -v -p codesigning\` prints beside the identity name.`;
1170
+ return null;
1171
+ }
1172
+ const LAN_HOST = /^(?!-)[A-Za-z0-9-]+(?:\.(?!-)[A-Za-z0-9-]+)*$/;
1173
+ function iosLanHostSetting(settings) {
1174
+ const raw = iosString(settings, "lanHost");
1175
+ return typeof raw === "string" && LAN_HOST.test(raw.trim()) ? raw.trim() : null;
1176
+ }
1177
+ function iosLanHostSettingError(settings) {
1178
+ const raw = iosString(settings, "lanHost");
1179
+ if (raw === void 0) return null;
1180
+ if (typeof raw !== "string" || !LAN_HOST.test(raw.trim())) return `Invalid ios.lanHost setting ${JSON.stringify(raw)}. Expected a bare address or hostname the phone can reach, such as "192.168.1.42" -- never a URL, a scheme, or a port.`;
1181
+ return null;
1182
+ }
1183
+ function unknownSettingKeys(settings, prefix = "") {
1184
+ if (!isPlainObject(settings)) return [];
1185
+ const unknown = [];
1186
+ for (const [key, value] of Object.entries(settings)) {
1187
+ const path = prefix ? `${prefix}.${key}` : key;
1188
+ if (path === "$schema") continue;
1189
+ if (KNOWN_SETTINGS.has(path) && (!isPlainObject(value) || ![...KNOWN_SETTINGS].some((k) => k.startsWith(`${path}.`)))) continue;
1190
+ if (isPlainObject(value) && [...KNOWN_SETTINGS].some((k) => k.startsWith(`${path}.`))) {
1191
+ unknown.push(...unknownSettingKeys(value, path));
1192
+ continue;
1193
+ }
1194
+ unknown.push(path);
1195
+ }
1196
+ return unknown;
1197
+ }
1198
+ function readCommittedSettings(directory) {
1199
+ if (!directory) return {};
1200
+ const p = join$1(directory, ".stim.json");
1201
+ if (!existsSync$1(p)) return {};
1202
+ try {
1203
+ const parsed = JSON.parse(readFileSync$1(p, "utf-8"));
1204
+ return isPlainObject(parsed) ? parsed : {};
1205
+ } catch {
1206
+ return {};
1207
+ }
1208
+ }
1209
+ function setSettingValueAt(settings, path, value) {
1210
+ const segments = path.split(".");
1211
+ let node = settings;
1212
+ for (const segment of segments.slice(0, -1)) {
1213
+ const next = node[segment];
1214
+ if (!isPlainObject(next)) node[segment] = {};
1215
+ node = node[segment];
1216
+ }
1217
+ node[segments[segments.length - 1]] = value;
1218
+ }
1219
+ const MACHINE_READABLE_SETTINGS = SETTINGS.filter((setting) => isLayeredSetting(setting) && setting.scopes.includes("machine") && !setting.key.startsWith("optimizations."));
1220
+ function machineLayerSettings(machine) {
1221
+ const settings = {};
1222
+ const optimizations = settingValueAt(machine, "optimizations");
1223
+ if (optimizations !== void 0) settings.optimizations = optimizations;
1224
+ for (const setting of MACHINE_READABLE_SETTINGS) {
1225
+ const value = settingValueAt(machine, setting.key);
1226
+ if (value !== void 0) setSettingValueAt(settings, setting.key, value);
1227
+ }
1228
+ return Object.keys(settings).length > 0 ? settings : null;
1229
+ }
1230
+ function settingsLayers({ projectPath, gitCommonDir, repoRoot }) {
1231
+ const machine = loadConfig$1();
1232
+ const machineFile = getConfigPath$1();
1233
+ const committedDir = projectPath ?? repoRoot;
1234
+ const machineSettings = machineLayerSettings(machine);
1235
+ return [
1236
+ projectPath ? {
1237
+ scope: "workspace",
1238
+ file: `${machineFile} (projects["${projectPath}"].settings)`,
1239
+ settings: getProjectSettings(projectPath)
1240
+ } : null,
1241
+ gitCommonDir ? {
1242
+ scope: "repo",
1243
+ file: `${machineFile} (repos["${gitCommonDir}"].settings)`,
1244
+ settings: getRepoSettings(gitCommonDir)
1245
+ } : null,
1246
+ committedDir ? {
1247
+ scope: "committed",
1248
+ file: join$1(committedDir, ".stim.json"),
1249
+ settings: readCommittedSettings(committedDir)
1250
+ } : null,
1251
+ machineSettings ? {
1252
+ scope: "machine",
1253
+ file: machineFile,
1254
+ settings: machineSettings
1255
+ } : null
1256
+ ].filter((layer) => layer !== null);
1257
+ }
1258
+ function settingOrigin(layers, dottedKey) {
1259
+ for (const layer of layers) {
1260
+ const value = settingValueAt(layer.settings, dottedKey);
1261
+ if (value !== void 0) return {
1262
+ file: layer.file,
1263
+ value
1264
+ };
1265
+ }
1266
+ return null;
1267
+ }
1268
+ /** The layer a resolved setting's value came from, or null when no layer set it (a default or a flag). */
1269
+ function settingOriginScope(layers, dottedKey) {
1270
+ for (const layer of layers) if (settingValueAt(layer.settings, dottedKey) !== void 0) return layer.scope;
1271
+ return null;
1272
+ }
1273
+ function settingFile(context, dottedKey) {
1274
+ return settingOrigin(settingsLayers(context), dottedKey)?.file ?? null;
1275
+ }
1276
+ function resolveSettings(context) {
1277
+ return mergeSettingsLayers(settingsLayers(context).map((layer) => layer.settings));
1278
+ }
1279
+ function settingsForProject(root) {
1280
+ return resolveSettings({
1281
+ projectPath: root,
1282
+ gitCommonDir: gitCommonDir(root),
1283
+ repoRoot: repoRoot(root) ?? root
1284
+ });
1285
+ }
1286
+ function projectOptimizations(root) {
1287
+ return resolveOptimizations(settingsForProject(root));
1288
+ }
1289
+ function projectMetroSharedCache(root) {
1290
+ return resolveMetroSharedCache(settingsForProject(root));
1291
+ }
1292
+ function cacheBlock(layer) {
1293
+ if (!isPlainObject(layer) || !isPlainObject(layer.cache)) return null;
1294
+ return layer.cache;
1295
+ }
1296
+ function resolveCacheProviderConfig({ projectPath, gitCommonDir, repoRoot }) {
1297
+ const layers = [
1298
+ {
1299
+ settings: projectPath ? getProjectSettings(projectPath) : {},
1300
+ baseDir: projectPath ?? null
1301
+ },
1302
+ {
1303
+ settings: gitCommonDir ? getRepoSettings(gitCommonDir) : {},
1304
+ baseDir: repoRoot ?? projectPath ?? null
1305
+ },
1306
+ {
1307
+ settings: readCommittedSettings(projectPath),
1308
+ baseDir: projectPath ?? null
1309
+ }
1310
+ ];
1311
+ const references = layers.map((layer) => {
1312
+ const reference = cacheBlock(layer.settings)?.provider;
1313
+ return typeof reference === "string" && reference.trim() !== "" ? reference.trim() : null;
1314
+ });
1315
+ const selected = layers.findIndex((layer, index) => references[index] !== null && layer.baseDir !== null);
1316
+ if (selected === -1) return null;
1317
+ const provider = references[selected];
1318
+ const baseDir = layers[selected].baseDir;
1319
+ return {
1320
+ provider,
1321
+ options: mergeSettingsLayers(layers.map((layer, index) => {
1322
+ const namesProvider = references[index] === provider && layer.baseDir === baseDir;
1323
+ const overridesAbove = index < selected && references[index] === null;
1324
+ if (!namesProvider && !overridesAbove) return null;
1325
+ const block = cacheBlock(layer.settings)?.options;
1326
+ return isPlainObject(block) ? block : null;
1327
+ })),
1328
+ baseDir
1329
+ };
1330
+ }
1331
+ function cacheProviderSettingError(settings) {
1332
+ const block = settings.cache;
1333
+ if (!isPlainObject(block)) return block === void 0 ? null : `Invalid cache setting ${JSON.stringify(block)}. Expected an object with provider and options.`;
1334
+ if ("provider" in block && (typeof block.provider !== "string" || block.provider.trim() === "")) return `Invalid cache.provider setting ${JSON.stringify(block.provider)}. Expected a module path or package name.`;
1335
+ if ("options" in block && !isPlainObject(block.options)) return `Invalid cache.options setting ${JSON.stringify(block.options)}. Expected an object.`;
1336
+ return null;
1337
+ }
1338
+ function parseIosRemote(remote) {
1339
+ const selected = remote.trim();
1340
+ const reserved = selected.toLowerCase();
1341
+ return REMOTE_DEVICE_BACKENDS.includes(reserved) ? {
1342
+ kind: "backend",
1343
+ backend: reserved
1344
+ } : {
1345
+ kind: "machine",
1346
+ machine: reserved === "auto" ? "auto" : selected
1347
+ };
1348
+ }
1349
+ function remoteIosSetting(settings) {
1350
+ const ios = settings.ios;
1351
+ return isPlainObject(ios) && typeof ios.remote === "string" ? parseIosRemote(ios.remote) : null;
1352
+ }
1353
+ function remoteAndroidSetting(settings) {
1354
+ return remoteSetting(settings, "android");
1355
+ }
1356
+ function remoteSetting(settings, platform) {
1357
+ const block = settings[platform];
1358
+ if (!isPlainObject(block)) return null;
1359
+ const remote = block.remote;
1360
+ return typeof remote === "string" && REMOTE_DEVICE_BACKENDS.includes(remote) ? remote : null;
1361
+ }
1362
+ function minutesSetting(settings, block, name) {
1363
+ const parent = settings[block];
1364
+ const value = isPlainObject(parent) ? parent[name] : void 0;
1365
+ const setting = settingDefinition(`${block}.${name}`);
1366
+ return setting && value !== void 0 && settingValueError(setting, value) === null ? value : Number(setting?.default);
1367
+ }
1368
+ function metroIdleStopMinutesSetting(settings) {
1369
+ return minutesSetting(settings, "metro", "idleStopMinutes");
1370
+ }
1371
+ function deviceIdleShutdownMinutesSetting(settings) {
1372
+ return minutesSetting(settings, "devices", "idleShutdownMinutes");
1373
+ }
1374
+ function tunnelModeSetting(settings) {
1375
+ const block = settings.metro;
1376
+ if (typeof block !== "object" || block === null) return null;
1377
+ const mode = block.tunnel;
1378
+ return typeof mode === "string" && TUNNEL_MODES.includes(mode) ? mode : null;
1379
+ }
1380
+ function metroTunnelSettingError(settings) {
1381
+ const block = settings.metro;
1382
+ if (!isPlainObject(block) || !("ngrokUrl" in block)) return null;
1383
+ if (block.tunnel !== "ngrok") return "metro.ngrokUrl requires metro.tunnel to be \"ngrok\".";
1384
+ if (normalizedHttpsUrl(block.ngrokUrl) === null) return "metro.ngrokUrl must be a valid HTTPS URL.";
1385
+ return null;
1386
+ }
1387
+ function metroPortSetting(projectPath, env = process.env) {
1388
+ const setting = settingDefinition("metro.port");
1389
+ const fromEnv = env.STIM_METRO_PORT?.trim();
1390
+ const value = fromEnv ? coerceSettingText(setting, fromEnv) : settingValueAt(settingsForProject(projectPath), "metro.port");
1391
+ if (value === void 0) return {
1392
+ port: null,
1393
+ error: null
1394
+ };
1395
+ const problem = settingValueError(setting, value);
1396
+ if (problem === null) return {
1397
+ port: value,
1398
+ error: null
1399
+ };
1400
+ return {
1401
+ port: null,
1402
+ error: `Invalid ${fromEnv ? `STIM_METRO_PORT value ${JSON.stringify(env.STIM_METRO_PORT)}` : `metro.port setting ${JSON.stringify(value)}`}. Expected ${problem}.`
1403
+ };
1404
+ }
1405
+ function publicUrlSetting(settings) {
1406
+ const block = settings.metro;
1407
+ if (typeof block !== "object" || block === null) return null;
1408
+ const url = block.publicUrl;
1409
+ return typeof url === "string" && url.trim() ? url : null;
1410
+ }
1411
+ function metroWarmupUrlSetting(settings, platform) {
1412
+ const value = settingValueAt(settings, `metro.warmupUrl.${platform}`);
1413
+ return typeof value === "string" ? value : null;
1414
+ }
1415
+ function ngrokUrlSetting(settings) {
1416
+ const block = settings.metro;
1417
+ if (typeof block !== "object" || block === null) return null;
1418
+ if (block.tunnel !== "ngrok") return null;
1419
+ return normalizedHttpsUrl(block.ngrokUrl);
1420
+ }
1421
+ function normalizedHttpsUrl(raw) {
1422
+ if (typeof raw !== "string" || !raw.trim()) return null;
1423
+ try {
1424
+ return new URL(raw.trim()).protocol === "https:" ? raw.trim().replace(/\/+$/, "") : null;
1425
+ } catch {
1426
+ return null;
1427
+ }
1428
+ }
1429
+ function webSettings(settings) {
1430
+ const url = settingValueAt(settings, "web.url");
1431
+ const viewport = settingValueAt(settings, "web.viewport");
1432
+ return {
1433
+ url: typeof url === "string" ? url : null,
1434
+ ignoreCertificateErrors: settingValueAt(settings, "web.ignoreCertificateErrors") === true,
1435
+ viewport: WEB_VIEWPORTS.includes(viewport) ? viewport : "desktop"
1436
+ };
1437
+ }
1438
+ //#endregion
1439
+ export { cloneIgnoredEntries as $, readCommittedSettings as A, settingsLayers as B, metroTunnelSettingError as C, filesystemDevice as Ct, projectMetroSharedCache as D, parseIosRemote as E, temporaryRoot as Et, settingFile as F, detectProviders as G, unknownSettingKeys as H, settingOrigin as I, artifactCachePolicy as J, planMetroReach as K, settingOriginScope as L, remoteIosSetting as M, resolveCacheProviderConfig as N, projectOptimizations as O, resolveSettings as P, branchExists as Q, settingShapeErrors as R, metroPortSetting as S, warmWorktreePaths as St, ngrokUrlSetting as T, removeTemporaryEntry as Tt, webSettings as U, tunnelModeSetting as V, PUBLIC_METRO_ENV as W, optimizationBuildProfile as X, compilerCacheFallbackMessage as Y, resolveOptimizations as Z, iosSigningIdentitySha1Setting as _, resolveFullRef as _t, SETTING_SHAPE_REMEDY as a, gitCommonDirOnDisk as at, mergeSettingsLayers as b, sourceCheckoutOf as bt, androidDataPartitionSizeBytes as c, hasUncommittedWork as ct, cacheProviderSettingError as d, listWorktrees as dt, deleteBranch as et, deviceIdleShutdownMinutesSetting as f, locallyKnownUpstream as ft, iosSigningIdentitySettingError as g, repoRoot as gt, iosSigningIdentitySetting as h, removeWorktree as ht, SETTINGS_WITH_RESOLVE_TIME_FALLBACK as i, gitCommonDir as it, remoteAndroidSetting as j, publicUrlSetting as k, androidDataPartitionSizeGbSetting as l, isPodInstallChurn as lt, iosLanHostSettingError as m, readWorktreeExclude as mt, PATH_SETTINGS as n, dirtyFingerprintFiles as nt, androidAvdConfigSetting as o, hasPopulatedSubmodules as ot, iosLanHostSetting as p, podsOutOfSync as pt, resolveTailscaleBinary as q, REMOTE_DEVICE_BACKENDS as r, dirtyPaths as rt, androidAvdConfigSettingError as s, hasRemote as st, ANDROID_AVD_CONFIG_HELP as t, depsOutOfSync as tt, androidDataPartitionSizeGbSettingError as u, linkedWorktreesOnDisk as ut, iosSigningIdentitySha1SettingError as v, resolveSourceCheckout as vt, metroWarmupUrlSetting as w, makeTemporaryDirectory as wt, metroIdleStopMinutesSetting as x, unpushedCommits as xt, iosSimSlimProfileSetting as y, restoreFile as yt, settingValueAt as z };