@houwert/conductor 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,6 +16,7 @@ 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 cdp_discovery_js_1 = require("../drivers/cdp-discovery.js");
19
20
  // Captured during discoverAvailableDevices so listDevices() can report it.
20
21
  // Module-scoped because the discover function returns Device[]; bolting an
21
22
  // extra return field onto the public type would ripple beyond this fix.
@@ -104,6 +105,9 @@ async function discoverBootedDevices() {
104
105
  catch {
105
106
  /* vega CLI not installed */
106
107
  }
108
+ // External CDP endpoints (e.g. an Electron app started with
109
+ // --remote-debugging-port): one device per discovered webview/tile.
110
+ devices.push(...(await (0, cdp_discovery_js_1.discoverCdpDevices)()));
107
111
  return devices;
108
112
  }
109
113
  async function discoverAvailableDevices() {
@@ -1,7 +1,4 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.HELP = void 0;
7
4
  exports.webTargets = webTargets;
@@ -14,37 +11,8 @@ exports.webTargets = webTargets;
14
11
  * Playwright browser and works before any daemon session exists. Use the printed
15
12
  * target IDs with `--cdp-url` / `--cdp-target` to bind a session to a tile.
16
13
  */
17
- const http_1 = __importDefault(require("http"));
18
14
  const output_js_1 = require("../output.js");
19
- /** Derive the `http://host:port` base from a CDP URL (which may be ws:// or include a path). */
20
- function httpBase(cdpUrl) {
21
- const u = new URL(cdpUrl);
22
- const proto = u.protocol === 'https:' || u.protocol === 'wss:' ? 'https:' : 'http:';
23
- return `${proto}//${u.host}`;
24
- }
25
- function fetchTargets(cdpUrl) {
26
- const url = `${httpBase(cdpUrl)}/json/list`;
27
- return new Promise((resolve, reject) => {
28
- const req = http_1.default.get(url, (res) => {
29
- const chunks = [];
30
- res.on('data', (c) => chunks.push(c));
31
- res.on('end', () => {
32
- if ((res.statusCode ?? 0) >= 300) {
33
- reject(new Error(`HTTP ${res.statusCode} from ${url}`));
34
- return;
35
- }
36
- try {
37
- resolve(JSON.parse(Buffer.concat(chunks).toString('utf-8')));
38
- }
39
- catch (err) {
40
- reject(err);
41
- }
42
- });
43
- });
44
- req.setTimeout(5000, () => req.destroy(new Error(`Timed out fetching ${url}`)));
45
- req.on('error', reject);
46
- });
47
- }
15
+ const cdp_discovery_js_1 = require("../drivers/cdp-discovery.js");
48
16
  async function webTargets(cdpUrl, opts) {
49
17
  if (!cdpUrl) {
50
18
  console.error('web-targets requires --cdp-url <url> (e.g. --cdp-url http://127.0.0.1:9222).\n' +
@@ -53,7 +21,7 @@ async function webTargets(cdpUrl, opts) {
53
21
  }
54
22
  let targets;
55
23
  try {
56
- targets = await fetchTargets(cdpUrl);
24
+ targets = await (0, cdp_discovery_js_1.fetchCdpTargets)(cdpUrl);
57
25
  }
58
26
  catch (err) {
59
27
  console.error(`Could not reach CDP endpoint at ${cdpUrl}: ${err instanceof Error ? err.message : String(err)}`);
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DEFAULT_CDP_PORTS = void 0;
7
+ exports.httpBase = httpBase;
8
+ exports.fetchCdpTargets = fetchCdpTargets;
9
+ exports.formatCdpDeviceId = formatCdpDeviceId;
10
+ exports.parseCdpDeviceId = parseCdpDeviceId;
11
+ exports.cdpTargetsToDevices = cdpTargetsToDevices;
12
+ exports.discoverCdpDevices = discoverCdpDevices;
13
+ /**
14
+ * Discovery of externally-launched CDP endpoints (e.g. an Electron app started
15
+ * with `--remote-debugging-port`, which exposes one page target per webview/tile).
16
+ *
17
+ * Unlike the Playwright web driver (which conductor launches itself) these
18
+ * browsers already exist — so "discovery" means finding the DevTools HTTP
19
+ * endpoint. CDP servers don't advertise themselves, so we probe a small range of
20
+ * localhost ports and enumerate each reachable endpoint's page targets via
21
+ * `/json/list`. The same target-fetch is reused by the `web-targets` command.
22
+ */
23
+ const http_1 = __importDefault(require("http"));
24
+ /** Default localhost ports scanned for CDP endpoints. Covers Chromium/Electron's
25
+ * conventional `--remote-debugging-port` values without a wide scan. */
26
+ exports.DEFAULT_CDP_PORTS = [9222, 9223, 9224, 9225, 9226, 9227, 9228, 9229];
27
+ const PROBE_TIMEOUT_MS = 300;
28
+ /** Derive the `http://host:port` base from a CDP URL (which may be ws:// or include a path). */
29
+ function httpBase(cdpUrl) {
30
+ const u = new URL(cdpUrl);
31
+ const proto = u.protocol === 'https:' || u.protocol === 'wss:' ? 'https:' : 'http:';
32
+ return `${proto}//${u.host}`;
33
+ }
34
+ /** GET a CDP DevTools JSON endpoint, parsing the array response. */
35
+ function getJson(url, timeoutMs) {
36
+ return new Promise((resolve, reject) => {
37
+ const req = http_1.default.get(url, (res) => {
38
+ const chunks = [];
39
+ res.on('data', (c) => chunks.push(c));
40
+ res.on('end', () => {
41
+ if ((res.statusCode ?? 0) >= 300) {
42
+ reject(new Error(`HTTP ${res.statusCode} from ${url}`));
43
+ return;
44
+ }
45
+ try {
46
+ resolve(JSON.parse(Buffer.concat(chunks).toString('utf-8')));
47
+ }
48
+ catch (err) {
49
+ reject(err);
50
+ }
51
+ });
52
+ });
53
+ req.setTimeout(timeoutMs, () => req.destroy(new Error(`Timed out fetching ${url}`)));
54
+ req.on('error', reject);
55
+ });
56
+ }
57
+ /** Fetch the CDP page targets exposed at `cdpUrl`'s `/json/list`. */
58
+ function fetchCdpTargets(cdpUrl, timeoutMs = 5000) {
59
+ return getJson(`${httpBase(cdpUrl)}/json/list`, timeoutMs);
60
+ }
61
+ /**
62
+ * Device-id encoding for a discovered CDP webview: `web:cdp:<port>:<targetId>`.
63
+ * Self-describing so the id alone hydrates the CDP url + target at bind time —
64
+ * a discovered webview is drivable with no `--cdp-*` flags. Localhost is assumed
65
+ * (discovery only scans localhost).
66
+ */
67
+ function formatCdpDeviceId(port, targetId) {
68
+ return `web:cdp:${port}:${targetId}`;
69
+ }
70
+ /** Parse a `web:cdp:<port>:<targetId>` device id, or undefined if it isn't one. */
71
+ function parseCdpDeviceId(deviceId) {
72
+ const m = /^web:cdp:(\d+):(.+)$/.exec(deviceId);
73
+ if (!m)
74
+ return undefined;
75
+ const port = Number(m[1]);
76
+ if (!Number.isInteger(port) || port <= 0)
77
+ return undefined;
78
+ return { port, targetId: m[2], cdpUrl: `http://127.0.0.1:${port}` };
79
+ }
80
+ /** Map a reachable endpoint's page targets to discovered `web` devices. */
81
+ function cdpTargetsToDevices(port, targets) {
82
+ return targets
83
+ .filter((t) => t.type === 'page')
84
+ .map((t) => ({
85
+ id: formatCdpDeviceId(port, t.id),
86
+ name: t.title || t.url || t.id,
87
+ platform: 'web',
88
+ status: 'running',
89
+ }));
90
+ }
91
+ /**
92
+ * Scan localhost CDP ports and return every discovered webview as a device.
93
+ * Probes run in parallel with a short timeout and swallow all errors — an
94
+ * unreachable port simply contributes nothing. Safe to call on the hot device-
95
+ * resolution path.
96
+ */
97
+ async function discoverCdpDevices(ports = exports.DEFAULT_CDP_PORTS) {
98
+ const results = await Promise.all(ports.map(async (port) => {
99
+ try {
100
+ const targets = await fetchCdpTargets(`http://127.0.0.1:${port}`, PROBE_TIMEOUT_MS);
101
+ return cdpTargetsToDevices(port, targets);
102
+ }
103
+ catch {
104
+ return [];
105
+ }
106
+ }));
107
+ return results.flat();
108
+ }
package/dist/index.js CHANGED
@@ -64,6 +64,7 @@ const options_js_1 = require("./commands/options.js");
64
64
  const web_targets_js_1 = require("./commands/web-targets.js");
65
65
  const session_js_2 = require("./session.js");
66
66
  const device_picker_js_1 = require("./device-picker.js");
67
+ const cdp_discovery_js_1 = require("./drivers/cdp-discovery.js");
67
68
  const update_check_js_1 = require("./update-check.js");
68
69
  const pkg_root_js_1 = require("./pkg-root.js");
69
70
  const fs_1 = __importDefault(require("fs"));
@@ -326,6 +327,9 @@ async function main() {
326
327
  if (isWebSession && !NO_DEVICE_COMMANDS.has(command)) {
327
328
  const cdpUrlFlag = argv['cdp-url'];
328
329
  const cdpTargetFlag = argv['cdp-target'];
330
+ // A discovered `web:cdp:<port>:<target>` device id is self-describing —
331
+ // derive the CDP url/target from it so it's drivable with no --cdp-* flags.
332
+ const fromDeviceId = (0, cdp_discovery_js_1.parseCdpDeviceId)(sessionName);
329
333
  if (cdpUrlFlag || cdpTargetFlag) {
330
334
  if (cdpUrlFlag)
331
335
  process.env.CONDUCTOR_CDP_URL = cdpUrlFlag;
@@ -336,6 +340,13 @@ async function main() {
336
340
  cdpTargetId: process.env.CONDUCTOR_CDP_TARGET_ID,
337
341
  }, sessionName);
338
342
  }
343
+ else if (fromDeviceId) {
344
+ if (!process.env.CONDUCTOR_CDP_URL)
345
+ process.env.CONDUCTOR_CDP_URL = fromDeviceId.cdpUrl;
346
+ if (!process.env.CONDUCTOR_CDP_TARGET_ID) {
347
+ process.env.CONDUCTOR_CDP_TARGET_ID = fromDeviceId.targetId;
348
+ }
349
+ }
339
350
  else {
340
351
  const saved = await (0, session_js_2.getSession)(sessionName);
341
352
  if (saved.cdpUrl && !process.env.CONDUCTOR_CDP_URL) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@houwert/conductor",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "CLI tool for mobile app interactions — optimized for AI agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -47,6 +47,13 @@ Use a distinct fully-qualified `--device web:chromium:<label>` per target (a bar
47
47
  several webviews can be driven concurrently. Only `type=page` targets are
48
48
  controllable. See [Web testing → Attaching to an existing browser](../../../docs/web.md).
49
49
 
50
+ **Discovery:** endpoints on the conventional debugging ports (9222–9229 on
51
+ localhost) are found automatically — `list-devices` shows each webview as a
52
+ booted `web:cdp:<port>:<targetId>` device. That id is self-describing, so you can
53
+ drive it directly (`conductor --device web:cdp:9222:<targetId> <cmd>`) without
54
+ `--cdp-url`/`--cdp-target`. Use `web-targets --cdp-url <url>` for endpoints on a
55
+ non-default port, or to list targets before binding.
56
+
50
57
  ### Vega (Amazon Fire TV)
51
58
 
52
59
  Vega is a React Native OS driven through Amazon's own `vega`/`kepler` CLI (not