rnxsim 0.1.419 → 0.1.420
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/cloud-client.ts +164 -12
- package/cli/cloud-dispatch.ts +7 -2
- package/cli/commands/box/checkout-plane.ts +78 -40
- package/cli/commands/platform.ts +30 -9
- package/cli/outbound-endpoints.ts +6 -0
- package/cli/shell-init.ts +1 -1
- package/cli/ws-bridge.ts +7 -5
- 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/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +6 -4
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +55 -28
- 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 +7 -5
- 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-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 +4 -3
- 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 +9 -6
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +64 -34
- package/dist-lib/vite.cjs +1 -1
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +9 -5
- package/src/dev-bundle-resolution.ts +6 -3
- package/src/dev-server-open.ts +3 -2
package/cli/cloud-client.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
fstatSync,
|
|
6
|
+
openSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
} from 'node:fs'
|
|
3
10
|
import { dirname, extname, join, resolve } from 'node:path'
|
|
4
11
|
import {
|
|
5
12
|
embedMetroAssetFiles,
|
|
@@ -17,6 +24,7 @@ import {
|
|
|
17
24
|
isRnxCloudCommandType,
|
|
18
25
|
RNX_CLOUD_MAX_ARTIFACT_BYTES,
|
|
19
26
|
type RnxCloudBoxCreateReceipt,
|
|
27
|
+
type RnxCloudCreateReceipt,
|
|
20
28
|
} from '../src/cloud-contract'
|
|
21
29
|
import {
|
|
22
30
|
RNX_METRO_MODULE_IDENTITY_VERSION,
|
|
@@ -119,11 +127,50 @@ function readImmutableBundleSource(bundlePath: string): string {
|
|
|
119
127
|
}
|
|
120
128
|
}
|
|
121
129
|
|
|
130
|
+
async function fetchBundleSource(bundleUrl: string): Promise<string> {
|
|
131
|
+
let response: Response
|
|
132
|
+
try {
|
|
133
|
+
response = await fetch(bundleUrl)
|
|
134
|
+
} catch (error) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`rnx ios --nano could not fetch bundle from ${bundleUrl}: ${
|
|
137
|
+
error instanceof Error ? error.message : String(error)
|
|
138
|
+
}`,
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`rnx ios --nano could not fetch bundle from ${bundleUrl}: HTTP ${response.status}${
|
|
144
|
+
response.statusText ? ` ${response.statusText}` : ''
|
|
145
|
+
}`,
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
const text = await response.text()
|
|
149
|
+
if (text.length === 0) {
|
|
150
|
+
throw new Error('rnx ios --nano bundle is empty')
|
|
151
|
+
}
|
|
152
|
+
if (text.includes('\0') || !text.includes('__d(') || !text.includes('__r(')) {
|
|
153
|
+
throw new Error('rnx ios --nano requires a Metro JavaScript bundle')
|
|
154
|
+
}
|
|
155
|
+
return text
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function loadBundleSource(bundleInput: string): Promise<string> {
|
|
159
|
+
if (bundleInput.startsWith('http://')) {
|
|
160
|
+
throw new Error('rnx ios --nano bundle URL must use HTTPS')
|
|
161
|
+
}
|
|
162
|
+
if (bundleInput.startsWith('https://')) {
|
|
163
|
+
return fetchBundleSource(bundleInput)
|
|
164
|
+
}
|
|
165
|
+
return readImmutableBundleSource(bundleInput)
|
|
166
|
+
}
|
|
167
|
+
|
|
122
168
|
// `react-native bundle --assets-dest` dest paths are not always the JS
|
|
123
169
|
// descriptor's httpServerLocation with `../` replaced by `_`. Metro 0.83 does
|
|
124
170
|
// that (`__node_modules`); Metro 0.84 often dests `node_modules` and keeps the
|
|
125
171
|
// project-root-relative app path. resolveMetroIosAssetDestFile matches both.
|
|
126
|
-
function listMetroAssetDestFiles(assetsPath
|
|
172
|
+
function listMetroAssetDestFiles(assetsPath?: string): string[] {
|
|
173
|
+
if (!assetsPath || !existsSync(assetsPath)) return []
|
|
127
174
|
const files: string[] = []
|
|
128
175
|
const walk = (dir: string, prefix: string) => {
|
|
129
176
|
let entries
|
|
@@ -183,19 +230,19 @@ async function inferCloudModuleIdentity(
|
|
|
183
230
|
* an ordinary unannotated Metro production bundle is identified through the
|
|
184
231
|
* published fingerprint registry; a withRNX footer remains the fast path.
|
|
185
232
|
*/
|
|
186
|
-
async function produceCloudArtifact(
|
|
233
|
+
export async function produceCloudArtifact(
|
|
187
234
|
bundlePath: string,
|
|
188
|
-
assetsPath
|
|
235
|
+
assetsPath?: string,
|
|
189
236
|
): Promise<{ bytes: Buffer; sha256: string; receipt: RnxCloudArtifactReceipt }> {
|
|
190
|
-
const source =
|
|
191
|
-
const destFiles = listMetroAssetDestFiles(assetsPath)
|
|
237
|
+
const source = await loadBundleSource(bundlePath)
|
|
238
|
+
const destFiles = assetsPath ? listMetroAssetDestFiles(assetsPath) : []
|
|
192
239
|
const embedded = await embedMetroAssetFiles(source, async (variant) => {
|
|
193
240
|
const relative = resolveMetroIosAssetDestFile(
|
|
194
241
|
destFiles,
|
|
195
242
|
variant.descriptor,
|
|
196
243
|
variant.scale,
|
|
197
244
|
)
|
|
198
|
-
if (relative) return readFileSync(join(assetsPath, relative))
|
|
245
|
+
if (relative && assetsPath) return readFileSync(join(assetsPath, relative))
|
|
199
246
|
// iOS copies only @1x..@3x, so a declared scale with no file is ordinary
|
|
200
247
|
// and the producer narrows the descriptor. an asset with no file at any
|
|
201
248
|
// scale is the real failure, and this is where the dest path a customer
|
|
@@ -212,8 +259,9 @@ async function produceCloudArtifact(
|
|
|
212
259
|
const classic =
|
|
213
260
|
metroIosAssetDestRelatives(variant.descriptor.httpServerLocation, filename)[0] ??
|
|
214
261
|
filename
|
|
262
|
+
const lookupPath = assetsPath ?? '.'
|
|
215
263
|
throw new Error(
|
|
216
|
-
`rnx ios --nano could not read ${join(
|
|
264
|
+
`rnx ios --nano could not read ${join(lookupPath, classic)} for ${variant.descriptor.name}.${variant.descriptor.type} at scale ${variant.scale}. Build the bundle with --assets-dest, or pass --assets <dir>.`,
|
|
217
265
|
)
|
|
218
266
|
})
|
|
219
267
|
const artifact = await buildRnxCloudArtifact(embedded.code, {
|
|
@@ -299,10 +347,13 @@ export async function createCloudBox(options: CreateCloudBoxOptions): Promise<{
|
|
|
299
347
|
artifact: RnxCloudArtifactReceipt
|
|
300
348
|
}> {
|
|
301
349
|
const apiOrigin = resolveCloudOrigin(options.apiOrigin)
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
resolve(options.assetsPath
|
|
305
|
-
|
|
350
|
+
const isUrl = options.bundlePath.startsWith('https://')
|
|
351
|
+
const assetsPath = options.assetsPath
|
|
352
|
+
? resolve(options.assetsPath)
|
|
353
|
+
: isUrl
|
|
354
|
+
? undefined
|
|
355
|
+
: resolve(dirname(options.bundlePath))
|
|
356
|
+
const artifact = await produceCloudArtifact(options.bundlePath, assetsPath)
|
|
306
357
|
let response: Response
|
|
307
358
|
try {
|
|
308
359
|
response = await fetch(`${apiOrigin}/v1/boxes`, {
|
|
@@ -521,3 +572,104 @@ export async function closeCloudBox(session: CloudSession): Promise<void> {
|
|
|
521
572
|
throw responseError(response.status, text)
|
|
522
573
|
}
|
|
523
574
|
}
|
|
575
|
+
|
|
576
|
+
export interface AddCloudSimulatorOptions {
|
|
577
|
+
device: string
|
|
578
|
+
authorization: string
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export async function addCloudSimulator(
|
|
582
|
+
session: CloudSession,
|
|
583
|
+
options: AddCloudSimulatorOptions,
|
|
584
|
+
): Promise<{
|
|
585
|
+
receipt: RnxCloudCreateReceipt
|
|
586
|
+
session: CloudSession
|
|
587
|
+
}> {
|
|
588
|
+
let response: Response
|
|
589
|
+
try {
|
|
590
|
+
response = await fetch(
|
|
591
|
+
`${session.apiOrigin}/v1/boxes/${encodeURIComponent(session.boxId)}/simulators`,
|
|
592
|
+
{
|
|
593
|
+
method: 'POST',
|
|
594
|
+
headers: {
|
|
595
|
+
authorization: `Bearer ${session.boxToken}`,
|
|
596
|
+
'x-rnx-account-authorization': options.authorization,
|
|
597
|
+
'content-type': 'application/json',
|
|
598
|
+
},
|
|
599
|
+
body: JSON.stringify({ device: options.device }),
|
|
600
|
+
},
|
|
601
|
+
)
|
|
602
|
+
} catch (error) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`could not reach RNX Cloud at ${session.apiOrigin}: ${
|
|
605
|
+
error instanceof Error ? error.message : String(error)
|
|
606
|
+
}`,
|
|
607
|
+
)
|
|
608
|
+
}
|
|
609
|
+
const responseText = await response.text()
|
|
610
|
+
if (!response.ok) {
|
|
611
|
+
if (response.status === 404) {
|
|
612
|
+
try {
|
|
613
|
+
const parsed: unknown = JSON.parse(responseText)
|
|
614
|
+
if (
|
|
615
|
+
isRecord(parsed) &&
|
|
616
|
+
(parsed.error === 'unknown_artifact' ||
|
|
617
|
+
(isRecord(parsed.error) && parsed.error.code === 'unknown_artifact'))
|
|
618
|
+
) {
|
|
619
|
+
throw new Error(
|
|
620
|
+
'this box does not hold a new bundle; run `rnx close` and create again',
|
|
621
|
+
)
|
|
622
|
+
}
|
|
623
|
+
} catch (err) {
|
|
624
|
+
if (err instanceof Error && err.message.startsWith('this box does not hold')) {
|
|
625
|
+
throw err
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
throw new Error(
|
|
629
|
+
'this box does not hold a new bundle; run `rnx close` and create again',
|
|
630
|
+
)
|
|
631
|
+
}
|
|
632
|
+
throw responseError(response.status, responseText)
|
|
633
|
+
}
|
|
634
|
+
let parsed: unknown
|
|
635
|
+
try {
|
|
636
|
+
parsed = JSON.parse(responseText)
|
|
637
|
+
} catch {
|
|
638
|
+
throw new Error('RNX Cloud simulator create returned unreadable JSON')
|
|
639
|
+
}
|
|
640
|
+
if (!isRecord(parsed) || !isRecord(parsed.simulator)) {
|
|
641
|
+
throw new Error('RNX Cloud simulator create returned an invalid receipt')
|
|
642
|
+
}
|
|
643
|
+
const simulator = parsed.simulator
|
|
644
|
+
const simId = simulator.simId
|
|
645
|
+
const claim = isRecord(simulator.claim) ? simulator.claim : null
|
|
646
|
+
const token = simulator.token
|
|
647
|
+
const artifact = isRecord(simulator.artifact) ? simulator.artifact : null
|
|
648
|
+
if (
|
|
649
|
+
typeof simId !== 'string' ||
|
|
650
|
+
!simId ||
|
|
651
|
+
!claim ||
|
|
652
|
+
typeof claim.id !== 'string' ||
|
|
653
|
+
!claim.id ||
|
|
654
|
+
typeof claim.expiresAt !== 'number' ||
|
|
655
|
+
typeof token !== 'string' ||
|
|
656
|
+
!token ||
|
|
657
|
+
!artifact ||
|
|
658
|
+
typeof artifact.id !== 'string' ||
|
|
659
|
+
typeof artifact.bytes !== 'number'
|
|
660
|
+
) {
|
|
661
|
+
throw new Error('RNX Cloud simulator create returned an invalid receipt')
|
|
662
|
+
}
|
|
663
|
+
const receipt: RnxCloudCreateReceipt = {
|
|
664
|
+
simId,
|
|
665
|
+
artifact: { id: artifact.id as `sha256:${string}`, bytes: artifact.bytes },
|
|
666
|
+
claim: { id: claim.id, expiresAt: claim.expiresAt },
|
|
667
|
+
}
|
|
668
|
+
const updatedSession = createCloudSession({
|
|
669
|
+
...session,
|
|
670
|
+
simId,
|
|
671
|
+
claimId: claim.id,
|
|
672
|
+
token,
|
|
673
|
+
})
|
|
674
|
+
return { receipt, session: updatedSession }
|
|
675
|
+
}
|
package/cli/cloud-dispatch.ts
CHANGED
|
@@ -129,13 +129,18 @@ export async function dispatchCloudBoundary({
|
|
|
129
129
|
command,
|
|
130
130
|
args,
|
|
131
131
|
}: CloudBoundaryInvocation): Promise<CloudBoundaryResult> {
|
|
132
|
-
if (command === 'ios' && args.includes('--nano')
|
|
133
|
-
|
|
132
|
+
if (command === 'ios' && (args.includes('--nano') || args.includes('--cloud')))
|
|
133
|
+
return { handled: false }
|
|
134
|
+
if (command === 'android' && (args.includes('--nano') || args.includes('--cloud'))) {
|
|
134
135
|
console.error(
|
|
135
136
|
' rnx android --nano is not available; RNX Cloud currently supports iOS',
|
|
136
137
|
)
|
|
137
138
|
return { handled: true, code: 1 }
|
|
138
139
|
}
|
|
140
|
+
if ((command === 'ios' || command === 'android') && args.includes('--micro')) {
|
|
141
|
+
console.error(` rnx ${command} --micro is not implemented`)
|
|
142
|
+
return { handled: true, code: 1 }
|
|
143
|
+
}
|
|
139
144
|
if (!CLOUD_TARGET_COMMANDS.has(command)) return { handled: false }
|
|
140
145
|
|
|
141
146
|
const [{ parseBridgeCliArgs }, cloudClient, cloudSession] = await Promise.all([
|
|
@@ -51,6 +51,23 @@ const BINARY_SNIFF_BYTES = 8192
|
|
|
51
51
|
// merely busy.
|
|
52
52
|
const LIST_FILES_TIMEOUT_MS = 10_000
|
|
53
53
|
|
|
54
|
+
// the same busy machine fails this call in two ways that are both a failed
|
|
55
|
+
// READ rather than an answer: the spawn raises, and a child exits 0 having
|
|
56
|
+
// written nothing. docs/testing.md measured the second one directly on
|
|
57
|
+
// 2026-08-27 under CPU oversubscription, where a git child exited 0 with empty
|
|
58
|
+
// stdout while the very next `/bin/echo` read back fine.
|
|
59
|
+
//
|
|
60
|
+
// the answer to a failed read is to read again. every box command re-indexes
|
|
61
|
+
// before it runs, so a single lost answer is otherwise the difference between
|
|
62
|
+
// `touch created.txt` working and the box telling the user it has no file set
|
|
63
|
+
// to serve: measured here as exit 1 in 103ms, which is what took the box
|
|
64
|
+
// conformance suite red under a load average of 47. the conditions the two
|
|
65
|
+
// tests in test/rnxBoxCheckoutPlane.test.ts hold (a git that never answers, a
|
|
66
|
+
// git that always answers with nothing) are permanent, so they still fail, and
|
|
67
|
+
// they fail naming the same thing.
|
|
68
|
+
const LIST_FILES_ATTEMPTS = 3
|
|
69
|
+
const LIST_FILES_RETRY_MS = 25
|
|
70
|
+
|
|
54
71
|
function looksBinary(filePath: string, size: number): boolean {
|
|
55
72
|
if (size === 0) return false
|
|
56
73
|
let fd: number | undefined
|
|
@@ -103,51 +120,72 @@ export class CheckoutFilePlane implements ProjectFilePlane {
|
|
|
103
120
|
return this.entries
|
|
104
121
|
}
|
|
105
122
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// the deadline kills with SIGTERM, so that signal is what tells a bound
|
|
129
|
-
// run apart from git failing on its own terms, and it is worth naming
|
|
130
|
-
// separately: "git was killed by SIGTERM" reads as git's problem, when
|
|
131
|
-
// what happened is that it never answered.
|
|
132
|
-
const killed =
|
|
133
|
-
error instanceof Error && 'signal' in error && error.signal === 'SIGTERM'
|
|
134
|
-
throw new CheckoutRequiresGitError(
|
|
135
|
-
killed
|
|
136
|
-
? `git did not list the project's files in ${this.root} within ${this.listTimeoutMs / 1000}s, so the box has no file set to serve. the checkout may be on a stalled filesystem, or this machine may be too loaded to start a subprocess.`
|
|
137
|
-
: `could not list the project's files with git in ${this.root}: ${message}`,
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// a busy machine also hands back a child that exits 0 having written
|
|
142
|
-
// nothing, which docs/testing.md owns: empty child output never means an
|
|
143
|
-
// empty answer. without this the box would build an empty index and every
|
|
144
|
-
// read would report the file as missing, which is a broken project rather
|
|
145
|
-
// than a broken box, and nothing would say so.
|
|
123
|
+
private listFiles(): string {
|
|
124
|
+
// -z keeps paths containing spaces or newlines intact. a local Box sees
|
|
125
|
+
// tracked plus visible untracked files, while a durable cloud import is
|
|
126
|
+
// deliberately restricted to the checkout's tracked source.
|
|
127
|
+
const args = this.trackedOnly
|
|
128
|
+
? ['ls-files', '-z']
|
|
129
|
+
: ['ls-files', '-co', '--exclude-standard', '-z']
|
|
130
|
+
const stdout = execFileSync('git', args, {
|
|
131
|
+
cwd: this.root,
|
|
132
|
+
encoding: 'utf8',
|
|
133
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
134
|
+
timeout: this.listTimeoutMs,
|
|
135
|
+
// stating the environment is not redundant here. bun 1.3.14 resolves
|
|
136
|
+
// the binary for the synchronous form against the PATH it captured at
|
|
137
|
+
// startup and ignores a later change to `process.env.PATH`, so without
|
|
138
|
+
// this it runs a different git than every other call in the process.
|
|
139
|
+
env: process.env,
|
|
140
|
+
})
|
|
141
|
+
// empty child output never means an empty answer. without this the box
|
|
142
|
+
// would build an empty index and every read would report the file as
|
|
143
|
+
// missing, which is a broken project rather than a broken box, and nothing
|
|
144
|
+
// would say so.
|
|
146
145
|
if (stdout.length === 0) {
|
|
147
146
|
throw new CheckoutRequiresGitError(
|
|
148
147
|
`git listed no ${this.trackedOnly ? 'tracked ' : ''}files in ${this.root}. either the checkout contains no eligible files, or this machine could not read the child process's output.`,
|
|
149
148
|
)
|
|
150
149
|
}
|
|
150
|
+
return stdout
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async refresh(): Promise<void> {
|
|
154
|
+
let stdout = ''
|
|
155
|
+
let lastFailure: CheckoutRequiresGitError | undefined
|
|
156
|
+
for (let attempt = 1; attempt <= LIST_FILES_ATTEMPTS; attempt++) {
|
|
157
|
+
try {
|
|
158
|
+
stdout = this.listFiles()
|
|
159
|
+
lastFailure = undefined
|
|
160
|
+
break
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (error instanceof CheckoutRequiresGitError) {
|
|
163
|
+
lastFailure = error
|
|
164
|
+
} else {
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
166
|
+
// the deadline kills with SIGTERM, so that signal is what tells a
|
|
167
|
+
// bound run apart from git failing on its own terms, and it is worth
|
|
168
|
+
// naming separately: "git was killed by SIGTERM" reads as git's
|
|
169
|
+
// problem, when what happened is that it never answered.
|
|
170
|
+
const killed =
|
|
171
|
+
error instanceof Error && 'signal' in error && error.signal === 'SIGTERM'
|
|
172
|
+
lastFailure = new CheckoutRequiresGitError(
|
|
173
|
+
killed
|
|
174
|
+
? `git did not list the project's files in ${this.root} within ${this.listTimeoutMs / 1000}s, so the box has no file set to serve. the checkout may be on a stalled filesystem, or this machine may be too loaded to start a subprocess.`
|
|
175
|
+
: `could not list the project's files with git in ${this.root}: ${message}`,
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
if (attempt < LIST_FILES_ATTEMPTS) {
|
|
179
|
+
await new Promise((resolve) =>
|
|
180
|
+
setTimeout(resolve, LIST_FILES_RETRY_MS * attempt),
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (lastFailure) {
|
|
186
|
+
lastFailure.message = `${lastFailure.message} (${LIST_FILES_ATTEMPTS} attempts)`
|
|
187
|
+
throw lastFailure
|
|
188
|
+
}
|
|
151
189
|
|
|
152
190
|
const next = new Map<string, ProjectFileMeta>()
|
|
153
191
|
// rebuilt alongside the index rather than pruned, so a deleted file's
|
package/cli/commands/platform.ts
CHANGED
|
@@ -27,7 +27,7 @@ function isDeviceModel(value: string): value is DeviceModel {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const CLOUD_USAGE =
|
|
30
|
-
'usage: rnx ios --nano <local bundle.js> [--assets <dir>] [--device <model>]'
|
|
30
|
+
'usage: rnx ios --nano <local bundle.js|https://bundle-url> [--assets <dir>] [--device <model>]'
|
|
31
31
|
|
|
32
32
|
function cloudInputFromArgs(args: string[]): {
|
|
33
33
|
bundlePath: string
|
|
@@ -37,7 +37,7 @@ function cloudInputFromArgs(args: string[]): {
|
|
|
37
37
|
let assetsPath: string | null = null
|
|
38
38
|
for (let index = 0; index < args.length; index++) {
|
|
39
39
|
const arg = args[index]
|
|
40
|
-
if (arg === '--nano') continue
|
|
40
|
+
if (arg === '--nano' || arg === '--cloud') continue
|
|
41
41
|
if (arg === '--device') {
|
|
42
42
|
index++
|
|
43
43
|
continue
|
|
@@ -76,6 +76,13 @@ export async function runPlatformCommand(
|
|
|
76
76
|
args: string[],
|
|
77
77
|
opts: { port?: number } = {},
|
|
78
78
|
): Promise<number | undefined> {
|
|
79
|
+
if (args.includes('--micro')) {
|
|
80
|
+
console.error(
|
|
81
|
+
` ${rnxPublicBrand.commandName} ${platform} --micro is not implemented`,
|
|
82
|
+
)
|
|
83
|
+
return 1
|
|
84
|
+
}
|
|
85
|
+
|
|
79
86
|
// `--device` is applied to the settings store before dispatch, so a
|
|
80
87
|
// contradiction shows up here as a platform mismatch. refuse it by name
|
|
81
88
|
// rather than silently overriding whichever one the user meant.
|
|
@@ -114,7 +121,7 @@ export async function runPlatformCommand(
|
|
|
114
121
|
}
|
|
115
122
|
}
|
|
116
123
|
|
|
117
|
-
if (args.includes('--nano')) {
|
|
124
|
+
if (args.includes('--nano') || args.includes('--cloud')) {
|
|
118
125
|
if (platform !== 'ios') {
|
|
119
126
|
console.error(
|
|
120
127
|
` ${rnxPublicBrand.commandName} android --nano is not available; RNX Cloud currently supports iOS`,
|
|
@@ -128,15 +135,26 @@ export async function runPlatformCommand(
|
|
|
128
135
|
readCloudSession,
|
|
129
136
|
requireCloudSessionShellUpdate,
|
|
130
137
|
} = await import('../cloud-session')
|
|
131
|
-
if (readCloudSession()) {
|
|
132
|
-
throw new Error(
|
|
133
|
-
'this shell already targets an RNX Cloud box; run `rnx close` or create the next box in another shell',
|
|
134
|
-
)
|
|
135
|
-
}
|
|
136
138
|
const descriptor = requireCloudSessionShellUpdate()
|
|
137
139
|
const { authHeaderOrExit } = await import('../auth')
|
|
138
|
-
const { closeCloudBox, createCloudBox } = await import('../cloud-client')
|
|
139
140
|
const { header } = authHeaderOrExit('ios --nano')
|
|
141
|
+
const existingSession = readCloudSession()
|
|
142
|
+
if (existingSession) {
|
|
143
|
+
const { addCloudSimulator } = await import('../cloud-client')
|
|
144
|
+
const added = await addCloudSimulator(existingSession, {
|
|
145
|
+
device: selectedDevice,
|
|
146
|
+
authorization: header,
|
|
147
|
+
})
|
|
148
|
+
publishCloudSessionToShell(added.session, descriptor)
|
|
149
|
+
console.log(` cloud box: ${existingSession.boxId} (nano)`)
|
|
150
|
+
console.log(` simulator: ${added.receipt.simId}`)
|
|
151
|
+
console.log(` claim expires: ${added.receipt.claim.expiresAt}`)
|
|
152
|
+
console.log(
|
|
153
|
+
' commands: describe, find, get tree|node|count|memory, wait ready|selector, do, reset, logs, state, screenshot',
|
|
154
|
+
)
|
|
155
|
+
return 0
|
|
156
|
+
}
|
|
157
|
+
const { closeCloudBox, createCloudBox } = await import('../cloud-client')
|
|
140
158
|
const created = await createCloudBox({
|
|
141
159
|
bundlePath: input.bundlePath,
|
|
142
160
|
assetsPath: input.assetsPath ?? undefined,
|
|
@@ -193,6 +211,9 @@ export async function runPlatformCommand(
|
|
|
193
211
|
'--no-hmr',
|
|
194
212
|
'--no-describe',
|
|
195
213
|
'--quiet',
|
|
214
|
+
'--nano',
|
|
215
|
+
'--cloud',
|
|
216
|
+
'--micro',
|
|
196
217
|
],
|
|
197
218
|
stripValueFlags: [
|
|
198
219
|
'--base-url',
|
|
@@ -125,12 +125,18 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
|
|
|
125
125
|
category: 'rnx_cloud_sim',
|
|
126
126
|
count: 1,
|
|
127
127
|
},
|
|
128
|
+
'packages/sootsim/cli/cloud-client.ts :: fetch :: `${session.apiOrigin}/v1/boxes/${encodeURIComponent(session.boxId)}/simulators`':
|
|
129
|
+
{ category: 'rnx_cloud_sim', count: 1 },
|
|
128
130
|
'packages/sootsim/cli/cloud-client.ts :: fetch :: `${session.apiOrigin}/v1/sims/${encodeURIComponent(session.simId)}/claim`':
|
|
129
131
|
{ category: 'rnx_cloud_sim', count: 1 },
|
|
130
132
|
'packages/sootsim/cli/cloud-client.ts :: fetch :: `${session.apiOrigin}/v1/boxes/${encodeURIComponent(session.boxId)}`':
|
|
131
133
|
{ category: 'rnx_cloud_sim', count: 1 },
|
|
132
134
|
'packages/sootsim/cli/cloud-client.ts :: fetch :: `${this.session.apiOrigin}/v1/sims/${encodeURIComponent(this.session.simId)}/commands`':
|
|
133
135
|
{ category: 'rnx_cloud_sim', count: 1 },
|
|
136
|
+
'packages/sootsim/cli/cloud-client.ts :: fetch :: bundleUrl': {
|
|
137
|
+
category: 'guest_app_network',
|
|
138
|
+
count: 1,
|
|
139
|
+
},
|
|
134
140
|
// maestro's flow `http` API. the url is whatever the flow author wrote, and
|
|
135
141
|
// it is fetched from a child process because that API is synchronous.
|
|
136
142
|
'packages/sootsim/cli/internal-child.ts :: fetch :: request.url': {
|
package/cli/shell-init.ts
CHANGED
|
@@ -30,7 +30,7 @@ export function renderRnxShellInit(shell: string): string {
|
|
|
30
30
|
claim|close) ;;
|
|
31
31
|
ios)
|
|
32
32
|
for _rnx_shell_session in "$@"; do
|
|
33
|
-
if [ "$_rnx_shell_session" = '--nano' ]; then
|
|
33
|
+
if [ "$_rnx_shell_session" = '--nano' ] || [ "$_rnx_shell_session" = '--cloud' ]; then
|
|
34
34
|
_rnx_shell_create=1
|
|
35
35
|
break
|
|
36
36
|
fi
|
package/cli/ws-bridge.ts
CHANGED
|
@@ -706,14 +706,16 @@ export function createBridge(wsPort: number, opts: WsBridgeOptions = {}): WsBrid
|
|
|
706
706
|
}
|
|
707
707
|
} catch {}
|
|
708
708
|
ws.close()
|
|
709
|
-
//
|
|
710
|
-
// full 250ms after its work is done. that
|
|
711
|
-
// prompt latency, since the shell spawns
|
|
712
|
-
|
|
709
|
+
// clear the grace timer on a real close, or a one-shot `rnx` command is
|
|
710
|
+
// held on the event loop for the full 250ms after its work is done. that
|
|
711
|
+
// delay lands directly in in-box prompt latency, since the shell spawns
|
|
712
|
+
// rnx and waits on child close.
|
|
713
|
+
const terminateGrace = setTimeout(() => {
|
|
713
714
|
if (ws.readyState !== WebSocket.CLOSED) {
|
|
714
715
|
ws.terminate()
|
|
715
716
|
}
|
|
716
|
-
}, 250)
|
|
717
|
+
}, 250)
|
|
718
|
+
ws.once('close', () => clearTimeout(terminateGrace))
|
|
717
719
|
},
|
|
718
720
|
}
|
|
719
721
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.420 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.420 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.420 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.420 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.420 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.420 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.420 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/beta.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.420 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/beta.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.420 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.420 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.420 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.420 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.420 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|