stim 1.18.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.
- package/README.md +21 -4
- package/dist/{activity-BxnkDZDi.mjs → activity-BrGh9EAI.mjs} +3 -3
- package/dist/{agent-device-usage-output-CZ66YA4T.mjs → agent-device-usage-output-DVyv_hWj.mjs} +7 -7
- package/dist/android-0vv-OruW.mjs +6 -0
- package/dist/android-Brx6EKvJ.mjs +1258 -0
- package/dist/{android-DC3ZLuw0.mjs → android-BsYNR81W.mjs} +103 -45
- package/dist/{android-CBwCNicO.mjs → android-CK3fpEyZ.mjs} +2 -2
- package/dist/{android-cas-BBFyr9CA.mjs → android-cas-DCAuXryb.mjs} +9 -5
- package/dist/android-cas-compiler.mjs +1 -1
- package/dist/api-run.mjs +159 -8
- package/dist/{api-D0VtFhqk.mjs → api-zuX3ynYN.mjs} +9 -7
- package/dist/api.d.mts +104 -4
- package/dist/api.mjs +1 -1
- package/dist/{app-install-D-PUENug.mjs → app-install-0LgaQRT7.mjs} +39 -18
- package/dist/artifact-CnC0mfov.mjs +807 -0
- package/dist/artifact-DClXJNWx.mjs +772 -0
- package/dist/artifact-lifecycle-DzPpBYvK.mjs +30 -0
- package/dist/{web-DTU0uiWj.mjs → browser-web-CzBmhmgs.mjs} +136 -174
- package/dist/{budget-DIhL2BKx.mjs → budget-BONq9vXY.mjs} +3096 -3244
- package/dist/build-BixRI6sB.mjs +462 -0
- package/dist/build-plan-CP0tckNU.mjs +553 -0
- package/dist/{cache-manifest-BI7ozfmH.mjs → cache-manifest-qA3dT2x2.mjs} +1 -1
- package/dist/cache-manifest.mjs +1 -1
- package/dist/ccache-CjHQ5F8g.mjs +142 -0
- package/dist/{chrome-DJnmnS5q.mjs → chrome-Dp2lMLBe.mjs} +1 -1
- package/dist/{cli-DNJMwADx.mjs → cli-B2R8wH1F.mjs} +20 -20
- package/dist/cli.mjs +1 -1
- package/dist/{client-DzhapoMB.mjs → client-BLy1Pw2V.mjs} +172 -165
- package/dist/collector-run.d.mts +10 -0
- package/dist/collector-run.mjs +7 -7
- package/dist/{command-output-CBtzyIgF.mjs → command-output-BjPN99ZB.mjs} +2 -2
- package/dist/{config-CgYMahz7.mjs → config-D0LhTY3S.mjs} +13 -13
- package/dist/{created-devices-DKbeWsiK.mjs → created-devices-HU_DAb_Q.mjs} +7 -6
- package/dist/{dependency-state-xKoOypBm.mjs → dependency-state-B2m8eqsS.mjs} +2 -2
- package/dist/{detached-entry-BLfcgc4O.mjs → detached-entry-BXq4fIl-.mjs} +1 -1
- package/dist/{dev-client-CLnIH02x.mjs → dev-client-ClATaJoH.mjs} +2 -2
- package/dist/{device-D7oCHGWi.mjs → device-B7lsC0K1.mjs} +8 -8
- package/dist/device-DVCwAFlX.mjs +727 -0
- package/dist/{device-capacity-EUOOYr8T.mjs → device-capacity-Ca_ke1LE.mjs} +25 -15
- package/dist/device-host-worker.mjs +38 -18
- package/dist/{device-ios-6MewKKnc.mjs → device-ios-CRUqPxKw.mjs} +12 -11
- package/dist/{device-lease-Co-EwifR.mjs → device-lease-DHOBXGgS.mjs} +135 -22
- package/dist/{device-lease-run-w1xhCqIW.mjs → device-lease-run-CvDeHTkG.mjs} +4 -4
- package/dist/{device-pool-F5WvRPtD.mjs → device-pool-L2-5GxMy.mjs} +3 -3
- package/dist/{device-remote-tHbd1fKy.mjs → device-remote-Bmg42063.mjs} +17 -69
- package/dist/doctor-C2MSuMMg.mjs +515 -0
- package/dist/{doctor-B9ckYUoU.mjs → doctor-DPAqhVCK.mjs} +334 -721
- package/dist/doctor-watchman-CB4NnVZm.mjs +260 -0
- package/dist/{simslim-EHBQ2R4G.mjs → eas-build-Dw_ec2Km.mjs} +6 -152
- package/dist/{stim-installations-aR_WsNOA.mjs → eas-session-ledger-DZlC-Fq3.mjs} +57 -4
- package/dist/{error-diagnostics-BRXOqDzG.mjs → error-diagnostics-Du7I_GuY.mjs} +17 -12
- package/dist/errors-DzkMYh8Q.mjs +205 -0
- package/dist/{exec-DWhCZDGm.mjs → exec-B81Qodjn.mjs} +69 -15
- package/dist/{gc-Dh6mXOKC.mjs → gc-PWp34zGb.mjs} +23 -22
- package/dist/{gradle-C6GKnunQ.mjs → gradle-C2OEKCCI.mjs} +50 -125
- package/dist/{guide-tm6igAaa.mjs → guide-Dai9lphv.mjs} +862 -538
- package/dist/{hosted-android-Bv285t_t.mjs → hosted-android-DQ-Agsd8.mjs} +5 -5
- package/dist/{hosted-client-C-rMtppF.mjs → hosted-client-Io01s5By.mjs} +9 -3
- package/dist/{hosted-ios-D5mNmtj4.mjs → hosted-ios-DIxFnYp-.mjs} +3 -3
- package/dist/{hosted-logs-DN8Iz4mJ.mjs → hosted-logs-DP-ABU_M.mjs} +8 -8
- package/dist/{hosted-macos-B-wEjlwG.mjs → hosted-macos-Aziw9T_9.mjs} +7 -6
- package/dist/{hosted-native-DeeOG8xZ.mjs → hosted-native-BdAQi21M.mjs} +23 -16
- package/dist/{idle-shutdown-jjtxAn5C.mjs → idle-shutdown-DdMqoSUB.mjs} +7 -7
- package/dist/ios-BZ9dg2Fr.mjs +9 -0
- package/dist/{ios-B3SbOWdw.mjs → ios-CXLIUxXJ.mjs} +180 -24
- package/dist/ios-CZuv6gRv.mjs +1112 -0
- package/dist/{ios-Bvbf0SGY.mjs → ios-VGvNdsv2.mjs} +1 -1
- package/dist/{ios-device-Bn7jWDB1.mjs → ios-device-B1B14xND.mjs} +1 -1
- package/dist/{ios-device-vOaByMhw.mjs → ios-device-BreZOpM0.mjs} +3 -3
- package/dist/ios-project-B9drt_aY.mjs +197 -0
- package/dist/{ios-state-B2pES6Vs.mjs → ios-state-CjrL2qv-.mjs} +1 -1
- package/dist/js-swap-C8icNYIY.mjs +432 -0
- package/dist/launch-DxH0THLP.mjs +745 -0
- package/dist/launch-YrbqaIFj.mjs +1530 -0
- package/dist/{launch-verify-hskNcWkZ.mjs → launch-verify-BzRdbuO1.mjs} +26 -7
- package/dist/{logs-1Ylc1Ri6.mjs → logs-DHaRNfg7.mjs} +11 -11
- package/dist/macos-C0iljhsN.mjs +2 -0
- package/dist/{macos-DF6C7wUU.mjs → macos-V_3Ui7g4.mjs} +125 -155
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.mjs +13 -10
- package/dist/{metro-Dh8T4Ytd.mjs → metro-D_j9sN-B.mjs} +209 -43
- package/dist/{metro-gateway-CEUw9RMH.mjs → metro-gateway-BBJldOdi.mjs} +2 -2
- package/dist/miss-reason-BPAzZS0r.mjs +238 -0
- package/dist/{named-ports-BFIz1Shz.mjs → named-ports-C2lvoPsb.mjs} +12 -5
- package/dist/native-android-lHzaew7r.mjs +226 -0
- package/dist/native-gradle-inputs-BQnIdAlL.mjs +356 -0
- package/dist/{native-runtime-D_603JiC.mjs → native-runtime-BRmWL7ru.mjs} +9 -8
- package/dist/native-xcode-inputs-eutS19jU.mjs +307 -0
- package/dist/native-xcode-ios-DP3cHLwu.mjs +434 -0
- package/dist/{ndjson-LGZAMsqa.mjs → ndjson-GOnbyvso.mjs} +6 -6
- package/dist/offload-worker.d.mts +12 -6
- package/dist/offload-worker.mjs +409 -73
- package/dist/{ownership-D6Se7gg_.mjs → ownership-DXzMMSx6.mjs} +2 -2
- package/dist/{placement-log-BcPhRwR4.mjs → placement-log-reFbjqEr.mjs} +7 -3
- package/dist/plan-BDk2AeY_.mjs +260 -0
- package/dist/plan-placement-D_YYlE6N.mjs +73 -0
- package/dist/{ports-BLTtfBq-.mjs → ports-jzvAJXXL.mjs} +3 -3
- package/dist/{prebuild-juCyreaR.mjs → prebuild-Bw44QxVh.mjs} +4 -4
- package/dist/{preview-D5V07Nrl.mjs → preview-BaBJ72Al.mjs} +51 -34
- package/dist/project-B8_duAzW.mjs +1067 -0
- package/dist/project-plan-BPwbLwQu.mjs +10 -0
- package/dist/{pull-request-u4vp7zmK.mjs → pull-request-vOhLNmIb.mjs} +1 -1
- package/dist/pull-requests.mjs +1 -1
- package/dist/react-native-android-chm1DMlA.mjs +921 -0
- package/dist/react-native-build-oAw6gQeW.mjs +239 -0
- package/dist/react-native-doctor-DYXoh-6I.mjs +78 -0
- package/dist/react-native-ios-SCTK_CT5.mjs +595 -0
- package/dist/{recordings-BAWltsVt.mjs → recordings-Bk-VivUV.mjs} +1 -1
- package/dist/{reload-DvyWzVjJ.mjs → reload-D34GkY78.mjs} +17 -14
- package/dist/remote-BU9UyhCJ.mjs +192 -0
- package/dist/{remote-cache-BGg7aGIZ.mjs → remote-cache-Bsts6f6Y.mjs} +5 -5
- package/dist/result-BDoRk1k3.mjs +186 -0
- package/dist/{run-vhtDgpHO.mjs → run-qvff78hv.mjs} +3 -3
- package/dist/{server-bare-B9E7S1aR.mjs → server-bare-dWjAarki.mjs} +6 -6
- package/dist/server-command-7XIKlWtO.mjs +69 -0
- package/dist/{server-expo-DoBHhuzK.mjs → server-expo-CsYu2aKL.mjs} +107 -82
- package/dist/server-expo-DV43vep4.mjs +2 -0
- package/dist/settings-Bbztm4oV.mjs +1503 -0
- package/dist/{settings-Byp2AQ_E.mjs → settings-CT3E71-i.mjs} +6 -7
- package/dist/{state-lKh8shBp.d.mts → settings-vk3mJGSm.d.mts} +4 -2
- package/dist/settings.schema.json +156 -6
- package/dist/simslim-ChMfe8rg.mjs +149 -0
- package/dist/{slot-launch-C1nqh4mu.mjs → slot-launch-B9hn96wd.mjs} +6 -6
- package/dist/{stage-g_WqWeaE.mjs → stage-BNKKdDNc.mjs} +6 -3
- package/dist/start-CbQCSnXQ.mjs +2 -0
- package/dist/{start-D-IeOyoK.mjs → start-CdvcTaXr.mjs} +113 -77
- package/dist/{state-xnqpx3AD.mjs → state-Boo0IMJq.mjs} +4 -2
- package/dist/{state-BG-aAk4k.mjs → state-DI84A-uu.mjs} +29 -10
- package/dist/{stats-DY6ekz8F.mjs → stats-BIIDrjA8.mjs} +6 -6
- package/dist/{status-BEeL6IKz.mjs → status-BDGHwkCs.mjs} +3 -1
- package/dist/{status-DCjZeE-K.mjs → status-DARMkc9q.mjs} +80 -294
- package/dist/status-watch-DvmI3SmN.mjs +296 -0
- package/dist/{stim-desktop-Behws_fi.mjs → stim-desktop-8GQX2OV8.mjs} +1 -1
- package/dist/{stop-DIJtD2GL.mjs → stop-CCZS-C9m.mjs} +2 -2
- package/dist/stop-WQPQHLuy.mjs +72 -0
- package/dist/{stop-BTjs3xbc.mjs → stop-iNemHsJC.mjs} +81 -33
- package/dist/supervisor-run.d.mts +7 -2
- package/dist/supervisor-run.mjs +32 -20
- package/dist/{support-Bwvbrq5-.mjs → support-CnE_SIjt.mjs} +17 -7
- package/dist/{support-BaayeYCf.mjs → support-DCco5fut.mjs} +61 -8
- package/dist/swiftpm-macos-afeNUExW.mjs +119 -0
- package/dist/{native-run-B06FkmZx.mjs → tailnet-CoJUb2tB.mjs} +113 -6
- package/dist/toolchain-BIupIro1.mjs +2 -0
- package/dist/{toolchain-Bnyd_K3A.mjs → toolchain-DqyDXs30.mjs} +225 -315
- package/dist/{trigger-CJMhwXe8.mjs → trigger-C0MWBi8O.mjs} +3 -3
- package/dist/trigger-SHLOWJ4O.mjs +2 -0
- package/dist/{warm-progress-CjB-MP6H.mjs → warm-progress-C3ASD6z5.mjs} +134 -37
- package/dist/{status-Dcr0Qexd.mjs → watchman-CQl51YZu.mjs} +75 -7
- package/dist/web-B3eldHFH.mjs +123 -0
- package/dist/web-BLXpOe6b.mjs +2 -0
- package/dist/web-run.mjs +10 -10
- package/dist/{workspace-state-C59dqG-Z.mjs → workspace-state-CSIFuuDa.mjs} +33 -33
- package/dist/{workspaces-xXItD8Dj.mjs → workspaces-D2xSHBDY.mjs} +1297 -1211
- package/dist/{worktree-BSYlofkq.mjs → worktree-BxDqaTev.mjs} +102 -63
- package/dist/worktree-q7_OZNY0.mjs +2 -0
- package/package.json +8 -6
- package/shim/bundle-response.cjs +2 -1
- package/shim/bundle-response.d.cts +1 -1
- package/shim/expo-metro-config.cjs +1 -1
- package/shim/native-android.gradle +101 -0
- package/dist/android-BKA16DTf.mjs +0 -4
- package/dist/android-BvwuMUJ9.mjs +0 -3572
- package/dist/guide-status-Bhf9SJXc.mjs +0 -125
- package/dist/ios-CPdkoKaa.mjs +0 -3953
- package/dist/ios-DNpdygiv.mjs +0 -7
- package/dist/macos-Ck7OBMNY.mjs +0 -2
- package/dist/metro-store-BAHFcJm6.mjs +0 -106
- package/dist/plan-placement-CtxkCO54.mjs +0 -1859
- package/dist/project-C3ChRLZy.mjs +0 -212
- package/dist/projects-DD7dBTCj.mjs +0 -92
- package/dist/server-expo-Db9HBUoa.mjs +0 -2
- package/dist/settings-CQyMElEP.mjs +0 -701
- package/dist/start-NTc1Ifqs.mjs +0 -2
- package/dist/stop-BGhKMOuH.mjs +0 -48
- package/dist/trigger-DWAq9hkw.mjs +0 -2
- package/dist/web-B6_bFDn7.mjs +0 -2
- package/dist/worktree-BJfnU6r5.mjs +0 -2
- package/dist/worktree-CsKYexJM.mjs +0 -753
|
@@ -0,0 +1,1503 @@
|
|
|
1
|
+
import { t as getExecutor } from "./exec-B81Qodjn.mjs";
|
|
2
|
+
import { _ as loadConfig$1, d as getConfigDir, f as getConfigPath$1, h as getRepoSettings, m as getProjectSettings } from "./config-D0LhTY3S.mjs";
|
|
3
|
+
import { accessSync, chmodSync, constants, lstatSync, mkdirSync, mkdtempSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, normalize, relative as relative$1, resolve as resolve$1, sep as sep$1 } from "path";
|
|
6
|
+
import { REMOTE_DEVICE_BACKENDS, SETTINGS, SETTING_GROUPS, TUNNEL_MODES, WEB_VIEWPORTS, acceptsShape, coerceSettingText, expectedShape, isLayeredSetting, settingDefinition, settingValueError } from "@stim-cli/core/state";
|
|
7
|
+
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";
|
|
8
|
+
import { buildCacheRoot } from "@stim-cli/core";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
//#region src/temporary.ts
|
|
11
|
+
function existingPath(path) {
|
|
12
|
+
const missing = [];
|
|
13
|
+
let current = resolve(path);
|
|
14
|
+
while (!lstatSync(current, { throwIfNoEntry: false })) {
|
|
15
|
+
missing.unshift(basename(current));
|
|
16
|
+
current = dirname(current);
|
|
17
|
+
}
|
|
18
|
+
const existing = realpathSync(current);
|
|
19
|
+
return {
|
|
20
|
+
existing,
|
|
21
|
+
resolved: join(existing, ...missing)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function filesystemDevice(path) {
|
|
25
|
+
return statSync(existingPath(path).existing).dev;
|
|
26
|
+
}
|
|
27
|
+
function outsideWorkingTree(path) {
|
|
28
|
+
let current = path;
|
|
29
|
+
while (true) {
|
|
30
|
+
if (lstatSync(join(current, ".git"), { throwIfNoEntry: false })) return false;
|
|
31
|
+
const parent = dirname(current);
|
|
32
|
+
if (parent === current) return true;
|
|
33
|
+
current = parent;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function inside(parent, path) {
|
|
37
|
+
const rel = relative(parent, path);
|
|
38
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
39
|
+
}
|
|
40
|
+
function temporaryRoot(near) {
|
|
41
|
+
const reference = existingPath(near);
|
|
42
|
+
const insideReference = (path) => inside(reference.resolved, path);
|
|
43
|
+
const override = process.env.STIM_TMPDIR ?? loadConfig$1()?.tempDir;
|
|
44
|
+
if (override !== void 0) {
|
|
45
|
+
if (typeof override !== "string" || !isAbsolute(override)) throw new Error("STIM_TMPDIR / tempDir must be an absolute directory path.");
|
|
46
|
+
const { existing, resolved } = existingPath(override);
|
|
47
|
+
if (!statSync(existing).isDirectory() || !outsideWorkingTree(existing)) throw new Error(`Temporary directory ${override} must be outside Git working trees.`);
|
|
48
|
+
if (insideReference(resolved)) throw new Error(`Temporary directory ${override} must not be inside ${near}.`);
|
|
49
|
+
accessSync(existing, constants.W_OK);
|
|
50
|
+
return resolved;
|
|
51
|
+
}
|
|
52
|
+
const { existing } = reference;
|
|
53
|
+
const cache = existingPath(buildCacheRoot()).resolved;
|
|
54
|
+
const device = statSync(existing).dev;
|
|
55
|
+
const candidates = [tmpdir()];
|
|
56
|
+
let current = existing === reference.resolved || !statSync(existing).isDirectory() ? dirname(existing) : existing;
|
|
57
|
+
while (statSync(current).dev === device) {
|
|
58
|
+
candidates.push(current);
|
|
59
|
+
const parent = dirname(current);
|
|
60
|
+
if (parent === current) break;
|
|
61
|
+
current = parent;
|
|
62
|
+
}
|
|
63
|
+
for (const candidate of candidates) try {
|
|
64
|
+
const path = realpathSync(candidate);
|
|
65
|
+
if (statSync(path).dev !== device || insideReference(path) || inside(cache, path) || !outsideWorkingTree(path)) continue;
|
|
66
|
+
accessSync(path, constants.W_OK);
|
|
67
|
+
return path;
|
|
68
|
+
} catch {}
|
|
69
|
+
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.`);
|
|
70
|
+
}
|
|
71
|
+
function makeTemporaryDirectory(near, prefix) {
|
|
72
|
+
const root = temporaryRoot(near);
|
|
73
|
+
mkdirSync(root, {
|
|
74
|
+
recursive: true,
|
|
75
|
+
mode: 448
|
|
76
|
+
});
|
|
77
|
+
return mkdtempSync(join(root, prefix));
|
|
78
|
+
}
|
|
79
|
+
function removeTemporaryEntry(path) {
|
|
80
|
+
const stat = lstatSync(path, { throwIfNoEntry: false });
|
|
81
|
+
if (stat?.isDirectory()) {
|
|
82
|
+
chmodSync(path, stat.mode | 448);
|
|
83
|
+
for (const entry of readdirSync(path, { withFileTypes: true })) if (entry.isDirectory()) removeTemporaryEntry(join(path, entry.name));
|
|
84
|
+
}
|
|
85
|
+
rmSync(path, {
|
|
86
|
+
recursive: true,
|
|
87
|
+
force: true
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/workspace/worktree.ts
|
|
92
|
+
const CARRY_SKIP_BASENAMES = /* @__PURE__ */ new Set([
|
|
93
|
+
".DerivedData",
|
|
94
|
+
".DS_Store",
|
|
95
|
+
".idea",
|
|
96
|
+
".jj"
|
|
97
|
+
]);
|
|
98
|
+
function isCarrySkipped(rel) {
|
|
99
|
+
return /(^|\/)android\/build\/generated\/autolinking(\/|$)/.test(rel) || String(rel).split("/").some((seg) => CARRY_SKIP_BASENAMES.has(seg));
|
|
100
|
+
}
|
|
101
|
+
function nativePath(path) {
|
|
102
|
+
return sep$1 === "/" ? path : path.replaceAll("/", sep$1);
|
|
103
|
+
}
|
|
104
|
+
function gitCommonDir(cwd) {
|
|
105
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
106
|
+
"-C",
|
|
107
|
+
cwd,
|
|
108
|
+
"rev-parse",
|
|
109
|
+
"--path-format=absolute",
|
|
110
|
+
"--git-common-dir"
|
|
111
|
+
]);
|
|
112
|
+
return out ? nativePath(out.trim()) : null;
|
|
113
|
+
}
|
|
114
|
+
function repoRoot(cwd) {
|
|
115
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
116
|
+
"-C",
|
|
117
|
+
cwd,
|
|
118
|
+
"rev-parse",
|
|
119
|
+
"--show-toplevel"
|
|
120
|
+
]);
|
|
121
|
+
return out ? nativePath(out.trim()) : null;
|
|
122
|
+
}
|
|
123
|
+
function locallyKnownUpstream(projectRoot) {
|
|
124
|
+
try {
|
|
125
|
+
const name = getExecutor().runFile("git", [
|
|
126
|
+
"-C",
|
|
127
|
+
projectRoot,
|
|
128
|
+
"rev-parse",
|
|
129
|
+
"--abbrev-ref",
|
|
130
|
+
"--symbolic-full-name",
|
|
131
|
+
"@{upstream}"
|
|
132
|
+
], { timeoutMs: 5e3 });
|
|
133
|
+
const counts = getExecutor().runFile("git", [
|
|
134
|
+
"-C",
|
|
135
|
+
projectRoot,
|
|
136
|
+
"rev-list",
|
|
137
|
+
"--left-right",
|
|
138
|
+
"--count",
|
|
139
|
+
"HEAD...@{upstream}"
|
|
140
|
+
], { timeoutMs: 5e3 }).trim().split(/\s+/).map(Number);
|
|
141
|
+
const ahead = counts[0] ?? NaN;
|
|
142
|
+
const behind = counts[1] ?? NaN;
|
|
143
|
+
if (!name || !Number.isInteger(ahead) || !Number.isInteger(behind)) return null;
|
|
144
|
+
return {
|
|
145
|
+
name,
|
|
146
|
+
ahead,
|
|
147
|
+
behind
|
|
148
|
+
};
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function warmWorktreePaths(cwd) {
|
|
154
|
+
const currentRoot = repoRoot(cwd);
|
|
155
|
+
if (!currentRoot) throw new Error("Not a git repository.");
|
|
156
|
+
const target = realpathSync$1(currentRoot);
|
|
157
|
+
const source = resolveSourceCheckout(target);
|
|
158
|
+
if ("refusal" in source) throw new Error(source.refusal);
|
|
159
|
+
if (!source.entries.find((entry) => canonicalPath(entry.path) === target)) throw new Error("Could not identify the linked worktree and its source checkout.");
|
|
160
|
+
const root = realpathSync$1(source.path);
|
|
161
|
+
if (root === target) throw new Error("Run stim worktree warm from a linked worktree, not the source checkout.");
|
|
162
|
+
const sourceRoot = repoRoot(root);
|
|
163
|
+
const sourceCommon = gitCommonDir(root);
|
|
164
|
+
const targetCommon = gitCommonDir(target);
|
|
165
|
+
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.");
|
|
166
|
+
return {
|
|
167
|
+
root,
|
|
168
|
+
target,
|
|
169
|
+
common: realpathSync$1(targetCommon)
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function matchesInclude(path, patterns) {
|
|
173
|
+
for (const pattern of patterns || []) {
|
|
174
|
+
const rooted = pattern.startsWith("/");
|
|
175
|
+
const escaped = (rooted ? pattern.slice(1) : pattern).replace(/[.+^${}()|[\]\\?]/g, "\\$&").replace(/\*\*\//g, "::GLOBSTAR::").replace(/\*/g, "[^/]+").replace(/::GLOBSTAR::/g, "(?:.*/)?").replace(/\\\?/g, "[^/]");
|
|
176
|
+
if (new RegExp(`${rooted ? "^" : "(^|/)"}${escaped}$`).test(path)) return true;
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
function readWorktreeExclude(root) {
|
|
181
|
+
return readPatternFile(join$1(root, ".worktreeexclude"));
|
|
182
|
+
}
|
|
183
|
+
function readPatternFile(p) {
|
|
184
|
+
if (!existsSync$1(p)) return null;
|
|
185
|
+
return readFileSync$1(p, "utf-8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
|
|
186
|
+
}
|
|
187
|
+
function canonicalPath(path) {
|
|
188
|
+
try {
|
|
189
|
+
return realpathSync$1(path);
|
|
190
|
+
} catch {
|
|
191
|
+
return resolve$1(path);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function nestedWorktreePaths(root) {
|
|
195
|
+
const source = canonicalPath(root);
|
|
196
|
+
const paths = /* @__PURE__ */ new Set();
|
|
197
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
198
|
+
"-C",
|
|
199
|
+
root,
|
|
200
|
+
"worktree",
|
|
201
|
+
"list",
|
|
202
|
+
"--porcelain"
|
|
203
|
+
]);
|
|
204
|
+
if (out === null) throw new Error("Could not list Git worktrees. Refusing to carry ignored files.");
|
|
205
|
+
for (const entry of parseWorktrees(out)) {
|
|
206
|
+
const rel = relative$1(source, canonicalPath(entry.path));
|
|
207
|
+
if (!rel || rel === ".." || rel.startsWith(`..${sep$1}`) || isAbsolute$1(rel)) continue;
|
|
208
|
+
paths.add(rel.split(sep$1).join("/"));
|
|
209
|
+
}
|
|
210
|
+
return [...paths];
|
|
211
|
+
}
|
|
212
|
+
function overlapsNestedWorktree(rel, nestedPaths) {
|
|
213
|
+
const path = String(rel).replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
214
|
+
return nestedPaths.some((nested) => path === nested || path.startsWith(`${nested}/`) || nested.startsWith(`${path}/`));
|
|
215
|
+
}
|
|
216
|
+
function listTrackedPaths(dir) {
|
|
217
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
218
|
+
"-C",
|
|
219
|
+
dir,
|
|
220
|
+
"ls-files",
|
|
221
|
+
"-z"
|
|
222
|
+
]);
|
|
223
|
+
if (out === null) return null;
|
|
224
|
+
return out.split("\0").filter(Boolean);
|
|
225
|
+
}
|
|
226
|
+
function trackedGuard(dir) {
|
|
227
|
+
const paths = listTrackedPaths(dir);
|
|
228
|
+
if (paths === null) return {
|
|
229
|
+
known: false,
|
|
230
|
+
covers: () => false
|
|
231
|
+
};
|
|
232
|
+
const entries = new Set(paths);
|
|
233
|
+
const covered = new Set(paths);
|
|
234
|
+
for (const p of paths) for (let i = p.indexOf("/"); i !== -1; i = p.indexOf("/", i + 1)) covered.add(p.slice(0, i));
|
|
235
|
+
return {
|
|
236
|
+
known: true,
|
|
237
|
+
covers: (rel) => {
|
|
238
|
+
if (covered.has(rel)) return true;
|
|
239
|
+
for (let i = rel.indexOf("/"); i !== -1; i = rel.indexOf("/", i + 1)) if (entries.has(rel.slice(0, i))) return true;
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function listGitignoredEntries(root) {
|
|
245
|
+
const args = [
|
|
246
|
+
"-C",
|
|
247
|
+
root,
|
|
248
|
+
"ls-files",
|
|
249
|
+
"--others",
|
|
250
|
+
"--ignored",
|
|
251
|
+
"--exclude-standard",
|
|
252
|
+
"--directory",
|
|
253
|
+
"--no-empty-directory",
|
|
254
|
+
"-z"
|
|
255
|
+
];
|
|
256
|
+
const out = getExecutor().runFile("git", args);
|
|
257
|
+
if (!out) return [];
|
|
258
|
+
return out.split("\0").filter(Boolean).map((e) => e.endsWith("/") ? e.slice(0, -1) : e);
|
|
259
|
+
}
|
|
260
|
+
function listCarryableIgnoredEntries(root, patterns) {
|
|
261
|
+
const nested = nestedWorktreePaths(root);
|
|
262
|
+
return listGitignoredEntries(root).filter((rel) => !isCarrySkipped(rel) && !overlapsNestedWorktree(rel, nested) && !matchesInclude(rel, patterns));
|
|
263
|
+
}
|
|
264
|
+
function missingDestinationReason(target, rel) {
|
|
265
|
+
const parts = rel.split("/");
|
|
266
|
+
for (let i = 1; i < parts.length; i++) {
|
|
267
|
+
const parent = parts.slice(0, i).join("/");
|
|
268
|
+
const stat = lstatSync$1(join$1(target, parent), { throwIfNoEntry: false });
|
|
269
|
+
if (stat?.isSymbolicLink()) return `symlink ancestor: ${parent}`;
|
|
270
|
+
if (stat && !stat.isDirectory()) return `non-directory ancestor: ${parent}`;
|
|
271
|
+
}
|
|
272
|
+
return lstatSync$1(join$1(target, rel), { throwIfNoEntry: false }) ? "exists" : null;
|
|
273
|
+
}
|
|
274
|
+
function removeCopiedExclusions(path, rel) {
|
|
275
|
+
const entries = readdirSync$1(path, { withFileTypes: true });
|
|
276
|
+
const excluded = entries.filter((entry) => isCarrySkipped(`${rel}/${entry.name}`));
|
|
277
|
+
if (excluded.length > 0) {
|
|
278
|
+
const stat = lstatSync$1(path);
|
|
279
|
+
chmodSync$1(path, stat.mode | 448);
|
|
280
|
+
try {
|
|
281
|
+
for (const entry of excluded) removeTemporaryEntry(join$1(path, entry.name));
|
|
282
|
+
} finally {
|
|
283
|
+
utimesSync$1(path, stat.atime, stat.mtime);
|
|
284
|
+
chmodSync$1(path, stat.mode);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
for (const entry of entries) {
|
|
288
|
+
const child = `${rel}/${entry.name}`;
|
|
289
|
+
if (entry.isDirectory() && !isCarrySkipped(child)) removeCopiedExclusions(join$1(path, entry.name), child);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function cloneIgnoredEntries({ root, target, patterns }) {
|
|
293
|
+
const copied = [];
|
|
294
|
+
const skipped = [];
|
|
295
|
+
const failed = [];
|
|
296
|
+
let cloned = true;
|
|
297
|
+
const guard = trackedGuard(target);
|
|
298
|
+
if (!guard.known) throw new Error("Could not list the destination's tracked files.");
|
|
299
|
+
const nested = nestedWorktreePaths(target);
|
|
300
|
+
for (const rel of listCarryableIgnoredEntries(root, patterns)) {
|
|
301
|
+
const from = join$1(root, rel);
|
|
302
|
+
const to = join$1(target, rel);
|
|
303
|
+
try {
|
|
304
|
+
const reason = guard.covers(rel) ? "tracked" : overlapsNestedWorktree(rel, nested) ? "nested worktree" : missingDestinationReason(target, rel);
|
|
305
|
+
if (reason) {
|
|
306
|
+
skipped.push({
|
|
307
|
+
file: rel,
|
|
308
|
+
reason
|
|
309
|
+
});
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
mkdirSync$1(dirname$1(to), { recursive: true });
|
|
313
|
+
try {
|
|
314
|
+
getExecutor().runFile("cp", [
|
|
315
|
+
"-Rc",
|
|
316
|
+
from,
|
|
317
|
+
to
|
|
318
|
+
]);
|
|
319
|
+
} catch {
|
|
320
|
+
removeTemporaryEntry(to);
|
|
321
|
+
getExecutor().runFile("cp", [
|
|
322
|
+
"-R",
|
|
323
|
+
from,
|
|
324
|
+
to
|
|
325
|
+
]);
|
|
326
|
+
cloned = false;
|
|
327
|
+
}
|
|
328
|
+
if (lstatSync$1(to).isDirectory()) removeCopiedExclusions(to, rel);
|
|
329
|
+
copied.push(rel);
|
|
330
|
+
} catch (e) {
|
|
331
|
+
failed.push({
|
|
332
|
+
file: rel,
|
|
333
|
+
error: String(e?.message || e)
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
copied,
|
|
339
|
+
skipped,
|
|
340
|
+
failed,
|
|
341
|
+
cloned
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function podsOutOfSync(target, copiedEntries, { read = readFileSync$1 } = {}) {
|
|
345
|
+
const problems = [];
|
|
346
|
+
for (const rel of copiedEntries || []) {
|
|
347
|
+
if (rel !== "Pods" && !rel.endsWith("/Pods")) continue;
|
|
348
|
+
const iosDir = rel === "Pods" ? "" : rel.slice(0, -5);
|
|
349
|
+
const manifest = join$1(target, rel, "Manifest.lock");
|
|
350
|
+
const podfileLock = join$1(target, iosDir, "Podfile.lock");
|
|
351
|
+
if (!existsSync$1(manifest)) continue;
|
|
352
|
+
if (!existsSync$1(podfileLock)) {
|
|
353
|
+
problems.push({
|
|
354
|
+
dir: iosDir || ".",
|
|
355
|
+
reason: "missing"
|
|
356
|
+
});
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
if (read(manifest, "utf-8") !== read(podfileLock, "utf-8")) problems.push({
|
|
361
|
+
dir: iosDir || ".",
|
|
362
|
+
reason: "mismatch"
|
|
363
|
+
});
|
|
364
|
+
} catch {}
|
|
365
|
+
}
|
|
366
|
+
return problems;
|
|
367
|
+
}
|
|
368
|
+
const LOCKFILE_NAMES = [
|
|
369
|
+
"pnpm-lock.yaml",
|
|
370
|
+
"yarn.lock",
|
|
371
|
+
"bun.lock",
|
|
372
|
+
"bun.lockb",
|
|
373
|
+
"package-lock.json"
|
|
374
|
+
];
|
|
375
|
+
function npmInstallMatches(wanted, installed) {
|
|
376
|
+
if (!wanted.packages || !installed.packages) return null;
|
|
377
|
+
for (const [key, entry] of Object.entries(wanted.packages)) {
|
|
378
|
+
if (!key.includes("node_modules/") || entry.link) continue;
|
|
379
|
+
const present = installed.packages[key];
|
|
380
|
+
if (!present && (entry.optional || entry.devOptional)) continue;
|
|
381
|
+
if (present?.version !== entry.version) return false;
|
|
382
|
+
}
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
function lastYamlDocument(text) {
|
|
386
|
+
return (text.split(/^---$/m).at(-1) ?? "").trim();
|
|
387
|
+
}
|
|
388
|
+
function yarnClassicInstallMatches(lockfile, integrity) {
|
|
389
|
+
if (!integrity.lockfileEntries) return null;
|
|
390
|
+
const wanted = [...lockfile.matchAll(/^ {2}resolved "([^"]+)"$/gm)].flatMap((m) => m[1] ?? []);
|
|
391
|
+
if (wanted.length === 0) return null;
|
|
392
|
+
const installed = new Set(Object.values(integrity.lockfileEntries));
|
|
393
|
+
return wanted.every((url) => installed.has(url));
|
|
394
|
+
}
|
|
395
|
+
function yarnBerryInstallMatches(lockfile, state) {
|
|
396
|
+
const installed = new Set([...state.matchAll(/^"?([^\s"#][^"]*?)"?:$/gm)].flatMap((m) => m[1]?.replace(/@virtual:[^#]+#/, "@") ?? []));
|
|
397
|
+
let entries = 0;
|
|
398
|
+
for (const entry of lockfile.split(/\n(?=\S)/)) {
|
|
399
|
+
const resolution = /^ {2}resolution: "([^"]+)"$/m.exec(entry)?.[1];
|
|
400
|
+
if (!resolution) continue;
|
|
401
|
+
entries += 1;
|
|
402
|
+
if (installed.has(resolution) || /^ {2}conditions: /m.test(entry)) continue;
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
return entries === 0 ? null : true;
|
|
406
|
+
}
|
|
407
|
+
function installedMatchesLockfile(dir, lockfile, { read = readFileSync$1 } = {}) {
|
|
408
|
+
const text = (rel) => {
|
|
409
|
+
try {
|
|
410
|
+
return read(join$1(dir, rel), "utf-8").replaceAll("\r\n", "\n");
|
|
411
|
+
} catch {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
const wanted = text(lockfile);
|
|
416
|
+
if (wanted === null) return null;
|
|
417
|
+
try {
|
|
418
|
+
if (lockfile === "package-lock.json") {
|
|
419
|
+
const installed = text("node_modules/.package-lock.json");
|
|
420
|
+
return installed === null ? null : npmInstallMatches(JSON.parse(wanted), JSON.parse(installed));
|
|
421
|
+
}
|
|
422
|
+
if (lockfile === "pnpm-lock.yaml") {
|
|
423
|
+
const installed = text("node_modules/.pnpm/lock.yaml");
|
|
424
|
+
return installed === null ? null : lastYamlDocument(wanted) === lastYamlDocument(installed);
|
|
425
|
+
}
|
|
426
|
+
if (lockfile === "yarn.lock") {
|
|
427
|
+
if (/^__metadata:$/m.test(wanted)) {
|
|
428
|
+
const berry = text("node_modules/.yarn-state.yml");
|
|
429
|
+
return berry === null ? null : yarnBerryInstallMatches(wanted, berry);
|
|
430
|
+
}
|
|
431
|
+
const classic = text("node_modules/.yarn-integrity");
|
|
432
|
+
return classic === null ? null : yarnClassicInstallMatches(wanted, JSON.parse(classic));
|
|
433
|
+
}
|
|
434
|
+
} catch {}
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
function depsOutOfSync(root, target, copiedEntries, { read = readFileSync$1 } = {}) {
|
|
438
|
+
const problems = [];
|
|
439
|
+
for (const rel of copiedEntries || []) {
|
|
440
|
+
if (rel !== "node_modules" && !rel.endsWith("/node_modules")) continue;
|
|
441
|
+
const dir = rel === "node_modules" ? "" : rel.slice(0, -13);
|
|
442
|
+
const name = LOCKFILE_NAMES.find((candidate) => existsSync$1(join$1(target, dir, candidate)));
|
|
443
|
+
if (!name) continue;
|
|
444
|
+
const installed = installedMatchesLockfile(join$1(target, dir), name, { read });
|
|
445
|
+
if (installed === false) {
|
|
446
|
+
problems.push({
|
|
447
|
+
dir: dir || ".",
|
|
448
|
+
lockfile: name,
|
|
449
|
+
reason: "installed"
|
|
450
|
+
});
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const source = join$1(root, dir, name);
|
|
454
|
+
if (installed === true || !existsSync$1(source)) continue;
|
|
455
|
+
try {
|
|
456
|
+
if (read(source, "utf-8") !== read(join$1(target, dir, name), "utf-8")) problems.push({
|
|
457
|
+
dir: dir || ".",
|
|
458
|
+
lockfile: name,
|
|
459
|
+
reason: "lockfile"
|
|
460
|
+
});
|
|
461
|
+
} catch {}
|
|
462
|
+
}
|
|
463
|
+
return problems;
|
|
464
|
+
}
|
|
465
|
+
const FINGERPRINT_INPUT_FILES = [
|
|
466
|
+
"app.json",
|
|
467
|
+
"app.config.ts",
|
|
468
|
+
"app.config.js",
|
|
469
|
+
"app.config.mjs",
|
|
470
|
+
"package.json"
|
|
471
|
+
];
|
|
472
|
+
function dirtyFingerprintFiles(root) {
|
|
473
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
474
|
+
"-C",
|
|
475
|
+
root,
|
|
476
|
+
"status",
|
|
477
|
+
"--porcelain",
|
|
478
|
+
"--",
|
|
479
|
+
...FINGERPRINT_INPUT_FILES
|
|
480
|
+
]);
|
|
481
|
+
if (out === null || out.trim() === "") return [];
|
|
482
|
+
return out.split("\n").map((line) => normalizePorcelainLine(line.trimEnd())).filter((line) => line !== "").map((line) => line.slice(3).trim()).filter((path) => path !== "");
|
|
483
|
+
}
|
|
484
|
+
function hasUncommittedWork(dir) {
|
|
485
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
486
|
+
"--no-optional-locks",
|
|
487
|
+
"-C",
|
|
488
|
+
dir,
|
|
489
|
+
"status",
|
|
490
|
+
"--porcelain"
|
|
491
|
+
]);
|
|
492
|
+
if (out === null) return null;
|
|
493
|
+
return out.trim().length > 0;
|
|
494
|
+
}
|
|
495
|
+
function dirtyPaths(dir, { limit = 10 } = {}) {
|
|
496
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
497
|
+
"--no-optional-locks",
|
|
498
|
+
"-C",
|
|
499
|
+
dir,
|
|
500
|
+
"status",
|
|
501
|
+
"--porcelain"
|
|
502
|
+
]);
|
|
503
|
+
if (out === null) return [];
|
|
504
|
+
return out.split("\n").map((l) => normalizePorcelainLine(l.trimEnd())).filter(Boolean).slice(0, limit);
|
|
505
|
+
}
|
|
506
|
+
function normalizePorcelainLine(line) {
|
|
507
|
+
if (line === "" || line[2] === " ") return line;
|
|
508
|
+
return ` ${line}`;
|
|
509
|
+
}
|
|
510
|
+
function restoreFile(dir, file) {
|
|
511
|
+
return getExecutor().runFileQuiet("git", [
|
|
512
|
+
"-C",
|
|
513
|
+
dir,
|
|
514
|
+
"checkout",
|
|
515
|
+
"--",
|
|
516
|
+
file
|
|
517
|
+
]) !== null;
|
|
518
|
+
}
|
|
519
|
+
function isPodInstallChurn(paths) {
|
|
520
|
+
if (!paths || paths.length === 0) return false;
|
|
521
|
+
return paths.every((line) => /(?:^|\/)(?:Podfile\.lock|project\.pbxproj)$/.test(line.slice(3).trim()));
|
|
522
|
+
}
|
|
523
|
+
const SAFE_BRANCH_NAME = /^[A-Za-z0-9@._/-]+$/;
|
|
524
|
+
function unpushedCommits(dir) {
|
|
525
|
+
const exec = getExecutor();
|
|
526
|
+
const branch = exec.runFileQuiet("git", [
|
|
527
|
+
"-C",
|
|
528
|
+
dir,
|
|
529
|
+
"symbolic-ref",
|
|
530
|
+
"--quiet",
|
|
531
|
+
"--short",
|
|
532
|
+
"HEAD"
|
|
533
|
+
]);
|
|
534
|
+
const own = branch === null ? "" : branch.trim();
|
|
535
|
+
const protection = own && SAFE_BRANCH_NAME.test(own) ? [
|
|
536
|
+
"--remotes",
|
|
537
|
+
`--exclude=${own}`,
|
|
538
|
+
"--branches"
|
|
539
|
+
] : ["--remotes"];
|
|
540
|
+
if (!own && exec.runFileQuiet("git", [
|
|
541
|
+
"-C",
|
|
542
|
+
dir,
|
|
543
|
+
"rev-parse",
|
|
544
|
+
"--abbrev-ref",
|
|
545
|
+
"HEAD"
|
|
546
|
+
]) === "HEAD") protection.push("--branches");
|
|
547
|
+
const out = exec.runFileQuiet("git", [
|
|
548
|
+
"-C",
|
|
549
|
+
dir,
|
|
550
|
+
"log",
|
|
551
|
+
"--oneline",
|
|
552
|
+
"HEAD",
|
|
553
|
+
"--not",
|
|
554
|
+
...protection
|
|
555
|
+
]);
|
|
556
|
+
if (out === null) return null;
|
|
557
|
+
return out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
558
|
+
}
|
|
559
|
+
function hasRemote(dir) {
|
|
560
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
561
|
+
"-C",
|
|
562
|
+
dir,
|
|
563
|
+
"remote"
|
|
564
|
+
]);
|
|
565
|
+
return Boolean(out && out.trim().length > 0);
|
|
566
|
+
}
|
|
567
|
+
function branchExists(cwd, branch) {
|
|
568
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
569
|
+
"-C",
|
|
570
|
+
cwd,
|
|
571
|
+
"rev-parse",
|
|
572
|
+
"--verify",
|
|
573
|
+
"--quiet",
|
|
574
|
+
`refs/heads/${branch}`
|
|
575
|
+
]);
|
|
576
|
+
return Boolean(out);
|
|
577
|
+
}
|
|
578
|
+
function resolveFullRef(cwd, ref) {
|
|
579
|
+
try {
|
|
580
|
+
const out = getExecutor().runFile("git", [
|
|
581
|
+
"-C",
|
|
582
|
+
cwd,
|
|
583
|
+
"rev-parse",
|
|
584
|
+
"--verify",
|
|
585
|
+
"--quiet",
|
|
586
|
+
"--end-of-options",
|
|
587
|
+
`${ref}^{commit}`
|
|
588
|
+
]);
|
|
589
|
+
return out && out.trim() ? out.trim() : null;
|
|
590
|
+
} catch {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
function removeWorktree(path, { from, force = false, platform = process.platform }) {
|
|
595
|
+
const args = [
|
|
596
|
+
...platform === "win32" ? ["-c", "core.longpaths=true"] : [],
|
|
597
|
+
"-C",
|
|
598
|
+
from,
|
|
599
|
+
"worktree",
|
|
600
|
+
"remove",
|
|
601
|
+
...force ? ["--force"] : [],
|
|
602
|
+
"--",
|
|
603
|
+
path
|
|
604
|
+
];
|
|
605
|
+
getExecutor().runFile("git", args);
|
|
606
|
+
}
|
|
607
|
+
function deleteBranch(cwd, branch, expectedSha) {
|
|
608
|
+
if (!SAFE_BRANCH_NAME.test(branch) || branch.startsWith("-")) throw new Error(`Refusing branch ${JSON.stringify(branch)}: it is not a safe local branch name.`);
|
|
609
|
+
getExecutor().runFile("git", [
|
|
610
|
+
"-C",
|
|
611
|
+
cwd,
|
|
612
|
+
"update-ref",
|
|
613
|
+
"-d",
|
|
614
|
+
`refs/heads/${branch}`,
|
|
615
|
+
expectedSha
|
|
616
|
+
]);
|
|
617
|
+
}
|
|
618
|
+
function parseWorktrees(out) {
|
|
619
|
+
const entries = [];
|
|
620
|
+
let current = {};
|
|
621
|
+
for (const line of out.split("\n")) if (line.startsWith("worktree ")) {
|
|
622
|
+
if (current.path) entries.push(current);
|
|
623
|
+
current = { path: nativePath(line.slice(9)) };
|
|
624
|
+
} else if (line.startsWith("branch ")) current.branch = line.slice(7).replace("refs/heads/", "");
|
|
625
|
+
else if (line === "prunable" || line.startsWith("prunable ")) current.prunable = true;
|
|
626
|
+
else if (line === "locked" || line.startsWith("locked ")) current.locked = true;
|
|
627
|
+
else if (line === "bare") current.bare = true;
|
|
628
|
+
if (current.path) entries.push(current);
|
|
629
|
+
return entries;
|
|
630
|
+
}
|
|
631
|
+
function hasPopulatedSubmodules(worktree) {
|
|
632
|
+
const modules = getExecutor().runFileQuiet("git", [
|
|
633
|
+
"-C",
|
|
634
|
+
worktree,
|
|
635
|
+
"rev-parse",
|
|
636
|
+
"--git-path",
|
|
637
|
+
"modules"
|
|
638
|
+
]);
|
|
639
|
+
if (modules && existsSync$1(resolve$1(worktree, nativePath(modules)))) return true;
|
|
640
|
+
return (getExecutor().runFileQuiet("git", [
|
|
641
|
+
"-C",
|
|
642
|
+
worktree,
|
|
643
|
+
"ls-files",
|
|
644
|
+
"--stage",
|
|
645
|
+
"-z"
|
|
646
|
+
]) ?? "").split("\0").some((line) => line.startsWith("160000 ") && existsSync$1(join$1(worktree, line.slice(line.indexOf(" ") + 1), ".git")));
|
|
647
|
+
}
|
|
648
|
+
function listWorktrees(cwd) {
|
|
649
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
650
|
+
"-C",
|
|
651
|
+
cwd,
|
|
652
|
+
"worktree",
|
|
653
|
+
"list",
|
|
654
|
+
"--porcelain"
|
|
655
|
+
]);
|
|
656
|
+
return out ? parseWorktrees(out) : [];
|
|
657
|
+
}
|
|
658
|
+
function branchOf(head) {
|
|
659
|
+
return head.startsWith("ref: refs/heads/") ? head.slice(16) : void 0;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* The linked worktrees recorded under a git common dir, read from `worktrees/<id>/gitdir` and `HEAD` without running
|
|
663
|
+
* git or touching the worktree directories, sorted by path. The worktree with the branch the common dir's own `HEAD`
|
|
664
|
+
* names (the main checkout's branch, or a bare repository's source checkout) is left out.
|
|
665
|
+
*/
|
|
666
|
+
function linkedWorktreesOnDisk(commonDir) {
|
|
667
|
+
const admin = join$1(commonDir, "worktrees");
|
|
668
|
+
let ids;
|
|
669
|
+
let sourceBranch;
|
|
670
|
+
try {
|
|
671
|
+
ids = readdirSync$1(admin);
|
|
672
|
+
sourceBranch = branchOf(readFileSync$1(join$1(commonDir, "HEAD"), "utf-8").trim());
|
|
673
|
+
} catch {
|
|
674
|
+
return [];
|
|
675
|
+
}
|
|
676
|
+
return ids.flatMap((id) => {
|
|
677
|
+
try {
|
|
678
|
+
const gitdir = readFileSync$1(join$1(admin, id, "gitdir"), "utf-8").trim();
|
|
679
|
+
const branch = branchOf(readFileSync$1(join$1(admin, id, "HEAD"), "utf-8").trim());
|
|
680
|
+
if (branch && branch === sourceBranch) return [];
|
|
681
|
+
const path = dirname$1(resolve$1(admin, id, nativePath(gitdir)));
|
|
682
|
+
return [branch ? {
|
|
683
|
+
path,
|
|
684
|
+
branch
|
|
685
|
+
} : { path }];
|
|
686
|
+
} catch {
|
|
687
|
+
return [];
|
|
688
|
+
}
|
|
689
|
+
}).toSorted((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* The git common dir of the repository containing `start`, read from `.git` and `commondir` files without running
|
|
693
|
+
* git. Returns null when `start` is missing, unreadable, or not inside a repository.
|
|
694
|
+
*/
|
|
695
|
+
function gitCommonDirOnDisk(start) {
|
|
696
|
+
try {
|
|
697
|
+
for (let dir = realpathSync$1.native(start);; dir = dirname$1(dir)) {
|
|
698
|
+
const dotGit = join$1(dir, ".git");
|
|
699
|
+
if (existsSync$1(dotGit)) {
|
|
700
|
+
if (statSync$1(dotGit).isDirectory()) return realpathSync$1.native(dotGit);
|
|
701
|
+
const gitdir = /^gitdir: (.+)$/m.exec(readFileSync$1(dotGit, "utf-8"))?.[1]?.trim();
|
|
702
|
+
if (!gitdir) return null;
|
|
703
|
+
const linked = resolve$1(dir, gitdir);
|
|
704
|
+
const commondir = join$1(linked, "commondir");
|
|
705
|
+
return realpathSync$1.native(existsSync$1(commondir) ? resolve$1(linked, readFileSync$1(commondir, "utf-8").trim()) : linked);
|
|
706
|
+
}
|
|
707
|
+
if (dirname$1(dir) === dir) return null;
|
|
708
|
+
}
|
|
709
|
+
} catch {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
function selectSourceCheckout(entries, bareHead) {
|
|
714
|
+
const first = entries[0];
|
|
715
|
+
if (!first) return { refusal: "Git lists no worktrees for this repository." };
|
|
716
|
+
if (!first.bare) return { path: first.path };
|
|
717
|
+
const bare = first.path;
|
|
718
|
+
const checkouts = entries.filter((entry) => !entry.bare && !entry.prunable);
|
|
719
|
+
const listed = checkouts.map((entry) => `${entry.branch ?? "detached"} (${entry.path})`).join(", ") || "none";
|
|
720
|
+
const pointHead = ` git -C ${bare} symbolic-ref HEAD refs/heads/<branch>`;
|
|
721
|
+
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}` };
|
|
722
|
+
const matches = checkouts.filter((entry) => entry.branch === bareHead);
|
|
723
|
+
const [only] = matches;
|
|
724
|
+
if (only && matches.length === 1) return { path: only.path };
|
|
725
|
+
if (!only) {
|
|
726
|
+
const stale = entries.find((entry) => entry.prunable && entry.branch === bareHead);
|
|
727
|
+
const beside = checkouts[0] ? dirname$1(checkouts[0].path) : bare;
|
|
728
|
+
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}` };
|
|
729
|
+
}
|
|
730
|
+
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}` };
|
|
731
|
+
}
|
|
732
|
+
function bareHeadBranch(bare) {
|
|
733
|
+
const out = getExecutor().runFileQuiet("git", [
|
|
734
|
+
"-C",
|
|
735
|
+
bare,
|
|
736
|
+
"symbolic-ref",
|
|
737
|
+
"--quiet",
|
|
738
|
+
"HEAD"
|
|
739
|
+
])?.trim();
|
|
740
|
+
return out?.startsWith("refs/heads/") ? out.slice(11) : null;
|
|
741
|
+
}
|
|
742
|
+
function sourceCheckoutOf(entries) {
|
|
743
|
+
const first = entries[0];
|
|
744
|
+
return selectSourceCheckout(entries, first?.bare ? bareHeadBranch(first.path) : null);
|
|
745
|
+
}
|
|
746
|
+
function resolveSourceCheckout(cwd) {
|
|
747
|
+
const entries = listWorktrees(cwd);
|
|
748
|
+
return {
|
|
749
|
+
entries,
|
|
750
|
+
...sourceCheckoutOf(entries)
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
//#endregion
|
|
754
|
+
//#region src/engine/metro-reach.ts
|
|
755
|
+
const PUBLIC_METRO_ENV = "STIM_METRO_PUBLIC_URL";
|
|
756
|
+
const MANAGED_PROVIDERS = ["ngrok", "cloudflared"];
|
|
757
|
+
const NAMED = {
|
|
758
|
+
cloudflared: "`cloudflared` (brew install cloudflared)",
|
|
759
|
+
ngrok: "`ngrok` (brew install ngrok, then `ngrok config add-authtoken <token>`)",
|
|
760
|
+
tailscale: "Tailscale and sign in to your tailnet"
|
|
761
|
+
};
|
|
762
|
+
function planMetroReach({ mode, metroPort, publicUrl = null, isExpo, available = [] }) {
|
|
763
|
+
const named = publicUrl?.trim().replace(/\/+$/, "") || null;
|
|
764
|
+
if (mode === "off") return {
|
|
765
|
+
origin: `http://localhost:${metroPort}`,
|
|
766
|
+
gate: false
|
|
767
|
+
};
|
|
768
|
+
if (named) return {
|
|
769
|
+
origin: named,
|
|
770
|
+
gate: true
|
|
771
|
+
};
|
|
772
|
+
if (mode === "expo") {
|
|
773
|
+
if (!isExpo) return {
|
|
774
|
+
failed: "metro.tunnel is \"expo\", but this workspace does not run an Expo dev server.",
|
|
775
|
+
remedy: "Use \"auto\" to let stim start a tunnel, or \"off\" if the device shares this machine."
|
|
776
|
+
};
|
|
777
|
+
return { expoTunnel: true };
|
|
778
|
+
}
|
|
779
|
+
if (mode === "cloudflared" || mode === "ngrok" || mode === "tailscale") {
|
|
780
|
+
if (!available.includes(mode)) return {
|
|
781
|
+
failed: `metro.tunnel is "${mode}", but ${mode} is not on PATH.`,
|
|
782
|
+
remedy: `Install ${NAMED[mode] ?? mode}, or set metro.tunnel to "auto".`
|
|
783
|
+
};
|
|
784
|
+
return { start: mode };
|
|
785
|
+
}
|
|
786
|
+
const provider = available.find((candidate) => candidate !== "tailscale");
|
|
787
|
+
if (provider) return { start: provider };
|
|
788
|
+
return {
|
|
789
|
+
failed: `A remote device cannot reach this workspace's Metro on port ${metroPort}, and no tunnel is available to give it one.`,
|
|
790
|
+
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".`
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
function resolveTailscaleBinary(findExecutable) {
|
|
794
|
+
return findExecutable("tailscale") ?? findExecutable("/Applications/Tailscale.app/Contents/MacOS/Tailscale");
|
|
795
|
+
}
|
|
796
|
+
function detectProviders(onPath, mode = "auto") {
|
|
797
|
+
if (mode === "tailscale") return resolveTailscaleBinary((bin) => onPath(bin) ? bin : null) ? ["tailscale"] : [];
|
|
798
|
+
return MANAGED_PROVIDERS.filter((p) => onPath(p));
|
|
799
|
+
}
|
|
800
|
+
//#endregion
|
|
801
|
+
//#region src/workspace/settings.ts
|
|
802
|
+
function isPlainObject(v) {
|
|
803
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
804
|
+
}
|
|
805
|
+
function mergeSettingsLayers(layers) {
|
|
806
|
+
const out = {};
|
|
807
|
+
for (const layer of layers) {
|
|
808
|
+
if (!isPlainObject(layer)) continue;
|
|
809
|
+
for (const [key, value] of Object.entries(layer)) {
|
|
810
|
+
if (key === "__proto__") continue;
|
|
811
|
+
if (isPlainObject(value) && isPlainObject(out[key])) out[key] = mergeSettingsLayers([out[key], value]);
|
|
812
|
+
else if (!(key in out)) out[key] = value;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return out;
|
|
816
|
+
}
|
|
817
|
+
const LAYERED_SETTINGS = SETTINGS.filter(isLayeredSetting);
|
|
818
|
+
const KNOWN_SETTINGS = /* @__PURE__ */ new Set([...LAYERED_SETTINGS.map((setting) => setting.key), ...SETTING_GROUPS]);
|
|
819
|
+
const BOUNDS_CHECKED_WHEN_USED = /* @__PURE__ */ new Set(["android.dataPartitionSizeGb"]);
|
|
820
|
+
const SETTINGS_WITH_RESOLVE_TIME_FALLBACK = /* @__PURE__ */ new Set(["optimizations.android.casToolchain"]);
|
|
821
|
+
const PATH_SETTINGS = Object.freeze(LAYERED_SETTINGS.filter((setting) => setting.type.kind === "path").map((setting) => setting.key));
|
|
822
|
+
function settingValueAt(settings, path) {
|
|
823
|
+
let node = settings;
|
|
824
|
+
for (const segment of path.split(".")) {
|
|
825
|
+
if (!isPlainObject(node)) return void 0;
|
|
826
|
+
node = node[segment];
|
|
827
|
+
}
|
|
828
|
+
return node;
|
|
829
|
+
}
|
|
830
|
+
function settingShapeErrors(settings) {
|
|
831
|
+
const errors = [];
|
|
832
|
+
for (const group of SETTING_GROUPS) {
|
|
833
|
+
const value = settingValueAt(settings, group);
|
|
834
|
+
if (value === void 0 || isPlainObject(value)) continue;
|
|
835
|
+
errors.push(`Invalid ${group} setting ${JSON.stringify(value)}. Expected an object.`);
|
|
836
|
+
}
|
|
837
|
+
for (const setting of LAYERED_SETTINGS) {
|
|
838
|
+
if (SETTINGS_WITH_RESOLVE_TIME_FALLBACK.has(setting.key)) continue;
|
|
839
|
+
const value = settingValueAt(settings, setting.key);
|
|
840
|
+
if (value === void 0) continue;
|
|
841
|
+
const expected = setting.key === "ios.remote" || setting.key === "android.remote" || setting.key === "android.deviceProfile" || setting.type.kind === "number" && !BOUNDS_CHECKED_WHEN_USED.has(setting.key) ? settingValueError(setting, value) : acceptsShape(setting, value) ? null : expectedShape(setting.type);
|
|
842
|
+
if (expected) errors.push(`Invalid ${setting.key} setting ${JSON.stringify(value)}. Expected ${expected}.`);
|
|
843
|
+
}
|
|
844
|
+
return errors;
|
|
845
|
+
}
|
|
846
|
+
const SETTING_SHAPE_REMEDY = "Run `stim guide settings` for the shape each setting takes.";
|
|
847
|
+
const MIN_ANDROID_DATA_PARTITION_SIZE_GB = 6;
|
|
848
|
+
const DEFAULT_ANDROID_DATA_PARTITION_SIZE_GB = 8;
|
|
849
|
+
const MAX_ANDROID_DATA_PARTITION_SIZE_GB = 16384;
|
|
850
|
+
function validateAndroidDataPartitionSizeGb(raw) {
|
|
851
|
+
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.`);
|
|
852
|
+
return raw;
|
|
853
|
+
}
|
|
854
|
+
function androidDataPartitionSizeBytes(sizeGb) {
|
|
855
|
+
return validateAndroidDataPartitionSizeGb(sizeGb) * 1024 ** 3;
|
|
856
|
+
}
|
|
857
|
+
function androidDataPartitionSizeGbRaw(settings) {
|
|
858
|
+
if (!isPlainObject(settings) || !isPlainObject(settings.android)) return void 0;
|
|
859
|
+
return settings.android.dataPartitionSizeGb;
|
|
860
|
+
}
|
|
861
|
+
function androidDataPartitionSizeGbSetting(settings) {
|
|
862
|
+
const raw = androidDataPartitionSizeGbRaw(settings);
|
|
863
|
+
if (raw === void 0) return DEFAULT_ANDROID_DATA_PARTITION_SIZE_GB;
|
|
864
|
+
return validateAndroidDataPartitionSizeGb(raw);
|
|
865
|
+
}
|
|
866
|
+
function androidDataPartitionSizeGbSettingError(settings) {
|
|
867
|
+
try {
|
|
868
|
+
androidDataPartitionSizeGbSetting(settings);
|
|
869
|
+
return null;
|
|
870
|
+
} catch (error) {
|
|
871
|
+
return String(error?.message || error);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function yesNoAvdValue(value) {
|
|
875
|
+
if (value === true || value === "true" || value === "yes") return "yes";
|
|
876
|
+
if (value === false || value === "false" || value === "no") return "no";
|
|
877
|
+
throw new Error("expected true, false, \"yes\", or \"no\"");
|
|
878
|
+
}
|
|
879
|
+
function boundedIntegerAvdRule(min, max, unit) {
|
|
880
|
+
return {
|
|
881
|
+
help: `integer ${min}-${max}${unit ? ` ${unit}` : ""}`,
|
|
882
|
+
parse: (value) => {
|
|
883
|
+
const parsed = typeof value === "number" ? value : typeof value === "string" && /^(0|[1-9]\d*)$/.test(value) ? Number(value) : NaN;
|
|
884
|
+
if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) throw new Error(`expected an integer from ${min} through ${max}${unit ? ` ${unit}` : ""}`);
|
|
885
|
+
return String(parsed);
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
function enumAvdRule(values) {
|
|
890
|
+
return {
|
|
891
|
+
help: values.join("|"),
|
|
892
|
+
parse: (value) => {
|
|
893
|
+
if (typeof value !== "string" || !values.includes(value)) throw new Error(`expected one of: ${values.join(", ")}`);
|
|
894
|
+
return value;
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
const ANDROID_AVD_CONFIG_RULES = {
|
|
899
|
+
"hw.cpu.ncore": boundedIntegerAvdRule(1, 64),
|
|
900
|
+
"hw.gpu.mode": enumAvdRule([
|
|
901
|
+
"auto",
|
|
902
|
+
"host",
|
|
903
|
+
"software",
|
|
904
|
+
"lavapipe",
|
|
905
|
+
"swiftshader",
|
|
906
|
+
"swangle"
|
|
907
|
+
]),
|
|
908
|
+
"hw.initialOrientation": enumAvdRule(["portrait", "landscape"]),
|
|
909
|
+
"hw.lcd.density": boundedIntegerAvdRule(72, 1e3, "dpi"),
|
|
910
|
+
"hw.lcd.vsync": boundedIntegerAvdRule(1, 1e3, "Hz"),
|
|
911
|
+
"hw.ramSize": boundedIntegerAvdRule(1536, 8192, "MB"),
|
|
912
|
+
"hw.screen": enumAvdRule([
|
|
913
|
+
"no-touch",
|
|
914
|
+
"touch",
|
|
915
|
+
"multi-touch"
|
|
916
|
+
]),
|
|
917
|
+
"runtime.network.latency": enumAvdRule([
|
|
918
|
+
"none",
|
|
919
|
+
"gsm",
|
|
920
|
+
"hscsd",
|
|
921
|
+
"gprs",
|
|
922
|
+
"edge",
|
|
923
|
+
"umts",
|
|
924
|
+
"hsdpa",
|
|
925
|
+
"lte",
|
|
926
|
+
"evdo",
|
|
927
|
+
"5g"
|
|
928
|
+
]),
|
|
929
|
+
"runtime.network.speed": enumAvdRule([
|
|
930
|
+
"gsm",
|
|
931
|
+
"hscsd",
|
|
932
|
+
"gprs",
|
|
933
|
+
"edge",
|
|
934
|
+
"umts",
|
|
935
|
+
"hsdpa",
|
|
936
|
+
"lte",
|
|
937
|
+
"evdo",
|
|
938
|
+
"5g",
|
|
939
|
+
"full"
|
|
940
|
+
]),
|
|
941
|
+
"vm.heapSize": boundedIntegerAvdRule(16, 4096, "MB")
|
|
942
|
+
};
|
|
943
|
+
const YES_NO_AVD_RULE = {
|
|
944
|
+
help: "true|false|yes|no",
|
|
945
|
+
parse: yesNoAvdValue
|
|
946
|
+
};
|
|
947
|
+
for (const key of [
|
|
948
|
+
"hw.accelerometer",
|
|
949
|
+
"hw.accelerometer_uncalibrated",
|
|
950
|
+
"hw.audioInput",
|
|
951
|
+
"hw.audioOutput",
|
|
952
|
+
"hw.battery",
|
|
953
|
+
"hw.dPad",
|
|
954
|
+
"hw.gps",
|
|
955
|
+
"hw.gpu.enabled",
|
|
956
|
+
"hw.gyroscope",
|
|
957
|
+
"hw.keyboard",
|
|
958
|
+
"hw.mainKeys",
|
|
959
|
+
"hw.rotaryInput",
|
|
960
|
+
"hw.trackBall",
|
|
961
|
+
"showDeviceFrame"
|
|
962
|
+
]) ANDROID_AVD_CONFIG_RULES[key] = YES_NO_AVD_RULE;
|
|
963
|
+
const ANDROID_AVD_CONFIG_KEYS = Object.freeze(Object.keys(ANDROID_AVD_CONFIG_RULES).toSorted());
|
|
964
|
+
const ANDROID_AVD_CONFIG_HELP = Object.freeze(ANDROID_AVD_CONFIG_KEYS.map((key) => `${key}: ${ANDROID_AVD_CONFIG_RULES[key].help}`));
|
|
965
|
+
function isSafeAndroidAvdConfigKey(key) {
|
|
966
|
+
return Object.hasOwn(ANDROID_AVD_CONFIG_RULES, key);
|
|
967
|
+
}
|
|
968
|
+
function normalizedAndroidAvdConfigEntry(key, value) {
|
|
969
|
+
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.`);
|
|
970
|
+
if (typeof value === "string" && /[\r\n\0]/.test(value)) throw new Error(`Invalid android.avdConfig value for ${key}: expected one line.`);
|
|
971
|
+
try {
|
|
972
|
+
return [key, ANDROID_AVD_CONFIG_RULES[key].parse(value)];
|
|
973
|
+
} catch (error) {
|
|
974
|
+
throw new Error(`Invalid android.avdConfig value for ${key}: ${String(error?.message || error)}.`, { cause: error });
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
function parseAndroidAvdConfigIni(contents) {
|
|
978
|
+
const entries = {};
|
|
979
|
+
const lines = contents.replace(/^\uFEFF/, "").split(/\r?\n/);
|
|
980
|
+
for (let index = 0; index < lines.length; index++) {
|
|
981
|
+
const line = String(lines[index] ?? "").trim();
|
|
982
|
+
if (!line || line.startsWith("#") || line.startsWith(";")) continue;
|
|
983
|
+
const separator = line.indexOf("=");
|
|
984
|
+
if (separator <= 0) throw new Error(`Invalid android.avdConfigFile line ${index + 1}: expected key=value.`);
|
|
985
|
+
const [key, value] = normalizedAndroidAvdConfigEntry(line.slice(0, separator), line.slice(separator + 1).trim());
|
|
986
|
+
if (Object.hasOwn(entries, key)) throw new Error(`Invalid android.avdConfigFile line ${index + 1}: duplicate key ${key}.`);
|
|
987
|
+
entries[key] = value;
|
|
988
|
+
}
|
|
989
|
+
return entries;
|
|
990
|
+
}
|
|
991
|
+
function pathEscapesRoot(root, path) {
|
|
992
|
+
const child = relative$1(root, path);
|
|
993
|
+
return child === ".." || child.startsWith(`..${sep$1}`) || isAbsolute$1(child);
|
|
994
|
+
}
|
|
995
|
+
function realpathInside(root, path) {
|
|
996
|
+
const base = realpathSync$1(root);
|
|
997
|
+
const real = realpathSync$1(resolve$1(base, path));
|
|
998
|
+
if (pathEscapesRoot(base, real)) throw new Error(`it resolves outside ${base}`);
|
|
999
|
+
return real;
|
|
1000
|
+
}
|
|
1001
|
+
function androidAvdConfigSetting(settings, settingsRoot) {
|
|
1002
|
+
if (!isPlainObject(settings) || !isPlainObject(settings.android)) return {};
|
|
1003
|
+
const android = settings.android;
|
|
1004
|
+
let entries = {};
|
|
1005
|
+
if ("avdConfigFile" in android) {
|
|
1006
|
+
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.");
|
|
1007
|
+
try {
|
|
1008
|
+
const root = realpathSync$1(settingsRoot);
|
|
1009
|
+
const candidate = resolve$1(root, android.avdConfigFile);
|
|
1010
|
+
if (pathEscapesRoot(root, candidate)) throw new Error("path escapes the settings root");
|
|
1011
|
+
const path = realpathSync$1(candidate);
|
|
1012
|
+
if (pathEscapesRoot(root, path)) throw new Error("symlink target escapes the settings root");
|
|
1013
|
+
const stat = statSync$1(path);
|
|
1014
|
+
if (!stat.isFile()) throw new Error("path is not a regular file");
|
|
1015
|
+
if (stat.size > 65536) throw new Error("file exceeds 64 KiB");
|
|
1016
|
+
const contents = readFileSync$1(path, "utf8");
|
|
1017
|
+
if (Buffer.byteLength(contents) > 65536) throw new Error("file exceeds 64 KiB");
|
|
1018
|
+
entries = parseAndroidAvdConfigIni(contents);
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
throw new Error(`Could not read valid android.avdConfigFile ${android.avdConfigFile}: ${String(error?.message || error)}`, { cause: error });
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if ("avdConfig" in android) {
|
|
1024
|
+
if (!isPlainObject(android.avdConfig)) throw new Error("Invalid android.avdConfig setting. Expected an object of native AVD key/value pairs.");
|
|
1025
|
+
for (const [key, value] of Object.entries(android.avdConfig)) {
|
|
1026
|
+
const [normalizedKey, normalizedValue] = normalizedAndroidAvdConfigEntry(key, value);
|
|
1027
|
+
entries[normalizedKey] = normalizedValue;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return entries;
|
|
1031
|
+
}
|
|
1032
|
+
function androidAvdConfigSettingError(settings, projectPath) {
|
|
1033
|
+
try {
|
|
1034
|
+
androidAvdConfigSetting(settings, projectPath);
|
|
1035
|
+
return null;
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
return String(error?.message || error);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
function iosProjectDirSetting(settings, appRoot) {
|
|
1041
|
+
const value = iosString(settings, "projectPath");
|
|
1042
|
+
if (value === void 0 || typeof value === "string" && normalize(value).replace(/[\\/]+$/, "") === "ios") return {
|
|
1043
|
+
dir: join$1(appRoot, "ios"),
|
|
1044
|
+
relative: "ios",
|
|
1045
|
+
custom: false
|
|
1046
|
+
};
|
|
1047
|
+
if (typeof value !== "string" || !value.trim() || value !== value.trim() || /[\r\n\0]/.test(value) || isAbsolute$1(value)) throw new Error("Invalid ios.projectPath setting. Expected a directory path relative to the app directory.");
|
|
1048
|
+
try {
|
|
1049
|
+
const root = realpathSync$1(appRoot);
|
|
1050
|
+
const candidate = resolve$1(root, value);
|
|
1051
|
+
if (pathEscapesRoot(root, candidate)) throw new Error("path escapes the app directory");
|
|
1052
|
+
const dir = realpathSync$1(candidate);
|
|
1053
|
+
if (pathEscapesRoot(root, dir)) throw new Error("symlink target escapes the app directory");
|
|
1054
|
+
if (!statSync$1(dir).isDirectory()) throw new Error("path is not a directory");
|
|
1055
|
+
if (!readdirSync$1(dir).some((name) => name.endsWith(".xcworkspace") || name.endsWith(".xcodeproj"))) throw new Error("it holds no .xcworkspace or .xcodeproj");
|
|
1056
|
+
return {
|
|
1057
|
+
dir,
|
|
1058
|
+
relative: relative$1(root, dir) || ".",
|
|
1059
|
+
custom: true
|
|
1060
|
+
};
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
throw new Error(`Could not use ios.projectPath ${value}: ${String(error?.message || error)}`, { cause: error });
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
function iosProjectDirSettingError(settings, appRoot) {
|
|
1066
|
+
try {
|
|
1067
|
+
iosProjectDirSetting(settings, appRoot);
|
|
1068
|
+
return null;
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
return String(error?.message || error);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
const INVALID_IOS_PROJECT_DIR = "\0invalid ios.projectPath";
|
|
1074
|
+
const DEFAULT_ANDROID_GRADLE_ROOT = "android";
|
|
1075
|
+
const DEFAULT_ANDROID_MODULE = ":app";
|
|
1076
|
+
const ANDROID_MODULE = /^(:(?!\.\.?(?::|$))[A-Za-z0-9._-]+)+$/;
|
|
1077
|
+
function androidString(settings, key) {
|
|
1078
|
+
if (!isPlainObject(settings) || !isPlainObject(settings.android)) return void 0;
|
|
1079
|
+
return settings.android[key];
|
|
1080
|
+
}
|
|
1081
|
+
function hasGradleScript(dir, name) {
|
|
1082
|
+
return existsSync$1(join$1(dir, `${name}.gradle`)) || existsSync$1(join$1(dir, `${name}.gradle.kts`));
|
|
1083
|
+
}
|
|
1084
|
+
function androidLayoutSetting(settings, appRoot, bound) {
|
|
1085
|
+
const rootValue = androidString(settings, "gradleRoot");
|
|
1086
|
+
const moduleValue = androidString(settings, "module");
|
|
1087
|
+
if (!(rootValue !== void 0 && rootValue !== "android" || moduleValue !== void 0 && moduleValue !== ":app")) {
|
|
1088
|
+
const gradleRoot = join$1(appRoot, DEFAULT_ANDROID_GRADLE_ROOT);
|
|
1089
|
+
return {
|
|
1090
|
+
gradleRoot,
|
|
1091
|
+
gradleRootRelative: DEFAULT_ANDROID_GRADLE_ROOT,
|
|
1092
|
+
module: DEFAULT_ANDROID_MODULE,
|
|
1093
|
+
moduleDir: join$1(gradleRoot, "app"),
|
|
1094
|
+
custom: false
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
const rawRoot = rootValue ?? "android";
|
|
1098
|
+
if (typeof rawRoot !== "string" || !rawRoot.trim() || rawRoot !== rawRoot.trim() || /[\r\n\0]/.test(rawRoot) || isAbsolute$1(rawRoot)) throw new Error("Invalid android.gradleRoot setting. Expected a directory path relative to the app directory.");
|
|
1099
|
+
const module = moduleValue ?? ":app";
|
|
1100
|
+
if (typeof module !== "string" || !ANDROID_MODULE.test(module)) throw new Error(`Invalid android.module setting ${JSON.stringify(module)}. Expected a Gradle project path such as :app.`);
|
|
1101
|
+
try {
|
|
1102
|
+
const app = realpathSync$1(appRoot);
|
|
1103
|
+
const limit = realpathSync$1(bound);
|
|
1104
|
+
const gradleRoot = realpathSync$1(resolve$1(app, rawRoot));
|
|
1105
|
+
if (pathEscapesRoot(limit, gradleRoot)) throw new Error(`it resolves outside ${limit}`);
|
|
1106
|
+
if (!statSync$1(gradleRoot).isDirectory()) throw new Error("it is not a directory");
|
|
1107
|
+
if (!hasGradleScript(gradleRoot, "settings")) throw new Error("it holds no settings.gradle or settings.gradle.kts");
|
|
1108
|
+
const mapped = join$1(gradleRoot, ...module.split(":").filter(Boolean));
|
|
1109
|
+
if (!hasGradleScript(mapped, "build")) throw new Error(`module ${module} has no build.gradle or build.gradle.kts at ${mapped}`);
|
|
1110
|
+
const moduleDir = realpathSync$1(mapped);
|
|
1111
|
+
if (pathEscapesRoot(limit, moduleDir)) throw new Error(`module ${module} resolves outside ${limit}`);
|
|
1112
|
+
return {
|
|
1113
|
+
gradleRoot,
|
|
1114
|
+
gradleRootRelative: relative$1(app, gradleRoot).split(sep$1).join("/") || ".",
|
|
1115
|
+
module,
|
|
1116
|
+
moduleDir,
|
|
1117
|
+
custom: true
|
|
1118
|
+
};
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
throw new Error(`Could not use android.gradleRoot ${rawRoot}: ${String(error?.message || error)}`, { cause: error });
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
function androidLayoutSettingError(settings, appRoot, bound) {
|
|
1124
|
+
try {
|
|
1125
|
+
androidLayoutSetting(settings, appRoot, bound);
|
|
1126
|
+
return null;
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
return String(error?.message || error);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
const INVALID_ANDROID_GRADLE_ROOT = "\0invalid android.gradleRoot";
|
|
1132
|
+
function defaultAndroidLayout(appRoot) {
|
|
1133
|
+
return androidLayoutSetting({}, appRoot, appRoot);
|
|
1134
|
+
}
|
|
1135
|
+
function resolveAndroidLayout(settings, root, bound) {
|
|
1136
|
+
if (settings === null) return defaultAndroidLayout(root);
|
|
1137
|
+
try {
|
|
1138
|
+
return androidLayoutSetting(settings, root, bound);
|
|
1139
|
+
} catch {
|
|
1140
|
+
const gradleRoot = join$1(root, INVALID_ANDROID_GRADLE_ROOT);
|
|
1141
|
+
return {
|
|
1142
|
+
gradleRoot,
|
|
1143
|
+
gradleRootRelative: INVALID_ANDROID_GRADLE_ROOT,
|
|
1144
|
+
module: DEFAULT_ANDROID_MODULE,
|
|
1145
|
+
moduleDir: join$1(gradleRoot, "app"),
|
|
1146
|
+
custom: true
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function resolveIosProjectDir(settings, root) {
|
|
1151
|
+
if (settings === null) return {
|
|
1152
|
+
dir: join$1(root, "ios"),
|
|
1153
|
+
relative: "ios",
|
|
1154
|
+
custom: false
|
|
1155
|
+
};
|
|
1156
|
+
try {
|
|
1157
|
+
return iosProjectDirSetting(settings, root);
|
|
1158
|
+
} catch {
|
|
1159
|
+
return {
|
|
1160
|
+
dir: join$1(root, INVALID_IOS_PROJECT_DIR),
|
|
1161
|
+
relative: INVALID_IOS_PROJECT_DIR,
|
|
1162
|
+
custom: true
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
function iosSimSlimProfileSetting(settings, settingsRoot) {
|
|
1167
|
+
if (!isPlainObject(settings) || !isPlainObject(settings.ios) || !("simslimProfile" in settings.ios)) return null;
|
|
1168
|
+
const value = settings.ios.simslimProfile;
|
|
1169
|
+
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.");
|
|
1170
|
+
try {
|
|
1171
|
+
const root = realpathSync$1(settingsRoot);
|
|
1172
|
+
const candidate = resolve$1(root, value);
|
|
1173
|
+
if (pathEscapesRoot(root, candidate)) throw new Error("path escapes the settings root");
|
|
1174
|
+
const path = realpathSync$1(candidate);
|
|
1175
|
+
if (pathEscapesRoot(root, path)) throw new Error("symlink target escapes the settings root");
|
|
1176
|
+
const stat = statSync$1(path);
|
|
1177
|
+
if (!stat.isFile()) throw new Error("path is not a regular file");
|
|
1178
|
+
if (stat.size > 65536) throw new Error("file exceeds 64 KiB");
|
|
1179
|
+
return path;
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
throw new Error(`Could not read ios.simslimProfile ${value}: ${String(error?.message || error)}`, { cause: error });
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
function iosString(settings, key) {
|
|
1185
|
+
if (!isPlainObject(settings) || !isPlainObject(settings.ios)) return void 0;
|
|
1186
|
+
return settings.ios[key];
|
|
1187
|
+
}
|
|
1188
|
+
function iosSigningIdentitySetting(settings) {
|
|
1189
|
+
const raw = iosString(settings, "signingIdentity");
|
|
1190
|
+
return typeof raw === "string" && raw.trim() !== "" ? raw.trim() : null;
|
|
1191
|
+
}
|
|
1192
|
+
function iosSigningIdentitySettingError(settings) {
|
|
1193
|
+
const raw = iosString(settings, "signingIdentity");
|
|
1194
|
+
if (raw === void 0) return null;
|
|
1195
|
+
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)".`;
|
|
1196
|
+
return null;
|
|
1197
|
+
}
|
|
1198
|
+
const SIGNING_IDENTITY_SHA1 = /^[0-9A-Fa-f]{40}$/;
|
|
1199
|
+
function iosSigningIdentitySha1Setting(settings) {
|
|
1200
|
+
const raw = iosString(settings, "signingIdentitySha1");
|
|
1201
|
+
return typeof raw === "string" && SIGNING_IDENTITY_SHA1.test(raw.trim()) ? raw.trim().toUpperCase() : null;
|
|
1202
|
+
}
|
|
1203
|
+
function iosSigningIdentitySha1SettingError(settings) {
|
|
1204
|
+
const raw = iosString(settings, "signingIdentitySha1");
|
|
1205
|
+
if (raw === void 0) return null;
|
|
1206
|
+
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.`;
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
const LAN_HOST = /^(?!-)[A-Za-z0-9-]+(?:\.(?!-)[A-Za-z0-9-]+)*$/;
|
|
1210
|
+
function iosLanHostSetting(settings) {
|
|
1211
|
+
const raw = iosString(settings, "lanHost");
|
|
1212
|
+
return typeof raw === "string" && LAN_HOST.test(raw.trim()) ? raw.trim() : null;
|
|
1213
|
+
}
|
|
1214
|
+
function iosLanHostSettingError(settings) {
|
|
1215
|
+
const raw = iosString(settings, "lanHost");
|
|
1216
|
+
if (raw === void 0) return null;
|
|
1217
|
+
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.`;
|
|
1218
|
+
return null;
|
|
1219
|
+
}
|
|
1220
|
+
function unknownSettingKeys(settings, prefix = "") {
|
|
1221
|
+
if (!isPlainObject(settings)) return [];
|
|
1222
|
+
const unknown = [];
|
|
1223
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
1224
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
1225
|
+
if (path === "$schema") continue;
|
|
1226
|
+
if (KNOWN_SETTINGS.has(path) && (!isPlainObject(value) || ![...KNOWN_SETTINGS].some((k) => k.startsWith(`${path}.`)))) continue;
|
|
1227
|
+
if (isPlainObject(value) && [...KNOWN_SETTINGS].some((k) => k.startsWith(`${path}.`))) {
|
|
1228
|
+
unknown.push(...unknownSettingKeys(value, path));
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
unknown.push(path);
|
|
1232
|
+
}
|
|
1233
|
+
return unknown;
|
|
1234
|
+
}
|
|
1235
|
+
function readCommittedSettings(directory) {
|
|
1236
|
+
if (!directory) return {};
|
|
1237
|
+
const p = join$1(directory, ".stim.json");
|
|
1238
|
+
if (!existsSync$1(p)) return {};
|
|
1239
|
+
try {
|
|
1240
|
+
const parsed = JSON.parse(readFileSync$1(p, "utf-8"));
|
|
1241
|
+
return isPlainObject(parsed) ? parsed : {};
|
|
1242
|
+
} catch {
|
|
1243
|
+
return {};
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
function setSettingValueAt(settings, path, value) {
|
|
1247
|
+
const segments = path.split(".");
|
|
1248
|
+
let node = settings;
|
|
1249
|
+
for (const segment of segments.slice(0, -1)) {
|
|
1250
|
+
const next = node[segment];
|
|
1251
|
+
if (!isPlainObject(next)) node[segment] = {};
|
|
1252
|
+
node = node[segment];
|
|
1253
|
+
}
|
|
1254
|
+
node[segments[segments.length - 1]] = value;
|
|
1255
|
+
}
|
|
1256
|
+
const MACHINE_READABLE_SETTINGS = SETTINGS.filter((setting) => isLayeredSetting(setting) && setting.scopes.includes("machine") && !setting.key.startsWith("optimizations."));
|
|
1257
|
+
function machineLayerSettings(machine) {
|
|
1258
|
+
const settings = {};
|
|
1259
|
+
const optimizations = settingValueAt(machine, "optimizations");
|
|
1260
|
+
if (optimizations !== void 0) settings.optimizations = optimizations;
|
|
1261
|
+
for (const setting of MACHINE_READABLE_SETTINGS) {
|
|
1262
|
+
const value = settingValueAt(machine, setting.key);
|
|
1263
|
+
if (value !== void 0) setSettingValueAt(settings, setting.key, value);
|
|
1264
|
+
}
|
|
1265
|
+
return Object.keys(settings).length > 0 ? settings : null;
|
|
1266
|
+
}
|
|
1267
|
+
function settingsLayers({ projectPath, gitCommonDir, repoRoot }) {
|
|
1268
|
+
const machine = loadConfig$1();
|
|
1269
|
+
const machineFile = getConfigPath$1();
|
|
1270
|
+
const committedDir = projectPath ?? repoRoot;
|
|
1271
|
+
const machineSettings = machineLayerSettings(machine);
|
|
1272
|
+
return [
|
|
1273
|
+
projectPath ? {
|
|
1274
|
+
scope: "workspace",
|
|
1275
|
+
file: `${machineFile} (projects["${projectPath}"].settings)`,
|
|
1276
|
+
settings: getProjectSettings(projectPath)
|
|
1277
|
+
} : null,
|
|
1278
|
+
gitCommonDir ? {
|
|
1279
|
+
scope: "repo",
|
|
1280
|
+
file: `${machineFile} (repos["${gitCommonDir}"].settings)`,
|
|
1281
|
+
settings: getRepoSettings(gitCommonDir)
|
|
1282
|
+
} : null,
|
|
1283
|
+
committedDir ? {
|
|
1284
|
+
scope: "committed",
|
|
1285
|
+
file: join$1(committedDir, ".stim.json"),
|
|
1286
|
+
settings: readCommittedSettings(committedDir)
|
|
1287
|
+
} : null,
|
|
1288
|
+
machineSettings ? {
|
|
1289
|
+
scope: "machine",
|
|
1290
|
+
file: machineFile,
|
|
1291
|
+
settings: machineSettings
|
|
1292
|
+
} : null
|
|
1293
|
+
].filter((layer) => layer !== null);
|
|
1294
|
+
}
|
|
1295
|
+
function settingOrigin(layers, dottedKey) {
|
|
1296
|
+
for (const layer of layers) {
|
|
1297
|
+
const value = settingValueAt(layer.settings, dottedKey);
|
|
1298
|
+
if (value !== void 0) return {
|
|
1299
|
+
file: layer.file,
|
|
1300
|
+
value
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
return null;
|
|
1304
|
+
}
|
|
1305
|
+
/** The layer a resolved setting's value came from, or null when no layer set it (a default or a flag). */
|
|
1306
|
+
function settingOriginScope(layers, dottedKey) {
|
|
1307
|
+
for (const layer of layers) if (settingValueAt(layer.settings, dottedKey) !== void 0) return layer.scope;
|
|
1308
|
+
return null;
|
|
1309
|
+
}
|
|
1310
|
+
function settingFile(context, dottedKey) {
|
|
1311
|
+
return settingOrigin(settingsLayers(context), dottedKey)?.file ?? null;
|
|
1312
|
+
}
|
|
1313
|
+
function resolveSettings(context) {
|
|
1314
|
+
return mergeSettingsLayers(settingsLayers(context).map((layer) => layer.settings));
|
|
1315
|
+
}
|
|
1316
|
+
function projectSettingsContext(root, git = {
|
|
1317
|
+
gitCommonDir,
|
|
1318
|
+
repoRoot
|
|
1319
|
+
}) {
|
|
1320
|
+
return {
|
|
1321
|
+
projectPath: root,
|
|
1322
|
+
gitCommonDir: git.gitCommonDir(root),
|
|
1323
|
+
repoRoot: git.repoRoot(root)
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
function resolveProjectSettings(root) {
|
|
1327
|
+
const context = projectSettingsContext(root);
|
|
1328
|
+
return {
|
|
1329
|
+
context,
|
|
1330
|
+
settings: resolveSettings(context)
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
const METRO_COMMAND_PORT = "{port}";
|
|
1334
|
+
function metroCommandSetting(settings) {
|
|
1335
|
+
const value = settingValueAt(settings, "metro.command");
|
|
1336
|
+
if (!Array.isArray(value) || !value.every((arg) => typeof arg === "string")) return null;
|
|
1337
|
+
return typeof value[0] === "string" && value[0].trim() !== "" ? value : null;
|
|
1338
|
+
}
|
|
1339
|
+
function metroCommandSettingError(settings) {
|
|
1340
|
+
if (settingValueAt(settings, "metro.command") === void 0) return null;
|
|
1341
|
+
const command = metroCommandSetting(settings);
|
|
1342
|
+
if (!command) return "metro.command must be a non-empty array of strings whose first entry is the program to run.";
|
|
1343
|
+
if (!command.some((arg) => arg.includes("{port}"))) return `metro.command must pass ${METRO_COMMAND_PORT} so the dev server listens on this workspace's reserved port.`;
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
function runnableMetroCommand(settings) {
|
|
1347
|
+
return metroCommandSettingError(settings) ? null : metroCommandSetting(settings);
|
|
1348
|
+
}
|
|
1349
|
+
function cacheBlock(layer) {
|
|
1350
|
+
if (!isPlainObject(layer) || !isPlainObject(layer.cache)) return null;
|
|
1351
|
+
return layer.cache;
|
|
1352
|
+
}
|
|
1353
|
+
function resolveCacheProviderConfig({ projectPath, gitCommonDir, repoRoot }) {
|
|
1354
|
+
const layers = [
|
|
1355
|
+
{
|
|
1356
|
+
settings: projectPath ? getProjectSettings(projectPath) : {},
|
|
1357
|
+
baseDir: projectPath ?? null
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
settings: gitCommonDir ? getRepoSettings(gitCommonDir) : {},
|
|
1361
|
+
baseDir: repoRoot ?? projectPath ?? null
|
|
1362
|
+
},
|
|
1363
|
+
{
|
|
1364
|
+
settings: readCommittedSettings(projectPath),
|
|
1365
|
+
baseDir: projectPath ?? null
|
|
1366
|
+
}
|
|
1367
|
+
];
|
|
1368
|
+
const references = layers.map((layer) => {
|
|
1369
|
+
const reference = cacheBlock(layer.settings)?.provider;
|
|
1370
|
+
return typeof reference === "string" && reference.trim() !== "" ? reference.trim() : null;
|
|
1371
|
+
});
|
|
1372
|
+
const selected = layers.findIndex((layer, index) => references[index] !== null && layer.baseDir !== null);
|
|
1373
|
+
if (selected === -1) return null;
|
|
1374
|
+
const provider = references[selected];
|
|
1375
|
+
const baseDir = layers[selected].baseDir;
|
|
1376
|
+
return {
|
|
1377
|
+
provider,
|
|
1378
|
+
options: mergeSettingsLayers(layers.map((layer, index) => {
|
|
1379
|
+
const namesProvider = references[index] === provider && layer.baseDir === baseDir;
|
|
1380
|
+
const overridesAbove = index < selected && references[index] === null;
|
|
1381
|
+
if (!namesProvider && !overridesAbove) return null;
|
|
1382
|
+
const block = cacheBlock(layer.settings)?.options;
|
|
1383
|
+
return isPlainObject(block) ? block : null;
|
|
1384
|
+
})),
|
|
1385
|
+
baseDir
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
function cacheProviderSettingError(settings) {
|
|
1389
|
+
const block = settings.cache;
|
|
1390
|
+
if (!isPlainObject(block)) return block === void 0 ? null : `Invalid cache setting ${JSON.stringify(block)}. Expected an object with provider and options.`;
|
|
1391
|
+
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.`;
|
|
1392
|
+
if ("options" in block && !isPlainObject(block.options)) return `Invalid cache.options setting ${JSON.stringify(block.options)}. Expected an object.`;
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
1395
|
+
/** The remote target `remote` names, or null for `local`, which runs on this Mac whatever a lower layer sets. */
|
|
1396
|
+
function parseIosRemote(remote) {
|
|
1397
|
+
const selected = remote.trim();
|
|
1398
|
+
const reserved = selected.toLowerCase();
|
|
1399
|
+
if (reserved === "local") return null;
|
|
1400
|
+
return REMOTE_DEVICE_BACKENDS.includes(reserved) ? {
|
|
1401
|
+
kind: "backend",
|
|
1402
|
+
backend: reserved
|
|
1403
|
+
} : {
|
|
1404
|
+
kind: "machine",
|
|
1405
|
+
machine: reserved === "auto" ? "auto" : selected
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
function remoteIosSetting(settings) {
|
|
1409
|
+
const ios = settings.ios;
|
|
1410
|
+
return isPlainObject(ios) && typeof ios.remote === "string" ? parseIosRemote(ios.remote) : null;
|
|
1411
|
+
}
|
|
1412
|
+
function remoteEasFallbackSetting(settings) {
|
|
1413
|
+
return settingValueAt(settings, "remote.easFallback") === true;
|
|
1414
|
+
}
|
|
1415
|
+
function remoteAndroidSetting(settings) {
|
|
1416
|
+
const android = settings.android;
|
|
1417
|
+
return isPlainObject(android) && typeof android.remote === "string" ? parseIosRemote(android.remote) : null;
|
|
1418
|
+
}
|
|
1419
|
+
function minutesSetting(settings, block, name) {
|
|
1420
|
+
const parent = settings[block];
|
|
1421
|
+
const value = isPlainObject(parent) ? parent[name] : void 0;
|
|
1422
|
+
const setting = settingDefinition(`${block}.${name}`);
|
|
1423
|
+
return setting && value !== void 0 && settingValueError(setting, value) === null ? value : Number(setting?.default);
|
|
1424
|
+
}
|
|
1425
|
+
function metroIdleStopMinutesSetting(settings) {
|
|
1426
|
+
return minutesSetting(settings, "metro", "idleStopMinutes");
|
|
1427
|
+
}
|
|
1428
|
+
function deviceIdleShutdownMinutesSetting(settings) {
|
|
1429
|
+
return minutesSetting(settings, "devices", "idleShutdownMinutes");
|
|
1430
|
+
}
|
|
1431
|
+
function deviceReclaimIdleMinutesSetting(settings) {
|
|
1432
|
+
return minutesSetting(settings, "devices", "reclaimIdleMinutes");
|
|
1433
|
+
}
|
|
1434
|
+
function maintenanceKeepSetting(settings) {
|
|
1435
|
+
const value = settingValueAt(settings, "maintenance.keep");
|
|
1436
|
+
return value !== void 0 && value !== false;
|
|
1437
|
+
}
|
|
1438
|
+
function tunnelModeSetting(settings) {
|
|
1439
|
+
const block = settings.metro;
|
|
1440
|
+
if (typeof block !== "object" || block === null) return null;
|
|
1441
|
+
const mode = block.tunnel;
|
|
1442
|
+
return typeof mode === "string" && TUNNEL_MODES.includes(mode) ? mode : null;
|
|
1443
|
+
}
|
|
1444
|
+
function metroTunnelSettingError(settings) {
|
|
1445
|
+
const block = settings.metro;
|
|
1446
|
+
if (!isPlainObject(block) || block.ngrokUrl === void 0) return null;
|
|
1447
|
+
if (block.tunnel !== "ngrok") return "metro.ngrokUrl requires metro.tunnel to be \"ngrok\".";
|
|
1448
|
+
if (normalizedHttpsUrl(block.ngrokUrl) === null) return "metro.ngrokUrl must be a valid HTTPS URL.";
|
|
1449
|
+
return null;
|
|
1450
|
+
}
|
|
1451
|
+
function metroPortSetting(settings, env = process.env) {
|
|
1452
|
+
const setting = settingDefinition("metro.port");
|
|
1453
|
+
const fromEnv = env.STIM_METRO_PORT?.trim();
|
|
1454
|
+
const value = fromEnv ? coerceSettingText(setting, fromEnv) : settingValueAt(settings, "metro.port");
|
|
1455
|
+
if (value === void 0) return {
|
|
1456
|
+
port: null,
|
|
1457
|
+
error: null
|
|
1458
|
+
};
|
|
1459
|
+
const problem = settingValueError(setting, value);
|
|
1460
|
+
if (problem === null) return {
|
|
1461
|
+
port: value,
|
|
1462
|
+
error: null
|
|
1463
|
+
};
|
|
1464
|
+
return {
|
|
1465
|
+
port: null,
|
|
1466
|
+
error: `Invalid ${fromEnv ? `STIM_METRO_PORT value ${JSON.stringify(env.STIM_METRO_PORT)}` : `metro.port setting ${JSON.stringify(value)}`}. Expected ${problem}.`
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
function publicUrlSetting(settings) {
|
|
1470
|
+
const block = settings.metro;
|
|
1471
|
+
if (typeof block !== "object" || block === null) return null;
|
|
1472
|
+
const url = block.publicUrl;
|
|
1473
|
+
return typeof url === "string" && url.trim() ? url : null;
|
|
1474
|
+
}
|
|
1475
|
+
function metroWarmupUrlSetting(settings, platform) {
|
|
1476
|
+
const value = settingValueAt(settings, `metro.warmupUrl.${platform}`);
|
|
1477
|
+
return typeof value === "string" ? value : null;
|
|
1478
|
+
}
|
|
1479
|
+
function ngrokUrlSetting(settings) {
|
|
1480
|
+
const block = settings.metro;
|
|
1481
|
+
if (typeof block !== "object" || block === null) return null;
|
|
1482
|
+
if (block.tunnel !== "ngrok") return null;
|
|
1483
|
+
return normalizedHttpsUrl(block.ngrokUrl);
|
|
1484
|
+
}
|
|
1485
|
+
function normalizedHttpsUrl(raw) {
|
|
1486
|
+
if (typeof raw !== "string" || !raw.trim()) return null;
|
|
1487
|
+
try {
|
|
1488
|
+
return new URL(raw.trim()).protocol === "https:" ? raw.trim().replace(/\/+$/, "") : null;
|
|
1489
|
+
} catch {
|
|
1490
|
+
return null;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
function webSettings(settings) {
|
|
1494
|
+
const url = settingValueAt(settings, "web.url");
|
|
1495
|
+
const viewport = settingValueAt(settings, "web.viewport");
|
|
1496
|
+
return {
|
|
1497
|
+
url: typeof url === "string" ? url : null,
|
|
1498
|
+
ignoreCertificateErrors: settingValueAt(settings, "web.ignoreCertificateErrors") === true,
|
|
1499
|
+
viewport: WEB_VIEWPORTS.includes(viewport) ? viewport : "desktop"
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
//#endregion
|
|
1503
|
+
export { settingValueAt as $, metroIdleStopMinutesSetting as A, sourceCheckoutOf as At, remoteAndroidSetting as B, iosSigningIdentitySha1Setting as C, podsOutOfSync as Ct, mergeSettingsLayers as D, resolveFullRef as Dt, maintenanceKeepSetting as E, repoRoot as Et, parseIosRemote as F, removeTemporaryEntry as Ft, resolveIosProjectDir as G, remoteIosSetting as H, projectSettingsContext as I, temporaryRoot as It, runnableMetroCommand as J, resolveProjectSettings as K, publicUrlSetting as L, metroTunnelSettingError as M, warmWorktreePaths as Mt, metroWarmupUrlSetting as N, filesystemDevice as Nt, metroCommandSetting as O, resolveSourceCheckout as Ot, ngrokUrlSetting as P, makeTemporaryDirectory as Pt, settingShapeErrors as Q, readCommittedSettings as R, iosSigningIdentitySettingError as S, locallyKnownUpstream as St, iosSimSlimProfileSetting as T, removeWorktree as Tt, resolveAndroidLayout as U, remoteEasFallbackSetting as V, resolveCacheProviderConfig as W, settingOrigin as X, settingFile as Y, settingOriginScope as Z, iosLanHostSetting as _, hasRemote as _t, SETTING_SHAPE_REMEDY as a, detectProviders as at, iosProjectDirSettingError as b, linkedWorktreesOnDisk as bt, androidDataPartitionSizeBytes as c, branchExists as ct, androidLayoutSetting as d, depsOutOfSync as dt, settingsLayers as et, androidLayoutSettingError as f, dirtyFingerprintFiles as ft, deviceReclaimIdleMinutesSetting as g, hasPopulatedSubmodules as gt, deviceIdleShutdownMinutesSetting as h, gitCommonDirOnDisk as ht, SETTINGS_WITH_RESOLVE_TIME_FALLBACK as i, PUBLIC_METRO_ENV as it, metroPortSetting as j, unpushedCommits as jt, metroCommandSettingError as k, restoreFile as kt, androidDataPartitionSizeGbSetting as l, cloneIgnoredEntries as lt, defaultAndroidLayout as m, gitCommonDir as mt, METRO_COMMAND_PORT as n, unknownSettingKeys as nt, androidAvdConfigSetting as o, planMetroReach as ot, cacheProviderSettingError as p, dirtyPaths as pt, resolveSettings as q, PATH_SETTINGS as r, webSettings as rt, androidAvdConfigSettingError as s, resolveTailscaleBinary as st, ANDROID_AVD_CONFIG_HELP as t, tunnelModeSetting as tt, androidDataPartitionSizeGbSettingError as u, deleteBranch as ut, iosLanHostSettingError as v, hasUncommittedWork as vt, iosSigningIdentitySha1SettingError as w, readWorktreeExclude as wt, iosSigningIdentitySetting as x, listWorktrees as xt, iosProjectDirSetting as y, isPodInstallChurn as yt, realpathInside as z };
|