@run-cloud/sdk 0.27.0 → 0.29.0

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/dist/index.js CHANGED
@@ -665,9 +665,11 @@ async function parseApiError(response) {
665
665
  ? normalizedScreenshotErrorCode(rawCode)
666
666
  : action === "accessibility"
667
667
  ? normalizedAccessibilityErrorCode(rawCode)
668
- : action !== "open-url" && (action !== undefined || interactionStatus !== undefined)
669
- ? normalizedInteractionErrorCode(rawCode)
670
- : optionalString(rawCode);
668
+ : action === "camera-video" || action === "microphone-audio"
669
+ ? optionalString(rawCode)
670
+ : action !== "open-url" && (action !== undefined || interactionStatus !== undefined)
671
+ ? normalizedInteractionErrorCode(rawCode)
672
+ : optionalString(rawCode);
671
673
  return {
672
674
  detail,
673
675
  code,
@@ -885,6 +887,25 @@ function validateXcodeBuildOptions(options) {
885
887
  throw new Error("Xcode build accepts either workspace or project, not both");
886
888
  if (options.xcodegen && options.reactNative)
887
889
  throw new Error("XcodeGen options cannot be combined with Expo or React Native options");
890
+ if (options.reuse) {
891
+ if (!/^xcb_[A-Za-z0-9]+$/.test(options.reuse.basisBuildId)) {
892
+ throw new Error("reuse.basisBuildId must be a run.cloud Xcode build ID");
893
+ }
894
+ if (!options.reactNative)
895
+ throw new Error("reuse requires reactNative options");
896
+ if (options.workspace || options.project || options.scheme || options.xcodegen) {
897
+ throw new Error("reuse cannot select a workspace, project, scheme, or XcodeGen spec");
898
+ }
899
+ if (options.sdk && options.sdk !== "iphonesimulator") {
900
+ throw new Error("reuse requires sdk iphonesimulator");
901
+ }
902
+ if (options.configuration && options.configuration !== "Release") {
903
+ throw new Error("reuse requires configuration Release");
904
+ }
905
+ if (options.signing || options.cloudSigning || options.appStore || options.appstore) {
906
+ throw new Error("reuse cannot be combined with signing or App Store upload");
907
+ }
908
+ }
888
909
  const devServerUrl = options.reactNative?.devServerUrl ?? options.reactNative?.devServerURL;
889
910
  if (devServerUrl && options.configuration === "Release")
890
911
  throw new Error("reactNative.devServerUrl is only supported for Debug builds");
@@ -919,6 +940,8 @@ export class Client {
919
940
  images;
920
941
  remotion;
921
942
  xcode;
943
+ capabilities;
944
+ mobile;
922
945
  apiKey;
923
946
  apiUrl;
924
947
  fetchImpl;
@@ -946,6 +969,8 @@ export class Client {
946
969
  this.images = new ImageClient(this);
947
970
  this.remotion = new RemotionClient(this.sandboxes, this.snapshots);
948
971
  this.xcode = new XcodeBuildsClient(this);
972
+ this.capabilities = new CapabilitiesClient(this);
973
+ this.mobile = new MobileClient(this);
949
974
  }
950
975
  async usage(options = {}) {
951
976
  const query = options.orgId ? `?orgId=${encodeURIComponent(options.orgId)}` : "";
@@ -1283,6 +1308,16 @@ export class XcodeBuildsClient {
1283
1308
  const idempotencyKey = options.idempotencyKey ?? `xcode_${randomUUID()}`;
1284
1309
  return await retryXcodeOperation(async () => await this.client.uploadForm("/run-cloud/xcode/builds", form, { "Idempotency-Key": idempotencyKey }));
1285
1310
  }
1311
+ /** Reuse a compatible simulator native shell and replace only its embedded JavaScript bundle. */
1312
+ async rebundle(sourcePath, basisBuildId, options = {}) {
1313
+ return await this.build(sourcePath, {
1314
+ ...options,
1315
+ sdk: "iphonesimulator",
1316
+ configuration: "Release",
1317
+ reactNative: options.reactNative ?? {},
1318
+ reuse: { basisBuildId },
1319
+ });
1320
+ }
1286
1321
  async get(id) {
1287
1322
  return await retryXcodeOperation(async () => await this.client.request("GET", `/run-cloud/xcode/builds/${encodeURIComponent(id)}`));
1288
1323
  }
@@ -1295,8 +1330,21 @@ export class XcodeBuildsClient {
1295
1330
  const result = await retryXcodeOperation(async () => await this.client.request("GET", `/run-cloud/xcode/builds${query.size ? `?${query}` : ""}`));
1296
1331
  return result.items;
1297
1332
  }
1298
- async logs(id, offset = 0) {
1299
- return await retryXcodeOperation(async () => await this.client.request("GET", `/run-cloud/xcode/builds/${encodeURIComponent(id)}/logs?offset=${offset}`));
1333
+ /** Read an ordered, replayable event page. Persist nextCursor after processing the page. */
1334
+ async events(options = {}) {
1335
+ const query = new URLSearchParams();
1336
+ if (options.orgId)
1337
+ query.set("orgId", options.orgId);
1338
+ if (options.after !== undefined)
1339
+ query.set("after", String(options.after));
1340
+ if (options.buildId)
1341
+ query.set("buildId", options.buildId);
1342
+ if (options.limit !== undefined)
1343
+ query.set("limit", String(options.limit));
1344
+ return await retryXcodeOperation(async () => await this.client.request("GET", `/run-cloud/xcode/events${query.size ? `?${query}` : ""}`));
1345
+ }
1346
+ async logs(id, offset = 0, source) {
1347
+ return await retryXcodeOperation(async () => await this.client.request("GET", `/run-cloud/xcode/builds/${encodeURIComponent(id)}/logs?offset=${offset}${source ? `&source=${source}` : ""}`));
1300
1348
  }
1301
1349
  /** Poll state and emit sanitized log chunks until the build is terminal. */
1302
1350
  async wait(id, options = {}) {
@@ -1304,15 +1352,17 @@ export class XcodeBuildsClient {
1304
1352
  const deadline = Date.now() + timeoutSeconds * 1_000;
1305
1353
  const interval = options.pollIntervalMs ?? 1_000;
1306
1354
  let offset = 0;
1355
+ let source;
1307
1356
  for (;;) {
1308
1357
  if (options.signal?.aborted)
1309
1358
  throw new DOMException("The operation was aborted", "AbortError");
1310
1359
  let build = await this.get(id);
1311
1360
  if (build.logs.available || build.status === "running" || build.status === "queued") {
1312
- const chunk = await this.logs(id, offset);
1361
+ const chunk = await this.logs(id, offset, source);
1313
1362
  if (chunk.text)
1314
1363
  options.onLog?.(chunk.text);
1315
1364
  offset = chunk.nextOffset;
1365
+ source = chunk.source ?? source;
1316
1366
  if (["succeeded", "failed", "cancelled"].includes(build.status) && chunk.complete)
1317
1367
  return build;
1318
1368
  }
@@ -1362,8 +1412,12 @@ export class SimulatorClient {
1362
1412
  return await this.client.request("PATCH", `/run-cloud/${this.platform}/${encodeURIComponent(id)}/tags`, { tags });
1363
1413
  }
1364
1414
  async list(options = {}) {
1365
- const query = options.all ? "?all=1" : "";
1366
- return await this.client.request("GET", `/run-cloud/${this.platform}${query}`);
1415
+ const query = new URLSearchParams();
1416
+ if (options.all)
1417
+ query.set("all", "1");
1418
+ if (options.orgId)
1419
+ query.set("orgId", options.orgId);
1420
+ return await this.client.request("GET", `/run-cloud/${this.platform}${query.size ? `?${query}` : ""}`);
1367
1421
  }
1368
1422
  async get(id) {
1369
1423
  return await this.client.request("GET", `/run-cloud/${this.platform}/${encodeURIComponent(id)}`);
@@ -1405,6 +1459,141 @@ export class SimulatorClient {
1405
1459
  });
1406
1460
  return await this.installAsset(id, asset.id);
1407
1461
  }
1462
+ /** Upload a deterministic video and present it to the app as live camera input. */
1463
+ async uploadCameraVideo(id, video, options = {}) {
1464
+ const contentType = video.type.toLowerCase();
1465
+ if (contentType !== "video/mp4" && contentType !== "video/quicktime") {
1466
+ throw new Error("camera video must have content type video/mp4 or video/quicktime");
1467
+ }
1468
+ const bundleId = options.bundleId?.trim();
1469
+ if (bundleId && bundleId.length > 256) {
1470
+ throw new Error("bundleId must be at most 256 characters");
1471
+ }
1472
+ const inferredName = video.name;
1473
+ const filename = options.filename
1474
+ ?? (typeof inferredName === "string" && inferredName ? inferredName : undefined)
1475
+ ?? (contentType === "video/quicktime" ? "camera.mov" : "camera.mp4");
1476
+ const asset = await this.client.assets.upload(video, {
1477
+ filename,
1478
+ name: options.name,
1479
+ });
1480
+ try {
1481
+ const result = await this.client.request("POST", `/run-cloud/${this.platform}/${encodeURIComponent(id)}/camera-video`, {
1482
+ assetId: asset.id,
1483
+ ...(bundleId ? { bundleId } : {}),
1484
+ }, undefined, {
1485
+ errorContext: { sessionId: id, platform: this.platform, action: "camera-video" },
1486
+ transportErrorCode: "camera_video_transport_error",
1487
+ transportErrorMessage: "camera video injection could not reach the run.cloud API",
1488
+ });
1489
+ const response = result && typeof result === "object" && !Array.isArray(result)
1490
+ ? result
1491
+ : null;
1492
+ if (!response
1493
+ || response.ok !== true
1494
+ || response.platform !== this.platform
1495
+ || response.sessionId !== id
1496
+ || (response.bundleId !== null && typeof response.bundleId !== "string")
1497
+ || response.filename !== filename
1498
+ || response.contentType !== contentType
1499
+ || response.byteSize !== video.size
1500
+ || response.source !== "video"
1501
+ || (response.pid !== null && typeof response.pid !== "number")
1502
+ || !isAsset(response.asset)
1503
+ || response.asset.id !== asset.id) {
1504
+ throw new RunCloudError(0, "camera video injection returned an invalid acknowledgement", {
1505
+ code: "camera_video_invalid_response",
1506
+ retryable: true,
1507
+ sessionId: id,
1508
+ platform: this.platform,
1509
+ action: "camera-video",
1510
+ });
1511
+ }
1512
+ return result;
1513
+ }
1514
+ catch (error) {
1515
+ await this.client.assets.delete(asset.id).catch(() => undefined);
1516
+ throw error;
1517
+ }
1518
+ }
1519
+ /** Upload audio and deliver its decoded samples to the app microphone. */
1520
+ async uploadMicrophoneAudio(id, audio, options = {}) {
1521
+ const contentType = audio.type.toLowerCase();
1522
+ const supported = new Set([
1523
+ "audio/aac",
1524
+ "audio/m4a",
1525
+ "audio/mp4",
1526
+ "audio/mpeg",
1527
+ "audio/wav",
1528
+ "audio/x-m4a",
1529
+ "audio/x-wav",
1530
+ ]);
1531
+ if (!supported.has(contentType)) {
1532
+ throw new Error("audio must be AAC, M4A, MP3, MP4 audio, or WAV");
1533
+ }
1534
+ const bundleId = options.bundleId?.trim();
1535
+ if (bundleId && bundleId.length > 256) {
1536
+ throw new Error("bundleId must be at most 256 characters");
1537
+ }
1538
+ const inferredName = audio.name;
1539
+ const extension = contentType.includes("wav")
1540
+ ? "wav"
1541
+ : contentType === "audio/mpeg"
1542
+ ? "mp3"
1543
+ : contentType === "audio/aac"
1544
+ ? "aac"
1545
+ : "m4a";
1546
+ const filename = options.filename
1547
+ ?? (typeof inferredName === "string" && inferredName ? inferredName : undefined)
1548
+ ?? `microphone.${extension}`;
1549
+ const asset = await this.client.assets.upload(audio, {
1550
+ filename,
1551
+ name: options.name,
1552
+ });
1553
+ try {
1554
+ const result = await this.client.request("POST", `/run-cloud/${this.platform}/${encodeURIComponent(id)}/microphone-audio`, {
1555
+ assetId: asset.id,
1556
+ ...(bundleId ? { bundleId } : {}),
1557
+ }, undefined, {
1558
+ errorContext: { sessionId: id, platform: this.platform, action: "microphone-audio" },
1559
+ transportErrorCode: "microphone_audio_transport_error",
1560
+ transportErrorMessage: "microphone audio injection could not reach the run.cloud API",
1561
+ });
1562
+ const response = result && typeof result === "object" && !Array.isArray(result)
1563
+ ? result
1564
+ : null;
1565
+ const optionalFinite = (value) => value === undefined
1566
+ || (typeof value === "number" && Number.isFinite(value) && value >= 0);
1567
+ if (!response
1568
+ || response.ok !== true
1569
+ || response.platform !== this.platform
1570
+ || response.sessionId !== id
1571
+ || typeof response.bundleId !== "string"
1572
+ || response.filename !== filename
1573
+ || response.contentType !== contentType
1574
+ || response.byteSize !== audio.size
1575
+ || (response.pid !== null && typeof response.pid !== "number")
1576
+ || !optionalFinite(response.sampleRate)
1577
+ || !optionalFinite(response.channels)
1578
+ || !optionalFinite(response.sampleCount)
1579
+ || !optionalFinite(response.durationMs)
1580
+ || !isAsset(response.asset)
1581
+ || response.asset.id !== asset.id) {
1582
+ throw new RunCloudError(0, "microphone audio injection returned an invalid acknowledgement", {
1583
+ code: "microphone_audio_invalid_response",
1584
+ retryable: true,
1585
+ sessionId: id,
1586
+ platform: this.platform,
1587
+ action: "microphone-audio",
1588
+ });
1589
+ }
1590
+ return result;
1591
+ }
1592
+ catch (error) {
1593
+ await this.client.assets.delete(asset.id).catch(() => undefined);
1594
+ throw error;
1595
+ }
1596
+ }
1408
1597
  async keepAlive(id) {
1409
1598
  const result = await this.client.request("POST", `/run-cloud/${this.platform}/${encodeURIComponent(id)}/activity`, {});
1410
1599
  if (result.ok !== true
@@ -1725,40 +1914,6 @@ export class IosSimulatorClient extends SimulatorClient {
1725
1914
  });
1726
1915
  return await this.client.request("POST", `/run-cloud/ios/${encodeURIComponent(id)}/videos`, { assetId: asset.id });
1727
1916
  }
1728
- async uploadMicrophoneAudio(id, audio, options = {}) {
1729
- const contentType = audio.type.toLowerCase();
1730
- const supported = new Set([
1731
- "audio/aac",
1732
- "audio/m4a",
1733
- "audio/mp4",
1734
- "audio/mpeg",
1735
- "audio/wav",
1736
- "audio/x-m4a",
1737
- "audio/x-wav",
1738
- ]);
1739
- if (!supported.has(contentType)) {
1740
- throw new Error("audio must be AAC, M4A, MP3, MP4 audio, or WAV");
1741
- }
1742
- const inferredName = audio.name;
1743
- const extension = contentType.includes("wav")
1744
- ? "wav"
1745
- : contentType === "audio/mpeg"
1746
- ? "mp3"
1747
- : contentType === "audio/aac"
1748
- ? "aac"
1749
- : "m4a";
1750
- const filename = options.filename
1751
- ?? (typeof inferredName === "string" && inferredName ? inferredName : undefined)
1752
- ?? `microphone.${extension}`;
1753
- const asset = await this.client.assets.upload(audio, {
1754
- filename,
1755
- name: options.name,
1756
- });
1757
- return await this.client.request("POST", `/run-cloud/ios/${encodeURIComponent(id)}/microphone-audio`, {
1758
- assetId: asset.id,
1759
- ...(options.bundleId ? { bundleId: options.bundleId } : {}),
1760
- });
1761
- }
1762
1917
  }
1763
1918
  export class SimulatorsClient {
1764
1919
  client;
@@ -1770,11 +1925,15 @@ export class SimulatorsClient {
1770
1925
  return await new SimulatorClient(this.client, platform).create(rest);
1771
1926
  }
1772
1927
  async list(options = {}) {
1773
- const { platform, all } = options;
1928
+ const { platform, all, orgId } = options;
1774
1929
  if (platform)
1775
- return await new SimulatorClient(this.client, platform).list({ all });
1776
- const query = all ? "?all=1" : "";
1777
- return await this.client.request("GET", `/run-cloud/simulators${query}`);
1930
+ return await new SimulatorClient(this.client, platform).list({ all, orgId });
1931
+ const query = new URLSearchParams();
1932
+ if (all)
1933
+ query.set("all", "1");
1934
+ if (orgId)
1935
+ query.set("orgId", orgId);
1936
+ return await this.client.request("GET", `/run-cloud/simulators${query.size ? `?${query}` : ""}`);
1778
1937
  }
1779
1938
  async get(id, options = {}) {
1780
1939
  return await new SimulatorClient(this.client, options.platform ?? "ios").get(id);
@@ -1792,6 +1951,14 @@ export class SimulatorsClient {
1792
1951
  const { platform = "ios", ...installOptions } = options;
1793
1952
  return await new SimulatorClient(this.client, platform).install(id, application, installOptions);
1794
1953
  }
1954
+ async uploadCameraVideo(id, video, options = {}) {
1955
+ const { platform = "ios", ...mediaOptions } = options;
1956
+ return await new SimulatorClient(this.client, platform).uploadCameraVideo(id, video, mediaOptions);
1957
+ }
1958
+ async uploadMicrophoneAudio(id, audio, options = {}) {
1959
+ const { platform = "ios", ...mediaOptions } = options;
1960
+ return await new SimulatorClient(this.client, platform).uploadMicrophoneAudio(id, audio, mediaOptions);
1961
+ }
1795
1962
  async keepAlive(id, options = {}) {
1796
1963
  return await new SimulatorClient(this.client, options.platform ?? "ios").keepAlive(id);
1797
1964
  }
@@ -2417,8 +2584,9 @@ export class AssetsClient {
2417
2584
  constructor(client) {
2418
2585
  this.client = client;
2419
2586
  }
2420
- async list() {
2421
- return await this.client.request("GET", "/run-cloud/assets");
2587
+ async list(options = {}) {
2588
+ const query = options.orgId ? `?orgId=${encodeURIComponent(options.orgId)}` : "";
2589
+ return await this.client.request("GET", `/run-cloud/assets${query}`);
2422
2590
  }
2423
2591
  /** Upload through the buffered multipart endpoint instead of signed storage URLs. */
2424
2592
  async uploadDirect(file, options = {}) {
@@ -2440,6 +2608,7 @@ export class AssetsClient {
2440
2608
  const uploadRunId = options.uploadRunId ?? this.defaultUploadRunId;
2441
2609
  const prepared = await this.client.request("POST", "/run-cloud/assets/uploads", {
2442
2610
  filename,
2611
+ ...(options.orgId ? { orgId: options.orgId } : {}),
2443
2612
  ...(options.name ? { name: options.name } : {}),
2444
2613
  contentType: file.type || "application/octet-stream",
2445
2614
  byteSize: file.size,
@@ -2525,4 +2694,50 @@ export class AssetsClient {
2525
2694
  return await this.client.request("DELETE", `/run-cloud/assets/${encodeURIComponent(id)}`);
2526
2695
  }
2527
2696
  }
2697
+ export class CapabilitiesClient {
2698
+ client;
2699
+ constructor(client) {
2700
+ this.client = client;
2701
+ }
2702
+ /** Mint a short-lived bearer token. The token is returned only by this call. */
2703
+ async create(options) {
2704
+ return await this.client.request("POST", "/run-cloud/capabilities", options);
2705
+ }
2706
+ async list(options = {}) {
2707
+ const query = new URLSearchParams();
2708
+ if (options.orgId)
2709
+ query.set("orgId", options.orgId);
2710
+ if (options.active === false)
2711
+ query.set("active", "0");
2712
+ const result = await this.client.request("GET", `/run-cloud/capabilities${query.size ? `?${query}` : ""}`);
2713
+ return result.items;
2714
+ }
2715
+ async revoke(id) {
2716
+ return await this.client.request("DELETE", `/run-cloud/capabilities/${encodeURIComponent(id)}`);
2717
+ }
2718
+ }
2719
+ export class MobileClient {
2720
+ client;
2721
+ constructor(client) {
2722
+ this.client = client;
2723
+ }
2724
+ /** Idempotently create a mobile session and install one existing Asset or Xcode build output. */
2725
+ async launch(source, options) {
2726
+ if (!options.idempotencyKey?.trim()) {
2727
+ throw new Error("mobile.launch requires a stable idempotencyKey");
2728
+ }
2729
+ if (Boolean(source.assetId) === Boolean(source.buildId)) {
2730
+ throw new Error("mobile.launch requires exactly one assetId or buildId");
2731
+ }
2732
+ return await this.client.request("POST", "/run-cloud/mobile/launch", {
2733
+ ...(source.assetId ? { assetId: source.assetId } : { buildId: source.buildId }),
2734
+ ...(options.orgId ? { orgId: options.orgId } : {}),
2735
+ ...(options.platform ? { platform: options.platform } : {}),
2736
+ ...(options.session ? { session: options.session } : {}),
2737
+ }, { "Idempotency-Key": options.idempotencyKey });
2738
+ }
2739
+ async getLaunch(id) {
2740
+ return await this.client.request("GET", `/run-cloud/mobile/launches/${encodeURIComponent(id)}`);
2741
+ }
2742
+ }
2528
2743
  //# sourceMappingURL=index.js.map