rnxsim 0.1.313 → 0.1.314
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/LICENSE +11 -10
- package/README.md +5 -0
- package/cli/app-config.ts +65 -0
- package/cli/app-fonts.ts +408 -0
- package/cli/app-project.ts +231 -0
- package/cli/app-splash.ts +185 -0
- package/cli/app-state-reset.ts +24 -0
- package/cli/auth.ts +155 -0
- package/cli/bin.ts +594 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2830 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/commands/agent-wrapper.ts +986 -0
- package/cli/commands/agent.ts +423 -0
- package/cli/commands/app-fonts.ts +98 -0
- package/cli/commands/assert.ts +541 -0
- package/cli/commands/auth.ts +59 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +87 -0
- package/cli/commands/config.ts +32 -0
- package/cli/commands/control.ts +2142 -0
- package/cli/commands/cpu-profile.ts +269 -0
- package/cli/commands/daemon-mac-app.ts +169 -0
- package/cli/commands/daemon.ts +874 -0
- package/cli/commands/debug.ts +719 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +197 -0
- package/cli/commands/detox.ts +385 -0
- package/cli/commands/device.ts +133 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/electron.ts +95 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1124 -0
- package/cli/commands/inspect/actions.ts +622 -0
- package/cli/commands/inspect/core.ts +2405 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +192 -0
- package/cli/commands/inspect/env.ts +23 -0
- package/cli/commands/inspect/find.ts +171 -0
- package/cli/commands/inspect/get-layout.ts +39 -0
- package/cli/commands/inspect/keyboard.ts +52 -0
- package/cli/commands/inspect/list.ts +58 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +82 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +22 -0
- package/cli/commands/inspect/settling.ts +158 -0
- package/cli/commands/inspect/shared.ts +353 -0
- package/cli/commands/inspect/sleep.ts +14 -0
- package/cli/commands/inspect/tree.ts +32 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +210 -0
- package/cli/commands/inspect/wait-idle.ts +24 -0
- package/cli/commands/inspect/wait-ready.ts +74 -0
- package/cli/commands/inspect/wait-selector.ts +54 -0
- package/cli/commands/inspect/wait.ts +31 -0
- package/cli/commands/inspect.ts +4519 -0
- package/cli/commands/install-desktop.ts +351 -0
- package/cli/commands/login.ts +331 -0
- package/cli/commands/logout.ts +31 -0
- package/cli/commands/maestro-generate.ts +361 -0
- package/cli/commands/maestro.ts +453 -0
- package/cli/commands/mode.ts +57 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +66 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/profile.ts +108 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1434 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +85 -0
- package/cli/commands/runtime.ts +351 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +143 -0
- package/cli/commands/screenshot-mode.ts +37 -0
- package/cli/commands/screenshot.ts +488 -0
- package/cli/commands/screenshots-capture.ts +607 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +168 -0
- package/cli/commands/setup.ts +545 -0
- package/cli/commands/shell-boolean-mode.ts +81 -0
- package/cli/commands/skills.ts +467 -0
- package/cli/commands/slides.ts +361 -0
- package/cli/commands/state.ts +87 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +54 -0
- package/cli/commands/three-mode.ts +763 -0
- package/cli/commands/timeline.ts +122 -0
- package/cli/commands/upgrade.ts +208 -0
- package/cli/commands/upload.ts +1225 -0
- package/cli/commands/version.ts +54 -0
- package/cli/commands/what-happened.ts +327 -0
- package/cli/current-sim.ts +204 -0
- package/cli/desktop-companion.ts +300 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +180 -0
- package/cli/drivers/playwright.ts +698 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +120 -0
- package/cli/flow-session.ts +187 -0
- package/cli/help.ts +80 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +216 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/maestro-js.ts +334 -0
- package/cli/open-url.ts +8 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +211 -0
- package/cli/prompt.ts +51 -0
- package/cli/recording-access.ts +107 -0
- package/cli/registry.ts +1 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/run-registry.ts +226 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/setup-repository.ts +187 -0
- package/cli/telemetry.ts +187 -0
- package/cli/ws-bridge.ts +798 -0
- package/dist-cli/bin.js +5 -5
- package/dist-cli/chunks/{agent-XZ2KTPCU.js → agent-7YBDCYMA.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-JJYYW2WH.js → agent-wrapper-2GHFBHCR.js} +2 -2
- package/dist-cli/chunks/{app-fonts-IXRNQG6B.js → app-fonts-RSNVPQSU.js} +2 -2
- package/dist-cli/chunks/{assert-54T5SK5F.js → assert-XCMX3XJX.js} +2 -2
- package/dist-cli/chunks/{auth-FI5UDI45.js → auth-B442HRAX.js} +2 -2
- package/dist-cli/chunks/{beta-JV6UKADW.js → beta-XJ55JK3M.js} +2 -2
- package/dist-cli/chunks/camera-UGYSSLIK.js +33 -0
- package/dist-cli/chunks/{chunk-3NV2NCNX.js → chunk-277AEQZX.js} +2 -2
- package/dist-cli/chunks/{chunk-2YR5BGA5.js → chunk-2JNSK774.js} +2 -2
- package/dist-cli/chunks/{chunk-RSZWCKNT.js → chunk-32WOTSTR.js} +3 -3
- package/dist-cli/chunks/{chunk-WEXDAC74.js → chunk-3S753SNQ.js} +2 -2
- package/dist-cli/chunks/{chunk-IJO63TDP.js → chunk-46ZOLOYA.js} +2 -2
- package/dist-cli/chunks/{chunk-WUSWBCWA.js → chunk-5L7ELDQL.js} +8 -9
- package/dist-cli/chunks/{chunk-WF3T4SVI.js → chunk-7OOPFSQS.js} +2 -2
- package/dist-cli/chunks/{chunk-TZFFR3SD.js → chunk-7SV3RPRW.js} +2 -2
- package/dist-cli/chunks/{chunk-WWZIXIRD.js → chunk-7ZC35MOU.js} +1 -1
- package/dist-cli/chunks/chunk-AMG5E6CC.js +9 -0
- package/dist-cli/chunks/{chunk-WINYQ44O.js → chunk-APWNH3A4.js} +1 -1
- package/dist-cli/chunks/chunk-B57XUKY3.js +4 -0
- package/dist-cli/chunks/{chunk-YDGQTMQL.js → chunk-C2NL26TD.js} +1 -1
- package/dist-cli/chunks/{chunk-NMF2ZMZQ.js → chunk-D2FNUWAB.js} +4 -4
- package/dist-cli/chunks/{chunk-YIFT42WN.js → chunk-E5T4XSJ3.js} +2 -2
- package/dist-cli/chunks/{chunk-46EUUFJ5.js → chunk-EAC34EQS.js} +1 -1
- package/dist-cli/chunks/{chunk-OVFJFXUD.js → chunk-EG32ML36.js} +2 -2
- package/dist-cli/chunks/{chunk-5YJCOWCH.js → chunk-FXUAC6D5.js} +1 -1
- package/dist-cli/chunks/chunk-GQSL4USA.js +6 -0
- package/dist-cli/chunks/{chunk-VFCMSYZK.js → chunk-HAXW27SS.js} +2 -2
- package/dist-cli/chunks/{chunk-2D2UPBBR.js → chunk-IZAHPAN6.js} +1 -1
- package/dist-cli/chunks/{chunk-7GN3LVWB.js → chunk-J62KM5TB.js} +2 -2
- package/dist-cli/chunks/{chunk-BTWORNNG.js → chunk-JEMCD5E4.js} +1 -1
- package/dist-cli/chunks/{chunk-DCEMHR2Y.js → chunk-JZS3Q37N.js} +2 -2
- package/dist-cli/chunks/{chunk-D4FFVGI5.js → chunk-KR4JON7D.js} +1 -1
- package/dist-cli/chunks/chunk-KWCYKQIQ.js +4 -0
- package/dist-cli/chunks/{chunk-VNQEB4L7.js → chunk-MCWPL644.js} +2 -2
- package/dist-cli/chunks/chunk-MJYK3N2I.js +4 -0
- package/dist-cli/chunks/{chunk-5TEF3ET3.js → chunk-O6TRIZNS.js} +2 -2
- package/dist-cli/chunks/{chunk-QKDWYITG.js → chunk-P7XL2E73.js} +3 -3
- package/dist-cli/chunks/{chunk-BBULZ7CG.js → chunk-PFQTUKQ4.js} +62 -87
- package/dist-cli/chunks/{chunk-GGRX24GF.js → chunk-PG5RZCTN.js} +2 -2
- package/dist-cli/chunks/{chunk-W6K4EFPH.js → chunk-QGRI2Z4M.js} +2 -2
- package/dist-cli/chunks/{chunk-ZMJD5GEC.js → chunk-QLXXE7GE.js} +1 -1
- package/dist-cli/chunks/{chunk-VZXWHRUZ.js → chunk-QOJJJWJE.js} +89 -133
- package/dist-cli/chunks/{chunk-OZSSI4WN.js → chunk-RWZY5427.js} +2 -2
- package/dist-cli/chunks/{chunk-UC6U3MML.js → chunk-RZKU2K3J.js} +2 -2
- package/dist-cli/chunks/{chunk-IJ5CAZZC.js → chunk-SBV4IK4H.js} +1 -1
- package/dist-cli/chunks/{chunk-DZS6WPUI.js → chunk-SGMVFFMK.js} +1 -1
- package/dist-cli/chunks/{chunk-OHAZNXLK.js → chunk-TAX4UT2N.js} +1 -1
- package/dist-cli/chunks/{chunk-5DHC6KHQ.js → chunk-TUOFAWXT.js} +1 -1
- package/dist-cli/chunks/{chunk-GASE6UBA.js → chunk-UHZLOHGP.js} +1 -1
- package/dist-cli/chunks/{chunk-F5ZRSS3C.js → chunk-VC7V76U3.js} +1 -1
- package/dist-cli/chunks/{chunk-RTN5C5RL.js → chunk-VQVMLW4U.js} +1 -1
- package/dist-cli/chunks/{chunk-WMIIKMGK.js → chunk-VUKKYPZN.js} +2 -2
- package/dist-cli/chunks/{chunk-5TPRP5QT.js → chunk-WGGRJDRE.js} +1 -1
- package/dist-cli/chunks/chunk-X6H76EKP.js +15 -0
- package/dist-cli/chunks/chunk-XLZ5FNRT.js +27 -0
- package/dist-cli/chunks/{chunk-HI5TFJWN.js → chunk-XULEACM4.js} +2 -2
- package/dist-cli/chunks/{chunk-MJRLLB4R.js → chunk-YFSDM7AX.js} +4 -4
- package/dist-cli/chunks/chunk-YWI3UEVX.js +5 -0
- package/dist-cli/chunks/{chunk-XEVZYVIW.js → chunk-ZBSJO4NB.js} +10 -9
- package/dist-cli/chunks/{cleanup-P27PA6JI.js → cleanup-EYLGCA6Z.js} +2 -2
- package/dist-cli/chunks/cli-version-LL2UGIHE.js +4 -0
- package/dist-cli/chunks/{compat-ZD65FED3.js → compat-TLJYHB4E.js} +2 -2
- package/dist-cli/chunks/{config-XMJRNM2A.js → config-4JWUOEXK.js} +2 -2
- package/dist-cli/chunks/{control-KMIQT3QP.js → control-HPAOYF4N.js} +2 -2
- package/dist-cli/chunks/daemon-OYLASXLE.js +4 -0
- package/dist-cli/chunks/{debug-PT4HOP7N.js → debug-4BKXF6KI.js} +5 -5
- package/dist-cli/chunks/{desktop-S3FG72AK.js → desktop-ZQ6ZD2S6.js} +3 -3
- package/dist-cli/chunks/{detox-B3D4IFCN.js → detox-WNPASSS3.js} +2 -2
- package/dist-cli/chunks/{device-XBNDSB2R.js → device-BXLXVG3V.js} +2 -2
- package/dist-cli/chunks/{diagnose-HMQXJE5N.js → diagnose-3QX7W5RW.js} +2 -2
- package/dist-cli/chunks/{disk-cleanup-BLCZ5BSZ.js → disk-cleanup-KSWKI7WB.js} +2 -2
- package/dist-cli/chunks/drivers-EPIEFF7P.js +4 -0
- package/dist-cli/chunks/{film-BJGTBYZB.js → film-GDMR33VO.js} +3 -3
- package/dist-cli/chunks/flow-NKMPBYCJ.js +4 -0
- package/dist-cli/chunks/help-OGCBHOKA.js +4 -0
- package/dist-cli/chunks/{hidden-runtime-alias-ANOYADHM.js → hidden-runtime-alias-S2GTDX3T.js} +2 -2
- package/dist-cli/chunks/home-paths-QRCDLTTV.js +4 -0
- package/dist-cli/chunks/inspect-FUYMOZPY.js +4 -0
- package/dist-cli/chunks/install-desktop-FR6YKW7Y.js +4 -0
- package/dist-cli/chunks/{login-FJ737MWG.js → login-IJAPUZHI.js} +4 -4
- package/dist-cli/chunks/{logout-ZCNMMHMY.js → logout-QMXDFU2X.js} +2 -2
- package/dist-cli/chunks/{maestro-SZTNKLDF.js → maestro-ZXU3YCVX.js} +3 -3
- package/dist-cli/chunks/{maestro-generate-DCFAIZ4H.js → maestro-generate-PYB5QY7K.js} +3 -3
- package/dist-cli/chunks/{mode-GRMQCRXR.js → mode-WGUCL5FZ.js} +2 -2
- package/dist-cli/chunks/{optional-demo-registry-W36EWFFB.js → optional-demo-registry-WH2O6H36.js} +2 -2
- package/dist-cli/chunks/{perf-QYBAAUZG.js → perf-TOD3UFAH.js} +2 -2
- package/dist-cli/chunks/{permissions-3QCQ6VF4.js → permissions-I5BRJGTB.js} +2 -2
- package/dist-cli/chunks/{record-QPPC2S4E.js → record-ZYL2FYSK.js} +3 -3
- package/dist-cli/chunks/{report-issue-7NMFP4HK.js → report-issue-TAI6DYZO.js} +2 -2
- package/dist-cli/chunks/reset-7YGYKQPQ.js +4 -0
- package/dist-cli/chunks/runtime-B4JO6QRI.js +4 -0
- package/dist-cli/chunks/{screenshot-command-67AECJFB.js → screenshot-command-7ANLODZY.js} +7 -7
- package/dist-cli/chunks/{screenshot-layers-ASWBYPJL.js → screenshot-layers-A7FYXSVU.js} +3 -3
- package/dist-cli/chunks/{screenshots-capture-PXA3HFQK.js → screenshots-capture-WT2ZY6CB.js} +2 -2
- package/dist-cli/chunks/serve-TG5WKA4V.js +44 -0
- package/dist-cli/chunks/{setup-7DWPMRSB.js → setup-PCWLD22V.js} +2 -2
- package/dist-cli/chunks/{skills-S3Y22TUA.js → skills-27ZHWCQS.js} +2 -2
- package/dist-cli/chunks/state-ZQVY46ZO.js +14 -0
- package/dist-cli/chunks/{storage-XUIMJWAJ.js → storage-OYC57C4X.js} +6 -6
- package/dist-cli/chunks/store-32HSPZHI.js +4 -0
- package/dist-cli/chunks/telemetry-PBJR7XHR.js +4 -0
- package/dist-cli/chunks/{timeline-TMPLQPSP.js → timeline-RAJJFQT6.js} +2 -2
- package/dist-cli/chunks/{upgrade-7HDSIM7K.js → upgrade-OEFNZRIP.js} +2 -2
- package/dist-cli/chunks/upload-LQJITLKK.js +4 -0
- package/dist-cli/chunks/version-NIXTY6PL.js +6 -0
- package/dist-cli/chunks/{web-DG3WBYD3.js → web-XBUTBVGR.js} +2 -2
- package/dist-cli/chunks/{what-happened-XFVUTZR7.js → what-happened-YNHRFUDX.js} +3 -3
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract.cjs +20 -0
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +67 -28
- package/dist-lib/host/bridge-host.cjs +140 -12
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +2815 -40
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +2549 -2061
- package/dist-lib/sdk.mjs +2543 -2061
- package/dist-lib/skills.cjs +480 -280
- package/dist-lib/vite.cjs +1 -1
- package/package.json +8 -2
- package/src/bridge-constants.ts +3 -4
- package/src/bridge-contract.ts +251 -0
- package/src/connect.ts +83 -0
- package/src/disk-cleanup.ts +30 -0
- package/src/home-paths.ts +81 -38
- package/src/host/bridge-host.ts +134 -6
- package/src/index.ts +27 -1
- package/src/sdk.ts +8 -0
- package/src/sim-client.ts +660 -0
- package/dist-cli/chunks/camera-VL73YIKP.js +0 -22
- package/dist-cli/chunks/chunk-4NPPOV2N.js +0 -5
- package/dist-cli/chunks/chunk-FSUYIVJ6.js +0 -9
- package/dist-cli/chunks/chunk-G2WW6L2C.js +0 -23
- package/dist-cli/chunks/chunk-KTHV3RUS.js +0 -26
- package/dist-cli/chunks/chunk-LF2ZVT7O.js +0 -6
- package/dist-cli/chunks/chunk-NFK7T35W.js +0 -4
- package/dist-cli/chunks/chunk-TIVZIMMW.js +0 -4
- package/dist-cli/chunks/cli-version-WWLPBDQ7.js +0 -4
- package/dist-cli/chunks/daemon-G2ME7NLB.js +0 -4
- package/dist-cli/chunks/drivers-LDECZGP2.js +0 -4
- package/dist-cli/chunks/flow-UEQNVTU7.js +0 -4
- package/dist-cli/chunks/help-T5FYSVGB.js +0 -4
- package/dist-cli/chunks/home-paths-GT3LFNOR.js +0 -4
- package/dist-cli/chunks/inspect-ZA6XF5LD.js +0 -4
- package/dist-cli/chunks/install-desktop-TIMUDHPL.js +0 -4
- package/dist-cli/chunks/runtime-XOAXMSTU.js +0 -4
- package/dist-cli/chunks/serve-BI2NBAXG.js +0 -44
- package/dist-cli/chunks/store-JTHEJLAZ.js +0 -4
- package/dist-cli/chunks/telemetry-ZYJGD2DB.js +0 -4
- package/dist-cli/chunks/upload-GMSZPWM6.js +0 -4
- package/dist-cli/chunks/version-HOCHZ37L.js +0 -6
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// sootsim desktop install — download and install the optional desktop GUI
|
|
2
|
+
//
|
|
3
|
+
// the CLI + daemon are the canonical sootsim surface; this command is only
|
|
4
|
+
// for users who want the Electron-based GUI companion on top of that.
|
|
5
|
+
//
|
|
6
|
+
// fetches the right platform artifact from https://contrast.dev/api/electron-release/
|
|
7
|
+
// and installs it into the OS-native location that findDesktopCompanion() looks for:
|
|
8
|
+
// macOS: ~/Applications/sootsim.app (or /Applications if writable)
|
|
9
|
+
// linux: ~/Applications/sootsim.AppImage
|
|
10
|
+
// win: spawns the NSIS installer
|
|
11
|
+
//
|
|
12
|
+
// post-install the electron-updater feed (same URL) handles upgrades inside the
|
|
13
|
+
// running app, so this command is a first-install bootstrap, not an update path.
|
|
14
|
+
|
|
15
|
+
import { spawn, spawnSync } from 'child_process'
|
|
16
|
+
import {
|
|
17
|
+
chmodSync,
|
|
18
|
+
copyFileSync,
|
|
19
|
+
createWriteStream,
|
|
20
|
+
existsSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
rmSync,
|
|
23
|
+
statSync,
|
|
24
|
+
unlinkSync,
|
|
25
|
+
} from 'fs'
|
|
26
|
+
import { tmpdir } from 'os'
|
|
27
|
+
import { dirname, join, resolve } from 'path'
|
|
28
|
+
import { findDesktopCompanion } from '../desktop-companion'
|
|
29
|
+
import { confirm } from '../prompt'
|
|
30
|
+
|
|
31
|
+
const DOWNLOAD_BASE = 'https://contrast.dev/api/electron-release'
|
|
32
|
+
|
|
33
|
+
type TargetPlatform = 'mac-arm64' | 'mac-x64' | 'win-x64' | 'linux-x64' | 'linux-arm64'
|
|
34
|
+
|
|
35
|
+
interface Target {
|
|
36
|
+
platform: TargetPlatform
|
|
37
|
+
filename: string
|
|
38
|
+
url: string
|
|
39
|
+
install: (downloaded: string) => Promise<string>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function detectTarget(): Target | null {
|
|
43
|
+
const arch = process.arch
|
|
44
|
+
if (process.platform === 'darwin') {
|
|
45
|
+
const platform: TargetPlatform = arch === 'arm64' ? 'mac-arm64' : 'mac-x64'
|
|
46
|
+
const filename = `sootsim-latest-${platform}.dmg`
|
|
47
|
+
return {
|
|
48
|
+
platform,
|
|
49
|
+
filename,
|
|
50
|
+
url: `${DOWNLOAD_BASE}/${filename}`,
|
|
51
|
+
install: installMacDmg,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (process.platform === 'linux') {
|
|
55
|
+
// ship arch-appropriate AppImages. on Apple Silicon Macs (the default
|
|
56
|
+
// host for orb/multipass VMs) the linux VM is arm64, so the x64
|
|
57
|
+
// AppImage fails at runtime with "Dynamic loader not found:
|
|
58
|
+
// /lib64/ld-linux-x86-64.so.2". detect the running arch and pick the
|
|
59
|
+
// matching artifact.
|
|
60
|
+
const platform: TargetPlatform = arch === 'arm64' ? 'linux-arm64' : 'linux-x64'
|
|
61
|
+
const filename = `sootsim-latest-${platform}.AppImage`
|
|
62
|
+
return {
|
|
63
|
+
platform,
|
|
64
|
+
filename,
|
|
65
|
+
url: `${DOWNLOAD_BASE}/${filename}`,
|
|
66
|
+
install: installLinuxAppImage,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (process.platform === 'win32') {
|
|
70
|
+
const filename = `sootsim-latest-win-x64.exe`
|
|
71
|
+
return {
|
|
72
|
+
platform: 'win-x64',
|
|
73
|
+
filename,
|
|
74
|
+
url: `${DOWNLOAD_BASE}/${filename}`,
|
|
75
|
+
install: installWindowsExe,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface DownloadOptions {
|
|
82
|
+
url: string
|
|
83
|
+
dest: string
|
|
84
|
+
onProgress?: (received: number, total: number | null) => void
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function download({ url, dest, onProgress }: DownloadOptions): Promise<void> {
|
|
88
|
+
const response = await fetch(url, { redirect: 'follow' })
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw new Error(`${response.status} ${response.statusText} (${url})`)
|
|
91
|
+
}
|
|
92
|
+
const contentLength = response.headers.get('content-length')
|
|
93
|
+
const total = contentLength ? Number(contentLength) : null
|
|
94
|
+
if (!response.body) {
|
|
95
|
+
throw new Error(`empty response body (${url})`)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
mkdirSync(dirname(dest), { recursive: true })
|
|
99
|
+
const file = createWriteStream(dest)
|
|
100
|
+
const reader = response.body.getReader()
|
|
101
|
+
let received = 0
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
while (true) {
|
|
105
|
+
const { done, value } = await reader.read()
|
|
106
|
+
if (done) break
|
|
107
|
+
file.write(Buffer.from(value))
|
|
108
|
+
received += value.byteLength
|
|
109
|
+
onProgress?.(received, total)
|
|
110
|
+
}
|
|
111
|
+
} finally {
|
|
112
|
+
await new Promise<void>((resolve, reject) => {
|
|
113
|
+
file.end((err?: Error | null) => (err ? reject(err) : resolve()))
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function renderProgress(received: number, total: number | null): string {
|
|
119
|
+
const mb = (received / (1024 * 1024)).toFixed(1)
|
|
120
|
+
if (!total) return ` ${mb} MB`
|
|
121
|
+
const pct = Math.min(100, Math.round((received / total) * 100))
|
|
122
|
+
const totalMb = (total / (1024 * 1024)).toFixed(1)
|
|
123
|
+
const barWidth = 24
|
|
124
|
+
const filled = Math.round((pct / 100) * barWidth)
|
|
125
|
+
const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled)
|
|
126
|
+
return ` ${bar} ${pct}% ${mb} / ${totalMb} MB`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function installMacDmg(dmgPath: string): Promise<string> {
|
|
130
|
+
const mountOutput = spawnSync(
|
|
131
|
+
'hdiutil',
|
|
132
|
+
['attach', '-nobrowse', '-readonly', dmgPath],
|
|
133
|
+
{
|
|
134
|
+
encoding: 'utf8',
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
if (mountOutput.status !== 0) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`hdiutil attach failed: ${mountOutput.stderr || mountOutput.stdout}`.trim(),
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
const mountLines = mountOutput.stdout.trim().split('\n')
|
|
143
|
+
const mountLine = mountLines[mountLines.length - 1]
|
|
144
|
+
const mountPoint = mountLine.split('\t').pop()?.trim()
|
|
145
|
+
if (!mountPoint || !existsSync(mountPoint)) {
|
|
146
|
+
throw new Error(`could not determine dmg mount point (output: ${mountOutput.stdout})`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const appSource = join(mountPoint, 'sootsim.app')
|
|
150
|
+
if (!existsSync(appSource)) {
|
|
151
|
+
spawnSync('hdiutil', ['detach', '-force', mountPoint])
|
|
152
|
+
throw new Error(`sootsim.app not found inside ${mountPoint}`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// prefer /Applications if writable, otherwise ~/Applications (same paths
|
|
156
|
+
// findDesktopCompanion checks, so no extra config needed post-install).
|
|
157
|
+
const systemApps = '/Applications'
|
|
158
|
+
const userApps = resolve(process.env.HOME || '', 'Applications')
|
|
159
|
+
let destRoot = systemApps
|
|
160
|
+
try {
|
|
161
|
+
const probe = join(systemApps, `.sootsim-write-probe-${process.pid}`)
|
|
162
|
+
spawnSync('touch', [probe])
|
|
163
|
+
if (existsSync(probe)) rmSync(probe, { force: true })
|
|
164
|
+
else destRoot = userApps
|
|
165
|
+
} catch {
|
|
166
|
+
destRoot = userApps
|
|
167
|
+
}
|
|
168
|
+
if (destRoot === userApps) {
|
|
169
|
+
mkdirSync(destRoot, { recursive: true })
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const appDest = join(destRoot, 'sootsim.app')
|
|
173
|
+
if (existsSync(appDest)) rmSync(appDest, { recursive: true, force: true })
|
|
174
|
+
|
|
175
|
+
const copy = spawnSync('cp', ['-R', appSource, appDest])
|
|
176
|
+
const detach = spawnSync('hdiutil', ['detach', '-force', mountPoint])
|
|
177
|
+
if (copy.status !== 0) {
|
|
178
|
+
throw new Error(`copy to ${appDest} failed: ${copy.stderr?.toString() || ''}`.trim())
|
|
179
|
+
}
|
|
180
|
+
if (detach.status !== 0) {
|
|
181
|
+
console.warn(
|
|
182
|
+
` warning: failed to unmount ${mountPoint} (${detach.stderr?.toString().trim() || 'unknown error'})`,
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// clear the quarantine attribute so gatekeeper doesn't nag on first launch;
|
|
187
|
+
// the app is already notarized so this is safe.
|
|
188
|
+
spawnSync('xattr', ['-dr', 'com.apple.quarantine', appDest])
|
|
189
|
+
|
|
190
|
+
return appDest
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function installLinuxAppImage(appImagePath: string): Promise<string> {
|
|
194
|
+
const destDir = resolve(process.env.HOME || '', 'Applications')
|
|
195
|
+
mkdirSync(destDir, { recursive: true })
|
|
196
|
+
const dest = join(destDir, 'sootsim.AppImage')
|
|
197
|
+
if (existsSync(dest)) rmSync(dest, { force: true })
|
|
198
|
+
// copyFile + unlink instead of rename: /tmp and ~/Applications are
|
|
199
|
+
// routinely on separate mounts in containers / VMs, where renameSync
|
|
200
|
+
// fails EXDEV. copy is portable across mounts and the unlink at the
|
|
201
|
+
// end keeps the staging dir clean.
|
|
202
|
+
copyFileSync(appImagePath, dest)
|
|
203
|
+
unlinkSync(appImagePath)
|
|
204
|
+
chmodSync(dest, 0o755)
|
|
205
|
+
return dest
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function installWindowsExe(exePath: string): Promise<string> {
|
|
209
|
+
// nsis installer prompts and self-elevates; run it and let it take over.
|
|
210
|
+
await new Promise<void>((resolvePromise, rejectPromise) => {
|
|
211
|
+
const child = spawn('cmd', ['/c', 'start', '""', '/wait', exePath], {
|
|
212
|
+
stdio: 'inherit',
|
|
213
|
+
})
|
|
214
|
+
child.once('error', rejectPromise)
|
|
215
|
+
child.once('exit', (code) => {
|
|
216
|
+
if (code === 0) resolvePromise()
|
|
217
|
+
else rejectPromise(new Error(`installer exited with code ${code}`))
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
return exePath
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function runInstallDesktop(args: string[]) {
|
|
224
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
225
|
+
console.log(`
|
|
226
|
+
rnxsim desktop install — download and install the optional desktop GUI
|
|
227
|
+
|
|
228
|
+
the CLI + daemon are the canonical rnx surface; the GUI is optional.
|
|
229
|
+
if you just want to drive a running metro/expo dev server from the
|
|
230
|
+
terminal, you don't need this.
|
|
231
|
+
|
|
232
|
+
usage:
|
|
233
|
+
rnxsim desktop install [options]
|
|
234
|
+
|
|
235
|
+
options:
|
|
236
|
+
-y, --yes skip confirmation and install immediately
|
|
237
|
+
--force reinstall even if the companion is already present
|
|
238
|
+
|
|
239
|
+
examples:
|
|
240
|
+
rnxsim desktop install
|
|
241
|
+
rnxsim desktop install --yes
|
|
242
|
+
`)
|
|
243
|
+
process.exit(0)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const skipConfirm =
|
|
247
|
+
args.includes('--yes') ||
|
|
248
|
+
args.includes('-y') ||
|
|
249
|
+
process.env.SOOTSIM_NO_PROMPT === '1' ||
|
|
250
|
+
process.env.CI === '1' ||
|
|
251
|
+
!process.stdin.isTTY
|
|
252
|
+
const force = args.includes('--force')
|
|
253
|
+
|
|
254
|
+
const existing = findDesktopCompanion()
|
|
255
|
+
if (existing && !force) {
|
|
256
|
+
console.log(` rnxsim desktop already installed at: ${existing.path}`)
|
|
257
|
+
console.log(' pass --force to reinstall.')
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const target = detectTarget()
|
|
262
|
+
if (!target) {
|
|
263
|
+
console.error(` no desktop build available for ${process.platform}/${process.arch}.`)
|
|
264
|
+
console.error(
|
|
265
|
+
' supported: darwin-arm64, darwin-x64, linux-x64, linux-arm64, win32-x64',
|
|
266
|
+
)
|
|
267
|
+
process.exit(1)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
console.log(` platform: ${target.platform}`)
|
|
271
|
+
console.log(` download: ${target.url}`)
|
|
272
|
+
console.log()
|
|
273
|
+
|
|
274
|
+
if (!skipConfirm) {
|
|
275
|
+
const ok = await confirm('download and install now?', true)
|
|
276
|
+
if (!ok) {
|
|
277
|
+
console.log(' cancelled.')
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
console.log()
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const workDir = join(tmpdir(), `sootsim-install-${Date.now()}`)
|
|
284
|
+
const downloadPath = join(workDir, target.filename)
|
|
285
|
+
|
|
286
|
+
let lastDraw = 0
|
|
287
|
+
process.stdout.write(` downloading ${target.filename}...\n`)
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
await download({
|
|
291
|
+
url: target.url,
|
|
292
|
+
dest: downloadPath,
|
|
293
|
+
onProgress: (received, total) => {
|
|
294
|
+
const now = Date.now()
|
|
295
|
+
if (now - lastDraw < 100 && received < (total ?? Infinity)) return
|
|
296
|
+
lastDraw = now
|
|
297
|
+
if (process.stdout.isTTY) {
|
|
298
|
+
process.stdout.write(`\r\x1b[2K${renderProgress(received, total)}`)
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
})
|
|
302
|
+
} catch (err) {
|
|
303
|
+
rmSync(workDir, { recursive: true, force: true })
|
|
304
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
305
|
+
console.error(`\n download failed: ${message}`)
|
|
306
|
+
console.error(
|
|
307
|
+
` the ${target.platform} desktop build may not be published yet, or your`,
|
|
308
|
+
)
|
|
309
|
+
console.error(
|
|
310
|
+
' network blocked the request. check your connection and try again later;',
|
|
311
|
+
)
|
|
312
|
+
console.error(' the CLI + daemon work without the desktop app (run: rnxsim --help).')
|
|
313
|
+
process.exit(1)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (process.stdout.isTTY) process.stdout.write('\n')
|
|
317
|
+
const size = statSync(downloadPath).size
|
|
318
|
+
console.log(` downloaded ${(size / (1024 * 1024)).toFixed(1)} MB`)
|
|
319
|
+
console.log()
|
|
320
|
+
console.log(` installing...`)
|
|
321
|
+
|
|
322
|
+
const { trackCliEvent, flushCliTelemetry } = await import('../telemetry')
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const installedAt = await target.install(downloadPath)
|
|
326
|
+
trackCliEvent({
|
|
327
|
+
event: 'cli_install_desktop_succeeded',
|
|
328
|
+
properties: { platform: target.platform, size_bytes: size },
|
|
329
|
+
})
|
|
330
|
+
console.log(` installed: ${installedAt}`)
|
|
331
|
+
} catch (err) {
|
|
332
|
+
trackCliEvent({
|
|
333
|
+
event: 'cli_install_desktop_failed',
|
|
334
|
+
properties: {
|
|
335
|
+
platform: target.platform,
|
|
336
|
+
error: err instanceof Error ? err.message : String(err),
|
|
337
|
+
},
|
|
338
|
+
})
|
|
339
|
+
await flushCliTelemetry()
|
|
340
|
+
console.error(` install failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
341
|
+
console.error(` keeping download at ${downloadPath} for manual install.`)
|
|
342
|
+
process.exit(1)
|
|
343
|
+
} finally {
|
|
344
|
+
rmSync(workDir, { recursive: true, force: true })
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
console.log()
|
|
348
|
+
console.log(' next steps:')
|
|
349
|
+
console.log(' rnxsim desktop launch the desktop app')
|
|
350
|
+
console.log(' rnxsim open <target> open a demo or bundle in it')
|
|
351
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { exec } from 'node:child_process'
|
|
2
|
+
import { randomBytes } from 'node:crypto'
|
|
3
|
+
import http from 'node:http'
|
|
4
|
+
import {
|
|
5
|
+
refreshSharedDesktopAuthSession,
|
|
6
|
+
writeSharedDesktopAuthSession,
|
|
7
|
+
} from '../../src/auth/shared-session'
|
|
8
|
+
import { rnxPublicBrand } from '../../src/public-brand'
|
|
9
|
+
|
|
10
|
+
const REMOTE_DEFAULT_ORIGIN = process.env.SOOTSIM_AUTH_ORIGIN || 'https://contrast.dev'
|
|
11
|
+
const LOCAL_DEFAULT_ORIGIN = 'https://contrast.localhost:3000'
|
|
12
|
+
|
|
13
|
+
type LoginResult =
|
|
14
|
+
| { ok: true; token: string; userId?: string; email?: string }
|
|
15
|
+
| { ok: false; error: string }
|
|
16
|
+
|
|
17
|
+
async function resolveDefaultAuthOrigin(explicitOrigin?: string) {
|
|
18
|
+
if (explicitOrigin) return explicitOrigin
|
|
19
|
+
if (process.env.SOOTSIM_AUTH_ORIGIN) return process.env.SOOTSIM_AUTH_ORIGIN
|
|
20
|
+
if (process.env.SOOTSIM_UPLOAD_ORIGIN) return process.env.SOOTSIM_UPLOAD_ORIGIN
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetch(`${LOCAL_DEFAULT_ORIGIN}/api/auth/me`)
|
|
24
|
+
if (res.ok) return LOCAL_DEFAULT_ORIGIN
|
|
25
|
+
} catch {}
|
|
26
|
+
|
|
27
|
+
return REMOTE_DEFAULT_ORIGIN
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function printHelp() {
|
|
31
|
+
console.log(`
|
|
32
|
+
rnxsim login — sign in so uploads can attach to your account
|
|
33
|
+
|
|
34
|
+
usage:
|
|
35
|
+
rnxsim login [--origin <url>]
|
|
36
|
+
|
|
37
|
+
options:
|
|
38
|
+
--origin <url> auth host (default: auto)
|
|
39
|
+
prefers ${LOCAL_DEFAULT_ORIGIN} when available, otherwise
|
|
40
|
+
falls back to ${REMOTE_DEFAULT_ORIGIN}
|
|
41
|
+
override with SOOTSIM_AUTH_ORIGIN env var
|
|
42
|
+
-h, --help
|
|
43
|
+
`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function extractStringFlag(args: string[], name: string): string | undefined {
|
|
47
|
+
const idx = args.findIndex((a) => a === name)
|
|
48
|
+
if (idx < 0) return undefined
|
|
49
|
+
const value = args[idx + 1]
|
|
50
|
+
args.splice(idx, 2)
|
|
51
|
+
return value
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function openBrowser(url: string) {
|
|
55
|
+
const command =
|
|
56
|
+
process.platform === 'darwin'
|
|
57
|
+
? `open "${url}"`
|
|
58
|
+
: process.platform === 'win32'
|
|
59
|
+
? `start "" "${url}"`
|
|
60
|
+
: `xdg-open "${url}"`
|
|
61
|
+
|
|
62
|
+
exec(command, (err) => {
|
|
63
|
+
if (!err) return
|
|
64
|
+
console.log(' could not open browser automatically.')
|
|
65
|
+
console.log(` open this URL manually:\n ${url}\n`)
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function renderCallbackPage(body: string) {
|
|
70
|
+
return `<!doctype html>
|
|
71
|
+
<html>
|
|
72
|
+
<body style="font-family:system-ui;background:#000;color:#fff;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
|
|
73
|
+
<div style="max-width:480px;text-align:center">
|
|
74
|
+
<h1 style="font-size:28px;margin:0 0 12px">${rnxPublicBrand.name}</h1>
|
|
75
|
+
<p style="color:#ccc">${body}</p>
|
|
76
|
+
</div>
|
|
77
|
+
</body>
|
|
78
|
+
</html>`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function runDevLogin(origin: string): Promise<LoginResult> {
|
|
82
|
+
try {
|
|
83
|
+
const res = await fetch(`${origin.replace(/\/$/, '')}/api/dev-login`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
})
|
|
86
|
+
if (res.status === 403) {
|
|
87
|
+
return { ok: false, error: 'local dev-login unavailable' }
|
|
88
|
+
}
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
const text = await res.text().catch(() => '')
|
|
91
|
+
return { ok: false, error: `dev-login ${res.status}: ${text}` }
|
|
92
|
+
}
|
|
93
|
+
const body = (await res.json()) as {
|
|
94
|
+
ok?: boolean
|
|
95
|
+
token?: string
|
|
96
|
+
email?: string
|
|
97
|
+
user?: { id?: string }
|
|
98
|
+
}
|
|
99
|
+
if (!body.token) {
|
|
100
|
+
return { ok: false, error: 'dev-login response missing token' }
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
ok: true,
|
|
104
|
+
token: body.token,
|
|
105
|
+
userId: body.user?.id,
|
|
106
|
+
email: body.email,
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
error: err instanceof Error ? err.message : 'dev-login fetch failed',
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isLocalOrigin(origin: string): boolean {
|
|
117
|
+
try {
|
|
118
|
+
const hostname = new URL(origin).hostname.replace(/^\[|\]$/g, '').toLowerCase()
|
|
119
|
+
if (hostname === 'localhost') return true
|
|
120
|
+
if (/^127(?:\.\d{1,3}){3}$/.test(hostname)) return true
|
|
121
|
+
if (hostname === '::1' || hostname === '0.0.0.0') return true
|
|
122
|
+
if (hostname.endsWith('.local')) return true
|
|
123
|
+
if (hostname.endsWith('.localhost')) return true
|
|
124
|
+
// VM↔host bridge aliases — orb, docker, lima, multipass all forward the
|
|
125
|
+
// host on a well-known name (IPv4 or IPv6) so a VM-internal `sootsim login`
|
|
126
|
+
// can still take the dev-login fast path against the host's Contrast dev
|
|
127
|
+
// server. without this the CLI falls through to a browser callback that
|
|
128
|
+
// is unreachable from a headless VM.
|
|
129
|
+
if (hostname === 'host.containers.internal') return true
|
|
130
|
+
if (hostname === 'host.docker.internal') return true
|
|
131
|
+
if (hostname === 'host.orb.internal') return true
|
|
132
|
+
if (hostname === 'host.lima.internal') return true
|
|
133
|
+
// orb's IPv6 form of host.orb.internal: fd07:b51a:cc66:f0::fe (always
|
|
134
|
+
// this fixed address; it's a stable orb ULA assignment, see orb docs).
|
|
135
|
+
if (hostname === 'fd07:b51a:cc66:f0::fe') return true
|
|
136
|
+
// lima's qemu (slirp) backend does NOT resolve host.lima.internal — the
|
|
137
|
+
// host is the fixed slirp gateway 192.168.5.2. a lima/qemu user therefore
|
|
138
|
+
// targets the raw gateway IP, which would otherwise miss the dev-login fast
|
|
139
|
+
// path and fall through to the headless-broken browser callback. recognize
|
|
140
|
+
// it (and multipass's default bridge .1 gateways) so dev-login still fires.
|
|
141
|
+
// safe: no-device dev-login requests are accepted only in local dev.
|
|
142
|
+
if (hostname === '192.168.5.2') return true
|
|
143
|
+
if (/^192\.168\.(?:64|139)\.1$/.test(hostname)) return true
|
|
144
|
+
return false
|
|
145
|
+
} catch {
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function runLogin(args: string[]) {
|
|
151
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
152
|
+
printHelp()
|
|
153
|
+
process.exit(0)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const mutableArgs = [...args]
|
|
157
|
+
const origin = await resolveDefaultAuthOrigin(
|
|
158
|
+
extractStringFlag(mutableArgs, '--origin'),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
// in dev (localhost), skip the browser and sign in as the shared dev
|
|
162
|
+
// user via POST /api/dev-login. the no-device contract is local-only, so this
|
|
163
|
+
// path is safe to take unconditionally against local hosts.
|
|
164
|
+
if (isLocalOrigin(origin)) {
|
|
165
|
+
console.log(` detected local origin (${origin}) — signing in as dev user`)
|
|
166
|
+
const devResult = await runDevLogin(origin)
|
|
167
|
+
if (devResult.ok) {
|
|
168
|
+
writeSharedDesktopAuthSession({
|
|
169
|
+
token: devResult.token,
|
|
170
|
+
user: devResult.userId ? { id: devResult.userId, email: devResult.email } : null,
|
|
171
|
+
origin,
|
|
172
|
+
source: 'cli',
|
|
173
|
+
})
|
|
174
|
+
const refreshed = await refreshSharedDesktopAuthSession(origin)
|
|
175
|
+
const label =
|
|
176
|
+
refreshed?.user?.email ||
|
|
177
|
+
devResult.email ||
|
|
178
|
+
refreshed?.user?.id ||
|
|
179
|
+
devResult.userId
|
|
180
|
+
const { trackCliEvent, flushCliTelemetry } = await import('../telemetry')
|
|
181
|
+
trackCliEvent({
|
|
182
|
+
event: 'cli_login_succeeded',
|
|
183
|
+
identity: { userId: devResult.userId ?? refreshed?.user?.id ?? null },
|
|
184
|
+
properties: { origin, mode: 'dev' },
|
|
185
|
+
})
|
|
186
|
+
await flushCliTelemetry()
|
|
187
|
+
console.log(` signed in${label ? ` as ${label}` : ''} (dev)`)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
console.log(` dev-login not available (${devResult.error}); falling back to browser`)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const state = randomBytes(16).toString('hex')
|
|
194
|
+
|
|
195
|
+
// flush a start event BEFORE the browser wait: queued telemetry only drains
|
|
196
|
+
// on process exit, so a login that hangs (or gets ctrl+c'd) would otherwise
|
|
197
|
+
// leave zero trace. first-time-user hangs were invisible in posthog.
|
|
198
|
+
{
|
|
199
|
+
const { trackCliEvent, flushCliTelemetry } = await import('../telemetry')
|
|
200
|
+
trackCliEvent({ event: 'cli_login_started', properties: { origin } })
|
|
201
|
+
await flushCliTelemetry()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// the browser round-trip needs human action (github's authorize screen on
|
|
205
|
+
// first sign-in), so the wait is generous, but it must not be infinite: a
|
|
206
|
+
// closed tab, denied consent, or blocked popup used to hang the CLI forever.
|
|
207
|
+
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000
|
|
208
|
+
|
|
209
|
+
const result = await new Promise<LoginResult>((resolve) => {
|
|
210
|
+
let timer: ReturnType<typeof setTimeout>
|
|
211
|
+
let settled = false
|
|
212
|
+
const finish = (r: LoginResult) => {
|
|
213
|
+
if (settled) return
|
|
214
|
+
settled = true
|
|
215
|
+
clearTimeout(timer)
|
|
216
|
+
resolve(r)
|
|
217
|
+
server.close()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const server = http.createServer((req, res) => {
|
|
221
|
+
try {
|
|
222
|
+
const url = new URL(req.url || '/', 'http://127.0.0.1')
|
|
223
|
+
const returnedState = url.searchParams.get('state')
|
|
224
|
+
const token = url.searchParams.get('token')
|
|
225
|
+
const email = url.searchParams.get('email') || undefined
|
|
226
|
+
const userId = url.searchParams.get('userId') || undefined
|
|
227
|
+
const error = url.searchParams.get('error')
|
|
228
|
+
|
|
229
|
+
// stray localhost traffic (port scanners, favicon fetches, browser
|
|
230
|
+
// prefetch) must not abort the login. only a request carrying our
|
|
231
|
+
// state is the real callback; rfc 8252 loopback servers ignore the
|
|
232
|
+
// rest. aborting here used to kill the login mid-wait.
|
|
233
|
+
if (returnedState !== state) {
|
|
234
|
+
res.statusCode = 404
|
|
235
|
+
res.end('not found')
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
res.setHeader('content-type', 'text/html; charset=utf-8')
|
|
240
|
+
|
|
241
|
+
if (error) {
|
|
242
|
+
res.end(renderCallbackPage(`sign-in failed: ${error}`))
|
|
243
|
+
finish({ ok: false, error })
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!token) {
|
|
248
|
+
res.end(
|
|
249
|
+
renderCallbackPage('missing token. close this tab and retry `rnxsim login`.'),
|
|
250
|
+
)
|
|
251
|
+
finish({ ok: false, error: 'missing token' })
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
res.end(renderCallbackPage('sign-in complete. you can return to the terminal.'))
|
|
256
|
+
finish({ ok: true, token, email, userId })
|
|
257
|
+
} catch (err) {
|
|
258
|
+
res.statusCode = 500
|
|
259
|
+
res.end(renderCallbackPage('sign-in callback failed. retry `rnxsim login`.'))
|
|
260
|
+
finish({
|
|
261
|
+
ok: false,
|
|
262
|
+
error: err instanceof Error ? err.message : 'callback failed',
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
timer = setTimeout(() => {
|
|
268
|
+
finish({
|
|
269
|
+
ok: false,
|
|
270
|
+
error:
|
|
271
|
+
'timed out waiting for the browser sign-in (5 minutes). rerun `rnxsim login` and complete the github authorize screen if it appears.',
|
|
272
|
+
})
|
|
273
|
+
}, LOGIN_TIMEOUT_MS)
|
|
274
|
+
|
|
275
|
+
server.listen(0, '127.0.0.1', () => {
|
|
276
|
+
const address = server.address()
|
|
277
|
+
if (!address || typeof address === 'string') {
|
|
278
|
+
finish({ ok: false, error: 'failed to bind callback server' })
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const url = new URL(`${origin.replace(/\/$/, '')}/auth/sootsim-cli`)
|
|
283
|
+
url.searchParams.set('state', state)
|
|
284
|
+
url.searchParams.set('port', String(address.port))
|
|
285
|
+
url.searchParams.set('contrast', origin)
|
|
286
|
+
|
|
287
|
+
console.log(' opening browser for rnxsim login...')
|
|
288
|
+
console.log(
|
|
289
|
+
' first time here? github will show an authorize screen. approve it to continue.',
|
|
290
|
+
)
|
|
291
|
+
console.log(` waiting up to 5 minutes for the browser (ctrl+c to cancel)`)
|
|
292
|
+
console.log(` if nothing opens, visit:\n ${url.toString()}\n`)
|
|
293
|
+
openBrowser(url.toString())
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
if (!result.ok) {
|
|
298
|
+
const { trackCliEvent, flushCliTelemetry } = await import('../telemetry')
|
|
299
|
+
trackCliEvent({
|
|
300
|
+
event: 'cli_login_failed',
|
|
301
|
+
properties: { origin, error: result.error },
|
|
302
|
+
})
|
|
303
|
+
await flushCliTelemetry()
|
|
304
|
+
console.error(` login failed: ${result.error}`)
|
|
305
|
+
process.exit(1)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
writeSharedDesktopAuthSession({
|
|
309
|
+
token: result.token,
|
|
310
|
+
user: result.userId
|
|
311
|
+
? {
|
|
312
|
+
id: result.userId,
|
|
313
|
+
email: result.email,
|
|
314
|
+
}
|
|
315
|
+
: null,
|
|
316
|
+
origin,
|
|
317
|
+
source: 'cli',
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const refreshed = await refreshSharedDesktopAuthSession(origin)
|
|
321
|
+
const label =
|
|
322
|
+
refreshed?.user?.email || result.email || refreshed?.user?.id || result.userId
|
|
323
|
+
const { trackCliEvent, flushCliTelemetry } = await import('../telemetry')
|
|
324
|
+
trackCliEvent({
|
|
325
|
+
event: 'cli_login_succeeded',
|
|
326
|
+
identity: { userId: result.userId ?? refreshed?.user?.id ?? null },
|
|
327
|
+
properties: { origin },
|
|
328
|
+
})
|
|
329
|
+
await flushCliTelemetry()
|
|
330
|
+
console.log(` signed in${label ? ` as ${label}` : ''}`)
|
|
331
|
+
}
|