runcloud 0.1.125 → 0.1.127
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 +13 -2
- package/dist/commands/run-cloud.js +90 -4
- package/dist/commands/sandbox.js +3 -3
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/runcloud-0.1.127.tgz +0 -0
- package/skills/run-cloud-ios-simulator/SKILL.md +18 -4
- package/runcloud-0.1.125.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
|
|
@@ -287,8 +298,8 @@ runcloud image refresh python:3.12-slim # re-pin the tag to its current u
|
|
|
287
298
|
runcloud sandbox create --image python:3.12-slim
|
|
288
299
|
```
|
|
289
300
|
|
|
290
|
-
Built in: `runcloud/agent-base` (the default
|
|
291
|
-
`runcloud/
|
|
301
|
+
Built in: `runcloud/agent-base` (the default, including with `--expose`),
|
|
302
|
+
`runcloud/desktop`, and `newly/postgres`.
|
|
292
303
|
|
|
293
304
|
## Demos
|
|
294
305
|
|
|
@@ -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/commands/sandbox.js
CHANGED
|
@@ -314,7 +314,7 @@ export function registerSandbox(program) {
|
|
|
314
314
|
withOutput(sandbox
|
|
315
315
|
.command('create')
|
|
316
316
|
.description('Spawn a microVM sandbox')
|
|
317
|
-
.option('--image <ref>', 'base image (default runcloud/agent-base
|
|
317
|
+
.option('--image <ref>', 'base image (default runcloud/agent-base)')
|
|
318
318
|
.option('--region <region>', 'placement region')
|
|
319
319
|
.option('--name <name>', 'human-readable name; with --expose, the hostname prefix <name>-box.run.cloud')
|
|
320
320
|
.option('--org <id>', 'billing and ownership organization')
|
|
@@ -339,7 +339,7 @@ export function registerSandbox(program) {
|
|
|
339
339
|
throw new Error('--persistent and --expose keep the sandbox running; drop the non-zero --idle-pause');
|
|
340
340
|
}
|
|
341
341
|
const body = {
|
|
342
|
-
image: opts.image ??
|
|
342
|
+
image: opts.image ?? 'runcloud/agent-base',
|
|
343
343
|
};
|
|
344
344
|
if (opts.name)
|
|
345
345
|
body.name = opts.name;
|
|
@@ -810,7 +810,7 @@ export function registerSandbox(program) {
|
|
|
810
810
|
.argument('<name>', 'hostname prefix: <name>-box.run.cloud')
|
|
811
811
|
.option('--sandbox <id>', 'attach an existing sandbox created with --idle-pause 0')
|
|
812
812
|
.option('--port <port>', 'guest port to publish', parsePort, 3000)
|
|
813
|
-
.option('--image <ref>', 'base image for a new sandbox
|
|
813
|
+
.option('--image <ref>', 'base image for a new sandbox', 'runcloud/agent-base')
|
|
814
814
|
.option('--region <region>', 'placement region for a new sandbox')
|
|
815
815
|
.option('--org <id>', 'billing and ownership organization for a new sandbox')
|
|
816
816
|
.option('--cpu <cores>', 'vCPU allocation for a new box', parseCpuCores, DEFAULT_BOX_CPU_CORES)
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = '0.1.
|
|
1
|
+
export const CLI_VERSION = '0.1.127';
|
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.125.tgz
DELETED
|
Binary file
|