// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Aspire.Cli.Configuration;
using Aspire.Cli.Packaging;
using Microsoft.Extensions.Configuration;
namespace Aspire.Cli;
/// <summary>
/// Metadata for an Aspire feature flag.
/// </summary>
/// <param name="Name">The feature flag name (without the "features." prefix).</param>
/// <param name="Description">A description of what the feature does.</param>
/// <param name="DefaultValue">The default value if not explicitly configured.</param>
/// <param name="Hidden">
/// When <c>true</c>, the feature is omitted from user-facing surfaces (the <c>aspire config</c>
/// available-features listing and <c>aspire config info</c> / its generated VS Code schemas). The
/// flag still works if set directly in configuration; hiding only removes it from discovery.
/// </param>
internal sealed record FeatureMetadata(string Name, string Description, bool DefaultValue, bool Hidden = false);
// this is a copy of Shared/KnownResourceNames.cs
internal static class KnownFeatures
{
public static string FeaturePrefix => "features";
public static string UpdateNotificationsEnabled => "updateNotificationsEnabled";
public static string ShowDeprecatedPackages => "showDeprecatedPackages";
public static string StagingChannelEnabled => "stagingChannelEnabled";
public static string DefaultWatchEnabled => "defaultWatchEnabled";
public static string ShowAllTemplates => "showAllTemplates";
public static string ExperimentalPolyglotRust => "experimentalPolyglotRust";
public static string ExperimentalPolyglotJava => "experimentalPolyglotJava";
public static string ExperimentalPolyglotGo => "experimentalPolyglotGo";
public static string ExperimentalPolyglotPython => "experimentalPolyglotPython";
public static string ExperimentalHostingIntegrations => "experimentalHostingIntegrations";
public static string NuGetSignatureVerificationEnabled => "nugetSignatureVerificationEnabled";
public static string AspireSkillsRemoteFetchEnabled => "aspireSkillsRemoteFetchEnabled";
public static string PolyglotIntegrationFilterEnabled => "polyglotIntegrationFilterEnabled";
private static readonly Dictionary<string, FeatureMetadata> s_featureMetadata = new()
{
[UpdateNotificationsEnabled] = new(
UpdateNotificationsEnabled,
"Check if update notifications are disabled and set version check environment variable",
DefaultValue: true),
[ShowDeprecatedPackages] = new(
ShowDeprecatedPackages,
"Show or hide deprecated packages in 'aspire add' search results",
DefaultValue: false),
[StagingChannelEnabled] = new(
StagingChannelEnabled,
"Enable or disable access to the staging channel for early access to preview features and packages",
DefaultValue: false),
[DefaultWatchEnabled] = new(
DefaultWatchEnabled,
"Enable or disable watch mode by default when running Aspire applications for automatic restarts on file changes",
DefaultValue: false),
[ShowAllTemplates] = new(
ShowAllTemplates,
"Show all available templates including experimental ones in 'aspire new' and 'aspire init' commands",
DefaultValue: false),
[ExperimentalPolyglotRust] = new(
ExperimentalPolyglotRust,
"Enable or disable experimental Rust language support for polyglot Aspire applications",
DefaultValue: false),
[ExperimentalPolyglotJava] = new(
ExperimentalPolyglotJava,
"Enable or disable experimental Java language support for polyglot Aspire applications",
DefaultValue: false),
[ExperimentalPolyglotGo] = new(
ExperimentalPolyglotGo,
"Enable or disable experimental Go language support for polyglot Aspire applications",
DefaultValue: false),
[ExperimentalPolyglotPython] = new(
ExperimentalPolyglotPython,
"Enable or disable experimental Python language support for polyglot Aspire applications",
DefaultValue: false),
[ExperimentalHostingIntegrations] = new(
ExperimentalHostingIntegrations,
"Enable experimental hosting integrations authored outside .NET",
DefaultValue: false),
[NuGetSignatureVerificationEnabled] = new(
NuGetSignatureVerificationEnabled,
"Enable or disable defaulting the DOTNET_NUGET_SIGNATURE_VERIFICATION environment variable for NuGet operations",
DefaultValue: true),
[AspireSkillsRemoteFetchEnabled] = new(
AspireSkillsRemoteFetchEnabled,
"(Preview) Allow the Aspire CLI to download the aspire-skills bundle from GitHub. When disabled (the 13.4 default), the CLI only uses the cached bundle and the embedded snapshot baked into the CLI; toggle on to opt in to the remote fetch path.",
DefaultValue: false,
// Hidden from discovery while the remote-fetch path is preview-only: the CLI ships with a
// trusted SHA-512 embedded snapshot, and the remote path stays off by default. The flag is
// still honored if set directly in config.
Hidden: true),
[PolyglotIntegrationFilterEnabled] = new(
PolyglotIntegrationFilterEnabled,
"(Experimental) Restrict 'aspire add', 'aspire integration list', and 'aspire integration search' in non-C# AppHosts to integrations carrying the 'polyglot' NuGet tag. Disabled by default because no remote feed resolves the tag usefully today: Azure DevOps Artifacts feeds ignore 'tags:' query scoping, and nuget.org returns no first-party integrations for it. The filter fails closed, so enabling it against a remote feed hides every integration. Enable it only against a local package source or hive, where the tag is read from the nuspec.",
DefaultValue: false)
};
/// <summary>
/// Gets metadata for a specific feature.
/// </summary>
public static FeatureMetadata? GetFeatureMetadata(string featureName)
{
return s_featureMetadata.TryGetValue(featureName, out var metadata) ? metadata : null;
}
/// <summary>
/// Gets all available feature metadata.
/// </summary>
public static IEnumerable<FeatureMetadata> GetAllFeatureMetadata()
{
return s_featureMetadata.Values.OrderBy(m => m.Name);
}
/// <summary>
/// Gets all available feature names (without the "features." prefix).
/// </summary>
public static IEnumerable<string> GetAllFeatureNames()
{
return s_featureMetadata.Keys.OrderBy(name => name);
}
/// <summary>
/// Resolves external hosting integration opt-in for the target AppHost.
/// </summary>
public static bool IsHostingIntegrationsEnabled(IFeatures features, AspireConfigFile config)
{
// --apphost can target a different directory than the CLI's configuration scope.
// Honor that AppHost's explicit setting before falling back to CLI/global settings.
return config.Features is { } projectFeatures
&& projectFeatures.TryGetValue(ExperimentalHostingIntegrations, out var enabled)
? enabled
: features.IsFeatureEnabled(ExperimentalHostingIntegrations, defaultValue: false);
}
/// <summary>
/// Determines whether the staging channel is enabled by checking both the feature flag
/// and the configured channel. The staging channel is considered enabled if either the
/// <see cref="StagingChannelEnabled"/> feature flag is <c>true</c>, or the configured
/// channel is set to <c>"staging"</c>.
/// </summary>
/// <param name="features">The feature flags service.</param>
/// <param name="configuration">The configuration to check for the channel setting.</param>
/// <returns><c>true</c> if the staging channel should be available; otherwise, <c>false</c>.</returns>
/// <remarks>
/// Note that the channel check reads <c>configuration["channel"]</c> (the layered .NET
/// configuration — environment variables, command-line, global / per-project
/// <c>aspire.config.json#channel</c>), NOT
/// <see cref="CliExecutionContext.IdentityChannel"/>. Callers that also need to expose
/// staging for a CLI baked with <c>AspireCliChannel=staging</c> should combine this
/// helper with an identity-channel check.
/// </remarks>
public static bool IsStagingChannelEnabled(IFeatures features, IConfiguration configuration)
{
return features.IsFeatureEnabled(StagingChannelEnabled, false)
|| string.Equals(configuration["channel"], PackageChannelNames.Staging, StringComparisons.ChannelName);
}
}