rnxsim 0.1.381 → 0.1.382
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/README.md +19 -6
- package/cli/app-fonts.ts +1 -1
- package/cli/bin.ts +118 -26
- package/cli/bridge-diagnostics.ts +1 -1
- package/cli/bridge-flow-runner.ts +9 -9
- package/cli/browser-evals.ts +3 -3
- package/cli/cli-update.ts +261 -0
- package/cli/cloud-client.ts +407 -0
- package/cli/cloud-dispatch.ts +165 -0
- package/cli/cloud-session.ts +156 -0
- package/cli/command-contract.ts +2 -2
- package/cli/command-registry.ts +9 -0
- package/cli/commands/agent-wrapper.ts +4 -4
- package/cli/commands/agent.ts +3 -3
- package/cli/commands/box/checkout-plane.ts +16 -8
- package/cli/commands/box/rnx-command.ts +2 -2
- package/cli/commands/box/search-command.ts +2 -2
- package/cli/commands/box.ts +123 -70
- package/cli/commands/config.ts +3 -2
- package/cli/commands/control.ts +6 -6
- package/cli/commands/cpu-profile.ts +2 -2
- package/cli/commands/daemon.ts +7 -7
- package/cli/commands/debug.ts +2 -2
- package/cli/commands/detox.ts +7 -7
- package/cli/commands/diagnose.ts +1 -1
- package/cli/commands/electron.ts +1 -1
- package/cli/commands/film.ts +1 -1
- package/cli/commands/flow.ts +4 -4
- package/cli/commands/inspect/core.ts +58 -7
- package/cli/commands/inspect/env.ts +5 -3
- package/cli/commands/inspect/shared.ts +2 -1
- package/cli/commands/inspect/wait-event.ts +1 -1
- package/cli/commands/inspect/wait-ready.ts +15 -5
- package/cli/commands/inspect.ts +31 -9
- package/cli/commands/install-desktop.ts +2 -2
- package/cli/commands/maestro.ts +3 -3
- package/cli/commands/permissions.ts +1 -1
- package/cli/commands/platform.ts +91 -0
- package/cli/commands/record.ts +3 -3
- package/cli/commands/report-issue.ts +1 -1
- package/cli/commands/runtime.ts +1 -1
- package/cli/commands/screenshot-mode.ts +1 -1
- package/cli/commands/screenshot.ts +25 -0
- package/cli/commands/serve.ts +1 -1
- package/cli/commands/skills.ts +1 -1
- package/cli/commands/timeline.ts +1 -1
- package/cli/commands/upgrade.ts +8 -34
- package/cli/commands/upload.ts +3 -3
- package/cli/commands/version.ts +14 -3
- package/cli/commands/what-happened.ts +2 -2
- package/cli/current-sim.ts +43 -30
- package/cli/desktop-companion.ts +1 -1
- package/cli/drivers/electron.ts +1 -1
- package/cli/drivers/playwright.ts +3 -3
- package/cli/drivers/types.ts +4 -4
- package/cli/flow-file.ts +3 -3
- package/cli/help-core.ts +18 -11
- package/cli/hidden-runtime-alias.ts +1 -1
- package/cli/hints.ts +2 -2
- package/cli/maestro-js.ts +2 -2
- package/cli/outbound-endpoints.ts +40 -6
- package/cli/parse-args.ts +1 -1
- package/cli/registry.ts +1 -1
- package/cli/resolve-assets.ts +2 -2
- package/cli/secure-file.ts +31 -0
- package/cli/shell-init.ts +98 -0
- package/cli/ws-bridge.ts +18 -8
- package/detox/expectations.ts +1 -1
- package/detox/gestures.ts +3 -3
- package/detox/index.ts +3 -3
- 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 +15 -8
- package/dist-lib/bridge-contract-input.mjs +15 -8
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +107 -0
- package/dist-lib/capture-contract.mjs +80 -0
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +32 -0
- package/dist-lib/cloud-contract.mjs +7 -0
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +4 -4
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +9 -1
- package/dist-lib/host/bridge-host.cjs +15 -10
- 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 +248 -73
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.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 +44 -6
- package/dist-lib/sdk.mjs +44 -6
- package/dist-lib/skills.cjs +435 -155
- package/dist-lib/vite.cjs +1 -1
- package/package.json +20 -4
- package/scripts/dev-server-scanner.ts +3 -3
- package/skills/contrast/SKILL.md +1 -1
- package/skills/rnx-perf/SKILL.md +68 -52
- package/src/app-url.ts +2 -3
- package/src/bridge-contract-input.ts +17 -6
- package/src/bridge-contract.ts +25 -4
- package/src/capture-contract.ts +423 -0
- package/src/cloud-contract.ts +15 -0
- package/src/home-paths.ts +9 -3
- package/src/host/agent-host.ts +1 -1
- package/src/host/bridge-host.ts +13 -11
- package/src/host/plane-host.ts +2 -2
- package/src/index.ts +21 -0
- package/src/native-dev-bundle-url.ts +2 -2
- package/src/screenshots/orchestrate.ts +1 -1
- package/src/screenshots/org-deck.ts +1 -1
- package/src/settings-bundle.ts +1 -1
- package/src/sim-client.ts +4 -0
- package/src/vite-plugin-one.ts +2 -2
- package/src/vite-plugin.ts +1 -1
- package/cli/commands/box/cloud-box.ts +0 -294
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
2
|
+
import { closeSync, fstatSync, openSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { extname, resolve } from 'node:path'
|
|
4
|
+
import { assertSelfContainedMetroArtifactAssets } from '../../contrast-bundler/src/metroAssetArtifact'
|
|
5
|
+
import {
|
|
6
|
+
RNX_CLOUD_MAX_ARTIFACT_BYTES,
|
|
7
|
+
type RnxCloudCreateReceipt,
|
|
8
|
+
} from '../src/cloud-contract'
|
|
9
|
+
import { rnxPublicBrand } from '../src/public-brand'
|
|
10
|
+
import {
|
|
11
|
+
createCloudSession,
|
|
12
|
+
normalizeCloudOrigin,
|
|
13
|
+
readCloudSession,
|
|
14
|
+
type CloudSession,
|
|
15
|
+
} from './cloud-session'
|
|
16
|
+
import type {
|
|
17
|
+
BridgeClaimResult,
|
|
18
|
+
BridgeSimInfo,
|
|
19
|
+
ParsedBridgeCliArgs,
|
|
20
|
+
WsBridge,
|
|
21
|
+
} from './ws-bridge'
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_RNX_CLOUD_ORIGIN = rnxPublicBrand.origin
|
|
24
|
+
export const MAX_RNX_CLOUD_BUNDLE_BYTES = RNX_CLOUD_MAX_ARTIFACT_BYTES
|
|
25
|
+
|
|
26
|
+
interface CreateCloudSimulatorOptions {
|
|
27
|
+
bundlePath: string
|
|
28
|
+
device: string
|
|
29
|
+
authorization: string
|
|
30
|
+
apiOrigin?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface CloudCommandEnvelope {
|
|
34
|
+
id: string
|
|
35
|
+
claimId: string
|
|
36
|
+
command: Record<string, unknown>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
40
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function responseError(status: number, text: string): Error {
|
|
44
|
+
if (text) {
|
|
45
|
+
try {
|
|
46
|
+
const parsed: unknown = JSON.parse(text)
|
|
47
|
+
const error = isRecord(parsed) ? parsed.error : null
|
|
48
|
+
if (isRecord(error) && typeof error.message === 'string') {
|
|
49
|
+
return new Error(`RNX Cloud request failed (${status}): ${error.message}`)
|
|
50
|
+
}
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
53
|
+
return new Error(
|
|
54
|
+
`RNX Cloud request failed (${status})${text.trim() ? `: ${text.trim()}` : ''}`,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveCloudOrigin(value?: string): string {
|
|
59
|
+
const configured = value?.trim() || process.env.RNX_CLOUD_ORIGIN?.trim()
|
|
60
|
+
const source = configured || DEFAULT_RNX_CLOUD_ORIGIN
|
|
61
|
+
return normalizeCloudOrigin(source)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function readImmutableBundle(
|
|
65
|
+
bundlePath: string,
|
|
66
|
+
): Promise<{ bytes: Buffer; sha256: string }> {
|
|
67
|
+
const filepath = resolve(bundlePath)
|
|
68
|
+
if (extname(filepath) !== '.js') {
|
|
69
|
+
throw new Error('rnx ios --cloud requires one local Metro .js bundle')
|
|
70
|
+
}
|
|
71
|
+
const descriptor = openSync(filepath, 'r')
|
|
72
|
+
try {
|
|
73
|
+
const before = fstatSync(descriptor, { bigint: true })
|
|
74
|
+
if (!before.isFile()) {
|
|
75
|
+
throw new Error('rnx ios --cloud bundle must be a regular local file')
|
|
76
|
+
}
|
|
77
|
+
if (before.size <= 0n) {
|
|
78
|
+
throw new Error('rnx ios --cloud bundle is empty')
|
|
79
|
+
}
|
|
80
|
+
if (before.size > BigInt(MAX_RNX_CLOUD_BUNDLE_BYTES)) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`rnx ios --cloud bundle exceeds the ${MAX_RNX_CLOUD_BUNDLE_BYTES}-byte limit`,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
const bytes = readFileSync(descriptor)
|
|
86
|
+
const after = fstatSync(descriptor, { bigint: true })
|
|
87
|
+
if (
|
|
88
|
+
before.dev !== after.dev ||
|
|
89
|
+
before.ino !== after.ino ||
|
|
90
|
+
before.size !== after.size ||
|
|
91
|
+
before.mtimeNs !== after.mtimeNs ||
|
|
92
|
+
bytes.length !== Number(before.size)
|
|
93
|
+
) {
|
|
94
|
+
throw new Error('rnx ios --cloud bundle changed while it was being read')
|
|
95
|
+
}
|
|
96
|
+
let source: string
|
|
97
|
+
try {
|
|
98
|
+
source = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
|
|
99
|
+
} catch {
|
|
100
|
+
throw new Error('rnx ios --cloud bundle must be valid UTF-8 JavaScript')
|
|
101
|
+
}
|
|
102
|
+
if (source.includes('\0') || !source.includes('__d(') || !source.includes('__r(')) {
|
|
103
|
+
throw new Error('rnx ios --cloud requires a Metro JavaScript bundle')
|
|
104
|
+
}
|
|
105
|
+
await assertSelfContainedMetroArtifactAssets(source)
|
|
106
|
+
return {
|
|
107
|
+
bytes,
|
|
108
|
+
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
closeSync(descriptor)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readCreateResponse(
|
|
116
|
+
value: unknown,
|
|
117
|
+
expectedBytes: number,
|
|
118
|
+
expectedSha256: string,
|
|
119
|
+
): {
|
|
120
|
+
receipt: RnxCloudCreateReceipt
|
|
121
|
+
token: string
|
|
122
|
+
} {
|
|
123
|
+
if (!isRecord(value)) throw new Error('RNX Cloud create returned invalid JSON')
|
|
124
|
+
const simId = value.simId
|
|
125
|
+
const artifact = value.artifact
|
|
126
|
+
const claim = value.claim
|
|
127
|
+
const token = value.token
|
|
128
|
+
if (
|
|
129
|
+
typeof simId !== 'string' ||
|
|
130
|
+
!simId ||
|
|
131
|
+
!isRecord(artifact) ||
|
|
132
|
+
typeof artifact.id !== 'string' ||
|
|
133
|
+
artifact.id !== `sha256:${expectedSha256}` ||
|
|
134
|
+
typeof artifact.bytes !== 'number' ||
|
|
135
|
+
!Number.isInteger(artifact.bytes) ||
|
|
136
|
+
artifact.bytes !== expectedBytes ||
|
|
137
|
+
!isRecord(claim) ||
|
|
138
|
+
typeof claim.id !== 'string' ||
|
|
139
|
+
!claim.id ||
|
|
140
|
+
typeof claim.expiresAt !== 'number' ||
|
|
141
|
+
typeof token !== 'string' ||
|
|
142
|
+
!token
|
|
143
|
+
) {
|
|
144
|
+
throw new Error('RNX Cloud create returned an invalid simulator receipt')
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
receipt: {
|
|
148
|
+
simId,
|
|
149
|
+
artifact: { id: `sha256:${expectedSha256}`, bytes: artifact.bytes },
|
|
150
|
+
claim: { id: claim.id, expiresAt: claim.expiresAt },
|
|
151
|
+
},
|
|
152
|
+
token,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function createCloudSimulator(
|
|
157
|
+
options: CreateCloudSimulatorOptions,
|
|
158
|
+
): Promise<{ receipt: RnxCloudCreateReceipt; session: CloudSession }> {
|
|
159
|
+
const apiOrigin = resolveCloudOrigin(options.apiOrigin)
|
|
160
|
+
const artifact = await readImmutableBundle(options.bundlePath)
|
|
161
|
+
let response: Response
|
|
162
|
+
try {
|
|
163
|
+
response = await fetch(`${apiOrigin}/v1/sims`, {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: {
|
|
166
|
+
authorization: options.authorization,
|
|
167
|
+
'content-type': 'application/javascript',
|
|
168
|
+
'x-rnx-artifact-sha256': artifact.sha256,
|
|
169
|
+
'x-rnx-platform': 'ios',
|
|
170
|
+
'x-rnx-device': options.device,
|
|
171
|
+
},
|
|
172
|
+
body: new Uint8Array(artifact.bytes),
|
|
173
|
+
})
|
|
174
|
+
} catch (error) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`could not reach RNX Cloud at ${apiOrigin}: ${
|
|
177
|
+
error instanceof Error ? error.message : String(error)
|
|
178
|
+
}`,
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
const responseText = await response.text()
|
|
182
|
+
if (!response.ok) throw responseError(response.status, responseText)
|
|
183
|
+
let parsed: unknown
|
|
184
|
+
try {
|
|
185
|
+
parsed = JSON.parse(responseText)
|
|
186
|
+
} catch {
|
|
187
|
+
throw new Error('RNX Cloud create returned unreadable JSON')
|
|
188
|
+
}
|
|
189
|
+
const created = readCreateResponse(parsed, artifact.bytes.length, artifact.sha256)
|
|
190
|
+
const session = createCloudSession({
|
|
191
|
+
simId: created.receipt.simId,
|
|
192
|
+
apiOrigin,
|
|
193
|
+
claimId: created.receipt.claim.id,
|
|
194
|
+
token: created.token,
|
|
195
|
+
})
|
|
196
|
+
return { receipt: created.receipt, session }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function resolveCloudSessionForParsed(
|
|
200
|
+
parsed: Pick<ParsedBridgeCliArgs, 'simId' | 'simIdSource'>,
|
|
201
|
+
): CloudSession | null {
|
|
202
|
+
const session = readCloudSession()
|
|
203
|
+
if (!session) return null
|
|
204
|
+
if (parsed.simIdSource === 'flag' && parsed.simId !== session.simId) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`this shell is connected to RNX Cloud simulator ${session.simId}; ${
|
|
207
|
+
parsed.simId ? `--sim ${parsed.simId}` : 'the requested simulator'
|
|
208
|
+
} is not available in this shell`,
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
return session
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function cloudCommand(command: {
|
|
215
|
+
type: string
|
|
216
|
+
[key: string]: unknown
|
|
217
|
+
}): Record<string, unknown> {
|
|
218
|
+
const clean: Record<string, unknown> = {}
|
|
219
|
+
for (const [key, value] of Object.entries(command)) {
|
|
220
|
+
if (key !== 'id' && key !== 'simId') clean[key] = value
|
|
221
|
+
}
|
|
222
|
+
if (command.type === 'query') {
|
|
223
|
+
const query = command.query
|
|
224
|
+
if (
|
|
225
|
+
!isRecord(query) ||
|
|
226
|
+
(query.kind !== 'tree' && query.kind !== 'find' && query.kind !== 'count')
|
|
227
|
+
) {
|
|
228
|
+
throw new Error('RNX Cloud supports only structured tree, node, and count queries')
|
|
229
|
+
}
|
|
230
|
+
return clean
|
|
231
|
+
}
|
|
232
|
+
if (command.type === 'waitFor') {
|
|
233
|
+
const options = command.waitForOptions
|
|
234
|
+
const condition = isRecord(options) ? options.condition : null
|
|
235
|
+
if (
|
|
236
|
+
!isRecord(condition) ||
|
|
237
|
+
(condition.type !== 'ready' &&
|
|
238
|
+
condition.type !== 'present' &&
|
|
239
|
+
condition.type !== 'absent')
|
|
240
|
+
) {
|
|
241
|
+
throw new Error('RNX Cloud supports only ready and selector waits')
|
|
242
|
+
}
|
|
243
|
+
return clean
|
|
244
|
+
}
|
|
245
|
+
if (command.type === 'state') return clean
|
|
246
|
+
if (command.type === 'capture' || command.type === 'captureRegions') return clean
|
|
247
|
+
if (command.type === 'setAppearance' || command.type === 'openUrl') return clean
|
|
248
|
+
if (
|
|
249
|
+
command.type === 'screenshot' &&
|
|
250
|
+
command.crop === undefined &&
|
|
251
|
+
command.captureFilter === undefined &&
|
|
252
|
+
(command.layers === undefined || command.layers === 'tenant')
|
|
253
|
+
) {
|
|
254
|
+
return clean
|
|
255
|
+
}
|
|
256
|
+
throw new Error(`RNX Cloud does not support bridge command type ${command.type}`)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
class CloudBridge implements WsBridge {
|
|
260
|
+
readonly plane = 'cloud' as const
|
|
261
|
+
|
|
262
|
+
constructor(private readonly session: CloudSession) {}
|
|
263
|
+
|
|
264
|
+
async send(
|
|
265
|
+
command: { type: string; [key: string]: unknown },
|
|
266
|
+
options?: { timeoutMs?: number },
|
|
267
|
+
): Promise<unknown> {
|
|
268
|
+
const body: CloudCommandEnvelope = {
|
|
269
|
+
id: randomUUID(),
|
|
270
|
+
claimId: this.session.claimId,
|
|
271
|
+
command: cloudCommand(command),
|
|
272
|
+
}
|
|
273
|
+
let response: Response
|
|
274
|
+
try {
|
|
275
|
+
response = await fetch(
|
|
276
|
+
`${this.session.apiOrigin}/v1/sims/${encodeURIComponent(this.session.simId)}/commands`,
|
|
277
|
+
{
|
|
278
|
+
method: 'POST',
|
|
279
|
+
headers: {
|
|
280
|
+
authorization: `Bearer ${this.session.token}`,
|
|
281
|
+
'content-type': 'application/json',
|
|
282
|
+
},
|
|
283
|
+
body: JSON.stringify(body),
|
|
284
|
+
...(options?.timeoutMs
|
|
285
|
+
? { signal: AbortSignal.timeout(options.timeoutMs) }
|
|
286
|
+
: {}),
|
|
287
|
+
},
|
|
288
|
+
)
|
|
289
|
+
} catch (error) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`could not reach RNX Cloud at ${this.session.apiOrigin}: ${
|
|
292
|
+
error instanceof Error ? error.message : String(error)
|
|
293
|
+
}`,
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
const text = await response.text()
|
|
297
|
+
if (!response.ok) throw responseError(response.status, text)
|
|
298
|
+
let parsed: unknown
|
|
299
|
+
try {
|
|
300
|
+
parsed = JSON.parse(text)
|
|
301
|
+
} catch {
|
|
302
|
+
throw new Error('RNX Cloud command returned unreadable JSON')
|
|
303
|
+
}
|
|
304
|
+
if (!isRecord(parsed) || parsed.id !== body.id) {
|
|
305
|
+
throw new Error('RNX Cloud command returned an invalid command receipt')
|
|
306
|
+
}
|
|
307
|
+
if (typeof parsed.error === 'string' && parsed.error) throw new Error(parsed.error)
|
|
308
|
+
if (!Object.hasOwn(parsed, 'result')) {
|
|
309
|
+
throw new Error('RNX Cloud command receipt has no result')
|
|
310
|
+
}
|
|
311
|
+
return parsed.result
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
listSims(): Promise<BridgeSimInfo[]> {
|
|
315
|
+
return Promise.reject(new Error('RNX Cloud does not expose a local simulator list'))
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
focusSim(): Promise<never> {
|
|
319
|
+
return Promise.reject(
|
|
320
|
+
new Error('RNX Cloud simulators have no browser window to focus'),
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
closeSim(): Promise<never> {
|
|
325
|
+
return Promise.reject(new Error('RNX Cloud close uses the simulator DELETE route'))
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
claim(): Promise<BridgeClaimResult> {
|
|
329
|
+
return Promise.reject(new Error('RNX Cloud claim uses the simulator claim route'))
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
close(): void {}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function createCloudBridgeForParsed(parsed: ParsedBridgeCliArgs): WsBridge | null {
|
|
336
|
+
const session = resolveCloudSessionForParsed(parsed)
|
|
337
|
+
return session ? new CloudBridge(session) : null
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function readClaimResponse(value: unknown): { id: string; expiresAt: number } {
|
|
341
|
+
const claim = isRecord(value) ? value.claim : null
|
|
342
|
+
if (
|
|
343
|
+
!isRecord(claim) ||
|
|
344
|
+
typeof claim.id !== 'string' ||
|
|
345
|
+
!claim.id ||
|
|
346
|
+
typeof claim.expiresAt !== 'number'
|
|
347
|
+
) {
|
|
348
|
+
throw new Error('RNX Cloud claim returned an invalid receipt')
|
|
349
|
+
}
|
|
350
|
+
return { id: claim.id, expiresAt: claim.expiresAt }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export async function claimCloudSimulator(
|
|
354
|
+
session: CloudSession,
|
|
355
|
+
options: { force?: boolean } = {},
|
|
356
|
+
): Promise<{
|
|
357
|
+
claim: { id: string; expiresAt: number }
|
|
358
|
+
session: CloudSession
|
|
359
|
+
}> {
|
|
360
|
+
const response = await fetch(
|
|
361
|
+
`${session.apiOrigin}/v1/sims/${encodeURIComponent(session.simId)}/claim`,
|
|
362
|
+
{
|
|
363
|
+
method: 'POST',
|
|
364
|
+
headers: {
|
|
365
|
+
authorization: `Bearer ${session.token}`,
|
|
366
|
+
'content-type': 'application/json',
|
|
367
|
+
},
|
|
368
|
+
body: JSON.stringify(
|
|
369
|
+
options.force ? { force: true } : { claimId: session.claimId },
|
|
370
|
+
),
|
|
371
|
+
},
|
|
372
|
+
)
|
|
373
|
+
const text = await response.text()
|
|
374
|
+
if (!response.ok) throw responseError(response.status, text)
|
|
375
|
+
let parsed: unknown
|
|
376
|
+
try {
|
|
377
|
+
parsed = JSON.parse(text)
|
|
378
|
+
} catch {
|
|
379
|
+
throw new Error('RNX Cloud claim returned unreadable JSON')
|
|
380
|
+
}
|
|
381
|
+
const claim = readClaimResponse(parsed)
|
|
382
|
+
return {
|
|
383
|
+
claim,
|
|
384
|
+
session: createCloudSession({ ...session, claimId: claim.id }),
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export async function confirmCloudSimulator(session: CloudSession): Promise<void> {
|
|
389
|
+
const confirmed = await claimCloudSimulator(session)
|
|
390
|
+
if (confirmed.session.claimId !== session.claimId) {
|
|
391
|
+
throw new Error('RNX Cloud confirmation replaced the initial claim')
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export async function closeCloudSimulator(session: CloudSession): Promise<void> {
|
|
396
|
+
const response = await fetch(
|
|
397
|
+
`${session.apiOrigin}/v1/sims/${encodeURIComponent(session.simId)}`,
|
|
398
|
+
{
|
|
399
|
+
method: 'DELETE',
|
|
400
|
+
headers: { authorization: `Bearer ${session.token}` },
|
|
401
|
+
},
|
|
402
|
+
)
|
|
403
|
+
const text = await response.text()
|
|
404
|
+
if (!response.ok && response.status !== 404) {
|
|
405
|
+
throw responseError(response.status, text)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
interface CloudBoundaryInvocation {
|
|
2
|
+
command: string
|
|
3
|
+
args: string[]
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type CloudBoundaryResult = { handled: false } | { handled: true; code: number }
|
|
7
|
+
|
|
8
|
+
const CLOUD_TARGET_COMMANDS = new Set([
|
|
9
|
+
'assert',
|
|
10
|
+
'camera',
|
|
11
|
+
'claim',
|
|
12
|
+
'close',
|
|
13
|
+
'debug',
|
|
14
|
+
'describe',
|
|
15
|
+
'detox',
|
|
16
|
+
'do',
|
|
17
|
+
'film',
|
|
18
|
+
'find',
|
|
19
|
+
'get',
|
|
20
|
+
'logs',
|
|
21
|
+
'maestro',
|
|
22
|
+
'mode',
|
|
23
|
+
'network',
|
|
24
|
+
'perf',
|
|
25
|
+
'permissions',
|
|
26
|
+
'record',
|
|
27
|
+
'reset',
|
|
28
|
+
'screenshot',
|
|
29
|
+
'shell',
|
|
30
|
+
'state',
|
|
31
|
+
'storage',
|
|
32
|
+
'timeline',
|
|
33
|
+
'wait',
|
|
34
|
+
'what-happened',
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
function firstVerb(positional: string[]): string | null {
|
|
38
|
+
return positional.find((value) => !value.startsWith('-')) ?? null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function supportsCloudRead(
|
|
42
|
+
command: string,
|
|
43
|
+
args: string[],
|
|
44
|
+
positional: string[],
|
|
45
|
+
): boolean {
|
|
46
|
+
if (command === 'describe' || command === 'find') return true
|
|
47
|
+
if (command === 'state') return firstVerb(positional) === null
|
|
48
|
+
if (command === 'get') {
|
|
49
|
+
const verb = firstVerb(positional)
|
|
50
|
+
return verb === 'tree' || verb === 'node' || verb === 'count'
|
|
51
|
+
}
|
|
52
|
+
if (command === 'wait') {
|
|
53
|
+
const verb = firstVerb(positional)
|
|
54
|
+
return verb === 'ready' || verb === 'selector'
|
|
55
|
+
}
|
|
56
|
+
if (command === 'logs') {
|
|
57
|
+
const verb = firstVerb(positional)
|
|
58
|
+
return (
|
|
59
|
+
!args.includes('--tail') &&
|
|
60
|
+
!args.includes('-f') &&
|
|
61
|
+
verb !== 'tail' &&
|
|
62
|
+
verb !== 'clear' &&
|
|
63
|
+
verb !== 'purge'
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
if (command === 'screenshot') {
|
|
67
|
+
return !args.some((arg) =>
|
|
68
|
+
[
|
|
69
|
+
'--area',
|
|
70
|
+
'--gallery',
|
|
71
|
+
'--id',
|
|
72
|
+
'--no-shell',
|
|
73
|
+
'--shell-only',
|
|
74
|
+
'--text',
|
|
75
|
+
'--themes',
|
|
76
|
+
'--with-frame',
|
|
77
|
+
].includes(arg),
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function refuseCloudInvocation(
|
|
84
|
+
command: string,
|
|
85
|
+
positional: string[],
|
|
86
|
+
): CloudBoundaryResult {
|
|
87
|
+
const verb = firstVerb(positional)
|
|
88
|
+
const name = verb ? `${command} ${verb}` : command
|
|
89
|
+
console.error(` rnx ${name} is not available for an RNX Cloud simulator`)
|
|
90
|
+
return { handled: true, code: 1 }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function dispatchCloudBoundary({
|
|
94
|
+
command,
|
|
95
|
+
args,
|
|
96
|
+
}: CloudBoundaryInvocation): Promise<CloudBoundaryResult> {
|
|
97
|
+
if (command === 'ios' && args.includes('--cloud')) return { handled: false }
|
|
98
|
+
if (command === 'android' && args.includes('--cloud')) {
|
|
99
|
+
console.error(
|
|
100
|
+
' rnx android --cloud is not available; RNX Cloud currently supports iOS',
|
|
101
|
+
)
|
|
102
|
+
return { handled: true, code: 1 }
|
|
103
|
+
}
|
|
104
|
+
if (!CLOUD_TARGET_COMMANDS.has(command)) return { handled: false }
|
|
105
|
+
|
|
106
|
+
const [{ parseBridgeCliArgs }, cloudClient, cloudSession] = await Promise.all([
|
|
107
|
+
import('./ws-bridge'),
|
|
108
|
+
import('./cloud-client'),
|
|
109
|
+
import('./cloud-session'),
|
|
110
|
+
])
|
|
111
|
+
const parsed = parseBridgeCliArgs(args)
|
|
112
|
+
const positionalTarget =
|
|
113
|
+
command === 'claim' || command === 'close' ? firstVerb(parsed.positional) : null
|
|
114
|
+
const target: Pick<typeof parsed, 'simId' | 'simIdSource'> = positionalTarget
|
|
115
|
+
? { simId: positionalTarget, simIdSource: 'flag' }
|
|
116
|
+
: parsed
|
|
117
|
+
let session
|
|
118
|
+
try {
|
|
119
|
+
session = cloudClient.resolveCloudSessionForParsed(target)
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(` ${error instanceof Error ? error.message : String(error)}`)
|
|
122
|
+
return { handled: true, code: 1 }
|
|
123
|
+
}
|
|
124
|
+
if (!session) return { handled: false }
|
|
125
|
+
|
|
126
|
+
if (command === 'claim') {
|
|
127
|
+
try {
|
|
128
|
+
const descriptor = cloudSession.requireCloudSessionShellUpdate()
|
|
129
|
+
const claimed = await cloudClient.claimCloudSimulator(session, {
|
|
130
|
+
force: args.includes('--force'),
|
|
131
|
+
})
|
|
132
|
+
cloudSession.publishCloudSessionToShell(claimed.session, descriptor)
|
|
133
|
+
console.log(` claimed: ${session.simId}`)
|
|
134
|
+
console.log(` expires: ${claimed.claim.expiresAt}`)
|
|
135
|
+
return { handled: true, code: 0 }
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error(
|
|
138
|
+
` claim failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
139
|
+
)
|
|
140
|
+
return { handled: true, code: 1 }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (command === 'close') {
|
|
145
|
+
if (args.includes('--all') || args.includes('--others')) {
|
|
146
|
+
return refuseCloudInvocation(command, parsed.positional)
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const descriptor = cloudSession.requireCloudSessionShellUpdate()
|
|
150
|
+
await cloudClient.closeCloudSimulator(session)
|
|
151
|
+
cloudSession.publishCloudSessionToShell(null, descriptor)
|
|
152
|
+
console.log(` closed: ${session.simId}`)
|
|
153
|
+
return { handled: true, code: 0 }
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(
|
|
156
|
+
` close failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
157
|
+
)
|
|
158
|
+
return { handled: true, code: 1 }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return supportsCloudRead(command, args, parsed.positional)
|
|
163
|
+
? { handled: false }
|
|
164
|
+
: refuseCloudInvocation(command, parsed.positional)
|
|
165
|
+
}
|