@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.
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 +20 -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 +2 -34
  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/cdp-discovery.js +108 -0
  37. package/dist/drivers/flow-runner.js +38 -8
  38. package/dist/drivers/ios.js +5 -2
  39. package/dist/drivers/log-sources/metro-discovery.js +43 -0
  40. package/dist/drivers/log-sources/vega.js +77 -0
  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 +18 -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 +23 -2
@@ -10,7 +10,7 @@ exports.pickSystemImage = pickSystemImage;
10
10
  exports.buildAvdmanagerCreateArgs = buildAvdmanagerCreateArgs;
11
11
  exports.startDevice = startDevice;
12
12
  exports.HELP = ` start-device
13
- --platform <ios|android|tvos|web> Boot a simulator/emulator, or start the web driver (Playwright)
13
+ --platform <ios|android|tvos|web|vega> Boot a simulator/emulator, start the web driver (Playwright), or boot/attach a Vega VVD
14
14
  --os-version <n> iOS/tvOS version (e.g. 18) or Android API level (e.g. 33)
15
15
  --avd <name> Android AVD name (default: first available; created if missing + --device-type)
16
16
  --name <name> Set a custom name on the device after creation (iOS/tvOS/web)
@@ -28,6 +28,7 @@ const protocol_js_1 = require("../daemon/protocol.js");
28
28
  const bootstrap_js_1 = require("../drivers/bootstrap.js");
29
29
  const output_js_1 = require("../output.js");
30
30
  const utils_js_1 = require("../utils.js");
31
+ const cli_js_1 = require("../drivers/vega/cli.js");
31
32
  const IOS_BOOT_TIMEOUT_MS = 120000;
32
33
  const ANDROID_BOOT_TIMEOUT_MS = 120000;
33
34
  const POLL_MS = 1000;
@@ -694,10 +695,64 @@ async function startWebDriver(opts, browser, name) {
694
695
  (0, output_js_1.printSuccess)(`Web driver ready: ${displayName} (${session})`, opts);
695
696
  return 0;
696
697
  }
698
+ // ── Vega (Amazon Fire TV) ───────────────────────────────────────────────────
699
+ const VEGA_BOOT_TIMEOUT_MS = 180000;
700
+ /** Pick the target VVD from a device list: by name if given, else first virtual, else first. */
701
+ function pickVegaDevice(devices, deviceName) {
702
+ return ((deviceName ? devices.find((d) => d.serial === deviceName) : undefined) ??
703
+ devices.find((d) => d.isVirtual) ??
704
+ devices[0]);
705
+ }
706
+ /**
707
+ * Boot (or attach to) a Vega Virtual Device. If one is already running we attach;
708
+ * otherwise we launch it via `vega virtual-device start` and poll `vega device
709
+ * list` until it appears — mirroring how the Android emulator is booted. Then we
710
+ * prewarm the log daemon.
711
+ */
712
+ async function startVega(opts, deviceName) {
713
+ const cli = new cli_js_1.VegaCli();
714
+ let devices;
715
+ try {
716
+ devices = await cli.listDevices();
717
+ }
718
+ catch {
719
+ (0, output_js_1.printError)('The Vega SDK CLI (`vega`/`kepler`) was not found. Install it and ensure it is on ' +
720
+ 'PATH (or set CONDUCTOR_VEGA_CLI), then retry.', opts);
721
+ return 1;
722
+ }
723
+ let device = pickVegaDevice(devices, deviceName);
724
+ if (!device) {
725
+ console.log(`No running Vega device — booting a VVD${deviceName ? ` (${deviceName})` : ''} ` +
726
+ `via \`vega virtual-device start\`...`);
727
+ try {
728
+ cli.spawnVirtualDeviceStart(deviceName);
729
+ }
730
+ catch (e) {
731
+ (0, output_js_1.printError)(`Failed to start a Vega Virtual Device: ${e instanceof Error ? e.message : String(e)}`, opts);
732
+ return 1;
733
+ }
734
+ const deadline = Date.now() + VEGA_BOOT_TIMEOUT_MS;
735
+ while (Date.now() < deadline && !device) {
736
+ await (0, utils_js_1.sleep)(POLL_MS);
737
+ const current = await cli.listDevices().catch(() => []);
738
+ device = pickVegaDevice(current, deviceName);
739
+ }
740
+ if (!device) {
741
+ (0, output_js_1.printError)(`Vega Virtual Device did not become ready within ${VEGA_BOOT_TIMEOUT_MS / 1000}s. ` +
742
+ `Check \`vega virtual-device list\` — you may need to create one first, or pass ` +
743
+ `--name <vvd>.`, opts);
744
+ return 1;
745
+ }
746
+ }
747
+ // Prewarm the daemon so device/Metro log collection is already running.
748
+ await (0, runner_js_1.prewarmDriver)(`vega:${device.serial}`);
749
+ (0, output_js_1.printSuccess)(`Vega device ready: ${device.serial}`, opts);
750
+ return 0;
751
+ }
697
752
  // ── Entry point ───────────────────────────────────────────────────────────────
698
753
  async function startDevice(platform, opts, flags) {
699
754
  if (!platform) {
700
- (0, output_js_1.printError)('start-device requires --platform ios|android|tvos|web', opts);
755
+ (0, output_js_1.printError)('start-device requires --platform ios|android|tvos|web|vega', opts);
701
756
  return 1;
702
757
  }
703
758
  switch (platform.toLowerCase()) {
@@ -709,8 +764,10 @@ async function startDevice(platform, opts, flags) {
709
764
  return startAndroid(flags.avd, opts, flags.deviceType, flags.osVersion, flags.systemImage);
710
765
  case 'web':
711
766
  return startWebDriver(opts, flags.browser, flags.name);
767
+ case 'vega':
768
+ return startVega(opts, flags.name);
712
769
  default:
713
- (0, output_js_1.printError)(`Unknown platform "${platform}". Use ios, android, tvos, or web.`, opts);
770
+ (0, output_js_1.printError)(`Unknown platform "${platform}". Use ios, android, tvos, web, or vega.`, opts);
714
771
  return 1;
715
772
  }
716
773
  }
@@ -9,6 +9,7 @@ const output_js_1 = require("../output.js");
9
9
  const ios_js_1 = require("../drivers/ios.js");
10
10
  const android_js_1 = require("../drivers/android.js");
11
11
  const web_js_1 = require("../drivers/web.js");
12
+ const vega_js_1 = require("../drivers/vega.js");
12
13
  async function stopApp(appId, opts = {}, sessionName = 'default') {
13
14
  const session = await (0, session_js_1.getSession)(sessionName);
14
15
  const resolvedAppId = appId ?? session.appId;
@@ -23,6 +24,9 @@ async function stopApp(appId, opts = {}, sessionName = 'default') {
23
24
  else if (driver instanceof web_js_1.WebDriver) {
24
25
  await driver.terminateApp();
25
26
  }
27
+ else if (driver instanceof vega_js_1.VegaDriver) {
28
+ await driver.stopApp(resolvedAppId);
29
+ }
26
30
  else if (driver instanceof android_js_1.AndroidDriver) {
27
31
  await driver.stopApp(resolvedAppId);
28
32
  }
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HELP = void 0;
4
4
  exports.stopDevice = stopDevice;
5
5
  exports.HELP = ` stop-device [<name-or-id>]
6
- --platform <ios|tvos|android|web> Scope to a single platform
6
+ --platform <ios|tvos|android|web|vega> Scope to a single platform
7
7
  --all Stop all booted simulators / running emulators / web sessions`;
8
8
  const runner_js_1 = require("../runner.js");
9
9
  const sdk_js_1 = require("../android/sdk.js");
@@ -35,6 +35,7 @@ async function stopDevice(nameOrId, opts, flags) {
35
35
  const includeTvOS = !platform || platform === 'tvos';
36
36
  const includeAndroid = !platform || platform === 'android';
37
37
  const includeWeb = !platform || platform === 'web';
38
+ const includeVega = !platform || platform === 'vega';
38
39
  const stopped = [];
39
40
  // ── --all mode ───────────────────────────────────────────────────────────
40
41
  if (flags.all) {
@@ -48,6 +49,8 @@ async function stopDevice(nameOrId, opts, flags) {
48
49
  continue;
49
50
  if (d.platform === 'web' && !includeWeb)
50
51
  continue;
52
+ if (d.platform === 'vega' && !includeVega)
53
+ continue;
51
54
  try {
52
55
  if (d.platform === 'ios' || d.platform === 'tvos') {
53
56
  await shutdownSimulator(d.id);
@@ -55,7 +58,8 @@ async function stopDevice(nameOrId, opts, flags) {
55
58
  else if (d.platform === 'android') {
56
59
  await killEmulator(d.id);
57
60
  }
58
- else if (d.platform === 'web') {
61
+ else if (d.platform === 'web' || d.platform === 'vega') {
62
+ // Vega VVD lifecycle is owned by Amazon's tooling — we only stop our log daemon.
59
63
  await (0, client_js_1.stopDaemon)(d.id);
60
64
  }
61
65
  stopped.push({ id: d.id, name: d.name, platform: d.platform });
@@ -96,7 +100,8 @@ async function stopDevice(nameOrId, opts, flags) {
96
100
  else if (match.platform === 'android') {
97
101
  await killEmulator(match.id);
98
102
  }
99
- else if (match.platform === 'web') {
103
+ else if (match.platform === 'web' || match.platform === 'vega') {
104
+ // Vega VVD lifecycle is owned by Amazon's tooling — we only stop our log daemon.
100
105
  await (0, client_js_1.stopDaemon)(match.id);
101
106
  }
102
107
  }
@@ -12,6 +12,7 @@ const output_js_1 = require("../output.js");
12
12
  const ios_js_1 = require("../drivers/ios.js");
13
13
  const android_js_1 = require("../drivers/android.js");
14
14
  const web_js_1 = require("../drivers/web.js");
15
+ const vega_js_1 = require("../drivers/vega.js");
15
16
  const utils_js_1 = require("../utils.js");
16
17
  function parseCoordPair(s) {
17
18
  const [xs, ys] = s.split(',').map((p) => p.trim());
@@ -70,7 +71,7 @@ async function swipe(direction, opts = {}, sessionName = 'default', flags = {})
70
71
  }
71
72
  await driver.swipe(startX, startY, endX, endY, durationMs);
72
73
  }
73
- else if (driver instanceof android_js_1.AndroidDriver) {
74
+ else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
74
75
  const { widthPixels: w, heightPixels: h } = await driver.deviceInfo();
75
76
  const durationMs = flags.duration ?? 500;
76
77
  if (flags.start && flags.end) {
@@ -22,6 +22,7 @@ const output_js_1 = require("../output.js");
22
22
  const ios_js_1 = require("../drivers/ios.js");
23
23
  const android_js_1 = require("../drivers/android.js");
24
24
  const web_js_1 = require("../drivers/web.js");
25
+ const vega_js_1 = require("../drivers/vega.js");
25
26
  const wait_js_1 = require("../drivers/wait.js");
26
27
  const direct_ios_selector_js_1 = require("../drivers/direct-ios-selector.js");
27
28
  const snapshot_store_js_1 = require("../snapshot-store.js");
@@ -68,6 +69,10 @@ async function tap(query, opts = {}, sessionName = 'default', flags = {}) {
68
69
  else if (driver instanceof web_js_1.WebDriver) {
69
70
  el = await (0, wait_js_1.waitForWebElement)(() => driver.viewHierarchy(), sel);
70
71
  }
72
+ else if (driver instanceof vega_js_1.VegaDriver) {
73
+ // Vega emits uiautomator-style XML, so it reuses the Android resolver.
74
+ el = await (0, wait_js_1.waitForAndroidElement)(() => driver.viewHierarchy(), sel);
75
+ }
71
76
  else if (driver instanceof android_js_1.AndroidDriver) {
72
77
  el = await (0, wait_js_1.waitForAndroidElement)(() => driver.viewHierarchy(), sel);
73
78
  }
@@ -75,14 +80,12 @@ async function tap(query, opts = {}, sessionName = 'default', flags = {}) {
75
80
  return;
76
81
  }
77
82
  if (flags.longPress) {
78
- if (driver instanceof ios_js_1.IOSDriver) {
79
- await driver.tap(el.centerX, el.centerY, 1.5);
80
- }
81
- else if (driver instanceof web_js_1.WebDriver) {
82
- await driver.tap(el.centerX, el.centerY, 1.5);
83
+ if (driver instanceof android_js_1.AndroidDriver) {
84
+ await driver.swipe(el.centerX, el.centerY, el.centerX, el.centerY, 1500);
83
85
  }
84
86
  else {
85
- await driver.swipe(el.centerX, el.centerY, el.centerX, el.centerY, 1500);
87
+ // iOS, web, and vega express a long press as a held tap.
88
+ await driver.tap(el.centerX, el.centerY, 1.5);
86
89
  }
87
90
  }
88
91
  else if (flags.doubleTap) {
@@ -8,6 +8,7 @@ const output_js_1 = require("../output.js");
8
8
  const ios_js_1 = require("../drivers/ios.js");
9
9
  const android_js_1 = require("../drivers/android.js");
10
10
  const web_js_1 = require("../drivers/web.js");
11
+ const vega_js_1 = require("../drivers/vega.js");
11
12
  async function uninstallApp(appId, opts = {}, sessionName = 'default') {
12
13
  if (!appId) {
13
14
  (0, output_js_1.printError)('uninstall-app requires <appId>', opts);
@@ -20,6 +21,9 @@ async function uninstallApp(appId, opts = {}, sessionName = 'default') {
20
21
  else if (driver instanceof web_js_1.WebDriver) {
21
22
  throw new Error('uninstall-app is not supported on web');
22
23
  }
24
+ else if (driver instanceof vega_js_1.VegaDriver) {
25
+ throw new Error('uninstall-app is not supported on vega (Amazon Fire TV)');
26
+ }
23
27
  else if (driver instanceof android_js_1.AndroidDriver) {
24
28
  await driver.uninstallApp(appId);
25
29
  }
@@ -1,7 +1,4 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.HELP = void 0;
7
4
  exports.webTargets = webTargets;
@@ -14,37 +11,8 @@ exports.webTargets = webTargets;
14
11
  * Playwright browser and works before any daemon session exists. Use the printed
15
12
  * target IDs with `--cdp-url` / `--cdp-target` to bind a session to a tile.
16
13
  */
17
- const http_1 = __importDefault(require("http"));
18
14
  const output_js_1 = require("../output.js");
19
- /** Derive the `http://host:port` base from a CDP URL (which may be ws:// or include a path). */
20
- function httpBase(cdpUrl) {
21
- const u = new URL(cdpUrl);
22
- const proto = u.protocol === 'https:' || u.protocol === 'wss:' ? 'https:' : 'http:';
23
- return `${proto}//${u.host}`;
24
- }
25
- function fetchTargets(cdpUrl) {
26
- const url = `${httpBase(cdpUrl)}/json/list`;
27
- return new Promise((resolve, reject) => {
28
- const req = http_1.default.get(url, (res) => {
29
- const chunks = [];
30
- res.on('data', (c) => chunks.push(c));
31
- res.on('end', () => {
32
- if ((res.statusCode ?? 0) >= 300) {
33
- reject(new Error(`HTTP ${res.statusCode} from ${url}`));
34
- return;
35
- }
36
- try {
37
- resolve(JSON.parse(Buffer.concat(chunks).toString('utf-8')));
38
- }
39
- catch (err) {
40
- reject(err);
41
- }
42
- });
43
- });
44
- req.setTimeout(5000, () => req.destroy(new Error(`Timed out fetching ${url}`)));
45
- req.on('error', reject);
46
- });
47
- }
15
+ const cdp_discovery_js_1 = require("../drivers/cdp-discovery.js");
48
16
  async function webTargets(cdpUrl, opts) {
49
17
  if (!cdpUrl) {
50
18
  console.error('web-targets requires --cdp-url <url> (e.g. --cdp-url http://127.0.0.1:9222).\n' +
@@ -53,7 +21,7 @@ async function webTargets(cdpUrl, opts) {
53
21
  }
54
22
  let targets;
55
23
  try {
56
- targets = await fetchTargets(cdpUrl);
24
+ targets = await (0, cdp_discovery_js_1.fetchCdpTargets)(cdpUrl);
57
25
  }
58
26
  catch (err) {
59
27
  console.error(`Could not reach CDP endpoint at ${cdpUrl}: ${err instanceof Error ? err.message : String(err)}`);
@@ -112,7 +112,10 @@ async function workspaceInfo(opts = {}) {
112
112
  const devices = await (0, list_devices_js_1.discoverBootedDevices)().catch(() => []);
113
113
  let metroPort = null;
114
114
  for (const d of devices) {
115
- if (d.platform !== 'ios' && d.platform !== 'tvos' && d.platform !== 'android')
115
+ if (d.platform !== 'ios' &&
116
+ d.platform !== 'tvos' &&
117
+ d.platform !== 'android' &&
118
+ d.platform !== 'vega')
116
119
  continue;
117
120
  const port = await (0, metro_discovery_js_1.discoverMetroPortForDevice)(d.platform, d.id).catch(() => null);
118
121
  if (port) {
@@ -32,6 +32,7 @@ const http_1 = __importDefault(require("http"));
32
32
  const types_js_1 = require("../drivers/log-sources/types.js");
33
33
  const ios_js_1 = require("../drivers/log-sources/ios.js");
34
34
  const android_js_1 = require("../drivers/log-sources/android.js");
35
+ const vega_js_1 = require("../drivers/log-sources/vega.js");
35
36
  const metro_js_1 = require("../drivers/log-sources/metro.js");
36
37
  const metro_discovery_js_1 = require("../drivers/log-sources/metro-discovery.js");
37
38
  const MAX_BUFFER = 5000;
@@ -75,7 +76,10 @@ class LogCollector {
75
76
  // Metro is always auto-discovered. Discovery retries forever with a
76
77
  // backoff ceiling — the app may be launched long after the daemon starts,
77
78
  // and the cost per attempt is tiny (a few spawns + one HTTP call).
78
- if (this.platform === 'ios' || this.platform === 'tvos' || this.platform === 'android') {
79
+ if (this.platform === 'ios' ||
80
+ this.platform === 'tvos' ||
81
+ this.platform === 'android' ||
82
+ this.platform === 'vega') {
79
83
  this.startMetroAutoDiscovery();
80
84
  }
81
85
  }
@@ -146,6 +150,10 @@ class LogCollector {
146
150
  else if (this.platform === 'android') {
147
151
  this.source = new android_js_1.AndroidLogSource(this.deviceId, this.appId);
148
152
  }
153
+ else if (this.platform === 'vega') {
154
+ // deviceId is `vega:<serial>`; VegaCli wants the bare serial.
155
+ this.source = new vega_js_1.VegaLogSource(this.deviceId.replace(/^vega:/, ''));
156
+ }
149
157
  else {
150
158
  return; // Unsupported platform for device log collection
151
159
  }
@@ -85,6 +85,9 @@ let _driverStartError = null;
85
85
  async function ensureDriverRunning() {
86
86
  if (_restartInProgress || !_driverStarted)
87
87
  return;
88
+ // Vega has no driver process/port to health-check — control is host-side via the CLI.
89
+ if (driverPlatform === 'vega')
90
+ return;
88
91
  let alive;
89
92
  if (driverPlatform === 'android') {
90
93
  const probe = new android_js_1.AndroidDriver(sessionName, driverPort);
@@ -197,6 +200,9 @@ async function main() {
197
200
  if (driverPlatform === 'tvos') {
198
201
  dlog('tvOS: leaving driver running to preserve app state');
199
202
  }
203
+ else if (driverPlatform === 'vega') {
204
+ dlog('vega: no driver process to stop (control is host-side via the CLI)');
205
+ }
200
206
  else if (driverPlatform === 'web') {
201
207
  dlog('Stopping web driver');
202
208
  try {
@@ -321,59 +327,14 @@ async function main() {
321
327
  driverPlatform = platform;
322
328
  driverPort = await (0, bootstrap_js_1.getDriverPort)(platform, sessionName);
323
329
  dlog(`Platform: ${platform}, port: ${driverPort}`);
324
- let driverAlive;
325
- if (platform === 'android') {
326
- const probe = new android_js_1.AndroidDriver(sessionName, driverPort);
327
- await probe.connect();
328
- driverAlive = await probe.isAlive().catch(() => false);
329
- probe.close();
330
- }
331
- else {
332
- // 'ios', 'tvos', and 'web' all use an HTTP server — port open = alive
333
- driverAlive = await (0, bootstrap_js_1.isPortOpen)(driverPort);
334
- }
335
- if (driverAlive) {
330
+ // Vega has no driver process — control is host-side via the vega CLI.
331
+ // The daemon exists only to collect device + Metro logs.
332
+ if (platform === 'vega') {
336
333
  _driverStarted = true;
337
- dlog(`Driver already running on port ${driverPort}`);
334
+ dlog('vega: no driver process to start; collecting logs only');
338
335
  }
339
336
  else {
340
- // Android: install APKs before starting the driver.
341
- // iOS/tvOS: xcodebuild installs silently via DependentProductPaths.
342
- // Web: ensure Playwright browser binary is installed.
343
- if (platform === 'android') {
344
- dlog(`Installing Android driver on ${sessionName}`);
345
- await (0, bootstrap_js_1.installDriver)(sessionName);
346
- dlog(`Driver installation complete`);
347
- }
348
- else if (platform === 'web' && !cdpUrl) {
349
- // Only install Playwright browser when launching standalone.
350
- // In CDP mode we attach to the host app's browser (e.g. Electron).
351
- const browser = (0, bootstrap_js_1.webBrowserName)(sessionName);
352
- await (0, bootstrap_js_1.ensurePlaywrightBrowser)(browser, dlog);
353
- }
354
- dlog(`Starting ${platform} driver on port ${driverPort}`);
355
- try {
356
- if (platform === 'ios') {
357
- await (0, bootstrap_js_1.startIOSDriver)(sessionName, driverPort);
358
- }
359
- else if (platform === 'tvos') {
360
- // First install — the runner takes foreground; ask it to hand
361
- // focus back to whatever app the user had open.
362
- await (0, bootstrap_js_1.startTvOSDriver)(sessionName, driverPort, /* restoreFocusAfterLaunch */ true);
363
- }
364
- else if (platform === 'web') {
365
- await (0, web_server_js_1.startWebServer)(driverPort, (0, bootstrap_js_1.webBrowserName)(sessionName), dlog, cdpUrl, cdpTargetId);
366
- }
367
- else {
368
- await (0, bootstrap_js_1.startAndroidDriver)(sessionName, driverPort);
369
- }
370
- _driverStarted = true;
371
- dlog(`Driver started successfully`);
372
- }
373
- catch (err) {
374
- _driverStartError = err instanceof Error ? err.message : String(err);
375
- dlog(`Driver startup error: ${_driverStartError}`);
376
- }
337
+ await startDriverForPlatform(platform);
377
338
  }
378
339
  // Start collecting logs once the driver is (or was already) running.
379
340
  if (_driverStarted) {
@@ -394,6 +355,65 @@ async function main() {
394
355
  }
395
356
  });
396
357
  }
358
+ /**
359
+ * Bring up the driver process for a non-vega platform. Sets `_driverStarted` /
360
+ * `_driverStartError`. Extracted so the vega path can skip it entirely.
361
+ */
362
+ async function startDriverForPlatform(platform) {
363
+ let driverAlive;
364
+ if (platform === 'android') {
365
+ const probe = new android_js_1.AndroidDriver(sessionName, driverPort);
366
+ await probe.connect();
367
+ driverAlive = await probe.isAlive().catch(() => false);
368
+ probe.close();
369
+ }
370
+ else {
371
+ // 'ios', 'tvos', and 'web' all use an HTTP server — port open = alive
372
+ driverAlive = await (0, bootstrap_js_1.isPortOpen)(driverPort);
373
+ }
374
+ if (driverAlive) {
375
+ _driverStarted = true;
376
+ dlog(`Driver already running on port ${driverPort}`);
377
+ return;
378
+ }
379
+ // Android: install APKs before starting the driver.
380
+ // iOS/tvOS: xcodebuild installs silently via DependentProductPaths.
381
+ // Web: ensure Playwright browser binary is installed.
382
+ if (platform === 'android') {
383
+ dlog(`Installing Android driver on ${sessionName}`);
384
+ await (0, bootstrap_js_1.installDriver)(sessionName);
385
+ dlog(`Driver installation complete`);
386
+ }
387
+ else if (platform === 'web' && !cdpUrl) {
388
+ // Only install Playwright browser when launching standalone.
389
+ // In CDP mode we attach to the host app's browser (e.g. Electron).
390
+ const browser = (0, bootstrap_js_1.webBrowserName)(sessionName);
391
+ await (0, bootstrap_js_1.ensurePlaywrightBrowser)(browser, dlog);
392
+ }
393
+ dlog(`Starting ${platform} driver on port ${driverPort}`);
394
+ try {
395
+ if (platform === 'ios') {
396
+ await (0, bootstrap_js_1.startIOSDriver)(sessionName, driverPort);
397
+ }
398
+ else if (platform === 'tvos') {
399
+ // First install — the runner takes foreground; ask it to hand
400
+ // focus back to whatever app the user had open.
401
+ await (0, bootstrap_js_1.startTvOSDriver)(sessionName, driverPort, /* restoreFocusAfterLaunch */ true);
402
+ }
403
+ else if (platform === 'web') {
404
+ await (0, web_server_js_1.startWebServer)(driverPort, (0, bootstrap_js_1.webBrowserName)(sessionName), dlog, cdpUrl, cdpTargetId);
405
+ }
406
+ else {
407
+ await (0, bootstrap_js_1.startAndroidDriver)(sessionName, driverPort);
408
+ }
409
+ _driverStarted = true;
410
+ dlog(`Driver started successfully`);
411
+ }
412
+ catch (err) {
413
+ _driverStartError = err instanceof Error ? err.message : String(err);
414
+ dlog(`Driver startup error: ${_driverStartError}`);
415
+ }
416
+ }
397
417
  main().catch((err) => {
398
418
  console.error('Daemon error:', err);
399
419
  process.exit(1);
@@ -53,6 +53,11 @@ async function detectPlatform(deviceId) {
53
53
  _platformCache.set(deviceId, 'web');
54
54
  return 'web';
55
55
  }
56
+ // Vega (Amazon Fire TV): "vega:<serial>" (e.g. "vega:VirtualDevice")
57
+ if (deviceId === 'vega' || deviceId.startsWith('vega:')) {
58
+ _platformCache.set(deviceId, 'vega');
59
+ return 'vega';
60
+ }
56
61
  // Check if it looks like an iOS/tvOS simulator UUID (8-4-4-4-12 hex chars)
57
62
  const iosUuidRe = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
58
63
  if (iosUuidRe.test(deviceId)) {
@@ -83,6 +88,7 @@ const IOS_BASE_PORT = 1075;
83
88
  const TVOS_BASE_PORT = 2075;
84
89
  const ANDROID_BASE_PORT = 3763;
85
90
  const WEB_BASE_PORT = 4075;
91
+ const VEGA_BASE_PORT = 5075;
86
92
  const PORT_FILE = path_1.default.join(os_1.default.homedir(), '.conductor', 'ports.json');
87
93
  const PORT_LOCK = PORT_FILE + '.lock';
88
94
  const PORT_LOCK_TIMEOUT_MS = 5000;
@@ -97,6 +103,7 @@ function readPortState() {
97
103
  nextTvosPort: TVOS_BASE_PORT,
98
104
  nextAndroidPort: ANDROID_BASE_PORT,
99
105
  nextWebPort: WEB_BASE_PORT,
106
+ nextVegaPort: VEGA_BASE_PORT,
100
107
  };
101
108
  }
102
109
  }
@@ -145,6 +152,8 @@ async function getDriverPort(platform, deviceId) {
145
152
  state.nextTvosPort = TVOS_BASE_PORT;
146
153
  if (state.nextWebPort === undefined)
147
154
  state.nextWebPort = WEB_BASE_PORT;
155
+ if (state.nextVegaPort === undefined)
156
+ state.nextVegaPort = VEGA_BASE_PORT;
148
157
  let port;
149
158
  if (platform === 'ios') {
150
159
  port = state.nextIosPort++;
@@ -155,6 +164,9 @@ async function getDriverPort(platform, deviceId) {
155
164
  else if (platform === 'web') {
156
165
  port = state.nextWebPort++;
157
166
  }
167
+ else if (platform === 'vega') {
168
+ port = state.nextVegaPort++;
169
+ }
158
170
  else {
159
171
  port = state.nextAndroidPort++;
160
172
  }
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DEFAULT_CDP_PORTS = void 0;
7
+ exports.httpBase = httpBase;
8
+ exports.fetchCdpTargets = fetchCdpTargets;
9
+ exports.formatCdpDeviceId = formatCdpDeviceId;
10
+ exports.parseCdpDeviceId = parseCdpDeviceId;
11
+ exports.cdpTargetsToDevices = cdpTargetsToDevices;
12
+ exports.discoverCdpDevices = discoverCdpDevices;
13
+ /**
14
+ * Discovery of externally-launched CDP endpoints (e.g. an Electron app started
15
+ * with `--remote-debugging-port`, which exposes one page target per webview/tile).
16
+ *
17
+ * Unlike the Playwright web driver (which conductor launches itself) these
18
+ * browsers already exist — so "discovery" means finding the DevTools HTTP
19
+ * endpoint. CDP servers don't advertise themselves, so we probe a small range of
20
+ * localhost ports and enumerate each reachable endpoint's page targets via
21
+ * `/json/list`. The same target-fetch is reused by the `web-targets` command.
22
+ */
23
+ const http_1 = __importDefault(require("http"));
24
+ /** Default localhost ports scanned for CDP endpoints. Covers Chromium/Electron's
25
+ * conventional `--remote-debugging-port` values without a wide scan. */
26
+ exports.DEFAULT_CDP_PORTS = [9222, 9223, 9224, 9225, 9226, 9227, 9228, 9229];
27
+ const PROBE_TIMEOUT_MS = 300;
28
+ /** Derive the `http://host:port` base from a CDP URL (which may be ws:// or include a path). */
29
+ function httpBase(cdpUrl) {
30
+ const u = new URL(cdpUrl);
31
+ const proto = u.protocol === 'https:' || u.protocol === 'wss:' ? 'https:' : 'http:';
32
+ return `${proto}//${u.host}`;
33
+ }
34
+ /** GET a CDP DevTools JSON endpoint, parsing the array response. */
35
+ function getJson(url, timeoutMs) {
36
+ return new Promise((resolve, reject) => {
37
+ const req = http_1.default.get(url, (res) => {
38
+ const chunks = [];
39
+ res.on('data', (c) => chunks.push(c));
40
+ res.on('end', () => {
41
+ if ((res.statusCode ?? 0) >= 300) {
42
+ reject(new Error(`HTTP ${res.statusCode} from ${url}`));
43
+ return;
44
+ }
45
+ try {
46
+ resolve(JSON.parse(Buffer.concat(chunks).toString('utf-8')));
47
+ }
48
+ catch (err) {
49
+ reject(err);
50
+ }
51
+ });
52
+ });
53
+ req.setTimeout(timeoutMs, () => req.destroy(new Error(`Timed out fetching ${url}`)));
54
+ req.on('error', reject);
55
+ });
56
+ }
57
+ /** Fetch the CDP page targets exposed at `cdpUrl`'s `/json/list`. */
58
+ function fetchCdpTargets(cdpUrl, timeoutMs = 5000) {
59
+ return getJson(`${httpBase(cdpUrl)}/json/list`, timeoutMs);
60
+ }
61
+ /**
62
+ * Device-id encoding for a discovered CDP webview: `web:cdp:<port>:<targetId>`.
63
+ * Self-describing so the id alone hydrates the CDP url + target at bind time —
64
+ * a discovered webview is drivable with no `--cdp-*` flags. Localhost is assumed
65
+ * (discovery only scans localhost).
66
+ */
67
+ function formatCdpDeviceId(port, targetId) {
68
+ return `web:cdp:${port}:${targetId}`;
69
+ }
70
+ /** Parse a `web:cdp:<port>:<targetId>` device id, or undefined if it isn't one. */
71
+ function parseCdpDeviceId(deviceId) {
72
+ const m = /^web:cdp:(\d+):(.+)$/.exec(deviceId);
73
+ if (!m)
74
+ return undefined;
75
+ const port = Number(m[1]);
76
+ if (!Number.isInteger(port) || port <= 0)
77
+ return undefined;
78
+ return { port, targetId: m[2], cdpUrl: `http://127.0.0.1:${port}` };
79
+ }
80
+ /** Map a reachable endpoint's page targets to discovered `web` devices. */
81
+ function cdpTargetsToDevices(port, targets) {
82
+ return targets
83
+ .filter((t) => t.type === 'page')
84
+ .map((t) => ({
85
+ id: formatCdpDeviceId(port, t.id),
86
+ name: t.title || t.url || t.id,
87
+ platform: 'web',
88
+ status: 'running',
89
+ }));
90
+ }
91
+ /**
92
+ * Scan localhost CDP ports and return every discovered webview as a device.
93
+ * Probes run in parallel with a short timeout and swallow all errors — an
94
+ * unreachable port simply contributes nothing. Safe to call on the hot device-
95
+ * resolution path.
96
+ */
97
+ async function discoverCdpDevices(ports = exports.DEFAULT_CDP_PORTS) {
98
+ const results = await Promise.all(ports.map(async (port) => {
99
+ try {
100
+ const targets = await fetchCdpTargets(`http://127.0.0.1:${port}`, PROBE_TIMEOUT_MS);
101
+ return cdpTargetsToDevices(port, targets);
102
+ }
103
+ catch {
104
+ return [];
105
+ }
106
+ }));
107
+ return results.flat();
108
+ }