File: Utils\CliHostEnvironment.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 Microsoft.Extensions.Configuration;
using Spectre.Console;
 
namespace Aspire.Cli.Utils;
 
/// <summary>
/// Provides information about the CLI host environment capabilities.
/// </summary>
internal interface ICliHostEnvironment
{
    /// <summary>
    /// Gets whether the host supports interactive input (e.g., prompts, user input).
    /// </summary>
    bool SupportsInteractiveInput { get; }
 
    /// <summary>
    /// Gets whether the host supports interactive output (e.g., spinners, progress bars).
    /// </summary>
    bool SupportsInteractiveOutput { get; }
 
    /// <summary>
    /// Gets whether the host supports colors and ANSI codes.
    /// </summary>
    bool SupportsAnsi { get; }
}
 
/// <summary>
/// Default implementation that detects CLI host environment capabilities from configuration.
/// </summary>
internal sealed class CliHostEnvironment : ICliHostEnvironment
{
    // Common CI environment variables
    // https://github.com/watson/ci-info/blob/master/vendors.json
    private static readonly string[] s_ciEnvironmentVariables =
    [
        "CI", // Generic CI indicator
        "GITHUB_ACTIONS",
        "AZURE_PIPELINES",
        "TF_BUILD", // Azure Pipelines alternative
        "JENKINS_URL",
        "GITLAB_CI",
        "CIRCLECI",
        "TRAVIS",
        "BUILDKITE",
        "APPVEYOR",
        "TEAMCITY_VERSION",
        "BITBUCKET_BUILD_NUMBER",
        "CODEBUILD_BUILD_ID", // AWS CodeBuild
    ];
 
    /// <summary>
    /// Gets whether the host supports interactive input (e.g., prompts, user input).
    /// </summary>
    public bool SupportsInteractiveInput { get; }
 
    /// <summary>
    /// Gets whether the host supports interactive output (e.g., spinners, progress bars).
    /// </summary>
    public bool SupportsInteractiveOutput { get; }
 
    /// <summary>
    /// Gets whether the host supports colors and ANSI codes.
    /// </summary>
    public bool SupportsAnsi { get; }
 
    public CliHostEnvironment(IConfiguration configuration, bool nonInteractive)
    {
        // If --non-interactive is explicitly set, disable interactive input and output.
        // This takes precedence over all other settings including ASPIRE_PLAYGROUND.
        if (nonInteractive)
        {
            SupportsInteractiveInput = false;
            SupportsInteractiveOutput = false;
            SupportsAnsi = DetectAnsiSupport(configuration);
        }
        // Check if ASPIRE_PLAYGROUND is set to force interactive mode
        else if (IsPlaygroundMode(configuration))
        {
            SupportsInteractiveInput = true;
            SupportsInteractiveOutput = true;
            SupportsAnsi = true;
        }
        else
        {
            SupportsInteractiveInput = DetectInteractiveInput(configuration);
            SupportsInteractiveOutput = DetectInteractiveOutput(configuration);
            SupportsAnsi = DetectAnsiSupport(configuration);
        }
    }
 
    private static bool DetectAnsiSupport(IConfiguration configuration)
    {
        if (!TryDetectAnsiSupportConfiguration(configuration, out var supportsAnsi))
        {
            // If there is no explicit configuration to enable or disable ANSI support, attempt to detect it.
            // This is required because some terminals don't support ANSI output, e.g. https://github.com/dotnet/aspire/issues/13737
 
            // TODO: Creating a fake console here is a hack to run ANSI detection logic.
            // Update this to use AnsiCapabilities once it's available in Spectre.Console 0.60+ instead of creating a full AnsiConsole instance.
            var ansiConsole = AnsiConsole.Create(new AnsiConsoleSettings
            {
                Out = new AnsiConsoleOutput(TextWriter.Null),
                Ansi = AnsiSupport.Detect,
                ColorSystem = ColorSystemSupport.Detect
            });
 
            supportsAnsi = ansiConsole.Profile.Capabilities.Ansi;
        }
 
        return supportsAnsi;
    }
 
    private static bool DetectInteractiveInput(IConfiguration configuration)
    {
        // Check if explicitly disabled via configuration
        var nonInteractive = configuration["ASPIRE_NON_INTERACTIVE"];
        if (!string.IsNullOrEmpty(nonInteractive) &&
            (nonInteractive.Equals("true", StringComparison.OrdinalIgnoreCase) ||
             nonInteractive.Equals("1", StringComparison.Ordinal)))
        {
            return false;
        }
 
        // Check if running in CI environment (no interactive input possible)
        if (IsCI(configuration))
        {
            return false;
        }
 
        return true;
    }
 
    private static bool DetectInteractiveOutput(IConfiguration configuration)
    {
        // Check if explicitly disabled via configuration
        var nonInteractive = configuration["ASPIRE_NON_INTERACTIVE"];
        if (!string.IsNullOrEmpty(nonInteractive) &&
            (nonInteractive.Equals("true", StringComparison.OrdinalIgnoreCase) ||
             nonInteractive.Equals("1", StringComparison.Ordinal)))
        {
            return false;
        }
 
        // Check if running in CI environment (spinners pollute logs)
        if (IsCI(configuration))
        {
            return false;
        }
 
        return true;
    }
 
    private static bool TryDetectAnsiSupportConfiguration(IConfiguration configuration, out bool supportsAnsi)
    {
        // Check for ASPIRE_ANSI_PASS_THRU to force ANSI even when redirected
        if (IsAnsiPassThruEnabled(configuration))
        {
            supportsAnsi = true;
            return true;
        }
 
        // Check for NO_COLOR to explicitly disable ANSI output.
        // If neither override is set, return false to let the caller fall back to Spectre detection.
        var noColor = configuration["NO_COLOR"];
        if (!string.IsNullOrEmpty(noColor))
        {
            supportsAnsi = false;
            return true;
        }
 
        supportsAnsi = default;
        return false;
    }
 
    private static bool IsAnsiPassThruEnabled(IConfiguration configuration)
    {
        var ansiPassThru = configuration["ASPIRE_ANSI_PASS_THRU"];
        return !string.IsNullOrEmpty(ansiPassThru) &&
               (ansiPassThru.Equals("true", StringComparison.OrdinalIgnoreCase) ||
                ansiPassThru.Equals("1", StringComparison.Ordinal));
    }
 
    private static bool IsCI(IConfiguration configuration)
    {
        foreach (var envVar in s_ciEnvironmentVariables)
        {
            var value = configuration[envVar];
            if (!string.IsNullOrEmpty(value))
            {
                // For CI variable, only return true if it's "true" or "1"
                if (envVar == "CI")
                {
                    return value.Equals("true", StringComparison.OrdinalIgnoreCase) ||
                           value.Equals("1", StringComparison.Ordinal);
                }
                return true;
            }
        }
 
        return false;
    }
 
    /// <summary>
    /// Gets whether the ASPIRE_PLAYGROUND environment variable is set to force interactive mode.
    /// </summary>
    internal static bool IsPlaygroundMode(IConfiguration configuration)
    {
        var playgroundMode = configuration["ASPIRE_PLAYGROUND"];
        return !string.IsNullOrEmpty(playgroundMode) &&
               playgroundMode.Equals("true", StringComparison.OrdinalIgnoreCase);
    }
}