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,1364 @@
|
|
|
1
|
+
import { a as workspaceProcessLockError, g as process_identity_exports, h as withDrainingCancellation, i as withWorkspaceProcessLock, r as WorkspaceProcessCleanupError, t as getExecutor, x as ownership_claim_exports } from "./exec-DEoAhcze.mjs";
|
|
2
|
+
import { l as workspaceAgentDeviceDir, m as workspaceLogsDir$1, n as ensureWorkspaceStorage, s as supervisorLogFile } from "./paths-DQtTgsBf.mjs";
|
|
3
|
+
import { E as upsertProject, O as withConfigLockAsync$1, p as getProject } from "./config-DlrsduwG.mjs";
|
|
4
|
+
import { _ as stepTimer, l as phaseLine } from "./command-output-gwpm28fu.mjs";
|
|
5
|
+
import { f as throwIfCancelled, u as cancellableSleep } from "./operation-exec-BW0em61G.mjs";
|
|
6
|
+
import { i as recordWorkspaceUse, l as withWorkspaceStateLockAsync, n as clearWorkspaceStateKeys, r as readWorkspaceState$1, s as updateWorkspaceStateAsync } from "./workspace-state-U08dEzhG.mjs";
|
|
7
|
+
import { A as metroIdleStopMinutesSetting, G as resolveCacheProviderConfig, J as resolveProjectSettingsAsync, K as resolveIosProjectDir, M as metroTunnelSettingError, O as metroCommandSetting, P as ngrokUrlSetting, R as publicUrlSetting, U as remoteIosSetting, V as remoteAndroidSetting, W as resolveAndroidLayout, a as SETTING_SHAPE_REMEDY, ct as detectProviders, et as settingShapeErrors, h as deviceIdleShutdownMinutesSetting, it as unknownSettingKeys, j as metroPortSetting, k as metroCommandSettingError, lt as planMetroReach, m as defaultAndroidLayout, p as cacheProviderSettingError, rt as tunnelModeSetting, st as PUBLIC_METRO_ENV } from "./settings-C7O8A6ZI.mjs";
|
|
8
|
+
import { E as MODE_EXPO, M as readMetroTunnel, O as clearManagedMetroTunnel, S as readAppJson, T as MODE_COMMAND, c as projectProblem, i as findProjectRoot$1, t as NO_PROJECT_REFUSAL, v as detectIsExpo, w as MODE_BARE, x as readAppConfigText } from "./project-wM_XFTvE.mjs";
|
|
9
|
+
import { i as levelRank } from "./ndjson-CgE_o3Dl.mjs";
|
|
10
|
+
import { _ as PORT_INSPECTION_REMEDY, a as listeningPidsAsync, c as pidExists, d as resolveProjectMetro, f as signalProcessTree, g as reserveMetroPort, h as isMetroRunning, u as processCwds, v as PortInspectionError } from "./metro-DiXtA3Ty.mjs";
|
|
11
|
+
import { In as runProcessOperation, Kn as readProjectConfig, Pn as exitAfterFlush, Zn as remoteCliInvocation, p as stopOwnedMetroForReset, t as budgetGate } from "./budget-Cvwn54qV.mjs";
|
|
12
|
+
import { _ as stopTunnel, g as startTunnelSequence, h as resolveSupervisorTarget, v as terminateChild, x as withManagedTunnelLock, y as withManagedRemoteWorktreeLock } from "./workspaces-CpANmBlq.mjs";
|
|
13
|
+
import { a as workspaceLinkLine, o as workspaceLinks } from "./stim-desktop-D5OGpYZM.mjs";
|
|
14
|
+
import { t as spawnEntry } from "./spawn-entry-jw19m8Nl.mjs";
|
|
15
|
+
import { n as windowsLauncherArgs } from "./detached-entry-BwNEtIrP.mjs";
|
|
16
|
+
import { appendFileSync, closeSync, mkdirSync, openSync, readFileSync } from "node:fs";
|
|
17
|
+
import { dirname } from "node:path";
|
|
18
|
+
import { ambientContext, ambientEnv } from "@stim-cli/core";
|
|
19
|
+
import { IDLE_STOP_KEY, queryLogs } from "@stim-cli/core/state";
|
|
20
|
+
import { join as join$1 } from "path";
|
|
21
|
+
import chalk from "chalk";
|
|
22
|
+
import { existsSync as existsSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1 } from "fs";
|
|
23
|
+
import { CACHE_PROVIDER_ENV, cacheProviderEnv } from "@stim-cli/cache";
|
|
24
|
+
import { EventEmitter } from "node:events";
|
|
25
|
+
//#region src/workspace/app-id.ts
|
|
26
|
+
const ANDROID_PACKAGE_NAME = /^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/;
|
|
27
|
+
function androidPackageFilter(problems) {
|
|
28
|
+
return (id, source) => {
|
|
29
|
+
if (id === null || ANDROID_PACKAGE_NAME.test(id)) return id;
|
|
30
|
+
problems.push(`Invalid Android package ${JSON.stringify(id)} in ${source}. Expected dot-separated letters, digits and underscores, such as com.example.app.`);
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function configString(config, platform, key) {
|
|
35
|
+
if (!config || typeof config !== "object") return null;
|
|
36
|
+
const expoField = config.expo;
|
|
37
|
+
const section = (expoField && typeof expoField === "object" ? expoField : config)[platform];
|
|
38
|
+
if (!section || typeof section !== "object") return null;
|
|
39
|
+
const value = section[key];
|
|
40
|
+
return typeof value === "string" && value ? value : null;
|
|
41
|
+
}
|
|
42
|
+
function literalString(text, key) {
|
|
43
|
+
return text?.match(new RegExp(`${key}\\s*:\\s*["']([^"']+)["']`))?.[1] ?? null;
|
|
44
|
+
}
|
|
45
|
+
async function idsFromConfig(projectRoot, options) {
|
|
46
|
+
const read = await readProjectConfig(projectRoot, options);
|
|
47
|
+
if (!read.unavailable) return {
|
|
48
|
+
bundleId: configString(read.config, "ios", "bundleIdentifier"),
|
|
49
|
+
androidPackage: configString(read.config, "android", "package")
|
|
50
|
+
};
|
|
51
|
+
const appJson = readAppJson(projectRoot);
|
|
52
|
+
const text = readAppConfigText(projectRoot);
|
|
53
|
+
return {
|
|
54
|
+
bundleId: configString(appJson, "ios", "bundleIdentifier") ?? literalString(text, "bundleIdentifier"),
|
|
55
|
+
androidPackage: configString(appJson, "android", "package") ?? literalString(text, "package")
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function detectAppIds(projectRoot, iosDir = join$1(projectRoot, "ios"), androidModuleDir = defaultAndroidLayout(projectRoot).moduleDir, config = {}) {
|
|
59
|
+
const ids = await idsFromConfig(projectRoot, config);
|
|
60
|
+
const problems = [];
|
|
61
|
+
const accept = androidPackageFilter(problems);
|
|
62
|
+
return {
|
|
63
|
+
bundleId: ids.bundleId ?? detectBundleIdFromPbxproj(iosDir),
|
|
64
|
+
androidPackage: accept(ids.androidPackage, "the Expo config android.package") ?? detectAndroidPackageFromGradle(androidModuleDir, accept),
|
|
65
|
+
androidPackageProblem: problems.length > 0 ? problems.join(" ") : null
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function detectBundleId(projectRoot, iosDir = join$1(projectRoot, "ios")) {
|
|
69
|
+
return (await detectAppIds(projectRoot, iosDir)).bundleId;
|
|
70
|
+
}
|
|
71
|
+
async function detectAndroidPackage(projectRoot, androidModuleDir = defaultAndroidLayout(projectRoot).moduleDir) {
|
|
72
|
+
return (await detectAppIds(projectRoot, void 0, androidModuleDir)).androidPackage;
|
|
73
|
+
}
|
|
74
|
+
function detectBundleIdFromPbxproj(iosDir) {
|
|
75
|
+
if (!existsSync$1(iosDir)) return null;
|
|
76
|
+
let entries;
|
|
77
|
+
try {
|
|
78
|
+
entries = readdirSync$1(iosDir, { withFileTypes: true });
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
if (!entry.isDirectory() || !entry.name.endsWith(".xcodeproj")) continue;
|
|
84
|
+
const pbx = join$1(iosDir, entry.name, "project.pbxproj");
|
|
85
|
+
if (!existsSync$1(pbx)) continue;
|
|
86
|
+
let text;
|
|
87
|
+
try {
|
|
88
|
+
text = readFileSync$1(pbx, "utf-8");
|
|
89
|
+
} catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const concrete = [...text.matchAll(/PRODUCT_BUNDLE_IDENTIFIER\s*=\s*([^;\s"]+)\s*;/g)].map((m) => m[1]).filter((id) => !!id && !id.startsWith("$") && !id.includes("("));
|
|
93
|
+
if (concrete.length === 0) continue;
|
|
94
|
+
const counts = {};
|
|
95
|
+
for (const id of concrete) counts[id] = (counts[id] || 0) + 1;
|
|
96
|
+
let best = null, bestCount = 0, bestLen = Infinity;
|
|
97
|
+
for (const [id, count] of Object.entries(counts)) if (count > bestCount || count === bestCount && id.length < bestLen) {
|
|
98
|
+
best = id;
|
|
99
|
+
bestCount = count;
|
|
100
|
+
bestLen = id.length;
|
|
101
|
+
}
|
|
102
|
+
return best;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
function detectAndroidPackageFromGradle(moduleDir, accept) {
|
|
107
|
+
const gradle = [join$1(moduleDir, "build.gradle"), join$1(moduleDir, "build.gradle.kts")].find((file) => existsSync$1(file));
|
|
108
|
+
if (!gradle) return null;
|
|
109
|
+
let text;
|
|
110
|
+
try {
|
|
111
|
+
text = readFileSync$1(gradle, "utf-8");
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const nsId = text.match(/namespace\s*=?\s*["']([^"']+)["']/)?.[1];
|
|
116
|
+
const namespace = nsId ? accept(nsId, gradle) : null;
|
|
117
|
+
if (namespace) return namespace;
|
|
118
|
+
const appId = text.match(/applicationId\s*=?\s*["']([^"']+)["']/)?.[1];
|
|
119
|
+
return appId ? accept(appId, gradle) : null;
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/commands/start.ts
|
|
123
|
+
const DEFAULT_WAIT_SECONDS = 60;
|
|
124
|
+
const POLL_MS = 500;
|
|
125
|
+
const LOG_TAIL_LINES = 5;
|
|
126
|
+
async function resolveStartupMetro(port, root) {
|
|
127
|
+
const signal = ambientContext()?.signal;
|
|
128
|
+
throwIfCancelled(signal, "Metro startup");
|
|
129
|
+
const result = await resolveProjectMetro(port, root, {
|
|
130
|
+
pidsOf: (candidate) => listeningPidsAsync(candidate, process.platform, signal),
|
|
131
|
+
cwdOf: async (pid) => (await processCwds([pid], signal)).get(pid) ?? null,
|
|
132
|
+
probe: (candidate) => isMetroRunning(candidate, signal)
|
|
133
|
+
});
|
|
134
|
+
throwIfCancelled(signal, "Metro startup");
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
function waitForStartup(ms) {
|
|
138
|
+
return cancellableSleep(ms, {
|
|
139
|
+
signal: ambientContext()?.signal,
|
|
140
|
+
what: "Metro startup"
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function writeNote(line) {
|
|
144
|
+
console.error(line);
|
|
145
|
+
}
|
|
146
|
+
function supervisorEntry() {
|
|
147
|
+
return spawnEntry("supervisor-run");
|
|
148
|
+
}
|
|
149
|
+
function wantsExpoOwnTunnel({ isExpo, remote, mode, publicUrl }) {
|
|
150
|
+
return remote && isExpo && !publicUrl && mode === "expo";
|
|
151
|
+
}
|
|
152
|
+
function parseWait(value) {
|
|
153
|
+
if (value === void 0 || value === null) return { seconds: DEFAULT_WAIT_SECONDS };
|
|
154
|
+
const seconds = Number(value);
|
|
155
|
+
if (!Number.isFinite(seconds) || seconds <= 0) return { error: `Invalid --wait value ${JSON.stringify(value)}. Pass a number of seconds, e.g. --wait 90.` };
|
|
156
|
+
return { seconds };
|
|
157
|
+
}
|
|
158
|
+
function liveSupervisor({ state, project, port, isAlive = pidExists, inspectIdentity = process_identity_exports.inspectProcessIdentity } = {}) {
|
|
159
|
+
const target = resolveSupervisorTarget({
|
|
160
|
+
state: state?.supervisor,
|
|
161
|
+
record: project?.supervisor,
|
|
162
|
+
reservedPort: port,
|
|
163
|
+
isAlive,
|
|
164
|
+
inspectIdentity
|
|
165
|
+
});
|
|
166
|
+
return target.status === "ours" && target.port === port ? {
|
|
167
|
+
pid: target.pid,
|
|
168
|
+
port: target.port,
|
|
169
|
+
mode: target.mode ?? null,
|
|
170
|
+
startedAt: target.startedAt ?? null
|
|
171
|
+
} : null;
|
|
172
|
+
}
|
|
173
|
+
function startFacts({ root, port, supervisor, logsDir, alreadyRunning }) {
|
|
174
|
+
return {
|
|
175
|
+
port,
|
|
176
|
+
supervisorPid: supervisor?.pid ?? null,
|
|
177
|
+
mode: supervisor?.mode ?? null,
|
|
178
|
+
logsDir,
|
|
179
|
+
...root ? { agentDevice: { stateDir: workspaceAgentDeviceDir(root) } } : {},
|
|
180
|
+
alreadyRunning: Boolean(alreadyRunning)
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function startError({ code, message, remedy = null }) {
|
|
184
|
+
return {
|
|
185
|
+
code,
|
|
186
|
+
message,
|
|
187
|
+
remedy: remedy ?? null
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function tailLines(text, n = LOG_TAIL_LINES) {
|
|
191
|
+
return String(text || "").split("\n").filter((l) => l.trim() !== "").slice(-n);
|
|
192
|
+
}
|
|
193
|
+
function readLogTail(file, n = LOG_TAIL_LINES) {
|
|
194
|
+
try {
|
|
195
|
+
return tailLines(readFileSync(file, "utf-8"), n);
|
|
196
|
+
} catch {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function startCommand(program) {
|
|
201
|
+
registerStart(program);
|
|
202
|
+
}
|
|
203
|
+
function supervisorSignalable(child, inspectIdentity = process_identity_exports.inspectProcessIdentity) {
|
|
204
|
+
return !child.recordedIdentity || inspectIdentity(child.recordedIdentity) === "same";
|
|
205
|
+
}
|
|
206
|
+
function recordedSupervisorProcess(recorded) {
|
|
207
|
+
const pid = typeof recorded?.pid === "number" ? recorded.pid : void 0;
|
|
208
|
+
return Object.assign(new EventEmitter(), {
|
|
209
|
+
pid,
|
|
210
|
+
...recorded ? { recordedIdentity: recorded } : {},
|
|
211
|
+
stdout: null,
|
|
212
|
+
stderr: null,
|
|
213
|
+
kill: (signal = "SIGTERM") => pid !== void 0 && signalProcessTree(pid, typeof signal === "number" ? "SIGTERM" : signal),
|
|
214
|
+
unref() {}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const RECORDED_SUPERVISOR_WAIT_MS = 15e3;
|
|
218
|
+
function providersOnPath(mode) {
|
|
219
|
+
return detectProviders((bin) => {
|
|
220
|
+
try {
|
|
221
|
+
return Boolean(getExecutor().findExecutable(bin));
|
|
222
|
+
} catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}, mode);
|
|
226
|
+
}
|
|
227
|
+
const DEFAULT_START_DEPS = {
|
|
228
|
+
providers: providersOnPath,
|
|
229
|
+
startTunnelSequence,
|
|
230
|
+
isTunnelAlive: (pid, processToken) => processToken === void 0 ? pidExists(pid) : (0, process_identity_exports.inspectProcessIdentity)({
|
|
231
|
+
pid,
|
|
232
|
+
processToken
|
|
233
|
+
}) === "same",
|
|
234
|
+
writeTunnelRecord: (root, patch) => updateWorkspaceStateAsync(root, (state) => ({
|
|
235
|
+
...state,
|
|
236
|
+
...patch
|
|
237
|
+
})),
|
|
238
|
+
stopTunnel,
|
|
239
|
+
writeSupervisorRecord: (root, patch) => updateWorkspaceStateAsync(root, (state) => ({
|
|
240
|
+
...state,
|
|
241
|
+
...patch
|
|
242
|
+
})),
|
|
243
|
+
terminateSupervisorChild: async (child) => {
|
|
244
|
+
if (child.pid && process.platform !== "win32") try {
|
|
245
|
+
signalProcessTree(child.pid, "SIGTERM", { group: true });
|
|
246
|
+
} catch {}
|
|
247
|
+
return terminateChild(child, {
|
|
248
|
+
alreadyExited: false,
|
|
249
|
+
timeoutMs: 1e3,
|
|
250
|
+
isAlive: pidExists,
|
|
251
|
+
waitForGroup: process.platform !== "win32"
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
withWorktreeLock: withManagedRemoteWorktreeLock,
|
|
255
|
+
withTunnelLock: withManagedTunnelLock,
|
|
256
|
+
clearTunnelRecord: (root, record) => withWorkspaceStateLockAsync(root, () => clearManagedMetroTunnel(root, record)),
|
|
257
|
+
budgetGate,
|
|
258
|
+
platform: process.platform
|
|
259
|
+
};
|
|
260
|
+
function normalizeManagedTunnelUrl(url) {
|
|
261
|
+
return url.trim().replace(/\/+$/, "");
|
|
262
|
+
}
|
|
263
|
+
function registerStart(program, overrides = {}) {
|
|
264
|
+
const d = {
|
|
265
|
+
...DEFAULT_START_DEPS,
|
|
266
|
+
...overrides
|
|
267
|
+
};
|
|
268
|
+
program.command("start").description("Start this workspace's dev server under a detached supervisor and wait until it verifies as this project's. Idempotent: a healthy dev server on the reserved port is a no-op. Structured logs land in the global workspace logs directory.").option("--json", "Emit the facts as a single JSON line on stdout; every other line goes to stderr").option("--wait <seconds>", `How long to wait for the dev server to answer (default ${DEFAULT_WAIT_SECONDS})`).option("--remote", "Prepare the dev server for a remote device").option("--reset-cache", "Restart owned Metro and clear this app's Metro caches").action(async (opts) => {
|
|
269
|
+
const json = Boolean(opts.json);
|
|
270
|
+
const out = (line) => {
|
|
271
|
+
if (json) console.error(line);
|
|
272
|
+
else console.log(line);
|
|
273
|
+
};
|
|
274
|
+
const note = writeNote;
|
|
275
|
+
const fail = ({ code, message, remedy = null, lines = [], reclaimed = [], exitCode = 1 }) => {
|
|
276
|
+
note(chalk.red(message));
|
|
277
|
+
for (const line of lines) note(chalk.dim(` ${line}`));
|
|
278
|
+
if (remedy) note(chalk.dim(remedy));
|
|
279
|
+
note(chalk.red(`failed: ${code}`));
|
|
280
|
+
if (json) console.log(JSON.stringify({
|
|
281
|
+
...startError({
|
|
282
|
+
code,
|
|
283
|
+
message,
|
|
284
|
+
remedy
|
|
285
|
+
}),
|
|
286
|
+
...reclaimed.length ? { reclaimed } : {}
|
|
287
|
+
}));
|
|
288
|
+
process.exit(exitCode);
|
|
289
|
+
};
|
|
290
|
+
const wait = parseWait(opts.wait);
|
|
291
|
+
if (wait.error) return fail({
|
|
292
|
+
code: "STIM_BAD_ARG",
|
|
293
|
+
message: wait.error,
|
|
294
|
+
remedy: "Pass a whole number of seconds, e.g. --wait 90."
|
|
295
|
+
});
|
|
296
|
+
const waitSeconds = wait.seconds;
|
|
297
|
+
const root = findProjectRoot$1(process.cwd());
|
|
298
|
+
if (!root) return fail(NO_PROJECT_REFUSAL);
|
|
299
|
+
const problem = projectProblem(root, "dev-server");
|
|
300
|
+
if (problem) return fail({
|
|
301
|
+
code: "STIM_NO_PROJECT",
|
|
302
|
+
message: problem.message,
|
|
303
|
+
remedy: problem.remedy
|
|
304
|
+
});
|
|
305
|
+
try {
|
|
306
|
+
ensureWorkspaceStorage(root);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
return fail({
|
|
309
|
+
code: error?.code || "STIM_WORKSPACE_STATE",
|
|
310
|
+
message: `Could not prepare this workspace's Stim state: ${error?.message || error}`,
|
|
311
|
+
remedy: "Check that STIM_HOME is writable and has free space. An EPERM on a directory you can write is a sandbox: allow writes to STIM_HOME, or run Stim with the sandbox disabled (`stim guide errors sandbox`)."
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
recordWorkspaceUse(root);
|
|
315
|
+
const { context: settingsContext, settings } = await resolveProjectSettingsAsync(root);
|
|
316
|
+
const [shapeError, ...moreShapeErrors] = settingShapeErrors(settings);
|
|
317
|
+
if (shapeError) return fail({
|
|
318
|
+
code: "STIM_BAD_ARG",
|
|
319
|
+
message: shapeError,
|
|
320
|
+
lines: moreShapeErrors,
|
|
321
|
+
remedy: SETTING_SHAPE_REMEDY
|
|
322
|
+
});
|
|
323
|
+
for (const key of unknownSettingKeys(settings)) note(chalk.yellow(`Warning: setting "${key}" is not read by Stim and will be ignored.`));
|
|
324
|
+
const cacheProviderError = cacheProviderSettingError(settings);
|
|
325
|
+
if (cacheProviderError) note(chalk.yellow(`Warning: ${cacheProviderError} No cache provider is used.`));
|
|
326
|
+
const { value: result, interrupted } = await runProcessOperation(root, (context) => withDrainingCancellation(context, context.signal, async () => {
|
|
327
|
+
try {
|
|
328
|
+
return await startDevServer({
|
|
329
|
+
root,
|
|
330
|
+
settings,
|
|
331
|
+
settingsContext,
|
|
332
|
+
waitSeconds,
|
|
333
|
+
remote: Boolean(opts.remote),
|
|
334
|
+
resetCache: Boolean(opts.resetCache),
|
|
335
|
+
out,
|
|
336
|
+
note
|
|
337
|
+
}, d);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "STIM_CANCELLED") throw error;
|
|
340
|
+
return {
|
|
341
|
+
ok: false,
|
|
342
|
+
error: startError({
|
|
343
|
+
code: "STIM_CANCELLED",
|
|
344
|
+
message: error.message
|
|
345
|
+
}),
|
|
346
|
+
lines: [],
|
|
347
|
+
reclaimed: []
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}));
|
|
351
|
+
if (!result.ok) return fail({
|
|
352
|
+
...result.error,
|
|
353
|
+
lines: result.lines,
|
|
354
|
+
reclaimed: result.reclaimed,
|
|
355
|
+
exitCode: interrupted ?? 1
|
|
356
|
+
});
|
|
357
|
+
report({
|
|
358
|
+
json,
|
|
359
|
+
out,
|
|
360
|
+
facts: result.facts,
|
|
361
|
+
waited: result.waited,
|
|
362
|
+
reclaimed: result.reclaimed,
|
|
363
|
+
links: await workspaceLinks(root)
|
|
364
|
+
});
|
|
365
|
+
if (interrupted !== null) exitAfterFlush(interrupted);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
var StartRefusal = class extends Error {
|
|
369
|
+
refusal;
|
|
370
|
+
constructor(refusal) {
|
|
371
|
+
super(refusal.message);
|
|
372
|
+
this.refusal = refusal;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
async function startDevServer({ root, settings, settingsContext, waitSeconds = DEFAULT_WAIT_SECONDS, remote: remoteFlag = false, resetCache = false, out, note }, overrides = {}) {
|
|
376
|
+
const d = {
|
|
377
|
+
...DEFAULT_START_DEPS,
|
|
378
|
+
...overrides
|
|
379
|
+
};
|
|
380
|
+
const operationSignal = ambientContext()?.signal;
|
|
381
|
+
throwIfCancelled(operationSignal, "Metro startup");
|
|
382
|
+
const waitTimer = stepTimer();
|
|
383
|
+
let reclaimed = [];
|
|
384
|
+
const fail = (refusal) => {
|
|
385
|
+
throw new StartRefusal(refusal);
|
|
386
|
+
};
|
|
387
|
+
const isExpo = detectIsExpo(root);
|
|
388
|
+
const metroCommand = metroCommandSetting(settings);
|
|
389
|
+
const serverMode = metroCommand ? MODE_COMMAND : isExpo ? MODE_EXPO : MODE_BARE;
|
|
390
|
+
const expoServer = serverMode === MODE_EXPO;
|
|
391
|
+
const worktreeRoot = settingsContext.repoRoot ?? root;
|
|
392
|
+
const cacheProvider = resolveCacheProviderConfig(settingsContext);
|
|
393
|
+
const run = async () => {
|
|
394
|
+
const settingError = metroTunnelSettingError(settings);
|
|
395
|
+
if (settingError) return fail({
|
|
396
|
+
code: "STIM_BAD_ARG",
|
|
397
|
+
message: settingError,
|
|
398
|
+
remedy: "Set metro.tunnel to \"ngrok\" and metro.ngrokUrl to an HTTPS URL, or remove metro.ngrokUrl."
|
|
399
|
+
});
|
|
400
|
+
if (metroCommand && d.platform === "win32") return fail({
|
|
401
|
+
code: "STIM_BAD_ARG",
|
|
402
|
+
message: "metro.command is not supported on Windows: Stim cannot read a process working directory there, so it cannot prove the Metro a wrapper starts belongs to this app.",
|
|
403
|
+
remedy: "Remove metro.command on Windows, or run this app from macOS or Linux."
|
|
404
|
+
});
|
|
405
|
+
if (metroCommand && resetCache) return fail({
|
|
406
|
+
code: "STIM_BAD_ARG",
|
|
407
|
+
message: "--reset-cache cannot reach a dev server started by metro.command; Stim does not know its reset flag.",
|
|
408
|
+
remedy: "Add the command's own cache-reset flag to metro.command (for React Native, --reset-cache), then run stim stop and stim start."
|
|
409
|
+
});
|
|
410
|
+
const commandError = metroCommandSettingError(settings);
|
|
411
|
+
if (commandError) return fail({
|
|
412
|
+
code: "STIM_BAD_ARG",
|
|
413
|
+
message: commandError,
|
|
414
|
+
remedy: "Set metro.command to the argv that starts the dev server, e.g. [\"npx\", \"react-native\", \"start\", \"--port\", \"{port}\"], or remove it."
|
|
415
|
+
});
|
|
416
|
+
const remote = remoteFlag || remoteIosSetting(settings)?.kind === "backend" || remoteAndroidSetting(settings)?.kind === "backend";
|
|
417
|
+
const tunnelMode = tunnelModeSetting(settings) ?? "auto";
|
|
418
|
+
const publicUrl = publicUrlSetting(settings);
|
|
419
|
+
const tunnel = wantsExpoOwnTunnel({
|
|
420
|
+
isExpo: expoServer,
|
|
421
|
+
remote,
|
|
422
|
+
mode: tunnelMode,
|
|
423
|
+
publicUrl
|
|
424
|
+
});
|
|
425
|
+
if (tunnel) note(chalk.dim("note requesting an Expo tunnel for this workspace's dev server"));
|
|
426
|
+
const managedRemote = remote && !tunnel && !publicUrl && tunnelMode !== "off";
|
|
427
|
+
const runStart = async (claim, tunnelClaim) => {
|
|
428
|
+
const claims = [claim, tunnelClaim];
|
|
429
|
+
const appIds = await detectAppIds(root, resolveIosProjectDir(settings, root).dir, resolveAndroidLayout(settings, root, settingsContext.repoRoot ?? root).moduleDir, {
|
|
430
|
+
context: ambientContext(),
|
|
431
|
+
run: async (file, args, options) => {
|
|
432
|
+
for (const held of claims) (0, ownership_claim_exports.markClaimChildPending)(held);
|
|
433
|
+
let child;
|
|
434
|
+
let identity;
|
|
435
|
+
let output = "";
|
|
436
|
+
let failure;
|
|
437
|
+
try {
|
|
438
|
+
const invocation = remoteCliInvocation(file, args, "expo");
|
|
439
|
+
output = await getExecutor().runFileAsync(invocation.file, invocation.args, {
|
|
440
|
+
...options,
|
|
441
|
+
cwd: root,
|
|
442
|
+
containProcessTree: true,
|
|
443
|
+
signal: operationSignal ?? new AbortController().signal,
|
|
444
|
+
onSpawn: (spawned) => {
|
|
445
|
+
child = spawned;
|
|
446
|
+
if (!spawned.pid) return;
|
|
447
|
+
const processToken = (0, process_identity_exports.captureProcessToken)(spawned.pid);
|
|
448
|
+
if (!processToken) throw new Error("Cannot record the Expo config process identity.");
|
|
449
|
+
identity = {
|
|
450
|
+
pid: spawned.pid,
|
|
451
|
+
processToken
|
|
452
|
+
};
|
|
453
|
+
for (const held of claims) (0, ownership_claim_exports.setClaimChild)(held, identity);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
} catch (error) {
|
|
457
|
+
failure = { error };
|
|
458
|
+
}
|
|
459
|
+
const pid = child?.pid;
|
|
460
|
+
if (pid) {
|
|
461
|
+
const alive = () => d.platform === "win32" ? !identity || (0, process_identity_exports.inspectProcessIdentity)(identity) !== "gone" : (0, ownership_claim_exports.processGroupAlive)(pid);
|
|
462
|
+
const deadline = Date.now() + 1e3;
|
|
463
|
+
while (alive() && Date.now() < deadline) await cancellableSleep(25);
|
|
464
|
+
if (alive()) throw new WorkspaceProcessCleanupError(claims, `Could not confirm cleanup of Expo config process ${pid}.`, "Inspect and stop the Expo config process and its helpers before retrying.");
|
|
465
|
+
}
|
|
466
|
+
for (const held of claims) (0, ownership_claim_exports.clearClaimChild)(held);
|
|
467
|
+
if (failure) throw failure.error;
|
|
468
|
+
return output;
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
await withConfigLockAsync$1(() => {
|
|
472
|
+
upsertProject(root, {
|
|
473
|
+
bundleId: appIds.bundleId ?? void 0,
|
|
474
|
+
androidPackage: appIds.androidPackage ?? void 0,
|
|
475
|
+
isExpo
|
|
476
|
+
});
|
|
477
|
+
}, operationSignal);
|
|
478
|
+
const gateBudget = async () => {
|
|
479
|
+
const budget = await d.budgetGate({
|
|
480
|
+
root,
|
|
481
|
+
note,
|
|
482
|
+
env: ambientEnv(),
|
|
483
|
+
signal: operationSignal
|
|
484
|
+
});
|
|
485
|
+
reclaimed = budget.reclaimed;
|
|
486
|
+
if (budget.refusal) return fail(budget.refusal);
|
|
487
|
+
};
|
|
488
|
+
if (resetCache) {
|
|
489
|
+
await gateBudget();
|
|
490
|
+
const pin = metroPortSetting(settings, ambientEnv());
|
|
491
|
+
if (pin.port !== null || pin.error) {
|
|
492
|
+
const pinCheck = await resolveWorkspaceMetroPort(root, settings, note, "start", false);
|
|
493
|
+
if (typeof pinCheck !== "number") return fail(pinCheck);
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
await stopOwnedMetroForReset(root, operationSignal);
|
|
497
|
+
} catch (error) {
|
|
498
|
+
const failure = error;
|
|
499
|
+
return fail({
|
|
500
|
+
code: failure.code ?? "STIM_WORKSPACE_STATE",
|
|
501
|
+
message: failure.message,
|
|
502
|
+
remedy: failure.remedy
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
note(phaseLine("cache", "clearing this app's Metro transform and file-map caches; devices preserved"));
|
|
506
|
+
}
|
|
507
|
+
const logsDir = workspaceLogsDir$1(root);
|
|
508
|
+
const logFile = supervisorLogFile(root);
|
|
509
|
+
const port = await resolvePort(root, settings, note, fail);
|
|
510
|
+
let publicOrigin = remote ? publicUrl : null;
|
|
511
|
+
let resolution = await resolveStartupMetro(port, root);
|
|
512
|
+
let supervisor = liveSupervisor({
|
|
513
|
+
state: readWorkspaceState$1(root),
|
|
514
|
+
project: getProject(root),
|
|
515
|
+
port
|
|
516
|
+
});
|
|
517
|
+
const recordedTarget = resolveSupervisorTarget({
|
|
518
|
+
state: readWorkspaceState$1(root)?.supervisor,
|
|
519
|
+
record: getProject(root)?.supervisor,
|
|
520
|
+
reservedPort: port
|
|
521
|
+
});
|
|
522
|
+
if (recordedTarget.status === "unverified") return fail({
|
|
523
|
+
code: "STIM_SUPERVISOR_EXITED",
|
|
524
|
+
message: `Cannot reuse or replace the recorded supervisor: ${recordedTarget.reason}.`,
|
|
525
|
+
remedy: "Stop it with the tool that started it, then retry `stim start`. Stim leaves unverified processes alone."
|
|
526
|
+
});
|
|
527
|
+
if (!resetCache && !resolution.metro && !supervisor) await gateBudget();
|
|
528
|
+
let managedTunnel = null;
|
|
529
|
+
const spawned = { child: null };
|
|
530
|
+
let spawnedTs = null;
|
|
531
|
+
let childExit = null;
|
|
532
|
+
let spawnedIdentity = null;
|
|
533
|
+
let unknownWindowsSpawn = false;
|
|
534
|
+
let windowsLauncher = null;
|
|
535
|
+
let tunnelPending = null;
|
|
536
|
+
const handedOffSupervisor = () => {
|
|
537
|
+
if (!spawnedIdentity) return null;
|
|
538
|
+
const target = resolveSupervisorTarget({
|
|
539
|
+
state: readWorkspaceState$1(root)?.supervisor,
|
|
540
|
+
record: getProject(root)?.supervisor,
|
|
541
|
+
reservedPort: port
|
|
542
|
+
});
|
|
543
|
+
return target.status === "ours" && typeof target.pid === "number" && target.pid === spawnedIdentity.pid && target.processToken === spawnedIdentity.processToken && target.port === port ? {
|
|
544
|
+
pid: target.pid,
|
|
545
|
+
port,
|
|
546
|
+
mode: target.mode ?? null,
|
|
547
|
+
startedAt: target.startedAt ?? null
|
|
548
|
+
} : null;
|
|
549
|
+
};
|
|
550
|
+
const spawnDirect = (supervisorArgs, childEnv) => {
|
|
551
|
+
const fd = openSync(logFile, "a");
|
|
552
|
+
let child;
|
|
553
|
+
try {
|
|
554
|
+
child = getExecutor().spawn(process.execPath, supervisorArgs, {
|
|
555
|
+
cwd: root,
|
|
556
|
+
detached: true,
|
|
557
|
+
stdio: [
|
|
558
|
+
"ignore",
|
|
559
|
+
fd,
|
|
560
|
+
fd
|
|
561
|
+
],
|
|
562
|
+
env: childEnv
|
|
563
|
+
});
|
|
564
|
+
} finally {
|
|
565
|
+
closeSync(fd);
|
|
566
|
+
}
|
|
567
|
+
child.unref?.();
|
|
568
|
+
child.on?.("exit", (code, signal) => {
|
|
569
|
+
childExit = {
|
|
570
|
+
code,
|
|
571
|
+
signal
|
|
572
|
+
};
|
|
573
|
+
});
|
|
574
|
+
child.on?.("error", (err) => {
|
|
575
|
+
childExit = {
|
|
576
|
+
code: null,
|
|
577
|
+
signal: null,
|
|
578
|
+
error: err
|
|
579
|
+
};
|
|
580
|
+
});
|
|
581
|
+
return child;
|
|
582
|
+
};
|
|
583
|
+
const recordedSupervisor = async (since, waitMs = RECORDED_SUPERVISOR_WAIT_MS) => {
|
|
584
|
+
const deadline = Date.now() + waitMs;
|
|
585
|
+
while (Date.now() < deadline) {
|
|
586
|
+
const record = readWorkspaceState$1(root)?.supervisor;
|
|
587
|
+
if (record?.pid && record.port === port && Date.parse(String(record.startedAt)) >= since && pidExists(record.pid)) return {
|
|
588
|
+
pid: record.pid,
|
|
589
|
+
processToken: record.processToken
|
|
590
|
+
};
|
|
591
|
+
await cancellableSleep(25);
|
|
592
|
+
}
|
|
593
|
+
return null;
|
|
594
|
+
};
|
|
595
|
+
const spawnThroughWindowsShell = async (supervisorArgs, childEnv) => {
|
|
596
|
+
const [entry, ...args] = supervisorArgs;
|
|
597
|
+
const launcher = windowsLauncherArgs({
|
|
598
|
+
entry,
|
|
599
|
+
args,
|
|
600
|
+
cwd: root,
|
|
601
|
+
logFile
|
|
602
|
+
});
|
|
603
|
+
const since = Date.now();
|
|
604
|
+
const shell = getExecutor().spawn(launcher.file, launcher.args, {
|
|
605
|
+
cwd: root,
|
|
606
|
+
stdio: [
|
|
607
|
+
"ignore",
|
|
608
|
+
"ignore",
|
|
609
|
+
"pipe"
|
|
610
|
+
],
|
|
611
|
+
env: {
|
|
612
|
+
...childEnv,
|
|
613
|
+
...launcher.env
|
|
614
|
+
},
|
|
615
|
+
windowsHide: true
|
|
616
|
+
});
|
|
617
|
+
const launch = {
|
|
618
|
+
child: shell,
|
|
619
|
+
since,
|
|
620
|
+
closed: false
|
|
621
|
+
};
|
|
622
|
+
windowsLauncher = launch;
|
|
623
|
+
unknownWindowsSpawn = shell.pid !== void 0;
|
|
624
|
+
const stderr = [];
|
|
625
|
+
shell.stderr?.on("data", (chunk) => stderr.push(String(chunk)));
|
|
626
|
+
const closed = new Promise((resolve) => {
|
|
627
|
+
shell.once("close", (code, signal) => {
|
|
628
|
+
launch.closed = true;
|
|
629
|
+
resolve({
|
|
630
|
+
code,
|
|
631
|
+
signal
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
shell.once("error", (error) => resolve({
|
|
635
|
+
code: null,
|
|
636
|
+
signal: null,
|
|
637
|
+
error
|
|
638
|
+
}));
|
|
639
|
+
});
|
|
640
|
+
const waiting = new AbortController();
|
|
641
|
+
const signal = operationSignal ? AbortSignal.any([operationSignal, waiting.signal]) : waiting.signal;
|
|
642
|
+
let exit;
|
|
643
|
+
try {
|
|
644
|
+
exit = await Promise.race([closed, cancellableSleep(Math.min(RECORDED_SUPERVISOR_WAIT_MS, waitSeconds * 1e3), {
|
|
645
|
+
signal,
|
|
646
|
+
what: "Windows supervisor launcher"
|
|
647
|
+
}).then(() => fail({
|
|
648
|
+
code: "STIM_METRO_TIMEOUT",
|
|
649
|
+
message: "The Windows supervisor launcher did not finish before the startup deadline.",
|
|
650
|
+
remedy: "Inspect the launcher and supervisor before retrying."
|
|
651
|
+
}))]);
|
|
652
|
+
} finally {
|
|
653
|
+
waiting.abort();
|
|
654
|
+
}
|
|
655
|
+
const launcherFailed = exit.code !== 0 || exit.error;
|
|
656
|
+
unknownWindowsSpawn = !launcherFailed;
|
|
657
|
+
const recorded = launcherFailed ? null : await recordedSupervisor(since);
|
|
658
|
+
if (recorded) unknownWindowsSpawn = false;
|
|
659
|
+
if (recorded === null) {
|
|
660
|
+
const reason = launcherFailed ? `the supervisor launcher exited (${exit.error ? exit.error.message : `code ${exit.code}`})` : `the supervisor did not record itself within ${RECORDED_SUPERVISOR_WAIT_MS / 1e3}s`;
|
|
661
|
+
appendFileSync(logFile, `Stim start: ${reason}.\n${stderr.join("")}`);
|
|
662
|
+
childExit = {
|
|
663
|
+
code: exit.code,
|
|
664
|
+
signal: exit.signal,
|
|
665
|
+
...exit.error ? { error: exit.error } : {}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
windowsLauncher = null;
|
|
669
|
+
return recordedSupervisorProcess(recorded);
|
|
670
|
+
};
|
|
671
|
+
const spawnSupervisor = async (origin) => {
|
|
672
|
+
mkdirSync(logsDir, { recursive: true });
|
|
673
|
+
spawnedTs = Date.now();
|
|
674
|
+
const supervisorArgs = [
|
|
675
|
+
supervisorEntry(),
|
|
676
|
+
"--root",
|
|
677
|
+
root,
|
|
678
|
+
"--port",
|
|
679
|
+
String(port),
|
|
680
|
+
...tunnel ? ["--tunnel"] : [],
|
|
681
|
+
...resetCache ? ["--reset-cache"] : [],
|
|
682
|
+
"--idle-stop-minutes",
|
|
683
|
+
String(metroIdleStopMinutesSetting(settings)),
|
|
684
|
+
"--device-idle-minutes",
|
|
685
|
+
String(deviceIdleShutdownMinutesSetting(settings))
|
|
686
|
+
];
|
|
687
|
+
const childEnv = {
|
|
688
|
+
...ambientEnv(),
|
|
689
|
+
...origin ? {
|
|
690
|
+
[PUBLIC_METRO_ENV]: origin,
|
|
691
|
+
EXPO_PACKAGER_PROXY_URL: origin
|
|
692
|
+
} : {}
|
|
693
|
+
};
|
|
694
|
+
childEnv[CACHE_PROVIDER_ENV] = cacheProviderEnv(cacheProvider);
|
|
695
|
+
for (const held of claims) (0, ownership_claim_exports.markClaimChildPending)(held);
|
|
696
|
+
const child = d.platform === "win32" ? await spawnThroughWindowsShell(supervisorArgs, childEnv) : spawnDirect(supervisorArgs, childEnv);
|
|
697
|
+
spawned.child = child;
|
|
698
|
+
if (child.pid) {
|
|
699
|
+
const processToken = child.recordedIdentity?.processToken ?? (0, process_identity_exports.captureProcessToken)(child.pid);
|
|
700
|
+
if (processToken) {
|
|
701
|
+
spawnedIdentity = {
|
|
702
|
+
pid: child.pid,
|
|
703
|
+
processToken
|
|
704
|
+
};
|
|
705
|
+
for (const held of claims) (0, ownership_claim_exports.setClaimChild)(held, spawnedIdentity);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
out(chalk.dim(phaseLine("metro", `starting on port ${port} (${serverMode}, supervisor pid ${child.pid})`)));
|
|
709
|
+
return child;
|
|
710
|
+
};
|
|
711
|
+
const waitForSupervisorHandoff = async (child) => {
|
|
712
|
+
const deadline = Date.now() + 5e3;
|
|
713
|
+
while (Date.now() < deadline) {
|
|
714
|
+
const found = handedOffSupervisor();
|
|
715
|
+
if (found) return found;
|
|
716
|
+
if (childExit !== null || (child.pid ? !pidExists(child.pid) : true)) return null;
|
|
717
|
+
await cancellableSleep(25);
|
|
718
|
+
}
|
|
719
|
+
return null;
|
|
720
|
+
};
|
|
721
|
+
const reconcileSpawnedSupervisor = async () => {
|
|
722
|
+
try {
|
|
723
|
+
const launch = windowsLauncher;
|
|
724
|
+
if (launch) {
|
|
725
|
+
if (!await terminateChild(launch.child, {
|
|
726
|
+
alreadyExited: launch.closed || launch.child.exitCode !== null || launch.child.signalCode !== null,
|
|
727
|
+
timeoutMs: 1e3,
|
|
728
|
+
isAlive: pidExists
|
|
729
|
+
})) throw new WorkspaceProcessCleanupError(claims, `Could not confirm cleanup of Windows supervisor launcher pid ${launch.child.pid}.`, "Inspect and stop the launcher and its supervisor with their original tooling before retrying.");
|
|
730
|
+
if (unknownWindowsSpawn) {
|
|
731
|
+
const recorded = await recordedSupervisor(launch.since, 1e3);
|
|
732
|
+
if (recorded) {
|
|
733
|
+
spawned.child = recordedSupervisorProcess(recorded);
|
|
734
|
+
spawnedIdentity = recorded;
|
|
735
|
+
for (const held of claims) (0, ownership_claim_exports.setClaimChild)(held, recorded);
|
|
736
|
+
unknownWindowsSpawn = false;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
windowsLauncher = null;
|
|
740
|
+
}
|
|
741
|
+
const child = spawned.child;
|
|
742
|
+
if (tunnelPending) throw tunnelPending;
|
|
743
|
+
if (child?.pid && !handedOffSupervisor()) {
|
|
744
|
+
const hasPublishedSupervisor = () => {
|
|
745
|
+
const identity = spawnedIdentity;
|
|
746
|
+
return identity !== null && [readWorkspaceState$1(root)?.supervisor, getProject(root)?.supervisor].some((record) => record?.pid === identity.pid && record?.processToken === identity.processToken);
|
|
747
|
+
};
|
|
748
|
+
if (hasPublishedSupervisor()) {
|
|
749
|
+
for (const held of claims) (0, ownership_claim_exports.markClaimChildPending)(held);
|
|
750
|
+
throw new WorkspaceProcessCleanupError(claims, `Supervisor pid ${child.pid} published ownership that cannot be reconciled with the workspace registry.`, "Inspect the supervisor and its dev server, then stop them with their original tooling before retrying.");
|
|
751
|
+
}
|
|
752
|
+
const same = spawnedIdentity && (0, process_identity_exports.inspectProcessIdentity)(spawnedIdentity) === "same";
|
|
753
|
+
const supervisorCleanup = childExit !== null && !(0, ownership_claim_exports.processGroupAlive)(child.pid) || Boolean(same && supervisorSignalable(child) && await d.terminateSupervisorChild(child));
|
|
754
|
+
const publicationRacedCleanup = hasPublishedSupervisor();
|
|
755
|
+
if (!supervisorCleanup || d.platform !== "win32" && (0, ownership_claim_exports.processGroupAlive)(child.pid) || publicationRacedCleanup) {
|
|
756
|
+
if (publicationRacedCleanup) for (const held of claims) (0, ownership_claim_exports.markClaimChildPending)(held);
|
|
757
|
+
throw new WorkspaceProcessCleanupError(claims, `Could not confirm cleanup of supervisor pid ${child.pid}.`, `Stop supervisor pid ${child.pid} with its original tooling before retrying.`);
|
|
758
|
+
}
|
|
759
|
+
for (const held of claims) (0, ownership_claim_exports.clearClaimChild)(held);
|
|
760
|
+
} else if (unknownWindowsSpawn) throw new WorkspaceProcessCleanupError(claims, "The Windows launcher started a supervisor but its identity was not recorded.", "Inspect and stop it with its original tooling before retrying.");
|
|
761
|
+
} catch (cause) {
|
|
762
|
+
if (cause instanceof WorkspaceProcessCleanupError) throw cause;
|
|
763
|
+
throw new WorkspaceProcessCleanupError(claims, `Could not reconcile supervisor ownership: ${cause.message}`, "Inspect and stop the supervisor with its original tooling before retrying.");
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
try {
|
|
767
|
+
if (remote && !tunnel && !publicUrl && tunnelMode !== "off") {
|
|
768
|
+
const available = d.providers(tunnelMode);
|
|
769
|
+
const plan = planMetroReach({
|
|
770
|
+
mode: tunnelMode,
|
|
771
|
+
metroPort: port,
|
|
772
|
+
publicUrl,
|
|
773
|
+
isExpo: expoServer,
|
|
774
|
+
available
|
|
775
|
+
});
|
|
776
|
+
if ("failed" in plan) return fail({
|
|
777
|
+
code: "STIM_REMOTE_METRO_UNREACHABLE",
|
|
778
|
+
message: plan.failed,
|
|
779
|
+
remedy: plan.remedy
|
|
780
|
+
});
|
|
781
|
+
if ("start" in plan) {
|
|
782
|
+
const candidates = tunnelMode === "auto" ? available.filter((provider) => provider !== "tailscale") : [plan.start];
|
|
783
|
+
const expectedStableUrl = ngrokUrlSetting(settings);
|
|
784
|
+
let acquisition;
|
|
785
|
+
try {
|
|
786
|
+
acquisition = await (async () => {
|
|
787
|
+
let tracking;
|
|
788
|
+
let startedCleanup = null;
|
|
789
|
+
const recorded = readMetroTunnel(root);
|
|
790
|
+
if (recorded?.kind === "managed" && d.isTunnelAlive(recorded.pid)) {
|
|
791
|
+
if (!recorded.processToken) return { failed: {
|
|
792
|
+
code: "STIM_REMOTE_START_REQUIRED",
|
|
793
|
+
message: "The recorded managed Metro tunnel has no process identity token.",
|
|
794
|
+
remedy: "Inspect the process, stop it with the provider tooling, remove the stale metroTunnel state, and retry."
|
|
795
|
+
} };
|
|
796
|
+
if (!d.isTunnelAlive(recorded.pid, recorded.processToken)) return { failed: {
|
|
797
|
+
code: "STIM_REMOTE_START_REQUIRED",
|
|
798
|
+
message: `The recorded ${recorded.provider} tunnel pid ${recorded.pid} no longer has its recorded process identity.`,
|
|
799
|
+
remedy: "Run `stim stop`, resolve any ownership refusal, then retry `stim start --remote`."
|
|
800
|
+
} };
|
|
801
|
+
if (!(recorded.port === port && candidates.includes(recorded.provider) && (!expectedStableUrl || normalizeManagedTunnelUrl(recorded.url) === expectedStableUrl))) return { failed: {
|
|
802
|
+
code: "STIM_REMOTE_START_REQUIRED",
|
|
803
|
+
message: `A different managed Metro tunnel is already running for this workspace.`,
|
|
804
|
+
remedy: "Run `stim stop`, then `stim start --remote`."
|
|
805
|
+
} };
|
|
806
|
+
tracking = {
|
|
807
|
+
record: {
|
|
808
|
+
provider: recorded.provider,
|
|
809
|
+
pid: recorded.pid,
|
|
810
|
+
url: normalizeManagedTunnelUrl(recorded.url),
|
|
811
|
+
port: recorded.port,
|
|
812
|
+
startedAt: recorded.startedAt,
|
|
813
|
+
processToken: recorded.processToken,
|
|
814
|
+
logFile: recorded.logFile
|
|
815
|
+
},
|
|
816
|
+
startedHere: false
|
|
817
|
+
};
|
|
818
|
+
} else {
|
|
819
|
+
const currentResolution = await resolveStartupMetro(port, root);
|
|
820
|
+
const currentSupervisor = liveSupervisor({
|
|
821
|
+
state: readWorkspaceState$1(root),
|
|
822
|
+
project: getProject(root),
|
|
823
|
+
port
|
|
824
|
+
});
|
|
825
|
+
if (currentResolution.metro || currentSupervisor) return { failed: {
|
|
826
|
+
code: "STIM_REMOTE_START_REQUIRED",
|
|
827
|
+
message: `The dev server on port ${port} is local-only and cannot gain a managed tunnel while it is running.`,
|
|
828
|
+
remedy: "Run `stim stop`, then `stim start --remote`."
|
|
829
|
+
} };
|
|
830
|
+
throwIfCancelled(operationSignal, "Metro tunnel startup");
|
|
831
|
+
for (const held of claims) (0, ownership_claim_exports.markClaimChildPending)(held);
|
|
832
|
+
tunnelPending = new WorkspaceProcessCleanupError(claims, "A new managed Metro tunnel could not be recorded or confirmed stopped.", "Inspect and stop the tunnel with its provider tooling before retrying.");
|
|
833
|
+
const started = await d.startTunnelSequence({
|
|
834
|
+
claims,
|
|
835
|
+
signal: operationSignal,
|
|
836
|
+
providers: candidates,
|
|
837
|
+
port,
|
|
838
|
+
ngrokUrl: expectedStableUrl,
|
|
839
|
+
requireReachable: false
|
|
840
|
+
});
|
|
841
|
+
if ("failed" in started) {
|
|
842
|
+
if (started.cleanupFailed) {
|
|
843
|
+
tunnelPending = new WorkspaceProcessCleanupError(claims, started.reason, "Stop the tunnel with its provider tooling before retrying.");
|
|
844
|
+
throw tunnelPending;
|
|
845
|
+
}
|
|
846
|
+
for (const held of claims) (0, ownership_claim_exports.clearClaimChild)(held);
|
|
847
|
+
tunnelPending = null;
|
|
848
|
+
throwIfCancelled(operationSignal, "Metro tunnel startup");
|
|
849
|
+
return { failed: {
|
|
850
|
+
code: "STIM_REMOTE_METRO_UNREACHABLE",
|
|
851
|
+
message: `Could not start a managed Metro tunnel for port ${port}.`,
|
|
852
|
+
remedy: started.reason
|
|
853
|
+
} };
|
|
854
|
+
}
|
|
855
|
+
for (const held of claims) (0, ownership_claim_exports.setClaimChild)(held, {
|
|
856
|
+
pid: started.pid,
|
|
857
|
+
processToken: started.processToken
|
|
858
|
+
});
|
|
859
|
+
const record = {
|
|
860
|
+
provider: started.provider,
|
|
861
|
+
pid: started.pid,
|
|
862
|
+
url: normalizeManagedTunnelUrl(started.url),
|
|
863
|
+
port,
|
|
864
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
865
|
+
processToken: started.processToken,
|
|
866
|
+
logFile: started.logFile
|
|
867
|
+
};
|
|
868
|
+
startedCleanup = started.cleanup;
|
|
869
|
+
try {
|
|
870
|
+
await d.writeTunnelRecord(root, { metroTunnel: {
|
|
871
|
+
kind: "managed",
|
|
872
|
+
...record
|
|
873
|
+
} });
|
|
874
|
+
const stored = readMetroTunnel(root);
|
|
875
|
+
if (stored?.kind !== "managed" || stored.pid !== record.pid || stored.processToken !== record.processToken || stored.port !== record.port || stored.provider !== record.provider || stored.url !== record.url) throw new Error("The workspace did not retain this tunnel ownership record.");
|
|
876
|
+
for (const held of claims) (0, ownership_claim_exports.clearClaimChild)(held);
|
|
877
|
+
tunnelPending = null;
|
|
878
|
+
} catch (err) {
|
|
879
|
+
const recordError = err instanceof Error ? err.message : String(err);
|
|
880
|
+
const stopped = await started.cleanup();
|
|
881
|
+
if (stopped.status === "stopped") {
|
|
882
|
+
for (const held of claims) (0, ownership_claim_exports.clearClaimChild)(held);
|
|
883
|
+
tunnelPending = null;
|
|
884
|
+
} else tunnelPending = new WorkspaceProcessCleanupError(claims, `Could not record the managed Metro tunnel: ${recordError}`, `Cleanup failed. Unmanaged pid ${record.pid} may still be running: ${stopped.reason ?? "unknown error"}`);
|
|
885
|
+
return { failed: {
|
|
886
|
+
code: "STIM_REMOTE_METRO_UNREACHABLE",
|
|
887
|
+
message: `Could not record the managed Metro tunnel: ${recordError}`,
|
|
888
|
+
remedy: stopped.status === "failed" ? `Cleanup failed. Unmanaged pid ${record.pid} may still be running: ${stopped.reason ?? "unknown error"}` : "The tunnel process was stopped. Fix the workspace write error, then retry `stim start --remote`."
|
|
889
|
+
} };
|
|
890
|
+
}
|
|
891
|
+
tracking = {
|
|
892
|
+
record,
|
|
893
|
+
startedHere: true
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
const currentResolution = await resolveStartupMetro(port, root);
|
|
897
|
+
const currentSupervisor = liveSupervisor({
|
|
898
|
+
state: readWorkspaceState$1(root),
|
|
899
|
+
project: getProject(root),
|
|
900
|
+
port
|
|
901
|
+
});
|
|
902
|
+
if (currentResolution.metro || currentSupervisor) {
|
|
903
|
+
if (tracking.startedHere && startedCleanup) {
|
|
904
|
+
const stopped = await startedCleanup();
|
|
905
|
+
if (stopped.status === "failed") return { failed: {
|
|
906
|
+
code: "STIM_REMOTE_START_REQUIRED",
|
|
907
|
+
message: `The dev server on port ${port} started before the managed tunnel was ready.`,
|
|
908
|
+
remedy: `Tunnel cleanup failed for pid ${tracking.record.pid}: ${stopped.reason ?? "unknown error"}. The tunnel record remains available to \`stim stop\`.`
|
|
909
|
+
} };
|
|
910
|
+
await d.clearTunnelRecord(root, tracking.record);
|
|
911
|
+
return { failed: {
|
|
912
|
+
code: "STIM_REMOTE_START_REQUIRED",
|
|
913
|
+
message: `The dev server on port ${port} started before the managed tunnel was ready.`,
|
|
914
|
+
remedy: "Run `stim stop`, then `stim start --remote`."
|
|
915
|
+
} };
|
|
916
|
+
}
|
|
917
|
+
return {
|
|
918
|
+
origin: tracking.record.url,
|
|
919
|
+
tunnel: tracking
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
if (!d.isTunnelAlive(tracking.record.pid, tracking.record.processToken)) {
|
|
923
|
+
if (tracking.startedHere && startedCleanup) {
|
|
924
|
+
const stopped = await startedCleanup();
|
|
925
|
+
if (stopped.status === "failed") return { failed: {
|
|
926
|
+
code: "STIM_REMOTE_METRO_UNREACHABLE",
|
|
927
|
+
message: `The managed ${tracking.record.provider} tunnel is no longer verified running before supervisor startup.`,
|
|
928
|
+
remedy: `Cleanup failed. Unmanaged pid ${tracking.record.pid} may still be running: ${stopped.reason ?? "unknown error"}. The tunnel record remains available to \`stim stop\`.`
|
|
929
|
+
} };
|
|
930
|
+
}
|
|
931
|
+
await d.clearTunnelRecord(root, tracking.record);
|
|
932
|
+
return { failed: {
|
|
933
|
+
code: "STIM_REMOTE_METRO_UNREACHABLE",
|
|
934
|
+
message: `The managed ${tracking.record.provider} tunnel is no longer verified running before supervisor startup.`,
|
|
935
|
+
remedy: "Retry `stim start --remote`."
|
|
936
|
+
} };
|
|
937
|
+
}
|
|
938
|
+
let child;
|
|
939
|
+
try {
|
|
940
|
+
child = await spawnSupervisor(tracking.record.url);
|
|
941
|
+
} catch (err) {
|
|
942
|
+
return { failed: {
|
|
943
|
+
code: "STIM_SUPERVISOR_EXITED",
|
|
944
|
+
message: `Could not spawn the dev server supervisor: ${err?.message || err}`,
|
|
945
|
+
remedy: "Fix the spawn error, then retry `stim start --remote`."
|
|
946
|
+
} };
|
|
947
|
+
}
|
|
948
|
+
const handoffRecord = {
|
|
949
|
+
pid: child.pid,
|
|
950
|
+
processToken: child.recordedIdentity ? child.recordedIdentity.processToken ?? null : child.pid ? (0, process_identity_exports.captureProcessToken)(child.pid) : null,
|
|
951
|
+
port,
|
|
952
|
+
mode: serverMode,
|
|
953
|
+
startedAt: new Date(spawnedTs ?? Date.now()).toISOString()
|
|
954
|
+
};
|
|
955
|
+
try {
|
|
956
|
+
await d.writeSupervisorRecord(root, { supervisor: handoffRecord });
|
|
957
|
+
} catch {
|
|
958
|
+
if (!await waitForSupervisorHandoff(child)) return { failed: {
|
|
959
|
+
code: "STIM_SUPERVISOR_EXITED",
|
|
960
|
+
message: `Could not record supervisor pid ${child.pid ?? "unknown"} before releasing the managed start lock.`,
|
|
961
|
+
remedy: "The supervisor process was stopped. Retry `stim start --remote`."
|
|
962
|
+
} };
|
|
963
|
+
}
|
|
964
|
+
return {
|
|
965
|
+
origin: tracking.record.url,
|
|
966
|
+
tunnel: tracking
|
|
967
|
+
};
|
|
968
|
+
})();
|
|
969
|
+
} catch (err) {
|
|
970
|
+
if (err instanceof WorkspaceProcessCleanupError) throw err;
|
|
971
|
+
throwIfCancelled(operationSignal, "Metro startup");
|
|
972
|
+
if (err instanceof Error && "code" in err && err.code === "STIM_CANCELLED") throw err;
|
|
973
|
+
return fail({
|
|
974
|
+
code: "STIM_REMOTE_METRO_UNREACHABLE",
|
|
975
|
+
message: `Could not start the managed Metro tunnel: ${err?.message || err}`,
|
|
976
|
+
remedy: "Retry `stim start --remote` after the other start command finishes."
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
if ("failed" in acquisition) return fail(acquisition.failed);
|
|
980
|
+
publicOrigin = acquisition.origin;
|
|
981
|
+
managedTunnel = acquisition.tunnel;
|
|
982
|
+
if (managedTunnel.record.provider === "tailscale") note(phaseLine("lan", `tailnet-only Metro: ${publicOrigin}`));
|
|
983
|
+
resolution = await resolveStartupMetro(port, root);
|
|
984
|
+
supervisor = liveSupervisor({
|
|
985
|
+
state: readWorkspaceState$1(root),
|
|
986
|
+
project: getProject(root),
|
|
987
|
+
port
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
const managedTunnelExited = () => managedTunnel !== null && !d.isTunnelAlive(managedTunnel.record.pid, managedTunnel.record.processToken);
|
|
992
|
+
const failExitedManagedTunnel = async () => {
|
|
993
|
+
const tracked = managedTunnel;
|
|
994
|
+
const record = tracked.record;
|
|
995
|
+
const stopped = tracked.startedHere ? await d.stopTunnel(record) : { status: "missing" };
|
|
996
|
+
if (stopped.status !== "failed") await d.clearTunnelRecord(root, record);
|
|
997
|
+
return fail({
|
|
998
|
+
code: "STIM_REMOTE_METRO_UNREACHABLE",
|
|
999
|
+
message: `The managed ${record.provider} tunnel is no longer verified running before the dev server became ready.`,
|
|
1000
|
+
remedy: stopped.status === "failed" ? `The tunnel cleanup also failed: ${stopped.reason ?? "unknown error"}. Run \`stim stop\`, then retry.` : "Run `stim stop`, then `stim start --remote`."
|
|
1001
|
+
});
|
|
1002
|
+
};
|
|
1003
|
+
const requireExpoTunnel = () => {
|
|
1004
|
+
if (tunnel && (!supervisor || readMetroTunnel(root)?.kind !== "expo")) fail({
|
|
1005
|
+
code: "STIM_REMOTE_START_REQUIRED",
|
|
1006
|
+
message: `The Expo dev server on port ${port} is local-only and cannot gain a tunnel while it is running.`,
|
|
1007
|
+
remedy: "Run `stim stop`, then `stim start --remote`."
|
|
1008
|
+
});
|
|
1009
|
+
};
|
|
1010
|
+
if (!spawned.child && resolution.metro) {
|
|
1011
|
+
if (managedTunnelExited()) return failExitedManagedTunnel();
|
|
1012
|
+
if (tunnel && supervisor) {
|
|
1013
|
+
if (!await waitForExpoTunnel({
|
|
1014
|
+
root,
|
|
1015
|
+
seconds: waitSeconds,
|
|
1016
|
+
aborted: () => !liveSupervisor({
|
|
1017
|
+
state: readWorkspaceState$1(root),
|
|
1018
|
+
project: getProject(root),
|
|
1019
|
+
port
|
|
1020
|
+
})
|
|
1021
|
+
})) {
|
|
1022
|
+
const stillLive = liveSupervisor({
|
|
1023
|
+
state: readWorkspaceState$1(root),
|
|
1024
|
+
project: getProject(root),
|
|
1025
|
+
port
|
|
1026
|
+
});
|
|
1027
|
+
return fail({
|
|
1028
|
+
code: "STIM_REMOTE_START_REQUIRED",
|
|
1029
|
+
message: stillLive ? `The Expo dev server on port ${port} is local-only and cannot gain a tunnel while it is running.` : `The Expo dev server on port ${port} stopped before its tunnel became ready.`,
|
|
1030
|
+
remedy: stillLive ? "Run `stim stop`, then `stim start --remote`." : "Run `stim start --remote` again."
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
supervisor = liveSupervisor({
|
|
1034
|
+
state: readWorkspaceState$1(root),
|
|
1035
|
+
project: getProject(root),
|
|
1036
|
+
port
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
requireExpoTunnel();
|
|
1040
|
+
if (!supervisor) {
|
|
1041
|
+
note(chalk.dim(`A dev server for this project already answers on port ${port}, started outside Stim.`));
|
|
1042
|
+
note(chalk.dim("Leaving it alone: Stim will not start a second bundler over a working one."));
|
|
1043
|
+
}
|
|
1044
|
+
if (managedTunnelExited()) return failExitedManagedTunnel();
|
|
1045
|
+
await withWorkspaceStateLockAsync(root, () => clearWorkspaceStateKeys(root, [IDLE_STOP_KEY]), operationSignal);
|
|
1046
|
+
return startFacts({
|
|
1047
|
+
root,
|
|
1048
|
+
port,
|
|
1049
|
+
supervisor,
|
|
1050
|
+
logsDir,
|
|
1051
|
+
alreadyRunning: true
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
if (!spawned.child && supervisor) {
|
|
1055
|
+
note(chalk.dim(`Supervisor pid ${supervisor.pid} is already running for this workspace; waiting for it to answer on port ${port}...`));
|
|
1056
|
+
if (!await waitForMetro({
|
|
1057
|
+
root,
|
|
1058
|
+
port,
|
|
1059
|
+
seconds: waitSeconds,
|
|
1060
|
+
aborted: managedTunnel ? managedTunnelExited : void 0
|
|
1061
|
+
})) {
|
|
1062
|
+
if (managedTunnelExited()) return failExitedManagedTunnel();
|
|
1063
|
+
return fail({
|
|
1064
|
+
code: "STIM_METRO_TIMEOUT",
|
|
1065
|
+
message: `Supervisor pid ${supervisor.pid} did not serve port ${port} within ${waitSeconds}s.`,
|
|
1066
|
+
lines: logTailLines(logFile),
|
|
1067
|
+
remedy: "Run `stim stop` to halt it, then `stim start` again."
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
if (tunnel) {
|
|
1071
|
+
if (!await waitForExpoTunnel({
|
|
1072
|
+
root,
|
|
1073
|
+
seconds: waitSeconds,
|
|
1074
|
+
aborted: () => !liveSupervisor({
|
|
1075
|
+
state: readWorkspaceState$1(root),
|
|
1076
|
+
project: getProject(root),
|
|
1077
|
+
port
|
|
1078
|
+
})
|
|
1079
|
+
})) {
|
|
1080
|
+
const stillLive = liveSupervisor({
|
|
1081
|
+
state: readWorkspaceState$1(root),
|
|
1082
|
+
project: getProject(root),
|
|
1083
|
+
port
|
|
1084
|
+
});
|
|
1085
|
+
return fail({
|
|
1086
|
+
code: "STIM_REMOTE_START_REQUIRED",
|
|
1087
|
+
message: stillLive ? `The Expo dev server on port ${port} is local-only and cannot gain a tunnel while it is running.` : `The Expo dev server on port ${port} stopped before its tunnel became ready.`,
|
|
1088
|
+
remedy: stillLive ? "Run `stim stop`, then `stim start --remote`." : "Run `stim start --remote` again."
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
supervisor = liveSupervisor({
|
|
1093
|
+
state: readWorkspaceState$1(root),
|
|
1094
|
+
project: getProject(root),
|
|
1095
|
+
port
|
|
1096
|
+
}) || (tunnel ? null : supervisor);
|
|
1097
|
+
requireExpoTunnel();
|
|
1098
|
+
if (managedTunnelExited()) return failExitedManagedTunnel();
|
|
1099
|
+
return startFacts({
|
|
1100
|
+
root,
|
|
1101
|
+
port,
|
|
1102
|
+
supervisor,
|
|
1103
|
+
logsDir,
|
|
1104
|
+
alreadyRunning: true
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
if (managedTunnelExited()) return failExitedManagedTunnel();
|
|
1108
|
+
const child = spawned.child ?? await spawnSupervisor(publicOrigin);
|
|
1109
|
+
const attemptStartedTs = spawnedTs ?? Date.now();
|
|
1110
|
+
if (!await waitForMetro({
|
|
1111
|
+
root,
|
|
1112
|
+
port,
|
|
1113
|
+
seconds: waitSeconds,
|
|
1114
|
+
aborted: () => childExit !== null || (child.pid ? !pidExists(child.pid) : false) || managedTunnelExited()
|
|
1115
|
+
})) {
|
|
1116
|
+
if (managedTunnelExited()) return failExitedManagedTunnel();
|
|
1117
|
+
const gone = childExit !== null || (child.pid ? !pidExists(child.pid) : false);
|
|
1118
|
+
const exitInfo = childExit;
|
|
1119
|
+
const how = exitInfo ? exitInfo.signal ? `signal ${exitInfo.signal}` : `code ${exitInfo.code}` : "without being observed";
|
|
1120
|
+
return fail(gone ? {
|
|
1121
|
+
code: "STIM_SUPERVISOR_EXITED",
|
|
1122
|
+
message: `The supervisor exited (${how}) before the dev server came up on port ${port}.`,
|
|
1123
|
+
lines: failureEvidence({
|
|
1124
|
+
logFile,
|
|
1125
|
+
logsDir,
|
|
1126
|
+
sinceTs: attemptStartedTs
|
|
1127
|
+
}),
|
|
1128
|
+
remedy: "Fix the error above and run `stim start` again; `stim logs --errors` has the full records."
|
|
1129
|
+
} : {
|
|
1130
|
+
code: "STIM_METRO_TIMEOUT",
|
|
1131
|
+
message: `The dev server did not answer on port ${port} within ${waitSeconds}s.`,
|
|
1132
|
+
lines: failureEvidence({
|
|
1133
|
+
logFile,
|
|
1134
|
+
logsDir,
|
|
1135
|
+
sinceTs: attemptStartedTs
|
|
1136
|
+
}),
|
|
1137
|
+
remedy: "It may still be starting. Run `stim stop` to halt it, or `stim logs` to follow along."
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
if (managedTunnelExited()) return failExitedManagedTunnel();
|
|
1141
|
+
if (tunnel) {
|
|
1142
|
+
if (!await waitForExpoTunnel({
|
|
1143
|
+
root,
|
|
1144
|
+
seconds: waitSeconds,
|
|
1145
|
+
aborted: () => childExit !== null || (child.pid ? !pidExists(child.pid) : false)
|
|
1146
|
+
})) {
|
|
1147
|
+
const gone = childExit !== null || (child.pid ? !pidExists(child.pid) : false);
|
|
1148
|
+
return fail({
|
|
1149
|
+
code: gone ? "STIM_SUPERVISOR_EXITED" : "STIM_METRO_TIMEOUT",
|
|
1150
|
+
message: gone ? `The supervisor exited before the Expo tunnel became ready on port ${port}.` : `Expo did not report a tunnel URL within ${waitSeconds}s.`,
|
|
1151
|
+
remedy: "Run `stim stop`, then `stim start --remote`."
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
supervisor = await waitForSupervisorHandoff(child);
|
|
1156
|
+
if (!supervisor) return fail({
|
|
1157
|
+
code: "STIM_SUPERVISOR_EXITED",
|
|
1158
|
+
message: "The dev server answered before its supervisor ownership could be verified.",
|
|
1159
|
+
remedy: "Retry `stim start` after the supervisor exits or records its ownership."
|
|
1160
|
+
});
|
|
1161
|
+
return startFacts({
|
|
1162
|
+
root,
|
|
1163
|
+
port,
|
|
1164
|
+
supervisor,
|
|
1165
|
+
logsDir,
|
|
1166
|
+
alreadyRunning: false
|
|
1167
|
+
});
|
|
1168
|
+
} finally {
|
|
1169
|
+
await reconcileSpawnedSupervisor();
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
const startLocked = async () => {
|
|
1173
|
+
try {
|
|
1174
|
+
return await d.withTunnelLock(root, (tunnelClaim) => withWorkspaceProcessLock(dirname(workspaceLogsDir$1(root)), "metro-start", (claim) => runStart(claim, tunnelClaim), {
|
|
1175
|
+
external: true,
|
|
1176
|
+
signal: operationSignal
|
|
1177
|
+
}), { signal: operationSignal });
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
if (error instanceof WorkspaceProcessCleanupError || error instanceof StartRefusal) throw error;
|
|
1180
|
+
throwIfCancelled(operationSignal, "Metro startup");
|
|
1181
|
+
if (workspaceProcessLockError(error) === "refused") return fail({
|
|
1182
|
+
code: "STIM_WORKTREE_REMOVAL_IN_PROGRESS",
|
|
1183
|
+
message: `The workspace at ${root} is being removed.`,
|
|
1184
|
+
remedy: "Retry `stim start` after workspace removal finishes."
|
|
1185
|
+
});
|
|
1186
|
+
if (workspaceProcessLockError(error) !== "timeout") throw error;
|
|
1187
|
+
return fail({
|
|
1188
|
+
code: "STIM_METRO_TIMEOUT",
|
|
1189
|
+
message: "Another Metro start or reset is still running for this app.",
|
|
1190
|
+
remedy: "Wait for it to finish, then retry `stim start`."
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
if (!managedRemote) return startLocked();
|
|
1195
|
+
try {
|
|
1196
|
+
return await d.withWorktreeLock(worktreeRoot, startLocked, { signal: operationSignal });
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
const lockError = workspaceProcessLockError(err);
|
|
1199
|
+
if (lockError === "refused") return fail({
|
|
1200
|
+
code: "STIM_WORKTREE_REMOVAL_IN_PROGRESS",
|
|
1201
|
+
message: `The worktree at ${worktreeRoot} is being removed.`,
|
|
1202
|
+
remedy: "Retry `stim start --remote` after `stim worktree remove` finishes."
|
|
1203
|
+
});
|
|
1204
|
+
if (lockError === "timeout") return fail({
|
|
1205
|
+
code: "STIM_REMOTE_METRO_UNREACHABLE",
|
|
1206
|
+
message: `Could not acquire the managed remote worktree lock: ${err?.message || err}`,
|
|
1207
|
+
remedy: "Retry `stim start --remote` after the other remote start command finishes."
|
|
1208
|
+
});
|
|
1209
|
+
throw err;
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
try {
|
|
1213
|
+
return {
|
|
1214
|
+
ok: true,
|
|
1215
|
+
facts: await run(),
|
|
1216
|
+
waited: waitTimer(),
|
|
1217
|
+
reclaimed
|
|
1218
|
+
};
|
|
1219
|
+
} catch (error) {
|
|
1220
|
+
if (error instanceof WorkspaceProcessCleanupError) return {
|
|
1221
|
+
ok: false,
|
|
1222
|
+
error: startError(error),
|
|
1223
|
+
lines: [],
|
|
1224
|
+
reclaimed,
|
|
1225
|
+
retainedClaimRoots: [...new Set(error.claims.map((claim) => claim.root))]
|
|
1226
|
+
};
|
|
1227
|
+
if (!(error instanceof StartRefusal)) throw error;
|
|
1228
|
+
return {
|
|
1229
|
+
ok: false,
|
|
1230
|
+
error: startError(error.refusal),
|
|
1231
|
+
lines: error.refusal.lines ?? [],
|
|
1232
|
+
reclaimed
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
async function resolvePort(root, settings, note, fail) {
|
|
1237
|
+
const result = await resolveWorkspaceMetroPort(root, settings, note, "start");
|
|
1238
|
+
return typeof result === "number" ? result : fail(result);
|
|
1239
|
+
}
|
|
1240
|
+
async function resolveWorkspaceMetroPort(root, settings, note, command, reserve = true) {
|
|
1241
|
+
const setting = metroPortSetting(settings, ambientEnv());
|
|
1242
|
+
if (setting.error) return {
|
|
1243
|
+
code: "STIM_BAD_ARG",
|
|
1244
|
+
message: setting.error,
|
|
1245
|
+
remedy: SETTING_SHAPE_REMEDY
|
|
1246
|
+
};
|
|
1247
|
+
const pinned = setting.port;
|
|
1248
|
+
const project = getProject(root);
|
|
1249
|
+
const recorded = project?.metroPort;
|
|
1250
|
+
if (pinned === null && command === "web") return recorded ?? await reservePort(root, null);
|
|
1251
|
+
if (recorded) {
|
|
1252
|
+
const supervisor = resolveSupervisorTarget({
|
|
1253
|
+
state: readWorkspaceState$1(root)?.supervisor,
|
|
1254
|
+
record: project.supervisor,
|
|
1255
|
+
reservedPort: recorded
|
|
1256
|
+
});
|
|
1257
|
+
if (supervisor.status !== "none" && supervisor.status !== "stale") {
|
|
1258
|
+
if (pinned === null || recorded === pinned) return recorded;
|
|
1259
|
+
if (supervisor.status === "unverified") return {
|
|
1260
|
+
code: "STIM_BAD_ARG",
|
|
1261
|
+
message: `Cannot move this workspace's Metro to metro.port ${pinned}: ${supervisor.reason}.`,
|
|
1262
|
+
remedy: `Stop that supervisor with the tool that started it, then retry, or restore metro.port to ${recorded}. Stim leaves unverified processes alone.`
|
|
1263
|
+
};
|
|
1264
|
+
return {
|
|
1265
|
+
code: "STIM_BAD_ARG",
|
|
1266
|
+
message: `This workspace's dev server is running on port ${recorded}, and metro.port pins it to ${pinned}.`,
|
|
1267
|
+
remedy: "Run `stim stop`, then retry, or unset or restore metro.port."
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if (pinned !== null) {
|
|
1272
|
+
const held = await resolveStartupMetro(pinned, root);
|
|
1273
|
+
if (held.notOurs) return {
|
|
1274
|
+
code: "STIM_BAD_ARG",
|
|
1275
|
+
message: `Port ${pinned} is held by something else (${held.notOurs}), and metro.port pins this workspace's Metro to it.`,
|
|
1276
|
+
remedy: "Stop the process on that port, or set another metro.port."
|
|
1277
|
+
};
|
|
1278
|
+
if (recorded === pinned) return recorded;
|
|
1279
|
+
} else if (recorded) {
|
|
1280
|
+
const held = await resolveStartupMetro(recorded, root);
|
|
1281
|
+
if (!held.notOurs) return recorded;
|
|
1282
|
+
const fresh = await reservePort(root, null);
|
|
1283
|
+
if (typeof fresh === "number" && fresh !== recorded) {
|
|
1284
|
+
note(chalk.yellow(`Port ${recorded} is held by something else (${held.notOurs}).`));
|
|
1285
|
+
note(chalk.dim(`Reserved port ${fresh} for this project instead.`));
|
|
1286
|
+
}
|
|
1287
|
+
return fresh;
|
|
1288
|
+
}
|
|
1289
|
+
if (!reserve && pinned !== null) return pinned;
|
|
1290
|
+
return reservePort(root, pinned);
|
|
1291
|
+
}
|
|
1292
|
+
async function reservePort(root, pinned) {
|
|
1293
|
+
try {
|
|
1294
|
+
return await reserveMetroPort(root, void 0, void 0, pinned, ambientContext()?.signal);
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
throwIfCancelled(ambientContext()?.signal, "Metro startup");
|
|
1297
|
+
if (error instanceof PortInspectionError) return {
|
|
1298
|
+
code: error.code,
|
|
1299
|
+
message: error.reason,
|
|
1300
|
+
remedy: PORT_INSPECTION_REMEDY
|
|
1301
|
+
};
|
|
1302
|
+
return {
|
|
1303
|
+
code: "STIM_BAD_ARG",
|
|
1304
|
+
message: error.message,
|
|
1305
|
+
remedy: "Give each workspace its own metro.port, in the workspace layer or the environment."
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
async function waitForMetro({ root, port, seconds, aborted = () => false, probe = resolveStartupMetro }) {
|
|
1310
|
+
const deadline = Date.now() + seconds * 1e3;
|
|
1311
|
+
while (Date.now() < deadline) {
|
|
1312
|
+
if ((await probe(port, root)).metro) return true;
|
|
1313
|
+
if (aborted()) return false;
|
|
1314
|
+
await waitForStartup(POLL_MS);
|
|
1315
|
+
}
|
|
1316
|
+
const last = await probe(port, root);
|
|
1317
|
+
return Boolean(last.metro);
|
|
1318
|
+
}
|
|
1319
|
+
async function waitForExpoTunnel({ root, seconds, aborted = () => false }) {
|
|
1320
|
+
const deadline = Date.now() + seconds * 1e3;
|
|
1321
|
+
while (Date.now() < deadline) {
|
|
1322
|
+
if (aborted()) return false;
|
|
1323
|
+
if (readMetroTunnel(root)?.kind === "expo") return true;
|
|
1324
|
+
await waitForStartup(POLL_MS);
|
|
1325
|
+
}
|
|
1326
|
+
return !aborted() && readMetroTunnel(root)?.kind === "expo";
|
|
1327
|
+
}
|
|
1328
|
+
function logTailLines(logFile) {
|
|
1329
|
+
return [...readLogTail(logFile), `Supervisor log: ${logFile}`];
|
|
1330
|
+
}
|
|
1331
|
+
function failureEvidence({ logFile, logsDir, sinceTs }) {
|
|
1332
|
+
const supTail = readLogTail(logFile);
|
|
1333
|
+
const lines = supTail.length > 0 ? [...supTail, `Supervisor log: ${logFile}`] : [];
|
|
1334
|
+
let errors = [];
|
|
1335
|
+
let all = [];
|
|
1336
|
+
try {
|
|
1337
|
+
all = queryLogs({ dir: logsDir });
|
|
1338
|
+
errors = all.filter((record) => levelRank(record.level) >= levelRank("error"));
|
|
1339
|
+
} catch {}
|
|
1340
|
+
const since = (rs) => rs.filter((r) => typeof r.ts === "number" && r.ts >= sinceTs).slice(-8);
|
|
1341
|
+
let recent = since(errors);
|
|
1342
|
+
const fellBack = recent.length === 0;
|
|
1343
|
+
if (fellBack) recent = since(all);
|
|
1344
|
+
for (const r of recent) lines.push(`${String(r.src ?? "?")}: ${String(r.msg ?? "").split("\n")[0] ?? ""}`);
|
|
1345
|
+
if (recent.length > 0) lines.push(fellBack ? "Full records: `stim logs`" : "Full records: `stim logs --errors`");
|
|
1346
|
+
return lines;
|
|
1347
|
+
}
|
|
1348
|
+
function report({ json, out, facts, waited, reclaimed, links }) {
|
|
1349
|
+
if (json) {
|
|
1350
|
+
console.log(JSON.stringify({
|
|
1351
|
+
...facts,
|
|
1352
|
+
...links ? { links } : {},
|
|
1353
|
+
...reclaimed.length ? { reclaimed } : {}
|
|
1354
|
+
}));
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
const who = facts.supervisorPid ? `supervisor pid ${facts.supervisorPid}${facts.mode ? ` (${facts.mode})` : ""}` : "started outside Stim";
|
|
1358
|
+
out(chalk.green(`OK: dev server on port ${facts.port}, ${who}${facts.alreadyRunning ? " (already running)" : ""} ${waited}`));
|
|
1359
|
+
out(chalk.dim(phaseLine("logs", facts.logsDir)));
|
|
1360
|
+
if (facts.agentDevice) out(chalk.dim(phaseLine("agent-device", `AGENT_DEVICE_STATE_DIR=${facts.agentDevice.stateDir}`)));
|
|
1361
|
+
if (links) console.error(chalk.dim(workspaceLinkLine(links)));
|
|
1362
|
+
}
|
|
1363
|
+
//#endregion
|
|
1364
|
+
export { registerStart as a, startDevServer as c, supervisorSignalable as d, tailLines as f, detectBundleId as g, detectAppIds as h, readLogTail as i, startFacts as l, detectAndroidPackage as m, liveSupervisor as n, resolveWorkspaceMetroPort as o, wantsExpoOwnTunnel as p, parseWait as r, startCommand as s, failureEvidence as t, supervisorEntry as u };
|