@houwert/conductor 0.29.2 → 0.30.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 (50) 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 +5 -0
  18. package/dist/commands/launch-app.js +3 -2
  19. package/dist/commands/list-apps.js +5 -0
  20. package/dist/commands/list-devices.js +12 -0
  21. package/dist/commands/memory.js +5 -0
  22. package/dist/commands/press-key.js +15 -0
  23. package/dist/commands/profile-frames.js +4 -2
  24. package/dist/commands/profile.js +111 -7
  25. package/dist/commands/screenshot.js +5 -2
  26. package/dist/commands/scroll-until-visible.js +5 -2
  27. package/dist/commands/scroll.js +4 -1
  28. package/dist/commands/start-device.js +27 -3
  29. package/dist/commands/stop-app.js +2 -1
  30. package/dist/commands/stop-device.js +12 -1
  31. package/dist/commands/swipe.js +4 -1
  32. package/dist/commands/tap.js +3 -2
  33. package/dist/commands/uninstall-app.js +4 -0
  34. package/dist/daemon/server.js +14 -4
  35. package/dist/drivers/bootstrap.js +10 -0
  36. package/dist/drivers/flow-runner.js +16 -3
  37. package/dist/drivers/roku/app-ui-parser.js +122 -0
  38. package/dist/drivers/roku/discovery.js +136 -0
  39. package/dist/drivers/roku/ecp-client.js +396 -0
  40. package/dist/drivers/roku/key-mapping.js +67 -0
  41. package/dist/drivers/roku.js +237 -0
  42. package/dist/drivers/vega/page-source-parser.js +5 -118
  43. package/dist/drivers/xml.js +128 -0
  44. package/dist/enum-options.js +3 -1
  45. package/dist/index.js +1 -1
  46. package/dist/runner.js +32 -0
  47. package/package.json +1 -1
  48. package/skills/conductor-device-interact/SKILL.md +4 -3
  49. package/skills/conductor-device-setup/SKILL.md +35 -2
  50. package/skills/conductor-profiler/SKILL.md +9 -4
@@ -4,6 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.REACT_PROFILER_STOP = exports.REACT_PROFILER_READ = exports.REACT_PROFILER_INSTALL = exports.HELP = void 0;
7
+ exports.isUnsymbolised = isUnsymbolised;
8
+ exports.rollupByDso = rollupByDso;
9
+ exports.unsymbolisedPercent = unsymbolisedPercent;
7
10
  exports.parseSimpleperfReport = parseSimpleperfReport;
8
11
  exports.profileCpu = profileCpu;
9
12
  exports.heapGrowth = heapGrowth;
@@ -37,9 +40,80 @@ const output_js_1 = require("../output.js");
37
40
  const bootstrap_js_1 = require("../drivers/bootstrap.js");
38
41
  const sdk_js_1 = require("../android/sdk.js");
39
42
  const runner_js_1 = require("../runner.js");
43
+ const device_js_1 = require("../android/device.js");
40
44
  const memory_js_1 = require("./memory.js");
41
45
  const metro_cdp_js_1 = require("../drivers/metro-cdp.js");
42
46
  const profile_gc_js_1 = require("./profile-gc.js");
47
+ /** simpleperf renders an unresolved address as `libfoo.so[+1a62f8]`. */
48
+ function isUnsymbolised(symbol) {
49
+ return /\[\+[0-9a-fx]+\]\s*$/i.test(symbol);
50
+ }
51
+ function shortDso(dso) {
52
+ return dso.includes('/') ? dso.slice(dso.lastIndexOf('/') + 1) : dso;
53
+ }
54
+ /**
55
+ * Sum a flat symbol table by library.
56
+ *
57
+ * A stripped app library shows up as a dozen `libfoo.so[+offset]` rows that a
58
+ * reader cannot tell apart from a dozen unrelated hotspots. Rolled up, one
59
+ * library dominating is immediately visible — which is the actual finding when
60
+ * a profile has no single hot symbol.
61
+ */
62
+ function rollupByDso(entries) {
63
+ const byDso = new Map();
64
+ for (const e of entries) {
65
+ const key = shortDso(e.dso);
66
+ const slot = byDso.get(key) ?? { percent: 0, symbols: 0, unsym: 0 };
67
+ slot.percent += e.percent;
68
+ slot.symbols++;
69
+ if (isUnsymbolised(e.symbol))
70
+ slot.unsym++;
71
+ byDso.set(key, slot);
72
+ }
73
+ return [...byDso.entries()]
74
+ .map(([dso, v]) => ({
75
+ dso,
76
+ percent: Math.round(v.percent * 100) / 100,
77
+ symbols: v.symbols,
78
+ unsymbolised: v.unsym === v.symbols,
79
+ }))
80
+ .sort((a, b) => b.percent - a.percent);
81
+ }
82
+ /** Share of the sampled overhead that resolved to no function name. */
83
+ function unsymbolisedPercent(entries) {
84
+ const total = entries.reduce((a, e) => a + e.percent, 0);
85
+ if (total <= 0)
86
+ return 0;
87
+ const unsym = entries.filter((e) => isUnsymbolised(e.symbol)).reduce((a, e) => a + e.percent, 0);
88
+ return Math.round((unsym / total) * 10000) / 100;
89
+ }
90
+ /**
91
+ * Turn a simpleperf failure into something actionable.
92
+ *
93
+ * simpleperf needs the target to be `android:debuggable` or to declare
94
+ * `<profileable android:shell="true"/>`. A stock release APK is neither, and
95
+ * the resulting error says only `exited with 1`.
96
+ */
97
+ async function explainSimpleperfFailure(deviceId, appId, stderr) {
98
+ // Drop simpleperf's PMU probing chatter; it is present on every run.
99
+ const meaningful = stderr
100
+ .split(/\r?\n/)
101
+ .filter((l) => l.trim() && !/cannot read event type|Failed to read event type/i.test(l))
102
+ .slice(-4)
103
+ .join('\n');
104
+ if (appId) {
105
+ const pkg = await (0, device_js_1.adbShell)(deviceId, ['dumpsys', 'package', appId]);
106
+ if (pkg.success && /^\s*flags=\[/m.test(pkg.stdout) && !/\bDEBUGGABLE\b/.test(pkg.stdout)) {
107
+ return (`simpleperf cannot profile ${appId}: the installed APK is neither debuggable nor ` +
108
+ `profileable.\nsimpleperf needs android:debuggable, or ` +
109
+ `\`<profileable android:shell="true"/>\` inside <application> — the latter is a ` +
110
+ `one-line manifest change that keeps the build a release build and is the right fix ` +
111
+ `for a perf build.\n` +
112
+ (meaningful ? `simpleperf said:\n${meaningful}` : ''));
113
+ }
114
+ }
115
+ return `simpleperf record failed.\n${meaningful || '(no diagnostic output)'}`;
116
+ }
43
117
  /**
44
118
  * Parse `simpleperf report --sort dso,symbol`. The header row is located by its
45
119
  * `Overhead` column rather than by line number, since simpleperf prefixes the
@@ -105,11 +179,13 @@ async function recordAndroidCpu(deviceId, appId, durationSec, out, report) {
105
179
  else {
106
180
  recordArgs.push('-a');
107
181
  }
108
- await new Promise((resolve, reject) => {
109
- const proc = (0, child_process_1.spawn)(adb, recordArgs, { stdio: 'inherit', env });
110
- proc.on('close', (code) => code === 0 ? resolve() : reject(new Error(`simpleperf record exited with ${code}`)));
111
- proc.on('error', reject);
112
- });
182
+ // Capture rather than inherit: simpleperf prints a wall of `cannot read event
183
+ // type` lines while probing PMU support, and those are not the error. Letting
184
+ // them through buries whatever actually went wrong.
185
+ const rec = await (0, runner_js_1.spawnCommand)(adb, recordArgs, { env });
186
+ if (!rec.success) {
187
+ throw new Error(await explainSimpleperfFailure(deviceId, appId, rec.stderr));
188
+ }
113
189
  // Symbolize on-device before pulling: /system/bin/simpleperf resolves against
114
190
  // the libraries actually loaded there, which a host-side report cannot do
115
191
  // without a matching symfs.
@@ -168,6 +244,10 @@ async function profileCpu(opts, sessionName, profileOpts) {
168
244
  else if (platform === 'android') {
169
245
  entries = await recordAndroidCpu(sessionName, profileOpts.appId, profileOpts.durationSec, out, profileOpts.report ? { top: profileOpts.top ?? 30 } : undefined);
170
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
+ }
171
251
  else if (platform === 'vega') {
172
252
  // Vega is Amazon's own OS, not Android — no simpleperf, no dumpsys. A
173
253
  // physical Fire TV Stick runs Fire OS (Android) over adb and is a
@@ -182,17 +262,41 @@ async function profileCpu(opts, sessionName, profileOpts) {
182
262
  (0, output_js_1.printError)(`profile cpu is not supported on platform ${platform ?? '(unknown)'}`, opts);
183
263
  return 1;
184
264
  }
265
+ const byDso = entries ? rollupByDso(entries) : undefined;
266
+ const unsymPercent = entries ? unsymbolisedPercent(entries) : undefined;
185
267
  if (opts.json) {
186
- (0, output_js_1.printData)({ out, durationSec: profileOpts.durationSec, platform, symbols: entries }, opts);
268
+ (0, output_js_1.printData)({
269
+ out,
270
+ durationSec: profileOpts.durationSec,
271
+ platform,
272
+ symbols: entries,
273
+ byDso,
274
+ unsymbolisedPercent: unsymPercent,
275
+ }, opts);
187
276
  }
188
277
  else {
189
278
  (0, output_js_1.printSuccess)(`profile cpu — recorded ${profileOpts.durationSec}s → ${out}`, opts);
279
+ if (byDso && byDso.length > 0) {
280
+ // Lead with the rollup: a flat profile has no hot symbol, and the real
281
+ // finding is usually which library the samples are spread across.
282
+ console.log(`\n by library`);
283
+ for (const d of byDso.slice(0, 10)) {
284
+ console.log(` ${`${d.percent}%`.padStart(9)} ${d.dso} (${d.symbols} symbol${d.symbols === 1 ? '' : 's'}` +
285
+ `${d.unsymbolised ? ', unsymbolised' : ''})`);
286
+ }
287
+ }
190
288
  if (entries) {
191
- console.log(` ${'overhead'.padStart(9)} symbol`);
289
+ console.log(`\n ${'overhead'.padStart(9)} symbol`);
192
290
  for (const e of entries) {
193
291
  console.log(` ${`${e.percent}%`.padStart(9)} ${e.symbol} [${e.dso}]`);
194
292
  }
195
293
  }
294
+ if (unsymPercent !== undefined && unsymPercent > 10) {
295
+ console.log(`\n note: ${unsymPercent}% of sampled overhead resolved to a raw address rather ` +
296
+ `than a function — those libraries are stripped. Read the by-library rollup above ` +
297
+ `instead of the symbol table; a dozen \`lib.so[+offset]\` rows are one library, ` +
298
+ `not a dozen findings.`);
299
+ }
196
300
  }
197
301
  return 0;
198
302
  }
@@ -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,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|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");
@@ -36,6 +36,7 @@ async function stopDevice(nameOrId, opts, flags) {
36
36
  const includeAndroid = !platform || platform === 'android';
37
37
  const includeWeb = !platform || platform === 'web';
38
38
  const includeVega = !platform || platform === 'vega';
39
+ const includeRoku = !platform || platform === 'roku';
39
40
  const stopped = [];
40
41
  // ── --all mode ───────────────────────────────────────────────────────────
41
42
  if (flags.all) {
@@ -51,6 +52,8 @@ async function stopDevice(nameOrId, opts, flags) {
51
52
  continue;
52
53
  if (d.platform === 'vega' && !includeVega)
53
54
  continue;
55
+ if (d.platform === 'roku' && !includeRoku)
56
+ continue;
54
57
  try {
55
58
  if (d.platform === 'ios' || d.platform === 'tvos') {
56
59
  await shutdownSimulator(d.id);
@@ -62,6 +65,10 @@ async function stopDevice(nameOrId, opts, flags) {
62
65
  // Vega VVD lifecycle is owned by Amazon's tooling — we only stop our log daemon.
63
66
  await (0, client_js_1.stopDaemon)(d.id);
64
67
  }
68
+ else if (d.platform === 'roku') {
69
+ // A Roku is physical hardware we never booted; there is nothing to stop.
70
+ continue;
71
+ }
65
72
  stopped.push({ id: d.id, name: d.name, platform: d.platform });
66
73
  }
67
74
  catch (e) {
@@ -104,6 +111,10 @@ async function stopDevice(nameOrId, opts, flags) {
104
111
  // Vega VVD lifecycle is owned by Amazon's tooling — we only stop our log daemon.
105
112
  await (0, client_js_1.stopDaemon)(match.id);
106
113
  }
114
+ else if (match.platform === 'roku') {
115
+ (0, output_js_1.printError)(`${match.name} is a physical Roku device — conductor never booted it, so there is nothing to stop.`, opts);
116
+ return 1;
117
+ }
107
118
  }
108
119
  catch (e) {
109
120
  (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());
@@ -71,7 +72,9 @@ async function swipe(direction, opts = {}, sessionName = 'default', flags = {})
71
72
  }
72
73
  await driver.swipe(startX, startY, endX, endY, durationMs);
73
74
  }
74
- else if (driver instanceof android_js_1.AndroidDriver || driver instanceof vega_js_1.VegaDriver) {
75
+ else if (driver instanceof android_js_1.AndroidDriver ||
76
+ driver instanceof vega_js_1.VegaDriver ||
77
+ driver instanceof roku_js_1.RokuDriver) {
75
78
  const { widthPixels: w, heightPixels: h } = await driver.deviceInfo();
76
79
  const durationMs = flags.duration ?? 500;
77
80
  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
  }
@@ -177,8 +177,9 @@ let _driverStartError = null;
177
177
  async function ensureDriverRunning() {
178
178
  if (_restartInProgress || !_driverStarted)
179
179
  return;
180
- // Vega has no driver process/port to health-check — control is host-side via the CLI.
181
- if (driverPlatform === 'vega')
180
+ // Vega and Roku have no driver process/port to health-check — control is
181
+ // host-side (the vega CLI) or over the network (Roku ECP).
182
+ if (driverPlatform === 'vega' || driverPlatform === 'roku')
182
183
  return;
183
184
  let alive;
184
185
  if (driverPlatform === 'android') {
@@ -317,6 +318,9 @@ async function main() {
317
318
  else if (driverPlatform === 'vega') {
318
319
  dlog('vega: no driver process to stop (control is host-side via the CLI)');
319
320
  }
321
+ else if (driverPlatform === 'roku') {
322
+ dlog('roku: no driver process to stop (control is ECP over the network)');
323
+ }
320
324
  else if (driverPlatform === 'web') {
321
325
  dlog('Stopping web driver');
322
326
  try {
@@ -449,6 +453,12 @@ async function main() {
449
453
  _driverStarted = true;
450
454
  dlog('vega: no driver process to start; collecting logs only');
451
455
  }
456
+ else if (platform === 'roku') {
457
+ // Roku has neither a driver process nor a log stream to collect, so
458
+ // the daemon has nothing to do — commands drive the device directly.
459
+ _driverStarted = true;
460
+ dlog('roku: no driver process and no device logs; daemon is idle');
461
+ }
452
462
  else {
453
463
  await startDriverForPlatform(platform);
454
464
  }
@@ -487,8 +497,8 @@ async function main() {
487
497
  });
488
498
  }
489
499
  /**
490
- * Bring up the driver process for a non-vega platform. Sets `_driverStarted` /
491
- * `_driverStartError`. Extracted so the vega path can skip it entirely.
500
+ * Bring up the driver process for a platform that has one. Sets `_driverStarted` /
501
+ * `_driverStartError`. Extracted so the vega and roku paths can skip it entirely.
492
502
  */
493
503
  async function startDriverForPlatform(platform) {
494
504
  let driverAlive;
@@ -63,6 +63,11 @@ async function detectPlatform(deviceId) {
63
63
  _platformCache.set(deviceId, 'vega');
64
64
  return 'vega';
65
65
  }
66
+ // Roku: "roku:<host>" (e.g. "roku:192.168.1.100")
67
+ if (deviceId === 'roku' || deviceId.startsWith('roku:')) {
68
+ _platformCache.set(deviceId, 'roku');
69
+ return 'roku';
70
+ }
66
71
  // Check if it looks like an iOS/tvOS simulator UUID (8-4-4-4-12 hex chars)
67
72
  const iosUuidRe = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
68
73
  if (iosUuidRe.test(deviceId)) {
@@ -174,6 +179,11 @@ async function getDriverPort(platform, deviceId) {
174
179
  else if (platform === 'vega') {
175
180
  port = state.nextVegaPort++;
176
181
  }
182
+ else if (platform === 'roku') {
183
+ // Roku is driven entirely over the network (ECP on the device's own port
184
+ // 8060) — there is no host-side driver process, so no port to reserve.
185
+ return 0;
186
+ }
177
187
  else {
178
188
  port = state.nextAndroidPort++;
179
189
  }
@@ -21,6 +21,8 @@ const ios_js_1 = require("./ios.js");
21
21
  const android_js_1 = require("./android.js");
22
22
  const web_js_1 = require("./web.js");
23
23
  const vega_js_1 = require("./vega.js");
24
+ const roku_js_1 = require("./roku.js");
25
+ const key_mapping_js_1 = require("./roku/key-mapping.js");
24
26
  const wait_js_1 = require("./wait.js");
25
27
  const direct_ios_selector_js_1 = require("./direct-ios-selector.js");
26
28
  const perf_hooks_1 = require("perf_hooks");
@@ -552,7 +554,9 @@ function getConductorObj(driver, output) {
552
554
  ? 'web'
553
555
  : driver instanceof vega_js_1.VegaDriver
554
556
  ? 'vega'
555
- : 'android';
557
+ : driver instanceof roku_js_1.RokuDriver
558
+ ? 'roku'
559
+ : 'android';
556
560
  return {
557
561
  platform,
558
562
  copiedText: output['__copiedText'] ?? '',
@@ -684,7 +688,7 @@ async function executeCommandBody(key, val, driver, opts) {
684
688
  await driver.pressKey('delete');
685
689
  }
686
690
  else {
687
- // Android, web, and vega all expose eraseAllText.
691
+ // Android, web, vega, and roku all expose eraseAllText.
688
692
  await driver.eraseAllText(n);
689
693
  }
690
694
  break;
@@ -825,7 +829,7 @@ async function executeCommandBody(key, val, driver, opts) {
825
829
  await driver.back();
826
830
  else if (driver instanceof web_js_1.WebDriver)
827
831
  await driver.goBack();
828
- else if (driver instanceof vega_js_1.VegaDriver)
832
+ else if (driver instanceof vega_js_1.VegaDriver || driver instanceof roku_js_1.RokuDriver)
829
833
  await driver.back();
830
834
  // iOS has no hardware back button — noop
831
835
  break;
@@ -1113,6 +1117,11 @@ async function executeCommandBody(key, val, driver, opts) {
1113
1117
  throw new Error(`pressKey: key "${val}" is not supported on vega`);
1114
1118
  await driver.pressButton(button);
1115
1119
  }
1120
+ else if (driver instanceof roku_js_1.RokuDriver) {
1121
+ if (!(0, key_mapping_js_1.rokuEcpKey)(keyName))
1122
+ throw new Error(`pressKey: key "${val}" is not supported on roku`);
1123
+ await driver.pressKeyNamed(keyName);
1124
+ }
1116
1125
  else {
1117
1126
  const keycode = ANDROID_KEYCODES[keyName];
1118
1127
  if (keycode === undefined)
@@ -1133,6 +1142,10 @@ async function executeCommandBody(key, val, driver, opts) {
1133
1142
  else if (driver instanceof vega_js_1.VegaDriver) {
1134
1143
  // No reliable keyboard-hide primitive on vega — noop
1135
1144
  }
1145
+ else if (driver instanceof roku_js_1.RokuDriver) {
1146
+ // Roku dismisses its on-screen keyboard with Back.
1147
+ await driver.back();
1148
+ }
1136
1149
  else {
1137
1150
  await driver.pressKeyEvent(111); // KEYCODE_ESCAPE
1138
1151
  }