@houwert/conductor 0.24.1 → 0.25.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/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,7 @@ 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");
30
33
  const install_js_1 = require("./commands/install.js");
31
34
  const init_js_1 = require("./commands/init.js");
32
35
  const device_pool_js_1 = require("./commands/device-pool.js");
@@ -80,6 +83,27 @@ const COMMAND_HELP = {
80
83
  'download-app': download_app_js_1.HELP,
81
84
  'install-app': install_app_js_1.HELP,
82
85
  'launch-app': launch_app_js_1.HELP,
86
+ 'native-ping': native_js_1.PING_HELP,
87
+ 'native-inspect': native_js_1.INSPECT_HELP,
88
+ 'native-nav': native_js_1.NAV_HELP,
89
+ 'native-screenshot': native_js_1.SCREENSHOT_HELP,
90
+ 'native-image': native_js_1.IMAGE_HELP,
91
+ 'native-snapshot': native_js_1.SNAPSHOT_HELP,
92
+ 'native-view': native_js_1.VIEW_HELP,
93
+ 'native-set': native_js_1.SET_HELP,
94
+ 'native-props': native_js_1.PROPS_HELP,
95
+ 'native-rn-set': native_rn_js_1.RN_SET_HELP,
96
+ 'native-rn-props': native_rn_js_1.RN_PROPS_HELP,
97
+ 'native-constraints': native_js_1.CONSTRAINTS_HELP,
98
+ 'native-hittest': native_js_1.HITTEST_HELP,
99
+ 'native-highlight': native_js_1.HIGHLIGHT_HELP,
100
+ 'native-find': native_js_1.FIND_HELP,
101
+ 'native-raw': native_js_1.RAW_HELP,
102
+ 'native-console': native_js_1.CONSOLE_HELP,
103
+ 'native-network': native_js_1.NETWORK_HELP,
104
+ 'native-heap': native_js_1.HEAP_HELP,
105
+ 'native-appearance': native_js_1.APPEARANCE_HELP,
106
+ 'native-eval': native_js_1.EVAL_HELP,
83
107
  'stop-app': stop_app_js_1.HELP,
84
108
  'clear-state': clear_state_js_1.HELP,
85
109
  'uninstall-app': uninstall_app_js_1.HELP,
@@ -110,6 +134,7 @@ const COMMAND_HELP = {
110
134
  'daemon-start': daemon_js_1.HELP_DAEMON_START,
111
135
  'daemon-stop': daemon_js_1.HELP_DAEMON_STOP,
112
136
  'daemon-status': daemon_js_1.HELP_DAEMON_STATUS,
137
+ 'input-server': input_server_js_1.HELP,
113
138
  'device-pool': device_pool_js_1.HELP,
114
139
  'run-parallel': run_parallel_js_1.HELP,
115
140
  'run-sequence': run_sequence_js_1.HELP,
@@ -240,6 +265,9 @@ async function main() {
240
265
  'cdp-url',
241
266
  'cdp-target',
242
267
  'duration',
268
+ 'react-tag',
269
+ 'path',
270
+ 'value',
243
271
  ],
244
272
  alias: { h: 'help', v: 'verbose', V: 'version', o: 'output', y: 'yes' },
245
273
  });
@@ -424,9 +452,119 @@ async function main() {
424
452
  clearKeychain: argv['clear-keychain'],
425
453
  stopApp: argv['stop-app'] !== false,
426
454
  launchArgs,
455
+ inject: argv['inject'],
427
456
  });
428
457
  break;
429
458
  }
459
+ case 'native-ping': {
460
+ exitCode = await (0, native_js_1.nativePing)(opts, sessionName);
461
+ break;
462
+ }
463
+ case 'native-inspect': {
464
+ exitCode = await (0, native_js_1.nativeInspect)(opts, sessionName);
465
+ break;
466
+ }
467
+ case 'native-nav': {
468
+ exitCode = await (0, native_js_1.nativeNav)(opts, sessionName);
469
+ break;
470
+ }
471
+ case 'native-screenshot': {
472
+ exitCode = await (0, native_js_1.nativeScreenshot)(argv['output'], opts, sessionName);
473
+ break;
474
+ }
475
+ case 'native-image': {
476
+ exitCode = await (0, native_js_1.nativeImage)(rest[0], argv['output'], opts, sessionName);
477
+ break;
478
+ }
479
+ case 'native-snapshot': {
480
+ exitCode = await (0, native_js_1.nativeSnapshot)(rest[0], argv['with-subviews'], argv['output'], opts, sessionName);
481
+ break;
482
+ }
483
+ case 'native-view': {
484
+ exitCode = await (0, native_js_1.nativeView)(rest[0], opts, sessionName);
485
+ break;
486
+ }
487
+ case 'native-set': {
488
+ exitCode = await (0, native_js_1.nativeSet)(rest, opts, sessionName);
489
+ break;
490
+ }
491
+ case 'native-props': {
492
+ exitCode = await (0, native_js_1.nativeProps)(rest[0], opts, sessionName);
493
+ break;
494
+ }
495
+ case 'native-rn-set': {
496
+ const rnOpts = {
497
+ port: argv['port'] !== undefined ? Number(argv['port']) : undefined,
498
+ targetIndex: argv['target'] !== undefined ? Number(argv['target']) : undefined,
499
+ };
500
+ exitCode = await (0, native_rn_js_1.nativeRnSet)({
501
+ reactTag: argv['react-tag'],
502
+ path: argv['path'],
503
+ value: argv['value'],
504
+ }, opts, sessionName, rnOpts);
505
+ break;
506
+ }
507
+ case 'native-rn-props': {
508
+ const rnOpts = {
509
+ port: argv['port'] !== undefined ? Number(argv['port']) : undefined,
510
+ targetIndex: argv['target'] !== undefined ? Number(argv['target']) : undefined,
511
+ };
512
+ exitCode = await (0, native_rn_js_1.nativeRnProps)({ reactTag: argv['react-tag'] }, opts, sessionName, rnOpts);
513
+ break;
514
+ }
515
+ case 'native-constraints': {
516
+ exitCode = await (0, native_js_1.nativeConstraints)(rest[0], opts, sessionName);
517
+ break;
518
+ }
519
+ case 'native-hittest': {
520
+ exitCode = await (0, native_js_1.nativeHittest)(rest[0], opts, sessionName);
521
+ break;
522
+ }
523
+ case 'native-highlight': {
524
+ exitCode = await (0, native_js_1.nativeHighlight)(rest[0], opts, sessionName);
525
+ break;
526
+ }
527
+ case 'native-find': {
528
+ exitCode = await (0, native_js_1.nativeFind)({
529
+ className: argv['class'],
530
+ text: argv['text'],
531
+ }, opts, sessionName);
532
+ break;
533
+ }
534
+ case 'native-raw': {
535
+ exitCode = await (0, native_js_1.nativeRaw)(rest[0], argv['output'], opts, sessionName);
536
+ break;
537
+ }
538
+ case 'native-console': {
539
+ exitCode = await (0, native_js_1.nativeConsole)(argv['since'] !== undefined ? Number(argv['since']) : undefined, opts, sessionName);
540
+ break;
541
+ }
542
+ case 'native-network': {
543
+ exitCode = await (0, native_js_1.nativeNetwork)(argv['since'] !== undefined ? Number(argv['since']) : undefined, opts, sessionName);
544
+ break;
545
+ }
546
+ case 'native-heap': {
547
+ exitCode = await (0, native_js_1.nativeHeap)({
548
+ className: argv['class'],
549
+ pattern: argv['pattern'],
550
+ read: argv['read'],
551
+ key: argv['key'],
552
+ }, opts, sessionName);
553
+ break;
554
+ }
555
+ case 'native-appearance': {
556
+ exitCode = await (0, native_js_1.nativeAppearance)({
557
+ style: rest[0],
558
+ direction: argv['direction'],
559
+ contentSize: argv['content-size'],
560
+ animSpeed: argv['anim-speed'],
561
+ }, opts, sessionName);
562
+ break;
563
+ }
564
+ case 'native-eval': {
565
+ exitCode = await (0, native_js_1.nativeEval)(rest.join(' '), argv['mode'] === 'full' ? 'full' : 'expr', opts, sessionName);
566
+ break;
567
+ }
430
568
  case 'stop-app': {
431
569
  const appId = rest[0];
432
570
  exitCode = await (0, stop_app_js_1.stopApp)(appId, opts, sessionName);
@@ -706,6 +844,9 @@ async function main() {
706
844
  case 'daemon-status':
707
845
  exitCode = await (0, daemon_js_1.daemonStatusCmd)(opts, sessionName);
708
846
  break;
847
+ case 'input-server':
848
+ exitCode = await (0, input_server_js_1.inputServer)(opts, sessionName);
849
+ break;
709
850
  case 'device-pool': {
710
851
  const acquire = argv['acquire'];
711
852
  const release = argv['release'];
package/dist/runner.js CHANGED
@@ -3,6 +3,7 @@ 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;
6
7
  exports.runDirect = runDirect;
7
8
  exports.spawnCommand = spawnCommand;
8
9
  exports.runInlineFlow = runInlineFlow;
@@ -243,6 +244,38 @@ async function prewarmDriver(deviceId) {
243
244
  /* best-effort: a later command will report a real failure */
244
245
  }
245
246
  }
247
+ /**
248
+ * Resolve the streaming-input socket for a session's device, starting the
249
+ * daemon (and its driver + input server) if needed. Returns the loopback
250
+ * WebSocket URL the host IDE connects to. Throws if the platform has no
251
+ * streaming input (web/vega) or the port never comes up.
252
+ */
253
+ async function inputServerInfo(sessionName = 'default') {
254
+ const deviceId = await resolveDeviceId(sessionName);
255
+ if (!deviceId) {
256
+ throw new Error('No device found. Connect a device or start a simulator, then run again.');
257
+ }
258
+ const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId);
259
+ if (platform !== 'ios' && platform !== 'tvos' && platform !== 'android') {
260
+ throw new Error(`Streaming input is not available for ${platform} devices.`);
261
+ }
262
+ await (0, client_js_1.startDaemon)(deviceId);
263
+ // The input server starts just after the driver — poll status until its port appears.
264
+ const deadline = Date.now() + 60000;
265
+ while (Date.now() < deadline) {
266
+ const status = await (0, client_js_1.fetchDaemonStatus)(deviceId);
267
+ if (status && typeof status.inputPort === 'number') {
268
+ return {
269
+ device: deviceId,
270
+ platform,
271
+ inputPort: status.inputPort,
272
+ url: `ws://127.0.0.1:${status.inputPort}/input`,
273
+ };
274
+ }
275
+ await new Promise((r) => setTimeout(r, 500));
276
+ }
277
+ throw new Error(`Input server for ${deviceId} did not come up within timeout.`);
278
+ }
246
279
  /**
247
280
  * Execute a function with the driver for the given session.
248
281
  * 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.24.1",
3
+ "version": "0.25.0",
4
4
  "description": "CLI tool for mobile app interactions — optimized for AI agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -51,6 +51,19 @@ 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
+
56
+ ## Streaming input (host IDEs)
57
+
58
+ For continuous, low-latency input (live drags, fast typing) a host IDE can open
59
+ one persistent WebSocket per device instead of spawning a command per event.
60
+ `conductor input-server` ensures the daemon + driver are up and prints the
61
+ loopback URL (`ws://127.0.0.1:<port>/input`; also in `daemon-status --json` as
62
+ `inputPort`). The server sends a `hello` with per-platform capabilities, then
63
+ accepts normalized (0..1) frames: `pointer{id,phase,x,y}`, `key{code,mods,down}`,
64
+ `text{value}`, `button{name}`, `scroll{x,y,dx,dy}`, `tvremote{button}`. Conductor
65
+ owns coord→device translation and keymaps. For scripted, one-off actions use the
66
+ discrete commands above — this is for interactive host UIs.
54
67
 
55
68
  ## Discovering valid values
56
69
 
@@ -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