File: Commands\DocsSearchCommand.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 System.Text.Json;
using Aspire.Cli.Configuration;
using Aspire.Cli.Interaction;
using Aspire.Cli.Mcp.Docs;
using Aspire.Cli.Resources;
using Aspire.Cli.Telemetry;
using Aspire.Cli.Utils;
using Microsoft.Extensions.Logging;
using Spectre.Console;
 
namespace Aspire.Cli.Commands;
 
/// <summary>
/// Command to search Aspire documentation by keywords.
/// </summary>
internal sealed class DocsSearchCommand : BaseCommand
{
    private readonly IDocsSearchService _docsSearchService;
    private readonly ILogger<DocsSearchCommand> _logger;
 
    private static readonly Argument<string> s_queryArgument = new("query")
    {
        Description = DocsCommandStrings.QueryArgumentDescription
    };
 
    private static readonly Option<OutputFormat> s_formatOption = new("--format")
    {
        Description = DocsCommandStrings.FormatOptionDescription
    };
 
    private static readonly Option<int?> s_limitOption = new("--limit", "-n")
    {
        Description = DocsCommandStrings.LimitOptionDescription
    };
 
    public DocsSearchCommand(
        IInteractionService interactionService,
        IDocsSearchService docsSearchService,
        IFeatures features,
        ICliUpdateNotifier updateNotifier,
        CliExecutionContext executionContext,
        AspireCliTelemetry telemetry,
        ILogger<DocsSearchCommand> logger)
        : base("search", DocsCommandStrings.SearchDescription, features, updateNotifier, executionContext, interactionService, telemetry)
    {
        _docsSearchService = docsSearchService;
        _logger = logger;
 
        Arguments.Add(s_queryArgument);
        Options.Add(s_formatOption);
        Options.Add(s_limitOption);
    }
 
    protected override bool UpdateNotificationsEnabled => false;
 
    protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
    {
        using var activity = Telemetry.StartDiagnosticActivity(Name);
 
        var query = parseResult.GetValue(s_queryArgument)!;
        var format = parseResult.GetValue(s_formatOption);
        var limit = Math.Clamp(parseResult.GetValue(s_limitOption) ?? 5, 1, 10);
 
        _logger.LogDebug("Searching documentation for '{Query}' (limit: {Limit})", query, limit);
 
        // Search docs with status indicator
        var response = await InteractionService.ShowStatusAsync(
            DocsCommandStrings.LoadingDocumentation,
            async () => await _docsSearchService.SearchAsync(query, limit, cancellationToken));
 
        if (response is null || response.Results.Count is 0)
        {
            InteractionService.DisplayError(string.Format(CultureInfo.CurrentCulture, DocsCommandStrings.NoResultsFound, query));
            return ExitCodeConstants.Success; // Not an error, just no results
        }
 
        if (format is OutputFormat.Json)
        {
            var json = JsonSerializer.Serialize(response.Results.ToArray(), JsonSourceGenerationContext.RelaxedEscaping.SearchResultArray);
            InteractionService.DisplayRawText(json);
        }
        else
        {
            InteractionService.DisplaySuccess(string.Format(CultureInfo.CurrentCulture, DocsCommandStrings.FoundSearchResults, response.Results.Count, query));
 
            // Results are already sorted by score (highest first) from the search service
            var table = new Table();
            table.AddColumn("Title");
            table.AddColumn("Slug");
            table.AddColumn("Section");
            table.AddColumn("Score");
 
            foreach (var result in response.Results)
            {
                table.AddRow(
                    Markup.Escape(result.Title),
                    Markup.Escape(result.Slug),
                    Markup.Escape(result.Section ?? "-"),
                    result.Score.ToString("F2", CultureInfo.InvariantCulture)); // Two decimal places
            }
 
            AnsiConsole.Write(table);
        }
 
        return ExitCodeConstants.Success;
    }
}