@houwert/conductor 0.2.0 → 0.3.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/.claude-plugin/plugin.json +4 -1
- package/README.md +121 -21
- package/dist/commands/assert-not-visible.js +5 -0
- package/dist/commands/assert-visible.js +15 -0
- package/dist/commands/back.js +6 -1
- package/dist/commands/cheat-sheet.js +2 -0
- package/dist/commands/copy-app.js +55 -0
- package/dist/commands/daemon.js +4 -0
- package/dist/commands/device-pool.js +4 -0
- package/dist/commands/erase-text.js +2 -0
- package/dist/commands/focused.js +233 -0
- package/dist/commands/foreground-app.js +2 -0
- package/dist/commands/hide-keyboard.js +2 -0
- package/dist/commands/inspect.js +23 -1
- package/dist/commands/install.js +69 -4
- package/dist/commands/launch-app.js +6 -0
- package/dist/commands/list-apps.js +2 -0
- package/dist/commands/list-devices.js +70 -7
- package/dist/commands/open-link.js +2 -0
- package/dist/commands/press-key.js +73 -7
- package/dist/commands/run-flow-inline.js +3 -0
- package/dist/commands/run-flow.js +4 -0
- package/dist/commands/run-parallel.js +2 -0
- package/dist/commands/screenshot.js +2 -0
- package/dist/commands/scroll-until-visible.js +6 -0
- package/dist/commands/scroll.js +6 -0
- package/dist/commands/session.js +2 -0
- package/dist/commands/set-location.js +2 -0
- package/dist/commands/set-orientation.js +2 -0
- package/dist/commands/start-device.js +311 -9
- package/dist/commands/stop-app.js +2 -0
- package/dist/commands/swipe.js +10 -0
- package/dist/commands/tap.js +20 -0
- package/dist/commands/type.js +2 -0
- package/dist/daemon/server.js +64 -22
- package/dist/device-picker.js +65 -0
- package/dist/drivers/android.js +14 -13
- package/dist/drivers/bootstrap.js +179 -6
- package/dist/drivers/element-resolver.js +10 -0
- package/dist/drivers/flow-runner.js +6 -5
- package/dist/drivers/ios.js +2 -1
- package/dist/index.js +120 -97
- package/dist/postinstall.js +2 -2
- package/dist/runner.js +40 -6
- 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-config.xctestrun +121 -0
- package/drivers/tvos/conductor-driver-tvos.zip +0 -0
- package/drivers/tvos/conductor-driver-tvosUITests-Runner.zip +0 -0
- package/package.json +1 -1
- package/skills/conductor/SKILL.md +44 -6
- package/skills/conductor/references/flow-syntax.md +1 -0
- package/skills/skills.yaml +8 -0
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ const swipe_js_1 = require("./commands/swipe.js");
|
|
|
17
17
|
const assert_visible_js_1 = require("./commands/assert-visible.js");
|
|
18
18
|
const screenshot_js_1 = require("./commands/screenshot.js");
|
|
19
19
|
const inspect_js_1 = require("./commands/inspect.js");
|
|
20
|
+
const focused_js_1 = require("./commands/focused.js");
|
|
20
21
|
const run_flow_js_1 = require("./commands/run-flow.js");
|
|
21
22
|
const run_flow_inline_js_1 = require("./commands/run-flow-inline.js");
|
|
22
23
|
const press_key_js_1 = require("./commands/press-key.js");
|
|
@@ -28,6 +29,7 @@ const device_pool_js_1 = require("./commands/device-pool.js");
|
|
|
28
29
|
const run_parallel_js_1 = require("./commands/run-parallel.js");
|
|
29
30
|
const foreground_app_js_1 = require("./commands/foreground-app.js");
|
|
30
31
|
const list_apps_js_1 = require("./commands/list-apps.js");
|
|
32
|
+
const copy_app_js_1 = require("./commands/copy-app.js");
|
|
31
33
|
const erase_text_js_1 = require("./commands/erase-text.js");
|
|
32
34
|
const assert_not_visible_js_1 = require("./commands/assert-not-visible.js");
|
|
33
35
|
const open_link_js_1 = require("./commands/open-link.js");
|
|
@@ -36,100 +38,56 @@ const scroll_until_visible_js_1 = require("./commands/scroll-until-visible.js");
|
|
|
36
38
|
const set_location_js_1 = require("./commands/set-location.js");
|
|
37
39
|
const set_orientation_js_1 = require("./commands/set-orientation.js");
|
|
38
40
|
const start_device_js_1 = require("./commands/start-device.js");
|
|
39
|
-
const
|
|
41
|
+
const device_picker_js_1 = require("./device-picker.js");
|
|
40
42
|
const update_check_js_1 = require("./update-check.js");
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
scroll [--direction down|up|left|right]
|
|
79
|
-
swipe --direction <UP|DOWN|LEFT|RIGHT>
|
|
80
|
-
--start <x,y> Start coordinate (0–1 normalised or absolute px)
|
|
81
|
-
--end <x,y> End coordinate (0–1 normalised or absolute px)
|
|
82
|
-
--duration <ms> Swipe duration in milliseconds (default: 500)
|
|
83
|
-
scroll-until-visible <element> Scroll until element is visible
|
|
84
|
-
--id <id> Match by accessibility id instead of text
|
|
85
|
-
--text <text> Match by text only (not id)
|
|
86
|
-
--direction <down|up|left|right> Scroll direction (default: down)
|
|
87
|
-
--timeout <ms> Max time in milliseconds (default: 30000)
|
|
88
|
-
assert-visible <element> Assert element is visible
|
|
89
|
-
--id <id> Match by accessibility id instead of text
|
|
90
|
-
--text <text> Match by text only (not id)
|
|
91
|
-
--index <n> Pick the nth match (0-based)
|
|
92
|
-
--timeout <ms> Max wait time in milliseconds
|
|
93
|
-
--optional Do not fail if element is not found
|
|
94
|
-
--focused Match only focused elements
|
|
95
|
-
--enabled / --no-enabled Match by enabled state
|
|
96
|
-
--checked / --no-checked Match by checked state
|
|
97
|
-
--selected / --no-selected Match by selected state
|
|
98
|
-
--below <text> Match element below the given reference
|
|
99
|
-
--above <text> Match element above the given reference
|
|
100
|
-
--left-of <text> Match element left of the given reference
|
|
101
|
-
--right-of <text> Match element right of the given reference
|
|
102
|
-
assert-not-visible <element> Assert element is absent from screen
|
|
103
|
-
--id <id> Match by accessibility id instead of text
|
|
104
|
-
--text <text> Match by text only (not id)
|
|
105
|
-
--timeout <ms> Max check time in milliseconds (default: 1000)
|
|
106
|
-
open-link <url> Open a URL / deep link
|
|
107
|
-
set-location --lat <n> --lng <n> Set GPS coordinates
|
|
108
|
-
set-orientation <portrait|landscape> Set device orientation
|
|
109
|
-
screenshot [--output <path>] Take screenshot
|
|
110
|
-
inspect Print UI hierarchy
|
|
111
|
-
run-flow <file> [--device <id>] Run a Maestro YAML flow file
|
|
112
|
-
--env KEY=VALUE Inject env var (repeatable; overrides flow env block)
|
|
113
|
-
--benchmark Print elapsed time for each command and total flow time
|
|
114
|
-
run-flow-inline <yaml> Run inline YAML commands
|
|
115
|
-
--benchmark Print elapsed time for each command and total flow time
|
|
116
|
-
session [--clear] [--list] Show, clear, or list device sessions
|
|
117
|
-
cheat-sheet Print command reference
|
|
118
|
-
install Install/reinstall Claude Code plugin
|
|
119
|
-
daemon-start Start background daemon (manages driver process)
|
|
120
|
-
daemon-stop [--all] Stop background daemon (--all stops every session's daemon)
|
|
121
|
-
daemon-status Show daemon status
|
|
122
|
-
device-pool --list List all devices and pool status
|
|
123
|
-
device-pool --acquire Claim a free device (prints device ID)
|
|
124
|
-
device-pool --release <id> Release a device back to the pool
|
|
125
|
-
run-parallel --flows-dir <path> Run flows in parallel across all devices
|
|
126
|
-
|
|
127
|
-
Options:
|
|
43
|
+
const COMMAND_HELP = {
|
|
44
|
+
'start-device': start_device_js_1.HELP,
|
|
45
|
+
'list-devices': list_devices_js_1.HELP,
|
|
46
|
+
'foreground-app': foreground_app_js_1.HELP,
|
|
47
|
+
'list-apps': list_apps_js_1.HELP,
|
|
48
|
+
'copy-app': copy_app_js_1.HELP,
|
|
49
|
+
'launch-app': launch_app_js_1.HELP,
|
|
50
|
+
'stop-app': stop_app_js_1.HELP,
|
|
51
|
+
tap: tap_js_1.HELP,
|
|
52
|
+
type: type_js_1.HELP,
|
|
53
|
+
'erase-text': erase_text_js_1.HELP,
|
|
54
|
+
back: back_js_1.HELP,
|
|
55
|
+
'press-key': press_key_js_1.HELP,
|
|
56
|
+
'hide-keyboard': hide_keyboard_js_1.HELP,
|
|
57
|
+
scroll: scroll_js_1.HELP,
|
|
58
|
+
swipe: swipe_js_1.HELP,
|
|
59
|
+
'scroll-until-visible': scroll_until_visible_js_1.HELP,
|
|
60
|
+
'assert-visible': assert_visible_js_1.HELP,
|
|
61
|
+
'assert-not-visible': assert_not_visible_js_1.HELP,
|
|
62
|
+
'open-link': open_link_js_1.HELP,
|
|
63
|
+
'set-location': set_location_js_1.HELP,
|
|
64
|
+
'set-orientation': set_orientation_js_1.HELP,
|
|
65
|
+
screenshot: screenshot_js_1.HELP,
|
|
66
|
+
inspect: inspect_js_1.HELP,
|
|
67
|
+
focused: focused_js_1.HELP,
|
|
68
|
+
'run-flow': run_flow_js_1.HELP,
|
|
69
|
+
'run-flow-inline': run_flow_inline_js_1.HELP,
|
|
70
|
+
session: session_js_1.HELP,
|
|
71
|
+
'cheat-sheet': cheat_sheet_js_1.HELP,
|
|
72
|
+
install: install_js_1.HELP,
|
|
73
|
+
'daemon-start': daemon_js_1.HELP_DAEMON_START,
|
|
74
|
+
'daemon-stop': daemon_js_1.HELP_DAEMON_STOP,
|
|
75
|
+
'daemon-status': daemon_js_1.HELP_DAEMON_STATUS,
|
|
76
|
+
'device-pool': device_pool_js_1.HELP,
|
|
77
|
+
'run-parallel': run_parallel_js_1.HELP,
|
|
78
|
+
};
|
|
79
|
+
const OPTIONS_HELP = `Options:
|
|
128
80
|
--device <id> Target device ID (also keys the session and daemon)
|
|
81
|
+
--device-name <n> Target a booted device by name (resolved to ID from booted devices)
|
|
129
82
|
--json Output as machine-readable JSON
|
|
130
83
|
--verbose, -v Log daemon calls, fallbacks, and raw output
|
|
131
|
-
--help, -h Show this help
|
|
132
|
-
|
|
84
|
+
--help, -h Show this help`;
|
|
85
|
+
const HELP = `Usage: conductor <command> [args] [options]
|
|
86
|
+
|
|
87
|
+
Commands:
|
|
88
|
+
${Object.values(COMMAND_HELP).join('\n')}
|
|
89
|
+
|
|
90
|
+
${OPTIONS_HELP}`;
|
|
133
91
|
async function main() {
|
|
134
92
|
(0, update_check_js_1.checkForUpdates)();
|
|
135
93
|
const argv = (0, minimist_1.default)(process.argv.slice(2), {
|
|
@@ -140,6 +98,7 @@ async function main() {
|
|
|
140
98
|
'list',
|
|
141
99
|
'verbose',
|
|
142
100
|
'skills',
|
|
101
|
+
'check',
|
|
143
102
|
'all',
|
|
144
103
|
'acquire',
|
|
145
104
|
'release',
|
|
@@ -150,6 +109,7 @@ async function main() {
|
|
|
150
109
|
'double-tap',
|
|
151
110
|
'optional',
|
|
152
111
|
'benchmark',
|
|
112
|
+
'dump',
|
|
153
113
|
],
|
|
154
114
|
string: [
|
|
155
115
|
'device',
|
|
@@ -171,6 +131,11 @@ async function main() {
|
|
|
171
131
|
'platform',
|
|
172
132
|
'os-version',
|
|
173
133
|
'avd',
|
|
134
|
+
'name',
|
|
135
|
+
'device-name',
|
|
136
|
+
'device-type',
|
|
137
|
+
'from',
|
|
138
|
+
'to',
|
|
174
139
|
],
|
|
175
140
|
alias: { h: 'help', v: 'verbose' },
|
|
176
141
|
});
|
|
@@ -178,20 +143,64 @@ async function main() {
|
|
|
178
143
|
(0, verbose_js_1.setVerbose)(true);
|
|
179
144
|
const [command, ...rest] = argv._;
|
|
180
145
|
const opts = { json: argv['json'] };
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
const explicitDevice = argv['device'];
|
|
184
|
-
const sessionName = explicitDevice ?? (await (0, runner_js_1.detectFirstDevice)()) ?? 'default';
|
|
146
|
+
// Handle help and unknown commands before device resolution —
|
|
147
|
+
// no point prompting for a device if we're just printing help or erroring out.
|
|
185
148
|
if (!command || argv['help']) {
|
|
186
|
-
|
|
149
|
+
if (command && argv['help'] && COMMAND_HELP[command]) {
|
|
150
|
+
console.log(`Usage: conductor ${command} [options]\n\n${COMMAND_HELP[command]}\n\n${OPTIONS_HELP}`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
console.log(HELP);
|
|
154
|
+
}
|
|
187
155
|
process.exit(0);
|
|
188
156
|
}
|
|
157
|
+
// Commands that don't need a device session
|
|
158
|
+
const NO_DEVICE_COMMANDS = new Set([
|
|
159
|
+
'list-devices',
|
|
160
|
+
'start-device',
|
|
161
|
+
'cheat-sheet',
|
|
162
|
+
'install',
|
|
163
|
+
'copy-app',
|
|
164
|
+
'device-pool',
|
|
165
|
+
'run-parallel',
|
|
166
|
+
]);
|
|
167
|
+
if (!NO_DEVICE_COMMANDS.has(command) && !COMMAND_HELP[command]) {
|
|
168
|
+
console.error(`Unknown command: ${command}`);
|
|
169
|
+
console.error('Run `conductor --help` for usage.');
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
// The device ID is the natural key for both the session file and the daemon.
|
|
173
|
+
// Use --device if given, otherwise detect the first booted device, otherwise 'default'.
|
|
174
|
+
// Only resolve for commands that actually need a device.
|
|
175
|
+
let sessionName = 'default';
|
|
176
|
+
if (!NO_DEVICE_COMMANDS.has(command)) {
|
|
177
|
+
const explicitDevice = argv['device'];
|
|
178
|
+
const deviceName = argv['device-name'];
|
|
179
|
+
if (explicitDevice && deviceName) {
|
|
180
|
+
console.error('Error: --device and --device-name are mutually exclusive.');
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
if (deviceName) {
|
|
184
|
+
const devices = await (0, list_devices_js_1.discoverBootedDevices)();
|
|
185
|
+
const match = devices.find((d) => d.name === deviceName);
|
|
186
|
+
if (!match) {
|
|
187
|
+
console.error(`Error: No booted device found with name "${deviceName}". Run \`conductor list-devices\` to see booted devices.`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
sessionName = match.id;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
sessionName = explicitDevice ?? (await (0, device_picker_js_1.pickDevice)()) ?? 'default';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
189
196
|
let exitCode = 0;
|
|
190
197
|
switch (command) {
|
|
191
198
|
case 'start-device':
|
|
192
199
|
exitCode = await (0, start_device_js_1.startDevice)(argv['platform'], opts, {
|
|
193
200
|
osVersion: argv['os-version'],
|
|
194
201
|
avd: argv['avd'],
|
|
202
|
+
name: argv['name'],
|
|
203
|
+
deviceType: argv['device-type'],
|
|
195
204
|
});
|
|
196
205
|
break;
|
|
197
206
|
case 'list-devices':
|
|
@@ -203,6 +212,13 @@ async function main() {
|
|
|
203
212
|
case 'list-apps':
|
|
204
213
|
exitCode = await (0, list_apps_js_1.listApps)(opts, sessionName);
|
|
205
214
|
break;
|
|
215
|
+
case 'copy-app': {
|
|
216
|
+
const bundleId = rest[0] ?? '';
|
|
217
|
+
const from = argv['from'];
|
|
218
|
+
const to = argv['to'];
|
|
219
|
+
exitCode = await (0, copy_app_js_1.copyApp)(bundleId, from ?? '', to ?? '', opts);
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
206
222
|
case 'launch-app': {
|
|
207
223
|
const appId = rest[0] ?? '';
|
|
208
224
|
// --argument key=value (repeatable) → Record<string, string>
|
|
@@ -366,7 +382,13 @@ async function main() {
|
|
|
366
382
|
break;
|
|
367
383
|
}
|
|
368
384
|
case 'inspect':
|
|
369
|
-
exitCode = await (0, inspect_js_1.inspect)(opts, sessionName);
|
|
385
|
+
exitCode = await (0, inspect_js_1.inspect)(opts, sessionName, { dump: argv['dump'] });
|
|
386
|
+
break;
|
|
387
|
+
case 'focused':
|
|
388
|
+
exitCode = await (0, focused_js_1.focused)(opts, sessionName, {
|
|
389
|
+
poll: argv['poll'],
|
|
390
|
+
interval: argv['interval'] !== undefined ? Number(argv['interval']) : undefined,
|
|
391
|
+
});
|
|
370
392
|
break;
|
|
371
393
|
case 'run-flow': {
|
|
372
394
|
const file = rest[0] ?? '';
|
|
@@ -388,7 +410,7 @@ async function main() {
|
|
|
388
410
|
exitCode = await (0, cheat_sheet_js_1.cheatSheet)();
|
|
389
411
|
break;
|
|
390
412
|
case 'install':
|
|
391
|
-
exitCode = await (0, install_js_1.installSkills)(opts);
|
|
413
|
+
exitCode = await (0, install_js_1.installSkills)(opts, argv['skills'], argv['check']);
|
|
392
414
|
break;
|
|
393
415
|
case 'daemon-start':
|
|
394
416
|
exitCode = await (0, daemon_js_1.daemonStart)(opts, sessionName);
|
|
@@ -413,6 +435,7 @@ async function main() {
|
|
|
413
435
|
break;
|
|
414
436
|
}
|
|
415
437
|
default:
|
|
438
|
+
// Should be unreachable — unknown commands are caught before device resolution.
|
|
416
439
|
console.error(`Unknown command: ${command}`);
|
|
417
440
|
console.error('Run `conductor --help` for usage.');
|
|
418
441
|
exitCode = 1;
|
package/dist/postinstall.js
CHANGED
|
@@ -4,8 +4,8 @@ const install_js_1 = require("./commands/install.js");
|
|
|
4
4
|
// Runs as `postinstall` after `npm install -g @houwert/conductor`.
|
|
5
5
|
// Silent on failure so it never breaks the npm install.
|
|
6
6
|
try {
|
|
7
|
-
(0, install_js_1.installPlugin)();
|
|
8
|
-
process.stdout.write(
|
|
7
|
+
const version = (0, install_js_1.installPlugin)();
|
|
8
|
+
process.stdout.write(`✓ Conductor Claude Code plugin installed (v${version})\n`);
|
|
9
9
|
}
|
|
10
10
|
catch {
|
|
11
11
|
// Silent on failure — never break npm install
|
package/dist/runner.js
CHANGED
|
@@ -95,20 +95,54 @@ async function getDriver(sessionName = 'default') {
|
|
|
95
95
|
const port = await (0, bootstrap_js_1.getDriverPort)(platform, deviceId);
|
|
96
96
|
(0, verbose_js_1.log)(`getDriver: platform=${platform} deviceId=${deviceId} port=${port}`);
|
|
97
97
|
let driver;
|
|
98
|
-
if (!(await (0, bootstrap_js_1.isPortOpen)(port))) {
|
|
99
|
-
(0, verbose_js_1.log)(`Driver not running — starting daemon for ${deviceId}...`);
|
|
100
|
-
await (0, client_js_1.startDaemon)(deviceId);
|
|
101
|
-
await waitForPort(port);
|
|
102
|
-
}
|
|
103
98
|
if (platform === 'ios') {
|
|
104
|
-
|
|
99
|
+
if (!(await (0, bootstrap_js_1.isPortOpen)(port))) {
|
|
100
|
+
(0, verbose_js_1.log)(`Driver not running — starting daemon for ${deviceId}...`);
|
|
101
|
+
await (0, client_js_1.startDaemon)(deviceId);
|
|
102
|
+
await waitForPort(port);
|
|
103
|
+
}
|
|
104
|
+
const iosDriver = new ios_js_1.IOSDriver(port, '127.0.0.1', deviceId, 'ios');
|
|
105
105
|
if (!(await iosDriver.isAlive())) {
|
|
106
106
|
throw new Error(`iOS XCTest driver on port ${port} is not responding.\n` +
|
|
107
107
|
`Run: conductor daemon-start --device ${deviceId}`);
|
|
108
108
|
}
|
|
109
109
|
driver = iosDriver;
|
|
110
110
|
}
|
|
111
|
+
else if (platform === 'tvos') {
|
|
112
|
+
if (!(await (0, bootstrap_js_1.isPortOpen)(port))) {
|
|
113
|
+
(0, verbose_js_1.log)(`tvOS driver not running — starting daemon for ${deviceId}...`);
|
|
114
|
+
await (0, client_js_1.startDaemon)(deviceId);
|
|
115
|
+
await waitForPort(port);
|
|
116
|
+
}
|
|
117
|
+
const tvosDriver = new ios_js_1.IOSDriver(port, '127.0.0.1', deviceId, 'tvos');
|
|
118
|
+
if (!(await tvosDriver.isAlive())) {
|
|
119
|
+
throw new Error(`tvOS XCTest driver on port ${port} is not responding.\n` +
|
|
120
|
+
`Run: conductor daemon-start --device ${deviceId}`);
|
|
121
|
+
}
|
|
122
|
+
driver = tvosDriver;
|
|
123
|
+
}
|
|
111
124
|
else {
|
|
125
|
+
// Ensure the daemon is running — it handles APK install and driver startup.
|
|
126
|
+
await (0, client_js_1.startDaemon)(deviceId);
|
|
127
|
+
// Poll isAlive() directly instead of isPortOpen(): with ADB port forwarding the
|
|
128
|
+
// local port appears open as soon as `adb forward` runs, before the gRPC server
|
|
129
|
+
// on the device is ready, making isPortOpen() unreliable as a readiness signal.
|
|
130
|
+
const ANDROID_READY_TIMEOUT_MS = 180000;
|
|
131
|
+
const deadline = Date.now() + ANDROID_READY_TIMEOUT_MS;
|
|
132
|
+
let alive = false;
|
|
133
|
+
while (Date.now() < deadline) {
|
|
134
|
+
const probe = new android_js_1.AndroidDriver(deviceId, port);
|
|
135
|
+
await probe.connect();
|
|
136
|
+
alive = await probe.isAlive().catch(() => false);
|
|
137
|
+
probe.close();
|
|
138
|
+
if (alive)
|
|
139
|
+
break;
|
|
140
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
141
|
+
}
|
|
142
|
+
if (!alive) {
|
|
143
|
+
throw new Error(`Android gRPC driver on port ${port} is not responding.\n` +
|
|
144
|
+
`Make sure the Conductor driver APK is installed: conductor install --device ${deviceId}`);
|
|
145
|
+
}
|
|
112
146
|
const androidDriver = new android_js_1.AndroidDriver(deviceId, port);
|
|
113
147
|
await androidDriver.connect();
|
|
114
148
|
driver = androidDriver;
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>__xctestrun_metadata__</key>
|
|
6
|
+
<dict>
|
|
7
|
+
<key>ContainerInfo</key>
|
|
8
|
+
<dict>
|
|
9
|
+
<key>ContainerName</key>
|
|
10
|
+
<string>conductor-driver-ios</string>
|
|
11
|
+
<key>SchemeName</key>
|
|
12
|
+
<string>conductor-driver-tvos</string>
|
|
13
|
+
</dict>
|
|
14
|
+
<key>FormatVersion</key>
|
|
15
|
+
<integer>1</integer>
|
|
16
|
+
</dict>
|
|
17
|
+
<key>conductor-driver-tvosUITests</key>
|
|
18
|
+
<dict>
|
|
19
|
+
<key>BlueprintName</key>
|
|
20
|
+
<string>conductor-driver-tvosUITests</string>
|
|
21
|
+
<key>BlueprintProviderName</key>
|
|
22
|
+
<string>conductor-driver-ios</string>
|
|
23
|
+
<key>BlueprintProviderRelativePath</key>
|
|
24
|
+
<string>conductor-driver-ios.xcodeproj</string>
|
|
25
|
+
<key>BundleIdentifiersForCrashReportEmphasis</key>
|
|
26
|
+
<array>
|
|
27
|
+
<string>dev.houwert.ConductorDriverLib</string>
|
|
28
|
+
<string>dev.houwert.conductor-driver-tvos</string>
|
|
29
|
+
<string>dev.houwert.conductor-driver-tvosUITests</string>
|
|
30
|
+
</array>
|
|
31
|
+
<key>CommandLineArguments</key>
|
|
32
|
+
<array/>
|
|
33
|
+
<key>DefaultTestExecutionTimeAllowance</key>
|
|
34
|
+
<integer>600</integer>
|
|
35
|
+
<key>DependentProductPaths</key>
|
|
36
|
+
<array>
|
|
37
|
+
<string>__TESTROOT__/Debug-appletvsimulator/ConductorDriverLib.framework</string>
|
|
38
|
+
<string>__TESTROOT__/Debug-appletvsimulator/conductor-driver-tvos.app</string>
|
|
39
|
+
<string>__TESTROOT__/Debug-appletvsimulator/conductor-driver-tvosUITests-Runner.app</string>
|
|
40
|
+
<string>__TESTROOT__/Debug-appletvsimulator/conductor-driver-tvosUITests-Runner.app/PlugIns/conductor-driver-tvosUITests.xctest</string>
|
|
41
|
+
</array>
|
|
42
|
+
<key>DiagnosticCollectionPolicy</key>
|
|
43
|
+
<integer>1</integer>
|
|
44
|
+
<key>EnvironmentVariables</key>
|
|
45
|
+
<dict>
|
|
46
|
+
<key>APP_DISTRIBUTOR_ID_OVERRIDE</key>
|
|
47
|
+
<string>com.apple.AppStore</string>
|
|
48
|
+
<key>OS_ACTIVITY_DT_MODE</key>
|
|
49
|
+
<string>YES</string>
|
|
50
|
+
<key>SQLITE_ENABLE_THREAD_ASSERTIONS</key>
|
|
51
|
+
<string>1</string>
|
|
52
|
+
<key>TERM</key>
|
|
53
|
+
<string>dumb</string>
|
|
54
|
+
</dict>
|
|
55
|
+
<key>IsUITestBundle</key>
|
|
56
|
+
<true/>
|
|
57
|
+
<key>IsXCTRunnerHostedTestBundle</key>
|
|
58
|
+
<true/>
|
|
59
|
+
<key>PreferredScreenCaptureFormat</key>
|
|
60
|
+
<string>screenRecording</string>
|
|
61
|
+
<key>ProductModuleName</key>
|
|
62
|
+
<string>conductor_driver_tvosUITests</string>
|
|
63
|
+
<key>RunOrder</key>
|
|
64
|
+
<integer>0</integer>
|
|
65
|
+
<key>SystemAttachmentLifetime</key>
|
|
66
|
+
<string>deleteOnSuccess</string>
|
|
67
|
+
<key>TestBundlePath</key>
|
|
68
|
+
<string>__TESTHOST__/PlugIns/conductor-driver-tvosUITests.xctest</string>
|
|
69
|
+
<key>TestHostBundleIdentifier</key>
|
|
70
|
+
<string>dev.houwert.conductor-driver-tvosUITests.xctrunner</string>
|
|
71
|
+
<key>TestHostPath</key>
|
|
72
|
+
<string>__TESTROOT__/Debug-appletvsimulator/conductor-driver-tvosUITests-Runner.app</string>
|
|
73
|
+
<key>TestLanguage</key>
|
|
74
|
+
<string></string>
|
|
75
|
+
<key>TestRegion</key>
|
|
76
|
+
<string></string>
|
|
77
|
+
<key>TestTimeoutsEnabled</key>
|
|
78
|
+
<false/>
|
|
79
|
+
<key>TestingEnvironmentVariables</key>
|
|
80
|
+
<dict>
|
|
81
|
+
<key>DYLD_FRAMEWORK_PATH</key>
|
|
82
|
+
<string>__TESTROOT__/Debug-appletvsimulator:__TESTROOT__/Debug-appletvsimulator/PackageFrameworks:__PLATFORMS__/AppleTVSimulator.platform/Developer/Library/Frameworks</string>
|
|
83
|
+
<key>DYLD_LIBRARY_PATH</key>
|
|
84
|
+
<string>__TESTROOT__/Debug-appletvsimulator:__PLATFORMS__/AppleTVSimulator.platform/Developer/usr/lib</string>
|
|
85
|
+
<key>XCODE_SCHEME_NAME</key>
|
|
86
|
+
<string>conductor-driver-tvos</string>
|
|
87
|
+
<key>__XCODE_BUILT_PRODUCTS_DIR_PATHS</key>
|
|
88
|
+
<string>__TESTROOT__/Debug-appletvsimulator</string>
|
|
89
|
+
<key>__XPC_DYLD_FRAMEWORK_PATH</key>
|
|
90
|
+
<string>__TESTROOT__/Debug-appletvsimulator</string>
|
|
91
|
+
<key>__XPC_DYLD_LIBRARY_PATH</key>
|
|
92
|
+
<string>__TESTROOT__/Debug-appletvsimulator</string>
|
|
93
|
+
</dict>
|
|
94
|
+
<key>ToolchainsSettingValue</key>
|
|
95
|
+
<array/>
|
|
96
|
+
<key>UITargetAppCommandLineArguments</key>
|
|
97
|
+
<array/>
|
|
98
|
+
<key>UITargetAppEnvironmentVariables</key>
|
|
99
|
+
<dict>
|
|
100
|
+
<key>APP_DISTRIBUTOR_ID_OVERRIDE</key>
|
|
101
|
+
<string>com.apple.AppStore</string>
|
|
102
|
+
<key>DYLD_FRAMEWORK_PATH</key>
|
|
103
|
+
<string>__TESTROOT__/Debug-appletvsimulator:__TESTROOT__/Debug-appletvsimulator/PackageFrameworks</string>
|
|
104
|
+
<key>DYLD_LIBRARY_PATH</key>
|
|
105
|
+
<string>__TESTROOT__/Debug-appletvsimulator</string>
|
|
106
|
+
<key>XCODE_SCHEME_NAME</key>
|
|
107
|
+
<string>conductor-driver-tvos</string>
|
|
108
|
+
<key>__XCODE_BUILT_PRODUCTS_DIR_PATHS</key>
|
|
109
|
+
<string>__TESTROOT__/Debug-appletvsimulator</string>
|
|
110
|
+
<key>__XPC_DYLD_FRAMEWORK_PATH</key>
|
|
111
|
+
<string>__TESTROOT__/Debug-appletvsimulator</string>
|
|
112
|
+
<key>__XPC_DYLD_LIBRARY_PATH</key>
|
|
113
|
+
<string>__TESTROOT__/Debug-appletvsimulator</string>
|
|
114
|
+
</dict>
|
|
115
|
+
<key>UITargetAppPath</key>
|
|
116
|
+
<string>__TESTROOT__/Debug-appletvsimulator/conductor-driver-tvos.app</string>
|
|
117
|
+
<key>UserAttachmentLifetime</key>
|
|
118
|
+
<string>deleteOnSuccess</string>
|
|
119
|
+
</dict>
|
|
120
|
+
</dict>
|
|
121
|
+
</plist>
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: conductor
|
|
3
|
+
version: 0.2.0
|
|
4
|
+
description: "Token-efficient CLI for mobile UI testing (iOS simulator + Android emulator), designed for AI agents"
|
|
5
|
+
metadata.openclaw:
|
|
6
|
+
category: service
|
|
7
|
+
requires:
|
|
8
|
+
bins: [conductor]
|
|
9
|
+
---
|
|
10
|
+
|
|
1
11
|
# conductor
|
|
2
12
|
|
|
3
13
|
A token-efficient CLI for Conductor mobile UI testing, designed for AI agents.
|
|
@@ -10,6 +20,7 @@ conductor <command> [args] [options]
|
|
|
10
20
|
|
|
11
21
|
Global options:
|
|
12
22
|
- `--device <id>` — target device ID; also keys the session and daemon (auto-detected if omitted)
|
|
23
|
+
- `--device-name <name>` — target a booted device by its human-readable name (resolved to ID from booted devices); mutually exclusive with `--device`
|
|
13
24
|
- `--help` — show help
|
|
14
25
|
- `--json` — machine-readable JSON output (avoid unless you need structured parsing)
|
|
15
26
|
- `--verbose / -v` — log daemon calls, driver fallbacks, and raw output
|
|
@@ -20,7 +31,7 @@ Global options:
|
|
|
20
31
|
|
|
21
32
|
### `list-devices`
|
|
22
33
|
|
|
23
|
-
List connected Android emulators and iOS simulators.
|
|
34
|
+
List connected Android emulators and iOS simulators. Shows both booted (running) devices and available (shutdown) simulators/emulators that can be started with `start-device`.
|
|
24
35
|
|
|
25
36
|
```bash
|
|
26
37
|
conductor list-devices
|
|
@@ -28,10 +39,17 @@ conductor list-devices
|
|
|
28
39
|
|
|
29
40
|
Output:
|
|
30
41
|
```
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
Booted devices:
|
|
43
|
+
android device emulator-5554 Pixel_6_API_33
|
|
44
|
+
ios booted ABC123-DEF456 iPhone 15
|
|
45
|
+
|
|
46
|
+
Available devices:
|
|
47
|
+
ios shutdown DEF789-ABC012 iPhone 16 Pro
|
|
48
|
+
android available Pixel_7_API_34 Pixel_7_API_34
|
|
33
49
|
```
|
|
34
50
|
|
|
51
|
+
Exits with code 0 if any booted or available devices exist, 1 only if both lists are empty.
|
|
52
|
+
|
|
35
53
|
---
|
|
36
54
|
|
|
37
55
|
### `foreground-app`
|
|
@@ -56,6 +74,22 @@ conductor list-apps --device emulator-5554
|
|
|
56
74
|
|
|
57
75
|
---
|
|
58
76
|
|
|
77
|
+
### `copy-app <bundleId>`
|
|
78
|
+
|
|
79
|
+
Copy an installed app from one iOS simulator to another. Useful when you have a compiled `.app` bundle on one simulator and want to install it on others without recompiling.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
conductor copy-app com.example.myapp --from C59D3241-FB6A-4E3B-AE7B-A82D3C933889 --to 86B1BC33-7D83-47FF-AAFE-1BD70FC53038
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Flags:
|
|
86
|
+
- `--from <id>` — source device ID (must be an iOS simulator)
|
|
87
|
+
- `--to <id>` — target device ID (must be an iOS simulator)
|
|
88
|
+
|
|
89
|
+
Both flags are required. The command reads the `.app` bundle path from the source simulator and installs it on the target.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
59
93
|
### `launch-app <appId>`
|
|
60
94
|
|
|
61
95
|
Launch an app by bundle ID and save it to the session.
|
|
@@ -159,9 +193,9 @@ conductor back
|
|
|
159
193
|
|
|
160
194
|
### `press-key <key>`
|
|
161
195
|
|
|
162
|
-
Press a hardware or keyboard key. Valid keys: `Enter`, `Backspace`, `Home`, `End`, `Tab`, `Delete`, `Escape`, `VolumeUp`, `VolumeDown`, `Power`, `Lock`, `Back`, `Camera`, `Search`.
|
|
196
|
+
Press a hardware or keyboard key. Valid keys: `Enter`, `Backspace`, `Home`, `End`, `Tab`, `Delete`, `Escape`, `VolumeUp`, `VolumeDown`, `Power`, `Lock`, `Back`, `Camera`, `Search`, `Remote Dpad Up`, `Remote Dpad Down`, `Remote Dpad Left`, `Remote Dpad Right`, `Remote Dpad Center`.
|
|
163
197
|
|
|
164
|
-
Not all keys are supported on all platforms — unsupported keys are silently ignored (e.g. `Back` on iOS).
|
|
198
|
+
Not all keys are supported on all platforms — unsupported keys are silently ignored (e.g. `Back` on iOS). The `Remote Dpad *` keys are Android TV only.
|
|
165
199
|
|
|
166
200
|
```bash
|
|
167
201
|
conductor press-key Enter
|
|
@@ -659,6 +693,8 @@ Boot an iOS simulator or Android emulator. Creates the Simulator window and wait
|
|
|
659
693
|
conductor start-device --platform ios
|
|
660
694
|
conductor start-device --platform android
|
|
661
695
|
conductor start-device --platform ios --os-version 18
|
|
696
|
+
conductor start-device --platform ios --device-type "iPhone 16 Pro"
|
|
697
|
+
conductor start-device --platform ios --name "Test Device"
|
|
662
698
|
conductor start-device --platform android --avd Pixel_6_API_33
|
|
663
699
|
```
|
|
664
700
|
|
|
@@ -666,8 +702,10 @@ Flags:
|
|
|
666
702
|
- `--platform <ios|android>` — required
|
|
667
703
|
- `--os-version <n>` — filter by OS version (iOS: e.g. `18`; Android: API level e.g. `33`)
|
|
668
704
|
- `--avd <name>` — Android AVD name to launch (default: first available AVD)
|
|
705
|
+
- `--name <name>` — set a custom name on the simulator after boot (iOS only)
|
|
706
|
+
- `--device-type <name>` — iOS device type to boot (e.g. `"iPhone 16 Pro"`); if no existing simulator matches, one is created automatically
|
|
669
707
|
|
|
670
|
-
iOS picks the first available iPhone simulator matching the OS version
|
|
708
|
+
iOS picks the first available iPhone simulator matching the OS version and device type filters (or any if unfiltered). Android launches the named AVD, or the first AVD found.
|
|
671
709
|
|
|
672
710
|
---
|
|
673
711
|
|
|
@@ -51,6 +51,7 @@ appId: com.example.myapp
|
|
|
51
51
|
- pressKey: Enter
|
|
52
52
|
- pressKey: Backspace
|
|
53
53
|
- pressKey: Home
|
|
54
|
+
- pressKey: Remote Dpad Down # Android TV only; also Up, Left, Right, Center, Remote Media Play Pause, Remote Media Stop, Remote Media Next, Remote Media Previous, Remote Media Rewind, Remote Media Fast Forward, Remote System Navigation Up, Remote System Navigation Down, Remote Button A, Remote Button B, Remote Menu, TV Input, TV Input HDMI 1/2/3
|
|
54
55
|
- pasteText: "pasted content" # paste text into focused field
|
|
55
56
|
- copyTextFrom: "Element" # copy element's text to output.textContent
|
|
56
57
|
```
|