rnxsim 0.1.433 → 0.1.434
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 +55 -13
- package/cli/commands/box.ts +89 -14
- package/cli/outbound-endpoints.ts +5 -0
- package/cli/send-to-box.ts +112 -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 +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +5324 -1371
- 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 +298 -91
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +313 -104
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +7 -1
- package/dist-lib/menu.mjs +7 -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 +302 -95
- 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 +5006 -1441
- package/dist-lib/vite.cjs +298 -91
- package/package.json +2 -2
- package/src/bridge-contract.ts +6 -0
- package/src/host/bridge-host.ts +79 -0
- package/src/host/replacement-module-handler.ts +20 -96
- package/src/menu.ts +8 -0
package/cli/cloud-client.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from '../../contrast-bundler/src/metroCloudArtifact'
|
|
25
25
|
import { inferMetroModuleIdentity } from '../../sootsim-engine/src/metro-fingerprint'
|
|
26
26
|
import { loadMetroFingerprintRegistry } from '../../sootsim-engine/src/metro-fingerprint-registry-client'
|
|
27
|
+
import { isLoopbackHost } from '../src/backend-origin'
|
|
27
28
|
import {
|
|
28
29
|
isRnxCloudCommandType,
|
|
29
30
|
RNX_CLOUD_MAX_ARTIFACT_BYTES,
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
type CloudSession,
|
|
44
45
|
} from './cloud-session'
|
|
45
46
|
import { detectProject, findProjectRoot } from './commands/detect'
|
|
47
|
+
import type { StorageSnapshot } from '../../sootsim-engine/src/preview/storage-snapshot-contract'
|
|
46
48
|
import type {
|
|
47
49
|
BridgeClaimResult,
|
|
48
50
|
BridgeSimInfo,
|
|
@@ -74,6 +76,14 @@ interface CreateCloudBoxOptions {
|
|
|
74
76
|
/** the `--assets-dest` directory Metro wrote beside the bundle. */
|
|
75
77
|
assetsPath?: string
|
|
76
78
|
apiOrigin?: string
|
|
79
|
+
/** the name this box carries in the account's box list. */
|
|
80
|
+
boxName?: string | null
|
|
81
|
+
/**
|
|
82
|
+
* the storage and route the first simulator boots with, captured from a
|
|
83
|
+
* running app. a snapshot reaches a megabyte, so a create that carries one
|
|
84
|
+
* is multipart rather than a bare artifact body.
|
|
85
|
+
*/
|
|
86
|
+
storage?: StorageSnapshot | null
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
interface CloudCommandEnvelope {
|
|
@@ -200,11 +210,27 @@ async function fetchBundleSource(bundleUrl: string): Promise<string> {
|
|
|
200
210
|
return text
|
|
201
211
|
}
|
|
202
212
|
|
|
213
|
+
// a bundle served over plain http is only trustworthy when it comes from this
|
|
214
|
+
// machine, which is what "send to box" reads: the Metro dev server the app is
|
|
215
|
+
// already running on.
|
|
216
|
+
function isLoopbackBundleUrl(bundleInput: string): boolean {
|
|
217
|
+
if (!bundleInput.startsWith('http://')) return false
|
|
218
|
+
try {
|
|
219
|
+
return isLoopbackHost(new URL(bundleInput).hostname)
|
|
220
|
+
} catch {
|
|
221
|
+
return false
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isRemoteBundleInput(bundleInput: string): boolean {
|
|
226
|
+
return bundleInput.startsWith('https://') || bundleInput.startsWith('http://')
|
|
227
|
+
}
|
|
228
|
+
|
|
203
229
|
async function loadBundleSource(bundleInput: string): Promise<string> {
|
|
204
|
-
if (bundleInput.startsWith('http://')) {
|
|
230
|
+
if (bundleInput.startsWith('http://') && !isLoopbackBundleUrl(bundleInput)) {
|
|
205
231
|
throw new Error(`${REMOTE_COMMAND} bundle URL must use HTTPS`)
|
|
206
232
|
}
|
|
207
|
-
if (bundleInput
|
|
233
|
+
if (isRemoteBundleInput(bundleInput)) {
|
|
208
234
|
return fetchBundleSource(bundleInput)
|
|
209
235
|
}
|
|
210
236
|
return readImmutableBundleSource(bundleInput)
|
|
@@ -606,7 +632,7 @@ export async function produceCloudArtifact(
|
|
|
606
632
|
// has nothing to search for.
|
|
607
633
|
const search: MetroAssetSearch = assetsPath
|
|
608
634
|
? explicitMetroAssetDest(resolve(assetsPath), descriptors)
|
|
609
|
-
: descriptors.length > 0 && !bundlePath
|
|
635
|
+
: descriptors.length > 0 && !isRemoteBundleInput(bundlePath)
|
|
610
636
|
? searchMetroAssetDest(bundlePath, descriptors)
|
|
611
637
|
: { dest: null, searched: [], truncated: false }
|
|
612
638
|
const dest = search.dest
|
|
@@ -772,20 +798,36 @@ export async function createCloudBox(options: CreateCloudBoxOptions): Promise<{
|
|
|
772
798
|
}> {
|
|
773
799
|
const apiOrigin = resolveCloudOrigin(options.apiOrigin)
|
|
774
800
|
const artifact = await produceCloudArtifact(options.bundlePath, options.assetsPath)
|
|
801
|
+
const boxName = options.boxName?.trim()
|
|
802
|
+
const headers: Record<string, string> = {
|
|
803
|
+
authorization: options.authorization,
|
|
804
|
+
'x-rnx-box-size': 'nano',
|
|
805
|
+
'x-rnx-artifact-sha256': artifact.sha256,
|
|
806
|
+
'x-rnx-platform': 'ios',
|
|
807
|
+
'x-rnx-device': options.device,
|
|
808
|
+
...(options.accountId ? { 'x-rnx-account-id': options.accountId } : null),
|
|
809
|
+
...(boxName ? { 'x-rnx-box-name': boxName } : null),
|
|
810
|
+
}
|
|
811
|
+
let body: BodyInit
|
|
812
|
+
if (options.storage) {
|
|
813
|
+
const form = new FormData()
|
|
814
|
+
form.append(
|
|
815
|
+
'artifact',
|
|
816
|
+
new Blob([new Uint8Array(artifact.bytes)], { type: 'application/javascript' }),
|
|
817
|
+
'bundle.js',
|
|
818
|
+
)
|
|
819
|
+
form.append('storage', JSON.stringify(options.storage))
|
|
820
|
+
body = form
|
|
821
|
+
} else {
|
|
822
|
+
headers['content-type'] = 'application/javascript'
|
|
823
|
+
body = new Uint8Array(artifact.bytes)
|
|
824
|
+
}
|
|
775
825
|
let response: Response
|
|
776
826
|
try {
|
|
777
827
|
response = await fetch(`${apiOrigin}/v1/boxes`, {
|
|
778
828
|
method: 'POST',
|
|
779
|
-
headers
|
|
780
|
-
|
|
781
|
-
'content-type': 'application/javascript',
|
|
782
|
-
'x-rnx-box-size': 'nano',
|
|
783
|
-
'x-rnx-artifact-sha256': artifact.sha256,
|
|
784
|
-
'x-rnx-platform': 'ios',
|
|
785
|
-
'x-rnx-device': options.device,
|
|
786
|
-
...(options.accountId ? { 'x-rnx-account-id': options.accountId } : {}),
|
|
787
|
-
},
|
|
788
|
-
body: new Uint8Array(artifact.bytes),
|
|
829
|
+
headers,
|
|
830
|
+
body,
|
|
789
831
|
})
|
|
790
832
|
} catch (error) {
|
|
791
833
|
throw new Error(
|
package/cli/commands/box.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
resolveCloudBoxReference,
|
|
28
28
|
} from 'rnx-cloud-box/client'
|
|
29
29
|
import { rnxPublicBrand } from '../../src/public-brand'
|
|
30
|
-
import { authHeaderOrExit, cloudAccountIdOrExit } from '../auth'
|
|
30
|
+
import { authHeaderOrExit, cloudAccountIdOrExit, type CliAuth } from '../auth'
|
|
31
31
|
import { findRepositoryPackageRoot } from '../setup-repository'
|
|
32
32
|
import { resolvePlacement } from '../ws-bridge'
|
|
33
33
|
import { CheckoutFilePlane, CheckoutRequiresGitError } from './box/checkout-plane'
|
|
@@ -69,6 +69,9 @@ function usage(): void {
|
|
|
69
69
|
${rnx} box create --json create without entering the shell
|
|
70
70
|
${rnx} box connect <id> enter the shell of a cloud box you own
|
|
71
71
|
${rnx} box connect <id> --json print its reference without a shell
|
|
72
|
+
${rnx} box send send the running local app to a nano box
|
|
73
|
+
${rnx} box send --name=<n> name the box it creates
|
|
74
|
+
${rnx} box send --no-auth leave the signed-in session behind
|
|
72
75
|
${rnx} box open <id> watch a cloud box in your browser
|
|
73
76
|
${rnx} box open <id> --print print the viewing URL instead of opening it
|
|
74
77
|
${rnx} box ls cloud boxes running on this account
|
|
@@ -105,6 +108,7 @@ export async function runBox(
|
|
|
105
108
|
const subcommand = args.find((arg) => !arg.startsWith('-'))
|
|
106
109
|
if (subcommand === 'create') return runCloudBoxCreate(args, opts)
|
|
107
110
|
if (subcommand === 'connect') return runCloudBoxConnect(args)
|
|
111
|
+
if (subcommand === 'send') return runBoxSend(args, opts)
|
|
108
112
|
if (subcommand === 'open') return runCloudBoxOpen(args)
|
|
109
113
|
if (subcommand === 'stop' || subcommand === 'delete') {
|
|
110
114
|
return runCloudBoxStop(args, subcommand === 'delete')
|
|
@@ -138,12 +142,15 @@ function flag(args: string[], name: string): string | undefined {
|
|
|
138
142
|
return undefined
|
|
139
143
|
}
|
|
140
144
|
|
|
141
|
-
|
|
145
|
+
// the account service, which owns /api/v1/rnx/sessions and the box connect
|
|
146
|
+
// route. a login session names the origin it was issued by; an api key
|
|
147
|
+
// carries none, so it goes where every other key-authenticated command goes.
|
|
148
|
+
function apiOrigin(args: string[], auth: CliAuth): string {
|
|
142
149
|
return (
|
|
143
150
|
flag(args, 'api-origin') ??
|
|
144
151
|
process.env[API_ORIGIN_ENV]?.trim() ??
|
|
145
|
-
|
|
146
|
-
)
|
|
152
|
+
(auth.kind === 'session' ? auth.origin : 'https://contrast.dev')
|
|
153
|
+
).replace(/\/+$/, '')
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
function requireEndpoint(args: string[], label: string): string | null {
|
|
@@ -157,6 +164,46 @@ function requireEndpoint(args: string[], label: string): string | null {
|
|
|
157
164
|
return null
|
|
158
165
|
}
|
|
159
166
|
|
|
167
|
+
// `rnx box send` — the CLI half of "send to box". the shell's rail button runs
|
|
168
|
+
// the same function inside the daemon; this is the same send for an agent or a
|
|
169
|
+
// terminal, and it prints the id and URL either way.
|
|
170
|
+
async function runBoxSend(args: string[], opts: BoxCommandOptions): Promise<number> {
|
|
171
|
+
const rnx = rnxPublicBrand.commandName
|
|
172
|
+
const { auth, header } = authHeaderOrExit('box send')
|
|
173
|
+
const accountId = cloudAccountIdOrExit(auth)
|
|
174
|
+
const { createBridgeFromParsed, parseBridgeCliArgs } = await import('../ws-bridge')
|
|
175
|
+
const parsed = parseBridgeCliArgs(
|
|
176
|
+
args.filter((arg) => arg !== 'send'),
|
|
177
|
+
{
|
|
178
|
+
port: opts.port,
|
|
179
|
+
stripBooleanFlags: ['--no-auth', '--json'],
|
|
180
|
+
stripValueFlags: ['--name', '--assets', '--api-origin'],
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
const bridge = createBridgeFromParsed(parsed)
|
|
184
|
+
try {
|
|
185
|
+
const { sendToBox } = await import('../send-to-box')
|
|
186
|
+
const result = await sendToBox({
|
|
187
|
+
send: (command) => bridge.send(command),
|
|
188
|
+
authorization: header,
|
|
189
|
+
accountId,
|
|
190
|
+
carryAuth: !args.includes('--no-auth'),
|
|
191
|
+
boxName: flag(args, 'name') ?? null,
|
|
192
|
+
assetsPath: flag(args, 'assets') ?? null,
|
|
193
|
+
apiOrigin: flag(args, 'api-origin'),
|
|
194
|
+
})
|
|
195
|
+
if (args.includes('--json')) console.log(JSON.stringify(result))
|
|
196
|
+
return 0
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error(
|
|
199
|
+
` ${rnx} box send: ${error instanceof Error ? error.message : String(error)}`,
|
|
200
|
+
)
|
|
201
|
+
return 1
|
|
202
|
+
} finally {
|
|
203
|
+
bridge.close()
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
160
207
|
async function runCloudBoxStop(args: string[], purge: boolean): Promise<number> {
|
|
161
208
|
const rnx = rnxPublicBrand.commandName
|
|
162
209
|
const label = purge ? 'delete' : 'stop'
|
|
@@ -174,7 +221,7 @@ async function runCloudBoxStop(args: string[], purge: boolean): Promise<number>
|
|
|
174
221
|
const client = createBoxClient(
|
|
175
222
|
createCloudBoxProvider({
|
|
176
223
|
endpoint,
|
|
177
|
-
authority: { kind: 'account', apiOrigin: apiOrigin(args), apiKey, accountId },
|
|
224
|
+
authority: { kind: 'account', apiOrigin: apiOrigin(args, auth), apiKey, accountId },
|
|
178
225
|
}),
|
|
179
226
|
)
|
|
180
227
|
try {
|
|
@@ -204,7 +251,7 @@ async function runCloudBoxList(args: string[]): Promise<number> {
|
|
|
204
251
|
const client = createBoxClient(
|
|
205
252
|
createCloudBoxProvider({
|
|
206
253
|
endpoint: flag(args, 'endpoint') ?? process.env[ENDPOINT_ENV]?.trim() ?? '',
|
|
207
|
-
authority: { kind: 'account', apiOrigin: apiOrigin(args), apiKey, accountId },
|
|
254
|
+
authority: { kind: 'account', apiOrigin: apiOrigin(args, auth), apiKey, accountId },
|
|
208
255
|
}),
|
|
209
256
|
)
|
|
210
257
|
try {
|
|
@@ -244,9 +291,9 @@ async function runCloudBoxConnect(args: string[]): Promise<number> {
|
|
|
244
291
|
// the ACCOUNT's credential. the account service is the only thing that knows
|
|
245
292
|
// which account a Box belongs to, so it resolves the name and hands back the
|
|
246
293
|
// Box's own token; nothing here writes that token down.
|
|
247
|
-
const { header } = authHeaderOrExit('box connect')
|
|
294
|
+
const { auth, header } = authHeaderOrExit('box connect')
|
|
248
295
|
const apiKey = header.replace(/^Bearer /, '')
|
|
249
|
-
const origin = apiOrigin(args)
|
|
296
|
+
const origin = apiOrigin(args, auth)
|
|
250
297
|
|
|
251
298
|
let session: BoxShellSession | null = null
|
|
252
299
|
let heartbeat: { stop: () => void } | null = null
|
|
@@ -318,12 +365,12 @@ async function runCloudBoxOpen(args: string[]): Promise<number> {
|
|
|
318
365
|
// the ACCOUNT's credential, for the same reason connect takes one: the
|
|
319
366
|
// account service is what knows which account owns this Box, and it hands
|
|
320
367
|
// back the Box's own token rather than this process storing one.
|
|
321
|
-
const { header } = authHeaderOrExit('box open')
|
|
368
|
+
const { auth, header } = authHeaderOrExit('box open')
|
|
322
369
|
const apiKey = header.replace(/^Bearer /, '')
|
|
323
370
|
|
|
324
371
|
try {
|
|
325
372
|
const reference = await resolveCloudBoxReference({
|
|
326
|
-
apiOrigin: apiOrigin(args),
|
|
373
|
+
apiOrigin: apiOrigin(args, auth),
|
|
327
374
|
apiKey,
|
|
328
375
|
boxId,
|
|
329
376
|
})
|
|
@@ -443,7 +490,7 @@ async function runCloudBoxCreate(
|
|
|
443
490
|
const client = createBoxClient(
|
|
444
491
|
createCloudBoxProvider({
|
|
445
492
|
endpoint,
|
|
446
|
-
authority: { kind: 'account', apiOrigin: apiOrigin(args), apiKey, accountId },
|
|
493
|
+
authority: { kind: 'account', apiOrigin: apiOrigin(args, auth), apiKey, accountId },
|
|
447
494
|
}),
|
|
448
495
|
)
|
|
449
496
|
let session: BoxShellSession | null = null
|
|
@@ -487,7 +534,7 @@ async function runCloudBoxCreate(
|
|
|
487
534
|
const sessionId = box.description.sessionId
|
|
488
535
|
if (!sessionId) throw new BoxClientError('the box service returned no session id')
|
|
489
536
|
heartbeat = heartbeatSession({
|
|
490
|
-
apiOrigin: apiOrigin(args),
|
|
537
|
+
apiOrigin: apiOrigin(args, auth),
|
|
491
538
|
apiKey,
|
|
492
539
|
sessionId,
|
|
493
540
|
})
|
|
@@ -642,15 +689,30 @@ async function repl(
|
|
|
642
689
|
let inputEnded = false
|
|
643
690
|
// the user typed `exit`: abandon anything still queued behind it.
|
|
644
691
|
let stopped = false
|
|
692
|
+
// the command running right now, so Ctrl-C interrupts it rather than the CLI.
|
|
693
|
+
let interrupt: AbortController | null = null
|
|
645
694
|
|
|
646
695
|
const run = async (command: string) => {
|
|
647
696
|
if (stopped) return
|
|
697
|
+
const controller = new AbortController()
|
|
698
|
+
interrupt = controller
|
|
648
699
|
try {
|
|
649
700
|
await beforeCommand?.()
|
|
650
|
-
const result = await session.exec(command
|
|
651
|
-
|
|
701
|
+
const result = await session.exec(command, {
|
|
702
|
+
signal: controller.signal,
|
|
703
|
+
// written as it arrives rather than collected: a terminal shows a
|
|
704
|
+
// command's output while it runs, and stderr keeps its own stream so
|
|
705
|
+
// piping the shell's stdout somewhere still separates the two.
|
|
706
|
+
onOutput: (stream, chunk) => {
|
|
707
|
+
if (stream === 'stdout') process.stdout.write(chunk)
|
|
708
|
+
else process.stderr.write(chunk)
|
|
709
|
+
},
|
|
710
|
+
})
|
|
711
|
+
if (result.exitCode !== 0) console.log(`exit code: ${result.exitCode}`)
|
|
652
712
|
} catch (error) {
|
|
653
713
|
console.error(error instanceof Error ? error.message : String(error))
|
|
714
|
+
} finally {
|
|
715
|
+
interrupt = null
|
|
654
716
|
}
|
|
655
717
|
if (!inputEnded && !stopped) prompt()
|
|
656
718
|
}
|
|
@@ -658,6 +720,19 @@ async function repl(
|
|
|
658
720
|
return new Promise<number>((resolve) => {
|
|
659
721
|
prompt()
|
|
660
722
|
|
|
723
|
+
// Ctrl-C while a command is running interrupts the command. at an idle
|
|
724
|
+
// prompt there is nothing to interrupt, so it still leaves the shell,
|
|
725
|
+
// which is the only way out of a box that has stopped answering.
|
|
726
|
+
rl.on('SIGINT', () => {
|
|
727
|
+
if (interrupt) {
|
|
728
|
+
process.stdout.write('^C\n')
|
|
729
|
+
interrupt.abort()
|
|
730
|
+
return
|
|
731
|
+
}
|
|
732
|
+
stopped = true
|
|
733
|
+
rl.close()
|
|
734
|
+
})
|
|
735
|
+
|
|
661
736
|
rl.on('line', (line) => {
|
|
662
737
|
const command = line.trim()
|
|
663
738
|
if (!command) {
|
|
@@ -237,6 +237,11 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
|
|
|
237
237
|
},
|
|
238
238
|
'packages/sootsim/scripts/smoke-cli-binary.ts :: fetch :: `http://127.0.0.1:${port}/healthz`':
|
|
239
239
|
{ category: 'local_development', count: 1 },
|
|
240
|
+
// the binary smoke's replacement-module probe against its own loopback server
|
|
241
|
+
'packages/sootsim/scripts/smoke-cli-binary.ts :: fetch :: url': {
|
|
242
|
+
category: 'local_development',
|
|
243
|
+
count: 1,
|
|
244
|
+
},
|
|
240
245
|
'packages/sootsim-engine/src/auth/openLogin.ts :: fetch :: `${CONTRAST_ORIGIN}/api/dev-login`':
|
|
241
246
|
{ category: 'contrast_user_action', count: 1 },
|
|
242
247
|
'packages/sootsim-engine/src/auth/shared-session.ts :: fetch :: `${CONTRAST_ORIGIN}/api/auth/me`':
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// "send to box": the running local app becomes a nano cloud simulator.
|
|
2
|
+
//
|
|
3
|
+
// one function serves both callers. the rail button in the shell reaches it
|
|
4
|
+
// through the local daemon's `/__send-to-box` route, and `rnx box send` calls
|
|
5
|
+
// it directly; both hand it a way to run a bridge command against the sim that
|
|
6
|
+
// is running the app, and both get the same simulator id and URL back.
|
|
7
|
+
|
|
8
|
+
import { isStorageSnapshot } from '../../sootsim-engine/src/preview/storage-snapshot-contract'
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_DEVICE_BY_PLATFORM,
|
|
11
|
+
getRuntimePlatform,
|
|
12
|
+
listDeviceModels,
|
|
13
|
+
} from '../../sootsim-engine/src/settings/devices'
|
|
14
|
+
import { toRNXProductionBundleUrl } from '../src/metro-production-bundle'
|
|
15
|
+
import { createCloudBox } from './cloud-client'
|
|
16
|
+
import type { StorageSnapshot } from '../../sootsim-engine/src/preview/storage-snapshot-contract'
|
|
17
|
+
import type { DeviceModel } from '../../sootsim-engine/src/settings/devices'
|
|
18
|
+
import type { BridgeCommandType } from '../src/bridge-contract'
|
|
19
|
+
|
|
20
|
+
export interface SendToBoxOptions {
|
|
21
|
+
/** runs one bridge command against the sim running the app. */
|
|
22
|
+
send: (command: { type: BridgeCommandType; [key: string]: unknown }) => Promise<unknown>
|
|
23
|
+
authorization: string
|
|
24
|
+
/** the account the box bills. an api key names its own, so it sends none. */
|
|
25
|
+
accountId: string | null
|
|
26
|
+
/** the cloud device model. defaults to the device the sim is running on. */
|
|
27
|
+
device?: DeviceModel
|
|
28
|
+
/**
|
|
29
|
+
* carry the signed-in session into the box. on by default in the shell: the
|
|
30
|
+
* box is the sender's own, and an app that boots signed out is not the app
|
|
31
|
+
* they were looking at.
|
|
32
|
+
*/
|
|
33
|
+
carryAuth: boolean
|
|
34
|
+
boxName?: string | null
|
|
35
|
+
/** the `--assets-dest` directory Metro wrote, when the sender knows it. */
|
|
36
|
+
assetsPath?: string | null
|
|
37
|
+
apiOrigin?: string
|
|
38
|
+
simId?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SendToBoxResult {
|
|
42
|
+
boxId: string
|
|
43
|
+
simId: string
|
|
44
|
+
url: string
|
|
45
|
+
carriedAuth: boolean
|
|
46
|
+
warnings: string[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
50
|
+
return !!value && typeof value === 'object' && !Array.isArray(value)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// the box opens on the device the sender was looking at. a remote simulator is
|
|
54
|
+
// iOS only, so an Android sim sends to the default iPhone rather than refusing.
|
|
55
|
+
function cloudDevice(capturedModel: unknown): DeviceModel {
|
|
56
|
+
const captured = listDeviceModels().find((model) => model === capturedModel)
|
|
57
|
+
return captured && getRuntimePlatform(captured) === 'ios'
|
|
58
|
+
? captured
|
|
59
|
+
: DEFAULT_DEVICE_BY_PLATFORM.ios
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function sendToBox(options: SendToBoxOptions): Promise<SendToBoxResult> {
|
|
63
|
+
const captured: unknown = await options.send({
|
|
64
|
+
type: 'evaluate',
|
|
65
|
+
simId: options.simId,
|
|
66
|
+
code: '(typeof window.__sootsimCaptureBundle === "function") ? window.__sootsimCaptureBundle() : null',
|
|
67
|
+
})
|
|
68
|
+
const bundleUrl = isRecord(captured) ? captured.bundleUrl : null
|
|
69
|
+
if (!isRecord(captured) || typeof bundleUrl !== 'string' || !bundleUrl) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
'send to box needs a simulator running an app: this one has no bundle loaded',
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
// the snapshot is taken before the production bundle is fetched, so the box
|
|
75
|
+
// gets the state the app was in when the button was pressed rather than the
|
|
76
|
+
// state it drifted to while Metro rebuilt.
|
|
77
|
+
const dumped: unknown = await options.send({
|
|
78
|
+
type: 'storageSnapshot',
|
|
79
|
+
simId: options.simId,
|
|
80
|
+
storageSnapshotOptions: { includeCredentials: options.carryAuth },
|
|
81
|
+
})
|
|
82
|
+
if (!isStorageSnapshot(dumped)) {
|
|
83
|
+
throw new Error('the simulator returned a storage snapshot this build cannot read')
|
|
84
|
+
}
|
|
85
|
+
const storage: StorageSnapshot = dumped
|
|
86
|
+
const created = await createCloudBox({
|
|
87
|
+
bundlePath: toRNXProductionBundleUrl(bundleUrl),
|
|
88
|
+
assetsPath: options.assetsPath ?? undefined,
|
|
89
|
+
device:
|
|
90
|
+
options.device ??
|
|
91
|
+
cloudDevice(isRecord(captured.deviceSpec) ? captured.deviceSpec.model : null),
|
|
92
|
+
authorization: options.authorization,
|
|
93
|
+
accountId: options.accountId,
|
|
94
|
+
apiOrigin: options.apiOrigin,
|
|
95
|
+
boxName: options.boxName ?? null,
|
|
96
|
+
storage,
|
|
97
|
+
})
|
|
98
|
+
const simId = created.receipt.simulator.simId
|
|
99
|
+
const url = `${created.session.apiOrigin.replace(/\/$/, '')}/sim/${simId}`
|
|
100
|
+
// the local terminal is where an agent driving this checkout reads results,
|
|
101
|
+
// so the id and the URL land there whichever caller asked for the send.
|
|
102
|
+
console.log(` box simulator: ${simId}`)
|
|
103
|
+
console.log(` ${url}`)
|
|
104
|
+
for (const warning of created.warnings) console.warn(` warning: ${warning}`)
|
|
105
|
+
return {
|
|
106
|
+
boxId: created.receipt.boxId,
|
|
107
|
+
simId,
|
|
108
|
+
url,
|
|
109
|
+
carriedAuth: options.carryAuth,
|
|
110
|
+
warnings: created.warnings,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.434 | (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.434 | (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.434 | (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.434 | (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.434 | (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.434 | (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.434 | (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.434 | (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.434 | (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.434 | (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.434 | (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.434 | (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.434 | (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.434 | (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/cloud.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.434 | (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/cloud.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.434 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
4
4
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
package/dist-lib/config.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.434 | (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/detox/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.434 | (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.434 | (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/home-paths.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.434 | (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;
|