|
// 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.CommandLine.Parsing;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using Aspire.Cli.Agents;
using Aspire.Cli.Agents.ClaudeCode;
using Aspire.Cli.Agents.CopilotCli;
using Aspire.Cli.Agents.OpenCode;
using Aspire.Cli.Agents.VsCode;
using Aspire.Cli.Backchannel;
using Aspire.Cli.Bundles;
using Aspire.Cli.Caching;
using Aspire.Cli.Certificates;
using Aspire.Cli.Commands;
using Aspire.Cli.Commands.Sdk;
using Aspire.Cli.Configuration;
using Aspire.Cli.Diagnostics;
using Aspire.Cli.DotNet;
using Aspire.Cli.Git;
using Aspire.Cli.Interaction;
using Aspire.Cli.Layout;
using Aspire.Cli.Mcp;
using Aspire.Cli.Mcp.Docs;
using Aspire.Cli.NuGet;
using Aspire.Cli.Packaging;
using Aspire.Cli.Projects;
using Aspire.Cli.Resources;
using Aspire.Cli.Scaffolding;
using Aspire.Cli.Telemetry;
using Aspire.Cli.Templating;
using Aspire.Cli.Utils;
using Aspire.Cli.Utils.EnvironmentChecker;
using Aspire.Hosting;
using Aspire.Shared;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Spectre.Console;
using RootCommand = Aspire.Cli.Commands.RootCommand;
namespace Aspire.Cli;
public class Program
{
private static string GetUsersAspirePath()
{
var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var aspirePath = Path.Combine(homeDirectory, ".aspire");
return aspirePath;
}
/// <summary>
/// Parses logging options from command-line arguments.
/// Returns the console log level (if specified) and whether debug mode is enabled.
/// </summary>
private static (LogLevel? ConsoleLogLevel, bool DebugMode) ParseLoggingOptions(string[]? args)
{
if (args is null || args.Length == 0)
{
return (null, false);
}
// Check for --debug or -d (backward compatibility)
var debugMode = args.Any(a => a == "--debug" || a == "-d");
// Check for --debug-level or -v
LogLevel? logLevel = null;
for (var i = 0; i < args.Length; i++)
{
if ((args[i] == "--debug-level" || args[i] == "-v") && i + 1 < args.Length)
{
if (Enum.TryParse<LogLevel>(args[i + 1], ignoreCase: true, out var parsedLevel))
{
logLevel = parsedLevel;
}
break;
}
}
// --debug implies Debug log level if --verbosity not specified
if (debugMode && logLevel is null)
{
logLevel = LogLevel.Debug;
}
return (logLevel, debugMode);
}
/// <summary>
/// Parses --log-file from raw args before the host is built.
/// Used by --detach to tell the child CLI where to write its log.
/// </summary>
internal static string? ParseLogFileOption(string[]? args)
{
if (args is null)
{
return null;
}
for (var i = 0; i < args.Length; i++)
{
if (args[i] == "--")
{
break;
}
if (args[i] == "--log-file" && i + 1 < args.Length)
{
return args[i + 1];
}
}
return null;
}
private static string GetGlobalSettingsPath()
{
var usersAspirePath = GetUsersAspirePath();
var globalSettingsPath = Path.Combine(usersAspirePath, "globalsettings.json");
return globalSettingsPath;
}
internal static async Task<IHost> BuildApplicationAsync(string[] args, Dictionary<string, string?>? configurationValues = null)
{
// Check for --non-interactive flag early
var nonInteractive = args?.Any(a => a == CommonOptionNames.NonInteractive) ?? false;
// Check if running MCP start command - all logs should go to stderr to keep stdout clean for MCP protocol
// Support both old 'mcp start' and new 'agent mcp' commands
var isMcpStartCommand = args?.Length >= 2 &&
((args[0] == "mcp" && args[1] == "start") || (args[0] == "agent" && args[1] == "mcp"));
var settings = new HostApplicationBuilderSettings
{
Configuration = new ConfigurationManager()
};
settings.Configuration.AddEnvironmentVariables();
if (configurationValues is not null)
{
settings.Configuration.AddInMemoryCollection(configurationValues);
}
var builder = Host.CreateEmptyApplicationBuilder(settings);
// Set up settings with appropriate paths.
var globalSettingsFilePath = GetGlobalSettingsPath();
var globalSettingsFile = new FileInfo(globalSettingsFilePath);
var workingDirectory = new DirectoryInfo(Environment.CurrentDirectory);
ConfigurationHelper.RegisterSettingsFiles(builder.Configuration, workingDirectory, globalSettingsFile);
await TrySetLocaleOverrideAsync(LocaleHelpers.GetLocaleOverride(builder.Configuration));
#if !DEBUG
// In release builds, limit shutdown wait time for telemetry flush to 200ms
// to ensure the CLI exits quickly even if waiting on shutdown tasks.
builder.Services.Configure<HostOptions>(options =>
{
options.ShutdownTimeout = TimeSpan.FromMilliseconds(200);
});
#endif
// Always configure OpenTelemetry.
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
});
// Configure OpenTelemetry tracing. TelemetryManager reads configuration and creates
// separate TracerProvider instances:
// - Azure Monitor provider with filtering (only exports activities with EXTERNAL_TELEMETRY=true)
// - Diagnostic provider for OTLP/console exporters (exports all activities, DEBUG only)
builder.Services.AddSingleton(new TelemetryManager(builder.Configuration, args));
// Parse logging options from args
var (consoleLogLevel, debugMode) = ParseLoggingOptions(args);
var extensionEndpoint = builder.Configuration[KnownConfigNames.ExtensionEndpoint];
// Always register FileLoggerProvider to capture logs to disk
// This captures complete CLI session details for diagnostics
var logsDirectory = Path.Combine(GetUsersAspirePath(), "logs");
var logFilePath = ParseLogFileOption(args);
var fileLoggerProvider = logFilePath is not null
? new FileLoggerProvider(logFilePath)
: new FileLoggerProvider(logsDirectory, TimeProvider.System);
builder.Services.AddSingleton(fileLoggerProvider); // Register for direct access to LogFilePath
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider>(fileLoggerProvider));
// Configure console logging based on --verbosity or --debug
if (consoleLogLevel is not null && !isMcpStartCommand && extensionEndpoint is null)
{
builder.Logging.AddFilter("Aspire.Cli", consoleLogLevel.Value);
builder.Logging.AddFilter("Microsoft.Hosting.Lifetime", LogLevel.Warning); // Reduce noise from hosting lifecycle
// Use custom Spectre Console logger for clean debug output to stderr
builder.Services.AddSingleton<ILoggerProvider>(sp =>
new SpectreConsoleLoggerProvider(sp.GetRequiredService<ConsoleEnvironment>().Error.Profile.Out.Writer));
}
// For MCP start command, configure console logger to route all logs to stderr
// This keeps stdout clean for MCP protocol JSON-RPC messages
if (isMcpStartCommand)
{
if (consoleLogLevel is not null)
{
builder.Logging.AddFilter("Aspire.Cli", consoleLogLevel.Value);
builder.Logging.AddFilter("Microsoft.Hosting.Lifetime", LogLevel.Warning); // Reduce noise from hosting lifecycle
}
builder.Logging.AddConsole(consoleLogOptions =>
{
// Configure all logs to go to stderr
consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
});
}
// Shared services.
builder.Services.AddSingleton(sp =>
{
var logFilePath = sp.GetRequiredService<FileLoggerProvider>().LogFilePath;
return BuildCliExecutionContext(debugMode, logsDirectory, logFilePath);
});
builder.Services.AddSingleton(s => new ConsoleEnvironment(
BuildAnsiConsole(s, Console.Out),
BuildAnsiConsole(s, Console.Error)));
builder.Services.AddSingleton(s => s.GetRequiredService<ConsoleEnvironment>().Out);
builder.Services.AddSingleton<ICliHostEnvironment>(provider =>
{
var configuration = provider.GetRequiredService<IConfiguration>();
return new CliHostEnvironment(configuration, nonInteractive);
});
builder.Services.AddSingleton(TimeProvider.System);
AddInteractionServices(builder);
builder.Services.AddSingleton<IProjectLocator, ProjectLocator>();
builder.Services.AddSingleton<ISolutionLocator, SolutionLocator>();
builder.Services.AddSingleton<ILanguageService, LanguageService>();
builder.Services.AddSingleton<IScaffoldingService, ScaffoldingService>();
builder.Services.AddSingleton<FallbackProjectParser>();
builder.Services.AddSingleton<IProjectUpdater, ProjectUpdater>();
builder.Services.AddSingleton<INewCommandPrompter, NewCommandPrompter>();
builder.Services.AddSingleton<IAddCommandPrompter, AddCommandPrompter>();
builder.Services.AddSingleton<IPublishCommandPrompter, PublishCommandPrompter>();
builder.Services.AddSingleton<ICertificateService, CertificateService>();
builder.Services.AddSingleton(BuildConfigurationService);
builder.Services.AddSingleton<IFeatures, Features>();
builder.Services.AddTelemetryServices();
builder.Services.AddTransient<IDotNetCliExecutionFactory, DotNetCliExecutionFactory>();
// Register certificate tool runner implementations - factory chooses based on embedded bundle
builder.Services.AddSingleton<ICertificateToolRunner>(sp =>
{
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
var bundleService = sp.GetRequiredService<IBundleService>();
if (bundleService.IsBundle)
{
return new BundleCertificateToolRunner(
bundleService,
loggerFactory.CreateLogger<BundleCertificateToolRunner>());
}
// Fall back to SDK-based runner
return new SdkCertificateToolRunner(loggerFactory.CreateLogger<SdkCertificateToolRunner>());
});
builder.Services.AddTransient<IDotNetCliRunner, DotNetCliRunner>();
builder.Services.AddSingleton<IDiskCache, DiskCache>();
builder.Services.AddSingleton<IDotNetSdkInstaller, DotNetSdkInstaller>();
builder.Services.AddTransient<IAppHostCliBackchannel, AppHostCliBackchannel>();
// Register both NuGetPackageCache implementations - factory chooses based on embedded bundle
builder.Services.AddSingleton<NuGetPackageCache>();
builder.Services.AddSingleton<BundleNuGetPackageCache>();
builder.Services.AddSingleton<INuGetPackageCache>(sp =>
{
if (sp.GetRequiredService<IBundleService>().IsBundle)
{
return sp.GetRequiredService<BundleNuGetPackageCache>();
}
// Fall back to SDK-based cache
return sp.GetRequiredService<NuGetPackageCache>();
});
builder.Services.AddSingleton<NuGetPackagePrefetcher>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<NuGetPackagePrefetcher>());
builder.Services.AddSingleton<AuxiliaryBackchannelMonitor>();
builder.Services.AddSingleton<IAuxiliaryBackchannelMonitor>(sp => sp.GetRequiredService<AuxiliaryBackchannelMonitor>());
builder.Services.AddHostedService(sp => sp.GetRequiredService<AuxiliaryBackchannelMonitor>());
builder.Services.AddSingleton<ICliUpdateNotifier, CliUpdateNotifier>();
builder.Services.AddSingleton<IPackagingService, PackagingService>();
builder.Services.AddSingleton<IBundleService, BundleService>();
builder.Services.AddSingleton<IAppHostServerProjectFactory, AppHostServerProjectFactory>();
builder.Services.AddSingleton<ICliDownloader, CliDownloader>();
builder.Services.AddSingleton<IFirstTimeUseNoticeSentinel>(_ => new FirstTimeUseNoticeSentinel(GetUsersAspirePath()));
builder.Services.AddSingleton<IBannerService, BannerService>();
builder.Services.AddMemoryCache();
// MCP server: aspire.dev docs services.
builder.Services.AddSingleton<IDocsCache, DocsCache>();
builder.Services.AddHttpClient<IDocsFetcher, DocsFetcher>();
builder.Services.AddSingleton<IDocsIndexService, DocsIndexService>();
builder.Services.AddSingleton<IDocsSearchService, DocsSearchService>();
// Bundle layout services (for polyglot apphost without .NET SDK).
// Registered before NuGetPackageCache so the factory can choose implementation.
builder.Services.AddSingleton<ILayoutDiscovery, LayoutDiscovery>();
builder.Services.AddSingleton<BundleNuGetService>();
// Git repository operations.
builder.Services.AddSingleton<IGitRepository, GitRepository>();
// OpenCode CLI operations.
builder.Services.AddSingleton<IOpenCodeCliRunner, OpenCodeCliRunner>();
// Claude Code CLI operations.
builder.Services.AddSingleton<IClaudeCodeCliRunner, ClaudeCodeCliRunner>();
// VS Code CLI operations.
builder.Services.AddSingleton<IVsCodeCliRunner, VsCodeCliRunner>();
builder.Services.AddSingleton<ICopilotCliRunner, CopilotCliRunner>();
// Agent environment detection.
builder.Services.AddSingleton<IAgentEnvironmentDetector, AgentEnvironmentDetector>();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IAgentEnvironmentScanner, VsCodeAgentEnvironmentScanner>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IAgentEnvironmentScanner, CopilotCliAgentEnvironmentScanner>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IAgentEnvironmentScanner, OpenCodeAgentEnvironmentScanner>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IAgentEnvironmentScanner, ClaudeCodeAgentEnvironmentScanner>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IAgentEnvironmentScanner, DeprecatedMcpCommandScanner>());
// Template factories.
builder.Services.AddSingleton<ITemplateProvider, TemplateProvider>();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ITemplateFactory, DotNetTemplateFactory>());
// Language discovery for polyglot support.
builder.Services.AddSingleton<ILanguageDiscovery, DefaultLanguageDiscovery>();
// AppHost server session factory for RPC communication.
builder.Services.AddSingleton<IAppHostServerSessionFactory, AppHostServerSessionFactory>();
// AppHost project handlers.
builder.Services.AddSingleton<DotNetAppHostProject>();
builder.Services.AddSingleton<Func<LanguageInfo, GuestAppHostProject>>(sp =>
{
return language => ActivatorUtilities.CreateInstance<GuestAppHostProject>(sp, language);
});
builder.Services.AddSingleton<IAppHostProjectFactory, AppHostProjectFactory>();
// Environment checking services.
builder.Services.AddSingleton<IEnvironmentCheck, WslEnvironmentCheck>();
builder.Services.AddSingleton<IEnvironmentCheck, DotNetSdkCheck>();
builder.Services.AddSingleton<IEnvironmentCheck, DeprecatedWorkloadCheck>();
builder.Services.AddSingleton<IEnvironmentCheck, DevCertsCheck>();
builder.Services.AddSingleton<IEnvironmentCheck, ContainerRuntimeCheck>();
builder.Services.AddSingleton<IEnvironmentCheck, DeprecatedAgentConfigCheck>();
builder.Services.AddSingleton<IEnvironmentChecker, EnvironmentChecker>();
// MCP server transport factory - creates transport only when needed to avoid
// capturing stdin/stdout before the MCP server command is actually executed.
builder.Services.AddSingleton<IMcpTransportFactory, StdioMcpTransportFactory>();
// Commands.
builder.Services.AddTransient<NewCommand>();
builder.Services.AddTransient<InitCommand>();
builder.Services.AddTransient<RunCommand>();
builder.Services.AddTransient<StopCommand>();
builder.Services.AddTransient<StartCommand>();
builder.Services.AddTransient<RestartCommand>();
builder.Services.AddTransient<WaitCommand>();
builder.Services.AddTransient<ResourceCommand>();
builder.Services.AddTransient<PsCommand>();
builder.Services.AddTransient<ResourcesCommand>();
builder.Services.AddTransient<LogsCommand>();
builder.Services.AddTransient<AddCommand>();
builder.Services.AddTransient<PublishCommand>();
builder.Services.AddTransient<ConfigCommand>();
builder.Services.AddTransient<CacheCommand>();
builder.Services.AddTransient<DoctorCommand>();
builder.Services.AddTransient<UpdateCommand>();
builder.Services.AddTransient<DeployCommand>();
builder.Services.AddTransient<DoCommand>();
builder.Services.AddTransient<ExecCommand>();
builder.Services.AddTransient<McpCommand>();
builder.Services.AddTransient<McpStartCommand>();
builder.Services.AddTransient<McpInitCommand>();
builder.Services.AddTransient<AgentCommand>();
builder.Services.AddTransient<AgentMcpCommand>();
builder.Services.AddTransient<AgentInitCommand>();
builder.Services.AddTransient<TelemetryCommand>();
builder.Services.AddTransient<TelemetryLogsCommand>();
builder.Services.AddTransient<TelemetrySpansCommand>();
builder.Services.AddTransient<TelemetryTracesCommand>();
builder.Services.AddTransient<DocsCommand>();
builder.Services.AddTransient<DocsListCommand>();
builder.Services.AddTransient<DocsSearchCommand>();
builder.Services.AddTransient<DocsGetCommand>();
builder.Services.AddTransient<SdkCommand>();
builder.Services.AddTransient<SdkGenerateCommand>();
builder.Services.AddTransient<SdkDumpCommand>();
builder.Services.AddTransient<SetupCommand>();
builder.Services.AddTransient<RootCommand>();
builder.Services.AddTransient<ExtensionInternalCommand>();
var app = builder.Build();
return app;
}
private static DirectoryInfo GetHivesDirectory()
{
var homeDirectory = GetUsersAspirePath();
var hivesDirectory = Path.Combine(homeDirectory, "hives");
return new DirectoryInfo(hivesDirectory);
}
private static DirectoryInfo GetSdksDirectory()
{
var homeDirectory = GetUsersAspirePath();
var sdksPath = Path.Combine(homeDirectory, "sdks");
return new DirectoryInfo(sdksPath);
}
private static CliExecutionContext BuildCliExecutionContext(bool debugMode, string logsDirectory, string logFilePath)
{
var workingDirectory = new DirectoryInfo(Environment.CurrentDirectory);
var hivesDirectory = GetHivesDirectory();
var cacheDirectory = GetCacheDirectory();
var sdksDirectory = GetSdksDirectory();
return new CliExecutionContext(workingDirectory, hivesDirectory, cacheDirectory, sdksDirectory, new DirectoryInfo(logsDirectory), logFilePath, debugMode);
}
private static DirectoryInfo GetCacheDirectory()
{
var homeDirectory = GetUsersAspirePath();
var cacheDirectoryPath = Path.Combine(homeDirectory, "cache");
return new DirectoryInfo(cacheDirectoryPath);
}
private static async Task TrySetLocaleOverrideAsync(string? localeOverride)
{
if (localeOverride is not null)
{
var result = LocaleHelpers.TrySetLocaleOverride(localeOverride);
string errorMessage;
switch (result)
{
case SetLocaleResult.Success:
return;
case SetLocaleResult.InvalidLocale:
errorMessage = string.Format(CultureInfo.CurrentCulture, ErrorStrings.UnsupportedLocaleProvided, localeOverride, string.Join(", ", LocaleHelpers.SupportedLocales));
break;
case SetLocaleResult.UnsupportedLocale:
errorMessage = string.Format(CultureInfo.CurrentCulture, ErrorStrings.InvalidLocaleProvided, localeOverride);
break;
default:
throw new InvalidOperationException($"Unexpected result: {result}");
}
// This writes directly to Console.Error because services aren't available yet.
await Console.Error.WriteLineAsync(errorMessage);
}
}
private static IConfigurationService BuildConfigurationService(IServiceProvider serviceProvider)
{
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
var executionContext = serviceProvider.GetRequiredService<CliExecutionContext>();
var globalSettingsFile = new FileInfo(GetGlobalSettingsPath());
return new ConfigurationService(configuration, executionContext, globalSettingsFile);
}
internal static async Task DisplayFirstTimeUseNoticeIfNeededAsync(IServiceProvider serviceProvider, string[] args, CancellationToken cancellationToken = default)
{
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
var isInformationalCommand = args.Any(a => CommonOptionNames.InformationalOptionNames.Contains(a));
var noLogo = args.Any(a => a == CommonOptionNames.NoLogo) || configuration.GetBool(CliConfigNames.NoLogo, defaultValue: false) || isInformationalCommand;
var showBanner = args.Any(a => a == CommonOptionNames.Banner);
var sentinel = serviceProvider.GetRequiredService<IFirstTimeUseNoticeSentinel>();
var isFirstRun = !sentinel.Exists();
// Show banner if explicitly requested OR on first run (unless suppressed by noLogo)
if (showBanner || (isFirstRun && !noLogo))
{
var bannerService = serviceProvider.GetRequiredService<IBannerService>();
await bannerService.DisplayBannerAsync(cancellationToken);
}
// Only show telemetry notice on first run (not when banner is explicitly requested)
if (isFirstRun)
{
if (!noLogo)
{
// Write to stderr to avoid interfering with tools that parse stdout
var consoleEnvironment = serviceProvider.GetRequiredService<ConsoleEnvironment>();
consoleEnvironment.Error.WriteLine();
consoleEnvironment.Error.WriteLine(RootCommandStrings.FirstTimeUseTelemetryNotice);
consoleEnvironment.Error.WriteLine();
}
// Don't persist the sentinel for informational commands (--version, --help, etc.)
// so the first-run experience is shown on the next real command invocation.
if (!isInformationalCommand)
{
sentinel.CreateIfNotExists();
}
}
}
private static IAnsiConsole BuildAnsiConsole(IServiceProvider serviceProvider, TextWriter writer)
{
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
var hostEnvironment = serviceProvider.GetRequiredService<ICliHostEnvironment>();
var isPlayground = CliHostEnvironment.IsPlaygroundMode(configuration);
// Create custom output that handles width detection better in CI environments
// and encapsulates ASPIRE_CONSOLE_WIDTH environment variable handling
var output = new AspireAnsiConsoleOutput(writer, configuration);
var settings = new AnsiConsoleSettings()
{
Ansi = isPlayground ? AnsiSupport.Yes : AnsiSupport.Detect,
Interactive = isPlayground ? InteractionSupport.Yes : InteractionSupport.Detect,
ColorSystem = isPlayground ? ColorSystemSupport.Standard : ColorSystemSupport.Detect,
Out = output,
};
// Use SupportsAnsi from hostEnvironment which already checks ASPIRE_ANSI_PASS_THRU
if (hostEnvironment.SupportsAnsi)
{
settings.Ansi = AnsiSupport.Yes;
// Using EightBit color system for better color support of Aspire brand colors in terminals that support ANSI
settings.ColorSystem = ColorSystemSupport.EightBit;
}
if (isPlayground)
{
// Enrichers interfere with interactive playground experience so
// this suppresses the default enrichers so that the CLI experience
// is more like what we would get in an interactive experience.
settings.Enrichment.UseDefaultEnrichers = false;
settings.Enrichment.Enrichers = new()
{
new AspirePlaygroundEnricher()
};
}
var ansiConsole = AnsiConsole.Create(settings);
return ansiConsole;
}
public static async Task<int> Main(string[] args)
{
// Setup handling of CTRL-C as early as possible so that if
// we get a CTRL-C anywhere that is not handled by Spectre Console
// already that we know to trigger cancellation.
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (sender, eventArgs) =>
{
cts.Cancel();
eventArgs.Cancel = true;
};
Console.OutputEncoding = Encoding.UTF8;
using var app = await BuildApplicationAsync(args);
await app.StartAsync().ConfigureAwait(false);
// Display first run experience if this is the first time the CLI is run on this machine
await DisplayFirstTimeUseNoticeIfNeededAsync(app.Services, args, cts.Token);
var rootCommand = app.Services.GetRequiredService<RootCommand>();
var invokeConfig = new InvocationConfiguration()
{
// Disable default exception handler so we can log exceptions to telemetry.
EnableDefaultExceptionHandler = false
};
var telemetry = app.Services.GetRequiredService<AspireCliTelemetry>();
var telemetryManager = app.Services.GetRequiredService<TelemetryManager>();
var logger = app.Services.GetRequiredService<ILogger<Program>>();
using var mainActivity = telemetry.StartReportedActivity(name: TelemetryConstants.Activities.Main, kind: ActivityKind.Internal);
if (mainActivity != null)
{
var currentProcess = Process.GetCurrentProcess();
mainActivity.SetStartTime(currentProcess.StartTime);
mainActivity.AddTag(TelemetryConstants.Tags.ProcessPid, currentProcess.Id);
mainActivity.AddTag(TelemetryConstants.Tags.ProcessExecutableName, "aspire");
}
// Create a dedicated logger for CLI session info
var cliLogger = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger<Program>();
try
{
// Log command invocation details for debugging
var commandLine = args.Length > 0 ? $"aspire {string.Join(" ", args)}" : "aspire";
var workingDir = Environment.CurrentDirectory;
cliLogger.LogInformation("Command: {CommandLine}", commandLine);
cliLogger.LogInformation("Working directory: {WorkingDirectory}", workingDir);
logger.LogDebug("Parsing arguments: {Args}", string.Join(" ", args));
var parseResult = rootCommand.Parse(args);
var commandName = GetCommandName(parseResult);
logger.LogDebug("Executing command: {CommandName}", commandName);
mainActivity?.SetTag(TelemetryConstants.Tags.CommandName, commandName);
var exitCode = await parseResult.InvokeAsync(invokeConfig, cts.Token);
// Log exit code for debugging
cliLogger.LogInformation("Exit code: {ExitCode}", exitCode);
mainActivity?.SetTag(TelemetryConstants.Tags.ProcessExitCode, exitCode);
mainActivity?.Stop();
return exitCode;
}
catch (Exception ex)
{
const int unknownErrorExitCode = 1;
// Catch block is used instead of System.Commandline's default handler behavior.
// Allows logging of exceptions to telemetry.
// Don't log or display cancellation exceptions.
if (!(ex is OperationCanceledException && cts.IsCancellationRequested))
{
logger.LogError(ex, "An unexpected error occurred.");
telemetry.RecordError("An unexpected error occurred.", ex);
var interactionService = app.Services.GetRequiredService<IInteractionService>();
interactionService.DisplayError(string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.UnexpectedErrorOccurred, ex.Message));
}
// Log exit code for debugging
cliLogger.LogError("Exit code: {ExitCode} (exception)", unknownErrorExitCode);
mainActivity?.SetTag(TelemetryConstants.Tags.ProcessExitCode, unknownErrorExitCode);
mainActivity?.Stop();
return unknownErrorExitCode;
}
finally
{
// Shutting down telemetry manager to flush any remaining telemetry and will take time.
// Start shutdown of telemetry manager immediately and run concurrently with app shutdown.
var shutdownTelemetryTask = telemetryManager.ShutdownAsync();
await app.StopAsync().ConfigureAwait(false);
await shutdownTelemetryTask;
}
}
private static string GetCommandName(ParseResult r)
{
// Walk the parent command tree to find the top-level command name and get the full command name for this parseresult.
var parentNames = new List<string> { r.CommandResult.Command.Name };
var current = r.CommandResult.Parent;
while (current is CommandResult parentCommandResult)
{
parentNames.Add(parentCommandResult.Command.Name);
current = parentCommandResult.Parent;
}
parentNames.Reverse();
return string.Join(' ', parentNames);
}
private static void AddInteractionServices(HostApplicationBuilder builder)
{
var extensionEndpoint = builder.Configuration[KnownConfigNames.ExtensionEndpoint];
if (extensionEndpoint is not null)
{
builder.Services.AddSingleton<IExtensionRpcTarget, ExtensionRpcTarget>();
builder.Services.AddSingleton<IExtensionBackchannel, ExtensionBackchannel>();
var extensionPromptEnabled = builder.Configuration[KnownConfigNames.ExtensionPromptEnabled] is "true";
builder.Services.AddSingleton<IInteractionService>(provider =>
{
var consoleEnvironment = provider.GetRequiredService<ConsoleEnvironment>();
consoleEnvironment.Out.Profile.Width = 256; // VS code terminal will handle wrapping so set a large width here.
var executionContext = provider.GetRequiredService<CliExecutionContext>();
var hostEnvironment = provider.GetRequiredService<ICliHostEnvironment>();
var consoleInteractionService = new ConsoleInteractionService(consoleEnvironment, executionContext, hostEnvironment);
return new ExtensionInteractionService(consoleInteractionService,
provider.GetRequiredService<IExtensionBackchannel>(),
extensionPromptEnabled);
});
}
else
{
builder.Services.AddSingleton<IInteractionService>(provider =>
{
var consoleEnvironment = provider.GetRequiredService<ConsoleEnvironment>();
var executionContext = provider.GetRequiredService<CliExecutionContext>();
var hostEnvironment = provider.GetRequiredService<ICliHostEnvironment>();
return new ConsoleInteractionService(consoleEnvironment, executionContext, hostEnvironment);
});
}
}
}
internal class AspirePlaygroundEnricher : IProfileEnricher
{
public string Name => "Aspire Playground";
public bool Enabled(IDictionary<string, string> environmentVariables)
{
if (!environmentVariables.TryGetValue("ASPIRE_PLAYGROUND", out var value))
{
return false;
}
if (!bool.TryParse(value, out var isEnabled))
{
return false;
}
return isEnabled;
}
public void Enrich(Profile profile)
{
profile.Capabilities.Interactive = true;
}
}
|