peachsim 0.1.527 → 0.1.570
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 +26 -26
- package/cli/app-fonts.ts +1 -1
- package/cli/app-splash.ts +1 -1
- package/cli/bin.ts +1 -1
- package/cli/bridge-diagnostics.ts +1 -1
- package/cli/bridge-flow-runner.ts +57 -5
- package/cli/cloud-client.ts +273 -6
- package/cli/cloud-dispatch.ts +22 -2
- package/cli/cloud-session.ts +60 -4
- package/cli/cloud-test-tunnel.ts +354 -0
- package/cli/commands/box/bundle-plane.ts +2 -2
- package/cli/commands/box/checkout-plane.ts +1 -1
- package/cli/commands/box/rnx-command.ts +2 -2
- package/cli/commands/box/search-command.ts +2 -2
- package/cli/commands/box.ts +8 -8
- package/cli/commands/compat.ts +1 -1
- package/cli/commands/control.ts +22 -3
- package/cli/commands/detox.ts +1 -1
- package/cli/commands/device.ts +1 -1
- package/cli/commands/diagnose.ts +1 -1
- package/cli/commands/film.ts +1 -1
- package/cli/commands/flow.ts +60 -3
- package/cli/commands/inspect/actions.ts +1 -1
- package/cli/commands/inspect/core.ts +6 -7
- package/cli/commands/inspect/find.ts +2 -1
- package/cli/commands/inspect/resolve-target.ts +5 -3
- package/cli/commands/inspect/shared.ts +1 -1
- package/cli/commands/inspect/wait-event.ts +1 -1
- package/cli/commands/inspect/wait-selector.ts +15 -4
- package/cli/commands/inspect.ts +19 -14
- package/cli/commands/maestro.ts +464 -43
- package/cli/commands/permissions.ts +1 -1
- package/cli/commands/platform.ts +205 -8
- package/cli/commands/preview.ts +2 -2
- package/cli/commands/record.ts +2 -2
- package/cli/commands/report-issue.ts +2 -2
- package/cli/commands/screenshot-layers.ts +1 -1
- package/cli/commands/screenshot.ts +1 -1
- package/cli/commands/setup.ts +2 -2
- package/cli/commands/test.ts +440 -115
- package/cli/commands/three-mode.ts +1 -1
- package/cli/commands/upload.ts +5 -5
- package/cli/commands/what-happened.ts +1 -1
- package/cli/desktop-companion.ts +3 -3
- package/cli/drivers/playwright-sim-host.ts +2 -2
- package/cli/flow-file.ts +7 -0
- package/cli/flow-live-status.ts +65 -8
- package/cli/help-core.ts +3 -3
- package/cli/hidden-runtime-alias.ts +1 -1
- package/cli/maestro-report.ts +178 -0
- package/cli/main.ts +1 -1
- package/cli/outbound-endpoints.ts +185 -136
- package/cli/parse-args.ts +1 -1
- package/cli/registry.ts +1 -1
- package/cli/resolve-assets.ts +1 -1
- package/cli/run-registry.ts +8 -0
- package/cli/send-to-box.ts +4 -4
- package/cli/ws-bridge.ts +3 -5
- package/detox/expectations.ts +1 -1
- package/detox/gestures.ts +2 -2
- package/detox/index.ts +71 -29
- package/detox/jest-preset.cjs +3 -3
- package/detox/jest-setup-after-env.cjs +1 -1
- package/dist-lib/agent-daemon-client.cjs +2 -2
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +2 -2
- package/dist-lib/attached-projects.cjs +2 -2
- 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 +94 -3
- package/dist-lib/capture-contract.mjs +93 -3
- 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 +2 -2
- package/dist-lib/cloud.mjs +2 -2
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +106 -25
- package/dist-lib/detox/jest-preset.cjs +3 -3
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +137 -61
- package/dist-lib/host/fetch-proxy-handler.cjs +28 -2
- package/dist-lib/host/fetch-proxy-overrides.cjs +33 -3
- package/dist-lib/host/fetch-proxy-overrides.mjs +30 -2
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +209 -16
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/menu.cjs +2 -2
- package/dist-lib/menu.mjs +2 -2
- 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 +3 -3
- package/dist-lib/metro-production-bundle.mjs +3 -3
- package/dist-lib/metro.cjs +8 -8
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +2 -2
- 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 +31 -15
- package/dist-lib/sdk.cjs +3 -2
- package/dist-lib/sdk.mjs +3 -2
- package/dist-lib/skills.cjs +362 -163
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +583 -16
- package/dist-lib/vite.cjs +29 -14
- package/package.json +9 -9
- package/scripts/dev-server-scanner.ts +41 -14
- package/scripts/optional-demo-registry.ts +3 -3
- package/skills/contrast/SKILL.md +9 -9
- package/skills/rnx-debug/SKILL.md +14 -4
- package/skills/rnx-perf/SKILL.md +5 -5
- package/skills/rnx-setup/SKILL.md +11 -11
- package/skills/rnx-test/SKILL.md +11 -11
- package/skills/rnx-visual/SKILL.md +6 -6
- package/src/agent-sessions.ts +3 -3
- package/src/attached-projects.ts +1 -1
- package/src/bridge-contract-input.ts +1 -1
- package/src/bridge-contract.ts +23 -5
- package/src/capture-contract.ts +145 -3
- package/src/config.ts +2 -2
- package/src/connect.ts +2 -2
- package/src/home-paths.ts +3 -3
- package/src/host/bridge-host.ts +2 -2
- package/src/host/fetch-proxy-handler.ts +4 -3
- package/src/host/fetch-proxy-overrides.ts +41 -0
- package/src/host/plane-host.ts +2 -2
- package/src/index.ts +3 -0
- package/src/metro-plugin.ts +4 -4
- package/src/metro-production-bundle.ts +2 -2
- package/src/native-seam-manifest.ts +6 -6
- package/src/public-brand.ts +1 -1
- package/src/rolldown-plugin-swift.ts +8 -5
- package/src/runtime-assets.ts +8 -6
- package/src/screenshots/frame-compose.ts +24 -56
- package/src/screenshots/org-deck.ts +1 -1
- package/src/screenshots/registry.ts +4 -4
- package/src/screenshots/schema.ts +1 -1
- package/src/sdk.ts +2 -2
- package/src/settings-bundle.ts +1 -1
- package/src/sim-client.ts +2 -4
- package/src/skills/builtin/compat-check.ts +1 -1
- package/src/vite-plugin-one.ts +1 -1
- package/src/vite-plugin-swift.ts +11 -6
- package/src/vite-plugin.ts +30 -17
- package/src/worklets-babel.ts +2 -2
package/cli/cloud-client.ts
CHANGED
|
@@ -23,8 +23,8 @@ import {
|
|
|
23
23
|
buildRnxCloudArtifact,
|
|
24
24
|
type RnxCloudArtifactReceipt,
|
|
25
25
|
} from '../../contrast-bundler/src/metroCloudArtifact.ts'
|
|
26
|
-
import { loadMetroFingerprintRegistry } from '../../
|
|
27
|
-
import { inferMetroModuleIdentity } from '../../
|
|
26
|
+
import { loadMetroFingerprintRegistry } from '../../peach-engine/src/metro-fingerprint-registry-client.ts'
|
|
27
|
+
import { inferMetroModuleIdentity } from '../../peach-engine/src/metro-fingerprint.ts'
|
|
28
28
|
import { isLoopbackHost } from '../src/backend-origin.ts'
|
|
29
29
|
import {
|
|
30
30
|
isRnxCloudCommandType,
|
|
@@ -44,17 +44,18 @@ import {
|
|
|
44
44
|
type RNXMetroModuleIdentityMetadata,
|
|
45
45
|
} from '../src/metro-production-bundle.ts'
|
|
46
46
|
import { rnxPublicBrand } from '../src/public-brand.ts'
|
|
47
|
-
import { CHOOSE_ACCOUNT_HINT } from './auth.ts'
|
|
47
|
+
import { CHOOSE_ACCOUNT_HINT, resolveCliAuth } from './auth.ts'
|
|
48
48
|
import {
|
|
49
49
|
createCloudSession,
|
|
50
50
|
normalizeCloudOrigin,
|
|
51
51
|
readCloudSession,
|
|
52
|
+
type CloudBoxSession,
|
|
52
53
|
type CloudSession,
|
|
53
54
|
type CloudSimSession,
|
|
54
55
|
type RemotePlatform,
|
|
55
56
|
} from './cloud-session.ts'
|
|
56
57
|
import { detectProject } from './commands/detect.ts'
|
|
57
|
-
import type { StorageSnapshot } from '../../
|
|
58
|
+
import type { StorageSnapshot } from '../../peach-engine/src/preview/storage-snapshot-contract.ts'
|
|
58
59
|
import type {
|
|
59
60
|
BridgeClaimResult,
|
|
60
61
|
BridgeSimInfo,
|
|
@@ -69,6 +70,22 @@ export const MAX_RNX_CLOUD_BUNDLE_BYTES = RNX_CLOUD_MAX_ARTIFACT_BYTES
|
|
|
69
70
|
const REMOTE_COMMAND = 'rnx remote'
|
|
70
71
|
const PREVIEW_COMMAND = 'rnx preview'
|
|
71
72
|
|
|
73
|
+
export class UnsupportedRemoteHostError extends Error {
|
|
74
|
+
readonly code = 'unsupported_host' as const
|
|
75
|
+
readonly platform: string
|
|
76
|
+
readonly host: string
|
|
77
|
+
|
|
78
|
+
constructor(platform: string, host: string, message?: string) {
|
|
79
|
+
super(
|
|
80
|
+
message ??
|
|
81
|
+
`rnx remote ${platform} does not support --${host}: peachsim/cloud only creates iOS simulators`,
|
|
82
|
+
)
|
|
83
|
+
this.name = 'UnsupportedRemoteHostError'
|
|
84
|
+
this.platform = platform
|
|
85
|
+
this.host = host
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
72
89
|
// a Metro asset dest is a shallow tree of images. a walk that runs past this
|
|
73
90
|
// is pointed at something else entirely, and its extra entries cannot resolve
|
|
74
91
|
// an asset anyway.
|
|
@@ -1072,7 +1089,7 @@ function readSimCreateResponse(
|
|
|
1072
1089
|
// typed 400, never by a cli-side branch.
|
|
1073
1090
|
export async function createRemoteSim(options: CreateRemoteSimOptions): Promise<{
|
|
1074
1091
|
receipt: RnxCloudSimCreateResponse
|
|
1075
|
-
session:
|
|
1092
|
+
session: CloudSimSession
|
|
1076
1093
|
artifact: RnxCloudArtifactReceipt
|
|
1077
1094
|
assetsDirectory: string | null
|
|
1078
1095
|
warnings: string[]
|
|
@@ -1320,6 +1337,18 @@ export function resolveCloudSessionForParsed(
|
|
|
1320
1337
|
): CloudSession | null {
|
|
1321
1338
|
const session = readCloudSession()
|
|
1322
1339
|
if (!session) return null
|
|
1340
|
+
// a box holds one simulator and names it by the box, so --sim names a box
|
|
1341
|
+
// rather than a sim there and this shell already knows which box it is on.
|
|
1342
|
+
if (session.service === 'box') {
|
|
1343
|
+
if (parsed.simIdSource === 'flag' && parsed.simId !== session.boxId) {
|
|
1344
|
+
throw new Error(
|
|
1345
|
+
`this shell is connected to box ${session.boxId}; ${
|
|
1346
|
+
parsed.simId ? `--sim ${parsed.simId}` : 'the requested simulator'
|
|
1347
|
+
} is not available in this shell`,
|
|
1348
|
+
)
|
|
1349
|
+
}
|
|
1350
|
+
return session
|
|
1351
|
+
}
|
|
1323
1352
|
if (parsed.simIdSource === 'flag' && parsed.simId !== session.simId) {
|
|
1324
1353
|
throw new Error(
|
|
1325
1354
|
`this shell is connected to remote simulator ${session.simId}; ${
|
|
@@ -1436,12 +1465,248 @@ class CloudBridge implements WsBridge {
|
|
|
1436
1465
|
close(): void {}
|
|
1437
1466
|
}
|
|
1438
1467
|
|
|
1468
|
+
class CloudBoxBridge implements WsBridge {
|
|
1469
|
+
readonly plane = 'cloud' as const
|
|
1470
|
+
|
|
1471
|
+
constructor(private readonly session: CloudBoxSession) {}
|
|
1472
|
+
|
|
1473
|
+
async send(
|
|
1474
|
+
command: { type: string; [key: string]: unknown },
|
|
1475
|
+
options?: { timeoutMs?: number },
|
|
1476
|
+
): Promise<unknown> {
|
|
1477
|
+
const id = randomUUID()
|
|
1478
|
+
// the wire id is this request's, so the command carries none. `simId` names
|
|
1479
|
+
// a local sim among several and a box has exactly one.
|
|
1480
|
+
const payload: Record<string, unknown> = {}
|
|
1481
|
+
for (const [key, value] of Object.entries(command)) {
|
|
1482
|
+
if (key !== 'id' && key !== 'simId') payload[key] = value
|
|
1483
|
+
}
|
|
1484
|
+
let response: Response
|
|
1485
|
+
try {
|
|
1486
|
+
response = await fetch(
|
|
1487
|
+
`${this.session.apiOrigin}/v1/boxes/${encodeURIComponent(this.session.boxId)}/commands`,
|
|
1488
|
+
{
|
|
1489
|
+
method: 'POST',
|
|
1490
|
+
headers: {
|
|
1491
|
+
authorization: `Bearer ${this.session.boxToken}`,
|
|
1492
|
+
'content-type': 'application/json',
|
|
1493
|
+
},
|
|
1494
|
+
body: JSON.stringify({ id, command: payload }),
|
|
1495
|
+
...(options?.timeoutMs
|
|
1496
|
+
? { signal: AbortSignal.timeout(options.timeoutMs) }
|
|
1497
|
+
: {}),
|
|
1498
|
+
},
|
|
1499
|
+
)
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
throw new Error(
|
|
1502
|
+
`could not reach box ${this.session.boxId} at ${this.session.apiOrigin}: ${
|
|
1503
|
+
error instanceof Error ? error.message : String(error)
|
|
1504
|
+
}`,
|
|
1505
|
+
)
|
|
1506
|
+
}
|
|
1507
|
+
const text = await response.text()
|
|
1508
|
+
if (!response.ok) throw responseError(response.status, text)
|
|
1509
|
+
let parsed: unknown
|
|
1510
|
+
try {
|
|
1511
|
+
parsed = JSON.parse(text)
|
|
1512
|
+
} catch {
|
|
1513
|
+
throw new Error('box command returned unreadable JSON')
|
|
1514
|
+
}
|
|
1515
|
+
if (!isRecord(parsed) || parsed.id !== id) {
|
|
1516
|
+
throw new Error('box command returned an invalid command receipt')
|
|
1517
|
+
}
|
|
1518
|
+
if (typeof parsed.error === 'string' && parsed.error) throw new Error(parsed.error)
|
|
1519
|
+
if (!Object.hasOwn(parsed, 'result')) {
|
|
1520
|
+
throw new Error('box command receipt has no result')
|
|
1521
|
+
}
|
|
1522
|
+
return parsed.result
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
listSims(): Promise<BridgeSimInfo[]> {
|
|
1526
|
+
return Promise.reject(new Error('a box does not expose a local simulator list'))
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
focusSim(): Promise<never> {
|
|
1530
|
+
return Promise.reject(new Error('a box simulator has no browser window to focus'))
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
resolveReloadedSim(): Promise<string | null> {
|
|
1534
|
+
// cloud sims carry no local browser host, so there is no lineage to follow
|
|
1535
|
+
return Promise.resolve(null)
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
closeSim(): Promise<never> {
|
|
1539
|
+
return Promise.reject(new Error('close a box simulator with rnx box stop'))
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
claim(): Promise<BridgeClaimResult> {
|
|
1543
|
+
return Promise.reject(
|
|
1544
|
+
new Error('a box simulator is held by its Box page and has nothing to claim'),
|
|
1545
|
+
)
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
close(): void {}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1439
1551
|
export function createCloudBridgeForParsed(
|
|
1440
1552
|
parsed: Pick<ParsedBridgeCliArgs, 'simId' | 'simIdSource'>,
|
|
1441
1553
|
): WsBridge | null {
|
|
1442
1554
|
const session = resolveCloudSessionForParsed(parsed)
|
|
1443
1555
|
if (!session) return null
|
|
1444
|
-
return
|
|
1556
|
+
return session.service === 'box'
|
|
1557
|
+
? new CloudBoxBridge(session)
|
|
1558
|
+
: new CloudBridge(session)
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// stops the box, which ends the simulator it holds. an account api key stops
|
|
1562
|
+
// with its own authority; a session login stops with the login, falling back
|
|
1563
|
+
// to the box token the session already holds.
|
|
1564
|
+
export async function closeCloudBox(session: CloudSession): Promise<void> {
|
|
1565
|
+
if (session.service !== 'box') {
|
|
1566
|
+
if (!session.boxId || !session.boxToken) return
|
|
1567
|
+
const response = await fetch(
|
|
1568
|
+
`${session.apiOrigin}/v1/boxes/${encodeURIComponent(session.boxId)}`,
|
|
1569
|
+
{
|
|
1570
|
+
method: 'DELETE',
|
|
1571
|
+
headers: { authorization: `Bearer ${session.boxToken}` },
|
|
1572
|
+
},
|
|
1573
|
+
)
|
|
1574
|
+
const text = await response.text()
|
|
1575
|
+
if (!response.ok && response.status !== 404) {
|
|
1576
|
+
throw responseError(response.status, text)
|
|
1577
|
+
}
|
|
1578
|
+
return
|
|
1579
|
+
}
|
|
1580
|
+
const auth = resolveCliAuth()
|
|
1581
|
+
let authHeader = `Bearer ${session.boxToken}`
|
|
1582
|
+
if (auth?.kind === 'api-key') {
|
|
1583
|
+
authHeader = `Bearer ${auth.secret}`
|
|
1584
|
+
} else if (auth?.kind === 'session') {
|
|
1585
|
+
authHeader = `Bearer ${auth.token}`
|
|
1586
|
+
}
|
|
1587
|
+
let response: Response
|
|
1588
|
+
try {
|
|
1589
|
+
response = await fetch(
|
|
1590
|
+
`${session.apiOrigin}/v1/boxes/${encodeURIComponent(session.boxId)}/stop`,
|
|
1591
|
+
{
|
|
1592
|
+
method: 'POST',
|
|
1593
|
+
headers: { authorization: authHeader },
|
|
1594
|
+
},
|
|
1595
|
+
)
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
throw new Error(
|
|
1598
|
+
`could not reach box ${session.boxId} at ${session.apiOrigin}: ${
|
|
1599
|
+
error instanceof Error ? error.message : String(error)
|
|
1600
|
+
}`,
|
|
1601
|
+
)
|
|
1602
|
+
}
|
|
1603
|
+
const text = await response.text()
|
|
1604
|
+
if (!response.ok && response.status !== 404) {
|
|
1605
|
+
throw responseError(response.status, text)
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
export interface BoxDisplayGrant {
|
|
1610
|
+
url: string
|
|
1611
|
+
authToken: string
|
|
1612
|
+
expectedPeer: unknown
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// how long `rnx remote` waits for a fresh box's page to attach its simulator
|
|
1616
|
+
// before giving up on the watch url. the page boots the bundle after the
|
|
1617
|
+
// browser is up, and a large bundle takes a while to parse.
|
|
1618
|
+
const BOX_DISPLAY_GRANT_WAIT_MS = 180_000
|
|
1619
|
+
const BOX_DISPLAY_GRANT_RETRY_MS = 2_000
|
|
1620
|
+
|
|
1621
|
+
function readBoxDisplayGrant(value: unknown): BoxDisplayGrant {
|
|
1622
|
+
if (!isRecord(value) || !isRecord(value.grant)) {
|
|
1623
|
+
throw new Error('box display-grant returned an invalid grant')
|
|
1624
|
+
}
|
|
1625
|
+
const grant = value.grant
|
|
1626
|
+
if (
|
|
1627
|
+
typeof grant.url !== 'string' ||
|
|
1628
|
+
!grant.url ||
|
|
1629
|
+
typeof grant.authToken !== 'string' ||
|
|
1630
|
+
!grant.authToken ||
|
|
1631
|
+
!isRecord(grant.expectedPeer)
|
|
1632
|
+
) {
|
|
1633
|
+
throw new Error('box display-grant returned an invalid grant')
|
|
1634
|
+
}
|
|
1635
|
+
return { url: grant.url, authToken: grant.authToken, expectedPeer: grant.expectedPeer }
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// mints a watch-only display grant from the box and builds the bare shell
|
|
1639
|
+
// watch url for it: the same /sootsim/#remoteEngine fragment the nano service
|
|
1640
|
+
// mints, pointed at the box's display socket instead. the box token stays in
|
|
1641
|
+
// this process; only the grant travels in the fragment.
|
|
1642
|
+
export async function requestBoxWatchUrl(options: {
|
|
1643
|
+
apiOrigin: string
|
|
1644
|
+
boxId: string
|
|
1645
|
+
boxToken: string
|
|
1646
|
+
}): Promise<string> {
|
|
1647
|
+
const started = Date.now()
|
|
1648
|
+
for (;;) {
|
|
1649
|
+
let response: Response
|
|
1650
|
+
try {
|
|
1651
|
+
response = await fetch(
|
|
1652
|
+
`${options.apiOrigin}/v1/boxes/${encodeURIComponent(options.boxId)}/display-grant`,
|
|
1653
|
+
{
|
|
1654
|
+
method: 'POST',
|
|
1655
|
+
headers: {
|
|
1656
|
+
authorization: `Bearer ${options.boxToken}`,
|
|
1657
|
+
'content-type': 'application/json',
|
|
1658
|
+
},
|
|
1659
|
+
body: '{}',
|
|
1660
|
+
},
|
|
1661
|
+
)
|
|
1662
|
+
} catch (error) {
|
|
1663
|
+
throw new Error(
|
|
1664
|
+
`could not reach box ${options.boxId} at ${options.apiOrigin}: ${
|
|
1665
|
+
error instanceof Error ? error.message : String(error)
|
|
1666
|
+
}`,
|
|
1667
|
+
)
|
|
1668
|
+
}
|
|
1669
|
+
if (response.status === 409) {
|
|
1670
|
+
// the page has not attached its simulator yet. this is the normal state
|
|
1671
|
+
// right after a create, so wait rather than refusing.
|
|
1672
|
+
if (Date.now() - started >= BOX_DISPLAY_GRANT_WAIT_MS) {
|
|
1673
|
+
throw new Error(
|
|
1674
|
+
`box ${options.boxId} has no simulator hosting it yet; try again once its page has booted`,
|
|
1675
|
+
)
|
|
1676
|
+
}
|
|
1677
|
+
await new Promise((resolve) => setTimeout(resolve, BOX_DISPLAY_GRANT_RETRY_MS))
|
|
1678
|
+
continue
|
|
1679
|
+
}
|
|
1680
|
+
const text = await response.text()
|
|
1681
|
+
if (!response.ok) throw responseError(response.status, text)
|
|
1682
|
+
let parsed: unknown
|
|
1683
|
+
try {
|
|
1684
|
+
parsed = JSON.parse(text)
|
|
1685
|
+
} catch {
|
|
1686
|
+
throw new Error('box display-grant returned unreadable JSON')
|
|
1687
|
+
}
|
|
1688
|
+
const grant = readBoxDisplayGrant(parsed)
|
|
1689
|
+
const watch = new URL('/sootsim/', rnxPublicBrand.origin)
|
|
1690
|
+
// the shell boots its stage at its own default device, while the box page
|
|
1691
|
+
// hosts its simulator at the grant's device profile. without ?device= the
|
|
1692
|
+
// tenant refuses the host hello on dimensions and paints nothing — the
|
|
1693
|
+
// same param SimPage and BoxHostPage already set on their stage urls.
|
|
1694
|
+
if (
|
|
1695
|
+
isRecord(grant.expectedPeer) &&
|
|
1696
|
+
typeof grant.expectedPeer.deviceProfile === 'string' &&
|
|
1697
|
+
grant.expectedPeer.deviceProfile
|
|
1698
|
+
) {
|
|
1699
|
+
watch.searchParams.set('device', grant.expectedPeer.deviceProfile)
|
|
1700
|
+
}
|
|
1701
|
+
watch.hash = `remoteEngine=${encodeURIComponent(
|
|
1702
|
+
JSON.stringify({
|
|
1703
|
+
url: grant.url,
|
|
1704
|
+
authToken: grant.authToken,
|
|
1705
|
+
expectedPeer: grant.expectedPeer,
|
|
1706
|
+
}),
|
|
1707
|
+
)}`
|
|
1708
|
+
return watch.toString()
|
|
1709
|
+
}
|
|
1445
1710
|
}
|
|
1446
1711
|
|
|
1447
1712
|
function readClaimResponse(value: unknown): { id: string; expiresAt: number } {
|
|
@@ -1516,6 +1781,8 @@ export async function closeCloudSession(session: CloudSession): Promise<void> {
|
|
|
1516
1781
|
}
|
|
1517
1782
|
return
|
|
1518
1783
|
}
|
|
1784
|
+
// box sessions close through closeCloudBox; only sim sessions reach this leg.
|
|
1785
|
+
if (session.service !== 'sim') return
|
|
1519
1786
|
const response = await fetch(
|
|
1520
1787
|
`${session.apiOrigin}/v1/sims/${encodeURIComponent(session.simId)}`,
|
|
1521
1788
|
{
|
package/cli/cloud-dispatch.ts
CHANGED
|
@@ -157,9 +157,14 @@ export async function dispatchCloudBoundary({
|
|
|
157
157
|
}
|
|
158
158
|
try {
|
|
159
159
|
const descriptor = cloudSession.requireCloudSessionShellUpdate()
|
|
160
|
-
await cloudClient.
|
|
160
|
+
if (session.service === 'box') await cloudClient.closeCloudBox(session)
|
|
161
|
+
else await cloudClient.closeCloudSession(session)
|
|
161
162
|
cloudSession.publishCloudSessionToShell(null, descriptor)
|
|
162
|
-
console.log(
|
|
163
|
+
console.log(
|
|
164
|
+
session.service === 'box'
|
|
165
|
+
? ` closed: remote box ${session.boxId}`
|
|
166
|
+
: ` closed: remote simulator ${session.simId}`,
|
|
167
|
+
)
|
|
163
168
|
return { handled: true, code: 0 }
|
|
164
169
|
} catch (error) {
|
|
165
170
|
console.error(
|
|
@@ -169,6 +174,21 @@ export async function dispatchCloudBoundary({
|
|
|
169
174
|
}
|
|
170
175
|
}
|
|
171
176
|
|
|
177
|
+
// a box relays the whole bridge vocabulary to the page hosting its
|
|
178
|
+
// simulator, which runs the same shell the local CLI drives. so there is no
|
|
179
|
+
// subset to police: every command below reaches it through the ordinary
|
|
180
|
+
// bridge, which createBridgeFromParsed points at the box. claim is specific
|
|
181
|
+
// to nano.
|
|
182
|
+
if (session.service === 'box') {
|
|
183
|
+
if (command === 'claim') {
|
|
184
|
+
console.error(
|
|
185
|
+
` rnx ${command} is not available for a box; its simulator is held by the Box page. use rnx box stop to end the box.`,
|
|
186
|
+
)
|
|
187
|
+
return { handled: true, code: 1 }
|
|
188
|
+
}
|
|
189
|
+
return { handled: false }
|
|
190
|
+
}
|
|
191
|
+
|
|
172
192
|
if (command === 'claim') {
|
|
173
193
|
try {
|
|
174
194
|
const descriptor = cloudSession.requireCloudSessionShellUpdate()
|
package/cli/cloud-session.ts
CHANGED
|
@@ -10,9 +10,10 @@ export const RNX_SHELL_UPDATE_PREFIX = '__RNX_SHELL_UPDATE_V1__'
|
|
|
10
10
|
export const RNX_SHELL_UPDATE_SET_PREFIX = `${RNX_SHELL_UPDATE_PREFIX}set:`
|
|
11
11
|
export const RNX_SHELL_UPDATE_UNSET = `${RNX_SHELL_UPDATE_PREFIX}unset`
|
|
12
12
|
|
|
13
|
-
// the shell's remote target
|
|
14
|
-
//
|
|
15
|
-
|
|
13
|
+
// the shell's remote target. two services answer rnx commands and they are
|
|
14
|
+
// addressed differently, so the session names which one it is rather than
|
|
15
|
+
// leaving a reader to infer it from which fields happen to be filled in.
|
|
16
|
+
export type CloudSession = CloudSimSession | CloudBoxSession
|
|
16
17
|
|
|
17
18
|
export type RemotePlatform = 'ios' | 'android'
|
|
18
19
|
|
|
@@ -43,6 +44,23 @@ export interface CloudSimSession {
|
|
|
43
44
|
boxToken?: string
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
/**
|
|
48
|
+
* a micro box, whose simulator lives in its own box page rather than in the
|
|
49
|
+
* service. there is nothing to claim and no separate simulator token: the box
|
|
50
|
+
* token addresses the box, and the box relays each command to the page that
|
|
51
|
+
* is hosting it.
|
|
52
|
+
*/
|
|
53
|
+
export interface CloudBoxSession {
|
|
54
|
+
version: 3
|
|
55
|
+
service: 'box'
|
|
56
|
+
boxId: string
|
|
57
|
+
boxToken: string
|
|
58
|
+
apiOrigin: string
|
|
59
|
+
platform?: RemotePlatform
|
|
60
|
+
device?: string
|
|
61
|
+
artifact?: CloudSessionArtifactIdentity
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
47
65
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
48
66
|
}
|
|
@@ -106,8 +124,35 @@ export function createCloudSession(input: {
|
|
|
106
124
|
return session
|
|
107
125
|
}
|
|
108
126
|
|
|
127
|
+
export function createCloudBoxSession(input: {
|
|
128
|
+
boxId: string
|
|
129
|
+
boxToken: string
|
|
130
|
+
apiOrigin: string
|
|
131
|
+
platform?: RemotePlatform
|
|
132
|
+
device?: string
|
|
133
|
+
artifact?: CloudSessionArtifactIdentity
|
|
134
|
+
}): CloudBoxSession {
|
|
135
|
+
const session: CloudBoxSession = {
|
|
136
|
+
version: CLOUD_SESSION_VERSION,
|
|
137
|
+
service: 'box',
|
|
138
|
+
boxId: input.boxId.trim(),
|
|
139
|
+
boxToken: input.boxToken,
|
|
140
|
+
apiOrigin: normalizeCloudOrigin(input.apiOrigin.trim()),
|
|
141
|
+
...(input.platform ? { platform: input.platform } : {}),
|
|
142
|
+
...(input.device?.trim() ? { device: input.device.trim() } : {}),
|
|
143
|
+
...(input.artifact ? { artifact: input.artifact } : {}),
|
|
144
|
+
}
|
|
145
|
+
if (!session.boxId || !session.boxToken) {
|
|
146
|
+
throw new Error('cloud box session requires a box, an origin, and a box token')
|
|
147
|
+
}
|
|
148
|
+
return session
|
|
149
|
+
}
|
|
150
|
+
|
|
109
151
|
export function serializeCloudSession(session: CloudSession): string {
|
|
110
|
-
const validated =
|
|
152
|
+
const validated =
|
|
153
|
+
session.service === 'box'
|
|
154
|
+
? createCloudBoxSession(session)
|
|
155
|
+
: createCloudSession(session)
|
|
111
156
|
return Buffer.from(JSON.stringify(validated), 'utf8').toString('base64url')
|
|
112
157
|
}
|
|
113
158
|
|
|
@@ -154,6 +199,17 @@ export function parseCloudSession(encoded: string): CloudSession {
|
|
|
154
199
|
const boxId = typeof parsed.boxId === 'string' ? parsed.boxId : undefined
|
|
155
200
|
const boxToken = typeof parsed.boxToken === 'string' ? parsed.boxToken : undefined
|
|
156
201
|
try {
|
|
202
|
+
if (parsed.service === 'box') {
|
|
203
|
+
if (!boxId || !boxToken) throw new Error('unreadable cloud box session')
|
|
204
|
+
return createCloudBoxSession({
|
|
205
|
+
boxId,
|
|
206
|
+
boxToken,
|
|
207
|
+
apiOrigin,
|
|
208
|
+
platform,
|
|
209
|
+
device,
|
|
210
|
+
artifact,
|
|
211
|
+
})
|
|
212
|
+
}
|
|
157
213
|
const simId = parsed.simId
|
|
158
214
|
const claimId = parsed.claimId
|
|
159
215
|
const token = parsed.token
|