create-vidra-app 0.1.1 → 0.1.3

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 CHANGED
@@ -26,13 +26,19 @@ This package also installs the `vidra` CLI used by scaffolded apps:
26
26
  vidra dev # start Vite + native host
27
27
  vidra dev --target windows # run the Windows host
28
28
  vidra build --target macos # build + package a macOS .dmg
29
+ vidra doctor # check your .NET / MAUI / Xcode setup
29
30
  ```
30
31
 
31
32
  ## Prerequisites
32
33
 
33
- - .NET 10 SDK with the MAUI workload
34
+ - .NET 10 SDK
35
+ - The .NET MAUI workload: `dotnet workload install maui`
34
36
  - Node.js 18+
35
- - Windows targets must be built on Windows; macOS targets on macOS
37
+ - macOS targets require Xcode; Windows targets must be built on Windows
38
+
39
+ If the MAUI workload is missing, `create-vidra-app` will detect it after
40
+ scaffolding and offer to install it for you. You can re-check at any time with
41
+ `vidra doctor`.
36
42
 
37
43
  ## License
38
44
 
package/dist/cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  // src/cli.ts
2
- import chalk5 from "chalk";
2
+ import chalk6 from "chalk";
3
3
 
4
4
  // src/commands/build.ts
5
5
  import path5 from "path";
6
6
  import fs4 from "fs-extra";
7
7
  import { execSync as execSync2 } from "child_process";
8
- import chalk3 from "chalk";
8
+ import chalk4 from "chalk";
9
9
 
10
10
  // src/utils.ts
11
11
  var parseArgs = (argv) => {
@@ -236,6 +236,250 @@ var windowsTarget = {
236
236
  }
237
237
  };
238
238
 
239
+ // src/doctor.ts
240
+ import { execFileSync as execFileSync2 } from "child_process";
241
+ import prompts from "prompts";
242
+ import chalk3 from "chalk";
243
+ var DOTNET = process.platform === "win32" ? "dotnet.exe" : "dotnet";
244
+ var MAUI_DOCS = "https://learn.microsoft.com/dotnet/maui/get-started/installation";
245
+ var bufToStr = (v) => v == null ? "" : Buffer.isBuffer(v) ? v.toString() : v;
246
+ var run = (cmd, args) => {
247
+ try {
248
+ const stdout = execFileSync2(cmd, args, {
249
+ encoding: "utf-8",
250
+ stdio: ["ignore", "pipe", "pipe"]
251
+ });
252
+ return { found: true, ok: true, stdout: stdout ?? "", stderr: "" };
253
+ } catch (e) {
254
+ const err = e;
255
+ return {
256
+ found: err.code !== "ENOENT",
257
+ ok: false,
258
+ stdout: bufToStr(err.stdout),
259
+ stderr: bufToStr(err.stderr)
260
+ };
261
+ }
262
+ };
263
+ var hasNet10Sdk = (listSdksOutput) => listSdksOutput.split(/\r?\n/).some((line) => /^10\./.test(line.trim()));
264
+ var newestNet10Sdk = (listSdksOutput) => listSdksOutput.split(/\r?\n/).map((line) => line.trim().split(/\s+/)[0]).filter((v) => /^10\./.test(v)).sort(
265
+ (a, b) => a.localeCompare(b, void 0, { numeric: true, sensitivity: "base" })
266
+ ).pop();
267
+ var outputMentionsMaui = (workloadListOutput) => /\bmaui\b/i.test(workloadListOutput);
268
+ var looksLikeMissingWorkload = (output) => [
269
+ /NETSDK1147/i,
270
+ /workloads?\s+must\s+be\s+installed/i,
271
+ /maui-maccatalyst/i,
272
+ /maui-windows/i,
273
+ /to\s+install\s+the\s+.*workload/i
274
+ ].some((re) => re.test(output));
275
+ var checkDotnetSdk = () => {
276
+ const res = run(DOTNET, ["--list-sdks"]);
277
+ if (!res.found) {
278
+ return {
279
+ name: ".NET SDK",
280
+ status: "missing",
281
+ detail: "`dotnet` was not found on your PATH",
282
+ fix: "Install the .NET 10 SDK \u2014 https://dotnet.microsoft.com/download"
283
+ };
284
+ }
285
+ if (!res.ok && !res.stdout) {
286
+ return {
287
+ name: ".NET SDK",
288
+ status: "unknown",
289
+ detail: "could not run `dotnet --list-sdks`"
290
+ };
291
+ }
292
+ if (hasNet10Sdk(res.stdout)) {
293
+ const newest = newestNet10Sdk(res.stdout);
294
+ return {
295
+ name: ".NET SDK",
296
+ status: "ok",
297
+ detail: newest ? `found ${newest}` : "found 10.x"
298
+ };
299
+ }
300
+ return {
301
+ name: ".NET SDK",
302
+ status: "missing",
303
+ detail: "no 10.x SDK installed",
304
+ fix: "Install the .NET 10 SDK \u2014 https://dotnet.microsoft.com/download"
305
+ };
306
+ };
307
+ var checkMauiWorkload = (dotnetOk) => {
308
+ if (!dotnetOk) {
309
+ return {
310
+ name: ".NET MAUI workload",
311
+ status: "unknown",
312
+ detail: "requires the .NET SDK first"
313
+ };
314
+ }
315
+ const res = run(DOTNET, ["workload", "list"]);
316
+ if (!res.found) {
317
+ return {
318
+ name: ".NET MAUI workload",
319
+ status: "unknown",
320
+ detail: "could not query workloads"
321
+ };
322
+ }
323
+ if (outputMentionsMaui(res.stdout)) {
324
+ return { name: ".NET MAUI workload", status: "ok", detail: "installed" };
325
+ }
326
+ return {
327
+ name: ".NET MAUI workload",
328
+ status: "missing",
329
+ detail: "not installed",
330
+ fix: "dotnet workload install maui"
331
+ };
332
+ };
333
+ var checkXcode = () => {
334
+ const res = run("xcode-select", ["-p"]);
335
+ if (!res.found || !res.ok) {
336
+ return {
337
+ name: "Xcode",
338
+ status: "missing",
339
+ detail: "not found",
340
+ fix: "Install Xcode from the App Store"
341
+ };
342
+ }
343
+ const devDir = res.stdout.trim();
344
+ if (/CommandLineTools/i.test(devDir)) {
345
+ return {
346
+ name: "Xcode",
347
+ status: "missing",
348
+ detail: "only Command Line Tools detected (Mac Catalyst needs full Xcode)",
349
+ fix: "Install Xcode, then: sudo xcode-select -s /Applications/Xcode.app"
350
+ };
351
+ }
352
+ return { name: "Xcode", status: "ok", detail: devDir };
353
+ };
354
+ var isMauiWorkloadInstalled = () => outputMentionsMaui(run(DOTNET, ["workload", "list"]).stdout);
355
+ var isInteractive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
356
+ var collectRequirements = (opts = {}) => {
357
+ const dotnet = checkDotnetSdk();
358
+ const reqs = [dotnet, checkMauiWorkload(dotnet.status === "ok")];
359
+ if (opts.includeXcode ?? process.platform === "darwin") {
360
+ reqs.push(checkXcode());
361
+ }
362
+ return reqs;
363
+ };
364
+ var printRequirements = (reqs) => {
365
+ for (const r of reqs) {
366
+ const icon = r.status === "ok" ? chalk3.green("\u2713") : r.status === "missing" ? chalk3.red("\u2717") : chalk3.yellow("?");
367
+ const detail = r.detail ? ` ${chalk3.dim(`(${r.detail})`)}` : "";
368
+ console.log(` ${icon} ${r.name}${detail}`);
369
+ if (r.status === "missing" && r.fix) {
370
+ console.log(` ${chalk3.dim("fix:")} ${chalk3.cyan(r.fix)}`);
371
+ }
372
+ }
373
+ };
374
+ var runDoctor = async () => {
375
+ console.log();
376
+ console.log(` ${chalk3.bold.cyan("vidra doctor")}`);
377
+ console.log();
378
+ const reqs = collectRequirements();
379
+ printRequirements(reqs);
380
+ console.log();
381
+ const missing = reqs.filter((r) => r.status === "missing");
382
+ if (missing.length === 0) {
383
+ console.log(
384
+ ` ${chalk3.green("All checks passed.")} You're ready to run ${chalk3.cyan(
385
+ "vidra dev"
386
+ )}.`
387
+ );
388
+ console.log();
389
+ return 0;
390
+ }
391
+ console.log(
392
+ ` ${chalk3.yellow(
393
+ `${missing.length} issue(s) found.`
394
+ )} Apply the fixes above, then re-run ${chalk3.cyan("vidra doctor")}.`
395
+ );
396
+ console.log();
397
+ return 1;
398
+ };
399
+ var installWorkload = (csprojPath) => {
400
+ const args = csprojPath ? ["workload", "restore", csprojPath] : ["workload", "install", "maui"];
401
+ console.log();
402
+ console.log(` ${chalk3.dim(`Running: ${DOTNET} ${args.join(" ")}`)}`);
403
+ console.log(
404
+ ` ${chalk3.dim(
405
+ "This can download several hundred MB and take a few minutes."
406
+ )}`
407
+ );
408
+ console.log();
409
+ try {
410
+ execFileSync2(DOTNET, args, { stdio: "inherit" });
411
+ return true;
412
+ } catch {
413
+ console.error();
414
+ console.error(` ${chalk3.red("Workload install failed.")}`);
415
+ console.error(
416
+ ` ${chalk3.dim(
417
+ "If this is a permissions error, your SDK is in a system location and needs elevation:"
418
+ )}`
419
+ );
420
+ console.error(` ${chalk3.cyan("sudo dotnet workload install maui")}`);
421
+ console.error();
422
+ return false;
423
+ }
424
+ };
425
+ var ensureMauiWorkload = async (opts = {}) => {
426
+ const dotnet = checkDotnetSdk();
427
+ if (dotnet.status === "missing") {
428
+ console.log();
429
+ console.log(` ${chalk3.yellow("!")} ${dotnet.name} \u2014 ${dotnet.detail}`);
430
+ if (dotnet.fix) {
431
+ console.log(` ${chalk3.dim("fix:")} ${chalk3.cyan(dotnet.fix)}`);
432
+ }
433
+ return false;
434
+ }
435
+ if (dotnet.status === "unknown") return true;
436
+ if (isMauiWorkloadInstalled()) return true;
437
+ console.log();
438
+ console.log(
439
+ ` ${chalk3.yellow("!")} The .NET MAUI workload is required but not installed.`
440
+ );
441
+ const interactive = opts.interactive ?? isInteractive();
442
+ if (interactive) {
443
+ let install = false;
444
+ try {
445
+ const res = await prompts({
446
+ type: "confirm",
447
+ name: "install",
448
+ message: "Install the .NET MAUI workload now?",
449
+ initial: true
450
+ });
451
+ install = Boolean(res.install);
452
+ } catch {
453
+ install = false;
454
+ }
455
+ if (install) {
456
+ if (installWorkload(opts.csprojPath) && isMauiWorkloadInstalled()) {
457
+ console.log(` ${chalk3.green("\u2713")} MAUI workload installed.`);
458
+ return true;
459
+ }
460
+ return false;
461
+ }
462
+ }
463
+ console.log(
464
+ ` ${chalk3.dim("run:")} ${chalk3.cyan("dotnet workload install maui")}`
465
+ );
466
+ console.log(` ${chalk3.dim("docs:")} ${chalk3.cyan(MAUI_DOCS)}`);
467
+ return false;
468
+ };
469
+ var printWorkloadHint = () => {
470
+ console.error();
471
+ console.error(
472
+ ` ${chalk3.yellow("This looks like a missing .NET MAUI workload.")}`
473
+ );
474
+ console.error(
475
+ ` ${chalk3.dim("fix: ")} ${chalk3.cyan("dotnet workload install maui")}`
476
+ );
477
+ console.error(
478
+ ` ${chalk3.dim("check:")} ${chalk3.cyan("vidra doctor")}`
479
+ );
480
+ console.error();
481
+ };
482
+
239
483
  // src/commands/build.ts
240
484
  var VERSION = "0.1.0";
241
485
  var TARGETS = {
@@ -248,35 +492,38 @@ var buildCommand = async (argv) => {
248
492
  const targetName = args["target"] || detectPlatform();
249
493
  console.log();
250
494
  console.log(
251
- ` ${chalk3.bold.cyan("vidra build")} ${chalk3.dim(`v${VERSION}`)}`
495
+ ` ${chalk4.bold.cyan("vidra build")} ${chalk4.dim(`v${VERSION}`)}`
252
496
  );
253
497
  console.log();
254
498
  const target = TARGETS[targetName];
255
499
  if (!target) {
256
500
  const supported = Object.keys(TARGETS).join(", ");
257
501
  console.error(
258
- chalk3.red(
502
+ chalk4.red(
259
503
  ` Unsupported target: ${targetName}. Supported: ${supported}`
260
504
  )
261
505
  );
262
506
  process.exit(1);
263
507
  }
264
508
  const project = detectProject(process.cwd());
265
- console.log(` ${chalk3.dim("Project:")} ${chalk3.cyan(project.projectName)}`);
266
- console.log(` ${chalk3.dim("Target:")} ${chalk3.cyan(target.name)} (${target.framework})`);
509
+ console.log(` ${chalk4.dim("Project:")} ${chalk4.cyan(project.projectName)}`);
510
+ console.log(` ${chalk4.dim("Target:")} ${chalk4.cyan(target.name)} (${target.framework})`);
267
511
  console.log();
512
+ if (!await ensureMauiWorkload({ csprojPath: project.csprojPath })) {
513
+ process.exit(1);
514
+ }
268
515
  stepBuildUi(project, verbose);
269
516
  stepCopyAssets(project);
270
517
  const publishDir = stepDotnetPublish(project, target, verbose);
271
518
  const bundlePath = target.findBundle(publishDir, project.projectName);
272
519
  if (!bundlePath) {
273
520
  console.error(
274
- chalk3.red(` Could not find build artifact in ${publishDir}`)
521
+ chalk4.red(` Could not find build artifact in ${publishDir}`)
275
522
  );
276
523
  process.exit(1);
277
524
  }
278
525
  console.log(
279
- ` ${chalk3.dim("Bundle:")} ${chalk3.cyan(path5.basename(bundlePath))}`
526
+ ` ${chalk4.dim("Bundle:")} ${chalk4.cyan(path5.basename(bundlePath))}`
280
527
  );
281
528
  if (target.name === "macos") {
282
529
  signMacAppBundleIfPossible(bundlePath, {
@@ -288,7 +535,7 @@ var buildCommand = async (argv) => {
288
535
  const outputDir = path5.join(project.root, "dist");
289
536
  fs4.ensureDirSync(outputDir);
290
537
  console.log();
291
- console.log(` ${chalk3.dim(`Packaging for ${target.name}...`)}`);
538
+ console.log(` ${chalk4.dim(`Packaging for ${target.name}...`)}`);
292
539
  const startPkg = Date.now();
293
540
  let outputPath;
294
541
  try {
@@ -298,24 +545,24 @@ var buildCommand = async (argv) => {
298
545
  });
299
546
  } catch (e) {
300
547
  const err = e;
301
- console.error(chalk3.red(` Packaging failed.`));
302
- console.error(chalk3.dim(err.stderr?.toString() || err.message));
548
+ console.error(chalk4.red(` Packaging failed.`));
549
+ console.error(chalk4.dim(err.stderr?.toString() || err.message));
303
550
  process.exit(1);
304
551
  }
305
552
  const pkgTime = ((Date.now() - startPkg) / 1e3).toFixed(1);
306
553
  const sizeBytes = fs4.statSync(outputPath).size;
307
554
  const sizeMB = (sizeBytes / (1024 * 1024)).toFixed(1);
308
555
  console.log(
309
- ` ${chalk3.green(">")} ${path5.basename(outputPath)} ${chalk3.dim(`(${sizeMB} MB, ${pkgTime}s)`)}`
556
+ ` ${chalk4.green(">")} ${path5.basename(outputPath)} ${chalk4.dim(`(${sizeMB} MB, ${pkgTime}s)`)}`
310
557
  );
311
558
  console.log();
312
559
  console.log(
313
- ` ${chalk3.green("Done!")} Output: ${chalk3.cyan(path5.relative(project.root, outputPath))}`
560
+ ` ${chalk4.green("Done!")} Output: ${chalk4.cyan(path5.relative(project.root, outputPath))}`
314
561
  );
315
562
  console.log();
316
563
  };
317
564
  var stepBuildUi = (project, verbose) => {
318
- console.log(` ${chalk3.dim("Building UI...")}`);
565
+ console.log(` ${chalk4.dim("Building UI...")}`);
319
566
  const start = Date.now();
320
567
  try {
321
568
  execSync2("npm run build", {
@@ -324,19 +571,19 @@ var stepBuildUi = (project, verbose) => {
324
571
  });
325
572
  } catch (e) {
326
573
  const err = e;
327
- console.error(chalk3.red(" Vite build failed."));
328
- console.error(chalk3.dim(err.stderr?.toString() || err.message));
574
+ console.error(chalk4.red(" Vite build failed."));
575
+ console.error(chalk4.dim(err.stderr?.toString() || err.message));
329
576
  process.exit(1);
330
577
  }
331
578
  const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
332
- console.log(` ${chalk3.green(">")} Vite build complete ${chalk3.dim(`(${elapsed}s)`)}`);
579
+ console.log(` ${chalk4.green(">")} Vite build complete ${chalk4.dim(`(${elapsed}s)`)}`);
333
580
  console.log();
334
581
  };
335
582
  var stepCopyAssets = (project) => {
336
- console.log(` ${chalk3.dim("Copying assets to host project...")}`);
583
+ console.log(` ${chalk4.dim("Copying assets to host project...")}`);
337
584
  const viteDist = path5.join(project.uiDir, "dist");
338
585
  if (!fs4.existsSync(viteDist)) {
339
- console.error(chalk3.red(` ui/dist not found. Vite build may have failed.`));
586
+ console.error(chalk4.red(` ui/dist not found. Vite build may have failed.`));
340
587
  process.exit(1);
341
588
  }
342
589
  const wwwroot = path5.join(project.hostDir, "Resources", "Raw", "wwwroot");
@@ -344,7 +591,7 @@ var stepCopyAssets = (project) => {
344
591
  fs4.copySync(viteDist, wwwroot);
345
592
  const fileCount = countFiles(wwwroot);
346
593
  console.log(
347
- ` ${chalk3.green(">")} ${fileCount} files -> ${chalk3.dim("Resources/Raw/wwwroot/")}`
594
+ ` ${chalk4.green(">")} ${fileCount} files -> ${chalk4.dim("Resources/Raw/wwwroot/")}`
348
595
  );
349
596
  console.log();
350
597
  };
@@ -361,7 +608,7 @@ var countFiles = (dir) => {
361
608
  };
362
609
  var stepDotnetPublish = (project, target, verbose) => {
363
610
  console.log(
364
- ` ${chalk3.dim(`Publishing .NET host (${target.framework})...`)}`
611
+ ` ${chalk4.dim(`Publishing .NET host (${target.framework})...`)}`
365
612
  );
366
613
  const start = Date.now();
367
614
  const extraArgs = target.extraPublishArgs ?? "-p:CreatePackage=false";
@@ -375,13 +622,15 @@ var stepDotnetPublish = (project, target, verbose) => {
375
622
  );
376
623
  } catch (e) {
377
624
  const err = e;
378
- console.error(chalk3.red(" dotnet publish failed."));
379
- console.error(chalk3.dim(err.stderr?.toString() || err.message));
625
+ const output = err.stderr?.toString() || err.message;
626
+ console.error(chalk4.red(" dotnet publish failed."));
627
+ console.error(chalk4.dim(output));
628
+ if (looksLikeMissingWorkload(output)) printWorkloadHint();
380
629
  process.exit(1);
381
630
  }
382
631
  const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
383
632
  console.log(
384
- ` ${chalk3.green(">")} dotnet publish complete ${chalk3.dim(`(${elapsed}s)`)}`
633
+ ` ${chalk4.green(">")} dotnet publish complete ${chalk4.dim(`(${elapsed}s)`)}`
385
634
  );
386
635
  const publishDir = path5.join(
387
636
  project.hostDir,
@@ -395,9 +644,9 @@ var stepDotnetPublish = (project, target, verbose) => {
395
644
  // src/commands/dev.ts
396
645
  import path6 from "path";
397
646
  import fs5 from "fs-extra";
398
- import { execFileSync as execFileSync2, spawn } from "child_process";
647
+ import { execFileSync as execFileSync3, spawn } from "child_process";
399
648
  import { request } from "http";
400
- import chalk4 from "chalk";
649
+ import chalk5 from "chalk";
401
650
  var VERSION2 = "0.1.0";
402
651
  var POLL_INTERVAL_MS = 500;
403
652
  var POLL_TIMEOUT_MS = 3e4;
@@ -422,7 +671,7 @@ var devCommand = async (argv) => {
422
671
  if (!target) {
423
672
  const supported = Object.keys(TARGETS2).join(", ");
424
673
  console.error(
425
- chalk4.red(
674
+ chalk5.red(
426
675
  ` Unsupported target: ${targetName}. Supported: ${supported}`
427
676
  )
428
677
  );
@@ -430,6 +679,9 @@ var devCommand = async (argv) => {
430
679
  }
431
680
  ensureTargetMatchesHostOs(target.name);
432
681
  const project = detectProject(process.cwd());
682
+ if (!await ensureMauiWorkload({ csprojPath: project.csprojPath })) {
683
+ process.exit(1);
684
+ }
433
685
  const session = new DevSession(project, target, viteUrl, verbose);
434
686
  await session.run();
435
687
  };
@@ -450,11 +702,11 @@ var DevSession = class {
450
702
  async run() {
451
703
  this.installSignalHandlers();
452
704
  console.log();
453
- console.log(` ${chalk4.bold.cyan("vidra dev")} ${chalk4.dim(`v${VERSION2}`)}`);
705
+ console.log(` ${chalk5.bold.cyan("vidra dev")} ${chalk5.dim(`v${VERSION2}`)}`);
454
706
  console.log();
455
- console.log(` ${chalk4.dim("Project:")} ${chalk4.cyan(this.project.projectName)}`);
707
+ console.log(` ${chalk5.dim("Project:")} ${chalk5.cyan(this.project.projectName)}`);
456
708
  console.log(
457
- ` ${chalk4.dim("Target:")} ${chalk4.cyan(this.target.name)} (${this.target.framework})`
709
+ ` ${chalk5.dim("Target:")} ${chalk5.cyan(this.target.name)} (${this.target.framework})`
458
710
  );
459
711
  console.log();
460
712
  const vite = this.startVite();
@@ -462,13 +714,13 @@ var DevSession = class {
462
714
  await waitForServer(this.viteUrl, POLL_TIMEOUT_MS);
463
715
  } catch (error) {
464
716
  console.error(
465
- chalk4.red(
717
+ chalk5.red(
466
718
  ` ${error.message}`
467
719
  )
468
720
  );
469
721
  this.shutdown(1);
470
722
  }
471
- console.log(` ${chalk4.dim("Vite:")} ${chalk4.cyan(this.viteUrl)}`);
723
+ console.log(` ${chalk5.dim("Vite:")} ${chalk5.cyan(this.viteUrl)}`);
472
724
  console.log();
473
725
  const host = this.target.name === "macos" ? this.launchMacosHost() : this.launchWindowsHost();
474
726
  await waitForExit(vite, host);
@@ -483,7 +735,7 @@ var DevSession = class {
483
735
  });
484
736
  }
485
737
  startVite() {
486
- console.log(` ${chalk4.dim("Starting Vite dev server...")}`);
738
+ console.log(` ${chalk5.dim("Starting Vite dev server...")}`);
487
739
  const vite = spawn(NPM_COMMAND, ["run", "dev"], {
488
740
  cwd: this.project.uiDir,
489
741
  stdio: ["ignore", "pipe", "pipe"]
@@ -492,10 +744,10 @@ var DevSession = class {
492
744
  }
493
745
  launchMacosHost() {
494
746
  console.log(
495
- ` ${chalk4.dim(`Building MAUI host (${this.target.framework})...`)}`
747
+ ` ${chalk5.dim(`Building MAUI host (${this.target.framework})...`)}`
496
748
  );
497
749
  try {
498
- execFileSync2(
750
+ execFileSync3(
499
751
  DOTNET_COMMAND,
500
752
  [
501
753
  "build",
@@ -511,8 +763,10 @@ var DevSession = class {
511
763
  }
512
764
  );
513
765
  } catch (error) {
514
- console.error(chalk4.red(" MAUI build failed."));
515
- console.error(chalk4.dim(formatExecError2(error)));
766
+ const output = formatExecError2(error);
767
+ console.error(chalk5.red(" MAUI build failed."));
768
+ console.error(chalk5.dim(output));
769
+ if (looksLikeMissingWorkload(output)) printWorkloadHint();
516
770
  process.exit(1);
517
771
  }
518
772
  const appBundle = findMacAppBundle(
@@ -522,7 +776,7 @@ var DevSession = class {
522
776
  );
523
777
  if (!appBundle) {
524
778
  console.error(
525
- chalk4.red(
779
+ chalk5.red(
526
780
  ` Could not find .app bundle in ${path6.join(this.project.hostDir, "bin", this.buildConfig, this.target.framework)}`
527
781
  )
528
782
  );
@@ -536,11 +790,11 @@ var DevSession = class {
536
790
  const binary = findMacExecutable(appBundle);
537
791
  if (!binary) {
538
792
  console.error(
539
- chalk4.red(` Could not find the app executable in ${appBundle}.`)
793
+ chalk5.red(` Could not find the app executable in ${appBundle}.`)
540
794
  );
541
795
  process.exit(1);
542
796
  }
543
- console.log(` ${chalk4.dim("Launching host...")}`);
797
+ console.log(` ${chalk5.dim("Launching host...")}`);
544
798
  const host = spawn(binary, [], {
545
799
  cwd: this.project.root,
546
800
  stdio: ["ignore", "pipe", "pipe"],
@@ -549,7 +803,7 @@ var DevSession = class {
549
803
  return this.registerChild(host, "host", path6.basename(binary));
550
804
  }
551
805
  launchWindowsHost() {
552
- console.log(` ${chalk4.dim("Launching host...")}`);
806
+ console.log(` ${chalk5.dim("Launching host...")}`);
553
807
  const host = spawn(
554
808
  DOTNET_COMMAND,
555
809
  [
@@ -578,21 +832,21 @@ var DevSession = class {
578
832
  if (tag === "ui") {
579
833
  const exitCode = code ?? 1;
580
834
  console.error(
581
- chalk4.red(`
835
+ chalk5.red(`
582
836
  ${label} exited with code ${exitCode}.`)
583
837
  );
584
838
  this.shutdown(exitCode);
585
839
  return;
586
840
  }
587
841
  if (code !== null && code !== 0) {
588
- console.error(chalk4.red(`
842
+ console.error(chalk5.red(`
589
843
  ${label} exited with code ${code}.`));
590
844
  }
591
845
  this.shutdown(code ?? 0);
592
846
  });
593
847
  child.on("error", (error) => {
594
848
  if (this.shuttingDown) return;
595
- console.error(chalk4.red(`
849
+ console.error(chalk5.red(`
596
850
  Failed to start ${label}: ${error.message}`));
597
851
  this.shutdown(1);
598
852
  });
@@ -609,12 +863,12 @@ var DevSession = class {
609
863
  };
610
864
  var ensureTargetMatchesHostOs = (targetName) => {
611
865
  if (targetName === "macos" && process.platform !== "darwin") {
612
- console.error(chalk4.red(" The macOS dev target can only run on macOS."));
866
+ console.error(chalk5.red(" The macOS dev target can only run on macOS."));
613
867
  process.exit(1);
614
868
  }
615
869
  if (targetName === "windows" && process.platform !== "win32") {
616
870
  console.error(
617
- chalk4.red(" The Windows dev target can only run on Windows.")
871
+ chalk5.red(" The Windows dev target can only run on Windows.")
618
872
  );
619
873
  process.exit(1);
620
874
  }
@@ -701,7 +955,7 @@ var killChild = (child) => {
701
955
  if (!child.pid || child.exitCode !== null) return;
702
956
  if (process.platform === "win32") {
703
957
  try {
704
- execFileSync2("taskkill", ["/PID", String(child.pid), "/T", "/F"], {
958
+ execFileSync3("taskkill", ["/PID", String(child.pid), "/T", "/F"], {
705
959
  stdio: "ignore"
706
960
  });
707
961
  } catch {
@@ -723,25 +977,27 @@ var formatExecError2 = (error) => {
723
977
  };
724
978
 
725
979
  // src/cli.ts
726
- var VERSION3 = "0.1.1";
980
+ var VERSION3 = "0.1.3";
727
981
  var printHelp = () => {
728
982
  console.log(`
729
- ${chalk5.bold("vidra")} ${chalk5.dim(`v${VERSION3}`)}
983
+ ${chalk6.bold("vidra")} ${chalk6.dim(`v${VERSION3}`)}
730
984
 
731
- ${chalk5.dim("Usage:")}
985
+ ${chalk6.dim("Usage:")}
732
986
  vidra <command> [options]
733
987
 
734
- ${chalk5.dim("Commands:")}
988
+ ${chalk6.dim("Commands:")}
735
989
  dev Start the development environment
736
990
  build Build and package the application for distribution
991
+ doctor Check that your environment is set up to build Vidra apps
737
992
  help Show this help message
738
993
 
739
- ${chalk5.dim("Examples:")}
740
- vidra dev ${chalk5.dim("# start Vite + native host")}
741
- vidra dev --target windows ${chalk5.dim("# run the Windows host")}
742
- vidra build ${chalk5.dim("# auto-detect platform")}
743
- vidra build --target macos ${chalk5.dim("# macOS DMG")}
744
- vidra build --verbose ${chalk5.dim("# show full build output")}
994
+ ${chalk6.dim("Examples:")}
995
+ vidra dev ${chalk6.dim("# start Vite + native host")}
996
+ vidra dev --target windows ${chalk6.dim("# run the Windows host")}
997
+ vidra build ${chalk6.dim("# auto-detect platform")}
998
+ vidra build --target macos ${chalk6.dim("# macOS DMG")}
999
+ vidra build --verbose ${chalk6.dim("# show full build output")}
1000
+ vidra doctor ${chalk6.dim("# verify .NET SDK + MAUI workload")}
745
1001
  `);
746
1002
  };
747
1003
  var main = async () => {
@@ -754,6 +1010,9 @@ var main = async () => {
754
1010
  case "build":
755
1011
  await buildCommand(args.slice(1));
756
1012
  break;
1013
+ case "doctor":
1014
+ process.exit(await runDoctor());
1015
+ break;
757
1016
  case "help":
758
1017
  case "--help":
759
1018
  case "-h":
@@ -765,13 +1024,13 @@ var main = async () => {
765
1024
  console.log(VERSION3);
766
1025
  break;
767
1026
  default:
768
- console.error(chalk5.red(` Unknown command: ${command}
1027
+ console.error(chalk6.red(` Unknown command: ${command}
769
1028
  `));
770
1029
  printHelp();
771
1030
  process.exit(1);
772
1031
  }
773
1032
  };
774
1033
  main().catch((e) => {
775
- console.error(chalk5.red(e.message));
1034
+ console.error(chalk6.red(e.message));
776
1035
  process.exit(1);
777
1036
  });
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
- import prompts from "prompts";
3
- import chalk2 from "chalk";
2
+ import prompts2 from "prompts";
3
+ import chalk3 from "chalk";
4
4
  import fs2 from "fs-extra";
5
5
  import path2 from "path";
6
6
  import { randomUUID } from "crypto";
@@ -112,6 +112,140 @@ var applyReplacements = (str, replacements) => {
112
112
  return str;
113
113
  };
114
114
 
115
+ // src/doctor.ts
116
+ import { execFileSync } from "child_process";
117
+ import prompts from "prompts";
118
+ import chalk2 from "chalk";
119
+ var DOTNET = process.platform === "win32" ? "dotnet.exe" : "dotnet";
120
+ var MAUI_DOCS = "https://learn.microsoft.com/dotnet/maui/get-started/installation";
121
+ var bufToStr = (v) => v == null ? "" : Buffer.isBuffer(v) ? v.toString() : v;
122
+ var run = (cmd, args) => {
123
+ try {
124
+ const stdout = execFileSync(cmd, args, {
125
+ encoding: "utf-8",
126
+ stdio: ["ignore", "pipe", "pipe"]
127
+ });
128
+ return { found: true, ok: true, stdout: stdout ?? "", stderr: "" };
129
+ } catch (e) {
130
+ const err = e;
131
+ return {
132
+ found: err.code !== "ENOENT",
133
+ ok: false,
134
+ stdout: bufToStr(err.stdout),
135
+ stderr: bufToStr(err.stderr)
136
+ };
137
+ }
138
+ };
139
+ var hasNet10Sdk = (listSdksOutput) => listSdksOutput.split(/\r?\n/).some((line) => /^10\./.test(line.trim()));
140
+ var newestNet10Sdk = (listSdksOutput) => listSdksOutput.split(/\r?\n/).map((line) => line.trim().split(/\s+/)[0]).filter((v) => /^10\./.test(v)).sort(
141
+ (a, b) => a.localeCompare(b, void 0, { numeric: true, sensitivity: "base" })
142
+ ).pop();
143
+ var outputMentionsMaui = (workloadListOutput) => /\bmaui\b/i.test(workloadListOutput);
144
+ var checkDotnetSdk = () => {
145
+ const res = run(DOTNET, ["--list-sdks"]);
146
+ if (!res.found) {
147
+ return {
148
+ name: ".NET SDK",
149
+ status: "missing",
150
+ detail: "`dotnet` was not found on your PATH",
151
+ fix: "Install the .NET 10 SDK \u2014 https://dotnet.microsoft.com/download"
152
+ };
153
+ }
154
+ if (!res.ok && !res.stdout) {
155
+ return {
156
+ name: ".NET SDK",
157
+ status: "unknown",
158
+ detail: "could not run `dotnet --list-sdks`"
159
+ };
160
+ }
161
+ if (hasNet10Sdk(res.stdout)) {
162
+ const newest = newestNet10Sdk(res.stdout);
163
+ return {
164
+ name: ".NET SDK",
165
+ status: "ok",
166
+ detail: newest ? `found ${newest}` : "found 10.x"
167
+ };
168
+ }
169
+ return {
170
+ name: ".NET SDK",
171
+ status: "missing",
172
+ detail: "no 10.x SDK installed",
173
+ fix: "Install the .NET 10 SDK \u2014 https://dotnet.microsoft.com/download"
174
+ };
175
+ };
176
+ var isMauiWorkloadInstalled = () => outputMentionsMaui(run(DOTNET, ["workload", "list"]).stdout);
177
+ var isInteractive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY);
178
+ var installWorkload = (csprojPath) => {
179
+ const args = csprojPath ? ["workload", "restore", csprojPath] : ["workload", "install", "maui"];
180
+ console.log();
181
+ console.log(` ${chalk2.dim(`Running: ${DOTNET} ${args.join(" ")}`)}`);
182
+ console.log(
183
+ ` ${chalk2.dim(
184
+ "This can download several hundred MB and take a few minutes."
185
+ )}`
186
+ );
187
+ console.log();
188
+ try {
189
+ execFileSync(DOTNET, args, { stdio: "inherit" });
190
+ return true;
191
+ } catch {
192
+ console.error();
193
+ console.error(` ${chalk2.red("Workload install failed.")}`);
194
+ console.error(
195
+ ` ${chalk2.dim(
196
+ "If this is a permissions error, your SDK is in a system location and needs elevation:"
197
+ )}`
198
+ );
199
+ console.error(` ${chalk2.cyan("sudo dotnet workload install maui")}`);
200
+ console.error();
201
+ return false;
202
+ }
203
+ };
204
+ var ensureMauiWorkload = async (opts = {}) => {
205
+ const dotnet = checkDotnetSdk();
206
+ if (dotnet.status === "missing") {
207
+ console.log();
208
+ console.log(` ${chalk2.yellow("!")} ${dotnet.name} \u2014 ${dotnet.detail}`);
209
+ if (dotnet.fix) {
210
+ console.log(` ${chalk2.dim("fix:")} ${chalk2.cyan(dotnet.fix)}`);
211
+ }
212
+ return false;
213
+ }
214
+ if (dotnet.status === "unknown") return true;
215
+ if (isMauiWorkloadInstalled()) return true;
216
+ console.log();
217
+ console.log(
218
+ ` ${chalk2.yellow("!")} The .NET MAUI workload is required but not installed.`
219
+ );
220
+ const interactive = opts.interactive ?? isInteractive();
221
+ if (interactive) {
222
+ let install = false;
223
+ try {
224
+ const res = await prompts({
225
+ type: "confirm",
226
+ name: "install",
227
+ message: "Install the .NET MAUI workload now?",
228
+ initial: true
229
+ });
230
+ install = Boolean(res.install);
231
+ } catch {
232
+ install = false;
233
+ }
234
+ if (install) {
235
+ if (installWorkload(opts.csprojPath) && isMauiWorkloadInstalled()) {
236
+ console.log(` ${chalk2.green("\u2713")} MAUI workload installed.`);
237
+ return true;
238
+ }
239
+ return false;
240
+ }
241
+ }
242
+ console.log(
243
+ ` ${chalk2.dim("run:")} ${chalk2.cyan("dotnet workload install maui")}`
244
+ );
245
+ console.log(` ${chalk2.dim("docs:")} ${chalk2.cyan(MAUI_DOCS)}`);
246
+ return false;
247
+ };
248
+
115
249
  // src/index.ts
116
250
  var __dirname = path2.dirname(fileURLToPath(import.meta.url));
117
251
  var CLI_ROOT = path2.resolve(__dirname, "..");
@@ -124,13 +258,13 @@ var VIDRA_VERSION = "0.1.0";
124
258
  var SDK_VERSION = "0.1.0";
125
259
  var main = async () => {
126
260
  console.log();
127
- console.log(chalk2.bold(" create-vidra-app"));
128
- console.log(chalk2.dim(" Scaffold a new Vidra application\n"));
261
+ console.log(chalk3.bold(" create-vidra-app"));
262
+ console.log(chalk3.dim(" Scaffold a new Vidra application\n"));
129
263
  const args = parseArgs(process.argv);
130
264
  let projectDir = args._[0];
131
265
  let appId = args["app-id"];
132
266
  if (!projectDir) {
133
- const res = await prompts(
267
+ const res = await prompts2(
134
268
  {
135
269
  type: "text",
136
270
  name: "projectDir",
@@ -147,7 +281,7 @@ var main = async () => {
147
281
  const appTitle = toTitleCase(projectDir);
148
282
  const appGuid = randomUUID().toUpperCase();
149
283
  if (!appId) {
150
- const res = await prompts(
284
+ const res = await prompts2(
151
285
  {
152
286
  type: "text",
153
287
  name: "appId",
@@ -162,7 +296,7 @@ var main = async () => {
162
296
  const root = path2.resolve(projectDir);
163
297
  if (fs2.existsSync(root) && fs2.readdirSync(root).length > 0) {
164
298
  console.error(
165
- chalk2.red(
299
+ chalk3.red(
166
300
  `
167
301
  Directory "${projectDir}" already exists and is not empty.
168
302
  `
@@ -171,13 +305,13 @@ var main = async () => {
171
305
  process.exit(1);
172
306
  }
173
307
  console.log();
174
- console.log(` ${chalk2.dim("Project:")} ${chalk2.cyan(projectName)}`);
175
- console.log(` ${chalk2.dim("Directory:")} ${chalk2.cyan(root)}`);
176
- console.log(` ${chalk2.dim("App ID:")} ${chalk2.cyan(appId)}`);
308
+ console.log(` ${chalk3.dim("Project:")} ${chalk3.cyan(projectName)}`);
309
+ console.log(` ${chalk3.dim("Directory:")} ${chalk3.cyan(root)}`);
310
+ console.log(` ${chalk3.dim("App ID:")} ${chalk3.cyan(appId)}`);
177
311
  console.log();
178
312
  const isMonorepo = fs2.existsSync(path2.join(LOCAL_SDK_DIR, "package.json"));
179
313
  const localFeedExists = isMonorepo && fs2.existsSync(LOCAL_FEED_DIR);
180
- const localFeedPath = localFeedExists ? toTextPath(LOCAL_FEED_DIR) : "";
314
+ const localFeedSource = localFeedExists ? ` <add key="vidra-local" value="${toTextPath(LOCAL_FEED_DIR)}" />` : "";
181
315
  const cliRef = isMonorepo ? `file:${toTextPath(LOCAL_CLI_DIR)}` : `^${VIDRA_VERSION}`;
182
316
  const sdkRef = isMonorepo ? `file:${toTextPath(LOCAL_SDK_DIR)}` : `^${SDK_VERSION}`;
183
317
  const replacements = {
@@ -189,55 +323,69 @@ var main = async () => {
189
323
  "{{cliVersion}}": cliRef,
190
324
  "{{vidraVersion}}": VIDRA_VERSION,
191
325
  "{{sdkVersion}}": sdkRef,
192
- "{{localFeedPath}}": localFeedPath
326
+ "{{localFeedSource}}": localFeedSource
193
327
  };
194
328
  const templateDir = path2.join(TEMPLATES_DIR, "react-vite");
195
329
  await scaffoldDir(templateDir, root, replacements);
196
- console.log(chalk2.dim(" Creating solution..."));
330
+ console.log(chalk3.dim(" Creating solution..."));
197
331
  exec(`dotnet new sln -n ${projectName} --force`, root);
198
332
  const slnFile = fs2.existsSync(path2.join(root, `${projectName}.slnx`)) ? `${projectName}.slnx` : `${projectName}.sln`;
199
333
  exec(
200
334
  `dotnet sln ${slnFile} add src/${projectName}.Host/${projectName}.Host.csproj`,
201
335
  root
202
336
  );
203
- console.log(chalk2.dim(" Installing npm dependencies..."));
337
+ console.log(chalk3.dim(" Installing npm dependencies..."));
204
338
  const uiDir = path2.join(root, "ui");
205
339
  const rootNpmOk = tryExec("npm install", root);
206
340
  const uiNpmOk = tryExec("npm install", uiDir);
207
341
  const npmOk = rootNpmOk && uiNpmOk;
208
342
  console.log();
209
- console.log(chalk2.green(" Done! ") + "Your Vidra app is ready.\n");
343
+ console.log(chalk3.green(" Done! ") + "Your Vidra app is ready.\n");
210
344
  if (localFeedExists) {
211
345
  console.log(
212
- chalk2.dim(" NuGet:") + " local feed \u2192 " + chalk2.cyan(LOCAL_FEED_DIR)
346
+ chalk3.dim(" NuGet:") + " local feed \u2192 " + chalk3.cyan(LOCAL_FEED_DIR)
213
347
  );
214
348
  } else if (isMonorepo) {
215
349
  console.log(
216
- chalk2.yellow(" Note: ") + "Local NuGet feed not found. Run " + chalk2.cyan("./pack-local.sh") + " in the Vidra repo, then update NuGet.Config."
350
+ chalk3.yellow(" Note: ") + "Local NuGet feed not found. Run " + chalk3.cyan("./pack-local.sh") + " in the Vidra repo, then update NuGet.Config."
217
351
  );
218
352
  }
219
353
  if (isMonorepo) {
220
354
  console.log(
221
- chalk2.dim(" npm: ") + " @vidra-dev/sdk \u2192 " + chalk2.cyan(LOCAL_SDK_DIR)
355
+ chalk3.dim(" npm: ") + " @vidra-dev/sdk \u2192 " + chalk3.cyan(LOCAL_SDK_DIR)
222
356
  );
223
357
  console.log(
224
- chalk2.dim(" npm: ") + " create-vidra-app \u2192 " + chalk2.cyan(LOCAL_CLI_DIR)
358
+ chalk3.dim(" npm: ") + " create-vidra-app \u2192 " + chalk3.cyan(LOCAL_CLI_DIR)
225
359
  );
226
360
  }
227
361
  console.log();
228
362
  if (!npmOk) {
229
363
  console.log(
230
- chalk2.yellow(" Note: ") + "`npm install` had errors. Re-run " + chalk2.cyan("npm install") + " in the project root and in " + chalk2.cyan("ui/") + " to retry.\n"
364
+ chalk3.yellow(" Note: ") + "`npm install` had errors. Re-run " + chalk3.cyan("npm install") + " in the project root and in " + chalk3.cyan("ui/") + " to retry.\n"
231
365
  );
232
366
  }
233
- console.log(chalk2.bold(" Next steps:\n"));
367
+ const hostCsproj = path2.join(
368
+ root,
369
+ "src",
370
+ `${projectName}.Host`,
371
+ `${projectName}.Host.csproj`
372
+ );
373
+ const prereqsReady = await ensureMauiWorkload({ csprojPath: hostCsproj });
374
+ console.log(chalk3.bold(" Next steps:\n"));
234
375
  console.log(` cd ${projectDir}`);
235
376
  console.log(
236
- ` npm run dev ${chalk2.dim("# starts Vite + MAUI host together")}`
377
+ ` npm run dev ${chalk3.dim("# starts Vite + MAUI host together")}`
237
378
  );
379
+ if (!prereqsReady) {
380
+ console.log(
381
+ ` ${chalk3.dim("# tip: run")} ${chalk3.cyan(
382
+ "vidra doctor"
383
+ )} ${chalk3.dim("to verify your setup first")}`
384
+ );
385
+ }
238
386
  console.log();
239
387
  };
240
388
  main().catch((e) => {
241
- console.error(chalk2.red(e.message));
389
+ console.error(chalk3.red(e.message));
242
390
  process.exit(1);
243
391
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-vidra-app",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Scaffold a new Vidra application (React + .NET MAUI)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,6 +3,6 @@
3
3
  <packageSources>
4
4
  <clear />
5
5
  <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
6
- <add key="vidra-local" value="{{localFeedPath}}" />
6
+ {{localFeedSource}}
7
7
  </packageSources>
8
8
  </configuration>
@@ -6,9 +6,19 @@ A cross-platform application built with [Vidra](https://github.com/user/vidra)
6
6
 
7
7
  ### Prerequisites
8
8
 
9
- - [.NET 10 SDK](https://dotnet.microsoft.com/download) with MAUI workload
9
+ - [.NET 10 SDK](https://dotnet.microsoft.com/download)
10
+ - The .NET MAUI workload: `dotnet workload install maui`
10
11
  - [Node.js](https://nodejs.org/) 18+
11
- - Windows development must be run from a Windows machine with the MAUI Windows workload installed
12
+ - macOS targets require Xcode; Windows targets must be built on Windows
13
+
14
+ Not sure if you're set up? Run:
15
+
16
+ ```bash
17
+ vidra doctor
18
+ ```
19
+
20
+ It checks your .NET SDK, the MAUI workload, and (on macOS) Xcode, and prints the
21
+ exact command to fix anything that's missing.
12
22
 
13
23
  ### Development
14
24