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,231 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
export const EXPO_APP_CONFIG_NAMES = [
|
|
5
|
+
'app.config.js',
|
|
6
|
+
'app.config.ts',
|
|
7
|
+
'app.config.cjs',
|
|
8
|
+
'app.config.mjs',
|
|
9
|
+
'app.json',
|
|
10
|
+
] as const
|
|
11
|
+
|
|
12
|
+
export const SOOTSIM_APP_CONFIG_NAMES = [
|
|
13
|
+
'sootsim.config.ts',
|
|
14
|
+
'sootsim.config.js',
|
|
15
|
+
'sootsim.config.mjs',
|
|
16
|
+
'sootsim.config.cjs',
|
|
17
|
+
] as const
|
|
18
|
+
|
|
19
|
+
const SKIP_DIR_NAMES = new Set([
|
|
20
|
+
'node_modules',
|
|
21
|
+
'artifacts',
|
|
22
|
+
'dist',
|
|
23
|
+
'build',
|
|
24
|
+
'out',
|
|
25
|
+
'coverage',
|
|
26
|
+
'tmp',
|
|
27
|
+
'public',
|
|
28
|
+
'ios',
|
|
29
|
+
'android',
|
|
30
|
+
'macos',
|
|
31
|
+
'windows',
|
|
32
|
+
'__fixtures__',
|
|
33
|
+
'__tests__',
|
|
34
|
+
'fixtures',
|
|
35
|
+
'fixture',
|
|
36
|
+
'examples',
|
|
37
|
+
'example',
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
const MAX_SCANNED_DIRECTORIES = 256
|
|
41
|
+
const MAX_SCAN_DEPTH = 4
|
|
42
|
+
|
|
43
|
+
export interface ResolvedAppProject {
|
|
44
|
+
appDir: string
|
|
45
|
+
configPath: string | null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function firstNamedFile(dir: string, names: readonly string[]): string | null {
|
|
49
|
+
for (const name of names) {
|
|
50
|
+
const candidate = path.join(dir, name)
|
|
51
|
+
try {
|
|
52
|
+
if (fs.statSync(candidate).isFile()) return candidate
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function findExpoAppConfig(dir: string): string | null {
|
|
59
|
+
return firstNamedFile(dir, EXPO_APP_CONFIG_NAMES)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function findSootSimAppConfig(dir: string): string | null {
|
|
63
|
+
return firstNamedFile(dir, SOOTSIM_APP_CONFIG_NAMES)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readWorkspacePatterns(rootDir: string): string[] {
|
|
67
|
+
try {
|
|
68
|
+
const value = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8'))
|
|
69
|
+
if (!value || typeof value !== 'object') return []
|
|
70
|
+
const workspaces = Reflect.get(value, 'workspaces')
|
|
71
|
+
if (Array.isArray(workspaces)) {
|
|
72
|
+
return workspaces.filter((entry): entry is string => typeof entry === 'string')
|
|
73
|
+
}
|
|
74
|
+
if (!workspaces || typeof workspaces !== 'object') return []
|
|
75
|
+
const packages = Reflect.get(workspaces, 'packages')
|
|
76
|
+
return Array.isArray(packages)
|
|
77
|
+
? packages.filter((entry): entry is string => typeof entry === 'string')
|
|
78
|
+
: []
|
|
79
|
+
} catch {
|
|
80
|
+
return []
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface DirectoryScan {
|
|
85
|
+
directories: string[]
|
|
86
|
+
complete: boolean
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function workspaceDirectories(rootDir: string): DirectoryScan {
|
|
90
|
+
const directories = new Set<string>()
|
|
91
|
+
let expanded = 0
|
|
92
|
+
let complete = true
|
|
93
|
+
for (const pattern of readWorkspacePatterns(rootDir)) {
|
|
94
|
+
if (expanded >= MAX_SCANNED_DIRECTORIES) {
|
|
95
|
+
complete = false
|
|
96
|
+
break
|
|
97
|
+
}
|
|
98
|
+
if (!pattern || path.isAbsolute(pattern)) continue
|
|
99
|
+
const segments = pattern.split(/[\\/]+/).filter(Boolean)
|
|
100
|
+
if (segments.length === 0 || segments.length > MAX_SCAN_DEPTH) continue
|
|
101
|
+
if (segments.some((segment) => segment === '..' || segment === '**')) continue
|
|
102
|
+
|
|
103
|
+
let current = [rootDir]
|
|
104
|
+
for (const segment of segments) {
|
|
105
|
+
const next: string[] = []
|
|
106
|
+
for (const parent of current) {
|
|
107
|
+
if (expanded >= MAX_SCANNED_DIRECTORIES) {
|
|
108
|
+
complete = false
|
|
109
|
+
break
|
|
110
|
+
}
|
|
111
|
+
expanded++
|
|
112
|
+
if (segment === '*') {
|
|
113
|
+
let entries: fs.Dirent[]
|
|
114
|
+
try {
|
|
115
|
+
entries = fs.readdirSync(parent, { withFileTypes: true })
|
|
116
|
+
} catch {
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue
|
|
121
|
+
if (entry.name.startsWith('.') || SKIP_DIR_NAMES.has(entry.name)) continue
|
|
122
|
+
next.push(path.join(parent, entry.name))
|
|
123
|
+
}
|
|
124
|
+
} else if (!segment.includes('*')) {
|
|
125
|
+
const candidate = path.join(parent, segment)
|
|
126
|
+
try {
|
|
127
|
+
const stat = fs.lstatSync(candidate)
|
|
128
|
+
if (stat.isDirectory() && !stat.isSymbolicLink()) next.push(candidate)
|
|
129
|
+
} catch {}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
current = next
|
|
133
|
+
if (current.length > MAX_SCANNED_DIRECTORIES) {
|
|
134
|
+
current.length = MAX_SCANNED_DIRECTORIES
|
|
135
|
+
complete = false
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
for (const directory of current) directories.add(directory)
|
|
139
|
+
}
|
|
140
|
+
return { directories: [...directories], complete }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function boundedConfigDirectories(rootDir: string): DirectoryScan {
|
|
144
|
+
const queue: Array<{ dir: string; depth: number }> = [{ dir: rootDir, depth: 0 }]
|
|
145
|
+
const matches: string[] = []
|
|
146
|
+
let visited = 0
|
|
147
|
+
let nextIndex = 0
|
|
148
|
+
let complete = true
|
|
149
|
+
|
|
150
|
+
while (nextIndex < queue.length && visited < MAX_SCANNED_DIRECTORIES) {
|
|
151
|
+
const current = queue[nextIndex++]
|
|
152
|
+
if (!current) break
|
|
153
|
+
visited++
|
|
154
|
+
if (current.depth > 0 && findExpoAppConfig(current.dir)) {
|
|
155
|
+
matches.push(current.dir)
|
|
156
|
+
if (matches.length > 1) return { directories: matches, complete: true }
|
|
157
|
+
}
|
|
158
|
+
if (current.depth >= MAX_SCAN_DEPTH) continue
|
|
159
|
+
|
|
160
|
+
let entries: fs.Dirent[]
|
|
161
|
+
try {
|
|
162
|
+
entries = fs.readdirSync(current.dir, { withFileTypes: true })
|
|
163
|
+
} catch {
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue
|
|
168
|
+
if (entry.name.startsWith('.') || SKIP_DIR_NAMES.has(entry.name)) continue
|
|
169
|
+
if (queue.length >= MAX_SCANNED_DIRECTORIES) {
|
|
170
|
+
complete = false
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
queue.push({ dir: path.join(current.dir, entry.name), depth: current.depth + 1 })
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { directories: matches, complete: complete && nextIndex === queue.length }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function resolveAppProject(
|
|
181
|
+
startDir: string,
|
|
182
|
+
opts: { trustStart?: boolean } = {},
|
|
183
|
+
): ResolvedAppProject | null {
|
|
184
|
+
const rootDir = path.resolve(startDir)
|
|
185
|
+
const rootConfigPath = findSootSimAppConfig(rootDir)
|
|
186
|
+
if (findExpoAppConfig(rootDir) || (opts.trustStart && fs.existsSync(rootDir))) {
|
|
187
|
+
let configPath = rootConfigPath
|
|
188
|
+
if (!configPath && opts.trustStart) {
|
|
189
|
+
let parent = path.dirname(rootDir)
|
|
190
|
+
for (
|
|
191
|
+
let depth = 0;
|
|
192
|
+
depth < MAX_SCAN_DEPTH && parent !== path.dirname(parent);
|
|
193
|
+
depth++
|
|
194
|
+
) {
|
|
195
|
+
configPath = findSootSimAppConfig(parent)
|
|
196
|
+
if (configPath) break
|
|
197
|
+
parent = path.dirname(parent)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { appDir: rootDir, configPath }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const workspaceScan = workspaceDirectories(rootDir)
|
|
204
|
+
const workspaceApps = workspaceScan.directories.filter((directory) =>
|
|
205
|
+
findExpoAppConfig(directory),
|
|
206
|
+
)
|
|
207
|
+
if (workspaceApps.length > 1) return null
|
|
208
|
+
if (!workspaceScan.complete) return null
|
|
209
|
+
if (workspaceApps.length === 1) {
|
|
210
|
+
const workspaceAppDir = workspaceApps[0]
|
|
211
|
+
return {
|
|
212
|
+
appDir: workspaceAppDir,
|
|
213
|
+
configPath: rootConfigPath ?? findSootSimAppConfig(workspaceAppDir),
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const fallbackScan = boundedConfigDirectories(rootDir)
|
|
218
|
+
const scannedApps = fallbackScan.directories
|
|
219
|
+
if (scannedApps.length > 1) return null
|
|
220
|
+
if (!fallbackScan.complete) return null
|
|
221
|
+
if (scannedApps.length === 1) {
|
|
222
|
+
const scannedAppDir = scannedApps[0]
|
|
223
|
+
return {
|
|
224
|
+
appDir: scannedAppDir,
|
|
225
|
+
configPath: rootConfigPath ?? findSootSimAppConfig(scannedAppDir),
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (rootConfigPath) return { appDir: rootDir, configPath: rootConfigPath }
|
|
230
|
+
return null
|
|
231
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import {
|
|
6
|
+
configPluginSplashFromExpoConfig,
|
|
7
|
+
type ConfigPluginSplashSpec,
|
|
8
|
+
} from 'sootsim-engine/engine/config-plugin-splash'
|
|
9
|
+
import { resolveAppProject } from './app-project'
|
|
10
|
+
|
|
11
|
+
const require = createRequire(import.meta.url)
|
|
12
|
+
|
|
13
|
+
// the filesystem scan and the engine's metro-manifest scan share the same
|
|
14
|
+
// parser (config-plugin-splash.ts) and produce the same resolved spec.
|
|
15
|
+
export type DiscoveredAppSplashSpec = ConfigPluginSplashSpec
|
|
16
|
+
|
|
17
|
+
type ExpoConfigGetter = (dir: string, opts: Record<string, unknown>) => { exp?: unknown }
|
|
18
|
+
|
|
19
|
+
function localOriginOf(value: string): string | null {
|
|
20
|
+
try {
|
|
21
|
+
const url = new URL(value)
|
|
22
|
+
const host = url.hostname.replace(/^\[|\]$/g, '').toLowerCase()
|
|
23
|
+
if (
|
|
24
|
+
host === 'localhost' ||
|
|
25
|
+
host.endsWith('.localhost') ||
|
|
26
|
+
host === '0.0.0.0' ||
|
|
27
|
+
host === '::1' ||
|
|
28
|
+
/^127(?:\.\d{1,3}){3}$/.test(host)
|
|
29
|
+
) {
|
|
30
|
+
return url.origin
|
|
31
|
+
}
|
|
32
|
+
} catch {}
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hasPackageJson(dir: string): boolean {
|
|
37
|
+
try {
|
|
38
|
+
return fs.statSync(path.join(dir, 'package.json')).isFile()
|
|
39
|
+
} catch {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// discover the app's configured splash by evaluating its expo config on the
|
|
45
|
+
// filesystem (mirrors discoverNativeLinkedAppFonts exactly). returns null when
|
|
46
|
+
// no splash is configured, no app config is found, or the bundle isn't local.
|
|
47
|
+
export function discoverConfigPluginAppSplash(opts: {
|
|
48
|
+
bundleUrl: string
|
|
49
|
+
repoDir?: string
|
|
50
|
+
exactProjectDir?: boolean
|
|
51
|
+
platform?: string
|
|
52
|
+
}): DiscoveredAppSplashSpec | null {
|
|
53
|
+
const metroOrigin = localOriginOf(opts.bundleUrl)
|
|
54
|
+
if (!metroOrigin) return null
|
|
55
|
+
|
|
56
|
+
const explicit = opts.repoDir || process.env.CONTRAST_REPO_DIR
|
|
57
|
+
const startDir = explicit ? path.resolve(explicit) : process.cwd()
|
|
58
|
+
|
|
59
|
+
if (!explicit && !hasPackageJson(startDir)) return null
|
|
60
|
+
|
|
61
|
+
const project = resolveAppProject(startDir, { trustStart: opts.exactProjectDir })
|
|
62
|
+
const selectedAppDir = project?.appDir ?? null
|
|
63
|
+
if (!selectedAppDir) return null
|
|
64
|
+
|
|
65
|
+
const platform = (opts.platform || process.env.CONTRAST_PLATFORM || 'ios').toLowerCase()
|
|
66
|
+
|
|
67
|
+
let getConfig: ExpoConfigGetter | null = null
|
|
68
|
+
try {
|
|
69
|
+
const modulePath = require.resolve('@expo/config', {
|
|
70
|
+
paths: [selectedAppDir, startDir],
|
|
71
|
+
})
|
|
72
|
+
const mod = require(modulePath) as { getConfig?: unknown }
|
|
73
|
+
if (typeof mod.getConfig === 'function') {
|
|
74
|
+
getConfig = mod.getConfig as ExpoConfigGetter
|
|
75
|
+
}
|
|
76
|
+
} catch {}
|
|
77
|
+
if (!getConfig) return null
|
|
78
|
+
|
|
79
|
+
let exp: unknown
|
|
80
|
+
try {
|
|
81
|
+
const result = getConfig(selectedAppDir, { skipSDKVersionRequirement: true })
|
|
82
|
+
if (result?.exp && typeof result.exp === 'object') exp = result.exp
|
|
83
|
+
} catch {}
|
|
84
|
+
if (!exp || typeof exp !== 'object') return null
|
|
85
|
+
|
|
86
|
+
return configPluginSplashFromExpoConfig(
|
|
87
|
+
exp,
|
|
88
|
+
platform === 'android' ? 'android' : 'ios',
|
|
89
|
+
metroOrigin,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// encode a resolved splash spec into the `?appSplash=` engine-URL wire value
|
|
94
|
+
// (URL-safe JSON). the engine decodes it with parseAppSplashSpec.
|
|
95
|
+
export function encodeAppSplashWire(spec: DiscoveredAppSplashSpec): string {
|
|
96
|
+
return encodeURIComponent(JSON.stringify(spec))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// parse a stamped `?appSplash=` wire value back into a spec (or null). used by
|
|
100
|
+
// the preview upload path to learn the splash image url it must stage.
|
|
101
|
+
export function parseAppSplashWire(wire: string): DiscoveredAppSplashSpec | null {
|
|
102
|
+
if (!wire) return null
|
|
103
|
+
try {
|
|
104
|
+
const decoded = JSON.parse(decodeURIComponent(wire)) as unknown
|
|
105
|
+
if (!decoded || typeof decoded !== 'object') return null
|
|
106
|
+
const spec = decoded as Record<string, unknown>
|
|
107
|
+
if (
|
|
108
|
+
spec.source !== 'expo-splash-screen' &&
|
|
109
|
+
spec.source !== 'expo-legacy' &&
|
|
110
|
+
spec.source !== 'react-native-bootsplash'
|
|
111
|
+
)
|
|
112
|
+
return null
|
|
113
|
+
if (typeof spec.backgroundColor !== 'string') return null
|
|
114
|
+
return decoded as DiscoveredAppSplashSpec
|
|
115
|
+
} catch {
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// every distinct image url a splash spec references (base + dark variant). used
|
|
121
|
+
// by the preview upload path so both the light and dark splash images get
|
|
122
|
+
// staged as share assets.
|
|
123
|
+
export function splashImageUrls(spec: DiscoveredAppSplashSpec): string[] {
|
|
124
|
+
const urls = new Set<string>()
|
|
125
|
+
if (spec.imageUrl) urls.add(spec.imageUrl)
|
|
126
|
+
if (spec.dark?.imageUrl) urls.add(spec.dark.imageUrl)
|
|
127
|
+
return [...urls]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// content type for a splash image url, by extension. config splash images ship
|
|
131
|
+
// as png/jpg/webp/gif/svg; fall back to octet-stream so the share store still
|
|
132
|
+
// serves bytes the engine's <img> can load.
|
|
133
|
+
export function splashImageContentType(url: string): string {
|
|
134
|
+
const ext = (url.split('?')[0].split('.').pop() || '').toLowerCase()
|
|
135
|
+
if (ext === 'png') return 'image/png'
|
|
136
|
+
if (ext === 'jpg' || ext === 'jpeg') return 'image/jpeg'
|
|
137
|
+
if (ext === 'webp') return 'image/webp'
|
|
138
|
+
if (ext === 'gif') return 'image/gif'
|
|
139
|
+
if (ext === 'svg') return 'image/svg+xml'
|
|
140
|
+
return 'application/octet-stream'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export type FetchedAppSplashImage = {
|
|
144
|
+
url: string
|
|
145
|
+
// sha256(url) — the share-store key, matching the font/asset convention.
|
|
146
|
+
urlhash: string
|
|
147
|
+
contentType: string
|
|
148
|
+
bytes: Uint8Array
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// fetch every splash image declared on an `?appSplash=` wire. like the config-
|
|
152
|
+
// plugin fonts, these images live in the native binary / launch storyboard,
|
|
153
|
+
// never in the metro bundle graph, so the preview upload path backfills them by
|
|
154
|
+
// re-fetching the declared urls off the still-live metro — the same staging
|
|
155
|
+
// pattern fetchAppFontFiles uses, keyed by sha256(url).
|
|
156
|
+
export async function fetchAppSplashImages(
|
|
157
|
+
wire: string,
|
|
158
|
+
hooks?: {
|
|
159
|
+
onStaged?: (info: { url: string; byteLength: number }) => void
|
|
160
|
+
onError?: (url: string, err: unknown) => void
|
|
161
|
+
},
|
|
162
|
+
): Promise<FetchedAppSplashImage[]> {
|
|
163
|
+
const spec = parseAppSplashWire(wire)
|
|
164
|
+
if (!spec) return []
|
|
165
|
+
const out: FetchedAppSplashImage[] = []
|
|
166
|
+
const seen = new Set<string>()
|
|
167
|
+
for (const url of splashImageUrls(spec)) {
|
|
168
|
+
const urlhash = createHash('sha256').update(url).digest('hex')
|
|
169
|
+
if (seen.has(urlhash)) continue
|
|
170
|
+
seen.add(urlhash)
|
|
171
|
+
try {
|
|
172
|
+
const res = await fetch(url)
|
|
173
|
+
if (!res.ok) {
|
|
174
|
+
hooks?.onError?.(url, new Error(`${res.status} ${res.statusText}`))
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
const bytes = new Uint8Array(await res.arrayBuffer())
|
|
178
|
+
out.push({ url, urlhash, contentType: splashImageContentType(url), bytes })
|
|
179
|
+
hooks?.onStaged?.({ url, byteLength: bytes.byteLength })
|
|
180
|
+
} catch (err) {
|
|
181
|
+
hooks?.onError?.(url, err)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return out
|
|
185
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// resets the GUEST APP to a fresh launch through the engine's own soft-reload
|
|
2
|
+
// bridge (`SootSim.bridges.hotRemount.reloadExternalApp`). that re-runs the
|
|
3
|
+
// guest app entry while keeping the engine bundle + worker alive — no heavy
|
|
4
|
+
// metro re-fetch of every lazy chunk, and the sim id does NOT rotate. when
|
|
5
|
+
// `resetStorage` is set it clears the guest app's tenant-scoped storage via the
|
|
6
|
+
// engine's own clear path (scoped localStorage + open-and-clear of the known
|
|
7
|
+
// tenant IndexedDBs / secure-store) — the maestro `clearState` contract.
|
|
8
|
+
//
|
|
9
|
+
// this replaces a host-realm `localStorage.clear()` + `indexedDB.deleteDatabase`
|
|
10
|
+
// loop that was wrong twice: (1) `localStorage.clear()` wiped engine settings
|
|
11
|
+
// and every co-tenant sim's tenant storage, not just this app's; (2) the delete
|
|
12
|
+
// loop ran in the host realm where the only database is the engine's own
|
|
13
|
+
// `preview-share-state`, which the engine holds open — `deleteDatabase` then
|
|
14
|
+
// hung forever (no success/error/blocked event fired), so the whole eval timed
|
|
15
|
+
// out at 60s and never cleared the guest's secure-store session either. that
|
|
16
|
+
// was the bluesky e2e suite's blanket setupApp failure.
|
|
17
|
+
export function resetGuestAppStateEval(resetStorage: boolean): string {
|
|
18
|
+
return `(async () => {
|
|
19
|
+
const bridge = globalThis.SootSim && globalThis.SootSim.bridges && globalThis.SootSim.bridges.hotRemount
|
|
20
|
+
if (!bridge || typeof bridge.reloadExternalApp !== 'function') return false
|
|
21
|
+
await bridge.reloadExternalApp({ resetStorage: ${resetStorage ? 'true' : 'false'} })
|
|
22
|
+
return true
|
|
23
|
+
})()`
|
|
24
|
+
}
|
package/cli/auth.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// sootsim cli auth resolution for cloud-hitting commands.
|
|
2
|
+
//
|
|
3
|
+
// priority order (first match wins):
|
|
4
|
+
// 1. RNX_API_KEY, or SOOTSIM_API_KEY if that is unset — ci path
|
|
5
|
+
// 2. CONTRAST_INSTALLATION_TOKEN/GITHUB_TOKEN + repo env — github action
|
|
6
|
+
// preview uploads from the Contrast runner, no extra secret
|
|
7
|
+
// 3. desktop session token from shared desktop auth (`rnxsim login`)
|
|
8
|
+
//
|
|
9
|
+
// callers wanting an auth header: `authHeaderOrExit('upload')`.
|
|
10
|
+
|
|
11
|
+
import { readSharedDesktopAuthSession } from '../src/auth/shared-session'
|
|
12
|
+
|
|
13
|
+
export type CliApiKeyEnvName = 'RNX_API_KEY' | 'SOOTSIM_API_KEY'
|
|
14
|
+
|
|
15
|
+
export type CliAuth =
|
|
16
|
+
| { kind: 'api-key'; secret: string; source: 'env'; envName: CliApiKeyEnvName }
|
|
17
|
+
| {
|
|
18
|
+
kind: 'github'
|
|
19
|
+
token: string
|
|
20
|
+
// historical name: this is the github owner/repo slug, not the
|
|
21
|
+
// numeric repository id.
|
|
22
|
+
repoId: string
|
|
23
|
+
repositoryId: string | null
|
|
24
|
+
owner: string | null
|
|
25
|
+
repo: string | null
|
|
26
|
+
installationId: string | null
|
|
27
|
+
source: 'contrast-runner' | 'github-actions'
|
|
28
|
+
}
|
|
29
|
+
| { kind: 'session'; token: string; origin: string }
|
|
30
|
+
|
|
31
|
+
export type GitHubUploadIdentity = {
|
|
32
|
+
repoId: string
|
|
33
|
+
owner?: string
|
|
34
|
+
repo?: string
|
|
35
|
+
installationId?: string | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── resolution ────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
const GITHUB_TOKEN_PREFIXES = ['ghs_', 'ghp_', 'gho_', 'ghu_', 'github_pat_']
|
|
41
|
+
|
|
42
|
+
function looksLikeGitHubToken(token: string | undefined): boolean {
|
|
43
|
+
// github's 2026 stateless installation tokens (`secrets.GITHUB_TOKEN` /
|
|
44
|
+
// github.token in actions) are ~520 chars and vary by payload — keep them
|
|
45
|
+
// opaque with NO upper length cap, matching the canonical server-side check
|
|
46
|
+
// in src/github/token.ts. the length floor + prefix is only a cheap typo
|
|
47
|
+
// guard before the cloud endpoints verify real repo access. an upper cap
|
|
48
|
+
// here silently rejects the long actions token, so resolveCliAuth() returns
|
|
49
|
+
// null and every CI `record start` dies with "set RNX_API_KEY".
|
|
50
|
+
return Boolean(
|
|
51
|
+
token &&
|
|
52
|
+
token.length >= 20 &&
|
|
53
|
+
GITHUB_TOKEN_PREFIXES.some((prefix) => token.startsWith(prefix)),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resolveGitHubActionsAuth(): CliAuth | null {
|
|
58
|
+
const contrastRunnerToken = process.env.CONTRAST_INSTALLATION_TOKEN?.trim()
|
|
59
|
+
const githubActionsToken = process.env.GITHUB_TOKEN?.trim()
|
|
60
|
+
const token = contrastRunnerToken || githubActionsToken
|
|
61
|
+
if (!looksLikeGitHubToken(token)) return null
|
|
62
|
+
|
|
63
|
+
const repoSlug = (
|
|
64
|
+
process.env.CONTRAST_REPO ||
|
|
65
|
+
process.env.GITHUB_REPOSITORY ||
|
|
66
|
+
''
|
|
67
|
+
).trim()
|
|
68
|
+
if (!repoSlug) return null
|
|
69
|
+
const [owner, repo] = repoSlug.includes('/') ? repoSlug.split('/', 2) : [null, null]
|
|
70
|
+
const repositoryId = (
|
|
71
|
+
process.env.CONTRAST_REPO_ID ||
|
|
72
|
+
process.env.GITHUB_REPOSITORY_ID ||
|
|
73
|
+
''
|
|
74
|
+
).trim()
|
|
75
|
+
|
|
76
|
+
const installationId = (
|
|
77
|
+
process.env.CONTRAST_INSTALLATION_ID ||
|
|
78
|
+
process.env.GITHUB_APP_INSTALLATION_ID ||
|
|
79
|
+
''
|
|
80
|
+
).trim()
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
kind: 'github',
|
|
84
|
+
token: token!,
|
|
85
|
+
repoId: repoSlug,
|
|
86
|
+
repositoryId: repositoryId || null,
|
|
87
|
+
owner: owner || null,
|
|
88
|
+
repo: repo || null,
|
|
89
|
+
installationId: installationId || null,
|
|
90
|
+
source: contrastRunnerToken ? 'contrast-runner' : 'github-actions',
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isCliApiKeySecret(secret: string): boolean {
|
|
95
|
+
return secret.startsWith('sk_rnx_') || secret.startsWith('sk_sootsim_')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readApiKeyFromEnv(): { secret: string; envName: CliApiKeyEnvName } | null {
|
|
99
|
+
for (const envName of ['RNX_API_KEY', 'SOOTSIM_API_KEY'] as const) {
|
|
100
|
+
const secret = process.env[envName]?.trim()
|
|
101
|
+
if (secret && isCliApiKeySecret(secret)) return { secret, envName }
|
|
102
|
+
}
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function resolveCliAuth(): CliAuth | null {
|
|
107
|
+
const envKey = readApiKeyFromEnv()
|
|
108
|
+
if (envKey) {
|
|
109
|
+
return {
|
|
110
|
+
kind: 'api-key',
|
|
111
|
+
secret: envKey.secret,
|
|
112
|
+
source: 'env',
|
|
113
|
+
envName: envKey.envName,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const githubAuth = resolveGitHubActionsAuth()
|
|
117
|
+
if (githubAuth) return githubAuth
|
|
118
|
+
const session = readSharedDesktopAuthSession()
|
|
119
|
+
if (session?.token) {
|
|
120
|
+
return { kind: 'session', token: session.token, origin: session.origin }
|
|
121
|
+
}
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function authHeaderValue(auth: CliAuth): string {
|
|
126
|
+
return auth.kind === 'api-key' ? `Bearer ${auth.secret}` : `Bearer ${auth.token}`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function githubUploadIdentity(auth: CliAuth | null): GitHubUploadIdentity | null {
|
|
130
|
+
if (!auth || auth.kind !== 'github') return null
|
|
131
|
+
return {
|
|
132
|
+
repoId: auth.repoId,
|
|
133
|
+
owner: auth.owner ?? undefined,
|
|
134
|
+
repo: auth.repo ?? undefined,
|
|
135
|
+
installationId: auth.installationId,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// fail fast with a friendly message — use at the top of cloud-hitting
|
|
140
|
+
// commands that can't proceed without auth.
|
|
141
|
+
export function authHeaderOrExit(commandLabel: string): {
|
|
142
|
+
auth: CliAuth
|
|
143
|
+
header: string
|
|
144
|
+
} {
|
|
145
|
+
const auth = resolveCliAuth()
|
|
146
|
+
if (auth) return { auth, header: authHeaderValue(auth) }
|
|
147
|
+
|
|
148
|
+
process.stderr.write(
|
|
149
|
+
`\n rnx ${commandLabel} needs to be authenticated.\n\n` +
|
|
150
|
+
` pick one:\n` +
|
|
151
|
+
` • run \`rnxsim login\` to sign in with your GH account\n` +
|
|
152
|
+
` • set RNX_API_KEY=sk_rnx_... (recommended for CI)\n\n`,
|
|
153
|
+
)
|
|
154
|
+
process.exit(1)
|
|
155
|
+
}
|