solana-mobile 0.0.1 → 0.1.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/README.md +9 -0
- package/dist/cli.cjs +800 -122
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +802 -124
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command, InvalidArgumentError } from "commander";
|
|
3
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { cancel, intro, isCancel, log, multiselect, note, outro, select, text } from "@clack/prompts";
|
|
5
5
|
import { createApp, detectInvokedPackageManager, fetchTemplateData, finalNote, getAppInfo, listTemplateIds, listTemplates, listVersions, validateProjectName } from "create-solana-dapp";
|
|
6
6
|
import { execFile, spawn } from "node:child_process";
|
|
7
|
-
import {
|
|
8
|
-
import { homedir } from "node:os";
|
|
7
|
+
import { access, mkdir, readFile, readdir, realpath, statfs, writeFile } from "node:fs/promises";
|
|
8
|
+
import { homedir, platform, release } from "node:os";
|
|
9
9
|
import { constants } from "node:fs";
|
|
10
|
-
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
11
10
|
//#region package.json
|
|
12
11
|
var package_default = {
|
|
13
12
|
bin: { "solana-mobile": "dist/cli.mjs" },
|
|
@@ -53,7 +52,7 @@ var package_default = {
|
|
|
53
52
|
"version": "changeset version && bun lint:fix"
|
|
54
53
|
},
|
|
55
54
|
type: "module",
|
|
56
|
-
version: "0.0
|
|
55
|
+
version: "0.1.0"
|
|
57
56
|
};
|
|
58
57
|
//#endregion
|
|
59
58
|
//#region src/core/util/read-package-string.ts
|
|
@@ -201,97 +200,106 @@ function resolveTemplate(templateName, templates) {
|
|
|
201
200
|
};
|
|
202
201
|
}
|
|
203
202
|
//#endregion
|
|
203
|
+
//#region src/doctor/data-access/doctor-environment.ts
|
|
204
|
+
const defaultDoctorEnvironment = {
|
|
205
|
+
architecture: process.arch,
|
|
206
|
+
environment: process.env,
|
|
207
|
+
getDiskSpace: async (path) => {
|
|
208
|
+
const stats = await statfs(path);
|
|
209
|
+
return stats.bavail * stats.bsize;
|
|
210
|
+
},
|
|
211
|
+
getHomeDirectory: homedir,
|
|
212
|
+
getPlatform: platform,
|
|
213
|
+
getRelease: release,
|
|
214
|
+
listDirectory: async (path) => readdir(path),
|
|
215
|
+
pathExists: async (path) => access(path).then(() => true, () => false),
|
|
216
|
+
resolvePath: async (path) => realpath(path),
|
|
217
|
+
runCommand: runExecutable$1
|
|
218
|
+
};
|
|
219
|
+
function runExecutable$1(command, args = [], timeout = 5e3) {
|
|
220
|
+
return new Promise((resolvePromise, reject) => {
|
|
221
|
+
execFile(command, args, {
|
|
222
|
+
timeout,
|
|
223
|
+
windowsHide: true
|
|
224
|
+
}, (error, stdout, stderr) => {
|
|
225
|
+
if (error) {
|
|
226
|
+
reject(error);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
resolvePromise({
|
|
230
|
+
path: command,
|
|
231
|
+
stderr,
|
|
232
|
+
stdout
|
|
233
|
+
});
|
|
234
|
+
}).stdin?.end();
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
async function findExecutable(name, environment, preferredDirectories = []) {
|
|
238
|
+
const extensions = environment.getPlatform() === "win32" ? [
|
|
239
|
+
"",
|
|
240
|
+
".exe",
|
|
241
|
+
".bat",
|
|
242
|
+
".cmd"
|
|
243
|
+
] : [""];
|
|
244
|
+
const pathDirectories = (environment.environment.PATH ?? "").split(delimiter).filter(Boolean);
|
|
245
|
+
for (const directory of [...preferredDirectories, ...pathDirectories]) for (const extension of extensions) {
|
|
246
|
+
const candidate = join(directory, `${name}${extension}`);
|
|
247
|
+
if (await environment.pathExists(candidate)) return candidate;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function parseVersion(output) {
|
|
251
|
+
return output.match(/(?:^|[^\d])(\d+(?:\.\d+){0,3})(?:[^\d]|$)/m)?.[1];
|
|
252
|
+
}
|
|
253
|
+
function sortVersions(values) {
|
|
254
|
+
return [...values].sort((left, right) => compareVersions(left, right));
|
|
255
|
+
}
|
|
256
|
+
function compareVersions(left, right) {
|
|
257
|
+
const leftParts = left.split(".").map(Number);
|
|
258
|
+
const rightParts = right.split(".").map(Number);
|
|
259
|
+
for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
|
|
260
|
+
const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
|
|
261
|
+
if (difference !== 0) return difference;
|
|
262
|
+
}
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
//#endregion
|
|
204
266
|
//#region src/doctor/data-access/check-adb-version.ts
|
|
205
|
-
const execFileAsync = promisify(execFile);
|
|
206
267
|
const minimumAdbMajorVersion = 33;
|
|
207
|
-
async function checkAdbVersion(
|
|
268
|
+
async function checkAdbVersion(environment = defaultDoctorEnvironment, sdkRoot) {
|
|
269
|
+
const executable = await findExecutable("adb", environment, sdkRoot ? [join(sdkRoot, "platform-tools")] : []);
|
|
270
|
+
if (!executable) return missingAdb();
|
|
208
271
|
try {
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
actual: "unknown",
|
|
213
|
-
message: "Unable to detect adb platform-tools version.",
|
|
214
|
-
name: "adb",
|
|
215
|
-
ok: false,
|
|
216
|
-
recommendation: "Install Android SDK Platform Tools 33 or higher.",
|
|
217
|
-
required: "33 or higher"
|
|
218
|
-
};
|
|
272
|
+
const output = await environment.runCommand(executable, ["version"]);
|
|
273
|
+
const version = parseAdbVersion(`${output.stdout}\n${output.stderr}`);
|
|
274
|
+
const passes = Boolean(version) && Number.parseInt(version ?? "", 10) >= minimumAdbMajorVersion;
|
|
219
275
|
return {
|
|
220
|
-
actual: version,
|
|
221
|
-
|
|
276
|
+
actual: version ?? "unknown",
|
|
277
|
+
category: "android-sdk",
|
|
278
|
+
details: [`Executable: ${executable}`],
|
|
279
|
+
message: version ? `Detected adb ${version}.` : "Unable to detect adb platform-tools version.",
|
|
222
280
|
name: "adb",
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
281
|
+
recommendation: passes ? void 0 : "Install or update Android SDK Platform Tools to version 33 or higher.",
|
|
282
|
+
required: "33 or higher",
|
|
283
|
+
status: passes ? "pass" : "fail"
|
|
226
284
|
};
|
|
227
285
|
} catch {
|
|
228
|
-
return
|
|
229
|
-
actual: "not found",
|
|
230
|
-
message: "adb is not available on PATH.",
|
|
231
|
-
name: "adb",
|
|
232
|
-
ok: false,
|
|
233
|
-
recommendation: "Install Android SDK Platform Tools 33 or higher and make sure adb is on PATH.",
|
|
234
|
-
required: "33 or higher"
|
|
235
|
-
};
|
|
286
|
+
return missingAdb();
|
|
236
287
|
}
|
|
237
288
|
}
|
|
238
289
|
function parseAdbVersion(output) {
|
|
239
290
|
return output.match(/^Version\s+(\d+(?:\.\d+)*)/im)?.[1];
|
|
240
291
|
}
|
|
241
|
-
|
|
242
|
-
const { stdout } = await execFileAsync(command, args);
|
|
243
|
-
return stdout;
|
|
244
|
-
}
|
|
245
|
-
//#endregion
|
|
246
|
-
//#region src/doctor/data-access/check-node-version.ts
|
|
247
|
-
const minimumNodeMajorVersion = 22;
|
|
248
|
-
function checkNodeVersion(version = process.version) {
|
|
249
|
-
const normalizedVersion = normalizeNodeVersion(version);
|
|
250
|
-
const majorVersion = normalizedVersion ? Number.parseInt(normalizedVersion, 10) : NaN;
|
|
251
|
-
if (!normalizedVersion || Number.isNaN(majorVersion)) return {
|
|
252
|
-
actual: "unknown",
|
|
253
|
-
message: "Unable to detect Node.js version.",
|
|
254
|
-
name: "node",
|
|
255
|
-
ok: false,
|
|
256
|
-
recommendation: "Install Node.js 22 or higher.",
|
|
257
|
-
required: "22 or higher"
|
|
258
|
-
};
|
|
292
|
+
function missingAdb() {
|
|
259
293
|
return {
|
|
260
|
-
actual:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
recommendation:
|
|
265
|
-
required: "
|
|
294
|
+
actual: "not found",
|
|
295
|
+
category: "android-sdk",
|
|
296
|
+
message: "adb is not available.",
|
|
297
|
+
name: "adb",
|
|
298
|
+
recommendation: "Install Android SDK Platform Tools 33 or higher.",
|
|
299
|
+
required: "33 or higher",
|
|
300
|
+
status: "fail"
|
|
266
301
|
};
|
|
267
302
|
}
|
|
268
|
-
function normalizeNodeVersion(version) {
|
|
269
|
-
return version.trim().replace(/^v/i, "");
|
|
270
|
-
}
|
|
271
|
-
//#endregion
|
|
272
|
-
//#region src/doctor/ui/doctor-ui-report.ts
|
|
273
|
-
function renderDoctorReport(results) {
|
|
274
|
-
for (const result of results) {
|
|
275
|
-
const message = `${result.name}: ${result.actual} (requires ${result.required})`;
|
|
276
|
-
if (result.ok) log.success(message);
|
|
277
|
-
else log.error(message);
|
|
278
|
-
}
|
|
279
|
-
const recommendations = results.filter((result) => !result.ok && result.recommendation).map((result) => result.recommendation);
|
|
280
|
-
if (recommendations.length === 0) {
|
|
281
|
-
outro("Your system is ready.");
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
log.warn("Recommendations");
|
|
285
|
-
for (const recommendation of recommendations) log.message(`- ${recommendation}`);
|
|
286
|
-
outro("Run doctor again after applying the recommendations.");
|
|
287
|
-
}
|
|
288
|
-
//#endregion
|
|
289
|
-
//#region src/doctor/doctor-feature-index.ts
|
|
290
|
-
async function runDoctor() {
|
|
291
|
-
const results = [await checkAdbVersion(), checkNodeVersion()];
|
|
292
|
-
renderDoctorReport(results);
|
|
293
|
-
return results.every((result) => result.ok) ? 0 : 1;
|
|
294
|
-
}
|
|
295
303
|
//#endregion
|
|
296
304
|
//#region src/emulator/data-access/resolve-android-sdk-root.ts
|
|
297
305
|
function resolveAndroidSdkRoot(environment = process.env, homeDirectory = homedir()) {
|
|
@@ -404,6 +412,712 @@ function getTagDisplay(tagId) {
|
|
|
404
412
|
return tagId.split(/[_-]/).filter(Boolean).map((segment) => `${segment[0]?.toUpperCase()}${segment.slice(1).toLowerCase()}`).join(" ");
|
|
405
413
|
}
|
|
406
414
|
//#endregion
|
|
415
|
+
//#region src/emulator/data-access/list-installed-avds.ts
|
|
416
|
+
async function defaultReadDirectory(directoryPath) {
|
|
417
|
+
return readdir(directoryPath, { withFileTypes: true });
|
|
418
|
+
}
|
|
419
|
+
async function defaultReadTextFile$1(filePath) {
|
|
420
|
+
return readFile(filePath, "utf8");
|
|
421
|
+
}
|
|
422
|
+
async function listInstalledAvds({ getHomeDirectory = homedir, readDirectory = defaultReadDirectory, readTextFile = defaultReadTextFile$1 } = {}) {
|
|
423
|
+
const avdRootDirectory = join(getHomeDirectory(), ".android", "avd");
|
|
424
|
+
const avdEntryNames = await listEntryNames(avdRootDirectory, readDirectory);
|
|
425
|
+
const registeredAvdNames = new Set(avdEntryNames.filter((name) => name.endsWith(".ini")).map((name) => name.slice(0, -4)));
|
|
426
|
+
const avdDirectoryNames = avdEntryNames.filter((name) => name.endsWith(".avd") && registeredAvdNames.has(name.slice(0, -4)));
|
|
427
|
+
return (await Promise.all(avdDirectoryNames.map(async (directoryName) => {
|
|
428
|
+
const configPath = join(avdRootDirectory, directoryName, "config.ini");
|
|
429
|
+
const name = directoryName.slice(0, -4);
|
|
430
|
+
let configValues = {};
|
|
431
|
+
try {
|
|
432
|
+
configValues = parseAvdConfig(await readTextFile(configPath));
|
|
433
|
+
} catch {
|
|
434
|
+
configValues = {};
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
device: configValues["hw.device.name"],
|
|
438
|
+
name,
|
|
439
|
+
target: configValues.target
|
|
440
|
+
};
|
|
441
|
+
}))).sort((left, right) => left.name.localeCompare(right.name));
|
|
442
|
+
}
|
|
443
|
+
async function listEntryNames(directoryPath, readDirectory) {
|
|
444
|
+
try {
|
|
445
|
+
return (await readDirectory(directoryPath)).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
|
|
446
|
+
} catch {
|
|
447
|
+
return [];
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region src/doctor/data-access/check-android-devices.ts
|
|
452
|
+
function parseAdbDevices(output) {
|
|
453
|
+
return output.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("List of devices attached")).flatMap((line) => {
|
|
454
|
+
const [serial, state, ...parts] = line.split(/\s+/);
|
|
455
|
+
if (!serial || !state) return [];
|
|
456
|
+
return [{
|
|
457
|
+
attributes: Object.fromEntries(parts.flatMap((part) => {
|
|
458
|
+
const index = part.indexOf(":");
|
|
459
|
+
return index > 0 ? [[part.slice(0, index), part.slice(index + 1)]] : [];
|
|
460
|
+
})),
|
|
461
|
+
kind: serial.startsWith("emulator-") ? "emulator" : "physical",
|
|
462
|
+
serial,
|
|
463
|
+
state
|
|
464
|
+
}];
|
|
465
|
+
}).sort((left, right) => left.serial.localeCompare(right.serial));
|
|
466
|
+
}
|
|
467
|
+
async function checkAndroidDevices(environment, adbAvailable, sdkRoot, installedAvds) {
|
|
468
|
+
const avdNames = (installedAvds ?? await listInstalledAvds()).map(({ name }) => name);
|
|
469
|
+
const avdCheck = {
|
|
470
|
+
actual: avdNames.length ? `${avdNames.length} installed (${avdNames.join(", ")})` : "none installed",
|
|
471
|
+
category: "emulator",
|
|
472
|
+
message: avdNames.length ? `Detected ${avdNames.length} installed Android emulator(s).` : "No Android emulators are installed.",
|
|
473
|
+
name: "Android emulators",
|
|
474
|
+
recommendation: avdNames.length ? void 0 : "Create one with: npx solana-mobile emulator create",
|
|
475
|
+
status: avdNames.length ? "pass" : "warn"
|
|
476
|
+
};
|
|
477
|
+
if (!adbAvailable) return [
|
|
478
|
+
avdCheck,
|
|
479
|
+
{
|
|
480
|
+
actual: "unknown",
|
|
481
|
+
category: "emulator",
|
|
482
|
+
message: "Running emulators cannot be checked without adb.",
|
|
483
|
+
name: "Running emulators",
|
|
484
|
+
status: "info"
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
actual: "unknown",
|
|
488
|
+
category: "device",
|
|
489
|
+
message: "Physical devices cannot be checked without adb.",
|
|
490
|
+
name: "Physical devices",
|
|
491
|
+
status: "info"
|
|
492
|
+
}
|
|
493
|
+
];
|
|
494
|
+
const adbExecutable = await findExecutable("adb", environment, sdkRoot ? [join(sdkRoot, "platform-tools")] : []);
|
|
495
|
+
if (!adbExecutable) return [
|
|
496
|
+
avdCheck,
|
|
497
|
+
{
|
|
498
|
+
actual: "unknown",
|
|
499
|
+
category: "emulator",
|
|
500
|
+
message: "Running emulators cannot be checked without adb.",
|
|
501
|
+
name: "Running emulators",
|
|
502
|
+
status: "info"
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
actual: "unknown",
|
|
506
|
+
category: "device",
|
|
507
|
+
message: "Physical devices cannot be checked without adb.",
|
|
508
|
+
name: "Physical devices",
|
|
509
|
+
status: "info"
|
|
510
|
+
}
|
|
511
|
+
];
|
|
512
|
+
let devices = [];
|
|
513
|
+
try {
|
|
514
|
+
devices = parseAdbDevices((await environment.runCommand(adbExecutable, ["devices", "-l"])).stdout);
|
|
515
|
+
} catch {}
|
|
516
|
+
const emulators = devices.filter(({ kind }) => kind === "emulator");
|
|
517
|
+
const physical = devices.filter(({ kind }) => kind === "physical");
|
|
518
|
+
const running = {
|
|
519
|
+
actual: emulators.length ? emulators.map(({ serial, state }) => `${serial} (${state})`).join(", ") : "none",
|
|
520
|
+
category: "emulator",
|
|
521
|
+
details: emulators.map(({ serial }) => `Serial: ${serial}`),
|
|
522
|
+
message: emulators.length ? "Detected running emulator instances." : "No emulator is currently running.",
|
|
523
|
+
name: "Running emulators",
|
|
524
|
+
status: "info"
|
|
525
|
+
};
|
|
526
|
+
const authorized = physical.filter(({ state }) => state === "device");
|
|
527
|
+
const problematic = physical.filter(({ state }) => state !== "device");
|
|
528
|
+
const recommendation = problematic.some(({ state }) => state === "unauthorized") ? "Unlock the phone and accept the USB debugging authorization dialog." : problematic.length ? "Reconnect the offline Android device and restart adb if necessary." : void 0;
|
|
529
|
+
return [
|
|
530
|
+
avdCheck,
|
|
531
|
+
running,
|
|
532
|
+
{
|
|
533
|
+
actual: physical.length ? physical.map(formatPhysicalDevice).join(", ") : "none connected",
|
|
534
|
+
category: "device",
|
|
535
|
+
details: physical.map(({ serial }) => `Serial: ${serial}`),
|
|
536
|
+
message: authorized.length ? `Detected ${authorized.length} authorized physical device(s).` : problematic.length ? "A physical device is connected but unavailable." : "No physical Android device is connected.",
|
|
537
|
+
name: "Physical devices",
|
|
538
|
+
recommendation,
|
|
539
|
+
status: authorized.length ? "pass" : problematic.length ? "warn" : "info"
|
|
540
|
+
}
|
|
541
|
+
];
|
|
542
|
+
}
|
|
543
|
+
async function checkEmulatorAcceleration(environment) {
|
|
544
|
+
const platform = environment.getPlatform();
|
|
545
|
+
if (platform === "darwin") return {
|
|
546
|
+
actual: "Hypervisor Framework available on supported Macs",
|
|
547
|
+
category: "emulator",
|
|
548
|
+
message: "macOS Android Emulator uses Hypervisor Framework when supported.",
|
|
549
|
+
name: "Emulator acceleration",
|
|
550
|
+
status: "info"
|
|
551
|
+
};
|
|
552
|
+
if (platform === "linux") {
|
|
553
|
+
const available = await access("/dev/kvm").then(() => true, () => false);
|
|
554
|
+
return {
|
|
555
|
+
actual: available ? "/dev/kvm available" : "/dev/kvm unavailable",
|
|
556
|
+
category: "emulator",
|
|
557
|
+
message: available ? "KVM acceleration is available." : "KVM acceleration is unavailable or inaccessible.",
|
|
558
|
+
name: "Emulator acceleration",
|
|
559
|
+
recommendation: available ? void 0 : "Enable KVM and grant the current user access to /dev/kvm.",
|
|
560
|
+
status: available ? "pass" : "warn"
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
return {
|
|
564
|
+
actual: "unknown",
|
|
565
|
+
category: "emulator",
|
|
566
|
+
message: "Emulator acceleration could not be determined reliably.",
|
|
567
|
+
name: "Emulator acceleration",
|
|
568
|
+
status: "info"
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
function formatPhysicalDevice(device) {
|
|
572
|
+
return `${device.attributes.model?.replaceAll("_", " ") ?? device.serial} (${device.state === "device" ? "authorized" : device.state})`;
|
|
573
|
+
}
|
|
574
|
+
//#endregion
|
|
575
|
+
//#region src/doctor/data-access/check-android-sdk.ts
|
|
576
|
+
async function resolveAndroidSdk(environment) {
|
|
577
|
+
const home = environment.getHomeDirectory();
|
|
578
|
+
const defaults = {
|
|
579
|
+
darwin: join(home, "Library", "Android", "sdk"),
|
|
580
|
+
linux: join(home, "Android", "Sdk"),
|
|
581
|
+
win32: environment.environment.LOCALAPPDATA ? join(environment.environment.LOCALAPPDATA, "Android", "Sdk") : void 0
|
|
582
|
+
};
|
|
583
|
+
const candidates = [
|
|
584
|
+
{
|
|
585
|
+
path: environment.environment.ANDROID_HOME,
|
|
586
|
+
source: "ANDROID_HOME"
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
path: environment.environment.ANDROID_SDK_ROOT,
|
|
590
|
+
source: "ANDROID_SDK_ROOT"
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
path: defaults[environment.getPlatform()],
|
|
594
|
+
source: "default location"
|
|
595
|
+
}
|
|
596
|
+
].filter((value) => Boolean(value.path));
|
|
597
|
+
const homeValue = environment.environment.ANDROID_HOME;
|
|
598
|
+
const rootValue = environment.environment.ANDROID_SDK_ROOT;
|
|
599
|
+
const conflict = homeValue && rootValue && homeValue !== rootValue ? `${homeValue} conflicts with ${rootValue}` : void 0;
|
|
600
|
+
for (const candidate of candidates) if (await environment.pathExists(candidate.path)) return {
|
|
601
|
+
conflict,
|
|
602
|
+
path: await environment.resolvePath(candidate.path).catch(() => candidate.path),
|
|
603
|
+
searched: candidates.map(({ path }) => path),
|
|
604
|
+
source: candidate.source
|
|
605
|
+
};
|
|
606
|
+
return {
|
|
607
|
+
conflict,
|
|
608
|
+
searched: candidates.map(({ path }) => path)
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
async function checkAndroidSdk(environment, resolution) {
|
|
612
|
+
const checks = [];
|
|
613
|
+
checks.push({
|
|
614
|
+
actual: resolution.path ? `${resolution.path} (${resolution.source})` : "not found",
|
|
615
|
+
category: "android-sdk",
|
|
616
|
+
details: [`Searched: ${resolution.searched.join(", ") || "no locations available"}`],
|
|
617
|
+
message: resolution.path ? `Resolved Android SDK from ${resolution.source}.` : "No valid Android SDK directory was found.",
|
|
618
|
+
name: "Android SDK",
|
|
619
|
+
recommendation: resolution.path ? void 0 : "Install the Android SDK or set ANDROID_HOME to its location.",
|
|
620
|
+
status: resolution.path ? "pass" : "fail"
|
|
621
|
+
});
|
|
622
|
+
if (resolution.conflict) checks.push({
|
|
623
|
+
actual: resolution.conflict,
|
|
624
|
+
category: "android-sdk",
|
|
625
|
+
message: "ANDROID_HOME and ANDROID_SDK_ROOT point to different locations.",
|
|
626
|
+
name: "Android SDK environment",
|
|
627
|
+
recommendation: "Make ANDROID_HOME and ANDROID_SDK_ROOT point to the same Android SDK.",
|
|
628
|
+
status: "warn"
|
|
629
|
+
});
|
|
630
|
+
if (!resolution.path) return checks;
|
|
631
|
+
checks.push(await checkVersionDirectories(environment, resolution.path, "platforms", "Android platforms", "android-", "Install an Android SDK platform through Android Studio SDK Manager."));
|
|
632
|
+
checks.push(await checkVersionDirectories(environment, resolution.path, "build-tools", "Build Tools", "", "Install Android SDK Build Tools through Android Studio SDK Manager."));
|
|
633
|
+
checks.push(await checkTool(environment, resolution.path, "emulator", [join(resolution.path, "emulator")], "Emulator", "Install Android Emulator through Android Studio SDK Manager.", "warn"));
|
|
634
|
+
const commandLineDirectories = await commandLineToolDirectories(environment, resolution.path);
|
|
635
|
+
checks.push(await checkTool(environment, resolution.path, "avdmanager", commandLineDirectories, "avdmanager", "Install Android SDK Command-line Tools; avdmanager is needed by solana-mobile emulator create.", "warn"));
|
|
636
|
+
const sdkManagerExecutable = await findExecutable("sdkmanager", environment, commandLineDirectories);
|
|
637
|
+
const sdkManager = await checkTool(environment, resolution.path, "sdkmanager", commandLineDirectories, "sdkmanager", "Install Android SDK Command-line Tools through Android Studio SDK Manager.", "warn", sdkManagerExecutable);
|
|
638
|
+
checks.push(sdkManager);
|
|
639
|
+
checks.push(await checkSdkLicenses(environment, sdkManagerExecutable));
|
|
640
|
+
return checks;
|
|
641
|
+
}
|
|
642
|
+
async function checkSdkLicenses(environment, sdkManagerExecutable) {
|
|
643
|
+
const instruction = `Run: ${sdkManagerExecutable ?? "sdkmanager"} --licenses`;
|
|
644
|
+
if (!sdkManagerExecutable) return {
|
|
645
|
+
actual: "unable to check",
|
|
646
|
+
category: "android-sdk",
|
|
647
|
+
message: "SDK licenses cannot be checked because sdkmanager is unavailable.",
|
|
648
|
+
name: "SDK licenses",
|
|
649
|
+
recommendation: instruction,
|
|
650
|
+
status: "info"
|
|
651
|
+
};
|
|
652
|
+
try {
|
|
653
|
+
const output = await environment.runCommand(sdkManagerExecutable, ["--licenses"]);
|
|
654
|
+
const status = parseSdkLicenseStatus(`${output.stdout}\n${output.stderr}`);
|
|
655
|
+
if (status === "accepted") return {
|
|
656
|
+
actual: "all accepted",
|
|
657
|
+
category: "android-sdk",
|
|
658
|
+
message: "All Android SDK package licenses are accepted.",
|
|
659
|
+
name: "SDK licenses",
|
|
660
|
+
status: "pass"
|
|
661
|
+
};
|
|
662
|
+
return {
|
|
663
|
+
actual: status === "unaccepted" ? "acceptance required" : "unable to verify",
|
|
664
|
+
category: "android-sdk",
|
|
665
|
+
message: "Android SDK package licenses require attention.",
|
|
666
|
+
name: "SDK licenses",
|
|
667
|
+
recommendation: instruction,
|
|
668
|
+
status: status === "unaccepted" ? "warn" : "info"
|
|
669
|
+
};
|
|
670
|
+
} catch {
|
|
671
|
+
return {
|
|
672
|
+
actual: "unable to verify",
|
|
673
|
+
category: "android-sdk",
|
|
674
|
+
message: "Android SDK package license status could not be determined.",
|
|
675
|
+
name: "SDK licenses",
|
|
676
|
+
recommendation: instruction,
|
|
677
|
+
status: "info"
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
function parseSdkLicenseStatus(output) {
|
|
682
|
+
if (/all sdk package licenses accepted/i.test(output)) return "accepted";
|
|
683
|
+
if (/licenses?.*not been accepted|accept\?\s*\(y\/n\)/is.test(output)) return "unaccepted";
|
|
684
|
+
return "unknown";
|
|
685
|
+
}
|
|
686
|
+
async function checkVersionDirectories(environment, sdkRoot, directory, name, prefix, recommendation) {
|
|
687
|
+
const versions = sortVersions((await environment.listDirectory(join(sdkRoot, directory)).catch(() => [])).map((entry) => entry.startsWith(prefix) ? entry.slice(prefix.length) : entry).filter((entry) => /^\d+(?:\.\d+)*$/.test(entry)));
|
|
688
|
+
return {
|
|
689
|
+
actual: versions.length ? name === "Android platforms" ? versions.map((version) => `API ${version}`).join(", ") : versions.at(-1) ?? "" : "none installed",
|
|
690
|
+
category: "android-sdk",
|
|
691
|
+
details: versions.map((version) => `${name}: ${version}`),
|
|
692
|
+
message: versions.length ? `Detected ${versions.length} installed ${name.toLowerCase()} version(s).` : `No ${name} are installed.`,
|
|
693
|
+
name,
|
|
694
|
+
recommendation: versions.length ? void 0 : recommendation,
|
|
695
|
+
required: "at least one installed version",
|
|
696
|
+
status: versions.length ? "pass" : "fail"
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
async function commandLineToolDirectories(environment, sdkRoot) {
|
|
700
|
+
const root = join(sdkRoot, "cmdline-tools");
|
|
701
|
+
return (await environment.listDirectory(root).catch(() => [])).sort().map((entry) => join(root, entry, "bin"));
|
|
702
|
+
}
|
|
703
|
+
async function checkTool(environment, sdkRoot, command, directories, name, recommendation, missingStatus, resolvedExecutable) {
|
|
704
|
+
const executable = resolvedExecutable ?? await findExecutable(command, environment, directories);
|
|
705
|
+
if (!executable) return {
|
|
706
|
+
actual: "not found",
|
|
707
|
+
category: name === "Emulator" ? "emulator" : "android-sdk",
|
|
708
|
+
message: `${name} is not available.`,
|
|
709
|
+
name,
|
|
710
|
+
recommendation,
|
|
711
|
+
status: missingStatus
|
|
712
|
+
};
|
|
713
|
+
let version;
|
|
714
|
+
try {
|
|
715
|
+
const output = await environment.runCommand(executable, ["-version"]);
|
|
716
|
+
version = parseVersion(`${output.stdout}\n${output.stderr}`);
|
|
717
|
+
} catch {
|
|
718
|
+
version = void 0;
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
actual: version ?? "available",
|
|
722
|
+
category: name === "Emulator" ? "emulator" : "android-sdk",
|
|
723
|
+
details: [`Executable: ${executable}`, `SDK root: ${sdkRoot}`],
|
|
724
|
+
message: `${name} is available.`,
|
|
725
|
+
name,
|
|
726
|
+
status: "pass"
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
//#endregion
|
|
730
|
+
//#region src/doctor/data-access/check-java.ts
|
|
731
|
+
function parseJavaVersion(output) {
|
|
732
|
+
const quoted = output.match(/version\s+"([^"]+)"/i)?.[1];
|
|
733
|
+
return quoted ? parseVersion(quoted) : parseVersion(output);
|
|
734
|
+
}
|
|
735
|
+
async function checkJava(environment) {
|
|
736
|
+
const java = await checkJavaExecutable(environment);
|
|
737
|
+
const compiler = await checkJavaCompiler(environment, java.status !== "fail");
|
|
738
|
+
return [
|
|
739
|
+
java,
|
|
740
|
+
compiler,
|
|
741
|
+
await checkJavaHome(environment, java.status !== "fail" && compiler.status !== "fail")
|
|
742
|
+
];
|
|
743
|
+
}
|
|
744
|
+
async function checkJavaExecutable(environment) {
|
|
745
|
+
const executable = await findExecutable("java", environment);
|
|
746
|
+
if (!executable) return javaFailure("Java", "Java is not available.", "Install JDK 17 or higher.");
|
|
747
|
+
try {
|
|
748
|
+
const result = await environment.runCommand(executable, ["-version"]);
|
|
749
|
+
const version = parseJavaVersion(`${result.stdout}\n${result.stderr}`);
|
|
750
|
+
const passes = Boolean(version) && Number.parseInt(version ?? "", 10) >= 17;
|
|
751
|
+
return {
|
|
752
|
+
actual: version ?? "unknown",
|
|
753
|
+
category: "java",
|
|
754
|
+
details: [`Executable: ${executable}`],
|
|
755
|
+
message: version ? `Detected Java ${version}.` : "Unable to parse the Java version.",
|
|
756
|
+
name: "Java",
|
|
757
|
+
recommendation: passes ? void 0 : "Install JDK 17 or higher.",
|
|
758
|
+
required: "17 or higher",
|
|
759
|
+
status: passes ? "pass" : "fail"
|
|
760
|
+
};
|
|
761
|
+
} catch {
|
|
762
|
+
return javaFailure("Java", "Java is not usable.", "Install JDK 17 or higher.");
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
async function checkJavaCompiler(environment, javaAvailable) {
|
|
766
|
+
const executable = await findExecutable("javac", environment);
|
|
767
|
+
if (!executable) return javaFailure("Java compiler", "javac is not available.", "Install a complete JDK 17 or higher, not only a JRE.");
|
|
768
|
+
try {
|
|
769
|
+
const result = await environment.runCommand(executable, ["-version"]);
|
|
770
|
+
const version = parseJavaVersion(`${result.stdout}\n${result.stderr}`);
|
|
771
|
+
return {
|
|
772
|
+
actual: version ?? "available",
|
|
773
|
+
category: "java",
|
|
774
|
+
details: [`Executable: ${executable}`],
|
|
775
|
+
message: `Detected Java compiler ${version ?? ""}.`.trim(),
|
|
776
|
+
name: "Java compiler",
|
|
777
|
+
required: "JDK 17 or higher",
|
|
778
|
+
status: javaAvailable ? "pass" : "fail"
|
|
779
|
+
};
|
|
780
|
+
} catch {
|
|
781
|
+
return javaFailure("Java compiler", "javac is not usable.", "Install a complete JDK 17 or higher.");
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
async function checkJavaHome(environment, toolsAvailable) {
|
|
785
|
+
const configured = environment.environment.JAVA_HOME;
|
|
786
|
+
if (!configured) return {
|
|
787
|
+
actual: "not set",
|
|
788
|
+
category: "java",
|
|
789
|
+
message: "JAVA_HOME is not set.",
|
|
790
|
+
name: "JAVA_HOME",
|
|
791
|
+
recommendation: "Set JAVA_HOME to your JDK path to make Java discovery deterministic.",
|
|
792
|
+
status: toolsAvailable ? "warn" : "fail"
|
|
793
|
+
};
|
|
794
|
+
const resolved = await environment.resolvePath(configured).catch(() => configured);
|
|
795
|
+
const javaPath = join(resolved, "bin", environment.getPlatform() === "win32" ? "java.exe" : "java");
|
|
796
|
+
if (!await environment.pathExists(resolved) || !await environment.pathExists(javaPath)) return {
|
|
797
|
+
actual: resolved,
|
|
798
|
+
category: "java",
|
|
799
|
+
message: "JAVA_HOME does not point to a usable JDK.",
|
|
800
|
+
name: "JAVA_HOME",
|
|
801
|
+
recommendation: environment.getPlatform() === "win32" ? "Set JAVA_HOME to the installed JDK directory in Windows environment variables." : "Set JAVA_HOME to the installed JDK directory.",
|
|
802
|
+
status: "fail"
|
|
803
|
+
};
|
|
804
|
+
return {
|
|
805
|
+
actual: resolved,
|
|
806
|
+
category: "java",
|
|
807
|
+
details: [`Java executable: ${javaPath}`],
|
|
808
|
+
message: `JAVA_HOME resolves to ${resolved}.`,
|
|
809
|
+
name: "JAVA_HOME",
|
|
810
|
+
status: "pass"
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
function javaFailure(name, message, recommendation) {
|
|
814
|
+
return {
|
|
815
|
+
actual: "not found",
|
|
816
|
+
category: "java",
|
|
817
|
+
message,
|
|
818
|
+
name,
|
|
819
|
+
recommendation,
|
|
820
|
+
required: "17 or higher",
|
|
821
|
+
status: "fail"
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
function checkNodeVersion(version = process.version) {
|
|
825
|
+
const normalizedVersion = normalizeNodeVersion(version);
|
|
826
|
+
const majorVersion = Number.parseInt(normalizedVersion, 10);
|
|
827
|
+
const passes = Boolean(normalizedVersion) && !Number.isNaN(majorVersion) && majorVersion >= 22;
|
|
828
|
+
return {
|
|
829
|
+
actual: normalizedVersion || "unknown",
|
|
830
|
+
category: "javascript",
|
|
831
|
+
message: passes ? `Detected Node.js ${normalizedVersion}.` : "Node.js 22 or higher is required.",
|
|
832
|
+
name: "Node.js",
|
|
833
|
+
recommendation: passes ? void 0 : "Install Node.js 22 or higher.",
|
|
834
|
+
required: "22 or higher",
|
|
835
|
+
status: passes ? "pass" : "fail"
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
function normalizeNodeVersion(version) {
|
|
839
|
+
return version.trim().replace(/^v/i, "");
|
|
840
|
+
}
|
|
841
|
+
//#endregion
|
|
842
|
+
//#region src/doctor/data-access/check-system-and-javascript.ts
|
|
843
|
+
const gigabyte = 1024 ** 3;
|
|
844
|
+
function checkOperatingSystem(environment) {
|
|
845
|
+
const names = {
|
|
846
|
+
darwin: "macOS",
|
|
847
|
+
linux: "Linux",
|
|
848
|
+
win32: "Windows"
|
|
849
|
+
};
|
|
850
|
+
const platform = environment.getPlatform();
|
|
851
|
+
const actual = `${names[platform] ?? "Unknown platform"} ${environment.getRelease()} ${environment.architecture}`;
|
|
852
|
+
return {
|
|
853
|
+
actual,
|
|
854
|
+
category: "system",
|
|
855
|
+
message: `Detected ${actual}.`,
|
|
856
|
+
name: "Operating system",
|
|
857
|
+
status: names[platform] ? "pass" : "info"
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
async function checkDiskSpace(environment, path = process.cwd()) {
|
|
861
|
+
try {
|
|
862
|
+
const available = await environment.getDiskSpace(path) / gigabyte;
|
|
863
|
+
const status = available < 10 ? "fail" : available < 20 ? "warn" : "pass";
|
|
864
|
+
return {
|
|
865
|
+
actual: `${Math.floor(available)} GB available`,
|
|
866
|
+
category: "system",
|
|
867
|
+
message: `Detected ${Math.floor(available)} GB of available disk space.`,
|
|
868
|
+
name: "Disk space",
|
|
869
|
+
recommendation: status === "pass" ? void 0 : "Free disk space for Android SDK packages, emulator images, Gradle caches, and project dependencies.",
|
|
870
|
+
required: "10 GB minimum; 20 GB recommended",
|
|
871
|
+
status
|
|
872
|
+
};
|
|
873
|
+
} catch {
|
|
874
|
+
return {
|
|
875
|
+
actual: "unknown",
|
|
876
|
+
category: "system",
|
|
877
|
+
message: "Unable to determine available disk space.",
|
|
878
|
+
name: "Disk space",
|
|
879
|
+
status: "info"
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
const packageManagers = [
|
|
884
|
+
{
|
|
885
|
+
command: "bun",
|
|
886
|
+
label: "Bun"
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
command: "npm",
|
|
890
|
+
label: "npm"
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
command: "pnpm",
|
|
894
|
+
label: "pnpm"
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
command: "yarn",
|
|
898
|
+
label: "Yarn"
|
|
899
|
+
}
|
|
900
|
+
];
|
|
901
|
+
async function checkPackageManagers(environment) {
|
|
902
|
+
const found = (await Promise.all(packageManagers.map(async ({ command, label }) => {
|
|
903
|
+
const executable = await findExecutable(command, environment);
|
|
904
|
+
if (!executable) return void 0;
|
|
905
|
+
try {
|
|
906
|
+
const result = await environment.runCommand(executable, ["--version"]);
|
|
907
|
+
return {
|
|
908
|
+
detail: `${label}: ${executable}`,
|
|
909
|
+
value: `${label} ${parseVersion(result.stdout) ?? "unknown"}`
|
|
910
|
+
};
|
|
911
|
+
} catch {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
}))).filter((value) => Boolean(value));
|
|
915
|
+
return {
|
|
916
|
+
actual: found.length ? found.map(({ value }) => value).join(", ") : "none found",
|
|
917
|
+
category: "javascript",
|
|
918
|
+
details: found.map(({ detail }) => detail),
|
|
919
|
+
message: found.length ? `Detected ${found.length} supported package manager(s).` : "No supported package manager found.",
|
|
920
|
+
name: "Package managers",
|
|
921
|
+
recommendation: found.length ? void 0 : "Install npm, pnpm, Yarn, or Bun.",
|
|
922
|
+
required: "at least one of npm, pnpm, Yarn, or Bun",
|
|
923
|
+
status: found.length ? "pass" : "fail"
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
//#endregion
|
|
927
|
+
//#region src/doctor/ui/doctor-ui-report.ts
|
|
928
|
+
const sections = [
|
|
929
|
+
{
|
|
930
|
+
categories: ["system"],
|
|
931
|
+
checks: ["Operating system", "Disk space"],
|
|
932
|
+
title: "System"
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
categories: ["javascript"],
|
|
936
|
+
checks: ["Node.js", "Package managers"],
|
|
937
|
+
title: "JavaScript"
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
categories: ["java"],
|
|
941
|
+
checks: [
|
|
942
|
+
"Java",
|
|
943
|
+
"Java compiler",
|
|
944
|
+
"JAVA_HOME"
|
|
945
|
+
],
|
|
946
|
+
title: "Java"
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
categories: ["android-sdk"],
|
|
950
|
+
checks: [
|
|
951
|
+
"Android SDK",
|
|
952
|
+
"Android SDK environment",
|
|
953
|
+
"Android platforms",
|
|
954
|
+
"Build Tools",
|
|
955
|
+
"adb",
|
|
956
|
+
"sdkmanager",
|
|
957
|
+
"avdmanager",
|
|
958
|
+
"SDK licenses"
|
|
959
|
+
],
|
|
960
|
+
title: "Android SDK"
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
categories: ["emulator", "device"],
|
|
964
|
+
checks: [
|
|
965
|
+
"Emulator",
|
|
966
|
+
"Android emulators",
|
|
967
|
+
"Running emulators",
|
|
968
|
+
"Emulator acceleration",
|
|
969
|
+
"Physical devices"
|
|
970
|
+
],
|
|
971
|
+
title: "Devices"
|
|
972
|
+
}
|
|
973
|
+
];
|
|
974
|
+
const styles = {
|
|
975
|
+
fail: ["\x1B[31m", "\x1B[39m"],
|
|
976
|
+
heading: ["\x1B[1m", "\x1B[22m"],
|
|
977
|
+
info: ["\x1B[36m", "\x1B[39m"],
|
|
978
|
+
muted: ["\x1B[2m", "\x1B[22m"],
|
|
979
|
+
pass: ["\x1B[32m", "\x1B[39m"],
|
|
980
|
+
warn: ["\x1B[33m", "\x1B[39m"]
|
|
981
|
+
};
|
|
982
|
+
const symbols = {
|
|
983
|
+
fail: "✗",
|
|
984
|
+
info: "•",
|
|
985
|
+
pass: "✓",
|
|
986
|
+
warn: "!"
|
|
987
|
+
};
|
|
988
|
+
function renderDoctorReport(report, verbose = false) {
|
|
989
|
+
process.stdout.write(`${formatDoctorReport(report, verbose, Boolean(process.stdout.isTTY))}\n`);
|
|
990
|
+
}
|
|
991
|
+
function formatDoctorReport(report, verbose = false, color = false) {
|
|
992
|
+
const output = [paint("Solana Mobile Doctor", "heading", color)];
|
|
993
|
+
for (const section of sections) {
|
|
994
|
+
const checks = report.checks.filter(({ category }) => section.categories.includes(category)).sort((left, right) => checkOrder(section.checks, left.name) - checkOrder(section.checks, right.name));
|
|
995
|
+
if (!checks.length) continue;
|
|
996
|
+
output.push("", paint(section.title, "heading", color), ...formatChecks(checks, verbose, color));
|
|
997
|
+
}
|
|
998
|
+
const readiness = [
|
|
999
|
+
capability("Project creation", report.capabilities.projectCreation),
|
|
1000
|
+
capability("Android build", report.capabilities.androidBuild),
|
|
1001
|
+
capability("Emulator workflow", report.capabilities.emulator),
|
|
1002
|
+
capability("Physical device workflow", report.capabilities.physicalDevice)
|
|
1003
|
+
];
|
|
1004
|
+
output.push("", paint("Readiness", "heading", color), ...formatChecks(readiness, false, color));
|
|
1005
|
+
if (report.recommendations.length) {
|
|
1006
|
+
output.push("", paint("Recommendations", "heading", color));
|
|
1007
|
+
for (const { name, recommendation } of recommendationChecks(report)) output.push(` ${paint("!", "warn", color)} ${name}: ${recommendation}`);
|
|
1008
|
+
}
|
|
1009
|
+
const summaryStatus = report.ready ? "pass" : report.capabilities.projectCreation ? "warn" : "fail";
|
|
1010
|
+
output.push("", `${paint(symbols[summaryStatus], summaryStatus, color)} ${finalMessage(report)}`);
|
|
1011
|
+
return output.join("\n");
|
|
1012
|
+
}
|
|
1013
|
+
function capability(name, ready) {
|
|
1014
|
+
return {
|
|
1015
|
+
actual: ready ? "ready" : "unavailable",
|
|
1016
|
+
category: "system",
|
|
1017
|
+
message: "",
|
|
1018
|
+
name,
|
|
1019
|
+
status: ready ? "pass" : "info"
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
function checkOrder(order, name) {
|
|
1023
|
+
const index = order.indexOf(name);
|
|
1024
|
+
return index === -1 ? order.length : index;
|
|
1025
|
+
}
|
|
1026
|
+
function finalMessage(report) {
|
|
1027
|
+
if (report.capabilities.projectCreation && report.capabilities.androidBuild) return "Ready for Solana Mobile development.";
|
|
1028
|
+
if (report.capabilities.projectCreation) return "Project creation is ready; the Android environment needs attention.";
|
|
1029
|
+
return "Not ready for Solana Mobile development.";
|
|
1030
|
+
}
|
|
1031
|
+
function formatChecks(checks, verbose, color) {
|
|
1032
|
+
const labelWidth = Math.max(...checks.map(({ name }) => name.length));
|
|
1033
|
+
return checks.flatMap((check) => {
|
|
1034
|
+
const requirement = check.status === "pass" || !check.required ? "" : ` (requires ${check.required})`;
|
|
1035
|
+
const line = ` ${paint(symbols[check.status], check.status, color)} ${check.name.padEnd(labelWidth)} ${check.actual}${requirement}`;
|
|
1036
|
+
if (!verbose || !check.details?.length) return [line];
|
|
1037
|
+
return [line, ...check.details.map((detail) => ` ${" ".repeat(labelWidth)} ${paint(detail, "muted", color)}`)];
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
function paint(value, style, color) {
|
|
1041
|
+
if (!color) return value;
|
|
1042
|
+
const [open, close] = styles[style];
|
|
1043
|
+
return `${open}${value}${close}`;
|
|
1044
|
+
}
|
|
1045
|
+
function recommendationChecks(report) {
|
|
1046
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1047
|
+
return report.checks.filter(({ recommendation }) => {
|
|
1048
|
+
if (!recommendation || seen.has(recommendation)) return false;
|
|
1049
|
+
seen.add(recommendation);
|
|
1050
|
+
return true;
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
//#endregion
|
|
1054
|
+
//#region src/doctor/doctor-feature-index.ts
|
|
1055
|
+
async function createDoctorReport(environment = defaultDoctorEnvironment) {
|
|
1056
|
+
const [disk, packageManagers, java] = await Promise.all([
|
|
1057
|
+
checkDiskSpace(environment),
|
|
1058
|
+
checkPackageManagers(environment),
|
|
1059
|
+
checkJava(environment)
|
|
1060
|
+
]);
|
|
1061
|
+
const sdkResolution = await resolveAndroidSdk(environment);
|
|
1062
|
+
const [sdkChecks, adb, acceleration] = await Promise.all([
|
|
1063
|
+
checkAndroidSdk(environment, sdkResolution),
|
|
1064
|
+
checkAdbVersion(environment, sdkResolution.path),
|
|
1065
|
+
checkEmulatorAcceleration(environment)
|
|
1066
|
+
]);
|
|
1067
|
+
const deviceChecks = await checkAndroidDevices(environment, adb.status === "pass", sdkResolution.path);
|
|
1068
|
+
return buildDoctorReport([
|
|
1069
|
+
checkOperatingSystem(environment),
|
|
1070
|
+
disk,
|
|
1071
|
+
checkNodeVersion(),
|
|
1072
|
+
packageManagers,
|
|
1073
|
+
...java,
|
|
1074
|
+
...sdkChecks,
|
|
1075
|
+
adb,
|
|
1076
|
+
acceleration,
|
|
1077
|
+
...deviceChecks
|
|
1078
|
+
]);
|
|
1079
|
+
}
|
|
1080
|
+
function buildDoctorReport(checks) {
|
|
1081
|
+
const capabilities = deriveCapabilities(checks);
|
|
1082
|
+
return {
|
|
1083
|
+
capabilities,
|
|
1084
|
+
checks,
|
|
1085
|
+
ready: capabilities.projectCreation && capabilities.androidBuild,
|
|
1086
|
+
recommendations: [...new Set(checks.flatMap(({ recommendation }) => recommendation ? [recommendation] : []))]
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
function deriveCapabilities(checks) {
|
|
1090
|
+
const passes = (name) => checks.some((check) => check.name === name && check.status === "pass");
|
|
1091
|
+
const androidBuild = [
|
|
1092
|
+
"Android SDK",
|
|
1093
|
+
"Android platforms",
|
|
1094
|
+
"Build Tools",
|
|
1095
|
+
"Java",
|
|
1096
|
+
"Java compiler",
|
|
1097
|
+
"adb"
|
|
1098
|
+
].every(passes);
|
|
1099
|
+
const projectCreation = ["Node.js", "Package managers"].every(passes);
|
|
1100
|
+
return {
|
|
1101
|
+
androidBuild,
|
|
1102
|
+
emulator: androidBuild && [
|
|
1103
|
+
"Android emulators",
|
|
1104
|
+
"Emulator",
|
|
1105
|
+
"avdmanager"
|
|
1106
|
+
].every(passes),
|
|
1107
|
+
physicalDevice: androidBuild && passes("Physical devices"),
|
|
1108
|
+
projectCreation
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
async function runDoctor(options = {}) {
|
|
1112
|
+
const report = await createDoctorReport();
|
|
1113
|
+
if (options.json) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
1114
|
+
else renderDoctorReport(report, options.verbose);
|
|
1115
|
+
return getDoctorExitCode(report);
|
|
1116
|
+
}
|
|
1117
|
+
function getDoctorExitCode(report) {
|
|
1118
|
+
return report.checks.some(({ status }) => status === "fail") ? 1 : 0;
|
|
1119
|
+
}
|
|
1120
|
+
//#endregion
|
|
407
1121
|
//#region src/emulator/data-access/run-executable.ts
|
|
408
1122
|
async function runExecutable(cmd, options = {}) {
|
|
409
1123
|
return new Promise((resolve, reject) => {
|
|
@@ -435,7 +1149,7 @@ async function runExecutable(cmd, options = {}) {
|
|
|
435
1149
|
}
|
|
436
1150
|
//#endregion
|
|
437
1151
|
//#region src/emulator/data-access/create-avd.ts
|
|
438
|
-
async function createAvd(options, { getHomeDirectory = homedir, pathExists = defaultPathExists(), readTextFile = defaultReadTextFile
|
|
1152
|
+
async function createAvd(options, { getHomeDirectory = homedir, pathExists = defaultPathExists(), readTextFile = defaultReadTextFile, runCommand = runExecutable, writeTextFile = defaultWriteTextFile } = {}) {
|
|
439
1153
|
const resolvedOptions = resolveCreateOptions(options);
|
|
440
1154
|
const toolPaths = getToolPaths(resolvedOptions.sdkRoot);
|
|
441
1155
|
const homeDirectory = getHomeDirectory();
|
|
@@ -504,49 +1218,13 @@ function getAvdConfigPath(homeDirectory, avdName) {
|
|
|
504
1218
|
function getSystemImageDirectory(sdkRoot, systemImage) {
|
|
505
1219
|
return join(sdkRoot, ...systemImage.split(";"));
|
|
506
1220
|
}
|
|
507
|
-
async function defaultReadTextFile
|
|
1221
|
+
async function defaultReadTextFile(filePath) {
|
|
508
1222
|
return readFile(filePath, "utf8");
|
|
509
1223
|
}
|
|
510
1224
|
async function isInstalledSystemImage(sdkRoot, systemImage, pathExists) {
|
|
511
1225
|
return pathExists(join(getSystemImageDirectory(sdkRoot, systemImage), "source.properties"));
|
|
512
1226
|
}
|
|
513
1227
|
//#endregion
|
|
514
|
-
//#region src/emulator/data-access/list-installed-avds.ts
|
|
515
|
-
async function defaultReadDirectory(directoryPath) {
|
|
516
|
-
return readdir(directoryPath, { withFileTypes: true });
|
|
517
|
-
}
|
|
518
|
-
async function defaultReadTextFile(filePath) {
|
|
519
|
-
return readFile(filePath, "utf8");
|
|
520
|
-
}
|
|
521
|
-
async function listInstalledAvds({ getHomeDirectory = homedir, readDirectory = defaultReadDirectory, readTextFile = defaultReadTextFile } = {}) {
|
|
522
|
-
const avdRootDirectory = join(getHomeDirectory(), ".android", "avd");
|
|
523
|
-
const avdEntryNames = await listEntryNames(avdRootDirectory, readDirectory);
|
|
524
|
-
const registeredAvdNames = new Set(avdEntryNames.filter((name) => name.endsWith(".ini")).map((name) => name.slice(0, -4)));
|
|
525
|
-
const avdDirectoryNames = avdEntryNames.filter((name) => name.endsWith(".avd") && registeredAvdNames.has(name.slice(0, -4)));
|
|
526
|
-
return (await Promise.all(avdDirectoryNames.map(async (directoryName) => {
|
|
527
|
-
const configPath = join(avdRootDirectory, directoryName, "config.ini");
|
|
528
|
-
const name = directoryName.slice(0, -4);
|
|
529
|
-
let configValues = {};
|
|
530
|
-
try {
|
|
531
|
-
configValues = parseAvdConfig(await readTextFile(configPath));
|
|
532
|
-
} catch {
|
|
533
|
-
configValues = {};
|
|
534
|
-
}
|
|
535
|
-
return {
|
|
536
|
-
device: configValues["hw.device.name"],
|
|
537
|
-
name,
|
|
538
|
-
target: configValues.target
|
|
539
|
-
};
|
|
540
|
-
}))).sort((left, right) => left.name.localeCompare(right.name));
|
|
541
|
-
}
|
|
542
|
-
async function listEntryNames(directoryPath, readDirectory) {
|
|
543
|
-
try {
|
|
544
|
-
return (await readDirectory(directoryPath)).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
|
|
545
|
-
} catch {
|
|
546
|
-
return [];
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
//#endregion
|
|
550
1228
|
//#region src/emulator/data-access/start-emulator.ts
|
|
551
1229
|
async function defaultStartProcess(cmd) {
|
|
552
1230
|
await new Promise((resolve, reject) => {
|
|
@@ -592,7 +1270,7 @@ async function promptEmulatorName(defaultName = DEFAULT_PROFILE.name, runText =
|
|
|
592
1270
|
}
|
|
593
1271
|
//#endregion
|
|
594
1272
|
//#region src/emulator/emulator-feature-create.ts
|
|
595
|
-
async function runEmulatorCreate(options = {}, { getHomeDirectory = homedir, pathExists = defaultPathExists(), readDirectory = defaultReadDirectory, readTextFile = defaultReadTextFile, runCommand = runExecutable, runText, startProcess = defaultStartProcess, writeTextFile = defaultWriteTextFile } = {}) {
|
|
1273
|
+
async function runEmulatorCreate(options = {}, { getHomeDirectory = homedir, pathExists = defaultPathExists(), readDirectory = defaultReadDirectory, readTextFile = defaultReadTextFile$1, runCommand = runExecutable, runText, startProcess = defaultStartProcess, writeTextFile = defaultWriteTextFile } = {}) {
|
|
596
1274
|
const profile = resolveEmulatorProfile(options.profile);
|
|
597
1275
|
const name = options.name ?? await promptEmulatorName(profile.name, runText);
|
|
598
1276
|
if (!name) return;
|
|
@@ -683,7 +1361,7 @@ async function selectInstalledEmulatorNames(avds, runMultiselect = multiselect)
|
|
|
683
1361
|
}
|
|
684
1362
|
//#endregion
|
|
685
1363
|
//#region src/emulator/emulator-feature-delete.ts
|
|
686
|
-
async function runEmulatorDelete(options, { getHomeDirectory = homedir, readDirectory = defaultReadDirectory, readTextFile = defaultReadTextFile, runCommand = runExecutable, runMultiselect } = {}) {
|
|
1364
|
+
async function runEmulatorDelete(options, { getHomeDirectory = homedir, readDirectory = defaultReadDirectory, readTextFile = defaultReadTextFile$1, runCommand = runExecutable, runMultiselect } = {}) {
|
|
687
1365
|
const names = options.names && options.names.length > 0 ? options.names : await selectInstalledEmulatorNames(await listInstalledAvds({
|
|
688
1366
|
getHomeDirectory,
|
|
689
1367
|
readDirectory,
|
|
@@ -717,7 +1395,7 @@ async function runEmulatorList(_options = {}) {
|
|
|
717
1395
|
}
|
|
718
1396
|
//#endregion
|
|
719
1397
|
//#region src/emulator/emulator-feature-start.ts
|
|
720
|
-
async function runEmulatorStart(options = {}, { getHomeDirectory = homedir, readDirectory = defaultReadDirectory, readTextFile = defaultReadTextFile, runSelect, startProcess = defaultStartProcess } = {}) {
|
|
1398
|
+
async function runEmulatorStart(options = {}, { getHomeDirectory = homedir, readDirectory = defaultReadDirectory, readTextFile = defaultReadTextFile$1, runSelect, startProcess = defaultStartProcess } = {}) {
|
|
721
1399
|
const name = options.name ?? await selectInstalledEmulatorName(await listInstalledAvds({
|
|
722
1400
|
getHomeDirectory,
|
|
723
1401
|
readDirectory,
|
|
@@ -932,8 +1610,8 @@ function createApp$1({ runEmulatorCreate: runEmulatorCreateCommand = runEmulator
|
|
|
932
1610
|
template: options.template ?? (options.minimal ? "kit-expo-minimal" : void 0)
|
|
933
1611
|
});
|
|
934
1612
|
});
|
|
935
|
-
app.command("doctor").description("Check local development dependencies").action(async () => {
|
|
936
|
-
process.exitCode = await runDoctorCommand();
|
|
1613
|
+
app.command("doctor").description("Check local development dependencies").option("--json", "Print a stable JSON report").option("--verbose", "Include resolved paths and diagnostic details").action(async (options) => {
|
|
1614
|
+
process.exitCode = await runDoctorCommand(options);
|
|
937
1615
|
});
|
|
938
1616
|
const emulatorCommand = app.command("emulator").alias("emu").description("Manage Android emulators");
|
|
939
1617
|
emulatorCommand.action(() => {
|