@houwert/conductor 0.21.0 → 0.23.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.
Files changed (52) hide show
  1. package/dist/commands/assert-not-visible.js +3 -1
  2. package/dist/commands/assert-visible.js +3 -1
  3. package/dist/commands/back.js +4 -0
  4. package/dist/commands/capture-ui.js +4 -2
  5. package/dist/commands/clipboard.js +9 -0
  6. package/dist/commands/crashes.js +5 -0
  7. package/dist/commands/delete-device.js +6 -1
  8. package/dist/commands/download-app.js +4 -0
  9. package/dist/commands/erase-text.js +2 -1
  10. package/dist/commands/focused.js +3 -1
  11. package/dist/commands/foreground-app.js +2 -1
  12. package/dist/commands/gestures.js +7 -0
  13. package/dist/commands/hide-keyboard.js +4 -0
  14. package/dist/commands/inspect.js +4 -3
  15. package/dist/commands/install-app.js +11 -0
  16. package/dist/commands/launch-app.js +6 -0
  17. package/dist/commands/list-apps.js +5 -0
  18. package/dist/commands/list-devices.js +16 -0
  19. package/dist/commands/memory.js +5 -0
  20. package/dist/commands/press-key.js +32 -4
  21. package/dist/commands/profile.js +5 -0
  22. package/dist/commands/screenshot.js +3 -1
  23. package/dist/commands/scroll-until-visible.js +3 -1
  24. package/dist/commands/scroll.js +2 -1
  25. package/dist/commands/start-device.js +60 -3
  26. package/dist/commands/stop-app.js +4 -0
  27. package/dist/commands/stop-device.js +8 -3
  28. package/dist/commands/swipe.js +2 -1
  29. package/dist/commands/tap.js +9 -6
  30. package/dist/commands/uninstall-app.js +4 -0
  31. package/dist/commands/web-targets.js +84 -0
  32. package/dist/commands/workspace.js +4 -1
  33. package/dist/daemon/log-collector.js +9 -1
  34. package/dist/daemon/server.js +70 -50
  35. package/dist/drivers/bootstrap.js +12 -0
  36. package/dist/drivers/flow-runner.js +38 -8
  37. package/dist/drivers/ios.js +5 -2
  38. package/dist/drivers/log-sources/metro-discovery.js +56 -2
  39. package/dist/drivers/log-sources/vega.js +77 -0
  40. package/dist/drivers/metro-cdp.js +11 -3
  41. package/dist/drivers/vega/automation-client.js +79 -0
  42. package/dist/drivers/vega/cli.js +199 -0
  43. package/dist/drivers/vega/connection.js +48 -0
  44. package/dist/drivers/vega/input.js +100 -0
  45. package/dist/drivers/vega/page-source-parser.js +229 -0
  46. package/dist/drivers/vega.js +165 -0
  47. package/dist/enum-options.js +15 -3
  48. package/dist/index.js +46 -2
  49. package/dist/runner.js +31 -0
  50. package/package.json +1 -1
  51. package/skills/conductor-device-interact/SKILL.md +4 -3
  52. package/skills/conductor-device-setup/SKILL.md +59 -28
@@ -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, '&amp;')
119
+ .replace(/</g, '&lt;')
120
+ .replace(/>/g, '&gt;')
121
+ .replace(/"/g, '&quot;');
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(/&lt;/g, '<')
224
+ .replace(/&gt;/g, '>')
225
+ .replace(/&quot;/g, '"')
226
+ .replace(/&#39;/g, "'")
227
+ .replace(/&apos;/g, "'")
228
+ .replace(/&amp;/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;
@@ -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 Android TV; hardware buttons (Home, Lock, Power, Volume…) target iOS/Android.',
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: [{ value: 'ios' }, { value: 'android' }, { value: 'tvos' }, { value: 'web' }],
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: [{ value: 'ios' }, { value: 'android' }, { value: 'tvos' }, { value: 'web' }],
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
@@ -61,6 +61,8 @@ const memory_js_1 = require("./commands/memory.js");
61
61
  const metro_js_1 = require("./commands/metro.js");
62
62
  const clipboard_js_1 = require("./commands/clipboard.js");
63
63
  const options_js_1 = require("./commands/options.js");
64
+ const web_targets_js_1 = require("./commands/web-targets.js");
65
+ const session_js_2 = require("./session.js");
64
66
  const device_picker_js_1 = require("./device-picker.js");
65
67
  const update_check_js_1 = require("./update-check.js");
66
68
  const pkg_root_js_1 = require("./pkg-root.js");
@@ -123,11 +125,15 @@ const COMMAND_HELP = {
123
125
  clipboard: clipboard_js_1.HELP,
124
126
  paste: ' paste Trigger OS-level paste (or type clipboard on iOS)',
125
127
  'list-options': options_js_1.HELP,
128
+ 'web-targets': web_targets_js_1.HELP,
126
129
  };
127
130
  const OPTIONS_HELP = `Options:
128
131
  --device <id> Target device ID (also keys the session and daemon)
129
132
  --device-name <n> Target a booted device by name (resolved to ID from booted devices)
130
- --platform <p> Filter to devices of this platform (ios, android, tvos, web)
133
+ --platform <p> Filter to devices of this platform (ios, android, tvos, web, vega)
134
+ --cdp-url <url> Attach the web driver to an existing browser over CDP (e.g. an
135
+ Electron app started with --remote-debugging-port). Remembered per session.
136
+ --cdp-target <id> Pick which CDP page target to control (see \`conductor web-targets\`)
131
137
  --json Output as machine-readable JSON
132
138
  --options List valid values for a command's enumerated parameters and exit
133
139
  --verbose, -v Log daemon calls, fallbacks, and raw output
@@ -230,6 +236,9 @@ async function main() {
230
236
  'height',
231
237
  'user-agent',
232
238
  'color-scheme',
239
+ 'cdp-url',
240
+ 'cdp-target',
241
+ 'duration',
233
242
  ],
234
243
  alias: { h: 'help', v: 'verbose', V: 'version', o: 'output', y: 'yes' },
235
244
  });
@@ -274,6 +283,7 @@ async function main() {
274
283
  'metro',
275
284
  'workspace',
276
285
  'list-options',
286
+ 'web-targets',
277
287
  // `logs --list` and `logs --source metro` only query Metro on localhost — no device needed
278
288
  // `logs` always needs a device session — Metro discovery is device-scoped.
279
289
  // `daemon-stop --all` stops every daemon — no device needed
@@ -309,8 +319,38 @@ async function main() {
309
319
  explicitDevice ?? (await (0, device_picker_js_1.pickDevice)(argv['platform'])) ?? 'default';
310
320
  }
311
321
  }
322
+ // CDP attach settings (web only): --cdp-url/--cdp-target map to the env the daemon
323
+ // reads. Passing them once persists them to the session so later commands for the
324
+ // same --device don't need the flags; absent flags hydrate from the saved session.
325
+ const isWebSession = sessionName === 'web' || sessionName.startsWith('web:');
326
+ if (isWebSession && !NO_DEVICE_COMMANDS.has(command)) {
327
+ const cdpUrlFlag = argv['cdp-url'];
328
+ const cdpTargetFlag = argv['cdp-target'];
329
+ if (cdpUrlFlag || cdpTargetFlag) {
330
+ if (cdpUrlFlag)
331
+ process.env.CONDUCTOR_CDP_URL = cdpUrlFlag;
332
+ if (cdpTargetFlag)
333
+ process.env.CONDUCTOR_CDP_TARGET_ID = cdpTargetFlag;
334
+ await (0, session_js_2.updateSession)({
335
+ cdpUrl: process.env.CONDUCTOR_CDP_URL,
336
+ cdpTargetId: process.env.CONDUCTOR_CDP_TARGET_ID,
337
+ }, sessionName);
338
+ }
339
+ else {
340
+ const saved = await (0, session_js_2.getSession)(sessionName);
341
+ if (saved.cdpUrl && !process.env.CONDUCTOR_CDP_URL) {
342
+ process.env.CONDUCTOR_CDP_URL = saved.cdpUrl;
343
+ }
344
+ if (saved.cdpTargetId && !process.env.CONDUCTOR_CDP_TARGET_ID) {
345
+ process.env.CONDUCTOR_CDP_TARGET_ID = saved.cdpTargetId;
346
+ }
347
+ }
348
+ }
312
349
  let exitCode = 0;
313
350
  switch (command) {
351
+ case 'web-targets':
352
+ exitCode = await (0, web_targets_js_1.webTargets)(argv['cdp-url'], opts);
353
+ break;
314
354
  case 'start-device':
315
355
  exitCode = await (0, start_device_js_1.startDevice)(argv['platform'], opts, {
316
356
  osVersion: argv['os-version'],
@@ -432,7 +472,11 @@ async function main() {
432
472
  break;
433
473
  case 'press-key': {
434
474
  const key = rest[0] ?? '';
435
- exitCode = await (0, press_key_js_1.pressKey)(key, opts, sessionName);
475
+ const durationArg = argv['duration'];
476
+ exitCode = await (0, press_key_js_1.pressKey)(key, opts, sessionName, {
477
+ longPress: argv['long-press'],
478
+ duration: durationArg !== undefined ? Number(durationArg) : undefined,
479
+ });
436
480
  break;
437
481
  }
438
482
  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": "@houwert/conductor",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "CLI tool for mobile app interactions — optimized for AI agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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.