@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.
- package/dist/commands/memory.js +23 -2
- package/dist/commands/start-device.js +173 -9
- package/dist/drivers/android.js +3 -1
- package/dist/index.js +2 -0
- package/package.json +1 -1
package/dist/commands/memory.js
CHANGED
|
@@ -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")
|
|
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
|
-
|
|
447
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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:
|
package/dist/drivers/android.js
CHANGED
|
@@ -203,7 +203,9 @@ class AndroidDriver {
|
|
|
203
203
|
}
|
|
204
204
|
async getForegroundApp() {
|
|
205
205
|
const output = await this.adbOutput(['shell', 'dumpsys', 'activity', 'activities']);
|
|
206
|
-
|
|
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;
|