rnxsim 0.1.433 → 0.1.435

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 (60) hide show
  1. package/cli/cloud-client.ts +55 -13
  2. package/cli/commands/box.ts +89 -14
  3. package/cli/commands/inspect.ts +8 -1
  4. package/cli/outbound-endpoints.ts +5 -0
  5. package/cli/send-to-box.ts +112 -0
  6. package/dist-lib/agent-daemon-client.cjs +1 -1
  7. package/dist-lib/agent-events.cjs +1 -1
  8. package/dist-lib/agent-identity.cjs +1 -1
  9. package/dist-lib/agent-sessions.cjs +1 -1
  10. package/dist-lib/attached-projects.cjs +1 -1
  11. package/dist-lib/auth/shared-session.cjs +1 -1
  12. package/dist-lib/backend-origin.cjs +1 -1
  13. package/dist-lib/beta.cjs +1 -1
  14. package/dist-lib/beta.mjs +1 -1
  15. package/dist-lib/bridge-constants.cjs +1 -1
  16. package/dist-lib/bridge-contract-input.cjs +1 -1
  17. package/dist-lib/bridge-contract-input.mjs +1 -1
  18. package/dist-lib/bridge-contract.cjs +1 -1
  19. package/dist-lib/bridge-contract.mjs +1 -1
  20. package/dist-lib/capture-contract.cjs +1 -1
  21. package/dist-lib/capture-contract.mjs +1 -1
  22. package/dist-lib/cli-constants.cjs +1 -1
  23. package/dist-lib/cloud-contract.cjs +1 -1
  24. package/dist-lib/cloud-contract.mjs +1 -1
  25. package/dist-lib/cloud.cjs +1 -1
  26. package/dist-lib/cloud.mjs +1 -1
  27. package/dist-lib/config.cjs +1 -1
  28. package/dist-lib/detox/index.cjs +1 -1
  29. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  30. package/dist-lib/home-paths.cjs +1 -1
  31. package/dist-lib/host/bridge-host.cjs +5324 -1371
  32. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  33. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  34. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  35. package/dist-lib/host/replacement-module-handler.cjs +298 -91
  36. package/dist-lib/host/websocket-proxy.cjs +1 -1
  37. package/dist-lib/index.cjs +313 -104
  38. package/dist-lib/jump-to-source-babel.cjs +1 -1
  39. package/dist-lib/menu.cjs +7 -1
  40. package/dist-lib/menu.mjs +7 -1
  41. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  42. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  43. package/dist-lib/metro-production-bundle.cjs +1 -1
  44. package/dist-lib/metro-production-bundle.mjs +1 -1
  45. package/dist-lib/metro.cjs +302 -95
  46. package/dist-lib/profiles.cjs +1 -1
  47. package/dist-lib/public-brand.cjs +1 -1
  48. package/dist-lib/react-native-host-modules.cjs +1 -1
  49. package/dist-lib/react-native-host-modules.mjs +1 -1
  50. package/dist-lib/render-mode.cjs +1 -1
  51. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  52. package/dist-lib/sdk.cjs +1 -1
  53. package/dist-lib/sdk.mjs +1 -1
  54. package/dist-lib/skills.cjs +5006 -1441
  55. package/dist-lib/vite.cjs +298 -91
  56. package/package.json +2 -2
  57. package/src/bridge-contract.ts +6 -0
  58. package/src/host/bridge-host.ts +79 -0
  59. package/src/host/replacement-module-handler.ts +20 -96
  60. package/src/menu.ts +8 -0
@@ -24,6 +24,7 @@ import {
24
24
  } from '../../contrast-bundler/src/metroCloudArtifact'
25
25
  import { inferMetroModuleIdentity } from '../../sootsim-engine/src/metro-fingerprint'
26
26
  import { loadMetroFingerprintRegistry } from '../../sootsim-engine/src/metro-fingerprint-registry-client'
27
+ import { isLoopbackHost } from '../src/backend-origin'
27
28
  import {
28
29
  isRnxCloudCommandType,
29
30
  RNX_CLOUD_MAX_ARTIFACT_BYTES,
@@ -43,6 +44,7 @@ import {
43
44
  type CloudSession,
44
45
  } from './cloud-session'
45
46
  import { detectProject, findProjectRoot } from './commands/detect'
47
+ import type { StorageSnapshot } from '../../sootsim-engine/src/preview/storage-snapshot-contract'
46
48
  import type {
47
49
  BridgeClaimResult,
48
50
  BridgeSimInfo,
@@ -74,6 +76,14 @@ interface CreateCloudBoxOptions {
74
76
  /** the `--assets-dest` directory Metro wrote beside the bundle. */
75
77
  assetsPath?: string
76
78
  apiOrigin?: string
79
+ /** the name this box carries in the account's box list. */
80
+ boxName?: string | null
81
+ /**
82
+ * the storage and route the first simulator boots with, captured from a
83
+ * running app. a snapshot reaches a megabyte, so a create that carries one
84
+ * is multipart rather than a bare artifact body.
85
+ */
86
+ storage?: StorageSnapshot | null
77
87
  }
78
88
 
79
89
  interface CloudCommandEnvelope {
@@ -200,11 +210,27 @@ async function fetchBundleSource(bundleUrl: string): Promise<string> {
200
210
  return text
201
211
  }
202
212
 
213
+ // a bundle served over plain http is only trustworthy when it comes from this
214
+ // machine, which is what "send to box" reads: the Metro dev server the app is
215
+ // already running on.
216
+ function isLoopbackBundleUrl(bundleInput: string): boolean {
217
+ if (!bundleInput.startsWith('http://')) return false
218
+ try {
219
+ return isLoopbackHost(new URL(bundleInput).hostname)
220
+ } catch {
221
+ return false
222
+ }
223
+ }
224
+
225
+ function isRemoteBundleInput(bundleInput: string): boolean {
226
+ return bundleInput.startsWith('https://') || bundleInput.startsWith('http://')
227
+ }
228
+
203
229
  async function loadBundleSource(bundleInput: string): Promise<string> {
204
- if (bundleInput.startsWith('http://')) {
230
+ if (bundleInput.startsWith('http://') && !isLoopbackBundleUrl(bundleInput)) {
205
231
  throw new Error(`${REMOTE_COMMAND} bundle URL must use HTTPS`)
206
232
  }
207
- if (bundleInput.startsWith('https://')) {
233
+ if (isRemoteBundleInput(bundleInput)) {
208
234
  return fetchBundleSource(bundleInput)
209
235
  }
210
236
  return readImmutableBundleSource(bundleInput)
@@ -606,7 +632,7 @@ export async function produceCloudArtifact(
606
632
  // has nothing to search for.
607
633
  const search: MetroAssetSearch = assetsPath
608
634
  ? explicitMetroAssetDest(resolve(assetsPath), descriptors)
609
- : descriptors.length > 0 && !bundlePath.startsWith('https://')
635
+ : descriptors.length > 0 && !isRemoteBundleInput(bundlePath)
610
636
  ? searchMetroAssetDest(bundlePath, descriptors)
611
637
  : { dest: null, searched: [], truncated: false }
612
638
  const dest = search.dest
@@ -772,20 +798,36 @@ export async function createCloudBox(options: CreateCloudBoxOptions): Promise<{
772
798
  }> {
773
799
  const apiOrigin = resolveCloudOrigin(options.apiOrigin)
774
800
  const artifact = await produceCloudArtifact(options.bundlePath, options.assetsPath)
801
+ const boxName = options.boxName?.trim()
802
+ const headers: Record<string, string> = {
803
+ authorization: options.authorization,
804
+ 'x-rnx-box-size': 'nano',
805
+ 'x-rnx-artifact-sha256': artifact.sha256,
806
+ 'x-rnx-platform': 'ios',
807
+ 'x-rnx-device': options.device,
808
+ ...(options.accountId ? { 'x-rnx-account-id': options.accountId } : null),
809
+ ...(boxName ? { 'x-rnx-box-name': boxName } : null),
810
+ }
811
+ let body: BodyInit
812
+ if (options.storage) {
813
+ const form = new FormData()
814
+ form.append(
815
+ 'artifact',
816
+ new Blob([new Uint8Array(artifact.bytes)], { type: 'application/javascript' }),
817
+ 'bundle.js',
818
+ )
819
+ form.append('storage', JSON.stringify(options.storage))
820
+ body = form
821
+ } else {
822
+ headers['content-type'] = 'application/javascript'
823
+ body = new Uint8Array(artifact.bytes)
824
+ }
775
825
  let response: Response
776
826
  try {
777
827
  response = await fetch(`${apiOrigin}/v1/boxes`, {
778
828
  method: 'POST',
779
- headers: {
780
- authorization: options.authorization,
781
- 'content-type': 'application/javascript',
782
- 'x-rnx-box-size': 'nano',
783
- 'x-rnx-artifact-sha256': artifact.sha256,
784
- 'x-rnx-platform': 'ios',
785
- 'x-rnx-device': options.device,
786
- ...(options.accountId ? { 'x-rnx-account-id': options.accountId } : {}),
787
- },
788
- body: new Uint8Array(artifact.bytes),
829
+ headers,
830
+ body,
789
831
  })
790
832
  } catch (error) {
791
833
  throw new Error(
@@ -27,7 +27,7 @@ import {
27
27
  resolveCloudBoxReference,
28
28
  } from 'rnx-cloud-box/client'
29
29
  import { rnxPublicBrand } from '../../src/public-brand'
30
- import { authHeaderOrExit, cloudAccountIdOrExit } from '../auth'
30
+ import { authHeaderOrExit, cloudAccountIdOrExit, type CliAuth } from '../auth'
31
31
  import { findRepositoryPackageRoot } from '../setup-repository'
32
32
  import { resolvePlacement } from '../ws-bridge'
33
33
  import { CheckoutFilePlane, CheckoutRequiresGitError } from './box/checkout-plane'
@@ -69,6 +69,9 @@ function usage(): void {
69
69
  ${rnx} box create --json create without entering the shell
70
70
  ${rnx} box connect <id> enter the shell of a cloud box you own
71
71
  ${rnx} box connect <id> --json print its reference without a shell
72
+ ${rnx} box send send the running local app to a nano box
73
+ ${rnx} box send --name=<n> name the box it creates
74
+ ${rnx} box send --no-auth leave the signed-in session behind
72
75
  ${rnx} box open <id> watch a cloud box in your browser
73
76
  ${rnx} box open <id> --print print the viewing URL instead of opening it
74
77
  ${rnx} box ls cloud boxes running on this account
@@ -105,6 +108,7 @@ export async function runBox(
105
108
  const subcommand = args.find((arg) => !arg.startsWith('-'))
106
109
  if (subcommand === 'create') return runCloudBoxCreate(args, opts)
107
110
  if (subcommand === 'connect') return runCloudBoxConnect(args)
111
+ if (subcommand === 'send') return runBoxSend(args, opts)
108
112
  if (subcommand === 'open') return runCloudBoxOpen(args)
109
113
  if (subcommand === 'stop' || subcommand === 'delete') {
110
114
  return runCloudBoxStop(args, subcommand === 'delete')
@@ -138,12 +142,15 @@ function flag(args: string[], name: string): string | undefined {
138
142
  return undefined
139
143
  }
140
144
 
141
- function apiOrigin(args: string[]): string {
145
+ // the account service, which owns /api/v1/rnx/sessions and the box connect
146
+ // route. a login session names the origin it was issued by; an api key
147
+ // carries none, so it goes where every other key-authenticated command goes.
148
+ function apiOrigin(args: string[], auth: CliAuth): string {
142
149
  return (
143
150
  flag(args, 'api-origin') ??
144
151
  process.env[API_ORIGIN_ENV]?.trim() ??
145
- rnxPublicBrand.origin
146
- )
152
+ (auth.kind === 'session' ? auth.origin : 'https://contrast.dev')
153
+ ).replace(/\/+$/, '')
147
154
  }
148
155
 
149
156
  function requireEndpoint(args: string[], label: string): string | null {
@@ -157,6 +164,46 @@ function requireEndpoint(args: string[], label: string): string | null {
157
164
  return null
158
165
  }
159
166
 
167
+ // `rnx box send` — the CLI half of "send to box". the shell's rail button runs
168
+ // the same function inside the daemon; this is the same send for an agent or a
169
+ // terminal, and it prints the id and URL either way.
170
+ async function runBoxSend(args: string[], opts: BoxCommandOptions): Promise<number> {
171
+ const rnx = rnxPublicBrand.commandName
172
+ const { auth, header } = authHeaderOrExit('box send')
173
+ const accountId = cloudAccountIdOrExit(auth)
174
+ const { createBridgeFromParsed, parseBridgeCliArgs } = await import('../ws-bridge')
175
+ const parsed = parseBridgeCliArgs(
176
+ args.filter((arg) => arg !== 'send'),
177
+ {
178
+ port: opts.port,
179
+ stripBooleanFlags: ['--no-auth', '--json'],
180
+ stripValueFlags: ['--name', '--assets', '--api-origin'],
181
+ },
182
+ )
183
+ const bridge = createBridgeFromParsed(parsed)
184
+ try {
185
+ const { sendToBox } = await import('../send-to-box')
186
+ const result = await sendToBox({
187
+ send: (command) => bridge.send(command),
188
+ authorization: header,
189
+ accountId,
190
+ carryAuth: !args.includes('--no-auth'),
191
+ boxName: flag(args, 'name') ?? null,
192
+ assetsPath: flag(args, 'assets') ?? null,
193
+ apiOrigin: flag(args, 'api-origin'),
194
+ })
195
+ if (args.includes('--json')) console.log(JSON.stringify(result))
196
+ return 0
197
+ } catch (error) {
198
+ console.error(
199
+ ` ${rnx} box send: ${error instanceof Error ? error.message : String(error)}`,
200
+ )
201
+ return 1
202
+ } finally {
203
+ bridge.close()
204
+ }
205
+ }
206
+
160
207
  async function runCloudBoxStop(args: string[], purge: boolean): Promise<number> {
161
208
  const rnx = rnxPublicBrand.commandName
162
209
  const label = purge ? 'delete' : 'stop'
@@ -174,7 +221,7 @@ async function runCloudBoxStop(args: string[], purge: boolean): Promise<number>
174
221
  const client = createBoxClient(
175
222
  createCloudBoxProvider({
176
223
  endpoint,
177
- authority: { kind: 'account', apiOrigin: apiOrigin(args), apiKey, accountId },
224
+ authority: { kind: 'account', apiOrigin: apiOrigin(args, auth), apiKey, accountId },
178
225
  }),
179
226
  )
180
227
  try {
@@ -204,7 +251,7 @@ async function runCloudBoxList(args: string[]): Promise<number> {
204
251
  const client = createBoxClient(
205
252
  createCloudBoxProvider({
206
253
  endpoint: flag(args, 'endpoint') ?? process.env[ENDPOINT_ENV]?.trim() ?? '',
207
- authority: { kind: 'account', apiOrigin: apiOrigin(args), apiKey, accountId },
254
+ authority: { kind: 'account', apiOrigin: apiOrigin(args, auth), apiKey, accountId },
208
255
  }),
209
256
  )
210
257
  try {
@@ -244,9 +291,9 @@ async function runCloudBoxConnect(args: string[]): Promise<number> {
244
291
  // the ACCOUNT's credential. the account service is the only thing that knows
245
292
  // which account a Box belongs to, so it resolves the name and hands back the
246
293
  // Box's own token; nothing here writes that token down.
247
- const { header } = authHeaderOrExit('box connect')
294
+ const { auth, header } = authHeaderOrExit('box connect')
248
295
  const apiKey = header.replace(/^Bearer /, '')
249
- const origin = apiOrigin(args)
296
+ const origin = apiOrigin(args, auth)
250
297
 
251
298
  let session: BoxShellSession | null = null
252
299
  let heartbeat: { stop: () => void } | null = null
@@ -318,12 +365,12 @@ async function runCloudBoxOpen(args: string[]): Promise<number> {
318
365
  // the ACCOUNT's credential, for the same reason connect takes one: the
319
366
  // account service is what knows which account owns this Box, and it hands
320
367
  // back the Box's own token rather than this process storing one.
321
- const { header } = authHeaderOrExit('box open')
368
+ const { auth, header } = authHeaderOrExit('box open')
322
369
  const apiKey = header.replace(/^Bearer /, '')
323
370
 
324
371
  try {
325
372
  const reference = await resolveCloudBoxReference({
326
- apiOrigin: apiOrigin(args),
373
+ apiOrigin: apiOrigin(args, auth),
327
374
  apiKey,
328
375
  boxId,
329
376
  })
@@ -443,7 +490,7 @@ async function runCloudBoxCreate(
443
490
  const client = createBoxClient(
444
491
  createCloudBoxProvider({
445
492
  endpoint,
446
- authority: { kind: 'account', apiOrigin: apiOrigin(args), apiKey, accountId },
493
+ authority: { kind: 'account', apiOrigin: apiOrigin(args, auth), apiKey, accountId },
447
494
  }),
448
495
  )
449
496
  let session: BoxShellSession | null = null
@@ -487,7 +534,7 @@ async function runCloudBoxCreate(
487
534
  const sessionId = box.description.sessionId
488
535
  if (!sessionId) throw new BoxClientError('the box service returned no session id')
489
536
  heartbeat = heartbeatSession({
490
- apiOrigin: apiOrigin(args),
537
+ apiOrigin: apiOrigin(args, auth),
491
538
  apiKey,
492
539
  sessionId,
493
540
  })
@@ -642,15 +689,30 @@ async function repl(
642
689
  let inputEnded = false
643
690
  // the user typed `exit`: abandon anything still queued behind it.
644
691
  let stopped = false
692
+ // the command running right now, so Ctrl-C interrupts it rather than the CLI.
693
+ let interrupt: AbortController | null = null
645
694
 
646
695
  const run = async (command: string) => {
647
696
  if (stopped) return
697
+ const controller = new AbortController()
698
+ interrupt = controller
648
699
  try {
649
700
  await beforeCommand?.()
650
- const result = await session.exec(command)
651
- if (result.text && result.text !== '(no output)') console.log(result.text)
701
+ const result = await session.exec(command, {
702
+ signal: controller.signal,
703
+ // written as it arrives rather than collected: a terminal shows a
704
+ // command's output while it runs, and stderr keeps its own stream so
705
+ // piping the shell's stdout somewhere still separates the two.
706
+ onOutput: (stream, chunk) => {
707
+ if (stream === 'stdout') process.stdout.write(chunk)
708
+ else process.stderr.write(chunk)
709
+ },
710
+ })
711
+ if (result.exitCode !== 0) console.log(`exit code: ${result.exitCode}`)
652
712
  } catch (error) {
653
713
  console.error(error instanceof Error ? error.message : String(error))
714
+ } finally {
715
+ interrupt = null
654
716
  }
655
717
  if (!inputEnded && !stopped) prompt()
656
718
  }
@@ -658,6 +720,19 @@ async function repl(
658
720
  return new Promise<number>((resolve) => {
659
721
  prompt()
660
722
 
723
+ // Ctrl-C while a command is running interrupts the command. at an idle
724
+ // prompt there is nothing to interrupt, so it still leaves the shell,
725
+ // which is the only way out of a box that has stopped answering.
726
+ rl.on('SIGINT', () => {
727
+ if (interrupt) {
728
+ process.stdout.write('^C\n')
729
+ interrupt.abort()
730
+ return
731
+ }
732
+ stopped = true
733
+ rl.close()
734
+ })
735
+
661
736
  rl.on('line', (line) => {
662
737
  const command = line.trim()
663
738
  if (!command) {
@@ -2526,7 +2526,14 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2526
2526
  rnxExit(1)
2527
2527
  }
2528
2528
  }
2529
- // step 3: type through the visible keyboard so key pop animations render
2529
+ // step 3: clear existing text then type through the visible keyboard
2530
+ const existingText =
2531
+ typeof keyboardState.focusedInput?.text === 'string'
2532
+ ? keyboardState.focusedInput.text
2533
+ : ''
2534
+ for (let i = 0; i < existingText.length; i++) {
2535
+ await bridge.send({ type: 'keyboard', action: 'press', key: 'delete' })
2536
+ }
2530
2537
  await bridge.send({ type: 'keyboard', action: 'type', text })
2531
2538
  const secureTextEntry =
2532
2539
  targetSecureTextEntry || isSecureKeyboardState(keyboardState as any)
@@ -237,6 +237,11 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
237
237
  },
238
238
  'packages/sootsim/scripts/smoke-cli-binary.ts :: fetch :: `http://127.0.0.1:${port}/healthz`':
239
239
  { category: 'local_development', count: 1 },
240
+ // the binary smoke's replacement-module probe against its own loopback server
241
+ 'packages/sootsim/scripts/smoke-cli-binary.ts :: fetch :: url': {
242
+ category: 'local_development',
243
+ count: 1,
244
+ },
240
245
  'packages/sootsim-engine/src/auth/openLogin.ts :: fetch :: `${CONTRAST_ORIGIN}/api/dev-login`':
241
246
  { category: 'contrast_user_action', count: 1 },
242
247
  'packages/sootsim-engine/src/auth/shared-session.ts :: fetch :: `${CONTRAST_ORIGIN}/api/auth/me`':
@@ -0,0 +1,112 @@
1
+ // "send to box": the running local app becomes a nano cloud simulator.
2
+ //
3
+ // one function serves both callers. the rail button in the shell reaches it
4
+ // through the local daemon's `/__send-to-box` route, and `rnx box send` calls
5
+ // it directly; both hand it a way to run a bridge command against the sim that
6
+ // is running the app, and both get the same simulator id and URL back.
7
+
8
+ import { isStorageSnapshot } from '../../sootsim-engine/src/preview/storage-snapshot-contract'
9
+ import {
10
+ DEFAULT_DEVICE_BY_PLATFORM,
11
+ getRuntimePlatform,
12
+ listDeviceModels,
13
+ } from '../../sootsim-engine/src/settings/devices'
14
+ import { toRNXProductionBundleUrl } from '../src/metro-production-bundle'
15
+ import { createCloudBox } from './cloud-client'
16
+ import type { StorageSnapshot } from '../../sootsim-engine/src/preview/storage-snapshot-contract'
17
+ import type { DeviceModel } from '../../sootsim-engine/src/settings/devices'
18
+ import type { BridgeCommandType } from '../src/bridge-contract'
19
+
20
+ export interface SendToBoxOptions {
21
+ /** runs one bridge command against the sim running the app. */
22
+ send: (command: { type: BridgeCommandType; [key: string]: unknown }) => Promise<unknown>
23
+ authorization: string
24
+ /** the account the box bills. an api key names its own, so it sends none. */
25
+ accountId: string | null
26
+ /** the cloud device model. defaults to the device the sim is running on. */
27
+ device?: DeviceModel
28
+ /**
29
+ * carry the signed-in session into the box. on by default in the shell: the
30
+ * box is the sender's own, and an app that boots signed out is not the app
31
+ * they were looking at.
32
+ */
33
+ carryAuth: boolean
34
+ boxName?: string | null
35
+ /** the `--assets-dest` directory Metro wrote, when the sender knows it. */
36
+ assetsPath?: string | null
37
+ apiOrigin?: string
38
+ simId?: string
39
+ }
40
+
41
+ export interface SendToBoxResult {
42
+ boxId: string
43
+ simId: string
44
+ url: string
45
+ carriedAuth: boolean
46
+ warnings: string[]
47
+ }
48
+
49
+ function isRecord(value: unknown): value is Record<string, unknown> {
50
+ return !!value && typeof value === 'object' && !Array.isArray(value)
51
+ }
52
+
53
+ // the box opens on the device the sender was looking at. a remote simulator is
54
+ // iOS only, so an Android sim sends to the default iPhone rather than refusing.
55
+ function cloudDevice(capturedModel: unknown): DeviceModel {
56
+ const captured = listDeviceModels().find((model) => model === capturedModel)
57
+ return captured && getRuntimePlatform(captured) === 'ios'
58
+ ? captured
59
+ : DEFAULT_DEVICE_BY_PLATFORM.ios
60
+ }
61
+
62
+ export async function sendToBox(options: SendToBoxOptions): Promise<SendToBoxResult> {
63
+ const captured: unknown = await options.send({
64
+ type: 'evaluate',
65
+ simId: options.simId,
66
+ code: '(typeof window.__sootsimCaptureBundle === "function") ? window.__sootsimCaptureBundle() : null',
67
+ })
68
+ const bundleUrl = isRecord(captured) ? captured.bundleUrl : null
69
+ if (!isRecord(captured) || typeof bundleUrl !== 'string' || !bundleUrl) {
70
+ throw new Error(
71
+ 'send to box needs a simulator running an app: this one has no bundle loaded',
72
+ )
73
+ }
74
+ // the snapshot is taken before the production bundle is fetched, so the box
75
+ // gets the state the app was in when the button was pressed rather than the
76
+ // state it drifted to while Metro rebuilt.
77
+ const dumped: unknown = await options.send({
78
+ type: 'storageSnapshot',
79
+ simId: options.simId,
80
+ storageSnapshotOptions: { includeCredentials: options.carryAuth },
81
+ })
82
+ if (!isStorageSnapshot(dumped)) {
83
+ throw new Error('the simulator returned a storage snapshot this build cannot read')
84
+ }
85
+ const storage: StorageSnapshot = dumped
86
+ const created = await createCloudBox({
87
+ bundlePath: toRNXProductionBundleUrl(bundleUrl),
88
+ assetsPath: options.assetsPath ?? undefined,
89
+ device:
90
+ options.device ??
91
+ cloudDevice(isRecord(captured.deviceSpec) ? captured.deviceSpec.model : null),
92
+ authorization: options.authorization,
93
+ accountId: options.accountId,
94
+ apiOrigin: options.apiOrigin,
95
+ boxName: options.boxName ?? null,
96
+ storage,
97
+ })
98
+ const simId = created.receipt.simulator.simId
99
+ const url = `${created.session.apiOrigin.replace(/\/$/, '')}/sim/${simId}`
100
+ // the local terminal is where an agent driving this checkout reads results,
101
+ // so the id and the URL land there whichever caller asked for the send.
102
+ console.log(` box simulator: ${simId}`)
103
+ console.log(` ${url}`)
104
+ for (const warning of created.warnings) console.warn(` warning: ${warning}`)
105
+ return {
106
+ boxId: created.receipt.boxId,
107
+ simId,
108
+ url,
109
+ carriedAuth: options.carryAuth,
110
+ warnings: created.warnings,
111
+ }
112
+ }
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
package/dist-lib/beta.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
package/dist-lib/beta.mjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/beta.ts
4
4
  var IS_BETA = true;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/bridge-contract.ts
4
4
  var SIM_LONG_PRESS_MAX_MS = 5e3;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/bridge-contract.ts
4
4
  var SIM_LONG_PRESS_DEFAULT_MS = 500;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/capture-contract.ts
4
4
  var RNX_SCREEN_CAPTURE_VERSION = 1;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/cloud-contract.ts
4
4
  var RNX_CLOUD_MAX_ARTIFACT_BYTES = 20 * 1024 * 1024;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
4
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.433 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.435 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;