@jetstart/cli 1.2.0 → 1.5.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/cli.js +18 -1
- package/dist/commands/android-emulator.d.ts +8 -0
- package/dist/commands/android-emulator.js +280 -0
- package/dist/commands/create.d.ts +1 -6
- package/dist/commands/create.js +119 -0
- package/dist/commands/dev.d.ts +1 -7
- package/dist/commands/dev.js +69 -10
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +5 -1
- package/dist/commands/install-audit.d.ts +9 -0
- package/dist/commands/install-audit.js +185 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/index.js +8 -0
- package/dist/utils/android-sdk.d.ts +81 -0
- package/dist/utils/android-sdk.js +432 -0
- package/dist/utils/downloader.d.ts +35 -0
- package/dist/utils/downloader.js +214 -0
- package/dist/utils/emulator-deployer.d.ts +29 -0
- package/dist/utils/emulator-deployer.js +224 -0
- package/dist/utils/emulator.d.ts +101 -0
- package/dist/utils/emulator.js +410 -0
- package/dist/utils/java.d.ts +25 -0
- package/dist/utils/java.js +363 -0
- package/dist/utils/system-tools.d.ts +93 -0
- package/dist/utils/system-tools.js +599 -0
- package/dist/utils/template.js +777 -748
- package/package.json +7 -3
- package/src/cli.ts +20 -2
- package/src/commands/android-emulator.ts +304 -0
- package/src/commands/create.ts +128 -5
- package/src/commands/dev.ts +71 -18
- package/src/commands/index.ts +3 -1
- package/src/commands/install-audit.ts +227 -0
- package/src/types/index.ts +30 -0
- package/src/utils/android-sdk.ts +478 -0
- package/src/utils/downloader.ts +201 -0
- package/src/utils/emulator-deployer.ts +210 -0
- package/src/utils/emulator.ts +463 -0
- package/src/utils/java.ts +369 -0
- package/src/utils/system-tools.ts +648 -0
- package/src/utils/template.ts +875 -867
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands/build.d.ts.map +0 -1
- package/dist/commands/build.js.map +0 -1
- package/dist/commands/create.d.ts.map +0 -1
- package/dist/commands/create.js.map +0 -1
- package/dist/commands/dev.d.ts.map +0 -1
- package/dist/commands/dev.js.map +0 -1
- package/dist/commands/index.d.ts.map +0 -1
- package/dist/commands/index.js.map +0 -1
- package/dist/commands/logs.d.ts.map +0 -1
- package/dist/commands/logs.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/prompt.d.ts.map +0 -1
- package/dist/utils/prompt.js.map +0 -1
- package/dist/utils/spinner.d.ts.map +0 -1
- package/dist/utils/spinner.js.map +0 -1
- package/dist/utils/template.d.ts.map +0 -1
- package/dist/utils/template.js.map +0 -1
|
@@ -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
|
+
}
|