rnxsim 0.1.499 → 0.1.501
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 +1 -1
- package/cli/bridge-flow-runner.ts +6 -2
- package/cli/cloud-client.ts +90 -7
- package/cli/cloud-dispatch.ts +27 -27
- package/cli/cloud-session.ts +61 -6
- package/cli/commands/control.ts +33 -31
- package/cli/commands/detox.ts +157 -159
- package/cli/commands/inspect/core.ts +2 -0
- package/cli/commands/platform.ts +170 -48
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/test.ts +403 -95
- package/cli/main.ts +11 -0
- package/cli/outbound-endpoints.ts +10 -0
- package/cli/ws-bridge.ts +18 -19
- package/detox/jest-preset.cjs +1 -6
- 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-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +7 -1
- package/dist-lib/cloud-contract.mjs +5 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/detox/jest-preset.cjs +1 -6
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +54 -29
- 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/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +52 -22
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.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/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +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 +7 -8
- package/dist-lib/sdk.mjs +7 -8
- package/dist-lib/skills.cjs +128 -63
- package/dist-lib/vite.cjs +1 -1
- package/package.json +1 -1
- package/skills/rnx-test/SKILL.md +2 -2
- package/src/cloud-contract.ts +2 -0
- package/src/sim-client.ts +8 -10
package/README.md
CHANGED
|
@@ -483,7 +483,9 @@ export class SootSimBridgeFlowRunner {
|
|
|
483
483
|
}
|
|
484
484
|
|
|
485
485
|
async waitForTree(timeout: number = DEFAULT_TIMEOUT) {
|
|
486
|
-
let status = await inspectWaitReady(this.bridge, scaledTimeout(timeout)
|
|
486
|
+
let status = await inspectWaitReady(this.bridge, scaledTimeout(timeout), {
|
|
487
|
+
simId: this.opts.simId,
|
|
488
|
+
})
|
|
487
489
|
if (status.ready) return
|
|
488
490
|
if (status.externalError) {
|
|
489
491
|
throw new Error(`app failed to load: ${status.externalError}`)
|
|
@@ -512,7 +514,9 @@ export class SootSimBridgeFlowRunner {
|
|
|
512
514
|
)
|
|
513
515
|
this.opts.simId = successor
|
|
514
516
|
saveCurrentSimId(successor)
|
|
515
|
-
status = await inspectWaitReady(this.bridge, scaledTimeout(timeout)
|
|
517
|
+
status = await inspectWaitReady(this.bridge, scaledTimeout(timeout), {
|
|
518
|
+
simId: this.opts.simId,
|
|
519
|
+
})
|
|
516
520
|
if (status.ready) return
|
|
517
521
|
}
|
|
518
522
|
}
|
package/cli/cloud-client.ts
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
type RNXMetroModuleIdentityMetadata,
|
|
37
37
|
} from '../src/metro-production-bundle.ts'
|
|
38
38
|
import { rnxPublicBrand } from '../src/public-brand.ts'
|
|
39
|
-
import { CHOOSE_ACCOUNT_HINT } from './auth.ts'
|
|
39
|
+
import { CHOOSE_ACCOUNT_HINT, resolveCliAuth } from './auth.ts'
|
|
40
40
|
import {
|
|
41
41
|
createCloudSession,
|
|
42
42
|
normalizeCloudOrigin,
|
|
@@ -44,8 +44,25 @@ import {
|
|
|
44
44
|
type CloudBoxSession,
|
|
45
45
|
type CloudSession,
|
|
46
46
|
type CloudSimSession,
|
|
47
|
+
type RemotePlatform,
|
|
47
48
|
} from './cloud-session.ts'
|
|
48
|
-
|
|
49
|
+
|
|
50
|
+
export class UnsupportedRemoteHostError extends Error {
|
|
51
|
+
readonly code = 'unsupported_host' as const
|
|
52
|
+
readonly platform: string
|
|
53
|
+
readonly host: string
|
|
54
|
+
|
|
55
|
+
constructor(platform: string, host: string, message?: string) {
|
|
56
|
+
super(
|
|
57
|
+
message ??
|
|
58
|
+
`rnx remote ${platform} does not support --${host}: rnxsim/cloud only creates iOS simulators`,
|
|
59
|
+
)
|
|
60
|
+
this.name = 'UnsupportedRemoteHostError'
|
|
61
|
+
this.platform = platform
|
|
62
|
+
this.host = host
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
import { detectProject } from './commands/detect.ts'
|
|
49
66
|
import type { StorageSnapshot } from '../../sootsim-engine/src/preview/storage-snapshot-contract.ts'
|
|
50
67
|
import type {
|
|
51
68
|
BridgeClaimResult,
|
|
@@ -58,7 +75,7 @@ export const DEFAULT_RNX_CLOUD_ORIGIN = rnxPublicBrand.origin
|
|
|
58
75
|
export const MAX_RNX_CLOUD_BUNDLE_BYTES = RNX_CLOUD_MAX_ARTIFACT_BYTES
|
|
59
76
|
|
|
60
77
|
// every message this module writes names the command through this constant.
|
|
61
|
-
const REMOTE_COMMAND = 'rnx
|
|
78
|
+
const REMOTE_COMMAND = 'rnx remote'
|
|
62
79
|
const PREVIEW_COMMAND = 'rnx preview'
|
|
63
80
|
|
|
64
81
|
// a Metro asset dest is a shallow tree of images. a walk that runs past this
|
|
@@ -72,6 +89,7 @@ interface CreateCloudBoxOptions {
|
|
|
72
89
|
authorization: string
|
|
73
90
|
/** the account to bill when the authorization is a session token; an api key names its own */
|
|
74
91
|
accountId: string | null
|
|
92
|
+
platform?: RemotePlatform
|
|
75
93
|
apiOrigin?: string
|
|
76
94
|
/** the name this box carries in the account's box list. */
|
|
77
95
|
boxName?: string | null
|
|
@@ -502,9 +520,28 @@ function searchMetroAssetDest(
|
|
|
502
520
|
resolve(bundleDirectory, '..', 'assets'),
|
|
503
521
|
]
|
|
504
522
|
const cwd = process.cwd()
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
523
|
+
let projectDirectory: string | null = null
|
|
524
|
+
for (const start of [bundleDirectory, cwd]) {
|
|
525
|
+
let directory = resolve(start)
|
|
526
|
+
while (true) {
|
|
527
|
+
if (detectProject(directory)?.hasMetroConfig) {
|
|
528
|
+
projectDirectory = directory
|
|
529
|
+
break
|
|
530
|
+
}
|
|
531
|
+
const parent = dirname(directory)
|
|
532
|
+
if (parent === directory) break
|
|
533
|
+
directory = parent
|
|
534
|
+
}
|
|
535
|
+
if (projectDirectory) break
|
|
536
|
+
}
|
|
537
|
+
if (projectDirectory) {
|
|
538
|
+
candidates.push(
|
|
539
|
+
join(projectDirectory, 'assets'),
|
|
540
|
+
join(projectDirectory, 'dist'),
|
|
541
|
+
join(projectDirectory, 'dist', 'assets'),
|
|
542
|
+
join(projectDirectory, 'build'),
|
|
543
|
+
join(projectDirectory, 'build', 'assets'),
|
|
544
|
+
)
|
|
508
545
|
}
|
|
509
546
|
const searched: string[] = []
|
|
510
547
|
const holders: { dest: MetroAssetDest; verified: number }[] = []
|
|
@@ -833,6 +870,10 @@ export async function createCloudBox(options: CreateCloudBoxOptions): Promise<{
|
|
|
833
870
|
assetsDirectory: string | null
|
|
834
871
|
warnings: string[]
|
|
835
872
|
}> {
|
|
873
|
+
const platform = options.platform ?? 'ios'
|
|
874
|
+
if (platform === 'android') {
|
|
875
|
+
throw new UnsupportedRemoteHostError('android', 'nano')
|
|
876
|
+
}
|
|
836
877
|
const apiOrigin = resolveCloudOrigin(options.apiOrigin)
|
|
837
878
|
const artifact = await produceCloudArtifact(options.bundlePath)
|
|
838
879
|
const boxName = options.boxName?.trim()
|
|
@@ -840,7 +881,7 @@ export async function createCloudBox(options: CreateCloudBoxOptions): Promise<{
|
|
|
840
881
|
authorization: options.authorization,
|
|
841
882
|
'x-rnx-box-size': 'nano',
|
|
842
883
|
'x-rnx-artifact-sha256': artifact.sha256,
|
|
843
|
-
'x-rnx-platform':
|
|
884
|
+
'x-rnx-platform': platform,
|
|
844
885
|
'x-rnx-device': options.device,
|
|
845
886
|
...(options.accountId ? { 'x-rnx-account-id': options.accountId } : null),
|
|
846
887
|
...(boxName ? { 'x-rnx-box-name': boxName } : null),
|
|
@@ -889,6 +930,12 @@ export async function createCloudBox(options: CreateCloudBoxOptions): Promise<{
|
|
|
889
930
|
apiOrigin,
|
|
890
931
|
claimId: created.receipt.simulator.claim.id,
|
|
891
932
|
token: created.simulatorToken,
|
|
933
|
+
platform,
|
|
934
|
+
device: options.device,
|
|
935
|
+
artifact: {
|
|
936
|
+
sha256: artifact.sha256,
|
|
937
|
+
bytes: artifact.bytes.length,
|
|
938
|
+
},
|
|
892
939
|
})
|
|
893
940
|
return {
|
|
894
941
|
receipt: created.receipt,
|
|
@@ -1185,6 +1232,36 @@ export async function confirmCloudSimulator(session: CloudSimSession): Promise<v
|
|
|
1185
1232
|
|
|
1186
1233
|
// deletes the box, which closes every simulator it holds
|
|
1187
1234
|
export async function closeCloudBox(session: CloudSession): Promise<void> {
|
|
1235
|
+
if (session.service === 'box') {
|
|
1236
|
+
const auth = resolveCliAuth()
|
|
1237
|
+
let authHeader = `Bearer ${session.boxToken}`
|
|
1238
|
+
if (auth?.kind === 'api-key') {
|
|
1239
|
+
authHeader = `Bearer ${auth.secret}`
|
|
1240
|
+
} else if (auth?.kind === 'session') {
|
|
1241
|
+
authHeader = `Bearer ${auth.token}`
|
|
1242
|
+
}
|
|
1243
|
+
let response: Response
|
|
1244
|
+
try {
|
|
1245
|
+
response = await fetch(
|
|
1246
|
+
`${session.apiOrigin}/v1/boxes/${encodeURIComponent(session.boxId)}/stop`,
|
|
1247
|
+
{
|
|
1248
|
+
method: 'POST',
|
|
1249
|
+
headers: { authorization: authHeader },
|
|
1250
|
+
},
|
|
1251
|
+
)
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
throw new Error(
|
|
1254
|
+
`could not reach box ${session.boxId} at ${session.apiOrigin}: ${
|
|
1255
|
+
error instanceof Error ? error.message : String(error)
|
|
1256
|
+
}`,
|
|
1257
|
+
)
|
|
1258
|
+
}
|
|
1259
|
+
const text = await response.text()
|
|
1260
|
+
if (!response.ok && response.status !== 404) {
|
|
1261
|
+
throw responseError(response.status, text)
|
|
1262
|
+
}
|
|
1263
|
+
return
|
|
1264
|
+
}
|
|
1188
1265
|
const response = await fetch(
|
|
1189
1266
|
`${session.apiOrigin}/v1/boxes/${encodeURIComponent(session.boxId)}`,
|
|
1190
1267
|
{
|
|
@@ -1300,6 +1377,12 @@ export async function addCloudSimulator(
|
|
|
1300
1377
|
simId,
|
|
1301
1378
|
claimId: claim.id,
|
|
1302
1379
|
token,
|
|
1380
|
+
platform: 'ios',
|
|
1381
|
+
device: options.device,
|
|
1382
|
+
artifact: {
|
|
1383
|
+
sha256: artifact.id.replace(/^sha256:/, ''),
|
|
1384
|
+
bytes: artifact.bytes,
|
|
1385
|
+
},
|
|
1303
1386
|
})
|
|
1304
1387
|
return { receipt, session: updatedSession }
|
|
1305
1388
|
}
|
package/cli/cloud-dispatch.ts
CHANGED
|
@@ -126,12 +126,8 @@ export async function dispatchCloudBoundary({
|
|
|
126
126
|
command,
|
|
127
127
|
args,
|
|
128
128
|
}: CloudBoundaryInvocation): Promise<CloudBoundaryResult> {
|
|
129
|
-
if (command === 'ios' && args.includes('--remote'))
|
|
130
|
-
|
|
131
|
-
console.error(
|
|
132
|
-
' rnx android --remote is not available; remote simulators are iOS only',
|
|
133
|
-
)
|
|
134
|
-
return { handled: true, code: 1 }
|
|
129
|
+
if ((command === 'ios' || command === 'android') && args.includes('--remote')) {
|
|
130
|
+
return { handled: false }
|
|
135
131
|
}
|
|
136
132
|
if (!CLOUD_TARGET_COMMANDS.has(command)) return { handled: false }
|
|
137
133
|
|
|
@@ -155,13 +151,35 @@ export async function dispatchCloudBoundary({
|
|
|
155
151
|
}
|
|
156
152
|
if (!session) return { handled: false }
|
|
157
153
|
|
|
154
|
+
if (command === 'close') {
|
|
155
|
+
if (args.includes('--all') || args.includes('--others')) {
|
|
156
|
+
return refuseCloudInvocation(command, parsed.positional)
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const descriptor = cloudSession.requireCloudSessionShellUpdate()
|
|
160
|
+
await cloudClient.closeCloudBox(session)
|
|
161
|
+
cloudSession.publishCloudSessionToShell(null, descriptor)
|
|
162
|
+
console.log(
|
|
163
|
+
session.service === 'box'
|
|
164
|
+
? ` closed: remote box ${session.boxId}`
|
|
165
|
+
: ` closed: remote simulator ${session.simId}`,
|
|
166
|
+
)
|
|
167
|
+
return { handled: true, code: 0 }
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error(
|
|
170
|
+
` close failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
171
|
+
)
|
|
172
|
+
return { handled: true, code: 1 }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
158
176
|
// a box relays the whole bridge vocabulary to the page hosting its
|
|
159
177
|
// simulator, which runs the same shell the local CLI drives. so there is no
|
|
160
178
|
// subset to police: every command below reaches it through the ordinary
|
|
161
|
-
// bridge, which createBridgeFromParsed points at the box.
|
|
162
|
-
//
|
|
179
|
+
// bridge, which createBridgeFromParsed points at the box. claim is specific
|
|
180
|
+
// to nano.
|
|
163
181
|
if (session.service === 'box') {
|
|
164
|
-
if (command === 'claim'
|
|
182
|
+
if (command === 'claim') {
|
|
165
183
|
console.error(
|
|
166
184
|
` rnx ${command} is not available for a box; its simulator is held by the Box page. use rnx box stop to end the box.`,
|
|
167
185
|
)
|
|
@@ -188,24 +206,6 @@ export async function dispatchCloudBoundary({
|
|
|
188
206
|
}
|
|
189
207
|
}
|
|
190
208
|
|
|
191
|
-
if (command === 'close') {
|
|
192
|
-
if (args.includes('--all') || args.includes('--others')) {
|
|
193
|
-
return refuseCloudInvocation(command, parsed.positional)
|
|
194
|
-
}
|
|
195
|
-
try {
|
|
196
|
-
const descriptor = cloudSession.requireCloudSessionShellUpdate()
|
|
197
|
-
await cloudClient.closeCloudBox(session)
|
|
198
|
-
cloudSession.publishCloudSessionToShell(null, descriptor)
|
|
199
|
-
console.log(` closed: remote simulator ${session.simId}`)
|
|
200
|
-
return { handled: true, code: 0 }
|
|
201
|
-
} catch (error) {
|
|
202
|
-
console.error(
|
|
203
|
-
` close failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
204
|
-
)
|
|
205
|
-
return { handled: true, code: 1 }
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
209
|
// these inputs use the same perform executor as an inline chain. a CPU
|
|
210
210
|
// simulator has focused text inputs but no browser keyboard to inspect.
|
|
211
211
|
if (
|
package/cli/cloud-session.ts
CHANGED
|
@@ -15,6 +15,13 @@ export const RNX_SHELL_UPDATE_UNSET = `${RNX_SHELL_UPDATE_PREFIX}unset`
|
|
|
15
15
|
// leaving a reader to infer it from which fields happen to be filled in.
|
|
16
16
|
export type CloudSession = CloudSimSession | CloudBoxSession
|
|
17
17
|
|
|
18
|
+
export type RemotePlatform = 'ios' | 'android'
|
|
19
|
+
|
|
20
|
+
export interface CloudSessionArtifactIdentity {
|
|
21
|
+
sha256: string
|
|
22
|
+
bytes?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
/**
|
|
19
26
|
* one remote host, addressed by the box token, and the simulator inside it
|
|
20
27
|
* that ordinary commands reach with the simulator token. this is what
|
|
@@ -29,6 +36,9 @@ export interface CloudSimSession {
|
|
|
29
36
|
apiOrigin: string
|
|
30
37
|
claimId: string
|
|
31
38
|
token: string
|
|
39
|
+
platform?: RemotePlatform
|
|
40
|
+
device?: string
|
|
41
|
+
artifact?: CloudSessionArtifactIdentity
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
/**
|
|
@@ -43,6 +53,9 @@ export interface CloudBoxSession {
|
|
|
43
53
|
boxId: string
|
|
44
54
|
boxToken: string
|
|
45
55
|
apiOrigin: string
|
|
56
|
+
platform?: RemotePlatform
|
|
57
|
+
device?: string
|
|
58
|
+
artifact?: CloudSessionArtifactIdentity
|
|
46
59
|
}
|
|
47
60
|
|
|
48
61
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -79,8 +92,11 @@ export function createCloudSession(input: {
|
|
|
79
92
|
apiOrigin: string
|
|
80
93
|
claimId: string
|
|
81
94
|
token: string
|
|
95
|
+
platform?: RemotePlatform
|
|
96
|
+
device?: string
|
|
97
|
+
artifact?: CloudSessionArtifactIdentity
|
|
82
98
|
}): CloudSimSession {
|
|
83
|
-
const session = {
|
|
99
|
+
const session: CloudSimSession = {
|
|
84
100
|
version: CLOUD_SESSION_VERSION,
|
|
85
101
|
service: 'sim',
|
|
86
102
|
boxId: input.boxId.trim(),
|
|
@@ -89,7 +105,10 @@ export function createCloudSession(input: {
|
|
|
89
105
|
apiOrigin: normalizeCloudOrigin(input.apiOrigin.trim()),
|
|
90
106
|
claimId: input.claimId.trim(),
|
|
91
107
|
token: input.token,
|
|
92
|
-
|
|
108
|
+
...(input.platform ? { platform: input.platform } : {}),
|
|
109
|
+
...(input.device?.trim() ? { device: input.device.trim() } : {}),
|
|
110
|
+
...(input.artifact ? { artifact: input.artifact } : {}),
|
|
111
|
+
}
|
|
93
112
|
if (
|
|
94
113
|
!session.boxId ||
|
|
95
114
|
!session.boxToken ||
|
|
@@ -106,14 +125,20 @@ export function createCloudBoxSession(input: {
|
|
|
106
125
|
boxId: string
|
|
107
126
|
boxToken: string
|
|
108
127
|
apiOrigin: string
|
|
128
|
+
platform?: RemotePlatform
|
|
129
|
+
device?: string
|
|
130
|
+
artifact?: CloudSessionArtifactIdentity
|
|
109
131
|
}): CloudBoxSession {
|
|
110
|
-
const session = {
|
|
132
|
+
const session: CloudBoxSession = {
|
|
111
133
|
version: CLOUD_SESSION_VERSION,
|
|
112
134
|
service: 'box',
|
|
113
135
|
boxId: input.boxId.trim(),
|
|
114
136
|
boxToken: input.boxToken,
|
|
115
137
|
apiOrigin: normalizeCloudOrigin(input.apiOrigin.trim()),
|
|
116
|
-
|
|
138
|
+
...(input.platform ? { platform: input.platform } : {}),
|
|
139
|
+
...(input.device?.trim() ? { device: input.device.trim() } : {}),
|
|
140
|
+
...(input.artifact ? { artifact: input.artifact } : {}),
|
|
141
|
+
}
|
|
117
142
|
if (!session.boxId || !session.boxToken) {
|
|
118
143
|
throw new Error('cloud box session requires a box, an origin, and a box token')
|
|
119
144
|
}
|
|
@@ -160,9 +185,29 @@ export function parseCloudSession(encoded: string): CloudSession {
|
|
|
160
185
|
) {
|
|
161
186
|
throw new Error(`${RNX_CLOUD_SESSION_ENV} is invalid`)
|
|
162
187
|
}
|
|
188
|
+
const platform =
|
|
189
|
+
parsed.platform === 'ios' || parsed.platform === 'android'
|
|
190
|
+
? parsed.platform
|
|
191
|
+
: undefined
|
|
192
|
+
const device = typeof parsed.device === 'string' ? parsed.device : undefined
|
|
193
|
+
const artifact =
|
|
194
|
+
isRecord(parsed.artifact) && typeof parsed.artifact.sha256 === 'string'
|
|
195
|
+
? {
|
|
196
|
+
sha256: parsed.artifact.sha256,
|
|
197
|
+
bytes:
|
|
198
|
+
typeof parsed.artifact.bytes === 'number' ? parsed.artifact.bytes : undefined,
|
|
199
|
+
}
|
|
200
|
+
: undefined
|
|
163
201
|
try {
|
|
164
202
|
if (parsed.service === 'box') {
|
|
165
|
-
return createCloudBoxSession({
|
|
203
|
+
return createCloudBoxSession({
|
|
204
|
+
boxId,
|
|
205
|
+
boxToken,
|
|
206
|
+
apiOrigin,
|
|
207
|
+
platform,
|
|
208
|
+
device,
|
|
209
|
+
artifact,
|
|
210
|
+
})
|
|
166
211
|
}
|
|
167
212
|
const simId = parsed.simId
|
|
168
213
|
const claimId = parsed.claimId
|
|
@@ -175,7 +220,17 @@ export function parseCloudSession(encoded: string): CloudSession {
|
|
|
175
220
|
) {
|
|
176
221
|
throw new Error('unreadable cloud session')
|
|
177
222
|
}
|
|
178
|
-
return createCloudSession({
|
|
223
|
+
return createCloudSession({
|
|
224
|
+
boxId,
|
|
225
|
+
boxToken,
|
|
226
|
+
simId,
|
|
227
|
+
apiOrigin,
|
|
228
|
+
claimId,
|
|
229
|
+
token,
|
|
230
|
+
platform,
|
|
231
|
+
device,
|
|
232
|
+
artifact,
|
|
233
|
+
})
|
|
179
234
|
} catch {
|
|
180
235
|
throw new Error(`${RNX_CLOUD_SESSION_ENV} is invalid`)
|
|
181
236
|
}
|
package/cli/commands/control.ts
CHANGED
|
@@ -1520,12 +1520,18 @@ export async function runOpenCommand(
|
|
|
1520
1520
|
printBridgeWorldNotice(parsed.wsPort, baseUrl)
|
|
1521
1521
|
const savedSimId = readCurrentSimId()
|
|
1522
1522
|
const shouldReuseCurrentSim = !openInNewSim
|
|
1523
|
+
// every open stamps its page URL with a one-off token and matches the sim by
|
|
1524
|
+
// it. a sim id alone cannot tell the page this open loaded from the page it
|
|
1525
|
+
// replaced, which stays registered until the browser commits the navigation.
|
|
1526
|
+
const token = `cli-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
|
|
1527
|
+
const simMatchesToken = (sim: BridgeSimInfo) =>
|
|
1528
|
+
sim.url?.includes(`inspectOpen=${token}`) ?? false
|
|
1523
1529
|
|
|
1524
1530
|
if (shouldReuseCurrentSim) {
|
|
1525
1531
|
const bridge = createBridgeFromParsed(parsed)
|
|
1526
1532
|
const simHint = parsed.simId ? ` --sim ${parsed.simId}` : ''
|
|
1527
1533
|
let fallBackToNewSim = false
|
|
1528
|
-
let
|
|
1534
|
+
let navigatedShellUrl: string | null = null
|
|
1529
1535
|
|
|
1530
1536
|
try {
|
|
1531
1537
|
let targetSim: BridgeSimInfo | null = null
|
|
@@ -1585,27 +1591,24 @@ export async function runOpenCommand(
|
|
|
1585
1591
|
}
|
|
1586
1592
|
}
|
|
1587
1593
|
if (!fallBackToNewSim && targetSim) {
|
|
1588
|
-
// a navigation that crosses origins tears down the page context, so
|
|
1589
|
-
// the guest reconnects as a NEW sim and the id we are holding stops
|
|
1590
|
-
// existing. record what was connected first, so the wait below can
|
|
1591
|
-
// follow the reload instead of polling an id the browser retired.
|
|
1592
|
-
preNavSimIds = new Set(sims.map((sim) => sim.id))
|
|
1593
1594
|
const resolvedBaseUrl =
|
|
1594
1595
|
hasExplicitBaseUrl || looksLikeSootsimUrl(target)
|
|
1595
1596
|
? baseUrl
|
|
1596
1597
|
: deriveCurrentSimBaseUrl(targetSim, baseUrl)
|
|
1597
|
-
|
|
1598
|
+
navigatedShellUrl =
|
|
1598
1599
|
targetShellUrl ??
|
|
1599
1600
|
(await buildShellUrl(
|
|
1600
1601
|
target,
|
|
1601
1602
|
applyRNXConfigToUrl(resolvedBaseUrl, runtimeConfig),
|
|
1602
1603
|
buildShellUrlOpts,
|
|
1603
1604
|
))
|
|
1605
|
+
const navigationUrl = new URL(navigatedShellUrl)
|
|
1606
|
+
navigationUrl.searchParams.set('inspectOpen', token)
|
|
1604
1607
|
bridge
|
|
1605
1608
|
.send({
|
|
1606
1609
|
type: 'evaluate',
|
|
1607
1610
|
simId: targetSim.id,
|
|
1608
|
-
code: `window.location.href = ${JSON.stringify(
|
|
1611
|
+
code: `window.location.href = ${JSON.stringify(navigationUrl.toString())}`,
|
|
1609
1612
|
})
|
|
1610
1613
|
.catch(() => {})
|
|
1611
1614
|
}
|
|
@@ -1621,21 +1624,30 @@ export async function runOpenCommand(
|
|
|
1621
1624
|
rnxExit(1)
|
|
1622
1625
|
}
|
|
1623
1626
|
|
|
1624
|
-
if (!fallBackToNewSim && targetSim) {
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
const
|
|
1627
|
+
if (!fallBackToNewSim && targetSim && navigatedShellUrl) {
|
|
1628
|
+
// the navigated page may keep the sim id (restored from session
|
|
1629
|
+
// storage) or register a new one, so match by token, never by id.
|
|
1630
|
+
const navigatedSim = await waitForSimMatch(
|
|
1628
1631
|
parsed.wsPort,
|
|
1629
1632
|
parsed.commandTimeoutMs,
|
|
1630
1633
|
(sim) =>
|
|
1631
1634
|
sim.readyState === 'open' &&
|
|
1632
1635
|
simUsesDriver(sim, driverId) &&
|
|
1633
|
-
(sim
|
|
1636
|
+
simMatchesToken(sim),
|
|
1634
1637
|
{ attempts: Math.round(30 * flowTimeoutScale()) },
|
|
1635
1638
|
)
|
|
1636
|
-
if (
|
|
1637
|
-
|
|
1639
|
+
if (!navigatedSim) {
|
|
1640
|
+
console.error(
|
|
1641
|
+
` timed out waiting for sim ${targetSim.id} to load ${navigatedShellUrl}`,
|
|
1642
|
+
)
|
|
1643
|
+
await printBridgeFailureDiagnostics(bridge, {
|
|
1644
|
+
errorsCommand: `rnx get errors 5${simHint}`,
|
|
1645
|
+
warningsCommand: `rnx get warnings 5${simHint}`,
|
|
1646
|
+
requestsCommand: `rnx get requests 5${simHint}`,
|
|
1647
|
+
})
|
|
1648
|
+
rnxExit(1)
|
|
1638
1649
|
}
|
|
1650
|
+
targetSim = navigatedSim
|
|
1639
1651
|
const ready = await waitForSimReady(
|
|
1640
1652
|
parsed.wsPort,
|
|
1641
1653
|
parsed.commandTimeoutMs,
|
|
@@ -1659,18 +1671,12 @@ export async function runOpenCommand(
|
|
|
1659
1671
|
}
|
|
1660
1672
|
ready.bridge.close()
|
|
1661
1673
|
saveCurrentSimId(targetSim.id)
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
(await buildShellUrl(
|
|
1669
|
-
target,
|
|
1670
|
-
applyRNXConfigToUrl(resolvedBaseUrl, runtimeConfig),
|
|
1671
|
-
buildShellUrlOpts,
|
|
1672
|
-
))
|
|
1673
|
-
printOpenedSim(shellUrl, { ...targetSim, url: shellUrl }, 'current sim', quiet)
|
|
1674
|
+
printOpenedSim(
|
|
1675
|
+
navigatedShellUrl,
|
|
1676
|
+
{ ...targetSim, url: navigatedShellUrl },
|
|
1677
|
+
'current sim',
|
|
1678
|
+
quiet,
|
|
1679
|
+
)
|
|
1674
1680
|
await maybeDescribeAfterOpen(parsed.wsPort, targetSim.id, args)
|
|
1675
1681
|
return parsed.wsPort
|
|
1676
1682
|
}
|
|
@@ -1680,7 +1686,6 @@ export async function runOpenCommand(
|
|
|
1680
1686
|
}
|
|
1681
1687
|
|
|
1682
1688
|
const configuredBaseUrl = applyRNXConfigToUrl(baseUrl, runtimeConfig)
|
|
1683
|
-
const token = `cli-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
|
|
1684
1689
|
const openUrl = await buildOpenUrl(target, configuredBaseUrl, token, buildShellUrlOpts)
|
|
1685
1690
|
const ownerPid = getStableOwnerPid()
|
|
1686
1691
|
if (!ownerPid) {
|
|
@@ -1689,9 +1694,6 @@ export async function runOpenCommand(
|
|
|
1689
1694
|
)
|
|
1690
1695
|
rnxExit(1)
|
|
1691
1696
|
}
|
|
1692
|
-
const simMatchesToken = (sim: BridgeSimInfo) =>
|
|
1693
|
-
sim.url?.includes(`inspectOpen=${token}`) ?? false
|
|
1694
|
-
|
|
1695
1697
|
const driver = getDriver(driverId)
|
|
1696
1698
|
if (!driver) {
|
|
1697
1699
|
console.error(` unknown driver "${driverId}" — run \`rnx list --drivers\``)
|