@houwert/conductor 0.4.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 +13 -10
- 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 +12 -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-app.js +51 -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 +69 -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 +83 -41
- package/skills/skills.yaml +1 -1
package/dist/index.js
CHANGED
|
@@ -32,6 +32,8 @@ const run_parallel_js_1 = require("./commands/run-parallel.js");
|
|
|
32
32
|
const foreground_app_js_1 = require("./commands/foreground-app.js");
|
|
33
33
|
const list_apps_js_1 = require("./commands/list-apps.js");
|
|
34
34
|
const copy_app_js_1 = require("./commands/copy-app.js");
|
|
35
|
+
const download_app_js_1 = require("./commands/download-app.js");
|
|
36
|
+
const install_app_js_1 = require("./commands/install-app.js");
|
|
35
37
|
const erase_text_js_1 = require("./commands/erase-text.js");
|
|
36
38
|
const assert_not_visible_js_1 = require("./commands/assert-not-visible.js");
|
|
37
39
|
const open_link_js_1 = require("./commands/open-link.js");
|
|
@@ -40,20 +42,25 @@ const scroll_until_visible_js_1 = require("./commands/scroll-until-visible.js");
|
|
|
40
42
|
const set_location_js_1 = require("./commands/set-location.js");
|
|
41
43
|
const set_orientation_js_1 = require("./commands/set-orientation.js");
|
|
42
44
|
const start_device_js_1 = require("./commands/start-device.js");
|
|
45
|
+
const delete_device_js_1 = require("./commands/delete-device.js");
|
|
46
|
+
const logs_js_1 = require("./commands/logs.js");
|
|
43
47
|
const device_picker_js_1 = require("./device-picker.js");
|
|
44
48
|
const update_check_js_1 = require("./update-check.js");
|
|
45
49
|
const COMMAND_HELP = {
|
|
46
50
|
'start-device': start_device_js_1.HELP,
|
|
51
|
+
'delete-device': delete_device_js_1.HELP,
|
|
47
52
|
'list-devices': list_devices_js_1.HELP,
|
|
48
53
|
'foreground-app': foreground_app_js_1.HELP,
|
|
49
54
|
'list-apps': list_apps_js_1.HELP,
|
|
50
55
|
'copy-app': copy_app_js_1.HELP,
|
|
56
|
+
'download-app': download_app_js_1.HELP,
|
|
57
|
+
'install-app': install_app_js_1.HELP,
|
|
51
58
|
'launch-app': launch_app_js_1.HELP,
|
|
52
59
|
'stop-app': stop_app_js_1.HELP,
|
|
53
60
|
'clear-state': clear_state_js_1.HELP,
|
|
54
61
|
'uninstall-app': uninstall_app_js_1.HELP,
|
|
55
|
-
tap: tap_js_1.HELP,
|
|
56
|
-
|
|
62
|
+
'tap-on': tap_js_1.HELP,
|
|
63
|
+
'input-text': type_js_1.HELP,
|
|
57
64
|
'erase-text': erase_text_js_1.HELP,
|
|
58
65
|
back: back_js_1.HELP,
|
|
59
66
|
'press-key': press_key_js_1.HELP,
|
|
@@ -66,23 +73,27 @@ const COMMAND_HELP = {
|
|
|
66
73
|
'open-link': open_link_js_1.HELP,
|
|
67
74
|
'set-location': set_location_js_1.HELP,
|
|
68
75
|
'set-orientation': set_orientation_js_1.HELP,
|
|
69
|
-
screenshot: screenshot_js_1.HELP,
|
|
76
|
+
'take-screenshot': screenshot_js_1.HELP,
|
|
70
77
|
inspect: inspect_js_1.HELP,
|
|
71
78
|
focused: focused_js_1.HELP,
|
|
72
79
|
'run-flow': run_flow_js_1.HELP,
|
|
73
80
|
'run-flow-inline': run_flow_inline_js_1.HELP,
|
|
74
81
|
session: session_js_1.HELP,
|
|
75
82
|
'cheat-sheet': cheat_sheet_js_1.HELP,
|
|
76
|
-
install: install_js_1.
|
|
83
|
+
'install-plugin': install_js_1.HELP_INSTALL_PLUGIN,
|
|
84
|
+
'install-skills': install_js_1.HELP_INSTALL_SKILLS,
|
|
85
|
+
'install-web': install_js_1.HELP_INSTALL_WEB,
|
|
77
86
|
'daemon-start': daemon_js_1.HELP_DAEMON_START,
|
|
78
87
|
'daemon-stop': daemon_js_1.HELP_DAEMON_STOP,
|
|
79
88
|
'daemon-status': daemon_js_1.HELP_DAEMON_STATUS,
|
|
80
89
|
'device-pool': device_pool_js_1.HELP,
|
|
81
90
|
'run-parallel': run_parallel_js_1.HELP,
|
|
91
|
+
logs: logs_js_1.HELP,
|
|
82
92
|
};
|
|
83
93
|
const OPTIONS_HELP = `Options:
|
|
84
94
|
--device <id> Target device ID (also keys the session and daemon)
|
|
85
95
|
--device-name <n> Target a booted device by name (resolved to ID from booted devices)
|
|
96
|
+
--platform <p> Filter to devices of this platform (ios, android, tvos, web)
|
|
86
97
|
--json Output as machine-readable JSON
|
|
87
98
|
--verbose, -v Log daemon calls, fallbacks, and raw output
|
|
88
99
|
--help, -h Show this help`;
|
|
@@ -101,7 +112,6 @@ async function main() {
|
|
|
101
112
|
'clear',
|
|
102
113
|
'list',
|
|
103
114
|
'verbose',
|
|
104
|
-
'skills',
|
|
105
115
|
'check',
|
|
106
116
|
'all',
|
|
107
117
|
'acquire',
|
|
@@ -138,8 +148,12 @@ async function main() {
|
|
|
138
148
|
'name',
|
|
139
149
|
'device-name',
|
|
140
150
|
'device-type',
|
|
151
|
+
'browser',
|
|
141
152
|
'from',
|
|
142
153
|
'to',
|
|
154
|
+
'source',
|
|
155
|
+
'metro-port',
|
|
156
|
+
'level',
|
|
143
157
|
],
|
|
144
158
|
alias: { h: 'help', v: 'verbose' },
|
|
145
159
|
});
|
|
@@ -162,11 +176,18 @@ async function main() {
|
|
|
162
176
|
const NO_DEVICE_COMMANDS = new Set([
|
|
163
177
|
'list-devices',
|
|
164
178
|
'start-device',
|
|
179
|
+
'delete-device',
|
|
165
180
|
'cheat-sheet',
|
|
166
|
-
'install',
|
|
181
|
+
'install-plugin',
|
|
182
|
+
'install-skills',
|
|
183
|
+
'install-web',
|
|
167
184
|
'copy-app',
|
|
168
185
|
'device-pool',
|
|
169
186
|
'run-parallel',
|
|
187
|
+
// `logs --list` and `logs --source metro` only query Metro on localhost — no device needed
|
|
188
|
+
...(command === 'logs' && (argv['list'] || argv['source'] === 'metro') ? ['logs'] : []),
|
|
189
|
+
// `daemon-stop --all` stops every daemon — no device needed
|
|
190
|
+
...(command === 'daemon-stop' && argv['all'] ? ['daemon-stop'] : []),
|
|
170
191
|
]);
|
|
171
192
|
if (!NO_DEVICE_COMMANDS.has(command) && !COMMAND_HELP[command]) {
|
|
172
193
|
console.error(`Unknown command: ${command}`);
|
|
@@ -194,7 +215,8 @@ async function main() {
|
|
|
194
215
|
sessionName = match.id;
|
|
195
216
|
}
|
|
196
217
|
else {
|
|
197
|
-
sessionName =
|
|
218
|
+
sessionName =
|
|
219
|
+
explicitDevice ?? (await (0, device_picker_js_1.pickDevice)(argv['platform'])) ?? 'default';
|
|
198
220
|
}
|
|
199
221
|
}
|
|
200
222
|
let exitCode = 0;
|
|
@@ -205,11 +227,18 @@ async function main() {
|
|
|
205
227
|
avd: argv['avd'],
|
|
206
228
|
name: argv['name'],
|
|
207
229
|
deviceType: argv['device-type'],
|
|
230
|
+
browser: argv['browser'],
|
|
208
231
|
});
|
|
209
232
|
break;
|
|
210
233
|
case 'list-devices':
|
|
211
234
|
exitCode = await (0, list_devices_js_1.listDevices)(opts);
|
|
212
235
|
break;
|
|
236
|
+
case 'delete-device':
|
|
237
|
+
exitCode = await (0, delete_device_js_1.deleteDevice)(rest[0], opts, {
|
|
238
|
+
platform: argv['platform'],
|
|
239
|
+
all: argv['all'],
|
|
240
|
+
});
|
|
241
|
+
break;
|
|
213
242
|
case 'foreground-app':
|
|
214
243
|
exitCode = await (0, foreground_app_js_1.foregroundApp)(opts, sessionName);
|
|
215
244
|
break;
|
|
@@ -223,6 +252,16 @@ async function main() {
|
|
|
223
252
|
exitCode = await (0, copy_app_js_1.copyApp)(bundleId, from ?? '', to ?? '', opts);
|
|
224
253
|
break;
|
|
225
254
|
}
|
|
255
|
+
case 'download-app': {
|
|
256
|
+
const appId = rest[0] ?? '';
|
|
257
|
+
exitCode = await (0, download_app_js_1.downloadApp)(appId, argv['output'], opts, sessionName);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case 'install-app': {
|
|
261
|
+
const appPath = rest[0] ?? '';
|
|
262
|
+
exitCode = await (0, install_app_js_1.installApp)(appPath, opts, sessionName);
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
226
265
|
case 'launch-app': {
|
|
227
266
|
const appId = rest[0] ?? '';
|
|
228
267
|
// --argument key=value (repeatable) → Record<string, string>
|
|
@@ -254,7 +293,7 @@ async function main() {
|
|
|
254
293
|
exitCode = await (0, uninstall_app_js_1.uninstallApp)(appId, opts, sessionName);
|
|
255
294
|
break;
|
|
256
295
|
}
|
|
257
|
-
case 'tap': {
|
|
296
|
+
case 'tap-on': {
|
|
258
297
|
const element = rest.join(' ');
|
|
259
298
|
exitCode = await (0, tap_js_1.tap)(element, opts, sessionName, {
|
|
260
299
|
id: argv['id'],
|
|
@@ -274,7 +313,7 @@ async function main() {
|
|
|
274
313
|
});
|
|
275
314
|
break;
|
|
276
315
|
}
|
|
277
|
-
case '
|
|
316
|
+
case 'input-text': {
|
|
278
317
|
const text = rest.join(' ');
|
|
279
318
|
exitCode = await (0, type_js_1.typeText)(text, opts, sessionName);
|
|
280
319
|
break;
|
|
@@ -390,7 +429,7 @@ async function main() {
|
|
|
390
429
|
exitCode = await (0, set_orientation_js_1.setOrientation)(orientation, opts, sessionName);
|
|
391
430
|
break;
|
|
392
431
|
}
|
|
393
|
-
case 'screenshot': {
|
|
432
|
+
case 'take-screenshot': {
|
|
394
433
|
const outPath = argv['output'];
|
|
395
434
|
exitCode = await (0, screenshot_js_1.screenshot)(outPath, opts, sessionName);
|
|
396
435
|
break;
|
|
@@ -404,6 +443,18 @@ async function main() {
|
|
|
404
443
|
interval: argv['interval'] !== undefined ? Number(argv['interval']) : undefined,
|
|
405
444
|
});
|
|
406
445
|
break;
|
|
446
|
+
case 'logs':
|
|
447
|
+
exitCode = await (0, logs_js_1.logs)(opts, sessionName, {
|
|
448
|
+
source: argv['source'],
|
|
449
|
+
level: argv['level'],
|
|
450
|
+
metro: argv['metro'] || argv['metro-port'] !== undefined,
|
|
451
|
+
metroPort: argv['metro-port'] !== undefined ? Number(argv['metro-port']) : undefined,
|
|
452
|
+
target: argv['target'] !== undefined ? Number(argv['target']) : undefined,
|
|
453
|
+
list: argv['list'],
|
|
454
|
+
recent: argv['recent'] !== undefined ? Number(argv['recent']) : undefined,
|
|
455
|
+
duration: argv['duration'] !== undefined ? Number(argv['duration']) : undefined,
|
|
456
|
+
});
|
|
457
|
+
break;
|
|
407
458
|
case 'run-flow': {
|
|
408
459
|
const file = rest[0] ?? '';
|
|
409
460
|
const rawEnv = argv['env'];
|
|
@@ -423,8 +474,14 @@ async function main() {
|
|
|
423
474
|
case 'cheat-sheet':
|
|
424
475
|
exitCode = await (0, cheat_sheet_js_1.cheatSheet)();
|
|
425
476
|
break;
|
|
426
|
-
case 'install':
|
|
427
|
-
exitCode = await (0, install_js_1.
|
|
477
|
+
case 'install-plugin':
|
|
478
|
+
exitCode = await (0, install_js_1.installPluginCli)(opts, argv['check']);
|
|
479
|
+
break;
|
|
480
|
+
case 'install-skills':
|
|
481
|
+
exitCode = await (0, install_js_1.installSkillsCli)(opts, argv['check']);
|
|
482
|
+
break;
|
|
483
|
+
case 'install-web':
|
|
484
|
+
exitCode = await (0, install_js_1.installWebCli)(opts, argv['check'], rest[0]);
|
|
428
485
|
break;
|
|
429
486
|
case 'daemon-start':
|
|
430
487
|
exitCode = await (0, daemon_js_1.daemonStart)(opts, sessionName);
|
package/dist/runner.js
CHANGED
|
@@ -11,6 +11,7 @@ const flow_runner_js_1 = require("./drivers/flow-runner.js");
|
|
|
11
11
|
const verbose_js_1 = require("./verbose.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 bootstrap_js_1 = require("./drivers/bootstrap.js");
|
|
15
16
|
const client_js_1 = require("./daemon/client.js");
|
|
16
17
|
/**
|
|
@@ -92,8 +93,10 @@ async function getDriver(sessionName = 'default') {
|
|
|
92
93
|
throw new Error('No device found. Connect a device or start a simulator, then run again.');
|
|
93
94
|
}
|
|
94
95
|
const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId);
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
// Web daemons resolve their own port after generating a unique session ID,
|
|
97
|
+
// so we defer getDriverPort for them to avoid allocating an unused port.
|
|
98
|
+
const port = platform !== 'web' ? await (0, bootstrap_js_1.getDriverPort)(platform, deviceId) : 0;
|
|
99
|
+
(0, verbose_js_1.log)(`getDriver: platform=${platform} deviceId=${deviceId} port=${port || '(deferred)'}`);
|
|
97
100
|
let driver;
|
|
98
101
|
if (platform === 'ios') {
|
|
99
102
|
if (!(await (0, bootstrap_js_1.isPortOpen)(port))) {
|
|
@@ -121,6 +124,33 @@ async function getDriver(sessionName = 'default') {
|
|
|
121
124
|
}
|
|
122
125
|
driver = tvosDriver;
|
|
123
126
|
}
|
|
127
|
+
else if (platform === 'web') {
|
|
128
|
+
const browser = (0, bootstrap_js_1.webBrowserName)(deviceId);
|
|
129
|
+
let webSession = deviceId;
|
|
130
|
+
if ((0, bootstrap_js_1.isUnqualifiedWebId)(deviceId)) {
|
|
131
|
+
const existing = await (0, client_js_1.findRunningWebSession)(browser);
|
|
132
|
+
if (existing) {
|
|
133
|
+
webSession = existing;
|
|
134
|
+
(0, verbose_js_1.log)(`Reusing running web session ${webSession}`);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
webSession = (0, bootstrap_js_1.generateWebSessionId)(browser);
|
|
138
|
+
(0, verbose_js_1.log)(`Generated new web session ${webSession}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const webPort = await (0, bootstrap_js_1.getDriverPort)('web', webSession);
|
|
142
|
+
if (!(await (0, bootstrap_js_1.isPortOpen)(webPort))) {
|
|
143
|
+
(0, verbose_js_1.log)(`Web driver not running — starting daemon for ${webSession}...`);
|
|
144
|
+
await (0, client_js_1.startDaemon)(webSession);
|
|
145
|
+
await waitForPort(webPort);
|
|
146
|
+
}
|
|
147
|
+
const webDriver = new web_js_1.WebDriver(webPort, '127.0.0.1', webSession);
|
|
148
|
+
if (!(await webDriver.isAlive())) {
|
|
149
|
+
throw new Error(`Web browser driver on port ${webPort} is not responding.\n` +
|
|
150
|
+
`Run: conductor daemon-start --device ${webSession}`);
|
|
151
|
+
}
|
|
152
|
+
driver = webDriver;
|
|
153
|
+
}
|
|
124
154
|
else {
|
|
125
155
|
// Ensure the daemon is running — it handles APK install and driver startup.
|
|
126
156
|
await (0, client_js_1.startDaemon)(deviceId);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@houwert/conductor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "CLI tool for mobile app interactions — optimized for AI agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,12 +35,15 @@
|
|
|
35
35
|
"@grpc/grpc-js": "^1.12.0",
|
|
36
36
|
"@grpc/proto-loader": "^0.7.15",
|
|
37
37
|
"js-yaml": "^4.1.1",
|
|
38
|
-
"minimist": "^1.2.8"
|
|
38
|
+
"minimist": "^1.2.8",
|
|
39
|
+
"playwright-core": "^1.52.0",
|
|
40
|
+
"ws": "^8.20.0"
|
|
39
41
|
},
|
|
40
42
|
"devDependencies": {
|
|
41
43
|
"@types/js-yaml": "^4.0.9",
|
|
42
44
|
"@types/minimist": "^1.2.5",
|
|
43
45
|
"@types/node": "^20.0.0",
|
|
46
|
+
"@types/ws": "^8.18.1",
|
|
44
47
|
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
|
45
48
|
"@typescript-eslint/parser": "^8.56.1",
|
|
46
49
|
"eslint": "^10.0.2",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: conductor
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.6.0
|
|
4
4
|
description: "Token-efficient CLI for mobile UI testing (iOS simulator + Android emulator), designed for AI agents"
|
|
5
5
|
metadata.openclaw:
|
|
6
6
|
category: service
|
|
@@ -90,6 +90,17 @@ Both flags are required. The command reads the `.app` bundle path from the sourc
|
|
|
90
90
|
|
|
91
91
|
---
|
|
92
92
|
|
|
93
|
+
### `install-app <path>`
|
|
94
|
+
|
|
95
|
+
Install an app from a local `.app` bundle, `.ipa`, or `.apk` file onto the device.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
conductor install-app ./build/MyApp.app
|
|
99
|
+
conductor install-app ./build/app-debug.apk --device emulator-5554
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
93
104
|
### `launch-app <appId>`
|
|
94
105
|
|
|
95
106
|
Launch an app by bundle ID and save it to the session.
|
|
@@ -146,21 +157,21 @@ conductor uninstall-app com.example.myapp --device emulator-5554
|
|
|
146
157
|
|
|
147
158
|
---
|
|
148
159
|
|
|
149
|
-
### `tap <element>`
|
|
160
|
+
### `tap-on <element>`
|
|
150
161
|
|
|
151
162
|
Tap a UI element by its text label or accessibility ID. **Try the most obvious text label or ID first** — e.g. `"Sign In"`, `"Submit"`, `"btn_login"`. Only run `inspect` to look up the exact identifier if your first attempt fails. **For icon-only buttons (no visible text label), always run `inspect` first** to find the accessibility ID before tapping.
|
|
152
163
|
|
|
153
164
|
```bash
|
|
154
|
-
conductor tap "Sign In"
|
|
155
|
-
conductor tap --id "btn_login" # match by accessibility ID instead of text
|
|
156
|
-
conductor tap --text "Edit" # match by text only (not id)
|
|
157
|
-
conductor tap "Next" --index 1 # pick the 2nd match (0-based)
|
|
158
|
-
conductor tap "Add to cart" --long-press
|
|
159
|
-
conductor tap "Like" --double-tap
|
|
160
|
-
conductor tap "Delete" --optional # do not fail if not found
|
|
161
|
-
conductor tap "Edit" --below "Username" # tap "Edit" that is below the "Username" element
|
|
162
|
-
conductor tap "Submit" --above "Footer"
|
|
163
|
-
conductor tap ">" --right-of "Email"
|
|
165
|
+
conductor tap-on "Sign In"
|
|
166
|
+
conductor tap-on --id "btn_login" # match by accessibility ID instead of text
|
|
167
|
+
conductor tap-on --text "Edit" # match by text only (not id)
|
|
168
|
+
conductor tap-on "Next" --index 1 # pick the 2nd match (0-based)
|
|
169
|
+
conductor tap-on "Add to cart" --long-press
|
|
170
|
+
conductor tap-on "Like" --double-tap
|
|
171
|
+
conductor tap-on "Delete" --optional # do not fail if not found
|
|
172
|
+
conductor tap-on "Edit" --below "Username" # tap "Edit" that is below the "Username" element
|
|
173
|
+
conductor tap-on "Submit" --above "Footer"
|
|
174
|
+
conductor tap-on ">" --right-of "Email"
|
|
164
175
|
```
|
|
165
176
|
|
|
166
177
|
Flags:
|
|
@@ -181,13 +192,13 @@ Flags:
|
|
|
181
192
|
|
|
182
193
|
---
|
|
183
194
|
|
|
184
|
-
### `
|
|
195
|
+
### `input-text <text>`
|
|
185
196
|
|
|
186
197
|
Type text into the currently focused input field.
|
|
187
198
|
|
|
188
199
|
```bash
|
|
189
|
-
conductor
|
|
190
|
-
conductor
|
|
200
|
+
conductor input-text "hello@example.com"
|
|
201
|
+
conductor input-text "my password"
|
|
191
202
|
```
|
|
192
203
|
|
|
193
204
|
---
|
|
@@ -205,7 +216,7 @@ conductor erase-text 10 # erase 10 characters
|
|
|
205
216
|
|
|
206
217
|
### `back`
|
|
207
218
|
|
|
208
|
-
Press the Android back button. **Android only** — iOS has no back button. On iOS, always run `inspect` first to find the exact label or accessibility ID of the in-app back button, then use `tap` to press it.
|
|
219
|
+
Press the Android back button. **Android only** — iOS has no back button. On iOS, always run `inspect` first to find the exact label or accessibility ID of the in-app back button, then use `tap-on` to press it.
|
|
209
220
|
|
|
210
221
|
```bash
|
|
211
222
|
conductor back
|
|
@@ -375,14 +386,14 @@ conductor set-orientation landscape
|
|
|
375
386
|
|
|
376
387
|
---
|
|
377
388
|
|
|
378
|
-
### `screenshot`
|
|
389
|
+
### `take-screenshot`
|
|
379
390
|
|
|
380
|
-
Take a screenshot of the current screen. **Prefer `inspect` over `screenshot`** when you need to understand what is on screen — the view hierarchy tells you element text, IDs, and structure without consuming a vision token. Use `screenshot` only for visual evidence, debugging rendering issues, or when a human needs to see the screen.
|
|
391
|
+
Take a screenshot of the current screen. **Prefer `inspect` over `take-screenshot`** when you need to understand what is on screen — the view hierarchy tells you element text, IDs, and structure without consuming a vision token. Use `take-screenshot` only for visual evidence, debugging rendering issues, or when a human needs to see the screen.
|
|
381
392
|
|
|
382
393
|
```bash
|
|
383
|
-
conductor screenshot
|
|
384
|
-
conductor screenshot --output /tmp/screen.png
|
|
385
|
-
conductor screenshot --output ./screenshots/login.png
|
|
394
|
+
conductor take-screenshot
|
|
395
|
+
conductor take-screenshot --output /tmp/screen.png
|
|
396
|
+
conductor take-screenshot --output ./screenshots/login.png
|
|
386
397
|
```
|
|
387
398
|
|
|
388
399
|
Default output: `./screenshot-<timestamp>.png`
|
|
@@ -393,9 +404,9 @@ Default output: `./screenshot-<timestamp>.png`
|
|
|
393
404
|
|
|
394
405
|
### `inspect`
|
|
395
406
|
|
|
396
|
-
Print the UI element hierarchy of the current screen. Use this when a `tap` or `assert-visible` fails and you need to discover the exact element text or accessibility ID.
|
|
407
|
+
Print the UI element hierarchy of the current screen. Use this when a `tap-on` or `assert-visible` fails and you need to discover the exact element text or accessibility ID.
|
|
397
408
|
|
|
398
|
-
The hierarchy shows each element's type, text, and accessibility ID (resourceId / accessibilityIdentifier). Use the `text` value or `id` value as the argument to `tap` / `assert-visible`.
|
|
409
|
+
The hierarchy shows each element's type, text, and accessibility ID (resourceId / accessibilityIdentifier). Use the `text` value or `id` value as the argument to `tap-on` / `assert-visible`.
|
|
399
410
|
|
|
400
411
|
```bash
|
|
401
412
|
conductor inspect
|
|
@@ -403,6 +414,37 @@ conductor inspect
|
|
|
403
414
|
|
|
404
415
|
---
|
|
405
416
|
|
|
417
|
+
### `logs`
|
|
418
|
+
|
|
419
|
+
Stream app console/log output in real time. Auto-detects the best log source based on the platform:
|
|
420
|
+
|
|
421
|
+
- **Web** — captures Playwright browser console events
|
|
422
|
+
- **React Native (iOS/Android)** — connects to the Metro dev server via Chrome DevTools Protocol
|
|
423
|
+
- **Native iOS** — streams from the simulator via `simctl log stream`
|
|
424
|
+
- **Native Android** — streams from the device via `adb logcat`
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
conductor logs # auto-detect source and stream
|
|
428
|
+
conductor logs --source metro # force Metro CDP (for React Native JS logs)
|
|
429
|
+
conductor logs --source device # force platform-native logs (simctl/logcat)
|
|
430
|
+
conductor logs --level warn # only show warnings and errors
|
|
431
|
+
conductor logs --level error # only show errors
|
|
432
|
+
conductor logs --metro-port 8082 # custom Metro dev server port (default: 8081)
|
|
433
|
+
conductor logs --target 1 # connect to a specific Metro debugger target (0-based)
|
|
434
|
+
conductor logs --list # list available Metro debugger targets and exit
|
|
435
|
+
conductor logs --list --json # list targets as JSON (includes appId, logicalDeviceId)
|
|
436
|
+
conductor logs --json # NDJSON output (one JSON object per line)
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
For iOS/Android, auto-detection tries Metro first (2s timeout), then falls back to device logs. When multiple devices share one Metro instance, use `--list` to see available targets, then `--target <n>` to connect to a specific one. `--list` and `--source metro` don't require a running device session.
|
|
440
|
+
|
|
441
|
+
Exits on Ctrl+C. Each line in `--json` mode is a complete JSON object:
|
|
442
|
+
```json
|
|
443
|
+
{"timestamp":"...","level":"log","message":"User tapped sign-in","stackTrace":null,"source":"metro"}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
406
448
|
### `run-flow <file>`
|
|
407
449
|
|
|
408
450
|
Execute a Maestro YAML flow file.
|
|
@@ -449,12 +491,12 @@ conductor cheat-sheet
|
|
|
449
491
|
|
|
450
492
|
---
|
|
451
493
|
|
|
452
|
-
### `install`
|
|
494
|
+
### `install-skills`
|
|
453
495
|
|
|
454
496
|
Copy the conductor skill files into the current project's `.claude/skills/conductor/` directory, so AI agents in that project can use the `cheat-sheet` command locally.
|
|
455
497
|
|
|
456
498
|
```bash
|
|
457
|
-
conductor install
|
|
499
|
+
conductor install-skills
|
|
458
500
|
```
|
|
459
501
|
|
|
460
502
|
---
|
|
@@ -530,13 +572,13 @@ Exits with code 0 if all flows pass, 1 if any fail.
|
|
|
530
572
|
|
|
531
573
|
## Typical Agent Workflow
|
|
532
574
|
|
|
533
|
-
**Prefer individual CLI commands over `run-flow` / `run-flow-inline`.** Use `tap`, `
|
|
575
|
+
**Prefer individual CLI commands over `run-flow` / `run-flow-inline`.** Use `tap-on`, `input-text`, `scroll`, `swipe`, `assert-visible`, etc. directly — they are faster, give immediate feedback per step, and make failures easier to diagnose. Only reach for `run-flow` or `run-flow-inline` when you need Maestro-specific YAML features (conditional logic, `runScript`, retryTapIfNoChange`, etc.) that have no CLI equivalent.
|
|
534
576
|
|
|
535
|
-
**Always `launch-app` before interacting.** Never attempt to `tap`, `
|
|
577
|
+
**Always `launch-app` before interacting.** Never attempt to `tap-on`, `input-text`, `scroll`, or navigate before the app is launched and the session is set. `launch-app` both opens the app and saves the `appId`/`deviceId` to the session so all subsequent commands know which device and app to target.
|
|
536
578
|
|
|
537
|
-
**To understand what is on screen, run `inspect` first** — it gives you element text, IDs, and structure. Only take a `screenshot` when you need visual evidence or are debugging a rendering issue.
|
|
579
|
+
**To understand what is on screen, run `inspect` first** — it gives you element text, IDs, and structure. Only take a `take-screenshot` when you need visual evidence or are debugging a rendering issue.
|
|
538
580
|
|
|
539
|
-
**Try obvious text labels and IDs first. Only run `inspect` when a `tap` or `assert-visible` fails and you need to discover the exact identifier.**
|
|
581
|
+
**Try obvious text labels and IDs first. Only run `inspect` when a `tap-on` or `assert-visible` fails and you need to discover the exact identifier.**
|
|
540
582
|
|
|
541
583
|
Single-agent (default session, no flag needed):
|
|
542
584
|
```bash
|
|
@@ -547,17 +589,17 @@ conductor list-devices
|
|
|
547
589
|
conductor launch-app com.example.myapp --device emulator-5554
|
|
548
590
|
|
|
549
591
|
# 3. Interact using the most likely text labels or IDs
|
|
550
|
-
conductor tap "Sign In" # try the obvious label first
|
|
551
|
-
conductor tap "username_field" # or a guessed test ID
|
|
552
|
-
conductor
|
|
553
|
-
conductor tap "password_field"
|
|
554
|
-
conductor
|
|
555
|
-
conductor tap "Login"
|
|
592
|
+
conductor tap-on "Sign In" # try the obvious label first
|
|
593
|
+
conductor tap-on "username_field" # or a guessed test ID
|
|
594
|
+
conductor input-text "user@example.com"
|
|
595
|
+
conductor tap-on "password_field"
|
|
596
|
+
conductor input-text "secret123"
|
|
597
|
+
conductor tap-on "Login"
|
|
556
598
|
|
|
557
599
|
# → If a tap fails, run inspect to find the real identifier:
|
|
558
600
|
conductor inspect
|
|
559
601
|
# → hierarchy shows text="Log in", id="btn_login" — retry with correct value
|
|
560
|
-
conductor tap "Log in"
|
|
602
|
+
conductor tap-on "Log in"
|
|
561
603
|
|
|
562
604
|
# 4. Assert — try expected text directly
|
|
563
605
|
conductor assert-visible "Dashboard"
|
|
@@ -566,7 +608,7 @@ conductor assert-visible "Dashboard"
|
|
|
566
608
|
conductor inspect
|
|
567
609
|
|
|
568
610
|
# 6. Screenshot only for visual evidence or rendering checks
|
|
569
|
-
conductor screenshot --output /tmp/tester-ios-iphone-16/post-login.png
|
|
611
|
+
conductor take-screenshot --output /tmp/tester-ios-iphone-16/post-login.png
|
|
570
612
|
|
|
571
613
|
# 7. Run a full flow only when CLI commands aren't sufficient
|
|
572
614
|
# Write the YAML to /tmp/<agent-name>/ first — never pass YAML inline, it doesn't work
|
|
@@ -663,13 +705,13 @@ conductor launch-app com.example.myapp --device emulator-5554
|
|
|
663
705
|
**Every subsequent command carries `--device`:**
|
|
664
706
|
```bash
|
|
665
707
|
# Agent tester-ios-iphone-16
|
|
666
|
-
conductor tap "Sign In" --device C59D3241-FB6A-4E3B-AE7B-A82D3C933889
|
|
667
|
-
conductor
|
|
708
|
+
conductor tap-on "Sign In" --device C59D3241-FB6A-4E3B-AE7B-A82D3C933889
|
|
709
|
+
conductor input-text "user@a.com" --device C59D3241-FB6A-4E3B-AE7B-A82D3C933889
|
|
668
710
|
conductor inspect --device C59D3241-FB6A-4E3B-AE7B-A82D3C933889
|
|
669
711
|
|
|
670
712
|
# Agent tester-android-pixel-6 (runs fully in parallel)
|
|
671
|
-
conductor tap "Sign In" --device emulator-5554
|
|
672
|
-
conductor
|
|
713
|
+
conductor tap-on "Sign In" --device emulator-5554
|
|
714
|
+
conductor input-text "user@b.com" --device emulator-5554
|
|
673
715
|
conductor inspect --device emulator-5554
|
|
674
716
|
```
|
|
675
717
|
|