@houwert/conductor 0.33.0 → 0.33.1
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/dist/commands/screenshot.js +54 -2
- package/dist/drivers/devicectl.js +24 -0
- package/dist/drivers/ios-displays.js +51 -0
- package/dist/enum-options.js +11 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
- package/skills/conductor-device-setup/SKILL.md +5 -2
- package/skills/conductor-inspect/SKILL.md +1 -1
|
@@ -5,8 +5,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.HELP = void 0;
|
|
7
7
|
exports.screenshot = screenshot;
|
|
8
|
-
exports.HELP = ` take-screenshot [<element>] [--output <path>] [--full-page]
|
|
8
|
+
exports.HELP = ` take-screenshot [<element>] [--output <path>] [--full-page] [--display <panel>]
|
|
9
9
|
Take screenshot (--full-page: web only, capture entire scrollable page)
|
|
10
|
+
--display <cover|inner|id> Which display to capture (default: whichever panel
|
|
11
|
+
is live). An unknown value lists the device's displays
|
|
10
12
|
<element> Crop to the element matched by text (positional)
|
|
11
13
|
--id <id> Crop to the element matched by accessibility id
|
|
12
14
|
--text <text> Crop to the element matched by text only (not id)
|
|
@@ -21,9 +23,14 @@ exports.HELP = ` take-screenshot [<element>] [--output <path>] [--full-page]
|
|
|
21
23
|
--left-of <text> Match element left of the given reference
|
|
22
24
|
--right-of <text> Match element right of the given reference`;
|
|
23
25
|
const path_1 = __importDefault(require("path"));
|
|
26
|
+
const os_1 = __importDefault(require("os"));
|
|
24
27
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
28
|
+
const child_process_1 = require("child_process");
|
|
29
|
+
const util_1 = require("util");
|
|
25
30
|
const runner_js_1 = require("../runner.js");
|
|
26
31
|
const output_js_1 = require("../output.js");
|
|
32
|
+
const devicectl_js_1 = require("../drivers/devicectl.js");
|
|
33
|
+
const ios_displays_js_1 = require("../drivers/ios-displays.js");
|
|
27
34
|
const ios_js_1 = require("../drivers/ios.js");
|
|
28
35
|
const android_js_1 = require("../drivers/android.js");
|
|
29
36
|
const web_js_1 = require("../drivers/web.js");
|
|
@@ -33,6 +40,51 @@ const wait_js_1 = require("../drivers/wait.js");
|
|
|
33
40
|
const direct_ios_selector_js_1 = require("../drivers/direct-ios-selector.js");
|
|
34
41
|
const png_crop_js_1 = require("../png-crop.js");
|
|
35
42
|
const DEFAULT_MARGIN_PX = 8;
|
|
43
|
+
const exec = (0, util_1.promisify)(child_process_1.execFile);
|
|
44
|
+
/**
|
|
45
|
+
* Grab the screen, pointing at the right panel on a multi-display device.
|
|
46
|
+
*
|
|
47
|
+
* The driver always screenshots `XCUIScreen.main`, which on a foldable is the
|
|
48
|
+
* cover panel — powered off, and so a black image, whenever the device is
|
|
49
|
+
* unfolded. When the device reports more than one integrated panel we capture
|
|
50
|
+
* the live one through simctl instead. Ordinary devices keep the driver path.
|
|
51
|
+
*/
|
|
52
|
+
async function captureScreen(driver, opts, displayOverride) {
|
|
53
|
+
const deviceId = driver instanceof ios_js_1.IOSDriver ? driver.deviceId : undefined;
|
|
54
|
+
if (!deviceId || (!displayOverride && !(driver instanceof ios_js_1.IOSDriver))) {
|
|
55
|
+
return await driver.screenshot(opts);
|
|
56
|
+
}
|
|
57
|
+
const displays = await (0, devicectl_js_1.listDisplays)(deviceId).catch(() => []);
|
|
58
|
+
if (!displays.length) {
|
|
59
|
+
if (displayOverride)
|
|
60
|
+
throw new Error("could not read this device's displays");
|
|
61
|
+
return await driver.screenshot(opts);
|
|
62
|
+
}
|
|
63
|
+
const choice = (0, ios_displays_js_1.pickCaptureDisplay)(displays, displayOverride);
|
|
64
|
+
if (choice.error)
|
|
65
|
+
throw new Error(choice.error);
|
|
66
|
+
const primary = displays.find((d) => d.primary);
|
|
67
|
+
// Nothing to redirect: the driver already captures the primary panel.
|
|
68
|
+
if (choice.displayId === null || (primary && choice.displayId === primary.displayId)) {
|
|
69
|
+
return await driver.screenshot(opts);
|
|
70
|
+
}
|
|
71
|
+
const file = path_1.default.join(os_1.default.tmpdir(), `conductor-shot-${Date.now()}.png`);
|
|
72
|
+
try {
|
|
73
|
+
await exec('xcrun', [
|
|
74
|
+
'simctl',
|
|
75
|
+
'io',
|
|
76
|
+
deviceId,
|
|
77
|
+
'screenshot',
|
|
78
|
+
'--display',
|
|
79
|
+
String(choice.displayId),
|
|
80
|
+
file,
|
|
81
|
+
]);
|
|
82
|
+
return await promises_1.default.readFile(file);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
await promises_1.default.unlink(file).catch(() => { });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
36
88
|
async function screenshot(outputPath, opts = {}, sessionName = 'default', fullPage = false, query = '', flags = {}) {
|
|
37
89
|
const timestamp = Date.now();
|
|
38
90
|
const defaultName = `screenshot-${timestamp}.png`;
|
|
@@ -63,7 +115,7 @@ async function screenshot(outputPath, opts = {}, sessionName = 'default', fullPa
|
|
|
63
115
|
: '';
|
|
64
116
|
const margin = flags.margin ?? DEFAULT_MARGIN_PX;
|
|
65
117
|
const result = await (0, runner_js_1.runDirect)(async (driver) => {
|
|
66
|
-
const buf = await driver
|
|
118
|
+
const buf = await captureScreen(driver, { fullPage }, flags.display);
|
|
67
119
|
let out = buf;
|
|
68
120
|
if (sel) {
|
|
69
121
|
let el;
|
|
@@ -17,6 +17,7 @@ exports.terminateApp = terminateApp;
|
|
|
17
17
|
exports.listApps = listApps;
|
|
18
18
|
exports.getOrientation = getOrientation;
|
|
19
19
|
exports.setOrientation = setOrientation;
|
|
20
|
+
exports.listDisplays = listDisplays;
|
|
20
21
|
/**
|
|
21
22
|
* `xcrun devicectl` wrapper — the physical-device counterpart to `simctl`.
|
|
22
23
|
*
|
|
@@ -269,3 +270,26 @@ async function getOrientation(deviceId) {
|
|
|
269
270
|
async function setOrientation(deviceId, orientation) {
|
|
270
271
|
await devicectl(['device', 'orientation', 'set', '--device', deviceId, orientation]);
|
|
271
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* List the device's displays. Foldables report two integrated panels and flag
|
|
275
|
+
* which one is live, which is the only reliable way to know where to point a
|
|
276
|
+
* screenshot: the swap follows the system's own transition logic, not the
|
|
277
|
+
* hinge angle.
|
|
278
|
+
*/
|
|
279
|
+
async function listDisplays(deviceId) {
|
|
280
|
+
const parsed = await devicectlJson(['device', 'info', 'displays', '--device', deviceId], 15000);
|
|
281
|
+
return (parsed.result?.displays ?? [])
|
|
282
|
+
.filter((d) => typeof d.displayId === 'number')
|
|
283
|
+
.map((d) => {
|
|
284
|
+
// `type` is a single-key object, e.g. { integrated: {} } or { carPlay: {} }.
|
|
285
|
+
const kind = Object.keys(d.type ?? {})[0] ?? '';
|
|
286
|
+
return {
|
|
287
|
+
displayId: d.displayId,
|
|
288
|
+
name: d.name ?? '',
|
|
289
|
+
active: d.active === true,
|
|
290
|
+
primary: d.primary === true,
|
|
291
|
+
kind,
|
|
292
|
+
integrated: kind === 'integrated',
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pickCaptureDisplay = pickCaptureDisplay;
|
|
4
|
+
/**
|
|
5
|
+
* Roles, which mean the same thing on any platform that grows a foldable.
|
|
6
|
+
* Everything else is addressed by display id: a device's own class and panel
|
|
7
|
+
* names (integrated/carPlay, LCD-1) are platform vocabulary that wouldn't carry
|
|
8
|
+
* over to Android, and every display it could name already has an id. Unknown
|
|
9
|
+
* values list the device's displays so the ids are discoverable.
|
|
10
|
+
*/
|
|
11
|
+
const ROLE_ALIASES = {
|
|
12
|
+
cover: 'primary',
|
|
13
|
+
main: 'primary',
|
|
14
|
+
primary: 'primary',
|
|
15
|
+
inner: 'secondary',
|
|
16
|
+
secondary: 'secondary',
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Pick the display to capture. With no override this is whichever panel is
|
|
20
|
+
* live, which keeps screenshots working across a fold without the caller
|
|
21
|
+
* having to think about it. Returns null for ordinary single-display devices
|
|
22
|
+
* so they keep using the driver path untouched.
|
|
23
|
+
*/
|
|
24
|
+
function pickCaptureDisplay(displays, override) {
|
|
25
|
+
const integrated = displays.filter((d) => d.integrated);
|
|
26
|
+
if (override) {
|
|
27
|
+
const key = override.trim().toLowerCase();
|
|
28
|
+
const id = Number(key);
|
|
29
|
+
if (Number.isInteger(id) && displays.some((d) => d.displayId === id)) {
|
|
30
|
+
return { displayId: id };
|
|
31
|
+
}
|
|
32
|
+
const role = ROLE_ALIASES[key];
|
|
33
|
+
if (role) {
|
|
34
|
+
const match = role === 'primary' ? integrated.find((d) => d.primary) : integrated.find((d) => !d.primary);
|
|
35
|
+
if (match)
|
|
36
|
+
return { displayId: match.displayId };
|
|
37
|
+
}
|
|
38
|
+
const known = displays
|
|
39
|
+
.map((d) => `${d.displayId} (${[d.name, d.kind].filter(Boolean).join(', ')})`)
|
|
40
|
+
.join('; ');
|
|
41
|
+
return {
|
|
42
|
+
displayId: null,
|
|
43
|
+
error: `this device has no '${override}' display — it reports: ${known}`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Single-panel devices: nothing to choose, let the driver handle it.
|
|
47
|
+
if (integrated.length < 2)
|
|
48
|
+
return { displayId: null };
|
|
49
|
+
const live = integrated.find((d) => d.active) ?? integrated.find((d) => d.primary);
|
|
50
|
+
return { displayId: live ? live.displayId : null };
|
|
51
|
+
}
|
package/dist/enum-options.js
CHANGED
|
@@ -59,6 +59,17 @@ exports.ENUM_PARAMS = [
|
|
|
59
59
|
{ value: 'faceDown', description: 'iOS only' },
|
|
60
60
|
],
|
|
61
61
|
},
|
|
62
|
+
{
|
|
63
|
+
command: 'take-screenshot',
|
|
64
|
+
param: '--display',
|
|
65
|
+
description: 'Which display to capture (default: whichever panel is live)',
|
|
66
|
+
// Source: ROLE_ALIASES in drivers/ios-displays.ts. Any display id the
|
|
67
|
+
// device reports is also accepted, so the list is not exhaustive.
|
|
68
|
+
values: [
|
|
69
|
+
{ value: 'cover', description: 'Foldable outer panel (XCUIScreen.main)' },
|
|
70
|
+
{ value: 'inner', description: 'Foldable inner panel, live when unfolded' },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
62
73
|
{
|
|
63
74
|
command: 'set-fold',
|
|
64
75
|
param: '<state>',
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -39,8 +39,11 @@ conductor list-apps # installed app ids / package names (--json adds ap
|
|
|
39
39
|
`set-fold` drives the hinge the same way Device Hub's slider does, so the device
|
|
40
40
|
really folds: SpringBoard swaps between the cover and inner displays, and
|
|
41
41
|
`devicectl device motion hinge-angle` reports the new angle. `take-screenshot`
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
follows the fold automatically — it captures whichever panel is live, so an
|
|
43
|
+
unfolded device gives you the inner screen rather than the powered-off cover.
|
|
44
|
+
Pass `--display cover` or `--display inner` to pin it to one panel, or a display
|
|
45
|
+
id for anything else the device has attached (CarPlay, an external screen). An
|
|
46
|
+
unknown value lists that device's displays with their ids.
|
|
44
47
|
|
|
45
48
|
Angles are in degrees, 0 (shut) to 180 (flat). `closed`/`book`/`open` map to
|
|
46
49
|
0/130/180. Named poses always swap the display; an arbitrary mid-way angle sets
|
|
@@ -17,7 +17,7 @@ here. Always observe before you act, and confirm after.
|
|
|
17
17
|
| `conductor inspect [--dump]` | Print the UI hierarchy (`--dump` = raw driver output) |
|
|
18
18
|
| `conductor inspect --at <x,y> [--tappable]` | Topmost view at a screen point |
|
|
19
19
|
| `conductor focused [--poll [ms]]` | Metadata of the focused element. `--poll` watches changes — only with a bounded use, then stop it |
|
|
20
|
-
| `conductor take-screenshot [<element>] [--output <path>] [--full-page]` | Screenshot; crop to a matched element; `--full-page` (web) |
|
|
20
|
+
| `conductor take-screenshot [<element>] [--output <path>] [--full-page] [--display <panel>]` | Screenshot; crop to a matched element; `--full-page` (web); `--display cover\|inner\|<id>` picks a display (default: whichever panel is live) |
|
|
21
21
|
|
|
22
22
|
`capture-ui` is the workhorse: it returns the screen as structured data **and**
|
|
23
23
|
gives each element a ref like `@e3` that `conductor tap-on @e3` taps by cached
|