avd_manager 1.0.4
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/.github/FUNDING.yml +3 -0
- package/.github/workflows/build-release.yml +103 -0
- package/.github/workflows/ci.yml +32 -0
- package/.versionrc.json +15 -0
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +270 -0
- package/analysis_options.yaml +30 -0
- package/bin/avdm.dart +176 -0
- package/docs/.nojekyll +0 -0
- package/docs/404.html +28 -0
- package/docs/README.md +171 -0
- package/docs/_coverpage.md +8 -0
- package/docs/_sidebar.md +15 -0
- package/docs/assets/404.png +0 -0
- package/docs/assets/avdmbanner.png +0 -0
- package/docs/assets/badges.png +0 -0
- package/docs/assets/banner-light.png +0 -0
- package/docs/assets/cover.png +0 -0
- package/docs/assets/favicon.png +0 -0
- package/docs/assets/logo.png +0 -0
- package/docs/commands/create.md +24 -0
- package/docs/commands/delete.md +18 -0
- package/docs/commands/launch.md +17 -0
- package/docs/commands/list.md +32 -0
- package/docs/getting-started.md +135 -0
- package/docs/index.html +105 -0
- package/docs/theme.css +99 -0
- package/example/bin/example.dart +15 -0
- package/lib/avd_utils.dart +186 -0
- package/lib/commands/create.dart +86 -0
- package/lib/commands/delete.dart +31 -0
- package/lib/commands/launch.dart +68 -0
- package/lib/commands/list.dart +183 -0
- package/lib/src/version.dart +37 -0
- package/npm/LICENSE +21 -0
- package/npm/README.md +97 -0
- package/npm/bin/avdm-linux +0 -0
- package/npm/bin/avdm-macos +0 -0
- package/npm/bin/avdm-windows.exe +0 -0
- package/npm/index.js +16 -0
- package/npm/package.json +18 -0
- package/package.json +16 -0
- package/pubspec.yaml +28 -0
- package/scripts/debug_list_devices.dart +15 -0
- package/scripts/sync-versions.js +13 -0
- package/test/create_command_test.dart +38 -0
- package/test/list_avds_test.dart +76 -0
- package/test/utils_test.dart +52 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import 'dart:convert';
|
|
2
|
+
import 'dart:io';
|
|
3
|
+
import 'package:path/path.dart' as p;
|
|
4
|
+
import 'package:args/args.dart';
|
|
5
|
+
|
|
6
|
+
class AvdInfo {
|
|
7
|
+
final String name;
|
|
8
|
+
final int sizeBytes;
|
|
9
|
+
|
|
10
|
+
AvdInfo({required this.name, required this.sizeBytes});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Function to parse the command-line arguments and return a Map
|
|
14
|
+
Map<String, String> parseArguments(List<String> arguments) {
|
|
15
|
+
final parser = ArgParser();
|
|
16
|
+
parser.addOption('sort', defaultsTo: 'name');
|
|
17
|
+
parser.addOption('min-size', defaultsTo: '0');
|
|
18
|
+
|
|
19
|
+
final argResults = parser.parse(arguments);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
'sort': argResults['sort'] ?? 'name',
|
|
23
|
+
'min-size': argResults['min-size'] ?? '0',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Future<List<AvdInfo>> getAvailableAvds() async {
|
|
28
|
+
final avdHome = Platform.environment['ANDROID_AVD_HOME'] ??
|
|
29
|
+
p.join(Platform.environment['HOME']!, '.android', 'avd');
|
|
30
|
+
|
|
31
|
+
final avdDirectory = Directory(avdHome);
|
|
32
|
+
if (!await avdDirectory.exists()) {
|
|
33
|
+
print('❌ AVD directory does not exist at $avdHome');
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
final avdDirs = await avdDirectory
|
|
38
|
+
.list()
|
|
39
|
+
.where((entity) => entity is Directory && entity.path.endsWith('.avd'))
|
|
40
|
+
.toList();
|
|
41
|
+
|
|
42
|
+
List<AvdInfo> avds = [];
|
|
43
|
+
for (final entity in avdDirs) {
|
|
44
|
+
final dir = entity as Directory;
|
|
45
|
+
final size = await _getDirectorySize(dir);
|
|
46
|
+
final name = p.basenameWithoutExtension(dir.path);
|
|
47
|
+
avds.add(AvdInfo(name: name, sizeBytes: size));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return avds;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Future<int> _getDirectorySize(Directory dir) async {
|
|
54
|
+
int size = 0;
|
|
55
|
+
await for (var entity in dir.list(recursive: true)) {
|
|
56
|
+
if (entity is File) {
|
|
57
|
+
size += await entity.length();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return size;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Future<ProcessResult> runAvdManager(List<String> args) async {
|
|
64
|
+
final javaHomeExpr = Platform.isMacOS ? r'$(echo $JAVA_HOME)' : r'$JAVA_HOME';
|
|
65
|
+
final fullCommand =
|
|
66
|
+
'export JAVA_HOME=$javaHomeExpr && avdmanager ${args.map((a) => '"$a"').join(' ')}';
|
|
67
|
+
return await Process.run(
|
|
68
|
+
'bash',
|
|
69
|
+
['-lc', fullCommand],
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Future<ProcessResult> runSdkManager(List<String> args) async {
|
|
74
|
+
final environment = Map<String, String>.from(Platform.environment);
|
|
75
|
+
final javaHome = Platform.environment['JAVA_HOME'];
|
|
76
|
+
if (javaHome != null) {
|
|
77
|
+
environment['JAVA_HOME'] = javaHome;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
final process = await Process.start(
|
|
81
|
+
'sdkmanager',
|
|
82
|
+
['--install', ...args],
|
|
83
|
+
environment: environment,
|
|
84
|
+
runInShell: false,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Pre-answer any prompt with yes so sdkmanager can proceed.
|
|
88
|
+
for (var i = 0; i < 20; i++) {
|
|
89
|
+
process.stdin.writeln('y');
|
|
90
|
+
}
|
|
91
|
+
await process.stdin.close();
|
|
92
|
+
|
|
93
|
+
final stdoutBuffer = StringBuffer();
|
|
94
|
+
final stderrBuffer = StringBuffer();
|
|
95
|
+
|
|
96
|
+
final stdoutDone = process.stdout.transform(utf8.decoder).forEach((chunk) {
|
|
97
|
+
stdoutBuffer.write(chunk);
|
|
98
|
+
stdout.write(chunk);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
final stderrDone = process.stderr.transform(utf8.decoder).forEach((chunk) {
|
|
102
|
+
stderrBuffer.write(chunk);
|
|
103
|
+
stderr.write(chunk);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
final exitCode = await process.exitCode.timeout(
|
|
107
|
+
const Duration(minutes: 30),
|
|
108
|
+
onTimeout: () {
|
|
109
|
+
process.kill(ProcessSignal.sigkill);
|
|
110
|
+
return -1;
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
await Future.wait([stdoutDone, stderrDone]);
|
|
115
|
+
|
|
116
|
+
return ProcessResult(
|
|
117
|
+
process.pid,
|
|
118
|
+
exitCode,
|
|
119
|
+
stdoutBuffer.toString(),
|
|
120
|
+
stderrBuffer.toString(),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
Future<void> createAndPatchAvd(String name,
|
|
125
|
+
{required String device, required String systemImage}) async {
|
|
126
|
+
print('📦 Ensuring system image is installed...');
|
|
127
|
+
final sdkResult = await runSdkManager([systemImage]);
|
|
128
|
+
if (sdkResult.exitCode == -1) {
|
|
129
|
+
print('❌ System image installation timed out after 30 minutes.');
|
|
130
|
+
print('Attempted system image package: $systemImage');
|
|
131
|
+
print('This can happen when the image is large or your network is slow.');
|
|
132
|
+
print('Please verify that sdkmanager is available and retry.');
|
|
133
|
+
print('stdout:\n${sdkResult.stdout}');
|
|
134
|
+
print('stderr:\n${sdkResult.stderr}');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (sdkResult.exitCode != 0) {
|
|
139
|
+
print('❌ Failed to install system image: $systemImage');
|
|
140
|
+
print('stdout:\n${sdkResult.stdout}');
|
|
141
|
+
print('stderr:\n${sdkResult.stderr}');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
print('🧰 Creating AVD...');
|
|
146
|
+
final javaHomeExpr = Platform.isMacOS ? r'$(echo $JAVA_HOME)' : r'$JAVA_HOME';
|
|
147
|
+
|
|
148
|
+
final command = '''
|
|
149
|
+
export JAVA_HOME=$javaHomeExpr;
|
|
150
|
+
echo no | avdmanager create avd -n "$name" -k "$systemImage" --device "$device"
|
|
151
|
+
''';
|
|
152
|
+
|
|
153
|
+
final result = await Process.run(
|
|
154
|
+
'bash',
|
|
155
|
+
['-c', command],
|
|
156
|
+
runInShell: true,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (result.exitCode != 0) {
|
|
160
|
+
print('❌ Failed to create AVD:\n${result.stdout}\n${result.stderr}');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
print('✅ AVD $name created successfully.');
|
|
165
|
+
|
|
166
|
+
// Confirm AVD actually exists
|
|
167
|
+
final avdHome = Platform.environment['ANDROID_AVD_HOME'] ??
|
|
168
|
+
'${Platform.environment['HOME']}/.android/avd';
|
|
169
|
+
final configPath = '$avdHome/$name.avd/config.ini';
|
|
170
|
+
final configFile = File(configPath);
|
|
171
|
+
|
|
172
|
+
if (!configFile.existsSync()) {
|
|
173
|
+
print('❌ Could not find config.ini for $name.');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
print('🔧 Patching config.ini for performance...');
|
|
178
|
+
var config = configFile.readAsStringSync();
|
|
179
|
+
if (!config.contains('hw.gpu.enabled')) {
|
|
180
|
+
config += '\nhw.gpu.enabled=yes\nhw.gpu.mode=auto\n';
|
|
181
|
+
configFile.writeAsStringSync(config);
|
|
182
|
+
print('✅ Patched config.ini for GPU acceleration.');
|
|
183
|
+
} else {
|
|
184
|
+
print('config.ini already patched.');
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import 'dart:io';
|
|
2
|
+
import 'package:avd_manager/avd_utils.dart';
|
|
3
|
+
|
|
4
|
+
Future<List<String>> listAvailableDevices() async {
|
|
5
|
+
final result = await runAvdManager(['list', 'device']);
|
|
6
|
+
if (result.exitCode != 0) {
|
|
7
|
+
print('❌ Failed to list devices');
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
final devices = <String>[];
|
|
12
|
+
final regex = RegExp(r'id: \d+ or "(.*?)"');
|
|
13
|
+
for (final line in result.stdout.toString().split('\n')) {
|
|
14
|
+
final match = regex.firstMatch(line);
|
|
15
|
+
if (match != null) {
|
|
16
|
+
devices.add(match.group(1)!);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (devices.isEmpty) {
|
|
21
|
+
print('⚠️ No devices found.');
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
return devices;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Future<String?> promptForDevice(List<String> devices) async {
|
|
28
|
+
print('\n📱 Available Devices:\n');
|
|
29
|
+
for (int i = 0; i < devices.length; i++) {
|
|
30
|
+
print(' [${i + 1}] ${devices[i]}');
|
|
31
|
+
}
|
|
32
|
+
stdout.write('\nEnter device number: ');
|
|
33
|
+
final input = stdin.readLineSync();
|
|
34
|
+
final index = int.tryParse(input ?? '');
|
|
35
|
+
if (index != null && index > 0 && index <= devices.length) {
|
|
36
|
+
return devices[index - 1];
|
|
37
|
+
}
|
|
38
|
+
print('❌ Invalid selection.');
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Future<void> createAvd(String name,
|
|
43
|
+
{String? device, required String apiLevel, String? abi}) async {
|
|
44
|
+
final systemImage = await _getSystemImage(apiLevel, abi: abi);
|
|
45
|
+
|
|
46
|
+
device ??= await _selectDevice();
|
|
47
|
+
if (device == null) {
|
|
48
|
+
print('❌ No device selected.');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await createAndPatchAvd(name, device: device, systemImage: systemImage);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
Future<String> _getSystemImage(String apiLevel, {String? abi}) async {
|
|
56
|
+
final selectedAbi = abi ?? await _resolveDefaultAbi();
|
|
57
|
+
|
|
58
|
+
if (selectedAbi.isEmpty) {
|
|
59
|
+
return 'system-images;android-$apiLevel;google_apis;x86';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return 'system-images;android-$apiLevel;google_apis;$selectedAbi';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
Future<String> _resolveDefaultAbi() async {
|
|
66
|
+
try {
|
|
67
|
+
final result = await Process.run('uname', ['-m']);
|
|
68
|
+
final hostArch = result.stdout.toString().trim().toLowerCase();
|
|
69
|
+
if (hostArch == 'arm64' || hostArch == 'aarch64') {
|
|
70
|
+
return 'arm64-v8a';
|
|
71
|
+
}
|
|
72
|
+
if (hostArch == 'x86_64' || hostArch == 'amd64') {
|
|
73
|
+
return 'x86';
|
|
74
|
+
}
|
|
75
|
+
} catch (_) {
|
|
76
|
+
// ignore and fall back to x86
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return 'x86';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
Future<String?> _selectDevice() async {
|
|
83
|
+
final devices = await listAvailableDevices();
|
|
84
|
+
if (devices.isEmpty) return null;
|
|
85
|
+
return await promptForDevice(devices);
|
|
86
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import 'dart:io';
|
|
2
|
+
import 'package:path/path.dart' as p;
|
|
3
|
+
|
|
4
|
+
Future<void> deleteAvd(String name) async {
|
|
5
|
+
final avdHome = Platform.environment['ANDROID_AVD_HOME'] ??
|
|
6
|
+
p.join(Platform.environment['HOME']!, '.android', 'avd');
|
|
7
|
+
final iniPath = p.join(avdHome, '$name.ini');
|
|
8
|
+
final avdPath = p.join(avdHome, '$name.avd');
|
|
9
|
+
|
|
10
|
+
final iniFile = File(iniPath);
|
|
11
|
+
final avdDir = Directory(avdPath);
|
|
12
|
+
|
|
13
|
+
if (!await iniFile.exists() && !await avdDir.exists()) {
|
|
14
|
+
print('❌ AVD "$name" does not exist.');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
if (await iniFile.exists()) {
|
|
20
|
+
await iniFile.delete();
|
|
21
|
+
print('🗑️ Deleted $iniPath');
|
|
22
|
+
}
|
|
23
|
+
if (await avdDir.exists()) {
|
|
24
|
+
await avdDir.delete(recursive: true);
|
|
25
|
+
print('🗑️ Deleted $avdPath');
|
|
26
|
+
}
|
|
27
|
+
print('✅ AVD "$name" deleted successfully.');
|
|
28
|
+
} catch (e) {
|
|
29
|
+
print('❌ Failed to delete AVD "$name": $e');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import 'dart:io';
|
|
2
|
+
import 'package:process_run/shell.dart';
|
|
3
|
+
|
|
4
|
+
/// Lists available AVDs by reading ~/.android/avd directory
|
|
5
|
+
Future<List<String>> listAvds() async {
|
|
6
|
+
final home = Platform.environment['HOME'] ?? '';
|
|
7
|
+
final avdDir = Directory('$home/.android/avd');
|
|
8
|
+
if (!await avdDir.exists()) return [];
|
|
9
|
+
|
|
10
|
+
final avds = <String>[];
|
|
11
|
+
|
|
12
|
+
await for (var entity in avdDir.list()) {
|
|
13
|
+
if (entity is File && entity.path.endsWith('.ini')) {
|
|
14
|
+
final name = entity.uri.pathSegments.last.replaceAll('.ini', '');
|
|
15
|
+
avds.add(name);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return avds;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Prompt the user to choose an AVD
|
|
23
|
+
Future<String?> promptAvdSelection(List<String> avds) async {
|
|
24
|
+
print('\n📱 Available AVDs:\n');
|
|
25
|
+
for (int i = 0; i < avds.length; i++) {
|
|
26
|
+
print(' [${i + 1}] ${avds[i]}');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
stdout.write('\nEnter AVD number to launch: ');
|
|
30
|
+
final input = stdin.readLineSync();
|
|
31
|
+
final index = int.tryParse(input ?? '');
|
|
32
|
+
|
|
33
|
+
if (index != null && index > 0 && index <= avds.length) {
|
|
34
|
+
return avds[index - 1];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
print('❌ Invalid selection.');
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Launches the given AVD with optional flags
|
|
42
|
+
Future<void> launchAvd(String? name, {List<String>? extraArgs}) async {
|
|
43
|
+
final shell = Shell();
|
|
44
|
+
|
|
45
|
+
if (name == null) {
|
|
46
|
+
final avds = await listAvds();
|
|
47
|
+
if (avds.isEmpty) {
|
|
48
|
+
print('❌ No AVDs found.');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
name = await promptAvdSelection(avds);
|
|
52
|
+
if (name == null) return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
print('🚀 Launching AVD "$name"...');
|
|
56
|
+
|
|
57
|
+
final args = [
|
|
58
|
+
'-avd',
|
|
59
|
+
name,
|
|
60
|
+
...?extraArgs,
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await shell.run('emulator ${args.join(' ')}');
|
|
65
|
+
} catch (e) {
|
|
66
|
+
print('❌ Failed to launch AVD "$name": $e');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import 'dart:io';
|
|
2
|
+
import 'package:avd_manager/avd_utils.dart';
|
|
3
|
+
import 'package:path/path.dart' as p;
|
|
4
|
+
|
|
5
|
+
// parse size strings like "500MB" into bytes
|
|
6
|
+
int parseSize(String input) {
|
|
7
|
+
if (input.isEmpty) {
|
|
8
|
+
throw FormatException('Size cannot be empty');
|
|
9
|
+
}
|
|
10
|
+
final pattern = RegExp(r'^(\d+(?:\.\d+)?)([KMG]B)$', caseSensitive: false);
|
|
11
|
+
final match = pattern.firstMatch(input.trim());
|
|
12
|
+
|
|
13
|
+
if (match == null) {
|
|
14
|
+
throw FormatException(
|
|
15
|
+
'Invalid size format: $input. Use formats like 500MB or 1GB');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
final value = double.parse(match.group(1)!); // The numeric part
|
|
19
|
+
if (value <= 0) {
|
|
20
|
+
throw FormatException('Size must be greater than 0');
|
|
21
|
+
}
|
|
22
|
+
final unit = match.group(2)!.toUpperCase(); // The unit (MB, GB, etc.)
|
|
23
|
+
|
|
24
|
+
switch (unit) {
|
|
25
|
+
case 'KB':
|
|
26
|
+
return (value * 1024).toInt();
|
|
27
|
+
case 'MB':
|
|
28
|
+
return (value * 1024 * 1024).toInt(); // Convert MB to bytes
|
|
29
|
+
case 'GB':
|
|
30
|
+
return (value * 1024 * 1024 * 1024).toInt(); // Convert GB to bytes
|
|
31
|
+
default:
|
|
32
|
+
throw FormatException('Unknown size unit: $unit');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Format size strings like "500MB" into bytes
|
|
37
|
+
String formatSize(int bytes) {
|
|
38
|
+
if (bytes >= 1 << 30) return '${(bytes / (1 << 30)).toStringAsFixed(2)} GB';
|
|
39
|
+
if (bytes >= 1 << 20) return '${(bytes / (1 << 20)).toStringAsFixed(2)} MB';
|
|
40
|
+
if (bytes >= 1 << 10) return '${(bytes / (1 << 10)).toStringAsFixed(2)} KB';
|
|
41
|
+
return '$bytes B';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Define a simple class to hold AVD data
|
|
45
|
+
class Avd {
|
|
46
|
+
final String name;
|
|
47
|
+
final int size;
|
|
48
|
+
final String sizeStr;
|
|
49
|
+
|
|
50
|
+
Avd(this.name, this.size, this.sizeStr);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// ## ✅ Helper: `_getDirectorySize()
|
|
54
|
+
Future<int> _getDirectorySize(Directory directory) async {
|
|
55
|
+
int totalSize = 0;
|
|
56
|
+
if (await directory.exists()) {
|
|
57
|
+
await for (final fileSystemEntity in directory.list(recursive: true)) {
|
|
58
|
+
if (fileSystemEntity is File) {
|
|
59
|
+
totalSize += await fileSystemEntity.length();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return totalSize;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
String _formatSize(int bytes) {
|
|
67
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
68
|
+
double size = bytes.toDouble();
|
|
69
|
+
int unit = 0;
|
|
70
|
+
|
|
71
|
+
while (size > 1024 && unit < units.length - 1) {
|
|
72
|
+
size /= 1024;
|
|
73
|
+
unit++;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return '${size.toStringAsFixed(2)} ${units[unit]}';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// ## ✅ Main Listing Function
|
|
80
|
+
|
|
81
|
+
Future<void> listAvds(Map<String, String> cmd) async {
|
|
82
|
+
// Get AVD home directory
|
|
83
|
+
final avdHome = Platform.environment['ANDROID_AVD_HOME'] ??
|
|
84
|
+
p.join(Platform.environment['HOME']!, '.android', 'avd');
|
|
85
|
+
|
|
86
|
+
final avdDir = Directory(avdHome);
|
|
87
|
+
|
|
88
|
+
if (!await avdDir.exists()) {
|
|
89
|
+
print('❌ AVD directory not found at $avdHome');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// List all AVD directories
|
|
94
|
+
final avdDirectories = avdDir.listSync().whereType<Directory>();
|
|
95
|
+
final avdData = <Avd>[];
|
|
96
|
+
|
|
97
|
+
// Fetch size for each AVD asynchronously and build Avd objects
|
|
98
|
+
for (final avd in avdDirectories) {
|
|
99
|
+
final name = p.basenameWithoutExtension(avd.path);
|
|
100
|
+
final size = await _getDirectorySize(avd);
|
|
101
|
+
final sizeStr = _formatSize(size);
|
|
102
|
+
avdData.add(Avd(name, size, sizeStr));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Minimum AVDs check
|
|
106
|
+
if (avdData.isEmpty) {
|
|
107
|
+
print('No AVDs found.');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Extract command-line arguments
|
|
112
|
+
final sortBy = cmd['sort'] ?? 'name';
|
|
113
|
+
final minSizeStr = cmd['min-size'] ?? '0';
|
|
114
|
+
final nameStr = cmd['name'] ?? 'list';
|
|
115
|
+
|
|
116
|
+
// Skip size filtering if 'min-size' is '0'
|
|
117
|
+
if (minSizeStr != '0' && minSizeStr.isNotEmpty) {
|
|
118
|
+
try {
|
|
119
|
+
final minBytes = parseSize(minSizeStr);
|
|
120
|
+
avdData.removeWhere((avd) => avd.size <= minBytes);
|
|
121
|
+
final remainingCount = avdData.length;
|
|
122
|
+
if (remainingCount == 0) {
|
|
123
|
+
print(
|
|
124
|
+
'No AVDs found with size greater than ${minSizeStr.toUpperCase()}');
|
|
125
|
+
return;
|
|
126
|
+
} else {
|
|
127
|
+
print(
|
|
128
|
+
'Found $remainingCount AVD(s) with size greater than ${minSizeStr.toUpperCase()}:');
|
|
129
|
+
}
|
|
130
|
+
//sort list avds by name
|
|
131
|
+
if (nameStr == 'list') {
|
|
132
|
+
avdData.sort(
|
|
133
|
+
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
|
134
|
+
} else if (nameStr == 'size') {
|
|
135
|
+
avdData.sort((a, b) => a.size.compareTo(b.size));
|
|
136
|
+
} else {
|
|
137
|
+
print('Invalid sort option: $nameStr. Defaulting to sorting by name.');
|
|
138
|
+
avdData.sort(
|
|
139
|
+
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
|
140
|
+
}
|
|
141
|
+
} catch (e) {
|
|
142
|
+
print(
|
|
143
|
+
'❌ Invalid size format: $minSizeStr. Use formats like 500MB or 1GB.');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Sort AVDs if 'sort' argument is provided
|
|
149
|
+
if (sortBy == 'size') {
|
|
150
|
+
avdData.sort((a, b) => a.size.compareTo(b.size));
|
|
151
|
+
} else if (sortBy == 'name') {
|
|
152
|
+
avdData
|
|
153
|
+
.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Loop and print the sorted AVD data
|
|
157
|
+
for (final avd in avdData) {
|
|
158
|
+
print('• ${avd.name} (AVD directory size: ${avd.sizeStr})');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// ## ✅ Helper: `getAvailableAvds()
|
|
163
|
+
Future<List<AvdInfo>> getAvailableAvds() async {
|
|
164
|
+
final avdHome = Platform.environment['ANDROID_AVD_HOME'] ??
|
|
165
|
+
p.join(Platform.environment['HOME']!, '.android', 'avd');
|
|
166
|
+
final avdDirectory = Directory(avdHome);
|
|
167
|
+
if (!await avdDirectory.exists()) {
|
|
168
|
+
print('❌ AVD directory does not exist at $avdHome');
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
final avdDirs = await avdDirectory
|
|
172
|
+
.list()
|
|
173
|
+
.where((entity) => entity is Directory && entity.path.endsWith('.avd'))
|
|
174
|
+
.toList();
|
|
175
|
+
List<AvdInfo> avds = [];
|
|
176
|
+
for (final entity in avdDirs) {
|
|
177
|
+
final dir = entity as Directory;
|
|
178
|
+
final size = await _getDirectorySize(dir);
|
|
179
|
+
final name = p.basenameWithoutExtension(dir.path);
|
|
180
|
+
avds.add(AvdInfo(name: name, sizeBytes: size));
|
|
181
|
+
}
|
|
182
|
+
return avds;
|
|
183
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import 'dart:io';
|
|
2
|
+
import 'package:yaml/yaml.dart';
|
|
3
|
+
|
|
4
|
+
String getVersion() {
|
|
5
|
+
try {
|
|
6
|
+
// Look for pubspec.yaml in the project root
|
|
7
|
+
final pubspecPath = _findPubspecPath();
|
|
8
|
+
|
|
9
|
+
if (pubspecPath != null && File(pubspecPath).existsSync()) {
|
|
10
|
+
final pubspecContent = File(pubspecPath).readAsStringSync();
|
|
11
|
+
final pubspec = loadYaml(pubspecContent) as Map;
|
|
12
|
+
return pubspec['version'] ?? 'unknown';
|
|
13
|
+
}
|
|
14
|
+
} catch (e) {
|
|
15
|
+
// Silently fail and return unknown
|
|
16
|
+
}
|
|
17
|
+
return 'unknown';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Find pubspec.yaml by walking up the directory tree
|
|
21
|
+
String? _findPubspecPath() {
|
|
22
|
+
var current = Directory.current;
|
|
23
|
+
|
|
24
|
+
while (true) {
|
|
25
|
+
final pubspecFile = File('${current.path}/pubspec.yaml');
|
|
26
|
+
if (pubspecFile.existsSync()) {
|
|
27
|
+
return pubspecFile.path;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
final parent = current.parent;
|
|
31
|
+
if (parent.path == current.path) {
|
|
32
|
+
// Reached filesystem root
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
current = parent;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/npm/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rabi Iya
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|