@newly/cli 0.1.109 → 0.1.111

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.
@@ -67,6 +67,10 @@ runcloud ios open-url myapp://path --id <id> # open a URL or deep
67
67
  runcloud ios tap <id> 0.5 0.75 # normalized top-left display coordinates
68
68
  runcloud ios type-text <id> "Hello from iOS!" # printable US ASCII, tab, and line feed
69
69
  runcloud ios screenshot <id> --output ios.png # save a PNG without exposing the viewer URL
70
+ runcloud ios recording start <id> --json # start an idempotent MP4 screen recording
71
+ runcloud ios recording stop <id> <recording-id> --json # stop and finalize it
72
+ runcloud ios recording download <id> <recording-id> \
73
+ --output ios.mp4 --json # download through the authenticated API
70
74
  runcloud ios logs <id> --tail 200 # read logs from this lease
71
75
  runcloud ios logs <id> --follow # follow new log entries
72
76
  runcloud ios delete <id> # release the session
@@ -96,6 +100,12 @@ report `capsLock`, `numLock`, and `scrollLock` as unsupported keys.
96
100
  Coordinates are inclusive normalized display coordinates: `(0, 0)` is the
97
101
  top-left and `(1, 1)` is the bottom-right.
98
102
 
103
+ The `recording` group is shared by iOS and Android. It provides `start`,
104
+ `list`, `status` (`get` alias), `stop`, and `download`. Pass a stable
105
+ `--idempotency-key` to retry `start` without creating a second recording.
106
+ `download --output <path>` validates the MP4 and reports its byte size and
107
+ SHA-256 digest without printing a signed storage URL.
108
+
99
109
  Interaction commands wait for a correlated simulator acknowledgement. They
100
110
  accept `--timeout <milliseconds>` (15 seconds by default), `--request-id <id>`,
101
111
  and `--json`. Successful JSON is the API's typed completion envelope. Failures
@@ -485,6 +485,47 @@ async function captureSimulatorScreenshot(platform, id, opts) {
485
485
  else
486
486
  console.log(`Saved ${platform} simulator screenshot to ${output} (${bytes.byteLength} bytes)`);
487
487
  }
488
+ function simulatorRecordingBase(platform, sessionId) {
489
+ return `/run-cloud/${platform}/${encodeURIComponent(sessionId)}/recordings`;
490
+ }
491
+ function recordingIdempotencyKey(value) {
492
+ if (value === undefined)
493
+ return undefined;
494
+ const key = value.trim();
495
+ const hasControlCharacter = [...key].some((character) => {
496
+ const code = character.charCodeAt(0);
497
+ return code <= 31 || code === 127;
498
+ });
499
+ if (!key || key.length > 200 || hasControlCharacter) {
500
+ throw new Error('--idempotency-key must contain 1-200 printable characters');
501
+ }
502
+ return key;
503
+ }
504
+ async function downloadSimulatorRecording(platform, sessionId, recordingId, opts) {
505
+ const bytes = await client().getBinary(`${simulatorRecordingBase(platform, sessionId)}/${encodeURIComponent(recordingId)}/download`);
506
+ if (bytes.length < 8 || bytes.subarray(4, 8).toString('ascii') !== 'ftyp') {
507
+ throw new Error('Simulator recording response was not a valid MP4 file');
508
+ }
509
+ const output = resolve(opts.output);
510
+ mkdirSync(dirname(output), { recursive: true });
511
+ writeFileSync(output, bytes);
512
+ const result = {
513
+ ok: true,
514
+ sessionId,
515
+ recordingId,
516
+ platform,
517
+ action: 'recording.download',
518
+ status: 'completed',
519
+ output,
520
+ contentType: 'video/mp4',
521
+ byteSize: bytes.byteLength,
522
+ sha256: createHash('sha256').update(bytes).digest('hex'),
523
+ };
524
+ if (opts.json)
525
+ console.log(JSON.stringify(result, null, 2));
526
+ else
527
+ console.log(`Saved ${platform} simulator recording to ${output} (${bytes.byteLength} bytes)`);
528
+ }
488
529
  function fileBlob(path) {
489
530
  const resolved = resolve(path);
490
531
  if (!existsSync(resolved))
@@ -715,7 +756,7 @@ function registerSimulatorCommands(program, platform) {
715
756
  .addOption(new Option('--dev-host <host>', 'dev-only: target a specific simulator fleet host').hideHelp())
716
757
  .action((opts) => action(async () => {
717
758
  print(await createSimulatorSession(platform, opts), opts);
718
- }));
759
+ }, opts));
719
760
  simulator
720
761
  .command('list')
721
762
  .description(`List active ${label} sessions`)
@@ -724,7 +765,7 @@ function registerSimulatorCommands(program, platform) {
724
765
  .action((opts) => action(async () => {
725
766
  const qs = opts.all ? '?all=1' : '';
726
767
  print(await client().get(`/run-cloud/${platform}${qs}`), opts);
727
- }));
768
+ }, opts));
728
769
  simulator
729
770
  .command('get')
730
771
  .description(`Show ${article} ${label} session`)
@@ -732,7 +773,7 @@ function registerSimulatorCommands(program, platform) {
732
773
  .option('--json', 'output JSON', false)
733
774
  .action((id, opts) => action(async () => {
734
775
  print(await client().get(`/run-cloud/${platform}/${encodeURIComponent(id)}`), opts);
735
- }));
776
+ }, opts));
736
777
  simulator
737
778
  .command('delete')
738
779
  .description(`Release ${article} ${label} session`)
@@ -740,7 +781,7 @@ function registerSimulatorCommands(program, platform) {
740
781
  .option('--json', 'output JSON', false)
741
782
  .action((id, opts) => action(async () => {
742
783
  print(await client().delete(`/run-cloud/${platform}/${encodeURIComponent(id)}`), opts);
743
- }));
784
+ }, opts));
744
785
  simulator
745
786
  .command('open-url')
746
787
  .description(`Open a URL or deep link in ${article} ${label} session`)
@@ -749,7 +790,7 @@ function registerSimulatorCommands(program, platform) {
749
790
  .option('--json', 'output JSON', false)
750
791
  .action((url, opts) => action(async () => {
751
792
  print(await client().post(`/run-cloud/${platform}/${encodeURIComponent(opts.id)}/open-url`, { url }), opts);
752
- }));
793
+ }, opts));
753
794
  addInteractionOptions(simulator
754
795
  .command('tap')
755
796
  .description(`Tap normalized display coordinates in ${article} ${label} session`)
@@ -882,6 +923,56 @@ function registerSimulatorCommands(program, platform) {
882
923
  .description(`Capture the current display from ${article} ${label} session`)
883
924
  .argument('<id>', 'session id')
884
925
  .requiredOption('-o, --output <path>', 'PNG output path')).action((id, opts) => action(() => captureSimulatorScreenshot(platform, id, opts), opts));
926
+ const recording = simulator
927
+ .command('recording')
928
+ .alias('recordings')
929
+ .description(`Record the display of ${article} ${label} session`);
930
+ recording
931
+ .command('start')
932
+ .description(`Start an MP4 screen recording for ${article} active ${label} session`)
933
+ .argument('<id>', 'session id')
934
+ .option('--idempotency-key <key>', 'retry-safe key; reuse it to receive the same recording')
935
+ .option('--json', 'output JSON', false)
936
+ .action((id, opts) => action(async () => {
937
+ const key = recordingIdempotencyKey(opts.idempotencyKey);
938
+ const headers = key ? { 'Idempotency-Key': key } : {};
939
+ print(await client().postWithHeaders(simulatorRecordingBase(platform, id), {}, headers), opts);
940
+ }, opts));
941
+ recording
942
+ .command('list')
943
+ .description(`List retained screen recordings for ${article} ${label} session`)
944
+ .argument('<id>', 'session id')
945
+ .option('--json', 'output JSON', false)
946
+ .action((id, opts) => action(async () => {
947
+ print(await client().get(simulatorRecordingBase(platform, id)), opts);
948
+ }, opts));
949
+ recording
950
+ .command('status')
951
+ .alias('get')
952
+ .description(`Show lifecycle state, failures, events, and retrieval metadata for a recording`)
953
+ .argument('<id>', 'session id')
954
+ .argument('<recording-id>', 'recording id')
955
+ .option('--json', 'output JSON', false)
956
+ .action((id, recordingId, opts) => action(async () => {
957
+ print(await client().get(`${simulatorRecordingBase(platform, id)}/${encodeURIComponent(recordingId)}`), opts);
958
+ }, opts));
959
+ recording
960
+ .command('stop')
961
+ .description(`Stop and finalize a screen recording as an MP4`)
962
+ .argument('<id>', 'session id')
963
+ .argument('<recording-id>', 'recording id')
964
+ .option('--json', 'output JSON', false)
965
+ .action((id, recordingId, opts) => action(async () => {
966
+ print(await client().post(`${simulatorRecordingBase(platform, id)}/${encodeURIComponent(recordingId)}/stop`), opts);
967
+ }, opts));
968
+ recording
969
+ .command('download')
970
+ .description(`Download a ready screen recording through the authenticated API`)
971
+ .argument('<id>', 'session id')
972
+ .argument('<recording-id>', 'recording id')
973
+ .requiredOption('-o, --output <path>', 'MP4 output path')
974
+ .option('--json', 'output file metadata as JSON', false)
975
+ .action((id, recordingId, opts) => action(() => downloadSimulatorRecording(platform, id, recordingId, opts), opts));
885
976
  simulator
886
977
  .command('logs')
887
978
  .description(`Read or follow logs from ${article} active ${label} session`)
@@ -921,7 +1012,7 @@ function registerSimulatorCommands(program, platform) {
921
1012
  for (const entry of parsed.entries)
922
1013
  writeSimulatorLogEntry(entry, opts.json === true);
923
1014
  }
924
- }));
1015
+ }, opts));
925
1016
  return simulator;
926
1017
  }
927
1018
  function jsonErrorEnvelope(error) {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = '0.1.109';
1
+ export const CLI_VERSION = '0.1.111';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newly/cli",
3
- "version": "0.1.109",
3
+ "version": "0.1.111",
4
4
  "description": "Develop, provision, and deploy Newly backends from your terminal",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",