| File: Commands\DoctorCommand.cs | Web Access |
| Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.CommandLine; using System.Globalization; using Aspire.Cli.Configuration; using Aspire.Cli.Interaction; using Aspire.Cli.Resources; using Aspire.Cli.Utils; using Aspire.Cli.Telemetry; using Aspire.Cli.Utils.EnvironmentChecker; using Spectre.Console; namespace Aspire.Cli.Commands; internal sealed class DoctorCommand : BaseCommand { internal override HelpGroup HelpGroup => HelpGroup.ToolsAndConfiguration; private readonly IEnvironmentChecker _environmentChecker; private readonly IAnsiConsole _ansiConsole; private static readonly Option<OutputFormat> s_formatOption = new("--format") { Description = DoctorCommandStrings.JsonOptionDescription }; public DoctorCommand( IEnvironmentChecker environmentChecker, IFeatures features, ICliUpdateNotifier updateNotifier, CliExecutionContext executionContext, IInteractionService interactionService, IAnsiConsole ansiConsole, AspireCliTelemetry telemetry) : base("doctor", DoctorCommandStrings.Description, features, updateNotifier, executionContext, interactionService, telemetry) { _environmentChecker = environmentChecker; _ansiConsole = ansiConsole; Options.Add(s_formatOption); } protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) { var format = parseResult.GetValue(s_formatOption); // Run all prerequisite checks var results = await InteractionService.ShowStatusAsync( DoctorCommandStrings.CheckingPrerequisites, async () => await _environmentChecker.CheckAllAsync(cancellationToken)); if (format == OutputFormat.Json) { OutputJson(results); } else { OutputHumanReadable(results); } // Exit code: 0 if no failures (warnings are OK), 1 (InvalidCommand) if any failures var hasFailures = results.Any(r => r.Status == EnvironmentCheckStatus.Fail); return hasFailures ? ExitCodeConstants.InvalidCommand : ExitCodeConstants.Success; } private void OutputJson(IReadOnlyList<EnvironmentCheckResult> results) { var passed = results.Count(r => r.Status == EnvironmentCheckStatus.Pass); var warnings = results.Count(r => r.Status == EnvironmentCheckStatus.Warning); var failed = results.Count(r => r.Status == EnvironmentCheckStatus.Fail); var response = new DoctorCheckResponse { Checks = results.ToList(), Summary = new DoctorCheckSummary { Passed = passed, Warnings = warnings, Failed = failed } }; var json = System.Text.Json.JsonSerializer.Serialize(response, JsonSourceGenerationContext.RelaxedEscaping.DoctorCheckResponse); // Use DisplayRawText to write directly to console without any formatting // Structured output always goes to stdout. InteractionService.DisplayRawText(json, ConsoleOutput.Standard); } private void OutputHumanReadable(IReadOnlyList<EnvironmentCheckResult> results) { _ansiConsole.WriteLine(); _ansiConsole.MarkupLine($"[bold]{DoctorCommandStrings.EnvironmentCheckHeader}[/]"); _ansiConsole.WriteLine(new string('=', DoctorCommandStrings.EnvironmentCheckHeader.Length)); _ansiConsole.WriteLine(); // Group results by category var groupedResults = results .GroupBy(r => r.Category) .OrderBy(g => GetCategoryOrder(g.Key)); foreach (var group in groupedResults) { var categoryHeader = GetCategoryHeader(group.Key); _ansiConsole.MarkupLine($"[bold]{categoryHeader}[/]"); foreach (var result in group) { OutputCheckResult(result); } _ansiConsole.WriteLine(); } // Output summary var passed = results.Count(r => r.Status == EnvironmentCheckStatus.Pass); var warnings = results.Count(r => r.Status == EnvironmentCheckStatus.Warning); var failed = results.Count(r => r.Status == EnvironmentCheckStatus.Fail); _ansiConsole.MarkupLine($"[bold]{string.Format(CultureInfo.CurrentCulture, DoctorCommandStrings.SummaryFormat, passed, warnings, failed)}[/]"); // Show link to detailed prerequisites if there are warnings or failures if (warnings > 0 || failed > 0) { _ansiConsole.MarkupLine(DoctorCommandStrings.DetailedPrerequisitesLink); } } private void OutputCheckResult(EnvironmentCheckResult result) { var (icon, color) = GetStatusIconAndColor(result.Status); var iconPrefix = ConsoleHelpers.FormatEmojiPrefix(icon, _ansiConsole); // Primary grid: icon + message (wrapped lines stay aligned with message text) var messageGrid = new Grid(); messageGrid.AddColumn(); messageGrid.AddRow( new Markup($"[{color}]{iconPrefix}{result.Message.EscapeMarkup()}[/]")); _ansiConsole.Write(new Padder(messageGrid, new Padding(2, 0))); // Secondary grid: details, fix suggestions, and links (indented further than message) var hasDetails = !string.IsNullOrEmpty(result.Details); var hasFix = !string.IsNullOrEmpty(result.Fix); var hasLink = !string.IsNullOrEmpty(result.Link); if (hasDetails || hasFix || hasLink) { var detailGrid = new Grid(); detailGrid.AddColumn(); if (hasFix) { var fixLines = result.Fix!.Split('\n', StringSplitOptions.RemoveEmptyEntries); foreach (var line in fixLines) { detailGrid.AddRow(new Markup($"{line.Trim().EscapeMarkup()}")); } } if (hasLink) { detailGrid.AddRow(new Markup($"See: {result.Link!.EscapeMarkup()}")); } if (hasDetails) { detailGrid.AddRow(new Markup($"[dim]Details:[/]")); detailGrid.AddRow(new Markup($"[dim]{result.Details!.EscapeMarkup()}[/]")); } _ansiConsole.Write(new Padder(detailGrid, new Padding(7, 0))); } } private static (KnownEmoji Icon, string Color) GetStatusIconAndColor(EnvironmentCheckStatus status) { return status switch { EnvironmentCheckStatus.Pass => (KnownEmojis.CheckMark, "green"), EnvironmentCheckStatus.Warning => (KnownEmojis.Warning, "yellow"), EnvironmentCheckStatus.Fail => (KnownEmojis.CrossMark, "red"), _ => (KnownEmojis.Information, "grey") }; } private static string GetCategoryHeader(string category) { return category switch { "sdk" => DoctorCommandStrings.SdkCategoryHeader, "container" => DoctorCommandStrings.ContainerCategoryHeader, "environment" => DoctorCommandStrings.EnvironmentCategoryHeader, _ => category }; } private static int GetCategoryOrder(string category) { return category switch { "sdk" => 1, "container" => 2, "environment" => 3, _ => 99 }; } }