@houwert/conductor 0.16.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.
@@ -22,6 +22,7 @@ const ios_js_1 = require("../drivers/ios.js");
22
22
  const android_js_1 = require("../drivers/android.js");
23
23
  const web_js_1 = require("../drivers/web.js");
24
24
  const wait_js_1 = require("../drivers/wait.js");
25
+ const direct_ios_selector_js_1 = require("../drivers/direct-ios-selector.js");
25
26
  async function assertVisible(element, opts = {}, sessionName = 'default', flags = {}) {
26
27
  if (!element && !flags.id && !flags.text) {
27
28
  (0, output_js_1.printError)('assert-visible requires <element> or --id <id>', opts);
@@ -49,7 +50,7 @@ async function assertVisible(element, opts = {}, sessionName = 'default', flags
49
50
  const driver = await (0, runner_js_1.getDriver)(sessionName);
50
51
  const find = async () => {
51
52
  if (driver instanceof ios_js_1.IOSDriver) {
52
- return (0, wait_js_1.waitForIOSElement)(() => driver.viewHierarchy().then((h) => h.axElement), sel, timeoutMs);
53
+ return (0, wait_js_1.waitForIOSElement)((o) => driver.viewHierarchy(false, [], { cache: o?.cached }).then((h) => h.axElement), sel, timeoutMs, undefined, (0, direct_ios_selector_js_1.makeIOSDirectResolver)(driver, sel));
53
54
  }
54
55
  else if (driver instanceof web_js_1.WebDriver) {
55
56
  return (0, wait_js_1.waitForWebElement)(() => driver.viewHierarchy(), sel, timeoutMs);
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP = void 0;
4
+ exports.clipboardRead = clipboardRead;
5
+ exports.clipboardWrite = clipboardWrite;
6
+ exports.paste = paste;
7
+ exports.HELP = ` clipboard read Print the iOS simulator clipboard
8
+ clipboard write <text> Set the iOS simulator clipboard
9
+ paste Paste the clipboard into the focused field (iOS only)`;
10
+ const runner_js_1 = require("../runner.js");
11
+ const output_js_1 = require("../output.js");
12
+ const ios_js_1 = require("../drivers/ios.js");
13
+ const android_js_1 = require("../drivers/android.js");
14
+ const web_js_1 = require("../drivers/web.js");
15
+ const ANDROID_MSG = 'clipboard is iOS-only. On Android, use `conductor input-text` to type instead.';
16
+ async function clipboardRead(opts = {}, sessionName = 'default') {
17
+ const result = await (0, runner_js_1.runDirect)(async (driver) => {
18
+ if (driver instanceof ios_js_1.IOSDriver)
19
+ return await driver.clipboardRead();
20
+ if (driver instanceof android_js_1.AndroidDriver)
21
+ throw new Error(ANDROID_MSG);
22
+ if (driver instanceof web_js_1.WebDriver)
23
+ throw new Error('clipboard read is not supported on Web');
24
+ return '';
25
+ }, sessionName);
26
+ if (result.success) {
27
+ if (opts.json)
28
+ (0, output_js_1.printData)({ text: result.stdout }, opts);
29
+ else
30
+ process.stdout.write(result.stdout);
31
+ return 0;
32
+ }
33
+ else {
34
+ (0, output_js_1.printError)(`clipboard read — failed\n${result.stderr}`, opts);
35
+ return 1;
36
+ }
37
+ }
38
+ async function clipboardWrite(text, opts = {}, sessionName = 'default') {
39
+ const result = await (0, runner_js_1.runDirect)(async (driver) => {
40
+ if (driver instanceof ios_js_1.IOSDriver) {
41
+ await driver.clipboardWrite(text);
42
+ }
43
+ else if (driver instanceof android_js_1.AndroidDriver) {
44
+ throw new Error(ANDROID_MSG);
45
+ }
46
+ else if (driver instanceof web_js_1.WebDriver) {
47
+ throw new Error('clipboard write is not supported on Web');
48
+ }
49
+ }, sessionName);
50
+ if (result.success) {
51
+ (0, output_js_1.printSuccess)('clipboard write — done', opts);
52
+ return 0;
53
+ }
54
+ else {
55
+ (0, output_js_1.printError)(`clipboard write — failed\n${result.stderr}`, opts);
56
+ return 1;
57
+ }
58
+ }
59
+ async function paste(opts = {}, sessionName = 'default') {
60
+ const result = await (0, runner_js_1.runDirect)(async (driver) => {
61
+ if (driver instanceof ios_js_1.IOSDriver) {
62
+ // iOS has no universal OS-level paste — read the clipboard and type it into
63
+ // the focused field. For app-specific Cmd+V handling, callers should issue
64
+ // press-key paste explicitly via the keyboard route.
65
+ const text = await driver.clipboardRead();
66
+ if (text)
67
+ await driver.inputText(text);
68
+ }
69
+ else if (driver instanceof android_js_1.AndroidDriver) {
70
+ throw new Error('paste is iOS-only. On Android, use `conductor input-text` instead.');
71
+ }
72
+ else if (driver instanceof web_js_1.WebDriver) {
73
+ throw new Error('paste is not supported on Web');
74
+ }
75
+ }, sessionName);
76
+ if (result.success) {
77
+ (0, output_js_1.printSuccess)('paste — done', opts);
78
+ return 0;
79
+ }
80
+ else {
81
+ (0, output_js_1.printError)(`paste — failed\n${result.stderr}`, opts);
82
+ return 1;
83
+ }
84
+ }
@@ -0,0 +1,262 @@
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.HELP = void 0;
7
+ exports.crashesList = crashesList;
8
+ exports.crashesShow = crashesShow;
9
+ exports.crashesTail = crashesTail;
10
+ exports.HELP = ` crashes list [--app <bundleId>] [--since <duration>]
11
+ List recent crash reports (iOS host + Android logcat)
12
+ crashes show <id> Print a specific crash report
13
+ crashes tail Stream new crash reports as they appear`;
14
+ const fs_1 = __importDefault(require("fs"));
15
+ const os_1 = __importDefault(require("os"));
16
+ const path_1 = __importDefault(require("path"));
17
+ const child_process_1 = require("child_process");
18
+ const output_js_1 = require("../output.js");
19
+ const bootstrap_js_1 = require("../drivers/bootstrap.js");
20
+ const sdk_js_1 = require("../android/sdk.js");
21
+ const IOS_REPORTS_DIR = path_1.default.join(os_1.default.homedir(), 'Library', 'Logs', 'DiagnosticReports');
22
+ function parseSince(s) {
23
+ if (!s)
24
+ return 0;
25
+ const m = s.match(/^(\d+)(s|m|h|d)?$/);
26
+ if (!m)
27
+ return 0;
28
+ const n = Number(m[1]);
29
+ const unit = m[2] ?? 's';
30
+ const mult = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[unit] ?? 1000;
31
+ return n * mult;
32
+ }
33
+ function listIosReports(opts) {
34
+ if (!fs_1.default.existsSync(IOS_REPORTS_DIR))
35
+ return [];
36
+ const now = Date.now();
37
+ const entries = fs_1.default.readdirSync(IOS_REPORTS_DIR);
38
+ const out = [];
39
+ for (const file of entries) {
40
+ if (!file.endsWith('.ips') && !file.endsWith('.crash'))
41
+ continue;
42
+ const full = path_1.default.join(IOS_REPORTS_DIR, file);
43
+ let stat;
44
+ try {
45
+ stat = fs_1.default.statSync(full);
46
+ }
47
+ catch {
48
+ continue;
49
+ }
50
+ if (opts.sinceMs > 0 && now - stat.mtimeMs > opts.sinceMs)
51
+ continue;
52
+ const text = (() => {
53
+ try {
54
+ return fs_1.default.readFileSync(full, 'utf-8');
55
+ }
56
+ catch {
57
+ return '';
58
+ }
59
+ })();
60
+ const report = parseIpsReport(file, full, text, stat.mtimeMs);
61
+ if (opts.app && report.app && !report.app.includes(opts.app))
62
+ continue;
63
+ out.push(report);
64
+ }
65
+ return out.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
66
+ }
67
+ function parseIpsReport(id, full, text, mtimeMs) {
68
+ // Newer .ips files are JSON-LD style: first line is summary JSON, then body JSON.
69
+ // Older .crash files are plain text. Be defensive.
70
+ let app = null;
71
+ let signal = null;
72
+ let threadName = null;
73
+ const topFrames = [];
74
+ let type = 'crash';
75
+ try {
76
+ const firstNewline = text.indexOf('\n');
77
+ if (firstNewline > 0) {
78
+ const summary = JSON.parse(text.slice(0, firstNewline));
79
+ app = summary.bundleID ?? summary.app_name ?? null;
80
+ }
81
+ }
82
+ catch {
83
+ // ignore — fall back to text parsing
84
+ }
85
+ const procMatch = text.match(/Process:\s+(\S+)/);
86
+ if (!app && procMatch)
87
+ app = procMatch[1];
88
+ const sigMatch = text.match(/Exception Type:\s+(\S+)/);
89
+ if (sigMatch)
90
+ signal = sigMatch[1];
91
+ const threadMatch = text.match(/Thread \d+ (Crashed|name):\s*([^\n]+)/);
92
+ if (threadMatch)
93
+ threadName = threadMatch[2].trim();
94
+ const faultMatch = text.includes('fault');
95
+ if (faultMatch)
96
+ type = 'fault';
97
+ const frameLines = text.split('\n');
98
+ for (const line of frameLines) {
99
+ if (/^\s*\d+\s+\S+\s+0x[0-9a-f]+/i.test(line)) {
100
+ topFrames.push(line.trim());
101
+ if (topFrames.length >= 10)
102
+ break;
103
+ }
104
+ }
105
+ return {
106
+ id,
107
+ timestamp: new Date(mtimeMs).toISOString(),
108
+ app,
109
+ type,
110
+ signal,
111
+ threadName,
112
+ topFrames,
113
+ sourceFile: full,
114
+ platform: 'ios',
115
+ };
116
+ }
117
+ async function listAndroidReports(deviceId, opts) {
118
+ const adb = (0, sdk_js_1.resolveAndroidTool)('adb');
119
+ const env = (0, sdk_js_1.androidSpawnEnv)();
120
+ const sinceArg = opts.sinceMs > 0 ? ['-T', String(Math.floor((Date.now() - opts.sinceMs) / 1000))] : [];
121
+ const output = await new Promise((resolve) => {
122
+ const proc = (0, child_process_1.spawn)(adb, ['-s', deviceId, 'logcat', '-d', '-b', 'crash', ...sinceArg], {
123
+ stdio: ['ignore', 'pipe', 'ignore'],
124
+ env,
125
+ });
126
+ let buf = '';
127
+ proc.stdout.on('data', (c) => {
128
+ buf += c.toString();
129
+ });
130
+ proc.on('close', () => resolve(buf));
131
+ proc.on('error', () => resolve(''));
132
+ });
133
+ const reports = [];
134
+ const blocks = output.split(/\n(?=\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
135
+ for (let i = 0; i < blocks.length; i++) {
136
+ const block = blocks[i];
137
+ if (!/FATAL EXCEPTION|AndroidRuntime|tombstone/i.test(block))
138
+ continue;
139
+ const appMatch = block.match(/Process: ([\w.]+)/);
140
+ const app = appMatch ? appMatch[1] : null;
141
+ if (opts.app && app && !app.includes(opts.app))
142
+ continue;
143
+ const tsMatch = block.match(/^(\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+)/);
144
+ const sigMatch = block.match(/Signal\s+\d+\s+\(([^)]+)\)/);
145
+ const topFrames = [];
146
+ for (const line of block.split('\n')) {
147
+ if (/^\s+at\s/.test(line)) {
148
+ topFrames.push(line.trim());
149
+ if (topFrames.length >= 10)
150
+ break;
151
+ }
152
+ }
153
+ reports.push({
154
+ id: `android-${i}-${tsMatch?.[1] ?? Date.now()}`,
155
+ timestamp: tsMatch
156
+ ? new Date().getFullYear() + '-' + tsMatch[1].replace(' ', 'T')
157
+ : new Date().toISOString(),
158
+ app,
159
+ type: 'logcat',
160
+ signal: sigMatch ? sigMatch[1] : null,
161
+ threadName: null,
162
+ topFrames,
163
+ sourceFile: null,
164
+ platform: 'android',
165
+ });
166
+ }
167
+ return reports;
168
+ }
169
+ async function crashesList(opts, sessionName, listOpts) {
170
+ const sinceMs = parseSince(listOpts.since);
171
+ const platform = sessionName !== 'default' ? await (0, bootstrap_js_1.detectPlatform)(sessionName).catch(() => null) : null;
172
+ const reports = [];
173
+ // Always include iOS host-side reports — they aren't device-scoped.
174
+ reports.push(...listIosReports({ app: listOpts.app, sinceMs }));
175
+ if (platform === 'android' && sessionName !== 'default') {
176
+ reports.push(...(await listAndroidReports(sessionName, { app: listOpts.app, sinceMs })));
177
+ }
178
+ if (opts.json) {
179
+ (0, output_js_1.printData)({ count: reports.length, reports }, opts);
180
+ }
181
+ else {
182
+ if (reports.length === 0)
183
+ console.log('No crash reports found.');
184
+ for (const r of reports) {
185
+ console.log(`${r.timestamp} ${r.platform} ${r.type} ${r.app ?? '?'} ${r.signal ?? '-'} ${r.id}`);
186
+ }
187
+ }
188
+ return 0;
189
+ }
190
+ async function crashesShow(id, opts) {
191
+ if (!id) {
192
+ (0, output_js_1.printError)('crashes show requires an <id>', opts);
193
+ return 1;
194
+ }
195
+ // For iOS, id is the file name in DiagnosticReports.
196
+ const ios = path_1.default.join(IOS_REPORTS_DIR, id);
197
+ if (fs_1.default.existsSync(ios)) {
198
+ const text = fs_1.default.readFileSync(ios, 'utf-8');
199
+ if (opts.json)
200
+ (0, output_js_1.printData)({ id, source: ios, body: text }, opts);
201
+ else
202
+ console.log(text);
203
+ return 0;
204
+ }
205
+ (0, output_js_1.printError)(`crashes show — no report found for "${id}"`, opts);
206
+ return 1;
207
+ }
208
+ async function crashesTail(opts, sessionName) {
209
+ console.log('Watching for new crash reports… (Ctrl+C to stop)');
210
+ // iOS host directory watcher
211
+ let lastSeen = Date.now();
212
+ if (fs_1.default.existsSync(IOS_REPORTS_DIR)) {
213
+ fs_1.default.watch(IOS_REPORTS_DIR, (event, file) => {
214
+ if (!file)
215
+ return;
216
+ const full = path_1.default.join(IOS_REPORTS_DIR, file);
217
+ try {
218
+ const stat = fs_1.default.statSync(full);
219
+ if (stat.mtimeMs <= lastSeen)
220
+ return;
221
+ lastSeen = stat.mtimeMs;
222
+ const text = fs_1.default.readFileSync(full, 'utf-8');
223
+ const report = parseIpsReport(file, full, text, stat.mtimeMs);
224
+ if (opts.json)
225
+ (0, output_js_1.printData)(report, opts);
226
+ else
227
+ console.log(`${report.timestamp} ios ${report.type} ${report.app ?? '?'} ${report.signal ?? '-'} ${report.id}`);
228
+ }
229
+ catch {
230
+ // ignore
231
+ }
232
+ });
233
+ }
234
+ // Android: spawn `adb logcat -b crash` streaming
235
+ if (sessionName !== 'default') {
236
+ const platform = await (0, bootstrap_js_1.detectPlatform)(sessionName).catch(() => null);
237
+ if (platform === 'android') {
238
+ const adb = (0, sdk_js_1.resolveAndroidTool)('adb');
239
+ const proc = (0, child_process_1.spawn)(adb, ['-s', sessionName, 'logcat', '-b', 'crash'], {
240
+ stdio: ['ignore', 'pipe', 'ignore'],
241
+ env: (0, sdk_js_1.androidSpawnEnv)(),
242
+ });
243
+ let buf = '';
244
+ proc.stdout.on('data', (chunk) => {
245
+ buf += chunk.toString();
246
+ const lines = buf.split('\n');
247
+ buf = lines.pop() ?? '';
248
+ for (const line of lines) {
249
+ if (/FATAL EXCEPTION|AndroidRuntime|tombstone/.test(line)) {
250
+ if (opts.json)
251
+ (0, output_js_1.printData)({ platform: 'android', line }, opts);
252
+ else
253
+ console.log(`android ${line}`);
254
+ }
255
+ }
256
+ });
257
+ }
258
+ }
259
+ // Keep alive
260
+ await new Promise(() => { });
261
+ return 0;
262
+ }
@@ -0,0 +1,244 @@
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.HELP = void 0;
7
+ exports.debugStatus = debugStatus;
8
+ exports.debugEvaluate = debugEvaluate;
9
+ exports.debugComponentTree = debugComponentTree;
10
+ exports.debugInspectElement = debugInspectElement;
11
+ exports.debugLogRegistry = debugLogRegistry;
12
+ exports.debugReload = debugReload;
13
+ exports.HELP = ` debug status [--port N] Show RN debugger connection info
14
+ debug evaluate <expr> [--port N] Run JS in the app runtime (Hermes/Fusebox)
15
+ debug component-tree [--port N] Print the React component tree (on-screen)
16
+ debug inspect-element <x,y> Print the React component at a screen point
17
+ debug log-registry [--source metro] Summarize recent Metro/Hermes console logs`;
18
+ const crypto_1 = __importDefault(require("crypto"));
19
+ const output_js_1 = require("../output.js");
20
+ const metro_cdp_js_1 = require("../drivers/metro-cdp.js");
21
+ const bootstrap_js_1 = require("../drivers/bootstrap.js");
22
+ const metro_js_1 = require("../drivers/log-sources/metro.js");
23
+ const metro_scripts_js_1 = require("../drivers/metro-scripts.js");
24
+ const logs_js_1 = require("./logs.js");
25
+ function newRequestId() {
26
+ return crypto_1.default.randomBytes(6).toString('hex');
27
+ }
28
+ function resolveSession(sessionName) {
29
+ if (!sessionName || sessionName === 'default') {
30
+ return { deviceId: undefined, platformPromise: Promise.resolve(undefined) };
31
+ }
32
+ return {
33
+ deviceId: sessionName,
34
+ platformPromise: (0, bootstrap_js_1.detectPlatform)(sessionName).catch(() => undefined),
35
+ };
36
+ }
37
+ async function debugStatus(opts, sessionName, debugOpts) {
38
+ const port = debugOpts.port ?? 8081;
39
+ try {
40
+ const targets = await (0, metro_js_1.fetchTargets)(port, 'localhost');
41
+ const { deviceId, platformPromise } = resolveSession(sessionName);
42
+ const platform = await platformPromise;
43
+ const client = new metro_cdp_js_1.MetroCdpClient();
44
+ await client.connect({ port, deviceId, platform, targetIndex: debugOpts.targetIndex });
45
+ await client.enableDomain('Runtime');
46
+ await client.enableDomain('Debugger');
47
+ // Give a beat for Debugger.scriptParsed events to flow in before reporting count.
48
+ await new Promise((r) => setTimeout(r, 300));
49
+ const info = {
50
+ port,
51
+ host: 'localhost',
52
+ deviceId: deviceId ?? null,
53
+ platform: platform ?? null,
54
+ connected: client.isConnected(),
55
+ enabledDomains: [...client.getEnabledDomains()],
56
+ loadedScripts: client.getLoadedScripts().size,
57
+ targets: targets.map((t) => ({
58
+ title: t.title ?? null,
59
+ deviceName: t.deviceName ?? null,
60
+ appId: t.appId ?? null,
61
+ id: t.id ?? null,
62
+ })),
63
+ };
64
+ client.close();
65
+ if (opts.json)
66
+ (0, output_js_1.printData)(info, opts);
67
+ else {
68
+ console.log(`port: ${info.port}\n` +
69
+ `deviceId: ${info.deviceId ?? '(none)'}\n` +
70
+ `platform: ${info.platform ?? '(none)'}\n` +
71
+ `connected: ${info.connected}\n` +
72
+ `enabledDomains: ${info.enabledDomains.join(', ')}\n` +
73
+ `loadedScripts: ${info.loadedScripts}\n` +
74
+ `targets (${info.targets.length}):\n` +
75
+ info.targets
76
+ .map((t, i) => ` ${i}: ${t.title ?? '(no title)'} device=${t.deviceName}`)
77
+ .join('\n'));
78
+ }
79
+ return 0;
80
+ }
81
+ catch (err) {
82
+ (0, output_js_1.printError)(`debug status — ${err instanceof Error ? err.message : String(err)}`, opts);
83
+ return 1;
84
+ }
85
+ }
86
+ async function debugEvaluate(expr, opts, sessionName, debugOpts) {
87
+ if (!expr) {
88
+ (0, output_js_1.printError)('debug evaluate requires a JS expression', opts);
89
+ return 1;
90
+ }
91
+ const port = debugOpts.port ?? 8081;
92
+ const { deviceId, platformPromise } = resolveSession(sessionName);
93
+ try {
94
+ const platform = await platformPromise;
95
+ const client = new metro_cdp_js_1.MetroCdpClient();
96
+ await client.connect({ port, deviceId, platform, targetIndex: debugOpts.targetIndex });
97
+ const value = await client.evaluate(expr);
98
+ client.close();
99
+ if (opts.json)
100
+ (0, output_js_1.printData)({ result: value }, opts);
101
+ else
102
+ console.log(typeof value === 'string' ? value : JSON.stringify(value, null, 2));
103
+ return 0;
104
+ }
105
+ catch (err) {
106
+ (0, output_js_1.printError)(`debug evaluate — ${err instanceof Error ? err.message : String(err)}`, opts);
107
+ return 1;
108
+ }
109
+ }
110
+ async function debugComponentTree(opts, sessionName, debugOpts) {
111
+ const port = debugOpts.port ?? 8081;
112
+ const { deviceId, platformPromise } = resolveSession(sessionName);
113
+ try {
114
+ const platform = await platformPromise;
115
+ const client = new metro_cdp_js_1.MetroCdpClient();
116
+ await client.connect({ port, deviceId, platform, targetIndex: debugOpts.targetIndex });
117
+ const awaitCallback = await client.installCallbackBinding();
118
+ const requestId = newRequestId();
119
+ const pending = awaitCallback(requestId, 15000);
120
+ await client.evaluate((0, metro_scripts_js_1.makeComponentTreeScript)(requestId), false);
121
+ const result = (await pending);
122
+ client.close();
123
+ if (result.error) {
124
+ (0, output_js_1.printError)(`debug component-tree — ${result.error}`, opts);
125
+ return 1;
126
+ }
127
+ const components = result.components ?? [];
128
+ // Filter to on-screen components: rect present and inside the screen.
129
+ const screenW = result.screenW ?? 0;
130
+ const screenH = result.screenH ?? 0;
131
+ const onScreen = components.filter((c) => {
132
+ if (!c.rect)
133
+ return false;
134
+ const { x, y, w, h } = c.rect;
135
+ if (w <= 0 || h <= 0)
136
+ return false;
137
+ if (screenW > 0 && (x + w < 0 || x > screenW))
138
+ return false;
139
+ if (screenH > 0 && (y + h < 0 || y > screenH))
140
+ return false;
141
+ return true;
142
+ });
143
+ if (opts.json) {
144
+ (0, output_js_1.printData)({
145
+ count: onScreen.length,
146
+ total: components.length,
147
+ fabric: result.fabric ?? false,
148
+ screenW,
149
+ screenH,
150
+ components: onScreen,
151
+ }, opts);
152
+ }
153
+ else {
154
+ for (const c of onScreen) {
155
+ const parts = [' '.repeat(c.depth) + c.name];
156
+ if (c.testID)
157
+ parts.push(`testID=${c.testID}`);
158
+ if (c.label)
159
+ parts.push(`label=${JSON.stringify(c.label)}`);
160
+ if (c.text)
161
+ parts.push(`text=${JSON.stringify(c.text.slice(0, 40))}`);
162
+ if (c.rect) {
163
+ const r = c.rect;
164
+ parts.push(`[${Math.round(r.x)},${Math.round(r.y)} ${Math.round(r.w)}x${Math.round(r.h)}]`);
165
+ }
166
+ console.log(parts.join(' '));
167
+ }
168
+ console.log(`\n${onScreen.length} on-screen / ${components.length} total (${result.fabric ? 'Fabric' : 'Paper'})`);
169
+ }
170
+ return 0;
171
+ }
172
+ catch (err) {
173
+ (0, output_js_1.printError)(`debug component-tree — ${err instanceof Error ? err.message : String(err)}`, opts);
174
+ return 1;
175
+ }
176
+ }
177
+ async function debugInspectElement(at, opts, sessionName, debugOpts) {
178
+ const m = at.match(/^\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*$/);
179
+ if (!m) {
180
+ (0, output_js_1.printError)('debug inspect-element expects "<x>,<y>"', opts);
181
+ return 1;
182
+ }
183
+ const x = Number(m[1]);
184
+ const y = Number(m[2]);
185
+ const port = debugOpts.port ?? 8081;
186
+ const { deviceId, platformPromise } = resolveSession(sessionName);
187
+ try {
188
+ const platform = await platformPromise;
189
+ const client = new metro_cdp_js_1.MetroCdpClient();
190
+ await client.connect({ port, deviceId, platform, targetIndex: debugOpts.targetIndex });
191
+ const awaitCallback = await client.installCallbackBinding();
192
+ const requestId = newRequestId();
193
+ const pending = awaitCallback(requestId, 8000);
194
+ await client.evaluate((0, metro_scripts_js_1.makeInspectElementScript)(x, y, requestId), false);
195
+ const result = (await pending);
196
+ client.close();
197
+ if (result.error) {
198
+ (0, output_js_1.printError)(`debug inspect-element — ${result.error}`, opts);
199
+ return 1;
200
+ }
201
+ if (opts.json)
202
+ (0, output_js_1.printData)(result, opts);
203
+ else {
204
+ console.log(`Components at (${x}, ${y}) — closest first:`);
205
+ for (const item of result.items ?? []) {
206
+ const src = item.frame
207
+ ? ` (${item.frame.file}:${item.frame.line}${item.frame.original ? ' [original]' : ''})`
208
+ : '';
209
+ console.log(`${' '.repeat(item.depth)}${item.name}${src}`);
210
+ }
211
+ }
212
+ return 0;
213
+ }
214
+ catch (err) {
215
+ (0, output_js_1.printError)(`debug inspect-element — ${err instanceof Error ? err.message : String(err)}`, opts);
216
+ return 1;
217
+ }
218
+ }
219
+ async function debugLogRegistry(opts, sessionName) {
220
+ // Delegate to the existing `logs` command in summary mode (--list).
221
+ return (0, logs_js_1.logs)(opts, sessionName, { source: 'metro', list: true });
222
+ }
223
+ async function debugReload(opts, sessionName, debugOpts) {
224
+ const port = debugOpts.port ?? 8081;
225
+ const { deviceId, platformPromise } = resolveSession(sessionName);
226
+ try {
227
+ const platform = await platformPromise;
228
+ await (0, metro_cdp_js_1.cdpCall)('Page.reload', undefined, {
229
+ port,
230
+ deviceId,
231
+ platform,
232
+ targetIndex: debugOpts.targetIndex,
233
+ });
234
+ if (opts.json)
235
+ (0, output_js_1.printData)({ reloaded: true, port, method: 'cdp' }, opts);
236
+ else
237
+ console.log(`reloaded port=${port}`);
238
+ return 0;
239
+ }
240
+ catch (err) {
241
+ (0, output_js_1.printError)(`debug reload — ${err instanceof Error ? err.message : String(err)}`, opts);
242
+ return 1;
243
+ }
244
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HELP = void 0;
4
+ exports.flowRecord = flowRecord;
5
+ exports.HELP = ` flow record start [--out path] Start a YAML flow recording for this session
6
+ flow record finish Close the active recording, print the file path
7
+ flow record echo <text> Insert a console.log step
8
+ flow record status Show the active recording path (if any)`;
9
+ const output_js_1 = require("../output.js");
10
+ const flow_recorder_js_1 = require("../drivers/flow-recorder.js");
11
+ const session_js_1 = require("../session.js");
12
+ async function flowRecord(sub, rest, opts, sessionName, argv) {
13
+ if (sub === 'start') {
14
+ const out = argv['out'];
15
+ const session = await (0, session_js_1.getSession)(sessionName);
16
+ const target = await (0, flow_recorder_js_1.startRecording)(sessionName, out, session.appId);
17
+ if (opts.json)
18
+ (0, output_js_1.printData)({ recordingPath: target }, opts);
19
+ else
20
+ (0, output_js_1.printSuccess)(`flow record start — writing to ${target}`, opts);
21
+ return 0;
22
+ }
23
+ if (sub === 'finish') {
24
+ const out = await (0, flow_recorder_js_1.finishRecording)(sessionName);
25
+ if (!out) {
26
+ (0, output_js_1.printError)('flow record finish — no active recording for this session', opts);
27
+ return 1;
28
+ }
29
+ if (opts.json)
30
+ (0, output_js_1.printData)({ recordingPath: out }, opts);
31
+ else
32
+ (0, output_js_1.printSuccess)(`flow record finish — closed ${out}`, opts);
33
+ return 0;
34
+ }
35
+ if (sub === 'echo') {
36
+ const active = await (0, flow_recorder_js_1.getActiveRecording)(sessionName);
37
+ if (!active) {
38
+ (0, output_js_1.printError)('flow record echo — no active recording (run `flow record start` first)', opts);
39
+ return 1;
40
+ }
41
+ (0, flow_recorder_js_1.appendEcho)(active, rest.join(' '));
42
+ if (opts.json)
43
+ (0, output_js_1.printData)({ ok: true }, opts);
44
+ else
45
+ (0, output_js_1.printSuccess)('flow record echo — appended', opts);
46
+ return 0;
47
+ }
48
+ if (sub === 'status') {
49
+ const active = await (0, flow_recorder_js_1.getActiveRecording)(sessionName);
50
+ if (opts.json)
51
+ (0, output_js_1.printData)({ active }, opts);
52
+ else
53
+ console.log(active ? `recording: ${active}` : 'no active recording');
54
+ return 0;
55
+ }
56
+ (0, output_js_1.printError)('Usage: conductor flow record <start|finish|echo|status>', opts);
57
+ return 1;
58
+ }