rnxsim 0.1.519 → 0.1.520

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 (68) hide show
  1. package/cli/cloud-client.ts +419 -287
  2. package/cli/cloud-dispatch.ts +2 -21
  3. package/cli/cloud-session.ts +31 -85
  4. package/cli/commands/box.ts +15 -11
  5. package/cli/commands/inspect.ts +34 -1
  6. package/cli/commands/platform.ts +184 -232
  7. package/cli/main.ts +10 -2
  8. package/cli/outbound-endpoints.ts +21 -6
  9. package/dist-lib/agent-daemon-client.cjs +1 -1
  10. package/dist-lib/agent-events.cjs +1 -1
  11. package/dist-lib/agent-identity.cjs +1 -1
  12. package/dist-lib/agent-sessions.cjs +1 -1
  13. package/dist-lib/attached-projects.cjs +1 -1
  14. package/dist-lib/auth/shared-session.cjs +1 -1
  15. package/dist-lib/backend-origin.cjs +1 -1
  16. package/dist-lib/beta.cjs +1 -1
  17. package/dist-lib/beta.mjs +1 -1
  18. package/dist-lib/bridge-constants.cjs +1 -1
  19. package/dist-lib/bridge-contract-input.cjs +1 -1
  20. package/dist-lib/bridge-contract-input.mjs +1 -1
  21. package/dist-lib/bridge-contract.cjs +1 -1
  22. package/dist-lib/bridge-contract.mjs +1 -1
  23. package/dist-lib/capture-contract.cjs +1 -1
  24. package/dist-lib/capture-contract.mjs +1 -1
  25. package/dist-lib/cli-constants.cjs +1 -1
  26. package/dist-lib/cloud-contract.cjs +1 -1
  27. package/dist-lib/cloud-contract.mjs +1 -1
  28. package/dist-lib/cloud.cjs +1 -1
  29. package/dist-lib/cloud.mjs +1 -1
  30. package/dist-lib/config.cjs +1 -1
  31. package/dist-lib/detox/index.cjs +1 -1
  32. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  33. package/dist-lib/home-paths.cjs +1 -1
  34. package/dist-lib/host/bridge-host.cjs +112 -39
  35. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  36. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  37. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  38. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  39. package/dist-lib/host/websocket-proxy.cjs +1 -1
  40. package/dist-lib/index.cjs +43 -117
  41. package/dist-lib/jump-to-source-babel.cjs +1 -1
  42. package/dist-lib/jump-to-source-native.cjs +1 -1
  43. package/dist-lib/menu.cjs +30 -1
  44. package/dist-lib/menu.mjs +27 -1
  45. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  46. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  47. package/dist-lib/metro-production-bundle.cjs +1 -1
  48. package/dist-lib/metro-production-bundle.mjs +1 -1
  49. package/dist-lib/metro.cjs +1 -1
  50. package/dist-lib/profiles.cjs +1 -1
  51. package/dist-lib/public-brand.cjs +1 -1
  52. package/dist-lib/react-native-host-modules.cjs +1 -1
  53. package/dist-lib/react-native-host-modules.mjs +1 -1
  54. package/dist-lib/render-mode.cjs +1 -1
  55. package/dist-lib/scripts/dev-server-scanner.cjs +41 -4
  56. package/dist-lib/sdk.cjs +1 -1
  57. package/dist-lib/sdk.mjs +1 -1
  58. package/dist-lib/skills.cjs +138 -164
  59. package/dist-lib/swift.cjs +674 -12
  60. package/dist-lib/vite.cjs +26 -3
  61. package/package.json +1 -1
  62. package/scripts/dev-server-scanner.ts +72 -5
  63. package/src/cloud-contract.ts +7 -1
  64. package/src/cloud.ts +11 -4
  65. package/src/connect.ts +1 -1
  66. package/src/menu.ts +44 -0
  67. package/src/vite-plugin-one.ts +12 -0
  68. package/src/vite-plugin-swift.ts +17 -5
@@ -157,13 +157,9 @@ export async function dispatchCloudBoundary({
157
157
  }
158
158
  try {
159
159
  const descriptor = cloudSession.requireCloudSessionShellUpdate()
160
- await cloudClient.closeCloudBox(session)
160
+ await cloudClient.closeCloudSession(session)
161
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
- )
162
+ console.log(` closed: remote simulator ${session.simId}`)
167
163
  return { handled: true, code: 0 }
168
164
  } catch (error) {
169
165
  console.error(
@@ -173,21 +169,6 @@ export async function dispatchCloudBoundary({
173
169
  }
174
170
  }
175
171
 
176
- // a box relays the whole bridge vocabulary to the page hosting its
177
- // simulator, which runs the same shell the local CLI drives. so there is no
178
- // subset to police: every command below reaches it through the ordinary
179
- // bridge, which createBridgeFromParsed points at the box. claim is specific
180
- // to nano.
181
- if (session.service === 'box') {
182
- if (command === 'claim') {
183
- console.error(
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.`,
185
- )
186
- return { handled: true, code: 1 }
187
- }
188
- return { handled: false }
189
- }
190
-
191
172
  if (command === 'claim') {
192
173
  try {
193
174
  const descriptor = cloudSession.requireCloudSessionShellUpdate()
@@ -10,10 +10,9 @@ export const RNX_SHELL_UPDATE_PREFIX = '__RNX_SHELL_UPDATE_V1__'
10
10
  export const RNX_SHELL_UPDATE_SET_PREFIX = `${RNX_SHELL_UPDATE_PREFIX}set:`
11
11
  export const RNX_SHELL_UPDATE_UNSET = `${RNX_SHELL_UPDATE_PREFIX}unset`
12
12
 
13
- // the shell's remote target. two services answer rnx commands and they are
14
- // addressed differently, so the session names which one it is rather than
15
- // leaving a reader to infer it from which fields happen to be filled in.
16
- export type CloudSession = CloudSimSession | CloudBoxSession
13
+ // the shell's remote target: one simulator on the instance API, addressed by
14
+ // its own id and token.
15
+ export type CloudSession = CloudSimSession
17
16
 
18
17
  export type RemotePlatform = 'ios' | 'android'
19
18
 
@@ -23,39 +22,25 @@ export interface CloudSessionArtifactIdentity {
23
22
  }
24
23
 
25
24
  /**
26
- * one remote host, addressed by the box token, and the simulator inside it
27
- * that ordinary commands reach with the simulator token. this is what
28
- * `rnx ios --remote` produces.
25
+ * one remote simulator and the claim ordinary commands drive it with. the
26
+ * watch URL the create returned rides along so a reattach reopens the same
27
+ * simulator. a nano-box simulator keeps the box that hosts it; an instance
28
+ * API simulator has no box.
29
29
  */
30
30
  export interface CloudSimSession {
31
31
  version: 3
32
32
  service: 'sim'
33
- boxId: string
34
- boxToken: string
35
33
  simId: string
36
34
  apiOrigin: string
37
35
  claimId: string
38
36
  token: string
37
+ streamUrl?: string | null
39
38
  platform?: RemotePlatform
40
39
  device?: string
41
40
  artifact?: CloudSessionArtifactIdentity
42
- }
43
-
44
- /**
45
- * a micro box, whose simulator lives in its own Box page rather than in the
46
- * service. there is nothing to claim and no separate simulator token: the box
47
- * token addresses the box, and the box relays each command to the page that
48
- * is hosting it.
49
- */
50
- export interface CloudBoxSession {
51
- version: 3
52
- service: 'box'
53
- boxId: string
54
- boxToken: string
55
- apiOrigin: string
56
- platform?: RemotePlatform
57
- device?: string
58
- artifact?: CloudSessionArtifactIdentity
41
+ /** nano-box simulators only: the box that hosts them. absent on the instance API. */
42
+ boxId?: string
43
+ boxToken?: string
59
44
  }
60
45
 
61
46
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -86,70 +71,43 @@ export function normalizeCloudOrigin(source: string): string {
86
71
  }
87
72
 
88
73
  export function createCloudSession(input: {
89
- boxId: string
90
- boxToken: string
91
74
  simId: string
92
75
  apiOrigin: string
93
76
  claimId: string
94
77
  token: string
78
+ streamUrl?: string | null
95
79
  platform?: RemotePlatform
96
80
  device?: string
97
81
  artifact?: CloudSessionArtifactIdentity
82
+ boxId?: string
83
+ boxToken?: string
98
84
  }): CloudSimSession {
85
+ const boxId = input.boxId?.trim() ?? ''
86
+ const boxToken = input.boxToken ?? ''
87
+ if ((boxId === '') !== (boxToken === '')) {
88
+ throw new Error('cloud session names a box with both its id and its token')
89
+ }
99
90
  const session: CloudSimSession = {
100
91
  version: CLOUD_SESSION_VERSION,
101
92
  service: 'sim',
102
- boxId: input.boxId.trim(),
103
- boxToken: input.boxToken,
104
93
  simId: input.simId.trim(),
105
94
  apiOrigin: normalizeCloudOrigin(input.apiOrigin.trim()),
106
95
  claimId: input.claimId.trim(),
107
96
  token: input.token,
97
+ ...(input.streamUrl ? { streamUrl: input.streamUrl } : {}),
108
98
  ...(input.platform ? { platform: input.platform } : {}),
109
99
  ...(input.device?.trim() ? { device: input.device.trim() } : {}),
110
100
  ...(input.artifact ? { artifact: input.artifact } : {}),
101
+ ...(boxId && boxToken ? { boxId, boxToken } : {}),
111
102
  }
112
- if (
113
- !session.boxId ||
114
- !session.boxToken ||
115
- !session.simId ||
116
- !session.claimId ||
117
- !session.token
118
- ) {
119
- throw new Error('cloud session requires box, simulator, origin, claim, and tokens')
120
- }
121
- return session
122
- }
123
-
124
- export function createCloudBoxSession(input: {
125
- boxId: string
126
- boxToken: string
127
- apiOrigin: string
128
- platform?: RemotePlatform
129
- device?: string
130
- artifact?: CloudSessionArtifactIdentity
131
- }): CloudBoxSession {
132
- const session: CloudBoxSession = {
133
- version: CLOUD_SESSION_VERSION,
134
- service: 'box',
135
- boxId: input.boxId.trim(),
136
- boxToken: input.boxToken,
137
- apiOrigin: normalizeCloudOrigin(input.apiOrigin.trim()),
138
- ...(input.platform ? { platform: input.platform } : {}),
139
- ...(input.device?.trim() ? { device: input.device.trim() } : {}),
140
- ...(input.artifact ? { artifact: input.artifact } : {}),
141
- }
142
- if (!session.boxId || !session.boxToken) {
143
- throw new Error('cloud box session requires a box, an origin, and a box token')
103
+ if (!session.simId || !session.claimId || !session.token) {
104
+ throw new Error('cloud session requires a simulator, an origin, a claim, and a token')
144
105
  }
145
106
  return session
146
107
  }
147
108
 
148
109
  export function serializeCloudSession(session: CloudSession): string {
149
- const validated =
150
- session.service === 'sim'
151
- ? createCloudSession(session)
152
- : createCloudBoxSession(session)
110
+ const validated = createCloudSession(session)
153
111
  return Buffer.from(JSON.stringify(validated), 'utf8').toString('base64url')
154
112
  }
155
113
 
@@ -175,14 +133,8 @@ export function parseCloudSession(encoded: string): CloudSession {
175
133
  if (!isRecord(parsed) || parsed.version !== CLOUD_SESSION_VERSION) {
176
134
  throw new Error(`${RNX_CLOUD_SESSION_ENV} is invalid`)
177
135
  }
178
- const boxId = parsed.boxId
179
- const boxToken = parsed.boxToken
180
136
  const apiOrigin = parsed.apiOrigin
181
- if (
182
- typeof boxId !== 'string' ||
183
- typeof boxToken !== 'string' ||
184
- typeof apiOrigin !== 'string'
185
- ) {
137
+ if (typeof apiOrigin !== 'string') {
186
138
  throw new Error(`${RNX_CLOUD_SESSION_ENV} is invalid`)
187
139
  }
188
140
  const platform =
@@ -198,17 +150,10 @@ export function parseCloudSession(encoded: string): CloudSession {
198
150
  typeof parsed.artifact.bytes === 'number' ? parsed.artifact.bytes : undefined,
199
151
  }
200
152
  : undefined
153
+ const streamUrl = typeof parsed.streamUrl === 'string' ? parsed.streamUrl : undefined
154
+ const boxId = typeof parsed.boxId === 'string' ? parsed.boxId : undefined
155
+ const boxToken = typeof parsed.boxToken === 'string' ? parsed.boxToken : undefined
201
156
  try {
202
- if (parsed.service === 'box') {
203
- return createCloudBoxSession({
204
- boxId,
205
- boxToken,
206
- apiOrigin,
207
- platform,
208
- device,
209
- artifact,
210
- })
211
- }
212
157
  const simId = parsed.simId
213
158
  const claimId = parsed.claimId
214
159
  const token = parsed.token
@@ -221,15 +166,16 @@ export function parseCloudSession(encoded: string): CloudSession {
221
166
  throw new Error('unreadable cloud session')
222
167
  }
223
168
  return createCloudSession({
224
- boxId,
225
- boxToken,
226
169
  simId,
227
170
  apiOrigin,
228
171
  claimId,
229
172
  token,
173
+ streamUrl,
230
174
  platform,
231
175
  device,
232
176
  artifact,
177
+ boxId,
178
+ boxToken,
233
179
  })
234
180
  } catch {
235
181
  throw new Error(`${RNX_CLOUD_SESSION_ENV} is invalid`)
@@ -11,7 +11,6 @@
11
11
  // boxTarget below.
12
12
 
13
13
  import { spawn } from 'node:child_process'
14
- import fs from 'node:fs/promises'
15
14
  import path from 'node:path'
16
15
  import { createInterface } from 'node:readline'
17
16
  import { BoxClientError, createBoxClient } from '@rnx/box/client'
@@ -34,6 +33,7 @@ import {
34
33
  import { readDefaultBox, writeDefaultBox } from '../../src/home-paths'
35
34
  import { rnxPublicBrand } from '../../src/public-brand'
36
35
  import { authHeaderOrExit, cloudAccountIdOrExit, type CliAuth } from '../auth'
36
+ import { produceCloudBoxArtifact, type CloudBoxArtifactProduction } from '../cloud-client'
37
37
  import { findRepositoryPackageRoot } from '../setup-repository'
38
38
  import { resolvePlacement } from '../ws-bridge'
39
39
  import { BundleFilePlane } from './box/bundle-plane'
@@ -214,7 +214,7 @@ function requireEndpoint(args: string[], label: string): string | null {
214
214
 
215
215
  /**
216
216
  * the cloud box service this command talks to, or null with the reason
217
- * printed. exported because `rnx ios --remote` creates a box too, and a second
217
+ * printed. exported because `rnx box send` creates a box too, and a second
218
218
  * spelling of where boxes live is a second place to configure.
219
219
  */
220
220
  export function cloudBoxEndpoint(args: string[], command: string): string | null {
@@ -520,10 +520,10 @@ function cloudBoxClient(options: {
520
520
  /**
521
521
  * create a Box that runs an app somebody already built.
522
522
  *
523
- * `box create --bundle` reaches this with a file from disk and `rnx ios
524
- * --remote` with the artifact it just produced. both are the same box: the
525
- * bundle is the whole project, the provenance says so, and the page that opens
526
- * it boots that bundle instead of building anything.
523
+ * `box create --bundle` reaches this with a file from disk and `rnx box send`
524
+ * with the artifact it just produced. both are the same box: the bundle is
525
+ * the whole project, the provenance says so, and the page that opens it boots
526
+ * that bundle instead of building anything.
527
527
  */
528
528
  export async function createPrebuiltBundleBox(options: {
529
529
  bundle: Uint8Array
@@ -589,22 +589,26 @@ async function runCloudBoxCreate(
589
589
  // a prebuilt box carries no sources, so there is no checkout to read and
590
590
  // this can run anywhere, including a directory that is not a repository.
591
591
  const resolved = path.resolve(bundlePath)
592
- let read: Buffer
592
+ // the page boots these bytes with no asset server behind it, so the Metro
593
+ // assets beside the bundle are embedded the way every other remote path
594
+ // embeds them. the box service owns the size ceiling, not this producer.
595
+ let artifact: CloudBoxArtifactProduction
593
596
  try {
594
- read = await fs.readFile(resolved)
597
+ artifact = await produceCloudBoxArtifact(resolved)
595
598
  } catch (error) {
596
599
  console.error(
597
- ` ${rnx} box create: could not read the bundle at ${resolved}: ${
600
+ ` ${rnx} box create: could not prepare the bundle at ${resolved}: ${
598
601
  error instanceof Error ? error.message : String(error)
599
602
  }`,
600
603
  )
601
604
  return 1
602
605
  }
603
- if (read.byteLength === 0) {
606
+ if (artifact.bytes.byteLength === 0) {
604
607
  console.error(` ${rnx} box create: the bundle at ${resolved} is empty.`)
605
608
  return 1
606
609
  }
607
- source = { bundle: new Uint8Array(read) }
610
+ for (const warning of artifact.warnings) console.error(` ${warning}`)
611
+ source = { bundle: new Uint8Array(artifact.bytes) }
608
612
  defaultName = path.basename(resolved).replace(/\.[^.]+$/, '')
609
613
  } else {
610
614
  const root = findRepositoryPackageRoot(process.cwd())
@@ -648,6 +648,35 @@ export function printShellPerfReport(result: Record<string, any>) {
648
648
  )
649
649
  }
650
650
  }
651
+ const liveFrames = result.liveFrames
652
+ if (liveFrames && typeof liveFrames === 'object') {
653
+ const tenant = liveFrames.tenant
654
+ const compositorLive =
655
+ liveFrames.compositor && typeof liveFrames.compositor === 'object'
656
+ ? liveFrames.compositor
657
+ : null
658
+ console.log(``)
659
+ console.log(` live frames (GL / camera / video / WebGPU / maps):`)
660
+ if (tenant && typeof tenant === 'object') {
661
+ const presentation = tenant.presentation ?? {}
662
+ console.log(
663
+ ` tenant published ${presentation.publishes ?? 0} · skipped ${presentation.backpressureSkips ?? 0} (in-flight) · stale-healed ${presentation.staleHeals ?? 0} · density ${tenant.renderDensity ?? '?'}`,
664
+ )
665
+ } else {
666
+ console.log(` tenant stats unavailable (tenant did not answer)`)
667
+ }
668
+ if (compositorLive) {
669
+ console.log(
670
+ ` compositor received ${compositorLive.publishes ?? 0} on ${compositorLive.activeChannels ?? 0} channel(s) · ${compositorLive.uploads ?? 0} texture uploads · ${compositorLive.uploadMsTotal ?? 0}ms total / ${compositorLive.uploadMsMax ?? 0}ms max`,
671
+ )
672
+ const shared = compositorLive.sharedDeviceImages
673
+ if (shared && typeof shared === 'object') {
674
+ console.log(
675
+ ` shared device: ${shared.bitmapUploadRequests ?? 0} upload requests · ${shared.bitmapUploadsHeld ?? 0} held (motion) · ${shared.bitmapUploadBytes ?? 0} bytes · ${shared.bitmapCacheHits ?? 0} cache hits`,
676
+ )
677
+ }
678
+ }
679
+ }
651
680
  if (surfaces.length > 0) {
652
681
  console.log(``)
653
682
  console.log(` surfaces (last 100 frames):`)
@@ -3749,9 +3778,13 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3749
3778
  // wrap in async to support await in worker mode. no identifier
3750
3779
  // rewriting — the snippet runs verbatim so tokens like `root`,
3751
3780
  // `debug`, `test` refer to whatever the user actually typed.
3781
+ // multi-statement snippets need a block body: the bare expression
3782
+ // form terminates at the first `;` (Unexpected token).
3752
3783
  let expandedCode = code
3753
3784
  if (!expandedCode.startsWith('(async')) {
3754
- expandedCode = `(async () => ${expandedCode})()`
3785
+ expandedCode = /[;\n]/.test(expandedCode)
3786
+ ? `(async () => { ${expandedCode} })()`
3787
+ : `(async () => ${expandedCode})()`
3755
3788
  }
3756
3789
  const result = await bridge.send({ type: 'evaluate', code: expandedCode })
3757
3790
  console.log(JSON.stringify(result, null, 2))