runcloud 0.1.123 → 0.1.124

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.
@@ -853,6 +853,7 @@ async function pushAsset(path, opts = {}) {
853
853
  const api = client();
854
854
  const prepared = await api.post('/run-cloud/assets/uploads', {
855
855
  filename: name,
856
+ ...(opts.orgId ? { orgId: opts.orgId } : {}),
856
857
  ...(opts.name ? { name: opts.name } : {}),
857
858
  contentType: blob.type || 'application/octet-stream',
858
859
  byteSize: blob.size,
@@ -886,6 +887,7 @@ async function createSimulatorSession(platform, opts) {
886
887
  .filter((id) => typeof id === 'string' && id.length > 0),
887
888
  ];
888
889
  return await client().post(`/run-cloud/${platform}`, {
890
+ ...(opts.orgId ? { orgId: opts.orgId } : {}),
889
891
  model: opts.model,
890
892
  region: opts.region,
891
893
  displayName: opts.displayName,
@@ -965,6 +967,7 @@ function registerSimulatorCommands(program, platform) {
965
967
  .option('--model <model>', platform === 'ios' ? 'simulator model' : 'emulator model', platform === 'ios' ? 'iphone' : 'pixel')
966
968
  .option('--region <region>', 'fleet region')
967
969
  .option('--display-name <name>', 'human-readable name')
970
+ .option('--org-id <id>', 'organization to own the session')
968
971
  .option('--label <key=value>', 'label to attach to the session', (v, p) => [...p, v], [])
969
972
  .option('--install <file>', 'local app artifact to upload and install', (v, p) => [...p, v], [])
970
973
  .option('--install-asset <name-or-id>', 'previously uploaded asset to install', (v, p) => [...p, v], [])
@@ -1416,6 +1419,7 @@ export function registerRunCloud(program) {
1416
1419
  .description('Upload an app asset for simulator installation')
1417
1420
  .argument('<file>')
1418
1421
  .option('-n, --name <name>', 'asset name')
1422
+ .option('--org-id <id>', 'organization to own the Asset')
1419
1423
  .option('--json', 'output JSON', false)
1420
1424
  .action((file, opts) => action(async () => {
1421
1425
  print(await pushAsset(file, opts), opts);
@@ -1423,9 +1427,11 @@ export function registerRunCloud(program) {
1423
1427
  asset
1424
1428
  .command('list')
1425
1429
  .description('List uploaded assets')
1430
+ .option('--org-id <id>', 'organization ID')
1426
1431
  .option('--json', 'output JSON', false)
1427
1432
  .action((opts) => action(async () => {
1428
- print(await client().get('/run-cloud/assets'), opts);
1433
+ const query = opts.orgId ? `?orgId=${encodeURIComponent(opts.orgId)}` : '';
1434
+ print(await client().get(`/run-cloud/assets${query}`), opts);
1429
1435
  }));
1430
1436
  asset
1431
1437
  .command('pull')
@@ -1446,6 +1452,113 @@ export function registerRunCloud(program) {
1446
1452
  .action((id, opts) => action(async () => {
1447
1453
  print(await client().delete(`/run-cloud/assets/${encodeURIComponent(id)}`), opts);
1448
1454
  }));
1455
+ const capability = program
1456
+ .command('capability')
1457
+ .alias('capabilities')
1458
+ .description('Mint, inspect, and revoke short-lived resource capabilities');
1459
+ capability
1460
+ .command('create')
1461
+ .description('Mint a bearer token bound to explicit org resources')
1462
+ .requiredOption('--scope <scope>', 'asset.read, mobile.launch, xcode.build.create, or xcode.build.read; repeatable', (value, previous) => [...previous, value], [])
1463
+ .option('--asset <id>', 'allowed Asset ID; repeatable', (value, previous) => [...previous, value], [])
1464
+ .option('--build <id>', 'allowed Xcode build ID; repeatable', (value, previous) => [...previous, value], [])
1465
+ .option('--idempotency-key <key>', 'allowed build or launch idempotency key; repeatable', (value, previous) => [...previous, value], [])
1466
+ .option('--org-id <id>', 'organization ID')
1467
+ .option('--name <name>', 'human-readable capability name')
1468
+ .option('--expires-in <seconds>', 'lifetime from 60 to 3600 seconds', '900')
1469
+ .option('--json', 'output JSON, including the one-time token', false)
1470
+ .action((opts) => action(async () => {
1471
+ const expiresInSeconds = Number(opts.expiresIn);
1472
+ if (!Number.isInteger(expiresInSeconds) || expiresInSeconds < 60 || expiresInSeconds > 3600) {
1473
+ throw new Error('--expires-in must be an integer from 60 to 3600');
1474
+ }
1475
+ const resources = {
1476
+ ...(opts.asset?.length ? { assetIds: opts.asset } : {}),
1477
+ ...(opts.build?.length ? { buildIds: opts.build } : {}),
1478
+ ...(opts.idempotencyKey?.length ? { idempotencyKeys: opts.idempotencyKey } : {}),
1479
+ };
1480
+ if (Object.keys(resources).length === 0) {
1481
+ throw new Error('Provide at least one --asset, --build, or --idempotency-key resource');
1482
+ }
1483
+ print(await client().post('/run-cloud/capabilities', {
1484
+ scopes: opts.scope,
1485
+ resources,
1486
+ expiresInSeconds,
1487
+ ...(opts.orgId ? { orgId: opts.orgId } : {}),
1488
+ ...(opts.name ? { name: opts.name } : {}),
1489
+ }), opts);
1490
+ }));
1491
+ capability
1492
+ .command('list')
1493
+ .description('List capability metadata; secret token values are never returned')
1494
+ .option('--org-id <id>', 'organization ID')
1495
+ .option('--all', 'include expired and revoked capabilities', false)
1496
+ .option('--json', 'output JSON', false)
1497
+ .action((opts) => action(async () => {
1498
+ const query = new URLSearchParams();
1499
+ if (opts.orgId)
1500
+ query.set('orgId', opts.orgId);
1501
+ if (opts.all)
1502
+ query.set('active', '0');
1503
+ print(await client().get(`/run-cloud/capabilities${query.size ? `?${query}` : ''}`), opts);
1504
+ }));
1505
+ capability
1506
+ .command('revoke')
1507
+ .description('Revoke a capability immediately')
1508
+ .argument('<id>')
1509
+ .option('--json', 'output JSON', false)
1510
+ .action((id, opts) => action(async () => {
1511
+ print(await client().delete(`/run-cloud/capabilities/${encodeURIComponent(id)}`), opts);
1512
+ }));
1513
+ const mobile = program
1514
+ .command('mobile')
1515
+ .description('Idempotently launch an Asset or Xcode build in a mobile session');
1516
+ mobile
1517
+ .command('launch')
1518
+ .description('Create one session and install one reusable output')
1519
+ .option('--asset <id>', 'Asset ID to install')
1520
+ .option('--build <id>', 'successful Xcode simulator build to install')
1521
+ .requiredOption('--idempotency-key <key>', 'stable retry key owned by this launch intent')
1522
+ .option('--platform <ios|android>', 'mobile platform', 'ios')
1523
+ .option('--org-id <id>', 'organization ID')
1524
+ .option('--model <model>', 'simulator or emulator model')
1525
+ .option('--region <region>', 'fleet region')
1526
+ .option('--display-name <name>', 'human-readable session name')
1527
+ .option('--label <key=value>', 'session tag; repeatable', (value, previous) => [...previous, value], [])
1528
+ .option('--inactivity-timeout <duration>', 'idle timeout such as 60s, 3m, or none')
1529
+ .option('--hard-timeout <duration>', 'maximum lifetime such as 10m or 1h')
1530
+ .option('--codec <auto|mjpeg|webrtc>', 'viewer codec', 'auto')
1531
+ .option('--json', 'output JSON', false)
1532
+ .action((opts) => action(async () => {
1533
+ if (Boolean(opts.asset) === Boolean(opts.build)) {
1534
+ throw new Error('Provide exactly one of --asset or --build');
1535
+ }
1536
+ if (opts.platform !== 'ios' && opts.platform !== 'android') {
1537
+ throw new Error('--platform must be ios or android');
1538
+ }
1539
+ print(await client().postWithHeaders('/run-cloud/mobile/launch', {
1540
+ ...(opts.asset ? { assetId: opts.asset } : { buildId: opts.build }),
1541
+ platform: opts.platform,
1542
+ ...(opts.orgId ? { orgId: opts.orgId } : {}),
1543
+ session: {
1544
+ ...(opts.model ? { model: opts.model } : {}),
1545
+ ...(opts.region ? { region: opts.region } : {}),
1546
+ ...(opts.displayName ? { displayName: opts.displayName } : {}),
1547
+ tags: parseTags(opts.label),
1548
+ ...(opts.inactivityTimeout ? { inactivityTimeout: opts.inactivityTimeout } : {}),
1549
+ ...(opts.hardTimeout ? { hardTimeout: opts.hardTimeout } : {}),
1550
+ ...(opts.codec ? { codec: opts.codec } : {}),
1551
+ },
1552
+ }, { 'Idempotency-Key': opts.idempotencyKey }), opts);
1553
+ }));
1554
+ mobile
1555
+ .command('get')
1556
+ .description('Read one mobile launch operation and its session or failure')
1557
+ .argument('<id>')
1558
+ .option('--json', 'output JSON', false)
1559
+ .action((id, opts) => action(async () => {
1560
+ print(await client().get(`/run-cloud/mobile/launches/${encodeURIComponent(id)}`), opts);
1561
+ }));
1449
1562
  const skills = program.command('skills').description('Install run.cloud skills for AI coding agents');
1450
1563
  skills
1451
1564
  .command('install')
@@ -309,20 +309,22 @@ export async function requestFor(flags) {
309
309
  timeoutSeconds,
310
310
  };
311
311
  }
312
- async function logChunk(client, id, offset) {
313
- return await retryXcodeRequest(async () => await client.get(`/run-cloud/xcode/builds/${encodeURIComponent(id)}/logs?offset=${offset}`));
312
+ async function logChunk(client, id, offset, source) {
313
+ return await retryXcodeRequest(async () => await client.get(`/run-cloud/xcode/builds/${encodeURIComponent(id)}/logs?offset=${offset}${source ? `&source=${source}` : ''}`));
314
314
  }
315
315
  async function waitForBuild(client, id, flags) {
316
316
  const timeout = integerFlag(flags.waitTimeout, '--wait-timeout', 1, 23400, 22500);
317
317
  const deadline = Date.now() + timeout * 1000;
318
318
  let offset = 0;
319
+ let source;
319
320
  for (;;) {
320
321
  const build = await retryXcodeRequest(async () => await client.get(`/run-cloud/xcode/builds/${encodeURIComponent(id)}`));
321
322
  if (build.logs.available || build.status === 'queued' || build.status === 'running') {
322
- const chunk = await logChunk(client, id, offset);
323
+ const chunk = await logChunk(client, id, offset, source);
323
324
  if (chunk.text)
324
325
  (flags.json ? process.stderr : process.stdout).write(chunk.text);
325
326
  offset = chunk.nextOffset;
327
+ source = chunk.source ?? source;
326
328
  if (TERMINAL.has(build.status) && chunk.complete)
327
329
  return build;
328
330
  }
@@ -369,7 +371,7 @@ export function registerXcode(program) {
369
371
  .option('--provisioning-profile <path>', 'provisioning profile; repeatable', repeated, [])
370
372
  .option('--signing-method <method>', 'app-store-connect, release-testing, or debugging')
371
373
  .option('--team-id <id>', 'Apple Developer team ID for cloud signing')
372
- .option('--upload <filename>', 'filename for the retained run.cloud artifact')
374
+ .option('--upload <filename>', 'public filename for the retained run.cloud artifact')
373
375
  .option('--signed-upload-url <url>', 'also PUT the artifact to this presigned HTTPS URL')
374
376
  .option('--upload-to-appstore', 'upload the signed IPA to App Store Connect', false)
375
377
  .option('--asc-key-id <id>', 'App Store Connect API key ID')
@@ -425,6 +427,64 @@ export function registerXcode(program) {
425
427
  throw new Error(`${reason}\n${next}\nBuild ID: ${build.id}`);
426
428
  }
427
429
  }));
430
+ xcode
431
+ .command('rebundle')
432
+ .description('Reuse a compatible React Native native shell and replace only its JavaScript bundle')
433
+ .argument('[path]', 'local Expo or React Native source directory', '.')
434
+ .requiredOption('--basis-build <id>', 'successful simulator build providing the native shell')
435
+ .option('--expo-app-dir <path>', 'relative Expo app directory in a monorepo')
436
+ .option('--ignore <regex>', 'exclude source paths matching a regular expression; repeatable', repeated, [])
437
+ .option('--include <regex>', 'force-include matching source paths; repeatable', repeated, [])
438
+ .option('--additional-file <local=remote>', 'include an extra file; repeatable', repeated, [])
439
+ .option('--org-id <id>', 'organization to own the build')
440
+ .option('--idempotency-key <key>', 'stable key for retry-safe submission')
441
+ .option('--timeout-seconds <seconds>', 'remote bundle timeout, max 21600', '3600')
442
+ .option('--wait-timeout <seconds>', 'client wait timeout, max 23400', '22500')
443
+ .option('--no-wait', 'return after the control plane accepts the build')
444
+ .option('--detach', 'alias for --no-wait; terminal state remains queryable', false)
445
+ .option('-o, --output <path>', 'download the rebundled app here after success')
446
+ .option('--force', 'overwrite --output if it exists', false)
447
+ .option('--json', 'print the build record as JSON; streamed logs go to stderr', false)
448
+ .action((sourcePath, flags) => action(async () => {
449
+ if (!/^xcb_[A-Za-z0-9]+$/.test(flags.basisBuild)) {
450
+ throw new Error('--basis-build must be a run.cloud Xcode build ID');
451
+ }
452
+ const request = await requestFor({
453
+ ...flags,
454
+ sdk: 'iphonesimulator',
455
+ configuration: 'Release',
456
+ });
457
+ request.reactNative = { ...(flags.expoAppDir ? { expoAppDir: flags.expoAppDir } : {}) };
458
+ request.reuse = { basisBuildId: flags.basisBuild };
459
+ const source = await packSource(sourcePath, flags);
460
+ const archiveBuffer = new ArrayBuffer(source.body.byteLength);
461
+ new Uint8Array(archiveBuffer).set(source.body);
462
+ const form = new FormData();
463
+ form.append('request', JSON.stringify(request));
464
+ form.append('source', new Blob([archiveBuffer], { type: 'application/gzip' }), source.filename);
465
+ const client = api();
466
+ const idempotencyKey = flags.idempotencyKey ?? `xcode_rebundle_${randomUUID()}`;
467
+ let build = await retryXcodeRequest(async () => await client.uploadForm('/run-cloud/xcode/builds', form, { 'Idempotency-Key': idempotencyKey }));
468
+ if (flags.detach || flags.wait === false) {
469
+ print(build, flags.json);
470
+ return;
471
+ }
472
+ if (!flags.json)
473
+ console.error(`Rebundle ${build.id} accepted; streaming remote logs.`);
474
+ build = await waitForBuild(client, build.id, flags);
475
+ const artifactPath = flags.output && build.artifact.available
476
+ ? await downloadArtifact(client, build, flags.output, flags.force)
477
+ : undefined;
478
+ if (flags.json)
479
+ print({ ...build, ...(artifactPath ? { artifactPath } : {}) }, true);
480
+ else if (build.status === 'succeeded') {
481
+ console.log(`Rebundle ${build.id} succeeded.`);
482
+ console.log(artifactPath ? `Artifact: ${artifactPath}` : `Asset: ${build.artifact.assetId}`);
483
+ }
484
+ if (build.status !== 'succeeded') {
485
+ throw new Error(`${build.failure?.reason ?? `Rebundle ended with ${build.status}`}\n${build.failure?.action ?? `Inspect logs with runcloud xcode logs ${build.id}`}\nBuild ID: ${build.id}`);
486
+ }
487
+ }));
428
488
  xcode
429
489
  .command('get')
430
490
  .description('Show product-visible Xcode build state')
@@ -447,6 +507,30 @@ export function registerXcode(program) {
447
507
  const result = await retryXcodeRequest(async () => await api().get(`/run-cloud/xcode/builds?${query}`));
448
508
  print(flags.json ? result : result.items, flags.json);
449
509
  }));
510
+ xcode
511
+ .command('events')
512
+ .description('Read ordered replayable Xcode build events after a durable cursor')
513
+ .option('--org-id <id>', 'organization ID')
514
+ .option('--build-id <id>', 'filter to one build')
515
+ .option('--after <cursor>', 'last processed event sequence', '0')
516
+ .option('--limit <count>', 'maximum events, 1 to 500', '100')
517
+ .option('--json', 'print the page, including nextCursor, as JSON', false)
518
+ .action((flags) => action(async () => {
519
+ const after = integerFlag(flags.after, '--after', 0, Number.MAX_SAFE_INTEGER, 0);
520
+ const limit = integerFlag(flags.limit, '--limit', 1, 500, 100);
521
+ const query = new URLSearchParams({ after: String(after), limit: String(limit) });
522
+ if (flags.orgId)
523
+ query.set('orgId', flags.orgId);
524
+ if (flags.buildId)
525
+ query.set('buildId', flags.buildId);
526
+ const page = await retryXcodeRequest(async () => await api().get(`/run-cloud/xcode/events?${query}`));
527
+ if (flags.json)
528
+ print(page, true);
529
+ else {
530
+ print(page.items);
531
+ console.error(`nextCursor: ${page.nextCursor}`);
532
+ }
533
+ }));
450
534
  xcode
451
535
  .command('logs')
452
536
  .description('Read the sanitized live or retained Xcode build log')
@@ -456,13 +540,15 @@ export function registerXcode(program) {
456
540
  .action((id, flags) => action(async () => {
457
541
  const client = api();
458
542
  let offset = 0;
543
+ let source;
459
544
  for (;;) {
460
- const chunk = await logChunk(client, id, offset);
545
+ const chunk = await logChunk(client, id, offset, source);
461
546
  if (flags.json)
462
547
  console.log(JSON.stringify(chunk));
463
548
  else if (chunk.text)
464
549
  process.stdout.write(chunk.text);
465
550
  offset = chunk.nextOffset;
551
+ source = chunk.source ?? source;
466
552
  if (!flags.follow || chunk.complete)
467
553
  return;
468
554
  await new Promise((resolvePromise) => setTimeout(resolvePromise, 1000));
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = '0.1.123';
1
+ export const CLI_VERSION = '0.1.124';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcloud",
3
- "version": "0.1.123",
3
+ "version": "0.1.124",
4
4
  "description": "Create and control run.cloud remote mobile simulators and cloud sandboxes",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
Binary file
Binary file