@houwert/conductor 0.24.1 → 0.26.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/input-server.js +28 -0
- package/dist/commands/launch-app.js +30 -3
- package/dist/commands/native-rn.js +110 -0
- package/dist/commands/native.js +320 -0
- package/dist/commands/stream-server.js +28 -0
- package/dist/daemon/client.js +5 -0
- package/dist/daemon/h264-annexb.js +264 -0
- package/dist/daemon/input-backends.js +203 -0
- package/dist/daemon/input-protocol.js +40 -0
- package/dist/daemon/input-router.js +110 -0
- package/dist/daemon/input-server.js +124 -0
- package/dist/daemon/server.js +131 -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/bootstrap.js +87 -0
- package/dist/drivers/eval-compiler.js +131 -0
- package/dist/drivers/ios-hid.js +95 -0
- package/dist/drivers/ios-inproc.js +198 -0
- package/dist/drivers/ios.js +39 -8
- package/dist/drivers/metro-scripts.js +174 -0
- package/dist/index.js +146 -0
- package/dist/runner.js +78 -0
- package/package.json +1 -1
- package/skills/conductor-device-interact/SKILL.md +28 -0
- package/skills/conductor-device-setup/SKILL.md +1 -1
- package/skills/conductor-inspect/SKILL.md +43 -0
- package/skills/conductor-metro-debugger/SKILL.md +10 -0
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.makeComponentTreeScript = makeComponentTreeScript;
|
|
19
|
+
exports.makeOverridePropsScript = makeOverridePropsScript;
|
|
20
|
+
exports.makeRnPropsScript = makeRnPropsScript;
|
|
19
21
|
exports.makeInspectElementScript = makeInspectElementScript;
|
|
20
22
|
/** RN internals + navigation/safe-area wrappers we always strip from the tree. */
|
|
21
23
|
const SKIP_NAMES = [
|
|
@@ -289,6 +291,178 @@ function makeComponentTreeScript(requestId) {
|
|
|
289
291
|
}
|
|
290
292
|
})();`;
|
|
291
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Shared JS: locate a host fiber by its native reactTag, across every registered
|
|
296
|
+
* renderer and both RN architectures (Paper `_nativeTag`/`canonical.nativeTag`,
|
|
297
|
+
* Fabric bridgeless `__nativeTag` on the state node / public instance). Defines
|
|
298
|
+
* `findByTag(hook, TAG)` returning `{ fiber, renderer }` or null.
|
|
299
|
+
*/
|
|
300
|
+
const FIBER_BY_TAG_HELPERS = `
|
|
301
|
+
function nativeTagOf(f) {
|
|
302
|
+
if (typeof f.type !== 'string' || !f.stateNode) return null;
|
|
303
|
+
var sn = f.stateNode;
|
|
304
|
+
if (typeof sn._nativeTag === 'number') return sn._nativeTag;
|
|
305
|
+
if (typeof sn.__nativeTag === 'number') return sn.__nativeTag;
|
|
306
|
+
if (sn.canonical) {
|
|
307
|
+
if (typeof sn.canonical.nativeTag === 'number') return sn.canonical.nativeTag;
|
|
308
|
+
var pi = sn.canonical.publicInstance;
|
|
309
|
+
if (pi && typeof pi.__nativeTag === 'number') return pi.__nativeTag;
|
|
310
|
+
}
|
|
311
|
+
if (sn.node && typeof sn.node.__nativeTag === 'number') return sn.node.__nativeTag;
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
function findHostByTag(root, TAG) {
|
|
315
|
+
var stack = [root.current || root], seen = 0;
|
|
316
|
+
while (stack.length && seen < 40000) {
|
|
317
|
+
var f = stack.pop(); seen++;
|
|
318
|
+
if (!f) continue;
|
|
319
|
+
if (nativeTagOf(f) === TAG) return f;
|
|
320
|
+
if (f.sibling) stack.push(f.sibling);
|
|
321
|
+
if (f.child) stack.push(f.child);
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
function findByTag(hook, TAG) {
|
|
326
|
+
var entries = [];
|
|
327
|
+
hook.renderers.forEach(function(r, id) { entries.push([id, r]); });
|
|
328
|
+
for (var i = 0; i < entries.length; i++) {
|
|
329
|
+
var id = entries[i][0], r = entries[i][1], roots = null;
|
|
330
|
+
try { roots = hook.getFiberRoots(id); } catch (e) {}
|
|
331
|
+
if (!roots) continue;
|
|
332
|
+
var arr = Array.from(roots);
|
|
333
|
+
for (var j = 0; j < arr.length; j++) {
|
|
334
|
+
var hf = findHostByTag(arr[j], TAG);
|
|
335
|
+
if (hf) return { fiber: hf, renderer: r };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
339
|
+
}`;
|
|
340
|
+
/**
|
|
341
|
+
* Live-edit props via React DevTools' `overrideProps(fiber, path, value)` — the
|
|
342
|
+
* same call the DevTools "edit prop" UI makes. Maps `reactTag` → host fiber, then
|
|
343
|
+
* picks the fiber that owns the top-level path key (for `children`, prefers the
|
|
344
|
+
* composite `<Text>` ancestor, so the visible string changes). Returns a JSON
|
|
345
|
+
* string: `{status:'ok',applied:true}` or `{status:'error',message}`.
|
|
346
|
+
*/
|
|
347
|
+
function makeOverridePropsScript(reactTag, path, valueJson) {
|
|
348
|
+
return `(function() {
|
|
349
|
+
try {
|
|
350
|
+
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
351
|
+
if (!hook || !hook.renderers || !hook.getFiberRoots) {
|
|
352
|
+
return JSON.stringify({ status: 'error', message: 'No React DevTools hook — not a dev/debug build?' });
|
|
353
|
+
}
|
|
354
|
+
var TAG = ${JSON.stringify(reactTag)};
|
|
355
|
+
var PATH = ${JSON.stringify(path)};
|
|
356
|
+
var VALUE = ${valueJson};
|
|
357
|
+
${FIBER_BY_TAG_HELPERS}
|
|
358
|
+
var hit = findByTag(hook, TAG);
|
|
359
|
+
if (!hit) return JSON.stringify({ status: 'error', message: 'no fiber for reactTag ' + TAG });
|
|
360
|
+
var renderer = hit.renderer;
|
|
361
|
+
if (!renderer || typeof renderer.overrideProps !== 'function') {
|
|
362
|
+
return JSON.stringify({ status: 'error', message: 'renderer has no overrideProps — is this a dev build?' });
|
|
363
|
+
}
|
|
364
|
+
// Pick the fiber that owns the top path key. For 'children' prefer the
|
|
365
|
+
// nearest fiber whose children is a string (the renderable <Text>/RCTText),
|
|
366
|
+
// so overriding actually swaps the visible glyphs.
|
|
367
|
+
var key = PATH[0];
|
|
368
|
+
var cur = hit.fiber, hops = 0, fallback = null;
|
|
369
|
+
while (cur && hops < 10) {
|
|
370
|
+
var p = cur.memoizedProps;
|
|
371
|
+
if (p && typeof p === 'object' && Object.prototype.hasOwnProperty.call(p, key)) {
|
|
372
|
+
if (fallback === null) fallback = cur;
|
|
373
|
+
if (key !== 'children') { fallback = cur; break; }
|
|
374
|
+
if (typeof p.children === 'string') { fallback = cur; break; }
|
|
375
|
+
}
|
|
376
|
+
cur = cur.return; hops++;
|
|
377
|
+
}
|
|
378
|
+
var target = fallback || hit.fiber;
|
|
379
|
+
// RN styles are often arrays (StyleSheet composition). overrideProps' setIn
|
|
380
|
+
// can't create missing intermediates, and a key set on the array object is
|
|
381
|
+
// ignored by flattening — so for a 'style.<key>' path we flatten the current
|
|
382
|
+
// style to a plain object (what RN does anyway), apply the override, and set
|
|
383
|
+
// the whole 'style'. Works whether style started as an object or an array.
|
|
384
|
+
if (PATH[0] === 'style' && PATH.length >= 2) {
|
|
385
|
+
function flattenStyle(s) {
|
|
386
|
+
var acc = {};
|
|
387
|
+
(function merge(x) {
|
|
388
|
+
if (!x) return;
|
|
389
|
+
if (Array.isArray(x)) { for (var i = 0; i < x.length; i++) merge(x[i]); return; }
|
|
390
|
+
if (typeof x === 'object') { for (var k in x) if (Object.prototype.hasOwnProperty.call(x, k)) acc[k] = x[k]; }
|
|
391
|
+
})(s);
|
|
392
|
+
return acc;
|
|
393
|
+
}
|
|
394
|
+
var flat = flattenStyle(target.memoizedProps && target.memoizedProps.style);
|
|
395
|
+
var keys = PATH.slice(1), o = flat;
|
|
396
|
+
for (var i = 0; i < keys.length - 1; i++) {
|
|
397
|
+
if (typeof o[keys[i]] !== 'object' || o[keys[i]] === null) o[keys[i]] = {};
|
|
398
|
+
o = o[keys[i]];
|
|
399
|
+
}
|
|
400
|
+
o[keys[keys.length - 1]] = VALUE;
|
|
401
|
+
renderer.overrideProps(target, ['style'], flat);
|
|
402
|
+
return JSON.stringify({ status: 'ok', applied: true, note: 'style flattened to object; override applied' });
|
|
403
|
+
}
|
|
404
|
+
renderer.overrideProps(target, PATH, VALUE);
|
|
405
|
+
return JSON.stringify({ status: 'ok', applied: true });
|
|
406
|
+
} catch (e) {
|
|
407
|
+
return JSON.stringify({ status: 'error', message: String((e && e.message) || e) });
|
|
408
|
+
}
|
|
409
|
+
})();`;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Dump a host fiber's `memoizedProps` (the real JSX props RN passed to the native
|
|
413
|
+
* view — the JS-side analog of the native `/props` rawProps that Fabric drops).
|
|
414
|
+
* Functions become `"[Function: name]"`; cycles/over-depth become markers.
|
|
415
|
+
* Returns a JSON string `{status:'ok',props:{...}}` or `{status:'error',message}`.
|
|
416
|
+
*/
|
|
417
|
+
function makeRnPropsScript(reactTag) {
|
|
418
|
+
return `(function() {
|
|
419
|
+
try {
|
|
420
|
+
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
421
|
+
if (!hook || !hook.renderers || !hook.getFiberRoots) {
|
|
422
|
+
return JSON.stringify({ status: 'error', message: 'No React DevTools hook — not a dev/debug build?' });
|
|
423
|
+
}
|
|
424
|
+
var TAG = ${JSON.stringify(reactTag)};
|
|
425
|
+
${FIBER_BY_TAG_HELPERS}
|
|
426
|
+
var hit = findByTag(hook, TAG);
|
|
427
|
+
if (!hit) return JSON.stringify({ status: 'error', message: 'no fiber for reactTag ' + TAG });
|
|
428
|
+
var seen = [];
|
|
429
|
+
function ser(v, depth) {
|
|
430
|
+
if (v === null || v === undefined) return v === undefined ? undefined : null;
|
|
431
|
+
var t = typeof v;
|
|
432
|
+
if (t === 'string' || t === 'boolean') return v;
|
|
433
|
+
if (t === 'number') return isFinite(v) ? v : String(v);
|
|
434
|
+
if (t === 'function') return '[Function: ' + (v.name || 'anonymous') + ']';
|
|
435
|
+
if (t === 'symbol') return v.toString();
|
|
436
|
+
if (t === 'bigint') return String(v) + 'n';
|
|
437
|
+
if (t === 'object') {
|
|
438
|
+
if (depth > 6) return '[Object: max depth]';
|
|
439
|
+
if (seen.indexOf(v) !== -1) return '[Circular]';
|
|
440
|
+
if (v && v.$$typeof) return '[ReactElement]';
|
|
441
|
+
seen.push(v);
|
|
442
|
+
var out;
|
|
443
|
+
if (Array.isArray(v)) {
|
|
444
|
+
out = [];
|
|
445
|
+
for (var i = 0; i < v.length && i < 200; i++) out.push(ser(v[i], depth + 1));
|
|
446
|
+
} else {
|
|
447
|
+
out = {};
|
|
448
|
+
var keys = Object.keys(v);
|
|
449
|
+
for (var k = 0; k < keys.length; k++) {
|
|
450
|
+
var val = ser(v[keys[k]], depth + 1);
|
|
451
|
+
if (val !== undefined) out[keys[k]] = val;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
seen.pop();
|
|
455
|
+
return out;
|
|
456
|
+
}
|
|
457
|
+
return String(v);
|
|
458
|
+
}
|
|
459
|
+
var props = ser(hit.fiber.memoizedProps || {}, 0);
|
|
460
|
+
return JSON.stringify({ status: 'ok', props: props });
|
|
461
|
+
} catch (e) {
|
|
462
|
+
return JSON.stringify({ status: 'error', message: String((e && e.message) || e) });
|
|
463
|
+
}
|
|
464
|
+
})();`;
|
|
465
|
+
}
|
|
292
466
|
/**
|
|
293
467
|
* Inspect-at-point script. Uses React DevTools's own
|
|
294
468
|
* `renderer.rendererConfig.getInspectorDataForViewAtPoint(inspectRef, x, y, cb)`,
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,8 @@ const verbose_js_1 = require("./verbose.js");
|
|
|
9
9
|
const sdk_js_1 = require("./android/sdk.js");
|
|
10
10
|
const list_devices_js_1 = require("./commands/list-devices.js");
|
|
11
11
|
const launch_app_js_1 = require("./commands/launch-app.js");
|
|
12
|
+
const native_js_1 = require("./commands/native.js");
|
|
13
|
+
const native_rn_js_1 = require("./commands/native-rn.js");
|
|
12
14
|
const stop_app_js_1 = require("./commands/stop-app.js");
|
|
13
15
|
const clear_state_js_1 = require("./commands/clear-state.js");
|
|
14
16
|
const uninstall_app_js_1 = require("./commands/uninstall-app.js");
|
|
@@ -27,6 +29,8 @@ const run_flow_inline_js_1 = require("./commands/run-flow-inline.js");
|
|
|
27
29
|
const press_key_js_1 = require("./commands/press-key.js");
|
|
28
30
|
const session_js_1 = require("./commands/session.js");
|
|
29
31
|
const daemon_js_1 = require("./commands/daemon.js");
|
|
32
|
+
const input_server_js_1 = require("./commands/input-server.js");
|
|
33
|
+
const stream_server_js_1 = require("./commands/stream-server.js");
|
|
30
34
|
const install_js_1 = require("./commands/install.js");
|
|
31
35
|
const init_js_1 = require("./commands/init.js");
|
|
32
36
|
const device_pool_js_1 = require("./commands/device-pool.js");
|
|
@@ -80,6 +84,27 @@ const COMMAND_HELP = {
|
|
|
80
84
|
'download-app': download_app_js_1.HELP,
|
|
81
85
|
'install-app': install_app_js_1.HELP,
|
|
82
86
|
'launch-app': launch_app_js_1.HELP,
|
|
87
|
+
'native-ping': native_js_1.PING_HELP,
|
|
88
|
+
'native-inspect': native_js_1.INSPECT_HELP,
|
|
89
|
+
'native-nav': native_js_1.NAV_HELP,
|
|
90
|
+
'native-screenshot': native_js_1.SCREENSHOT_HELP,
|
|
91
|
+
'native-image': native_js_1.IMAGE_HELP,
|
|
92
|
+
'native-snapshot': native_js_1.SNAPSHOT_HELP,
|
|
93
|
+
'native-view': native_js_1.VIEW_HELP,
|
|
94
|
+
'native-set': native_js_1.SET_HELP,
|
|
95
|
+
'native-props': native_js_1.PROPS_HELP,
|
|
96
|
+
'native-rn-set': native_rn_js_1.RN_SET_HELP,
|
|
97
|
+
'native-rn-props': native_rn_js_1.RN_PROPS_HELP,
|
|
98
|
+
'native-constraints': native_js_1.CONSTRAINTS_HELP,
|
|
99
|
+
'native-hittest': native_js_1.HITTEST_HELP,
|
|
100
|
+
'native-highlight': native_js_1.HIGHLIGHT_HELP,
|
|
101
|
+
'native-find': native_js_1.FIND_HELP,
|
|
102
|
+
'native-raw': native_js_1.RAW_HELP,
|
|
103
|
+
'native-console': native_js_1.CONSOLE_HELP,
|
|
104
|
+
'native-network': native_js_1.NETWORK_HELP,
|
|
105
|
+
'native-heap': native_js_1.HEAP_HELP,
|
|
106
|
+
'native-appearance': native_js_1.APPEARANCE_HELP,
|
|
107
|
+
'native-eval': native_js_1.EVAL_HELP,
|
|
83
108
|
'stop-app': stop_app_js_1.HELP,
|
|
84
109
|
'clear-state': clear_state_js_1.HELP,
|
|
85
110
|
'uninstall-app': uninstall_app_js_1.HELP,
|
|
@@ -110,6 +135,8 @@ const COMMAND_HELP = {
|
|
|
110
135
|
'daemon-start': daemon_js_1.HELP_DAEMON_START,
|
|
111
136
|
'daemon-stop': daemon_js_1.HELP_DAEMON_STOP,
|
|
112
137
|
'daemon-status': daemon_js_1.HELP_DAEMON_STATUS,
|
|
138
|
+
'input-server': input_server_js_1.HELP,
|
|
139
|
+
'stream-server': stream_server_js_1.HELP,
|
|
113
140
|
'device-pool': device_pool_js_1.HELP,
|
|
114
141
|
'run-parallel': run_parallel_js_1.HELP,
|
|
115
142
|
'run-sequence': run_sequence_js_1.HELP,
|
|
@@ -240,6 +267,9 @@ async function main() {
|
|
|
240
267
|
'cdp-url',
|
|
241
268
|
'cdp-target',
|
|
242
269
|
'duration',
|
|
270
|
+
'react-tag',
|
|
271
|
+
'path',
|
|
272
|
+
'value',
|
|
243
273
|
],
|
|
244
274
|
alias: { h: 'help', v: 'verbose', V: 'version', o: 'output', y: 'yes' },
|
|
245
275
|
});
|
|
@@ -424,9 +454,119 @@ async function main() {
|
|
|
424
454
|
clearKeychain: argv['clear-keychain'],
|
|
425
455
|
stopApp: argv['stop-app'] !== false,
|
|
426
456
|
launchArgs,
|
|
457
|
+
inject: argv['inject'],
|
|
427
458
|
});
|
|
428
459
|
break;
|
|
429
460
|
}
|
|
461
|
+
case 'native-ping': {
|
|
462
|
+
exitCode = await (0, native_js_1.nativePing)(opts, sessionName);
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
case 'native-inspect': {
|
|
466
|
+
exitCode = await (0, native_js_1.nativeInspect)(opts, sessionName);
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
case 'native-nav': {
|
|
470
|
+
exitCode = await (0, native_js_1.nativeNav)(opts, sessionName);
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
case 'native-screenshot': {
|
|
474
|
+
exitCode = await (0, native_js_1.nativeScreenshot)(argv['output'], opts, sessionName);
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
case 'native-image': {
|
|
478
|
+
exitCode = await (0, native_js_1.nativeImage)(rest[0], argv['output'], opts, sessionName);
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
case 'native-snapshot': {
|
|
482
|
+
exitCode = await (0, native_js_1.nativeSnapshot)(rest[0], argv['with-subviews'], argv['output'], opts, sessionName);
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
case 'native-view': {
|
|
486
|
+
exitCode = await (0, native_js_1.nativeView)(rest[0], opts, sessionName);
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
case 'native-set': {
|
|
490
|
+
exitCode = await (0, native_js_1.nativeSet)(rest, opts, sessionName);
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
case 'native-props': {
|
|
494
|
+
exitCode = await (0, native_js_1.nativeProps)(rest[0], opts, sessionName);
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
case 'native-rn-set': {
|
|
498
|
+
const rnOpts = {
|
|
499
|
+
port: argv['port'] !== undefined ? Number(argv['port']) : undefined,
|
|
500
|
+
targetIndex: argv['target'] !== undefined ? Number(argv['target']) : undefined,
|
|
501
|
+
};
|
|
502
|
+
exitCode = await (0, native_rn_js_1.nativeRnSet)({
|
|
503
|
+
reactTag: argv['react-tag'],
|
|
504
|
+
path: argv['path'],
|
|
505
|
+
value: argv['value'],
|
|
506
|
+
}, opts, sessionName, rnOpts);
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
case 'native-rn-props': {
|
|
510
|
+
const rnOpts = {
|
|
511
|
+
port: argv['port'] !== undefined ? Number(argv['port']) : undefined,
|
|
512
|
+
targetIndex: argv['target'] !== undefined ? Number(argv['target']) : undefined,
|
|
513
|
+
};
|
|
514
|
+
exitCode = await (0, native_rn_js_1.nativeRnProps)({ reactTag: argv['react-tag'] }, opts, sessionName, rnOpts);
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
case 'native-constraints': {
|
|
518
|
+
exitCode = await (0, native_js_1.nativeConstraints)(rest[0], opts, sessionName);
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
case 'native-hittest': {
|
|
522
|
+
exitCode = await (0, native_js_1.nativeHittest)(rest[0], opts, sessionName);
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
case 'native-highlight': {
|
|
526
|
+
exitCode = await (0, native_js_1.nativeHighlight)(rest[0], opts, sessionName);
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
case 'native-find': {
|
|
530
|
+
exitCode = await (0, native_js_1.nativeFind)({
|
|
531
|
+
className: argv['class'],
|
|
532
|
+
text: argv['text'],
|
|
533
|
+
}, opts, sessionName);
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
case 'native-raw': {
|
|
537
|
+
exitCode = await (0, native_js_1.nativeRaw)(rest[0], argv['output'], opts, sessionName);
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
case 'native-console': {
|
|
541
|
+
exitCode = await (0, native_js_1.nativeConsole)(argv['since'] !== undefined ? Number(argv['since']) : undefined, opts, sessionName);
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
case 'native-network': {
|
|
545
|
+
exitCode = await (0, native_js_1.nativeNetwork)(argv['since'] !== undefined ? Number(argv['since']) : undefined, opts, sessionName);
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
case 'native-heap': {
|
|
549
|
+
exitCode = await (0, native_js_1.nativeHeap)({
|
|
550
|
+
className: argv['class'],
|
|
551
|
+
pattern: argv['pattern'],
|
|
552
|
+
read: argv['read'],
|
|
553
|
+
key: argv['key'],
|
|
554
|
+
}, opts, sessionName);
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
case 'native-appearance': {
|
|
558
|
+
exitCode = await (0, native_js_1.nativeAppearance)({
|
|
559
|
+
style: rest[0],
|
|
560
|
+
direction: argv['direction'],
|
|
561
|
+
contentSize: argv['content-size'],
|
|
562
|
+
animSpeed: argv['anim-speed'],
|
|
563
|
+
}, opts, sessionName);
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
case 'native-eval': {
|
|
567
|
+
exitCode = await (0, native_js_1.nativeEval)(rest.join(' '), argv['mode'] === 'full' ? 'full' : 'expr', opts, sessionName);
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
430
570
|
case 'stop-app': {
|
|
431
571
|
const appId = rest[0];
|
|
432
572
|
exitCode = await (0, stop_app_js_1.stopApp)(appId, opts, sessionName);
|
|
@@ -706,6 +846,12 @@ async function main() {
|
|
|
706
846
|
case 'daemon-status':
|
|
707
847
|
exitCode = await (0, daemon_js_1.daemonStatusCmd)(opts, sessionName);
|
|
708
848
|
break;
|
|
849
|
+
case 'input-server':
|
|
850
|
+
exitCode = await (0, input_server_js_1.inputServer)(opts, sessionName);
|
|
851
|
+
break;
|
|
852
|
+
case 'stream-server':
|
|
853
|
+
exitCode = await (0, stream_server_js_1.streamServer)(opts, sessionName);
|
|
854
|
+
break;
|
|
709
855
|
case 'device-pool': {
|
|
710
856
|
const acquire = argv['acquire'];
|
|
711
857
|
const release = argv['release'];
|
package/dist/runner.js
CHANGED
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.detectFirstDevice = detectFirstDevice;
|
|
4
4
|
exports.getDriver = getDriver;
|
|
5
5
|
exports.prewarmDriver = prewarmDriver;
|
|
6
|
+
exports.inputServerInfo = inputServerInfo;
|
|
7
|
+
exports.streamServerInfo = streamServerInfo;
|
|
6
8
|
exports.runDirect = runDirect;
|
|
7
9
|
exports.spawnCommand = spawnCommand;
|
|
8
10
|
exports.runInlineFlow = runInlineFlow;
|
|
@@ -243,6 +245,82 @@ async function prewarmDriver(deviceId) {
|
|
|
243
245
|
/* best-effort: a later command will report a real failure */
|
|
244
246
|
}
|
|
245
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Resolve the streaming-input socket for a session's device, starting the
|
|
250
|
+
* daemon (and its driver + input server) if needed. Returns the loopback
|
|
251
|
+
* WebSocket URL the host IDE connects to. Throws if the platform has no
|
|
252
|
+
* streaming input (web/vega) or the port never comes up.
|
|
253
|
+
*/
|
|
254
|
+
async function inputServerInfo(sessionName = 'default') {
|
|
255
|
+
const deviceId = await resolveDeviceId(sessionName);
|
|
256
|
+
if (!deviceId) {
|
|
257
|
+
throw new Error('No device found. Connect a device or start a simulator, then run again.');
|
|
258
|
+
}
|
|
259
|
+
const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId);
|
|
260
|
+
if (platform !== 'ios' && platform !== 'tvos' && platform !== 'android') {
|
|
261
|
+
throw new Error(`Streaming input is not available for ${platform} devices.`);
|
|
262
|
+
}
|
|
263
|
+
await (0, client_js_1.startDaemon)(deviceId);
|
|
264
|
+
// The input server starts just after the driver — poll status until its port appears.
|
|
265
|
+
const deadline = Date.now() + 60000;
|
|
266
|
+
while (Date.now() < deadline) {
|
|
267
|
+
const status = await (0, client_js_1.fetchDaemonStatus)(deviceId);
|
|
268
|
+
if (status && typeof status.inputPort === 'number') {
|
|
269
|
+
return {
|
|
270
|
+
device: deviceId,
|
|
271
|
+
platform,
|
|
272
|
+
inputPort: status.inputPort,
|
|
273
|
+
url: `ws://127.0.0.1:${status.inputPort}/input`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
277
|
+
}
|
|
278
|
+
throw new Error(`Input server for ${deviceId} did not come up within timeout.`);
|
|
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
|
+
}
|
|
246
324
|
/**
|
|
247
325
|
* Execute a function with the driver for the given session.
|
|
248
326
|
* Returns a RunResult for consistent error handling across commands.
|
package/package.json
CHANGED
|
@@ -51,6 +51,34 @@ conductor assert-visible "Dashboard"
|
|
|
51
51
|
| `conductor gesture <json\|--file path>` | Play a multi-touch path |
|
|
52
52
|
| `conductor clipboard read` / `clipboard write <text>` / `paste` | Clipboard (iOS) |
|
|
53
53
|
| `conductor list-options [command]` | List valid values for enumerated params |
|
|
54
|
+
| `conductor input-server` | Start (if needed) and print the streaming-input WebSocket URL for the device |
|
|
55
|
+
| `conductor stream-server` | Start (if needed) and print the live video-stream WebSocket URL for the device |
|
|
56
|
+
|
|
57
|
+
## Streaming input (host IDEs)
|
|
58
|
+
|
|
59
|
+
For continuous, low-latency input (live drags, fast typing) a host IDE can open
|
|
60
|
+
one persistent WebSocket per device instead of spawning a command per event.
|
|
61
|
+
`conductor input-server` ensures the daemon + driver are up and prints the
|
|
62
|
+
loopback URL (`ws://127.0.0.1:<port>/input`; also in `daemon-status --json` as
|
|
63
|
+
`inputPort`). The server sends a `hello` with per-platform capabilities, then
|
|
64
|
+
accepts normalized (0..1) frames: `pointer{id,phase,x,y}`, `key{code,mods,down}`,
|
|
65
|
+
`text{value}`, `button{name}`, `scroll{x,y,dx,dy}`, `tvremote{button}`. Conductor
|
|
66
|
+
owns coord→device translation and keymaps. For scripted, one-off actions use the
|
|
67
|
+
discrete commands above — this is for interactive host UIs.
|
|
68
|
+
|
|
69
|
+
## Streaming video (host IDEs / device viewers)
|
|
70
|
+
|
|
71
|
+
For live device mirroring, `conductor stream-server` ensures the daemon + capture
|
|
72
|
+
backend are up and prints the loopback URL
|
|
73
|
+
(`ws://127.0.0.1:<port>/stream?device=<id>&platform=<ios|tvos|android|web>`; also
|
|
74
|
+
in `daemon-status --json` as `streamPort`). One capture fans out to N subscribers.
|
|
75
|
+
On connect the server sends a JSON `config` frame
|
|
76
|
+
(`{t:"config",codec:"h264",width,height,rotation,fps,sps,pps,avcC,codecString}`),
|
|
77
|
+
then **binary** frames — each one H.264 Annex B access unit, keyframe-led; a late
|
|
78
|
+
joiner gets the cached config + a keyframe immediately. iOS/tvOS only for now
|
|
79
|
+
(host-side SimulatorKit → VideoToolbox capture); Android/web are follow-ons.
|
|
80
|
+
This is capture only — input stays on `input-server`. For a still image use
|
|
81
|
+
`screenshot` / `capture-ui`; this socket is for continuous low-latency mirroring.
|
|
54
82
|
|
|
55
83
|
## Discovering valid values
|
|
56
84
|
|
|
@@ -74,7 +74,7 @@ screen recording, clipboard, `clear-state`/`uninstall-app`.
|
|
|
74
74
|
| Command | Purpose |
|
|
75
75
|
| ----------------------------------------------------- | ---------------------------------------------------------------------- |
|
|
76
76
|
| `conductor install-app <path>` | Install .app / .ipa / .apk |
|
|
77
|
-
| `conductor launch-app <appId>` | Launch app (saved to session); `--no-stop-app`, `--argument key=value` |
|
|
77
|
+
| `conductor launch-app <appId>` | Launch app (saved to session); `--no-stop-app`, `--argument key=value`, `--inject` |
|
|
78
78
|
| `conductor stop-app [<appId>]` | Stop app |
|
|
79
79
|
| `conductor uninstall-app <appId>` | Uninstall app |
|
|
80
80
|
| `conductor copy-app <bundleId> --from <id> --to <id>` | Copy an installed app between iOS simulators |
|
|
@@ -29,6 +29,49 @@ conductor capture-ui --output /tmp/screen.json
|
|
|
29
29
|
conductor tap-on @e5
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
## Native in-process inspection (iOS/tvOS simulator)
|
|
33
|
+
|
|
34
|
+
The commands above observe the app **externally** (accessibility snapshots), so
|
|
35
|
+
they can't see real component colors, fonts, or the view-controller stack. When
|
|
36
|
+
you need that native detail, launch the app with an injected in-process library
|
|
37
|
+
and use the `native-*` commands. Requires `launch-app <appId> --inject` first
|
|
38
|
+
(iOS/tvOS simulator only).
|
|
39
|
+
|
|
40
|
+
| Command | Purpose |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `conductor native-ping` | Verify the injected in-process control library is alive |
|
|
43
|
+
| `conductor native-inspect` | Real UIView/CALayer tree: resolved colors (`#RRGGBBAA`), fonts, text (incl. React Native Fabric), corner radius, borders, shadows, gradients, and each node's `absFrame` |
|
|
44
|
+
| `conductor native-nav` | Navigation state: `UINavigationController` stacks, tab selection, presented controllers, titles |
|
|
45
|
+
| `conductor native-screenshot --output <p.png>` | In-process PNG of the key window |
|
|
46
|
+
| `conductor native-image <x,y,w,h> --output <p.png>` | Extract a component as a PNG — pass a node's `absFrame` from `native-inspect` |
|
|
47
|
+
| `conductor native-snapshot <id> --output <p.png>` | Isolated PNG of one view's own content (transparent) — per-layer texture for a 3D explosion; `--with-subviews` composites the subtree |
|
|
48
|
+
| `conductor native-console [--since <n>]` / `native-network [--since <n>]` | App stdout/stderr + captured HTTP; poll with the returned `cursor` |
|
|
49
|
+
| `conductor native-heap --pattern <s> \| --class <name> \| --read <addr> [--key <keyPath>]` | Live-object browser (find classes/instances, read a property off an address) |
|
|
50
|
+
| `conductor native-appearance <light\|dark\|system> \| --direction <ltr\|rtl> \| --anim-speed <n>` | Force appearance / RTL / freeze animations app-wide |
|
|
51
|
+
| `conductor native-eval '<swift>'` | Compile & run arbitrary Swift inside the app (full UIKit / ObjC-runtime access); `--mode full` for a whole function body. e.g. `native-eval 'UIScreen.main.bounds'` |
|
|
52
|
+
| `conductor native-raw <path>` | Escape hatch — GET any in-process endpoint (e.g. `'/get?id=..&keyPath=layer.cornerRadius'`, `'/class?id=..'`, `'/responders?id=..'`, `'/swiftui'`, `'/defaults'`, `'/focus'`, `'/snapshots?scale=0.5'`). Full list in `packages/ios-inproc/README.md`. |
|
|
53
|
+
| `conductor native-view <id>` | Full property detail for one view (class chain, transform, layer, gestures, text/font) |
|
|
54
|
+
| `conductor native-set <id> <key> <value>` | **Live-edit** a property: alpha, hidden, backgroundColor, tintColor, cornerRadius, borderWidth, borderColor, frame, text, textColor. `text`/`textColor` work on RN Fabric text views too |
|
|
55
|
+
| `conductor native-props <id>` | React Native Fabric props: typed `ViewProps` + the raw JS prop bag (Fabric host views only) |
|
|
56
|
+
|
|
57
|
+
> **Editing RN Fabric text/props:** the native plane can't set text on `RCTParagraphComponentView` (no native setter) and `native-props` returns `rawProps: null` on Fabric. Edit through React instead with `conductor native-rn-set --react-tag <n> --path children --value '"…"'` (and read raw JSX props with `native-rn-props --react-tag <n>`). `reactTag` comes from this tree's `rn.reactTag`. See the conductor-metro-debugger skill. Dev builds only.
|
|
58
|
+
| `conductor native-constraints <id>` | Auto Layout constraints affecting a view + ambiguity |
|
|
59
|
+
| `conductor native-hittest <x,y>` | Topmost view at a point + ancestor chain (select-by-point) |
|
|
60
|
+
| `conductor native-highlight <id>` | Flash a highlight over the view on the device |
|
|
61
|
+
| `conductor native-find [--class <name>] [--text <s>]` | Search views by class and/or text |
|
|
62
|
+
|
|
63
|
+
Every `native-inspect` node has a stable `id` (for this launch). The Reveal-style loop:
|
|
64
|
+
inspect → pick an `id` → `native-view` for detail → `native-set` to edit live → see it
|
|
65
|
+
on the device. IDs are pointer-based and reset each launch, so re-inspect after relaunch.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
conductor launch-app com.example.app --inject
|
|
69
|
+
conductor native-inspect # tree with ids, colors, fonts, absFrame
|
|
70
|
+
conductor native-view 0x10280d0c0 # full detail for a view
|
|
71
|
+
conductor native-set 0x10280d0c0 backgroundColor '#FF3B30FF' # live-edit, visible on device
|
|
72
|
+
conductor native-image 816,286,288,288 --output /tmp/avatar.png
|
|
73
|
+
```
|
|
74
|
+
|
|
32
75
|
## Assertions
|
|
33
76
|
|
|
34
77
|
| Command | Purpose |
|
|
@@ -18,6 +18,16 @@ Playwright web.
|
|
|
18
18
|
| `conductor debug component-tree [--port N]` | On-screen React component tree |
|
|
19
19
|
| `conductor debug inspect-element <x,y>` | React component at a screen point |
|
|
20
20
|
| `conductor debug log-registry [--source metro]` | Summarize recent Metro/Hermes console logs |
|
|
21
|
+
| `conductor native-rn-set --react-tag <n> --path <dot.path> --value <json>` | Live-edit an RN component's props via React DevTools `overrideProps` (text via `--path children`, color via `--path style.color`). Dev builds only |
|
|
22
|
+
| `conductor native-rn-props --react-tag <n>` | Raw JSX props (`memoizedProps`) of an RN fiber by reactTag — the JS-side truth for Fabric where native `/props` `rawProps` is null |
|
|
23
|
+
|
|
24
|
+
`--react-tag` comes from `native-inspect`'s `rn.reactTag`. `--value` is JSON (a bare
|
|
25
|
+
string works for text). `--path` is a dot path into props: `children`, `style.color`,
|
|
26
|
+
`style.fontSize`, `accessibilityLabel`. `style.color` works whether the component's
|
|
27
|
+
`style` is an object or a composed array. These drive React itself over Metro CDP, so
|
|
28
|
+
they need a **dev/debug build** (the DevTools backend must be active) and a running
|
|
29
|
+
Metro; on a release build they return a clear "not a dev build" error. Output is
|
|
30
|
+
`{"status":"ok","applied":true}` or `{"status":"error","message":"…"}`.
|
|
21
31
|
|
|
22
32
|
## Logs
|
|
23
33
|
|