@houwert/conductor 0.5.0 → 0.7.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 +120 -32
- package/dist/daemon/web-server.js +892 -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 +74 -13
- 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
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ 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");
|
|
35
36
|
const install_app_js_1 = require("./commands/install-app.js");
|
|
36
37
|
const erase_text_js_1 = require("./commands/erase-text.js");
|
|
37
38
|
const assert_not_visible_js_1 = require("./commands/assert-not-visible.js");
|
|
@@ -41,21 +42,28 @@ const scroll_until_visible_js_1 = require("./commands/scroll-until-visible.js");
|
|
|
41
42
|
const set_location_js_1 = require("./commands/set-location.js");
|
|
42
43
|
const set_orientation_js_1 = require("./commands/set-orientation.js");
|
|
43
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");
|
|
44
47
|
const device_picker_js_1 = require("./device-picker.js");
|
|
45
48
|
const update_check_js_1 = require("./update-check.js");
|
|
49
|
+
const pkg_root_js_1 = require("./pkg-root.js");
|
|
50
|
+
const fs_1 = __importDefault(require("fs"));
|
|
51
|
+
const path_1 = __importDefault(require("path"));
|
|
46
52
|
const COMMAND_HELP = {
|
|
47
53
|
'start-device': start_device_js_1.HELP,
|
|
54
|
+
'delete-device': delete_device_js_1.HELP,
|
|
48
55
|
'list-devices': list_devices_js_1.HELP,
|
|
49
56
|
'foreground-app': foreground_app_js_1.HELP,
|
|
50
57
|
'list-apps': list_apps_js_1.HELP,
|
|
51
58
|
'copy-app': copy_app_js_1.HELP,
|
|
59
|
+
'download-app': download_app_js_1.HELP,
|
|
52
60
|
'install-app': install_app_js_1.HELP,
|
|
53
61
|
'launch-app': launch_app_js_1.HELP,
|
|
54
62
|
'stop-app': stop_app_js_1.HELP,
|
|
55
63
|
'clear-state': clear_state_js_1.HELP,
|
|
56
64
|
'uninstall-app': uninstall_app_js_1.HELP,
|
|
57
|
-
tap: tap_js_1.HELP,
|
|
58
|
-
|
|
65
|
+
'tap-on': tap_js_1.HELP,
|
|
66
|
+
'input-text': type_js_1.HELP,
|
|
59
67
|
'erase-text': erase_text_js_1.HELP,
|
|
60
68
|
back: back_js_1.HELP,
|
|
61
69
|
'press-key': press_key_js_1.HELP,
|
|
@@ -68,25 +76,30 @@ const COMMAND_HELP = {
|
|
|
68
76
|
'open-link': open_link_js_1.HELP,
|
|
69
77
|
'set-location': set_location_js_1.HELP,
|
|
70
78
|
'set-orientation': set_orientation_js_1.HELP,
|
|
71
|
-
screenshot: screenshot_js_1.HELP,
|
|
79
|
+
'take-screenshot': screenshot_js_1.HELP,
|
|
72
80
|
inspect: inspect_js_1.HELP,
|
|
73
81
|
focused: focused_js_1.HELP,
|
|
74
82
|
'run-flow': run_flow_js_1.HELP,
|
|
75
83
|
'run-flow-inline': run_flow_inline_js_1.HELP,
|
|
76
84
|
session: session_js_1.HELP,
|
|
77
85
|
'cheat-sheet': cheat_sheet_js_1.HELP,
|
|
78
|
-
install: install_js_1.
|
|
86
|
+
'install-plugin': install_js_1.HELP_INSTALL_PLUGIN,
|
|
87
|
+
'install-skills': install_js_1.HELP_INSTALL_SKILLS,
|
|
88
|
+
'install-web': install_js_1.HELP_INSTALL_WEB,
|
|
79
89
|
'daemon-start': daemon_js_1.HELP_DAEMON_START,
|
|
80
90
|
'daemon-stop': daemon_js_1.HELP_DAEMON_STOP,
|
|
81
91
|
'daemon-status': daemon_js_1.HELP_DAEMON_STATUS,
|
|
82
92
|
'device-pool': device_pool_js_1.HELP,
|
|
83
93
|
'run-parallel': run_parallel_js_1.HELP,
|
|
94
|
+
logs: logs_js_1.HELP,
|
|
84
95
|
};
|
|
85
96
|
const OPTIONS_HELP = `Options:
|
|
86
97
|
--device <id> Target device ID (also keys the session and daemon)
|
|
87
98
|
--device-name <n> Target a booted device by name (resolved to ID from booted devices)
|
|
99
|
+
--platform <p> Filter to devices of this platform (ios, android, tvos, web)
|
|
88
100
|
--json Output as machine-readable JSON
|
|
89
101
|
--verbose, -v Log daemon calls, fallbacks, and raw output
|
|
102
|
+
--version, -V Print version number
|
|
90
103
|
--help, -h Show this help`;
|
|
91
104
|
const HELP = `Usage: conductor <command> [args] [options]
|
|
92
105
|
|
|
@@ -100,10 +113,10 @@ async function main() {
|
|
|
100
113
|
boolean: [
|
|
101
114
|
'json',
|
|
102
115
|
'help',
|
|
116
|
+
'version',
|
|
103
117
|
'clear',
|
|
104
118
|
'list',
|
|
105
119
|
'verbose',
|
|
106
|
-
'skills',
|
|
107
120
|
'check',
|
|
108
121
|
'all',
|
|
109
122
|
'acquire',
|
|
@@ -140,15 +153,25 @@ async function main() {
|
|
|
140
153
|
'name',
|
|
141
154
|
'device-name',
|
|
142
155
|
'device-type',
|
|
156
|
+
'browser',
|
|
143
157
|
'from',
|
|
144
158
|
'to',
|
|
159
|
+
'source',
|
|
160
|
+
'metro-port',
|
|
161
|
+
'level',
|
|
145
162
|
],
|
|
146
|
-
alias: { h: 'help', v: 'verbose' },
|
|
163
|
+
alias: { h: 'help', v: 'verbose', V: 'version' },
|
|
147
164
|
});
|
|
148
165
|
if (argv['verbose'])
|
|
149
166
|
(0, verbose_js_1.setVerbose)(true);
|
|
150
167
|
const [command, ...rest] = argv._;
|
|
151
168
|
const opts = { json: argv['json'] };
|
|
169
|
+
if (argv['version']) {
|
|
170
|
+
const pkgRoot = (0, pkg_root_js_1.findPkgRoot)(__dirname);
|
|
171
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(pkgRoot, 'package.json'), 'utf-8'));
|
|
172
|
+
console.log(pkg.version);
|
|
173
|
+
process.exit(0);
|
|
174
|
+
}
|
|
152
175
|
// Handle help and unknown commands before device resolution —
|
|
153
176
|
// no point prompting for a device if we're just printing help or erroring out.
|
|
154
177
|
if (!command || argv['help']) {
|
|
@@ -164,11 +187,18 @@ async function main() {
|
|
|
164
187
|
const NO_DEVICE_COMMANDS = new Set([
|
|
165
188
|
'list-devices',
|
|
166
189
|
'start-device',
|
|
190
|
+
'delete-device',
|
|
167
191
|
'cheat-sheet',
|
|
168
|
-
'install',
|
|
192
|
+
'install-plugin',
|
|
193
|
+
'install-skills',
|
|
194
|
+
'install-web',
|
|
169
195
|
'copy-app',
|
|
170
196
|
'device-pool',
|
|
171
197
|
'run-parallel',
|
|
198
|
+
// `logs --list` and `logs --source metro` only query Metro on localhost — no device needed
|
|
199
|
+
...(command === 'logs' && (argv['list'] || argv['source'] === 'metro') ? ['logs'] : []),
|
|
200
|
+
// `daemon-stop --all` stops every daemon — no device needed
|
|
201
|
+
...(command === 'daemon-stop' && argv['all'] ? ['daemon-stop'] : []),
|
|
172
202
|
]);
|
|
173
203
|
if (!NO_DEVICE_COMMANDS.has(command) && !COMMAND_HELP[command]) {
|
|
174
204
|
console.error(`Unknown command: ${command}`);
|
|
@@ -196,7 +226,8 @@ async function main() {
|
|
|
196
226
|
sessionName = match.id;
|
|
197
227
|
}
|
|
198
228
|
else {
|
|
199
|
-
sessionName =
|
|
229
|
+
sessionName =
|
|
230
|
+
explicitDevice ?? (await (0, device_picker_js_1.pickDevice)(argv['platform'])) ?? 'default';
|
|
200
231
|
}
|
|
201
232
|
}
|
|
202
233
|
let exitCode = 0;
|
|
@@ -207,11 +238,18 @@ async function main() {
|
|
|
207
238
|
avd: argv['avd'],
|
|
208
239
|
name: argv['name'],
|
|
209
240
|
deviceType: argv['device-type'],
|
|
241
|
+
browser: argv['browser'],
|
|
210
242
|
});
|
|
211
243
|
break;
|
|
212
244
|
case 'list-devices':
|
|
213
245
|
exitCode = await (0, list_devices_js_1.listDevices)(opts);
|
|
214
246
|
break;
|
|
247
|
+
case 'delete-device':
|
|
248
|
+
exitCode = await (0, delete_device_js_1.deleteDevice)(rest[0], opts, {
|
|
249
|
+
platform: argv['platform'],
|
|
250
|
+
all: argv['all'],
|
|
251
|
+
});
|
|
252
|
+
break;
|
|
215
253
|
case 'foreground-app':
|
|
216
254
|
exitCode = await (0, foreground_app_js_1.foregroundApp)(opts, sessionName);
|
|
217
255
|
break;
|
|
@@ -225,6 +263,11 @@ async function main() {
|
|
|
225
263
|
exitCode = await (0, copy_app_js_1.copyApp)(bundleId, from ?? '', to ?? '', opts);
|
|
226
264
|
break;
|
|
227
265
|
}
|
|
266
|
+
case 'download-app': {
|
|
267
|
+
const appId = rest[0] ?? '';
|
|
268
|
+
exitCode = await (0, download_app_js_1.downloadApp)(appId, argv['output'], opts, sessionName);
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
228
271
|
case 'install-app': {
|
|
229
272
|
const appPath = rest[0] ?? '';
|
|
230
273
|
exitCode = await (0, install_app_js_1.installApp)(appPath, opts, sessionName);
|
|
@@ -261,7 +304,7 @@ async function main() {
|
|
|
261
304
|
exitCode = await (0, uninstall_app_js_1.uninstallApp)(appId, opts, sessionName);
|
|
262
305
|
break;
|
|
263
306
|
}
|
|
264
|
-
case 'tap': {
|
|
307
|
+
case 'tap-on': {
|
|
265
308
|
const element = rest.join(' ');
|
|
266
309
|
exitCode = await (0, tap_js_1.tap)(element, opts, sessionName, {
|
|
267
310
|
id: argv['id'],
|
|
@@ -281,7 +324,7 @@ async function main() {
|
|
|
281
324
|
});
|
|
282
325
|
break;
|
|
283
326
|
}
|
|
284
|
-
case '
|
|
327
|
+
case 'input-text': {
|
|
285
328
|
const text = rest.join(' ');
|
|
286
329
|
exitCode = await (0, type_js_1.typeText)(text, opts, sessionName);
|
|
287
330
|
break;
|
|
@@ -397,7 +440,7 @@ async function main() {
|
|
|
397
440
|
exitCode = await (0, set_orientation_js_1.setOrientation)(orientation, opts, sessionName);
|
|
398
441
|
break;
|
|
399
442
|
}
|
|
400
|
-
case 'screenshot': {
|
|
443
|
+
case 'take-screenshot': {
|
|
401
444
|
const outPath = argv['output'];
|
|
402
445
|
exitCode = await (0, screenshot_js_1.screenshot)(outPath, opts, sessionName);
|
|
403
446
|
break;
|
|
@@ -411,6 +454,18 @@ async function main() {
|
|
|
411
454
|
interval: argv['interval'] !== undefined ? Number(argv['interval']) : undefined,
|
|
412
455
|
});
|
|
413
456
|
break;
|
|
457
|
+
case 'logs':
|
|
458
|
+
exitCode = await (0, logs_js_1.logs)(opts, sessionName, {
|
|
459
|
+
source: argv['source'],
|
|
460
|
+
level: argv['level'],
|
|
461
|
+
metro: argv['metro'] || argv['metro-port'] !== undefined,
|
|
462
|
+
metroPort: argv['metro-port'] !== undefined ? Number(argv['metro-port']) : undefined,
|
|
463
|
+
target: argv['target'] !== undefined ? Number(argv['target']) : undefined,
|
|
464
|
+
list: argv['list'],
|
|
465
|
+
recent: argv['recent'] !== undefined ? Number(argv['recent']) : undefined,
|
|
466
|
+
duration: argv['duration'] !== undefined ? Number(argv['duration']) : undefined,
|
|
467
|
+
});
|
|
468
|
+
break;
|
|
414
469
|
case 'run-flow': {
|
|
415
470
|
const file = rest[0] ?? '';
|
|
416
471
|
const rawEnv = argv['env'];
|
|
@@ -430,8 +485,14 @@ async function main() {
|
|
|
430
485
|
case 'cheat-sheet':
|
|
431
486
|
exitCode = await (0, cheat_sheet_js_1.cheatSheet)();
|
|
432
487
|
break;
|
|
433
|
-
case 'install':
|
|
434
|
-
exitCode = await (0, install_js_1.
|
|
488
|
+
case 'install-plugin':
|
|
489
|
+
exitCode = await (0, install_js_1.installPluginCli)(opts, argv['check']);
|
|
490
|
+
break;
|
|
491
|
+
case 'install-skills':
|
|
492
|
+
exitCode = await (0, install_js_1.installSkillsCli)(opts, argv['check']);
|
|
493
|
+
break;
|
|
494
|
+
case 'install-web':
|
|
495
|
+
exitCode = await (0, install_js_1.installWebCli)(opts, argv['check'], rest[0]);
|
|
435
496
|
break;
|
|
436
497
|
case 'daemon-start':
|
|
437
498
|
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.7.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.7.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
|
|
@@ -157,21 +157,21 @@ conductor uninstall-app com.example.myapp --device emulator-5554
|
|
|
157
157
|
|
|
158
158
|
---
|
|
159
159
|
|
|
160
|
-
### `tap <element>`
|
|
160
|
+
### `tap-on <element>`
|
|
161
161
|
|
|
162
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.
|
|
163
163
|
|
|
164
164
|
```bash
|
|
165
|
-
conductor tap "Sign In"
|
|
166
|
-
conductor tap --id "btn_login" # match by accessibility ID instead of text
|
|
167
|
-
conductor tap --text "Edit" # match by text only (not id)
|
|
168
|
-
conductor tap "Next" --index 1 # pick the 2nd match (0-based)
|
|
169
|
-
conductor tap "Add to cart" --long-press
|
|
170
|
-
conductor tap "Like" --double-tap
|
|
171
|
-
conductor tap "Delete" --optional # do not fail if not found
|
|
172
|
-
conductor tap "Edit" --below "Username" # tap "Edit" that is below the "Username" element
|
|
173
|
-
conductor tap "Submit" --above "Footer"
|
|
174
|
-
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"
|
|
175
175
|
```
|
|
176
176
|
|
|
177
177
|
Flags:
|
|
@@ -192,13 +192,13 @@ Flags:
|
|
|
192
192
|
|
|
193
193
|
---
|
|
194
194
|
|
|
195
|
-
### `
|
|
195
|
+
### `input-text <text>`
|
|
196
196
|
|
|
197
197
|
Type text into the currently focused input field.
|
|
198
198
|
|
|
199
199
|
```bash
|
|
200
|
-
conductor
|
|
201
|
-
conductor
|
|
200
|
+
conductor input-text "hello@example.com"
|
|
201
|
+
conductor input-text "my password"
|
|
202
202
|
```
|
|
203
203
|
|
|
204
204
|
---
|
|
@@ -216,7 +216,7 @@ conductor erase-text 10 # erase 10 characters
|
|
|
216
216
|
|
|
217
217
|
### `back`
|
|
218
218
|
|
|
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` 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.
|
|
220
220
|
|
|
221
221
|
```bash
|
|
222
222
|
conductor back
|
|
@@ -386,14 +386,14 @@ conductor set-orientation landscape
|
|
|
386
386
|
|
|
387
387
|
---
|
|
388
388
|
|
|
389
|
-
### `screenshot`
|
|
389
|
+
### `take-screenshot`
|
|
390
390
|
|
|
391
|
-
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.
|
|
392
392
|
|
|
393
393
|
```bash
|
|
394
|
-
conductor screenshot
|
|
395
|
-
conductor screenshot --output /tmp/screen.png
|
|
396
|
-
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
|
|
397
397
|
```
|
|
398
398
|
|
|
399
399
|
Default output: `./screenshot-<timestamp>.png`
|
|
@@ -404,9 +404,9 @@ Default output: `./screenshot-<timestamp>.png`
|
|
|
404
404
|
|
|
405
405
|
### `inspect`
|
|
406
406
|
|
|
407
|
-
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.
|
|
408
408
|
|
|
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` / `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`.
|
|
410
410
|
|
|
411
411
|
```bash
|
|
412
412
|
conductor inspect
|
|
@@ -414,6 +414,37 @@ conductor inspect
|
|
|
414
414
|
|
|
415
415
|
---
|
|
416
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
|
+
|
|
417
448
|
### `run-flow <file>`
|
|
418
449
|
|
|
419
450
|
Execute a Maestro YAML flow file.
|
|
@@ -460,12 +491,12 @@ conductor cheat-sheet
|
|
|
460
491
|
|
|
461
492
|
---
|
|
462
493
|
|
|
463
|
-
### `install`
|
|
494
|
+
### `install-skills`
|
|
464
495
|
|
|
465
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.
|
|
466
497
|
|
|
467
498
|
```bash
|
|
468
|
-
conductor install
|
|
499
|
+
conductor install-skills
|
|
469
500
|
```
|
|
470
501
|
|
|
471
502
|
---
|
|
@@ -541,13 +572,13 @@ Exits with code 0 if all flows pass, 1 if any fail.
|
|
|
541
572
|
|
|
542
573
|
## Typical Agent Workflow
|
|
543
574
|
|
|
544
|
-
**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.
|
|
545
576
|
|
|
546
|
-
**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.
|
|
547
578
|
|
|
548
|
-
**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.
|
|
549
580
|
|
|
550
|
-
**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.**
|
|
551
582
|
|
|
552
583
|
Single-agent (default session, no flag needed):
|
|
553
584
|
```bash
|
|
@@ -558,17 +589,17 @@ conductor list-devices
|
|
|
558
589
|
conductor launch-app com.example.myapp --device emulator-5554
|
|
559
590
|
|
|
560
591
|
# 3. Interact using the most likely text labels or IDs
|
|
561
|
-
conductor tap "Sign In" # try the obvious label first
|
|
562
|
-
conductor tap "username_field" # or a guessed test ID
|
|
563
|
-
conductor
|
|
564
|
-
conductor tap "password_field"
|
|
565
|
-
conductor
|
|
566
|
-
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"
|
|
567
598
|
|
|
568
599
|
# → If a tap fails, run inspect to find the real identifier:
|
|
569
600
|
conductor inspect
|
|
570
601
|
# → hierarchy shows text="Log in", id="btn_login" — retry with correct value
|
|
571
|
-
conductor tap "Log in"
|
|
602
|
+
conductor tap-on "Log in"
|
|
572
603
|
|
|
573
604
|
# 4. Assert — try expected text directly
|
|
574
605
|
conductor assert-visible "Dashboard"
|
|
@@ -577,7 +608,7 @@ conductor assert-visible "Dashboard"
|
|
|
577
608
|
conductor inspect
|
|
578
609
|
|
|
579
610
|
# 6. Screenshot only for visual evidence or rendering checks
|
|
580
|
-
conductor screenshot --output /tmp/tester-ios-iphone-16/post-login.png
|
|
611
|
+
conductor take-screenshot --output /tmp/tester-ios-iphone-16/post-login.png
|
|
581
612
|
|
|
582
613
|
# 7. Run a full flow only when CLI commands aren't sufficient
|
|
583
614
|
# Write the YAML to /tmp/<agent-name>/ first — never pass YAML inline, it doesn't work
|
|
@@ -674,13 +705,13 @@ conductor launch-app com.example.myapp --device emulator-5554
|
|
|
674
705
|
**Every subsequent command carries `--device`:**
|
|
675
706
|
```bash
|
|
676
707
|
# Agent tester-ios-iphone-16
|
|
677
|
-
conductor tap "Sign In" --device C59D3241-FB6A-4E3B-AE7B-A82D3C933889
|
|
678
|
-
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
|
|
679
710
|
conductor inspect --device C59D3241-FB6A-4E3B-AE7B-A82D3C933889
|
|
680
711
|
|
|
681
712
|
# Agent tester-android-pixel-6 (runs fully in parallel)
|
|
682
|
-
conductor tap "Sign In" --device emulator-5554
|
|
683
|
-
conductor
|
|
713
|
+
conductor tap-on "Sign In" --device emulator-5554
|
|
714
|
+
conductor input-text "user@b.com" --device emulator-5554
|
|
684
715
|
conductor inspect --device emulator-5554
|
|
685
716
|
```
|
|
686
717
|
|