@jetstart/cli 1.1.4 → 1.4.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 (68) hide show
  1. package/dist/cli.js +18 -1
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/android-emulator.d.ts +8 -0
  4. package/dist/commands/android-emulator.d.ts.map +1 -0
  5. package/dist/commands/android-emulator.js +280 -0
  6. package/dist/commands/android-emulator.js.map +1 -0
  7. package/dist/commands/create.d.ts +1 -6
  8. package/dist/commands/create.d.ts.map +1 -1
  9. package/dist/commands/create.js +119 -0
  10. package/dist/commands/create.js.map +1 -1
  11. package/dist/commands/dev.d.ts +1 -7
  12. package/dist/commands/dev.d.ts.map +1 -1
  13. package/dist/commands/dev.js +69 -10
  14. package/dist/commands/dev.js.map +1 -1
  15. package/dist/commands/index.d.ts +2 -0
  16. package/dist/commands/index.d.ts.map +1 -1
  17. package/dist/commands/index.js +5 -1
  18. package/dist/commands/index.js.map +1 -1
  19. package/dist/commands/install-audit.d.ts +9 -0
  20. package/dist/commands/install-audit.d.ts.map +1 -0
  21. package/dist/commands/install-audit.js +185 -0
  22. package/dist/commands/install-audit.js.map +1 -0
  23. package/dist/types/index.d.ts +22 -0
  24. package/dist/types/index.d.ts.map +1 -1
  25. package/dist/types/index.js +8 -0
  26. package/dist/types/index.js.map +1 -1
  27. package/dist/utils/android-sdk.d.ts +81 -0
  28. package/dist/utils/android-sdk.d.ts.map +1 -0
  29. package/dist/utils/android-sdk.js +432 -0
  30. package/dist/utils/android-sdk.js.map +1 -0
  31. package/dist/utils/downloader.d.ts +35 -0
  32. package/dist/utils/downloader.d.ts.map +1 -0
  33. package/dist/utils/downloader.js +214 -0
  34. package/dist/utils/downloader.js.map +1 -0
  35. package/dist/utils/emulator-deployer.d.ts +29 -0
  36. package/dist/utils/emulator-deployer.d.ts.map +1 -0
  37. package/dist/utils/emulator-deployer.js +224 -0
  38. package/dist/utils/emulator-deployer.js.map +1 -0
  39. package/dist/utils/emulator.d.ts +101 -0
  40. package/dist/utils/emulator.d.ts.map +1 -0
  41. package/dist/utils/emulator.js +410 -0
  42. package/dist/utils/emulator.js.map +1 -0
  43. package/dist/utils/java.d.ts +25 -0
  44. package/dist/utils/java.d.ts.map +1 -0
  45. package/dist/utils/java.js +363 -0
  46. package/dist/utils/java.js.map +1 -0
  47. package/dist/utils/system-tools.d.ts +93 -0
  48. package/dist/utils/system-tools.d.ts.map +1 -0
  49. package/dist/utils/system-tools.js +599 -0
  50. package/dist/utils/system-tools.js.map +1 -0
  51. package/dist/utils/template.d.ts.map +1 -1
  52. package/dist/utils/template.js +777 -748
  53. package/dist/utils/template.js.map +1 -1
  54. package/package.json +7 -3
  55. package/src/cli.ts +20 -2
  56. package/src/commands/android-emulator.ts +304 -0
  57. package/src/commands/create.ts +128 -5
  58. package/src/commands/dev.ts +71 -18
  59. package/src/commands/index.ts +3 -1
  60. package/src/commands/install-audit.ts +227 -0
  61. package/src/types/index.ts +30 -0
  62. package/src/utils/android-sdk.ts +478 -0
  63. package/src/utils/downloader.ts +201 -0
  64. package/src/utils/emulator-deployer.ts +210 -0
  65. package/src/utils/emulator.ts +463 -0
  66. package/src/utils/java.ts +369 -0
  67. package/src/utils/system-tools.ts +648 -0
  68. package/src/utils/template.ts +875 -867
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Android Emulator Deployment utilities
3
+ */
4
+
5
+ import { exec } from 'child_process';
6
+ import { promisify } from 'util';
7
+ import * as fs from 'fs-extra';
8
+ import inquirer from 'inquirer';
9
+ import { createAVDManager } from './emulator';
10
+ import { startSpinner, stopSpinner } from './spinner';
11
+ import { error as logError, info } from './logger';
12
+
13
+ const execAsync = promisify(exec);
14
+
15
+ export class EmulatorDeployer {
16
+ private adbPath: string;
17
+ private serial: string;
18
+
19
+ constructor(adbPath: string, serial: string) {
20
+ this.adbPath = adbPath;
21
+ this.serial = serial;
22
+ }
23
+
24
+ /**
25
+ * Install APK on emulator
26
+ */
27
+ async installAPK(apkPath: string, packageName?: string): Promise<void> {
28
+ const spinner = startSpinner('Installing APK on emulator...');
29
+
30
+ try {
31
+ // Verify APK exists
32
+ if (!(await fs.pathExists(apkPath))) {
33
+ throw new Error(`APK not found: ${apkPath}`);
34
+ }
35
+
36
+ // Use -r flag to reinstall if already exists
37
+ const { stdout, stderr } = await execAsync(
38
+ `"${this.adbPath}" -s ${this.serial} install -r "${apkPath}"`
39
+ );
40
+
41
+ if (stderr && stderr.includes('INSTALL_FAILED')) {
42
+ throw new Error(stderr);
43
+ }
44
+
45
+ stopSpinner(spinner, true, 'APK installed on emulator');
46
+ } catch (error) {
47
+ stopSpinner(spinner, false, 'Failed to install APK');
48
+ throw error;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Launch app on emulator
54
+ */
55
+ async launchApp(packageName: string, activityName: string = 'MainActivity'): Promise<void> {
56
+ const spinner = startSpinner('Launching app on emulator...');
57
+
58
+ try {
59
+ await execAsync(
60
+ `"${this.adbPath}" -s ${this.serial} shell am start -n ${packageName}/.${activityName}`
61
+ );
62
+
63
+ stopSpinner(spinner, true, 'App launched on emulator');
64
+ } catch (error) {
65
+ stopSpinner(spinner, false, 'Failed to launch app');
66
+ // Don't throw - launching is optional
67
+ logError(`Launch failed: ${(error as Error).message}`);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Check if device is ready
73
+ */
74
+ async isDeviceReady(): Promise<boolean> {
75
+ try {
76
+ const { stdout } = await execAsync(
77
+ `"${this.adbPath}" -s ${this.serial} shell getprop sys.boot_completed`
78
+ );
79
+ return stdout.trim() === '1';
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Wait for device to be ready
87
+ */
88
+ async waitForDevice(timeout: number = 120000): Promise<void> {
89
+ const spinner = startSpinner('Waiting for emulator to boot...');
90
+ const startTime = Date.now();
91
+
92
+ while (Date.now() - startTime < timeout) {
93
+ if (await this.isDeviceReady()) {
94
+ stopSpinner(spinner, true, 'Emulator ready');
95
+ return;
96
+ }
97
+ await new Promise((resolve) => setTimeout(resolve, 2000));
98
+ }
99
+
100
+ stopSpinner(spinner, false, 'Emulator boot timeout');
101
+ throw new Error('Emulator failed to boot within timeout');
102
+ }
103
+
104
+ /**
105
+ * Find or select emulator
106
+ */
107
+ static async findOrSelectEmulator(targetAVD?: string): Promise<EmulatorDeployer> {
108
+ const avdManager = createAVDManager();
109
+
110
+ try {
111
+ // First, try the AVD manager approach
112
+ const avds = await avdManager.listAVDs();
113
+ const runningAVDs = avds.filter((a) => a.running);
114
+
115
+ if (runningAVDs.length > 0) {
116
+ let selectedAVD;
117
+
118
+ if (targetAVD) {
119
+ // User specified a specific AVD
120
+ selectedAVD = runningAVDs.find((a) => a.name === targetAVD);
121
+ if (!selectedAVD) {
122
+ throw new Error(`Emulator "${targetAVD}" is not running`);
123
+ }
124
+ } else if (runningAVDs.length === 1) {
125
+ // Only one running - use it
126
+ selectedAVD = runningAVDs[0];
127
+ info(`Using emulator: ${selectedAVD.name}`);
128
+ } else {
129
+ // Multiple running - prompt user
130
+ const { avdName } = await inquirer.prompt([
131
+ {
132
+ type: 'list',
133
+ name: 'avdName',
134
+ message: 'Multiple emulators running. Select one:',
135
+ choices: runningAVDs.map((a) => ({
136
+ name: `${a.name} (${a.target || 'Unknown'})`,
137
+ value: a.name,
138
+ })),
139
+ },
140
+ ]);
141
+ selectedAVD = runningAVDs.find((a) => a.name === avdName)!;
142
+ }
143
+
144
+ // Get serial for this AVD
145
+ const serial = await (avdManager as any).findEmulatorSerial(selectedAVD.name);
146
+ if (serial) {
147
+ const adbPath = (avdManager as any).getADBPath();
148
+ return new EmulatorDeployer(adbPath, serial);
149
+ }
150
+ }
151
+ } catch (err) {
152
+ // Fallback to simpler ADB-based detection
153
+ info('Falling back to ADB device detection...');
154
+ }
155
+
156
+ // Fallback: Just use adb devices directly
157
+ const adbPath = (avdManager as any).getADBPath();
158
+ info(`ADB path: ${adbPath}`);
159
+
160
+ let stdout: string;
161
+ try {
162
+ const result = await execAsync(`"${adbPath}" devices`);
163
+ stdout = result.stdout;
164
+ info(`ADB devices output:\n${stdout}`);
165
+ } catch (err: any) {
166
+ logError(`Failed to run adb devices: ${err.message}`);
167
+ throw new Error('Failed to detect emulators. Make sure ADB is accessible.');
168
+ }
169
+
170
+ const lines = stdout.split('\n');
171
+ const devices: string[] = [];
172
+
173
+ for (const line of lines) {
174
+ const trimmedLine = line.trim();
175
+ info(`Checking line: "${trimmedLine}"`);
176
+ const match = trimmedLine.match(/^(emulator-\d+)\s+device$/);
177
+ if (match) {
178
+ devices.push(match[1]);
179
+ info(`Found emulator: ${match[1]}`);
180
+ }
181
+ }
182
+
183
+ if (devices.length === 0) {
184
+ throw new Error('No running emulators found. Start one with "jetstart android-emulator"');
185
+ }
186
+
187
+ let selectedSerial: string;
188
+
189
+ if (devices.length === 1) {
190
+ selectedSerial = devices[0];
191
+ info(`Using emulator: ${selectedSerial}`);
192
+ } else {
193
+ // Multiple running - prompt user
194
+ const { serial } = await inquirer.prompt([
195
+ {
196
+ type: 'list',
197
+ name: 'serial',
198
+ message: 'Multiple emulators running. Select one:',
199
+ choices: devices.map((d) => ({
200
+ name: d,
201
+ value: d,
202
+ })),
203
+ },
204
+ ]);
205
+ selectedSerial = serial;
206
+ }
207
+
208
+ return new EmulatorDeployer(adbPath, selectedSerial);
209
+ }
210
+ }
@@ -0,0 +1,463 @@
1
+ /**
2
+ * Android AVD and Emulator management utilities
3
+ */
4
+
5
+ import { spawn, ChildProcess, exec } from 'child_process';
6
+ import { promisify } from 'util';
7
+ import * as fs from 'fs-extra';
8
+ import * as path from 'path';
9
+ import * as os from 'os';
10
+ import { findAndroidSDK } from './system-tools';
11
+ import { AndroidSDKManager } from './android-sdk';
12
+ import { startSpinner, stopSpinner } from './spinner';
13
+ import { success, error as logError, warning, info } from './logger';
14
+
15
+ const execAsync = promisify(exec);
16
+
17
+ export interface AVDInfo {
18
+ name: string;
19
+ device: string;
20
+ path: string;
21
+ target: string;
22
+ basedOn?: string;
23
+ skin?: string;
24
+ sdcard?: string;
25
+ snapshot: boolean;
26
+ running?: boolean;
27
+ }
28
+
29
+ export interface CreateAVDOptions {
30
+ name: string;
31
+ device?: string;
32
+ apiLevel?: number;
33
+ abi?: string;
34
+ force?: boolean;
35
+ }
36
+
37
+ export interface EmulatorOptions {
38
+ avdName: string;
39
+ gpu?: 'auto' | 'host' | 'swiftshader_indirect' | 'angle_indirect' | 'guest';
40
+ noSnapshot?: boolean;
41
+ noBootAnim?: boolean;
42
+ wipeData?: boolean;
43
+ }
44
+
45
+ /**
46
+ * AVD Manager class
47
+ */
48
+ export class AVDManager {
49
+ private sdkRoot: string;
50
+
51
+ constructor(sdkRoot?: string) {
52
+ this.sdkRoot = sdkRoot || '';
53
+ }
54
+
55
+ /**
56
+ * Ensure SDK root is set
57
+ */
58
+ private async ensureSDKRoot(): Promise<void> {
59
+ if (this.sdkRoot && await fs.pathExists(this.sdkRoot)) {
60
+ return;
61
+ }
62
+
63
+ const existingSDK = await findAndroidSDK();
64
+ if (!existingSDK) {
65
+ throw new Error('Android SDK not found. Run "jetstart install-audit" to check installation.');
66
+ }
67
+
68
+ this.sdkRoot = existingSDK;
69
+ }
70
+
71
+ /**
72
+ * Get path to avdmanager executable
73
+ */
74
+ private getAVDManagerPath(): string {
75
+ const avdmanagerName = os.platform() === 'win32' ? 'avdmanager.bat' : 'avdmanager';
76
+ return path.join(this.sdkRoot, 'cmdline-tools', 'latest', 'bin', avdmanagerName);
77
+ }
78
+
79
+ /**
80
+ * Get path to emulator executable
81
+ */
82
+ private getEmulatorPath(): string {
83
+ const emulatorName = os.platform() === 'win32' ? 'emulator.exe' : 'emulator';
84
+ return path.join(this.sdkRoot, 'emulator', emulatorName);
85
+ }
86
+
87
+ /**
88
+ * Get path to adb executable
89
+ */
90
+ private getADBPath(): string {
91
+ const adbName = os.platform() === 'win32' ? 'adb.exe' : 'adb';
92
+ return path.join(this.sdkRoot, 'platform-tools', adbName);
93
+ }
94
+
95
+ /**
96
+ * Run avdmanager command
97
+ */
98
+ private async runAVDManager(args: string[]): Promise<string> {
99
+ await this.ensureSDKRoot();
100
+
101
+ const avdmanagerPath = this.getAVDManagerPath();
102
+
103
+ if (!await fs.pathExists(avdmanagerPath)) {
104
+ throw new Error('avdmanager not found. Install Android cmdline-tools first.');
105
+ }
106
+
107
+ return new Promise((resolve, reject) => {
108
+ const proc = spawn(avdmanagerPath, args, {
109
+ env: {
110
+ ...process.env,
111
+ ANDROID_HOME: this.sdkRoot,
112
+ ANDROID_SDK_ROOT: this.sdkRoot,
113
+ },
114
+ shell: true,
115
+ });
116
+
117
+ let output = '';
118
+ let errorOutput = '';
119
+
120
+ proc.stdout?.on('data', (data) => {
121
+ output += data.toString();
122
+ });
123
+
124
+ proc.stderr?.on('data', (data) => {
125
+ errorOutput += data.toString();
126
+ });
127
+
128
+ proc.on('close', (code) => {
129
+ if (code === 0) {
130
+ resolve(output);
131
+ } else {
132
+ reject(new Error(`avdmanager exited with code ${code}: ${errorOutput || output}`));
133
+ }
134
+ });
135
+
136
+ proc.on('error', (err) => {
137
+ reject(err);
138
+ });
139
+ });
140
+ }
141
+
142
+ /**
143
+ * List all AVDs
144
+ */
145
+ async listAVDs(): Promise<AVDInfo[]> {
146
+ try {
147
+ const output = await this.runAVDManager(['list', 'avd']);
148
+ const avds = this.parseAVDList(output);
149
+
150
+ // Check which AVDs are running
151
+ for (const avd of avds) {
152
+ avd.running = await this.isEmulatorRunning(avd.name);
153
+ }
154
+
155
+ return avds;
156
+ } catch (error) {
157
+ return [];
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Parse avdmanager list avd output
163
+ */
164
+ private parseAVDList(output: string): AVDInfo[] {
165
+ const avds: AVDInfo[] = [];
166
+ const lines = output.split('\n');
167
+
168
+ let currentAVD: Partial<AVDInfo> = {};
169
+
170
+ for (const line of lines) {
171
+ const trimmed = line.trim();
172
+
173
+ if (trimmed.startsWith('Name:')) {
174
+ if (currentAVD.name) {
175
+ avds.push(currentAVD as AVDInfo);
176
+ }
177
+ currentAVD = {
178
+ name: trimmed.split(':')[1]?.trim() || '',
179
+ snapshot: false,
180
+ };
181
+ } else if (trimmed.startsWith('Device:')) {
182
+ currentAVD.device = trimmed.split(':')[1]?.trim() || '';
183
+ } else if (trimmed.startsWith('Path:')) {
184
+ currentAVD.path = trimmed.split(':')[1]?.trim() || '';
185
+ } else if (trimmed.startsWith('Target:')) {
186
+ currentAVD.target = trimmed.split(':')[1]?.trim() || '';
187
+ } else if (trimmed.startsWith('Based on:')) {
188
+ currentAVD.basedOn = trimmed.split(':')[1]?.trim() || '';
189
+ } else if (trimmed.startsWith('Skin:')) {
190
+ currentAVD.skin = trimmed.split(':')[1]?.trim() || '';
191
+ } else if (trimmed.startsWith('Sdcard:')) {
192
+ currentAVD.sdcard = trimmed.split(':')[1]?.trim() || '';
193
+ } else if (trimmed.includes('Snapshot')) {
194
+ currentAVD.snapshot = true;
195
+ }
196
+ }
197
+
198
+ if (currentAVD.name) {
199
+ avds.push(currentAVD as AVDInfo);
200
+ }
201
+
202
+ return avds;
203
+ }
204
+
205
+ /**
206
+ * Create a new AVD
207
+ */
208
+ async createAVD(options: CreateAVDOptions): Promise<void> {
209
+ const {
210
+ name,
211
+ device = 'pixel_5',
212
+ apiLevel = 34,
213
+ abi = os.arch() === 'arm64' ? 'arm64-v8a' : 'x86_64',
214
+ force = false,
215
+ } = options;
216
+
217
+ const spinner = startSpinner(`Creating AVD: ${name}...`);
218
+
219
+ try {
220
+ // Check if system image is installed
221
+ const systemImagePackage = `system-images;android-${apiLevel};google_apis;${abi}`;
222
+ const sdkManager = new AndroidSDKManager(this.sdkRoot);
223
+
224
+ const isInstalled = await sdkManager.isComponentInstalled(systemImagePackage);
225
+ if (!isInstalled) {
226
+ stopSpinner(spinner, false, 'System image not installed');
227
+ info(`Installing system image: ${systemImagePackage}`);
228
+ await sdkManager.installComponent(systemImagePackage);
229
+ }
230
+
231
+ // Create AVD
232
+ const args = [
233
+ 'create', 'avd',
234
+ '--name', name,
235
+ '--package', systemImagePackage,
236
+ '--device', device,
237
+ ];
238
+
239
+ if (force) {
240
+ args.push('--force');
241
+ }
242
+
243
+ // Use spawn for interactive input
244
+ await new Promise<void>((resolve, reject) => {
245
+ const avdmanagerPath = this.getAVDManagerPath();
246
+ const proc = spawn(avdmanagerPath, args, {
247
+ env: {
248
+ ...process.env,
249
+ ANDROID_HOME: this.sdkRoot,
250
+ ANDROID_SDK_ROOT: this.sdkRoot,
251
+ },
252
+ shell: true,
253
+ });
254
+
255
+ // Auto-answer prompts with defaults
256
+ proc.stdin?.write('no\n'); // Hardware profile
257
+ proc.stdin?.end();
258
+
259
+ let output = '';
260
+
261
+ proc.stdout?.on('data', (data) => {
262
+ output += data.toString();
263
+ });
264
+
265
+ proc.stderr?.on('data', (data) => {
266
+ output += data.toString();
267
+ });
268
+
269
+ proc.on('close', (code) => {
270
+ if (code === 0) {
271
+ resolve();
272
+ } else {
273
+ reject(new Error(`Failed to create AVD: ${output}`));
274
+ }
275
+ });
276
+
277
+ proc.on('error', (err) => {
278
+ reject(err);
279
+ });
280
+ });
281
+
282
+ stopSpinner(spinner, true, `AVD "${name}" created successfully`);
283
+ } catch (error) {
284
+ stopSpinner(spinner, false, `Failed to create AVD: ${(error as Error).message}`);
285
+ throw error;
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Delete an AVD
291
+ */
292
+ async deleteAVD(name: string): Promise<void> {
293
+ const spinner = startSpinner(`Deleting AVD: ${name}...`);
294
+
295
+ try {
296
+ await this.runAVDManager(['delete', 'avd', '--name', name]);
297
+ stopSpinner(spinner, true, `AVD "${name}" deleted`);
298
+ } catch (error) {
299
+ stopSpinner(spinner, false, `Failed to delete AVD`);
300
+ throw error;
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Start an emulator
306
+ */
307
+ async startEmulator(options: EmulatorOptions): Promise<ChildProcess> {
308
+ await this.ensureSDKRoot();
309
+
310
+ const emulatorPath = this.getEmulatorPath();
311
+
312
+ if (!await fs.pathExists(emulatorPath)) {
313
+ throw new Error('Emulator not found. Install Android emulator package first.');
314
+ }
315
+
316
+ const {
317
+ avdName,
318
+ gpu = 'auto',
319
+ noSnapshot = true,
320
+ noBootAnim = true,
321
+ wipeData = false,
322
+ } = options;
323
+
324
+ const args = ['-avd', avdName];
325
+
326
+ if (gpu) args.push('-gpu', gpu);
327
+ if (noSnapshot) args.push('-no-snapshot-load');
328
+ if (noBootAnim) args.push('-no-boot-anim');
329
+ if (wipeData) args.push('-wipe-data');
330
+
331
+ info(`Starting emulator: ${avdName}...`);
332
+
333
+ const proc = spawn(emulatorPath, args, {
334
+ env: {
335
+ ...process.env,
336
+ ANDROID_HOME: this.sdkRoot,
337
+ ANDROID_SDK_ROOT: this.sdkRoot,
338
+ },
339
+ detached: true,
340
+ stdio: 'ignore',
341
+ shell: false,
342
+ });
343
+
344
+ proc.unref();
345
+
346
+ // Wait a moment for emulator to start
347
+ await new Promise(resolve => setTimeout(resolve, 2000));
348
+
349
+ success(`Emulator "${avdName}" started`);
350
+ info('Emulator is booting. This may take a few minutes...');
351
+
352
+ return proc;
353
+ }
354
+
355
+ /**
356
+ * Stop an emulator
357
+ */
358
+ async stopEmulator(avdName: string): Promise<void> {
359
+ const serial = await this.findEmulatorSerial(avdName);
360
+
361
+ if (!serial) {
362
+ throw new Error(`Emulator "${avdName}" is not running`);
363
+ }
364
+
365
+ const spinner = startSpinner(`Stopping emulator: ${avdName}...`);
366
+
367
+ try {
368
+ const adbPath = this.getADBPath();
369
+ await execAsync(`"${adbPath}" -s ${serial} emu kill`);
370
+ stopSpinner(spinner, true, `Emulator "${avdName}" stopped`);
371
+ } catch (error) {
372
+ stopSpinner(spinner, false, 'Failed to stop emulator');
373
+ throw error;
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Check if an emulator is running
379
+ */
380
+ async isEmulatorRunning(avdName: string): Promise<boolean> {
381
+ const serial = await this.findEmulatorSerial(avdName);
382
+ return serial !== null;
383
+ }
384
+
385
+ /**
386
+ * Find emulator serial number by AVD name
387
+ */
388
+ private async findEmulatorSerial(avdName: string): Promise<string | null> {
389
+ try {
390
+ const adbPath = this.getADBPath();
391
+ const { stdout } = await execAsync(`"${adbPath}" devices`);
392
+ const lines = stdout.split('\n');
393
+
394
+ for (const line of lines) {
395
+ const match = line.match(/^(emulator-\d+)\s+device$/);
396
+ if (match) {
397
+ const serial = match[1];
398
+
399
+ // Check AVD name
400
+ try {
401
+ const { stdout: nameOutput } = await execAsync(`"${adbPath}" -s ${serial} emu avd name`);
402
+ if (nameOutput.trim() === avdName) {
403
+ return serial;
404
+ }
405
+ } catch {
406
+ continue;
407
+ }
408
+ }
409
+ }
410
+
411
+ return null;
412
+ } catch {
413
+ return null;
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Get list of available system images for emulator
419
+ */
420
+ async listSystemImages(): Promise<string[]> {
421
+ try {
422
+ const sdkManager = new AndroidSDKManager(this.sdkRoot);
423
+ const output = await sdkManager.listInstalled();
424
+
425
+ return output
426
+ .filter(c => c.name.startsWith('system-images;'))
427
+ .map(c => c.name);
428
+ } catch {
429
+ return [];
430
+ }
431
+ }
432
+
433
+ /**
434
+ * Create JetStart-optimized AVD
435
+ */
436
+ async createJetStartAVD(): Promise<void> {
437
+ const avdName = 'JetStart-Pixel5-API34';
438
+
439
+ // Check if already exists
440
+ const avds = await this.listAVDs();
441
+ if (avds.some(a => a.name === avdName)) {
442
+ info(`AVD "${avdName}" already exists`);
443
+ return;
444
+ }
445
+
446
+ await this.createAVD({
447
+ name: avdName,
448
+ device: 'pixel_5',
449
+ apiLevel: 34,
450
+ abi: os.arch() === 'arm64' ? 'arm64-v8a' : 'x86_64',
451
+ force: false,
452
+ });
453
+
454
+ success(`JetStart-optimized AVD created: ${avdName}`);
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Create an AVD manager instance
460
+ */
461
+ export function createAVDManager(sdkRoot?: string): AVDManager {
462
+ return new AVDManager(sdkRoot);
463
+ }