@houwert/conductor 0.21.0 → 0.23.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 (52) hide show
  1. package/dist/commands/assert-not-visible.js +3 -1
  2. package/dist/commands/assert-visible.js +3 -1
  3. package/dist/commands/back.js +4 -0
  4. package/dist/commands/capture-ui.js +4 -2
  5. package/dist/commands/clipboard.js +9 -0
  6. package/dist/commands/crashes.js +5 -0
  7. package/dist/commands/delete-device.js +6 -1
  8. package/dist/commands/download-app.js +4 -0
  9. package/dist/commands/erase-text.js +2 -1
  10. package/dist/commands/focused.js +3 -1
  11. package/dist/commands/foreground-app.js +2 -1
  12. package/dist/commands/gestures.js +7 -0
  13. package/dist/commands/hide-keyboard.js +4 -0
  14. package/dist/commands/inspect.js +4 -3
  15. package/dist/commands/install-app.js +11 -0
  16. package/dist/commands/launch-app.js +6 -0
  17. package/dist/commands/list-apps.js +5 -0
  18. package/dist/commands/list-devices.js +16 -0
  19. package/dist/commands/memory.js +5 -0
  20. package/dist/commands/press-key.js +32 -4
  21. package/dist/commands/profile.js +5 -0
  22. package/dist/commands/screenshot.js +3 -1
  23. package/dist/commands/scroll-until-visible.js +3 -1
  24. package/dist/commands/scroll.js +2 -1
  25. package/dist/commands/start-device.js +60 -3
  26. package/dist/commands/stop-app.js +4 -0
  27. package/dist/commands/stop-device.js +8 -3
  28. package/dist/commands/swipe.js +2 -1
  29. package/dist/commands/tap.js +9 -6
  30. package/dist/commands/uninstall-app.js +4 -0
  31. package/dist/commands/web-targets.js +84 -0
  32. package/dist/commands/workspace.js +4 -1
  33. package/dist/daemon/log-collector.js +9 -1
  34. package/dist/daemon/server.js +70 -50
  35. package/dist/drivers/bootstrap.js +12 -0
  36. package/dist/drivers/flow-runner.js +38 -8
  37. package/dist/drivers/ios.js +5 -2
  38. package/dist/drivers/log-sources/metro-discovery.js +56 -2
  39. package/dist/drivers/log-sources/vega.js +77 -0
  40. package/dist/drivers/metro-cdp.js +11 -3
  41. package/dist/drivers/vega/automation-client.js +79 -0
  42. package/dist/drivers/vega/cli.js +199 -0
  43. package/dist/drivers/vega/connection.js +48 -0
  44. package/dist/drivers/vega/input.js +100 -0
  45. package/dist/drivers/vega/page-source-parser.js +229 -0
  46. package/dist/drivers/vega.js +165 -0
  47. package/dist/enum-options.js +15 -3
  48. package/dist/index.js +46 -2
  49. package/dist/runner.js +31 -0
  50. package/package.json +1 -1
  51. package/skills/conductor-device-interact/SKILL.md +4 -3
  52. package/skills/conductor-device-setup/SKILL.md +59 -28
@@ -160,8 +160,11 @@ class IOSDriver {
160
160
  await this.post('pressKey', { key });
161
161
  this.invalidateHierarchyCache();
162
162
  }
163
- async pressButton(button) {
164
- await this.post('pressButton', { button });
163
+ async pressButton(button, duration) {
164
+ await this.post('pressButton', {
165
+ button,
166
+ ...(duration !== undefined ? { duration } : {}),
167
+ });
165
168
  this.invalidateHierarchyCache();
166
169
  }
167
170
  async launchApp(bundleId, args) {
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isMetroPort = isMetroPort;
4
4
  exports.discoverMetroPortForDevice = discoverMetroPortForDevice;
5
5
  exports.getDeviceDisplayName = getDeviceDisplayName;
6
+ exports.deviceNameMatches = deviceNameMatches;
6
7
  exports.selectTargetForDevice = selectTargetForDevice;
7
8
  exports.targetsForDevice = targetsForDevice;
8
9
  /**
@@ -25,6 +26,10 @@ exports.targetsForDevice = targetsForDevice;
25
26
  const child_process_1 = require("child_process");
26
27
  const metro_js_1 = require("./metro.js");
27
28
  const sdk_js_1 = require("../../android/sdk.js");
29
+ const cli_js_1 = require("../vega/cli.js");
30
+ // Standard Metro dev-server ports to probe for the Vega VVD, which — unlike
31
+ // iOS/Android — exposes no host-visible socket mapping we can read.
32
+ const VEGA_METRO_CANDIDATE_PORTS = [8081, 8082];
28
33
  /**
29
34
  * Heuristic to reject ports that obviously aren't a Metro dev server (system
30
35
  * ports, well-known services). Candidates that pass are always verified via
@@ -61,6 +66,33 @@ async function discoverMetroPortForDevice(platform, deviceId) {
61
66
  if (platform === 'ios' || platform === 'tvos') {
62
67
  return discoverMetroPortIOS(deviceId);
63
68
  }
69
+ if (platform === 'vega') {
70
+ return discoverMetroPortVega(deviceId);
71
+ }
72
+ return null;
73
+ }
74
+ /**
75
+ * Locate the Metro port for a Vega VVD. There is no host-visible socket mapping,
76
+ * so probe the standard Metro ports and accept the first that is a real Metro
77
+ * server hosting a target for this device (matched by display name when known).
78
+ */
79
+ async function discoverMetroPortVega(deviceId) {
80
+ const displayName = await getDeviceDisplayName('vega', deviceId);
81
+ for (const port of VEGA_METRO_CANDIDATE_PORTS) {
82
+ try {
83
+ const targets = await (0, metro_js_1.fetchTargets)(port, 'localhost');
84
+ if (targets.length === 0)
85
+ continue;
86
+ // Prefer a name match; fall back to any target (single-device v1).
87
+ if (!displayName || targets.some((t) => deviceNameMatches(t.deviceName, displayName))) {
88
+ return port;
89
+ }
90
+ return port;
91
+ }
92
+ catch {
93
+ // not Metro on this port — try the next
94
+ }
95
+ }
64
96
  return null;
65
97
  }
66
98
  async function discoverMetroPortAndroid(deviceId) {
@@ -198,8 +230,30 @@ async function getDeviceDisplayName(platform, deviceId) {
198
230
  return null;
199
231
  }
200
232
  }
233
+ if (platform === 'vega') {
234
+ // deviceId is `vega:<serial>`; resolve the VVD's reported description.
235
+ const serial = deviceId.replace(/^vega:/, '');
236
+ try {
237
+ const devices = await new cli_js_1.VegaCli().listDevices();
238
+ const match = devices.find((d) => d.serial === serial);
239
+ return match?.description ?? serial;
240
+ }
241
+ catch {
242
+ return serial;
243
+ }
244
+ }
201
245
  return null;
202
246
  }
247
+ /**
248
+ * Whether a Metro target's `deviceName` refers to the same device as `displayName`.
249
+ * Tolerant of the suffixes Metro appends to the bare model name — e.g. Android's
250
+ * `ro.product.model` is `Chromecast` while Metro reports `Chromecast - 14 - API 34`.
251
+ */
252
+ function deviceNameMatches(targetDeviceName, displayName) {
253
+ if (!targetDeviceName)
254
+ return false;
255
+ return targetDeviceName === displayName || targetDeviceName.startsWith(`${displayName} `);
256
+ }
203
257
  /**
204
258
  * Filter Metro /json targets to those belonging to this device (by display
205
259
  * name), preferring the fusebox runtime if present. Returns undefined when
@@ -208,7 +262,7 @@ async function getDeviceDisplayName(platform, deviceId) {
208
262
  */
209
263
  function selectTargetForDevice(targets, displayName) {
210
264
  const withWs = targets.filter((t) => t.webSocketDebuggerUrl);
211
- const matches = withWs.filter((t) => t.deviceName === displayName);
265
+ const matches = withWs.filter((t) => deviceNameMatches(t.deviceName, displayName));
212
266
  if (matches.length === 0)
213
267
  return undefined;
214
268
  const fusebox = matches.find((t) => t.reactNative?.capabilities?.prefersFuseboxFrontend);
@@ -216,5 +270,5 @@ function selectTargetForDevice(targets, displayName) {
216
270
  }
217
271
  /** Convenience: all /json targets belonging to this device. */
218
272
  function targetsForDevice(targets, displayName) {
219
- return targets.filter((t) => t.webSocketDebuggerUrl && t.deviceName === displayName);
273
+ return targets.filter((t) => t.webSocketDebuggerUrl && deviceNameMatches(t.deviceName, displayName));
220
274
  }
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VegaLogSource = void 0;
4
+ const cli_js_1 = require("../vega/cli.js");
5
+ /** Map a Vega/Android-style level token to a LogEntry level. */
6
+ function mapLevel(token) {
7
+ switch (token.toUpperCase()) {
8
+ case 'V':
9
+ case 'VERBOSE':
10
+ return 'verbose';
11
+ case 'D':
12
+ case 'DEBUG':
13
+ return 'debug';
14
+ case 'I':
15
+ case 'INFO':
16
+ return 'info';
17
+ case 'W':
18
+ case 'WARN':
19
+ case 'WARNING':
20
+ return 'warning';
21
+ case 'E':
22
+ case 'ERROR':
23
+ case 'F':
24
+ case 'FATAL':
25
+ return 'error';
26
+ default:
27
+ return 'log';
28
+ }
29
+ }
30
+ class VegaLogSource {
31
+ constructor(serial) {
32
+ this.serial = serial;
33
+ this.proc = null;
34
+ this.callback = null;
35
+ this.buffer = '';
36
+ }
37
+ async connect() {
38
+ this.proc = new cli_js_1.VegaCli(this.serial).startLogStream();
39
+ const onData = (chunk) => {
40
+ this.buffer += chunk.toString('utf-8');
41
+ const lines = this.buffer.split('\n');
42
+ this.buffer = lines.pop() ?? '';
43
+ for (const line of lines) {
44
+ if (line.trim())
45
+ this.emit(line);
46
+ }
47
+ };
48
+ this.proc.stdout?.on('data', onData);
49
+ this.proc.stderr?.on('data', onData);
50
+ this.proc.on('error', () => {
51
+ /* vega CLI not available — no logs */
52
+ });
53
+ }
54
+ onEntry(callback) {
55
+ this.callback = callback;
56
+ }
57
+ disconnect() {
58
+ if (this.proc) {
59
+ this.proc.kill('SIGTERM');
60
+ this.proc = null;
61
+ }
62
+ }
63
+ emit(line) {
64
+ // Best-effort level extraction from a leading/embedded level token; the raw
65
+ // line is always preserved as the message.
66
+ const levelMatch = /\b([VDIWEF]|VERBOSE|DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL)\b/.exec(line);
67
+ const entry = {
68
+ timestamp: new Date().toISOString(),
69
+ level: levelMatch ? mapLevel(levelMatch[1]) : 'log',
70
+ message: line,
71
+ stackTrace: null,
72
+ source: 'device',
73
+ };
74
+ this.callback?.(entry);
75
+ }
76
+ }
77
+ exports.VegaLogSource = VegaLogSource;
@@ -40,12 +40,20 @@ function selectDebuggerUrl(targets, opts, displayName) {
40
40
  }
41
41
  return withWs[opts.targetIndex].webSocketDebuggerUrl;
42
42
  }
43
- if (displayName) {
44
- const target = (0, metro_discovery_js_1.selectTargetForDevice)(withWs, displayName);
43
+ // Device-scoped: must resolve to that device's own target. Never silently
44
+ // fall back to another device — that reloads the wrong app and reports success.
45
+ if (opts.deviceId) {
46
+ const target = displayName ? (0, metro_discovery_js_1.selectTargetForDevice)(withWs, displayName) : undefined;
45
47
  if (target)
46
48
  return target.webSocketDebuggerUrl;
49
+ const available = withWs
50
+ .map((t, i) => ` [${i}] ${t.deviceName ?? t.title ?? '(unnamed)'}`)
51
+ .join('\n');
52
+ throw new Error(`No Metro debugger target for device ${opts.deviceId}` +
53
+ (displayName ? ` (${displayName})` : '') +
54
+ `.\nAvailable targets:\n${available}\nPass --target <index> to pick one explicitly.`);
47
55
  }
48
- // Prefer the Hermes/React target by title, otherwise first.
56
+ // No device requested: prefer the Hermes/React target by title, otherwise first.
49
57
  const target = withWs.find((t) => t.title && /hermes|react/i.test(t.title)) ?? withWs[0];
50
58
  return target.webSocketDebuggerUrl;
51
59
  }
@@ -0,0 +1,79 @@
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.VegaAutomationClient = exports.VegaToolkitUnavailableError = void 0;
7
+ /**
8
+ * Talks to the on-device automation toolkit (the accessibility server Amazon's
9
+ * Appium Vega driver uses), which serves JSON-RPC on device TCP port 8383.
10
+ *
11
+ * Vega is not adb, so there is no host-side `adb forward`. Instead we run `curl`
12
+ * on the device (`vega … run-cmd`), writing the JSON-RPC response to a device file
13
+ * with `curl -o` and pulling it to the host with `copy-from`. Routing through a
14
+ * file (rather than the command's stdout) is required: `run-cmd` truncates large
15
+ * stdout — a full-screen screenshot PNG and a deep page-source tree both exceed it.
16
+ */
17
+ const promises_1 = __importDefault(require("fs/promises"));
18
+ const os_1 = __importDefault(require("os"));
19
+ const path_1 = __importDefault(require("path"));
20
+ const TOOLKIT_PORT = 8383;
21
+ class VegaToolkitUnavailableError extends Error {
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = 'VegaToolkitUnavailableError';
25
+ }
26
+ }
27
+ exports.VegaToolkitUnavailableError = VegaToolkitUnavailableError;
28
+ class VegaAutomationClient {
29
+ constructor(connection) {
30
+ this.connection = connection;
31
+ }
32
+ /** Current screen's page-source XML. */
33
+ async getPageSource() {
34
+ const result = await this.call('getPageSource');
35
+ return typeof result === 'string' ? result : JSON.stringify(result);
36
+ }
37
+ /** Current screen as PNG bytes (base64 in the RPC result). */
38
+ async getScreenshot() {
39
+ const result = await this.call('takeScreenshot');
40
+ if (typeof result !== 'string') {
41
+ throw new VegaToolkitUnavailableError('takeScreenshot returned no image data');
42
+ }
43
+ return Buffer.from(result, 'base64');
44
+ }
45
+ /** POST a parameterless JSON-RPC [method] to the toolkit and return its `result`. */
46
+ async call(method) {
47
+ const devicePath = `/tmp/conductor-vega-${method}.json`;
48
+ const payload = `{"jsonrpc":"2.0","id":1,"method":"${method}","params":{}}`;
49
+ await this.connection.shell(`curl -s -X POST -H 'Content-Type: application/json' -d '${payload}' ` +
50
+ `-o ${devicePath} http://127.0.0.1:${TOOLKIT_PORT}/jsonrpc`);
51
+ const hostFile = path_1.default.join(os_1.default.tmpdir(), `conductor-vega-${method}-${process.pid}-${TOOLKIT_PORT}.json`);
52
+ try {
53
+ await this.connection.copyFrom(devicePath, hostFile);
54
+ const raw = await promises_1.default.readFile(hostFile, 'utf-8').catch(() => '');
55
+ if (!raw) {
56
+ throw new VegaToolkitUnavailableError(`Empty response from the Vega automation toolkit (method=${method}). The toolkit ` +
57
+ `attaches at app launch after the enable flag is set — relaunch the app under test.`);
58
+ }
59
+ let node;
60
+ try {
61
+ node = JSON.parse(raw);
62
+ }
63
+ catch {
64
+ throw new VegaToolkitUnavailableError(`Malformed toolkit response for ${method}`);
65
+ }
66
+ if (node.error !== undefined && node.error !== null) {
67
+ throw new VegaToolkitUnavailableError(`Toolkit error for ${method}: ${JSON.stringify(node.error)}`);
68
+ }
69
+ if (node.result === undefined) {
70
+ throw new VegaToolkitUnavailableError(`Toolkit response for ${method} had no result`);
71
+ }
72
+ return node.result;
73
+ }
74
+ finally {
75
+ await promises_1.default.rm(hostFile, { force: true }).catch(() => { });
76
+ }
77
+ }
78
+ }
79
+ exports.VegaAutomationClient = VegaAutomationClient;
@@ -0,0 +1,199 @@
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.VegaCli = void 0;
7
+ exports.commandSucceeded = commandSucceeded;
8
+ exports.commandOutput = commandOutput;
9
+ /**
10
+ * Thin wrapper over Amazon's Vega developer CLI. Vega is not Android, so we drive
11
+ * it through the SDK's own binary (`vega`/`kepler`/`vda`) rather than adb — this is
12
+ * the only supported channel and keeps the same path working for physical Fire TVs.
13
+ *
14
+ * Command surface (verified against the Vega SDK via Maestro's reference driver):
15
+ * - `vega device list` prints one `<selector> : <profile> - <arch> - <os> - <host>`
16
+ * line per device; the `-d` selector is the first field (e.g. `VirtualDevice`).
17
+ * - on-device commands run via `vega device run-cmd -d <sel> -c '<cmd>'` (there is
18
+ * no `shell -c`; plain `shell` is interactive).
19
+ * - `-d` is a per-subcommand option, so it follows the subcommand name.
20
+ */
21
+ const child_process_1 = require("child_process");
22
+ const fs_1 = __importDefault(require("fs"));
23
+ const os_1 = __importDefault(require("os"));
24
+ const path_1 = __importDefault(require("path"));
25
+ const verbose_js_1 = require("../../verbose.js");
26
+ function commandSucceeded(r) {
27
+ return r.exitCode === 0;
28
+ }
29
+ function commandOutput(r) {
30
+ return `${r.stdout}\n${r.stderr}`.trim();
31
+ }
32
+ let _resolvedBinary = null;
33
+ class VegaCli {
34
+ constructor(serial, binary) {
35
+ this.serial = serial;
36
+ this.binary = binary ?? VegaCli.resolveBinary();
37
+ }
38
+ /** Run a raw CLI invocation and capture output. */
39
+ exec(args, timeoutSeconds = 120) {
40
+ (0, verbose_js_1.log)(`vega cli: ${this.binary} ${args.join(' ')}`);
41
+ return new Promise((resolve) => {
42
+ const proc = (0, child_process_1.spawn)(this.binary, args, { stdio: ['ignore', 'pipe', 'pipe'] });
43
+ let stdout = '';
44
+ let stderr = '';
45
+ const timer = setTimeout(() => {
46
+ proc.kill('SIGKILL');
47
+ }, timeoutSeconds * 1000);
48
+ proc.stdout.on('data', (c) => {
49
+ stdout += c.toString();
50
+ });
51
+ proc.stderr.on('data', (c) => {
52
+ stderr += c.toString();
53
+ });
54
+ proc.on('close', (code) => {
55
+ clearTimeout(timer);
56
+ resolve({ exitCode: code ?? 1, stdout, stderr });
57
+ });
58
+ proc.on('error', (err) => {
59
+ clearTimeout(timer);
60
+ resolve({ exitCode: 127, stdout: '', stderr: err.message });
61
+ });
62
+ });
63
+ }
64
+ /** Build `device <subcommand> [-d serial] <rest…>` — `-d` follows the subcommand. */
65
+ deviceArgs(subcommand, ...rest) {
66
+ const selector = this.serial ? ['-d', this.serial] : [];
67
+ return ['device', subcommand, ...selector, ...rest];
68
+ }
69
+ deviceExec(subcommand, rest = [], timeoutSeconds = 120) {
70
+ return this.exec(this.deviceArgs(subcommand, ...rest), timeoutSeconds);
71
+ }
72
+ async listDevices() {
73
+ const result = await this.exec(['device', 'list']);
74
+ if (!commandSucceeded(result)) {
75
+ (0, verbose_js_1.log)(`\`vega device list\` failed: ${commandOutput(result)}`);
76
+ return [];
77
+ }
78
+ return VegaCli.parseDeviceList(result.stdout);
79
+ }
80
+ /** Run a shell command on the device and return its stdout (via `run-cmd -c`). */
81
+ async shell(command) {
82
+ const result = await this.deviceExec('run-cmd', ['-c', command]);
83
+ if (!commandSucceeded(result)) {
84
+ throw new Error(`Vega run-cmd failed: ${commandOutput(result)}`);
85
+ }
86
+ return result.stdout;
87
+ }
88
+ /** Copy a device file to the host via `copy-from` (a file transfer, no stdout limit). */
89
+ async copyFrom(remotePath, localPath) {
90
+ const result = await this.deviceExec('copy-from', ['-s', remotePath, '-o', localPath]);
91
+ if (!commandSucceeded(result)) {
92
+ throw new Error(`Failed to copy ${remotePath} from device: ${commandOutput(result)}`);
93
+ }
94
+ }
95
+ async launchApp(appId) {
96
+ const result = await this.deviceExec('launch-app', ['-a', appId]);
97
+ if (!commandSucceeded(result)) {
98
+ throw new Error(`Failed to launch ${appId}: ${commandOutput(result)}`);
99
+ }
100
+ }
101
+ async terminateApp(appId) {
102
+ const result = await this.deviceExec('terminate-app', ['-a', appId]);
103
+ if (!commandSucceeded(result)) {
104
+ (0, verbose_js_1.log)(`Failed to terminate ${appId}: ${commandOutput(result)}`);
105
+ }
106
+ }
107
+ async installApp(vpkgPath) {
108
+ const result = await this.deviceExec('install-app', ['-p', vpkgPath], 300);
109
+ if (!commandSucceeded(result)) {
110
+ throw new Error(`Failed to install ${vpkgPath}: ${commandOutput(result)}`);
111
+ }
112
+ }
113
+ async listInstalledApps() {
114
+ const result = await this.deviceExec('installed-apps');
115
+ if (!commandSucceeded(result))
116
+ return [];
117
+ return result.stdout
118
+ .split('\n')
119
+ .map((l) => l.trim())
120
+ .filter((l) => l.length > 0 && l.includes('.') && !l.includes(' '));
121
+ }
122
+ /** Start streaming device logs with stdout/stderr piped; caller owns the process. */
123
+ startLogStream() {
124
+ return (0, child_process_1.spawn)(this.binary, this.deviceArgs('start-log-stream'), {
125
+ stdio: ['ignore', 'pipe', 'pipe'],
126
+ });
127
+ }
128
+ /**
129
+ * Boot a Vega Virtual Device via `vega virtual-device start [<name>]`, detached
130
+ * like the Android emulator — the process runs the VVD in the background and the
131
+ * caller polls {@link listDevices} for readiness. Returns the spawned process.
132
+ */
133
+ spawnVirtualDeviceStart(name) {
134
+ const args = ['virtual-device', 'start', ...(name ? [name] : [])];
135
+ const proc = (0, child_process_1.spawn)(this.binary, args, { detached: true, stdio: 'ignore' });
136
+ proc.unref();
137
+ return proc;
138
+ }
139
+ // Device lines look like: `VirtualDevice : tv - aarch64 - OS - amazon-<hostname>`.
140
+ // The `-d` selector is the first field (before " : "), not the trailing hostname.
141
+ static parseDeviceList(output) {
142
+ return output
143
+ .split('\n')
144
+ .map((l) => l.trim())
145
+ .filter((l) => l.includes(' : '))
146
+ .map((line) => {
147
+ const serial = line.slice(0, line.indexOf(' : ')).trim();
148
+ if (!serial)
149
+ return null;
150
+ const isVirtual = /^virtualdevice$/i.test(serial) || /^simulator$/i.test(serial) || /virtual/i.test(line);
151
+ return { serial, description: line, isVirtual };
152
+ })
153
+ .filter((d) => d !== null);
154
+ }
155
+ /** Resolve the Vega CLI binary: env override → PATH probe → ~/vega/bin → `vega`. */
156
+ static resolveBinary() {
157
+ if (_resolvedBinary)
158
+ return _resolvedBinary;
159
+ const envOverride = process.env.CONDUCTOR_VEGA_CLI;
160
+ if (envOverride && envOverride.trim()) {
161
+ _resolvedBinary = envOverride.trim();
162
+ return _resolvedBinary;
163
+ }
164
+ const candidates = ['vega', 'kepler', 'vda'];
165
+ for (const candidate of candidates) {
166
+ if (isOnPath(candidate)) {
167
+ _resolvedBinary = candidate;
168
+ return candidate;
169
+ }
170
+ }
171
+ const binDir = path_1.default.join(os_1.default.homedir(), 'vega', 'bin');
172
+ for (const candidate of candidates) {
173
+ const full = path_1.default.join(binDir, candidate);
174
+ if (fs_1.default.existsSync(full)) {
175
+ _resolvedBinary = full;
176
+ return full;
177
+ }
178
+ }
179
+ // Fall back to `vega`; the first invocation surfaces a clear "command not found".
180
+ _resolvedBinary = 'vega';
181
+ return _resolvedBinary;
182
+ }
183
+ }
184
+ exports.VegaCli = VegaCli;
185
+ function isOnPath(command) {
186
+ const dirs = (process.env.PATH ?? '').split(path_1.default.delimiter);
187
+ for (const dir of dirs) {
188
+ if (!dir)
189
+ continue;
190
+ try {
191
+ fs_1.default.accessSync(path_1.default.join(dir, command), fs_1.default.constants.X_OK);
192
+ return true;
193
+ }
194
+ catch {
195
+ /* not here */
196
+ }
197
+ }
198
+ return false;
199
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VegaDeviceConnection = void 0;
4
+ /**
5
+ * On-device operations for a single Vega target, all routed through {@link VegaCli}.
6
+ *
7
+ * The automation toolkit that serves the view hierarchy (device port 8383) only
8
+ * attaches when the flag file exists *at app launch*, so {@link ensureToolkitEnabled}
9
+ * must be called before launching the app under test.
10
+ */
11
+ const cli_js_1 = require("./cli.js");
12
+ const verbose_js_1 = require("../../verbose.js");
13
+ const TOOLKIT_ENABLE_FLAG = '/tmp/automation-toolkit.enable';
14
+ const SCREEN_SIZE_RE = /(\d+)\s*x\s*(\d+)/;
15
+ class VegaDeviceConnection {
16
+ constructor(serial, cli = new cli_js_1.VegaCli(serial)) {
17
+ this.serial = serial;
18
+ this.cli = cli;
19
+ }
20
+ shell(command) {
21
+ return this.cli.shell(command);
22
+ }
23
+ copyFrom(remotePath, localPath) {
24
+ return this.cli.copyFrom(remotePath, localPath);
25
+ }
26
+ /** Idempotently create the toolkit enable flag (read at app launch). */
27
+ async ensureToolkitEnabled() {
28
+ try {
29
+ await this.shell(`touch ${TOOLKIT_ENABLE_FLAG}`);
30
+ }
31
+ catch (err) {
32
+ (0, verbose_js_1.log)(`Failed to enable Vega automation toolkit flag: ${String(err)}`);
33
+ }
34
+ }
35
+ /**
36
+ * Device screen size via `inputd-cli get_screen_size`. This is also the
37
+ * developer-mode gate: with dev mode off the on-device shell service is down
38
+ * and no "<W> x <H>" prints.
39
+ */
40
+ async screenSize() {
41
+ const out = (await this.shell('inputd-cli get_screen_size')).trim();
42
+ const match = SCREEN_SIZE_RE.exec(out);
43
+ if (!match)
44
+ return null;
45
+ return { width: parseInt(match[1], 10), height: parseInt(match[2], 10) };
46
+ }
47
+ }
48
+ exports.VegaDeviceConnection = VegaDeviceConnection;
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VegaInput = exports.VegaInputUnavailableError = exports.VEGA_BUTTON_KEYS = void 0;
4
+ exports.shellQuote = shellQuote;
5
+ const SETTLE_MS = 300;
6
+ const LONG_PRESS_MS = 1000;
7
+ // Verified key names (via Maestro): select is KEY_ENTER (KEY_SELECT is a no-op),
8
+ // home is KEY_HOMEPAGE (KEY_HOME is inert), back is KEY_BACK.
9
+ exports.VEGA_BUTTON_KEYS = {
10
+ up: 'KEY_UP',
11
+ down: 'KEY_DOWN',
12
+ left: 'KEY_LEFT',
13
+ right: 'KEY_RIGHT',
14
+ select: 'KEY_ENTER',
15
+ menu: 'KEY_MENU',
16
+ home: 'KEY_HOMEPAGE',
17
+ back: 'KEY_BACK',
18
+ playPause: 'KEY_PLAYPAUSE',
19
+ rewind: 'KEY_REWIND',
20
+ fastForward: 'KEY_FASTFORWARD',
21
+ volumeUp: 'KEY_VOLUMEUP',
22
+ volumeDown: 'KEY_VOLUMEDOWN',
23
+ };
24
+ class VegaInputUnavailableError extends Error {
25
+ constructor(message) {
26
+ super(message);
27
+ this.name = 'VegaInputUnavailableError';
28
+ }
29
+ }
30
+ exports.VegaInputUnavailableError = VegaInputUnavailableError;
31
+ class VegaInput {
32
+ constructor(connection) {
33
+ this.connection = connection;
34
+ this.inputChecked = false;
35
+ }
36
+ /** Probe the input channel once; raises an actionable error if dev mode is off. */
37
+ async ensureInputAvailable() {
38
+ if (this.inputChecked)
39
+ return;
40
+ const size = await this.connection.screenSize().catch(() => null);
41
+ if (!size) {
42
+ throw new VegaInputUnavailableError('Vega input is unavailable: `inputd-cli` could not be reached over the device shell, ' +
43
+ "which means the VVD's developer mode is off. Enable it (`vsm developer-mode enable`, " +
44
+ 'e.g. via `vega device shell`) and retry.');
45
+ }
46
+ this.inputChecked = true;
47
+ }
48
+ async pressButton(button) {
49
+ const keyName = exports.VEGA_BUTTON_KEYS[button];
50
+ if (!keyName)
51
+ throw new Error(`Button "${button}" is not supported on Vega`);
52
+ await this.buttonPress(keyName);
53
+ }
54
+ async buttonPress(keyName) {
55
+ await this.ensureInputAvailable();
56
+ await this.connection.shell(`inputd-cli button_press ${keyName}`);
57
+ await settle();
58
+ }
59
+ async tap(x, y) {
60
+ await this.ensureInputAvailable();
61
+ await this.connection.shell(`inputd-cli touch ${Math.round(x)} ${Math.round(y)}`);
62
+ await settle();
63
+ }
64
+ async longPress(x, y) {
65
+ await this.ensureInputAvailable();
66
+ // Vega expresses a long press via the hold duration on a touch.
67
+ await this.connection.shell(`inputd-cli touch ${Math.round(x)} ${Math.round(y)} --holdDuration ${LONG_PRESS_MS}`);
68
+ await settle();
69
+ }
70
+ async swipe(startX, startY, endX, endY, durationMs) {
71
+ await this.ensureInputAvailable();
72
+ await this.connection.shell(`inputd-cli swipe ${Math.round(startX)} ${Math.round(startY)} ` +
73
+ `${Math.round(endX)} ${Math.round(endY)} --interval ${Math.round(durationMs)}`);
74
+ await settle();
75
+ }
76
+ async inputText(text) {
77
+ if (text.includes('\n') || text.includes('\r')) {
78
+ throw new Error('Vega keyboard text must not contain newlines');
79
+ }
80
+ await this.ensureInputAvailable();
81
+ await this.connection.shell(`inputd-cli send_text ${shellQuote(text)}`);
82
+ await settle();
83
+ }
84
+ async eraseText(charactersToErase) {
85
+ await this.ensureInputAvailable();
86
+ for (let i = 0; i < charactersToErase; i++) {
87
+ await this.connection.shell('inputd-cli button_press KEY_BACKSPACE');
88
+ }
89
+ await settle();
90
+ }
91
+ }
92
+ exports.VegaInput = VegaInput;
93
+ function settle() {
94
+ // Give the focus engine time to keep up (CI's software renderer is slow).
95
+ return new Promise((r) => setTimeout(r, SETTLE_MS));
96
+ }
97
+ /** Single-quote a string for a POSIX device shell, escaping embedded quotes. */
98
+ function shellQuote(value) {
99
+ return "'" + value.replace(/'/g, "'\\''") + "'";
100
+ }