@houwert/conductor 0.21.0 → 0.22.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.
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.HELP = void 0;
|
|
7
|
+
exports.webTargets = webTargets;
|
|
8
|
+
/**
|
|
9
|
+
* List the CDP page targets exposed by an external browser (e.g. an Electron app
|
|
10
|
+
* launched with `--remote-debugging-port`). Each target is a controllable page —
|
|
11
|
+
* for the Lightning emulator, one per tile plus its control/remote chrome.
|
|
12
|
+
*
|
|
13
|
+
* Reads the DevTools HTTP endpoint (`/json/list`) directly, so it needs no
|
|
14
|
+
* Playwright browser and works before any daemon session exists. Use the printed
|
|
15
|
+
* target IDs with `--cdp-url` / `--cdp-target` to bind a session to a tile.
|
|
16
|
+
*/
|
|
17
|
+
const http_1 = __importDefault(require("http"));
|
|
18
|
+
const output_js_1 = require("../output.js");
|
|
19
|
+
/** Derive the `http://host:port` base from a CDP URL (which may be ws:// or include a path). */
|
|
20
|
+
function httpBase(cdpUrl) {
|
|
21
|
+
const u = new URL(cdpUrl);
|
|
22
|
+
const proto = u.protocol === 'https:' || u.protocol === 'wss:' ? 'https:' : 'http:';
|
|
23
|
+
return `${proto}//${u.host}`;
|
|
24
|
+
}
|
|
25
|
+
function fetchTargets(cdpUrl) {
|
|
26
|
+
const url = `${httpBase(cdpUrl)}/json/list`;
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const req = http_1.default.get(url, (res) => {
|
|
29
|
+
const chunks = [];
|
|
30
|
+
res.on('data', (c) => chunks.push(c));
|
|
31
|
+
res.on('end', () => {
|
|
32
|
+
if ((res.statusCode ?? 0) >= 300) {
|
|
33
|
+
reject(new Error(`HTTP ${res.statusCode} from ${url}`));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString('utf-8')));
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
reject(err);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
req.setTimeout(5000, () => req.destroy(new Error(`Timed out fetching ${url}`)));
|
|
45
|
+
req.on('error', reject);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async function webTargets(cdpUrl, opts) {
|
|
49
|
+
if (!cdpUrl) {
|
|
50
|
+
console.error('web-targets requires --cdp-url <url> (e.g. --cdp-url http://127.0.0.1:9222).\n' +
|
|
51
|
+
'Launch the browser/Electron app with --remote-debugging-port to expose it.');
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
let targets;
|
|
55
|
+
try {
|
|
56
|
+
targets = await fetchTargets(cdpUrl);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
console.error(`Could not reach CDP endpoint at ${cdpUrl}: ${err instanceof Error ? err.message : String(err)}`);
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
// Only type="page" targets are controllable as Playwright Pages.
|
|
63
|
+
const pages = targets.filter((t) => t.type === 'page');
|
|
64
|
+
if (opts.json) {
|
|
65
|
+
(0, output_js_1.printData)(pages.map((t) => ({ id: t.id, title: t.title, url: t.url })), opts);
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
if (pages.length === 0) {
|
|
69
|
+
console.log('No page targets found. Is the app loaded and started with --remote-debugging-port?');
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
console.log(`Found ${pages.length} page target(s) at ${cdpUrl}:\n`);
|
|
73
|
+
pages.forEach((t, i) => {
|
|
74
|
+
console.log(` [${i}] ${t.title || '(untitled)'}`);
|
|
75
|
+
console.log(` url: ${t.url}`);
|
|
76
|
+
console.log(` target: ${t.id}`);
|
|
77
|
+
console.log(` bind: conductor --device web:chromium:t${i} --cdp-url ${cdpUrl} --cdp-target ${t.id} inspect`);
|
|
78
|
+
console.log('');
|
|
79
|
+
});
|
|
80
|
+
console.log('Bind a session to a target once (any command), then drop the --cdp-* flags on later\n' +
|
|
81
|
+
'commands for that --device — the attachment is remembered per session.');
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
exports.HELP = ' web-targets --cdp-url <url> List controllable CDP page targets (one per Electron webview/tile)';
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.isMetroPort = isMetroPort;
|
|
4
4
|
exports.discoverMetroPortForDevice = discoverMetroPortForDevice;
|
|
5
5
|
exports.getDeviceDisplayName = getDeviceDisplayName;
|
|
6
|
+
exports.deviceNameMatches = deviceNameMatches;
|
|
6
7
|
exports.selectTargetForDevice = selectTargetForDevice;
|
|
7
8
|
exports.targetsForDevice = targetsForDevice;
|
|
8
9
|
/**
|
|
@@ -200,6 +201,16 @@ async function getDeviceDisplayName(platform, deviceId) {
|
|
|
200
201
|
}
|
|
201
202
|
return null;
|
|
202
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Whether a Metro target's `deviceName` refers to the same device as `displayName`.
|
|
206
|
+
* Tolerant of the suffixes Metro appends to the bare model name — e.g. Android's
|
|
207
|
+
* `ro.product.model` is `Chromecast` while Metro reports `Chromecast - 14 - API 34`.
|
|
208
|
+
*/
|
|
209
|
+
function deviceNameMatches(targetDeviceName, displayName) {
|
|
210
|
+
if (!targetDeviceName)
|
|
211
|
+
return false;
|
|
212
|
+
return targetDeviceName === displayName || targetDeviceName.startsWith(`${displayName} `);
|
|
213
|
+
}
|
|
203
214
|
/**
|
|
204
215
|
* Filter Metro /json targets to those belonging to this device (by display
|
|
205
216
|
* name), preferring the fusebox runtime if present. Returns undefined when
|
|
@@ -208,7 +219,7 @@ async function getDeviceDisplayName(platform, deviceId) {
|
|
|
208
219
|
*/
|
|
209
220
|
function selectTargetForDevice(targets, displayName) {
|
|
210
221
|
const withWs = targets.filter((t) => t.webSocketDebuggerUrl);
|
|
211
|
-
const matches = withWs.filter((t) => t.deviceName
|
|
222
|
+
const matches = withWs.filter((t) => deviceNameMatches(t.deviceName, displayName));
|
|
212
223
|
if (matches.length === 0)
|
|
213
224
|
return undefined;
|
|
214
225
|
const fusebox = matches.find((t) => t.reactNative?.capabilities?.prefersFuseboxFrontend);
|
|
@@ -216,5 +227,5 @@ function selectTargetForDevice(targets, displayName) {
|
|
|
216
227
|
}
|
|
217
228
|
/** Convenience: all /json targets belonging to this device. */
|
|
218
229
|
function targetsForDevice(targets, displayName) {
|
|
219
|
-
return targets.filter((t) => t.webSocketDebuggerUrl && t.deviceName
|
|
230
|
+
return targets.filter((t) => t.webSocketDebuggerUrl && deviceNameMatches(t.deviceName, displayName));
|
|
220
231
|
}
|
|
@@ -40,12 +40,20 @@ function selectDebuggerUrl(targets, opts, displayName) {
|
|
|
40
40
|
}
|
|
41
41
|
return withWs[opts.targetIndex].webSocketDebuggerUrl;
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// Device-scoped: must resolve to that device's own target. Never silently
|
|
44
|
+
// fall back to another device — that reloads the wrong app and reports success.
|
|
45
|
+
if (opts.deviceId) {
|
|
46
|
+
const target = displayName ? (0, metro_discovery_js_1.selectTargetForDevice)(withWs, displayName) : undefined;
|
|
45
47
|
if (target)
|
|
46
48
|
return target.webSocketDebuggerUrl;
|
|
49
|
+
const available = withWs
|
|
50
|
+
.map((t, i) => ` [${i}] ${t.deviceName ?? t.title ?? '(unnamed)'}`)
|
|
51
|
+
.join('\n');
|
|
52
|
+
throw new Error(`No Metro debugger target for device ${opts.deviceId}` +
|
|
53
|
+
(displayName ? ` (${displayName})` : '') +
|
|
54
|
+
`.\nAvailable targets:\n${available}\nPass --target <index> to pick one explicitly.`);
|
|
47
55
|
}
|
|
48
|
-
//
|
|
56
|
+
// No device requested: prefer the Hermes/React target by title, otherwise first.
|
|
49
57
|
const target = withWs.find((t) => t.title && /hermes|react/i.test(t.title)) ?? withWs[0];
|
|
50
58
|
return target.webSocketDebuggerUrl;
|
|
51
59
|
}
|
package/dist/index.js
CHANGED
|
@@ -61,6 +61,8 @@ const memory_js_1 = require("./commands/memory.js");
|
|
|
61
61
|
const metro_js_1 = require("./commands/metro.js");
|
|
62
62
|
const clipboard_js_1 = require("./commands/clipboard.js");
|
|
63
63
|
const options_js_1 = require("./commands/options.js");
|
|
64
|
+
const web_targets_js_1 = require("./commands/web-targets.js");
|
|
65
|
+
const session_js_2 = require("./session.js");
|
|
64
66
|
const device_picker_js_1 = require("./device-picker.js");
|
|
65
67
|
const update_check_js_1 = require("./update-check.js");
|
|
66
68
|
const pkg_root_js_1 = require("./pkg-root.js");
|
|
@@ -123,11 +125,15 @@ const COMMAND_HELP = {
|
|
|
123
125
|
clipboard: clipboard_js_1.HELP,
|
|
124
126
|
paste: ' paste Trigger OS-level paste (or type clipboard on iOS)',
|
|
125
127
|
'list-options': options_js_1.HELP,
|
|
128
|
+
'web-targets': web_targets_js_1.HELP,
|
|
126
129
|
};
|
|
127
130
|
const OPTIONS_HELP = `Options:
|
|
128
131
|
--device <id> Target device ID (also keys the session and daemon)
|
|
129
132
|
--device-name <n> Target a booted device by name (resolved to ID from booted devices)
|
|
130
133
|
--platform <p> Filter to devices of this platform (ios, android, tvos, web)
|
|
134
|
+
--cdp-url <url> Attach the web driver to an existing browser over CDP (e.g. an
|
|
135
|
+
Electron app started with --remote-debugging-port). Remembered per session.
|
|
136
|
+
--cdp-target <id> Pick which CDP page target to control (see \`conductor web-targets\`)
|
|
131
137
|
--json Output as machine-readable JSON
|
|
132
138
|
--options List valid values for a command's enumerated parameters and exit
|
|
133
139
|
--verbose, -v Log daemon calls, fallbacks, and raw output
|
|
@@ -230,6 +236,8 @@ async function main() {
|
|
|
230
236
|
'height',
|
|
231
237
|
'user-agent',
|
|
232
238
|
'color-scheme',
|
|
239
|
+
'cdp-url',
|
|
240
|
+
'cdp-target',
|
|
233
241
|
],
|
|
234
242
|
alias: { h: 'help', v: 'verbose', V: 'version', o: 'output', y: 'yes' },
|
|
235
243
|
});
|
|
@@ -274,6 +282,7 @@ async function main() {
|
|
|
274
282
|
'metro',
|
|
275
283
|
'workspace',
|
|
276
284
|
'list-options',
|
|
285
|
+
'web-targets',
|
|
277
286
|
// `logs --list` and `logs --source metro` only query Metro on localhost — no device needed
|
|
278
287
|
// `logs` always needs a device session — Metro discovery is device-scoped.
|
|
279
288
|
// `daemon-stop --all` stops every daemon — no device needed
|
|
@@ -309,8 +318,38 @@ async function main() {
|
|
|
309
318
|
explicitDevice ?? (await (0, device_picker_js_1.pickDevice)(argv['platform'])) ?? 'default';
|
|
310
319
|
}
|
|
311
320
|
}
|
|
321
|
+
// CDP attach settings (web only): --cdp-url/--cdp-target map to the env the daemon
|
|
322
|
+
// reads. Passing them once persists them to the session so later commands for the
|
|
323
|
+
// same --device don't need the flags; absent flags hydrate from the saved session.
|
|
324
|
+
const isWebSession = sessionName === 'web' || sessionName.startsWith('web:');
|
|
325
|
+
if (isWebSession && !NO_DEVICE_COMMANDS.has(command)) {
|
|
326
|
+
const cdpUrlFlag = argv['cdp-url'];
|
|
327
|
+
const cdpTargetFlag = argv['cdp-target'];
|
|
328
|
+
if (cdpUrlFlag || cdpTargetFlag) {
|
|
329
|
+
if (cdpUrlFlag)
|
|
330
|
+
process.env.CONDUCTOR_CDP_URL = cdpUrlFlag;
|
|
331
|
+
if (cdpTargetFlag)
|
|
332
|
+
process.env.CONDUCTOR_CDP_TARGET_ID = cdpTargetFlag;
|
|
333
|
+
await (0, session_js_2.updateSession)({
|
|
334
|
+
cdpUrl: process.env.CONDUCTOR_CDP_URL,
|
|
335
|
+
cdpTargetId: process.env.CONDUCTOR_CDP_TARGET_ID,
|
|
336
|
+
}, sessionName);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
const saved = await (0, session_js_2.getSession)(sessionName);
|
|
340
|
+
if (saved.cdpUrl && !process.env.CONDUCTOR_CDP_URL) {
|
|
341
|
+
process.env.CONDUCTOR_CDP_URL = saved.cdpUrl;
|
|
342
|
+
}
|
|
343
|
+
if (saved.cdpTargetId && !process.env.CONDUCTOR_CDP_TARGET_ID) {
|
|
344
|
+
process.env.CONDUCTOR_CDP_TARGET_ID = saved.cdpTargetId;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
312
348
|
let exitCode = 0;
|
|
313
349
|
switch (command) {
|
|
350
|
+
case 'web-targets':
|
|
351
|
+
exitCode = await (0, web_targets_js_1.webTargets)(argv['cdp-url'], opts);
|
|
352
|
+
break;
|
|
314
353
|
case 'start-device':
|
|
315
354
|
exitCode = await (0, start_device_js_1.startDevice)(argv['platform'], opts, {
|
|
316
355
|
osVersion: argv['os-version'],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: conductor-device-setup
|
|
3
|
-
description: Boot, list, and manage devices and app installs for the conductor CLI — iOS simulators, Android emulators, tvOS simulators, and Playwright web browsers — plus sessions, the warm-driver daemon, and the parallel device pool. Use when starting or stopping a simulator/emulator/browser, installing or launching an app, setting up the web driver, keeping the driver warm, or coordinating multiple devices for parallel agents.
|
|
3
|
+
description: Boot, list, and manage devices and app installs for the conductor CLI — iOS simulators, Android emulators, tvOS simulators, and Playwright web browsers — plus sessions, the warm-driver daemon, and the parallel device pool. Use when starting or stopping a simulator/emulator/browser, installing or launching an app, setting up the web driver, attaching to an already-running browser over CDP (e.g. an Electron app / its webviews), keeping the driver warm, or coordinating multiple devices for parallel agents.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Conductor — device & app setup
|
|
@@ -19,27 +19,44 @@ conductor list-apps # installed app ids / package names
|
|
|
19
19
|
|
|
20
20
|
## Devices
|
|
21
21
|
|
|
22
|
-
| Command
|
|
23
|
-
|
|
24
|
-
| `conductor start-device --platform <ios\|android\|tvos\|web>`
|
|
25
|
-
| `conductor start-device --os-version <n> --device-type <name>`
|
|
26
|
-
| `conductor stop-device [<name-or-id>] [--all]`
|
|
27
|
-
| `conductor delete-device <name-or-id> [--all]`
|
|
28
|
-
| `conductor set-location --lat <n> --lng <n>`
|
|
29
|
-
| `conductor set-orientation <portrait\|landscape>`
|
|
30
|
-
| `conductor set-viewport [<w> <h>] [--preset mobile\|tablet\|desktop]` | Resize web viewport (web only)
|
|
31
|
-
| `conductor install-web [--check] [browser]`
|
|
22
|
+
| Command | Purpose |
|
|
23
|
+
| --------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
24
|
+
| `conductor start-device --platform <ios\|android\|tvos\|web>` | Boot a simulator/emulator or start the web driver |
|
|
25
|
+
| `conductor start-device --os-version <n> --device-type <name>` | Pick OS version + device type (creates if needed) |
|
|
26
|
+
| `conductor stop-device [<name-or-id>] [--all]` | Shut down device(s) |
|
|
27
|
+
| `conductor delete-device <name-or-id> [--all]` | Delete simulator(s)/AVD(s)/web session(s) |
|
|
28
|
+
| `conductor set-location --lat <n> --lng <n>` | Set GPS coordinates |
|
|
29
|
+
| `conductor set-orientation <portrait\|landscape>` | Set orientation |
|
|
30
|
+
| `conductor set-viewport [<w> <h>] [--preset mobile\|tablet\|desktop]` | Resize web viewport (web only) |
|
|
31
|
+
| `conductor install-web [--check] [browser]` | Install a Playwright browser (chromium/firefox/webkit); `--check` = status |
|
|
32
|
+
|
|
33
|
+
### Attach to an existing browser (CDP)
|
|
34
|
+
|
|
35
|
+
Instead of launching its own browser, the web driver can attach to one that's
|
|
36
|
+
already running and exposes CDP over a remote-debugging port — e.g. an Electron
|
|
37
|
+
app started with `--remote-debugging-port`, where each window / webview is a
|
|
38
|
+
separate page target you can drive independently.
|
|
39
|
+
|
|
40
|
+
| Command | Purpose |
|
|
41
|
+
| ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
42
|
+
| `conductor web-targets --cdp-url <url>` | List the controllable page targets the browser exposes (id, url, title) + a paste-ready bind command for each |
|
|
43
|
+
| `conductor --device web:<browser>:<label> --cdp-url <url> --cdp-target <id> <cmd>` | Bind a session to one target; the attachment persists so later commands for that `--device` don't need the flags |
|
|
44
|
+
|
|
45
|
+
Use a distinct fully-qualified `--device web:chromium:<label>` per target (a bare
|
|
46
|
+
`web` gets an auto-generated sub-id instead). Each target is its own session, so
|
|
47
|
+
several webviews can be driven concurrently. Only `type=page` targets are
|
|
48
|
+
controllable. See [Web testing → Attaching to an existing browser](../../../docs/web.md).
|
|
32
49
|
|
|
33
50
|
## App lifecycle
|
|
34
51
|
|
|
35
|
-
| Command
|
|
36
|
-
|
|
37
|
-
| `conductor install-app <path>`
|
|
38
|
-
| `conductor launch-app <appId>`
|
|
39
|
-
| `conductor stop-app [<appId>]`
|
|
40
|
-
| `conductor uninstall-app <appId>`
|
|
41
|
-
| `conductor copy-app <bundleId> --from <id> --to <id>` | Copy an installed app between iOS simulators
|
|
42
|
-
| `conductor download-app <appId> --output <path>`
|
|
52
|
+
| Command | Purpose |
|
|
53
|
+
| ----------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
54
|
+
| `conductor install-app <path>` | Install .app / .ipa / .apk |
|
|
55
|
+
| `conductor launch-app <appId>` | Launch app (saved to session); `--no-stop-app`, `--argument key=value` |
|
|
56
|
+
| `conductor stop-app [<appId>]` | Stop app |
|
|
57
|
+
| `conductor uninstall-app <appId>` | Uninstall app |
|
|
58
|
+
| `conductor copy-app <bundleId> --from <id> --to <id>` | Copy an installed app between iOS simulators |
|
|
59
|
+
| `conductor download-app <appId> --output <path>` | Download installed app binary |
|
|
43
60
|
|
|
44
61
|
### ⚠️ Destructive flags — ask the user first
|
|
45
62
|
|
|
@@ -54,15 +71,15 @@ you genuinely need one, ask the human first.
|
|
|
54
71
|
A **session** remembers the last device + app so you don't re-specify them.
|
|
55
72
|
Parallel agents each get their own `--session <name>` so they don't collide.
|
|
56
73
|
|
|
57
|
-
| Command
|
|
58
|
-
|
|
59
|
-
| `conductor session [--clear] [--list]` | Show, clear, or list sessions
|
|
60
|
-
| `conductor daemon-start`
|
|
61
|
-
| `conductor daemon-status`
|
|
62
|
-
| `conductor daemon-stop [--all]`
|
|
63
|
-
| `conductor device-pool --list`
|
|
64
|
-
| `conductor device-pool --acquire`
|
|
65
|
-
| `conductor device-pool --release <id>` | Release a device back to the pool
|
|
74
|
+
| Command | Purpose |
|
|
75
|
+
| -------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
76
|
+
| `conductor session [--clear] [--list]` | Show, clear, or list sessions |
|
|
77
|
+
| `conductor daemon-start` | Start the per-session background daemon (keeps the driver warm — do this for any multi-step session) |
|
|
78
|
+
| `conductor daemon-status` | Show daemon status |
|
|
79
|
+
| `conductor daemon-stop [--all]` | Stop this session's daemon (`--all` = every session) |
|
|
80
|
+
| `conductor device-pool --list` | List devices + pool status |
|
|
81
|
+
| `conductor device-pool --acquire` | Claim a free device (prints id) |
|
|
82
|
+
| `conductor device-pool --release <id>` | Release a device back to the pool |
|
|
66
83
|
|
|
67
84
|
Don't leave a daemon running when you're done — `daemon-stop` it.
|
|
68
85
|
|