stim 1.20.2 → 1.22.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 +141 -8
- package/dist/acquire-artifact-Co2k4f7O.mjs +171 -0
- package/dist/act-DsuqEkiM.mjs +2801 -0
- package/dist/activity-CMLmzp7o.mjs +1397 -0
- package/dist/{agent-device-usage-output-Dzgv6lgR.mjs → agent-device-usage-output-DUFaTMdd.mjs} +23 -67
- package/dist/{android-DCKZrNvV.mjs → android-ByRMUoiJ.mjs} +157 -93
- package/dist/{android-Mz_Inn3w.mjs → android-C7sVmyI9.mjs} +299 -116
- package/dist/{android-Dlzxshgp.mjs → android-DGkNqa_c.mjs} +5 -5
- package/dist/{android-HqP1_vB_.mjs → android-DR5lZ2Az.mjs} +2 -2
- package/dist/android-cas-compiler.mjs +2 -2
- package/dist/{android-cas-Bl-cLJBk.mjs → android-cas-ngferVq2.mjs} +7 -6
- package/dist/android-project-BCAAmMwz.mjs +12 -0
- package/dist/android-project-DVWyCbWU.d.mts +391 -0
- package/dist/api.d.mts +810 -64
- package/dist/api.mjs +782 -1
- package/dist/{artifact-HSwkIWEQ.mjs → artifact-8EY8SAPw.mjs} +240 -213
- package/dist/{artifact-D3czlUvT.mjs → artifact-DJPy518Y.mjs} +203 -164
- package/dist/artifact-platform-CXcmX_j3.mjs +27 -0
- package/dist/{browser-web-CA-F1GQo.mjs → browser-web-Bcv99PBM.mjs} +194 -64
- package/dist/budget-Cvwn54qV.mjs +9285 -0
- package/dist/{build-DYDbfkk5.mjs → build-CabC3G23.mjs} +153 -98
- package/dist/build-cache-Dv7RN200.mjs +602 -0
- package/dist/build-plan-DUC8Ka9y.mjs +248 -0
- package/dist/{cache-manifest-XH-fvDUj.mjs → cache-manifest-DSeQjt-Q.mjs} +2 -2
- package/dist/cache-manifest.mjs +1 -1
- package/dist/{ccache-zdKiYQ3E.mjs → ccache-Btwc36-I.mjs} +10 -9
- package/dist/{cdp-Dq_ypAlJ.mjs → cdp-3fwICjzi.mjs} +18 -5
- package/dist/{chrome-BnzZMqsR.mjs → chrome-Bbw689-6.mjs} +7 -4
- package/dist/{cli-CLP3diBm.mjs → cli-CHZa9Lvk.mjs} +20 -20
- package/dist/cli.mjs +1 -1
- package/dist/client-OyRoEuSr.mjs +1577 -0
- package/dist/collector-run.d.mts +4 -40
- package/dist/collector-run.mjs +9 -8
- package/dist/{command-output-BTy8hZ0F.mjs → command-output-gwpm28fu.mjs} +1 -1
- package/dist/config-DlrsduwG.mjs +955 -0
- package/dist/{created-devices-Dzd5C2LF.mjs → created-devices-x9rN6bhs.mjs} +14 -4
- package/dist/{dependency-state-DG0p31-y.mjs → dependency-state-1I9KZT1Y.mjs} +20 -5
- package/dist/{detached-entry-B9C_zeQC.mjs → detached-entry-BwNEtIrP.mjs} +1 -1
- package/dist/{dev-client-DAWVIGBu.mjs → dev-client-BSDR5h3F.mjs} +2 -2
- package/dist/{device-BECOiufl.mjs → device-B-L3Fbxy.mjs} +91 -34
- package/dist/device-DklWZLqb.mjs +1068 -0
- package/dist/{device-capacity-CBUAcRyQ.mjs → device-capacity-DK5_EwRS.mjs} +42 -13
- package/dist/device-host-worker.mjs +76 -79
- package/dist/{device-ios-COSi6oS1.mjs → device-ios-DoB6KFCO.mjs} +77 -51
- package/dist/{device-lease-run-Cw2M4z51.mjs → device-lease-run-xJPQ_QQG.mjs} +14 -26
- package/dist/{device-pool-Bf-pOhSw.mjs → device-pool-BMkdsefq.mjs} +5 -5
- package/dist/{doctor-kfMBnUw6.mjs → doctor-Bo7BXLux.mjs} +271 -320
- package/dist/{doctor-CB8oZKCk.mjs → doctor-D5_EzwfA.mjs} +186 -91
- package/dist/doctor-fingerprint-DZLoYFVb.mjs +16 -0
- package/dist/doctor-fingerprint.mjs +19 -0
- package/dist/doctor-thjWAV0z.mjs +2 -0
- package/dist/{doctor-watchman-BRyV2adA.mjs → doctor-watchman-N2wDotwQ.mjs} +40 -21
- package/dist/{eas-build-JyfqyHfC.mjs → eas-build-CwknaQYj.mjs} +53 -20
- package/dist/{build-plan-B7SZsc2N.mjs → eas-fallback-CSgAulr5.mjs} +41 -214
- package/dist/{watchman-CPQFZLXY.mjs → eas-session-ledger-DlY_serQ.mjs} +230 -11
- package/dist/{error-diagnostics-BR677IDM.mjs → error-diagnostics-DBWO2rxE.mjs} +172 -82
- package/dist/exec-DEoAhcze.mjs +858 -0
- package/dist/exec-DnYc4HGH.d.mts +78 -0
- package/dist/{gc-qWOMdnER.mjs → gc-CXxN-4SE.mjs} +1091 -1029
- package/dist/gc-DnNDQyaP.mjs +3 -0
- package/dist/{guide-G9cAbVAc.mjs → guide-Br0LEfKy.mjs} +1165 -297
- package/dist/{hosted-android-C3MHGld1.mjs → hosted-android-B-9f-jkd.mjs} +10 -8
- package/dist/{hosted-client-ZEKpS9vw.mjs → hosted-client-DZAPrcro.mjs} +34 -19
- package/dist/{hosted-ios-l-o3iM9J.mjs → hosted-ios-CsCS21hn.mjs} +1 -1
- package/dist/{hosted-logs-EzQM7-3Z.mjs → hosted-logs-FIRx9X2f.mjs} +77 -43
- package/dist/{hosted-macos-BACVAVqE.mjs → hosted-macos-_awYSpfv.mjs} +35 -22
- package/dist/{hosted-native-A4s1QPXe.mjs → hosted-native-BdzE668-.mjs} +49 -27
- package/dist/idle-C4cZ757K.d.mts +24 -0
- package/dist/{idle-shutdown-DtxhPTKe.mjs → idle-shutdown-DDg3SdvY.mjs} +11 -10
- package/dist/ios-CDvlmUs7.d.mts +24 -0
- package/dist/{ios-BwVCF7oA.mjs → ios-Ds1clbIt.mjs} +8 -8
- package/dist/{ios-device-D7pi2Sca.mjs → ios-device--h_-8ENa.mjs} +51 -26
- package/dist/{ios-device-B5veSDDQ.mjs → ios-device-BXZ0c7Kb.mjs} +1 -1
- package/dist/{ios-RkW4pnQf.mjs → ios-hcBpnEGk.mjs} +200 -93
- package/dist/{ios-Ce7bc7br.mjs → ios-pm4nL6By.mjs} +160 -108
- package/dist/{ios-project-K9hY_Jab.mjs → ios-project-CZhKODF3.mjs} +13 -13
- package/dist/ios-signing-BtAffm0A.mjs +489 -0
- package/dist/{ios-state-CBLe1xYw.mjs → ios-state-Dtw5K2aI.mjs} +1 -1
- package/dist/{ios-nOLJGVdy.mjs → ios-xtlRHaeB.mjs} +1 -1
- package/dist/{js-swap-Bb3nsS07.mjs → js-swap-Bwhh4S7V.mjs} +30 -25
- package/dist/{launch-C3qJ6Agb.mjs → launch-BjxemNpK.mjs} +85 -543
- package/dist/{launch-F4sOqAhB.mjs → launch-CCzXlc8r.mjs} +91 -90
- package/dist/logs-BfwwadsZ.mjs +479 -0
- package/dist/logs-jTS_lHSo.mjs +248 -0
- package/dist/{macos-D7uDvO-z.mjs → macos-DVlSSyUg.mjs} +1 -1
- package/dist/macos-NfV2D7dO.mjs +694 -0
- package/dist/macos-run.mjs +1 -1
- package/dist/maintenance-run.mjs +123 -248
- package/dist/{metro-DnCEjCxs.mjs → metro-DiXtA3Ty.mjs} +107 -30
- package/dist/metro-gateway-CKZqKqx7.mjs +209 -0
- package/dist/{named-ports-4WopRPg-.mjs → named-ports-DkEzOfzO.mjs} +42 -90
- package/dist/native-android-CqgnOgsT.mjs +645 -0
- package/dist/native-gradle-artifact-inputs-DHFnmrVf.mjs +270 -0
- package/dist/native-gradle-inputs-Dnh84Zbd.mjs +211 -0
- package/dist/native-inputs-TRTTTRxt.mjs +91 -0
- package/dist/native-run-NwUuxbkg.mjs +276 -0
- package/dist/{native-runtime-k8dTIrbK.mjs → native-runtime-B7KIafpM.mjs} +11 -9
- package/dist/native-source-Y0yLhFyQ.mjs +133 -0
- package/dist/{native-xcode-inputs-CykopJdM.mjs → native-xcode-inputs-D_VOfFL7.mjs} +26 -18
- package/dist/{native-xcode-ios-Bbf30Gm0.mjs → native-xcode-ios-BYDOHWLN.mjs} +104 -60
- package/dist/{ndjson-GOnbyvso.mjs → ndjson-CgE_o3Dl.mjs} +3 -3
- package/dist/offload-worker.d.mts +59 -41
- package/dist/offload-worker.mjs +478 -115
- package/dist/operation-exec-BW0em61G.mjs +171 -0
- package/dist/{ownership-DCtw_2IA.mjs → ownership-C5fo5Edb.mjs} +3 -3
- package/dist/{page-DRHy329N.mjs → page-DBL2PiUU.mjs} +12 -5
- package/dist/paths-BsJMoPP2.mjs +12 -0
- package/dist/paths-DQtTgsBf.mjs +42 -0
- package/dist/{placement-log-MOfPVgaM.mjs → placement-log-BnGgJRRZ.mjs} +5 -5
- package/dist/{plan-placement-V0TdzYXp.mjs → plan-placement-DIsMtN-d.mjs} +15 -11
- package/dist/{ports-DdSUicVo.mjs → ports-Cy1RkmuQ.mjs} +3 -3
- package/dist/prebuild-D4JoEAEs.mjs +484 -0
- package/dist/{project-_phNIJfs.mjs → project-wM_XFTvE.mjs} +65 -37
- package/dist/{pull-request-CQ8sPbZa.mjs → pull-request-W1IuLXDt.mjs} +102 -20
- package/dist/pull-request-eODyIqZF.d.mts +124 -0
- package/dist/pull-requests.d.mts +2 -120
- package/dist/pull-requests.mjs +2 -2
- package/dist/{react-native-android-CM1XRDwv.mjs → react-native-android-6OZ7MSM7.mjs} +121 -72
- package/dist/{gradle-DSrXb6lG.mjs → react-native-build-Dvd8hC21.mjs} +292 -15
- package/dist/react-native-doctor-DsoSF1Y9.mjs +234 -0
- package/dist/{react-native-ios-Dhq_eiYQ.mjs → react-native-ios-4u1Rn-GR.mjs} +71 -39
- package/dist/{recordings-D0d5aQt0.mjs → recordings-CQmfzAh7.mjs} +12 -9
- package/dist/registration-BcRgF6Ox.mjs +51 -0
- package/dist/{reload-C8JhxjSh.mjs → reload-BzkYabB-.mjs} +94 -47
- package/dist/reload-D7AVlb8N.mjs +2 -0
- package/dist/{remote-BiR6aYjy.mjs → remote-DjGDmlTZ.mjs} +26 -18
- package/dist/result-D2Ykz_iD.mjs +435 -0
- package/dist/{run-BtPgpKks.mjs → run-DidHghD1.mjs} +5 -5
- package/dist/selection-DSMbWEX2.mjs +620 -0
- package/dist/{server-bare-R2m_gsH8.mjs → server-bare-CafzuyFB.mjs} +3 -4
- package/dist/{server-command-CTtCkmcQ.mjs → server-command-C6QjeupS.mjs} +14 -7
- package/dist/{server-expo-DsguMGUF.mjs → server-expo-CrjB29pq.mjs} +54 -11
- package/dist/server-expo-D58c9L0M.mjs +2 -0
- package/dist/{settings-B717odTD.mjs → settings-C7O8A6ZI.mjs} +309 -119
- package/dist/settings-DpR-1Wnn.mjs +114 -0
- package/dist/settings-write-CzD60rOD.mjs +360 -0
- package/dist/settings.schema.json +176 -16
- package/dist/{simslim-CCr3rE-A.mjs → simslim-BNckWpXi.mjs} +14 -12
- package/dist/{slot-launch-DPt0m9Nd.mjs → slot-launch-CbfmNgM6.mjs} +15 -20
- package/dist/{spawn-entry-Dcb7sDUz.mjs → spawn-entry-jw19m8Nl.mjs} +0 -1
- package/dist/stage-Bps5oius.mjs +200 -0
- package/dist/{start-TXeMmmO2.mjs → start-Br-oxqMD.mjs} +1 -1
- package/dist/start-EojN4WFz.mjs +1364 -0
- package/dist/{state-D1Icyjlu.mjs → state--smjFSTL.mjs} +55 -13
- package/dist/{state-CG_OFoaA.mjs → state-08I3hw3_.mjs} +3 -2
- package/dist/{stats-BoY5sEPU.mjs → stats-D3GerlFW.mjs} +7 -13
- package/dist/stats-report-BzaPrRm6.mjs +16 -0
- package/dist/{status-BTA7kKiQ.mjs → status-BRROykC8.mjs} +7 -5
- package/dist/status-EvrK72L2.mjs +2 -0
- package/dist/{status-WgFbkRzj.mjs → status-WBDajew2.mjs} +554 -452
- package/dist/{status-watch-C23ILtf9.mjs → status-watch-Bg_u-pwR.mjs} +222 -25
- package/dist/status-worker.d.mts +1 -0
- package/dist/status-worker.mjs +19 -0
- package/dist/stim-desktop-D5OGpYZM.mjs +112 -0
- package/dist/{stop-DR6VoDIk.mjs → stop-DTZ9uTw3.mjs} +2 -2
- package/dist/{stop-DPEhunlw.mjs → stop-Dky10AEJ.mjs} +33 -23
- package/dist/{stop-BNmBMi1F.mjs → stop-Mv6W9FAk.mjs} +137 -98
- package/dist/supervisor-run.d.mts +8 -17
- package/dist/supervisor-run.mjs +56 -28
- package/dist/{support-CwjbFs9H.mjs → support-CNioewsd.mjs} +14 -14
- package/dist/{support-DMTqj3Hm.mjs → support-yUiOjnVY.mjs} +20 -13
- package/dist/swiftpm-artifact-inputs-DFcGsXDP.mjs +153 -0
- package/dist/swiftpm-macos-BIak-Rxv.mjs +532 -0
- package/dist/toolchain-BY8X8HXx.mjs +2 -0
- package/dist/toolchain-DpJMHq0n.mjs +876 -0
- package/dist/{trigger-CigAYjeL.mjs → trigger-Cbyc41d_.mjs} +21 -6
- package/dist/trigger-yb_nFzPF.mjs +2 -0
- package/dist/{warm-progress-BuY-Fvfn.mjs → warm-progress-CA3TgT3-.mjs} +263 -156
- package/dist/web-CZ07gd77.mjs +2 -0
- package/dist/{web-BDBMxDpx.mjs → web-DKx6yoFA.mjs} +20 -14
- package/dist/web-run.mjs +13 -12
- package/dist/windows-report.d.mts +1 -0
- package/dist/windows-report.mjs +18 -0
- package/dist/windows-report.ps1 +43 -0
- package/dist/{workspace-state-DJt_MMvf.mjs → workspace-state-U08dEzhG.mjs} +62 -58
- package/dist/{workspaces-C36s2jb9.mjs → workspaces-CpANmBlq.mjs} +1723 -1286
- package/dist/{worktree-CgvJYJ-Q.mjs → worktree-CHecZ4Oq.mjs} +1097 -547
- package/dist/worktree-ZqZVjD9K.mjs +2 -0
- package/dist/xcode-BXn9-xYx.mjs +896 -0
- package/package.json +7 -5
- package/shim/android-audio/Main.java +24 -0
- package/shim/android-audio/README.md +17 -0
- package/shim/android-audio.jar +0 -0
- package/shim/android-optimizations.gradle +7 -5
- package/dist/activity-BSV745Kz.mjs +0 -629
- package/dist/api-qvOzWyfO.mjs +0 -154
- package/dist/api-run.mjs +0 -294
- package/dist/app-install-BIKtMfPq.mjs +0 -896
- package/dist/artifact-lifecycle-DzPpBYvK.mjs +0 -30
- package/dist/budget-f7516Zk0.mjs +0 -4106
- package/dist/build-facts-DcLv2HSd.d.mts +0 -111
- package/dist/cancellation-C_22VQTH.mjs +0 -24
- package/dist/client-qQAaXSUs.mjs +0 -1771
- package/dist/config-BYDxlMY3.mjs +0 -562
- package/dist/device-DuiPJQSn.mjs +0 -290
- package/dist/device-lease-BX1sJQ2E.mjs +0 -1224
- package/dist/device-remote-xRT0Vmo9.mjs +0 -1272
- package/dist/eas-session-ledger-Cm0Gwq2F.mjs +0 -207
- package/dist/exec-DqKo9GKJ.mjs +0 -268
- package/dist/git-DDfFzkMm.mjs +0 -45
- package/dist/launch-verify-B6Q8hZP-.mjs +0 -428
- package/dist/logs-BGrEUaEs.mjs +0 -383
- package/dist/macos-Ch5VJmcS.mjs +0 -584
- package/dist/metro-gateway-CX9Xlkq2.mjs +0 -103
- package/dist/miss-reason-mKzZ4VvN.mjs +0 -238
- package/dist/native-android-D5qc3-_v.mjs +0 -226
- package/dist/native-gradle-inputs-BsLRrl7u.mjs +0 -349
- package/dist/ndjson-Du3j0AQ9.d.mts +0 -17
- package/dist/plan-C4TjdPsR.mjs +0 -260
- package/dist/prebuild-DYbucPXy.mjs +0 -212
- package/dist/preview-78a9kR_-.mjs +0 -1011
- package/dist/react-native-build-CtCccJtb.mjs +0 -239
- package/dist/react-native-doctor-CfRnFfjs.mjs +0 -78
- package/dist/remote-cache-OyOyVIS3.mjs +0 -526
- package/dist/result-CaJg5Iat.mjs +0 -186
- package/dist/server-expo-Bigriskl.mjs +0 -2
- package/dist/settings-BORNaaCO.mjs +0 -402
- package/dist/settings-vk3mJGSm.d.mts +0 -13
- package/dist/spawn-claims-DVS5RkMS.mjs +0 -72
- package/dist/stage-KNTQopqv.mjs +0 -157
- package/dist/start-BWqzODpb.mjs +0 -1116
- package/dist/stim-desktop-BzBN2Em2.mjs +0 -63
- package/dist/swiftpm-macos-DRNVJWlb.mjs +0 -119
- package/dist/tailnet-Dh-qiYzG.mjs +0 -265
- package/dist/toolchain-BFkAcstd.mjs +0 -2
- package/dist/toolchain-DxSsNssu.mjs +0 -1657
- package/dist/trigger-9_USgmxz.mjs +0 -2
- package/dist/web-Dq-i3wzJ.mjs +0 -2
- package/dist/worktree-DpLdCHga.mjs +0 -2
- package/dist/{api-run.d.mts → doctor-fingerprint.d.mts} +0 -0
- package/dist/{errors-DzkMYh8Q.mjs → errors-ccaPc4ox.mjs} +2 -2
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
import { _ as reportSignal, g as process_identity_exports, r as WorkspaceProcessCleanupError, t as getExecutor, x as ownership_claim_exports, y as trackReportResource } from "./exec-DEoAhcze.mjs";
|
|
2
|
+
import { t as runOperationFile } from "./operation-exec-BW0em61G.mjs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { ambientEnv, configDir, ensurePrivateDir } from "@stim-cli/core";
|
|
5
|
+
import { allMetroPorts, cannotHost, clientIdentity, configCorruptRepair, endpoint, findEnclosingWorktreeRoot, findPeer as findPeer$1, getConcurrencyLimits, getConfigPath, getConfigPath as getConfigPath$1, getProject, getProjectSettings, getRepoSettings, isJsonObject, isPathPrefix, loadConfig, loadConfig as loadConfig$1, machineEndpoint, parseMachine, parseMachine as parseMachine$1, validateAutomaticPool, withConfigLock, withConfigLock as withConfigLock$1, withConfigLockAsync, withConfigLockAsync as withConfigLockAsync$1 } from "@stim-cli/core/state";
|
|
6
|
+
import { dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, normalize, parse, sep as sep$1 } from "path";
|
|
7
|
+
import { WebSocket } from "ws";
|
|
8
|
+
import { HandshakeRefusal, MAX_SERVER_MESSAGE_BYTES, TRANSPORT_OVERHEAD_BYTES, decodeSecret, initiateNoise, newSecret } from "@stim-cli/core/noise-session";
|
|
9
|
+
import { existsSync, lstatSync, readdirSync, readlinkSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
10
|
+
import { InvalidArgumentError } from "commander";
|
|
11
|
+
//#region src/offload/access-ticket.ts
|
|
12
|
+
/** The setup ticket in `STIM_ACCESS_TICKET`, which Stim Desktop's Add Machine flow sets, or undefined. */
|
|
13
|
+
function readAccessTicket() {
|
|
14
|
+
const ticket = ambientEnv().STIM_ACCESS_TICKET?.trim();
|
|
15
|
+
return ticket && decodeSecret(ticket) ? ticket : void 0;
|
|
16
|
+
}
|
|
17
|
+
function readHostPermissions(value) {
|
|
18
|
+
const host = value;
|
|
19
|
+
return host && typeof host.name === "string" && typeof host.screenRecording === "boolean" && typeof host.accessibility === "boolean" ? {
|
|
20
|
+
name: host.name,
|
|
21
|
+
screenRecording: host.screenRecording,
|
|
22
|
+
accessibility: host.accessibility
|
|
23
|
+
} : void 0;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/offload/tailnet.ts
|
|
27
|
+
const MAC_APP_TAILSCALE = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
|
|
28
|
+
const HELLO_TIMEOUT_MS = 1e4;
|
|
29
|
+
/** How long a setup ticket `stim doctor --fix` generates stays valid; `stim-server setup` takes at most 2 hours. */
|
|
30
|
+
const GENERATED_TICKET_MS = 714e4;
|
|
31
|
+
function parseTailscaleStatus(output) {
|
|
32
|
+
try {
|
|
33
|
+
const status = JSON.parse(output);
|
|
34
|
+
return isJsonObject(status) && status.BackendState === "Running" ? status : null;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function tailscaleStatus() {
|
|
40
|
+
for (const binary of ["tailscale", MAC_APP_TAILSCALE]) {
|
|
41
|
+
const output = getExecutor().runFileQuiet(binary, ["status", "--json"], { timeoutMs: 5e3 });
|
|
42
|
+
if (output !== null) return parseTailscaleStatus(output);
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
async function tailscaleStatusAsync(signal) {
|
|
47
|
+
for (const binary of ["tailscale", MAC_APP_TAILSCALE]) {
|
|
48
|
+
signal?.throwIfAborted();
|
|
49
|
+
let output;
|
|
50
|
+
try {
|
|
51
|
+
output = await getExecutor().runFileAsync(binary, ["status", "--json"], {
|
|
52
|
+
timeoutMs: 5e3,
|
|
53
|
+
signal
|
|
54
|
+
});
|
|
55
|
+
} catch {
|
|
56
|
+
signal?.throwIfAborted();
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
signal?.throwIfAborted();
|
|
60
|
+
return parseTailscaleStatus(output);
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const TURNED_AWAY = /* @__PURE__ */ new Set([
|
|
65
|
+
"unauthorized",
|
|
66
|
+
"forbidden",
|
|
67
|
+
"protocol-unsupported",
|
|
68
|
+
"server-key-mismatch"
|
|
69
|
+
]);
|
|
70
|
+
/** The connect code for a protocol 1 stim-server, which closes on the binary Noise prefix before answering it. */
|
|
71
|
+
const SERVER_OUTDATED = "server-outdated";
|
|
72
|
+
/**
|
|
73
|
+
* The doctor finding for a protocol 1 stim-server on `entry`, shared by the build and device-host checks so doctor
|
|
74
|
+
* prints it once. `command` is the setup command, when this machine has an unexpired ticket for it.
|
|
75
|
+
*/
|
|
76
|
+
function outdatedServer(entry, version, command) {
|
|
77
|
+
const update = `On ${entry}, run \`stim-server service update --release ${version}\` (add \`--label <name>\` for a custom service label; a server Stim Desktop runs updates with Stim Desktop)`;
|
|
78
|
+
return {
|
|
79
|
+
title: `Remote machine ${entry} runs an older stim-server`,
|
|
80
|
+
detail: `The stim-server on ${entry} speaks an older protocol than this Stim, so it cannot authenticate this machine.`,
|
|
81
|
+
fix: command ? `${update}, then run this there to approve this machine: ${command}\nAfter approving, run \`stim doctor\` on this machine to reconnect.` : `${update}, then run \`stim doctor --fix\` here.`
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Opens a WebSocket to `target` and runs the protocol 2 handshake with this machine's client key. A returning client
|
|
86
|
+
* goes ahead only with the server that proves it holds the pinned key.
|
|
87
|
+
*/
|
|
88
|
+
function openAuthenticated(target, auth, timeoutMs, signal, operation) {
|
|
89
|
+
signal?.throwIfAborted();
|
|
90
|
+
return new Promise((resolve) => {
|
|
91
|
+
const options = {
|
|
92
|
+
handshakeTimeout: timeoutMs,
|
|
93
|
+
servername: target.servername,
|
|
94
|
+
headers: { Host: target.host },
|
|
95
|
+
maxPayload: MAX_SERVER_MESSAGE_BYTES + TRANSPORT_OVERHEAD_BYTES
|
|
96
|
+
};
|
|
97
|
+
const socket = new WebSocket(target.url, options);
|
|
98
|
+
if (reportSignal(operation)) trackReportResource(operation, new Promise((done) => socket.once("close", () => done())), () => socket.terminate());
|
|
99
|
+
let settled = false;
|
|
100
|
+
const finish = (value) => {
|
|
101
|
+
if (settled) return;
|
|
102
|
+
settled = true;
|
|
103
|
+
signal?.removeEventListener("abort", cancel);
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
if (!("send" in value)) {
|
|
106
|
+
socket.removeAllListeners("open");
|
|
107
|
+
socket.removeAllListeners("message");
|
|
108
|
+
socket.removeAllListeners("error");
|
|
109
|
+
socket.on("error", () => {});
|
|
110
|
+
socket.terminate();
|
|
111
|
+
}
|
|
112
|
+
resolve(value);
|
|
113
|
+
};
|
|
114
|
+
const cancel = () => finish({
|
|
115
|
+
failure: "cancelled",
|
|
116
|
+
refused: false,
|
|
117
|
+
code: "STIM_CANCELLED"
|
|
118
|
+
});
|
|
119
|
+
const timer = setTimeout(() => finish({
|
|
120
|
+
failure: "no reply in time",
|
|
121
|
+
refused: false
|
|
122
|
+
}), timeoutMs);
|
|
123
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
124
|
+
if (signal?.aborted) return cancel();
|
|
125
|
+
const failed = (error) => finish({
|
|
126
|
+
failure: /Unexpected server response: 502/.test(error.message) ? "no stim-server answers behind its tailscale serve route (HTTP 502)" : error.message,
|
|
127
|
+
refused: false
|
|
128
|
+
});
|
|
129
|
+
socket.once("error", failed);
|
|
130
|
+
socket.once("open", () => {
|
|
131
|
+
const secret = "ticket" in auth ? decodeSecret(auth.ticket) : null;
|
|
132
|
+
let keyPair;
|
|
133
|
+
try {
|
|
134
|
+
keyPair = clientIdentity();
|
|
135
|
+
} catch (error) {
|
|
136
|
+
return finish({
|
|
137
|
+
failure: `this machine's client key is unavailable: ${error.message}`,
|
|
138
|
+
refused: false
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
(async () => {
|
|
142
|
+
try {
|
|
143
|
+
const noise = await initiateNoise(socket, {
|
|
144
|
+
keyPair,
|
|
145
|
+
..."serverKey" in auth ? { serverKey: Buffer.from(auth.serverKey, "base64url") } : {},
|
|
146
|
+
...secret ? {
|
|
147
|
+
secret,
|
|
148
|
+
name: "name" in auth ? auth.name : ""
|
|
149
|
+
} : {},
|
|
150
|
+
timeoutMs
|
|
151
|
+
});
|
|
152
|
+
socket.off("error", failed);
|
|
153
|
+
socket.on("error", () => {});
|
|
154
|
+
finish(noise);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
const code = error instanceof HandshakeRefusal ? error.code : "handshake-failed";
|
|
157
|
+
if (code === "unexpected binary frame") return finish({
|
|
158
|
+
failure: "its stim-server speaks an older protocol than this Stim",
|
|
159
|
+
refused: true,
|
|
160
|
+
code: SERVER_OUTDATED
|
|
161
|
+
});
|
|
162
|
+
finish({
|
|
163
|
+
failure: error.message,
|
|
164
|
+
refused: TURNED_AWAY.has(code) || code === "unknown-secret",
|
|
165
|
+
code
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
})();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/** Sends `hello` on an authenticated connection; a close before the reply carries the server's refusal code. */
|
|
173
|
+
function sayHello(socket, params, timeoutMs) {
|
|
174
|
+
return new Promise((resolve) => {
|
|
175
|
+
const done = (reply) => {
|
|
176
|
+
clearTimeout(timer);
|
|
177
|
+
socket.off("message", onMessage);
|
|
178
|
+
socket.off("close", onClose);
|
|
179
|
+
resolve(reply);
|
|
180
|
+
};
|
|
181
|
+
const onMessage = (data, isBinary) => {
|
|
182
|
+
if (isBinary) return;
|
|
183
|
+
let message;
|
|
184
|
+
try {
|
|
185
|
+
message = JSON.parse(data.toString("utf8"));
|
|
186
|
+
} catch {
|
|
187
|
+
message = null;
|
|
188
|
+
}
|
|
189
|
+
if (!isJsonObject(message) || message.id !== 1) return;
|
|
190
|
+
if (isJsonObject(message.error)) return done({ error: {
|
|
191
|
+
code: String(message.error.code),
|
|
192
|
+
message: String(message.error.message)
|
|
193
|
+
} });
|
|
194
|
+
done({ result: isJsonObject(message.result) ? message.result : {} });
|
|
195
|
+
};
|
|
196
|
+
const onClose = (code, reason) => done({ error: {
|
|
197
|
+
code: reason.length ? reason.toString() : "closed",
|
|
198
|
+
message: `the connection closed before a hello result (${code}${reason.length ? ` ${reason.toString()}` : ""})`
|
|
199
|
+
} });
|
|
200
|
+
const timer = setTimeout(() => done({ error: {
|
|
201
|
+
code: "timeout",
|
|
202
|
+
message: "no reply in time"
|
|
203
|
+
} }), timeoutMs);
|
|
204
|
+
socket.on("message", onMessage);
|
|
205
|
+
socket.on("close", onClose);
|
|
206
|
+
socket.send(JSON.stringify({
|
|
207
|
+
id: 1,
|
|
208
|
+
method: "hello",
|
|
209
|
+
params
|
|
210
|
+
}));
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
async function hello(target, auth, operation) {
|
|
214
|
+
const socket = await openAuthenticated(target, auth, HELLO_TIMEOUT_MS, reportSignal(operation), operation);
|
|
215
|
+
if (!("send" in socket)) return socket.code ? { error: {
|
|
216
|
+
code: socket.code,
|
|
217
|
+
message: socket.failure
|
|
218
|
+
} } : { failed: socket.failure };
|
|
219
|
+
try {
|
|
220
|
+
if ("onServerKey" in auth) await auth.onServerKey?.(socket.remoteKey.toString("base64url"));
|
|
221
|
+
} catch (error) {
|
|
222
|
+
socket.close(1e3);
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
const reply = await sayHello(socket, {
|
|
226
|
+
client: {
|
|
227
|
+
name: "stim",
|
|
228
|
+
version: "1"
|
|
229
|
+
},
|
|
230
|
+
access: auth.access
|
|
231
|
+
}, HELLO_TIMEOUT_MS);
|
|
232
|
+
socket.close(1e3);
|
|
233
|
+
if ("error" in reply) return reply;
|
|
234
|
+
const { result } = reply;
|
|
235
|
+
const device = isJsonObject(result.device) ? result.device : null;
|
|
236
|
+
if (!device || typeof device.id !== "string" || !Array.isArray(result.capabilities)) return { failed: "the reply was not a hello result" };
|
|
237
|
+
return {
|
|
238
|
+
result,
|
|
239
|
+
serverKey: socket.remoteKey.toString("base64url")
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const realIo = {
|
|
243
|
+
status: tailscaleStatus,
|
|
244
|
+
hello
|
|
245
|
+
};
|
|
246
|
+
function doctorIo(operation) {
|
|
247
|
+
return {
|
|
248
|
+
status: () => tailscaleStatusAsync(operation?.signal),
|
|
249
|
+
hello: (target, auth) => hello(target, auth, operation)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/** Where to reach a paired machine now, or why Stim cannot; the handshake then checks its pinned key. */
|
|
253
|
+
async function resolveEndpoint(credential, status = () => tailscaleStatusAsync(signal), signal) {
|
|
254
|
+
return machineEndpoint(credential.machine, parseMachine(credential.machine) ? await status() : null);
|
|
255
|
+
}
|
|
256
|
+
/** The remedy for a pending request: the printed setup command, or where the person finds it. */
|
|
257
|
+
function setupRemedy(entry, command) {
|
|
258
|
+
return command ? `A person on ${entry} runs this there to approve this machine: ${command}\nAfter approving, run \`stim doctor\` on this machine to reconnect.` : `A person on ${entry} runs the \`stim-server setup\` command Stim Desktop showed for this machine.`;
|
|
259
|
+
}
|
|
260
|
+
function setupCommand(ticket, expiresAt, flags, version) {
|
|
261
|
+
return `STIM_SETUP_TICKET=${ticket} STIM_SETUP_ORIGIN=doctor npx --yes --package @stim-cli/server@${version} stim-server setup --expires ${expiresAt} ${flags.map((flag) => `--${flag}`).join(" ")}`;
|
|
262
|
+
}
|
|
263
|
+
function newPending(input, ticket, expiresAt, now) {
|
|
264
|
+
return {
|
|
265
|
+
machine: input.machine,
|
|
266
|
+
transport: "tailscale",
|
|
267
|
+
address: input.address,
|
|
268
|
+
state: "pending",
|
|
269
|
+
requestedAt: new Date(now).toISOString(),
|
|
270
|
+
ticket,
|
|
271
|
+
...expiresAt ? { expiresAt } : {}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function approved(input, reply, requestedAt) {
|
|
275
|
+
return {
|
|
276
|
+
state: "approved",
|
|
277
|
+
credential: {
|
|
278
|
+
machine: input.machine,
|
|
279
|
+
transport: "tailscale",
|
|
280
|
+
address: input.address,
|
|
281
|
+
state: "approved",
|
|
282
|
+
requestedAt,
|
|
283
|
+
serverKey: reply.serverKey,
|
|
284
|
+
deviceId: reply.result.device.id
|
|
285
|
+
},
|
|
286
|
+
result: reply.result
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Connects to one remote machine for one access and says what changed. A returning client pins the stored server key.
|
|
291
|
+
* A pending client spends its setup ticket in first contact until the worker's `stim-server setup` approves it; the
|
|
292
|
+
* worker answers `unknown-secret` before that. With `fix`, a machine with no credential, or a revoked or lapsed one,
|
|
293
|
+
* gets a new setup ticket: `STIM_ACCESS_TICKET` when set (Stim Desktop's Add Machine flow), else one this machine
|
|
294
|
+
* generates, with the command to run on the worker. A pending credential is replaced when `STIM_ACCESS_TICKET` names
|
|
295
|
+
* another ticket.
|
|
296
|
+
*/
|
|
297
|
+
async function reachMachine(input) {
|
|
298
|
+
const now = input.now ?? Date.now();
|
|
299
|
+
const { credential, io, access } = input;
|
|
300
|
+
const given = readAccessTicket();
|
|
301
|
+
const ask = () => {
|
|
302
|
+
if (given) return firstContact(newPending(input, given, void 0, now));
|
|
303
|
+
const other = input.other();
|
|
304
|
+
const shared = other?.state === "pending" && other.expiresAt ? other : void 0;
|
|
305
|
+
const ticket = shared ? {
|
|
306
|
+
ticket: shared.ticket,
|
|
307
|
+
expiresAt: shared.expiresAt
|
|
308
|
+
} : input.tickets.get(input.machine) ?? {
|
|
309
|
+
ticket: newSecret(),
|
|
310
|
+
expiresAt: new Date(now + GENERATED_TICKET_MS).toISOString()
|
|
311
|
+
};
|
|
312
|
+
input.tickets.set(input.machine, ticket);
|
|
313
|
+
return firstContact(newPending(input, ticket.ticket, ticket.expiresAt, now));
|
|
314
|
+
};
|
|
315
|
+
const firstContact = async (asked) => {
|
|
316
|
+
let pending = asked;
|
|
317
|
+
if (pending.serverKey) {
|
|
318
|
+
const resumed = await io.hello(input.endpoint, {
|
|
319
|
+
access,
|
|
320
|
+
serverKey: pending.serverKey
|
|
321
|
+
});
|
|
322
|
+
if ("result" in resumed && resumed.result.capabilities.includes(access)) return approved(input, resumed, pending.requestedAt);
|
|
323
|
+
}
|
|
324
|
+
const reply = await io.hello(input.endpoint, {
|
|
325
|
+
access,
|
|
326
|
+
ticket: pending.ticket,
|
|
327
|
+
name: input.deviceName,
|
|
328
|
+
onServerKey: (serverKey) => {
|
|
329
|
+
pending = {
|
|
330
|
+
...pending,
|
|
331
|
+
serverKey
|
|
332
|
+
};
|
|
333
|
+
return input.persist(pending);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
if ("result" in reply && reply.result.capabilities.includes(access)) return approved(input, reply, pending.requestedAt);
|
|
337
|
+
const command = pending.expiresAt ? setupCommand(pending.ticket, pending.expiresAt, input.setupFlags, input.serverVersion) : null;
|
|
338
|
+
if ("error" in reply && reply.error.code === "server-outdated") return {
|
|
339
|
+
state: "outdated",
|
|
340
|
+
credential: pending,
|
|
341
|
+
command
|
|
342
|
+
};
|
|
343
|
+
const sibling = "error" in reply && reply.error.code === "unknown-secret" ? input.other()?.serverKey : void 0;
|
|
344
|
+
if (sibling && sibling !== pending.serverKey) {
|
|
345
|
+
const again = await io.hello(input.endpoint, {
|
|
346
|
+
access,
|
|
347
|
+
serverKey: sibling
|
|
348
|
+
});
|
|
349
|
+
if ("result" in again && again.result.capabilities.includes(access)) return approved(input, again, pending.requestedAt);
|
|
350
|
+
}
|
|
351
|
+
if ("error" in reply && (reply.error.code === "unknown-secret" || reply.error.code === "forbidden")) return {
|
|
352
|
+
state: "pending",
|
|
353
|
+
credential: pending,
|
|
354
|
+
command,
|
|
355
|
+
reason: reply.error.message
|
|
356
|
+
};
|
|
357
|
+
return {
|
|
358
|
+
state: "unreachable",
|
|
359
|
+
credential: pending,
|
|
360
|
+
reason: "failed" in reply ? reply.failed : "error" in reply ? reply.error.message : "it did not grant this access"
|
|
361
|
+
};
|
|
362
|
+
};
|
|
363
|
+
if (!credential) return input.fix ? ask() : { state: "not-asked" };
|
|
364
|
+
if (credential.state === "approved") {
|
|
365
|
+
const reply = await io.hello(input.endpoint, {
|
|
366
|
+
access,
|
|
367
|
+
serverKey: credential.serverKey
|
|
368
|
+
});
|
|
369
|
+
if ("result" in reply && reply.result.capabilities.includes(access)) return approved(input, reply, credential.requestedAt);
|
|
370
|
+
if ("error" in reply && reply.error.code === "server-key-mismatch") return {
|
|
371
|
+
state: "key-changed",
|
|
372
|
+
credential
|
|
373
|
+
};
|
|
374
|
+
if ("error" in reply && reply.error.code === "server-outdated") return {
|
|
375
|
+
state: "outdated",
|
|
376
|
+
credential,
|
|
377
|
+
command: null
|
|
378
|
+
};
|
|
379
|
+
if ("error" in reply && (reply.error.code === "unauthorized" || reply.error.code === "forbidden")) return input.fix ? ask() : {
|
|
380
|
+
state: "revoked",
|
|
381
|
+
credential,
|
|
382
|
+
reason: reply.error.message
|
|
383
|
+
};
|
|
384
|
+
if ("result" in reply) return input.fix ? ask() : {
|
|
385
|
+
state: "revoked",
|
|
386
|
+
credential,
|
|
387
|
+
reason: "it no longer grants this access"
|
|
388
|
+
};
|
|
389
|
+
return {
|
|
390
|
+
state: "unreachable",
|
|
391
|
+
credential,
|
|
392
|
+
reason: "failed" in reply ? reply.failed : reply.error.message
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (input.fix && given && given !== credential.ticket) return ask();
|
|
396
|
+
if (credential.expiresAt && Date.parse(credential.expiresAt) <= now) return input.fix ? ask() : {
|
|
397
|
+
state: "lapsed",
|
|
398
|
+
credential
|
|
399
|
+
};
|
|
400
|
+
return firstContact(credential);
|
|
401
|
+
}
|
|
402
|
+
//#endregion
|
|
403
|
+
//#region src/devices/device-slots.ts
|
|
404
|
+
const DEFAULT_DEVICE_SLOT = "default";
|
|
405
|
+
const DEVICE_SLOT_RULE = "A device slot must be 1-64 letters, digits, underscores or hyphens, starting with a letter or digit.";
|
|
406
|
+
function isDeviceSlot(slot) {
|
|
407
|
+
return /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(slot) && ![
|
|
408
|
+
"constructor",
|
|
409
|
+
"prototype",
|
|
410
|
+
"__proto__"
|
|
411
|
+
].includes(slot);
|
|
412
|
+
}
|
|
413
|
+
function validateDeviceSlot(slot = "default") {
|
|
414
|
+
if (!isDeviceSlot(slot)) {
|
|
415
|
+
const error = /* @__PURE__ */ new Error(DEVICE_SLOT_RULE);
|
|
416
|
+
Object.assign(error, { code: "STIM_BAD_ARG" });
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
419
|
+
return slot;
|
|
420
|
+
}
|
|
421
|
+
function parseDeviceSlotOption(slot) {
|
|
422
|
+
if (!isDeviceSlot(slot)) throw new InvalidArgumentError(DEVICE_SLOT_RULE);
|
|
423
|
+
if (slot === "web") throw new InvalidArgumentError(`web names this workspace's owned Chrome (stim stop --slot web); choose another device slot name.`);
|
|
424
|
+
return slot;
|
|
425
|
+
}
|
|
426
|
+
function deviceSlotPlatforms(project, slot = DEFAULT_DEVICE_SLOT) {
|
|
427
|
+
validateDeviceSlot(slot);
|
|
428
|
+
if (slot === DEFAULT_DEVICE_SLOT) return project?.platforms;
|
|
429
|
+
return project?.deviceSlots && Object.hasOwn(project.deviceSlots, slot) ? project.deviceSlots[slot] : void 0;
|
|
430
|
+
}
|
|
431
|
+
function projectDeviceSlots(project) {
|
|
432
|
+
const slots = [{
|
|
433
|
+
slot: DEFAULT_DEVICE_SLOT,
|
|
434
|
+
platforms: project?.platforms ?? {}
|
|
435
|
+
}];
|
|
436
|
+
for (const [slot, platforms] of Object.entries(project?.deviceSlots ?? {})) {
|
|
437
|
+
validateDeviceSlot(slot);
|
|
438
|
+
if (slot === DEFAULT_DEVICE_SLOT) throw new Error("The default device slot must use the existing platforms record.");
|
|
439
|
+
if (!platforms || typeof platforms !== "object" || Array.isArray(platforms)) throw new Error(`Invalid device slot record: ${slot}`);
|
|
440
|
+
slots.push({
|
|
441
|
+
slot,
|
|
442
|
+
platforms
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
return slots;
|
|
446
|
+
}
|
|
447
|
+
function assignSlotDevice(project, platform, device, slot = DEFAULT_DEVICE_SLOT) {
|
|
448
|
+
validateDeviceSlot(slot);
|
|
449
|
+
if (platform !== "ios" && platform !== "android") throw new Error(`Unknown device platform: ${platform}`);
|
|
450
|
+
if (slot === DEFAULT_DEVICE_SLOT) project.platforms = {
|
|
451
|
+
...project.platforms,
|
|
452
|
+
[platform]: device
|
|
453
|
+
};
|
|
454
|
+
else project.deviceSlots = {
|
|
455
|
+
...project.deviceSlots,
|
|
456
|
+
[slot]: {
|
|
457
|
+
...deviceSlotPlatforms(project, slot),
|
|
458
|
+
[platform]: device
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function removeSlotDevice(project, platform, slot = DEFAULT_DEVICE_SLOT) {
|
|
463
|
+
const platforms = deviceSlotPlatforms(project, slot);
|
|
464
|
+
if (!platforms) return;
|
|
465
|
+
delete platforms[platform];
|
|
466
|
+
if (slot !== DEFAULT_DEVICE_SLOT && Object.keys(platforms).length === 0) {
|
|
467
|
+
delete project.deviceSlots[slot];
|
|
468
|
+
if (Object.keys(project.deviceSlots).length === 0) delete project.deviceSlots;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function deviceSlotKey(platform, slot = "default") {
|
|
472
|
+
validateDeviceSlot(slot);
|
|
473
|
+
if (platform !== "ios" && platform !== "android") throw new Error(`Unknown device platform: ${platform}`);
|
|
474
|
+
return slot === "default" ? platform : `${platform}:${slot}`;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* The slot key in a form that is legal as a file name. NTFS reads a colon in a file name as an
|
|
478
|
+
* alternate data stream: the write and the read of the exact path both succeed while `readdirSync`
|
|
479
|
+
* cannot see the file, so every non-default slot's logs would be invisible to `stim logs`. A slot
|
|
480
|
+
* name can contain neither a colon nor a dot, so the two forms stay one-to-one.
|
|
481
|
+
*/
|
|
482
|
+
function deviceSlotFileKey(platform, slot = "default") {
|
|
483
|
+
return deviceSlotKey(platform, slot).replace(":", ".");
|
|
484
|
+
}
|
|
485
|
+
function parseDeviceSlotKey(key) {
|
|
486
|
+
const [platform, slot = "default", extra] = key.split(":");
|
|
487
|
+
if (platform !== "ios" && platform !== "android" || extra !== void 0) return null;
|
|
488
|
+
try {
|
|
489
|
+
validateDeviceSlot(slot);
|
|
490
|
+
} catch {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
if (key !== deviceSlotKey(platform, slot)) return null;
|
|
494
|
+
return {
|
|
495
|
+
platform,
|
|
496
|
+
slot
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
//#endregion
|
|
500
|
+
//#region src/fs-util.ts
|
|
501
|
+
const WINDOWS = process.platform === "win32";
|
|
502
|
+
const DRIVE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
503
|
+
function volumeRootFor(path) {
|
|
504
|
+
const normalized = normalize(String(path));
|
|
505
|
+
if (WINDOWS) return parse(normalized).root.replace(/^[a-z]:/, (drive) => drive.toUpperCase());
|
|
506
|
+
const m = normalized.match(/^\/volumes\/([^/]+)/i);
|
|
507
|
+
return m ? `/Volumes/${m[1]}` : "/";
|
|
508
|
+
}
|
|
509
|
+
function resolveWorkspaceRealish(workspacePath) {
|
|
510
|
+
const raw = String(workspacePath);
|
|
511
|
+
if (!isAbsolute$1(raw)) return null;
|
|
512
|
+
let current = raw;
|
|
513
|
+
const MAX_HOPS = 40;
|
|
514
|
+
for (let hops = 0; hops < MAX_HOPS; hops++) {
|
|
515
|
+
const normalized = normalize(current);
|
|
516
|
+
const root = parse(normalized).root;
|
|
517
|
+
const segments = normalized.slice(root.length).split(sep$1).filter(Boolean);
|
|
518
|
+
let prefix = root.endsWith(sep$1) ? root.slice(0, -sep$1.length) : root;
|
|
519
|
+
let followedSymlink = false;
|
|
520
|
+
for (const segment of segments) {
|
|
521
|
+
prefix += `${sep$1}${segment}`;
|
|
522
|
+
let st;
|
|
523
|
+
try {
|
|
524
|
+
st = lstatSync(prefix);
|
|
525
|
+
} catch (err) {
|
|
526
|
+
if (err && err.code === "ENOENT") return normalized;
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
if (st.isSymbolicLink()) {
|
|
530
|
+
let target;
|
|
531
|
+
try {
|
|
532
|
+
target = readlinkSync(prefix);
|
|
533
|
+
} catch {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
current = `${isAbsolute$1(target) ? target : normalize(join$1(dirname$1(prefix), target))}${normalized.slice(prefix.length)}`;
|
|
537
|
+
followedSymlink = true;
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (!followedSymlink) return normalized;
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
function isRealMount(entryDev, rootDev) {
|
|
546
|
+
return entryDev != null && rootDev != null && entryDev !== rootDev;
|
|
547
|
+
}
|
|
548
|
+
function listWindowsVolumeRoots(statFn) {
|
|
549
|
+
const roots = [];
|
|
550
|
+
for (const letter of DRIVE_LETTERS) {
|
|
551
|
+
const root = `${letter}:${sep$1}`;
|
|
552
|
+
try {
|
|
553
|
+
statFn(root);
|
|
554
|
+
} catch {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
roots.push(root);
|
|
558
|
+
}
|
|
559
|
+
return roots;
|
|
560
|
+
}
|
|
561
|
+
function listMountedVolumes({ statFn = statSync } = {}) {
|
|
562
|
+
if (WINDOWS) return listWindowsVolumeRoots(statFn);
|
|
563
|
+
const roots = ["/"];
|
|
564
|
+
let rootDev;
|
|
565
|
+
try {
|
|
566
|
+
rootDev = statFn("/").dev;
|
|
567
|
+
} catch {
|
|
568
|
+
return roots;
|
|
569
|
+
}
|
|
570
|
+
try {
|
|
571
|
+
for (const name of readdirSync("/Volumes")) {
|
|
572
|
+
const path = join$1("/Volumes", name);
|
|
573
|
+
let entryDev;
|
|
574
|
+
try {
|
|
575
|
+
entryDev = statFn(path).dev;
|
|
576
|
+
} catch {
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
if (isRealMount(entryDev, rootDev)) roots.push(path);
|
|
580
|
+
}
|
|
581
|
+
} catch {}
|
|
582
|
+
return roots;
|
|
583
|
+
}
|
|
584
|
+
function resolveVolumeRoot(path) {
|
|
585
|
+
const realish = resolveWorkspaceRealish(path);
|
|
586
|
+
return realish === null ? null : volumeRootFor(realish);
|
|
587
|
+
}
|
|
588
|
+
function uncShareIsReachable(volume, statFn) {
|
|
589
|
+
if (!WINDOWS || !volume.startsWith(`${sep$1}${sep$1}`)) return false;
|
|
590
|
+
try {
|
|
591
|
+
return statFn(volume).isDirectory();
|
|
592
|
+
} catch {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function isOnMountedVolume(path, mountedVolumes, { statFn = statSync } = {}) {
|
|
597
|
+
const mounted = new Set(mountedVolumes || listMountedVolumes());
|
|
598
|
+
const volume = resolveVolumeRoot(path);
|
|
599
|
+
if (volume === null) return false;
|
|
600
|
+
return mounted.has(volume) || uncShareIsReachable(volume, statFn);
|
|
601
|
+
}
|
|
602
|
+
function directorySize(dir, { timeoutMs }) {
|
|
603
|
+
return measuredDirectorySize(dir, { timeoutMs }) ?? 0;
|
|
604
|
+
}
|
|
605
|
+
async function directorySizeAsync(dir, options) {
|
|
606
|
+
return await measuredDirectorySizeAsync(dir, options) ?? 0;
|
|
607
|
+
}
|
|
608
|
+
async function measuredDirectorySizeAsync(dir, { timeoutMs, signal, operation }) {
|
|
609
|
+
let out;
|
|
610
|
+
try {
|
|
611
|
+
out = await runOperationFile(operation, getExecutor(), "du", ["-sk", dir], {
|
|
612
|
+
timeoutMs,
|
|
613
|
+
killSignal: "SIGKILL",
|
|
614
|
+
signal,
|
|
615
|
+
cwd: operation?.cwd
|
|
616
|
+
});
|
|
617
|
+
} catch (error) {
|
|
618
|
+
if (error instanceof WorkspaceProcessCleanupError) throw error;
|
|
619
|
+
signal?.throwIfAborted();
|
|
620
|
+
operation?.signal.throwIfAborted();
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
if (!out) return 0;
|
|
624
|
+
const kb = parseInt(out.split(/\s+/)[0] ?? "", 10);
|
|
625
|
+
return isNaN(kb) ? 0 : kb * 1024;
|
|
626
|
+
}
|
|
627
|
+
function measuredDirectorySize(dir, { timeoutMs }) {
|
|
628
|
+
let out;
|
|
629
|
+
try {
|
|
630
|
+
out = getExecutor().runFile("du", ["-sk", dir], { timeoutMs });
|
|
631
|
+
} catch {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
if (!out) return 0;
|
|
635
|
+
const kb = parseInt(out.split(/\s+/)[0] ?? "", 10);
|
|
636
|
+
return isNaN(kb) ? 0 : kb * 1024;
|
|
637
|
+
}
|
|
638
|
+
function formatBytes(bytes) {
|
|
639
|
+
if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)}G`;
|
|
640
|
+
if (bytes >= 1024 ** 2) return `${Math.round(bytes / 1024 ** 2)}M`;
|
|
641
|
+
return `${Math.round(bytes / 1024)}K`;
|
|
642
|
+
}
|
|
643
|
+
//#endregion
|
|
644
|
+
//#region src/devices/avd-claim.ts
|
|
645
|
+
function acquireAvdClaim(avdName) {
|
|
646
|
+
const root = join(configDir(), "avd-locks", `${encodeURIComponent(avdName.toLowerCase())}.lock`);
|
|
647
|
+
const label = `AVD ${avdName}`;
|
|
648
|
+
const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
|
|
649
|
+
root,
|
|
650
|
+
mode: "exclusive",
|
|
651
|
+
label
|
|
652
|
+
});
|
|
653
|
+
if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
|
|
654
|
+
if (attempt.acquired) return attempt.acquired;
|
|
655
|
+
throw new ownership_claim_exports.ClaimRefusedError({
|
|
656
|
+
root,
|
|
657
|
+
claimPath: attempt.held?.path ?? attempt.waitingFor?.[0]?.path ?? root,
|
|
658
|
+
label,
|
|
659
|
+
reason: "another operation is creating, recovering, or tearing down this AVD"
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
//#endregion
|
|
663
|
+
//#region src/workspace/config.ts
|
|
664
|
+
function getConfigDir() {
|
|
665
|
+
return configDir();
|
|
666
|
+
}
|
|
667
|
+
function refuseRelativeStimPaths(env = process.env) {
|
|
668
|
+
for (const name of [
|
|
669
|
+
"STIM_HOME",
|
|
670
|
+
"STIM_BUILD_CACHE",
|
|
671
|
+
"STIM_METRO_CACHE"
|
|
672
|
+
]) {
|
|
673
|
+
const value = env[name];
|
|
674
|
+
if (!value || isAbsolute$1(value)) continue;
|
|
675
|
+
const error = /* @__PURE__ */ new Error(`${name}=${value} is not an absolute path. Set it to an absolute path, or unset it to use the default.`);
|
|
676
|
+
error.code = "STIM_RELATIVE_PATH";
|
|
677
|
+
throw error;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function saveConfig(config) {
|
|
681
|
+
ensurePrivateDir(getConfigDir());
|
|
682
|
+
const target = getConfigPath();
|
|
683
|
+
const tmp = `${target}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
684
|
+
writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
685
|
+
try {
|
|
686
|
+
renameSync(tmp, target);
|
|
687
|
+
} catch (err) {
|
|
688
|
+
try {
|
|
689
|
+
unlinkSync(tmp);
|
|
690
|
+
} catch {}
|
|
691
|
+
throw err;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const CONFIG_VERSION = 2;
|
|
695
|
+
function ensureConfig() {
|
|
696
|
+
return withConfigLock(() => {
|
|
697
|
+
const existing = loadConfig();
|
|
698
|
+
if (existing) {
|
|
699
|
+
if (existing.version !== CONFIG_VERSION) {
|
|
700
|
+
existing.version = CONFIG_VERSION;
|
|
701
|
+
saveConfig(existing);
|
|
702
|
+
}
|
|
703
|
+
return existing;
|
|
704
|
+
}
|
|
705
|
+
const fresh = {
|
|
706
|
+
version: CONFIG_VERSION,
|
|
707
|
+
projects: {},
|
|
708
|
+
repos: {}
|
|
709
|
+
};
|
|
710
|
+
saveConfig(fresh);
|
|
711
|
+
return fresh;
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
function requireAbsoluteProjectPath(projectPath) {
|
|
715
|
+
if (isAbsolute$1(projectPath)) return;
|
|
716
|
+
throw new Error(`Project key "${projectPath}" is not an absolute path. The registry is keyed by the realpath of a workspace root, so a relative key can never resolve.`);
|
|
717
|
+
}
|
|
718
|
+
function upsertProject(projectPath, fields) {
|
|
719
|
+
requireAbsoluteProjectPath(projectPath);
|
|
720
|
+
return withConfigLock(() => {
|
|
721
|
+
const cfg = ensureConfig();
|
|
722
|
+
const existing = cfg.projects[projectPath] || {
|
|
723
|
+
metroPort: null,
|
|
724
|
+
platforms: {}
|
|
725
|
+
};
|
|
726
|
+
cfg.projects[projectPath] = {
|
|
727
|
+
...existing,
|
|
728
|
+
...typeof fields === "function" ? fields(existing) : fields
|
|
729
|
+
};
|
|
730
|
+
saveConfig(cfg);
|
|
731
|
+
return cfg.projects[projectPath];
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
function removeProject(projectPath) {
|
|
735
|
+
withConfigLock(() => {
|
|
736
|
+
const cfg = loadConfig();
|
|
737
|
+
if (!cfg?.projects?.[projectPath]) return;
|
|
738
|
+
if (Object.keys(cfg.projects[projectPath].ports ?? {}).length) throw new Error(`Named ports remain for ${projectPath}; stop or release them before removing its registry entry.`);
|
|
739
|
+
for (const { platforms } of projectDeviceSlots(cfg.projects[projectPath])) if (platforms.android?.owned && platforms.android.avdName) (0, ownership_claim_exports.releaseClaim)(acquireAvdClaim(platforms.android.avdName));
|
|
740
|
+
delete cfg.projects[projectPath];
|
|
741
|
+
saveConfig(cfg);
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
function claimMetroPort(projectPath, port) {
|
|
745
|
+
return withConfigLock(() => {
|
|
746
|
+
const cfg = ensureConfig();
|
|
747
|
+
if (!cfg.projects[projectPath]) throw new Error(`Project not registered: ${projectPath}`);
|
|
748
|
+
for (const [path, proj] of Object.entries(cfg.projects)) {
|
|
749
|
+
if (path !== projectPath && proj?.metroPort === port) return null;
|
|
750
|
+
if (Object.values(proj?.ports ?? {}).includes(port)) return null;
|
|
751
|
+
}
|
|
752
|
+
cfg.projects[projectPath].metroPort = port;
|
|
753
|
+
saveConfig(cfg);
|
|
754
|
+
return port;
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
function releaseMetroPort(projectPath, port) {
|
|
758
|
+
withConfigLock(() => {
|
|
759
|
+
const cfg = loadConfig();
|
|
760
|
+
const project = cfg?.projects?.[projectPath];
|
|
761
|
+
if (!cfg || project?.metroPort !== port) return;
|
|
762
|
+
project.metroPort = null;
|
|
763
|
+
saveConfig(cfg);
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
function setDevice(projectPath, platform, deviceFields, slot = "default") {
|
|
767
|
+
requireAbsoluteProjectPath(projectPath);
|
|
768
|
+
withConfigLock(() => {
|
|
769
|
+
const cfg = ensureConfig();
|
|
770
|
+
if (!cfg.projects[projectPath]) throw new Error(`Project not registered: ${projectPath}`);
|
|
771
|
+
assignSlotDevice(cfg.projects[projectPath], platform, deviceFields, slot);
|
|
772
|
+
saveConfig(cfg);
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
function releaseAndroidConsolePort(projectPath, consolePort, slot = "default") {
|
|
776
|
+
return withConfigLock(() => {
|
|
777
|
+
const cfg = loadConfig();
|
|
778
|
+
const android = deviceSlotPlatforms(cfg?.projects?.[projectPath], slot)?.android;
|
|
779
|
+
if (!cfg || !android || android.consolePort !== consolePort) return false;
|
|
780
|
+
delete android.consolePort;
|
|
781
|
+
saveConfig(cfg);
|
|
782
|
+
return true;
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
function recordIosSchemeApprovals(projectPath, deviceUdid, schemeApprovals, slot = "default") {
|
|
786
|
+
withConfigLock(() => {
|
|
787
|
+
const cfg = loadConfig();
|
|
788
|
+
const ios = deviceSlotPlatforms(cfg?.projects?.[projectPath], slot)?.ios;
|
|
789
|
+
if (!cfg || !ios?.owned || ios.deviceUdid !== deviceUdid) return;
|
|
790
|
+
ios.schemeApprovals = schemeApprovals;
|
|
791
|
+
saveConfig(cfg);
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
function clearAndroidBootPending(projectPath, avdName, slot = "default") {
|
|
795
|
+
withConfigLock(() => {
|
|
796
|
+
const cfg = loadConfig();
|
|
797
|
+
const android = deviceSlotPlatforms(cfg?.projects?.[projectPath], slot)?.android;
|
|
798
|
+
if (!cfg || android?.avdName !== avdName || !android.bootPending) return;
|
|
799
|
+
delete android.bootPending;
|
|
800
|
+
saveConfig(cfg);
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
function clearDevice(projectPath, platform, slot = "default", expectedId) {
|
|
804
|
+
return withConfigLock(() => {
|
|
805
|
+
const cfg = loadConfig();
|
|
806
|
+
const project = cfg?.projects?.[projectPath];
|
|
807
|
+
if (!cfg || !project) return false;
|
|
808
|
+
const current = deviceSlotPlatforms(project, slot)?.[platform];
|
|
809
|
+
if (expectedId !== void 0 && (platform === "ios" ? current?.deviceUdid : current?.avdName) !== expectedId) return false;
|
|
810
|
+
removeSlotDevice(project, platform, slot);
|
|
811
|
+
saveConfig(cfg);
|
|
812
|
+
return true;
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
function setSupervisor(projectPath, { pid, port, startedAt, processToken }) {
|
|
816
|
+
requireAbsoluteProjectPath(projectPath);
|
|
817
|
+
return withConfigLock(() => {
|
|
818
|
+
const cfg = ensureConfig();
|
|
819
|
+
if (!cfg.projects[projectPath]) cfg.projects[projectPath] = {
|
|
820
|
+
metroPort: null,
|
|
821
|
+
platforms: {}
|
|
822
|
+
};
|
|
823
|
+
cfg.projects[projectPath].supervisor = {
|
|
824
|
+
pid,
|
|
825
|
+
port,
|
|
826
|
+
startedAt,
|
|
827
|
+
processToken
|
|
828
|
+
};
|
|
829
|
+
saveConfig(cfg);
|
|
830
|
+
return cfg.projects[projectPath].supervisor;
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
function clearSupervisor(projectPath, expected) {
|
|
834
|
+
return withConfigLock(() => {
|
|
835
|
+
const cfg = loadConfig();
|
|
836
|
+
const current = cfg?.projects?.[projectPath]?.supervisor;
|
|
837
|
+
if (!current) return true;
|
|
838
|
+
if (expected !== void 0 && !(0, process_identity_exports.sameProcessRecord)(current, expected)) return false;
|
|
839
|
+
delete cfg.projects[projectPath].supervisor;
|
|
840
|
+
saveConfig(cfg);
|
|
841
|
+
return true;
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
function targetSettings(cfg, target, create) {
|
|
845
|
+
if (target.scope === "machine") return cfg;
|
|
846
|
+
if (target.scope === "workspace") {
|
|
847
|
+
if (!cfg.projects[target.projectPath]) {
|
|
848
|
+
if (!create) return null;
|
|
849
|
+
cfg.projects[target.projectPath] = {
|
|
850
|
+
metroPort: null,
|
|
851
|
+
platforms: {}
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
const project = cfg.projects[target.projectPath];
|
|
855
|
+
if (!project.settings && create) project.settings = {};
|
|
856
|
+
return project.settings ?? null;
|
|
857
|
+
}
|
|
858
|
+
if (!cfg.repos[target.gitCommonDir]) {
|
|
859
|
+
if (!create) return null;
|
|
860
|
+
cfg.repos[target.gitCommonDir] = {};
|
|
861
|
+
}
|
|
862
|
+
const repo = cfg.repos[target.gitCommonDir];
|
|
863
|
+
if (!repo.settings && create) repo.settings = {};
|
|
864
|
+
return repo.settings ?? null;
|
|
865
|
+
}
|
|
866
|
+
function localDevicePoolOff(dottedKey, value) {
|
|
867
|
+
if (dottedKey === "remote.devicePoolDisabled") return Array.isArray(value) && value.includes("local");
|
|
868
|
+
if (dottedKey !== "remote.machines") return false;
|
|
869
|
+
const disabled = loadConfig()?.remote?.devicePoolDisabled;
|
|
870
|
+
return Array.isArray(disabled) && disabled.includes("local");
|
|
871
|
+
}
|
|
872
|
+
function writeConfigSetting(target, dottedKey, value) {
|
|
873
|
+
if (target.scope === "workspace") requireAbsoluteProjectPath(target.projectPath);
|
|
874
|
+
const status = target.scope === "machine" && localDevicePoolOff(dottedKey, value) ? realIo.status() : null;
|
|
875
|
+
return withConfigLock(() => applyConfigSetting(target, dottedKey, value, status));
|
|
876
|
+
}
|
|
877
|
+
/** Waits for the config lock and device-pool probe asynchronously; cancellation prevents a pending write. */
|
|
878
|
+
async function writeConfigSettingAsync(target, dottedKey, value, signal) {
|
|
879
|
+
if (target.scope === "workspace") requireAbsoluteProjectPath(target.projectPath);
|
|
880
|
+
signal?.throwIfAborted();
|
|
881
|
+
const status = target.scope === "machine" && (dottedKey === "remote.machines" || localDevicePoolOff(dottedKey, value)) ? await tailscaleStatusAsync(signal) : null;
|
|
882
|
+
return withConfigLockAsync(() => applyConfigSetting(target, dottedKey, value, status), signal);
|
|
883
|
+
}
|
|
884
|
+
function applyConfigSetting(target, dottedKey, value, status) {
|
|
885
|
+
const cfg = value === void 0 ? loadConfig() : ensureConfig();
|
|
886
|
+
const settings = cfg ? targetSettings(cfg, target, value !== void 0) : null;
|
|
887
|
+
if (!cfg || !settings) return false;
|
|
888
|
+
if (value === void 0) {
|
|
889
|
+
if (!deleteNested(settings, dottedKey)) return false;
|
|
890
|
+
} else writeNested(settings, dottedKey, value);
|
|
891
|
+
if (target.scope === "machine") {
|
|
892
|
+
if (dottedKey === "remote.buildPoolDisabled" || dottedKey === "remote.machines") validateAutomaticPool("build", cfg);
|
|
893
|
+
if (dottedKey === "remote.devicePoolDisabled" || dottedKey === "remote.machines") validateAutomaticPool("device", cfg, (machine) => !cannotHost(status, machine));
|
|
894
|
+
}
|
|
895
|
+
saveConfig(cfg);
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
function writeNested(obj, dottedKey, value) {
|
|
899
|
+
const keys = dottedKey.split(".");
|
|
900
|
+
let cur = obj;
|
|
901
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
902
|
+
const k = keys[i];
|
|
903
|
+
if (k === void 0) continue;
|
|
904
|
+
const next = cur[k];
|
|
905
|
+
if (typeof next !== "object" || next === null) cur[k] = {};
|
|
906
|
+
cur = cur[k];
|
|
907
|
+
}
|
|
908
|
+
const leaf = keys[keys.length - 1];
|
|
909
|
+
if (leaf !== void 0) cur[leaf] = value;
|
|
910
|
+
}
|
|
911
|
+
function deleteNested(obj, dottedKey) {
|
|
912
|
+
const keys = dottedKey.split(".");
|
|
913
|
+
const chain = [obj];
|
|
914
|
+
let cur = obj;
|
|
915
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
916
|
+
const k = keys[i];
|
|
917
|
+
if (k === void 0) return false;
|
|
918
|
+
const next = cur[k];
|
|
919
|
+
if (next == null || typeof next !== "object") return false;
|
|
920
|
+
cur = next;
|
|
921
|
+
chain.push(cur);
|
|
922
|
+
}
|
|
923
|
+
const leaf = keys[keys.length - 1];
|
|
924
|
+
if (leaf === void 0) return false;
|
|
925
|
+
if (!(leaf in cur)) return false;
|
|
926
|
+
delete cur[leaf];
|
|
927
|
+
for (let i = chain.length - 2; i >= 0; i--) {
|
|
928
|
+
const parent = chain[i];
|
|
929
|
+
const key = keys[i];
|
|
930
|
+
const child = chain[i + 1];
|
|
931
|
+
if (parent === void 0 || key === void 0 || child === void 0) break;
|
|
932
|
+
if (Object.keys(child).length === 0) delete parent[key];
|
|
933
|
+
else break;
|
|
934
|
+
}
|
|
935
|
+
return true;
|
|
936
|
+
}
|
|
937
|
+
function allConsolePortsAndSerials({ isMounted = isOnMountedVolume } = {}) {
|
|
938
|
+
const cfg = loadConfig();
|
|
939
|
+
const result = {
|
|
940
|
+
androidConsolePorts: [],
|
|
941
|
+
androidPhysicalSerials: []
|
|
942
|
+
};
|
|
943
|
+
if (!cfg) return result;
|
|
944
|
+
for (const [path, proj] of Object.entries(cfg.projects || {})) {
|
|
945
|
+
if (!existsSync(path) && isMounted(path)) continue;
|
|
946
|
+
for (const { platforms } of projectDeviceSlots(proj)) {
|
|
947
|
+
const android = platforms.android;
|
|
948
|
+
if (typeof android?.consolePort === "number") result.androidConsolePorts.push(android.consolePort);
|
|
949
|
+
if (android?.serial && !android.avdName) result.androidPhysicalSerials.push(android.serial);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return result;
|
|
953
|
+
}
|
|
954
|
+
//#endregion
|
|
955
|
+
export { outdatedServer as $, writeConfigSettingAsync as A, deviceSlotFileKey as B, saveConfig as C, withConfigLock$1 as D, upsertProject as E, isOnMountedVolume as F, projectDeviceSlots as G, deviceSlotPlatforms as H, listMountedVolumes as I, SERVER_OUTDATED as J, removeSlotDevice as K, measuredDirectorySizeAsync as L, directorySize as M, directorySizeAsync as N, withConfigLockAsync$1 as O, formatBytes as P, openAuthenticated as Q, volumeRootFor as R, removeProject as S, setSupervisor as T, parseDeviceSlotKey as U, deviceSlotKey as V, parseDeviceSlotOption as W, endpoint as X, doctorIo as Y, findPeer$1 as Z, loadConfig$1 as _, clearDevice as a, setupRemedy as at, releaseAndroidConsolePort as b, ensureConfig as c, getConfigDir as d, parseMachine$1 as et, getConfigPath$1 as f, isPathPrefix as g, getRepoSettings as h, clearAndroidBootPending as i, sayHello as it, acquireAvdClaim as j, writeConfigSetting as k, findEnclosingWorktreeRoot as l, getProjectSettings as m, allMetroPorts as n, realIo as nt, clearSupervisor as o, tailscaleStatusAsync as ot, getProject as p, validateDeviceSlot as q, claimMetroPort as r, resolveEndpoint as rt, configCorruptRepair as s, readHostPermissions as st, allConsolePortsAndSerials as t, reachMachine as tt, getConcurrencyLimits as u, recordIosSchemeApprovals as v, setDevice as w, releaseMetroPort as x, refuseRelativeStimPaths as y, assignSlotDevice as z };
|