rnxsim 0.1.441 → 0.1.443

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/bin.ts +3 -1
  2. package/cli/cloud-client.ts +153 -17
  3. package/cli/cloud-dispatch.ts +15 -0
  4. package/cli/cloud-session.ts +69 -17
  5. package/cli/commands/box/bundle-plane.ts +71 -0
  6. package/cli/commands/box.ts +274 -115
  7. package/cli/commands/control.ts +1 -137
  8. package/cli/commands/flow.ts +1 -1
  9. package/cli/commands/inspect/list.ts +119 -2
  10. package/cli/commands/inspect.ts +3 -2
  11. package/cli/commands/platform.ts +95 -12
  12. package/cli/commands/reset.ts +7 -3
  13. package/cli/commands/state.ts +7 -3
  14. package/cli/flow-export.ts +55 -0
  15. package/cli/flow-session.ts +0 -47
  16. package/cli/outbound-endpoints.ts +5 -0
  17. package/cli/rnx-in-process.ts +98 -0
  18. package/cli/ws-bridge.ts +30 -0
  19. package/dist-lib/agent-daemon-client.cjs +1 -1
  20. package/dist-lib/agent-events.cjs +1 -1
  21. package/dist-lib/agent-identity.cjs +1 -1
  22. package/dist-lib/agent-sessions.cjs +1 -1
  23. package/dist-lib/attached-projects.cjs +1 -1
  24. package/dist-lib/auth/shared-session.cjs +1 -1
  25. package/dist-lib/backend-origin.cjs +1 -1
  26. package/dist-lib/beta.cjs +1 -1
  27. package/dist-lib/beta.mjs +1 -1
  28. package/dist-lib/bridge-constants.cjs +1 -1
  29. package/dist-lib/bridge-contract-input.cjs +1 -1
  30. package/dist-lib/bridge-contract-input.mjs +1 -1
  31. package/dist-lib/bridge-contract.cjs +1 -1
  32. package/dist-lib/bridge-contract.mjs +1 -1
  33. package/dist-lib/capture-contract.cjs +1 -1
  34. package/dist-lib/capture-contract.mjs +1 -1
  35. package/dist-lib/cli-constants.cjs +1 -1
  36. package/dist-lib/cloud-contract.cjs +1 -1
  37. package/dist-lib/cloud-contract.mjs +1 -1
  38. package/dist-lib/cloud.cjs +1 -1
  39. package/dist-lib/cloud.mjs +1 -1
  40. package/dist-lib/config.cjs +1 -1
  41. package/dist-lib/detox/index.cjs +69 -14
  42. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  43. package/dist-lib/home-paths.cjs +17 -1
  44. package/dist-lib/host/bridge-host.cjs +24 -13
  45. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  46. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  47. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  48. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  49. package/dist-lib/host/websocket-proxy.cjs +1 -1
  50. package/dist-lib/index.cjs +1 -3
  51. package/dist-lib/jump-to-source-babel.cjs +1 -1
  52. package/dist-lib/menu.cjs +6 -19
  53. package/dist-lib/menu.mjs +6 -19
  54. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  55. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  56. package/dist-lib/metro-production-bundle.cjs +1 -1
  57. package/dist-lib/metro-production-bundle.mjs +1 -1
  58. package/dist-lib/metro.cjs +1 -1
  59. package/dist-lib/profiles.cjs +1 -1
  60. package/dist-lib/public-brand.cjs +1 -1
  61. package/dist-lib/react-native-host-modules.cjs +1 -1
  62. package/dist-lib/react-native-host-modules.mjs +1 -1
  63. package/dist-lib/render-mode.cjs +1 -1
  64. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  65. package/dist-lib/sdk.cjs +1 -1
  66. package/dist-lib/sdk.mjs +1 -1
  67. package/dist-lib/skills.cjs +274 -195
  68. package/dist-lib/vite.cjs +1 -1
  69. package/package.json +8 -1
  70. package/src/home-paths.ts +31 -1
  71. package/src/menu.ts +4 -17
@@ -64,6 +64,7 @@ import {
64
64
  BridgeSimLockedError,
65
65
  createBridge,
66
66
  createBridgeFromParsed,
67
+ formatLockOwner,
67
68
  parseBridgeCliArgs,
68
69
  printBridgeWorldNotice,
69
70
  resolveBridgePortForPin,
@@ -865,143 +866,6 @@ async function waitForSimReady(
865
866
  return null
866
867
  }
867
868
 
868
- // returns a short, human-friendly description of what a sim is showing —
869
- // e.g. "bundle host/...path", "connect :8081", "demo bluesky". returns null
870
- // when there's no useful summary beyond the raw URL itself, so the caller
871
- // can skip the redundant `loaded:` line in that case.
872
- export function summarizeSimUrl(url: string | undefined): string | null {
873
- if (!url) return null
874
- try {
875
- const parsed = new URL(url)
876
- if (parsed.searchParams.has('bundle')) {
877
- const bundleUrl = parsed.searchParams.get('bundle') || ''
878
- try {
879
- const bundle = new URL(bundleUrl)
880
- const shortPath =
881
- bundle.pathname.length > 36
882
- ? `...${bundle.pathname.slice(-36)}`
883
- : bundle.pathname
884
- return `bundle ${bundle.host}${shortPath}`
885
- } catch {
886
- return 'bundle'
887
- }
888
- }
889
- if (parsed.searchParams.has('port')) {
890
- return `connect :${parsed.searchParams.get('port') || ''}`
891
- }
892
- if (parsed.searchParams.has('open')) {
893
- return `connect ${parsed.searchParams.get('open') || ''}`
894
- }
895
- if (parsed.searchParams.has('demo')) {
896
- return `demo ${parsed.searchParams.get('demo') || 'default'}`
897
- }
898
- if (
899
- parsed.pathname.includes('/sootsim/index.html') ||
900
- parsed.pathname === '/sootsim/' ||
901
- parsed.pathname === '/sootsim'
902
- ) {
903
- return 'embedded rnx'
904
- }
905
- return null
906
- } catch {
907
- return null
908
- }
909
- }
910
-
911
- // a lock owner is either a friendly cli label ("active user") or a raw cli
912
- // identity key ("CLAUDE_CODE_SESSION_ID:<uuid>", "gppid-1234"). keep the list
913
- // readable by collapsing a "<source>:<value>" identity to a short form that
914
- // still says which agent holds it.
915
- function formatLockOwner(owner: string): string {
916
- const colon = owner.indexOf(':')
917
- if (colon <= 0) return owner.length > 24 ? `${owner.slice(0, 21)}…` : owner
918
- const source = owner.slice(0, colon)
919
- const value = owner.slice(colon + 1)
920
- const short = value.length > 12 ? `${value.slice(0, 8)}…` : value
921
- if (
922
- source === 'TM_SESSION' ||
923
- source === 'CLAUDE_CODE_SESSION_ID' ||
924
- source === 'CODEX_THREAD_ID'
925
- ) {
926
- return `agent ${short}`
927
- }
928
- return `${source} ${short}`
929
- }
930
-
931
- export function printConnectedSims(sims: BridgeSimInfo[], selectedId?: string) {
932
- if (sims.length === 0) {
933
- console.log(' no sims connected')
934
- return
935
- }
936
- console.log(` connected sims (${sims.length}):\n`)
937
- for (const sim of sims) {
938
- const lockTag =
939
- sim.lockedBy && sim.lockExpiresAt
940
- ? `locked by ${formatLockOwner(sim.lockedBy)} (${Math.max(0, Math.round((sim.lockExpiresAt - Date.now()) / 1000))}s)`
941
- : ''
942
- const tags = [
943
- sim.isPrimary ? 'primary' : '',
944
- sim.id === selectedId ? 'selected' : '',
945
- sim.readyState,
946
- sim.userVisible === false ? 'hidden' : sim.userVisible === true ? 'visible' : '',
947
- sim.attachedCliCount && sim.attachedCliCount > 0 ? 'in use' : '',
948
- sim.userFocused ? 'focused' : '',
949
- lockTag,
950
- ].filter(Boolean)
951
- console.log(` ${sim.id}${tags.length ? ` [${tags.join(', ')}]` : ''}`)
952
- const loaded = summarizeSimUrl(sim.url)
953
- if (loaded) {
954
- console.log(` loaded: ${loaded}`)
955
- } else if (sim.url) {
956
- // no friendly summary — show the raw url so the caller can still copy it.
957
- // truncate aggressively so a 400-char metro url doesn't dominate the list.
958
- const shown = sim.url.length > 96 ? `${sim.url.slice(0, 93)}…` : sim.url
959
- console.log(` url: ${shown}`)
960
- } else if (sim.origin) {
961
- console.log(` origin: ${sim.origin}`)
962
- }
963
- const engine = describeEngineSource(sim)
964
- if (engine) console.log(` engine: ${engine}`)
965
- if (sim.title) console.log(` title: ${sim.title}`)
966
- if (sim.visibilityState && sim.visibilityState !== 'visible') {
967
- console.log(` visibility: ${sim.visibilityState}`)
968
- }
969
- console.log(` connected: ${formatRelativeAge(Date.now() - sim.connectedAt)}`)
970
- if (sim.lastActiveAt && sim.lastActiveAt > 0) {
971
- console.log(` last active: ${formatRelativeAge(Date.now() - sim.lastActiveAt)}`)
972
- }
973
- }
974
- }
975
-
976
- // classify which engine a sim is running so dev-vs-published is never ambiguous
977
- // to agents or humans reading `rnx list`. dev shell serves local source on
978
- // :5173; the Contrast app embeds rnx on :3000; the bridge/daemon and the
979
- // /__soot plugin serve the active runtime dir — which, inside a dev checkout, is
980
- // the fresh dev-stack build (resolveDevCheckoutRuntimeRoot mode boundary), not a
981
- // published runtime. keep this label honest so it never says "published" while
982
- // actually serving local build output.
983
- function describeEngineSource(sim: { url?: string; origin?: string }): string | null {
984
- const ref = sim.url || sim.origin || ''
985
- if (!ref) return null
986
- try {
987
- const port = new URL(ref).port
988
- if (port === '5173') return 'dev — local source (:5173)'
989
- if (port === '3000') return 'Contrast app embedded (:3000)'
990
- if (resolveDevCheckoutRuntimeRoot()) return `dev — local build (:${port || '?'})`
991
- return `runtime — published engine (:${port || '?'})`
992
- } catch {
993
- return null
994
- }
995
- }
996
-
997
- function formatRelativeAge(ms: number): string {
998
- const sec = Math.max(0, Math.round(ms / 1000))
999
- if (sec < 60) return `${sec}s ago`
1000
- if (sec < 3600) return `${Math.round(sec / 60)}m ago`
1001
- if (sec < 86400) return `${Math.round(sec / 3600)}h ago`
1002
- return `${Math.round(sec / 86400)}d ago`
1003
- }
1004
-
1005
869
  export async function waitForSimMatch(
1006
870
  wsPort: number,
1007
871
  commandTimeoutMs: number,
@@ -16,10 +16,10 @@ import {
16
16
  } from '../bridge-flow-runner'
17
17
  import { readCurrentSimId, saveCurrentSimId } from '../current-sim'
18
18
  import { findDesktopCompanion } from '../desktop-companion'
19
+ import { exportFlowSession } from '../flow-export'
19
20
  import { parseFlowFile, validateFlowFile } from '../flow-file'
20
21
  import { FlowLiveStatusReporter } from '../flow-live-status'
21
22
  import {
22
- exportFlowSession,
23
23
  finalizeFlowSession,
24
24
  keepFlowCandidate,
25
25
  startFlowSession,
@@ -1,8 +1,8 @@
1
1
  // `rnx list` — connected sims, with the active one marked.
2
2
 
3
- import { printConnectedSims } from '../control'
3
+ import { resolveDevCheckoutRuntimeRoot } from '../../../src/runtime-assets'
4
+ import { type BridgeSimInfo, formatLockOwner, type WsBridge } from '../../ws-bridge'
4
5
  import { printJson, wantsJson } from './shared'
5
- import type { WsBridge } from '../../ws-bridge'
6
6
 
7
7
  export async function runListSubcommand(opts: {
8
8
  bridge: WsBridge
@@ -56,3 +56,120 @@ export async function runListSubcommand(opts: {
56
56
  console.log(` (${sims.length - filtered.length} more hidden — pass --all to show)`)
57
57
  }
58
58
  }
59
+
60
+ // returns a short, human-friendly description of what a sim is showing —
61
+ // e.g. "bundle host/...path", "connect :8081", "demo bluesky". returns null
62
+ // when there's no useful summary beyond the raw URL itself, so the caller
63
+ // can skip the redundant `loaded:` line in that case.
64
+ function summarizeSimUrl(url: string | undefined): string | null {
65
+ if (!url) return null
66
+ try {
67
+ const parsed = new URL(url)
68
+ if (parsed.searchParams.has('bundle')) {
69
+ const bundleUrl = parsed.searchParams.get('bundle') || ''
70
+ try {
71
+ const bundle = new URL(bundleUrl)
72
+ const shortPath =
73
+ bundle.pathname.length > 36
74
+ ? `...${bundle.pathname.slice(-36)}`
75
+ : bundle.pathname
76
+ return `bundle ${bundle.host}${shortPath}`
77
+ } catch {
78
+ return 'bundle'
79
+ }
80
+ }
81
+ if (parsed.searchParams.has('port')) {
82
+ return `connect :${parsed.searchParams.get('port') || ''}`
83
+ }
84
+ if (parsed.searchParams.has('open')) {
85
+ return `connect ${parsed.searchParams.get('open') || ''}`
86
+ }
87
+ if (parsed.searchParams.has('demo')) {
88
+ return `demo ${parsed.searchParams.get('demo') || 'default'}`
89
+ }
90
+ if (
91
+ parsed.pathname.includes('/sootsim/index.html') ||
92
+ parsed.pathname === '/sootsim/' ||
93
+ parsed.pathname === '/sootsim'
94
+ ) {
95
+ return 'embedded rnx'
96
+ }
97
+ return null
98
+ } catch {
99
+ return null
100
+ }
101
+ }
102
+
103
+ function printConnectedSims(sims: BridgeSimInfo[], selectedId?: string) {
104
+ if (sims.length === 0) {
105
+ console.log(' no sims connected')
106
+ return
107
+ }
108
+ console.log(` connected sims (${sims.length}):\n`)
109
+ for (const sim of sims) {
110
+ const lockTag =
111
+ sim.lockedBy && sim.lockExpiresAt
112
+ ? `locked by ${formatLockOwner(sim.lockedBy)} (${Math.max(0, Math.round((sim.lockExpiresAt - Date.now()) / 1000))}s)`
113
+ : ''
114
+ const tags = [
115
+ sim.isPrimary ? 'primary' : '',
116
+ sim.id === selectedId ? 'selected' : '',
117
+ sim.readyState,
118
+ sim.userVisible === false ? 'hidden' : sim.userVisible === true ? 'visible' : '',
119
+ sim.attachedCliCount && sim.attachedCliCount > 0 ? 'in use' : '',
120
+ sim.userFocused ? 'focused' : '',
121
+ lockTag,
122
+ ].filter(Boolean)
123
+ console.log(` ${sim.id}${tags.length ? ` [${tags.join(', ')}]` : ''}`)
124
+ const loaded = summarizeSimUrl(sim.url)
125
+ if (loaded) {
126
+ console.log(` loaded: ${loaded}`)
127
+ } else if (sim.url) {
128
+ // no friendly summary — show the raw url so the caller can still copy it.
129
+ // truncate aggressively so a 400-char metro url doesn't dominate the list.
130
+ const shown = sim.url.length > 96 ? `${sim.url.slice(0, 93)}…` : sim.url
131
+ console.log(` url: ${shown}`)
132
+ } else if (sim.origin) {
133
+ console.log(` origin: ${sim.origin}`)
134
+ }
135
+ const engine = describeEngineSource(sim)
136
+ if (engine) console.log(` engine: ${engine}`)
137
+ if (sim.title) console.log(` title: ${sim.title}`)
138
+ if (sim.visibilityState && sim.visibilityState !== 'visible') {
139
+ console.log(` visibility: ${sim.visibilityState}`)
140
+ }
141
+ console.log(` connected: ${formatRelativeAge(Date.now() - sim.connectedAt)}`)
142
+ if (sim.lastActiveAt && sim.lastActiveAt > 0) {
143
+ console.log(` last active: ${formatRelativeAge(Date.now() - sim.lastActiveAt)}`)
144
+ }
145
+ }
146
+ }
147
+
148
+ // classify which engine a sim is running so dev-vs-published is never ambiguous
149
+ // to agents or humans reading `rnx list`. dev shell serves local source on
150
+ // :5173; the Contrast app embeds rnx on :3000; the bridge/daemon and the
151
+ // /__soot plugin serve the active runtime dir — which, inside a dev checkout, is
152
+ // the fresh dev-stack build (resolveDevCheckoutRuntimeRoot mode boundary), not a
153
+ // published runtime. keep this label honest so it never says "published" while
154
+ // actually serving local build output.
155
+ function describeEngineSource(sim: { url?: string; origin?: string }): string | null {
156
+ const ref = sim.url || sim.origin || ''
157
+ if (!ref) return null
158
+ try {
159
+ const port = new URL(ref).port
160
+ if (port === '5173') return 'dev — local source (:5173)'
161
+ if (port === '3000') return 'Contrast app embedded (:3000)'
162
+ if (resolveDevCheckoutRuntimeRoot()) return `dev — local build (:${port || '?'})`
163
+ return `runtime — published engine (:${port || '?'})`
164
+ } catch {
165
+ return null
166
+ }
167
+ }
168
+
169
+ function formatRelativeAge(ms: number): string {
170
+ const sec = Math.max(0, Math.round(ms / 1000))
171
+ if (sec < 60) return `${sec}s ago`
172
+ if (sec < 3600) return `${Math.round(sec / 60)}m ago`
173
+ if (sec < 86400) return `${Math.round(sec / 3600)}h ago`
174
+ return `${Math.round(sec / 86400)}d ago`
175
+ }
@@ -19,6 +19,7 @@ import { shouldPrintInspectNotice } from '../inspect-notice-state'
19
19
  import { TOP_LEVEL_RUNTIME_COMMANDS } from '../parse-args'
20
20
  import { rethrowIfExit, rnxExit } from '../run-rnx'
21
21
  import {
22
+ type BridgeTransportOption,
22
23
  callInBridge,
23
24
  callInBridgeWrite,
24
25
  checkSimHealth,
@@ -107,7 +108,7 @@ import type {
107
108
  SootSimScrollPerformanceTrace,
108
109
  } from '@rnx/globals'
109
110
 
110
- interface InspectOptions {
111
+ interface InspectOptions extends BridgeTransportOption {
111
112
  port?: number
112
113
  verbose?: boolean
113
114
  timeoutMs?: number
@@ -1202,7 +1203,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
1202
1203
  return
1203
1204
  }
1204
1205
 
1205
- const bridge = createBridgeFromParsed(parsed)
1206
+ const bridge = (opts.createBridge ?? createBridgeFromParsed)(parsed)
1206
1207
  const noticeScope = simId || 'default'
1207
1208
  const consoleSummarySkip = new Set([
1208
1209
  'errors',
@@ -29,27 +29,33 @@ function isDeviceModel(value: string): value is DeviceModel {
29
29
  const REMOTE_USAGE =
30
30
  'usage: rnx ios --remote <local bundle.js|https://bundle-url> [--assets <dir>] [--device <model>]'
31
31
 
32
+ // flags this command reads for itself rather than treating as the bundle.
33
+ const REMOTE_VALUE_FLAGS = new Set(['--device', '--assets', '--name', '--endpoint'])
34
+
32
35
  function remoteInputFromArgs(args: string[]): {
33
36
  bundlePath: string
34
37
  assetsPath: string | null
38
+ name: string | null
35
39
  } {
36
40
  const positional: string[] = []
37
41
  let assetsPath: string | null = null
42
+ let name: string | null = null
38
43
  for (let index = 0; index < args.length; index++) {
39
44
  const arg = args[index]
40
- if (arg === '--remote') continue
41
- if (arg === '--device') {
42
- index++
43
- continue
44
- }
45
- if (arg === '--assets') {
46
- assetsPath = args[++index] ?? null
47
- if (!assetsPath) throw new Error('rnx ios --remote --assets requires a directory')
45
+ if (arg === '--remote' || arg === '--nano') continue
46
+ if (REMOTE_VALUE_FLAGS.has(arg)) {
47
+ const value = args[++index] ?? null
48
+ if (!value) throw new Error(`rnx ios --remote ${arg} requires a value`)
49
+ if (arg === '--assets') assetsPath = value
50
+ if (arg === '--name') name = value
48
51
  continue
49
52
  }
50
- if (arg.startsWith('--assets=')) {
51
- assetsPath = arg.slice('--assets='.length) || null
52
- if (!assetsPath) throw new Error('rnx ios --remote --assets requires a directory')
53
+ const assigned = arg.match(/^(--[a-z-]+)=(.*)$/)
54
+ if (assigned && REMOTE_VALUE_FLAGS.has(assigned[1] ?? '')) {
55
+ const value = assigned[2] ?? ''
56
+ if (!value) throw new Error(`rnx ios --remote ${assigned[1]} requires a value`)
57
+ if (assigned[1] === '--assets') assetsPath = value
58
+ if (assigned[1] === '--name') name = value
53
59
  continue
54
60
  }
55
61
  if (
@@ -68,7 +74,25 @@ function remoteInputFromArgs(args: string[]): {
68
74
  positional.push(arg)
69
75
  }
70
76
  if (positional.length !== 1 || !positional[0]) throw new Error(REMOTE_USAGE)
71
- return { bundlePath: positional[0], assetsPath }
77
+ return { bundlePath: positional[0], assetsPath, name }
78
+ }
79
+
80
+ /**
81
+ * the box's name, taken from what was built.
82
+ *
83
+ * a bundle arrives as a file or a URL and neither carries a name, so the last
84
+ * path segment without its extension is the one thing both have. box names are
85
+ * lowercase words joined by dashes, so anything else in it becomes a dash.
86
+ */
87
+ function boxNameForBundle(bundlePath: string): string {
88
+ const segment = bundlePath.split(/[?#]/)[0]?.split('/').filter(Boolean).at(-1) ?? ''
89
+ const stem = segment.replace(/\.[^.]+$/, '')
90
+ const name = stem
91
+ .toLowerCase()
92
+ .replace(/[^a-z0-9]+/g, '-')
93
+ .replace(/^-+|-+$/g, '')
94
+ if (!name) throw new Error(`rnx ios --remote could not name a box after ${bundlePath}`)
95
+ return name
72
96
  }
73
97
 
74
98
  export async function runPlatformCommand(
@@ -123,6 +147,7 @@ export async function runPlatformCommand(
123
147
  }
124
148
  try {
125
149
  const input = remoteInputFromArgs(args)
150
+ if (!args.includes('--nano')) return await runRemoteBox(input, args)
126
151
  const {
127
152
  publishCloudSessionToShell,
128
153
  readCloudSession,
@@ -232,3 +257,61 @@ export async function runPlatformCommand(
232
257
  const { runOpenCommand } = await import('./control')
233
258
  return runOpenCommand(args, { port: opts.port })
234
259
  }
260
+
261
+ /**
262
+ * `rnx ios --remote` — the app runs in a Box.
263
+ *
264
+ * the artifact is produced here, self-contained, exactly as it always was: one
265
+ * JavaScript file with its Metro assets embedded, so nothing it needs is left
266
+ * on this machine. it is then a Box created from an already-built app, which
267
+ * is what its page reads to boot the bundle instead of building one.
268
+ */
269
+ async function runRemoteBox(
270
+ input: { bundlePath: string; assetsPath: string | null; name: string | null },
271
+ args: string[],
272
+ ): Promise<number> {
273
+ const command = 'ios --remote'
274
+ const [{ produceCloudArtifact }, box, { authHeaderOrExit, cloudAccountIdOrExit }] =
275
+ await Promise.all([import('../cloud-client'), import('./box'), import('../auth')])
276
+ const endpoint = box.cloudBoxEndpoint(args, command)
277
+ if (!endpoint) return 1
278
+ const { auth, header } = authHeaderOrExit(command)
279
+ const accountId = cloudAccountIdOrExit(auth)
280
+ // the Box holds the bundle in parts, so the size a single message can carry
281
+ // is not what limits this one and there is no cap to apply here.
282
+ const artifact = await produceCloudArtifact(
283
+ input.bundlePath,
284
+ input.assetsPath ?? undefined,
285
+ Number.POSITIVE_INFINITY,
286
+ )
287
+ const created = await box.createPrebuiltBundleBox({
288
+ bundle: new Uint8Array(artifact.bytes),
289
+ name: input.name ?? boxNameForBundle(input.bundlePath),
290
+ endpoint,
291
+ apiOrigin: box.cloudBoxApiOrigin(args, auth),
292
+ apiKey: header.replace(/^Bearer /, ''),
293
+ accountId,
294
+ })
295
+ const produced = artifact.receipt
296
+ console.log(` box: ${created.id} (${artifact.bytes.length} bytes)`)
297
+ console.log(
298
+ ` source bundle: sha256:${produced.source.sha256} (${produced.source.bytes} bytes)`,
299
+ )
300
+ console.log(
301
+ ` normalized modules: ${produced.modules}; javascript ${produced.javascript.sourceBytes} -> ${produced.javascript.minifiedBytes} bytes`,
302
+ )
303
+ console.log(
304
+ ` embedded assets: ${produced.assets.descriptors} descriptors, ${produced.assets.variants} scales, ${produced.assets.bytes} bytes (${produced.assets.encodedBytes} encoded)`,
305
+ )
306
+ // the dest directory was chosen by resolving this bundle's descriptors
307
+ // against it, so say which one won rather than leaving it invisible.
308
+ if (artifact.assetsDirectory) console.log(` asset dest: ${artifact.assetsDirectory}`)
309
+ for (const warning of artifact.warnings) console.warn(` warning: ${warning}`)
310
+ const pageUrl = created.description.pageUrl
311
+ if (!pageUrl) {
312
+ console.error(' the box service returned no page to open')
313
+ return 1
314
+ }
315
+ console.log(` open ${pageUrl}`)
316
+ return box.openInBrowser(pageUrl)
317
+ }
@@ -1,5 +1,9 @@
1
1
  import { printBridgeFailureDiagnostics } from '../bridge-diagnostics'
2
- import { createBridgeFromParsed, parseBridgeCliArgs } from '../ws-bridge'
2
+ import {
3
+ type BridgeTransportOption,
4
+ createBridgeFromParsed,
5
+ parseBridgeCliArgs,
6
+ } from '../ws-bridge'
3
7
  import type { ResetResult } from '../../src/bridge-contract'
4
8
 
5
9
  function isResetResult(value: unknown): value is ResetResult {
@@ -30,7 +34,7 @@ options:
30
34
 
31
35
  export async function runReset(
32
36
  args: string[],
33
- opts: { port?: number } = {},
37
+ opts: { port?: number } & BridgeTransportOption = {},
34
38
  ): Promise<number> {
35
39
  if (args.includes('--help') || args.includes('-h')) {
36
40
  printHelp()
@@ -46,7 +50,7 @@ export async function runReset(
46
50
  return 1
47
51
  }
48
52
 
49
- const bridge = createBridgeFromParsed(parsed)
53
+ const bridge = (opts.createBridge ?? createBridgeFromParsed)(parsed)
50
54
  try {
51
55
  const value: unknown = await bridge.send({
52
56
  type: 'reset',
@@ -1,5 +1,9 @@
1
1
  import { printBridgeFailureDiagnostics } from '../bridge-diagnostics'
2
- import { createBridgeFromParsed, parseBridgeCliArgs } from '../ws-bridge'
2
+ import {
3
+ type BridgeTransportOption,
4
+ createBridgeFromParsed,
5
+ parseBridgeCliArgs,
6
+ } from '../ws-bridge'
3
7
  import { formatSemanticTree, isSimSemanticTree } from './inspect/core'
4
8
  import type { SimStateResult } from '../../src/bridge-contract'
5
9
 
@@ -29,7 +33,7 @@ options:
29
33
 
30
34
  export async function runState(
31
35
  args: string[],
32
- opts: { port?: number } = {},
36
+ opts: { port?: number } & BridgeTransportOption = {},
33
37
  ): Promise<number> {
34
38
  if (args.includes('--help') || args.includes('-h')) {
35
39
  printHelp()
@@ -45,7 +49,7 @@ export async function runState(
45
49
  return 1
46
50
  }
47
51
 
48
- const bridge = createBridgeFromParsed(parsed)
52
+ const bridge = (opts.createBridge ?? createBridgeFromParsed)(parsed)
49
53
  try {
50
54
  const value: unknown = await bridge.send({ type: 'state' })
51
55
  if (!isSimStateResult(value)) {
@@ -0,0 +1,55 @@
1
+ // turning a recorded flow draft into a maestro-compatible YAML file.
2
+ //
3
+ // this is separate from flow-session.ts, the draft store, because the two have
4
+ // different reach. every `do` verb writes a candidate into the store, so the
5
+ // store has to load wherever the CLI's runtime commands load, including inside
6
+ // a box shell running in workerd, where the `yaml` package does not load at
7
+ // all. YAML formatting and writing the file are the export step, and only
8
+ // `rnx flow` and `rnx maestro` ever reach it.
9
+
10
+ import { mkdirSync, writeFileSync } from 'node:fs'
11
+ import { dirname, resolve } from 'node:path'
12
+ import yaml from 'yaml'
13
+ import { validateFlowSteps } from './flow-file'
14
+ import { finalizeFlowSession, readFlowSession } from './flow-session'
15
+
16
+ export function exportFlowSession(outputPath?: string) {
17
+ const state = readFlowSession()
18
+ if (!state) return { active: false as const }
19
+
20
+ const yamlText = `${yaml.stringify(state.steps).trimEnd()}\n`
21
+ const issues = validateFlowSteps(state.steps)
22
+ if (issues.length > 0) {
23
+ return {
24
+ active: true as const,
25
+ valid: false as const,
26
+ issues,
27
+ yaml: yamlText,
28
+ stepCount: state.steps.length,
29
+ outputPath: null,
30
+ }
31
+ }
32
+
33
+ let resolvedOutputPath: string | null = null
34
+ if (outputPath) {
35
+ resolvedOutputPath = resolve(outputPath)
36
+ mkdirSync(dirname(resolvedOutputPath), { recursive: true })
37
+ writeFileSync(resolvedOutputPath, yamlText)
38
+ }
39
+
40
+ return {
41
+ active: true as const,
42
+ valid: true as const,
43
+ issues: [],
44
+ yaml: yamlText,
45
+ stepCount: state.steps.length,
46
+ outputPath: resolvedOutputPath,
47
+ }
48
+ }
49
+
50
+ export function endFlowSession(outputPath?: string) {
51
+ const result = exportFlowSession(outputPath)
52
+ if (!result.active) return result
53
+ finalizeFlowSession()
54
+ return result
55
+ }
@@ -1,8 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'
2
2
  import { tmpdir } from 'os'
3
3
  import { dirname, join, resolve } from 'path'
4
- import yaml from 'yaml'
5
- import { validateFlowSteps } from './flow-file'
6
4
 
7
5
  export type FlowDraftStep = Record<string, unknown>
8
6
 
@@ -140,48 +138,3 @@ export function keepFlowCandidate() {
140
138
  stepCount: next.steps.length,
141
139
  }
142
140
  }
143
-
144
- export function formatFlowDraftYaml(steps: FlowDraftStep[]) {
145
- return yaml.stringify(steps).trimEnd() + '\n'
146
- }
147
-
148
- export function exportFlowSession(outputPath?: string) {
149
- const state = readFlowSession()
150
- if (!state) return { active: false as const }
151
-
152
- const yamlText = formatFlowDraftYaml(state.steps)
153
- const issues = validateFlowSteps(state.steps)
154
- if (issues.length > 0) {
155
- return {
156
- active: true as const,
157
- valid: false as const,
158
- issues,
159
- yaml: yamlText,
160
- stepCount: state.steps.length,
161
- outputPath: null,
162
- }
163
- }
164
-
165
- let resolvedOutputPath: string | null = null
166
- if (outputPath) {
167
- resolvedOutputPath = resolve(outputPath)
168
- mkdirSync(dirname(resolvedOutputPath), { recursive: true })
169
- writeFileSync(resolvedOutputPath, yamlText)
170
- }
171
-
172
- return {
173
- active: true as const,
174
- valid: true as const,
175
- issues: [],
176
- yaml: yamlText,
177
- stepCount: state.steps.length,
178
- outputPath: resolvedOutputPath,
179
- }
180
- }
181
-
182
- export function endFlowSession(outputPath?: string) {
183
- const result = exportFlowSession(outputPath)
184
- if (!result.active) return result
185
- finalizeFlowSession()
186
- return result
187
- }
@@ -140,6 +140,8 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
140
140
  { category: 'rnx_cloud_sim', count: 1 },
141
141
  'packages/sootsim/cli/cloud-client.ts :: fetch :: `${this.session.apiOrigin}/v1/sims/${encodeURIComponent(this.session.simId)}/commands`':
142
142
  { category: 'rnx_cloud_sim', count: 1 },
143
+ 'packages/sootsim/cli/cloud-client.ts :: fetch :: `${this.session.apiOrigin}/v1/boxes/${encodeURIComponent(this.session.boxId)}/commands`':
144
+ { category: 'rnx_cloud_sim', count: 1 },
143
145
  'packages/sootsim/cli/cloud-client.ts :: fetch :: bundleUrl': {
144
146
  category: 'guest_app_network',
145
147
  count: 1,
@@ -351,6 +353,9 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
351
353
  // `/deps-native/engine-assets/` directory.
352
354
  'packages/sootsim-engine/src/headless/browser-tenant-worker.ts :: fetch :: new URL(name, assetBase).href':
353
355
  { category: 'same_origin_runtime', count: 1 },
356
+ // hashes its own delivered script to identify the engine to display clients.
357
+ 'packages/sootsim-engine/src/headless/browser-tenant-worker.ts :: fetch :: self.location.href':
358
+ { category: 'same_origin_runtime', count: 1 },
354
359
  // the tenant build's built-in app chunk prefetch list and the chunk bodies
355
360
  // it names, resolved against the worker chunk's own url.
356
361
  'packages/sootsim-engine/src/render-worker/app-in-worker.tsx :: fetch :: listUrl': {