@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
package/README.md CHANGED
@@ -26,7 +26,7 @@ conductor assert-visible "Dashboard"
26
26
  conductor take-screenshot --output /tmp/screen.png
27
27
  ```
28
28
 
29
- Targets iOS and tvOS simulators, physical iOS/tvOS devices, Android emulators and devices, Amazon Fire TV, and web via Playwright.
29
+ Targets iOS and tvOS simulators, physical iOS/tvOS devices, Android emulators and devices, Amazon Fire TV, Roku, and web via Playwright.
30
30
 
31
31
  ## Quick start
32
32
 
@@ -12,6 +12,7 @@ const ios_js_1 = require("../drivers/ios.js");
12
12
  const android_js_1 = require("../drivers/android.js");
13
13
  const web_js_1 = require("../drivers/web.js");
14
14
  const vega_js_1 = require("../drivers/vega.js");
15
+ const roku_js_1 = require("../drivers/roku.js");
15
16
  const wait_js_1 = require("../drivers/wait.js");
16
17
  async function assertNotVisible(element, opts = {}, sessionName = 'default', flags = {}) {
17
18
  if (!element && !flags.id && !flags.text) {
@@ -43,7 +44,9 @@ async function assertNotVisible(element, opts = {}, sessionName = 'default', fla
43
44
  else if (driver instanceof web_js_1.WebDriver) {
44
45
  await (0, wait_js_1.waitUntilWebElementGone)(() => driver.viewHierarchy(), sel, flags.timeout);
45
46
  }
46
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
47
+ else if (driver instanceof android_js_1.AndroidDriver ||
48
+ driver instanceof vega_js_1.VegaDriver ||
49
+ driver instanceof roku_js_1.RokuDriver) {
47
50
  // Vega emits uiautomator-style XML, so it reuses the Android resolver.
48
51
  await (0, wait_js_1.waitUntilAndroidElementGone)(() => driver.viewHierarchy(), sel, flags.timeout);
49
52
  }
@@ -22,6 +22,7 @@ const ios_js_1 = require("../drivers/ios.js");
22
22
  const android_js_1 = require("../drivers/android.js");
23
23
  const web_js_1 = require("../drivers/web.js");
24
24
  const vega_js_1 = require("../drivers/vega.js");
25
+ const roku_js_1 = require("../drivers/roku.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
  async function assertVisible(element, opts = {}, sessionName = 'default', flags = {}) {
@@ -56,8 +57,10 @@ async function assertVisible(element, opts = {}, sessionName = 'default', flags
56
57
  else if (driver instanceof web_js_1.WebDriver) {
57
58
  return (0, wait_js_1.waitForWebElement)(() => driver.viewHierarchy(), sel, timeoutMs);
58
59
  }
59
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
60
- // Vega emits uiautomator-style XML, so it reuses the Android resolver.
60
+ else if (driver instanceof android_js_1.AndroidDriver ||
61
+ driver instanceof vega_js_1.VegaDriver ||
62
+ driver instanceof roku_js_1.RokuDriver) {
63
+ // Vega and Roku emit uiautomator-style XML, so they reuse the Android resolver.
61
64
  return (0, wait_js_1.waitForAndroidElement)(() => driver.viewHierarchy(), sel, timeoutMs);
62
65
  }
63
66
  };
@@ -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 back(opts = {}, sessionName = 'default') {
13
14
  const result = await (0, runner_js_1.runDirect)(async (driver) => {
14
15
  if (driver instanceof ios_js_1.IOSDriver) {
@@ -20,7 +21,7 @@ async function back(opts = {}, sessionName = 'default') {
20
21
  else if (driver instanceof web_js_1.WebDriver) {
21
22
  await driver.goBack();
22
23
  }
23
- else if (driver instanceof vega_js_1.VegaDriver) {
24
+ else if (driver instanceof vega_js_1.VegaDriver || driver instanceof roku_js_1.RokuDriver) {
24
25
  await driver.back();
25
26
  }
26
27
  else if (driver instanceof android_js_1.AndroidDriver) {
@@ -14,6 +14,7 @@ const ios_js_1 = require("../drivers/ios.js");
14
14
  const android_js_1 = require("../drivers/android.js");
15
15
  const web_js_1 = require("../drivers/web.js");
16
16
  const vega_js_1 = require("../drivers/vega.js");
17
+ const roku_js_1 = require("../drivers/roku.js");
17
18
  const a11y_js_1 = require("../drivers/a11y.js");
18
19
  const snapshot_store_js_1 = require("../snapshot-store.js");
19
20
  async function captureUI(outputPath, opts = {}, sessionName = 'default') {
@@ -67,9 +68,12 @@ async function captureUI(outputPath, opts = {}, sessionName = 'default') {
67
68
  a11ySnapshot = built.a11ySnapshot;
68
69
  screenshotBuf = shot;
69
70
  }
70
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
71
- // Vega emits uiautomator-style XML, so it reuses the Android a11y builder.
72
- platform = driver instanceof vega_js_1.VegaDriver ? 'vega' : 'android';
71
+ else if (driver instanceof android_js_1.AndroidDriver ||
72
+ driver instanceof vega_js_1.VegaDriver ||
73
+ driver instanceof roku_js_1.RokuDriver) {
74
+ // Vega and Roku emit uiautomator-style XML, so they reuse the Android a11y builder.
75
+ platform =
76
+ driver instanceof vega_js_1.VegaDriver ? 'vega' : driver instanceof roku_js_1.RokuDriver ? 'roku' : 'android';
73
77
  const [info, xml, shot] = await Promise.all([
74
78
  driver.deviceInfo(),
75
79
  driver.viewHierarchy(),
@@ -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 ANDROID_MSG = 'clipboard is iOS-only. On Android, use `conductor input-text` to type instead.';
17
18
  async function clipboardRead(opts = {}, sessionName = 'default') {
18
19
  const result = await (0, runner_js_1.runDirect)(async (driver) => {
@@ -24,6 +25,8 @@ async function clipboardRead(opts = {}, sessionName = 'default') {
24
25
  throw new Error('clipboard read is not supported on Web');
25
26
  if (driver instanceof vega_js_1.VegaDriver)
26
27
  throw new Error('clipboard read is not supported on vega');
28
+ if (driver instanceof roku_js_1.RokuDriver)
29
+ throw new Error('clipboard read is not supported on roku');
27
30
  return '';
28
31
  }, sessionName);
29
32
  if (result.success) {
@@ -52,6 +55,9 @@ async function clipboardWrite(text, opts = {}, sessionName = 'default') {
52
55
  else if (driver instanceof vega_js_1.VegaDriver) {
53
56
  throw new Error('clipboard write is not supported on vega');
54
57
  }
58
+ else if (driver instanceof roku_js_1.RokuDriver) {
59
+ throw new Error('clipboard write is not supported on roku');
60
+ }
55
61
  }, sessionName);
56
62
  if (result.success) {
57
63
  (0, output_js_1.printSuccess)('clipboard write — done', opts);
@@ -81,6 +87,9 @@ async function paste(opts = {}, sessionName = 'default') {
81
87
  else if (driver instanceof vega_js_1.VegaDriver) {
82
88
  throw new Error('paste is not supported on vega');
83
89
  }
90
+ else if (driver instanceof roku_js_1.RokuDriver) {
91
+ throw new Error('paste is not supported on roku');
92
+ }
84
93
  }, sessionName);
85
94
  if (result.success) {
86
95
  (0, output_js_1.printSuccess)('paste — done', opts);
@@ -12,6 +12,7 @@ const ios_js_1 = require("../drivers/ios.js");
12
12
  const android_js_1 = require("../drivers/android.js");
13
13
  const web_js_1 = require("../drivers/web.js");
14
14
  const vega_js_1 = require("../drivers/vega.js");
15
+ const roku_js_1 = require("../drivers/roku.js");
15
16
  const wait_js_1 = require("../drivers/wait.js");
16
17
  const direct_ios_selector_js_1 = require("../drivers/direct-ios-selector.js");
17
18
  async function copyTextFrom(query, opts = {}, sessionName = 'default', flags = {}) {
@@ -33,7 +34,7 @@ async function copyTextFrom(query, opts = {}, sessionName = 'default', flags = {
33
34
  else if (driver instanceof web_js_1.WebDriver) {
34
35
  el = await (0, wait_js_1.waitForWebElement)(() => driver.viewHierarchy(), sel);
35
36
  }
36
- else if (driver instanceof vega_js_1.VegaDriver) {
37
+ else if (driver instanceof vega_js_1.VegaDriver || driver instanceof roku_js_1.RokuDriver) {
37
38
  el = await (0, wait_js_1.waitForAndroidElement)(() => driver.viewHierarchy(), sel);
38
39
  }
39
40
  else if (driver instanceof android_js_1.AndroidDriver) {
@@ -180,6 +180,11 @@ async function crashesList(opts, sessionName, listOpts) {
180
180
  if (!opts.json)
181
181
  console.error('note: crash reports are not supported on vega (Amazon Fire TV)');
182
182
  }
183
+ else if (platform === 'roku') {
184
+ // ECP exposes no crash log; Roku's are read from the dev console by hand.
185
+ if (!opts.json)
186
+ console.error('note: crash reports are not supported on roku');
187
+ }
183
188
  if (opts.json) {
184
189
  (0, output_js_1.printData)({ count: reports.length, reports }, opts);
185
190
  }
@@ -110,6 +110,11 @@ async function deleteDevice(nameOrId, opts, flags) {
110
110
  (0, output_js_1.printError)("delete-device is not supported on vega (Amazon Fire TV) — manage VVDs via Amazon's tooling.", opts);
111
111
  return 1;
112
112
  }
113
+ // A Roku is physical hardware conductor never created.
114
+ if (platform === 'roku' || (nameOrId && nameOrId.startsWith('roku:'))) {
115
+ (0, output_js_1.printError)('delete-device is not supported on roku — it is physical hardware.', opts);
116
+ return 1;
117
+ }
113
118
  const includeIOS = !platform || platform === 'ios';
114
119
  const includeTvOS = !platform || platform === 'tvos';
115
120
  const includeAndroid = !platform || platform === 'android';
@@ -37,6 +37,10 @@ async function downloadApp(appId, output, opts = {}, sessionName = 'default') {
37
37
  (0, output_js_1.printError)('download-app is not supported on vega (Amazon Fire TV).', opts);
38
38
  return 1;
39
39
  }
40
+ if (platform === 'roku') {
41
+ (0, output_js_1.printError)('download-app is not supported on roku.', opts);
42
+ return 1;
43
+ }
40
44
  if (platform === 'ios' || platform === 'tvos') {
41
45
  // Get the .app bundle path from the simulator
42
46
  const getPath = await (0, runner_js_1.spawnCommand)('xcrun', [
@@ -9,9 +9,12 @@ 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 eraseText(characters, opts = {}, sessionName = 'default') {
13
14
  const result = await (0, runner_js_1.runDirect)(async (driver) => {
14
- if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
15
+ if (driver instanceof android_js_1.AndroidDriver ||
16
+ driver instanceof vega_js_1.VegaDriver ||
17
+ driver instanceof roku_js_1.RokuDriver) {
15
18
  await driver.eraseAllText(characters);
16
19
  }
17
20
  else if (driver instanceof web_js_1.WebDriver) {
@@ -12,6 +12,7 @@ const ios_js_1 = require("../drivers/ios.js");
12
12
  const android_js_1 = require("../drivers/android.js");
13
13
  const web_js_1 = require("../drivers/web.js");
14
14
  const vega_js_1 = require("../drivers/vega.js");
15
+ const roku_js_1 = require("../drivers/roku.js");
15
16
  const element_resolver_js_1 = require("../drivers/element-resolver.js");
16
17
  // XCUIElementType rawValue → human-readable name.
17
18
  // Source: XCUIElementType enum in XCTest framework.
@@ -209,8 +210,10 @@ async function queryFocused(driver) {
209
210
  const node = findFocusedWeb(hierarchy.elements);
210
211
  return node ? formatWebElement(node) : null;
211
212
  }
212
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
213
- // Vega emits uiautomator-style XML, so it reuses the Android parser.
213
+ else if (driver instanceof android_js_1.AndroidDriver ||
214
+ driver instanceof vega_js_1.VegaDriver ||
215
+ driver instanceof roku_js_1.RokuDriver) {
216
+ // Vega and Roku emit uiautomator-style XML, so they reuse the Android parser.
214
217
  const xml = await driver.viewHierarchy();
215
218
  const nodes = (0, element_resolver_js_1.parseAndroidHierarchy)(xml);
216
219
  const node = nodes.find((n) => n.focused);
@@ -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 resolveDeviceId(sessionName) {
14
15
  if (sessionName !== 'default')
15
16
  return sessionName;
@@ -41,7 +42,9 @@ async function foregroundApp(opts = {}, sessionName = 'default') {
41
42
  else if (driver instanceof web_js_1.WebDriver) {
42
43
  appId = await driver.runningApp();
43
44
  }
44
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
45
+ else if (driver instanceof android_js_1.AndroidDriver ||
46
+ driver instanceof vega_js_1.VegaDriver ||
47
+ driver instanceof roku_js_1.RokuDriver) {
45
48
  appId = await driver.getForegroundApp();
46
49
  }
47
50
  else {
@@ -20,6 +20,7 @@ const output_js_1 = require("../output.js");
20
20
  const ios_js_1 = require("../drivers/ios.js");
21
21
  const web_js_1 = require("../drivers/web.js");
22
22
  const vega_js_1 = require("../drivers/vega.js");
23
+ const roku_js_1 = require("../drivers/roku.js");
23
24
  function parseCenter(s) {
24
25
  if (!s)
25
26
  return null;
@@ -111,6 +112,8 @@ async function pinch(opts, sessionName, pinchOpts) {
111
112
  throw new Error('pinch is not supported on Web');
112
113
  if (driver instanceof vega_js_1.VegaDriver)
113
114
  throw new Error('pinch is not supported on vega (Amazon Fire TV)');
115
+ if (driver instanceof roku_js_1.RokuDriver)
116
+ throw new Error('pinch is not supported on roku — Roku is D-pad driven, with no touch input');
114
117
  const { width, height, cx: defCx, cy: defCy } = await screenCenter(driver);
115
118
  const center = parseCenter(pinchOpts.center) ?? { x: defCx, y: defCy };
116
119
  // Start span is half the shorter screen dimension; end is scaled.
@@ -135,6 +138,8 @@ async function rotateGesture(opts, sessionName, rotateOpts) {
135
138
  throw new Error('rotate-gesture is not supported on Web');
136
139
  if (driver instanceof vega_js_1.VegaDriver)
137
140
  throw new Error('rotate-gesture is not supported on vega (Amazon Fire TV)');
141
+ if (driver instanceof roku_js_1.RokuDriver)
142
+ throw new Error('rotate-gesture is not supported on roku — Roku is D-pad driven, with no touch input');
138
143
  const { width, height, cx: defCx, cy: defCy } = await screenCenter(driver);
139
144
  const center = parseCenter(rotateOpts.center) ?? { x: defCx, y: defCy };
140
145
  const radius = Math.min(width, height) * 0.25;
@@ -192,6 +197,8 @@ async function gesture(rawJson, filePath, opts, sessionName) {
192
197
  throw new Error('gesture is not supported on Web');
193
198
  if (driver instanceof vega_js_1.VegaDriver)
194
199
  throw new Error('gesture is not supported on vega (Amazon Fire TV)');
200
+ if (driver instanceof roku_js_1.RokuDriver)
201
+ throw new Error('gesture is not supported on roku — Roku is D-pad driven, with no touch input');
195
202
  await playPaths(driver, paths);
196
203
  }, sessionName);
197
204
  if (result.success) {
@@ -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 hideKeyboard(opts = {}, sessionName = 'default') {
13
14
  const result = await (0, runner_js_1.runDirect)(async (driver) => {
14
15
  if (driver instanceof ios_js_1.IOSDriver) {
@@ -22,6 +23,10 @@ async function hideKeyboard(opts = {}, sessionName = 'default') {
22
23
  else if (driver instanceof vega_js_1.VegaDriver) {
23
24
  // Vega has no keyboard-hide primitive — no-op
24
25
  }
26
+ else if (driver instanceof roku_js_1.RokuDriver) {
27
+ // Roku dismisses its on-screen keyboard with Back.
28
+ await driver.back();
29
+ }
25
30
  else if (driver instanceof android_js_1.AndroidDriver) {
26
31
  await driver.pressKeyEvent(111); // KEYCODE_ESCAPE
27
32
  }
@@ -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
  const element_resolver_js_1 = require("../drivers/element-resolver.js");
14
15
  const a11y_js_1 = require("../drivers/a11y.js");
15
16
  async function inspect(opts = {}, sessionName = 'default', inspectOpts = {}) {
@@ -29,7 +30,9 @@ async function inspect(opts = {}, sessionName = 'default', inspectOpts = {}) {
29
30
  const hierarchy = await driver.viewHierarchy(false);
30
31
  hit = (0, element_resolver_js_1.findIOSViewAtPoint)(hierarchy.axElement, x, y, tappable);
31
32
  }
32
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
33
+ else if (driver instanceof android_js_1.AndroidDriver ||
34
+ driver instanceof vega_js_1.VegaDriver ||
35
+ driver instanceof roku_js_1.RokuDriver) {
33
36
  const xml = await driver.viewHierarchy();
34
37
  hit = (0, element_resolver_js_1.findAndroidViewAtPoint)(xml, x, y, tappable);
35
38
  }
@@ -68,7 +71,9 @@ async function inspect(opts = {}, sessionName = 'default', inspectOpts = {}) {
68
71
  const built = (0, a11y_js_1.buildWebA11y)(vh);
69
72
  raw = JSON.stringify({ ...vh, elements: built.hierarchy }, null, 2);
70
73
  }
71
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
74
+ else if (driver instanceof android_js_1.AndroidDriver ||
75
+ driver instanceof vega_js_1.VegaDriver ||
76
+ driver instanceof roku_js_1.RokuDriver) {
72
77
  const xml = await driver.viewHierarchy();
73
78
  const built = (0, a11y_js_1.buildAndroidA11y)(xml);
74
79
  raw = JSON.stringify(built.hierarchy, null, 2);
@@ -93,7 +98,9 @@ async function inspect(opts = {}, sessionName = 'default', inspectOpts = {}) {
93
98
  const hierarchy = await driver.viewHierarchy();
94
99
  text = (0, element_resolver_js_1.inspectWebToText)(hierarchy);
95
100
  }
96
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
101
+ else if (driver instanceof android_js_1.AndroidDriver ||
102
+ driver instanceof vega_js_1.VegaDriver ||
103
+ driver instanceof roku_js_1.RokuDriver) {
97
104
  const xml = await driver.viewHierarchy();
98
105
  text = (0, element_resolver_js_1.inspectAndroidToText)(xml);
99
106
  }
@@ -8,6 +8,7 @@ const sdk_js_1 = require("../android/sdk.js");
8
8
  const session_js_1 = require("../session.js");
9
9
  const output_js_1 = require("../output.js");
10
10
  const bootstrap_js_1 = require("../drivers/bootstrap.js");
11
+ const devicectl_js_1 = require("../drivers/devicectl.js");
11
12
  const cli_js_1 = require("../drivers/vega/cli.js");
12
13
  async function resolveDeviceId(sessionName) {
13
14
  if (sessionName !== 'default')
@@ -30,6 +31,11 @@ async function installApp(appPath, opts = {}, sessionName = 'default') {
30
31
  (0, output_js_1.printError)('install-app is not supported on web. Use launch-app with a URL instead.', opts);
31
32
  return 1;
32
33
  }
34
+ else if (platform === 'roku') {
35
+ (0, output_js_1.printError)('install-app is not supported on roku — sideload the channel through the ' +
36
+ "device's developer web server (http://<device-ip>).", opts);
37
+ return 1;
38
+ }
33
39
  else if (platform === 'vega') {
34
40
  // Vega installs a `.vpkg` through Amazon's CLI; strip the `vega:` id prefix.
35
41
  try {
@@ -41,10 +47,21 @@ async function installApp(appPath, opts = {}, sessionName = 'default') {
41
47
  }
42
48
  }
43
49
  else if (platform === 'ios' || platform === 'tvos') {
44
- const result = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'install', deviceId, appPath]);
45
- if (!result.success) {
46
- (0, output_js_1.printError)(`install-app failed: ${result.stderr}`, opts);
47
- return 1;
50
+ if ((await (0, bootstrap_js_1.detectDeviceKind)(deviceId)) === 'physical') {
51
+ try {
52
+ await (0, devicectl_js_1.installApp)(deviceId, appPath);
53
+ }
54
+ catch (e) {
55
+ (0, output_js_1.printError)(`install-app failed: ${e instanceof Error ? e.message : String(e)}`, opts);
56
+ return 1;
57
+ }
58
+ }
59
+ else {
60
+ const result = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'install', deviceId, appPath]);
61
+ if (!result.success) {
62
+ (0, output_js_1.printError)(`install-app failed: ${result.stderr}`, opts);
63
+ return 1;
64
+ }
48
65
  }
49
66
  }
50
67
  else {
@@ -28,6 +28,7 @@ const fs_1 = __importDefault(require("fs"));
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
  async function launchApp(appId, deviceId, opts = {}, sessionName = 'default', flags = {}) {
32
33
  if (!appId) {
33
34
  (0, output_js_1.printError)('launch-app requires <appId>', opts);
@@ -50,7 +51,7 @@ async function launchApp(appId, deviceId, opts = {}, sessionName = 'default', fl
50
51
  await driver.terminateApp(appId);
51
52
  else if (driver instanceof web_js_1.WebDriver)
52
53
  await driver.terminateApp();
53
- else if (driver instanceof vega_js_1.VegaDriver)
54
+ else if (driver instanceof vega_js_1.VegaDriver || driver instanceof roku_js_1.RokuDriver)
54
55
  await driver.terminateApp(appId);
55
56
  else if (driver instanceof android_js_1.AndroidDriver)
56
57
  await driver.stopApp(appId);
@@ -79,7 +80,7 @@ async function launchApp(appId, deviceId, opts = {}, sessionName = 'default', fl
79
80
  else if (driver instanceof web_js_1.WebDriver) {
80
81
  await driver.launchApp(appId);
81
82
  }
82
- else if (driver instanceof vega_js_1.VegaDriver) {
83
+ else if (driver instanceof vega_js_1.VegaDriver || driver instanceof roku_js_1.RokuDriver) {
83
84
  await driver.launchApp(appId, flags.launchArgs);
84
85
  }
85
86
  else if (driver instanceof android_js_1.AndroidDriver) {
@@ -8,6 +8,7 @@ const sdk_js_1 = require("../android/sdk.js");
8
8
  const session_js_1 = require("../session.js");
9
9
  const output_js_1 = require("../output.js");
10
10
  const bootstrap_js_1 = require("../drivers/bootstrap.js");
11
+ const devicectl_js_1 = require("../drivers/devicectl.js");
11
12
  const cli_js_1 = require("../drivers/vega/cli.js");
12
13
  async function resolveDeviceId(sessionName) {
13
14
  if (sessionName !== 'default')
@@ -29,10 +30,25 @@ async function listApps(opts = {}, sessionName = 'default') {
29
30
  (0, output_js_1.printError)('list-apps is not supported on web. Use foreground-app to get the current URL.', opts);
30
31
  return 1;
31
32
  }
33
+ else if (platform === 'roku') {
34
+ // ECP can name only the *active* channel, not enumerate installed ones.
35
+ (0, output_js_1.printError)('list-apps is not supported on roku. Use foreground-app to get the active channel id.', opts);
36
+ return 1;
37
+ }
32
38
  else if (platform === 'vega') {
33
39
  // Vega is driven through Amazon's CLI, not adb; strip the `vega:` id prefix.
34
40
  appIds = (await new cli_js_1.VegaCli(deviceId.replace(/^vega:/, '')).listInstalledApps()).sort();
35
41
  }
42
+ else if ((platform === 'ios' || platform === 'tvos') &&
43
+ (await (0, bootstrap_js_1.detectDeviceKind)(deviceId)) === 'physical') {
44
+ try {
45
+ appIds = (await (0, devicectl_js_1.listApps)(deviceId)).map((a) => a.id).sort();
46
+ }
47
+ catch (e) {
48
+ (0, output_js_1.printError)(`list-apps failed: ${e instanceof Error ? e.message : String(e)}`, opts);
49
+ return 1;
50
+ }
51
+ }
36
52
  else if (platform === 'ios' || platform === 'tvos') {
37
53
  const result = await (0, runner_js_1.spawnCommand)('bash', [
38
54
  '-c',
@@ -16,7 +16,9 @@ const bootstrap_js_1 = require("../drivers/bootstrap.js");
16
16
  const client_js_1 = require("../daemon/client.js");
17
17
  const protocol_js_1 = require("../daemon/protocol.js");
18
18
  const cli_js_1 = require("../drivers/vega/cli.js");
19
+ const discovery_js_1 = require("../drivers/roku/discovery.js");
19
20
  const cdp_discovery_js_1 = require("../drivers/cdp-discovery.js");
21
+ const devicectl_js_1 = require("../drivers/devicectl.js");
20
22
  // Captured during discoverAvailableDevices so listDevices() can report it.
21
23
  // Module-scoped because the discover function returns Device[]; bolting an
22
24
  // extra return field onto the public type would ripple beyond this fix.
@@ -109,6 +111,18 @@ async function discoverBootedDevices() {
109
111
  }
110
112
  }
111
113
  }
114
+ // Physical iOS/tvOS devices: paired and reachable is the closest analogue to
115
+ // a booted simulator — that's when conductor can actually drive them.
116
+ for (const d of await (0, devicectl_js_1.listPhysicalDevices)()) {
117
+ if (!d.available)
118
+ continue;
119
+ devices.push({
120
+ id: d.identifier,
121
+ name: d.name,
122
+ platform: d.platform,
123
+ status: d.developerModeEnabled ? 'connected' : 'developer mode off',
124
+ });
125
+ }
112
126
  // Vega (Amazon Fire TV): booted devices reported by the vega CLI. Best-effort —
113
127
  // the CLI is absent unless the Vega SDK is installed.
114
128
  try {
@@ -124,6 +138,17 @@ async function discoverBootedDevices() {
124
138
  catch {
125
139
  /* vega CLI not installed */
126
140
  }
141
+ // Roku: the pinned CONDUCTOR_ROKU_HOST device, plus SSDP hits when
142
+ // CONDUCTOR_ROKU_DISCOVERY is on. Physical hardware only — Roku has no emulator.
143
+ for (const d of await (0, discovery_js_1.discoverRokuDevices)().catch(() => [])) {
144
+ devices.push({
145
+ id: `roku:${d.host}`,
146
+ name: d.friendlyName || d.modelName,
147
+ platform: 'roku',
148
+ status: 'booted',
149
+ formFactor: 'tv',
150
+ });
151
+ }
127
152
  // External CDP endpoints (e.g. an Electron app started with
128
153
  // --remote-debugging-port): one device per discovered webview/tile.
129
154
  devices.push(...(await (0, cdp_discovery_js_1.discoverCdpDevices)()));
@@ -154,6 +179,18 @@ async function discoverAvailableDevices() {
154
179
  // ignore parse errors
155
180
  }
156
181
  }
182
+ // Physical devices that aren't currently reachable — listed so users can see
183
+ // why a device they expect is missing.
184
+ for (const d of await (0, devicectl_js_1.listPhysicalDevices)()) {
185
+ if (d.available)
186
+ continue;
187
+ devices.push({
188
+ id: d.identifier,
189
+ name: d.name,
190
+ platform: d.platform,
191
+ status: 'unavailable',
192
+ });
193
+ }
157
194
  // Android: list available AVDs
158
195
  const emu = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('emulator'), ['-list-avds'], {
159
196
  env: (0, sdk_js_1.androidSpawnEnv)(),
@@ -1121,6 +1121,11 @@ async function memory(appIdArg, opts = {}, sessionName = 'default', memOpts = {}
1121
1121
  (0, output_js_1.printError)('memory is not yet supported on vega (Amazon Fire TV)', opts);
1122
1122
  return 1;
1123
1123
  }
1124
+ if (platform === 'roku') {
1125
+ // ECP exposes no memory counters, and there is no host-side tooling to ask.
1126
+ (0, output_js_1.printError)('memory is not supported on roku', opts);
1127
+ return 1;
1128
+ }
1124
1129
  let report;
1125
1130
  if (platform === 'web') {
1126
1131
  report = await collectWeb(deviceId, sessionName, memOpts);
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ANDROID_KEYCODE = exports.VALID_KEYS = exports.HELP = void 0;
3
+ exports.ANDROID_KEYCODE = exports.TVOS_REMOTE_BUTTONS = exports.VALID_KEYS = exports.HELP = void 0;
4
4
  exports.dispatchKey = dispatchKey;
5
5
  exports.pressKey = pressKey;
6
6
  exports.HELP = ` press-key <key> Press a key (Enter, Backspace, Home, ...)
@@ -19,6 +19,8 @@ const ios_js_1 = require("../drivers/ios.js");
19
19
  const android_js_1 = require("../drivers/android.js");
20
20
  const web_js_1 = require("../drivers/web.js");
21
21
  const vega_js_1 = require("../drivers/vega.js");
22
+ const roku_js_1 = require("../drivers/roku.js");
23
+ const key_mapping_js_1 = require("../drivers/roku/key-mapping.js");
22
24
  exports.VALID_KEYS = [
23
25
  'Enter',
24
26
  'Backspace',
@@ -54,6 +56,15 @@ exports.VALID_KEYS = [
54
56
  'TV Input HDMI 1',
55
57
  'TV Input HDMI 2',
56
58
  'TV Input HDMI 3',
59
+ 'Remote Info',
60
+ 'Remote Instant Replay',
61
+ 'Remote Search',
62
+ 'Remote Page Up',
63
+ 'Remote Page Down',
64
+ 'Remote Guide',
65
+ 'Remote TV Provider',
66
+ 'Remote One Two Three',
67
+ 'Remote Four Colors',
57
68
  ];
58
69
  // iOS XCTest pressKey accepts these values (maps to XCUIKeyboardKey)
59
70
  const IOS_KEY_MAP = {
@@ -69,7 +80,9 @@ const IOS_BUTTON_MAP = {
69
80
  Power: 'lock',
70
81
  };
71
82
  // tvOS remote: map key names to pressButton values
72
- const TVOS_REMOTE_BUTTONS = {
83
+ // Page Up/Down and Guide need tvOS 14.3; the last three need tvOS 18.1. The
84
+ // driver reports a precondition error when the device's OS is older.
85
+ exports.TVOS_REMOTE_BUTTONS = {
73
86
  'Remote Dpad Up': 'up',
74
87
  'Remote Dpad Down': 'down',
75
88
  'Remote Dpad Left': 'left',
@@ -77,6 +90,12 @@ const TVOS_REMOTE_BUTTONS = {
77
90
  'Remote Dpad Center': 'select',
78
91
  'Remote Menu': 'menu',
79
92
  'Remote Media Play Pause': 'playPause',
93
+ 'Remote Page Up': 'pageUp',
94
+ 'Remote Page Down': 'pageDown',
95
+ 'Remote Guide': 'guide',
96
+ 'Remote TV Provider': 'tvProvider',
97
+ 'Remote One Two Three': 'oneTwoThree',
98
+ 'Remote Four Colors': 'fourColors',
80
99
  };
81
100
  // vega (Amazon Fire TV) remote: map key names to VegaDriver button values.
82
101
  const VEGA_REMOTE_BUTTONS = {
@@ -131,6 +150,13 @@ exports.ANDROID_KEYCODE = {
131
150
  'TV Input HDMI 1': 243,
132
151
  'TV Input HDMI 2': 244,
133
152
  'TV Input HDMI 3': 245,
153
+ 'Remote Info': 165,
154
+ 'Remote Instant Replay': 273, // KEYCODE_MEDIA_SKIP_BACKWARD
155
+ 'Remote Search': 84,
156
+ // Paging keys — the fast way through a long list. Whether a given app honours
157
+ // them varies, the same as on tvOS.
158
+ 'Remote Page Up': 92,
159
+ 'Remote Page Down': 93,
134
160
  };
135
161
  /**
136
162
  * Send `key` on an already-connected driver. Split out of `pressKey` so
@@ -140,7 +166,7 @@ exports.ANDROID_KEYCODE = {
140
166
  async function dispatchKey(driver, matched, holdSeconds) {
141
167
  if (driver instanceof ios_js_1.IOSDriver) {
142
168
  if (driver.platform === 'tvos') {
143
- const tvosButton = TVOS_REMOTE_BUTTONS[matched];
169
+ const tvosButton = exports.TVOS_REMOTE_BUTTONS[matched];
144
170
  const iosButton = IOS_BUTTON_MAP[matched];
145
171
  if (tvosButton) {
146
172
  await driver.pressButton(tvosButton, holdSeconds);
@@ -191,6 +217,13 @@ async function dispatchKey(driver, matched, holdSeconds) {
191
217
  }
192
218
  // Keys not mapped on vega are silently ignored
193
219
  }
220
+ else if (driver instanceof roku_js_1.RokuDriver) {
221
+ // Keys not mapped on roku are silently ignored, as on tvOS and vega — but a
222
+ // mapped key that the device rejects must still fail.
223
+ if ((0, key_mapping_js_1.rokuEcpKey)(matched)) {
224
+ await driver.pressKeyNamed(matched);
225
+ }
226
+ }
194
227
  else if (driver instanceof android_js_1.AndroidDriver) {
195
228
  const code = exports.ANDROID_KEYCODE[matched];
196
229
  if (code !== undefined) {
@@ -244,6 +244,10 @@ async function profileCpu(opts, sessionName, profileOpts) {
244
244
  else if (platform === 'android') {
245
245
  entries = await recordAndroidCpu(sessionName, profileOpts.appId, profileOpts.durationSec, out, profileOpts.report ? { top: profileOpts.top ?? 30 } : undefined);
246
246
  }
247
+ else if (platform === 'roku') {
248
+ (0, output_js_1.printError)('profile cpu is not supported on roku — ECP exposes no CPU counters.', opts);
249
+ return 1;
250
+ }
247
251
  else if (platform === 'vega') {
248
252
  // Vega is Amazon's own OS, not Android — no simpleperf, no dumpsys. A
249
253
  // physical Fire TV Stick runs Fire OS (Android) over adb and is a