@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
@@ -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 resolveDeviceId(sessionName) {
11
12
  if (sessionName !== 'default')
12
13
  return sessionName;
@@ -35,6 +36,9 @@ async function foregroundApp(opts = {}, sessionName = 'default') {
35
36
  const appIds = deviceId ? await getInstalledAppIds(deviceId) : [];
36
37
  appId = await driver.runningApp(appIds);
37
38
  }
39
+ else if (driver instanceof web_js_1.WebDriver) {
40
+ appId = await driver.runningApp();
41
+ }
38
42
  else if (driver instanceof android_js_1.AndroidDriver) {
39
43
  appId = await driver.getForegroundApp();
40
44
  }
@@ -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 hideKeyboard(opts = {}, sessionName = 'default') {
11
12
  const result = await (0, runner_js_1.runDirect)(async (driver) => {
12
13
  if (driver instanceof ios_js_1.IOSDriver) {
@@ -14,6 +15,9 @@ async function hideKeyboard(opts = {}, sessionName = 'default') {
14
15
  /* no keyboard visible */
15
16
  });
16
17
  }
18
+ else if (driver instanceof web_js_1.WebDriver) {
19
+ // No virtual keyboard on web — no-op
20
+ }
17
21
  else if (driver instanceof android_js_1.AndroidDriver) {
18
22
  await driver.pressKeyEvent(111); // KEYCODE_ESCAPE
19
23
  }
@@ -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
  const element_resolver_js_1 = require("../drivers/element-resolver.js");
11
12
  async function inspect(opts = {}, sessionName = 'default', inspectOpts = {}) {
12
13
  try {
@@ -17,6 +18,9 @@ async function inspect(opts = {}, sessionName = 'default', inspectOpts = {}) {
17
18
  const hierarchy = await driver.viewHierarchy(false);
18
19
  raw = JSON.stringify(hierarchy, null, 2);
19
20
  }
21
+ else if (driver instanceof web_js_1.WebDriver) {
22
+ raw = JSON.stringify(await driver.viewHierarchy(), null, 2);
23
+ }
20
24
  else if (driver instanceof android_js_1.AndroidDriver) {
21
25
  raw = await driver.viewHierarchy();
22
26
  }
@@ -36,6 +40,10 @@ async function inspect(opts = {}, sessionName = 'default', inspectOpts = {}) {
36
40
  const hierarchy = await driver.viewHierarchy(false);
37
41
  text = (0, element_resolver_js_1.inspectIOSToText)(hierarchy.axElement);
38
42
  }
43
+ else if (driver instanceof web_js_1.WebDriver) {
44
+ const hierarchy = await driver.viewHierarchy();
45
+ text = (0, element_resolver_js_1.inspectWebToText)(hierarchy);
46
+ }
39
47
  else if (driver instanceof android_js_1.AndroidDriver) {
40
48
  const xml = await driver.viewHierarchy();
41
49
  text = (0, element_resolver_js_1.inspectAndroidToText)(xml);
@@ -3,32 +3,29 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.HELP = void 0;
7
- exports.installSkills = installSkills;
6
+ exports.HELP_INSTALL_WEB = exports.HELP_INSTALL_SKILLS = exports.HELP_INSTALL_PLUGIN = void 0;
7
+ exports.installPluginCli = installPluginCli;
8
+ exports.installSkillsCli = installSkillsCli;
9
+ exports.installWebCli = installWebCli;
8
10
  exports.installLocalSkills = installLocalSkills;
9
11
  exports.installPlugin = installPlugin;
10
- exports.HELP = ` install Install/reinstall Claude Code plugin
11
- install --skills Copy skills into local .claude/skills/
12
- install --check Print current install status without modifying anything`;
12
+ exports.HELP_INSTALL_PLUGIN = ` install-plugin [--check] Register/update the global Claude Code plugin (status only with --check)`;
13
+ exports.HELP_INSTALL_SKILLS = ` install-skills [--check] Copy skills into local .claude/skills/ (status only with --check)`;
14
+ exports.HELP_INSTALL_WEB = ` install-web [--check] [browser] Install Playwright browser (chromium, firefox, webkit) (status only with --check)`;
13
15
  const fs_1 = __importDefault(require("fs"));
14
16
  const os_1 = __importDefault(require("os"));
15
17
  const path_1 = __importDefault(require("path"));
16
18
  const output_js_1 = require("../output.js");
17
19
  const pkg_root_js_1 = require("../pkg-root.js");
18
- async function installSkills(opts, skillsOnly = false, check = false) {
20
+ const bootstrap_js_1 = require("../drivers/bootstrap.js");
21
+ async function installPluginCli(opts, check) {
19
22
  try {
20
23
  if (check) {
21
- return checkInstallStatus(opts);
22
- }
23
- if (skillsOnly) {
24
- installLocalSkills();
25
- (0, output_js_1.printSuccess)('Conductor skills installed → .claude/skills/conductor/', opts);
26
- }
27
- else {
28
- const version = installPlugin();
29
- const pluginCacheDir = path_1.default.join(os_1.default.homedir(), '.claude', 'plugins', 'cache', 'conductor', 'conductor', version);
30
- (0, output_js_1.printSuccess)(`Conductor plugin installed (v${version}) → ${pluginCacheDir}`, opts);
24
+ return checkPluginInstallStatus(opts);
31
25
  }
26
+ const version = installPlugin();
27
+ const pluginCacheDir = path_1.default.join(os_1.default.homedir(), '.claude', 'plugins', 'cache', 'conductor', 'conductor', version);
28
+ (0, output_js_1.printSuccess)(`Conductor plugin installed (v${version}) → ${pluginCacheDir}`, opts);
32
29
  return 0;
33
30
  }
34
31
  catch (err) {
@@ -37,22 +34,18 @@ async function installSkills(opts, skillsOnly = false, check = false) {
37
34
  return 1;
38
35
  }
39
36
  }
40
- function checkInstallStatus(opts) {
37
+ function checkPluginInstallStatus(opts) {
41
38
  const installedPluginsPath = path_1.default.join(os_1.default.homedir(), '.claude', 'plugins', 'installed_plugins.json');
42
- const localSkillsPath = path_1.default.join(process.cwd(), '.claude', 'skills', 'conductor', 'SKILL.md');
43
39
  let pluginVersion = null;
44
40
  if (fs_1.default.existsSync(installedPluginsPath)) {
45
41
  const installed = JSON.parse(fs_1.default.readFileSync(installedPluginsPath, 'utf8'));
46
- const entry = installed.plugins.find((p) => p.name === 'conductor');
42
+ const plugins = Array.isArray(installed.plugins) ? installed.plugins : [];
43
+ const entry = plugins.find((p) => p.name === 'conductor');
47
44
  if (entry)
48
45
  pluginVersion = entry.version;
49
46
  }
50
- const hasLocalSkills = fs_1.default.existsSync(localSkillsPath);
51
47
  if (opts.json) {
52
- (0, output_js_1.printData)({
53
- globalPlugin: { installed: pluginVersion !== null, version: pluginVersion },
54
- localSkills: { installed: hasLocalSkills },
55
- }, opts);
48
+ (0, output_js_1.printData)({ globalPlugin: { installed: pluginVersion !== null, version: pluginVersion } }, opts);
56
49
  }
57
50
  else {
58
51
  if (pluginVersion) {
@@ -60,16 +53,75 @@ function checkInstallStatus(opts) {
60
53
  }
61
54
  else {
62
55
  console.log('Global plugin: not installed');
56
+ console.log('Run `npm install -g @houwert/conductor` or `conductor install-plugin` to register it.');
57
+ }
58
+ }
59
+ return 0;
60
+ }
61
+ async function installSkillsCli(opts, check) {
62
+ try {
63
+ if (check) {
64
+ return checkSkillsInstallStatus(opts);
65
+ }
66
+ installLocalSkills();
67
+ (0, output_js_1.printSuccess)('Conductor skills installed → .claude/skills/conductor/', opts);
68
+ return 0;
69
+ }
70
+ catch (err) {
71
+ const message = err instanceof Error ? err.message : String(err);
72
+ (0, output_js_1.printError)(`Install failed: ${message}`, opts);
73
+ return 1;
74
+ }
75
+ }
76
+ async function installWebCli(opts, check, browserArg) {
77
+ try {
78
+ if (check) {
79
+ return checkWebInstallStatus(opts);
63
80
  }
81
+ return installWebBrowser(browserArg, opts);
82
+ }
83
+ catch (err) {
84
+ const message = err instanceof Error ? err.message : String(err);
85
+ (0, output_js_1.printError)(`Install failed: ${message}`, opts);
86
+ return 1;
87
+ }
88
+ }
89
+ function checkSkillsInstallStatus(opts) {
90
+ const localSkillsPath = path_1.default.join(process.cwd(), '.claude', 'skills', 'conductor', 'SKILL.md');
91
+ const hasLocalSkills = fs_1.default.existsSync(localSkillsPath);
92
+ if (opts.json) {
93
+ (0, output_js_1.printData)({ localSkills: { installed: hasLocalSkills } }, opts);
94
+ }
95
+ else {
64
96
  if (hasLocalSkills) {
65
97
  console.log('Local skills: installed → .claude/skills/conductor/');
66
98
  }
67
99
  else {
68
100
  console.log('Local skills: not installed');
101
+ console.log('Run `conductor install-skills` to copy skills into this project.');
69
102
  }
70
- if (!pluginVersion && !hasLocalSkills) {
71
- console.log('\nRun `conductor install` to install the global plugin.');
72
- console.log('Run `conductor install --skills` to copy skills into this project.');
103
+ }
104
+ return 0;
105
+ }
106
+ function checkWebInstallStatus(opts) {
107
+ const webBrowsers = {
108
+ chromium: (0, bootstrap_js_1.isPlaywrightBrowserInstalled)('chromium'),
109
+ firefox: (0, bootstrap_js_1.isPlaywrightBrowserInstalled)('firefox'),
110
+ webkit: (0, bootstrap_js_1.isPlaywrightBrowserInstalled)('webkit'),
111
+ };
112
+ if (opts.json) {
113
+ (0, output_js_1.printData)({ webBrowsers }, opts);
114
+ }
115
+ else {
116
+ const installedBrowsers = Object.entries(webBrowsers)
117
+ .filter(([, v]) => v)
118
+ .map(([k]) => k);
119
+ if (installedBrowsers.length > 0) {
120
+ console.log(`Web browsers: ${installedBrowsers.join(', ')}`);
121
+ }
122
+ else {
123
+ console.log('Web browsers: none installed');
124
+ console.log('Run `conductor install-web` to install a Playwright browser (default: chromium).');
73
125
  }
74
126
  }
75
127
  return 0;
@@ -114,6 +166,30 @@ function installPlugin() {
114
166
  fs_1.default.writeFileSync(installedPluginsPath, JSON.stringify(installed, null, 2));
115
167
  return version;
116
168
  }
169
+ async function installWebBrowser(browserArg, opts) {
170
+ const validBrowsers = ['chromium', 'firefox', 'webkit'];
171
+ let browserName = 'chromium';
172
+ if (browserArg !== undefined && browserArg !== '') {
173
+ if (!validBrowsers.includes(browserArg)) {
174
+ (0, output_js_1.printError)(`Unknown browser "${browserArg}". Supported: ${validBrowsers.join(', ')}`, opts);
175
+ return 1;
176
+ }
177
+ browserName = browserArg;
178
+ }
179
+ try {
180
+ await (0, bootstrap_js_1.ensurePlaywrightBrowser)(browserName, (msg) => {
181
+ if (!opts.json)
182
+ console.log(msg);
183
+ });
184
+ (0, output_js_1.printSuccess)(`Playwright ${browserName} browser installed`, opts);
185
+ return 0;
186
+ }
187
+ catch (err) {
188
+ const msg = err instanceof Error ? err.message : String(err);
189
+ (0, output_js_1.printError)(msg, opts);
190
+ return 1;
191
+ }
192
+ }
117
193
  function copyDir(src, dest) {
118
194
  fs_1.default.mkdirSync(dest, { recursive: true });
119
195
  for (const entry of fs_1.default.readdirSync(src, { withFileTypes: true })) {
@@ -12,6 +12,7 @@ const session_js_1 = require("../session.js");
12
12
  const output_js_1 = require("../output.js");
13
13
  const ios_js_1 = require("../drivers/ios.js");
14
14
  const android_js_1 = require("../drivers/android.js");
15
+ const web_js_1 = require("../drivers/web.js");
15
16
  async function launchApp(appId, deviceId, opts = {}, sessionName = 'default', flags = {}) {
16
17
  if (!appId) {
17
18
  (0, output_js_1.printError)('launch-app requires <appId>', opts);
@@ -27,12 +28,17 @@ async function launchApp(appId, deviceId, opts = {}, sessionName = 'default', fl
27
28
  if (shouldStop) {
28
29
  if (driver instanceof ios_js_1.IOSDriver)
29
30
  await driver.terminateApp(appId);
31
+ else if (driver instanceof web_js_1.WebDriver)
32
+ await driver.terminateApp();
30
33
  else if (driver instanceof android_js_1.AndroidDriver)
31
34
  await driver.stopApp(appId);
32
35
  }
33
36
  if (driver instanceof ios_js_1.IOSDriver) {
34
37
  await driver.launchApp(appId, flags.launchArgs);
35
38
  }
39
+ else if (driver instanceof web_js_1.WebDriver) {
40
+ await driver.launchApp(appId);
41
+ }
36
42
  else if (driver instanceof android_js_1.AndroidDriver) {
37
43
  await driver.launchApp(appId, flags.launchArgs);
38
44
  }
@@ -7,6 +7,8 @@ exports.listDevices = listDevices;
7
7
  exports.HELP = ` list-devices List booted and available devices/simulators`;
8
8
  const runner_js_1 = require("../runner.js");
9
9
  const output_js_1 = require("../output.js");
10
+ const bootstrap_js_1 = require("../drivers/bootstrap.js");
11
+ const client_js_1 = require("../daemon/client.js");
10
12
  async function discoverBootedDevices() {
11
13
  const devices = [];
12
14
  // Try adb devices (Android)
@@ -49,6 +51,25 @@ async function discoverBootedDevices() {
49
51
  // ignore parse errors
50
52
  }
51
53
  }
54
+ // Check for running web browser sessions (daemon sessions starting with "web")
55
+ const sessions = (0, client_js_1.listDaemonSessions)();
56
+ for (const session of sessions) {
57
+ if (session === 'web' || session.startsWith('web:')) {
58
+ const status = await (0, client_js_1.daemonStatus)(session);
59
+ if (status.running) {
60
+ const browser = (0, bootstrap_js_1.webBrowserName)(session);
61
+ const parts = session.split(':');
62
+ const label = browser.charAt(0).toUpperCase() + browser.slice(1);
63
+ const name = parts.length > 2 ? `${label} (${parts[2]})` : label;
64
+ devices.push({
65
+ id: session,
66
+ name,
67
+ platform: 'web',
68
+ status: 'running',
69
+ });
70
+ }
71
+ }
72
+ }
52
73
  return devices;
53
74
  }
54
75
  async function discoverAvailableDevices() {
@@ -92,12 +113,20 @@ async function listDevices(opts) {
92
113
  discoverBootedDevices(),
93
114
  discoverAvailableDevices(),
94
115
  ]);
95
- if (devices.length === 0 && availableDevices.length === 0) {
116
+ // Detect installed Playwright browsers for web support
117
+ const webBrowsers = ['chromium', 'firefox', 'webkit'].filter((b) => (0, bootstrap_js_1.isPlaywrightBrowserInstalled)(b));
118
+ if (devices.length === 0 && availableDevices.length === 0 && webBrowsers.length === 0) {
96
119
  (0, output_js_1.printError)('No devices found. Start an emulator or simulator first.', opts);
97
120
  return 1;
98
121
  }
99
122
  if (opts.json) {
100
- (0, output_js_1.printData)({ status: 'ok', devices, availableDevices }, opts);
123
+ const webDevices = webBrowsers.map((b) => ({
124
+ id: b === 'chromium' ? 'web' : `web:${b}`,
125
+ name: b.charAt(0).toUpperCase() + b.slice(1),
126
+ platform: 'web',
127
+ status: 'available',
128
+ }));
129
+ (0, output_js_1.printData)({ status: 'ok', devices, availableDevices: [...availableDevices, ...webDevices] }, opts);
101
130
  }
102
131
  else {
103
132
  if (devices.length > 0) {
@@ -119,6 +148,14 @@ async function listDevices(opts) {
119
148
  else {
120
149
  console.log('No available devices.');
121
150
  }
151
+ if (webBrowsers.length > 0) {
152
+ console.log('');
153
+ console.log('Web browsers:');
154
+ for (const b of webBrowsers) {
155
+ const deviceId = b === 'chromium' ? 'web' : `web:${b}`;
156
+ console.log(` web available ${deviceId.padEnd(16)} ${b.charAt(0).toUpperCase() + b.slice(1)}`);
157
+ }
158
+ }
122
159
  }
123
160
  return 0;
124
161
  }
@@ -0,0 +1,193 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP = void 0;
4
+ exports.logs = logs;
5
+ exports.HELP = ` logs [--source <source>] [--level <level>] Stream app logs (console, Metro, or device)
6
+ --source <source> Log source: metro, device, or auto (default: auto)
7
+ --level <level> Minimum level: verbose, debug, log, info, warn, error
8
+ --metro Enable Metro logs for React Native apps (auto-discovers port)
9
+ --metro-port <port> Override Metro dev server port (skips auto-discovery)
10
+ --target <n> Metro debugger target index (when multiple devices share one Metro)
11
+ --list List available Metro debugger targets and exit
12
+ --recent <n> Return the last N buffered log entries and exit (agent-friendly)
13
+ --duration <seconds> Stream logs for N seconds, then exit
14
+ --json Output as NDJSON (one JSON object per line)`;
15
+ const runner_js_1 = require("../runner.js");
16
+ const ios_js_1 = require("../drivers/ios.js");
17
+ const android_js_1 = require("../drivers/android.js");
18
+ const web_js_1 = require("../drivers/web.js");
19
+ const types_js_1 = require("../drivers/log-sources/types.js");
20
+ const metro_js_1 = require("../drivers/log-sources/metro.js");
21
+ const daemon_js_1 = require("../drivers/log-sources/daemon.js");
22
+ const client_js_1 = require("../daemon/client.js");
23
+ const bootstrap_js_1 = require("../drivers/bootstrap.js");
24
+ function formatTimestamp(iso) {
25
+ try {
26
+ const d = new Date(iso);
27
+ if (isNaN(d.getTime()))
28
+ return iso;
29
+ return d.toLocaleTimeString('en-GB', { hour12: false });
30
+ }
31
+ catch {
32
+ return iso;
33
+ }
34
+ }
35
+ function formatEntry(entry, opts) {
36
+ if (opts.json) {
37
+ return JSON.stringify(entry);
38
+ }
39
+ const time = formatTimestamp(entry.timestamp);
40
+ const lvl = entry.level.padEnd(7);
41
+ let line = `[${lvl}] ${time} ${entry.message}`;
42
+ if (entry.stackTrace) {
43
+ line += '\n' + entry.stackTrace;
44
+ }
45
+ return line;
46
+ }
47
+ async function logs(opts = {}, sessionName = 'default', { source = 'auto', level, metro, metroPort, target, list, recent, duration } = {}) {
48
+ // --list: query Metro targets and print them without starting a log stream
49
+ if (list) {
50
+ try {
51
+ const targets = await (0, metro_js_1.fetchTargets)(metroPort ?? 8081, 'localhost');
52
+ const withWs = targets.filter((t) => t.webSocketDebuggerUrl);
53
+ if (withWs.length === 0) {
54
+ if (opts.json) {
55
+ console.log(JSON.stringify({ status: 'ok', targets: [] }));
56
+ }
57
+ else {
58
+ console.log('No Metro debugger targets found. Is the app running?');
59
+ }
60
+ return 0;
61
+ }
62
+ if (opts.json) {
63
+ const items = withWs.map((t, i) => ({
64
+ index: i,
65
+ title: t.title ?? null,
66
+ description: t.description ?? null,
67
+ deviceName: t.deviceName ?? null,
68
+ deviceId: t.deviceId ?? null,
69
+ appId: t.appId ?? null,
70
+ logicalDeviceId: t.reactNative?.logicalDeviceId ?? null,
71
+ }));
72
+ console.log(JSON.stringify({ status: 'ok', targets: items }));
73
+ }
74
+ else {
75
+ console.log('Metro debugger targets:');
76
+ for (let i = 0; i < withWs.length; i++) {
77
+ const t = withWs[i];
78
+ const label = t.title ?? t.deviceName ?? '(unnamed)';
79
+ const desc = t.description ? ` — ${t.description}` : '';
80
+ console.log(` ${i}: ${label}${desc}`);
81
+ }
82
+ }
83
+ return 0;
84
+ }
85
+ catch (err) {
86
+ const msg = err instanceof Error ? err.message : String(err);
87
+ if (opts.json) {
88
+ console.log(JSON.stringify({ status: 'error', message: msg }));
89
+ }
90
+ else {
91
+ console.error(`\u2717 logs --list \u2014 ${msg}`);
92
+ }
93
+ return 1;
94
+ }
95
+ }
96
+ try {
97
+ // ── Snapshot mode (--recent N) ──────────────────────────────────────────
98
+ // Single fetch from the daemon's log buffer, print, and exit immediately.
99
+ // This is the primary agent-friendly mode.
100
+ if (recent !== undefined) {
101
+ // Ensure daemon is running (starts it if needed)
102
+ await (0, runner_js_1.getDriver)(sessionName);
103
+ const minSeverity = level ? (types_js_1.LEVEL_SEVERITY[level] ?? 0) : 0;
104
+ // --metro with explicit port → use that port; --metro without port → auto-discover
105
+ const metroOpt = metro ? (metroPort ?? 'auto') : undefined;
106
+ const entries = await (0, client_js_1.fetchDaemonLogs)(sessionName, {
107
+ limit: recent,
108
+ level,
109
+ metro: metroOpt,
110
+ });
111
+ for (const entry of entries) {
112
+ const entrySeverity = types_js_1.LEVEL_SEVERITY[entry.level] ?? 0;
113
+ if (entrySeverity < minSeverity)
114
+ continue;
115
+ console.log(formatEntry(entry, opts));
116
+ }
117
+ return 0;
118
+ }
119
+ // ── Determine platform for streaming modes ─────────────────────────────
120
+ // When source is explicitly 'metro', skip device resolution entirely —
121
+ // Metro runs on the host, so we don't need a running driver or session.
122
+ let _platform = 'unknown';
123
+ if (source !== 'metro') {
124
+ const driver = await (0, runner_js_1.getDriver)(sessionName);
125
+ if (driver instanceof ios_js_1.IOSDriver) {
126
+ _platform = driver.platform;
127
+ }
128
+ else if (driver instanceof android_js_1.AndroidDriver) {
129
+ _platform = 'android';
130
+ }
131
+ else if (driver instanceof web_js_1.WebDriver) {
132
+ _platform = 'web';
133
+ }
134
+ else {
135
+ _platform = await (0, bootstrap_js_1.detectPlatform)(sessionName);
136
+ }
137
+ }
138
+ const minSeverity = level ? (types_js_1.LEVEL_SEVERITY[level] ?? 0) : 0;
139
+ // ── Create log source ──────────────────────────────────────────────────
140
+ let logSource;
141
+ if (source === 'metro') {
142
+ // Explicit --source metro: connect directly to Metro via CLI
143
+ logSource = new metro_js_1.MetroLogSource(metroPort ?? 8081, 'localhost', target);
144
+ await logSource.connect();
145
+ }
146
+ else {
147
+ // Device logs via daemon. When --metro is set, pass the metro port
148
+ // (or 'auto' for auto-discovery) so the daemon finds Metro for this device.
149
+ const metroOpt = metro ? (metroPort ?? 'auto') : undefined;
150
+ logSource = new daemon_js_1.DaemonLogSource(sessionName, metroOpt);
151
+ await logSource.connect();
152
+ }
153
+ // Set up graceful shutdown
154
+ const cleanup = () => {
155
+ logSource.disconnect();
156
+ process.exit(0);
157
+ };
158
+ process.on('SIGINT', cleanup);
159
+ process.on('SIGTERM', cleanup);
160
+ logSource.onEntry((entry) => {
161
+ const entrySeverity = types_js_1.LEVEL_SEVERITY[entry.level] ?? 0;
162
+ if (entrySeverity < minSeverity)
163
+ return;
164
+ console.log(formatEntry(entry, opts));
165
+ });
166
+ // ── Duration mode (--duration N) ───────────────────────────────────────
167
+ if (duration !== undefined) {
168
+ await new Promise((resolve) => {
169
+ setTimeout(() => {
170
+ logSource.disconnect();
171
+ resolve();
172
+ }, duration * 1000);
173
+ });
174
+ return 0;
175
+ }
176
+ // ── Streaming mode (default) ───────────────────────────────────────────
177
+ // Keep the process alive — the log source streams entries via callbacks
178
+ await new Promise(() => {
179
+ // Never resolves — exits via SIGINT/SIGTERM
180
+ });
181
+ return 0;
182
+ }
183
+ catch (err) {
184
+ const msg = err instanceof Error ? err.message : String(err);
185
+ if (opts.json) {
186
+ console.log(JSON.stringify({ status: 'error', message: msg }));
187
+ }
188
+ else {
189
+ console.error(`\u2717 logs \u2014 ${msg}`);
190
+ }
191
+ return 1;
192
+ }
193
+ }
@@ -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
  const VALID_KEYS = [
11
12
  'Enter',
12
13
  'Backspace',
@@ -139,6 +140,21 @@ async function pressKey(key, opts = {}, sessionName = 'default') {
139
140
  // Keys not mapped on iOS (e.g. Back, VolumeUp) are silently ignored
140
141
  }
141
142
  }
143
+ else if (driver instanceof web_js_1.WebDriver) {
144
+ const WEB_KEY_MAP = {
145
+ Enter: 'Enter',
146
+ Tab: 'Tab',
147
+ Backspace: 'Backspace',
148
+ Delete: 'Delete',
149
+ Escape: 'Escape',
150
+ Home: 'Home',
151
+ End: 'End',
152
+ };
153
+ const webKey = WEB_KEY_MAP[matched];
154
+ if (webKey) {
155
+ await driver.pressKey(webKey);
156
+ }
157
+ }
142
158
  else if (driver instanceof android_js_1.AndroidDriver) {
143
159
  const code = ANDROID_KEYCODE[matched];
144
160
  if (code !== undefined) {
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.HELP = void 0;
7
7
  exports.screenshot = screenshot;
8
- exports.HELP = ` screenshot [--output <path>] Take screenshot`;
8
+ exports.HELP = ` take-screenshot [--output <path>] Take screenshot`;
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const promises_1 = __importDefault(require("fs/promises"));
11
11
  const runner_js_1 = require("../runner.js");
@@ -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 element_resolver_js_1 = require("../drivers/element-resolver.js");
15
16
  const utils_js_1 = require("../utils.js");
16
17
  async function scrollUntilVisible(element, opts = {}, sessionName = 'default', flags = {}) {
@@ -49,6 +50,16 @@ async function scrollUntilVisible(element, opts = {}, sessionName = 'default', f
49
50
  const { widthPoints: w, heightPoints: h } = info;
50
51
  await driver.swipe(coords.startX * w, coords.startY * h, coords.endX * w, coords.endY * h, 0.5);
51
52
  }
53
+ else if (driver instanceof web_js_1.WebDriver) {
54
+ const hierarchy = await driver.viewHierarchy();
55
+ if ((0, element_resolver_js_1.findWebElement)(hierarchy, sel)) {
56
+ (0, output_js_1.printSuccess)(`scroll-until-visible ${label} — found`, opts);
57
+ return 0;
58
+ }
59
+ const info = await driver.deviceInfo();
60
+ const { widthPixels: w, heightPixels: h } = info;
61
+ await driver.swipe(coords.startX * w, coords.startY * h, coords.endX * w, coords.endY * h, 500);
62
+ }
52
63
  else if (driver instanceof android_js_1.AndroidDriver) {
53
64
  const xml = await driver.viewHierarchy();
54
65
  if ((0, element_resolver_js_1.findAndroidElement)(xml, sel)) {
@@ -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
  const utils_js_1 = require("../utils.js");
11
12
  async function scroll(direction = 'down', opts = {}, sessionName = 'default') {
12
13
  const valid = ['down', 'up', 'left', 'right'];
@@ -25,6 +26,11 @@ async function scroll(direction = 'down', opts = {}, sessionName = 'default') {
25
26
  const { widthPoints: w, heightPoints: h } = info;
26
27
  await driver.swipe(coords.startX * w, coords.startY * h, coords.endX * w, coords.endY * h, 0.5);
27
28
  }
29
+ else if (driver instanceof web_js_1.WebDriver) {
30
+ const info = await driver.deviceInfo();
31
+ const { widthPixels: w, heightPixels: h } = info;
32
+ await driver.swipe(coords.startX * w, coords.startY * h, coords.endX * w, coords.endY * h, 500);
33
+ }
28
34
  else if (driver instanceof android_js_1.AndroidDriver) {
29
35
  const info = await driver.deviceInfo();
30
36
  const { widthPixels: w, heightPixels: h } = info;