File: Commands\DocsGetCommand.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;
 
namespace Aspire.Cli.Commands;
 
/// <summary>
/// Command to get the full content of a documentation page by its slug.
/// </summary>
internal sealed partial class DocsGetCommand : BaseCommand
{
    private readonly IDocsIndexService _docsIndexService;
    private readonly ILogger<DocsGetCommand> _logger;
 
    private static readonly Argument<string> s_slugArgument = new("slug")
    {
        Description = DocsCommandStrings.SlugArgumentDescription
    };
 
    private static readonly Option<string?> s_sectionOption = new("--section")
    {
        Description = DocsCommandStrings.SectionOptionDescription
    };
 
    private static readonly Option<OutputFormat> s_formatOption = new("--format")
    {
        Description = DocsCommandStrings.FormatOptionDescription
    };
 
    public DocsGetCommand(
        IInteractionService interactionService,
        IDocsIndexService docsIndexService,
        IFeatures features,
        ICliUpdateNotifier updateNotifier,
        CliExecutionContext executionContext,
        AspireCliTelemetry telemetry,
        ILogger<DocsGetCommand> logger)
        : base("get", DocsCommandStrings.GetDescription, features, updateNotifier, executionContext, interactionService, telemetry)
    {
        _docsIndexService = docsIndexService;
        _logger = logger;
 
        Arguments.Add(s_slugArgument);
        Options.Add(s_sectionOption);
        Options.Add(s_formatOption);
    }
 
    protected override bool UpdateNotificationsEnabled => false;
 
    protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
    {
        using var activity = Telemetry.StartDiagnosticActivity(Name);
 
        var slug = parseResult.GetValue(s_slugArgument)!;
        var section = parseResult.GetValue(s_sectionOption);
        var format = parseResult.GetValue(s_formatOption);
 
        _logger.LogDebug("Getting documentation for slug '{Slug}' (section: {Section})", slug, section ?? "(all)");
 
        // Get doc with status indicator
        var doc = await InteractionService.ShowStatusAsync(
            DocsCommandStrings.LoadingDocumentation,
            async () => await _docsIndexService.GetDocumentAsync(slug, section, cancellationToken));
 
        if (doc is null)
        {
            InteractionService.DisplayError(string.Format(CultureInfo.CurrentCulture, DocsCommandStrings.DocumentNotFound, slug));
            return ExitCodeConstants.InvalidCommand;
        }
 
        if (format is OutputFormat.Json)
        {
            var json = JsonSerializer.Serialize(doc, JsonSourceGenerationContext.RelaxedEscaping.DocsContent);
            InteractionService.DisplayRawText(json);
        }
        else
        {
            // Format the markdown for better terminal readability
            var formatted = FormatMarkdownForTerminal(doc.Content);
            InteractionService.DisplayRawText(formatted);
        }
 
        return ExitCodeConstants.Success;
    }
 
    /// <summary>
    /// Formats minified markdown content for better terminal readability by inserting line breaks.
    /// </summary>
    private static string FormatMarkdownForTerminal(string content)
    {
        // The llms.txt format has markdown on single lines - insert breaks for readability
        // Add newline before headings (##, ###)
        content = HeadingRegex().Replace(content, "\n\n$0");
 
        // Add newlines around code blocks
        content = CodeBlockStartRegex().Replace(content, "\n$0\n");
        content = CodeBlockEndRegex().Replace(content, "\n$0\n");
 
        // Clean up excessive newlines
        content = ExcessiveNewlinesRegex().Replace(content, "\n\n");
 
        return content.Trim();
    }
 
    // Match markdown headings: ## or ### at start or after space (not C#)
    [System.Text.RegularExpressions.GeneratedRegex(@"(?<=\s)(#{2,6}\s)")]
    private static partial System.Text.RegularExpressions.Regex HeadingRegex();
 
    [System.Text.RegularExpressions.GeneratedRegex(@"(?<!\n)```\w*")]
    private static partial System.Text.RegularExpressions.Regex CodeBlockStartRegex();
 
    [System.Text.RegularExpressions.GeneratedRegex(@"```(?!\w)(?!\n)")]
    private static partial System.Text.RegularExpressions.Regex CodeBlockEndRegex();
 
    [System.Text.RegularExpressions.GeneratedRegex(@"\n{3,}")]
    private static partial System.Text.RegularExpressions.Regex ExcessiveNewlinesRegex();
}