rnxsim 0.1.519 → 0.1.521

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/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 +7 -2
  20. package/dist-lib/bridge-contract-input.mjs +7 -2
  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 +18 -2
  29. package/dist-lib/cloud.mjs +18 -2
  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 +44 -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 +766 -24
  60. package/dist-lib/vite.cjs +103 -30
  61. package/package.json +1 -1
  62. package/scripts/dev-server-scanner.ts +72 -5
  63. package/src/bridge-contract-input.ts +10 -0
  64. package/src/bridge-contract.ts +5 -0
  65. package/src/cloud-contract.ts +7 -1
  66. package/src/cloud.ts +45 -6
  67. package/src/connect.ts +1 -1
  68. package/src/menu.ts +44 -0
  69. package/src/swift.ts +2 -0
  70. package/src/vite-plugin-one.ts +12 -0
  71. package/src/vite-plugin-swift.ts +141 -39
@@ -14,8 +14,8 @@ import {
14
14
  } from 'sootsim-engine/settings'
15
15
  import { settingsStore } from 'sootsim-engine/settings/store'
16
16
  import { rnxPublicBrand } from '../../src/public-brand'
17
- import { UnsupportedRemoteHostError } from '../cloud-client'
18
17
  import { rethrowIfExit } from '../run-rnx'
18
+ import type { CloudSession } from '../cloud-session'
19
19
 
20
20
  const PLATFORM_LABEL: Record<RuntimePlatform, string> = {
21
21
  ios: 'iOS',
@@ -27,24 +27,23 @@ function isDeviceModel(value: string): value is DeviceModel {
27
27
  }
28
28
 
29
29
  // flags this command reads for itself rather than treating as the bundle.
30
- const REMOTE_VALUE_FLAGS = new Set(['--device', '--name', '--endpoint'])
30
+ const REMOTE_VALUE_FLAGS = new Set(['--device'])
31
31
 
32
32
  function remoteInputFromArgs(
33
33
  args: string[],
34
34
  platform: RuntimePlatform,
35
35
  ): {
36
36
  bundlePath: string
37
- name: string | null
37
+ reuse: boolean
38
+ fresh: boolean
38
39
  } {
39
40
  const positional: string[] = []
40
- let name: string | null = null
41
41
  for (let index = 0; index < args.length; index++) {
42
42
  const arg = args[index]
43
- if (arg === '--remote' || arg === '--nano') continue
43
+ if (arg === '--remote' || arg === '--reuse' || arg === '--new') continue
44
44
  if (REMOTE_VALUE_FLAGS.has(arg)) {
45
45
  const value = args[++index] ?? null
46
46
  if (!value) throw new Error(`rnx remote ${platform} ${arg} requires a value`)
47
- if (arg === '--name') name = value
48
47
  continue
49
48
  }
50
49
  const assigned = arg.match(/^(--[a-z-]+)=(.*)$/)
@@ -52,7 +51,6 @@ function remoteInputFromArgs(
52
51
  const value = assigned[2] ?? ''
53
52
  if (!value)
54
53
  throw new Error(`rnx remote ${platform} ${assigned[1]} requires a value`)
55
- if (assigned[1] === '--name') name = value
56
54
  continue
57
55
  }
58
56
  if (
@@ -74,28 +72,15 @@ function remoteInputFromArgs(
74
72
  }
75
73
  if (positional.length !== 1 || !positional[0]) {
76
74
  throw new Error(
77
- `usage: rnx remote ${platform} <local bundle.js|https://bundle-url> [--device <model>]`,
75
+ `usage: rnx remote ${platform} <local bundle.js|https://bundle-url> [--device <model>] [--reuse|--new]`,
78
76
  )
79
77
  }
80
- return { bundlePath: positional[0], name }
81
- }
82
-
83
- /**
84
- * the box's name, taken from what was built.
85
- *
86
- * a bundle arrives as a file or a URL and neither carries a name, so the last
87
- * path segment without its extension is the one thing both have. box names are
88
- * lowercase words joined by dashes, so anything else in it becomes a dash.
89
- */
90
- function boxNameForBundle(bundlePath: string): string {
91
- const segment = bundlePath.split(/[?#]/)[0]?.split('/').filter(Boolean).at(-1) ?? ''
92
- const stem = segment.replace(/\.[^.]+$/, '')
93
- const name = stem
94
- .toLowerCase()
95
- .replace(/[^a-z0-9]+/g, '-')
96
- .replace(/^-+|-+$/g, '')
97
- if (!name) throw new Error(`rnx remote could not name a box after ${bundlePath}`)
98
- return name
78
+ const reuse = args.includes('--reuse')
79
+ const fresh = args.includes('--new')
80
+ if (reuse && fresh) {
81
+ throw new Error(`rnx remote ${platform} cannot combine --reuse and --new`)
82
+ }
83
+ return { bundlePath: positional[0], reuse, fresh }
99
84
  }
100
85
 
101
86
  export async function runPlatformCommand(
@@ -144,112 +129,8 @@ export async function runPlatformCommand(
144
129
  }
145
130
 
146
131
  if (args.includes('--remote')) {
147
- if (platform === 'android' && args.includes('--nano')) {
148
- const error = new UnsupportedRemoteHostError('android', 'nano')
149
- console.error(` ${error.message}`)
150
- return 1
151
- }
152
132
  try {
153
- const input = remoteInputFromArgs(args, platform)
154
- if (!args.includes('--nano')) {
155
- return await runRemoteBox(input, args, platform, selectedDevice)
156
- }
157
- const {
158
- publishCloudSessionToShell,
159
- readCloudSession,
160
- requireCloudSessionShellUpdate,
161
- } = await import('../cloud-session')
162
- const descriptor = requireCloudSessionShellUpdate()
163
- const { authHeaderOrExit, cloudAccountIdOrExit } = await import('../auth')
164
- const { auth, header } = authHeaderOrExit(`${platform} --remote`)
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)
171
- const existingSession = readCloudSession()
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}`)
186
- console.log(
187
- ' commands: describe, find, get tree|node|count|memory, wait ready|selector, do, reset, logs, state, screenshot',
188
- )
189
- return 0
190
- }
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
- }
215
- const created = await createCloudBox({
216
- bundlePath: input.bundlePath,
217
- device: selectedDevice,
218
- authorization: header,
219
- accountId,
220
- platform,
221
- })
222
- try {
223
- publishCloudSessionToShell(created.session, descriptor)
224
- } catch (error) {
225
- await closeCloudBox(created.session)
226
- throw error
227
- }
228
- const receipt = created.receipt
229
- const produced = created.artifact
230
- console.log(` remote simulator: ${receipt.simulator.simId}`)
231
- console.log(` platform: ${platform}`)
232
- console.log(` artifact: ${receipt.artifact.id} (${receipt.artifact.bytes} bytes)`)
233
- console.log(
234
- ` source bundle: sha256:${produced.source.sha256} (${produced.source.bytes} bytes)`,
235
- )
236
- console.log(
237
- ` normalized modules: ${produced.modules}; javascript ${produced.javascript.sourceBytes} -> ${produced.javascript.minifiedBytes} bytes`,
238
- )
239
- console.log(
240
- ` embedded assets: ${produced.assets.descriptors} descriptors, ${produced.assets.variants} scales, ${produced.assets.bytes} bytes (${produced.assets.encodedBytes} encoded)`,
241
- )
242
- // the dest directory was chosen by resolving this bundle's descriptors
243
- // against it, so say which one won rather than leaving it invisible.
244
- if (created.assetsDirectory) {
245
- console.log(` asset dest: ${created.assetsDirectory}`)
246
- }
247
- console.log(` claim expires: ${receipt.simulator.claim.expiresAt}`)
248
- console.log(
249
- ' commands: describe, find, get tree|node|count|memory, wait ready|selector, do, reset, logs, state, screenshot',
250
- )
251
- for (const warning of created.warnings) console.warn(` warning: ${warning}`)
252
- return 0
133
+ return await runRemoteCreate(platform, args, selectedDevice)
253
134
  } catch (error) {
254
135
  rethrowIfExit(error)
255
136
  console.error(
@@ -304,130 +185,107 @@ export function runRemotePlatformCommand(
304
185
  args: string[],
305
186
  opts: { port?: number } = {},
306
187
  ): Promise<number | undefined> {
307
- if (platform === 'android' && args.includes('--nano')) {
308
- const error = new UnsupportedRemoteHostError('android', 'nano')
309
- console.error(` ${error.message}`)
310
- return Promise.resolve(1)
311
- }
312
188
  return runPlatformCommand(platform, ['--remote', ...args], opts)
313
189
  }
314
190
 
191
+ const REMOTE_COMMANDS_HELP =
192
+ ' commands: describe, find, get tree|node|count|memory, wait ready|selector, do, reset, logs, state, screenshot'
193
+
315
194
  /**
316
- * `rnx ios --remote` and `rnx android --remote` the app runs in a Box.
317
- *
318
- * the artifact is produced here, self-contained, exactly as it always was: one
319
- * JavaScript file with its Metro assets embedded, so nothing it needs is left
320
- * on this machine. it is then a Box created from an already-built app, which
321
- * is what its page reads to boot the bundle instead of building one.
195
+ * `rnx remote ios|android <bundle>` on the one customer plane: one typed
196
+ * create through the instance API for both platforms, and a reattach to the
197
+ * shell's own simulator when it already runs this bundle. the box stays the
198
+ * host behind the API and is never created or addressed here.
322
199
  */
323
- async function runRemoteBox(
324
- input: { bundlePath: string; name: string | null },
200
+ async function runRemoteCreate(
201
+ platform: RuntimePlatform,
325
202
  args: string[],
326
- platform: RuntimePlatform = 'ios',
327
- selectedDevice?: string,
203
+ selectedDevice: string,
328
204
  ): Promise<number> {
329
- const command = `${platform} --remote`
205
+ const input = remoteInputFromArgs(args, platform)
330
206
  const [
331
- { produceCloudArtifact, closeCloudBox },
332
- box,
333
- { cloudBoxConnectUrl, ensureCloudBoxBrowser, readCloudBoxSession },
334
207
  { authHeaderOrExit, cloudAccountIdOrExit },
335
208
  {
336
- createCloudBoxSession,
209
+ claimCloudSimulator,
210
+ closeCloudSession,
211
+ createRemoteSim,
212
+ getRemoteSim,
213
+ produceCloudArtifact,
214
+ },
215
+ {
216
+ createCloudSession,
337
217
  publishCloudSessionToShell,
338
218
  readCloudSession,
339
- serializeCloudSession,
340
- RNX_CLOUD_SESSION_ENV,
219
+ requireCloudSessionShellUpdate,
341
220
  },
221
+ { openInBrowser },
342
222
  ] = await Promise.all([
343
- import('../cloud-client'),
344
- import('./box'),
345
- import('rnx-cloud-box/client'),
346
223
  import('../auth'),
224
+ import('../cloud-client'),
347
225
  import('../cloud-session'),
226
+ import('./box'),
348
227
  ])
349
- const artifact = await produceCloudArtifact(input.bundlePath, Number.POSITIVE_INFINITY)
350
- const endpoint = box.cloudBoxEndpoint(args, command)
351
- if (!endpoint) return 1
228
+ const artifact = await produceCloudArtifact(input.bundlePath)
229
+ const descriptor = requireCloudSessionShellUpdate()
230
+ const { auth, header } = authHeaderOrExit(`${platform} --remote`)
231
+ const accountId = cloudAccountIdOrExit(auth)
352
232
 
353
233
  const existingSession = readCloudSession()
354
234
  if (
355
235
  existingSession &&
356
- existingSession.service === 'box' &&
357
- existingSession.apiOrigin === endpoint &&
236
+ !input.fresh &&
358
237
  existingSession.platform === platform &&
359
- existingSession.device === selectedDevice &&
360
238
  existingSession.artifact?.sha256 === artifact.sha256
361
239
  ) {
362
- const live = await readCloudBoxSession({
363
- endpoint: existingSession.apiOrigin,
364
- boxId: existingSession.boxId,
365
- accessToken: existingSession.boxToken,
240
+ // an api key reads the simulator back through the account, which mints a
241
+ // fresh watch url with the read. any other credential rereads with the
242
+ // simulator's own token and keeps the stored watch url.
243
+ const live = await getRemoteSim({
244
+ apiOrigin: existingSession.apiOrigin,
245
+ simId: existingSession.simId,
246
+ authorization: auth.kind === 'api-key' ? header : `Bearer ${existingSession.token}`,
366
247
  })
367
- if (live.sessionId) {
368
- // the simulator lives in the box's Browser Run tab, which lapses on its
369
- // own, so a reconnect restores it before promising commands will answer.
370
- await ensureCloudBoxBrowser({
371
- connectUrl: cloudBoxConnectUrl(existingSession.apiOrigin, existingSession.boxId),
372
- accessToken: existingSession.boxToken,
248
+ if (live && (live.status === 'open' || live.status === 'hibernated')) {
249
+ const claimed = await claimCloudSimulator(existingSession)
250
+ const streamUrl = live.streamUrl ?? existingSession.streamUrl ?? null
251
+ const session = createCloudSession({
252
+ ...existingSession,
253
+ claimId: claimed.claim.id,
254
+ ...(streamUrl ? { streamUrl } : {}),
373
255
  })
256
+ publishCloudSessionToShell(session, descriptor)
374
257
  console.log(
375
- ` reconnected to remote simulator: ${existingSession.boxId} (${platform})`,
376
- )
377
- console.log(
378
- ' commands: describe, find, get tree|node|count|memory, wait ready|selector, do, reset, logs, state, screenshot',
258
+ ` reconnected to remote simulator: ${existingSession.simId} (${platform})`,
379
259
  )
260
+ console.log(` claim expires: ${claimed.claim.expiresAt}`)
261
+ console.log(REMOTE_COMMANDS_HELP)
380
262
  return 0
381
263
  }
382
264
  }
383
265
 
384
- const { auth, header } = authHeaderOrExit(command)
385
- const accountId = cloudAccountIdOrExit(auth)
386
-
387
- if (existingSession) {
388
- await closeCloudBox(existingSession)
389
- try {
390
- publishCloudSessionToShell(null)
391
- } catch {
392
- // no shell update channel is required outside an interactive rnx shell
393
- }
394
- }
395
- const created = await box.createPrebuiltBundleBox({
396
- bundle: new Uint8Array(artifact.bytes),
397
- name: input.name ?? boxNameForBundle(input.bundlePath),
398
- endpoint,
399
- apiOrigin: box.cloudBoxApiOrigin(args, auth),
400
- apiKey: header.replace(/^Bearer /, ''),
266
+ // the new session supersedes the old one, so the shell update channel
267
+ // carries exactly one update: the shell accepts nothing else.
268
+ if (existingSession) await closeCloudSession(existingSession)
269
+ const created = await createRemoteSim({
270
+ bundlePath: input.bundlePath,
271
+ device: selectedDevice,
272
+ authorization: header,
401
273
  accountId,
402
- })
403
- // creating a box starts no browser, and only its Browser Run tab hosts the
404
- // simulator, so without this the page opens onto a box nobody is running.
405
- await ensureCloudBoxBrowser({
406
- connectUrl: cloudBoxConnectUrl(endpoint, created.id),
407
- accessToken: created.accessToken ?? '',
408
- })
409
- const produced = artifact.receipt
410
- const session = createCloudBoxSession({
411
- boxId: created.id,
412
- boxToken: created.accessToken ?? '',
413
- apiOrigin: endpoint,
414
274
  platform,
415
- device: selectedDevice,
416
- artifact: {
417
- sha256: artifact.sha256,
418
- bytes: artifact.bytes.length,
419
- },
275
+ reuse: input.reuse,
420
276
  })
421
277
  try {
422
- publishCloudSessionToShell(session)
423
- } catch {
424
- // shell update fd not configured
278
+ publishCloudSessionToShell(created.session, descriptor)
279
+ } catch (error) {
280
+ await closeCloudSession(created.session)
281
+ throw error
425
282
  }
426
- process.env[RNX_CLOUD_SESSION_ENV] = serializeCloudSession(session)
427
-
428
- console.log(` box: ${created.id} (${artifact.bytes.length} bytes)`)
283
+ const receipt = created.receipt
284
+ const produced = created.artifact
285
+ console.log(` remote simulator: ${receipt.simId}${created.reused ? ' (reused)' : ''}`)
429
286
  console.log(` platform: ${platform}`)
430
- if (selectedDevice) console.log(` device: ${selectedDevice}`)
287
+ console.log(` device: ${selectedDevice}`)
288
+ console.log(` artifact: ${receipt.artifact.id} (${receipt.artifact.bytes} bytes)`)
431
289
  console.log(
432
290
  ` source bundle: sha256:${produced.source.sha256} (${produced.source.bytes} bytes)`,
433
291
  )
@@ -439,20 +297,114 @@ async function runRemoteBox(
439
297
  )
440
298
  // the dest directory was chosen by resolving this bundle's descriptors
441
299
  // against it, so say which one won rather than leaving it invisible.
442
- if (artifact.assetsDirectory) console.log(` asset dest: ${artifact.assetsDirectory}`)
443
- for (const warning of artifact.warnings) console.warn(` warning: ${warning}`)
444
- console.log(
445
- ' commands: describe, find, get tree|node|count|memory, wait ready|selector, do, reset, logs, state, screenshot',
446
- )
447
- if (!created.description.pageUrl) {
448
- console.error(' the box service returned no page to open')
300
+ if (created.assetsDirectory) {
301
+ console.log(` asset dest: ${created.assetsDirectory}`)
302
+ }
303
+ console.log(` claim expires: ${receipt.claim.expiresAt}`)
304
+ console.log(REMOTE_COMMANDS_HELP)
305
+ for (const warning of created.warnings) console.warn(` warning: ${warning}`)
306
+ if (!created.session.streamUrl) {
307
+ console.log(' the service minted no watch url for this simulator')
308
+ return 0
309
+ }
310
+ return openInBrowser(created.session.streamUrl)
311
+ }
312
+
313
+ /** `rnx remote list`: the simulators this account runs that `rnx remote` made. */
314
+ export async function runRemoteList(args: string[]): Promise<number> {
315
+ try {
316
+ let json = false
317
+ for (const arg of args) {
318
+ if (arg === '--json') {
319
+ json = true
320
+ continue
321
+ }
322
+ throw new Error(`usage: rnx remote list [--json]`)
323
+ }
324
+ const [{ authHeaderOrExit, cloudAccountIdOrExit }, { listRemoteSims }] =
325
+ await Promise.all([import('../auth'), import('../cloud-client')])
326
+ const { auth, header } = authHeaderOrExit('remote list')
327
+ const accountId = cloudAccountIdOrExit(auth)
328
+ const sims = await listRemoteSims({ authorization: header, accountId })
329
+ if (json) {
330
+ console.log(JSON.stringify({ sims }))
331
+ return 0
332
+ }
333
+ if (sims.length === 0) {
334
+ console.log(' no remote simulators are running')
335
+ return 0
336
+ }
337
+ for (const sim of sims) {
338
+ const platformLabel = sim.labels['rnx.platform'] ?? 'unknown'
339
+ console.log(
340
+ ` ${sim.simId} ${sim.status} ${platformLabel} sha256:${sim.artifact.sha256.slice(0, 12)}`,
341
+ )
342
+ }
343
+ return 0
344
+ } catch (error) {
345
+ rethrowIfExit(error)
346
+ console.error(
347
+ ` ${rnxPublicBrand.commandName} remote list failed: ${
348
+ error instanceof Error ? error.message : String(error)
349
+ }`,
350
+ )
351
+ return 1
352
+ }
353
+ }
354
+
355
+ /** `rnx remote stop <id>`: delete one remote simulator by id. */
356
+ export async function runRemoteStop(args: string[]): Promise<number> {
357
+ try {
358
+ const positional = args.filter((arg) => !arg.startsWith('-'))
359
+ if (
360
+ args.some((arg) => arg.startsWith('-')) ||
361
+ positional.length !== 1 ||
362
+ !positional[0]
363
+ ) {
364
+ throw new Error(`usage: rnx remote stop <sim-id>`)
365
+ }
366
+ const simId = positional[0]
367
+ const [
368
+ { authHeaderOrExit, cloudAccountIdOrExit },
369
+ { deleteRemoteSim },
370
+ { publishCloudSessionToShell, readCloudSession, requireCloudSessionShellUpdate },
371
+ ] = await Promise.all([
372
+ import('../auth'),
373
+ import('../cloud-client'),
374
+ import('../cloud-session'),
375
+ ])
376
+ let current: CloudSession | null = null
377
+ try {
378
+ current = readCloudSession()
379
+ } catch {
380
+ current = null
381
+ }
382
+ // stopping the shell's own simulator clears the shell with it, so the
383
+ // update channel is required before anything is deleted.
384
+ const descriptor = current?.simId === simId ? requireCloudSessionShellUpdate() : null
385
+ const { auth, header } = authHeaderOrExit('remote stop')
386
+ const accountId = cloudAccountIdOrExit(auth)
387
+ const deleted = await deleteRemoteSim({
388
+ simId,
389
+ authorization: header,
390
+ accountId,
391
+ })
392
+ if (!deleted) {
393
+ console.error(
394
+ ` ${rnxPublicBrand.commandName} remote stop: ${simId} does not exist`,
395
+ )
396
+ return 1
397
+ }
398
+ if (descriptor !== null) publishCloudSessionToShell(null, descriptor)
399
+ console.log(` stopped remote simulator: ${deleted.simId}`)
400
+ return 0
401
+ } catch (error) {
402
+ rethrowIfExit(error)
403
+ console.error(
404
+ ` ${rnxPublicBrand.commandName} remote stop failed: ${
405
+ error instanceof Error ? error.message : String(error)
406
+ }`,
407
+ )
449
408
  return 1
450
409
  }
451
- const pageUrl = new URL(created.description.pageUrl)
452
- pageUrl.searchParams.set(
453
- 'device',
454
- selectedDevice ?? DEFAULT_DEVICE_BY_PLATFORM[platform],
455
- )
456
- console.log(` open ${pageUrl}`)
457
- return box.openInBrowser(pageUrl.toString())
458
410
  }
package/cli/main.ts CHANGED
@@ -242,6 +242,7 @@ const port = parsed.globalFlags['port'] as number | undefined
242
242
  const verbose = parsed.verbose
243
243
  const device = parsed.globalFlags['device'] as string | undefined
244
244
  const theme = parsed.globalFlags['theme'] as string | undefined
245
+ const homeScreen = parsed.globalFlags['home-screen'] as string | undefined
245
246
  const driver = parsed.globalFlags['driver'] as string | undefined
246
247
  const headless = (parsed.globalFlags['headless'] as boolean | undefined) === true
247
248
  const globalSimTarget = (parsed.globalFlags['sim'] ??
@@ -251,12 +252,13 @@ const commandArgsWithSim = globalSimTarget
251
252
  ? ['--sim', globalSimTarget, ...parsed.commandArgs]
252
253
  : parsed.commandArgs
253
254
 
254
- // apply device/theme to settings store if provided
255
- if (device || theme) {
255
+ // apply device/theme/home-screen to settings store if provided
256
+ if (device || theme || homeScreen) {
256
257
  const { settingsStore } = await import('sootsim-engine/settings/store')
257
258
  const overrides: Record<string, any> = {}
258
259
  if (device) overrides.deviceModel = device
259
260
  if (theme) overrides.colorScheme = theme
261
+ if (homeScreen) overrides.homeScreenVersion = homeScreen
260
262
  settingsStore.apply(overrides)
261
263
  }
262
264
 
@@ -515,6 +517,12 @@ try {
515
517
  { port },
516
518
  )
517
519
  await exitWithFlush(typeof code === 'number' ? code : 0)
520
+ } else if (platform === 'list') {
521
+ const { runRemoteList } = await import('./commands/platform')
522
+ await exitWithFlush(await runRemoteList(parsed.commandArgs.slice(1)))
523
+ } else if (platform === 'stop') {
524
+ const { runRemoteStop } = await import('./commands/platform')
525
+ await exitWithFlush(await runRemoteStop(parsed.commandArgs.slice(1)))
518
526
  } else {
519
527
  console.error(
520
528
  ` usage: ${rnxPublicBrand.commandName} remote ios|android <bundle.js|https://bundle-url>`,
@@ -140,18 +140,33 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
140
140
  category: 'rnx_cloud_sim',
141
141
  count: 1,
142
142
  },
143
- 'packages/sootsim/cli/cloud-client.ts :: fetch :: `${session.apiOrigin}/v1/boxes/${encodeURIComponent(session.boxId)}/simulators`':
144
- { category: 'rnx_cloud_sim', count: 1 },
145
143
  'packages/sootsim/cli/cloud-client.ts :: fetch :: `${session.apiOrigin}/v1/sims/${encodeURIComponent(session.simId)}/claim`':
146
144
  { category: 'rnx_cloud_sim', count: 1 },
147
- 'packages/sootsim/cli/cloud-client.ts :: fetch :: `${session.apiOrigin}/v1/boxes/${encodeURIComponent(session.boxId)}`':
145
+ // closing a nano-box simulator deletes its box; the local names changed
146
+ // with the close but the destination did not.
147
+ 'packages/sootsim/cli/cloud-client.ts :: fetch :: `${session.apiOrigin}/v1/boxes/${encodeURIComponent(boxId)}`':
148
148
  { category: 'rnx_cloud_sim', count: 1 },
149
- 'packages/sootsim/cli/cloud-client.ts :: fetch :: `${session.apiOrigin}/v1/boxes/${encodeURIComponent(session.boxId)}/stop`':
149
+ // closing an instance simulator deletes it by its own id with its own token.
150
+ 'packages/sootsim/cli/cloud-client.ts :: fetch :: `${session.apiOrigin}/v1/sims/${encodeURIComponent(session.simId)}`':
150
151
  { category: 'rnx_cloud_sim', count: 1 },
152
+ // `rnx remote` creates through the instance api and lists what it made.
153
+ // the create carries labels and wait=true in the query; the list filters
154
+ // by the remote label.
155
+ 'packages/sootsim/cli/cloud-client.ts :: fetch :: `${apiOrigin}/v1/sims?${query}`': {
156
+ category: 'rnx_cloud_sim',
157
+ count: 1,
158
+ },
159
+ 'packages/sootsim/cli/cloud-client.ts :: fetch :: `${apiOrigin}/v1/sims?${selector}`': {
160
+ category: 'rnx_cloud_sim',
161
+ count: 1,
162
+ },
163
+ // rereading one simulator and deleting one by id share the route: the
164
+ // reattach probe reads with either credential and `rnx remote stop`
165
+ // deletes with the account credential.
166
+ 'packages/sootsim/cli/cloud-client.ts :: fetch :: `${apiOrigin}/v1/sims/${encodeURIComponent(options.simId)}`':
167
+ { category: 'rnx_cloud_sim', count: 2 },
151
168
  'packages/sootsim/cli/cloud-client.ts :: fetch :: `${this.session.apiOrigin}/v1/sims/${encodeURIComponent(this.session.simId)}/commands`':
152
169
  { category: 'rnx_cloud_sim', count: 1 },
153
- 'packages/sootsim/cli/cloud-client.ts :: fetch :: `${this.session.apiOrigin}/v1/boxes/${encodeURIComponent(this.session.boxId)}/commands`':
154
- { category: 'rnx_cloud_sim', count: 1 },
155
170
  'packages/sootsim/cli/cloud-client.ts :: fetch :: bundleUrl': {
156
171
  category: 'guest_app_network',
157
172
  count: 1,
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.519 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.521 | (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.519 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.521 | (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.519 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.521 | (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.519 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.521 | (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.519 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.521 | (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.519 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.521 | (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.519 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.521 | (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.519 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.521 | (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.519 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.521 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/beta.ts
4
4
  var IS_BETA = true;