@houwert/conductor 0.22.0 → 0.24.0
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/dist/commands/assert-not-visible.js +3 -1
- package/dist/commands/assert-visible.js +3 -1
- package/dist/commands/back.js +4 -0
- package/dist/commands/capture-ui.js +4 -2
- package/dist/commands/clipboard.js +9 -0
- package/dist/commands/crashes.js +5 -0
- package/dist/commands/delete-device.js +6 -1
- package/dist/commands/download-app.js +4 -0
- package/dist/commands/erase-text.js +2 -1
- package/dist/commands/focused.js +3 -1
- package/dist/commands/foreground-app.js +2 -1
- package/dist/commands/gestures.js +7 -0
- package/dist/commands/hide-keyboard.js +4 -0
- package/dist/commands/inspect.js +4 -3
- package/dist/commands/install-app.js +11 -0
- package/dist/commands/launch-app.js +6 -0
- package/dist/commands/list-apps.js +5 -0
- package/dist/commands/list-devices.js +20 -0
- package/dist/commands/memory.js +5 -0
- package/dist/commands/press-key.js +32 -4
- package/dist/commands/profile.js +5 -0
- package/dist/commands/screenshot.js +3 -1
- package/dist/commands/scroll-until-visible.js +3 -1
- package/dist/commands/scroll.js +2 -1
- package/dist/commands/start-device.js +60 -3
- package/dist/commands/stop-app.js +4 -0
- package/dist/commands/stop-device.js +8 -3
- package/dist/commands/swipe.js +2 -1
- package/dist/commands/tap.js +9 -6
- package/dist/commands/uninstall-app.js +4 -0
- package/dist/commands/web-targets.js +2 -34
- package/dist/commands/workspace.js +4 -1
- package/dist/daemon/log-collector.js +9 -1
- package/dist/daemon/server.js +70 -50
- package/dist/drivers/bootstrap.js +12 -0
- package/dist/drivers/cdp-discovery.js +108 -0
- package/dist/drivers/flow-runner.js +38 -8
- package/dist/drivers/ios.js +5 -2
- package/dist/drivers/log-sources/metro-discovery.js +43 -0
- package/dist/drivers/log-sources/vega.js +77 -0
- package/dist/drivers/vega/automation-client.js +79 -0
- package/dist/drivers/vega/cli.js +199 -0
- package/dist/drivers/vega/connection.js +48 -0
- package/dist/drivers/vega/input.js +100 -0
- package/dist/drivers/vega/page-source-parser.js +229 -0
- package/dist/drivers/vega.js +165 -0
- package/dist/enum-options.js +15 -3
- package/dist/index.js +18 -2
- package/dist/runner.js +31 -0
- package/package.json +1 -1
- package/skills/conductor-device-interact/SKILL.md +4 -3
- package/skills/conductor-device-setup/SKILL.md +23 -2
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Converts the Vega automation toolkit's `getPageSource` XML into the
|
|
4
|
+
* uiautomator-style `<node>` XML that conductor's Android element resolver
|
|
5
|
+
* (`parseAndroidHierarchy`, `findAndroidElement`, `inspectAndroidToText`)
|
|
6
|
+
* already understands. This lets Vega reuse the entire Android inspection and
|
|
7
|
+
* element-resolution path unchanged.
|
|
8
|
+
*
|
|
9
|
+
* Toolkit shape: `<root><app appName><window x/y/width/height><child role test_id
|
|
10
|
+
* focusable selectable clickable focused selected>…<text>label</text></child>
|
|
11
|
+
* </window></app></root>`. `<traits>` subtrees are metadata (dropped); bare
|
|
12
|
+
* structural wrappers (no role, interactivity, or text) are flattened; the
|
|
13
|
+
* persistent Kepler launcher app is filtered so only the foreground app shows.
|
|
14
|
+
* Coordinates are absolute device pixels — emitted as `[x,y][x+w,y+h]`, exactly
|
|
15
|
+
* like the Android adapter (no normalization).
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.parseVegaPageSource = parseVegaPageSource;
|
|
19
|
+
const LAUNCHER_APP = 'com.amazon.keplerlauncherapp';
|
|
20
|
+
/** Parse Vega toolkit XML and re-emit it as uiautomator `<node>` XML. */
|
|
21
|
+
function parseVegaPageSource(xml) {
|
|
22
|
+
const root = parseXml(xml);
|
|
23
|
+
if (!root)
|
|
24
|
+
return '<?xml version="1.0" encoding="UTF-8"?>\n<hierarchy />';
|
|
25
|
+
const lines = ['<?xml version="1.0" encoding="UTF-8"?>', '<hierarchy>'];
|
|
26
|
+
for (const scope of foregroundScopes(root)) {
|
|
27
|
+
for (const child of scope.children) {
|
|
28
|
+
emitNode(child, lines, 1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
lines.push('</hierarchy>');
|
|
32
|
+
return lines.join('\n');
|
|
33
|
+
}
|
|
34
|
+
/** The `<app>` subtrees to render: foreground apps, excluding the launcher. */
|
|
35
|
+
function foregroundScopes(root) {
|
|
36
|
+
const apps = root.children.filter((c) => c.tag === 'app');
|
|
37
|
+
if (apps.length === 0)
|
|
38
|
+
return [root];
|
|
39
|
+
const foreground = apps.filter((a) => a.attrs['appName'] !== LAUNCHER_APP);
|
|
40
|
+
return foreground.length > 0 ? foreground : apps;
|
|
41
|
+
}
|
|
42
|
+
function emitNode(node, lines, depth) {
|
|
43
|
+
if (node.tag === 'traits' || node.tag === 'text')
|
|
44
|
+
return;
|
|
45
|
+
const meaningful = isMeaningful(node);
|
|
46
|
+
const rendered = node.children.filter((c) => c.tag !== 'traits' && c.tag !== 'text');
|
|
47
|
+
if (!meaningful) {
|
|
48
|
+
// Flatten structural wrapper — emit its children in its place.
|
|
49
|
+
for (const child of rendered)
|
|
50
|
+
emitNode(child, lines, depth);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const attrs = [];
|
|
54
|
+
const role = node.attrs['role'];
|
|
55
|
+
if (role)
|
|
56
|
+
attrs.push(`class="${xmlEscape(role)}"`);
|
|
57
|
+
const testId = node.attrs['test_id'];
|
|
58
|
+
if (testId)
|
|
59
|
+
attrs.push(`resource-id="${xmlEscape(testId)}"`);
|
|
60
|
+
const label = labelOf(node);
|
|
61
|
+
if (label)
|
|
62
|
+
attrs.push(`text="${xmlEscape(label)}"`);
|
|
63
|
+
attrs.push(`bounds="${boundsOf(node)}"`);
|
|
64
|
+
const interactive = isInteractive(node);
|
|
65
|
+
attrs.push(`clickable="${interactive ? 'true' : 'false'}"`);
|
|
66
|
+
if (node.attrs['focused'] !== undefined) {
|
|
67
|
+
attrs.push(`focused="${boolAttr(node, 'focused')}"`);
|
|
68
|
+
}
|
|
69
|
+
if (node.attrs['selected'] !== undefined) {
|
|
70
|
+
attrs.push(`selected="${boolAttr(node, 'selected')}"`);
|
|
71
|
+
}
|
|
72
|
+
attrs.push('enabled="true"');
|
|
73
|
+
const indent = ' '.repeat(depth);
|
|
74
|
+
if (rendered.length === 0) {
|
|
75
|
+
lines.push(`${indent}<node ${attrs.join(' ')} />`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
lines.push(`${indent}<node ${attrs.join(' ')}>`);
|
|
79
|
+
for (const child of rendered)
|
|
80
|
+
emitNode(child, lines, depth + 1);
|
|
81
|
+
lines.push(`${indent}</node>`);
|
|
82
|
+
}
|
|
83
|
+
// A node earns a line when it carries meaning: an explicit role, interactivity,
|
|
84
|
+
// or its own text. Bare structural wrappers are flattened.
|
|
85
|
+
function isMeaningful(node) {
|
|
86
|
+
return !!node.attrs['role'] || isInteractive(node) || labelOf(node).length > 0;
|
|
87
|
+
}
|
|
88
|
+
function isInteractive(node) {
|
|
89
|
+
return boolAttr(node, 'focusable') || boolAttr(node, 'selectable') || boolAttr(node, 'clickable');
|
|
90
|
+
}
|
|
91
|
+
/** Label = this node's direct text plus the text of its direct `<text>` children. */
|
|
92
|
+
function labelOf(node) {
|
|
93
|
+
const parts = [];
|
|
94
|
+
if (node.text.trim())
|
|
95
|
+
parts.push(node.text.trim());
|
|
96
|
+
for (const child of node.children) {
|
|
97
|
+
if (child.tag === 'text' && child.text.trim())
|
|
98
|
+
parts.push(child.text.trim());
|
|
99
|
+
}
|
|
100
|
+
return parts.join(' ').trim();
|
|
101
|
+
}
|
|
102
|
+
function boundsOf(node) {
|
|
103
|
+
const x = intAttr(node, 'x');
|
|
104
|
+
const y = intAttr(node, 'y');
|
|
105
|
+
const w = intAttr(node, 'width');
|
|
106
|
+
const h = intAttr(node, 'height');
|
|
107
|
+
return `[${x},${y}][${x + w},${y + h}]`;
|
|
108
|
+
}
|
|
109
|
+
function boolAttr(node, name) {
|
|
110
|
+
const v = (node.attrs[name] ?? '').trim().toLowerCase();
|
|
111
|
+
return v === 'true' || v === '1';
|
|
112
|
+
}
|
|
113
|
+
function intAttr(node, name) {
|
|
114
|
+
return parseInt(node.attrs[name] ?? '', 10) || 0;
|
|
115
|
+
}
|
|
116
|
+
function xmlEscape(s) {
|
|
117
|
+
return s
|
|
118
|
+
.replace(/&/g, '&')
|
|
119
|
+
.replace(/</g, '<')
|
|
120
|
+
.replace(/>/g, '>')
|
|
121
|
+
.replace(/"/g, '"');
|
|
122
|
+
}
|
|
123
|
+
// ── Minimal XML parser ────────────────────────────────────────────────────────
|
|
124
|
+
// Handles elements, attributes, text nodes, self-closing tags, and skips the XML
|
|
125
|
+
// declaration / comments. Sufficient for the well-formed toolkit output.
|
|
126
|
+
function parseXml(xml) {
|
|
127
|
+
let i = 0;
|
|
128
|
+
const n = xml.length;
|
|
129
|
+
function skipWhitespace() {
|
|
130
|
+
while (i < n && /\s/.test(xml[i]))
|
|
131
|
+
i++;
|
|
132
|
+
}
|
|
133
|
+
function parseNode() {
|
|
134
|
+
// Skip declarations, comments, and processing instructions before the tag.
|
|
135
|
+
while (i < n) {
|
|
136
|
+
skipWhitespace();
|
|
137
|
+
if (xml.startsWith('<?', i)) {
|
|
138
|
+
i = xml.indexOf('?>', i);
|
|
139
|
+
i = i === -1 ? n : i + 2;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (xml.startsWith('<!--', i)) {
|
|
143
|
+
i = xml.indexOf('-->', i);
|
|
144
|
+
i = i === -1 ? n : i + 3;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (xml.startsWith('<!', i)) {
|
|
148
|
+
i = xml.indexOf('>', i);
|
|
149
|
+
i = i === -1 ? n : i + 1;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
if (i >= n || xml[i] !== '<')
|
|
155
|
+
return null;
|
|
156
|
+
i++; // consume '<'
|
|
157
|
+
// Read tag name
|
|
158
|
+
const nameStart = i;
|
|
159
|
+
while (i < n && !/[\s/>]/.test(xml[i]))
|
|
160
|
+
i++;
|
|
161
|
+
const tag = xml.slice(nameStart, i);
|
|
162
|
+
const attrs = {};
|
|
163
|
+
// Read attributes
|
|
164
|
+
while (i < n) {
|
|
165
|
+
skipWhitespace();
|
|
166
|
+
if (xml[i] === '/' || xml[i] === '>')
|
|
167
|
+
break;
|
|
168
|
+
const attrNameStart = i;
|
|
169
|
+
while (i < n && !/[\s=/>]/.test(xml[i]))
|
|
170
|
+
i++;
|
|
171
|
+
const attrName = xml.slice(attrNameStart, i);
|
|
172
|
+
skipWhitespace();
|
|
173
|
+
let attrValue = '';
|
|
174
|
+
if (xml[i] === '=') {
|
|
175
|
+
i++; // consume '='
|
|
176
|
+
skipWhitespace();
|
|
177
|
+
const quote = xml[i];
|
|
178
|
+
if (quote === '"' || quote === "'") {
|
|
179
|
+
i++;
|
|
180
|
+
const valStart = i;
|
|
181
|
+
while (i < n && xml[i] !== quote)
|
|
182
|
+
i++;
|
|
183
|
+
attrValue = xmlUnescape(xml.slice(valStart, i));
|
|
184
|
+
i++; // consume closing quote
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (attrName)
|
|
188
|
+
attrs[attrName] = attrValue;
|
|
189
|
+
}
|
|
190
|
+
const node = { tag, attrs, children: [], text: '' };
|
|
191
|
+
if (xml[i] === '/') {
|
|
192
|
+
// self-closing
|
|
193
|
+
i = xml.indexOf('>', i);
|
|
194
|
+
i = i === -1 ? n : i + 1;
|
|
195
|
+
return node;
|
|
196
|
+
}
|
|
197
|
+
i++; // consume '>'
|
|
198
|
+
// Read children / text until closing tag
|
|
199
|
+
while (i < n) {
|
|
200
|
+
if (xml.startsWith('</', i)) {
|
|
201
|
+
i = xml.indexOf('>', i);
|
|
202
|
+
i = i === -1 ? n : i + 1;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
if (xml[i] === '<') {
|
|
206
|
+
const child = parseNode();
|
|
207
|
+
if (child)
|
|
208
|
+
node.children.push(child);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
const textStart = i;
|
|
212
|
+
while (i < n && xml[i] !== '<')
|
|
213
|
+
i++;
|
|
214
|
+
node.text += xmlUnescape(xml.slice(textStart, i));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return node;
|
|
218
|
+
}
|
|
219
|
+
return parseNode();
|
|
220
|
+
}
|
|
221
|
+
function xmlUnescape(s) {
|
|
222
|
+
return s
|
|
223
|
+
.replace(/</g, '<')
|
|
224
|
+
.replace(/>/g, '>')
|
|
225
|
+
.replace(/"/g, '"')
|
|
226
|
+
.replace(/'/g, "'")
|
|
227
|
+
.replace(/'/g, "'")
|
|
228
|
+
.replace(/&/g, '&');
|
|
229
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VegaDriver = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Driver client for Amazon Vega (Fire TV) devices. Vega is a Linux/React Native
|
|
6
|
+
* OS (not Android), reached through Amazon's `vega`/`kepler` CLI — there is no
|
|
7
|
+
* XCTest/gRPC driver process and no local control port. This class is a
|
|
8
|
+
* standalone, stateless client that shells out to the CLI plus the on-device
|
|
9
|
+
* automation toolkit:
|
|
10
|
+
* - view hierarchy from the toolkit (JSON-RPC on device port 8383), re-emitted
|
|
11
|
+
* as uiautomator XML so the Android element resolver handles it,
|
|
12
|
+
* - input via the stock `inputd-cli` (D-pad button_press, touch, swipe, send_text),
|
|
13
|
+
* - screenshots via the toolkit's `takeScreenshot`,
|
|
14
|
+
* - app lifecycle via `vega device …`.
|
|
15
|
+
*
|
|
16
|
+
* v1 targets the Vega Virtual Device (VVD); physical Fire TV sticks reuse the
|
|
17
|
+
* same path.
|
|
18
|
+
*/
|
|
19
|
+
const cli_js_1 = require("./vega/cli.js");
|
|
20
|
+
const connection_js_1 = require("./vega/connection.js");
|
|
21
|
+
const automation_client_js_1 = require("./vega/automation-client.js");
|
|
22
|
+
const input_js_1 = require("./vega/input.js");
|
|
23
|
+
const page_source_parser_js_1 = require("./vega/page-source-parser.js");
|
|
24
|
+
const UNSUPPORTED = (op) => new Error(`${op} is not supported on vega (Amazon Fire TV)`);
|
|
25
|
+
class VegaDriver {
|
|
26
|
+
/** `serial` is the bare Vega selector (e.g. `VirtualDevice`), not the `vega:` id. */
|
|
27
|
+
constructor(serial) {
|
|
28
|
+
this.serial = serial;
|
|
29
|
+
this.platform = 'vega';
|
|
30
|
+
this.screenSize = null;
|
|
31
|
+
this.cli = new cli_js_1.VegaCli(serial);
|
|
32
|
+
this.connection = new connection_js_1.VegaDeviceConnection(serial, this.cli);
|
|
33
|
+
this.automation = new automation_client_js_1.VegaAutomationClient(this.connection);
|
|
34
|
+
this.input = new input_js_1.VegaInput(this.connection);
|
|
35
|
+
}
|
|
36
|
+
async isAlive() {
|
|
37
|
+
const devices = await this.cli.listDevices().catch(() => []);
|
|
38
|
+
return devices.some((d) => d.serial === this.serial);
|
|
39
|
+
}
|
|
40
|
+
async deviceInfo() {
|
|
41
|
+
const size = await this.resolveScreenSize();
|
|
42
|
+
return { widthPixels: size.width, heightPixels: size.height };
|
|
43
|
+
}
|
|
44
|
+
async resolveScreenSize() {
|
|
45
|
+
if (this.screenSize)
|
|
46
|
+
return this.screenSize;
|
|
47
|
+
const size = (await this.connection.screenSize().catch(() => null)) ?? {
|
|
48
|
+
width: 1920,
|
|
49
|
+
height: 1080,
|
|
50
|
+
};
|
|
51
|
+
this.screenSize = size;
|
|
52
|
+
return size;
|
|
53
|
+
}
|
|
54
|
+
// ── Input ──────────────────────────────────────────────────────────────────
|
|
55
|
+
async tap(x, y, duration) {
|
|
56
|
+
if (duration && duration >= 0.5) {
|
|
57
|
+
await this.input.longPress(x, y);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
await this.input.tap(x, y);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async longPress(x, y) {
|
|
64
|
+
await this.input.longPress(x, y);
|
|
65
|
+
}
|
|
66
|
+
async swipe(startX, startY, endX, endY, durationMs) {
|
|
67
|
+
await this.input.swipe(startX, startY, endX, endY, durationMs);
|
|
68
|
+
}
|
|
69
|
+
async pressButton(button) {
|
|
70
|
+
await this.input.pressButton(button);
|
|
71
|
+
}
|
|
72
|
+
async back() {
|
|
73
|
+
await this.input.pressButton('back');
|
|
74
|
+
}
|
|
75
|
+
async inputText(text) {
|
|
76
|
+
await this.input.inputText(text);
|
|
77
|
+
}
|
|
78
|
+
async eraseAllText(charactersToErase = 50) {
|
|
79
|
+
await this.input.eraseText(charactersToErase);
|
|
80
|
+
}
|
|
81
|
+
// ── Inspection ───────────────────────────────────────────────────────────────
|
|
82
|
+
/** Returns uiautomator-style XML (consumed by the Android element resolver). */
|
|
83
|
+
async viewHierarchy() {
|
|
84
|
+
const xml = await this.automation.getPageSource();
|
|
85
|
+
return (0, page_source_parser_js_1.parseVegaPageSource)(xml);
|
|
86
|
+
}
|
|
87
|
+
async screenshot(_opts = {}) {
|
|
88
|
+
return this.automation.getScreenshot();
|
|
89
|
+
}
|
|
90
|
+
// ── App lifecycle ────────────────────────────────────────────────────────────
|
|
91
|
+
async launchApp(appId, _args) {
|
|
92
|
+
// Cold launch: terminate first so a singleton app restarts from initial state.
|
|
93
|
+
await this.cli.terminateApp(appId);
|
|
94
|
+
// The toolkit reads the enable flag at launch, so set it before (re)launching.
|
|
95
|
+
await this.connection.ensureToolkitEnabled();
|
|
96
|
+
await this.cli.launchApp(appId);
|
|
97
|
+
}
|
|
98
|
+
async terminateApp(appId) {
|
|
99
|
+
await this.cli.terminateApp(appId);
|
|
100
|
+
}
|
|
101
|
+
async stopApp(appId) {
|
|
102
|
+
await this.cli.terminateApp(appId);
|
|
103
|
+
}
|
|
104
|
+
async installApp(vpkgPath) {
|
|
105
|
+
await this.cli.installApp(vpkgPath);
|
|
106
|
+
}
|
|
107
|
+
async listInstalledApps() {
|
|
108
|
+
return this.cli.listInstalledApps();
|
|
109
|
+
}
|
|
110
|
+
async getForegroundApp() {
|
|
111
|
+
// The toolkit page source names the foreground (non-launcher) app.
|
|
112
|
+
const xml = await this.automation.getPageSource();
|
|
113
|
+
const match = /<app[^>]*\bappName="([^"]+)"/.exec(xml);
|
|
114
|
+
if (!match)
|
|
115
|
+
throw new Error('Could not determine foreground app');
|
|
116
|
+
return match[1];
|
|
117
|
+
}
|
|
118
|
+
// ── Unsupported on vega (gated with clear errors) ────────────────────────────
|
|
119
|
+
async clearAppState(_appId) {
|
|
120
|
+
throw UNSUPPORTED('clear-state');
|
|
121
|
+
}
|
|
122
|
+
async clearKeychain() {
|
|
123
|
+
// Not applicable on Vega — no-op.
|
|
124
|
+
}
|
|
125
|
+
async uninstallApp(_appId) {
|
|
126
|
+
throw UNSUPPORTED('uninstall-app');
|
|
127
|
+
}
|
|
128
|
+
async openLink(_url) {
|
|
129
|
+
throw UNSUPPORTED('open-link');
|
|
130
|
+
}
|
|
131
|
+
async setLocation(_latitude, _longitude) {
|
|
132
|
+
throw UNSUPPORTED('set-location');
|
|
133
|
+
}
|
|
134
|
+
async setOrientation(_orientation) {
|
|
135
|
+
// No-op: Vega/Fire TV is landscape-only.
|
|
136
|
+
}
|
|
137
|
+
async setPermissions(_appId, _permissions) {
|
|
138
|
+
// No runtime-permission grant primitive on Vega in v1 — no-op.
|
|
139
|
+
}
|
|
140
|
+
async addMedia(_filePath) {
|
|
141
|
+
throw UNSUPPORTED('add-media');
|
|
142
|
+
}
|
|
143
|
+
async setAirplaneMode(_enabled) {
|
|
144
|
+
throw UNSUPPORTED('airplane mode');
|
|
145
|
+
}
|
|
146
|
+
async getAirplaneMode() {
|
|
147
|
+
throw UNSUPPORTED('airplane mode');
|
|
148
|
+
}
|
|
149
|
+
async gesturePath(_paths) {
|
|
150
|
+
throw UNSUPPORTED('multi-finger gestures');
|
|
151
|
+
}
|
|
152
|
+
async startRecording(_outputPath) {
|
|
153
|
+
throw UNSUPPORTED('screen recording');
|
|
154
|
+
}
|
|
155
|
+
async stopRecording() {
|
|
156
|
+
throw UNSUPPORTED('screen recording');
|
|
157
|
+
}
|
|
158
|
+
async clipboardRead() {
|
|
159
|
+
throw UNSUPPORTED('clipboard');
|
|
160
|
+
}
|
|
161
|
+
async clipboardWrite(_text) {
|
|
162
|
+
throw UNSUPPORTED('clipboard');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
exports.VegaDriver = VegaDriver;
|
package/dist/enum-options.js
CHANGED
|
@@ -24,7 +24,7 @@ exports.ENUM_PARAMS = [
|
|
|
24
24
|
param: '<key>',
|
|
25
25
|
description: 'Key, hardware button, or remote button to press',
|
|
26
26
|
values: press_key_js_1.VALID_KEYS.map((value) => ({ value })),
|
|
27
|
-
note: 'Matched case-insensitively. Availability varies by platform: "Remote …" / "TV …" keys target tvOS and
|
|
27
|
+
note: 'Matched case-insensitively. Availability varies by platform: "Remote …" / "TV …" keys target tvOS, Android TV, and vega (Amazon Fire TV); hardware buttons (Home, Lock, Power, Volume…) target iOS/Android.',
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
command: 'scroll',
|
|
@@ -56,7 +56,13 @@ exports.ENUM_PARAMS = [
|
|
|
56
56
|
param: '--platform',
|
|
57
57
|
description: 'Platform of the device to start',
|
|
58
58
|
// Source: switch in commands/start-device.ts
|
|
59
|
-
values: [
|
|
59
|
+
values: [
|
|
60
|
+
{ value: 'ios' },
|
|
61
|
+
{ value: 'android' },
|
|
62
|
+
{ value: 'tvos' },
|
|
63
|
+
{ value: 'web' },
|
|
64
|
+
{ value: 'vega' },
|
|
65
|
+
],
|
|
60
66
|
},
|
|
61
67
|
{
|
|
62
68
|
command: 'install-web',
|
|
@@ -98,7 +104,13 @@ exports.ENUM_PARAMS = [
|
|
|
98
104
|
command: 'list-devices',
|
|
99
105
|
param: '--platform',
|
|
100
106
|
description: 'Filter listed devices by platform (also a global filter on most commands)',
|
|
101
|
-
values: [
|
|
107
|
+
values: [
|
|
108
|
+
{ value: 'ios' },
|
|
109
|
+
{ value: 'android' },
|
|
110
|
+
{ value: 'tvos' },
|
|
111
|
+
{ value: 'web' },
|
|
112
|
+
{ value: 'vega' },
|
|
113
|
+
],
|
|
102
114
|
},
|
|
103
115
|
];
|
|
104
116
|
/** All distinct command names that have at least one enumerated parameter. */
|
package/dist/index.js
CHANGED
|
@@ -64,6 +64,7 @@ const options_js_1 = require("./commands/options.js");
|
|
|
64
64
|
const web_targets_js_1 = require("./commands/web-targets.js");
|
|
65
65
|
const session_js_2 = require("./session.js");
|
|
66
66
|
const device_picker_js_1 = require("./device-picker.js");
|
|
67
|
+
const cdp_discovery_js_1 = require("./drivers/cdp-discovery.js");
|
|
67
68
|
const update_check_js_1 = require("./update-check.js");
|
|
68
69
|
const pkg_root_js_1 = require("./pkg-root.js");
|
|
69
70
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -130,7 +131,7 @@ const COMMAND_HELP = {
|
|
|
130
131
|
const OPTIONS_HELP = `Options:
|
|
131
132
|
--device <id> Target device ID (also keys the session and daemon)
|
|
132
133
|
--device-name <n> Target a booted device by name (resolved to ID from booted devices)
|
|
133
|
-
--platform <p> Filter to devices of this platform (ios, android, tvos, web)
|
|
134
|
+
--platform <p> Filter to devices of this platform (ios, android, tvos, web, vega)
|
|
134
135
|
--cdp-url <url> Attach the web driver to an existing browser over CDP (e.g. an
|
|
135
136
|
Electron app started with --remote-debugging-port). Remembered per session.
|
|
136
137
|
--cdp-target <id> Pick which CDP page target to control (see \`conductor web-targets\`)
|
|
@@ -238,6 +239,7 @@ async function main() {
|
|
|
238
239
|
'color-scheme',
|
|
239
240
|
'cdp-url',
|
|
240
241
|
'cdp-target',
|
|
242
|
+
'duration',
|
|
241
243
|
],
|
|
242
244
|
alias: { h: 'help', v: 'verbose', V: 'version', o: 'output', y: 'yes' },
|
|
243
245
|
});
|
|
@@ -325,6 +327,9 @@ async function main() {
|
|
|
325
327
|
if (isWebSession && !NO_DEVICE_COMMANDS.has(command)) {
|
|
326
328
|
const cdpUrlFlag = argv['cdp-url'];
|
|
327
329
|
const cdpTargetFlag = argv['cdp-target'];
|
|
330
|
+
// A discovered `web:cdp:<port>:<target>` device id is self-describing —
|
|
331
|
+
// derive the CDP url/target from it so it's drivable with no --cdp-* flags.
|
|
332
|
+
const fromDeviceId = (0, cdp_discovery_js_1.parseCdpDeviceId)(sessionName);
|
|
328
333
|
if (cdpUrlFlag || cdpTargetFlag) {
|
|
329
334
|
if (cdpUrlFlag)
|
|
330
335
|
process.env.CONDUCTOR_CDP_URL = cdpUrlFlag;
|
|
@@ -335,6 +340,13 @@ async function main() {
|
|
|
335
340
|
cdpTargetId: process.env.CONDUCTOR_CDP_TARGET_ID,
|
|
336
341
|
}, sessionName);
|
|
337
342
|
}
|
|
343
|
+
else if (fromDeviceId) {
|
|
344
|
+
if (!process.env.CONDUCTOR_CDP_URL)
|
|
345
|
+
process.env.CONDUCTOR_CDP_URL = fromDeviceId.cdpUrl;
|
|
346
|
+
if (!process.env.CONDUCTOR_CDP_TARGET_ID) {
|
|
347
|
+
process.env.CONDUCTOR_CDP_TARGET_ID = fromDeviceId.targetId;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
338
350
|
else {
|
|
339
351
|
const saved = await (0, session_js_2.getSession)(sessionName);
|
|
340
352
|
if (saved.cdpUrl && !process.env.CONDUCTOR_CDP_URL) {
|
|
@@ -471,7 +483,11 @@ async function main() {
|
|
|
471
483
|
break;
|
|
472
484
|
case 'press-key': {
|
|
473
485
|
const key = rest[0] ?? '';
|
|
474
|
-
|
|
486
|
+
const durationArg = argv['duration'];
|
|
487
|
+
exitCode = await (0, press_key_js_1.pressKey)(key, opts, sessionName, {
|
|
488
|
+
longPress: argv['long-press'],
|
|
489
|
+
duration: durationArg !== undefined ? Number(durationArg) : undefined,
|
|
490
|
+
});
|
|
475
491
|
break;
|
|
476
492
|
}
|
|
477
493
|
case 'list-options': {
|
package/dist/runner.js
CHANGED
|
@@ -14,6 +14,8 @@ const verbose_js_1 = require("./verbose.js");
|
|
|
14
14
|
const ios_js_1 = require("./drivers/ios.js");
|
|
15
15
|
const android_js_1 = require("./drivers/android.js");
|
|
16
16
|
const web_js_1 = require("./drivers/web.js");
|
|
17
|
+
const vega_js_1 = require("./drivers/vega.js");
|
|
18
|
+
const cli_js_1 = require("./drivers/vega/cli.js");
|
|
17
19
|
const bootstrap_js_1 = require("./drivers/bootstrap.js");
|
|
18
20
|
const client_js_1 = require("./daemon/client.js");
|
|
19
21
|
/**
|
|
@@ -75,9 +77,27 @@ async function detectFirstDevice() {
|
|
|
75
77
|
return session;
|
|
76
78
|
}
|
|
77
79
|
}
|
|
80
|
+
// Vega (Amazon Fire TV): query the vega CLI for a booted device. Best-effort —
|
|
81
|
+
// the CLI is absent unless the Vega SDK is installed.
|
|
82
|
+
try {
|
|
83
|
+
const devices = await new cli_js_1.VegaCli().listDevices();
|
|
84
|
+
const device = devices[0];
|
|
85
|
+
if (device) {
|
|
86
|
+
(0, verbose_js_1.log)(`detectFirstDevice: found Vega device "${device.serial}"`);
|
|
87
|
+
_cachedDeviceId = `vega:${device.serial}`;
|
|
88
|
+
return _cachedDeviceId;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
/* vega CLI not installed */
|
|
93
|
+
}
|
|
78
94
|
_cachedDeviceId = null;
|
|
79
95
|
return undefined;
|
|
80
96
|
}
|
|
97
|
+
/** Strip the `vega:` prefix to recover the bare Vega selector. */
|
|
98
|
+
function vegaSerial(deviceId) {
|
|
99
|
+
return deviceId.startsWith('vega:') ? deviceId.slice('vega:'.length) : deviceId;
|
|
100
|
+
}
|
|
81
101
|
/** Per-session driver cache (process lifetime). */
|
|
82
102
|
const _driverCache = new Map();
|
|
83
103
|
/**
|
|
@@ -166,6 +186,17 @@ async function getDriver(sessionName = 'default') {
|
|
|
166
186
|
}
|
|
167
187
|
driver = webDriver;
|
|
168
188
|
}
|
|
189
|
+
else if (platform === 'vega') {
|
|
190
|
+
// Vega has no driver process/port — control is host-side via the vega CLI.
|
|
191
|
+
// Start the daemon best-effort for log collection, but never block control on it.
|
|
192
|
+
(0, client_js_1.startDaemon)(deviceId).catch(() => { });
|
|
193
|
+
const vegaDriver = new vega_js_1.VegaDriver(vegaSerial(deviceId));
|
|
194
|
+
if (!(await vegaDriver.isAlive())) {
|
|
195
|
+
throw new Error(`No running Vega device matches "${vegaSerial(deviceId)}".\n` +
|
|
196
|
+
`Boot a VVD and check \`vega device list\`, or install the Vega SDK (\`vega\`/\`kepler\`).`);
|
|
197
|
+
}
|
|
198
|
+
driver = vegaDriver;
|
|
199
|
+
}
|
|
169
200
|
else {
|
|
170
201
|
// Ensure the daemon is running — it handles APK install and driver startup.
|
|
171
202
|
await (0, client_js_1.startDaemon)(deviceId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: conductor-device-interact
|
|
3
|
-
description: Drive a running iOS simulator, Android emulator, tvOS simulator, or Playwright web app with the conductor CLI. Use when launching apps, tapping UI elements, typing text, scrolling/swiping, performing gestures, pressing hardware/keyboard keys, opening URLs or deep links, navigating back, or verifying an app change in the real running app.
|
|
3
|
+
description: Drive a running iOS simulator, Android emulator, tvOS simulator, Vega (Amazon Fire TV) virtual device, or Playwright web app with the conductor CLI. Use when launching apps, tapping UI elements, typing text, scrolling/swiping, performing gestures, pressing hardware/keyboard/remote keys, opening URLs or deep links, navigating back, or verifying an app change in the real running app.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Conductor — device interaction
|
|
@@ -39,7 +39,7 @@ conductor assert-visible "Dashboard"
|
|
|
39
39
|
| `conductor tap-on <element>` | Tap by text, id, or `@eN`. `--long-press`, `--double-tap`, `--optional`, `--index <n>` |
|
|
40
40
|
| `conductor input-text <text>` | Type into the focused field |
|
|
41
41
|
| `conductor erase-text [n]` | Erase n characters (default 50) |
|
|
42
|
-
| `conductor press-key <key>` | Press a key (Enter, Backspace, Home, …) |
|
|
42
|
+
| `conductor press-key <key>` | Press a key (Enter, Backspace, Home, …) or a remote button (`Remote Dpad Up/Down/Left/Right/Center`, `Remote Menu`) for tvOS / Android TV / vega. `--long-press` / `--duration <seconds>` holds it |
|
|
43
43
|
| `conductor hide-keyboard` | Dismiss the on-screen keyboard |
|
|
44
44
|
| `conductor back` | Press back |
|
|
45
45
|
| `conductor scroll [--direction down\|up\|left\|right]` | Scroll |
|
|
@@ -90,7 +90,8 @@ relaunch without the flag. (See `conductor-device-setup`.)
|
|
|
90
90
|
|
|
91
91
|
## Tips
|
|
92
92
|
|
|
93
|
-
- `--device <id>` / `--device-name <name>` targets a device; `--platform <ios|android|tvos|web>` scopes by platform.
|
|
93
|
+
- `--device <id>` / `--device-name <name>` targets a device; `--platform <ios|android|tvos|web|vega>` scopes by platform.
|
|
94
|
+
- Vega (Amazon Fire TV) is D-pad driven: navigate with `press-key "Remote Dpad …"`; coordinate `tap-on` also works. `open-link`, `set-location`, gestures, and clipboard are unsupported. See `conductor-device-setup`.
|
|
94
95
|
- Add `--json` for machine-readable output; failed assertions exit non-zero.
|
|
95
96
|
- Run a per-session daemon for many commands (see `conductor-device-setup`).
|
|
96
97
|
- `conductor <command> --help` for exact flags.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: conductor-device-setup
|
|
3
|
-
description: Boot, list, and manage devices and app installs for the conductor CLI — iOS simulators, Android emulators, tvOS simulators, and Playwright web browsers — plus sessions, the warm-driver daemon, and the parallel device pool. Use when starting or stopping a simulator/emulator/browser, installing or launching an app, setting up the web driver, attaching to an already-running browser over CDP (e.g. an Electron app / its webviews), keeping the driver warm, or coordinating multiple devices for parallel agents.
|
|
3
|
+
description: Boot, list, and manage devices and app installs for the conductor CLI — iOS simulators, Android emulators, tvOS simulators, Vega (Amazon Fire TV) virtual devices, and Playwright web browsers — plus sessions, the warm-driver daemon, and the parallel device pool. Use when starting or stopping a simulator/emulator/browser, attaching to a Vega VVD, installing or launching an app, setting up the web driver, attaching to an already-running browser over CDP (e.g. an Electron app / its webviews), keeping the driver warm, or coordinating multiple devices for parallel agents.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Conductor — device & app setup
|
|
@@ -21,7 +21,7 @@ conductor list-apps # installed app ids / package names
|
|
|
21
21
|
|
|
22
22
|
| Command | Purpose |
|
|
23
23
|
| --------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
24
|
-
| `conductor start-device --platform <ios\|android\|tvos\|web>`
|
|
24
|
+
| `conductor start-device --platform <ios\|android\|tvos\|web\|vega>` | Boot a simulator/emulator, start the web driver, or attach to a Vega VVD |
|
|
25
25
|
| `conductor start-device --os-version <n> --device-type <name>` | Pick OS version + device type (creates if needed) |
|
|
26
26
|
| `conductor stop-device [<name-or-id>] [--all]` | Shut down device(s) |
|
|
27
27
|
| `conductor delete-device <name-or-id> [--all]` | Delete simulator(s)/AVD(s)/web session(s) |
|
|
@@ -47,6 +47,27 @@ Use a distinct fully-qualified `--device web:chromium:<label>` per target (a bar
|
|
|
47
47
|
several webviews can be driven concurrently. Only `type=page` targets are
|
|
48
48
|
controllable. See [Web testing → Attaching to an existing browser](../../../docs/web.md).
|
|
49
49
|
|
|
50
|
+
**Discovery:** endpoints on the conventional debugging ports (9222–9229 on
|
|
51
|
+
localhost) are found automatically — `list-devices` shows each webview as a
|
|
52
|
+
booted `web:cdp:<port>:<targetId>` device. That id is self-describing, so you can
|
|
53
|
+
drive it directly (`conductor --device web:cdp:9222:<targetId> <cmd>`) without
|
|
54
|
+
`--cdp-url`/`--cdp-target`. Use `web-targets --cdp-url <url>` for endpoints on a
|
|
55
|
+
non-default port, or to list targets before binding.
|
|
56
|
+
|
|
57
|
+
### Vega (Amazon Fire TV)
|
|
58
|
+
|
|
59
|
+
Vega is a React Native OS driven through Amazon's own `vega`/`kepler` CLI (not
|
|
60
|
+
adb/simctl) — install the Vega SDK and put the CLI on `PATH` (or set
|
|
61
|
+
`CONDUCTOR_VEGA_CLI`). `conductor start-device --platform vega` boots a Vega
|
|
62
|
+
Virtual Device (VVD) via `vega virtual-device start` (or attaches if one is
|
|
63
|
+
already running); pass `--name <vvd>` to pick a specific one. Devices show up in
|
|
64
|
+
`list-devices` as `vega:<serial>`. The VVD's
|
|
65
|
+
**developer mode** must be on for input, and the automation toolkit attaches at
|
|
66
|
+
app launch — so launch the app under test via conductor. Navigate with the D-pad
|
|
67
|
+
(`press-key "Remote Dpad …"` / `"Remote Dpad Center"`); coordinate `tap-on` also
|
|
68
|
+
works. Unsupported on Vega: deep links (`open-link`), `set-location`, gestures,
|
|
69
|
+
screen recording, clipboard, `clear-state`/`uninstall-app`.
|
|
70
|
+
|
|
50
71
|
## App lifecycle
|
|
51
72
|
|
|
52
73
|
| Command | Purpose |
|