runcloud 0.1.116 → 0.1.118
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 +8 -0
- package/dist/api.js +2 -2
- package/dist/commands/run-cloud.js +175 -2
- package/dist/diagnose.js +29 -21
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/runcloud-0.1.118.tgz +0 -0
- package/skills/run-cloud-ios-simulator/SKILL.md +16 -4
- package/runcloud-0.1.116.tgz +0 -0
package/README.md
CHANGED
|
@@ -66,6 +66,7 @@ runcloud ios get <id> # inspect a session (
|
|
|
66
66
|
runcloud ios open-url myapp://path --id <id> # open a URL or deep link
|
|
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
|
+
runcloud ios accessibility-tree <id> --json # read the current typed accessibility hierarchy
|
|
69
70
|
runcloud ios screenshot <id> --output ios.png # save a PNG without exposing the viewer URL
|
|
70
71
|
runcloud ios recording start <id> --json # start an idempotent MP4 screen recording
|
|
71
72
|
runcloud ios recording stop <id> <recording-id> --json # stop and finalize it
|
|
@@ -79,6 +80,7 @@ runcloud ios delete <id> # release the session
|
|
|
79
80
|
runcloud android create --model pixel
|
|
80
81
|
runcloud android swipe <id> 0.5 0.8 0.5 0.2 --duration 250
|
|
81
82
|
runcloud android press-key <id> enter
|
|
83
|
+
runcloud android accessibility-tree <id> --json
|
|
82
84
|
runcloud android screenshot <id> --output android.png --json
|
|
83
85
|
```
|
|
84
86
|
|
|
@@ -117,6 +119,12 @@ report `capsLock`, `numLock`, and `scrollLock` as unsupported keys.
|
|
|
117
119
|
Coordinates are inclusive normalized display coordinates: `(0, 0)` is the
|
|
118
120
|
top-left and `(1, 1)` is the bottom-right.
|
|
119
121
|
|
|
122
|
+
`accessibility-tree` (`accessibility` alias) returns an indented terminal tree
|
|
123
|
+
or a versioned JSON hierarchy with cross-platform roles, labels, values,
|
|
124
|
+
states, bounds, identifiers, and nested children. Secure field values are
|
|
125
|
+
always redacted. The command reads only the authenticated active session and
|
|
126
|
+
never releases it; use `--timeout` to change the 20-second client wait.
|
|
127
|
+
|
|
120
128
|
The `recording` group is shared by iOS and Android. It provides `start`,
|
|
121
129
|
`list`, `status` (`get` alias), `stop`, and `download`. Pass a stable
|
|
122
130
|
`--idempotency-key` to retry `start` without creating a second recording.
|
package/dist/api.js
CHANGED
|
@@ -116,8 +116,8 @@ export class ApiClient {
|
|
|
116
116
|
const contentType = res.headers.get('content-type') ?? '';
|
|
117
117
|
return contentType.includes('application/json') ? res.json() : res;
|
|
118
118
|
}
|
|
119
|
-
get(path) {
|
|
120
|
-
return this.request('GET', path);
|
|
119
|
+
get(path, options = {}) {
|
|
120
|
+
return this.request('GET', path, undefined, options);
|
|
121
121
|
}
|
|
122
122
|
async getText(path) {
|
|
123
123
|
const response = await this.request('GET', path);
|
|
@@ -53,8 +53,19 @@ export const SIMULATOR_SCREENSHOT_ERROR_CODES = [
|
|
|
53
53
|
'screenshot_invalid_response',
|
|
54
54
|
'screenshot_failed',
|
|
55
55
|
];
|
|
56
|
+
export const SIMULATOR_ACCESSIBILITY_ERROR_CODES = [
|
|
57
|
+
'active_session_not_found',
|
|
58
|
+
'simulator_capacity_unavailable',
|
|
59
|
+
'simulator_session_ended',
|
|
60
|
+
'accessibility_unavailable',
|
|
61
|
+
'accessibility_cancelled',
|
|
62
|
+
'accessibility_timeout',
|
|
63
|
+
'accessibility_transport_error',
|
|
64
|
+
'accessibility_invalid_response',
|
|
65
|
+
];
|
|
56
66
|
const simulatorInteractionErrorCodeSet = new Set(SIMULATOR_INTERACTION_ERROR_CODES);
|
|
57
67
|
const simulatorScreenshotErrorCodeSet = new Set(SIMULATOR_SCREENSHOT_ERROR_CODES);
|
|
68
|
+
const simulatorAccessibilityErrorCodeSet = new Set(SIMULATOR_ACCESSIBILITY_ERROR_CODES);
|
|
58
69
|
function normalizedInteractionErrorCode(value) {
|
|
59
70
|
if (typeof value === 'string' && simulatorInteractionErrorCodeSet.has(value)) {
|
|
60
71
|
return value;
|
|
@@ -85,7 +96,13 @@ function normalizedScreenshotErrorCode(value) {
|
|
|
85
96
|
? value
|
|
86
97
|
: 'screenshot_failed';
|
|
87
98
|
}
|
|
99
|
+
function normalizedAccessibilityErrorCode(value) {
|
|
100
|
+
return typeof value === 'string' && simulatorAccessibilityErrorCodeSet.has(value)
|
|
101
|
+
? value
|
|
102
|
+
: 'accessibility_unavailable';
|
|
103
|
+
}
|
|
88
104
|
const DEFAULT_INTERACTION_TIMEOUT_MS = 15_000;
|
|
105
|
+
const DEFAULT_ACCESSIBILITY_TIMEOUT_MS = 20_000;
|
|
89
106
|
const INTERACTION_RESPONSE_MARGIN_MS = 250;
|
|
90
107
|
const REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
91
108
|
export const RUN_CLOUD_SKILL_NAMES = [
|
|
@@ -392,10 +409,21 @@ class CliInteractionError extends Error {
|
|
|
392
409
|
}
|
|
393
410
|
}
|
|
394
411
|
async function withInteractionCancellation(timeoutMs, requestId, actionName, sessionId, platform, operation, run) {
|
|
412
|
+
const operationLabel = operation === 'interaction' ? 'interaction' : operation;
|
|
413
|
+
const cancelledCode = operation === 'screenshot'
|
|
414
|
+
? 'screenshot_cancelled'
|
|
415
|
+
: operation === 'accessibility'
|
|
416
|
+
? 'accessibility_cancelled'
|
|
417
|
+
: 'interaction_cancelled';
|
|
418
|
+
const timeoutCode = operation === 'screenshot'
|
|
419
|
+
? 'screenshot_timeout'
|
|
420
|
+
: operation === 'accessibility'
|
|
421
|
+
? 'accessibility_timeout'
|
|
422
|
+
: 'interaction_timeout';
|
|
395
423
|
const controller = new AbortController();
|
|
396
|
-
const onSigint = () => controller.abort(new CliInteractionError(`Simulator ${
|
|
424
|
+
const onSigint = () => controller.abort(new CliInteractionError(`Simulator ${operationLabel} cancelled by SIGINT`, cancelledCode, requestId, actionName, sessionId, platform));
|
|
397
425
|
process.once('SIGINT', onSigint);
|
|
398
|
-
const timer = setTimeout(() => controller.abort(new CliInteractionError(`Simulator ${actionName} did not complete within ${timeoutMs} ms`,
|
|
426
|
+
const timer = setTimeout(() => controller.abort(new CliInteractionError(`Simulator ${actionName} did not complete within ${timeoutMs} ms`, timeoutCode, requestId, actionName, sessionId, platform)), timeoutMs + INTERACTION_RESPONSE_MARGIN_MS);
|
|
399
427
|
try {
|
|
400
428
|
return await run(controller.signal);
|
|
401
429
|
}
|
|
@@ -485,6 +513,143 @@ async function captureSimulatorScreenshot(platform, id, opts) {
|
|
|
485
513
|
else
|
|
486
514
|
console.log(`Saved ${platform} simulator screenshot to ${output} (${bytes.byteLength} bytes)`);
|
|
487
515
|
}
|
|
516
|
+
function validatedAccessibilitySnapshot(value, platform) {
|
|
517
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
518
|
+
throw new Error('accessibility response must be an object');
|
|
519
|
+
}
|
|
520
|
+
const snapshot = value;
|
|
521
|
+
if (snapshot.schemaVersion !== 1 || snapshot.platform !== platform) {
|
|
522
|
+
throw new Error('accessibility schema or platform did not match the request');
|
|
523
|
+
}
|
|
524
|
+
if (!Array.isArray(snapshot.roots))
|
|
525
|
+
throw new Error('accessibility roots must be an array');
|
|
526
|
+
if (typeof snapshot.nodeCount !== 'number'
|
|
527
|
+
|| !Number.isInteger(snapshot.nodeCount)
|
|
528
|
+
|| snapshot.nodeCount < 0
|
|
529
|
+
|| snapshot.nodeCount > 500) {
|
|
530
|
+
throw new Error('accessibility nodeCount must be an integer from 0 to 500');
|
|
531
|
+
}
|
|
532
|
+
if (!snapshot.screen || typeof snapshot.screen !== 'object' || Array.isArray(snapshot.screen)) {
|
|
533
|
+
throw new Error('accessibility screen must be an object');
|
|
534
|
+
}
|
|
535
|
+
const screen = snapshot.screen;
|
|
536
|
+
if (screen.unit !== (platform === 'ios' ? 'points' : 'pixels')) {
|
|
537
|
+
throw new Error('accessibility screen unit did not match the platform');
|
|
538
|
+
}
|
|
539
|
+
const ids = new Set();
|
|
540
|
+
let count = 0;
|
|
541
|
+
const visit = (candidate, path, depth) => {
|
|
542
|
+
if (depth > 80 || count >= 500 || !candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
|
|
543
|
+
throw new Error('accessibility tree exceeded its schema bounds');
|
|
544
|
+
}
|
|
545
|
+
const node = candidate;
|
|
546
|
+
if (typeof node.id !== 'string' || !node.id || ids.has(node.id)) {
|
|
547
|
+
throw new Error('accessibility node identifiers must be non-empty and unique');
|
|
548
|
+
}
|
|
549
|
+
if (node.path !== path || typeof node.role !== 'string' || !Array.isArray(node.children)) {
|
|
550
|
+
throw new Error('accessibility node structure is invalid');
|
|
551
|
+
}
|
|
552
|
+
const states = node.states && typeof node.states === 'object' && !Array.isArray(node.states)
|
|
553
|
+
? node.states
|
|
554
|
+
: null;
|
|
555
|
+
const native = node.native && typeof node.native === 'object' && !Array.isArray(node.native)
|
|
556
|
+
? node.native
|
|
557
|
+
: null;
|
|
558
|
+
if (!states || !native || native.platform !== platform) {
|
|
559
|
+
throw new Error('accessibility node state or platform detail is invalid');
|
|
560
|
+
}
|
|
561
|
+
const secure = states.secure === true
|
|
562
|
+
|| native.password === true
|
|
563
|
+
|| (platform === 'ios' && /(?:secure|password)/iu.test([
|
|
564
|
+
native.type,
|
|
565
|
+
native.roleDescription,
|
|
566
|
+
native.subrole,
|
|
567
|
+
].filter((part) => typeof part === 'string').join(' ')));
|
|
568
|
+
if (secure) {
|
|
569
|
+
states.secure = true;
|
|
570
|
+
node.value = null;
|
|
571
|
+
}
|
|
572
|
+
ids.add(node.id);
|
|
573
|
+
count += 1;
|
|
574
|
+
node.children.forEach((child, index) => visit(child, `${path}.${index}`, depth + 1));
|
|
575
|
+
};
|
|
576
|
+
snapshot.roots.forEach((root, index) => visit(root, String(index), 0));
|
|
577
|
+
if (count !== snapshot.nodeCount)
|
|
578
|
+
throw new Error('accessibility nodeCount did not match the tree');
|
|
579
|
+
return snapshot;
|
|
580
|
+
}
|
|
581
|
+
function printAccessibilitySnapshot(snapshot) {
|
|
582
|
+
const platform = String(snapshot.platform);
|
|
583
|
+
const screen = snapshot.screen;
|
|
584
|
+
console.log(`${platform} accessibility tree: ${String(snapshot.nodeCount)} nodes, `
|
|
585
|
+
+ `${String(screen.width)}x${String(screen.height)} ${String(screen.unit)}`
|
|
586
|
+
+ `${snapshot.truncated === true ? ' (truncated)' : ''}`);
|
|
587
|
+
const visit = (candidate, depth) => {
|
|
588
|
+
const node = candidate;
|
|
589
|
+
const states = node.states;
|
|
590
|
+
const bounds = node.bounds && typeof node.bounds === 'object'
|
|
591
|
+
? node.bounds
|
|
592
|
+
: null;
|
|
593
|
+
const stateLabels = [
|
|
594
|
+
states.enabled === false ? 'disabled' : null,
|
|
595
|
+
states.focused === true ? 'focused' : null,
|
|
596
|
+
states.selected === true ? 'selected' : null,
|
|
597
|
+
states.checked === true ? 'checked' : states.checked === 'mixed' ? 'mixed' : null,
|
|
598
|
+
states.expanded === true ? 'expanded' : states.expanded === false ? 'collapsed' : null,
|
|
599
|
+
states.secure === true ? 'secure' : null,
|
|
600
|
+
].filter(Boolean);
|
|
601
|
+
const label = typeof node.label === 'string' && node.label ? ` "${node.label}"` : '';
|
|
602
|
+
const identifier = typeof node.identifier === 'string' && node.identifier
|
|
603
|
+
? ` #${node.identifier}`
|
|
604
|
+
: '';
|
|
605
|
+
const value = typeof node.value === 'string' && node.value
|
|
606
|
+
? ` = ${JSON.stringify(node.value.slice(0, 120))}`
|
|
607
|
+
: '';
|
|
608
|
+
const frame = bounds
|
|
609
|
+
? ` @ ${String(bounds.x)},${String(bounds.y)} ${String(bounds.width)}x${String(bounds.height)}`
|
|
610
|
+
: '';
|
|
611
|
+
const state = stateLabels.length ? ` [${stateLabels.join(', ')}]` : '';
|
|
612
|
+
console.log(`${' '.repeat(depth)}${String(node.role)}${label}${identifier}${value}${state}${frame}`);
|
|
613
|
+
for (const child of node.children)
|
|
614
|
+
visit(child, depth + 1);
|
|
615
|
+
};
|
|
616
|
+
for (const root of snapshot.roots)
|
|
617
|
+
visit(root, 0);
|
|
618
|
+
}
|
|
619
|
+
async function captureSimulatorAccessibilityTree(platform, id, opts) {
|
|
620
|
+
const timeoutMs = integerInRange(opts.timeout, '--timeout', 100, 60_000, DEFAULT_ACCESSIBILITY_TIMEOUT_MS);
|
|
621
|
+
const requestId = `cli_${randomUUID()}`;
|
|
622
|
+
let value;
|
|
623
|
+
try {
|
|
624
|
+
value = await withInteractionCancellation(timeoutMs, requestId, 'accessibility', id, platform, 'accessibility', async (signal) => await client().get(`/run-cloud/${platform}/${encodeURIComponent(id)}/accessibility`, { signal }));
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
if (error instanceof ApiError) {
|
|
628
|
+
Object.assign(error, {
|
|
629
|
+
code: normalizedAccessibilityErrorCode(error.code),
|
|
630
|
+
requestId: error.requestId ?? requestId,
|
|
631
|
+
sessionId: error.sessionId ?? id,
|
|
632
|
+
platform: error.platform ?? platform,
|
|
633
|
+
action: error.action ?? 'accessibility',
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
if (error instanceof ApiTransportError) {
|
|
637
|
+
throw new CliInteractionError('Simulator accessibility could not reach the run.cloud API', 'accessibility_transport_error', requestId, 'accessibility', id, platform);
|
|
638
|
+
}
|
|
639
|
+
throw error;
|
|
640
|
+
}
|
|
641
|
+
let snapshot;
|
|
642
|
+
try {
|
|
643
|
+
snapshot = validatedAccessibilitySnapshot(value, platform);
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
throw new CliInteractionError('Simulator accessibility returned an invalid tree', 'accessibility_invalid_response', requestId, 'accessibility', id, platform);
|
|
647
|
+
}
|
|
648
|
+
if (opts.json)
|
|
649
|
+
console.log(JSON.stringify(snapshot, null, 2));
|
|
650
|
+
else
|
|
651
|
+
printAccessibilitySnapshot(snapshot);
|
|
652
|
+
}
|
|
488
653
|
function simulatorRecordingBase(platform, sessionId) {
|
|
489
654
|
return `/run-cloud/${platform}/${encodeURIComponent(sessionId)}/recordings`;
|
|
490
655
|
}
|
|
@@ -973,6 +1138,14 @@ function registerSimulatorCommands(program, platform) {
|
|
|
973
1138
|
option: renderDebugOption(rawOption),
|
|
974
1139
|
enabled: booleanArgument(rawEnabled, 'enabled'),
|
|
975
1140
|
}, opts), opts));
|
|
1141
|
+
simulator
|
|
1142
|
+
.command('accessibility-tree')
|
|
1143
|
+
.alias('accessibility')
|
|
1144
|
+
.description(`Read the current accessibility hierarchy from ${article} active ${label} session`)
|
|
1145
|
+
.argument('<id>', 'session id')
|
|
1146
|
+
.option('--timeout <milliseconds>', 'capture timeout in milliseconds (100-60000)', String(DEFAULT_ACCESSIBILITY_TIMEOUT_MS))
|
|
1147
|
+
.option('--json', 'output the stable typed tree as JSON', false)
|
|
1148
|
+
.action((id, opts) => action(() => captureSimulatorAccessibilityTree(platform, id, opts), opts));
|
|
976
1149
|
addInteractionOptions(simulator
|
|
977
1150
|
.command('screenshot')
|
|
978
1151
|
.description(`Capture the current display from ${article} ${label} session`)
|
package/dist/diagnose.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
|
-
import { GoogleAuth
|
|
3
|
+
import { GoogleAuth } from 'google-auth-library';
|
|
4
4
|
import { renderSessionDiagnosticTerminal, writeSessionDiagnosticBundle, } from './sessionDiagnostics.js';
|
|
5
5
|
const execFileAsync = promisify(execFile);
|
|
6
6
|
const AUTH_ATTEMPT_TIMEOUT_MS = 10_000;
|
|
@@ -87,10 +87,8 @@ function extractAuthorization(headers) {
|
|
|
87
87
|
const h = headers;
|
|
88
88
|
return h?.Authorization ?? h?.authorization;
|
|
89
89
|
}
|
|
90
|
-
async function gcloudIdentityToken(audience
|
|
91
|
-
const args = ['auth', 'print-identity-token',
|
|
92
|
-
if (serviceAccount)
|
|
93
|
-
args.push(`--impersonate-service-account=${serviceAccount}`);
|
|
90
|
+
async function gcloudIdentityToken(audience) {
|
|
91
|
+
const args = ['auth', 'print-identity-token', '--audiences=' + audience];
|
|
94
92
|
const { stdout } = await execFileAsync('gcloud', args, { timeout: 15_000, maxBuffer: 1024 * 1024 });
|
|
95
93
|
const token = stdout.trim();
|
|
96
94
|
if (!token)
|
|
@@ -123,22 +121,32 @@ function exactAudienceAuthorization(authorization, audience) {
|
|
|
123
121
|
}
|
|
124
122
|
return authorization;
|
|
125
123
|
}
|
|
126
|
-
async function
|
|
127
|
-
return withDeadline('service-account
|
|
124
|
+
async function serviceAccountIdentityToken(audience, serviceAccount) {
|
|
125
|
+
return withDeadline('service-account ID-token minting', async (signal) => {
|
|
128
126
|
const sourceClient = await new GoogleAuth({
|
|
129
127
|
projectId: AUTH_DISCOVERY_PROJECT_ID,
|
|
130
128
|
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
|
|
131
129
|
clientOptions: { transporterOptions: { signal, timeout: AUTH_ATTEMPT_TIMEOUT_MS } },
|
|
132
130
|
}).getClient();
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
131
|
+
const sourceToken = await sourceClient.getAccessToken();
|
|
132
|
+
const authorization = typeof sourceToken === 'string' ? sourceToken : sourceToken.token;
|
|
133
|
+
if (!authorization)
|
|
134
|
+
throw new Error('ADC returned an empty source access token');
|
|
135
|
+
const response = await fetch('https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/' +
|
|
136
|
+
encodeURIComponent(serviceAccount) + ':generateIdToken', {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: {
|
|
139
|
+
Authorization: 'Bearer ' + authorization,
|
|
140
|
+
'Content-Type': 'application/json',
|
|
141
|
+
},
|
|
142
|
+
body: JSON.stringify({ audience, includeEmail: true }),
|
|
143
|
+
signal,
|
|
139
144
|
});
|
|
140
|
-
const
|
|
141
|
-
|
|
145
|
+
const body = await response.json().catch(() => ({}));
|
|
146
|
+
if (!response.ok || typeof body.token !== 'string') {
|
|
147
|
+
throw new Error("generateIdToken failed (" + response.status + "): " + JSON.stringify(body.error ?? body));
|
|
148
|
+
}
|
|
149
|
+
return exactAudienceAuthorization('Bearer ' + body.token, audience);
|
|
142
150
|
});
|
|
143
151
|
}
|
|
144
152
|
export async function resolveOpsAuthHeader(env = process.env, requestedAudience, requestedServiceAccount) {
|
|
@@ -158,13 +166,13 @@ export async function resolveOpsAuthHeader(env = process.env, requestedAudience,
|
|
|
158
166
|
catch (error) {
|
|
159
167
|
adcError = error instanceof Error ? error.message : String(error);
|
|
160
168
|
}
|
|
161
|
-
let
|
|
169
|
+
let serviceAccountError = 'not configured';
|
|
162
170
|
if (serviceAccount && env.DIAGNOSE_DISABLE_IMPERSONATION !== '1') {
|
|
163
171
|
try {
|
|
164
|
-
return await
|
|
172
|
+
return await serviceAccountIdentityToken(audience, serviceAccount);
|
|
165
173
|
}
|
|
166
174
|
catch (error) {
|
|
167
|
-
|
|
175
|
+
serviceAccountError = error instanceof Error ? error.message : String(error);
|
|
168
176
|
}
|
|
169
177
|
}
|
|
170
178
|
const token = env.DIAGNOSE_OPS_TOKEN;
|
|
@@ -173,13 +181,13 @@ export async function resolveOpsAuthHeader(env = process.env, requestedAudience,
|
|
|
173
181
|
}
|
|
174
182
|
if (env.DIAGNOSE_DISABLE_GCLOUD !== '1') {
|
|
175
183
|
try {
|
|
176
|
-
return exactAudienceAuthorization(`Bearer ${await gcloudIdentityToken(audience
|
|
184
|
+
return exactAudienceAuthorization(`Bearer ${await gcloudIdentityToken(audience)}`, audience);
|
|
177
185
|
}
|
|
178
186
|
catch (error) {
|
|
179
187
|
const gcloudError = error instanceof Error ? error.message : String(error);
|
|
180
188
|
throw new Error(`No usable ops credential for audience ${audience}.\n` +
|
|
181
189
|
`ADC failed: ${truncate(adcError, 500)}\n` +
|
|
182
|
-
`Service-account
|
|
190
|
+
`Service-account ID-token minting failed: ${truncate(serviceAccountError, 500)}\n` +
|
|
183
191
|
`gcloud failed: ${truncate(gcloudError, 500)}\n\n` +
|
|
184
192
|
'Repair access by adding the Google identity to control-plane `operators`, applying Terraform, ' +
|
|
185
193
|
'and running `gcloud auth application-default login`, then rerun. ' +
|
|
@@ -188,7 +196,7 @@ export async function resolveOpsAuthHeader(env = process.env, requestedAudience,
|
|
|
188
196
|
}
|
|
189
197
|
throw new Error(`No ops credential available for audience ${audience}. ` +
|
|
190
198
|
`ADC failed: ${truncate(adcError, 300)}. ` +
|
|
191
|
-
`Service-account
|
|
199
|
+
`Service-account ID-token minting failed: ${truncate(serviceAccountError, 300)}. ` +
|
|
192
200
|
'Run `gcloud auth application-default login` after the operator Terraform has been applied, or export DIAGNOSE_OPS_TOKEN.');
|
|
193
201
|
}
|
|
194
202
|
function truncate(value, max = 200) {
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = '0.1.
|
|
1
|
+
export const CLI_VERSION = '0.1.118';
|
package/package.json
CHANGED
|
Binary file
|
|
@@ -1,12 +1,12 @@
|
|
|
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
|
|
3
|
+
description: Operate run.cloud iOS simulator and Android emulator sessions with the CLI or TypeScript SDK. Use for creating, installing, inspecting accessibility or logs, controlling, embedding, smoke-testing, connecting local Metro, taking screenshots or screen recordings, injecting iOS media, or releasing remote mobile sessions.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Operate run.cloud Mobile Sessions
|
|
7
7
|
|
|
8
8
|
Use the `runcloud` CLI for terminal workflows, `@run-cloud/sdk` for TypeScript,
|
|
9
|
-
and `@
|
|
9
|
+
and `@runcloud/ui` for React embeds.
|
|
10
10
|
|
|
11
11
|
## Authenticate
|
|
12
12
|
|
|
@@ -58,6 +58,7 @@ runcloud ios swipe "$SESSION_ID" 0.5 0.8 0.5 0.2 --duration 300 --json
|
|
|
58
58
|
runcloud ios type-text "$SESSION_ID" 'hello' --json
|
|
59
59
|
runcloud ios press-key "$SESSION_ID" enter --json
|
|
60
60
|
runcloud ios press-button "$SESSION_ID" home --json
|
|
61
|
+
runcloud ios accessibility-tree "$SESSION_ID" --json
|
|
61
62
|
runcloud ios screenshot "$SESSION_ID" --output ios.png --json
|
|
62
63
|
```
|
|
63
64
|
|
|
@@ -83,6 +84,15 @@ Android and returns a non-retryable `unsupported_action` on iOS.
|
|
|
83
84
|
An acknowledgement means input dispatch completed. Confirm visible app effects
|
|
84
85
|
with a screenshot or the signed viewer when the outcome matters.
|
|
85
86
|
|
|
87
|
+
## Inspect Accessibility
|
|
88
|
+
|
|
89
|
+
Use `runcloud ios|android accessibility-tree <id> --json` to read the current
|
|
90
|
+
nested role, label, value, state, bounds, identifier, and child hierarchy. Read
|
|
91
|
+
again after a control when the task depends on a state or navigation change.
|
|
92
|
+
Check `truncated` before asserting that an element is absent. Prefer app-owned
|
|
93
|
+
identifiers when available; generated node ids and paths can change with the
|
|
94
|
+
hierarchy. Secure field values are always `null`.
|
|
95
|
+
|
|
86
96
|
## Record the Screen
|
|
87
97
|
|
|
88
98
|
Both platforms expose the same session-scoped recording lifecycle:
|
|
@@ -137,6 +147,7 @@ typed acknowledgements. Use `RunCloudError` fields such as `code`, `retryable`,
|
|
|
137
147
|
|
|
138
148
|
The lifecycle surface also includes `create`, `list`, `get`, `openUrl`, `logs`,
|
|
139
149
|
`followLogs`, and `delete`. Both platforms expose `screenshot`,
|
|
150
|
+
`accessibilityTree`,
|
|
140
151
|
`startRecording`, `listRecordings`, `getRecording`, `stopRecording`, and
|
|
141
152
|
`downloadRecording`; iOS additionally supports `uploadVideo` and
|
|
142
153
|
`uploadMicrophoneAudio`. Inspect the installed types for complete create,
|
|
@@ -170,9 +181,10 @@ unavailable, report that requirement instead of guessing a public URL.
|
|
|
170
181
|
|
|
171
182
|
## Embed a Session
|
|
172
183
|
|
|
173
|
-
- Use `RemoteControl` from `@
|
|
184
|
+
- Use `RemoteControl` from `@runcloud/ui` with the signed session URL.
|
|
174
185
|
- Its ref exposes promise-based `interact` and convenience methods matching the
|
|
175
|
-
SDK controls
|
|
186
|
+
SDK controls, plus `accessibilityTree`. Handle `onInteractionResult` for
|
|
187
|
+
inspectable acknowledgements and `onAccessibilityResult` for tree reads.
|
|
176
188
|
- Add `embed=1` to raw iframe URLs. Add `loadingGuard=1` when interaction should
|
|
177
189
|
wait for streaming and app readiness.
|
|
178
190
|
- Raw iframe requests use `run-cloud:interaction`; acknowledgements use
|
package/runcloud-0.1.116.tgz
DELETED
|
Binary file
|