@houwert/conductor 0.32.1 → 0.33.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/dist/commands/fold.js +69 -0
- package/dist/commands/get-orientation.js +40 -0
- package/dist/commands/set-orientation.js +59 -10
- package/dist/drivers/bootstrap.js +13 -0
- package/dist/drivers/devicectl.js +28 -0
- package/dist/drivers/ios-fold.js +209 -0
- package/dist/enum-options.js +21 -2
- package/dist/index.js +18 -0
- package/package.json +1 -1
- package/skills/conductor-device-setup/SKILL.md +27 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HELP = void 0;
|
|
4
|
+
exports.getFold = getFold;
|
|
5
|
+
exports.setFold = setFold;
|
|
6
|
+
exports.HELP = ` get-fold Print the hinge angle of a foldable device (iPhone Duo)
|
|
7
|
+
set-fold <closed|book|open|0-180> Fold, half-open, unfold, or set an exact hinge angle`;
|
|
8
|
+
const runner_js_1 = require("../runner.js");
|
|
9
|
+
const session_js_1 = require("../session.js");
|
|
10
|
+
const output_js_1 = require("../output.js");
|
|
11
|
+
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
12
|
+
const ios_fold_js_1 = require("../drivers/ios-fold.js");
|
|
13
|
+
async function resolveDeviceId(sessionName) {
|
|
14
|
+
if (sessionName !== 'default')
|
|
15
|
+
return sessionName;
|
|
16
|
+
const session = await (0, session_js_1.getSession)(sessionName);
|
|
17
|
+
return session.deviceId ?? (await (0, runner_js_1.detectFirstDevice)());
|
|
18
|
+
}
|
|
19
|
+
/** Fold control talks to the device directly, so no test driver is needed. */
|
|
20
|
+
async function foldableDeviceId(sessionName, opts) {
|
|
21
|
+
const deviceId = await resolveDeviceId(sessionName);
|
|
22
|
+
if (!deviceId) {
|
|
23
|
+
(0, output_js_1.printError)('No device found. Connect a device or start a simulator first.', opts);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId);
|
|
27
|
+
if (platform !== 'ios') {
|
|
28
|
+
(0, output_js_1.printError)(`fold control is iOS-only — foldable simulators such as the iPhone Duo`, opts);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return deviceId;
|
|
32
|
+
}
|
|
33
|
+
async function getFold(opts = {}, sessionName = 'default') {
|
|
34
|
+
const deviceId = await foldableDeviceId(sessionName, opts);
|
|
35
|
+
if (!deviceId)
|
|
36
|
+
return 1;
|
|
37
|
+
try {
|
|
38
|
+
const state = await (0, ios_fold_js_1.getFoldState)(deviceId);
|
|
39
|
+
if (opts.json)
|
|
40
|
+
(0, output_js_1.printData)(state, opts);
|
|
41
|
+
else
|
|
42
|
+
(0, output_js_1.printSuccess)(`fold: ${state.pose} (${state.angle}°)`, opts);
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
(0, output_js_1.printError)(`get-fold — failed\n${err instanceof Error ? err.message : String(err)}`, opts);
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function setFold(value, opts = {}, sessionName = 'default') {
|
|
51
|
+
const key = value.trim().toLowerCase();
|
|
52
|
+
const angle = (0, ios_fold_js_1.resolveFoldAngle)(value);
|
|
53
|
+
if (angle === null) {
|
|
54
|
+
(0, output_js_1.printError)(`set-fold must be one of: ${Object.keys(ios_fold_js_1.FOLD_POSES).join(', ')}, or an angle from 0 to 180`, opts);
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
const deviceId = await foldableDeviceId(sessionName, opts);
|
|
58
|
+
if (!deviceId)
|
|
59
|
+
return 1;
|
|
60
|
+
try {
|
|
61
|
+
await (0, ios_fold_js_1.setFoldAngle)(deviceId, angle);
|
|
62
|
+
(0, output_js_1.printSuccess)(`set-fold ${key in ios_fold_js_1.FOLD_POSES ? key : angle} (${angle}°) — done`, opts);
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
(0, output_js_1.printError)(`set-fold ${value} — failed\n${err instanceof Error ? err.message : String(err)}`, opts);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HELP = void 0;
|
|
4
|
+
exports.getOrientation = getOrientation;
|
|
5
|
+
exports.HELP = ` get-orientation Print the device's current orientation`;
|
|
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
|
+
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
10
|
+
const devicectl_js_1 = require("../drivers/devicectl.js");
|
|
11
|
+
async function resolveDeviceId(sessionName) {
|
|
12
|
+
if (sessionName !== 'default')
|
|
13
|
+
return sessionName;
|
|
14
|
+
const session = await (0, session_js_1.getSession)(sessionName);
|
|
15
|
+
return session.deviceId ?? (await (0, runner_js_1.detectFirstDevice)());
|
|
16
|
+
}
|
|
17
|
+
async function getOrientation(opts = {}, sessionName = 'default') {
|
|
18
|
+
const deviceId = await resolveDeviceId(sessionName);
|
|
19
|
+
if (!deviceId) {
|
|
20
|
+
(0, output_js_1.printError)('No device found. Connect a device or start a simulator first.', opts);
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId);
|
|
24
|
+
if (platform !== 'ios') {
|
|
25
|
+
(0, output_js_1.printError)('get-orientation is iOS-only (simulators and physical devices)', opts);
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const orientation = await (0, devicectl_js_1.getOrientation)(deviceId);
|
|
30
|
+
if (opts.json)
|
|
31
|
+
(0, output_js_1.printData)({ orientation }, opts);
|
|
32
|
+
else
|
|
33
|
+
(0, output_js_1.printSuccess)(`orientation: ${orientation}`, opts);
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
(0, output_js_1.printError)(`get-orientation — failed\n${err instanceof Error ? err.message : String(err)}`, opts);
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -2,24 +2,73 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HELP = void 0;
|
|
4
4
|
exports.setOrientation = setOrientation;
|
|
5
|
-
exports.HELP = ` set-orientation <
|
|
5
|
+
exports.HELP = ` set-orientation <orientation> Set device orientation
|
|
6
|
+
portrait | landscape (all platforms)
|
|
7
|
+
portraitUpsideDown | landscapeLeft | landscapeRight |
|
|
8
|
+
faceUp | faceDown (iOS only)`;
|
|
6
9
|
const runner_js_1 = require("../runner.js");
|
|
10
|
+
const session_js_1 = require("../session.js");
|
|
7
11
|
const output_js_1 = require("../output.js");
|
|
8
|
-
const
|
|
12
|
+
const bootstrap_js_1 = require("../drivers/bootstrap.js");
|
|
13
|
+
const devicectl_js_1 = require("../drivers/devicectl.js");
|
|
14
|
+
const ios_fold_js_1 = require("../drivers/ios-fold.js");
|
|
15
|
+
/** Accepted everywhere; other platforms handle these through their driver. */
|
|
16
|
+
const BASIC = {
|
|
17
|
+
portrait: 'portrait',
|
|
18
|
+
landscape: 'landscapeLeft',
|
|
19
|
+
};
|
|
20
|
+
/** iOS-only orientations, which the test driver has no concept of. */
|
|
21
|
+
const IOS_ONLY = {
|
|
22
|
+
portraitupsidedown: 'portraitUpsideDown',
|
|
23
|
+
landscapeleft: 'landscapeLeft',
|
|
24
|
+
landscaperight: 'landscapeRight',
|
|
25
|
+
faceup: 'faceUp',
|
|
26
|
+
facedown: 'faceDown',
|
|
27
|
+
};
|
|
9
28
|
async function setOrientation(orientation, opts = {}, sessionName = 'default') {
|
|
10
|
-
|
|
11
|
-
|
|
29
|
+
const key = orientation.toLowerCase();
|
|
30
|
+
const iosValue = BASIC[key] ?? IOS_ONLY[key];
|
|
31
|
+
if (!iosValue) {
|
|
32
|
+
(0, output_js_1.printError)(`set-orientation must be one of: ${[...Object.keys(BASIC), ...Object.values(IOS_ONLY)].join(', ')}`, opts);
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
const deviceId = sessionName !== 'default'
|
|
36
|
+
? sessionName
|
|
37
|
+
: ((await (0, session_js_1.getSession)(sessionName)).deviceId ?? (await (0, runner_js_1.detectFirstDevice)()));
|
|
38
|
+
if (!deviceId) {
|
|
39
|
+
(0, output_js_1.printError)('No device found. Connect a device or start a simulator first.', opts);
|
|
40
|
+
return 1;
|
|
41
|
+
}
|
|
42
|
+
const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId);
|
|
43
|
+
// On iOS everything goes through devicectl: it covers the full set and needs
|
|
44
|
+
// no test driver running. Other platforms keep using their driver.
|
|
45
|
+
if (platform === 'ios') {
|
|
46
|
+
try {
|
|
47
|
+
await (0, devicectl_js_1.setOrientation)(deviceId, iosValue);
|
|
48
|
+
// Foldables accept the call and ignore it — their pose machine owns
|
|
49
|
+
// orientation — so confirm, and rotate through the relay if it didn't take.
|
|
50
|
+
if ((await (0, devicectl_js_1.getOrientation)(deviceId).catch(() => null)) !== iosValue) {
|
|
51
|
+
await (0, ios_fold_js_1.setOrientationViaRelay)(deviceId, iosValue);
|
|
52
|
+
}
|
|
53
|
+
(0, output_js_1.printSuccess)(`set-orientation ${iosValue} — done`, opts);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
(0, output_js_1.printError)(`set-orientation ${orientation} — failed\n${err instanceof Error ? err.message : String(err)}`, opts);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!BASIC[key]) {
|
|
62
|
+
(0, output_js_1.printError)(`${iosValue} is iOS-only — use portrait or landscape on this platform`, opts);
|
|
12
63
|
return 1;
|
|
13
64
|
}
|
|
14
65
|
const result = await (0, runner_js_1.runDirect)(async (driver) => {
|
|
15
|
-
await driver.setOrientation(
|
|
66
|
+
await driver.setOrientation(key);
|
|
16
67
|
}, sessionName);
|
|
17
68
|
if (result.success) {
|
|
18
|
-
(0, output_js_1.printSuccess)(`set-orientation ${
|
|
69
|
+
(0, output_js_1.printSuccess)(`set-orientation ${key} — done`, opts);
|
|
19
70
|
return 0;
|
|
20
71
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return 1;
|
|
24
|
-
}
|
|
72
|
+
(0, output_js_1.printError)(`set-orientation ${orientation} — failed\n${result.stderr}`, opts);
|
|
73
|
+
return 1;
|
|
25
74
|
}
|
|
@@ -11,6 +11,7 @@ exports.getInputPort = getInputPort;
|
|
|
11
11
|
exports.getStreamPort = getStreamPort;
|
|
12
12
|
exports.getInprocDylibPath = getInprocDylibPath;
|
|
13
13
|
exports.getHidBinaryPath = getHidBinaryPath;
|
|
14
|
+
exports.getFoldDylibPath = getFoldDylibPath;
|
|
14
15
|
exports.getCaptureBinaryPath = getCaptureBinaryPath;
|
|
15
16
|
exports.installDriver = installDriver;
|
|
16
17
|
exports.isSimulatorBooted = isSimulatorBooted;
|
|
@@ -363,6 +364,18 @@ async function getHidBinaryPath() {
|
|
|
363
364
|
const p = path_1.default.join(dir, 'ios-hid', 'conductor-hid');
|
|
364
365
|
return fs_1.default.existsSync(p) ? p : null;
|
|
365
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* Absolute path to the fold controller dylib (`ios-fold/conductor-fold.dylib`),
|
|
369
|
+
* built by `packages/ios-fold/tools/build-fold.sh`. Injected into a foldable
|
|
370
|
+
* simulator's locationd to drive the hinge. Returns null if it hasn't been built.
|
|
371
|
+
*/
|
|
372
|
+
async function getFoldDylibPath() {
|
|
373
|
+
const dir = await getDriversDir().catch(() => null);
|
|
374
|
+
if (!dir)
|
|
375
|
+
return null;
|
|
376
|
+
const p = path_1.default.join(dir, 'ios-fold', 'conductor-fold.dylib');
|
|
377
|
+
return fs_1.default.existsSync(p) ? p : null;
|
|
378
|
+
}
|
|
366
379
|
/**
|
|
367
380
|
* Absolute path to the host-side Simulator video capture binary
|
|
368
381
|
* (`ios-capture/conductor-capture`), built by
|
|
@@ -3,6 +3,7 @@ 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.DEVICECTL_ORIENTATIONS = void 0;
|
|
6
7
|
exports.parseDevicectlDevices = parseDevicectlDevices;
|
|
7
8
|
exports.listPhysicalDevices = listPhysicalDevices;
|
|
8
9
|
exports.findPhysicalDevice = findPhysicalDevice;
|
|
@@ -14,6 +15,8 @@ exports.uninstallApp = uninstallApp;
|
|
|
14
15
|
exports.launchApp = launchApp;
|
|
15
16
|
exports.terminateApp = terminateApp;
|
|
16
17
|
exports.listApps = listApps;
|
|
18
|
+
exports.getOrientation = getOrientation;
|
|
19
|
+
exports.setOrientation = setOrientation;
|
|
17
20
|
/**
|
|
18
21
|
* `xcrun devicectl` wrapper — the physical-device counterpart to `simctl`.
|
|
19
22
|
*
|
|
@@ -241,3 +244,28 @@ async function listApps(deviceId) {
|
|
|
241
244
|
name: a.name ?? a.bundleIdentifier,
|
|
242
245
|
}));
|
|
243
246
|
}
|
|
247
|
+
/** Orientations devicectl accepts; a superset of the driver's portrait/landscape. */
|
|
248
|
+
exports.DEVICECTL_ORIENTATIONS = [
|
|
249
|
+
'portrait',
|
|
250
|
+
'portraitUpsideDown',
|
|
251
|
+
'landscapeLeft',
|
|
252
|
+
'landscapeRight',
|
|
253
|
+
'faceUp',
|
|
254
|
+
'faceDown',
|
|
255
|
+
];
|
|
256
|
+
/**
|
|
257
|
+
* Read the device's current orientation. Works for simulators as well as
|
|
258
|
+
* physical devices — CoreDevice covers both — and needs no running test driver.
|
|
259
|
+
*/
|
|
260
|
+
async function getOrientation(deviceId) {
|
|
261
|
+
const out = await devicectl(['device', 'orientation', 'get', '--device', deviceId]);
|
|
262
|
+
// Devices that track flat poses report "Non-flat Orientation" instead.
|
|
263
|
+
const match = out.match(/Current Device (?:Non-flat )?Orientation:\s*(\w+)/);
|
|
264
|
+
if (!match)
|
|
265
|
+
throw new Error(`could not parse orientation from devicectl: ${out.trim()}`);
|
|
266
|
+
return match[1];
|
|
267
|
+
}
|
|
268
|
+
/** Set the device's orientation. */
|
|
269
|
+
async function setOrientation(deviceId, orientation) {
|
|
270
|
+
await devicectl(['device', 'orientation', 'set', '--device', deviceId, orientation]);
|
|
271
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
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.RELAY_ORIENTATIONS = exports.FOLD_POSES = void 0;
|
|
7
|
+
exports.resolveFoldAngle = resolveFoldAngle;
|
|
8
|
+
exports.poseForAngle = poseForAngle;
|
|
9
|
+
exports.ensureFoldControllerInjected = ensureFoldControllerInjected;
|
|
10
|
+
exports.setFoldAngle = setFoldAngle;
|
|
11
|
+
exports.setOrientationViaRelay = setOrientationViaRelay;
|
|
12
|
+
exports.getFoldState = getFoldState;
|
|
13
|
+
/**
|
|
14
|
+
* Fold (hinge) control for foldable simulators, e.g. the iPhone Duo.
|
|
15
|
+
*
|
|
16
|
+
* Apple exposes no setter: `simctl` has nothing, and `devicectl` only *reads*
|
|
17
|
+
* the angle. The fold is driven by an AVP event that travels Device Hub ->
|
|
18
|
+
* CoreDevice remote HID -> the guest's dtuhidd -> locationd, whose CoreMotion
|
|
19
|
+
* relay synthesises the hinge HID event SpringBoard folds on. Conductor's own
|
|
20
|
+
* Indigo channel cannot carry it (backboardd rejects hinge events), so we
|
|
21
|
+
* inject `drivers/ios-fold/conductor-fold.dylib` into the simulator's locationd
|
|
22
|
+
* and feed that relay directly — see packages/ios-fold for the full rationale.
|
|
23
|
+
*
|
|
24
|
+
* Injection lasts until locationd restarts, so every call re-checks the pid the
|
|
25
|
+
* dylib recorded and re-injects when it no longer matches.
|
|
26
|
+
*/
|
|
27
|
+
const fs_1 = __importDefault(require("fs"));
|
|
28
|
+
const child_process_1 = require("child_process");
|
|
29
|
+
const util_1 = require("util");
|
|
30
|
+
const bootstrap_js_1 = require("./bootstrap.js");
|
|
31
|
+
const exec = (0, util_1.promisify)(child_process_1.execFile);
|
|
32
|
+
/** Named poses, mirroring Device Hub's three hinge buttons. */
|
|
33
|
+
exports.FOLD_POSES = {
|
|
34
|
+
closed: 0,
|
|
35
|
+
book: 130,
|
|
36
|
+
open: 180,
|
|
37
|
+
};
|
|
38
|
+
/** A simulator process's /tmp is the host's /tmp, so scope paths by device. */
|
|
39
|
+
const controlPath = (udid) => `/tmp/conductor-fold-${udid}`;
|
|
40
|
+
const orientationPath = (udid) => `/tmp/conductor-orientation-${udid}`;
|
|
41
|
+
/**
|
|
42
|
+
* The relay's own orientation vocabulary, which is not devicectl's spelling.
|
|
43
|
+
* Foldables ignore `devicectl device orientation set`, so they need this path.
|
|
44
|
+
*/
|
|
45
|
+
exports.RELAY_ORIENTATIONS = {
|
|
46
|
+
portrait: 'portrait',
|
|
47
|
+
portraitUpsideDown: 'pud',
|
|
48
|
+
landscapeLeft: 'landscape-left',
|
|
49
|
+
landscapeRight: 'landscape-right',
|
|
50
|
+
faceUp: 'faceup',
|
|
51
|
+
faceDown: 'facedown',
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a CLI value — a pose name or an angle in degrees — to an angle.
|
|
55
|
+
* Returns null for anything else, including an empty value: `Number('')` is 0,
|
|
56
|
+
* which would otherwise turn a bare `set-fold` into "fold the device shut".
|
|
57
|
+
*/
|
|
58
|
+
function resolveFoldAngle(value) {
|
|
59
|
+
const key = value.trim().toLowerCase();
|
|
60
|
+
if (!key)
|
|
61
|
+
return null;
|
|
62
|
+
if (key in exports.FOLD_POSES)
|
|
63
|
+
return exports.FOLD_POSES[key];
|
|
64
|
+
const angle = Number(key);
|
|
65
|
+
if (!Number.isFinite(angle) || angle < 0 || angle > 180)
|
|
66
|
+
return null;
|
|
67
|
+
return angle;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Name the pose when the angle is close to one, else call it partial. Which
|
|
71
|
+
* panel is lit is deliberately not derived from the angle: the system swaps
|
|
72
|
+
* displays on its own transition logic, so a hinge parked mid-way can still be
|
|
73
|
+
* driving the cover display.
|
|
74
|
+
*/
|
|
75
|
+
function poseForAngle(angle) {
|
|
76
|
+
for (const [name, value] of Object.entries(exports.FOLD_POSES)) {
|
|
77
|
+
if (Math.abs(value - angle) <= 10)
|
|
78
|
+
return name;
|
|
79
|
+
}
|
|
80
|
+
return 'partial';
|
|
81
|
+
}
|
|
82
|
+
async function locationdPid(udid) {
|
|
83
|
+
try {
|
|
84
|
+
const { stdout } = await exec('xcrun', [
|
|
85
|
+
'simctl',
|
|
86
|
+
'spawn',
|
|
87
|
+
udid,
|
|
88
|
+
'launchctl',
|
|
89
|
+
'print',
|
|
90
|
+
'system/com.apple.locationd',
|
|
91
|
+
]);
|
|
92
|
+
return stdout.match(/^\s*pid = (\d+)/m)?.[1] ?? null;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function recordedPid(udid) {
|
|
99
|
+
try {
|
|
100
|
+
return fs_1.default.readFileSync(`${controlPath(udid)}.pid`, 'utf8').trim() || null;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Ensure the fold controller is live inside locationd. DYLD_INSERT_LIBRARIES is
|
|
108
|
+
* set on the simulator's launchd only for the moment it takes to restart
|
|
109
|
+
* locationd — anything else launched in that window loads the dylib too, which
|
|
110
|
+
* is harmless (it no-ops outside locationd).
|
|
111
|
+
*/
|
|
112
|
+
async function ensureFoldControllerInjected(udid) {
|
|
113
|
+
const pid = await locationdPid(udid);
|
|
114
|
+
if (pid && pid === recordedPid(udid))
|
|
115
|
+
return;
|
|
116
|
+
// Restarting locationd resets CoreMotion's relay to a 0° hinge, which folds
|
|
117
|
+
// the device. Remember where the hinge was so it can be put back.
|
|
118
|
+
const previousAngle = await readHingeAngleViaDevicectl(udid).catch(() => null);
|
|
119
|
+
const dylib = await (0, bootstrap_js_1.getFoldDylibPath)();
|
|
120
|
+
if (!dylib) {
|
|
121
|
+
throw new Error('fold control requires drivers/ios-fold/conductor-fold.dylib — build it with packages/ios-fold/tools/build-fold.sh');
|
|
122
|
+
}
|
|
123
|
+
const spawn = (args) => exec('xcrun', ['simctl', 'spawn', udid, ...args]);
|
|
124
|
+
await spawn(['launchctl', 'setenv', 'DYLD_INSERT_LIBRARIES', dylib]);
|
|
125
|
+
try {
|
|
126
|
+
await spawn(['launchctl', 'kickstart', '-k', 'system/com.apple.locationd']);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
await spawn(['launchctl', 'unsetenv', 'DYLD_INSERT_LIBRARIES']).catch(() => { });
|
|
130
|
+
}
|
|
131
|
+
const deadline = Date.now() + 15000;
|
|
132
|
+
while (Date.now() < deadline) {
|
|
133
|
+
const current = await locationdPid(udid);
|
|
134
|
+
if (current && current === recordedPid(udid)) {
|
|
135
|
+
if (previousAngle !== null) {
|
|
136
|
+
fs_1.default.writeFileSync(controlPath(udid), String(previousAngle));
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
141
|
+
}
|
|
142
|
+
throw new Error('fold controller did not come up inside locationd (is this a foldable device?)');
|
|
143
|
+
}
|
|
144
|
+
/** Apply a hinge angle in degrees (0 = closed, 180 = open flat). */
|
|
145
|
+
async function setFoldAngle(udid, angle) {
|
|
146
|
+
if (!Number.isFinite(angle) || angle < 0 || angle > 180) {
|
|
147
|
+
throw new Error(`fold angle must be between 0 and 180 (got ${angle})`);
|
|
148
|
+
}
|
|
149
|
+
await ensureFoldControllerInjected(udid);
|
|
150
|
+
fs_1.default.writeFileSync(controlPath(udid), String(angle));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Rotate through the injected relay, the way Device Hub's orientation picker
|
|
154
|
+
* does. Only needed where devicectl's setter is ignored.
|
|
155
|
+
*/
|
|
156
|
+
async function setOrientationViaRelay(udid, orientation) {
|
|
157
|
+
const token = exports.RELAY_ORIENTATIONS[orientation];
|
|
158
|
+
if (!token)
|
|
159
|
+
throw new Error(`no relay mapping for orientation '${orientation}'`);
|
|
160
|
+
await ensureFoldControllerInjected(udid);
|
|
161
|
+
fs_1.default.writeFileSync(orientationPath(udid), token);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Read the current fold state. devicectl is the authority: the hinge can be
|
|
165
|
+
* moved from Device Hub as well as by us, so anything we cached locally would
|
|
166
|
+
* only cover the folds this process happened to see.
|
|
167
|
+
*/
|
|
168
|
+
async function getFoldState(udid) {
|
|
169
|
+
const angle = await readHingeAngleViaDevicectl(udid);
|
|
170
|
+
return { angle, pose: poseForAngle(angle) };
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* `devicectl device motion hinge-angle` is a monitor: it prints the current
|
|
174
|
+
* angle immediately but then streams until its session expires, so take the
|
|
175
|
+
* first sample and stop it rather than waiting around.
|
|
176
|
+
*/
|
|
177
|
+
async function readHingeAngleViaDevicectl(udid) {
|
|
178
|
+
return new Promise((resolve, reject) => {
|
|
179
|
+
const proc = (0, child_process_1.spawn)('xcrun', [
|
|
180
|
+
'devicectl',
|
|
181
|
+
'device',
|
|
182
|
+
'motion',
|
|
183
|
+
'hinge-angle',
|
|
184
|
+
'--device',
|
|
185
|
+
udid,
|
|
186
|
+
'--session-timeout',
|
|
187
|
+
'10',
|
|
188
|
+
]);
|
|
189
|
+
let out = '';
|
|
190
|
+
let settled = false;
|
|
191
|
+
const finish = (fn) => {
|
|
192
|
+
if (settled)
|
|
193
|
+
return;
|
|
194
|
+
settled = true;
|
|
195
|
+
clearTimeout(timer);
|
|
196
|
+
proc.kill('SIGTERM');
|
|
197
|
+
fn();
|
|
198
|
+
};
|
|
199
|
+
const timer = setTimeout(() => finish(() => reject(new Error('timed out reading hinge angle'))), 15000);
|
|
200
|
+
proc.stdout.on('data', (chunk) => {
|
|
201
|
+
out += chunk.toString();
|
|
202
|
+
const match = out.match(/Angle:\s*([0-9.]+)/);
|
|
203
|
+
if (match)
|
|
204
|
+
finish(() => resolve(Number(match[1])));
|
|
205
|
+
});
|
|
206
|
+
proc.on('error', (err) => finish(() => reject(err)));
|
|
207
|
+
proc.on('close', () => finish(() => reject(new Error('could not read hinge angle — is this a foldable device?'))));
|
|
208
|
+
});
|
|
209
|
+
}
|
package/dist/enum-options.js
CHANGED
|
@@ -48,8 +48,27 @@ exports.ENUM_PARAMS = [
|
|
|
48
48
|
command: 'set-orientation',
|
|
49
49
|
param: '<orientation>',
|
|
50
50
|
description: 'Device orientation',
|
|
51
|
-
// Source:
|
|
52
|
-
values: [
|
|
51
|
+
// Source: BASIC + IOS_ONLY in commands/set-orientation.ts
|
|
52
|
+
values: [
|
|
53
|
+
{ value: 'portrait' },
|
|
54
|
+
{ value: 'landscape' },
|
|
55
|
+
{ value: 'portraitUpsideDown', description: 'iOS only' },
|
|
56
|
+
{ value: 'landscapeLeft', description: 'iOS only' },
|
|
57
|
+
{ value: 'landscapeRight', description: 'iOS only' },
|
|
58
|
+
{ value: 'faceUp', description: 'iOS only' },
|
|
59
|
+
{ value: 'faceDown', description: 'iOS only' },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
command: 'set-fold',
|
|
64
|
+
param: '<state>',
|
|
65
|
+
description: 'Hinge state of a foldable device, or an angle from 0 to 180',
|
|
66
|
+
// Source: FOLD_POSES in drivers/ios-fold.ts
|
|
67
|
+
values: [
|
|
68
|
+
{ value: 'closed', description: '0\u00b0 — folded shut, cover display active' },
|
|
69
|
+
{ value: 'book', description: '130\u00b0 — half open' },
|
|
70
|
+
{ value: 'open', description: '180\u00b0 — open flat' },
|
|
71
|
+
],
|
|
53
72
|
},
|
|
54
73
|
{
|
|
55
74
|
command: 'start-device',
|
package/dist/index.js
CHANGED
|
@@ -58,6 +58,8 @@ const hide_keyboard_js_1 = require("./commands/hide-keyboard.js");
|
|
|
58
58
|
const scroll_until_visible_js_1 = require("./commands/scroll-until-visible.js");
|
|
59
59
|
const set_location_js_1 = require("./commands/set-location.js");
|
|
60
60
|
const set_orientation_js_1 = require("./commands/set-orientation.js");
|
|
61
|
+
const fold_js_1 = require("./commands/fold.js");
|
|
62
|
+
const get_orientation_js_1 = require("./commands/get-orientation.js");
|
|
61
63
|
const set_viewport_js_1 = require("./commands/set-viewport.js");
|
|
62
64
|
const start_device_js_1 = require("./commands/start-device.js");
|
|
63
65
|
const stop_device_js_1 = require("./commands/stop-device.js");
|
|
@@ -140,6 +142,9 @@ const COMMAND_HELP = {
|
|
|
140
142
|
travel: travel_js_1.HELP,
|
|
141
143
|
'record-video': record_video_js_1.HELP,
|
|
142
144
|
'set-orientation': set_orientation_js_1.HELP,
|
|
145
|
+
'get-fold': fold_js_1.HELP,
|
|
146
|
+
'set-fold': fold_js_1.HELP,
|
|
147
|
+
'get-orientation': get_orientation_js_1.HELP,
|
|
143
148
|
'set-viewport': set_viewport_js_1.HELP,
|
|
144
149
|
'take-screenshot': screenshot_js_1.HELP,
|
|
145
150
|
'capture-ui': capture_ui_js_1.HELP,
|
|
@@ -823,6 +828,19 @@ async function main() {
|
|
|
823
828
|
});
|
|
824
829
|
break;
|
|
825
830
|
}
|
|
831
|
+
case 'get-fold': {
|
|
832
|
+
exitCode = await (0, fold_js_1.getFold)(opts, sessionName);
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
case 'set-fold': {
|
|
836
|
+
const value = String(rest[0] ?? argv['fold'] ?? '');
|
|
837
|
+
exitCode = await (0, fold_js_1.setFold)(value, opts, sessionName);
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
case 'get-orientation': {
|
|
841
|
+
exitCode = await (0, get_orientation_js_1.getOrientation)(opts, sessionName);
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
826
844
|
case 'set-orientation': {
|
|
827
845
|
const orientation = (rest[0] ??
|
|
828
846
|
argv['orientation'] ??
|
package/package.json
CHANGED
|
@@ -27,10 +27,36 @@ conductor list-apps # installed app ids / package names (--json adds ap
|
|
|
27
27
|
| `conductor stop-device [<name-or-id>] [--all]` | Shut down device(s) |
|
|
28
28
|
| `conductor delete-device <name-or-id> [--all]` | Delete simulator(s)/AVD(s)/web session(s) |
|
|
29
29
|
| `conductor set-location --lat <n> --lng <n>` | Set GPS coordinates |
|
|
30
|
-
| `conductor set-orientation <portrait\|landscape>`
|
|
30
|
+
| `conductor set-orientation <portrait\|landscape\|portraitUpsideDown\|landscapeLeft\|landscapeRight\|faceUp\|faceDown>` | Set orientation (the last five are iOS only) |
|
|
31
|
+
| `conductor get-orientation` | Print the current orientation (iOS only) |
|
|
32
|
+
| `conductor set-fold <closed\|book\|open\|0-180>` | Fold/unfold a foldable device, or set an exact hinge angle (iPhone Duo) |
|
|
33
|
+
| `conductor get-fold` | Print the current hinge angle and pose |
|
|
31
34
|
| `conductor set-viewport [<w> <h>] [--preset mobile\|tablet\|desktop]` | Resize web viewport (web only) |
|
|
32
35
|
| `conductor install-web [--check] [browser]` | Install a Playwright browser (chromium/firefox/webkit); `--check` = status |
|
|
33
36
|
|
|
37
|
+
### Foldable devices (iPhone Duo)
|
|
38
|
+
|
|
39
|
+
`set-fold` drives the hinge the same way Device Hub's slider does, so the device
|
|
40
|
+
really folds: SpringBoard swaps between the cover and inner displays, and
|
|
41
|
+
`devicectl device motion hinge-angle` reports the new angle. `take-screenshot`
|
|
42
|
+
captures whichever display is active; to grab a specific panel directly, use
|
|
43
|
+
`xcrun simctl io <device> screenshot --display <1|3>` (1 = cover, 3 = inner).
|
|
44
|
+
|
|
45
|
+
Angles are in degrees, 0 (shut) to 180 (flat). `closed`/`book`/`open` map to
|
|
46
|
+
0/130/180. Named poses always swap the display; an arbitrary mid-way angle sets
|
|
47
|
+
the hinge but may leave the cover display active, because the system decides
|
|
48
|
+
when to swap on its own transition logic.
|
|
49
|
+
|
|
50
|
+
The first `set-fold` after a device boots takes a few seconds: it injects a
|
|
51
|
+
controller into the simulator's locationd (restarting it), because Apple ships
|
|
52
|
+
no fold setter in `simctl` or `devicectl`. The current angle is restored
|
|
53
|
+
afterwards, so injecting doesn't change the pose. Subsequent calls are instant. This is
|
|
54
|
+
iOS-simulator only.
|
|
55
|
+
|
|
56
|
+
`set-orientation` works on foldables too, but by the same route: they accept
|
|
57
|
+
`devicectl`'s setter and ignore it, so the command verifies the result and
|
|
58
|
+
rotates through the injected controller when needed.
|
|
59
|
+
|
|
34
60
|
### Attach to an existing browser (CDP)
|
|
35
61
|
|
|
36
62
|
Instead of launching its own browser, the web driver can attach to one that's
|