runcloud 0.1.107 → 0.1.109

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.
@@ -71,9 +71,14 @@ function escapeHtml(value) {
71
71
  .replaceAll('>', '>');
72
72
  }
73
73
 
74
+ function scriptData(value) {
75
+ return JSON.stringify(value).replaceAll('<', '\\u003c');
76
+ }
77
+
74
78
  function embedUrl(value) {
75
79
  const url = new URL(value);
76
80
  url.searchParams.set('embed', '1');
81
+ url.searchParams.set('loadingGuard', '1');
77
82
  return url.toString();
78
83
  }
79
84
 
@@ -130,6 +135,7 @@ export function mosaicViewerHtml(sessions, deadline = Date.now() + DEMO_LEASE_MS
130
135
  const deadline = ${deadline};
131
136
  const tiles = [...document.querySelectorAll('.tile')];
132
137
  const frames = [...document.querySelectorAll('iframe')];
138
+ const frameOrigins = ${scriptData(sessions.map((session) => new URL(session.url).origin))};
133
139
  const ready = new Set();
134
140
  const count = document.querySelector('#ready-count');
135
141
  const timer = document.querySelector('#timer');
@@ -141,7 +147,12 @@ export function mosaicViewerHtml(sessions, deadline = Date.now() + DEMO_LEASE_MS
141
147
  setInterval(tick, 250);
142
148
  window.addEventListener('message', (event) => {
143
149
  const index = frames.findIndex((frame) => frame.contentWindow === event.source);
144
- if (index < 0 || !event.data || typeof event.data !== 'object') return;
150
+ if (
151
+ index < 0
152
+ || event.origin !== frameOrigins[index]
153
+ || !event.data
154
+ || typeof event.data !== 'object'
155
+ ) return;
145
156
  if (event.data.type === 'ios-simulator:status' && event.data.streaming === true) {
146
157
  ready.add(index);
147
158
  tiles[index].dataset.ready = 'true';
@@ -35,8 +35,11 @@ test('renders eight signed embeds without printing them into the viewer URL', as
35
35
  }));
36
36
  const html = mosaicViewerHtml(sessions, 601_000);
37
37
  assert.equal((html.match(/<iframe /g) || []).length, 8);
38
- assert.match(html, /embed=1/);
39
- assert.doesNotMatch(html, /loadingGuard=1/);
38
+ assert.equal((html.match(/embed=1/g) || []).length, 8);
39
+ assert.equal((html.match(/loadingGuard=1/g) || []).length, 8);
40
+ assert.match(html, /const frameOrigins = \["https:\/\/sim\.example"/);
41
+ assert.match(html, /frame\.contentWindow === event\.source/);
42
+ assert.match(html, /event\.origin !== frameOrigins\[index\]/);
40
43
  assert.match(html, /const deadline = 601000/);
41
44
 
42
45
  const viewer = await startMosaicViewer(sessions, 601_000);
@@ -87,6 +87,7 @@ function scriptData(value) {
87
87
  function embedUrl(value) {
88
88
  const url = new URL(value);
89
89
  url.searchParams.set('embed', '1');
90
+ url.searchParams.set('loadingGuard', '1');
90
91
  return url.toString();
91
92
  }
92
93
 
@@ -193,6 +194,7 @@ export function cameraViewerHtml(sessions, duration, roomId, now = Date.now()) {
193
194
  const deadline = ${deadline};
194
195
  const devices = [...document.querySelectorAll('.device')];
195
196
  const frames = [...document.querySelectorAll('iframe')];
197
+ const frameOrigins = ${scriptData(sessions.map((session) => new URL(session.url).origin))};
196
198
  const ready = new Set();
197
199
  const count = document.querySelector('#ready-count');
198
200
  const timer = document.querySelector('#timer');
@@ -287,7 +289,12 @@ export function cameraViewerHtml(sessions, duration, roomId, now = Date.now()) {
287
289
  setInterval(tick, 250);
288
290
  window.addEventListener('message', (event) => {
289
291
  const index = frames.findIndex((frame) => frame.contentWindow === event.source);
290
- if (index < 0 || !event.data || typeof event.data !== 'object') return;
292
+ if (
293
+ index < 0
294
+ || event.origin !== frameOrigins[index]
295
+ || !event.data
296
+ || typeof event.data !== 'object'
297
+ ) return;
291
298
  if (event.data.type === 'ios-simulator:status' && event.data.streaming === true) {
292
299
  ready.add(index);
293
300
  devices[index].dataset.ready = 'true';
@@ -67,8 +67,11 @@ test('renders three signed embeds while keeping credentials out of the local URL
67
67
  const demoSessions = sessions();
68
68
  const html = cameraViewerHtml(demoSessions, 60, 'room-12345678', 1_000);
69
69
  assert.equal((html.match(/<iframe /g) || []).length, 3);
70
- assert.match(html, /embed=1/);
71
- assert.doesNotMatch(html, /loadingGuard=1/);
70
+ assert.equal((html.match(/embed=1/g) || []).length, 3);
71
+ assert.equal((html.match(/loadingGuard=1/g) || []).length, 3);
72
+ assert.match(html, /const frameOrigins = \["https:\/\/sim\.example"/);
73
+ assert.match(html, /frame\.contentWindow === event\.source/);
74
+ assert.match(html, /event\.origin !== frameOrigins\[index\]/);
72
75
  assert.match(html, /Connect webcam/);
73
76
  assert.match(html, /com\.apple\.mobilesafari/);
74
77
  assert.match(html, /audio: false/);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runcloud",
3
- "version": "0.1.107",
3
+ "version": "0.1.109",
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
@@ -1,44 +1,32 @@
1
1
  ---
2
2
  name: run-cloud-ios-simulator
3
- description: Operate run.cloud iOS simulator and Android emulator sessions with the CLI or TypeScript SDK. Use for creating, installing, inspecting, reading device logs, embedding, smoke-testing, connecting local Metro, capturing iOS screenshots, injecting iOS media, or releasing remote mobile sessions.
3
+ description: Operate run.cloud iOS simulator and Android emulator sessions with the CLI or TypeScript SDK. Use for creating, installing, inspecting, reading logs, controlling, embedding, smoke-testing, connecting local Metro, taking screenshots, injecting iOS media, or releasing remote mobile sessions.
4
4
  ---
5
5
 
6
6
  # Operate run.cloud Mobile Sessions
7
7
 
8
- Use run.cloud through the `runcloud` CLI for terminal workflows and
9
- `@run-cloud/sdk` for application, CI, or agent code.
8
+ Use the `runcloud` CLI for terminal workflows, `@run-cloud/sdk` for TypeScript,
9
+ and `@run-cloud/ui` for React embeds.
10
10
 
11
11
  ## Authenticate
12
12
 
13
13
  - Install the CLI with `npm install -g runcloud`.
14
- - Use `runcloud login` for an interactive browser handoff. Use
15
- `runcloud login --manual` when a local callback cannot open.
14
+ - Use `runcloud login`; add `--manual` when a local callback cannot open.
16
15
  - In CI, set `RUN_CLOUD_API_KEY`. `RUN_CLOUD_API_TOKEN` is an equivalent alias.
17
- - Set `RUN_CLOUD_API_URL` only to override the production default
18
- `https://api.run.cloud`.
19
- - Never print, commit, or place credentials in a skill file.
20
- - Treat signed simulator URLs and tunnel URLs as bearer secrets.
21
- - Require Node.js 20 or newer for the CLI and TypeScript SDK.
16
+ - Set `RUN_CLOUD_API_URL` only to override `https://api.run.cloud`.
17
+ - Require Node.js 20 or newer.
18
+ - Never print, commit, or place credentials in a skill file. Treat signed session
19
+ and tunnel URLs as bearer secrets.
22
20
 
23
- Inspect account and organization credit before starting metered work:
21
+ Check access and credit before starting metered work:
24
22
 
25
23
  ```bash
26
24
  runcloud account --json
27
25
  ```
28
26
 
29
- Sessions require product access, organization credit below its ceiling, and
30
- available fleet capacity. A create request may queue while capacity is full.
27
+ The SDK equivalents are `cloud.account()` and `cloud.usage({ orgId? })`.
31
28
 
32
- ## Choose the Interface
33
-
34
- - Prefer CLI commands with `--json` for shell automation.
35
- - Prefer `@run-cloud/sdk` for TypeScript applications and CI.
36
- - Inspect `runcloud <command> --help` or installed SDK types before using a
37
- method not documented here.
38
-
39
- ## Use the CLI
40
-
41
- Create an iOS session, capture its ID, open a deep link, and release it:
29
+ ## Create and Release Sessions
42
30
 
43
31
  ```bash
44
32
  SESSION_ID=$(runcloud ios create \
@@ -48,79 +36,51 @@ SESSION_ID=$(runcloud ios create \
48
36
  --json | jq -r '.id')
49
37
 
50
38
  trap 'runcloud ios delete "$SESSION_ID" >/dev/null 2>&1 || true' EXIT
51
-
52
39
  runcloud ios get "$SESSION_ID" --json
53
- runcloud ios open-url myapp://settings --id "$SESSION_ID" --json
54
40
  ```
55
41
 
56
- Use the corresponding `runcloud android` commands for Android artifacts.
57
-
58
- The shared mobile lifecycle is:
59
-
60
- - `runcloud ios|android create`
61
- - `runcloud ios|android list [--all]`
62
- - `runcloud ios|android get <id>`
63
- - `runcloud ios|android open-url <url> --id <id>`
64
- - `runcloud ios|android logs <id> [--tail N|--follow]`
65
- - `runcloud ios|android delete <id>`
66
-
67
- Create accepts `--model`, `--region`, `--display-name`, repeatable `--label`,
68
- repeatable `--install`, repeatable `--install-asset`,
69
- `--inactivity-timeout`, `--hard-timeout`, `--codec`, `--rm`, and `--json`.
70
-
71
- Use assets and samples when no local artifact is ready:
72
-
73
- ```bash
74
- runcloud sample download ios
75
- runcloud ios create --install ./run-cloud-sample-ios.app.tar.gz --json
76
-
77
- runcloud asset push ./build/MyApp.tar.gz --name my-app --json
78
- runcloud ios create --install-asset my-app --json
79
- runcloud asset list --json
80
- runcloud asset pull <asset-id> --output ./MyApp.tar.gz
81
- runcloud asset delete <asset-id> --json
82
- ```
42
+ Replace `ios` with `android` for an Android artifact. The shared lifecycle is
43
+ `create`, `list`, `get`, `open-url`, `logs`, and `delete`. Use `--json` whenever
44
+ another program consumes output, and inspect `runcloud ios|android --help` for
45
+ create and log options.
83
46
 
84
47
  iOS needs an Apple Silicon simulator-compatible `.app`, `.zip`, `.tar.gz`, or
85
- `.ipa` artifact. A device-signed App Store IPA is not a substitute. Android
86
- needs an emulator-compatible artifact such as an APK.
48
+ `.ipa`; a device-signed App Store IPA is not a substitute. Android needs an
49
+ emulator-compatible APK.
87
50
 
88
- ## Diagnose App Failures
51
+ ## Control a Session
89
52
 
90
- Read up to 1,000 retained entries from the current lease:
53
+ Both platform groups expose acknowledged controls:
91
54
 
92
55
  ```bash
93
- runcloud ios logs "$SESSION_ID" --tail 1000
94
- runcloud android logs "$SESSION_ID" --tail 1000
56
+ runcloud ios tap "$SESSION_ID" 0.5 0.3 --json
57
+ runcloud ios swipe "$SESSION_ID" 0.5 0.8 0.5 0.2 --duration 300 --json
58
+ runcloud ios type-text "$SESSION_ID" 'hello' --json
59
+ runcloud ios press-key "$SESSION_ID" enter --json
60
+ runcloud ios press-button "$SESSION_ID" home --json
61
+ runcloud ios screenshot "$SESSION_ID" --output ios.png --json
95
62
  ```
96
63
 
97
- Use `--follow` while reproducing an issue, and add `--json` when another tool
98
- will consume the entries. `--tail` and `--follow` are mutually exclusive.
99
- Before releasing a failed session, always capture a bounded snapshot and keep
100
- the relevant entries with the test evidence. A follow stream contains only new
101
- entries and is not a substitute for the retained snapshot.
64
+ The full control set is `tap`, `swipe`, `gesture`, `type-text`, `press-key`,
65
+ `press-button`, `rotate`, `reload`, `scroll`, `toggle-software-keyboard`,
66
+ `simulate-memory-warning`, `rotate-digital-crown`, `set-render-debug`, and
67
+ `screenshot`. Every interaction accepts `--request-id`, `--timeout`, and
68
+ `--json`; use `runcloud <platform> <control> --help` for its typed arguments.
102
69
 
103
- ## Connect Local Development
70
+ Coordinates use the current display orientation: `(0, 0)` is top-left and
71
+ `(1, 1)` is bottom-right. Gesture steps use one or two points with `begin`,
72
+ `move`, and `end` phases. Each `delayMs` is the pause before the next step, so
73
+ the final `end` step must use `0`. Key names are semantic US-keyboard names;
74
+ modifiers are `shift`, `control`, `alt`, and `meta`. Current mobile sessions do
75
+ not support Digital Crown input, and Android does not support iOS render-debug
76
+ controls or the `capsLock`, `numLock`, and `scrollLock` keys. Handle structured
77
+ `unsupported_action` errors instead of retrying them.
104
78
 
105
- Connect a local Metro or mock server to an active iOS session:
106
-
107
- ```bash
108
- runcloud ios tunnel "$SESSION_ID" \
109
- --local-port 8081 \
110
- --service metro \
111
- --json
112
-
113
- runcloud ios tunnel-status --json
114
- ```
115
-
116
- Use the run.cloud sidecar flow. Do not install or expose an unauthenticated
117
- third-party tunnel. If the sidecar is unavailable, report that requirement
118
- instead of guessing a public URL.
79
+ An acknowledgement means input dispatch completed. Confirm visible app effects
80
+ with a screenshot or the signed viewer when the outcome matters.
119
81
 
120
82
  ## Use the TypeScript SDK
121
83
 
122
- Install the SDK:
123
-
124
84
  ```bash
125
85
  npm install @run-cloud/sdk
126
86
  ```
@@ -132,81 +92,96 @@ import { writeFile } from "node:fs/promises";
132
92
  import { Client } from "@run-cloud/sdk";
133
93
 
134
94
  const cloud = new Client();
135
- const session = await cloud.ios.create({
136
- displayName: "Agent smoke",
137
- labels: { owner: "agent" },
95
+ const session = await cloud.android.create({
138
96
  inactivityTimeout: "60s",
139
97
  hardTimeout: "10m",
140
- codec: "auto",
98
+ tags: { owner: "agent" },
141
99
  });
142
100
 
143
101
  try {
144
- await cloud.ios.openUrl(session.id, "https://run.cloud");
145
- const screenshot = await cloud.ios.screenshot(session.id);
146
- await writeFile("run-cloud.png", screenshot);
102
+ await cloud.android.tap(session.id, { x: 0.5, y: 0.3 });
103
+ await cloud.android.typeText(session.id, "hello");
104
+ await cloud.android.pressKey(session.id, "enter");
105
+ const screenshot = await cloud.android.screenshot(session.id);
106
+ await writeFile("android.png", screenshot);
147
107
  } finally {
148
- await cloud.ios.delete(session.id);
108
+ await cloud.android.delete(session.id);
149
109
  }
150
110
  ```
151
111
 
152
- The mobile SDK surface is:
112
+ `cloud.ios`, `cloud.android`, and the platform-selectable `cloud.simulators`
113
+ expose `interact` plus convenience methods matching every CLI control above.
114
+ Interaction options accept `requestId`, `timeoutMs`, and `signal`; results are
115
+ typed acknowledgements. Use `RunCloudError` fields such as `code`, `retryable`,
116
+ `requestId`, and `action` when reporting API failures.
153
117
 
154
- - `cloud.account()` and `cloud.usage({ orgId? })`
155
- - `cloud.ios`: `create`, `list`, `get`, `openUrl`, `logs`, `followLogs`, `screenshot`,
156
- `uploadVideo`, `uploadMicrophoneAudio`, `delete`
157
- - `cloud.android`: `create`, `list`, `get`, `openUrl`, `logs`, `followLogs`, `delete`
158
- - `cloud.simulators`: runtime-platform `create`, `list`, `get`, `openUrl`,
159
- `delete`
160
- - `cloud.assets`: `upload`, `list`, `delete`
118
+ The lifecycle surface also includes `create`, `list`, `get`, `openUrl`, `logs`,
119
+ `followLogs`, and `delete`. Both platforms expose `screenshot`; iOS additionally
120
+ supports `uploadVideo` and `uploadMicrophoneAudio`. Inspect the installed types
121
+ for complete create, asset, log, and media options.
161
122
 
162
- Create options include `model`, `region`, `displayName`, `labels`,
163
- `installAssets`, `inactivityTimeout`, `hardTimeout`, and `codec`.
123
+ In compact form, `cloud.ios`: `create`, `list`, `get`, `openUrl`, `logs`, `followLogs`, `screenshot`.
124
+ `cloud.android` provides the same shared lifecycle and control operations.
125
+
126
+ ## Diagnose App Failures
127
+
128
+ ```bash
129
+ runcloud ios logs "$SESSION_ID" --tail 1000
130
+ runcloud android logs "$SESSION_ID" --tail 1000
131
+ ```
164
132
 
165
- Do not invent SDK methods for scripted taps, typing, recording, app lifecycle,
166
- or Android screenshots. Browser-stream interaction and iframe commands are
167
- separate from the public SDK.
133
+ Use `--follow` while reproducing an issue. Before releasing a failed session,
134
+ capture a bounded retained snapshot; a follow stream contains only new entries.
168
135
 
169
- ## Inject iOS Media
136
+ ## Connect Local Development
170
137
 
171
- Use `cloud.ios.uploadVideo(id, video, options)` for MP4 or QuickTime video. It
172
- stores a user-owned asset and imports it into Photos.
138
+ Connect a local Metro or mock server through the supported sidecar flow:
173
139
 
174
- Use `cloud.ios.uploadMicrophoneAudio(id, audio, options)` for AAC, M4A, MP3,
175
- MP4-audio, or WAV. Pass an optional `bundleId`; otherwise it targets the
176
- foreground app. The operation relaunches the target app with microphone
177
- permission and loops the decoded audio through `AVAudioEngine`.
140
+ ```bash
141
+ runcloud ios tunnel "$SESSION_ID" --local-port 8081 --service metro --json
142
+ runcloud ios tunnel-status --json
143
+ ```
178
144
 
179
- Delete uploaded assets when they are no longer needed.
145
+ Do not expose an unauthenticated third-party tunnel. If the sidecar is
146
+ unavailable, report that requirement instead of guessing a public URL.
180
147
 
181
148
  ## Embed a Session
182
149
 
183
- - Add `embed=1` to the signed session URL for the clean iframe UI.
184
- - Add `loadingGuard=1` when the iframe should block interaction until streaming
185
- and app launch are ready.
186
- - Verify `event.source` is the expected iframe before processing messages.
187
- - Handle `ios-simulator:status`, `ios-simulator:auth-error`,
188
- `ios-simulator:session-ended`, and
189
- `ios-simulator:session-restart-requested`.
190
- - Create a new session after a restart request; never reuse an ended URL.
191
- - Use `ios-simulator:command` for `reload`, `home`, `rotate`, `screenshot`, and
192
- `toggleAccessibility`.
193
-
194
- ## Run Maintained Demos
150
+ - Use `RemoteControl` from `@run-cloud/ui` with the signed session URL.
151
+ - Its ref exposes promise-based `interact` and convenience methods matching the
152
+ SDK controls. Handle `onInteractionResult` for inspectable acknowledgements.
153
+ - Add `embed=1` to raw iframe URLs. Add `loadingGuard=1` when interaction should
154
+ wait for streaming and app readiness.
155
+ - Raw iframe requests use `run-cloud:interaction`; acknowledgements use
156
+ `run-cloud:interaction-result`. Correlate them by `requestId`. To stop a
157
+ pending request, post `run-cloud:interaction-cancel` with the same
158
+ `requestId` and `action`.
159
+ - Verify `event.source` and the exact signed-URL origin, and use that origin as
160
+ the `postMessage` target.
161
+ - Legacy `ios-simulator:command` messages remain compatibility-only. Prefer the
162
+ generic acknowledged interaction channel for new code.
163
+ - Create a new session after an `ios-simulator:session-restart-requested`
164
+ message; never reuse an ended URL.
165
+
166
+ ## Assets, Samples, and Demos
195
167
 
196
168
  ```bash
169
+ runcloud sample download ios
170
+ runcloud asset push ./build/MyApp.tar.gz --name my-app --json
171
+ runcloud asset pull <asset-id>
172
+ runcloud ios create --install-asset my-app --json
197
173
  runcloud demo run eight-device-mosaic --open
198
174
  runcloud demo run live-camera-relay --open
199
175
  ```
200
176
 
201
- The bundled demos release their sessions automatically.
177
+ Delete uploaded assets when no longer needed. Bundled demos release their
178
+ sessions automatically.
202
179
 
203
180
  ## Guardrails
204
181
 
205
- - Release every session created during a task unless the user explicitly asks
206
- to keep it open.
182
+ - Release every session created during a task unless asked to keep it open.
207
183
  - Use inactivity and hard timeouts for unattended work.
208
- - Verify platform compatibility before changing application code after an
184
+ - Verify artifact/platform compatibility before changing app code after an
209
185
  install failure.
210
186
  - Do not expose credentials, signed viewer URLs, tunnel URLs, or simulator
211
187
  tokens in logs, screenshots, PR comments, or chat output.
212
- - Do not claim that browser iframe controls are public SDK methods.