@houwert/conductor 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -62
- package/dist/android/device.js +94 -0
- package/dist/commands/debug.js +6 -6
- package/dist/commands/focused.js +27 -0
- package/dist/commands/input-latency.js +348 -0
- package/dist/commands/metro.js +3 -1
- package/dist/commands/native-rn.js +2 -2
- package/dist/commands/network.js +2 -2
- package/dist/commands/press-key.js +106 -60
- package/dist/commands/profile-frames.js +793 -0
- package/dist/commands/profile-gc.js +90 -0
- package/dist/commands/profile-js.js +320 -0
- package/dist/commands/profile.js +391 -77
- package/dist/commands/run-flow.js +70 -4
- package/dist/drivers/flow-runner.js +7 -1
- package/dist/drivers/metro-cdp.js +28 -3
- package/dist/index.js +79 -43
- package/dist/stats.js +71 -0
- package/package.json +1 -1
- package/skills/conductor-create-flow/SKILL.md +1 -1
- package/skills/conductor-device-interact/SKILL.md +1 -1
- package/skills/conductor-profiler/SKILL.md +115 -7
- package/dist/commands/cases.js +0 -260
- package/skills/conductor-test-cases/SKILL.md +0 -65
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
<img src="assets/banner.png" alt="Conductor" width="800" />
|
|
4
4
|
|
|
5
|
-
#
|
|
5
|
+
# Conductor
|
|
6
6
|
|
|
7
|
-
**
|
|
7
|
+
**Mobile and web UI automation for AI agents.**
|
|
8
8
|
|
|
9
9
|
[](https://github.com/DouweBos/conductor/actions/workflows/ci.yml)
|
|
10
10
|
[](https://www.npmjs.com/package/@houwert/conductor)
|
|
@@ -14,13 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
Conductor is a token-efficient CLI for
|
|
17
|
+
Conductor is a token-efficient CLI for driving and inspecting running apps, built for AI agents. It is a TypeScript reimplementation and partial fork of [Maestro](https://maestro.mobile.dev) that ships its own native drivers, so there is no external CLI to install and no JVM to configure.
|
|
18
18
|
|
|
19
|
-
It
|
|
20
|
-
|
|
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.
|
|
19
|
+
It gives a coding agent the ability to operate an app while writing the code for it: navigate the UI, read the live view hierarchy, take screenshots, run flows, and drive several devices in parallel across concurrent agents.
|
|
24
20
|
|
|
25
21
|
```bash
|
|
26
22
|
conductor launch-app com.example.myapp
|
|
@@ -30,47 +26,85 @@ conductor assert-visible "Dashboard"
|
|
|
30
26
|
conductor take-screenshot --output /tmp/screen.png
|
|
31
27
|
```
|
|
32
28
|
|
|
33
|
-
|
|
29
|
+
Targets iOS and tvOS simulators, physical iOS/tvOS devices, Android emulators and devices, Amazon Fire TV, and web via Playwright.
|
|
34
30
|
|
|
35
|
-
##
|
|
31
|
+
## Quick start
|
|
36
32
|
|
|
37
33
|
```bash
|
|
38
34
|
npm install -g @houwert/conductor
|
|
39
35
|
```
|
|
40
36
|
|
|
41
|
-
|
|
37
|
+
Conductor is a pure CLI. To teach an AI agent how to use it, install the bundled skills into your repository:
|
|
42
38
|
|
|
43
39
|
```bash
|
|
44
|
-
conductor init # interactive: pick scope + skills, writes
|
|
40
|
+
conductor init # interactive: pick scope + skills, writes to .claude/skills/
|
|
45
41
|
conductor init --yes # non-interactive: install all skills into ./.claude/skills/
|
|
46
42
|
conductor init --global # install into ~/.claude/skills/ for all repos
|
|
47
|
-
conductor init --force # re-sync skills you
|
|
43
|
+
conductor init --force # re-sync skills you have already installed
|
|
48
44
|
```
|
|
49
45
|
|
|
50
|
-
`init` is the one manual setup step — run it once per
|
|
46
|
+
`init` is the one manual setup step — run it once per repository. In a terminal it prompts for which skills and where; piped or headless (CI, agent) it installs everything non-interactively. It writes a set of capability-scoped Claude Code skills — `conductor-device-interact`, `conductor-inspect`, `conductor-create-flow`, `conductor-native`, `conductor-metro-debugger`, `conductor-profiler` and `conductor-device-setup` — documenting every command and the act → observe → act workflow.
|
|
47
|
+
|
|
48
|
+
After upgrading conductor, re-run `conductor init --force` to re-sync the installed skills. `init` stamps the version it installed, so it can tell you when they are stale and prune skills no longer shipped. Integrating another way (a custom `CLAUDE.md`, a slash command) works equally well.
|
|
51
49
|
|
|
52
|
-
|
|
50
|
+
Run `conductor --help` for the full command reference, or `conductor <command> --help` for per-command flags.
|
|
53
51
|
|
|
54
|
-
###
|
|
52
|
+
### What the CLI can do
|
|
55
53
|
|
|
56
54
|
| Capability | Commands |
|
|
57
55
|
|---|---|
|
|
58
|
-
| App lifecycle | `launch-app`, `stop-app`, `clear-state`, `
|
|
59
|
-
| Interaction | `tap-on`, `input-text`, `scroll`, `scroll-until-visible`, `swipe`, `press-key`, `erase-text`, `hide-keyboard` |
|
|
60
|
-
| Inspection | `inspect`, `focused`, `
|
|
61
|
-
| Assertions | `assert-visible`, `assert-not-visible` |
|
|
56
|
+
| App lifecycle | `launch-app`, `stop-app`, `clear-state`, `install-app`, `uninstall-app`, `foreground-app`, `copy-app`, `download-app` |
|
|
57
|
+
| Interaction | `tap-on`, `input-text`, `scroll`, `scroll-until-visible`, `swipe`, `gesture`, `pinch`, `press-key`, `erase-text`, `hide-keyboard` |
|
|
58
|
+
| Inspection | `inspect`, `focused`, `capture-ui`, `take-screenshot`, `list-apps` |
|
|
59
|
+
| Assertions | `assert-visible`, `assert-not-visible`, `assert-true`, `assert-screenshot` |
|
|
62
60
|
| Navigation | `open-link`, `back` |
|
|
63
|
-
| Flows | `run-flow`, `run-flow-inline`, `run-parallel` |
|
|
64
|
-
| Devices | `start-device`, `list-devices`, `set-location`, `set-orientation` |
|
|
61
|
+
| Flows | `run-flow`, `run-flow-inline`, `run-parallel`, `run-sequence`, `flow` |
|
|
62
|
+
| Devices | `start-device`, `stop-device`, `list-devices`, `device-pool`, `set-location`, `set-orientation`, `set-permissions` |
|
|
63
|
+
| Debugging | `logs`, `crashes`, `network`, `memory`, `profile`, `metro`, `record-video`, `stream-server` |
|
|
64
|
+
| In-process (iOS/tvOS) | `native-inspect`, `native-find`, `native-set`, `native-eval`, `native-heap`, and more |
|
|
65
|
+
| Test cases | `cases` |
|
|
65
66
|
| Web setup | `install-web [browser]` (installs a Playwright browser; `--check` prints status) |
|
|
66
|
-
| Discovery | `list-options [command]` / `<command> --options`
|
|
67
|
+
| Discovery | `list-options [command]` / `<command> --options`, `workspace` |
|
|
68
|
+
|
|
69
|
+
## Conductor Studio
|
|
70
|
+
|
|
71
|
+
**[Conductor Studio](apps/studio)** is a desktop app (Electron + React) built on this CLI. It does three jobs: writing and managing Maestro tests, writing them with an agent, and tracking them as test cases.
|
|
72
|
+
|
|
73
|
+
It provides a flow editor with autocomplete and linting, a live device stream with element picking and a record mode that turns your interactions into flow steps, an agentic test writer that verifies described behaviour on a device and files a visual report, and local test case management. Light and dark, signed and notarized, auto-updating.
|
|
74
|
+
|
|
75
|
+
Studio bundles its own copy of the conductor CLI, so it needs nothing installed globally. The version it uses can be pinned from Settings.
|
|
76
|
+
|
|
77
|
+
**Download:** [Releases](https://github.com/DouweBos/conductor/releases) — macOS (Apple silicon). Studio releases are tagged `studio-v*`; the CLI's are tagged `cli-v*`.
|
|
78
|
+
|
|
79
|
+
See the [Studio README](apps/studio/README.md) for the full feature tour, architecture and release process.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pnpm dev:studio # run it from source
|
|
83
|
+
```
|
|
67
84
|
|
|
68
|
-
##
|
|
85
|
+
## Repository structure
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
conductor/
|
|
89
|
+
├── packages/
|
|
90
|
+
│ ├── cli/ # TypeScript CLI (@houwert/conductor)
|
|
91
|
+
│ ├── android-driver/ # Kotlin/Gradle instrumentation driver
|
|
92
|
+
│ ├── ios-driver/ # Swift/Xcode XCTest driver
|
|
93
|
+
│ ├── ios-inproc/ # Library injected into the app for a second inspection plane
|
|
94
|
+
│ ├── ios-hid/ # Host binary injecting HID below the XCTest layer
|
|
95
|
+
│ ├── ios-capture/ # Host binary capturing the Simulator framebuffer
|
|
96
|
+
│ └── studio-ui/ # Design system for Conductor Studio
|
|
97
|
+
├── apps/
|
|
98
|
+
│ └── studio/ # Conductor Studio — the desktop app
|
|
99
|
+
└── Makefile
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Building locally
|
|
69
103
|
|
|
70
104
|
### Prerequisites
|
|
71
105
|
|
|
72
|
-
- Node.js
|
|
73
|
-
- **iOS:** Xcode with command-line tools
|
|
106
|
+
- Node.js and pnpm 10
|
|
107
|
+
- **iOS/tvOS:** Xcode with command-line tools
|
|
74
108
|
- **Android:** Android SDK with `adb` on `PATH`
|
|
75
109
|
|
|
76
110
|
### Full build
|
|
@@ -79,7 +113,7 @@ When you upgrade conductor, re-run `conductor init --force` to re-sync the insta
|
|
|
79
113
|
make build
|
|
80
114
|
```
|
|
81
115
|
|
|
82
|
-
Builds the iOS
|
|
116
|
+
Builds the iOS and tvOS XCTest drivers, the in-process library and capture binary (xcodebuild), and the Android driver (Gradle); packages them all into the CLI and compiles TypeScript. Then link it globally:
|
|
83
117
|
|
|
84
118
|
```bash
|
|
85
119
|
cd packages/cli && pnpm link --global
|
|
@@ -97,48 +131,23 @@ pnpm install && pnpm build
|
|
|
97
131
|
### Individual targets
|
|
98
132
|
|
|
99
133
|
```bash
|
|
100
|
-
make build-cli
|
|
101
|
-
make build-ios-driver
|
|
134
|
+
make build-cli # CLI TypeScript only
|
|
135
|
+
make build-ios-driver # iOS XCTest driver
|
|
102
136
|
make build-android-driver # Android instrumentation driver
|
|
103
|
-
make package-cli
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
## 🗂️ Repository structure
|
|
107
|
-
|
|
108
|
-
```
|
|
109
|
-
conductor/
|
|
110
|
-
├── packages/
|
|
111
|
-
│ ├── cli/ # TypeScript CLI (@houwert/conductor)
|
|
112
|
-
│ ├── android-driver/ # Kotlin/Gradle instrumentation driver
|
|
113
|
-
│ ├── ios-driver/ # Swift/Xcode XCTest driver
|
|
114
|
-
│ └── studio-ui/ # Design system for Conductor Studio
|
|
115
|
-
├── apps/
|
|
116
|
-
│ └── studio/ # Conductor Studio — the desktop app
|
|
117
|
-
└── Makefile
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
## 🖥️ Conductor Studio
|
|
121
|
-
|
|
122
|
-
There's also a desktop app: **Conductor Studio**, a Maestro test workbench built
|
|
123
|
-
on this CLI — flow editor with autocomplete, live device stream with element
|
|
124
|
-
picking, an agentic test writer, and test case management. It lives in
|
|
125
|
-
[`apps/studio`](apps/studio); see its
|
|
126
|
-
[README](apps/studio/README.md) for what it does and how to run it.
|
|
127
|
-
|
|
128
|
-
```bash
|
|
129
|
-
pnpm dev:studio
|
|
137
|
+
make package-cli # Bundle drivers into CLI package
|
|
130
138
|
```
|
|
131
139
|
|
|
132
|
-
##
|
|
140
|
+
## Development
|
|
133
141
|
|
|
134
142
|
```bash
|
|
135
|
-
pnpm dev
|
|
136
|
-
pnpm lint
|
|
143
|
+
pnpm dev # TypeScript watch mode
|
|
144
|
+
pnpm lint # ESLint + Prettier check
|
|
137
145
|
pnpm lint:fix # Auto-fix formatting
|
|
138
|
-
pnpm test
|
|
146
|
+
pnpm test # Run test suite
|
|
139
147
|
```
|
|
140
148
|
|
|
141
|
-
##
|
|
149
|
+
## Requirements
|
|
142
150
|
|
|
143
|
-
- **iOS:** Xcode with a booted simulator
|
|
151
|
+
- **iOS/tvOS:** Xcode with a booted simulator, or a paired physical device
|
|
144
152
|
- **Android:** `adb` on `PATH` with a running emulator or connected device
|
|
153
|
+
- **Web:** a Playwright browser (`conductor install-web`)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.adb = adb;
|
|
4
|
+
exports.adbShell = adbShell;
|
|
5
|
+
exports.parseResumedPackage = parseResumedPackage;
|
|
6
|
+
exports.resolveAndroidForegroundApp = resolveAndroidForegroundApp;
|
|
7
|
+
exports.shellBracketed = shellBracketed;
|
|
8
|
+
exports.buildClockAnchor = buildClockAnchor;
|
|
9
|
+
exports.parseProcUptimeMs = parseProcUptimeMs;
|
|
10
|
+
exports.toDeviceRealtimeMs = toDeviceRealtimeMs;
|
|
11
|
+
/**
|
|
12
|
+
* Small adb helpers for read-only device queries.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately spawns adb directly rather than going through `runner.ts` so the
|
|
15
|
+
* profiling commands can query a device without booting the gRPC driver daemon —
|
|
16
|
+
* `dumpsys` needs neither, and starting the daemon would perturb what we measure.
|
|
17
|
+
*/
|
|
18
|
+
const child_process_1 = require("child_process");
|
|
19
|
+
const sdk_js_1 = require("./sdk.js");
|
|
20
|
+
function adb(deviceId, args) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const proc = (0, child_process_1.spawn)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', deviceId, ...args], {
|
|
23
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
24
|
+
env: (0, sdk_js_1.androidSpawnEnv)(),
|
|
25
|
+
});
|
|
26
|
+
let stdout = '';
|
|
27
|
+
let stderr = '';
|
|
28
|
+
proc.stdout.on('data', (c) => (stdout += c.toString()));
|
|
29
|
+
proc.stderr.on('data', (c) => (stderr += c.toString()));
|
|
30
|
+
proc.on('close', (code) => resolve({ success: code === 0, stdout, stderr }));
|
|
31
|
+
proc.on('error', (err) => resolve({ success: false, stdout: '', stderr: err.message }));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function adbShell(deviceId, args) {
|
|
35
|
+
return adb(deviceId, ['shell', ...args]);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Package name of the resumed activity, or undefined. Android spells this
|
|
39
|
+
* differently across API levels, hence the alternation.
|
|
40
|
+
*/
|
|
41
|
+
function parseResumedPackage(dumpsysActivities) {
|
|
42
|
+
const m = dumpsysActivities.match(/(?:m|top)?ResumedActivity[=:].*?([a-zA-Z][a-zA-Z0-9_]*(?:\.[a-zA-Z][a-zA-Z0-9_]*)+)\//);
|
|
43
|
+
return m ? m[1] : undefined;
|
|
44
|
+
}
|
|
45
|
+
async function resolveAndroidForegroundApp(deviceId) {
|
|
46
|
+
const dump = await adbShell(deviceId, ['dumpsys', 'activity', 'activities']);
|
|
47
|
+
return dump.success ? parseResumedPackage(dump.stdout) : undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Run `command` on the device bracketed by two CLOCK_REALTIME reads, in one
|
|
51
|
+
* adb invocation. The caller supplies the monotonic reading by parsing it out
|
|
52
|
+
* of `stdout` (for gfxinfo, its own `Uptime:` header line).
|
|
53
|
+
*/
|
|
54
|
+
async function shellBracketed(deviceId, command) {
|
|
55
|
+
const hostStart = Date.now();
|
|
56
|
+
// One argument, not argv: adb joins everything after `shell` with spaces and
|
|
57
|
+
// the device's own shell parses it, so the semicolons survive.
|
|
58
|
+
const res = await adbShell(deviceId, [`date +%s%N; ${command}; date +%s%N`]);
|
|
59
|
+
const hostElapsedMs = Date.now() - hostStart;
|
|
60
|
+
if (!res.success)
|
|
61
|
+
return undefined;
|
|
62
|
+
const stamps = [...res.stdout.matchAll(/^\s*(\d{16,})\s*$/gm)].map((m) => Number(m[1]) / 1e6);
|
|
63
|
+
if (stamps.length < 2)
|
|
64
|
+
return undefined;
|
|
65
|
+
const realtimeStartMs = stamps[0];
|
|
66
|
+
const realtimeEndMs = stamps[stamps.length - 1];
|
|
67
|
+
// Strip the two stamp lines so callers parse only the command's own output.
|
|
68
|
+
const stdout = res.stdout
|
|
69
|
+
.split(/\r?\n/)
|
|
70
|
+
.filter((l) => !/^\s*\d{16,}\s*$/.test(l))
|
|
71
|
+
.join('\n');
|
|
72
|
+
return { stdout, realtimeStartMs, realtimeEndMs, hostElapsedMs };
|
|
73
|
+
}
|
|
74
|
+
function buildClockAnchor(bracket, deviceMonotonicMs) {
|
|
75
|
+
const spread = bracket.realtimeEndMs - bracket.realtimeStartMs;
|
|
76
|
+
return {
|
|
77
|
+
deviceMonotonicMs,
|
|
78
|
+
deviceRealtimeMs: (bracket.realtimeStartMs + bracket.realtimeEndMs) / 2,
|
|
79
|
+
anchorErrorMs: Math.round((spread / 2) * 100) / 100,
|
|
80
|
+
hostEpochMs: Date.now(),
|
|
81
|
+
// The device cannot have spent more wall time on this than the host did
|
|
82
|
+
// waiting for it; if it claims to, the realtime clock was stepped.
|
|
83
|
+
clockStepped: spread > bracket.hostElapsedMs + 50 ? true : undefined,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Device CLOCK_MONOTONIC ms from /proc/uptime. Cheap (~1ms) but 10ms-grained. */
|
|
87
|
+
function parseProcUptimeMs(out) {
|
|
88
|
+
const m = out.match(/^\s*(\d+\.\d+)\s+\d+\.\d+\s*$/m);
|
|
89
|
+
return m ? Number(m[1]) * 1000 : undefined;
|
|
90
|
+
}
|
|
91
|
+
/** Convert a framestats vsync timestamp (ns, monotonic) to the app's Date.now() domain. */
|
|
92
|
+
function toDeviceRealtimeMs(anchor, monotonicNs) {
|
|
93
|
+
return monotonicNs / 1e6 - anchor.deviceMonotonicMs + anchor.deviceRealtimeMs;
|
|
94
|
+
}
|
package/dist/commands/debug.js
CHANGED
|
@@ -47,11 +47,11 @@ function resolveSession(sessionName) {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
async function debugStatus(opts, sessionName, debugOpts) {
|
|
50
|
-
const port = debugOpts.port ?? 8081;
|
|
51
50
|
try {
|
|
52
|
-
const targets = await (0, metro_js_1.fetchTargets)(port, 'localhost');
|
|
53
51
|
const { deviceId, platformPromise } = resolveSession(sessionName);
|
|
54
52
|
const platform = await platformPromise;
|
|
53
|
+
const port = await (0, metro_cdp_js_1.resolveMetroPort)({ port: debugOpts.port, deviceId, platform });
|
|
54
|
+
const targets = await (0, metro_js_1.fetchTargets)(port, 'localhost');
|
|
55
55
|
const client = new metro_cdp_js_1.MetroCdpClient();
|
|
56
56
|
await client.connect({ port, deviceId, platform, targetIndex: debugOpts.targetIndex });
|
|
57
57
|
await client.enableDomain('Runtime');
|
|
@@ -120,10 +120,10 @@ async function debugEvaluate(expr, opts, sessionName, debugOpts) {
|
|
|
120
120
|
return 1;
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
-
const port = debugOpts.port ?? 8081;
|
|
124
123
|
const { deviceId, platformPromise } = resolveSession(sessionName);
|
|
125
124
|
try {
|
|
126
125
|
const platform = await platformPromise;
|
|
126
|
+
const port = await (0, metro_cdp_js_1.resolveMetroPort)({ port: debugOpts.port, deviceId, platform });
|
|
127
127
|
const client = new metro_cdp_js_1.MetroCdpClient();
|
|
128
128
|
await client.connect({ port, deviceId, platform, targetIndex: debugOpts.targetIndex });
|
|
129
129
|
const value = await client.evaluate(expr);
|
|
@@ -140,10 +140,10 @@ async function debugEvaluate(expr, opts, sessionName, debugOpts) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
async function debugComponentTree(opts, sessionName, debugOpts) {
|
|
143
|
-
const port = debugOpts.port ?? 8081;
|
|
144
143
|
const { deviceId, platformPromise } = resolveSession(sessionName);
|
|
145
144
|
try {
|
|
146
145
|
const platform = await platformPromise;
|
|
146
|
+
const port = await (0, metro_cdp_js_1.resolveMetroPort)({ port: debugOpts.port, deviceId, platform });
|
|
147
147
|
const client = new metro_cdp_js_1.MetroCdpClient();
|
|
148
148
|
await client.connect({ port, deviceId, platform, targetIndex: debugOpts.targetIndex });
|
|
149
149
|
const awaitCallback = await client.installCallbackBinding();
|
|
@@ -214,10 +214,10 @@ async function debugInspectElement(at, opts, sessionName, debugOpts) {
|
|
|
214
214
|
}
|
|
215
215
|
const x = Number(m[1]);
|
|
216
216
|
const y = Number(m[2]);
|
|
217
|
-
const port = debugOpts.port ?? 8081;
|
|
218
217
|
const { deviceId, platformPromise } = resolveSession(sessionName);
|
|
219
218
|
try {
|
|
220
219
|
const platform = await platformPromise;
|
|
220
|
+
const port = await (0, metro_cdp_js_1.resolveMetroPort)({ port: debugOpts.port, deviceId, platform });
|
|
221
221
|
const client = new metro_cdp_js_1.MetroCdpClient();
|
|
222
222
|
await client.connect({ port, deviceId, platform, targetIndex: debugOpts.targetIndex });
|
|
223
223
|
const awaitCallback = await client.installCallbackBinding();
|
|
@@ -253,10 +253,10 @@ async function debugLogRegistry(opts, sessionName) {
|
|
|
253
253
|
return (0, logs_js_1.logs)(opts, sessionName, { source: 'metro', list: true });
|
|
254
254
|
}
|
|
255
255
|
async function debugReload(opts, sessionName, debugOpts) {
|
|
256
|
-
const port = debugOpts.port ?? 8081;
|
|
257
256
|
const { deviceId, platformPromise } = resolveSession(sessionName);
|
|
258
257
|
try {
|
|
259
258
|
const platform = await platformPromise;
|
|
259
|
+
const port = await (0, metro_cdp_js_1.resolveMetroPort)({ port: debugOpts.port, deviceId, platform });
|
|
260
260
|
await (0, metro_cdp_js_1.cdpCall)('Page.reload', undefined, {
|
|
261
261
|
port,
|
|
262
262
|
deviceId,
|
package/dist/commands/focused.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HELP = void 0;
|
|
4
|
+
exports.queryFocused = queryFocused;
|
|
5
|
+
exports.focusKey = focusKey;
|
|
4
6
|
exports.focused = focused;
|
|
5
7
|
exports.HELP = ` focused [--poll [interval_ms]] Print metadata of the currently focused element
|
|
6
8
|
--poll continuously watches for focus changes (default 500ms)`;
|
|
@@ -216,6 +218,31 @@ async function queryFocused(driver) {
|
|
|
216
218
|
}
|
|
217
219
|
throw new Error('Unknown driver type');
|
|
218
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Stable identity for a focused element, for detecting *that focus moved*.
|
|
223
|
+
*
|
|
224
|
+
* Deliberately excludes the visible label: TV rows repeat titles, so two
|
|
225
|
+
* neighbouring cards can print identically. Bounds plus whichever id the
|
|
226
|
+
* platform exposes distinguishes them.
|
|
227
|
+
*/
|
|
228
|
+
function focusKey(data) {
|
|
229
|
+
if (!data)
|
|
230
|
+
return '(none)';
|
|
231
|
+
const bounds = data.bounds;
|
|
232
|
+
const boundsKey = bounds
|
|
233
|
+
? Object.entries(bounds)
|
|
234
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
235
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
236
|
+
.join(',')
|
|
237
|
+
: '';
|
|
238
|
+
const id = data.identifier ||
|
|
239
|
+
data.resourceId ||
|
|
240
|
+
data.ref ||
|
|
241
|
+
data.contentDesc ||
|
|
242
|
+
'';
|
|
243
|
+
const type = data.elementTypeName || data.className || data.role || '';
|
|
244
|
+
return `${id}|${type}|${boundsKey}`;
|
|
245
|
+
}
|
|
219
246
|
async function focused(opts = {}, sessionName = 'default', { poll = false, interval = 500 } = {}) {
|
|
220
247
|
try {
|
|
221
248
|
const driver = await (0, runner_js_1.getDriver)(sessionName);
|