@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.
- 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 +4 -0
- package/dist/commands/clear-state.js +27 -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 +3 -1
- 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/commands/uninstall-app.js +31 -0
- package/dist/daemon/server.js +64 -22
- package/dist/device-picker.js +65 -0
- package/dist/drivers/android.js +18 -13
- package/dist/drivers/bootstrap.js +179 -6
- package/dist/drivers/element-resolver.js +10 -0
- package/dist/drivers/flow-runner.js +21 -5
- package/dist/drivers/ios.js +7 -1
- package/dist/index.js +134 -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 +66 -6
- package/skills/conductor/references/flow-syntax.md +3 -0
- 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
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<img src="assets/banner.png" alt="Conductor" width="800" />
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
# 🎼 Conductor
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**Give Claude hands. Let it drive your app.**
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
[](https://github.com/DouweBos/conductor/actions/workflows/ci.yml)
|
|
10
|
+
[](https://www.npmjs.com/package/@houwert/conductor)
|
|
11
|
+
[](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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
81
|
+
- Node.js + pnpm v9
|
|
82
|
+
- **iOS:** Xcode with command-line tools
|
|
83
|
+
- **Android:** Android SDK with `adb` on `PATH`
|
|
28
84
|
|
|
29
|
-
|
|
85
|
+
### Full build
|
|
30
86
|
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
##
|
|
136
|
+
## 📋 Requirements
|
|
35
137
|
|
|
36
|
-
-
|
|
37
|
-
- `
|
|
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");
|
package/dist/commands/back.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/commands/daemon.js
CHANGED
|
@@ -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");
|
package/dist/commands/inspect.js
CHANGED
|
@@ -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);
|