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.
Files changed (71) hide show
  1. package/README.md +1 -1
  2. package/cli/bridge-flow-runner.ts +6 -2
  3. package/cli/cloud-client.ts +90 -7
  4. package/cli/cloud-dispatch.ts +27 -27
  5. package/cli/cloud-session.ts +61 -6
  6. package/cli/commands/control.ts +33 -31
  7. package/cli/commands/detox.ts +157 -159
  8. package/cli/commands/inspect/core.ts +2 -0
  9. package/cli/commands/platform.ts +170 -48
  10. package/cli/commands/preview.ts +1 -1
  11. package/cli/commands/test.ts +403 -95
  12. package/cli/main.ts +11 -0
  13. package/cli/outbound-endpoints.ts +10 -0
  14. package/cli/ws-bridge.ts +18 -19
  15. package/detox/jest-preset.cjs +1 -6
  16. package/dist-lib/agent-daemon-client.cjs +1 -1
  17. package/dist-lib/agent-events.cjs +1 -1
  18. package/dist-lib/agent-identity.cjs +1 -1
  19. package/dist-lib/agent-sessions.cjs +1 -1
  20. package/dist-lib/attached-projects.cjs +1 -1
  21. package/dist-lib/auth/shared-session.cjs +1 -1
  22. package/dist-lib/backend-origin.cjs +1 -1
  23. package/dist-lib/beta.cjs +1 -1
  24. package/dist-lib/beta.mjs +1 -1
  25. package/dist-lib/bridge-constants.cjs +1 -1
  26. package/dist-lib/bridge-contract-input.cjs +1 -1
  27. package/dist-lib/bridge-contract-input.mjs +1 -1
  28. package/dist-lib/bridge-contract.cjs +1 -1
  29. package/dist-lib/bridge-contract.mjs +1 -1
  30. package/dist-lib/capture-contract.cjs +1 -1
  31. package/dist-lib/capture-contract.mjs +1 -1
  32. package/dist-lib/cli-constants.cjs +1 -1
  33. package/dist-lib/cloud-contract.cjs +7 -1
  34. package/dist-lib/cloud-contract.mjs +5 -1
  35. package/dist-lib/cloud.cjs +1 -1
  36. package/dist-lib/cloud.mjs +1 -1
  37. package/dist-lib/config.cjs +1 -1
  38. package/dist-lib/detox/index.cjs +1 -1
  39. package/dist-lib/detox/jest-preset.cjs +1 -6
  40. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  41. package/dist-lib/home-paths.cjs +1 -1
  42. package/dist-lib/host/bridge-host.cjs +54 -29
  43. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  44. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  45. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  46. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  47. package/dist-lib/host/websocket-proxy.cjs +1 -1
  48. package/dist-lib/index.cjs +52 -22
  49. package/dist-lib/jump-to-source-babel.cjs +1 -1
  50. package/dist-lib/jump-to-source-native.cjs +1 -1
  51. package/dist-lib/menu.cjs +1 -1
  52. package/dist-lib/menu.mjs +1 -1
  53. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  54. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  55. package/dist-lib/metro-production-bundle.cjs +1 -1
  56. package/dist-lib/metro-production-bundle.mjs +1 -1
  57. package/dist-lib/metro.cjs +1 -1
  58. package/dist-lib/profiles.cjs +1 -1
  59. package/dist-lib/public-brand.cjs +1 -1
  60. package/dist-lib/react-native-host-modules.cjs +1 -1
  61. package/dist-lib/react-native-host-modules.mjs +1 -1
  62. package/dist-lib/render-mode.cjs +1 -1
  63. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  64. package/dist-lib/sdk.cjs +7 -8
  65. package/dist-lib/sdk.mjs +7 -8
  66. package/dist-lib/skills.cjs +128 -63
  67. package/dist-lib/vite.cjs +1 -1
  68. package/package.json +1 -1
  69. package/skills/rnx-test/SKILL.md +2 -2
  70. package/src/cloud-contract.ts +2 -0
  71. package/src/sim-client.ts +8 -10
@@ -1,9 +1,8 @@
1
1
  // `rnx ios` and `rnx android` — start a simulator on that platform.
2
2
  //
3
- // the platform picks the device; the command context picks WHERE the sim runs
4
- // (this machine, a local box, or `--remote`). there is deliberately no
5
- // per-place spelling such as `rnx remote ios`, so this file only resolves the
6
- // device and hands the rest to the normal open path.
3
+ // the platform picks the device; the command context picks where the sim runs
4
+ // (this machine, a local box, or the `rnx remote` namespace). this file resolves
5
+ // the device and hands the rest to the normal open path.
7
6
 
8
7
  import {
9
8
  DEFAULT_DEVICE_BY_PLATFORM,
@@ -15,6 +14,7 @@ import {
15
14
  } from 'sootsim-engine/settings'
16
15
  import { settingsStore } from 'sootsim-engine/settings/store'
17
16
  import { rnxPublicBrand } from '../../src/public-brand'
17
+ import { UnsupportedRemoteHostError } from '../cloud-client'
18
18
  import { rethrowIfExit } from '../run-rnx'
19
19
 
20
20
  const PLATFORM_LABEL: Record<RuntimePlatform, string> = {
@@ -26,13 +26,13 @@ function isDeviceModel(value: string): value is DeviceModel {
26
26
  return value in devices
27
27
  }
28
28
 
29
- const REMOTE_USAGE =
30
- 'usage: rnx ios --remote <local bundle.js|https://bundle-url> [--device <model>]'
31
-
32
29
  // flags this command reads for itself rather than treating as the bundle.
33
30
  const REMOTE_VALUE_FLAGS = new Set(['--device', '--name', '--endpoint'])
34
31
 
35
- function remoteInputFromArgs(args: string[]): {
32
+ function remoteInputFromArgs(
33
+ args: string[],
34
+ platform: RuntimePlatform,
35
+ ): {
36
36
  bundlePath: string
37
37
  name: string | null
38
38
  } {
@@ -43,14 +43,15 @@ function remoteInputFromArgs(args: string[]): {
43
43
  if (arg === '--remote' || arg === '--nano') continue
44
44
  if (REMOTE_VALUE_FLAGS.has(arg)) {
45
45
  const value = args[++index] ?? null
46
- if (!value) throw new Error(`rnx ios --remote ${arg} requires a value`)
46
+ if (!value) throw new Error(`rnx remote ${platform} ${arg} requires a value`)
47
47
  if (arg === '--name') name = value
48
48
  continue
49
49
  }
50
50
  const assigned = arg.match(/^(--[a-z-]+)=(.*)$/)
51
51
  if (assigned && REMOTE_VALUE_FLAGS.has(assigned[1] ?? '')) {
52
52
  const value = assigned[2] ?? ''
53
- if (!value) throw new Error(`rnx ios --remote ${assigned[1]} requires a value`)
53
+ if (!value)
54
+ throw new Error(`rnx remote ${platform} ${assigned[1]} requires a value`)
54
55
  if (assigned[1] === '--name') name = value
55
56
  continue
56
57
  }
@@ -62,14 +63,20 @@ function remoteInputFromArgs(args: string[]): {
62
63
  arg.startsWith('--session=') ||
63
64
  arg.startsWith('--tab=')
64
65
  ) {
65
- throw new Error('rnx ios --remote creates a new simulator and cannot target --sim')
66
+ throw new Error(
67
+ `rnx remote ${platform} creates a new simulator and cannot target --sim`,
68
+ )
66
69
  }
67
70
  if (arg.startsWith('-')) {
68
- throw new Error(`rnx ios --remote does not support ${arg}`)
71
+ throw new Error(`rnx remote ${platform} does not support ${arg}`)
69
72
  }
70
73
  positional.push(arg)
71
74
  }
72
- if (positional.length !== 1 || !positional[0]) throw new Error(REMOTE_USAGE)
75
+ if (positional.length !== 1 || !positional[0]) {
76
+ throw new Error(
77
+ `usage: rnx remote ${platform} <local bundle.js|https://bundle-url> [--device <model>]`,
78
+ )
79
+ }
73
80
  return { bundlePath: positional[0], name }
74
81
  }
75
82
 
@@ -87,7 +94,7 @@ function boxNameForBundle(bundlePath: string): string {
87
94
  .toLowerCase()
88
95
  .replace(/[^a-z0-9]+/g, '-')
89
96
  .replace(/^-+|-+$/g, '')
90
- if (!name) throw new Error(`rnx ios --remote could not name a box after ${bundlePath}`)
97
+ if (!name) throw new Error(`rnx remote could not name a box after ${bundlePath}`)
91
98
  return name
92
99
  }
93
100
 
@@ -99,7 +106,9 @@ export async function runPlatformCommand(
99
106
  // `--device` is applied to the settings store before dispatch, so a
100
107
  // contradiction shows up here as a platform mismatch. refuse it by name
101
108
  // rather than silently overriding whichever one the user meant.
102
- const explicitDevice = args.find((_, i) => args[i - 1] === '--device')
109
+ const assignedDevice = args.find((arg) => arg.startsWith('--device='))
110
+ const explicitDevice =
111
+ args.find((_, i) => args[i - 1] === '--device') ?? assignedDevice?.slice(9)
103
112
  let selectedDevice: DeviceModel
104
113
  if (explicitDevice !== undefined) {
105
114
  if (!isDeviceModel(explicitDevice)) {
@@ -135,15 +144,16 @@ export async function runPlatformCommand(
135
144
  }
136
145
 
137
146
  if (args.includes('--remote')) {
138
- if (platform !== 'ios') {
139
- console.error(
140
- ` ${rnxPublicBrand.commandName} android --remote is not available; remote simulators are iOS only`,
141
- )
147
+ if (platform === 'android' && args.includes('--nano')) {
148
+ const error = new UnsupportedRemoteHostError('android', 'nano')
149
+ console.error(` ${error.message}`)
142
150
  return 1
143
151
  }
144
152
  try {
145
- const input = remoteInputFromArgs(args)
146
- if (!args.includes('--nano')) return await runRemoteBox(input, args)
153
+ const input = remoteInputFromArgs(args, platform)
154
+ if (!args.includes('--nano')) {
155
+ return await runRemoteBox(input, args, platform, selectedDevice)
156
+ }
147
157
  const {
148
158
  publishCloudSessionToShell,
149
159
  readCloudSession,
@@ -151,30 +161,63 @@ export async function runPlatformCommand(
151
161
  } = await import('../cloud-session')
152
162
  const descriptor = requireCloudSessionShellUpdate()
153
163
  const { authHeaderOrExit, cloudAccountIdOrExit } = await import('../auth')
154
- const { auth, header } = authHeaderOrExit('ios --remote')
164
+ const { auth, header } = authHeaderOrExit(`${platform} --remote`)
155
165
  const accountId = cloudAccountIdOrExit(auth)
166
+ const { closeCloudBox, createCloudBox, claimCloudSimulator, addCloudSimulator } =
167
+ await import('../cloud-client')
168
+ const artifact = await (
169
+ await import('../cloud-client')
170
+ ).produceCloudArtifact(input.bundlePath)
156
171
  const existingSession = readCloudSession()
157
- if (existingSession) {
158
- const { addCloudSimulator } = await import('../cloud-client')
159
- const added = await addCloudSimulator(existingSession, {
160
- device: selectedDevice,
161
- authorization: header,
162
- accountId,
163
- })
164
- publishCloudSessionToShell(added.session, descriptor)
165
- console.log(` remote simulator: ${added.receipt.simId}`)
166
- console.log(` claim expires: ${added.receipt.claim.expiresAt}`)
172
+ if (
173
+ existingSession &&
174
+ existingSession.service === 'sim' &&
175
+ existingSession.platform === platform &&
176
+ existingSession.artifact?.sha256 &&
177
+ existingSession.artifact.sha256 === artifact.sha256 &&
178
+ args.includes('--reconnect')
179
+ ) {
180
+ const claimed = await claimCloudSimulator(existingSession)
181
+ publishCloudSessionToShell(claimed.session, descriptor)
182
+ console.log(
183
+ ` reconnected to remote simulator: ${existingSession.simId} (${platform})`,
184
+ )
185
+ console.log(` claim expires: ${claimed.claim.expiresAt}`)
167
186
  console.log(
168
187
  ' commands: describe, find, get tree|node|count|memory, wait ready|selector, do, reset, logs, state, screenshot',
169
188
  )
170
189
  return 0
171
190
  }
172
- const { closeCloudBox, createCloudBox } = await import('../cloud-client')
191
+ if (existingSession) {
192
+ if (
193
+ existingSession.service === 'sim' &&
194
+ (!existingSession.platform || existingSession.platform === platform) &&
195
+ (!existingSession.artifact ||
196
+ existingSession.artifact.sha256 === artifact.sha256 ||
197
+ args.includes('--new'))
198
+ ) {
199
+ const added = await addCloudSimulator(existingSession, {
200
+ device: selectedDevice,
201
+ authorization: header,
202
+ accountId,
203
+ })
204
+ publishCloudSessionToShell(added.session, descriptor)
205
+ console.log(` remote simulator: ${added.receipt.simId}`)
206
+ console.log(` claim expires: ${added.receipt.claim.expiresAt}`)
207
+ console.log(
208
+ ' commands: describe, find, get tree|node|count|memory, wait ready|selector, do, reset, logs, state, screenshot',
209
+ )
210
+ return 0
211
+ }
212
+ await closeCloudBox(existingSession)
213
+ publishCloudSessionToShell(null, descriptor)
214
+ }
173
215
  const created = await createCloudBox({
174
216
  bundlePath: input.bundlePath,
175
217
  device: selectedDevice,
176
218
  authorization: header,
177
219
  accountId,
220
+ platform,
178
221
  })
179
222
  try {
180
223
  publishCloudSessionToShell(created.session, descriptor)
@@ -185,6 +228,7 @@ export async function runPlatformCommand(
185
228
  const receipt = created.receipt
186
229
  const produced = created.artifact
187
230
  console.log(` remote simulator: ${receipt.simulator.simId}`)
231
+ console.log(` platform: ${platform}`)
188
232
  console.log(` artifact: ${receipt.artifact.id} (${receipt.artifact.bytes} bytes)`)
189
233
  console.log(
190
234
  ` source bundle: sha256:${produced.source.sha256} (${produced.source.bytes} bytes)`,
@@ -209,7 +253,7 @@ export async function runPlatformCommand(
209
253
  } catch (error) {
210
254
  rethrowIfExit(error)
211
255
  console.error(
212
- ` ${rnxPublicBrand.commandName} ios --remote failed: ${
256
+ ` ${rnxPublicBrand.commandName} ${platform} --remote failed: ${
213
257
  error instanceof Error ? error.message : String(error)
214
258
  }`,
215
259
  )
@@ -259,17 +303,16 @@ export function runRemotePlatformCommand(
259
303
  args: string[],
260
304
  opts: { port?: number } = {},
261
305
  ): Promise<number | undefined> {
262
- if (platform === 'android') {
263
- console.error(
264
- ` ${rnxPublicBrand.commandName} remote android is not available: rnxsim/cloud only creates iOS simulators`,
265
- )
306
+ if (platform === 'android' && args.includes('--nano')) {
307
+ const error = new UnsupportedRemoteHostError('android', 'nano')
308
+ console.error(` ${error.message}`)
266
309
  return Promise.resolve(1)
267
310
  }
268
311
  return runPlatformCommand(platform, ['--remote', ...args], opts)
269
312
  }
270
313
 
271
314
  /**
272
- * `rnx ios --remote` — the app runs in a Box.
315
+ * `rnx ios --remote` and `rnx android --remote` — the app runs in a Box.
273
316
  *
274
317
  * the artifact is produced here, self-contained, exactly as it always was: one
275
318
  * JavaScript file with its Metro assets embedded, so nothing it needs is left
@@ -279,17 +322,69 @@ export function runRemotePlatformCommand(
279
322
  async function runRemoteBox(
280
323
  input: { bundlePath: string; name: string | null },
281
324
  args: string[],
325
+ platform: RuntimePlatform = 'ios',
326
+ selectedDevice?: string,
282
327
  ): Promise<number> {
283
- const command = 'ios --remote'
284
- const [{ produceCloudArtifact }, box, { authHeaderOrExit, cloudAccountIdOrExit }] =
285
- await Promise.all([import('../cloud-client'), import('./box'), import('../auth')])
328
+ const command = `${platform} --remote`
329
+ const [
330
+ { produceCloudArtifact, closeCloudBox },
331
+ box,
332
+ { readCloudBoxSession },
333
+ { authHeaderOrExit, cloudAccountIdOrExit },
334
+ {
335
+ createCloudBoxSession,
336
+ publishCloudSessionToShell,
337
+ readCloudSession,
338
+ serializeCloudSession,
339
+ RNX_CLOUD_SESSION_ENV,
340
+ },
341
+ ] = await Promise.all([
342
+ import('../cloud-client'),
343
+ import('./box'),
344
+ import('rnx-cloud-box/client'),
345
+ import('../auth'),
346
+ import('../cloud-session'),
347
+ ])
348
+ const artifact = await produceCloudArtifact(input.bundlePath, Number.POSITIVE_INFINITY)
286
349
  const endpoint = box.cloudBoxEndpoint(args, command)
287
350
  if (!endpoint) return 1
351
+
352
+ const existingSession = readCloudSession()
353
+ if (
354
+ existingSession &&
355
+ existingSession.service === 'box' &&
356
+ existingSession.apiOrigin === endpoint &&
357
+ existingSession.platform === platform &&
358
+ existingSession.device === selectedDevice &&
359
+ existingSession.artifact?.sha256 === artifact.sha256
360
+ ) {
361
+ const live = await readCloudBoxSession({
362
+ endpoint: existingSession.apiOrigin,
363
+ boxId: existingSession.boxId,
364
+ accessToken: existingSession.boxToken,
365
+ })
366
+ if (live.sessionId) {
367
+ console.log(
368
+ ` reconnected to remote simulator: ${existingSession.boxId} (${platform})`,
369
+ )
370
+ console.log(
371
+ ' commands: describe, find, get tree|node|count|memory, wait ready|selector, do, reset, logs, state, screenshot',
372
+ )
373
+ return 0
374
+ }
375
+ }
376
+
288
377
  const { auth, header } = authHeaderOrExit(command)
289
378
  const accountId = cloudAccountIdOrExit(auth)
290
- // the Box holds the bundle in parts, so the size a single message can carry
291
- // is not what limits this one and there is no cap to apply here.
292
- const artifact = await produceCloudArtifact(input.bundlePath, Number.POSITIVE_INFINITY)
379
+
380
+ if (existingSession) {
381
+ await closeCloudBox(existingSession)
382
+ try {
383
+ publishCloudSessionToShell(null)
384
+ } catch {
385
+ // no shell update channel is required outside an interactive rnx shell
386
+ }
387
+ }
293
388
  const created = await box.createPrebuiltBundleBox({
294
389
  bundle: new Uint8Array(artifact.bytes),
295
390
  name: input.name ?? boxNameForBundle(input.bundlePath),
@@ -299,7 +394,27 @@ async function runRemoteBox(
299
394
  accountId,
300
395
  })
301
396
  const produced = artifact.receipt
397
+ const session = createCloudBoxSession({
398
+ boxId: created.id,
399
+ boxToken: created.accessToken ?? '',
400
+ apiOrigin: endpoint,
401
+ platform,
402
+ device: selectedDevice,
403
+ artifact: {
404
+ sha256: artifact.sha256,
405
+ bytes: artifact.bytes.length,
406
+ },
407
+ })
408
+ try {
409
+ publishCloudSessionToShell(session)
410
+ } catch {
411
+ // shell update fd not configured
412
+ }
413
+ process.env[RNX_CLOUD_SESSION_ENV] = serializeCloudSession(session)
414
+
302
415
  console.log(` box: ${created.id} (${artifact.bytes.length} bytes)`)
416
+ console.log(` platform: ${platform}`)
417
+ if (selectedDevice) console.log(` device: ${selectedDevice}`)
303
418
  console.log(
304
419
  ` source bundle: sha256:${produced.source.sha256} (${produced.source.bytes} bytes)`,
305
420
  )
@@ -313,11 +428,18 @@ async function runRemoteBox(
313
428
  // against it, so say which one won rather than leaving it invisible.
314
429
  if (artifact.assetsDirectory) console.log(` asset dest: ${artifact.assetsDirectory}`)
315
430
  for (const warning of artifact.warnings) console.warn(` warning: ${warning}`)
316
- const pageUrl = created.description.pageUrl
317
- if (!pageUrl) {
431
+ console.log(
432
+ ' commands: describe, find, get tree|node|count|memory, wait ready|selector, do, reset, logs, state, screenshot',
433
+ )
434
+ if (!created.description.pageUrl) {
318
435
  console.error(' the box service returned no page to open')
319
436
  return 1
320
437
  }
438
+ const pageUrl = new URL(created.description.pageUrl)
439
+ pageUrl.searchParams.set(
440
+ 'device',
441
+ selectedDevice ?? DEFAULT_DEVICE_BY_PLATFORM[platform],
442
+ )
321
443
  console.log(` open ${pageUrl}`)
322
- return box.openInBrowser(pageUrl)
444
+ return box.openInBrowser(pageUrl.toString())
323
445
  }
@@ -26,7 +26,7 @@ rnx preview — publish a prebuilt React Native bundle as a standalone share
26
26
  usage:
27
27
  rnx preview <bundle.js|https://bundle-url> [--platform ios|android] [--open]
28
28
 
29
- The share contains the immutable build and its Metro assets. It is not a recorded replay.
29
+ The share contains the immutable build and its automatically discovered Metro assets. It is not a recorded replay.
30
30
  `)
31
31
  return 0
32
32
  }