@houwert/conductor 0.29.3 → 0.31.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 (53) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/assert-not-visible.js +4 -1
  3. package/dist/commands/assert-visible.js +5 -2
  4. package/dist/commands/back.js +2 -1
  5. package/dist/commands/capture-ui.js +7 -3
  6. package/dist/commands/clipboard.js +9 -0
  7. package/dist/commands/copy-text-from.js +2 -1
  8. package/dist/commands/crashes.js +5 -0
  9. package/dist/commands/delete-device.js +5 -0
  10. package/dist/commands/download-app.js +4 -0
  11. package/dist/commands/erase-text.js +4 -1
  12. package/dist/commands/focused.js +5 -2
  13. package/dist/commands/foreground-app.js +4 -1
  14. package/dist/commands/gestures.js +7 -0
  15. package/dist/commands/hide-keyboard.js +5 -0
  16. package/dist/commands/inspect.js +10 -3
  17. package/dist/commands/install-app.js +21 -4
  18. package/dist/commands/launch-app.js +3 -2
  19. package/dist/commands/list-apps.js +16 -0
  20. package/dist/commands/list-devices.js +37 -0
  21. package/dist/commands/memory.js +5 -0
  22. package/dist/commands/press-key.js +36 -3
  23. package/dist/commands/profile.js +4 -0
  24. package/dist/commands/screenshot.js +5 -2
  25. package/dist/commands/scroll-until-visible.js +5 -2
  26. package/dist/commands/scroll.js +4 -1
  27. package/dist/commands/start-device.js +27 -3
  28. package/dist/commands/stop-app.js +2 -1
  29. package/dist/commands/stop-device.js +25 -3
  30. package/dist/commands/swipe.js +8 -3
  31. package/dist/commands/tap.js +3 -2
  32. package/dist/commands/uninstall-app.js +4 -0
  33. package/dist/daemon/input-backends.js +26 -1
  34. package/dist/daemon/log-collector.js +6 -0
  35. package/dist/daemon/server.js +54 -11
  36. package/dist/drivers/bootstrap.js +263 -4
  37. package/dist/drivers/devicectl.js +243 -0
  38. package/dist/drivers/flow-runner.js +26 -4
  39. package/dist/drivers/ios.js +96 -4
  40. package/dist/drivers/roku/app-ui-parser.js +122 -0
  41. package/dist/drivers/roku/discovery.js +136 -0
  42. package/dist/drivers/roku/ecp-client.js +396 -0
  43. package/dist/drivers/roku/key-mapping.js +67 -0
  44. package/dist/drivers/roku.js +237 -0
  45. package/dist/drivers/vega/page-source-parser.js +5 -118
  46. package/dist/drivers/xml.js +128 -0
  47. package/dist/enum-options.js +3 -1
  48. package/dist/index.js +1 -1
  49. package/dist/runner.js +48 -8
  50. package/package.json +1 -1
  51. package/skills/conductor-device-interact/SKILL.md +14 -3
  52. package/skills/conductor-device-setup/SKILL.md +62 -2
  53. package/skills/conductor-profiler/SKILL.md +1 -1
@@ -28,6 +28,7 @@ const ios_js_1 = require("../drivers/ios.js");
28
28
  const android_js_1 = require("../drivers/android.js");
29
29
  const web_js_1 = require("../drivers/web.js");
30
30
  const vega_js_1 = require("../drivers/vega.js");
31
+ const roku_js_1 = require("../drivers/roku.js");
31
32
  const wait_js_1 = require("../drivers/wait.js");
32
33
  const direct_ios_selector_js_1 = require("../drivers/direct-ios-selector.js");
33
34
  const png_crop_js_1 = require("../png-crop.js");
@@ -85,8 +86,10 @@ async function screenshot(outputPath, opts = {}, sessionName = 'default', fullPa
85
86
  hierarchyH = info.heightPixels;
86
87
  el = await (0, wait_js_1.waitForWebElement)(() => driver.viewHierarchy(), sel);
87
88
  }
88
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
89
- // Vega emits uiautomator-style XML, so it reuses the Android resolver.
89
+ else if (driver instanceof android_js_1.AndroidDriver ||
90
+ driver instanceof vega_js_1.VegaDriver ||
91
+ driver instanceof roku_js_1.RokuDriver) {
92
+ // Vega and Roku emit uiautomator-style XML, so they reuse the Android resolver.
90
93
  const xml = await driver.viewHierarchy();
91
94
  // Android XML root bounds: derive from the first parseable <node bounds="[0,0][W,H]">
92
95
  const m = xml.match(/<node[^>]*bounds="\[0,0\]\[(\d+),(\d+)\]"/);
@@ -13,6 +13,7 @@ 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
15
  const vega_js_1 = require("../drivers/vega.js");
16
+ const roku_js_1 = require("../drivers/roku.js");
16
17
  const element_resolver_js_1 = require("../drivers/element-resolver.js");
17
18
  const direct_ios_selector_js_1 = require("../drivers/direct-ios-selector.js");
18
19
  const utils_js_1 = require("../utils.js");
@@ -78,8 +79,10 @@ async function scrollUntilVisible(element, opts = {}, sessionName = 'default', f
78
79
  const { widthPixels: w, heightPixels: h } = info;
79
80
  await driver.swipe(coords.startX * w, coords.startY * h, coords.endX * w, coords.endY * h, 500);
80
81
  }
81
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
82
- // Vega emits uiautomator-style XML, so it reuses the Android resolver.
82
+ else if (driver instanceof android_js_1.AndroidDriver ||
83
+ driver instanceof vega_js_1.VegaDriver ||
84
+ driver instanceof roku_js_1.RokuDriver) {
85
+ // Vega and Roku emit uiautomator-style XML, so they reuse the Android resolver.
83
86
  const xml = await driver.viewHierarchy();
84
87
  if ((0, element_resolver_js_1.findAndroidElement)(xml, sel)) {
85
88
  (0, output_js_1.printSuccess)(`scroll-until-visible ${label} — found`, opts);
@@ -9,6 +9,7 @@ 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
11
  const vega_js_1 = require("../drivers/vega.js");
12
+ const roku_js_1 = require("../drivers/roku.js");
12
13
  const utils_js_1 = require("../utils.js");
13
14
  async function scroll(direction = 'down', opts = {}, sessionName = 'default') {
14
15
  const valid = ['down', 'up', 'left', 'right'];
@@ -32,7 +33,9 @@ async function scroll(direction = 'down', opts = {}, sessionName = 'default') {
32
33
  const { widthPixels: w, heightPixels: h } = info;
33
34
  await driver.swipe(coords.startX * w, coords.startY * h, coords.endX * w, coords.endY * h, 500);
34
35
  }
35
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
36
+ else if (driver instanceof android_js_1.AndroidDriver ||
37
+ driver instanceof vega_js_1.VegaDriver ||
38
+ driver instanceof roku_js_1.RokuDriver) {
36
39
  const info = await driver.deviceInfo();
37
40
  const { widthPixels: w, heightPixels: h } = info;
38
41
  await driver.swipe(coords.startX * w, coords.startY * h, coords.endX * w, coords.endY * h, 500);
@@ -11,7 +11,7 @@ exports.buildAvdmanagerCreateArgs = buildAvdmanagerCreateArgs;
11
11
  exports.raiseAvdConfigRam = raiseAvdConfigRam;
12
12
  exports.startDevice = startDevice;
13
13
  exports.HELP = ` start-device
14
- --platform <ios|android|tvos|web|vega> Boot a simulator/emulator, start the web driver (Playwright), or boot/attach a Vega VVD
14
+ --platform <ios|android|tvos|web|vega|roku> Boot a simulator/emulator, start the web driver (Playwright), attach a Vega VVD, or check a Roku device
15
15
  --os-version <n> iOS/tvOS version (e.g. 18) or Android API level (e.g. 33)
16
16
  --avd <name> Android AVD name (default: first available; created if missing + --device-type)
17
17
  --name <name> Set a custom name on the device after creation (iOS/tvOS/web)
@@ -34,6 +34,7 @@ const bootstrap_js_1 = require("../drivers/bootstrap.js");
34
34
  const output_js_1 = require("../output.js");
35
35
  const utils_js_1 = require("../utils.js");
36
36
  const cli_js_1 = require("../drivers/vega/cli.js");
37
+ const discovery_js_1 = require("../drivers/roku/discovery.js");
37
38
  const IOS_BOOT_TIMEOUT_MS = 120000;
38
39
  const ANDROID_BOOT_TIMEOUT_MS = 120000;
39
40
  const POLL_MS = 1000;
@@ -807,10 +808,31 @@ async function startVega(opts, deviceName) {
807
808
  (0, output_js_1.printSuccess)(`Vega device ready: ${device.serial}`, opts);
808
809
  return 0;
809
810
  }
811
+ // ── Roku ────────────────────────────────────────────────────────────────────
812
+ /**
813
+ * Roku is physical hardware with no emulator, so there is nothing to boot: this
814
+ * resolves the target device (explicit `--name <host>`, else `CONDUCTOR_ROKU_HOST`
815
+ * or an SSDP hit) and confirms it answers on ECP, so a misconfigured device is
816
+ * reported here rather than on the first `tap-on`.
817
+ */
818
+ async function startRoku(opts, deviceName) {
819
+ const host = deviceName?.replace(/^roku:/, '') ?? process.env.CONDUCTOR_ROKU_HOST?.trim();
820
+ const device = host ? await (0, discovery_js_1.describe)(host) : (await (0, discovery_js_1.discoverRokuDevices)())[0];
821
+ if (!device) {
822
+ (0, output_js_1.printError)(host
823
+ ? `Cannot reach the Roku device at ${host} on ECP port 8060. Check that it is on ` +
824
+ `this network and in developer mode.`
825
+ : 'No Roku device found. Set CONDUCTOR_ROKU_HOST=<device-ip>, pass --name <device-ip>, ' +
826
+ 'or set CONDUCTOR_ROKU_DISCOVERY=true to scan the LAN.', opts);
827
+ return 1;
828
+ }
829
+ (0, output_js_1.printSuccess)(`Roku device ready: ${device.friendlyName || device.modelName} (roku:${device.host})`, opts);
830
+ return 0;
831
+ }
810
832
  // ── Entry point ───────────────────────────────────────────────────────────────
811
833
  async function startDevice(platform, opts, flags) {
812
834
  if (!platform) {
813
- (0, output_js_1.printError)('start-device requires --platform ios|android|tvos|web|vega', opts);
835
+ (0, output_js_1.printError)('start-device requires --platform ios|android|tvos|web|vega|roku', opts);
814
836
  return 1;
815
837
  }
816
838
  switch (platform.toLowerCase()) {
@@ -824,8 +846,10 @@ async function startDevice(platform, opts, flags) {
824
846
  return startWebDriver(opts, flags.browser, flags.name);
825
847
  case 'vega':
826
848
  return startVega(opts, flags.name);
849
+ case 'roku':
850
+ return startRoku(opts, flags.name);
827
851
  default:
828
- (0, output_js_1.printError)(`Unknown platform "${platform}". Use ios, android, tvos, web, or vega.`, opts);
852
+ (0, output_js_1.printError)(`Unknown platform "${platform}". Use ios, android, tvos, web, vega, or roku.`, opts);
829
853
  return 1;
830
854
  }
831
855
  }
@@ -10,6 +10,7 @@ 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
12
  const vega_js_1 = require("../drivers/vega.js");
13
+ const roku_js_1 = require("../drivers/roku.js");
13
14
  async function stopApp(appId, opts = {}, sessionName = 'default') {
14
15
  const session = await (0, session_js_1.getSession)(sessionName);
15
16
  const resolvedAppId = appId ?? session.appId;
@@ -24,7 +25,7 @@ async function stopApp(appId, opts = {}, sessionName = 'default') {
24
25
  else if (driver instanceof web_js_1.WebDriver) {
25
26
  await driver.terminateApp();
26
27
  }
27
- else if (driver instanceof vega_js_1.VegaDriver) {
28
+ else if (driver instanceof vega_js_1.VegaDriver || driver instanceof roku_js_1.RokuDriver) {
28
29
  await driver.stopApp(resolvedAppId);
29
30
  }
30
31
  else if (driver instanceof android_js_1.AndroidDriver) {
@@ -3,13 +3,14 @@ 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|vega> Scope to a single platform
6
+ --platform <ios|tvos|android|web|vega|roku> 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");
10
10
  const output_js_1 = require("../output.js");
11
11
  const client_js_1 = require("../daemon/client.js");
12
12
  const list_devices_js_1 = require("./list-devices.js");
13
+ const bootstrap_js_1 = require("../drivers/bootstrap.js");
13
14
  // ── iOS / tvOS ───────────────────────────────────────────────────────────────
14
15
  async function shutdownSimulator(udid) {
15
16
  const result = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'shutdown', udid]);
@@ -36,6 +37,7 @@ async function stopDevice(nameOrId, opts, flags) {
36
37
  const includeAndroid = !platform || platform === 'android';
37
38
  const includeWeb = !platform || platform === 'web';
38
39
  const includeVega = !platform || platform === 'vega';
40
+ const includeRoku = !platform || platform === 'roku';
39
41
  const stopped = [];
40
42
  // ── --all mode ───────────────────────────────────────────────────────────
41
43
  if (flags.all) {
@@ -51,8 +53,15 @@ async function stopDevice(nameOrId, opts, flags) {
51
53
  continue;
52
54
  if (d.platform === 'vega' && !includeVega)
53
55
  continue;
56
+ if (d.platform === 'roku' && !includeRoku)
57
+ continue;
54
58
  try {
55
- if (d.platform === 'ios' || d.platform === 'tvos') {
59
+ if ((d.platform === 'ios' || d.platform === 'tvos') &&
60
+ (await (0, bootstrap_js_1.detectDeviceKind)(d.id)) === 'physical') {
61
+ // Real hardware can't be shut down from here — just release our driver.
62
+ await (0, client_js_1.stopDaemon)(d.id);
63
+ }
64
+ else if (d.platform === 'ios' || d.platform === 'tvos') {
56
65
  await shutdownSimulator(d.id);
57
66
  }
58
67
  else if (d.platform === 'android') {
@@ -62,6 +71,10 @@ async function stopDevice(nameOrId, opts, flags) {
62
71
  // Vega VVD lifecycle is owned by Amazon's tooling — we only stop our log daemon.
63
72
  await (0, client_js_1.stopDaemon)(d.id);
64
73
  }
74
+ else if (d.platform === 'roku') {
75
+ // A Roku is physical hardware we never booted; there is nothing to stop.
76
+ continue;
77
+ }
65
78
  stopped.push({ id: d.id, name: d.name, platform: d.platform });
66
79
  }
67
80
  catch (e) {
@@ -94,7 +107,12 @@ async function stopDevice(nameOrId, opts, flags) {
94
107
  return 1;
95
108
  }
96
109
  try {
97
- if (match.platform === 'ios' || match.platform === 'tvos') {
110
+ if ((match.platform === 'ios' || match.platform === 'tvos') &&
111
+ (await (0, bootstrap_js_1.detectDeviceKind)(match.id)) === 'physical') {
112
+ // Real hardware can't be shut down from here — just release our driver.
113
+ await (0, client_js_1.stopDaemon)(match.id);
114
+ }
115
+ else if (match.platform === 'ios' || match.platform === 'tvos') {
98
116
  await shutdownSimulator(match.id);
99
117
  }
100
118
  else if (match.platform === 'android') {
@@ -104,6 +122,10 @@ async function stopDevice(nameOrId, opts, flags) {
104
122
  // Vega VVD lifecycle is owned by Amazon's tooling — we only stop our log daemon.
105
123
  await (0, client_js_1.stopDaemon)(match.id);
106
124
  }
125
+ else if (match.platform === 'roku') {
126
+ (0, output_js_1.printError)(`${match.name} is a physical Roku device — conductor never booted it, so there is nothing to stop.`, opts);
127
+ return 1;
128
+ }
107
129
  }
108
130
  catch (e) {
109
131
  (0, output_js_1.printError)(e instanceof Error ? e.message : String(e), opts);
@@ -13,6 +13,7 @@ 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
15
  const vega_js_1 = require("../drivers/vega.js");
16
+ const roku_js_1 = require("../drivers/roku.js");
16
17
  const utils_js_1 = require("../utils.js");
17
18
  function parseCoordPair(s) {
18
19
  const [xs, ys] = s.split(',').map((p) => p.trim());
@@ -25,8 +26,10 @@ async function swipe(direction, opts = {}, sessionName = 'default', flags = {})
25
26
  }
26
27
  const result = await (0, runner_js_1.runDirect)(async (driver) => {
27
28
  if (driver instanceof ios_js_1.IOSDriver && driver.platform === 'tvos') {
28
- throw new Error('swipe is not supported on tvOS — Apple TV uses focus-based navigation.\n' +
29
- 'Use press-key to navigate (e.g. conductor press-key left).');
29
+ throw new Error('swipe is not supported on tvOS — XCTest has no Siri Remote touch-surface\n' +
30
+ 'gesture ("Swipe events are only implemented for iOS, visionOS, and watchOS").\n' +
31
+ 'Navigate with press-key "Remote Dpad Left"/"Right"/"Up"/"Down", or cover\n' +
32
+ 'long lists faster with press-key "Remote Page Up"/"Remote Page Down".');
30
33
  }
31
34
  let startX, startY, endX, endY;
32
35
  if (driver instanceof ios_js_1.IOSDriver) {
@@ -71,7 +74,9 @@ async function swipe(direction, opts = {}, sessionName = 'default', flags = {})
71
74
  }
72
75
  await driver.swipe(startX, startY, endX, endY, durationMs);
73
76
  }
74
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
77
+ else if (driver instanceof android_js_1.AndroidDriver ||
78
+ driver instanceof vega_js_1.VegaDriver ||
79
+ driver instanceof roku_js_1.RokuDriver) {
75
80
  const { widthPixels: w, heightPixels: h } = await driver.deviceInfo();
76
81
  const durationMs = flags.duration ?? 500;
77
82
  if (flags.start && flags.end) {
@@ -28,6 +28,7 @@ const ios_js_1 = require("../drivers/ios.js");
28
28
  const android_js_1 = require("../drivers/android.js");
29
29
  const web_js_1 = require("../drivers/web.js");
30
30
  const vega_js_1 = require("../drivers/vega.js");
31
+ const roku_js_1 = require("../drivers/roku.js");
31
32
  const wait_js_1 = require("../drivers/wait.js");
32
33
  const direct_ios_selector_js_1 = require("../drivers/direct-ios-selector.js");
33
34
  const snapshot_store_js_1 = require("../snapshot-store.js");
@@ -88,8 +89,8 @@ async function tap(query, opts = {}, sessionName = 'default', flags = {}) {
88
89
  else if (driver instanceof web_js_1.WebDriver) {
89
90
  el = await (0, wait_js_1.waitForWebElement)(() => driver.viewHierarchy(), sel);
90
91
  }
91
- else if (driver instanceof vega_js_1.VegaDriver) {
92
- // Vega emits uiautomator-style XML, so it reuses the Android resolver.
92
+ else if (driver instanceof vega_js_1.VegaDriver || driver instanceof roku_js_1.RokuDriver) {
93
+ // Vega and Roku emit uiautomator-style XML, so they reuse the Android resolver.
93
94
  el = await (0, wait_js_1.waitForAndroidElement)(() => driver.viewHierarchy(), sel);
94
95
  }
95
96
  else if (driver instanceof android_js_1.AndroidDriver) {
@@ -9,6 +9,7 @@ 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
11
  const vega_js_1 = require("../drivers/vega.js");
12
+ const roku_js_1 = require("../drivers/roku.js");
12
13
  async function uninstallApp(appId, opts = {}, sessionName = 'default') {
13
14
  if (!appId) {
14
15
  (0, output_js_1.printError)('uninstall-app requires <appId>', opts);
@@ -24,6 +25,9 @@ async function uninstallApp(appId, opts = {}, sessionName = 'default') {
24
25
  else if (driver instanceof vega_js_1.VegaDriver) {
25
26
  throw new Error('uninstall-app is not supported on vega (Amazon Fire TV)');
26
27
  }
28
+ else if (driver instanceof roku_js_1.RokuDriver) {
29
+ throw new Error('uninstall-app is not supported on roku');
30
+ }
27
31
  else if (driver instanceof android_js_1.AndroidDriver) {
28
32
  await driver.uninstallApp(appId);
29
33
  }
@@ -34,6 +34,15 @@ const IOS_BUTTON = {
34
34
  ArrowDown: 'down',
35
35
  ArrowLeft: 'left',
36
36
  ArrowRight: 'right',
37
+ // Newer remote buttons; the driver rejects these on older tvOS.
38
+ pageUp: 'pageUp',
39
+ PageUp: 'pageUp',
40
+ pageDown: 'pageDown',
41
+ PageDown: 'pageDown',
42
+ guide: 'guide',
43
+ tvProvider: 'tvProvider',
44
+ oneTwoThree: 'oneTwoThree',
45
+ fourColors: 'fourColors',
37
46
  };
38
47
  function iosBackend(driver) {
39
48
  let size = null;
@@ -52,7 +61,23 @@ function iosBackend(driver) {
52
61
  touch: true,
53
62
  drag: true,
54
63
  multitouch: true,
55
- buttons: ['home', 'lock'],
64
+ // Advertise what this platform actually accepts so clients can hide
65
+ // controls rather than discover failures at press time.
66
+ buttons: tvos
67
+ ? [
68
+ 'home',
69
+ 'menu',
70
+ 'select',
71
+ 'up',
72
+ 'down',
73
+ 'left',
74
+ 'right',
75
+ 'playPause',
76
+ 'pageUp',
77
+ 'pageDown',
78
+ 'guide',
79
+ ]
80
+ : ['home', 'lock'],
56
81
  keyboard: true,
57
82
  text: true,
58
83
  tvRemote: tvos,
@@ -35,6 +35,7 @@ const android_js_1 = require("../drivers/log-sources/android.js");
35
35
  const vega_js_1 = require("../drivers/log-sources/vega.js");
36
36
  const metro_js_1 = require("../drivers/log-sources/metro.js");
37
37
  const metro_discovery_js_1 = require("../drivers/log-sources/metro-discovery.js");
38
+ const bootstrap_js_1 = require("../drivers/bootstrap.js");
38
39
  const MAX_BUFFER = 5000;
39
40
  const RESTART_DELAY_MS = 2000;
40
41
  const WEB_POLL_INTERVAL_MS = 500;
@@ -145,6 +146,11 @@ class LogCollector {
145
146
  return;
146
147
  try {
147
148
  if (this.platform === 'ios' || this.platform === 'tvos') {
149
+ // `simctl spawn ... log stream` has no devicectl equivalent, so OS logs
150
+ // are simulator-only. Metro still streams over the network below, which
151
+ // is the useful source for React Native apps anyway.
152
+ if ((await (0, bootstrap_js_1.detectDeviceKind)(this.deviceId)) === 'physical')
153
+ return;
148
154
  this.source = new ios_js_1.IOSLogSource(this.deviceId, this.appId);
149
155
  }
150
156
  else if (this.platform === 'android') {
@@ -84,6 +84,20 @@ function dlog(msg) {
84
84
  // ── Driver lifecycle ──────────────────────────────────────────────────────────
85
85
  let driverPort = 1075;
86
86
  let driverPlatform = 'ios';
87
+ /**
88
+ * Host the driver is reachable on. Simulators and every non-Apple platform use
89
+ * the host's loopback; a physical device runs the driver on its own, so we talk
90
+ * to it over the network. Memoised — resolution goes through mDNS.
91
+ */
92
+ let _driverHost = null;
93
+ async function driverHost() {
94
+ if (_driverHost)
95
+ return _driverHost;
96
+ if (driverPlatform !== 'ios' && driverPlatform !== 'tvos')
97
+ return '127.0.0.1';
98
+ _driverHost = await (0, bootstrap_js_1.resolveDriverHost)(sessionName).catch(() => '127.0.0.1');
99
+ return _driverHost;
100
+ }
87
101
  let logCollector = null;
88
102
  let inputServer = null;
89
103
  let inputPort = null;
@@ -109,7 +123,7 @@ async function startInputServerForPlatform() {
109
123
  makeBackend = () => (0, input_backends_js_1.androidBackend)(driver);
110
124
  }
111
125
  else {
112
- const driver = new ios_js_1.IOSDriver(driverPort, '127.0.0.1', sessionName, driverPlatform);
126
+ const driver = new ios_js_1.IOSDriver(driverPort, await driverHost(), sessionName, driverPlatform, (await (0, bootstrap_js_1.detectDeviceKind)(sessionName)) === 'physical');
113
127
  makeBackend = () => (0, input_backends_js_1.iosBackend)(driver);
114
128
  // Opt-in native held-touch backend for live drags (iOS only; single-touch).
115
129
  if (driverPlatform === 'ios' && process.env.CONDUCTOR_IOS_HID === '1') {
@@ -154,6 +168,12 @@ async function startVideoServerForPlatform() {
154
168
  return;
155
169
  if (driverPlatform !== 'ios' && driverPlatform !== 'tvos')
156
170
  return;
171
+ // Capture attaches to the Simulator's framebuffer via SimulatorKit, which has
172
+ // no counterpart on real hardware.
173
+ if ((await (0, bootstrap_js_1.detectDeviceKind)(sessionName)) === 'physical') {
174
+ dlog('video capture is simulator-only — stream server disabled for this device');
175
+ return;
176
+ }
157
177
  const binary = await (0, bootstrap_js_1.getCaptureBinaryPath)();
158
178
  if (!binary) {
159
179
  dlog('video capture binary not present — stream server disabled for this device');
@@ -177,8 +197,9 @@ let _driverStartError = null;
177
197
  async function ensureDriverRunning() {
178
198
  if (_restartInProgress || !_driverStarted)
179
199
  return;
180
- // Vega has no driver process/port to health-check — control is host-side via the CLI.
181
- if (driverPlatform === 'vega')
200
+ // Vega and Roku have no driver process/port to health-check — control is
201
+ // host-side (the vega CLI) or over the network (Roku ECP).
202
+ if (driverPlatform === 'vega' || driverPlatform === 'roku')
182
203
  return;
183
204
  let alive;
184
205
  if (driverPlatform === 'android') {
@@ -188,11 +209,13 @@ async function ensureDriverRunning() {
188
209
  probe.close();
189
210
  }
190
211
  else {
191
- // 'ios', 'tvos', and 'web' all use an HTTP server — port open = alive
192
- alive = await (0, bootstrap_js_1.isPortOpen)(driverPort);
212
+ // 'ios', 'tvos', and 'web' all use an HTTP server — port open = alive.
213
+ // Physical devices answer on the LAN rather than the host's loopback.
214
+ alive = await (0, bootstrap_js_1.isPortOpen)(driverPort, await driverHost());
193
215
  }
194
216
  if (!alive) {
195
217
  if ((driverPlatform === 'ios' || driverPlatform === 'tvos') &&
218
+ (await (0, bootstrap_js_1.detectDeviceKind)(sessionName)) === 'simulator' &&
196
219
  !(await (0, bootstrap_js_1.isSimulatorBooted)(sessionName))) {
197
220
  dlog(`Simulator ${sessionName} is not booted — skipping driver restart`);
198
221
  return;
@@ -200,7 +223,11 @@ async function ensureDriverRunning() {
200
223
  _restartInProgress = true;
201
224
  dlog(`Driver on port ${driverPort} not responding — restarting`);
202
225
  try {
203
- if (driverPlatform === 'ios') {
226
+ if ((driverPlatform === 'ios' || driverPlatform === 'tvos') &&
227
+ (await (0, bootstrap_js_1.detectDeviceKind)(sessionName)) === 'physical') {
228
+ await (0, bootstrap_js_1.startDeviceDriver)(sessionName, driverPlatform, driverPort);
229
+ }
230
+ else if (driverPlatform === 'ios') {
204
231
  await (0, bootstrap_js_1.startIOSDriver)(sessionName, driverPort);
205
232
  }
206
233
  else if (driverPlatform === 'tvos') {
@@ -317,6 +344,9 @@ async function main() {
317
344
  else if (driverPlatform === 'vega') {
318
345
  dlog('vega: no driver process to stop (control is host-side via the CLI)');
319
346
  }
347
+ else if (driverPlatform === 'roku') {
348
+ dlog('roku: no driver process to stop (control is ECP over the network)');
349
+ }
320
350
  else if (driverPlatform === 'web') {
321
351
  dlog('Stopping web driver');
322
352
  try {
@@ -329,7 +359,10 @@ async function main() {
329
359
  else {
330
360
  dlog(`Stopping driver on port ${driverPort}`);
331
361
  try {
332
- if (driverPlatform === 'ios') {
362
+ if (driverPlatform === 'ios' && (await (0, bootstrap_js_1.detectDeviceKind)(sessionName)) === 'physical') {
363
+ await (0, bootstrap_js_1.stopDeviceDriver)(sessionName, 'ios');
364
+ }
365
+ else if (driverPlatform === 'ios') {
333
366
  await (0, bootstrap_js_1.stopIOSDriver)(sessionName);
334
367
  }
335
368
  else {
@@ -449,6 +482,12 @@ async function main() {
449
482
  _driverStarted = true;
450
483
  dlog('vega: no driver process to start; collecting logs only');
451
484
  }
485
+ else if (platform === 'roku') {
486
+ // Roku has neither a driver process nor a log stream to collect, so
487
+ // the daemon has nothing to do — commands drive the device directly.
488
+ _driverStarted = true;
489
+ dlog('roku: no driver process and no device logs; daemon is idle');
490
+ }
452
491
  else {
453
492
  await startDriverForPlatform(platform);
454
493
  }
@@ -487,8 +526,8 @@ async function main() {
487
526
  });
488
527
  }
489
528
  /**
490
- * Bring up the driver process for a non-vega platform. Sets `_driverStarted` /
491
- * `_driverStartError`. Extracted so the vega path can skip it entirely.
529
+ * Bring up the driver process for a platform that has one. Sets `_driverStarted` /
530
+ * `_driverStartError`. Extracted so the vega and roku paths can skip it entirely.
492
531
  */
493
532
  async function startDriverForPlatform(platform) {
494
533
  let driverAlive;
@@ -500,7 +539,7 @@ async function startDriverForPlatform(platform) {
500
539
  }
501
540
  else {
502
541
  // 'ios', 'tvos', and 'web' all use an HTTP server — port open = alive
503
- driverAlive = await (0, bootstrap_js_1.isPortOpen)(driverPort);
542
+ driverAlive = await (0, bootstrap_js_1.isPortOpen)(driverPort, await driverHost());
504
543
  }
505
544
  if (driverAlive) {
506
545
  _driverStarted = true;
@@ -523,7 +562,11 @@ async function startDriverForPlatform(platform) {
523
562
  }
524
563
  dlog(`Starting ${platform} driver on port ${driverPort}`);
525
564
  try {
526
- if (platform === 'ios') {
565
+ if ((platform === 'ios' || platform === 'tvos') &&
566
+ (await (0, bootstrap_js_1.detectDeviceKind)(sessionName)) === 'physical') {
567
+ await (0, bootstrap_js_1.startDeviceDriver)(sessionName, platform, driverPort);
568
+ }
569
+ else if (platform === 'ios') {
527
570
  await (0, bootstrap_js_1.startIOSDriver)(sessionName, driverPort);
528
571
  }
529
572
  else if (platform === 'tvos') {