@houwert/conductor 0.15.0 → 0.17.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
@@ -30,6 +30,15 @@ const daemon_js_1 = require("./commands/daemon.js");
30
30
  const install_js_1 = require("./commands/install.js");
31
31
  const device_pool_js_1 = require("./commands/device-pool.js");
32
32
  const run_parallel_js_1 = require("./commands/run-parallel.js");
33
+ const run_sequence_js_1 = require("./commands/run-sequence.js");
34
+ const gestures_js_1 = require("./commands/gestures.js");
35
+ const workspace_js_1 = require("./commands/workspace.js");
36
+ const debug_js_1 = require("./commands/debug.js");
37
+ const network_js_1 = require("./commands/network.js");
38
+ const flow_record_js_1 = require("./commands/flow-record.js");
39
+ const profile_js_1 = require("./commands/profile.js");
40
+ const crashes_js_1 = require("./commands/crashes.js");
41
+ const flow_recorder_js_1 = require("./drivers/flow-recorder.js");
33
42
  const foreground_app_js_1 = require("./commands/foreground-app.js");
34
43
  const list_apps_js_1 = require("./commands/list-apps.js");
35
44
  const copy_app_js_1 = require("./commands/copy-app.js");
@@ -47,6 +56,8 @@ const stop_device_js_1 = require("./commands/stop-device.js");
47
56
  const delete_device_js_1 = require("./commands/delete-device.js");
48
57
  const logs_js_1 = require("./commands/logs.js");
49
58
  const memory_js_1 = require("./commands/memory.js");
59
+ const metro_js_1 = require("./commands/metro.js");
60
+ const clipboard_js_1 = require("./commands/clipboard.js");
50
61
  const device_picker_js_1 = require("./device-picker.js");
51
62
  const update_check_js_1 = require("./update-check.js");
52
63
  const pkg_root_js_1 = require("./pkg-root.js");
@@ -93,8 +104,19 @@ const COMMAND_HELP = {
93
104
  'daemon-status': daemon_js_1.HELP_DAEMON_STATUS,
94
105
  'device-pool': device_pool_js_1.HELP,
95
106
  'run-parallel': run_parallel_js_1.HELP,
107
+ 'run-sequence': run_sequence_js_1.HELP,
108
+ gestures: gestures_js_1.HELP,
109
+ workspace: workspace_js_1.HELP,
110
+ debug: debug_js_1.HELP,
111
+ network: network_js_1.HELP,
112
+ 'flow record': flow_record_js_1.HELP,
113
+ profile: profile_js_1.HELP,
114
+ crashes: crashes_js_1.HELP,
96
115
  logs: logs_js_1.HELP,
97
116
  memory: memory_js_1.HELP,
117
+ metro: metro_js_1.HELP,
118
+ clipboard: clipboard_js_1.HELP,
119
+ paste: ' paste Trigger OS-level paste (or type clipboard on iOS)',
98
120
  };
99
121
  const OPTIONS_HELP = `Options:
100
122
  --device <id> Target device ID (also keys the session and daemon)
@@ -133,6 +155,7 @@ async function main() {
133
155
  'optional',
134
156
  'benchmark',
135
157
  'dump',
158
+ 'tappable',
136
159
  'objects',
137
160
  'heap',
138
161
  'leaks',
@@ -173,6 +196,24 @@ async function main() {
173
196
  'vs',
174
197
  'top',
175
198
  'filter',
199
+ 'port',
200
+ 'target',
201
+ 'at',
202
+ 'file',
203
+ 'scale',
204
+ 'center',
205
+ 'degrees',
206
+ 'angle',
207
+ 'limit',
208
+ 'method',
209
+ 'body',
210
+ 'header',
211
+ 'url',
212
+ 'out',
213
+ 'track',
214
+ 'interval',
215
+ 'app',
216
+ 'since',
176
217
  ],
177
218
  alias: { h: 'help', v: 'verbose', V: 'version' },
178
219
  });
@@ -207,6 +248,8 @@ async function main() {
207
248
  'copy-app',
208
249
  'device-pool',
209
250
  'run-parallel',
251
+ 'metro',
252
+ 'workspace',
210
253
  // `logs --list` and `logs --source metro` only query Metro on localhost — no device needed
211
254
  // `logs` always needs a device session — Metro discovery is device-scoped.
212
255
  // `daemon-stop --all` stops every daemon — no device needed
@@ -461,7 +504,8 @@ async function main() {
461
504
  }
462
505
  case 'take-screenshot': {
463
506
  const outPath = argv['output'];
464
- exitCode = await (0, screenshot_js_1.screenshot)(outPath, opts, sessionName);
507
+ const fullPage = Boolean(argv['full-page']);
508
+ exitCode = await (0, screenshot_js_1.screenshot)(outPath, opts, sessionName, fullPage);
465
509
  break;
466
510
  }
467
511
  case 'capture-ui': {
@@ -470,7 +514,11 @@ async function main() {
470
514
  break;
471
515
  }
472
516
  case 'inspect':
473
- exitCode = await (0, inspect_js_1.inspect)(opts, sessionName, { dump: argv['dump'] });
517
+ exitCode = await (0, inspect_js_1.inspect)(opts, sessionName, {
518
+ dump: argv['dump'],
519
+ at: argv['at'],
520
+ tappableOnly: argv['tappable'],
521
+ });
474
522
  break;
475
523
  case 'focused':
476
524
  exitCode = await (0, focused_js_1.focused)(opts, sessionName, {
@@ -541,17 +589,231 @@ async function main() {
541
589
  exitCode = await (0, device_pool_js_1.devicePool)(action, releaseId, opts);
542
590
  break;
543
591
  }
592
+ case 'run-sequence': {
593
+ const file = argv['file'] ?? rest[0];
594
+ exitCode = await (0, run_sequence_js_1.runSequence)(file, opts, sessionName);
595
+ break;
596
+ }
544
597
  case 'run-parallel': {
545
598
  const flowsDir = argv['flows-dir'] ?? rest[0] ?? '';
546
599
  exitCode = await (0, run_parallel_js_1.runParallel)(flowsDir, opts);
547
600
  break;
548
601
  }
602
+ case 'crashes': {
603
+ const sub = (rest[0] ?? 'list').toLowerCase();
604
+ if (sub === 'list') {
605
+ exitCode = await (0, crashes_js_1.crashesList)(opts, sessionName, {
606
+ app: argv['app'],
607
+ since: argv['since'],
608
+ });
609
+ }
610
+ else if (sub === 'show') {
611
+ exitCode = await (0, crashes_js_1.crashesShow)(rest[1] ?? '', opts);
612
+ }
613
+ else if (sub === 'tail') {
614
+ exitCode = await (0, crashes_js_1.crashesTail)(opts, sessionName);
615
+ }
616
+ else {
617
+ console.error('Usage: conductor crashes <list|show|tail>');
618
+ exitCode = 1;
619
+ }
620
+ break;
621
+ }
622
+ case 'profile': {
623
+ const sub = (rest[0] ?? '').toLowerCase();
624
+ const port = argv['port'] !== undefined ? Number(argv['port']) : undefined;
625
+ const targetIndex = argv['target'] !== undefined ? Number(argv['target']) : undefined;
626
+ if (sub === 'cpu') {
627
+ const durationSec = argv['duration'] !== undefined ? Number(argv['duration']) : 10;
628
+ exitCode = await (0, profile_js_1.profileCpu)(opts, sessionName, {
629
+ durationSec,
630
+ out: argv['out'],
631
+ appId: rest[1],
632
+ });
633
+ }
634
+ else if (sub === 'memory') {
635
+ const trackSec = argv['track'] !== undefined ? Number(argv['track']) : 10;
636
+ const intervalMs = argv['interval'] !== undefined ? Number(argv['interval']) : 1000;
637
+ exitCode = await (0, profile_js_1.profileMemory)(opts, sessionName, {
638
+ trackSec,
639
+ intervalMs,
640
+ appId: rest[1],
641
+ });
642
+ }
643
+ else if (sub === 'react') {
644
+ const sub2 = (rest[1] ?? '').toLowerCase();
645
+ const top = argv['top'] !== undefined ? Number(argv['top']) : 20;
646
+ if (sub2 === 'start') {
647
+ exitCode = await (0, profile_js_1.profileReactStart)(opts, sessionName, { port, targetIndex });
648
+ }
649
+ else if (sub2 === 'stop') {
650
+ exitCode = await (0, profile_js_1.profileReactStop)(opts, sessionName, { port, targetIndex }, top);
651
+ }
652
+ else {
653
+ console.error('Usage: conductor profile react <start|stop>');
654
+ exitCode = 1;
655
+ }
656
+ }
657
+ else {
658
+ console.error('Usage: conductor profile <cpu|memory|react> [args]');
659
+ exitCode = 1;
660
+ }
661
+ break;
662
+ }
663
+ case 'flow': {
664
+ const sub1 = (rest[0] ?? '').toLowerCase();
665
+ if (sub1 !== 'record') {
666
+ console.error('Usage: conductor flow record <start|finish|echo|status>');
667
+ exitCode = 1;
668
+ break;
669
+ }
670
+ const sub2 = (rest[1] ?? '').toLowerCase();
671
+ exitCode = await (0, flow_record_js_1.flowRecord)(sub2, rest.slice(2).map(String), opts, sessionName, argv);
672
+ break;
673
+ }
674
+ case 'network': {
675
+ const sub = (rest[0] ?? '').toLowerCase();
676
+ const port = argv['port'] !== undefined ? Number(argv['port']) : undefined;
677
+ const targetIndex = argv['target'] !== undefined ? Number(argv['target']) : undefined;
678
+ if (sub === 'logs') {
679
+ const limit = argv['limit'] !== undefined ? Number(argv['limit']) : undefined;
680
+ exitCode = await (0, network_js_1.networkLogs)(opts, sessionName, { port, targetIndex, limit });
681
+ }
682
+ else if (sub === 'request') {
683
+ const url = rest[1] ?? argv['url'] ?? '';
684
+ const rawHeaders = argv['header'];
685
+ const headers = Array.isArray(rawHeaders)
686
+ ? rawHeaders
687
+ : rawHeaders
688
+ ? [rawHeaders]
689
+ : [];
690
+ exitCode = await (0, network_js_1.networkRequest)(url, opts, sessionName, {
691
+ port,
692
+ targetIndex,
693
+ method: argv['method'],
694
+ body: argv['body'],
695
+ headers,
696
+ });
697
+ }
698
+ else {
699
+ console.error('Usage: conductor network <logs|request> [args]');
700
+ exitCode = 1;
701
+ }
702
+ break;
703
+ }
704
+ case 'debug': {
705
+ const sub = (rest[0] ?? '').toLowerCase();
706
+ const debugOpts = {
707
+ port: argv['port'] !== undefined ? Number(argv['port']) : undefined,
708
+ targetIndex: argv['target'] !== undefined ? Number(argv['target']) : undefined,
709
+ };
710
+ if (sub === 'status') {
711
+ exitCode = await (0, debug_js_1.debugStatus)(opts, sessionName, debugOpts);
712
+ }
713
+ else if (sub === 'evaluate' || sub === 'eval') {
714
+ const expr = rest.slice(1).join(' ');
715
+ exitCode = await (0, debug_js_1.debugEvaluate)(expr, opts, sessionName, debugOpts);
716
+ }
717
+ else if (sub === 'component-tree') {
718
+ exitCode = await (0, debug_js_1.debugComponentTree)(opts, sessionName, debugOpts);
719
+ }
720
+ else if (sub === 'inspect-element') {
721
+ const at = rest[1] ?? argv['at'] ?? '';
722
+ exitCode = await (0, debug_js_1.debugInspectElement)(at, opts, sessionName, debugOpts);
723
+ }
724
+ else if (sub === 'log-registry') {
725
+ exitCode = await (0, debug_js_1.debugLogRegistry)(opts, sessionName);
726
+ }
727
+ else if (sub === 'reload') {
728
+ exitCode = await (0, debug_js_1.debugReload)(opts, sessionName, debugOpts);
729
+ }
730
+ else {
731
+ console.error('Usage: conductor debug <status|evaluate|component-tree|inspect-element|log-registry|reload>');
732
+ exitCode = 1;
733
+ }
734
+ break;
735
+ }
736
+ case 'workspace': {
737
+ const sub = (rest[0] ?? 'info').toLowerCase();
738
+ exitCode = await (0, workspace_js_1.workspaceCmd)(sub, opts);
739
+ break;
740
+ }
741
+ case 'pinch':
742
+ exitCode = await (0, gestures_js_1.pinch)(opts, sessionName, {
743
+ scale: argv['scale'] !== undefined ? Number(argv['scale']) : undefined,
744
+ center: argv['center'],
745
+ duration: argv['duration'] !== undefined ? Number(argv['duration']) : undefined,
746
+ angle: argv['angle'] !== undefined ? Number(argv['angle']) : undefined,
747
+ });
748
+ break;
749
+ case 'rotate-gesture':
750
+ exitCode = await (0, gestures_js_1.rotateGesture)(opts, sessionName, {
751
+ degrees: argv['degrees'] !== undefined ? Number(argv['degrees']) : undefined,
752
+ center: argv['center'],
753
+ duration: argv['duration'] !== undefined ? Number(argv['duration']) : undefined,
754
+ });
755
+ break;
756
+ case 'gesture': {
757
+ const file = argv['file'];
758
+ const rawJson = !file ? rest.join(' ') : undefined;
759
+ exitCode = await (0, gestures_js_1.gesture)(rawJson, file, opts, sessionName);
760
+ break;
761
+ }
762
+ case 'clipboard': {
763
+ const sub = (rest[0] ?? '').toLowerCase();
764
+ if (sub === 'read') {
765
+ exitCode = await (0, clipboard_js_1.clipboardRead)(opts, sessionName);
766
+ }
767
+ else if (sub === 'write') {
768
+ const text = rest.slice(1).join(' ');
769
+ exitCode = await (0, clipboard_js_1.clipboardWrite)(text, opts, sessionName);
770
+ }
771
+ else {
772
+ console.error('Usage: conductor clipboard <read|write> [text]');
773
+ exitCode = 1;
774
+ }
775
+ break;
776
+ }
777
+ case 'paste':
778
+ exitCode = await (0, clipboard_js_1.paste)(opts, sessionName);
779
+ break;
780
+ case 'metro': {
781
+ const sub = (rest[0] ?? '').toLowerCase();
782
+ const port = argv['port'] !== undefined ? Number(argv['port']) : undefined;
783
+ const targetIndex = argv['target'] !== undefined ? Number(argv['target']) : undefined;
784
+ const metroSession = argv['device'] ?? 'default';
785
+ if (sub === 'stop') {
786
+ exitCode = await (0, metro_js_1.metroStop)(opts, { port });
787
+ }
788
+ else if (sub === 'reload') {
789
+ exitCode = await (0, metro_js_1.metroReload)(opts, metroSession, { port, targetIndex });
790
+ }
791
+ else {
792
+ console.error('Usage: conductor metro <stop|reload> [--port N] [--target N]');
793
+ exitCode = 1;
794
+ }
795
+ break;
796
+ }
549
797
  default:
550
798
  // Should be unreachable — unknown commands are caught before device resolution.
551
799
  console.error(`Unknown command: ${command}`);
552
800
  console.error('Run `conductor --help` for usage.');
553
801
  exitCode = 1;
554
802
  }
803
+ // Flow recording — append a YAML step for action commands that succeeded.
804
+ if (exitCode === 0 && !NO_DEVICE_COMMANDS.has(command) && command !== 'flow') {
805
+ try {
806
+ const active = await (0, flow_recorder_js_1.getActiveRecording)(sessionName);
807
+ if (active) {
808
+ const step = (0, flow_recorder_js_1.commandToYamlStep)(command, rest.map(String), argv);
809
+ if (step)
810
+ (0, flow_recorder_js_1.appendStep)(active, step);
811
+ }
812
+ }
813
+ catch {
814
+ // Recording is best-effort — never fail the command for a bookkeeping issue.
815
+ }
816
+ }
555
817
  process.exit(exitCode);
556
818
  }
557
819
  main().catch((err) => {
package/dist/runner.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.detectFirstDevice = detectFirstDevice;
4
4
  exports.getDriver = getDriver;
5
+ exports.prewarmDriver = prewarmDriver;
5
6
  exports.runDirect = runDirect;
6
7
  exports.spawnCommand = spawnCommand;
7
8
  exports.runInlineFlow = runInlineFlow;
@@ -194,6 +195,23 @@ async function getDriver(sessionName = 'default') {
194
195
  _driverCache.set(sessionName, driver);
195
196
  return driver;
196
197
  }
198
+ /**
199
+ * Eagerly start the device daemon (and its XCTest driver) for `deviceId` so
200
+ * the runner is already warm by the time the first interaction command runs.
201
+ *
202
+ * `start-device` is the natural prewarm point: bringing the driver up there
203
+ * moves the one-off startup cost off the first `tap-on` / `assert-visible`.
204
+ * Best-effort — failures are swallowed since a real command will surface any
205
+ * genuine startup error with a proper message.
206
+ */
207
+ async function prewarmDriver(deviceId) {
208
+ try {
209
+ await (0, client_js_1.startDaemon)(deviceId);
210
+ }
211
+ catch {
212
+ /* best-effort: a later command will report a real failure */
213
+ }
214
+ }
197
215
  /**
198
216
  * Execute a function with the driver for the given session.
199
217
  * 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.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "CLI tool for mobile app interactions — optimized for AI agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -12,6 +12,8 @@ service ConductorDriver {
12
12
 
13
13
  rpc tap(TapRequest) returns (TapResponse) {}
14
14
 
15
+ rpc gesturePath(GesturePathRequest) returns (GesturePathResponse) {}
16
+
15
17
  rpc inputText(InputTextRequest) returns (InputTextResponse) {}
16
18
 
17
19
  rpc eraseAllText(EraseAllTextRequest) returns (EraseAllTextResponse) {}
@@ -76,6 +78,33 @@ message TapRequest {
76
78
 
77
79
  message TapResponse {}
78
80
 
81
+ // One step in a finger's path.
82
+ // x, y pixel coordinates on the device's natural orientation.
83
+ // dt_ms delay in milliseconds since the previous step. For the first
84
+ // step in a path, this is the initial offset before the touch
85
+ // goes down.
86
+ message GestureStep {
87
+ double x = 1;
88
+ double y = 2;
89
+ uint32 dt_ms = 3;
90
+ }
91
+
92
+ // A single finger's traced path. Must have at least one step. The first step
93
+ // is the down position; subsequent steps are moves; the finger lifts after
94
+ // the last step.
95
+ message GestureFingerPath {
96
+ repeated GestureStep steps = 1;
97
+ }
98
+
99
+ // Multi-finger gesture request. paths.size() == 1 is a single-finger gesture;
100
+ // paths.size() >= 2 dispatches a true multi-touch gesture via
101
+ // UiAutomation.dispatchGesture (API 24+).
102
+ message GesturePathRequest {
103
+ repeated GestureFingerPath paths = 1;
104
+ }
105
+
106
+ message GesturePathResponse {}
107
+
79
108
  message InputTextRequest {
80
109
  string text = 1;
81
110
  }