@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.
Files changed (53) hide show
  1. package/.claude-plugin/plugin.json +4 -1
  2. package/README.md +121 -21
  3. package/dist/commands/assert-not-visible.js +5 -0
  4. package/dist/commands/assert-visible.js +15 -0
  5. package/dist/commands/back.js +6 -1
  6. package/dist/commands/cheat-sheet.js +2 -0
  7. package/dist/commands/copy-app.js +55 -0
  8. package/dist/commands/daemon.js +4 -0
  9. package/dist/commands/device-pool.js +4 -0
  10. package/dist/commands/erase-text.js +2 -0
  11. package/dist/commands/focused.js +233 -0
  12. package/dist/commands/foreground-app.js +2 -0
  13. package/dist/commands/hide-keyboard.js +2 -0
  14. package/dist/commands/inspect.js +23 -1
  15. package/dist/commands/install.js +69 -4
  16. package/dist/commands/launch-app.js +6 -0
  17. package/dist/commands/list-apps.js +2 -0
  18. package/dist/commands/list-devices.js +70 -7
  19. package/dist/commands/open-link.js +2 -0
  20. package/dist/commands/press-key.js +73 -7
  21. package/dist/commands/run-flow-inline.js +3 -0
  22. package/dist/commands/run-flow.js +4 -0
  23. package/dist/commands/run-parallel.js +2 -0
  24. package/dist/commands/screenshot.js +2 -0
  25. package/dist/commands/scroll-until-visible.js +6 -0
  26. package/dist/commands/scroll.js +6 -0
  27. package/dist/commands/session.js +2 -0
  28. package/dist/commands/set-location.js +2 -0
  29. package/dist/commands/set-orientation.js +2 -0
  30. package/dist/commands/start-device.js +311 -9
  31. package/dist/commands/stop-app.js +2 -0
  32. package/dist/commands/swipe.js +10 -0
  33. package/dist/commands/tap.js +20 -0
  34. package/dist/commands/type.js +2 -0
  35. package/dist/daemon/server.js +64 -22
  36. package/dist/device-picker.js +65 -0
  37. package/dist/drivers/android.js +14 -13
  38. package/dist/drivers/bootstrap.js +179 -6
  39. package/dist/drivers/element-resolver.js +10 -0
  40. package/dist/drivers/flow-runner.js +6 -5
  41. package/dist/drivers/ios.js +2 -1
  42. package/dist/index.js +120 -97
  43. package/dist/postinstall.js +2 -2
  44. package/dist/runner.js +40 -6
  45. package/drivers/ios/conductor-driver-ios.zip +0 -0
  46. package/drivers/ios/conductor-driver-iosUITests-Runner.zip +0 -0
  47. package/drivers/tvos/conductor-driver-tvos-config.xctestrun +121 -0
  48. package/drivers/tvos/conductor-driver-tvos.zip +0 -0
  49. package/drivers/tvos/conductor-driver-tvosUITests-Runner.zip +0 -0
  50. package/package.json +1 -1
  51. package/skills/conductor/SKILL.md +44 -6
  52. package/skills/conductor/references/flow-syntax.md +1 -0
  53. 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 runner_js_1 = require("./runner.js");
41
+ const device_picker_js_1 = require("./device-picker.js");
40
42
  const update_check_js_1 = require("./update-check.js");
41
- const HELP = `
42
- Usage: conductor <command> [args] [options]
43
-
44
- Commands:
45
- start-device --platform <ios|android> Boot a simulator or emulator
46
- --os-version <n> iOS version (e.g. 18) or Android API level (e.g. 33)
47
- --avd <name> Android AVD name (default: first available)
48
- list-devices List connected devices/simulators
49
- foreground-app Print bundle ID / package of the foreground app
50
- list-apps List installed app IDs / package names
51
- launch-app <appId> Launch app (saves to session)
52
- --clear-state Clear app data/state before launching
53
- --clear-keychain Clear keychain before launching
54
- --no-stop-app Do not stop the app before launching (resume instead of restart)
55
- --argument key=value Set launch argument (repeatable)
56
- stop-app [<appId>] Stop app
57
- tap <element> Tap element by text or id
58
- --id <id> Match by accessibility id instead of text
59
- --text <text> Match by text only (not id)
60
- --index <n> Pick the nth match (0-based)
61
- --long-press Hold instead of tap
62
- --double-tap Double-tap the element
63
- --optional Do not fail if element is not found
64
- --focused Match only focused elements
65
- --enabled / --no-enabled Match by enabled state
66
- --checked / --no-checked Match by checked state
67
- --selected / --no-selected Match by selected state
68
- --below <text> Match element below the given reference
69
- --above <text> Match element above the given reference
70
- --left-of <text> Match element left of the given reference
71
- --right-of <text> Match element right of the given reference
72
- --verbose Log all candidates and chosen element
73
- type <text> Type text into focused field
74
- erase-text [n] Erase n characters (default: 50)
75
- back Press back button
76
- press-key <key> Press a key (Enter, Backspace, Home, ...)
77
- hide-keyboard Dismiss the on-screen keyboard
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
- `.trim();
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
- // The device ID is the natural key for both the session file and the daemon.
182
- // Use --device if given, otherwise detect the first booted device, otherwise 'default'.
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
- console.log(HELP);
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;
@@ -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('✓ Conductor Claude Code plugin installed\n');
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
- const iosDriver = new ios_js_1.IOSDriver(port, '127.0.0.1', deviceId);
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;
@@ -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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@houwert/conductor",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI tool for mobile app interactions — optimized for AI agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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
- android device emulator-5554 Pixel_6_API_33
32
- ios booted ABC123-DEF456 iPhone 15
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 filter (or any if unfiltered). Android launches the named AVD, or the first AVD found.
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
  ```
@@ -0,0 +1,8 @@
1
+ skills:
2
+ - name: conductor
3
+ path: conductor/SKILL.md
4
+ description: "Token-efficient CLI for mobile UI testing, designed for AI agents"
5
+ category: service
6
+ version: 0.2.0
7
+ requires:
8
+ bins: [conductor]