@houwert/conductor 0.13.1 → 0.14.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.
@@ -21,6 +21,7 @@ exports.HELP = ` run-parallel --flows-dir <path> Run flows in parallel acro
21
21
  const child_process_1 = require("child_process");
22
22
  const path_1 = __importDefault(require("path"));
23
23
  const fs_1 = __importDefault(require("fs"));
24
+ const sdk_js_1 = require("../android/sdk.js");
24
25
  const output_js_1 = require("../output.js");
25
26
  async function runParallel(flowsDir, opts = {}) {
26
27
  if (!flowsDir) {
@@ -107,7 +108,7 @@ async function runShard(deviceId, flowFile) {
107
108
  async function discoverAllDevices() {
108
109
  const devices = [];
109
110
  try {
110
- const out = await spawnCapture('adb', ['devices']);
111
+ const out = await spawnCapture((0, sdk_js_1.resolveAndroidTool)('adb'), ['devices']);
111
112
  for (const line of out.split('\n').slice(1)) {
112
113
  const id = line.trim().split(/\s+/)[0];
113
114
  if (id && !line.includes('offline') && id !== '')
@@ -15,6 +15,7 @@ exports.HELP = ` start-device
15
15
  const fs_1 = __importDefault(require("fs"));
16
16
  const child_process_1 = require("child_process");
17
17
  const runner_js_1 = require("../runner.js");
18
+ const sdk_js_1 = require("../android/sdk.js");
18
19
  const client_js_1 = require("../daemon/client.js");
19
20
  const protocol_js_1 = require("../daemon/protocol.js");
20
21
  const bootstrap_js_1 = require("../drivers/bootstrap.js");
@@ -390,7 +391,9 @@ async function startTvOS(osVersion, opts, name, deviceType) {
390
391
  }
391
392
  // ── Android ───────────────────────────────────────────────────────────────────
392
393
  async function listAVDs() {
393
- const result = await (0, runner_js_1.spawnCommand)('emulator', ['-list-avds']);
394
+ const result = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('emulator'), ['-list-avds'], {
395
+ env: (0, sdk_js_1.androidSpawnEnv)(),
396
+ });
394
397
  if (!result.success)
395
398
  throw new Error(`emulator -list-avds failed: ${result.stderr}`);
396
399
  return result.stdout
@@ -402,7 +405,9 @@ async function waitForAndroidBoot(avdName) {
402
405
  const deadline = Date.now() + ANDROID_BOOT_TIMEOUT_MS;
403
406
  const connectedBefore = new Set();
404
407
  // Snapshot currently connected devices so we can identify the new one
405
- const before = await (0, runner_js_1.spawnCommand)('adb', ['devices']);
408
+ const before = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('adb'), ['devices'], {
409
+ env: (0, sdk_js_1.androidSpawnEnv)(),
410
+ });
406
411
  for (const line of before.stdout.split('\n').slice(1)) {
407
412
  const id = line.trim().split(/\s+/)[0];
408
413
  if (id)
@@ -410,7 +415,9 @@ async function waitForAndroidBoot(avdName) {
410
415
  }
411
416
  while (Date.now() < deadline) {
412
417
  await (0, utils_js_1.sleep)(POLL_MS);
413
- const result = await (0, runner_js_1.spawnCommand)('adb', ['devices']);
418
+ const result = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('adb'), ['devices'], {
419
+ env: (0, sdk_js_1.androidSpawnEnv)(),
420
+ });
414
421
  if (!result.success)
415
422
  continue;
416
423
  for (const line of result.stdout.split('\n').slice(1)) {
@@ -419,13 +426,7 @@ async function waitForAndroidBoot(avdName) {
419
426
  const status = parts[1];
420
427
  if (id && status === 'device' && !connectedBefore.has(id)) {
421
428
  // Check boot completed
422
- const boot = await (0, runner_js_1.spawnCommand)('adb', [
423
- '-s',
424
- id,
425
- 'shell',
426
- 'getprop',
427
- 'sys.boot_completed',
428
- ]);
429
+ const boot = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', id, 'shell', 'getprop', 'sys.boot_completed'], { env: (0, sdk_js_1.androidSpawnEnv)() });
429
430
  if (boot.stdout.trim() === '1')
430
431
  return id;
431
432
  }
@@ -452,10 +453,7 @@ async function startAndroid(avdName, opts) {
452
453
  return 1;
453
454
  }
454
455
  console.log(`Launching emulator: ${target}...`);
455
- const proc = (0, child_process_1.spawn)('emulator', ['-avd', target, '-netdelay', 'none', '-netspeed', 'full'], {
456
- detached: true,
457
- stdio: 'ignore',
458
- });
456
+ const proc = (0, child_process_1.spawn)((0, sdk_js_1.resolveAndroidTool)('emulator'), ['-avd', target, '-netdelay', 'none', '-netspeed', 'full'], { detached: true, stdio: 'ignore', env: (0, sdk_js_1.androidSpawnEnv)() });
459
457
  proc.unref();
460
458
  let deviceId;
461
459
  try {
@@ -6,6 +6,7 @@ exports.HELP = ` stop-device [<name-or-id>]
6
6
  --platform <ios|tvos|android|web> Scope to a single platform
7
7
  --all Stop all booted simulators / running emulators / web sessions`;
8
8
  const runner_js_1 = require("../runner.js");
9
+ const sdk_js_1 = require("../android/sdk.js");
9
10
  const output_js_1 = require("../output.js");
10
11
  const client_js_1 = require("../daemon/client.js");
11
12
  const list_devices_js_1 = require("./list-devices.js");
@@ -18,7 +19,7 @@ async function shutdownSimulator(udid) {
18
19
  }
19
20
  // ── Android ──────────────────────────────────────────────────────────────────
20
21
  async function killEmulator(serial) {
21
- const result = await (0, runner_js_1.spawnCommand)('adb', ['-s', serial, 'emu', 'kill']);
22
+ const result = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', serial, 'emu', 'kill']);
22
23
  if (!result.success) {
23
24
  throw new Error(`Failed to kill emulator ${serial}: ${result.stderr.trim()}`);
24
25
  }
@@ -19,6 +19,7 @@ const url_1 = __importDefault(require("url"));
19
19
  const fs_1 = __importDefault(require("fs"));
20
20
  const path_1 = __importDefault(require("path"));
21
21
  const protocol_js_1 = require("./protocol.js");
22
+ const sdk_js_1 = require("../android/sdk.js");
22
23
  const bootstrap_js_1 = require("../drivers/bootstrap.js");
23
24
  const android_js_1 = require("../drivers/android.js");
24
25
  const web_server_js_1 = require("./web-server.js");
@@ -129,6 +130,7 @@ async function ensureDriverRunning() {
129
130
  }
130
131
  // ── Daemon main ──────────────────────────────────────────────────────────────
131
132
  async function main() {
133
+ (0, sdk_js_1.ensureAndroidEnv)();
132
134
  // Ensure per-session daemon directory exists
133
135
  fs_1.default.mkdirSync(path_1.default.dirname(PID_FILE), { recursive: true });
134
136
  fs_1.default.writeFileSync(PID_FILE, String(process.pid));
@@ -868,6 +868,44 @@ async function handleRequest(req, res, dlog) {
868
868
  }
869
869
  return;
870
870
  }
871
+ case '/heapSnapshot': {
872
+ // V8 heap snapshot via CDP. The snapshot streams as JSON chunks via
873
+ // `HeapProfiler.addHeapSnapshotChunk` events; we concatenate them and
874
+ // return the assembled JSON. Snapshots are large (10-100+ MB on real
875
+ // pages) — fine over localhost HTTP.
876
+ const p = await getPage(dlog);
877
+ const session = await p.context().newCDPSession(p);
878
+ const chunks = [];
879
+ const onChunk = (e) => {
880
+ chunks.push(e.chunk);
881
+ };
882
+ try {
883
+ await session.send('HeapProfiler.enable').catch(() => { });
884
+ // Optional: collect garbage before snapshotting so transient
885
+ // allocations don't muddy diff comparisons. Triggered via ?gc=1.
886
+ if (parsedUrl.query['gc']) {
887
+ await session.send('HeapProfiler.collectGarbage').catch(() => { });
888
+ }
889
+ session.on('HeapProfiler.addHeapSnapshotChunk', onChunk);
890
+ await session.send('HeapProfiler.takeHeapSnapshot', {
891
+ reportProgress: false,
892
+ captureNumericValue: false,
893
+ exposeInternals: false,
894
+ });
895
+ // Each chunk is already JSON text; concatenation yields the full snapshot JSON.
896
+ const body = chunks.join('');
897
+ res.writeHead(200, {
898
+ 'Content-Type': 'application/json',
899
+ 'Content-Length': Buffer.byteLength(body),
900
+ });
901
+ res.end(body);
902
+ }
903
+ finally {
904
+ session.off('HeapProfiler.addHeapSnapshotChunk', onChunk);
905
+ await session.detach().catch(() => { });
906
+ }
907
+ return;
908
+ }
871
909
  case '/consoleLogs': {
872
910
  const since = parsedUrl.query['since'] ?? '';
873
911
  const entries = since
@@ -50,6 +50,7 @@ const protoLoader = __importStar(require("@grpc/proto-loader"));
50
50
  const child_process_1 = require("child_process");
51
51
  const fs_1 = __importDefault(require("fs"));
52
52
  const path_1 = __importDefault(require("path"));
53
+ const sdk_js_1 = require("../android/sdk.js");
53
54
  // __dirname is available in CommonJS — points to dist/drivers/
54
55
  const PROTO_PATH = path_1.default.join(__dirname, '../../proto/conductor_android.proto');
55
56
  let _packageDef = null;
@@ -148,7 +149,9 @@ class AndroidDriver {
148
149
  // ── ADB-shell operations (not in gRPC proto) ─────────────────────────────
149
150
  adb(args) {
150
151
  return new Promise((resolve, reject) => {
151
- const proc = (0, child_process_1.spawn)('adb', ['-s', this.deviceId, ...args], { stdio: 'ignore' });
152
+ const proc = (0, child_process_1.spawn)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', this.deviceId, ...args], {
153
+ stdio: 'ignore',
154
+ });
152
155
  proc.on('close', (code) => {
153
156
  if (code === 0)
154
157
  resolve();
@@ -182,7 +185,7 @@ class AndroidDriver {
182
185
  }
183
186
  adbOutput(args) {
184
187
  return new Promise((resolve, reject) => {
185
- const proc = (0, child_process_1.spawn)('adb', ['-s', this.deviceId, ...args], {
188
+ const proc = (0, child_process_1.spawn)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', this.deviceId, ...args], {
186
189
  stdio: ['ignore', 'pipe', 'pipe'],
187
190
  });
188
191
  let stdout = '';
@@ -326,7 +329,7 @@ class AndroidDriver {
326
329
  if (this._recordingProcess)
327
330
  await this.stopRecording();
328
331
  this._recordingOutputPath = outputPath;
329
- this._recordingProcess = (0, child_process_1.spawn)('adb', ['-s', this.deviceId, 'shell', 'screenrecord', '/sdcard/conductor_recording.mp4'], { stdio: 'ignore' });
332
+ this._recordingProcess = (0, child_process_1.spawn)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', this.deviceId, 'shell', 'screenrecord', '/sdcard/conductor_recording.mp4'], { stdio: 'ignore' });
330
333
  }
331
334
  async stopRecording() {
332
335
  if (this._recordingProcess) {
@@ -42,6 +42,7 @@ const fs_1 = __importDefault(require("fs"));
42
42
  const path_1 = __importDefault(require("path"));
43
43
  const verbose_js_1 = require("../verbose.js");
44
44
  const utils_js_1 = require("../utils.js");
45
+ const sdk_js_1 = require("../android/sdk.js");
45
46
  /** Cache: deviceId → platform */
46
47
  const _platformCache = new Map();
47
48
  async function detectPlatform(deviceId) {
@@ -353,8 +354,24 @@ async function installDriver(deviceId) {
353
354
  throw new Error(`Conductor driver APKs not found at ${path_1.default.join(driversDir, 'android')}.\n` +
354
355
  `Run 'make package-cli' from the repo root to build and bundle the drivers.`);
355
356
  }
356
- await spawnAndWait('adb', ['-s', deviceId, 'install', '-r', '-t', '-g', appApk]);
357
- await spawnAndWait('adb', ['-s', deviceId, 'install', '-r', '-t', '-g', serverApk]);
357
+ await spawnAndWait((0, sdk_js_1.resolveAndroidTool)('adb'), [
358
+ '-s',
359
+ deviceId,
360
+ 'install',
361
+ '-r',
362
+ '-t',
363
+ '-g',
364
+ appApk,
365
+ ]);
366
+ await spawnAndWait((0, sdk_js_1.resolveAndroidTool)('adb'), [
367
+ '-s',
368
+ deviceId,
369
+ 'install',
370
+ '-r',
371
+ '-t',
372
+ '-g',
373
+ serverApk,
374
+ ]);
358
375
  (0, verbose_js_1.log)(`installDriver: done`);
359
376
  }
360
377
  // ── iOS bootstrap ─────────────────────────────────────────────────────────────
@@ -598,9 +615,15 @@ async function startAndroidDriver(deviceId, port = ANDROID_BASE_PORT) {
598
615
  }
599
616
  (0, verbose_js_1.log)(`Starting Android driver for device ${deviceId} on port ${port}`);
600
617
  // Step 1: ADB port forward
601
- await spawnAndWait('adb', ['-s', deviceId, 'forward', `tcp:${port}`, `tcp:${port}`]);
618
+ await spawnAndWait((0, sdk_js_1.resolveAndroidTool)('adb'), [
619
+ '-s',
620
+ deviceId,
621
+ 'forward',
622
+ `tcp:${port}`,
623
+ `tcp:${port}`,
624
+ ]);
602
625
  // Step 2: Get device API level to decide instrumentation flags
603
- const apiResult = await spawnCapture('adb', [
626
+ const apiResult = await spawnCapture((0, sdk_js_1.resolveAndroidTool)('adb'), [
604
627
  '-s',
605
628
  deviceId,
606
629
  'shell',
@@ -628,7 +651,7 @@ async function startAndroidDriver(deviceId, port = ANDROID_BASE_PORT) {
628
651
  String(port),
629
652
  CONDUCTOR_TEST_RUNNER,
630
653
  ];
631
- const proc = (0, child_process_1.spawn)('adb', instrArgs, {
654
+ const proc = (0, child_process_1.spawn)((0, sdk_js_1.resolveAndroidTool)('adb'), instrArgs, {
632
655
  detached: true,
633
656
  stdio: ['ignore', 'ignore', 'ignore'],
634
657
  });
@@ -647,7 +670,7 @@ async function startAndroidDriver(deviceId, port = ANDROID_BASE_PORT) {
647
670
  `Try running: conductor install --device ${deviceId}`);
648
671
  }
649
672
  async function stopAndroidDriver(deviceId, port = ANDROID_BASE_PORT) {
650
- await spawnAndWait('adb', [
673
+ await spawnAndWait((0, sdk_js_1.resolveAndroidTool)('adb'), [
651
674
  '-s',
652
675
  deviceId,
653
676
  'shell',
@@ -655,7 +678,13 @@ async function stopAndroidDriver(deviceId, port = ANDROID_BASE_PORT) {
655
678
  'force-stop',
656
679
  'dev.houwert.conductor',
657
680
  ]).catch(() => { });
658
- await spawnAndWait('adb', ['-s', deviceId, 'forward', '--remove', `tcp:${port}`]).catch(() => { });
681
+ await spawnAndWait((0, sdk_js_1.resolveAndroidTool)('adb'), [
682
+ '-s',
683
+ deviceId,
684
+ 'forward',
685
+ '--remove',
686
+ `tcp:${port}`,
687
+ ]).catch(() => { });
659
688
  }
660
689
  // ── Web bootstrap ────────────────────────────────────────────────────────────
661
690
  /**
@@ -783,8 +812,18 @@ async function uninstallDriver(deviceId, platform) {
783
812
  await spawnAndWait('xcrun', ['simctl', 'uninstall', deviceId, TVOS_RUNNER_BUNDLE_ID]).catch(() => { });
784
813
  }
785
814
  else {
786
- await spawnAndWait('adb', ['-s', deviceId, 'uninstall', 'dev.houwert.conductor']).catch(() => { });
787
- await spawnAndWait('adb', ['-s', deviceId, 'uninstall', 'dev.houwert.conductor.test']).catch(() => { });
815
+ await spawnAndWait((0, sdk_js_1.resolveAndroidTool)('adb'), [
816
+ '-s',
817
+ deviceId,
818
+ 'uninstall',
819
+ 'dev.houwert.conductor',
820
+ ]).catch(() => { });
821
+ await spawnAndWait((0, sdk_js_1.resolveAndroidTool)('adb'), [
822
+ '-s',
823
+ deviceId,
824
+ 'uninstall',
825
+ 'dev.houwert.conductor.test',
826
+ ]).catch(() => { });
788
827
  }
789
828
  }
790
829
  // ── Helpers ───────────────────────────────────────────────────────────────────
@@ -5,6 +5,7 @@ exports.AndroidLogSource = void 0;
5
5
  * Android log source — streams logs from an Android device/emulator via `adb logcat`.
6
6
  */
7
7
  const child_process_1 = require("child_process");
8
+ const sdk_js_1 = require("../../android/sdk.js");
8
9
  function mapPriority(priority) {
9
10
  // Android log priorities: 2=V, 3=D, 4=I, 5=W, 6=E, 7=F
10
11
  const p = typeof priority === 'string' ? priority.toUpperCase() : String(priority);
@@ -44,10 +45,7 @@ class AndroidLogSource {
44
45
  let pid;
45
46
  if (this.appId) {
46
47
  try {
47
- pid = (0, child_process_1.execSync)(`adb -s ${this.deviceId} shell pidof ${this.appId}`, {
48
- encoding: 'utf-8',
49
- timeout: 5000,
50
- }).trim();
48
+ pid = (0, child_process_1.execFileSync)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', this.deviceId, 'shell', 'pidof', this.appId], { encoding: 'utf-8', timeout: 5000 }).trim();
51
49
  }
52
50
  catch {
53
51
  // App may not be running yet — proceed without PID filter
@@ -55,7 +53,9 @@ class AndroidLogSource {
55
53
  }
56
54
  // Clear existing logcat buffer so we start fresh
57
55
  try {
58
- (0, child_process_1.execSync)(`adb -s ${this.deviceId} logcat -c`, { timeout: 5000 });
56
+ (0, child_process_1.execFileSync)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', this.deviceId, 'logcat', '-c'], {
57
+ timeout: 5000,
58
+ });
59
59
  }
60
60
  catch {
61
61
  // Ignore clear failures
@@ -71,7 +71,7 @@ class AndroidLogSource {
71
71
  if (pid) {
72
72
  args.push('--pid', pid);
73
73
  }
74
- this.proc = (0, child_process_1.spawn)('adb', args, { stdio: ['ignore', 'pipe', 'pipe'] });
74
+ this.proc = (0, child_process_1.spawn)((0, sdk_js_1.resolveAndroidTool)('adb'), args, { stdio: ['ignore', 'pipe', 'pipe'] });
75
75
  let stderrChunks = '';
76
76
  this.proc.stderr.on('data', (chunk) => {
77
77
  stderrChunks += chunk.toString('utf-8');
@@ -24,6 +24,7 @@ exports.targetsForDevice = targetsForDevice;
24
24
  */
25
25
  const child_process_1 = require("child_process");
26
26
  const metro_js_1 = require("./metro.js");
27
+ const sdk_js_1 = require("../../android/sdk.js");
27
28
  /** Metro dev-server port ranges we consider. */
28
29
  const METRO_PORT_RANGES = [
29
30
  [8080, 8099], // Metro default range
@@ -58,7 +59,12 @@ async function discoverMetroPortForDevice(platform, deviceId) {
58
59
  }
59
60
  async function discoverMetroPortAndroid(deviceId) {
60
61
  try {
61
- const output = await spawnCapture('adb', ['-s', deviceId, 'reverse', '--list']);
62
+ const output = await spawnCapture((0, sdk_js_1.resolveAndroidTool)('adb'), [
63
+ '-s',
64
+ deviceId,
65
+ 'reverse',
66
+ '--list',
67
+ ]);
62
68
  // Lines look like: host-13 tcp:8082 tcp:8082
63
69
  for (const line of output.split('\n')) {
64
70
  const match = line.match(/tcp:(\d+)\s+tcp:(\d+)/);
@@ -172,7 +178,7 @@ async function getDeviceDisplayName(platform, deviceId) {
172
178
  }
173
179
  if (platform === 'android') {
174
180
  try {
175
- const output = await spawnCapture('adb', [
181
+ const output = await spawnCapture((0, sdk_js_1.resolveAndroidTool)('adb'), [
176
182
  '-s',
177
183
  deviceId,
178
184
  'shell',
@@ -18,7 +18,7 @@ class WebDriver {
18
18
  this.host = host;
19
19
  this.deviceId = deviceId;
20
20
  }
21
- request(method, path, body) {
21
+ request(method, path, body, timeoutMs = 30000) {
22
22
  return new Promise((resolve, reject) => {
23
23
  const bodyBuf = body !== undefined ? Buffer.from(JSON.stringify(body), 'utf-8') : undefined;
24
24
  const options = {
@@ -38,7 +38,7 @@ class WebDriver {
38
38
  res.on('end', () => resolve({ status: res.statusCode ?? 0, data: Buffer.concat(chunks) }));
39
39
  res.on('error', reject);
40
40
  });
41
- req.setTimeout(30000, () => {
41
+ req.setTimeout(timeoutMs, () => {
42
42
  req.destroy(new Error(`Web driver request timed out: ${method} ${path}`));
43
43
  });
44
44
  req.on('error', reject);
@@ -139,6 +139,20 @@ class WebDriver {
139
139
  async memory() {
140
140
  return this.get('memory');
141
141
  }
142
+ /**
143
+ * Take a V8 heap snapshot via CDP. Returns the raw JSON text (large; tens
144
+ * of MB on a real page). Caller can write it to a `.heapsnapshot` file for
145
+ * Chrome DevTools and/or parse it for class statistics.
146
+ */
147
+ async heapSnapshot(opts = {}) {
148
+ // 5-minute timeout — large pages can take 30-60s to snapshot + transfer.
149
+ const qs = opts.gc ? '?gc=1' : '';
150
+ const { status, data } = await this.request('GET', `/heapSnapshot${qs}`, undefined, 5 * 60000);
151
+ if (status < 200 || status >= 300) {
152
+ throw new Error(`Web driver heapSnapshot failed (HTTP ${status}): ${data.toString('utf-8').slice(0, 200)}`);
153
+ }
154
+ return data.toString('utf-8');
155
+ }
142
156
  async eraseAllText(count = 50) {
143
157
  await this.post('eraseText', { count });
144
158
  }
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const minimist_1 = __importDefault(require("minimist"));
8
8
  const verbose_js_1 = require("./verbose.js");
9
+ const sdk_js_1 = require("./android/sdk.js");
9
10
  const list_devices_js_1 = require("./commands/list-devices.js");
10
11
  const launch_app_js_1 = require("./commands/launch-app.js");
11
12
  const stop_app_js_1 = require("./commands/stop-app.js");
@@ -110,6 +111,7 @@ ${Object.values(COMMAND_HELP).join('\n')}
110
111
 
111
112
  ${OPTIONS_HELP}`;
112
113
  async function main() {
114
+ (0, sdk_js_1.ensureAndroidEnv)();
113
115
  (0, update_check_js_1.checkForUpdates)();
114
116
  const argv = (0, minimist_1.default)(process.argv.slice(2), {
115
117
  boolean: [
@@ -131,6 +133,11 @@ async function main() {
131
133
  'optional',
132
134
  'benchmark',
133
135
  'dump',
136
+ 'objects',
137
+ 'heap',
138
+ 'leaks',
139
+ 'snapshots',
140
+ 'growth-only',
134
141
  ],
135
142
  string: [
136
143
  'device',
@@ -160,6 +167,11 @@ async function main() {
160
167
  'to',
161
168
  'source',
162
169
  'level',
170
+ 'save',
171
+ 'diff',
172
+ 'vs',
173
+ 'top',
174
+ 'filter',
163
175
  ],
164
176
  alias: { h: 'help', v: 'verbose', V: 'version' },
165
177
  });
@@ -475,7 +487,20 @@ async function main() {
475
487
  break;
476
488
  case 'memory': {
477
489
  const appId = rest[0];
478
- exitCode = await (0, memory_js_1.memory)(appId, opts, sessionName);
490
+ const all = argv['all'];
491
+ exitCode = await (0, memory_js_1.memory)(appId, opts, sessionName, {
492
+ objects: argv['objects'] || argv['heap'] || all,
493
+ leaks: argv['leaks'] || all,
494
+ top: argv['top'] !== undefined ? Number(argv['top']) : undefined,
495
+ save: argv['save'],
496
+ diff: argv['diff'],
497
+ diffOther: argv['vs'],
498
+ listSnapshots: argv['snapshots'],
499
+ // minimist treats --no-gc as gc=false; default-on lives in memory.ts.
500
+ gc: argv['gc'],
501
+ filter: argv['filter'],
502
+ growthOnly: argv['growth-only'],
503
+ });
479
504
  break;
480
505
  }
481
506
  case 'run-flow': {
package/dist/runner.js CHANGED
@@ -6,6 +6,7 @@ exports.runDirect = runDirect;
6
6
  exports.spawnCommand = spawnCommand;
7
7
  exports.runInlineFlow = runInlineFlow;
8
8
  const child_process_1 = require("child_process");
9
+ const sdk_js_1 = require("./android/sdk.js");
9
10
  const session_js_1 = require("./session.js");
10
11
  const flow_runner_js_1 = require("./drivers/flow-runner.js");
11
12
  const verbose_js_1 = require("./verbose.js");
@@ -24,7 +25,9 @@ async function detectFirstDevice() {
24
25
  if (_cachedDeviceId !== undefined)
25
26
  return _cachedDeviceId ?? undefined;
26
27
  // Android: adb devices
27
- const adb = await spawnCommand('adb', ['devices', '-l']).catch(() => null);
28
+ const adb = await spawnCommand((0, sdk_js_1.resolveAndroidTool)('adb'), ['devices', '-l'], {
29
+ env: (0, sdk_js_1.androidSpawnEnv)(),
30
+ }).catch(() => null);
28
31
  if (adb) {
29
32
  for (const line of adb.stdout.split('\n').slice(1)) {
30
33
  const id = line.trim().split(/\s+/)[0];
@@ -217,9 +220,12 @@ async function runDirect(fn, sessionName = 'default') {
217
220
  }
218
221
  }
219
222
  // ── Spawn helpers ─────────────────────────────────────────────────────────────
220
- async function spawnCommand(cmd, args) {
223
+ async function spawnCommand(cmd, args, options) {
221
224
  return new Promise((resolve) => {
222
- const proc = (0, child_process_1.spawn)(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] });
225
+ const proc = (0, child_process_1.spawn)(cmd, args, {
226
+ stdio: ['ignore', 'pipe', 'pipe'],
227
+ env: options?.env,
228
+ });
223
229
  let stdout = '';
224
230
  let stderr = '';
225
231
  proc.stdout.on('data', (chunk) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@houwert/conductor",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "CLI tool for mobile app interactions — optimized for AI agents",
5
5
  "license": "MIT",
6
6
  "repository": {