runcloud 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.
package/README.md CHANGED
@@ -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": "runcloud",
3
- "version": "0.1.124",
3
+ "version": "0.1.126",
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
@@ -84,6 +84,20 @@ Android and returns a non-retryable `unsupported_action` on iOS.
84
84
  An acknowledgement means input dispatch completed. Confirm visible app effects
85
85
  with a screenshot or the signed viewer when the outcome matters.
86
86
 
87
+ ## Inject Deterministic Media
88
+
89
+ Both platforms accept prerecorded camera and microphone input:
90
+
91
+ ```bash
92
+ runcloud ios camera inject "$SESSION_ID" ./pattern.mp4 --bundle-id com.example.Camera --json
93
+ runcloud android microphone inject "$SESSION_ID" ./tone.wav --bundle-id com.example.Recorder --json
94
+ ```
95
+
96
+ `microphone` has the `mic` alias. Arm the native camera or microphone receiver
97
+ before injecting, then verify app-owned frame or sample state; the service
98
+ acknowledgement alone does not prove capture. Delete the returned media asset
99
+ with `runcloud asset delete <asset-id>` after the proof.
100
+
87
101
  ## Inspect Accessibility
88
102
 
89
103
  Use `runcloud ios|android accessibility-tree <id> --json` to read the current
@@ -149,13 +163,13 @@ The lifecycle surface also includes `create`, `list`, `get`, `openUrl`, `logs`,
149
163
  `followLogs`, and `delete`. Both platforms expose `screenshot`,
150
164
  `accessibilityTree`,
151
165
  `startRecording`, `listRecordings`, `getRecording`, `stopRecording`, and
152
- `downloadRecording`; iOS additionally supports `uploadVideo` and
153
- `uploadMicrophoneAudio`. Inspect the installed types for complete create,
154
- asset, log, recording, and media options.
166
+ `downloadRecording`, `uploadCameraVideo`, and `uploadMicrophoneAudio`; iOS
167
+ additionally supports `uploadVideo` for Photos imports. Inspect the installed
168
+ types for complete create, asset, log, recording, and media options.
155
169
 
156
170
  In compact form, `cloud.ios`: `create`, `list`, `get`, `openUrl`, `logs`,
157
171
  `followLogs`, `screenshot`, and the five recording methods above.
158
- `cloud.android` provides the same shared lifecycle and control operations.
172
+ `cloud.android` provides the same shared lifecycle, media, and control operations.
159
173
 
160
174
  ## Diagnose App Failures
161
175
 
Binary file