rnxsim 0.1.442 → 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.
- package/cli/bin.ts +3 -1
- package/cli/cloud-client.ts +153 -17
- package/cli/cloud-dispatch.ts +15 -0
- package/cli/cloud-session.ts +69 -17
- package/cli/commands/box/bundle-plane.ts +71 -0
- package/cli/commands/box.ts +274 -115
- package/cli/commands/control.ts +1 -137
- package/cli/commands/flow.ts +1 -1
- package/cli/commands/inspect/list.ts +119 -2
- package/cli/commands/inspect.ts +3 -2
- package/cli/commands/platform.ts +95 -12
- package/cli/commands/reset.ts +7 -3
- package/cli/commands/state.ts +7 -3
- package/cli/flow-export.ts +55 -0
- package/cli/flow-session.ts +0 -47
- package/cli/outbound-endpoints.ts +5 -0
- package/cli/rnx-in-process.ts +98 -0
- package/cli/ws-bridge.ts +30 -0
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +69 -14
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +17 -1
- package/dist-lib/host/bridge-host.cjs +24 -13
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +1 -3
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +6 -19
- package/dist-lib/menu.mjs +6 -19
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +274 -195
- package/dist-lib/vite.cjs +1 -1
- package/package.json +8 -1
- package/src/home-paths.ts +31 -1
- package/src/menu.ts +4 -17
package/cli/commands/control.ts
CHANGED
|
@@ -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,
|
package/cli/commands/flow.ts
CHANGED
|
@@ -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 {
|
|
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
|
+
}
|
package/cli/commands/inspect.ts
CHANGED
|
@@ -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',
|
package/cli/commands/platform.ts
CHANGED
|
@@ -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
|
|
42
|
-
index
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
}
|
package/cli/commands/reset.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { printBridgeFailureDiagnostics } from '../bridge-diagnostics'
|
|
2
|
-
import {
|
|
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',
|
package/cli/commands/state.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { printBridgeFailureDiagnostics } from '../bridge-diagnostics'
|
|
2
|
-
import {
|
|
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
|
+
}
|
package/cli/flow-session.ts
CHANGED
|
@@ -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': {
|