@houwert/conductor 0.25.0 → 0.27.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/add-media.js +36 -0
- package/dist/commands/airplane-mode.js +46 -0
- package/dist/commands/assert-screenshot.js +68 -0
- package/dist/commands/assert-true.js +30 -0
- package/dist/commands/copy-text-from.js +63 -0
- package/dist/commands/record-video.js +126 -0
- package/dist/commands/set-permissions.js +54 -0
- package/dist/commands/stream-server.js +28 -0
- package/dist/commands/tap.js +41 -18
- package/dist/commands/travel.js +45 -0
- package/dist/daemon/h264-annexb.js +264 -0
- package/dist/daemon/server.js +47 -0
- package/dist/daemon/video-hub.js +58 -0
- package/dist/daemon/video-protocol.js +38 -0
- package/dist/daemon/video-server.js +88 -0
- package/dist/daemon/video-source.js +142 -0
- package/dist/drivers/android.js +4 -0
- package/dist/drivers/bootstrap.js +38 -0
- package/dist/drivers/flow-runner.js +1 -0
- package/dist/index.js +90 -0
- package/dist/runner.js +45 -0
- package/package.json +4 -1
- package/skills/conductor-device-interact/SKILL.md +24 -2
- package/skills/conductor-inspect/SKILL.md +3 -1
|
@@ -6,8 +6,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.detectPlatform = detectPlatform;
|
|
7
7
|
exports.getDriverPort = getDriverPort;
|
|
8
8
|
exports.getInputPort = getInputPort;
|
|
9
|
+
exports.getStreamPort = getStreamPort;
|
|
9
10
|
exports.getInprocDylibPath = getInprocDylibPath;
|
|
10
11
|
exports.getHidBinaryPath = getHidBinaryPath;
|
|
12
|
+
exports.getCaptureBinaryPath = getCaptureBinaryPath;
|
|
11
13
|
exports.installDriver = installDriver;
|
|
12
14
|
exports.isSimulatorBooted = isSimulatorBooted;
|
|
13
15
|
exports.isPortOpen = isPortOpen;
|
|
@@ -93,6 +95,7 @@ const ANDROID_BASE_PORT = 3763;
|
|
|
93
95
|
const WEB_BASE_PORT = 4075;
|
|
94
96
|
const VEGA_BASE_PORT = 5075;
|
|
95
97
|
const INPUT_BASE_PORT = 7075;
|
|
98
|
+
const STREAM_BASE_PORT = 8075;
|
|
96
99
|
const PORT_FILE = path_1.default.join(os_1.default.homedir(), '.conductor', 'ports.json');
|
|
97
100
|
const PORT_LOCK = PORT_FILE + '.lock';
|
|
98
101
|
const PORT_LOCK_TIMEOUT_MS = 5000;
|
|
@@ -200,6 +203,27 @@ async function getInputPort(deviceId) {
|
|
|
200
203
|
return port;
|
|
201
204
|
});
|
|
202
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Assign and persist a streaming-video WebSocket port for a device. Its own
|
|
208
|
+
* namespace: a device has a driver port (control), an input port (pointer/key
|
|
209
|
+
* frames), and this stream port (H.264 video fan-out).
|
|
210
|
+
*/
|
|
211
|
+
async function getStreamPort(deviceId) {
|
|
212
|
+
return withPortLock(() => {
|
|
213
|
+
const state = readPortState();
|
|
214
|
+
if (!state.streamAssignments)
|
|
215
|
+
state.streamAssignments = {};
|
|
216
|
+
if (state.nextStreamPort === undefined)
|
|
217
|
+
state.nextStreamPort = STREAM_BASE_PORT;
|
|
218
|
+
if (state.streamAssignments[deviceId] !== undefined) {
|
|
219
|
+
return state.streamAssignments[deviceId];
|
|
220
|
+
}
|
|
221
|
+
const port = state.nextStreamPort++;
|
|
222
|
+
state.streamAssignments[deviceId] = port;
|
|
223
|
+
writePortState(state);
|
|
224
|
+
return port;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
203
227
|
// ── Driver paths (bundled dev fallback + runtime download cache) ──────────────
|
|
204
228
|
/**
|
|
205
229
|
* Walk up from __dirname to find the package root (the directory containing
|
|
@@ -274,6 +298,20 @@ async function getHidBinaryPath() {
|
|
|
274
298
|
const p = path_1.default.join(dir, 'ios-hid', 'conductor-hid');
|
|
275
299
|
return fs_1.default.existsSync(p) ? p : null;
|
|
276
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* Absolute path to the host-side Simulator video capture binary
|
|
303
|
+
* (`ios-capture/conductor-capture`), built by
|
|
304
|
+
* `packages/ios-capture/tools/build-capture.sh`. Captures the framebuffer via
|
|
305
|
+
* SimulatorKit and serves a VideoToolbox H.264 Annex B stream. Returns null if
|
|
306
|
+
* it hasn't been built (streaming falls back to unavailable).
|
|
307
|
+
*/
|
|
308
|
+
async function getCaptureBinaryPath() {
|
|
309
|
+
const dir = await getDriversDir().catch(() => null);
|
|
310
|
+
if (!dir)
|
|
311
|
+
return null;
|
|
312
|
+
const p = path_1.default.join(dir, 'ios-capture', 'conductor-capture');
|
|
313
|
+
return fs_1.default.existsSync(p) ? p : null;
|
|
314
|
+
}
|
|
277
315
|
async function ensureDriversCache(pkgRoot) {
|
|
278
316
|
const pkgJsonPath = path_1.default.join(pkgRoot, 'package.json');
|
|
279
317
|
const pkg = JSON.parse(fs_1.default.readFileSync(pkgJsonPath, 'utf-8'));
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.parseFlowFile = parseFlowFile;
|
|
7
7
|
exports.parseFlowString = parseFlowString;
|
|
8
8
|
exports.executeFlow = executeFlow;
|
|
9
|
+
exports.resolvePoint = resolvePoint;
|
|
9
10
|
/**
|
|
10
11
|
* Native Conductor YAML flow parser and executor.
|
|
11
12
|
* Parses flow YAML files and executes commands directly using IOSDriver / AndroidDriver.
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ const press_key_js_1 = require("./commands/press-key.js");
|
|
|
30
30
|
const session_js_1 = require("./commands/session.js");
|
|
31
31
|
const daemon_js_1 = require("./commands/daemon.js");
|
|
32
32
|
const input_server_js_1 = require("./commands/input-server.js");
|
|
33
|
+
const stream_server_js_1 = require("./commands/stream-server.js");
|
|
33
34
|
const install_js_1 = require("./commands/install.js");
|
|
34
35
|
const init_js_1 = require("./commands/init.js");
|
|
35
36
|
const device_pool_js_1 = require("./commands/device-pool.js");
|
|
@@ -65,6 +66,14 @@ const metro_js_1 = require("./commands/metro.js");
|
|
|
65
66
|
const clipboard_js_1 = require("./commands/clipboard.js");
|
|
66
67
|
const options_js_1 = require("./commands/options.js");
|
|
67
68
|
const web_targets_js_1 = require("./commands/web-targets.js");
|
|
69
|
+
const copy_text_from_js_1 = require("./commands/copy-text-from.js");
|
|
70
|
+
const assert_true_js_1 = require("./commands/assert-true.js");
|
|
71
|
+
const set_permissions_js_1 = require("./commands/set-permissions.js");
|
|
72
|
+
const add_media_js_1 = require("./commands/add-media.js");
|
|
73
|
+
const airplane_mode_js_1 = require("./commands/airplane-mode.js");
|
|
74
|
+
const travel_js_1 = require("./commands/travel.js");
|
|
75
|
+
const record_video_js_1 = require("./commands/record-video.js");
|
|
76
|
+
const assert_screenshot_js_1 = require("./commands/assert-screenshot.js");
|
|
68
77
|
const session_js_2 = require("./session.js");
|
|
69
78
|
const device_picker_js_1 = require("./device-picker.js");
|
|
70
79
|
const cdp_discovery_js_1 = require("./drivers/cdp-discovery.js");
|
|
@@ -108,6 +117,7 @@ const COMMAND_HELP = {
|
|
|
108
117
|
'clear-state': clear_state_js_1.HELP,
|
|
109
118
|
'uninstall-app': uninstall_app_js_1.HELP,
|
|
110
119
|
'tap-on': tap_js_1.HELP,
|
|
120
|
+
'copy-text-from': copy_text_from_js_1.HELP,
|
|
111
121
|
'input-text': type_js_1.HELP,
|
|
112
122
|
'erase-text': erase_text_js_1.HELP,
|
|
113
123
|
back: back_js_1.HELP,
|
|
@@ -118,8 +128,15 @@ const COMMAND_HELP = {
|
|
|
118
128
|
'scroll-until-visible': scroll_until_visible_js_1.HELP,
|
|
119
129
|
'assert-visible': assert_visible_js_1.HELP,
|
|
120
130
|
'assert-not-visible': assert_not_visible_js_1.HELP,
|
|
131
|
+
'assert-true': assert_true_js_1.HELP,
|
|
132
|
+
'assert-screenshot': assert_screenshot_js_1.HELP,
|
|
121
133
|
'open-link': open_link_js_1.HELP,
|
|
122
134
|
'set-location': set_location_js_1.HELP,
|
|
135
|
+
'set-permissions': set_permissions_js_1.HELP,
|
|
136
|
+
'add-media': add_media_js_1.HELP,
|
|
137
|
+
'set-airplane-mode': airplane_mode_js_1.HELP,
|
|
138
|
+
travel: travel_js_1.HELP,
|
|
139
|
+
'record-video': record_video_js_1.HELP,
|
|
123
140
|
'set-orientation': set_orientation_js_1.HELP,
|
|
124
141
|
'set-viewport': set_viewport_js_1.HELP,
|
|
125
142
|
'take-screenshot': screenshot_js_1.HELP,
|
|
@@ -135,6 +152,7 @@ const COMMAND_HELP = {
|
|
|
135
152
|
'daemon-stop': daemon_js_1.HELP_DAEMON_STOP,
|
|
136
153
|
'daemon-status': daemon_js_1.HELP_DAEMON_STATUS,
|
|
137
154
|
'input-server': input_server_js_1.HELP,
|
|
155
|
+
'stream-server': stream_server_js_1.HELP,
|
|
138
156
|
'device-pool': device_pool_js_1.HELP,
|
|
139
157
|
'run-parallel': run_parallel_js_1.HELP,
|
|
140
158
|
'run-sequence': run_sequence_js_1.HELP,
|
|
@@ -204,6 +222,7 @@ async function main() {
|
|
|
204
222
|
'global',
|
|
205
223
|
'force',
|
|
206
224
|
'yes',
|
|
225
|
+
'update',
|
|
207
226
|
],
|
|
208
227
|
string: [
|
|
209
228
|
'device',
|
|
@@ -268,6 +287,11 @@ async function main() {
|
|
|
268
287
|
'react-tag',
|
|
269
288
|
'path',
|
|
270
289
|
'value',
|
|
290
|
+
'repeat',
|
|
291
|
+
'delay',
|
|
292
|
+
'speed',
|
|
293
|
+
'threshold',
|
|
294
|
+
'reference',
|
|
271
295
|
],
|
|
272
296
|
alias: { h: 'help', v: 'verbose', V: 'version', o: 'output', y: 'yes' },
|
|
273
297
|
});
|
|
@@ -313,6 +337,8 @@ async function main() {
|
|
|
313
337
|
'workspace',
|
|
314
338
|
'list-options',
|
|
315
339
|
'web-targets',
|
|
340
|
+
// assert-true evaluates a pure JS expression in the flow sandbox — no device involved.
|
|
341
|
+
'assert-true',
|
|
316
342
|
// `logs --list` and `logs --source metro` only query Metro on localhost — no device needed
|
|
317
343
|
// `logs` always needs a device session — Metro discovery is device-scoped.
|
|
318
344
|
// `daemon-stop --all` stops every daemon — no device needed
|
|
@@ -585,6 +611,9 @@ async function main() {
|
|
|
585
611
|
exitCode = await (0, tap_js_1.tap)(element, opts, sessionName, {
|
|
586
612
|
id: argv['id'],
|
|
587
613
|
text: argv['text'],
|
|
614
|
+
at: argv['at'],
|
|
615
|
+
repeat: argv['repeat'] !== undefined ? Number(argv['repeat']) : undefined,
|
|
616
|
+
delay: argv['delay'] !== undefined ? Number(argv['delay']) : undefined,
|
|
588
617
|
index: argv['index'] !== undefined ? Number(argv['index']) : undefined,
|
|
589
618
|
longPress: argv['long-press'],
|
|
590
619
|
doubleTap: argv['double-tap'],
|
|
@@ -700,6 +729,35 @@ async function main() {
|
|
|
700
729
|
});
|
|
701
730
|
break;
|
|
702
731
|
}
|
|
732
|
+
case 'assert-true': {
|
|
733
|
+
const expr = rest.join(' ');
|
|
734
|
+
const env = {};
|
|
735
|
+
const envArgs = [].concat(argv['env'] ?? []);
|
|
736
|
+
for (const e of envArgs) {
|
|
737
|
+
const idx = e.indexOf('=');
|
|
738
|
+
if (idx > 0)
|
|
739
|
+
env[e.slice(0, idx)] = e.slice(idx + 1);
|
|
740
|
+
}
|
|
741
|
+
exitCode = await (0, assert_true_js_1.assertTrue)(expr, opts, env);
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
case 'assert-screenshot': {
|
|
745
|
+
const reference = rest[0] ?? argv['reference'] ?? '';
|
|
746
|
+
exitCode = await (0, assert_screenshot_js_1.assertScreenshot)(reference, opts, sessionName, {
|
|
747
|
+
threshold: argv['threshold'] !== undefined ? Number(argv['threshold']) : undefined,
|
|
748
|
+
update: argv['update'],
|
|
749
|
+
});
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
case 'copy-text-from': {
|
|
753
|
+
const element = rest.join(' ');
|
|
754
|
+
exitCode = await (0, copy_text_from_js_1.copyTextFrom)(element, opts, sessionName, {
|
|
755
|
+
id: argv['id'],
|
|
756
|
+
text: argv['text'],
|
|
757
|
+
index: argv['index'] !== undefined ? Number(argv['index']) : undefined,
|
|
758
|
+
});
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
703
761
|
case 'open-link': {
|
|
704
762
|
const url = rest[0] ?? argv['url'] ?? '';
|
|
705
763
|
exitCode = await (0, open_link_js_1.openLink)(url, opts, sessionName);
|
|
@@ -717,6 +775,35 @@ async function main() {
|
|
|
717
775
|
}
|
|
718
776
|
break;
|
|
719
777
|
}
|
|
778
|
+
case 'set-permissions': {
|
|
779
|
+
exitCode = await (0, set_permissions_js_1.setPermissions)(rest.map(String), opts, sessionName);
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
case 'add-media': {
|
|
783
|
+
exitCode = await (0, add_media_js_1.addMedia)(rest.map(String), opts, sessionName);
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
case 'set-airplane-mode': {
|
|
787
|
+
const value = rest[0] ?? '';
|
|
788
|
+
exitCode = await (0, airplane_mode_js_1.setAirplaneMode)(value, opts, sessionName);
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
case 'toggle-airplane-mode': {
|
|
792
|
+
exitCode = await (0, airplane_mode_js_1.toggleAirplaneMode)(opts, sessionName);
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
case 'travel': {
|
|
796
|
+
exitCode = await (0, travel_js_1.travel)(rest.map(String), opts, sessionName, {
|
|
797
|
+
speed: argv['speed'] !== undefined ? Number(argv['speed']) : undefined,
|
|
798
|
+
});
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
case 'record-video': {
|
|
802
|
+
exitCode = await (0, record_video_js_1.recordVideo)(rest[0] ?? '', opts, sessionName, {
|
|
803
|
+
out: argv['out'],
|
|
804
|
+
});
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
720
807
|
case 'set-orientation': {
|
|
721
808
|
const orientation = (rest[0] ??
|
|
722
809
|
argv['orientation'] ??
|
|
@@ -847,6 +934,9 @@ async function main() {
|
|
|
847
934
|
case 'input-server':
|
|
848
935
|
exitCode = await (0, input_server_js_1.inputServer)(opts, sessionName);
|
|
849
936
|
break;
|
|
937
|
+
case 'stream-server':
|
|
938
|
+
exitCode = await (0, stream_server_js_1.streamServer)(opts, sessionName);
|
|
939
|
+
break;
|
|
850
940
|
case 'device-pool': {
|
|
851
941
|
const acquire = argv['acquire'];
|
|
852
942
|
const release = argv['release'];
|
package/dist/runner.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.detectFirstDevice = detectFirstDevice;
|
|
|
4
4
|
exports.getDriver = getDriver;
|
|
5
5
|
exports.prewarmDriver = prewarmDriver;
|
|
6
6
|
exports.inputServerInfo = inputServerInfo;
|
|
7
|
+
exports.streamServerInfo = streamServerInfo;
|
|
7
8
|
exports.runDirect = runDirect;
|
|
8
9
|
exports.spawnCommand = spawnCommand;
|
|
9
10
|
exports.runInlineFlow = runInlineFlow;
|
|
@@ -276,6 +277,50 @@ async function inputServerInfo(sessionName = 'default') {
|
|
|
276
277
|
}
|
|
277
278
|
throw new Error(`Input server for ${deviceId} did not come up within timeout.`);
|
|
278
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* Resolve the streaming-video socket for a session's device, starting the
|
|
282
|
+
* daemon (and its driver + video server) if needed. Returns the loopback
|
|
283
|
+
* WebSocket URL a viewer subscribes to. Throws if the platform has no live
|
|
284
|
+
* stream, the capture binary isn't built, or the port never comes up.
|
|
285
|
+
*/
|
|
286
|
+
async function streamServerInfo(sessionName = 'default') {
|
|
287
|
+
const deviceId = await resolveDeviceId(sessionName);
|
|
288
|
+
if (!deviceId) {
|
|
289
|
+
throw new Error('No device found. Connect a device or start a simulator, then run again.');
|
|
290
|
+
}
|
|
291
|
+
const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId);
|
|
292
|
+
if (platform !== 'ios' && platform !== 'tvos') {
|
|
293
|
+
throw new Error(`Live video streaming is not yet available for ${platform} devices.`);
|
|
294
|
+
}
|
|
295
|
+
await (0, client_js_1.startDaemon)(deviceId);
|
|
296
|
+
// The video server starts just after the input server — poll status until its port appears.
|
|
297
|
+
const start = Date.now();
|
|
298
|
+
const deadline = start + 60000;
|
|
299
|
+
while (Date.now() < deadline) {
|
|
300
|
+
const status = await (0, client_js_1.fetchDaemonStatus)(deviceId);
|
|
301
|
+
if (status && typeof status.streamPort === 'number') {
|
|
302
|
+
return {
|
|
303
|
+
device: deviceId,
|
|
304
|
+
platform,
|
|
305
|
+
streamPort: status.streamPort,
|
|
306
|
+
url: `ws://127.0.0.1:${status.streamPort}/stream?device=${encodeURIComponent(deviceId)}&platform=${platform}`,
|
|
307
|
+
codec: 'h264',
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
// A daemon whose input server is up but that still reports no streamPort
|
|
311
|
+
// after a grace period means the capture binary isn't built/available —
|
|
312
|
+
// fail fast rather than waiting out the full timeout.
|
|
313
|
+
if (status &&
|
|
314
|
+
status.streamPort === null &&
|
|
315
|
+
typeof status.inputPort === 'number' &&
|
|
316
|
+
Date.now() - start > 5000) {
|
|
317
|
+
throw new Error(`Video capture backend is not available for ${deviceId} ` +
|
|
318
|
+
`(the conductor-capture binary is missing from the installed drivers).`);
|
|
319
|
+
}
|
|
320
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
321
|
+
}
|
|
322
|
+
throw new Error(`Video server for ${deviceId} did not come up within timeout.`);
|
|
323
|
+
}
|
|
279
324
|
/**
|
|
280
325
|
* Execute a function with the driver for the given session.
|
|
281
326
|
* Returns a RunResult for consistent error handling across commands.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@houwert/conductor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "CLI tool for mobile app interactions — optimized for AI agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,13 +33,16 @@
|
|
|
33
33
|
"@grpc/proto-loader": "^0.7.15",
|
|
34
34
|
"js-yaml": "^4.1.1",
|
|
35
35
|
"minimist": "^1.2.8",
|
|
36
|
+
"pixelmatch": "^7.2.0",
|
|
36
37
|
"playwright-core": "^1.52.0",
|
|
38
|
+
"pngjs": "^7.0.0",
|
|
37
39
|
"ws": "^8.20.0"
|
|
38
40
|
},
|
|
39
41
|
"devDependencies": {
|
|
40
42
|
"@types/js-yaml": "^4.0.9",
|
|
41
43
|
"@types/minimist": "^1.2.5",
|
|
42
44
|
"@types/node": "^20.0.0",
|
|
45
|
+
"@types/pngjs": "^6.0.5",
|
|
43
46
|
"@types/ws": "^8.18.1",
|
|
44
47
|
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
|
45
48
|
"@typescript-eslint/parser": "^8.56.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: conductor-device-interact
|
|
3
|
-
description: Drive a running iOS simulator, Android emulator, tvOS simulator, Vega (Amazon Fire TV) virtual device, or Playwright web app with the conductor CLI. Use when launching apps, tapping UI elements, typing text, scrolling/swiping, performing gestures, pressing hardware/keyboard/remote keys, opening URLs or deep links, navigating back, or verifying an app change in the real running app.
|
|
3
|
+
description: Drive a running iOS simulator, Android emulator, tvOS simulator, Vega (Amazon Fire TV) virtual device, or Playwright web app with the conductor CLI. Use when launching apps, tapping UI elements (by selector or raw coordinate), typing text, scrolling/swiping, performing gestures, pressing hardware/keyboard/remote keys, opening URLs or deep links, navigating back, granting/denying app permissions, adding media to the gallery, setting GPS location or a travel route, toggling airplane mode, recording a screen video, or verifying an app change in the real running app.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Conductor — device interaction
|
|
@@ -36,7 +36,9 @@ conductor assert-visible "Dashboard"
|
|
|
36
36
|
|---|---|
|
|
37
37
|
| `conductor launch-app <appId>` | Launch app (saved to session). `--no-stop-app` resumes; `--argument key=value` passes launch args |
|
|
38
38
|
| `conductor stop-app [<appId>]` | Stop the app |
|
|
39
|
-
| `conductor tap-on <element>` | Tap by text, id, or `@eN`. `--long-press`, `--double-tap`, `--optional`, `--index <n>` |
|
|
39
|
+
| `conductor tap-on <element>` | Tap by text, id, or `@eN`. `--long-press`, `--double-tap`, `--optional`, `--index <n>`, `--repeat <n> --delay <ms>` |
|
|
40
|
+
| `conductor tap-on --at <x,y>` | Tap a raw coordinate (px `100,200`, percent `50%,50%`, or `0-1` fraction) — no element match |
|
|
41
|
+
| `conductor copy-text-from <element>` | Print an element's text (and copy to the iOS clipboard) |
|
|
40
42
|
| `conductor input-text <text>` | Type into the focused field |
|
|
41
43
|
| `conductor erase-text [n]` | Erase n characters (default 50) |
|
|
42
44
|
| `conductor press-key <key>` | Press a key (Enter, Backspace, Home, …) or a remote button (`Remote Dpad Up/Down/Left/Right/Center`, `Remote Menu`) for tvOS / Android TV / vega. `--long-press` / `--duration <seconds>` holds it |
|
|
@@ -49,9 +51,15 @@ conductor assert-visible "Dashboard"
|
|
|
49
51
|
| `conductor pinch [--scale N] [--center x,y]` | Two-finger pinch (scale<1 out, >1 in) |
|
|
50
52
|
| `conductor rotate-gesture [--degrees N] [--center x,y]` | Two-finger rotate |
|
|
51
53
|
| `conductor gesture <json\|--file path>` | Play a multi-touch path |
|
|
54
|
+
| `conductor set-permissions <perm=value>...` | Grant/deny app permissions (`camera=allow photos=deny`, `all=allow`) |
|
|
55
|
+
| `conductor add-media <path>...` | Push image/video files into the device gallery |
|
|
56
|
+
| `conductor set-airplane-mode <on\|off>` / `toggle-airplane-mode` | Airplane mode (Android only) |
|
|
57
|
+
| `conductor travel <lat,lng>... [--speed <m/s>]` | Move GPS through a route of coordinates |
|
|
58
|
+
| `conductor record-video start [--out <path>]` / `record-video stop` | Record a screen **video** (iOS/Android). Distinct from `flow record` (which records a YAML flow) |
|
|
52
59
|
| `conductor clipboard read` / `clipboard write <text>` / `paste` | Clipboard (iOS) |
|
|
53
60
|
| `conductor list-options [command]` | List valid values for enumerated params |
|
|
54
61
|
| `conductor input-server` | Start (if needed) and print the streaming-input WebSocket URL for the device |
|
|
62
|
+
| `conductor stream-server` | Start (if needed) and print the live video-stream WebSocket URL for the device |
|
|
55
63
|
|
|
56
64
|
## Streaming input (host IDEs)
|
|
57
65
|
|
|
@@ -65,6 +73,20 @@ accepts normalized (0..1) frames: `pointer{id,phase,x,y}`, `key{code,mods,down}`
|
|
|
65
73
|
owns coord→device translation and keymaps. For scripted, one-off actions use the
|
|
66
74
|
discrete commands above — this is for interactive host UIs.
|
|
67
75
|
|
|
76
|
+
## Streaming video (host IDEs / device viewers)
|
|
77
|
+
|
|
78
|
+
For live device mirroring, `conductor stream-server` ensures the daemon + capture
|
|
79
|
+
backend are up and prints the loopback URL
|
|
80
|
+
(`ws://127.0.0.1:<port>/stream?device=<id>&platform=<ios|tvos|android|web>`; also
|
|
81
|
+
in `daemon-status --json` as `streamPort`). One capture fans out to N subscribers.
|
|
82
|
+
On connect the server sends a JSON `config` frame
|
|
83
|
+
(`{t:"config",codec:"h264",width,height,rotation,fps,sps,pps,avcC,codecString}`),
|
|
84
|
+
then **binary** frames — each one H.264 Annex B access unit, keyframe-led; a late
|
|
85
|
+
joiner gets the cached config + a keyframe immediately. iOS/tvOS only for now
|
|
86
|
+
(host-side SimulatorKit → VideoToolbox capture); Android/web are follow-ons.
|
|
87
|
+
This is capture only — input stays on `input-server`. For a still image use
|
|
88
|
+
`screenshot` / `capture-ui`; this socket is for continuous low-latency mirroring.
|
|
89
|
+
|
|
68
90
|
## Discovering valid values
|
|
69
91
|
|
|
70
92
|
Several commands only accept a fixed set of values (`press-key <key>`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: conductor-inspect
|
|
3
|
-
description: Read the live UI state of a running app with the conductor CLI — view hierarchy, accessibility snapshot, screenshots, focused element, and element refs. Use when you need to see what's on screen, find an element's id/text/coordinates, take a screenshot, check focus,
|
|
3
|
+
description: Read the live UI state of a running app with the conductor CLI — view hierarchy, accessibility snapshot, screenshots, focused element, and element refs. Use when you need to see what's on screen, find an element's id/text/coordinates, take a screenshot, check focus, assert that something is (or isn't) visible, assert a JavaScript expression, or do visual-regression screenshot comparison before or after acting.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Conductor — inspection & assertions
|
|
@@ -78,6 +78,8 @@ conductor native-image 816,286,288,288 --output /tmp/avatar.png
|
|
|
78
78
|
|---|---|
|
|
79
79
|
| `conductor assert-visible <element> [--timeout ms]` | Assert element is visible (non-zero exit on failure) |
|
|
80
80
|
| `conductor assert-not-visible <element> [--timeout ms]` | Assert element is absent |
|
|
81
|
+
| `conductor assert-true <expr> [--env K=V]` | Assert a JavaScript expression is truthy (no device needed) |
|
|
82
|
+
| `conductor assert-screenshot <reference.png> [--threshold <0-1>] [--update]` | Visual regression vs a baseline image; `--update` (re)writes the baseline. Writes `<ref>.diff.png` on mismatch |
|
|
81
83
|
|
|
82
84
|
Both take the same selectors as `tap-on`: `--id`, `--text`, `--index`,
|
|
83
85
|
`--below` / `--above` / `--left-of` / `--right-of`, `--focused`, `--enabled`,
|