rnxsim 0.1.518 → 0.1.520
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 +419 -287
- package/cli/cloud-dispatch.ts +2 -21
- package/cli/cloud-session.ts +31 -85
- package/cli/commands/box.ts +15 -11
- package/cli/commands/inspect.ts +34 -1
- package/cli/commands/platform.ts +184 -232
- package/cli/main.ts +10 -2
- package/cli/outbound-endpoints.ts +21 -6
- package/detox/index.ts +9 -1
- 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 +5 -2
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +112 -39
- 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 +43 -117
- 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 +30 -1
- package/dist-lib/menu.mjs +27 -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 +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +41 -4
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +138 -164
- package/dist-lib/swift.cjs +674 -12
- package/dist-lib/vite.cjs +26 -3
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +72 -5
- package/src/cloud-contract.ts +7 -1
- package/src/cloud.ts +11 -4
- package/src/connect.ts +1 -1
- package/src/menu.ts +44 -0
- package/src/vite-plugin-one.ts +12 -0
- package/src/vite-plugin-swift.ts +17 -5
package/cli/cloud-client.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { dirname, extname, join, resolve } from 'node:path'
|
|
|
11
11
|
import {
|
|
12
12
|
embedMetroAssetFiles,
|
|
13
13
|
extractAssetDescriptors,
|
|
14
|
+
isEmbeddedAssetDescriptor,
|
|
14
15
|
metroAssetPlaceholderBytes,
|
|
15
16
|
metroAssetScaleFilename,
|
|
16
17
|
metroIosAssetDestRelatives,
|
|
@@ -27,44 +28,31 @@ import { inferMetroModuleIdentity } from '../../sootsim-engine/src/metro-fingerp
|
|
|
27
28
|
import { isLoopbackHost } from '../src/backend-origin.ts'
|
|
28
29
|
import {
|
|
29
30
|
isRnxCloudCommandType,
|
|
31
|
+
isRnxCloudSimStatus,
|
|
30
32
|
RNX_CLOUD_MAX_ARTIFACT_BYTES,
|
|
31
33
|
RNX_CLOUD_TOKEN_SCOPES,
|
|
32
34
|
rnxCloudCommandScope,
|
|
33
35
|
rnxCloudScopesAllowCommand,
|
|
34
36
|
type RnxCloudBoxCreateReceipt,
|
|
35
|
-
type
|
|
37
|
+
type RnxCloudClaim,
|
|
38
|
+
type RnxCloudSimCreateResponse,
|
|
39
|
+
type RnxCloudSimInstance,
|
|
40
|
+
type RnxCloudSimView,
|
|
36
41
|
} from '../src/cloud-contract.ts'
|
|
37
42
|
import {
|
|
38
43
|
RNX_METRO_MODULE_IDENTITY_VERSION,
|
|
39
44
|
type RNXMetroModuleIdentityMetadata,
|
|
40
45
|
} from '../src/metro-production-bundle.ts'
|
|
41
46
|
import { rnxPublicBrand } from '../src/public-brand.ts'
|
|
42
|
-
import { CHOOSE_ACCOUNT_HINT
|
|
47
|
+
import { CHOOSE_ACCOUNT_HINT } from './auth.ts'
|
|
43
48
|
import {
|
|
44
49
|
createCloudSession,
|
|
45
50
|
normalizeCloudOrigin,
|
|
46
51
|
readCloudSession,
|
|
47
|
-
type CloudBoxSession,
|
|
48
52
|
type CloudSession,
|
|
49
53
|
type CloudSimSession,
|
|
50
54
|
type RemotePlatform,
|
|
51
55
|
} from './cloud-session.ts'
|
|
52
|
-
|
|
53
|
-
export class UnsupportedRemoteHostError extends Error {
|
|
54
|
-
readonly code = 'unsupported_host' as const
|
|
55
|
-
readonly platform: string
|
|
56
|
-
readonly host: string
|
|
57
|
-
|
|
58
|
-
constructor(platform: string, host: string, message?: string) {
|
|
59
|
-
super(
|
|
60
|
-
message ??
|
|
61
|
-
`rnx remote ${platform} does not support --${host}: rnxsim/cloud only creates iOS simulators`,
|
|
62
|
-
)
|
|
63
|
-
this.name = 'UnsupportedRemoteHostError'
|
|
64
|
-
this.platform = platform
|
|
65
|
-
this.host = host
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
56
|
import { detectProject } from './commands/detect.ts'
|
|
69
57
|
import type { StorageSnapshot } from '../../sootsim-engine/src/preview/storage-snapshot-contract.ts'
|
|
70
58
|
import type {
|
|
@@ -129,7 +117,12 @@ function failureSentence(text: string): string | null {
|
|
|
129
117
|
}
|
|
130
118
|
if (!isRecord(parsed)) return null
|
|
131
119
|
if (isRecord(parsed.error) && typeof parsed.error.message === 'string') {
|
|
132
|
-
|
|
120
|
+
// the code is the contract a script matches on; the message is for people.
|
|
121
|
+
const code =
|
|
122
|
+
typeof parsed.error.code === 'string' && parsed.error.code
|
|
123
|
+
? ` [${parsed.error.code}]`
|
|
124
|
+
: ''
|
|
125
|
+
return `${parsed.error.message}${code}`
|
|
133
126
|
}
|
|
134
127
|
if (typeof parsed.message === 'string') return parsed.message
|
|
135
128
|
if (typeof parsed.error === 'string') return parsed.error
|
|
@@ -693,20 +686,68 @@ export async function producePreviewArtifact(
|
|
|
693
686
|
)
|
|
694
687
|
}
|
|
695
688
|
|
|
696
|
-
|
|
689
|
+
export interface CloudBoxArtifactProduction {
|
|
690
|
+
bytes: Buffer
|
|
691
|
+
assetsDirectory: string | null
|
|
692
|
+
warnings: string[]
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** prepares a prebuilt bundle for a Box by embedding any beside-the-bundle Metro assets. */
|
|
696
|
+
export async function produceCloudBoxArtifact(
|
|
697
|
+
bundlePath: string,
|
|
698
|
+
): Promise<CloudBoxArtifactProduction> {
|
|
699
|
+
const prepared = await embedBundleAssets(
|
|
700
|
+
bundlePath,
|
|
701
|
+
'rnx box create',
|
|
702
|
+
'allow-placeholders',
|
|
703
|
+
)
|
|
704
|
+
const warnings: string[] = []
|
|
705
|
+
if (prepared.search.unconfirmed && prepared.dest) {
|
|
706
|
+
warnings.push(
|
|
707
|
+
`rnx box create read this bundle's assets from ${prepared.dest.directory} on layout alone: every asset declares a scale that directory does not hold, so not one of Metro's hashes could be reproduced. Confirm these are the images this bundle was built against.`,
|
|
708
|
+
)
|
|
709
|
+
}
|
|
710
|
+
if (prepared.substituted.length > 0) {
|
|
711
|
+
warnings.push(
|
|
712
|
+
`rnx box create embedded a placeholder image for ${prepared.substituted.length} asset(s) no scale of which is in ${prepared.search.searched.join(', ') || 'any asset dest directory'}: ${prepared.substituted.join(', ')}. Rebuild with \`react-native bundle --assets-dest\` to ship the real images.`,
|
|
713
|
+
)
|
|
714
|
+
}
|
|
715
|
+
return {
|
|
716
|
+
bytes: Buffer.from(prepared.code, 'utf8'),
|
|
717
|
+
assetsDirectory: prepared.dest?.directory ?? null,
|
|
718
|
+
warnings,
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async function embedBundleAssets(
|
|
697
723
|
bundlePath: string,
|
|
698
|
-
maxBytes: number,
|
|
699
724
|
command: string,
|
|
700
725
|
assetPolicy: 'allow-placeholders' | 'require-assets',
|
|
701
|
-
): Promise<
|
|
726
|
+
): Promise<{
|
|
727
|
+
source: string
|
|
728
|
+
code: string
|
|
729
|
+
search: MetroAssetSearch
|
|
730
|
+
dest: MetroAssetDest | null
|
|
731
|
+
substituted: string[]
|
|
732
|
+
}> {
|
|
702
733
|
const source = await loadBundleSource(bundlePath)
|
|
703
|
-
const descriptors = extractAssetDescriptors(source)
|
|
734
|
+
const descriptors = extractAssetDescriptors(source).filter(
|
|
735
|
+
(descriptor) => !isEmbeddedAssetDescriptor(descriptor),
|
|
736
|
+
)
|
|
737
|
+
if (descriptors.length === 0) {
|
|
738
|
+
return {
|
|
739
|
+
source,
|
|
740
|
+
code: source,
|
|
741
|
+
search: { dest: null, searched: [], truncated: false, unconfirmed: false },
|
|
742
|
+
dest: null,
|
|
743
|
+
substituted: [],
|
|
744
|
+
}
|
|
745
|
+
}
|
|
704
746
|
// an https bundle has no local tree to search, and a bundle with no assets
|
|
705
747
|
// has nothing to search for.
|
|
706
|
-
const search: MetroAssetSearch =
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
: { dest: null, searched: [], truncated: false, unconfirmed: false }
|
|
748
|
+
const search: MetroAssetSearch = !isRemoteBundleInput(bundlePath)
|
|
749
|
+
? searchMetroAssetDest(bundlePath, descriptors)
|
|
750
|
+
: { dest: null, searched: [], truncated: false, unconfirmed: false }
|
|
710
751
|
const dest = search.dest
|
|
711
752
|
// `embedMetroAssetFiles` re-parses the bundle, so its descriptors are equal
|
|
712
753
|
// to the ones the search placed but not the same objects.
|
|
@@ -768,7 +809,23 @@ async function produceArtifact(
|
|
|
768
809
|
`${command} could not read ${join(dest?.directory ?? '.', classic)} for ${variant.descriptor.name}.${variant.descriptor.type} at scale ${variant.scale}, and has no stand-in for a .${variant.descriptor.type} asset. Rebuild with \`react-native bundle --assets-dest\`.`,
|
|
769
810
|
)
|
|
770
811
|
})
|
|
771
|
-
|
|
812
|
+
return {
|
|
813
|
+
source,
|
|
814
|
+
code: embedded.code,
|
|
815
|
+
search,
|
|
816
|
+
dest,
|
|
817
|
+
substituted,
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
async function produceArtifact(
|
|
822
|
+
bundlePath: string,
|
|
823
|
+
maxBytes: number,
|
|
824
|
+
command: string,
|
|
825
|
+
assetPolicy: 'allow-placeholders' | 'require-assets',
|
|
826
|
+
): Promise<CloudArtifactProduction> {
|
|
827
|
+
const prepared = await embedBundleAssets(bundlePath, command, assetPolicy)
|
|
828
|
+
const artifact = await buildRnxCloudArtifact(prepared.code, {
|
|
772
829
|
inferModuleIdentity: inferCloudModuleIdentity,
|
|
773
830
|
})
|
|
774
831
|
const bytes = Buffer.from(artifact.code, 'utf8')
|
|
@@ -779,14 +836,14 @@ async function produceArtifact(
|
|
|
779
836
|
)
|
|
780
837
|
}
|
|
781
838
|
const warnings: string[] = []
|
|
782
|
-
if (search.unconfirmed && dest) {
|
|
839
|
+
if (prepared.search.unconfirmed && prepared.dest) {
|
|
783
840
|
warnings.push(
|
|
784
|
-
`${command} read this bundle's assets from ${dest.directory} on layout alone: every asset declares a scale that directory does not hold, so not one of Metro's hashes could be reproduced. Confirm these are the images this bundle was built against.`,
|
|
841
|
+
`${command} read this bundle's assets from ${prepared.dest.directory} on layout alone: every asset declares a scale that directory does not hold, so not one of Metro's hashes could be reproduced. Confirm these are the images this bundle was built against.`,
|
|
785
842
|
)
|
|
786
843
|
}
|
|
787
|
-
if (substituted.length > 0) {
|
|
844
|
+
if (prepared.substituted.length > 0) {
|
|
788
845
|
warnings.push(
|
|
789
|
-
`${command} embedded a placeholder image for ${substituted.length} asset(s) no scale of which is in ${search.searched.join(', ') || 'any asset dest directory'}: ${substituted.join(', ')}. Rebuild with \`react-native bundle --assets-dest\` to ship the real images.`,
|
|
846
|
+
`${command} embedded a placeholder image for ${prepared.substituted.length} asset(s) no scale of which is in ${prepared.search.searched.join(', ') || 'any asset dest directory'}: ${prepared.substituted.join(', ')}. Rebuild with \`react-native bundle --assets-dest\` to ship the real images.`,
|
|
790
847
|
)
|
|
791
848
|
}
|
|
792
849
|
// an artifact this close to the ceiling is one asset away from being refused,
|
|
@@ -800,7 +857,7 @@ async function produceArtifact(
|
|
|
800
857
|
bytes,
|
|
801
858
|
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
802
859
|
receipt: artifact.receipt,
|
|
803
|
-
assetsDirectory: dest?.directory ?? null,
|
|
860
|
+
assetsDirectory: prepared.dest?.directory ?? null,
|
|
804
861
|
warnings,
|
|
805
862
|
}
|
|
806
863
|
}
|
|
@@ -864,8 +921,8 @@ function readCreateResponse(
|
|
|
864
921
|
}
|
|
865
922
|
}
|
|
866
923
|
|
|
867
|
-
//
|
|
868
|
-
//
|
|
924
|
+
// "send to box" wraps the running app in a nano box with its first simulator
|
|
925
|
+
// in one request, and the shell then targets that simulator
|
|
869
926
|
export async function createCloudBox(options: CreateCloudBoxOptions): Promise<{
|
|
870
927
|
receipt: RnxCloudBoxCreateReceipt
|
|
871
928
|
session: CloudSession
|
|
@@ -874,9 +931,6 @@ export async function createCloudBox(options: CreateCloudBoxOptions): Promise<{
|
|
|
874
931
|
warnings: string[]
|
|
875
932
|
}> {
|
|
876
933
|
const platform = options.platform ?? 'ios'
|
|
877
|
-
if (platform === 'android') {
|
|
878
|
-
throw new UnsupportedRemoteHostError('android', 'nano')
|
|
879
|
-
}
|
|
880
934
|
const apiOrigin = resolveCloudOrigin(options.apiOrigin)
|
|
881
935
|
const artifact = await produceCloudArtifact(options.bundlePath)
|
|
882
936
|
const boxName = options.boxName?.trim()
|
|
@@ -949,23 +1003,323 @@ export async function createCloudBox(options: CreateCloudBoxOptions): Promise<{
|
|
|
949
1003
|
}
|
|
950
1004
|
}
|
|
951
1005
|
|
|
1006
|
+
// one customer plane: the sim-service instance API. every `rnx remote`
|
|
1007
|
+
// simulator is created, listed, read, and deleted here; the box stays the
|
|
1008
|
+
// host behind it and is never the addressed object.
|
|
1009
|
+
|
|
1010
|
+
export interface CreateRemoteSimOptions {
|
|
1011
|
+
bundlePath: string
|
|
1012
|
+
device: string
|
|
1013
|
+
authorization: string
|
|
1014
|
+
/** the account to bill when the authorization is a session token; an api key names its own */
|
|
1015
|
+
accountId: string | null
|
|
1016
|
+
platform: RemotePlatform
|
|
1017
|
+
apiOrigin?: string
|
|
1018
|
+
/** match an existing simulator for the same artifact and labels instead of creating */
|
|
1019
|
+
reuse?: boolean
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
function readStringMap(value: unknown): Record<string, string> {
|
|
1023
|
+
if (!isRecord(value)) return {}
|
|
1024
|
+
const labels: Record<string, string> = {}
|
|
1025
|
+
for (const [key, label] of Object.entries(value)) {
|
|
1026
|
+
if (typeof label === 'string') labels[key] = label
|
|
1027
|
+
}
|
|
1028
|
+
return labels
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function readSimCreateResponse(
|
|
1032
|
+
value: unknown,
|
|
1033
|
+
expectedBytes: number,
|
|
1034
|
+
expectedSha256: string,
|
|
1035
|
+
): RnxCloudSimCreateResponse {
|
|
1036
|
+
if (!isRecord(value)) throw new Error('remote simulator create returned invalid JSON')
|
|
1037
|
+
const artifactId = `sha256:${expectedSha256}` as const
|
|
1038
|
+
const artifact = isRecord(value.artifact) ? value.artifact : null
|
|
1039
|
+
const claim = isRecord(value.claim) ? value.claim : null
|
|
1040
|
+
if (
|
|
1041
|
+
typeof value.simId !== 'string' ||
|
|
1042
|
+
!value.simId ||
|
|
1043
|
+
!artifact ||
|
|
1044
|
+
artifact.id !== artifactId ||
|
|
1045
|
+
artifact.bytes !== expectedBytes ||
|
|
1046
|
+
!claim ||
|
|
1047
|
+
typeof claim.id !== 'string' ||
|
|
1048
|
+
!claim.id ||
|
|
1049
|
+
typeof claim.expiresAt !== 'number' ||
|
|
1050
|
+
typeof value.token !== 'string' ||
|
|
1051
|
+
!value.token ||
|
|
1052
|
+
typeof value.status !== 'string' ||
|
|
1053
|
+
!isRnxCloudSimStatus(value.status) ||
|
|
1054
|
+
(value.streamUrl !== null && typeof value.streamUrl !== 'string')
|
|
1055
|
+
) {
|
|
1056
|
+
throw new Error('remote simulator create returned an invalid receipt')
|
|
1057
|
+
}
|
|
1058
|
+
return {
|
|
1059
|
+
simId: value.simId,
|
|
1060
|
+
artifact: { id: artifactId, bytes: expectedBytes },
|
|
1061
|
+
claim: { id: claim.id, expiresAt: claim.expiresAt },
|
|
1062
|
+
token: value.token,
|
|
1063
|
+
status: value.status,
|
|
1064
|
+
labels: readStringMap(value.labels),
|
|
1065
|
+
streamUrl: value.streamUrl,
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// `rnx remote ios|android <bundle>` creates one simulator through the
|
|
1070
|
+
// instance API and waits for its app to answer ready. both platforms run
|
|
1071
|
+
// this same typed path: an android create is refused by the service with a
|
|
1072
|
+
// typed 400, never by a cli-side branch.
|
|
1073
|
+
export async function createRemoteSim(options: CreateRemoteSimOptions): Promise<{
|
|
1074
|
+
receipt: RnxCloudSimCreateResponse
|
|
1075
|
+
session: CloudSession
|
|
1076
|
+
artifact: RnxCloudArtifactReceipt
|
|
1077
|
+
assetsDirectory: string | null
|
|
1078
|
+
warnings: string[]
|
|
1079
|
+
reused: boolean
|
|
1080
|
+
}> {
|
|
1081
|
+
const apiOrigin = resolveCloudOrigin(options.apiOrigin)
|
|
1082
|
+
const artifact = await produceCloudArtifact(options.bundlePath)
|
|
1083
|
+
const query = new URLSearchParams({
|
|
1084
|
+
labels: `rnx.remote=1,rnx.platform=${options.platform},rnx.bundle=${artifact.sha256.slice(0, 12)}`,
|
|
1085
|
+
wait: 'true',
|
|
1086
|
+
})
|
|
1087
|
+
if (options.reuse) query.set('reuseIfExists', 'true')
|
|
1088
|
+
const headers: Record<string, string> = {
|
|
1089
|
+
authorization: options.authorization,
|
|
1090
|
+
'content-type': 'application/javascript',
|
|
1091
|
+
'x-rnx-artifact-sha256': artifact.sha256,
|
|
1092
|
+
'x-rnx-platform': options.platform,
|
|
1093
|
+
'x-rnx-device': options.device,
|
|
1094
|
+
...(options.accountId ? { 'x-rnx-account-id': options.accountId } : null),
|
|
1095
|
+
}
|
|
1096
|
+
let response: Response
|
|
1097
|
+
try {
|
|
1098
|
+
response = await fetch(`${apiOrigin}/v1/sims?${query}`, {
|
|
1099
|
+
method: 'POST',
|
|
1100
|
+
headers,
|
|
1101
|
+
body: new Uint8Array(artifact.bytes),
|
|
1102
|
+
})
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
throw new Error(
|
|
1105
|
+
`${REMOTE_COMMAND} could not reach ${apiOrigin}: ${
|
|
1106
|
+
error instanceof Error ? error.message : String(error)
|
|
1107
|
+
}`,
|
|
1108
|
+
)
|
|
1109
|
+
}
|
|
1110
|
+
const responseText = await response.text()
|
|
1111
|
+
if (!response.ok) throw responseError(response.status, responseText)
|
|
1112
|
+
let parsed: unknown
|
|
1113
|
+
try {
|
|
1114
|
+
parsed = JSON.parse(responseText)
|
|
1115
|
+
} catch {
|
|
1116
|
+
throw new Error('remote simulator create returned unreadable JSON')
|
|
1117
|
+
}
|
|
1118
|
+
const receipt = readSimCreateResponse(parsed, artifact.bytes.length, artifact.sha256)
|
|
1119
|
+
const session = createCloudSession({
|
|
1120
|
+
simId: receipt.simId,
|
|
1121
|
+
apiOrigin,
|
|
1122
|
+
claimId: receipt.claim.id,
|
|
1123
|
+
token: receipt.token,
|
|
1124
|
+
streamUrl: receipt.streamUrl,
|
|
1125
|
+
platform: options.platform,
|
|
1126
|
+
device: options.device,
|
|
1127
|
+
artifact: {
|
|
1128
|
+
sha256: artifact.sha256,
|
|
1129
|
+
bytes: artifact.bytes.length,
|
|
1130
|
+
},
|
|
1131
|
+
})
|
|
1132
|
+
return {
|
|
1133
|
+
receipt,
|
|
1134
|
+
session,
|
|
1135
|
+
artifact: artifact.receipt,
|
|
1136
|
+
assetsDirectory: artifact.assetsDirectory,
|
|
1137
|
+
warnings: artifact.warnings,
|
|
1138
|
+
reused: response.status === 200,
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function readSimView(value: unknown): RnxCloudSimView {
|
|
1143
|
+
if (!isRecord(value) || !isRecord(value.labels)) {
|
|
1144
|
+
throw new Error('remote simulator list returned an invalid simulator')
|
|
1145
|
+
}
|
|
1146
|
+
const labels: Record<string, string> = {}
|
|
1147
|
+
for (const [key, label] of Object.entries(value.labels)) {
|
|
1148
|
+
if (typeof label !== 'string') {
|
|
1149
|
+
throw new Error('remote simulator list returned an invalid simulator')
|
|
1150
|
+
}
|
|
1151
|
+
labels[key] = label
|
|
1152
|
+
}
|
|
1153
|
+
const artifact = isRecord(value.artifact) ? value.artifact : null
|
|
1154
|
+
const claimValue = isRecord(value.claim) ? value.claim : null
|
|
1155
|
+
let claim: RnxCloudClaim | null = null
|
|
1156
|
+
if (claimValue !== null) {
|
|
1157
|
+
if (
|
|
1158
|
+
typeof claimValue.id !== 'string' ||
|
|
1159
|
+
!claimValue.id ||
|
|
1160
|
+
typeof claimValue.expiresAt !== 'number'
|
|
1161
|
+
) {
|
|
1162
|
+
throw new Error('remote simulator list returned an invalid simulator')
|
|
1163
|
+
}
|
|
1164
|
+
claim = { id: claimValue.id, expiresAt: claimValue.expiresAt }
|
|
1165
|
+
}
|
|
1166
|
+
const inactivityTimeoutMs = value.inactivityTimeoutMs
|
|
1167
|
+
const hardTimeoutAt = value.hardTimeoutAt
|
|
1168
|
+
if (
|
|
1169
|
+
typeof value.simId !== 'string' ||
|
|
1170
|
+
!value.simId ||
|
|
1171
|
+
typeof value.status !== 'string' ||
|
|
1172
|
+
!isRnxCloudSimStatus(value.status) ||
|
|
1173
|
+
!artifact ||
|
|
1174
|
+
typeof artifact.sha256 !== 'string' ||
|
|
1175
|
+
typeof artifact.bytes !== 'number' ||
|
|
1176
|
+
typeof value.createdAt !== 'number' ||
|
|
1177
|
+
typeof value.lastActiveAt !== 'number' ||
|
|
1178
|
+
(typeof inactivityTimeoutMs !== 'number' && inactivityTimeoutMs !== null) ||
|
|
1179
|
+
(typeof hardTimeoutAt !== 'number' && hardTimeoutAt !== null)
|
|
1180
|
+
) {
|
|
1181
|
+
throw new Error('remote simulator list returned an invalid simulator')
|
|
1182
|
+
}
|
|
1183
|
+
return {
|
|
1184
|
+
simId: value.simId,
|
|
1185
|
+
status: value.status,
|
|
1186
|
+
labels,
|
|
1187
|
+
artifact: { sha256: artifact.sha256, bytes: artifact.bytes },
|
|
1188
|
+
claim,
|
|
1189
|
+
createdAt: value.createdAt,
|
|
1190
|
+
lastActiveAt: value.lastActiveAt,
|
|
1191
|
+
inactivityTimeoutMs,
|
|
1192
|
+
hardTimeoutAt,
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// `rnx remote list`: the simulators this account runs that `rnx remote` made.
|
|
1197
|
+
export async function listRemoteSims(options: {
|
|
1198
|
+
apiOrigin?: string
|
|
1199
|
+
authorization: string
|
|
1200
|
+
accountId?: string | null
|
|
1201
|
+
}): Promise<RnxCloudSimView[]> {
|
|
1202
|
+
const apiOrigin = resolveCloudOrigin(options.apiOrigin)
|
|
1203
|
+
const selector = new URLSearchParams({ labelSelector: 'rnx.remote=1' })
|
|
1204
|
+
const headers: Record<string, string> = { authorization: options.authorization }
|
|
1205
|
+
if (options.accountId) headers['x-rnx-account-id'] = options.accountId
|
|
1206
|
+
let response: Response
|
|
1207
|
+
try {
|
|
1208
|
+
response = await fetch(`${apiOrigin}/v1/sims?${selector}`, { headers })
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
throw new Error(
|
|
1211
|
+
`${REMOTE_COMMAND} could not reach ${apiOrigin}: ${
|
|
1212
|
+
error instanceof Error ? error.message : String(error)
|
|
1213
|
+
}`,
|
|
1214
|
+
)
|
|
1215
|
+
}
|
|
1216
|
+
const responseText = await response.text()
|
|
1217
|
+
if (!response.ok) throw responseError(response.status, responseText)
|
|
1218
|
+
let parsed: unknown
|
|
1219
|
+
try {
|
|
1220
|
+
parsed = JSON.parse(responseText)
|
|
1221
|
+
} catch {
|
|
1222
|
+
throw new Error('remote simulator list returned unreadable JSON')
|
|
1223
|
+
}
|
|
1224
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.sims)) {
|
|
1225
|
+
throw new Error('remote simulator list returned invalid JSON')
|
|
1226
|
+
}
|
|
1227
|
+
return parsed.sims.map(readSimView)
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// one simulator reread by id: the reattach liveness probe, and the fresh
|
|
1231
|
+
// watch url an api-key caller mints with it. a simulator this account no
|
|
1232
|
+
// longer runs reads back as missing rather than forbidden, so 404 is null
|
|
1233
|
+
// and anything else refused throws.
|
|
1234
|
+
export async function getRemoteSim(options: {
|
|
1235
|
+
apiOrigin?: string
|
|
1236
|
+
simId: string
|
|
1237
|
+
authorization: string
|
|
1238
|
+
}): Promise<RnxCloudSimInstance | null> {
|
|
1239
|
+
const apiOrigin = resolveCloudOrigin(options.apiOrigin)
|
|
1240
|
+
let response: Response
|
|
1241
|
+
try {
|
|
1242
|
+
response = await fetch(`${apiOrigin}/v1/sims/${encodeURIComponent(options.simId)}`, {
|
|
1243
|
+
headers: { authorization: options.authorization },
|
|
1244
|
+
})
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
throw new Error(
|
|
1247
|
+
`${REMOTE_COMMAND} could not reach ${apiOrigin}: ${
|
|
1248
|
+
error instanceof Error ? error.message : String(error)
|
|
1249
|
+
}`,
|
|
1250
|
+
)
|
|
1251
|
+
}
|
|
1252
|
+
const responseText = await response.text()
|
|
1253
|
+
if (response.status === 404) return null
|
|
1254
|
+
if (!response.ok) throw responseError(response.status, responseText)
|
|
1255
|
+
let parsed: unknown
|
|
1256
|
+
try {
|
|
1257
|
+
parsed = JSON.parse(responseText)
|
|
1258
|
+
} catch {
|
|
1259
|
+
throw new Error('remote simulator read returned unreadable JSON')
|
|
1260
|
+
}
|
|
1261
|
+
if (!isRecord(parsed) || !isRecord(parsed.sim)) {
|
|
1262
|
+
throw new Error('remote simulator read returned invalid JSON')
|
|
1263
|
+
}
|
|
1264
|
+
const view = readSimView(parsed.sim)
|
|
1265
|
+
const streamUrl = parsed.sim.streamUrl
|
|
1266
|
+
if (streamUrl !== null && streamUrl !== undefined && typeof streamUrl !== 'string') {
|
|
1267
|
+
throw new Error('remote simulator read returned an invalid watch url')
|
|
1268
|
+
}
|
|
1269
|
+
return {
|
|
1270
|
+
...view,
|
|
1271
|
+
streamUrl: typeof streamUrl === 'string' ? streamUrl : null,
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// `rnx remote stop <id>`: delete one simulator by id. missing is null so the
|
|
1276
|
+
// caller can name what was not there; anything else refused throws.
|
|
1277
|
+
export async function deleteRemoteSim(options: {
|
|
1278
|
+
apiOrigin?: string
|
|
1279
|
+
simId: string
|
|
1280
|
+
authorization: string
|
|
1281
|
+
accountId?: string | null
|
|
1282
|
+
}): Promise<{ simId: string; status: string } | null> {
|
|
1283
|
+
const apiOrigin = resolveCloudOrigin(options.apiOrigin)
|
|
1284
|
+
const headers: Record<string, string> = { authorization: options.authorization }
|
|
1285
|
+
if (options.accountId) headers['x-rnx-account-id'] = options.accountId
|
|
1286
|
+
let response: Response
|
|
1287
|
+
try {
|
|
1288
|
+
response = await fetch(`${apiOrigin}/v1/sims/${encodeURIComponent(options.simId)}`, {
|
|
1289
|
+
method: 'DELETE',
|
|
1290
|
+
headers,
|
|
1291
|
+
})
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
throw new Error(
|
|
1294
|
+
`${REMOTE_COMMAND} could not reach ${apiOrigin}: ${
|
|
1295
|
+
error instanceof Error ? error.message : String(error)
|
|
1296
|
+
}`,
|
|
1297
|
+
)
|
|
1298
|
+
}
|
|
1299
|
+
const responseText = await response.text()
|
|
1300
|
+
if (response.status === 404) return null
|
|
1301
|
+
if (!response.ok) throw responseError(response.status, responseText)
|
|
1302
|
+
let parsed: unknown
|
|
1303
|
+
try {
|
|
1304
|
+
parsed = JSON.parse(responseText)
|
|
1305
|
+
} catch {
|
|
1306
|
+
throw new Error('remote simulator delete returned unreadable JSON')
|
|
1307
|
+
}
|
|
1308
|
+
if (
|
|
1309
|
+
!isRecord(parsed) ||
|
|
1310
|
+
typeof parsed.simId !== 'string' ||
|
|
1311
|
+
typeof parsed.status !== 'string'
|
|
1312
|
+
) {
|
|
1313
|
+
throw new Error('remote simulator delete returned invalid JSON')
|
|
1314
|
+
}
|
|
1315
|
+
return { simId: parsed.simId, status: parsed.status }
|
|
1316
|
+
}
|
|
1317
|
+
|
|
952
1318
|
export function resolveCloudSessionForParsed(
|
|
953
1319
|
parsed: Pick<ParsedBridgeCliArgs, 'simId' | 'simIdSource'>,
|
|
954
1320
|
): CloudSession | null {
|
|
955
1321
|
const session = readCloudSession()
|
|
956
1322
|
if (!session) return null
|
|
957
|
-
// a box holds one simulator and names it by the box, so --sim names a box
|
|
958
|
-
// rather than a sim there and this shell already knows which box it is on.
|
|
959
|
-
if (session.service === 'box') {
|
|
960
|
-
if (parsed.simIdSource === 'flag' && parsed.simId !== session.boxId) {
|
|
961
|
-
throw new Error(
|
|
962
|
-
`this shell is connected to box ${session.boxId}; ${
|
|
963
|
-
parsed.simId ? `--sim ${parsed.simId}` : 'the requested simulator'
|
|
964
|
-
} is not available in this shell`,
|
|
965
|
-
)
|
|
966
|
-
}
|
|
967
|
-
return session
|
|
968
|
-
}
|
|
969
1323
|
if (parsed.simIdSource === 'flag' && parsed.simId !== session.simId) {
|
|
970
1324
|
throw new Error(
|
|
971
1325
|
`this shell is connected to remote simulator ${session.simId}; ${
|
|
@@ -1082,107 +1436,12 @@ class CloudBridge implements WsBridge {
|
|
|
1082
1436
|
close(): void {}
|
|
1083
1437
|
}
|
|
1084
1438
|
|
|
1085
|
-
/**
|
|
1086
|
-
* a micro box's bridge.
|
|
1087
|
-
*
|
|
1088
|
-
* the box holds no simulator of its own: the one it shows lives in its Box
|
|
1089
|
-
* page, which runs the same shell the local CLI drives over the daemon bridge.
|
|
1090
|
-
* so this transport moves the whole bridge vocabulary, admitted by caller
|
|
1091
|
-
* scope exactly like the nano plane, and the box service is only the relay in
|
|
1092
|
-
* the middle. that is also why there is no claim: the box token addresses the
|
|
1093
|
-
* box, and the page answers for it.
|
|
1094
|
-
*/
|
|
1095
|
-
class CloudBoxBridge implements WsBridge {
|
|
1096
|
-
readonly plane = 'cloud' as const
|
|
1097
|
-
|
|
1098
|
-
constructor(private readonly session: CloudBoxSession) {}
|
|
1099
|
-
|
|
1100
|
-
async send(
|
|
1101
|
-
command: { type: string; [key: string]: unknown },
|
|
1102
|
-
options?: { timeoutMs?: number },
|
|
1103
|
-
): Promise<unknown> {
|
|
1104
|
-
const id = randomUUID()
|
|
1105
|
-
// the wire id is this request's, so the command carries none. `simId` names
|
|
1106
|
-
// a local sim among several and a box has exactly one.
|
|
1107
|
-
const payload: Record<string, unknown> = {}
|
|
1108
|
-
for (const [key, value] of Object.entries(command)) {
|
|
1109
|
-
if (key !== 'id' && key !== 'simId') payload[key] = value
|
|
1110
|
-
}
|
|
1111
|
-
let response: Response
|
|
1112
|
-
try {
|
|
1113
|
-
response = await fetch(
|
|
1114
|
-
`${this.session.apiOrigin}/v1/boxes/${encodeURIComponent(this.session.boxId)}/commands`,
|
|
1115
|
-
{
|
|
1116
|
-
method: 'POST',
|
|
1117
|
-
headers: {
|
|
1118
|
-
authorization: `Bearer ${this.session.boxToken}`,
|
|
1119
|
-
'content-type': 'application/json',
|
|
1120
|
-
},
|
|
1121
|
-
body: JSON.stringify({ id, command: payload }),
|
|
1122
|
-
...(options?.timeoutMs
|
|
1123
|
-
? { signal: AbortSignal.timeout(options.timeoutMs) }
|
|
1124
|
-
: {}),
|
|
1125
|
-
},
|
|
1126
|
-
)
|
|
1127
|
-
} catch (error) {
|
|
1128
|
-
throw new Error(
|
|
1129
|
-
`could not reach box ${this.session.boxId} at ${this.session.apiOrigin}: ${
|
|
1130
|
-
error instanceof Error ? error.message : String(error)
|
|
1131
|
-
}`,
|
|
1132
|
-
)
|
|
1133
|
-
}
|
|
1134
|
-
const text = await response.text()
|
|
1135
|
-
if (!response.ok) throw responseError(response.status, text)
|
|
1136
|
-
let parsed: unknown
|
|
1137
|
-
try {
|
|
1138
|
-
parsed = JSON.parse(text)
|
|
1139
|
-
} catch {
|
|
1140
|
-
throw new Error('box command returned unreadable JSON')
|
|
1141
|
-
}
|
|
1142
|
-
if (!isRecord(parsed) || parsed.id !== id) {
|
|
1143
|
-
throw new Error('box command returned an invalid command receipt')
|
|
1144
|
-
}
|
|
1145
|
-
if (typeof parsed.error === 'string' && parsed.error) throw new Error(parsed.error)
|
|
1146
|
-
if (!Object.hasOwn(parsed, 'result')) {
|
|
1147
|
-
throw new Error('box command receipt has no result')
|
|
1148
|
-
}
|
|
1149
|
-
return parsed.result
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
listSims(): Promise<BridgeSimInfo[]> {
|
|
1153
|
-
return Promise.reject(new Error('a box does not expose a local simulator list'))
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
focusSim(): Promise<never> {
|
|
1157
|
-
return Promise.reject(new Error('a box simulator has no browser window to focus'))
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
resolveReloadedSim(): Promise<string | null> {
|
|
1161
|
-
// cloud sims carry no local browser host, so there is no lineage to follow
|
|
1162
|
-
return Promise.resolve(null)
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
closeSim(): Promise<never> {
|
|
1166
|
-
return Promise.reject(new Error('close a box simulator with rnx box stop'))
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
claim(): Promise<BridgeClaimResult> {
|
|
1170
|
-
return Promise.reject(
|
|
1171
|
-
new Error('a box simulator is held by its Box page and has nothing to claim'),
|
|
1172
|
-
)
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
close(): void {}
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
1439
|
export function createCloudBridgeForParsed(
|
|
1179
1440
|
parsed: Pick<ParsedBridgeCliArgs, 'simId' | 'simIdSource'>,
|
|
1180
1441
|
): WsBridge | null {
|
|
1181
1442
|
const session = resolveCloudSessionForParsed(parsed)
|
|
1182
1443
|
if (!session) return null
|
|
1183
|
-
return session
|
|
1184
|
-
? new CloudBoxBridge(session)
|
|
1185
|
-
: new CloudBridge(session)
|
|
1444
|
+
return new CloudBridge(session)
|
|
1186
1445
|
}
|
|
1187
1446
|
|
|
1188
1447
|
function readClaimResponse(value: unknown): { id: string; expiresAt: number } {
|
|
@@ -1240,32 +1499,17 @@ export async function confirmCloudSimulator(session: CloudSimSession): Promise<v
|
|
|
1240
1499
|
}
|
|
1241
1500
|
}
|
|
1242
1501
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
try {
|
|
1255
|
-
response = await fetch(
|
|
1256
|
-
`${session.apiOrigin}/v1/boxes/${encodeURIComponent(session.boxId)}/stop`,
|
|
1257
|
-
{
|
|
1258
|
-
method: 'POST',
|
|
1259
|
-
headers: { authorization: authHeader },
|
|
1260
|
-
},
|
|
1261
|
-
)
|
|
1262
|
-
} catch (error) {
|
|
1263
|
-
throw new Error(
|
|
1264
|
-
`could not reach box ${session.boxId} at ${session.apiOrigin}: ${
|
|
1265
|
-
error instanceof Error ? error.message : String(error)
|
|
1266
|
-
}`,
|
|
1267
|
-
)
|
|
1268
|
-
}
|
|
1502
|
+
export async function closeCloudSession(session: CloudSession): Promise<void> {
|
|
1503
|
+
const boxId = session.boxId
|
|
1504
|
+
const boxToken = session.boxToken
|
|
1505
|
+
if (boxId && boxToken) {
|
|
1506
|
+
const response = await fetch(
|
|
1507
|
+
`${session.apiOrigin}/v1/boxes/${encodeURIComponent(boxId)}`,
|
|
1508
|
+
{
|
|
1509
|
+
method: 'DELETE',
|
|
1510
|
+
headers: { authorization: `Bearer ${boxToken}` },
|
|
1511
|
+
},
|
|
1512
|
+
)
|
|
1269
1513
|
const text = await response.text()
|
|
1270
1514
|
if (!response.ok && response.status !== 404) {
|
|
1271
1515
|
throw responseError(response.status, text)
|
|
@@ -1273,10 +1517,10 @@ export async function closeCloudBox(session: CloudSession): Promise<void> {
|
|
|
1273
1517
|
return
|
|
1274
1518
|
}
|
|
1275
1519
|
const response = await fetch(
|
|
1276
|
-
`${session.apiOrigin}/v1/
|
|
1520
|
+
`${session.apiOrigin}/v1/sims/${encodeURIComponent(session.simId)}`,
|
|
1277
1521
|
{
|
|
1278
1522
|
method: 'DELETE',
|
|
1279
|
-
headers: { authorization: `Bearer ${session.
|
|
1523
|
+
headers: { authorization: `Bearer ${session.token}` },
|
|
1280
1524
|
},
|
|
1281
1525
|
)
|
|
1282
1526
|
const text = await response.text()
|
|
@@ -1284,115 +1528,3 @@ export async function closeCloudBox(session: CloudSession): Promise<void> {
|
|
|
1284
1528
|
throw responseError(response.status, text)
|
|
1285
1529
|
}
|
|
1286
1530
|
}
|
|
1287
|
-
|
|
1288
|
-
export interface AddCloudSimulatorOptions {
|
|
1289
|
-
device: string
|
|
1290
|
-
authorization: string
|
|
1291
|
-
accountId: string | null
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
export async function addCloudSimulator(
|
|
1295
|
-
session: CloudSession,
|
|
1296
|
-
options: AddCloudSimulatorOptions,
|
|
1297
|
-
): Promise<{
|
|
1298
|
-
receipt: RnxCloudCreateReceipt
|
|
1299
|
-
session: CloudSession
|
|
1300
|
-
}> {
|
|
1301
|
-
let response: Response
|
|
1302
|
-
try {
|
|
1303
|
-
response = await fetch(
|
|
1304
|
-
`${session.apiOrigin}/v1/boxes/${encodeURIComponent(session.boxId)}/simulators`,
|
|
1305
|
-
{
|
|
1306
|
-
method: 'POST',
|
|
1307
|
-
headers: {
|
|
1308
|
-
authorization: `Bearer ${session.boxToken}`,
|
|
1309
|
-
'x-rnx-account-authorization': options.authorization,
|
|
1310
|
-
'content-type': 'application/json',
|
|
1311
|
-
...(options.accountId ? { 'x-rnx-account-id': options.accountId } : {}),
|
|
1312
|
-
},
|
|
1313
|
-
body: JSON.stringify({ device: options.device }),
|
|
1314
|
-
},
|
|
1315
|
-
)
|
|
1316
|
-
} catch (error) {
|
|
1317
|
-
throw new Error(
|
|
1318
|
-
`could not reach the remote simulator at ${session.apiOrigin}: ${
|
|
1319
|
-
error instanceof Error ? error.message : String(error)
|
|
1320
|
-
}`,
|
|
1321
|
-
)
|
|
1322
|
-
}
|
|
1323
|
-
const responseText = await response.text()
|
|
1324
|
-
if (!response.ok) {
|
|
1325
|
-
if (response.status === 404) {
|
|
1326
|
-
try {
|
|
1327
|
-
const parsed: unknown = JSON.parse(responseText)
|
|
1328
|
-
if (
|
|
1329
|
-
isRecord(parsed) &&
|
|
1330
|
-
(parsed.error === 'unknown_artifact' ||
|
|
1331
|
-
(isRecord(parsed.error) && parsed.error.code === 'unknown_artifact'))
|
|
1332
|
-
) {
|
|
1333
|
-
throw new Error(
|
|
1334
|
-
'this remote simulator does not hold a new bundle; run `rnx close` and create again',
|
|
1335
|
-
)
|
|
1336
|
-
}
|
|
1337
|
-
} catch (err) {
|
|
1338
|
-
if (
|
|
1339
|
-
err instanceof Error &&
|
|
1340
|
-
err.message.startsWith('this remote simulator does not hold')
|
|
1341
|
-
) {
|
|
1342
|
-
throw err
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
throw new Error(
|
|
1346
|
-
'this remote simulator does not hold a new bundle; run `rnx close` and create again',
|
|
1347
|
-
)
|
|
1348
|
-
}
|
|
1349
|
-
throw responseError(response.status, responseText)
|
|
1350
|
-
}
|
|
1351
|
-
let parsed: unknown
|
|
1352
|
-
try {
|
|
1353
|
-
parsed = JSON.parse(responseText)
|
|
1354
|
-
} catch {
|
|
1355
|
-
throw new Error('remote simulator create returned unreadable JSON')
|
|
1356
|
-
}
|
|
1357
|
-
if (!isRecord(parsed) || !isRecord(parsed.simulator)) {
|
|
1358
|
-
throw new Error('remote simulator create returned an invalid receipt')
|
|
1359
|
-
}
|
|
1360
|
-
const simulator = parsed.simulator
|
|
1361
|
-
const simId = simulator.simId
|
|
1362
|
-
const claim = isRecord(simulator.claim) ? simulator.claim : null
|
|
1363
|
-
const token = simulator.token
|
|
1364
|
-
const artifact = isRecord(simulator.artifact) ? simulator.artifact : null
|
|
1365
|
-
if (
|
|
1366
|
-
typeof simId !== 'string' ||
|
|
1367
|
-
!simId ||
|
|
1368
|
-
!claim ||
|
|
1369
|
-
typeof claim.id !== 'string' ||
|
|
1370
|
-
!claim.id ||
|
|
1371
|
-
typeof claim.expiresAt !== 'number' ||
|
|
1372
|
-
typeof token !== 'string' ||
|
|
1373
|
-
!token ||
|
|
1374
|
-
!artifact ||
|
|
1375
|
-
typeof artifact.id !== 'string' ||
|
|
1376
|
-
typeof artifact.bytes !== 'number'
|
|
1377
|
-
) {
|
|
1378
|
-
throw new Error('remote simulator create returned an invalid receipt')
|
|
1379
|
-
}
|
|
1380
|
-
const receipt: RnxCloudCreateReceipt = {
|
|
1381
|
-
simId,
|
|
1382
|
-
artifact: { id: artifact.id as `sha256:${string}`, bytes: artifact.bytes },
|
|
1383
|
-
claim: { id: claim.id, expiresAt: claim.expiresAt },
|
|
1384
|
-
}
|
|
1385
|
-
const updatedSession = createCloudSession({
|
|
1386
|
-
...session,
|
|
1387
|
-
simId,
|
|
1388
|
-
claimId: claim.id,
|
|
1389
|
-
token,
|
|
1390
|
-
platform: 'ios',
|
|
1391
|
-
device: options.device,
|
|
1392
|
-
artifact: {
|
|
1393
|
-
sha256: artifact.id.replace(/^sha256:/, ''),
|
|
1394
|
-
bytes: artifact.bytes,
|
|
1395
|
-
},
|
|
1396
|
-
})
|
|
1397
|
-
return { receipt, session: updatedSession }
|
|
1398
|
-
}
|