@houwert/conductor 0.14.0 → 0.15.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.
@@ -37,12 +37,29 @@ async function resolveDeviceId(sessionName) {
37
37
  const session = await (0, session_js_1.getSession)(sessionName);
38
38
  return session.deviceId ?? (await (0, runner_js_1.detectFirstDevice)());
39
39
  }
40
- async function resolveAppId(explicit, sessionName, deviceId) {
40
+ async function resolveAppId(explicit, sessionName, deviceId, platform) {
41
41
  if (explicit)
42
42
  return explicit;
43
43
  // No arg: always resolve from the live foreground app, not the session file.
44
44
  // The session's appId reflects the last `launch-app` call, which can be stale
45
45
  // if the user switched apps on the device by other means.
46
+ //
47
+ // On Android, foreground-app detection only needs `adb shell dumpsys` — going
48
+ // through getDriver() would force the gRPC daemon to start (and require the
49
+ // driver APK to be installed), which is wasteful for a read-only memory dump.
50
+ if (platform === 'android') {
51
+ const dump = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('adb'), ['-s', deviceId, 'shell', 'dumpsys', 'activity', 'activities'], { env: (0, sdk_js_1.androidSpawnEnv)() });
52
+ if (dump.success) {
53
+ // Different Android versions print this differently:
54
+ // API 28-: mResumedActivity: ActivityRecord{... pkg/.Activity ...}
55
+ // API 29+: topResumedActivity=ActivityRecord{... pkg/.Activity ...}
56
+ // ResumedActivity: ActivityRecord{... pkg/.Activity ...}
57
+ const m = dump.stdout.match(/(?:m|top)?ResumedActivity[=:].*?([a-zA-Z][a-zA-Z0-9_]*(?:\.[a-zA-Z][a-zA-Z0-9_]*)+)\//);
58
+ if (m)
59
+ return m[1];
60
+ }
61
+ return undefined;
62
+ }
46
63
  try {
47
64
  const driver = await (0, runner_js_1.getDriver)(sessionName);
48
65
  if (driver instanceof android_js_1.AndroidDriver)
@@ -1104,13 +1121,17 @@ async function memory(appIdArg, opts = {}, sessionName = 'default', memOpts = {}
1104
1121
  report = await collectWeb(deviceId, sessionName, memOpts);
1105
1122
  }
1106
1123
  else {
1107
- const appId = await resolveAppId(appIdArg, sessionName, deviceId);
1124
+ const appId = await resolveAppId(appIdArg, sessionName, deviceId, platform);
1108
1125
  if (platform === 'android') {
1109
1126
  report = await collectAndroid(deviceId, appId, memOpts);
1110
1127
  }
1111
1128
  else {
1112
1129
  report = await collectIOS(deviceId, platform, appId, memOpts);
1113
1130
  }
1131
+ if (!appId) {
1132
+ report.notes ?? (report.notes = []);
1133
+ report.notes.push('No foreground app detected — pass an app id (e.g. `conductor memory com.example.app --all`) to get per-app memory, objects, and leaks.');
1134
+ }
1114
1135
  }
1115
1136
  report.capturedAt = new Date().toISOString();
1116
1137
  // Diff current report against a saved snapshot.
@@ -4,13 +4,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.HELP = void 0;
7
+ exports.pickAndroidArch = pickAndroidArch;
8
+ exports.parseInstalledSystemImages = parseInstalledSystemImages;
9
+ exports.pickSystemImage = pickSystemImage;
10
+ exports.buildAvdmanagerCreateArgs = buildAvdmanagerCreateArgs;
7
11
  exports.startDevice = startDevice;
8
12
  exports.HELP = ` start-device
9
13
  --platform <ios|android|tvos|web> Boot a simulator/emulator, or start the web driver (Playwright)
10
14
  --os-version <n> iOS/tvOS version (e.g. 18) or Android API level (e.g. 33)
11
- --avd <name> Android AVD name (default: first available)
15
+ --avd <name> Android AVD name (default: first available; created if missing + --device-type)
12
16
  --name <name> Set a custom name on the device after creation (iOS/tvOS/web)
13
- --device-type <name> iOS/tvOS device type (e.g. "iPhone 16 Pro", "Apple TV 4K"); creates if needed
17
+ --device-type <name> iOS/tvOS device type (e.g. "iPhone 16 Pro", "Apple TV 4K") or
18
+ Android device profile (e.g. "pixel_7"); creates if needed
19
+ --system-image <id> Android only: override auto-picked system image
20
+ (e.g. "system-images;android-34;google_apis;arm64-v8a")
14
21
  --browser <chromium|firefox|webkit> Web only: which Playwright browser to launch (default: chromium)`;
15
22
  const fs_1 = __importDefault(require("fs"));
16
23
  const child_process_1 = require("child_process");
@@ -390,6 +397,9 @@ async function startTvOS(osVersion, opts, name, deviceType) {
390
397
  return 0;
391
398
  }
392
399
  // ── Android ───────────────────────────────────────────────────────────────────
400
+ // TODO: Replace bare command names with the resolver from `android-sdk-path-resolver-68477d`
401
+ // once it lands on main. For now we rely on `emulator`/`adb`/`avdmanager`/`sdkmanager`
402
+ // being on PATH.
393
403
  async function listAVDs() {
394
404
  const result = await (0, runner_js_1.spawnCommand)((0, sdk_js_1.resolveAndroidTool)('emulator'), ['-list-avds'], {
395
405
  env: (0, sdk_js_1.androidSpawnEnv)(),
@@ -401,6 +411,139 @@ async function listAVDs() {
401
411
  .map((l) => l.trim())
402
412
  .filter(Boolean);
403
413
  }
414
+ /** Pick the Android system-image arch tag based on the host CPU. */
415
+ function pickAndroidArch(arch = process.arch) {
416
+ return arch === 'arm64' ? 'arm64-v8a' : 'x86_64';
417
+ }
418
+ /**
419
+ * Parse `sdkmanager --list_installed` (or --list) output, returning installed
420
+ * system-image package paths like "system-images;android-34;google_apis;arm64-v8a".
421
+ */
422
+ function parseInstalledSystemImages(stdout) {
423
+ const images = [];
424
+ for (const rawLine of stdout.split('\n')) {
425
+ const line = rawLine.trim();
426
+ if (!line.startsWith('system-images;'))
427
+ continue;
428
+ // Format is "<path> | <version> | <description> | <location>" with leading whitespace
429
+ const path = line.split(/[|\s]+/)[0];
430
+ if (path && !images.includes(path))
431
+ images.push(path);
432
+ }
433
+ return images;
434
+ }
435
+ /**
436
+ * Pick the best installed system image matching the requested API level and arch.
437
+ * Prefers `google_apis` over `default` (no Play store dependency on Apple Silicon
438
+ * where Play images often lack arm64). Returns undefined if no match.
439
+ */
440
+ function pickSystemImage(installed, apiLevel, arch) {
441
+ const filtered = installed.filter((img) => {
442
+ const parts = img.split(';');
443
+ // parts: ["system-images", "android-<api>", "<variant>", "<arch>"]
444
+ if (parts.length < 4)
445
+ return false;
446
+ if (parts[3] !== arch)
447
+ return false;
448
+ if (apiLevel && parts[1] !== `android-${apiLevel}`)
449
+ return false;
450
+ return true;
451
+ });
452
+ if (filtered.length === 0)
453
+ return undefined;
454
+ // Variant preference: google_apis > google_apis_playstore > default > others
455
+ const rank = (img) => {
456
+ const variant = img.split(';')[2];
457
+ if (variant === 'google_apis')
458
+ return 0;
459
+ if (variant === 'google_apis_playstore')
460
+ return 1;
461
+ if (variant === 'default')
462
+ return 2;
463
+ return 3;
464
+ };
465
+ filtered.sort((a, b) => {
466
+ const r = rank(a) - rank(b);
467
+ if (r !== 0)
468
+ return r;
469
+ // Then prefer higher API level
470
+ const ai = parseInt(a.split(';')[1].replace('android-', ''), 10) || 0;
471
+ const bi = parseInt(b.split(';')[1].replace('android-', ''), 10) || 0;
472
+ return bi - ai;
473
+ });
474
+ return filtered[0];
475
+ }
476
+ /** Build the avdmanager argv for AVD creation — pure, exported for tests. */
477
+ function buildAvdmanagerCreateArgs(avdName, systemImage, deviceProfile) {
478
+ return ['create', 'avd', '-n', avdName, '-k', systemImage, '-d', deviceProfile];
479
+ }
480
+ async function listInstalledSystemImages() {
481
+ const result = await (0, runner_js_1.spawnCommand)('sdkmanager', ['--list_installed']);
482
+ if (!result.success) {
483
+ throw new Error(`sdkmanager --list_installed failed: ${result.stderr.trim() || result.stdout.trim()}`);
484
+ }
485
+ return parseInstalledSystemImages(result.stdout);
486
+ }
487
+ /** Run avdmanager create avd, piping "no\n" so it skips the custom hardware prompt. */
488
+ async function spawnAvdmanagerCreate(args) {
489
+ await new Promise((resolve, reject) => {
490
+ const proc = (0, child_process_1.spawn)('avdmanager', args, { stdio: ['pipe', 'pipe', 'pipe'] });
491
+ let stderr = '';
492
+ proc.stdout.on('data', () => {
493
+ /* discard */
494
+ });
495
+ proc.stderr.on('data', (chunk) => {
496
+ stderr += chunk.toString();
497
+ });
498
+ proc.on('error', (err) => reject(err));
499
+ proc.on('close', (code) => {
500
+ if (code === 0)
501
+ resolve();
502
+ else
503
+ reject(new Error(`avdmanager exited ${code}: ${stderr.trim()}`));
504
+ });
505
+ proc.stdin.end('no\n');
506
+ });
507
+ }
508
+ async function createAndroidAVD(avdName, deviceProfile, apiLevel, systemImageOverride) {
509
+ const arch = pickAndroidArch();
510
+ let systemImage;
511
+ if (systemImageOverride) {
512
+ systemImage = systemImageOverride;
513
+ }
514
+ else {
515
+ let installed;
516
+ try {
517
+ installed = await listInstalledSystemImages();
518
+ }
519
+ catch (e) {
520
+ const msg = e instanceof Error ? e.message : String(e);
521
+ throw new Error(`Could not list installed Android system images (${msg}). ` +
522
+ `Ensure the Android SDK command-line tools are installed and on PATH.`);
523
+ }
524
+ const picked = pickSystemImage(installed, apiLevel, arch);
525
+ if (!picked) {
526
+ const apiHint = apiLevel ?? '34';
527
+ throw new Error(`No installed Android system image matches arch=${arch}` +
528
+ (apiLevel ? `, api=${apiLevel}` : '') +
529
+ `.\nInstall one with:\n sdkmanager "system-images;android-${apiHint};google_apis;${arch}"`);
530
+ }
531
+ systemImage = picked;
532
+ }
533
+ const args = buildAvdmanagerCreateArgs(avdName, systemImage, deviceProfile);
534
+ try {
535
+ await spawnAvdmanagerCreate(args);
536
+ }
537
+ catch (e) {
538
+ const msg = e instanceof Error ? e.message : String(e);
539
+ throw new Error(`Failed to create AVD "${avdName}": ${msg}`);
540
+ }
541
+ // Validate the AVD now appears in emulator -list-avds
542
+ const avds = await listAVDs();
543
+ if (!avds.includes(avdName)) {
544
+ throw new Error(`AVD "${avdName}" was not registered after creation (not in emulator -list-avds).`);
545
+ }
546
+ }
404
547
  async function waitForAndroidBoot(avdName) {
405
548
  const deadline = Date.now() + ANDROID_BOOT_TIMEOUT_MS;
406
549
  const connectedBefore = new Set();
@@ -434,7 +577,7 @@ async function waitForAndroidBoot(avdName) {
434
577
  }
435
578
  throw new Error(`Android emulator (${avdName}) did not appear within ${ANDROID_BOOT_TIMEOUT_MS / 1000}s`);
436
579
  }
437
- async function startAndroid(avdName, opts) {
580
+ async function startAndroid(avdName, opts, deviceType, osVersion, systemImage) {
438
581
  let avds;
439
582
  try {
440
583
  avds = await listAVDs();
@@ -443,13 +586,34 @@ async function startAndroid(avdName, opts) {
443
586
  (0, output_js_1.printError)(`Failed to list AVDs: ${e instanceof Error ? e.message : String(e)}`, opts);
444
587
  return 1;
445
588
  }
446
- if (avds.length === 0) {
447
- (0, output_js_1.printError)('No Android AVDs found. Create one in Android Studio Device Manager.', opts);
589
+ let target = avdName ?? avds[0];
590
+ // If the requested AVD doesn't exist (or none exist) and a device profile was
591
+ // provided, create the AVD before booting.
592
+ const needsCreate = (avds.length === 0 || (avdName !== undefined && !avds.includes(avdName))) &&
593
+ deviceType !== undefined;
594
+ if (needsCreate) {
595
+ if (!avdName) {
596
+ (0, output_js_1.printError)('--avd <name> is required when creating an AVD with --device-type.', opts);
597
+ return 1;
598
+ }
599
+ console.log(`No AVD "${avdName}" found. Creating one with device profile "${deviceType}"...`);
600
+ try {
601
+ await createAndroidAVD(avdName, deviceType, osVersion, systemImage);
602
+ }
603
+ catch (e) {
604
+ (0, output_js_1.printError)(e instanceof Error ? e.message : String(e), opts);
605
+ return 1;
606
+ }
607
+ target = avdName;
608
+ }
609
+ else if (avds.length === 0) {
610
+ (0, output_js_1.printError)('No Android AVDs found. Pass --device-type <profile> --avd <name> to create one, ' +
611
+ 'or create one in Android Studio → Device Manager.', opts);
448
612
  return 1;
449
613
  }
450
- const target = avdName ?? avds[0];
451
- if (!avds.includes(target)) {
452
- (0, output_js_1.printError)(`AVD "${target}" not found. Available: ${avds.join(', ')}`, opts);
614
+ else if (!avds.includes(target)) {
615
+ (0, output_js_1.printError)(`AVD "${target}" not found. Available: ${avds.join(', ')}. ` +
616
+ `Pass --device-type to create it.`, opts);
453
617
  return 1;
454
618
  }
455
619
  console.log(`Launching emulator: ${target}...`);
@@ -524,7 +688,7 @@ async function startDevice(platform, opts, flags) {
524
688
  case 'tvos':
525
689
  return startTvOS(flags.osVersion, opts, flags.name, flags.deviceType);
526
690
  case 'android':
527
- return startAndroid(flags.avd, opts);
691
+ return startAndroid(flags.avd, opts, flags.deviceType, flags.osVersion, flags.systemImage);
528
692
  case 'web':
529
693
  return startWebDriver(opts, flags.browser, flags.name);
530
694
  default:
@@ -203,7 +203,9 @@ class AndroidDriver {
203
203
  }
204
204
  async getForegroundApp() {
205
205
  const output = await this.adbOutput(['shell', 'dumpsys', 'activity', 'activities']);
206
- const match = output.match(/mResumedActivity.*?([a-zA-Z][a-zA-Z0-9_]*(?:\.[a-zA-Z][a-zA-Z0-9_]*)+)\//);
206
+ // API 28- prints `mResumedActivity:`; API 29+ uses `ResumedActivity:` /
207
+ // `topResumedActivity=`. Accept all three so this works across versions.
208
+ const match = output.match(/(?:m|top)?ResumedActivity[=:].*?([a-zA-Z][a-zA-Z0-9_]*(?:\.[a-zA-Z][a-zA-Z0-9_]*)+)\//);
207
209
  if (!match)
208
210
  throw new Error('Could not determine foreground app');
209
211
  return match[1];
package/dist/index.js CHANGED
@@ -162,6 +162,7 @@ async function main() {
162
162
  'name',
163
163
  'device-name',
164
164
  'device-type',
165
+ 'system-image',
165
166
  'browser',
166
167
  'from',
167
168
  'to',
@@ -249,6 +250,7 @@ async function main() {
249
250
  avd: argv['avd'],
250
251
  name: argv['name'],
251
252
  deviceType: argv['device-type'],
253
+ systemImage: argv['system-image'],
252
254
  browser: argv['browser'],
253
255
  });
254
256
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@houwert/conductor",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "CLI tool for mobile app interactions — optimized for AI agents",
5
5
  "license": "MIT",
6
6
  "repository": {