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/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 { promisify } from "node:util";
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.1"
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(runCommand = runExecutable$1) {
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 version = parseAdbVersion(await runCommand("adb", ["version"]));
210
- const majorVersion = version ? Number.parseInt(version, 10) : NaN;
211
- if (!version || Number.isNaN(majorVersion)) return {
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
- message: `Detected adb ${version}.`,
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
- ok: majorVersion >= minimumAdbMajorVersion,
224
- recommendation: majorVersion >= minimumAdbMajorVersion ? void 0 : "Update Android SDK Platform Tools to version 33 or higher.",
225
- required: "33 or higher"
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
- async function runExecutable$1(command, args) {
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: normalizedVersion,
261
- message: `Detected Node.js ${normalizedVersion}.`,
262
- name: "node",
263
- ok: majorVersion >= minimumNodeMajorVersion,
264
- recommendation: majorVersion >= minimumNodeMajorVersion ? void 0 : "Install Node.js 22 or higher.",
265
- required: "22 or higher"
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$1, runCommand = runExecutable, writeTextFile = defaultWriteTextFile } = {}) {
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$1(filePath) {
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(() => {