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 +11 -0
- package/dist/commands/create.js +9 -0
- package/dist/commands/init.js +31 -0
- package/dist/commands/run-cloud.js +90 -4
- package/dist/config.js +3 -0
- package/dist/index.js +2 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/runcloud-0.1.126.tgz +0 -0
- package/skills/run-cloud-ios-simulator/SKILL.md +18 -4
- package/runcloud-0.1.124.tgz +0 -0
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
|
package/dist/commands/create.js
CHANGED
|
@@ -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:
|
|
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.
|
|
1
|
+
export const CLI_VERSION = '0.1.126';
|
package/package.json
CHANGED
|
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
|
|
153
|
-
`
|
|
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
|
|
package/runcloud-0.1.124.tgz
DELETED
|
Binary file
|