@houwert/conductor 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +12 -9
  2. package/dist/commands/assert-not-visible.js +4 -0
  3. package/dist/commands/assert-visible.js +4 -0
  4. package/dist/commands/back.js +4 -0
  5. package/dist/commands/cheat-sheet.js +11 -8
  6. package/dist/commands/delete-device.js +222 -0
  7. package/dist/commands/device-pool.js +33 -22
  8. package/dist/commands/download-app.js +87 -0
  9. package/dist/commands/erase-text.js +4 -0
  10. package/dist/commands/focused.js +39 -0
  11. package/dist/commands/foreground-app.js +4 -0
  12. package/dist/commands/hide-keyboard.js +4 -0
  13. package/dist/commands/inspect.js +8 -0
  14. package/dist/commands/install.js +103 -27
  15. package/dist/commands/launch-app.js +6 -0
  16. package/dist/commands/list-devices.js +39 -2
  17. package/dist/commands/logs.js +193 -0
  18. package/dist/commands/press-key.js +16 -0
  19. package/dist/commands/screenshot.js +1 -1
  20. package/dist/commands/scroll-until-visible.js +11 -0
  21. package/dist/commands/scroll.js +6 -0
  22. package/dist/commands/start-device.js +38 -4
  23. package/dist/commands/stop-app.js +4 -0
  24. package/dist/commands/swipe.js +22 -0
  25. package/dist/commands/tap.js +10 -3
  26. package/dist/commands/type.js +2 -2
  27. package/dist/commands/uninstall-app.js +4 -0
  28. package/dist/daemon/client.js +72 -9
  29. package/dist/daemon/log-collector.js +408 -0
  30. package/dist/daemon/server.js +110 -32
  31. package/dist/daemon/web-server.js +812 -0
  32. package/dist/device-picker.js +7 -2
  33. package/dist/drivers/bootstrap.js +124 -1
  34. package/dist/drivers/element-resolver.js +241 -30
  35. package/dist/drivers/flow-runner.js +63 -21
  36. package/dist/drivers/log-sources/android.js +156 -0
  37. package/dist/drivers/log-sources/daemon.js +112 -0
  38. package/dist/drivers/log-sources/ios.js +106 -0
  39. package/dist/drivers/log-sources/metro.js +252 -0
  40. package/dist/drivers/log-sources/types.js +13 -0
  41. package/dist/drivers/log-sources/web.js +96 -0
  42. package/dist/drivers/wait.js +57 -0
  43. package/dist/drivers/web.js +173 -0
  44. package/dist/index.js +62 -12
  45. package/dist/runner.js +32 -2
  46. package/drivers/ios/conductor-driver-ios.zip +0 -0
  47. package/drivers/ios/conductor-driver-iosUITests-Runner.zip +0 -0
  48. package/drivers/tvos/conductor-driver-tvos.zip +0 -0
  49. package/drivers/tvos/conductor-driver-tvosUITests-Runner.zip +0 -0
  50. package/package.json +5 -2
  51. package/skills/conductor/SKILL.md +72 -41
  52. package/skills/skills.yaml +1 -1
@@ -3,13 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HELP = void 0;
4
4
  exports.startDevice = startDevice;
5
5
  exports.HELP = ` start-device
6
- --platform <ios|android|tvos> Boot a simulator or emulator
6
+ --platform <ios|android|tvos|web> Boot a simulator/emulator, or start the web driver (Playwright)
7
7
  --os-version <n> iOS/tvOS version (e.g. 18) or Android API level (e.g. 33)
8
8
  --avd <name> Android AVD name (default: first available)
9
9
  --name <name> Set a custom name on the simulator after boot (iOS/tvOS only)
10
- --device-type <name> iOS/tvOS device type (e.g. "iPhone 16 Pro", "Apple TV 4K"); creates if needed`;
10
+ --device-type <name> iOS/tvOS device type (e.g. "iPhone 16 Pro", "Apple TV 4K"); creates if needed
11
+ --browser <chromium|firefox|webkit> Web only: which Playwright browser to launch (default: chromium)`;
11
12
  const child_process_1 = require("child_process");
12
13
  const runner_js_1 = require("../runner.js");
14
+ const client_js_1 = require("../daemon/client.js");
13
15
  const output_js_1 = require("../output.js");
14
16
  const utils_js_1 = require("../utils.js");
15
17
  const IOS_BOOT_TIMEOUT_MS = 120000;
@@ -460,10 +462,40 @@ async function startAndroid(avdName, opts) {
460
462
  (0, output_js_1.printSuccess)(`Emulator ready: ${target} (${deviceId})`, opts);
461
463
  return 0;
462
464
  }
465
+ function webSessionIdForBrowser(browserArg) {
466
+ const b = (browserArg ?? 'chromium').toLowerCase();
467
+ switch (b) {
468
+ case 'chromium':
469
+ return { session: 'web' };
470
+ case 'firefox':
471
+ return { session: 'web:firefox' };
472
+ case 'webkit':
473
+ return { session: 'web:webkit' };
474
+ default:
475
+ return {
476
+ error: `Unknown web browser "${browserArg}". Use chromium, firefox, or webkit.`,
477
+ };
478
+ }
479
+ }
480
+ async function startWebDriver(opts, browser) {
481
+ const resolved = webSessionIdForBrowser(browser);
482
+ if ('error' in resolved) {
483
+ (0, output_js_1.printError)(resolved.error, opts);
484
+ return 1;
485
+ }
486
+ const ready = await (0, client_js_1.startDaemon)(resolved.session);
487
+ if (!ready) {
488
+ (0, output_js_1.printError)(`Web driver did not become ready for session ${resolved.session}. ` +
489
+ 'Install a browser with `conductor install-web` if needed, then retry.', opts);
490
+ return 1;
491
+ }
492
+ (0, output_js_1.printSuccess)(`Web driver ready (${resolved.session})`, opts);
493
+ return 0;
494
+ }
463
495
  // ── Entry point ───────────────────────────────────────────────────────────────
464
496
  async function startDevice(platform, opts, flags) {
465
497
  if (!platform) {
466
- (0, output_js_1.printError)('start-device requires --platform ios|android', opts);
498
+ (0, output_js_1.printError)('start-device requires --platform ios|android|tvos|web', opts);
467
499
  return 1;
468
500
  }
469
501
  switch (platform.toLowerCase()) {
@@ -473,8 +505,10 @@ async function startDevice(platform, opts, flags) {
473
505
  return startTvOS(flags.osVersion, opts, flags.name, flags.deviceType);
474
506
  case 'android':
475
507
  return startAndroid(flags.avd, opts);
508
+ case 'web':
509
+ return startWebDriver(opts, flags.browser);
476
510
  default:
477
- (0, output_js_1.printError)(`Unknown platform "${platform}". Use ios, android, or tvos.`, opts);
511
+ (0, output_js_1.printError)(`Unknown platform "${platform}". Use ios, android, tvos, or web.`, opts);
478
512
  return 1;
479
513
  }
480
514
  }
@@ -8,6 +8,7 @@ const session_js_1 = require("../session.js");
8
8
  const output_js_1 = require("../output.js");
9
9
  const ios_js_1 = require("../drivers/ios.js");
10
10
  const android_js_1 = require("../drivers/android.js");
11
+ const web_js_1 = require("../drivers/web.js");
11
12
  async function stopApp(appId, opts = {}, sessionName = 'default') {
12
13
  const session = await (0, session_js_1.getSession)(sessionName);
13
14
  const resolvedAppId = appId ?? session.appId;
@@ -19,6 +20,9 @@ async function stopApp(appId, opts = {}, sessionName = 'default') {
19
20
  if (driver instanceof ios_js_1.IOSDriver) {
20
21
  await driver.terminateApp(resolvedAppId);
21
22
  }
23
+ else if (driver instanceof web_js_1.WebDriver) {
24
+ await driver.terminateApp();
25
+ }
22
26
  else if (driver instanceof android_js_1.AndroidDriver) {
23
27
  await driver.stopApp(resolvedAppId);
24
28
  }
@@ -11,6 +11,7 @@ const runner_js_1 = require("../runner.js");
11
11
  const output_js_1 = require("../output.js");
12
12
  const ios_js_1 = require("../drivers/ios.js");
13
13
  const android_js_1 = require("../drivers/android.js");
14
+ const web_js_1 = require("../drivers/web.js");
14
15
  const utils_js_1 = require("../utils.js");
15
16
  function parseCoordPair(s) {
16
17
  const [xs, ys] = s.split(',').map((p) => p.trim());
@@ -48,6 +49,27 @@ async function swipe(direction, opts = {}, sessionName = 'default', flags = {})
48
49
  }
49
50
  await driver.swipe(startX, startY, endX, endY, durationSec);
50
51
  }
52
+ else if (driver instanceof web_js_1.WebDriver) {
53
+ const { widthPixels: w, heightPixels: h } = await driver.deviceInfo();
54
+ const durationMs = flags.duration ?? 500;
55
+ if (flags.start && flags.end) {
56
+ const s = parseCoordPair(flags.start);
57
+ const e = parseCoordPair(flags.end);
58
+ startX = s.x <= 1 ? s.x * w : s.x;
59
+ startY = s.y <= 1 ? s.y * h : s.y;
60
+ endX = e.x <= 1 ? e.x * w : e.x;
61
+ endY = e.y <= 1 ? e.y * h : e.y;
62
+ }
63
+ else {
64
+ const normalized = direction.toLowerCase();
65
+ const coords = (0, utils_js_1.swipeCoords)(normalized);
66
+ startX = coords.startX * w;
67
+ startY = coords.startY * h;
68
+ endX = coords.endX * w;
69
+ endY = coords.endY * h;
70
+ }
71
+ await driver.swipe(startX, startY, endX, endY, durationMs);
72
+ }
51
73
  else if (driver instanceof android_js_1.AndroidDriver) {
52
74
  const { widthPixels: w, heightPixels: h } = await driver.deviceInfo();
53
75
  const durationMs = flags.duration ?? 500;
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HELP = void 0;
4
4
  exports.tap = tap;
5
- exports.HELP = ` tap <element> Tap element by text or id
5
+ exports.HELP = ` tap-on <element> Tap element by text or id
6
6
  --id <id> Match by accessibility id instead of text
7
7
  --text <text> Match by text only (not id)
8
8
  --index <n> Pick the nth match (0-based)
@@ -21,11 +21,12 @@ const runner_js_1 = require("../runner.js");
21
21
  const output_js_1 = require("../output.js");
22
22
  const ios_js_1 = require("../drivers/ios.js");
23
23
  const android_js_1 = require("../drivers/android.js");
24
+ const web_js_1 = require("../drivers/web.js");
24
25
  const wait_js_1 = require("../drivers/wait.js");
25
26
  const utils_js_1 = require("../utils.js");
26
27
  async function tap(query, opts = {}, sessionName = 'default', flags = {}) {
27
28
  if (!query && !flags.id && !flags.text) {
28
- (0, output_js_1.printError)('tap requires <element> or --id <id>', opts);
29
+ (0, output_js_1.printError)('tap-on requires <element> or --id <id>', opts);
29
30
  return 1;
30
31
  }
31
32
  const sel = {
@@ -43,13 +44,16 @@ async function tap(query, opts = {}, sessionName = 'default', flags = {}) {
43
44
  const label = flags.text ? `text="${flags.text}"` : flags.id ? `id="${flags.id}"` : `"${query}"`;
44
45
  const result = await (0, runner_js_1.runDirect)(async (driver) => {
45
46
  if (driver instanceof ios_js_1.IOSDriver && driver.platform === 'tvos') {
46
- throw new Error('tap is not supported on tvOS — Apple TV uses focus-based navigation.\n' +
47
+ throw new Error('tap-on is not supported on tvOS — Apple TV uses focus-based navigation.\n' +
47
48
  'Use press-key to navigate (e.g. conductor press-key "Remote Dpad Center").');
48
49
  }
49
50
  let el;
50
51
  if (driver instanceof ios_js_1.IOSDriver) {
51
52
  el = await (0, wait_js_1.waitForIOSElement)(() => driver.viewHierarchy().then((h) => h.axElement), sel);
52
53
  }
54
+ else if (driver instanceof web_js_1.WebDriver) {
55
+ el = await (0, wait_js_1.waitForWebElement)(() => driver.viewHierarchy(), sel);
56
+ }
53
57
  else if (driver instanceof android_js_1.AndroidDriver) {
54
58
  el = await (0, wait_js_1.waitForAndroidElement)(() => driver.viewHierarchy(), sel);
55
59
  }
@@ -60,6 +64,9 @@ async function tap(query, opts = {}, sessionName = 'default', flags = {}) {
60
64
  if (driver instanceof ios_js_1.IOSDriver) {
61
65
  await driver.tap(el.centerX, el.centerY, 1.5);
62
66
  }
67
+ else if (driver instanceof web_js_1.WebDriver) {
68
+ await driver.tap(el.centerX, el.centerY, 1.5);
69
+ }
63
70
  else {
64
71
  await driver.swipe(el.centerX, el.centerY, el.centerX, el.centerY, 1500);
65
72
  }
@@ -2,12 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HELP = void 0;
4
4
  exports.typeText = typeText;
5
- exports.HELP = ` type <text> Type text into focused field`;
5
+ exports.HELP = ` input-text <text> Type text into focused field`;
6
6
  const runner_js_1 = require("../runner.js");
7
7
  const output_js_1 = require("../output.js");
8
8
  async function typeText(text, opts = {}, sessionName = 'default') {
9
9
  if (text === undefined || text === '') {
10
- (0, output_js_1.printError)('type requires <text>', opts);
10
+ (0, output_js_1.printError)('input-text requires <text>', opts);
11
11
  return 1;
12
12
  }
13
13
  const result = await (0, runner_js_1.runDirect)(async (driver) => {
@@ -7,6 +7,7 @@ const runner_js_1 = require("../runner.js");
7
7
  const output_js_1 = require("../output.js");
8
8
  const ios_js_1 = require("../drivers/ios.js");
9
9
  const android_js_1 = require("../drivers/android.js");
10
+ const web_js_1 = require("../drivers/web.js");
10
11
  async function uninstallApp(appId, opts = {}, sessionName = 'default') {
11
12
  if (!appId) {
12
13
  (0, output_js_1.printError)('uninstall-app requires <appId>', opts);
@@ -16,6 +17,9 @@ async function uninstallApp(appId, opts = {}, sessionName = 'default') {
16
17
  if (driver instanceof ios_js_1.IOSDriver) {
17
18
  await driver.uninstallApp(appId);
18
19
  }
20
+ else if (driver instanceof web_js_1.WebDriver) {
21
+ throw new Error('uninstall-app is not supported on web');
22
+ }
19
23
  else if (driver instanceof android_js_1.AndroidDriver) {
20
24
  await driver.uninstallApp(appId);
21
25
  }
@@ -7,28 +7,30 @@ exports.startDaemon = startDaemon;
7
7
  exports.stopDaemon = stopDaemon;
8
8
  exports.listDaemonSessions = listDaemonSessions;
9
9
  exports.daemonStatus = daemonStatus;
10
- const net_1 = __importDefault(require("net"));
10
+ exports.findRunningWebSession = findRunningWebSession;
11
+ exports.fetchDaemonLogs = fetchDaemonLogs;
12
+ const http_1 = __importDefault(require("http"));
11
13
  const fs_1 = __importDefault(require("fs"));
12
14
  const os_1 = __importDefault(require("os"));
13
15
  const path_1 = __importDefault(require("path"));
14
16
  const child_process_1 = require("child_process");
15
17
  const protocol_js_1 = require("./protocol.js");
16
18
  const verbose_js_1 = require("../verbose.js");
19
+ const bootstrap_js_1 = require("../drivers/bootstrap.js");
17
20
  const STARTUP_POLL_MS = 200;
18
21
  const STARTUP_MAX_WAIT_MS = 10000;
19
22
  async function socketExists(sessionName) {
20
23
  return new Promise((resolve) => {
21
- const sock = net_1.default.createConnection((0, protocol_js_1.socketPath)(sessionName));
22
- sock.on('connect', () => {
23
- sock.destroy();
24
- resolve(true);
24
+ const req = http_1.default.get({ socketPath: (0, protocol_js_1.socketPath)(sessionName), path: '/status' }, (res) => {
25
+ res.resume();
26
+ resolve(res.statusCode === 200);
25
27
  });
26
- sock.on('error', () => resolve(false));
27
- sock.setTimeout(500);
28
- sock.on('timeout', () => {
29
- sock.destroy();
28
+ req.setTimeout(500);
29
+ req.on('timeout', () => {
30
+ req.destroy();
30
31
  resolve(false);
31
32
  });
33
+ req.on('error', () => resolve(false));
32
34
  });
33
35
  }
34
36
  async function waitForDaemon(sessionName) {
@@ -110,3 +112,64 @@ async function daemonStatus(sessionName = 'default') {
110
112
  return { running: true };
111
113
  }
112
114
  }
115
+ /**
116
+ * Find a running web daemon session that matches the given browser type.
117
+ * Scans `~/.conductor/daemons/` for `web:*` directories whose daemon socket
118
+ * is still alive. Returns the session name, or undefined if none found.
119
+ */
120
+ async function findRunningWebSession(browserName) {
121
+ for (const session of listDaemonSessions()) {
122
+ if (!(session === 'web' || session.startsWith('web:')))
123
+ continue;
124
+ if ((0, bootstrap_js_1.webBrowserName)(session) !== browserName)
125
+ continue;
126
+ if (await socketExists(session))
127
+ return session;
128
+ }
129
+ return undefined;
130
+ }
131
+ /**
132
+ * Fetch buffered log entries from the daemon's /logs HTTP endpoint.
133
+ * Used by `conductor logs --recent` for snapshot access.
134
+ *
135
+ * Pass `metro` port to opt in to Metro auto-discovery for React Native apps.
136
+ * The daemon will start polling Metro's /json endpoint for a debugger target
137
+ * matching this device and merge JS console entries into the log buffer.
138
+ */
139
+ async function fetchDaemonLogs(sessionName, opts = {}) {
140
+ const params = new URLSearchParams();
141
+ if (opts.since)
142
+ params.set('since', opts.since);
143
+ if (opts.level)
144
+ params.set('level', opts.level);
145
+ if (opts.limit)
146
+ params.set('limit', String(opts.limit));
147
+ if (opts.metro === 'auto') {
148
+ params.set('metro', '');
149
+ }
150
+ else if (opts.metro) {
151
+ params.set('metro', String(opts.metro));
152
+ }
153
+ const qs = params.toString();
154
+ const reqPath = qs ? `/logs?${qs}` : '/logs';
155
+ return new Promise((resolve, reject) => {
156
+ const req = http_1.default.get({ socketPath: (0, protocol_js_1.socketPath)(sessionName), path: reqPath }, (res) => {
157
+ const chunks = [];
158
+ res.on('data', (chunk) => chunks.push(chunk));
159
+ res.on('end', () => {
160
+ try {
161
+ const data = JSON.parse(Buffer.concat(chunks).toString('utf-8'));
162
+ resolve(data.entries ?? []);
163
+ }
164
+ catch {
165
+ resolve([]);
166
+ }
167
+ });
168
+ });
169
+ req.setTimeout(5000, () => {
170
+ req.destroy();
171
+ reject(new Error('Timeout fetching daemon logs'));
172
+ });
173
+ req.on('error', reject);
174
+ });
175
+ }