@houwert/conductor 0.2.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.
Files changed (60) hide show
  1. package/.claude-plugin/plugin.json +6 -0
  2. package/README.md +39 -0
  3. package/dist/commands/assert-not-visible.js +47 -0
  4. package/dist/commands/assert-visible.js +58 -0
  5. package/dist/commands/back.js +25 -0
  6. package/dist/commands/cheat-sheet.js +100 -0
  7. package/dist/commands/daemon.js +61 -0
  8. package/dist/commands/device-pool.js +202 -0
  9. package/dist/commands/erase-text.js +26 -0
  10. package/dist/commands/foreground-app.js +50 -0
  11. package/dist/commands/hide-keyboard.js +27 -0
  12. package/dist/commands/inspect.js +37 -0
  13. package/dist/commands/install.js +64 -0
  14. package/dist/commands/launch-app.js +42 -0
  15. package/dist/commands/list-apps.js +60 -0
  16. package/dist/commands/list-devices.js +61 -0
  17. package/dist/commands/open-link.js +22 -0
  18. package/dist/commands/press-key.js +91 -0
  19. package/dist/commands/run-flow-inline.js +25 -0
  20. package/dist/commands/run-flow.js +29 -0
  21. package/dist/commands/run-parallel.js +143 -0
  22. package/dist/commands/screenshot.js +29 -0
  23. package/dist/commands/scroll-until-visible.js +69 -0
  24. package/dist/commands/scroll.js +36 -0
  25. package/dist/commands/session.js +49 -0
  26. package/dist/commands/set-location.js +18 -0
  27. package/dist/commands/set-orientation.js +23 -0
  28. package/dist/commands/start-device.js +178 -0
  29. package/dist/commands/stop-app.js +32 -0
  30. package/dist/commands/swipe.js +72 -0
  31. package/dist/commands/tap.js +69 -0
  32. package/dist/commands/type.js +22 -0
  33. package/dist/daemon/client.js +112 -0
  34. package/dist/daemon/protocol.js +25 -0
  35. package/dist/daemon/server.js +208 -0
  36. package/dist/drivers/android.js +343 -0
  37. package/dist/drivers/bootstrap.js +371 -0
  38. package/dist/drivers/element-resolver.js +371 -0
  39. package/dist/drivers/flow-runner.js +1309 -0
  40. package/dist/drivers/ios.js +328 -0
  41. package/dist/drivers/js-engine.js +150 -0
  42. package/dist/drivers/wait.js +211 -0
  43. package/dist/index.js +426 -0
  44. package/dist/output.js +36 -0
  45. package/dist/pkg-root.js +28 -0
  46. package/dist/postinstall.js +12 -0
  47. package/dist/runner.js +190 -0
  48. package/dist/session.js +66 -0
  49. package/dist/update-check.js +109 -0
  50. package/dist/utils.js +19 -0
  51. package/dist/verbose.js +17 -0
  52. package/drivers/android/conductor-app.apk +0 -0
  53. package/drivers/android/conductor-server.apk +0 -0
  54. package/drivers/ios/conductor-driver-ios-config.xctestrun +126 -0
  55. package/drivers/ios/conductor-driver-ios.zip +0 -0
  56. package/drivers/ios/conductor-driver-iosUITests-Runner.zip +0 -0
  57. package/package.json +52 -0
  58. package/proto/conductor_android.proto +116 -0
  59. package/skills/conductor/SKILL.md +677 -0
  60. package/skills/conductor/references/flow-syntax.md +179 -0
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inspect = inspect;
4
+ const runner_js_1 = require("../runner.js");
5
+ const output_js_1 = require("../output.js");
6
+ const ios_js_1 = require("../drivers/ios.js");
7
+ const android_js_1 = require("../drivers/android.js");
8
+ const element_resolver_js_1 = require("../drivers/element-resolver.js");
9
+ async function inspect(opts = {}, sessionName = 'default') {
10
+ try {
11
+ const driver = await (0, runner_js_1.getDriver)(sessionName);
12
+ let text;
13
+ if (driver instanceof ios_js_1.IOSDriver) {
14
+ const hierarchy = await driver.viewHierarchy(false);
15
+ text = (0, element_resolver_js_1.inspectIOSToText)(hierarchy.axElement);
16
+ }
17
+ else if (driver instanceof android_js_1.AndroidDriver) {
18
+ const xml = await driver.viewHierarchy();
19
+ text = (0, element_resolver_js_1.inspectAndroidToText)(xml);
20
+ }
21
+ else {
22
+ throw new Error('Unknown driver type');
23
+ }
24
+ if (opts.json) {
25
+ console.log(JSON.stringify({ status: 'ok', hierarchy: text }));
26
+ }
27
+ else {
28
+ console.log(text);
29
+ }
30
+ return 0;
31
+ }
32
+ catch (err) {
33
+ const msg = err instanceof Error ? err.message : String(err);
34
+ (0, output_js_1.printError)(`inspect — failed\n${msg}`, opts);
35
+ return 1;
36
+ }
37
+ }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.installSkills = installSkills;
7
+ exports.installPlugin = installPlugin;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const output_js_1 = require("../output.js");
12
+ const pkg_root_js_1 = require("../pkg-root.js");
13
+ async function installSkills(opts) {
14
+ try {
15
+ installPlugin();
16
+ (0, output_js_1.printSuccess)('Conductor Claude Code plugin installed', opts);
17
+ return 0;
18
+ }
19
+ catch (err) {
20
+ const message = err instanceof Error ? err.message : String(err);
21
+ (0, output_js_1.printError)(`Plugin install failed: ${message}`, opts);
22
+ return 1;
23
+ }
24
+ }
25
+ function installPlugin() {
26
+ const pkgRoot = (0, pkg_root_js_1.findPkgRoot)(__dirname);
27
+ const pkgJsonPath = path_1.default.join(pkgRoot, 'package.json');
28
+ const pkg = JSON.parse(fs_1.default.readFileSync(pkgJsonPath, 'utf8'));
29
+ const version = pkg.version;
30
+ const pluginCacheDir = path_1.default.join(os_1.default.homedir(), '.claude', 'plugins', 'cache', 'conductor', 'conductor', version);
31
+ fs_1.default.mkdirSync(pluginCacheDir, { recursive: true });
32
+ const skillsSrc = path_1.default.join(pkgRoot, 'skills', 'conductor');
33
+ if (fs_1.default.existsSync(skillsSrc)) {
34
+ copyDir(skillsSrc, path_1.default.join(pluginCacheDir, 'skills', 'conductor'));
35
+ }
36
+ const pluginJsonSrc = path_1.default.join(pkgRoot, '.claude-plugin', 'plugin.json');
37
+ if (fs_1.default.existsSync(pluginJsonSrc)) {
38
+ const pluginMetaDir = path_1.default.join(pluginCacheDir, '.claude-plugin');
39
+ fs_1.default.mkdirSync(pluginMetaDir, { recursive: true });
40
+ fs_1.default.copyFileSync(pluginJsonSrc, path_1.default.join(pluginMetaDir, 'plugin.json'));
41
+ }
42
+ const installedPluginsPath = path_1.default.join(os_1.default.homedir(), '.claude', 'plugins', 'installed_plugins.json');
43
+ let installed = { plugins: [] };
44
+ if (fs_1.default.existsSync(installedPluginsPath)) {
45
+ installed = JSON.parse(fs_1.default.readFileSync(installedPluginsPath, 'utf8'));
46
+ }
47
+ installed.plugins = installed.plugins.filter((p) => p.name !== 'conductor');
48
+ installed.plugins.push({ name: 'conductor', version, path: pluginCacheDir });
49
+ fs_1.default.mkdirSync(path_1.default.dirname(installedPluginsPath), { recursive: true });
50
+ fs_1.default.writeFileSync(installedPluginsPath, JSON.stringify(installed, null, 2));
51
+ }
52
+ function copyDir(src, dest) {
53
+ fs_1.default.mkdirSync(dest, { recursive: true });
54
+ for (const entry of fs_1.default.readdirSync(src, { withFileTypes: true })) {
55
+ const srcPath = path_1.default.join(src, entry.name);
56
+ const destPath = path_1.default.join(dest, entry.name);
57
+ if (entry.isDirectory()) {
58
+ copyDir(srcPath, destPath);
59
+ }
60
+ else {
61
+ fs_1.default.copyFileSync(srcPath, destPath);
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.launchApp = launchApp;
4
+ const runner_js_1 = require("../runner.js");
5
+ const session_js_1 = require("../session.js");
6
+ const output_js_1 = require("../output.js");
7
+ const ios_js_1 = require("../drivers/ios.js");
8
+ const android_js_1 = require("../drivers/android.js");
9
+ async function launchApp(appId, deviceId, opts = {}, sessionName = 'default', flags = {}) {
10
+ if (!appId) {
11
+ (0, output_js_1.printError)('launch-app requires <appId>', opts);
12
+ return 1;
13
+ }
14
+ await (0, session_js_1.updateSession)({ appId, ...(deviceId ? { deviceId } : {}) }, sessionName);
15
+ const result = await (0, runner_js_1.runDirect)(async (driver) => {
16
+ if (flags.clearKeychain)
17
+ await driver.clearKeychain();
18
+ if (flags.clearState)
19
+ await driver.clearAppState(appId);
20
+ const shouldStop = flags.stopApp ?? true;
21
+ if (shouldStop) {
22
+ if (driver instanceof ios_js_1.IOSDriver)
23
+ await driver.terminateApp(appId);
24
+ else if (driver instanceof android_js_1.AndroidDriver)
25
+ await driver.stopApp(appId);
26
+ }
27
+ if (driver instanceof ios_js_1.IOSDriver) {
28
+ await driver.launchApp(appId, flags.launchArgs);
29
+ }
30
+ else if (driver instanceof android_js_1.AndroidDriver) {
31
+ await driver.launchApp(appId, flags.launchArgs);
32
+ }
33
+ }, sessionName);
34
+ if (result.success) {
35
+ (0, output_js_1.printSuccess)(`launch-app "${appId}" — done`, opts);
36
+ return 0;
37
+ }
38
+ else {
39
+ (0, output_js_1.printError)(`launch-app "${appId}" — failed\n${result.stderr}`, opts);
40
+ return 1;
41
+ }
42
+ }
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listApps = listApps;
4
+ const runner_js_1 = require("../runner.js");
5
+ const session_js_1 = require("../session.js");
6
+ const output_js_1 = require("../output.js");
7
+ const bootstrap_js_1 = require("../drivers/bootstrap.js");
8
+ async function resolveDeviceId(sessionName) {
9
+ if (sessionName !== 'default')
10
+ return sessionName;
11
+ const session = await (0, session_js_1.getSession)(sessionName);
12
+ return session.deviceId ?? (await (0, runner_js_1.detectFirstDevice)());
13
+ }
14
+ async function listApps(opts = {}, sessionName = 'default') {
15
+ const deviceId = await resolveDeviceId(sessionName);
16
+ if (!deviceId) {
17
+ (0, output_js_1.printError)('No device found. Connect a device or start a simulator first.', opts);
18
+ return 1;
19
+ }
20
+ const platform = await (0, bootstrap_js_1.detectPlatform)(deviceId);
21
+ let appIds;
22
+ if (platform === 'ios') {
23
+ const result = await (0, runner_js_1.spawnCommand)('bash', [
24
+ '-c',
25
+ `xcrun simctl listapps ${deviceId} | plutil -convert json - -o -`,
26
+ ]);
27
+ if (!result.success) {
28
+ (0, output_js_1.printError)(`list-apps failed: ${result.stderr}`, opts);
29
+ return 1;
30
+ }
31
+ try {
32
+ const parsed = JSON.parse(result.stdout);
33
+ appIds = Object.keys(parsed).sort();
34
+ }
35
+ catch {
36
+ (0, output_js_1.printError)('Failed to parse app list from simctl', opts);
37
+ return 1;
38
+ }
39
+ }
40
+ else {
41
+ const result = await (0, runner_js_1.spawnCommand)('adb', ['-s', deviceId, 'shell', 'pm', 'list', 'packages']);
42
+ if (!result.success) {
43
+ (0, output_js_1.printError)(`list-apps failed: ${result.stderr}`, opts);
44
+ return 1;
45
+ }
46
+ appIds = result.stdout
47
+ .split('\n')
48
+ .map((l) => l.trim().replace(/^package:/, ''))
49
+ .filter(Boolean)
50
+ .sort();
51
+ }
52
+ if (opts.json) {
53
+ (0, output_js_1.printData)({ status: 'ok', apps: appIds }, opts);
54
+ }
55
+ else {
56
+ for (const id of appIds)
57
+ console.log(id);
58
+ }
59
+ return 0;
60
+ }
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.listDevices = listDevices;
4
+ const runner_js_1 = require("../runner.js");
5
+ const output_js_1 = require("../output.js");
6
+ async function listDevices(opts) {
7
+ const devices = [];
8
+ // Try adb devices (Android)
9
+ const adb = await (0, runner_js_1.spawnCommand)('adb', ['devices', '-l']);
10
+ if (adb.success || adb.stdout.includes('List of devices')) {
11
+ const lines = adb.stdout.split('\n').slice(1); // skip header
12
+ for (const line of lines) {
13
+ const trimmed = line.trim();
14
+ if (!trimmed || trimmed === '')
15
+ continue;
16
+ const parts = trimmed.split(/\s+/);
17
+ const id = parts[0];
18
+ const status = parts[1] ?? 'unknown';
19
+ if (id && status) {
20
+ const modelMatch = trimmed.match(/model:(\S+)/);
21
+ const name = modelMatch ? modelMatch[1].replace(/_/g, ' ') : id;
22
+ devices.push({ id, name, platform: 'android', status });
23
+ }
24
+ }
25
+ }
26
+ // Try xcrun simctl list (iOS simulators)
27
+ const xcrun = await (0, runner_js_1.spawnCommand)('xcrun', ['simctl', 'list', 'devices', 'booted', '--json']);
28
+ if (xcrun.success) {
29
+ try {
30
+ const parsed = JSON.parse(xcrun.stdout);
31
+ for (const [_runtime, sims] of Object.entries(parsed.devices)) {
32
+ for (const sim of sims) {
33
+ if (sim.state === 'Booted') {
34
+ devices.push({
35
+ id: sim.udid,
36
+ name: sim.name,
37
+ platform: 'ios',
38
+ status: 'booted',
39
+ });
40
+ }
41
+ }
42
+ }
43
+ }
44
+ catch {
45
+ // ignore parse errors
46
+ }
47
+ }
48
+ if (devices.length === 0) {
49
+ (0, output_js_1.printError)('No devices found. Start an emulator or simulator first.', opts);
50
+ return 1;
51
+ }
52
+ if (opts.json) {
53
+ (0, output_js_1.printData)({ status: 'ok', devices }, opts);
54
+ }
55
+ else {
56
+ for (const d of devices) {
57
+ console.log(`${d.platform.padEnd(8)} ${d.status.padEnd(10)} ${d.id} ${d.name}`);
58
+ }
59
+ }
60
+ return 0;
61
+ }
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.openLink = openLink;
4
+ const runner_js_1 = require("../runner.js");
5
+ const output_js_1 = require("../output.js");
6
+ async function openLink(url, opts = {}, sessionName = 'default') {
7
+ if (!url) {
8
+ (0, output_js_1.printError)('open-link requires <url>', opts);
9
+ return 1;
10
+ }
11
+ const result = await (0, runner_js_1.runDirect)(async (driver) => {
12
+ await driver.openLink(url);
13
+ }, sessionName);
14
+ if (result.success) {
15
+ (0, output_js_1.printSuccess)(`open-link ${url} — done`, opts);
16
+ return 0;
17
+ }
18
+ else {
19
+ (0, output_js_1.printError)(`open-link ${url} — failed\n${result.stderr}`, opts);
20
+ return 1;
21
+ }
22
+ }
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.pressKey = pressKey;
4
+ const runner_js_1 = require("../runner.js");
5
+ const output_js_1 = require("../output.js");
6
+ const ios_js_1 = require("../drivers/ios.js");
7
+ const android_js_1 = require("../drivers/android.js");
8
+ const VALID_KEYS = [
9
+ 'Enter',
10
+ 'Backspace',
11
+ 'Home',
12
+ 'End',
13
+ 'Tab',
14
+ 'Delete',
15
+ 'Escape',
16
+ 'VolumeUp',
17
+ 'VolumeDown',
18
+ 'Power',
19
+ 'Lock',
20
+ 'Back',
21
+ 'Camera',
22
+ 'Search',
23
+ ];
24
+ // iOS XCTest pressKey accepts these values (maps to XCUIKeyboardKey)
25
+ const IOS_KEY_MAP = {
26
+ Backspace: 'delete',
27
+ Delete: 'delete',
28
+ Enter: 'enter',
29
+ Tab: 'tab',
30
+ };
31
+ // iOS pressButton for hardware buttons
32
+ const IOS_BUTTON_MAP = {
33
+ Home: 'home',
34
+ Lock: 'lock',
35
+ Power: 'lock',
36
+ };
37
+ // Android keyevent codes
38
+ const ANDROID_KEYCODE = {
39
+ Home: 3,
40
+ Back: 4,
41
+ Enter: 66,
42
+ Backspace: 67,
43
+ Delete: 67,
44
+ Tab: 61,
45
+ Lock: 26,
46
+ Power: 26,
47
+ VolumeUp: 24,
48
+ VolumeDown: 25,
49
+ Camera: 27,
50
+ Search: 84,
51
+ Escape: 111,
52
+ End: 123,
53
+ };
54
+ async function pressKey(key, opts = {}, sessionName = 'default') {
55
+ if (!key) {
56
+ (0, output_js_1.printError)(`press-key requires <key>. Valid keys: ${VALID_KEYS.join(', ')}`, opts);
57
+ return 1;
58
+ }
59
+ const matched = VALID_KEYS.find((k) => k.toLowerCase() === key.toLowerCase());
60
+ if (!matched) {
61
+ (0, output_js_1.printError)(`Unknown key "${key}". Valid keys: ${VALID_KEYS.join(', ')}`, opts);
62
+ return 1;
63
+ }
64
+ const result = await (0, runner_js_1.runDirect)(async (driver) => {
65
+ if (driver instanceof ios_js_1.IOSDriver) {
66
+ const iosKey = IOS_KEY_MAP[matched];
67
+ const iosButton = IOS_BUTTON_MAP[matched];
68
+ if (iosKey) {
69
+ await driver.pressKey(iosKey);
70
+ }
71
+ else if (iosButton) {
72
+ await driver.pressButton(iosButton);
73
+ }
74
+ // Keys not mapped on iOS (e.g. Back, VolumeUp) are silently ignored
75
+ }
76
+ else if (driver instanceof android_js_1.AndroidDriver) {
77
+ const code = ANDROID_KEYCODE[matched];
78
+ if (code !== undefined) {
79
+ await driver.pressKeyEvent(code);
80
+ }
81
+ }
82
+ }, sessionName);
83
+ if (result.success) {
84
+ (0, output_js_1.printSuccess)(`press-key ${matched} — done`, opts);
85
+ return 0;
86
+ }
87
+ else {
88
+ (0, output_js_1.printError)(`press-key ${matched} — failed\n${result.stderr}`, opts);
89
+ return 1;
90
+ }
91
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runFlowInline = runFlowInline;
4
+ const runner_js_1 = require("../runner.js");
5
+ const output_js_1 = require("../output.js");
6
+ /** run-flow-inline: Execute inline Maestro YAML commands natively via the flow runner. */
7
+ async function runFlowInline(yaml, opts = {}, sessionName = 'default', benchmark = false) {
8
+ if (!yaml) {
9
+ (0, output_js_1.printError)('run-flow-inline requires <yaml>', opts);
10
+ return 1;
11
+ }
12
+ const result = await (0, runner_js_1.runInlineFlow)(yaml, sessionName, benchmark);
13
+ if (result.success) {
14
+ (0, output_js_1.printSuccess)('run-flow-inline — done', opts);
15
+ if (!opts.json && result.stdout.trim()) {
16
+ console.log(result.stdout.trim());
17
+ }
18
+ return 0;
19
+ }
20
+ else {
21
+ const detail = result.stderr.trim() || result.stdout.trim();
22
+ (0, output_js_1.printError)(`run-flow-inline — failed\n${detail}`, opts);
23
+ return 1;
24
+ }
25
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runFlow = runFlow;
7
+ const path_1 = __importDefault(require("path"));
8
+ const runner_js_1 = require("../runner.js");
9
+ const flow_runner_js_1 = require("../drivers/flow-runner.js");
10
+ const output_js_1 = require("../output.js");
11
+ async function runFlow(file, opts = {}, sessionName = 'default', env = {}, benchmark = false) {
12
+ if (!file) {
13
+ (0, output_js_1.printError)('run-flow requires <file>', opts);
14
+ return 1;
15
+ }
16
+ const resolvedFile = path_1.default.resolve(process.cwd(), file);
17
+ try {
18
+ const driver = await (0, runner_js_1.getDriver)(sessionName);
19
+ const flow = await (0, flow_runner_js_1.parseFlowFile)(resolvedFile, env);
20
+ await (0, flow_runner_js_1.executeFlow)(flow, driver, { cwd: path_1.default.dirname(resolvedFile), env, benchmark });
21
+ (0, output_js_1.printSuccess)(`run-flow "${file}" — done`, opts);
22
+ return 0;
23
+ }
24
+ catch (err) {
25
+ const detail = err instanceof Error ? err.message : String(err);
26
+ (0, output_js_1.printError)(`run-flow "${file}" — failed\n${detail}`, opts);
27
+ return 1;
28
+ }
29
+ }
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runParallel = runParallel;
7
+ /**
8
+ * run-parallel: Run Maestro YAML flows in parallel across all booted simulators.
9
+ *
10
+ * Usage:
11
+ * conductor run-parallel --flows-dir ./tests
12
+ * conductor run-parallel --flows-dir ./tests --devices auto
13
+ *
14
+ * - Auto-detects all booted simulators (and connected Android devices)
15
+ * - Distributes flow files round-robin across devices
16
+ * - Spawns one child process per shard
17
+ * - Collects and prints aggregated pass/fail results
18
+ */
19
+ const child_process_1 = require("child_process");
20
+ const path_1 = __importDefault(require("path"));
21
+ const fs_1 = __importDefault(require("fs"));
22
+ const output_js_1 = require("../output.js");
23
+ async function runParallel(flowsDir, opts = {}) {
24
+ if (!flowsDir) {
25
+ (0, output_js_1.printError)('run-parallel requires --flows-dir <path>', opts);
26
+ return 1;
27
+ }
28
+ const resolvedDir = path_1.default.resolve(flowsDir);
29
+ if (!fs_1.default.existsSync(resolvedDir)) {
30
+ (0, output_js_1.printError)(`flows-dir not found: ${resolvedDir}`, opts);
31
+ return 1;
32
+ }
33
+ // Discover flow files
34
+ const flowFiles = fs_1.default
35
+ .readdirSync(resolvedDir)
36
+ .filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'))
37
+ .map((f) => path_1.default.join(resolvedDir, f));
38
+ if (flowFiles.length === 0) {
39
+ (0, output_js_1.printError)(`No YAML flow files found in: ${resolvedDir}`, opts);
40
+ return 1;
41
+ }
42
+ // Discover devices
43
+ const devices = await discoverAllDevices();
44
+ if (devices.length === 0) {
45
+ (0, output_js_1.printError)('No devices found. Connect a device or start a simulator.', opts);
46
+ return 1;
47
+ }
48
+ console.log(`Found ${devices.length} device(s), ${flowFiles.length} flow(s). Distributing...`);
49
+ // Assign flows to devices (round-robin)
50
+ const assignments = flowFiles.map((f, i) => ({
51
+ deviceId: devices[i % devices.length],
52
+ flowFile: f,
53
+ }));
54
+ // Run all shards in parallel
55
+ const promises = assignments.map(({ deviceId, flowFile }) => runShard(deviceId, flowFile));
56
+ const results = await Promise.all(promises);
57
+ // Print results
58
+ const passed = results.filter((r) => r.success);
59
+ const failed = results.filter((r) => !r.success);
60
+ if (opts.json) {
61
+ console.log(JSON.stringify({
62
+ status: failed.length === 0 ? 'ok' : 'error',
63
+ total: results.length,
64
+ passed: passed.length,
65
+ failed: failed.length,
66
+ results: results.map((r) => ({
67
+ deviceId: r.deviceId,
68
+ flowFile: path_1.default.basename(r.flowFile),
69
+ success: r.success,
70
+ })),
71
+ }));
72
+ }
73
+ else {
74
+ console.log('\n─── Results ───────────────────────────────────────');
75
+ for (const r of results) {
76
+ const status = r.success ? '✓ PASS' : '✗ FAIL';
77
+ console.log(`${status} ${path_1.default.basename(r.flowFile)} [${r.deviceId.slice(0, 8)}...]`);
78
+ if (!r.success && r.output.trim()) {
79
+ console.log(` ${r.output.trim().split('\n').join('\n ')}`);
80
+ }
81
+ }
82
+ console.log(`\nTotal: ${results.length} Passed: ${passed.length} Failed: ${failed.length}`);
83
+ }
84
+ return failed.length === 0 ? 0 : 1;
85
+ }
86
+ async function runShard(deviceId, flowFile) {
87
+ return new Promise((resolve) => {
88
+ const proc = (0, child_process_1.spawn)(process.execPath, // node
89
+ [process.argv[1], 'run-flow', flowFile, '--device', deviceId], { stdio: ['ignore', 'pipe', 'pipe'] });
90
+ let output = '';
91
+ proc.stdout?.on('data', (c) => {
92
+ output += c.toString();
93
+ });
94
+ proc.stderr?.on('data', (c) => {
95
+ output += c.toString();
96
+ });
97
+ proc.on('close', (code) => {
98
+ resolve({ deviceId, flowFile, success: code === 0, output });
99
+ });
100
+ proc.on('error', (err) => {
101
+ resolve({ deviceId, flowFile, success: false, output: err.message });
102
+ });
103
+ });
104
+ }
105
+ async function discoverAllDevices() {
106
+ const devices = [];
107
+ try {
108
+ const out = await spawnCapture('adb', ['devices']);
109
+ for (const line of out.split('\n').slice(1)) {
110
+ const id = line.trim().split(/\s+/)[0];
111
+ if (id && !line.includes('offline') && id !== '')
112
+ devices.push(id);
113
+ }
114
+ }
115
+ catch {
116
+ /* ok */
117
+ }
118
+ try {
119
+ const out = await spawnCapture('xcrun', ['simctl', 'list', 'devices', 'booted', '--json']);
120
+ const parsed = JSON.parse(out);
121
+ for (const sims of Object.values(parsed.devices)) {
122
+ for (const sim of sims) {
123
+ if (sim.state === 'Booted')
124
+ devices.push(sim.udid);
125
+ }
126
+ }
127
+ }
128
+ catch {
129
+ /* ok */
130
+ }
131
+ return devices;
132
+ }
133
+ function spawnCapture(cmd, args) {
134
+ return new Promise((resolve, reject) => {
135
+ const proc = (0, child_process_1.spawn)(cmd, args, { stdio: ['ignore', 'pipe', 'ignore'] });
136
+ let out = '';
137
+ proc.stdout?.on('data', (chunk) => {
138
+ out += chunk.toString();
139
+ });
140
+ proc.on('close', (code) => (code === 0 ? resolve(out) : reject(new Error(`${cmd} failed`))));
141
+ proc.on('error', reject);
142
+ });
143
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.screenshot = screenshot;
7
+ const path_1 = __importDefault(require("path"));
8
+ const promises_1 = __importDefault(require("fs/promises"));
9
+ const runner_js_1 = require("../runner.js");
10
+ const output_js_1 = require("../output.js");
11
+ async function screenshot(outputPath, opts = {}, sessionName = 'default') {
12
+ const timestamp = Date.now();
13
+ const defaultName = `screenshot-${timestamp}.png`;
14
+ const resolvedPath = outputPath
15
+ ? path_1.default.resolve(outputPath)
16
+ : path_1.default.resolve(process.cwd(), defaultName);
17
+ const result = await (0, runner_js_1.runDirect)(async (driver) => {
18
+ const buf = await driver.screenshot();
19
+ await promises_1.default.writeFile(resolvedPath, buf);
20
+ }, sessionName);
21
+ if (result.success) {
22
+ (0, output_js_1.printSuccess)(`screenshot saved to ${resolvedPath}`, opts);
23
+ return 0;
24
+ }
25
+ else {
26
+ (0, output_js_1.printError)(`screenshot — failed\n${result.stderr}`, opts);
27
+ return 1;
28
+ }
29
+ }