@houwert/conductor 0.2.0 → 0.4.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 (55) 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 +4 -0
  7. package/dist/commands/clear-state.js +27 -0
  8. package/dist/commands/copy-app.js +55 -0
  9. package/dist/commands/daemon.js +4 -0
  10. package/dist/commands/device-pool.js +4 -0
  11. package/dist/commands/erase-text.js +2 -0
  12. package/dist/commands/focused.js +233 -0
  13. package/dist/commands/foreground-app.js +2 -0
  14. package/dist/commands/hide-keyboard.js +2 -0
  15. package/dist/commands/inspect.js +23 -1
  16. package/dist/commands/install.js +69 -4
  17. package/dist/commands/launch-app.js +6 -0
  18. package/dist/commands/list-apps.js +3 -1
  19. package/dist/commands/list-devices.js +70 -7
  20. package/dist/commands/open-link.js +2 -0
  21. package/dist/commands/press-key.js +73 -7
  22. package/dist/commands/run-flow-inline.js +3 -0
  23. package/dist/commands/run-flow.js +4 -0
  24. package/dist/commands/run-parallel.js +2 -0
  25. package/dist/commands/screenshot.js +2 -0
  26. package/dist/commands/scroll-until-visible.js +6 -0
  27. package/dist/commands/scroll.js +6 -0
  28. package/dist/commands/session.js +2 -0
  29. package/dist/commands/set-location.js +2 -0
  30. package/dist/commands/set-orientation.js +2 -0
  31. package/dist/commands/start-device.js +311 -9
  32. package/dist/commands/stop-app.js +2 -0
  33. package/dist/commands/swipe.js +10 -0
  34. package/dist/commands/tap.js +20 -0
  35. package/dist/commands/type.js +2 -0
  36. package/dist/commands/uninstall-app.js +31 -0
  37. package/dist/daemon/server.js +64 -22
  38. package/dist/device-picker.js +65 -0
  39. package/dist/drivers/android.js +18 -13
  40. package/dist/drivers/bootstrap.js +179 -6
  41. package/dist/drivers/element-resolver.js +10 -0
  42. package/dist/drivers/flow-runner.js +21 -5
  43. package/dist/drivers/ios.js +7 -1
  44. package/dist/index.js +134 -97
  45. package/dist/postinstall.js +2 -2
  46. package/dist/runner.js +40 -6
  47. package/drivers/ios/conductor-driver-ios.zip +0 -0
  48. package/drivers/ios/conductor-driver-iosUITests-Runner.zip +0 -0
  49. package/drivers/tvos/conductor-driver-tvos-config.xctestrun +121 -0
  50. package/drivers/tvos/conductor-driver-tvos.zip +0 -0
  51. package/drivers/tvos/conductor-driver-tvosUITests-Runner.zip +0 -0
  52. package/package.json +1 -1
  53. package/skills/conductor/SKILL.md +66 -6
  54. package/skills/conductor/references/flow-syntax.md +3 -0
  55. package/skills/skills.yaml +8 -0
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "conductor",
3
+ "version": "0.2.0",
3
4
  "description": "CLI for mobile UI testing, designed for AI agents",
4
5
  "repository": "https://github.com/houwert/conductor",
5
- "author": { "name": "Houwert" }
6
+ "author": { "name": "Houwert" },
7
+ "category": "service",
8
+ "skills": ["conductor"]
6
9
  }
package/README.md CHANGED
@@ -1,39 +1,139 @@
1
- # Conductor
1
+ <div align="center">
2
2
 
3
- A token-efficient CLI for mobile UI testing, designed for AI agents.
3
+ <img src="assets/banner.png" alt="Conductor" width="800" />
4
4
 
5
- Inspired by [`@playwright/cli`](https://github.com/microsoft/playwright-cli), this is a TypeScript reimplementation and partial fork of [Maestro](https://github.com/mobile-dev-inc/maestro) that talks directly to the bundled native drivers — no external CLI installation required.
5
+ # 🎼 Conductor
6
6
 
7
- ## Installation
7
+ **Give Claude hands. Let it drive your app.**
8
8
 
9
- ```bash
10
- pnpm install
11
- pnpm build
12
- pnpm link --global
13
- ```
9
+ [![CI](https://github.com/DouweBos/conductor/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/DouweBos/conductor/actions/workflows/ci.yml)
10
+ [![npm version](https://img.shields.io/npm/v/@houwert/conductor)](https://www.npmjs.com/package/@houwert/conductor)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
12
+
13
+ </div>
14
+
15
+ ---
16
+
17
+ Conductor is a token-efficient CLI for mobile UI interactions, built for AI agents. It's a TypeScript reimplementation and partial fork of [Maestro](https://maestro.mobile.dev) that bundles its own native drivers — no external CLI, no setup friction, no nonsense.
18
+
19
+ It started as an experiment: what if Claude could tap through your app while it's writing the code? Turns out that's extremely useful. Now it's a proper tool.
14
20
 
15
- ## Quick Start
21
+ ## What it does
22
+
23
+ Conductor gives Claude Code the ability to interact with iOS simulators and Android emulators directly from a coding session. It can navigate UI, inspect the live hierarchy, take screenshots, run flows, and manage multiple devices in parallel across concurrent agents.
16
24
 
17
25
  ```bash
18
- conductor list-devices
19
26
  conductor launch-app com.example.myapp
20
27
  conductor tap "Sign In"
21
28
  conductor type "user@example.com"
29
+ conductor assert-visible "Dashboard"
22
30
  conductor screenshot --output /tmp/screen.png
23
31
  ```
24
32
 
25
- ## Documentation
33
+ One agent writes the feature. Another taps through the app. They talk. It works. 🤝
34
+
35
+ ## 🚀 Quick start
36
+
37
+ ```bash
38
+ npm install -g @houwert/conductor
39
+ ```
40
+
41
+ That's it. The postinstall script registers Conductor as a Claude Code plugin automatically — Claude gains full mobile UI control without any extra steps.
42
+
43
+ ## 🧠 Claude Skills
44
+
45
+ The plugin registers itself globally in `~/.claude/plugins/` and ships two skill files:
46
+
47
+ ```
48
+ skills/conductor/
49
+ ├── SKILL.md # Full command reference and agent workflow guide
50
+ └── references/
51
+ └── flow-syntax.md # Maestro YAML flow syntax reference
52
+ ```
53
+
54
+ Claude learns every available command, how to coordinate across devices, and how to write and run Maestro YAML flows. See [`skills/conductor/SKILL.md`](./skills/conductor/SKILL.md) for the full reference.
55
+
56
+ ### Install modes
57
+
58
+ | Command | What it does |
59
+ |---|---|
60
+ | `npm install -g @houwert/conductor` | Registers global plugin automatically (via postinstall) |
61
+ | `conductor install` | Re-register or update the global plugin |
62
+ | `conductor install --skills` | Copy skills into `.claude/skills/conductor/` in the current project |
63
+ | `conductor install --check` | Print current install status without modifying anything |
64
+
65
+ ### 📱 What Claude can do
66
+
67
+ | Capability | Commands |
68
+ |---|---|
69
+ | App lifecycle | `launch-app`, `stop-app`, `foreground-app`, `copy-app` |
70
+ | Interaction | `tap`, `type`, `scroll`, `scroll-until-visible`, `swipe`, `press-key`, `erase-text`, `hide-keyboard` |
71
+ | Inspection | `inspect`, `focused`, `screenshot`, `list-apps` |
72
+ | Assertions | `assert-visible`, `assert-not-visible` |
73
+ | Navigation | `open-link`, `back` |
74
+ | Flows | `run-flow`, `run-flow-inline`, `run-parallel` |
75
+ | Devices | `start-device`, `list-devices`, `set-location`, `set-orientation` |
76
+
77
+ ## 🔨 Building locally
78
+
79
+ ### Prerequisites
26
80
 
27
- See [`skills/conductor/SKILL.md`](./skills/conductor/SKILL.md) for full command reference.
81
+ - Node.js + pnpm v9
82
+ - **iOS:** Xcode with command-line tools
83
+ - **Android:** Android SDK with `adb` on `PATH`
28
84
 
29
- ## Requirements
85
+ ### Full build
30
86
 
31
- - Android: `adb` on `PATH` with a running emulator/device
32
- - iOS: Xcode with a running simulator
87
+ ```bash
88
+ make build
89
+ ```
90
+
91
+ Builds the iOS driver (xcodebuild), the Android driver (Gradle), packages both into the CLI, and compiles TypeScript. Then link it globally:
92
+
93
+ ```bash
94
+ cd packages/cli && pnpm link --global
95
+ ```
96
+
97
+ ### CLI only
98
+
99
+ If the drivers are already built and packaged:
100
+
101
+ ```bash
102
+ cd packages/cli
103
+ pnpm install && pnpm build
104
+ ```
105
+
106
+ ### Individual targets
107
+
108
+ ```bash
109
+ make build-cli # CLI TypeScript only
110
+ make build-ios-driver # iOS XCTest driver
111
+ make build-android-driver # Android instrumentation driver
112
+ make package-cli # Bundle drivers into CLI package
113
+ make copy-skills # Copy skills/ into packages/cli/skills/ (build artifact)
114
+ ```
115
+
116
+ ## 🗂️ Repository structure
117
+
118
+ ```
119
+ conductor/
120
+ ├── packages/
121
+ │ ├── cli/ # TypeScript CLI (@houwert/conductor)
122
+ │ ├── android-driver/ # Kotlin/Gradle instrumentation driver
123
+ │ └── ios-driver/ # Swift/Xcode XCTest driver
124
+ └── Makefile
125
+ ```
126
+
127
+ ## 🛠️ Development
128
+
129
+ ```bash
130
+ pnpm dev # TypeScript watch mode
131
+ pnpm lint # ESLint + Prettier check
132
+ pnpm lint:fix # Auto-fix formatting
133
+ pnpm test # Run test suite
134
+ ```
33
135
 
34
- ## Architecture
136
+ ## 📋 Requirements
35
137
 
36
- - `src/session.ts` persists `appId` + `deviceId` in `~/.conductor/session.json`
37
- - `src/runner.ts` resolves devices, manages driver lifecycle, executes flows natively
38
- - `src/commands/` — one file per command
39
- - `src/index.ts` — argument parsing with `minimist`, command dispatch
138
+ - **iOS:** Xcode with a booted simulator
139
+ - **Android:** `adb` on `PATH` with a running emulator or connected device
@@ -1,6 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP = void 0;
3
4
  exports.assertNotVisible = assertNotVisible;
5
+ exports.HELP = ` assert-not-visible <element> Assert element is absent from screen
6
+ --id <id> Match by accessibility id instead of text
7
+ --text <text> Match by text only (not id)
8
+ --timeout <ms> Max check time in milliseconds (default: 1000)`;
4
9
  const runner_js_1 = require("../runner.js");
5
10
  const output_js_1 = require("../output.js");
6
11
  const ios_js_1 = require("../drivers/ios.js");
@@ -1,6 +1,21 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP = void 0;
3
4
  exports.assertVisible = assertVisible;
5
+ exports.HELP = ` assert-visible <element> Assert element is visible
6
+ --id <id> Match by accessibility id instead of text
7
+ --text <text> Match by text only (not id)
8
+ --index <n> Pick the nth match (0-based)
9
+ --timeout <ms> Max wait time in milliseconds
10
+ --optional Do not fail if element is not found
11
+ --focused Match only focused elements
12
+ --enabled / --no-enabled Match by enabled state
13
+ --checked / --no-checked Match by checked state
14
+ --selected / --no-selected Match by selected state
15
+ --below <text> Match element below the given reference
16
+ --above <text> Match element above the given reference
17
+ --left-of <text> Match element left of the given reference
18
+ --right-of <text> Match element right of the given reference`;
4
19
  const runner_js_1 = require("../runner.js");
5
20
  const output_js_1 = require("../output.js");
6
21
  const ios_js_1 = require("../drivers/ios.js");
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP = void 0;
3
4
  exports.back = back;
5
+ exports.HELP = ` back Press back button`;
4
6
  const runner_js_1 = require("../runner.js");
5
7
  const output_js_1 = require("../output.js");
6
8
  const ios_js_1 = require("../drivers/ios.js");
@@ -8,7 +10,10 @@ const android_js_1 = require("../drivers/android.js");
8
10
  async function back(opts = {}, sessionName = 'default') {
9
11
  const result = await (0, runner_js_1.runDirect)(async (driver) => {
10
12
  if (driver instanceof ios_js_1.IOSDriver) {
11
- // iOS has no universal "back" concept — this is a no-op (same as maestro IOSDriver.backPress)
13
+ if (driver.platform === 'tvos') {
14
+ await driver.pressButton('menu');
15
+ }
16
+ // Plain iOS has no universal "back" concept — this is a no-op (same as maestro IOSDriver.backPress)
12
17
  }
13
18
  else if (driver instanceof android_js_1.AndroidDriver) {
14
19
  await driver.back(); // adb shell input keyevent 4
@@ -3,7 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HELP = void 0;
6
7
  exports.cheatSheet = cheatSheet;
8
+ exports.HELP = ` cheat-sheet Print command reference`;
7
9
  const promises_1 = __importDefault(require("fs/promises"));
8
10
  const path_1 = __importDefault(require("path"));
9
11
  async function cheatSheet() {
@@ -37,6 +39,8 @@ APP CONTROL
37
39
  --clear-keychain Wipe keychain before launching
38
40
  --argument key=value Set launch argument (repeatable)
39
41
  stop-app [<appId>] Stop app (uses session appId if omitted)
42
+ clear-state [<appId>] Clear app data/state (uses session appId if omitted)
43
+ uninstall-app <appId> Uninstall app from device
40
44
 
41
45
  INTERACTIONS
42
46
  tap <element> Tap element by text
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP = void 0;
4
+ exports.clearState = clearState;
5
+ exports.HELP = ` clear-state [<appId>] Clear app data/state`;
6
+ const runner_js_1 = require("../runner.js");
7
+ const session_js_1 = require("../session.js");
8
+ const output_js_1 = require("../output.js");
9
+ async function clearState(appId, opts = {}, sessionName = 'default') {
10
+ const session = await (0, session_js_1.getSession)(sessionName);
11
+ const resolvedAppId = appId ?? session.appId;
12
+ if (!resolvedAppId) {
13
+ (0, output_js_1.printError)('clear-state: no appId provided and no active session. Run launch-app first.', opts);
14
+ return 1;
15
+ }
16
+ const result = await (0, runner_js_1.runDirect)(async (driver) => {
17
+ await driver.clearAppState(resolvedAppId);
18
+ }, sessionName);
19
+ if (result.success) {
20
+ (0, output_js_1.printSuccess)(`clear-state "${resolvedAppId}" — done`, opts);
21
+ return 0;
22
+ }
23
+ else {
24
+ (0, output_js_1.printError)(`clear-state "${resolvedAppId}" — failed\n${result.stderr}`, opts);
25
+ return 1;
26
+ }
27
+ }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP = void 0;
4
+ exports.copyApp = copyApp;
5
+ exports.HELP = ` copy-app <bundleId> Copy an installed app between iOS simulators
6
+ --from <id> Source device ID
7
+ --to <id> Target device ID`;
8
+ const runner_js_1 = require("../runner.js");
9
+ const output_js_1 = require("../output.js");
10
+ const bootstrap_js_1 = require("../drivers/bootstrap.js");
11
+ async function copyApp(bundleId, from, to, opts = {}) {
12
+ if (!bundleId) {
13
+ (0, output_js_1.printError)('Usage: conductor copy-app <bundleId> --from <id> --to <id>', opts);
14
+ return 1;
15
+ }
16
+ if (!from || !to) {
17
+ (0, output_js_1.printError)('Both --from and --to are required.', opts);
18
+ return 1;
19
+ }
20
+ const [fromPlatform, toPlatform] = await Promise.all([(0, bootstrap_js_1.detectPlatform)(from), (0, bootstrap_js_1.detectPlatform)(to)]);
21
+ if (fromPlatform !== 'ios') {
22
+ (0, output_js_1.printError)(`--from device "${from}" is not an iOS simulator. copy-app only supports iOS.`, opts);
23
+ return 1;
24
+ }
25
+ if (toPlatform !== 'ios') {
26
+ (0, output_js_1.printError)(`--to device "${to}" is not an iOS simulator. copy-app only supports iOS.`, opts);
27
+ return 1;
28
+ }
29
+ // Get the .app bundle path from the source simulator
30
+ const getPath = await (0, runner_js_1.spawnCommand)('xcrun', [
31
+ 'simctl',
32
+ 'get_app_container',
33
+ from,
34
+ bundleId,
35
+ 'app',
36
+ ]);
37
+ if (!getPath.success) {
38
+ (0, output_js_1.printError)(`Failed to get app path from source device: ${getPath.stderr}`, opts);
39
+ return 1;
40
+ }
41
+ const appPath = getPath.stdout.trim();
42
+ // Install on target simulator
43
+ const install = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'install', to, appPath]);
44
+ if (!install.success) {
45
+ (0, output_js_1.printError)(`Failed to install app on target device: ${install.stderr}`, opts);
46
+ return 1;
47
+ }
48
+ if (opts.json) {
49
+ (0, output_js_1.printData)({ status: 'ok', bundleId, from, to, appPath }, opts);
50
+ }
51
+ else {
52
+ console.log(`Copied ${bundleId} from ${from} to ${to}`);
53
+ }
54
+ return 0;
55
+ }
@@ -1,8 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP_DAEMON_STATUS = exports.HELP_DAEMON_STOP = exports.HELP_DAEMON_START = void 0;
3
4
  exports.daemonStart = daemonStart;
4
5
  exports.daemonStop = daemonStop;
5
6
  exports.daemonStatusCmd = daemonStatusCmd;
7
+ exports.HELP_DAEMON_START = ` daemon-start Start background daemon (manages driver process)`;
8
+ exports.HELP_DAEMON_STOP = ` daemon-stop [--all] Stop background daemon (--all stops every session's daemon)`;
9
+ exports.HELP_DAEMON_STATUS = ` daemon-status Show daemon status`;
6
10
  const client_js_1 = require("../daemon/client.js");
7
11
  const output_js_1 = require("../output.js");
8
12
  async function daemonStart(opts = {}, sessionName = 'default') {
@@ -3,7 +3,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HELP = void 0;
6
7
  exports.devicePool = devicePool;
8
+ exports.HELP = ` device-pool --list List all devices and pool status
9
+ device-pool --acquire Claim a free device (prints device ID)
10
+ device-pool --release <id> Release a device back to the pool`;
7
11
  /**
8
12
  * device-pool: Manage a pool of available devices for concurrent multi-agent use.
9
13
  *
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP = void 0;
3
4
  exports.eraseText = eraseText;
5
+ exports.HELP = ` erase-text [n] Erase n characters (default: 50)`;
4
6
  const runner_js_1 = require("../runner.js");
5
7
  const output_js_1 = require("../output.js");
6
8
  const ios_js_1 = require("../drivers/ios.js");
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP = void 0;
4
+ exports.focused = focused;
5
+ exports.HELP = ` focused [--poll [interval_ms]] Print metadata of the currently focused element
6
+ --poll continuously watches for focus changes (default 500ms)`;
7
+ const runner_js_1 = require("../runner.js");
8
+ const output_js_1 = require("../output.js");
9
+ const ios_js_1 = require("../drivers/ios.js");
10
+ const android_js_1 = require("../drivers/android.js");
11
+ const element_resolver_js_1 = require("../drivers/element-resolver.js");
12
+ // XCUIElementType rawValue → human-readable name.
13
+ // Source: XCUIElementType enum in XCTest framework.
14
+ const IOS_ELEMENT_TYPE_NAMES = {
15
+ 0: 'Any',
16
+ 1: 'Other',
17
+ 2: 'Application',
18
+ 3: 'Group',
19
+ 4: 'Window',
20
+ 5: 'Sheet',
21
+ 6: 'Drawer',
22
+ 7: 'Alert',
23
+ 8: 'Dialog',
24
+ 9: 'Button',
25
+ 10: 'RadioButton',
26
+ 11: 'RadioGroup',
27
+ 12: 'CheckBox',
28
+ 13: 'DisclosureTriangle',
29
+ 14: 'PopUpButton',
30
+ 15: 'ComboBox',
31
+ 16: 'MenuButton',
32
+ 17: 'ToolbarButton',
33
+ 18: 'Popover',
34
+ 19: 'Keyboard',
35
+ 20: 'Key',
36
+ 21: 'NavigationBar',
37
+ 22: 'TabBar',
38
+ 23: 'TabGroup',
39
+ 24: 'Toolbar',
40
+ 25: 'StatusBar',
41
+ 26: 'Table',
42
+ 27: 'TableRow',
43
+ 28: 'TableColumn',
44
+ 29: 'Outline',
45
+ 30: 'OutlineRow',
46
+ 31: 'Browser',
47
+ 32: 'CollectionView',
48
+ 33: 'Slider',
49
+ 34: 'PageIndicator',
50
+ 35: 'ProgressIndicator',
51
+ 36: 'ActivityIndicator',
52
+ 37: 'SegmentedControl',
53
+ 38: 'Picker',
54
+ 39: 'PickerWheel',
55
+ 40: 'Switch',
56
+ 41: 'Toggle',
57
+ 42: 'Link',
58
+ 43: 'Image',
59
+ 44: 'Icon',
60
+ 45: 'SearchField',
61
+ 46: 'ScrollView',
62
+ 47: 'ScrollBar',
63
+ 48: 'StaticText',
64
+ 49: 'TextField',
65
+ 50: 'SecureTextField',
66
+ 51: 'DatePicker',
67
+ 52: 'TextView',
68
+ 53: 'Menu',
69
+ 54: 'MenuItem',
70
+ 55: 'MenuBar',
71
+ 56: 'MenuBarItem',
72
+ 57: 'Map',
73
+ 58: 'WebView',
74
+ 59: 'IncrementArrow',
75
+ 60: 'DecrementArrow',
76
+ 61: 'Timeline',
77
+ 62: 'RatingIndicator',
78
+ 63: 'ValueIndicator',
79
+ 64: 'SplitGroup',
80
+ 65: 'Splitter',
81
+ 66: 'RelevanceIndicator',
82
+ 67: 'ColorWell',
83
+ 68: 'HelpTag',
84
+ 69: 'Matte',
85
+ 70: 'DockItem',
86
+ 71: 'Ruler',
87
+ 72: 'RulerMarker',
88
+ 73: 'Grid',
89
+ 74: 'LevelIndicator',
90
+ 75: 'Cell',
91
+ 76: 'LayoutArea',
92
+ 77: 'LayoutItem',
93
+ 78: 'Handle',
94
+ 79: 'Stepper',
95
+ 80: 'Tab',
96
+ 81: 'TouchBar',
97
+ 82: 'StatusItem',
98
+ };
99
+ /** DFS children-first so the deepest focused node wins (matches deepestMatchingIOSElements pattern). */
100
+ function findFocusedIOS(node) {
101
+ for (const child of node.children ?? []) {
102
+ const found = findFocusedIOS(child);
103
+ if (found)
104
+ return found;
105
+ }
106
+ if (node.hasFocus)
107
+ return node;
108
+ return null;
109
+ }
110
+ function formatIOSElement(node) {
111
+ const { X, Y, Width, Height } = node.frame;
112
+ return {
113
+ text: node.label || node.title || node.value || node.placeholderValue || '',
114
+ identifier: node.identifier || '',
115
+ label: node.label,
116
+ title: node.title ?? null,
117
+ value: node.value ?? null,
118
+ placeholderValue: node.placeholderValue ?? null,
119
+ elementType: node.elementType,
120
+ elementTypeName: IOS_ELEMENT_TYPE_NAMES[node.elementType] ?? `Unknown(${node.elementType})`,
121
+ enabled: node.enabled,
122
+ selected: node.selected,
123
+ hasFocus: node.hasFocus,
124
+ bounds: {
125
+ x: Math.round(X),
126
+ y: Math.round(Y),
127
+ width: Math.round(Width),
128
+ height: Math.round(Height),
129
+ },
130
+ center: {
131
+ x: Math.round(X + Width / 2),
132
+ y: Math.round(Y + Height / 2),
133
+ },
134
+ };
135
+ }
136
+ function formatAndroidNode(node) {
137
+ const { x1, y1, x2, y2 } = node.bounds;
138
+ return {
139
+ text: node.text,
140
+ resourceId: node.resourceId,
141
+ contentDesc: node.contentDesc,
142
+ className: node.className,
143
+ packageName: node.packageName,
144
+ hintText: node.hintText || null,
145
+ error: node.error || null,
146
+ clickable: node.clickable,
147
+ enabled: node.enabled,
148
+ checked: node.checked,
149
+ focused: node.focused,
150
+ focusable: node.focusable,
151
+ selected: node.selected,
152
+ checkable: node.checkable,
153
+ longClickable: node.longClickable,
154
+ scrollable: node.scrollable,
155
+ password: node.password,
156
+ visibleToUser: node.visibleToUser,
157
+ bounds: { x1, y1, x2, y2 },
158
+ center: {
159
+ x: Math.round((x1 + x2) / 2),
160
+ y: Math.round((y1 + y2) / 2),
161
+ },
162
+ };
163
+ }
164
+ async function queryFocused(driver) {
165
+ if (driver instanceof ios_js_1.IOSDriver) {
166
+ const hierarchy = await driver.viewHierarchy(false);
167
+ const node = findFocusedIOS(hierarchy.axElement);
168
+ return node ? formatIOSElement(node) : null;
169
+ }
170
+ else if (driver instanceof android_js_1.AndroidDriver) {
171
+ const xml = await driver.viewHierarchy();
172
+ const nodes = (0, element_resolver_js_1.parseAndroidHierarchy)(xml);
173
+ const node = nodes.find((n) => n.focused);
174
+ return node ? formatAndroidNode(node) : null;
175
+ }
176
+ throw new Error('Unknown driver type');
177
+ }
178
+ async function focused(opts = {}, sessionName = 'default', { poll = false, interval = 500 } = {}) {
179
+ try {
180
+ const driver = await (0, runner_js_1.getDriver)(sessionName);
181
+ if (!poll) {
182
+ const data = await queryFocused(driver);
183
+ if (!data) {
184
+ if (opts.json) {
185
+ console.log(JSON.stringify({ status: 'ok', focused: null }));
186
+ }
187
+ else {
188
+ console.log('No element is currently focused');
189
+ }
190
+ return 0;
191
+ }
192
+ if (opts.json) {
193
+ console.log(JSON.stringify({ status: 'ok', focused: data }));
194
+ }
195
+ else {
196
+ console.log(JSON.stringify(data, null, 2));
197
+ }
198
+ return 0;
199
+ }
200
+ // --poll mode: repeatedly query and log on change
201
+ let lastJson = '';
202
+ const onSignal = () => process.exit(0);
203
+ process.on('SIGINT', onSignal);
204
+ process.on('SIGTERM', onSignal);
205
+ while (true) {
206
+ const data = await queryFocused(driver);
207
+ const currentJson = JSON.stringify(data);
208
+ if (currentJson !== lastJson) {
209
+ lastJson = currentJson;
210
+ if (!data) {
211
+ if (opts.json) {
212
+ console.log(JSON.stringify({ status: 'ok', focused: null }));
213
+ }
214
+ else {
215
+ console.log('No element is currently focused');
216
+ }
217
+ }
218
+ else if (opts.json) {
219
+ console.log(JSON.stringify({ status: 'ok', focused: data }));
220
+ }
221
+ else {
222
+ console.log(JSON.stringify(data, null, 2));
223
+ }
224
+ }
225
+ await new Promise((r) => setTimeout(r, interval));
226
+ }
227
+ }
228
+ catch (err) {
229
+ const msg = err instanceof Error ? err.message : String(err);
230
+ (0, output_js_1.printError)(`focused — failed\n${msg}`, opts);
231
+ return 1;
232
+ }
233
+ }
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP = void 0;
3
4
  exports.foregroundApp = foregroundApp;
5
+ exports.HELP = ` foreground-app Print bundle ID / package of the foreground app`;
4
6
  const runner_js_1 = require("../runner.js");
5
7
  const output_js_1 = require("../output.js");
6
8
  const ios_js_1 = require("../drivers/ios.js");
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP = void 0;
3
4
  exports.hideKeyboard = hideKeyboard;
5
+ exports.HELP = ` hide-keyboard Dismiss the on-screen keyboard`;
4
6
  const runner_js_1 = require("../runner.js");
5
7
  const output_js_1 = require("../output.js");
6
8
  const ios_js_1 = require("../drivers/ios.js");
@@ -1,14 +1,36 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP = void 0;
3
4
  exports.inspect = inspect;
5
+ exports.HELP = ` inspect [--dump] Print UI hierarchy (--dump for raw driver output)`;
4
6
  const runner_js_1 = require("../runner.js");
5
7
  const output_js_1 = require("../output.js");
6
8
  const ios_js_1 = require("../drivers/ios.js");
7
9
  const android_js_1 = require("../drivers/android.js");
8
10
  const element_resolver_js_1 = require("../drivers/element-resolver.js");
9
- async function inspect(opts = {}, sessionName = 'default') {
11
+ async function inspect(opts = {}, sessionName = 'default', inspectOpts = {}) {
10
12
  try {
11
13
  const driver = await (0, runner_js_1.getDriver)(sessionName);
14
+ if (inspectOpts.dump) {
15
+ let raw;
16
+ if (driver instanceof ios_js_1.IOSDriver) {
17
+ const hierarchy = await driver.viewHierarchy(false);
18
+ raw = JSON.stringify(hierarchy, null, 2);
19
+ }
20
+ else if (driver instanceof android_js_1.AndroidDriver) {
21
+ raw = await driver.viewHierarchy();
22
+ }
23
+ else {
24
+ throw new Error('Unknown driver type');
25
+ }
26
+ if (opts.json) {
27
+ console.log(JSON.stringify({ status: 'ok', dump: raw }));
28
+ }
29
+ else {
30
+ console.log(raw);
31
+ }
32
+ return 0;
33
+ }
12
34
  let text;
13
35
  if (driver instanceof ios_js_1.IOSDriver) {
14
36
  const hierarchy = await driver.viewHierarchy(false);