@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.
- package/README.md +12 -9
- package/dist/commands/assert-not-visible.js +4 -0
- package/dist/commands/assert-visible.js +4 -0
- package/dist/commands/back.js +4 -0
- package/dist/commands/cheat-sheet.js +11 -8
- package/dist/commands/delete-device.js +222 -0
- package/dist/commands/device-pool.js +33 -22
- package/dist/commands/download-app.js +87 -0
- package/dist/commands/erase-text.js +4 -0
- package/dist/commands/focused.js +39 -0
- package/dist/commands/foreground-app.js +4 -0
- package/dist/commands/hide-keyboard.js +4 -0
- package/dist/commands/inspect.js +8 -0
- package/dist/commands/install.js +103 -27
- package/dist/commands/launch-app.js +6 -0
- package/dist/commands/list-devices.js +39 -2
- package/dist/commands/logs.js +193 -0
- package/dist/commands/press-key.js +16 -0
- package/dist/commands/screenshot.js +1 -1
- package/dist/commands/scroll-until-visible.js +11 -0
- package/dist/commands/scroll.js +6 -0
- package/dist/commands/start-device.js +38 -4
- package/dist/commands/stop-app.js +4 -0
- package/dist/commands/swipe.js +22 -0
- package/dist/commands/tap.js +10 -3
- package/dist/commands/type.js +2 -2
- package/dist/commands/uninstall-app.js +4 -0
- package/dist/daemon/client.js +72 -9
- package/dist/daemon/log-collector.js +408 -0
- package/dist/daemon/server.js +110 -32
- package/dist/daemon/web-server.js +812 -0
- package/dist/device-picker.js +7 -2
- package/dist/drivers/bootstrap.js +124 -1
- package/dist/drivers/element-resolver.js +241 -30
- package/dist/drivers/flow-runner.js +63 -21
- package/dist/drivers/log-sources/android.js +156 -0
- package/dist/drivers/log-sources/daemon.js +112 -0
- package/dist/drivers/log-sources/ios.js +106 -0
- package/dist/drivers/log-sources/metro.js +252 -0
- package/dist/drivers/log-sources/types.js +13 -0
- package/dist/drivers/log-sources/web.js +96 -0
- package/dist/drivers/wait.js +57 -0
- package/dist/drivers/web.js +173 -0
- package/dist/index.js +62 -12
- package/dist/runner.js +32 -2
- package/drivers/ios/conductor-driver-ios.zip +0 -0
- package/drivers/ios/conductor-driver-iosUITests-Runner.zip +0 -0
- package/drivers/tvos/conductor-driver-tvos.zip +0 -0
- package/drivers/tvos/conductor-driver-tvosUITests-Runner.zip +0 -0
- package/package.json +5 -2
- package/skills/conductor/SKILL.md +72 -41
- 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
|
}
|
package/dist/commands/inspect.js
CHANGED
|
@@ -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);
|
package/dist/commands/install.js
CHANGED
|
@@ -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.
|
|
7
|
-
exports.
|
|
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.
|
|
11
|
-
install --
|
|
12
|
-
install --check
|
|
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
|
-
|
|
20
|
+
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
21
|
+
async function installPluginCli(opts, check) {
|
|
19
22
|
try {
|
|
20
23
|
if (check) {
|
|
21
|
-
return
|
|
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
|
|
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
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>]
|
|
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)) {
|
package/dist/commands/scroll.js
CHANGED
|
@@ -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;
|