@newly/cli 0.1.124 → 0.1.126

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 accessibility-tree <id> --json # read the current typed accessibility hierarchy
70
+ runcloud ios camera inject <id> ./quadrants.mp4 \
71
+ --bundle-id com.example.Camera # inject deterministic app-camera video
72
+ runcloud ios microphone inject <id> ./tone.wav \
73
+ --bundle-id com.example.Recorder # inject deterministic microphone audio
70
74
  runcloud ios screenshot <id> --output ios.png # save a PNG without exposing the viewer URL
71
75
  runcloud ios recording start <id> --json # start an idempotent MP4 screen recording
72
76
  runcloud ios recording stop <id> <recording-id> --json # stop and finalize it
@@ -81,6 +85,8 @@ runcloud android create --model pixel
81
85
  runcloud android swipe <id> 0.5 0.8 0.5 0.2 --duration 250
82
86
  runcloud android press-key <id> enter
83
87
  runcloud android accessibility-tree <id> --json
88
+ runcloud android camera inject <id> ./quadrants.mp4 --bundle-id com.example.Camera
89
+ runcloud android mic inject <id> ./tone.wav --bundle-id com.example.Recorder
84
90
  runcloud android screenshot <id> --output android.png --json
85
91
  ```
86
92
 
@@ -119,6 +125,11 @@ report `capsLock`, `numLock`, and `scrollLock` as unsupported keys.
119
125
  Coordinates are inclusive normalized display coordinates: `(0, 0)` is the
120
126
  top-left and `(1, 1)` is the bottom-right.
121
127
 
128
+ Camera injection accepts MP4 and MOV. Microphone injection accepts AAC, M4A,
129
+ MP3, MP4 audio, and WAV; `mic` is an alias for `microphone`. Each successful
130
+ JSON response contains the uploaded asset ID, so automation can delete it with
131
+ `runcloud asset delete <asset-id>` after the proof completes.
132
+
122
133
  `accessibility-tree` (`accessibility` alias) returns an indented terminal tree
123
134
  or a versioned JSON hierarchy with cross-platform roles, labels, values,
124
135
  states, bounds, identifiers, and nested children. Secure field values are
@@ -3,7 +3,15 @@ import { join } from 'node:path';
3
3
  import { ApiClient, ApiError, friendlyApiError } from '../api.js';
4
4
  import { requireCredentials, writeProjectLink } from '../config.js';
5
5
  import { extractSnapshot } from '../archive.js';
6
+ function assertNotManagedSession() {
7
+ const managedSessionProjectId = process.env['NEWLY_PROJECT_ID'];
8
+ if (!managedSessionProjectId)
9
+ return;
10
+ throw new Error(`This managed Newly session is already attached to project ${managedSessionProjectId}. ` +
11
+ '`newly create` would create a separate project; use `newly deploy`, `newly status`, or another project command instead.');
12
+ }
6
13
  export async function runCreate(client, dir, opts) {
14
+ assertNotManagedSession();
7
15
  if (existsSync(join(dir, 'backend'))) {
8
16
  throw new Error('A `backend/` directory already exists here. Run `newly create` in a directory without one.');
9
17
  }
@@ -42,6 +50,7 @@ export function registerCreate(program) {
42
50
  .option('--no-provision', 'create without provisioning the backend')
43
51
  .action(async (name, opts) => {
44
52
  try {
53
+ assertNotManagedSession();
45
54
  if (existsSync(join(process.cwd(), '.newly', 'project.json'))) {
46
55
  throw new Error('This directory is already linked to a project. Use a fresh directory for `newly create`.');
47
56
  }
@@ -0,0 +1,31 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { ApiClient, friendlyApiError } from '../api.js';
4
+ import { extractSnapshot } from '../archive.js';
5
+ import { requireCredentials, resolveProject } from '../config.js';
6
+ export async function runInit(client, dir, projectId) {
7
+ if (existsSync(join(dir, 'backend'))) {
8
+ throw new Error('A `backend/` directory already exists here. Use `newly deploy --env dev` to deploy it.');
9
+ }
10
+ const template = await client.getBinary('/newly_backend/backend_template');
11
+ await extractSnapshot(template, dir);
12
+ console.log(`Initialized Newly Backend for project ${projectId}. ` +
13
+ 'Run `newly deploy --env dev` to provision the project database and deploy the services.');
14
+ }
15
+ export function registerInit(program) {
16
+ program
17
+ .command('init')
18
+ .description('Initialize Newly Backend in the currently linked project')
19
+ .action(async () => {
20
+ try {
21
+ const dir = process.cwd();
22
+ const projectId = resolveProject(dir, undefined);
23
+ const credentials = requireCredentials();
24
+ await runInit(new ApiClient(credentials.apiUrl, credentials.token), dir, projectId);
25
+ }
26
+ catch (error) {
27
+ console.error(friendlyApiError(error));
28
+ process.exitCode = 1;
29
+ }
30
+ });
31
+ }
@@ -1,6 +1,6 @@
1
1
  import { Option } from 'commander';
2
2
  import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
3
- import { basename, dirname, join, resolve } from 'node:path';
3
+ import { basename, dirname, extname, join, resolve } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
6
  import { createHash, randomUUID } from 'node:crypto';
@@ -691,7 +691,7 @@ async function downloadSimulatorRecording(platform, sessionId, recordingId, opts
691
691
  else
692
692
  console.log(`Saved ${platform} simulator recording to ${output} (${bytes.byteLength} bytes)`);
693
693
  }
694
- function fileBlob(path) {
694
+ function fileBlob(path, contentType = 'application/octet-stream') {
695
695
  const resolved = resolve(path);
696
696
  if (!existsSync(resolved))
697
697
  throw new Error(`File does not exist: ${path}`);
@@ -699,7 +699,7 @@ function fileBlob(path) {
699
699
  throw new Error(`Not a file: ${path}`);
700
700
  const data = readFileSync(resolved);
701
701
  return {
702
- blob: new Blob([new Uint8Array(data)], { type: 'application/octet-stream' }),
702
+ blob: new Blob([new Uint8Array(data)], { type: contentType }),
703
703
  name: basename(resolved),
704
704
  };
705
705
  }
@@ -849,7 +849,7 @@ export async function runCloudDemo(name = 'eight-device-mosaic', opts = {}, load
849
849
  await demo.runDemo(args);
850
850
  }
851
851
  async function pushAsset(path, opts = {}) {
852
- const { blob, name } = fileBlob(path);
852
+ const { blob, name } = fileBlob(path, opts.contentType);
853
853
  const api = client();
854
854
  const prepared = await api.post('/run-cloud/assets/uploads', {
855
855
  filename: name,
@@ -875,6 +875,63 @@ async function pushAsset(path, opts = {}) {
875
875
  }
876
876
  return prepared.asset;
877
877
  }
878
+ function mediaContentType(kind, path) {
879
+ const extension = extname(path).toLowerCase();
880
+ if (kind === 'camera') {
881
+ if (extension === '.mp4' || extension === '.m4v')
882
+ return 'video/mp4';
883
+ if (extension === '.mov' || extension === '.qt')
884
+ return 'video/quicktime';
885
+ throw new Error('Camera video must be an MP4 or MOV file.');
886
+ }
887
+ const audioTypes = {
888
+ '.aac': 'audio/aac',
889
+ '.m4a': 'audio/m4a',
890
+ '.mp3': 'audio/mpeg',
891
+ '.mp4': 'audio/mp4',
892
+ '.wav': 'audio/wav',
893
+ };
894
+ const contentType = audioTypes[extension];
895
+ if (!contentType)
896
+ throw new Error('Microphone audio must be AAC, M4A, MP3, MP4 audio, or WAV.');
897
+ return contentType;
898
+ }
899
+ async function injectSimulatorMedia(platform, kind, sessionId, path, opts) {
900
+ const asset = await pushAsset(path, {
901
+ name: opts.name,
902
+ contentType: mediaContentType(kind, path),
903
+ });
904
+ if (typeof asset.id !== 'string' || asset.id.length === 0) {
905
+ throw new Error('run.cloud did not return an asset ID for the media upload.');
906
+ }
907
+ const action = kind === 'camera' ? 'camera-video' : 'microphone-audio';
908
+ const api = client();
909
+ try {
910
+ const result = await api.post(`/run-cloud/${platform}/${encodeURIComponent(sessionId)}/${action}`, {
911
+ assetId: asset.id,
912
+ ...(opts.bundleId ? { bundleId: opts.bundleId } : {}),
913
+ });
914
+ const response = result && typeof result === 'object' && !Array.isArray(result)
915
+ ? result
916
+ : null;
917
+ const responseAsset = response?.asset
918
+ && typeof response.asset === 'object'
919
+ && !Array.isArray(response.asset)
920
+ ? response.asset
921
+ : null;
922
+ if (response?.ok !== true
923
+ || response.platform !== platform
924
+ || response.sessionId !== sessionId
925
+ || responseAsset?.id !== asset.id) {
926
+ throw new Error(`run.cloud returned an invalid ${kind} injection acknowledgement.`);
927
+ }
928
+ return response;
929
+ }
930
+ catch (error) {
931
+ await api.delete(`/run-cloud/assets/${encodeURIComponent(asset.id)}`).catch(() => undefined);
932
+ throw error;
933
+ }
934
+ }
878
935
  async function createSimulatorSession(platform, opts) {
879
936
  assertDevHostAllowed(opts.devHost);
880
937
  const uploadedAssets = [];
@@ -1204,6 +1261,35 @@ function registerSimulatorCommands(program, platform) {
1204
1261
  .requiredOption('-o, --output <path>', 'MP4 output path')
1205
1262
  .option('--json', 'output file metadata as JSON', false)
1206
1263
  .action((id, recordingId, opts) => action(() => downloadSimulatorRecording(platform, id, recordingId, opts), opts));
1264
+ const camera = simulator
1265
+ .command('camera')
1266
+ .description(`Inject deterministic prerecorded input into ${article} ${label} app camera`);
1267
+ camera
1268
+ .command('inject')
1269
+ .description('Upload an MP4 or MOV file and loop it through the app camera')
1270
+ .argument('<id>', 'session id')
1271
+ .argument('<video>', 'MP4 or MOV input path')
1272
+ .option('--bundle-id <id>', 'target app bundle ID or Android package')
1273
+ .option('-n, --name <name>', 'retained media asset name')
1274
+ .option('--json', 'output JSON', false)
1275
+ .action((id, video, opts) => action(async () => {
1276
+ print(await injectSimulatorMedia(platform, 'camera', id, video, opts), opts);
1277
+ }, opts));
1278
+ const microphone = simulator
1279
+ .command('microphone')
1280
+ .alias('mic')
1281
+ .description(`Inject deterministic prerecorded audio into ${article} ${label} app microphone`);
1282
+ microphone
1283
+ .command('inject')
1284
+ .description('Upload supported audio and deliver its decoded samples to the app microphone')
1285
+ .argument('<id>', 'session id')
1286
+ .argument('<audio>', 'AAC, M4A, MP3, MP4 audio, or WAV input path')
1287
+ .option('--bundle-id <id>', 'target app bundle ID or Android package')
1288
+ .option('-n, --name <name>', 'retained media asset name')
1289
+ .option('--json', 'output JSON', false)
1290
+ .action((id, audio, opts) => action(async () => {
1291
+ print(await injectSimulatorMedia(platform, 'microphone', id, audio, opts), opts);
1292
+ }, opts));
1207
1293
  simulator
1208
1294
  .command('logs')
1209
1295
  .description(`Read or follow logs from ${article} active ${label} session`)
package/dist/config.js CHANGED
@@ -139,6 +139,9 @@ export function readProjectLink(dir) {
139
139
  export function resolveProject(dir, explicit) {
140
140
  if (explicit)
141
141
  return explicit;
142
+ const managedSessionProjectId = process.env['NEWLY_PROJECT_ID'];
143
+ if (managedSessionProjectId)
144
+ return managedSessionProjectId;
142
145
  const link = readProjectLink(dir);
143
146
  if (link?.projectId)
144
147
  return link.projectId;
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { registerLogin } from './commands/login.js';
4
4
  import { registerStatus } from './commands/status.js';
5
5
  import { registerLink } from './commands/link.js';
6
6
  import { registerCreate } from './commands/create.js';
7
+ import { registerInit } from './commands/init.js';
7
8
  import { registerPull } from './commands/pull.js';
8
9
  import { registerDeploy } from './commands/deploy.js';
9
10
  import { registerMisc } from './commands/misc.js';
@@ -29,6 +30,7 @@ registerLogin(program);
29
30
  registerStatus(program);
30
31
  registerLink(program);
31
32
  registerCreate(program);
33
+ registerInit(program);
32
34
  registerPull(program);
33
35
  registerDeploy(program);
34
36
  registerMisc(program);
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const CLI_VERSION = '0.1.124';
1
+ export const CLI_VERSION = '0.1.126';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newly/cli",
3
- "version": "0.1.124",
3
+ "version": "0.1.126",
4
4
  "description": "Develop, provision, and deploy Newly backends from your terminal",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",