diff --git a/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs b/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs index a61855bda5a..7558ea489b4 100644 --- a/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs +++ b/src/Aspire.Cli/Backchannel/AppHostBackchannel.cs @@ -11,13 +11,13 @@ namespace Aspire.Cli.Backchannel; internal sealed class AppHostBackchannel(ILogger logger, CliRpcTarget target) { - private readonly ActivitySource _activitySource = new(nameof(Aspire.Cli.Backchannel.AppHostBackchannel), "1.0.0"); + private readonly ActivitySource _activitySource = new(nameof(AppHostBackchannel)); private readonly TaskCompletionSource _rpcTaskCompletionSource = new(); private Process? _process; public async Task PingAsync(long timestamp, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(PingAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); var rpc = await _rpcTaskCompletionSource.Task; @@ -37,7 +37,7 @@ public async Task RequestStopAsync(CancellationToken cancellationToken) // of the AppHost process. The AppHost process will then trigger the shutdown // which will allow the CLI to await the pending run. - using var activity = _activitySource.StartActivity(nameof(RequestStopAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); var rpc = await _rpcTaskCompletionSource.Task; @@ -51,7 +51,7 @@ await rpc.InvokeWithCancellationAsync( public async Task<(string BaseUrlWithLoginToken, string? CodespacesUrlWithLoginToken)> GetDashboardUrlsAsync(CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(GetDashboardUrlsAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); var rpc = await _rpcTaskCompletionSource.Task; @@ -67,7 +67,7 @@ await rpc.InvokeWithCancellationAsync( public async IAsyncEnumerable<(string Resource, string Type, string State, string[] Endpoints)> GetResourceStatesAsync([EnumeratorCancellation]CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(GetResourceStatesAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); var rpc = await _rpcTaskCompletionSource.Task; @@ -88,7 +88,7 @@ await rpc.InvokeWithCancellationAsync( public async Task ConnectAsync(Process process, string socketPath, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(ConnectAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); _process = process; @@ -111,7 +111,7 @@ public async Task ConnectAsync(Process process, string socketPath, CancellationT public async Task GetPublishersAsync(CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(GetPublishersAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); var rpc = await _rpcTaskCompletionSource.Task.ConfigureAwait(false); @@ -127,7 +127,7 @@ public async Task GetPublishersAsync(CancellationToken cancellationTok public async IAsyncEnumerable<(string Id, string StatusText, bool IsComplete, bool IsError)> GetPublishingActivitiesAsync([EnumeratorCancellation]CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(GetPublishingActivitiesAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); var rpc = await _rpcTaskCompletionSource.Task; diff --git a/src/Aspire.Cli/Commands/AddCommand.cs b/src/Aspire.Cli/Commands/AddCommand.cs index 23c4380db1c..03d9545df9e 100644 --- a/src/Aspire.Cli/Commands/AddCommand.cs +++ b/src/Aspire.Cli/Commands/AddCommand.cs @@ -11,7 +11,7 @@ namespace Aspire.Cli.Commands; internal sealed class AddCommand : BaseCommand { - private readonly ActivitySource _activitySource = new ActivitySource("Aspire.Cli"); + private readonly ActivitySource _activitySource = new ActivitySource(nameof(AddCommand)); private readonly DotNetCliRunner _runner; private readonly INuGetPackageCache _nuGetPackageCache; @@ -42,7 +42,7 @@ public AddCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache) protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity($"{nameof(ExecuteAsync)}", ActivityKind.Internal); + using var activity = _activitySource.StartActivity(); try { @@ -62,7 +62,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell var packages = await AnsiConsole.Status().StartAsync( "Searching for Aspire packages...", - context => _nuGetPackageCache.GetPackagesAsync(effectiveAppHostProjectFile, prerelease, source, cancellationToken) + context => _nuGetPackageCache.GetPackagesAsync(effectiveAppHostProjectFile.Directory!, prerelease, source, cancellationToken) ); var version = parseResult.GetValue("--version"); diff --git a/src/Aspire.Cli/Commands/NewCommand.cs b/src/Aspire.Cli/Commands/NewCommand.cs index 7cbc4f104e1..f676383e29d 100644 --- a/src/Aspire.Cli/Commands/NewCommand.cs +++ b/src/Aspire.Cli/Commands/NewCommand.cs @@ -2,25 +2,27 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.CommandLine; -using System.CommandLine.Parsing; using System.Diagnostics; using Aspire.Cli.Utils; +using Semver; using Spectre.Console; namespace Aspire.Cli.Commands; internal sealed class NewCommand : BaseCommand { - private readonly ActivitySource _activitySource = new ActivitySource("Aspire.Cli"); + private readonly ActivitySource _activitySource = new ActivitySource(nameof(NewCommand)); private readonly DotNetCliRunner _runner; + private readonly INuGetPackageCache _nuGetPackageCache; - public NewCommand(DotNetCliRunner runner) : base("new", "Create a new Aspire sample project.") + public NewCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache) : base("new", "Create a new Aspire sample project.") { ArgumentNullException.ThrowIfNull(runner, nameof(runner)); + ArgumentNullException.ThrowIfNull(nuGetPackageCache, nameof(nuGetPackageCache)); _runner = runner; + _nuGetPackageCache = nuGetPackageCache; var templateArgument = new Argument("template"); - templateArgument.Validators.Add(ValidateProjectTemplate); templateArgument.Arity = ArgumentArity.ZeroOrOne; Arguments.Add(templateArgument); @@ -29,9 +31,6 @@ internal sealed class NewCommand : BaseCommand var outputOption = new Option("--output", "-o"); Options.Add(outputOption); - - var prereleaseOption = new Option("--prerelease"); - Options.Add(prereleaseOption); var sourceOption = new Option("--source", "-s"); Options.Add(sourceOption); @@ -40,7 +39,7 @@ internal sealed class NewCommand : BaseCommand Options.Add(templateVersionOption); } - private static void ValidateProjectTemplate(ArgumentResult result) + private static async Task<(string TemplateName, string TemplateDescription, string? PathAppendage)> GetProjectTemplateAsync(ParseResult parseResult, CancellationToken cancellationToken) { // TODO: We need to integrate with the template engine to interrogate // the list of available templates. For now we will just hard-code @@ -49,55 +48,91 @@ private static void ValidateProjectTemplate(ArgumentResult result) // Once we integrate with template engine we will also be able to // interrogate the various options and add them. For now we will // keep it simple. - string[] validTemplates = [ - "aspire-starter", - "aspire", - "aspire-apphost", - "aspire-servicedefaults", - "aspire-mstest", - "aspire-nunit", - "aspire-xunit" + (string TemplateName, string TemplateDescription, string? PathAppendage)[] validTemplates = [ + ("aspire-starter", "Aspire Starter App", "src") , + ("aspire", "Aspire Empty App", "src"), + ("aspire-apphost", "Aspire App Host", null), + ("aspire-servicedefaults", "Aspire Service Defaults", null), + ("aspire-mstest", "Aspire Test Project (MSTest)", null), + ("aspire-nunit", "Aspire Test Project (NUnit)", null), + ("aspire-xunit", "Aspire Test Project (xUnit)", null) ]; - var value = result.GetValueOrDefault(); - - if (value is null) + if (parseResult.GetValue("template") is { } templateName && validTemplates.SingleOrDefault(t => t.TemplateName == templateName) is { } template) { - // This is OK, for now we will use the default - // template of aspire-starter, but we might - // be able to do more intelligent selection in the - // future based on what is already in the working directory. - return; + return template; } - - if (value is { } templateName && !validTemplates.Contains(templateName)) + else { - result.AddError($"The specified template '{templateName}' is not valid. Valid templates are [{string.Join(", ", validTemplates)}]."); - return; + return await PromptUtils.PromptForSelectionAsync( + "Select a project template:", + validTemplates, + t => $"{t.TemplateName} ({t.TemplateDescription})", + cancellationToken + ); } } - protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) + private static async Task GetProjectNameAsync(ParseResult parseResult, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity($"{nameof(ExecuteAsync)}", ActivityKind.Internal); + if (parseResult.GetValue("--name") is not { } name) + { + var defaultName = new DirectoryInfo(Environment.CurrentDirectory).Name; + name = await PromptUtils.PromptForStringAsync("Enter the project name:", + defaultValue: defaultName, + cancellationToken: cancellationToken); + } - var templateVersion = parseResult.GetValue("--version"); - var prerelease = parseResult.GetValue("--prerelease"); + return name; + } - if (templateVersion is not null && prerelease) + private static async Task GetOutputPathAsync(ParseResult parseResult, string? pathAppendage, CancellationToken cancellationToken) + { + if (parseResult.GetValue("--output") is not { } outputPath) { - AnsiConsole.MarkupLine("[red bold]:thumbs_down: The --version and --prerelease options are mutually exclusive.[/]"); - return ExitCodeConstants.FailedToCreateNewProject; + outputPath = await PromptUtils.PromptForStringAsync( + "Enter the output path:", + defaultValue: Path.Combine(Environment.CurrentDirectory, pathAppendage ?? string.Empty), + cancellationToken: cancellationToken + ); } - else if (prerelease) + + return Path.GetFullPath(outputPath); + } + + private static async Task GetProjectTemplatesVersionAsync(ParseResult parseResult, CancellationToken cancellationToken) + { + if (parseResult.GetValue("--version") is { } version) { - templateVersion = "*-*"; + return version; } - else if (templateVersion is null) + else { - templateVersion = VersionHelper.GetDefaultTemplateVersion(); + version = await PromptUtils.PromptForStringAsync( + "Project templates version:", + defaultValue: VersionHelper.GetDefaultTemplateVersion(), + validator: (string value) => { + if (SemVersion.TryParse(value, out var parsedVersion)) + { + return ValidationResult.Success(); + } + + return ValidationResult.Error("Invalid version format. Please enter a valid version."); + }, + cancellationToken); + + return version; } + } + protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) + { + using var activity = _activitySource.StartActivity(); + + var template = await GetProjectTemplateAsync(parseResult, cancellationToken); + var name = await GetProjectNameAsync(parseResult, cancellationToken); + var outputPath = await GetOutputPathAsync(parseResult, template.PathAppendage, cancellationToken); + var version = await GetProjectTemplatesVersionAsync(parseResult, cancellationToken); var source = parseResult.GetValue("--source"); var templateInstallResult = await AnsiConsole.Status() @@ -106,7 +141,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell .StartAsync( ":ice: Getting latest templates...", async context => { - return await _runner.InstallTemplateAsync("Aspire.ProjectTemplates", templateVersion!, source, true, cancellationToken); + return await _runner.InstallTemplateAsync("Aspire.ProjectTemplates", version, source, true, cancellationToken); }); if (templateInstallResult.ExitCode != 0) @@ -117,23 +152,6 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell AnsiConsole.MarkupLine($":package: Using project templates version: {templateInstallResult.TemplateVersion}"); - var templateName = parseResult.GetValue("template") ?? "aspire-starter"; - - if (parseResult.GetValue("--output") is not { } outputPath) - { - outputPath = Environment.CurrentDirectory; - } - else - { - outputPath = Path.GetFullPath(outputPath); - } - - if (parseResult.GetValue("--name") is not { } name) - { - var outputPathDirectoryInfo = new DirectoryInfo(outputPath); - name = outputPathDirectoryInfo.Name; - } - int newProjectExitCode = await AnsiConsole.Status() .Spinner(Spinner.Known.Dots3) .SpinnerStyle(Style.Parse("purple")) @@ -141,11 +159,11 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell ":rocket: Creating new Aspire project...", async context => { return await _runner.NewProjectAsync( - templateName, - name, - outputPath, - cancellationToken); - }); + template.TemplateName, + name, + outputPath, + cancellationToken); + }); if (newProjectExitCode != 0) { diff --git a/src/Aspire.Cli/Commands/PublishCommand.cs b/src/Aspire.Cli/Commands/PublishCommand.cs index 496a282bf2f..f25893beb53 100644 --- a/src/Aspire.Cli/Commands/PublishCommand.cs +++ b/src/Aspire.Cli/Commands/PublishCommand.cs @@ -12,7 +12,7 @@ namespace Aspire.Cli.Commands; internal sealed class PublishCommand : BaseCommand { - private readonly ActivitySource _activitySource = new ActivitySource("Aspire.Cli"); + private readonly ActivitySource _activitySource = new ActivitySource(nameof(PublishCommand)); private readonly DotNetCliRunner _runner; public PublishCommand(DotNetCliRunner runner) : base("publish", "Generates deployment artifacts for an Aspire app host project.") @@ -34,7 +34,7 @@ internal sealed class PublishCommand : BaseCommand protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity($"{nameof(ExecuteAsync)}", ActivityKind.Internal); + using var activity = _activitySource.StartActivity(); var passedAppHostProjectFile = parseResult.GetValue("--project"); var effectiveAppHostProjectFile = ProjectFileHelper.UseOrFindAppHostProjectFile(passedAppHostProjectFile); diff --git a/src/Aspire.Cli/Commands/RunCommand.cs b/src/Aspire.Cli/Commands/RunCommand.cs index 566667fd8d1..f5067dffa53 100644 --- a/src/Aspire.Cli/Commands/RunCommand.cs +++ b/src/Aspire.Cli/Commands/RunCommand.cs @@ -14,7 +14,7 @@ namespace Aspire.Cli.Commands; internal sealed class RunCommand : BaseCommand { - private readonly ActivitySource _activitySource = new ActivitySource("Aspire.Cli"); + private readonly ActivitySource _activitySource = new ActivitySource(nameof(RunCommand)); private readonly DotNetCliRunner _runner; public RunCommand(DotNetCliRunner runner) : base("run", "Run an Aspire app host in development mode.") @@ -33,7 +33,7 @@ public RunCommand(DotNetCliRunner runner) : base("run", "Run an Aspire app host protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity($"{nameof(ExecuteAsync)}", ActivityKind.Internal); + using var activity = _activitySource.StartActivity(); var passedAppHostProjectFile = parseResult.GetValue("--project"); var effectiveAppHostProjectFile = ProjectFileHelper.UseOrFindAppHostProjectFile(passedAppHostProjectFile); diff --git a/src/Aspire.Cli/DotNetCliRunner.cs b/src/Aspire.Cli/DotNetCliRunner.cs index ae8188cd700..40eafffa202 100644 --- a/src/Aspire.Cli/DotNetCliRunner.cs +++ b/src/Aspire.Cli/DotNetCliRunner.cs @@ -15,13 +15,13 @@ namespace Aspire.Cli; internal sealed class DotNetCliRunner(ILogger logger, IServiceProvider serviceProvider) { - private readonly ActivitySource _activitySource = new ActivitySource(nameof(Aspire.Cli.DotNetCliRunner)); + private readonly ActivitySource _activitySource = new ActivitySource(nameof(DotNetCliRunner)); internal Func GetCurrentProcessId { get; set; } = () => Environment.ProcessId; public async Task<(int ExitCode, bool IsAspireHost, string? AspireHostingSdkVersion)> GetAppHostInformationAsync(FileInfo projectFile, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(GetAppHostInformationAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); string[] cliArgs = ["msbuild", "-getproperty:IsAspireHost,AspireHostingSDKVersion"]; @@ -79,7 +79,7 @@ internal sealed class DotNetCliRunner(ILogger logger, IServiceP public async Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, string[] args, IDictionary? env, TaskCompletionSource? backchannelCompletionSource, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(RunAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); if (watch && noBuild) { @@ -100,7 +100,7 @@ public async Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, public async Task CheckHttpCertificateAsync(CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(CheckHttpCertificateAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); string[] cliArgs = ["dev-certs", "https", "--check", "--trust"]; return await ExecuteAsync( @@ -114,7 +114,7 @@ public async Task CheckHttpCertificateAsync(CancellationToken cancellationT public async Task TrustHttpCertificateAsync(CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(TrustHttpCertificateAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); string[] cliArgs = ["dev-certs", "https", "--trust"]; return await ExecuteAsync( @@ -230,7 +230,7 @@ private static bool TryParsePackageVersionFromStdout(string stdout, [NotNullWhen public async Task NewProjectAsync(string templateName, string name, string outputPath, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(NewProjectAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); string[] cliArgs = ["new", templateName, "--name", name, "--output", outputPath]; return await ExecuteAsync( @@ -259,7 +259,7 @@ internal static string GetBackchannelSocketPath() public async Task ExecuteAsync(string[] args, IDictionary? env, DirectoryInfo workingDirectory, TaskCompletionSource? backchannelCompletionSource, Action? streamsCallback, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(ExecuteAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); var startInfo = new ProcessStartInfo("dotnet") { @@ -379,7 +379,7 @@ async Task ForwardStreamToLoggerAsync(StreamReader reader, string identifier, Pr private async Task StartBackchannelAsync(Process process, string socketPath, TaskCompletionSource backchannelCompletionSource, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(StartBackchannelAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(50)); @@ -431,7 +431,7 @@ private async Task StartBackchannelAsync(Process process, string socketPath, Tas public async Task BuildAsync(FileInfo projectFilePath, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(BuildAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); string[] cliArgs = ["build", projectFilePath.FullName]; return await ExecuteAsync( @@ -444,7 +444,7 @@ public async Task BuildAsync(FileInfo projectFilePath, CancellationToken ca } public async Task AddPackageAsync(FileInfo projectFilePath, string packageName, string packageVersion, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(AddPackageAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); string[] cliArgs = [ "add", @@ -477,9 +477,9 @@ public async Task AddPackageAsync(FileInfo projectFilePath, string packageN return result; } - public async Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(FileInfo projectFilePath, string query, bool prerelease, int take, int skip, string? nugetSource, CancellationToken cancellationToken) + public async Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, string? nugetSource, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(SearchPackagesAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); List cliArgs = [ "package", @@ -510,7 +510,7 @@ public async Task AddPackageAsync(FileInfo projectFilePath, string packageN var result = await ExecuteAsync( args: cliArgs.ToArray(), env: null, - workingDirectory: projectFilePath.Directory!, + workingDirectory: workingDirectory!, backchannelCompletionSource: null, streamsCallback: (_, output, _) => { // We need to read the output of the streams @@ -550,6 +550,12 @@ public async Task AddPackageAsync(FileInfo projectFilePath, string packageN foreach (var packageResult in sourcePackagesArray.EnumerateArray()) { var id = packageResult.GetProperty("id").GetString(); + + // var version = prerelease switch { + // true => packageResult.GetProperty("version").GetString(), + // false => packageResult.GetProperty("latestVersion").GetString() + // }; + var version = packageResult.GetProperty("latestVersion").GetString(); foundPackages.Add(new NuGetPackage diff --git a/src/Aspire.Cli/NuGetPackageCache.cs b/src/Aspire.Cli/NuGetPackageCache.cs index ef90821c873..402c99a9f92 100644 --- a/src/Aspire.Cli/NuGetPackageCache.cs +++ b/src/Aspire.Cli/NuGetPackageCache.cs @@ -8,18 +8,18 @@ namespace Aspire.Cli; internal interface INuGetPackageCache { - Task> GetPackagesAsync(FileInfo projectFile, bool prerelease, string? source, CancellationToken cancellationToken); + Task> GetPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, string? source, CancellationToken cancellationToken); } internal sealed class NuGetPackageCache(ILogger logger, DotNetCliRunner cliRunner) : INuGetPackageCache { - private readonly ActivitySource _activitySource = new(nameof(Aspire.Cli.NuGetPackageCache), "1.0.0"); + private readonly ActivitySource _activitySource = new(nameof(NuGetPackageCache)); private const int SearchPageSize = 100; - public async Task> GetPackagesAsync(FileInfo projectFile, bool prerelease, string? source, CancellationToken cancellationToken) + public async Task> GetPackagesAsync(DirectoryInfo workingDirectory, bool prerelease, string? source, CancellationToken cancellationToken) { - using var activity = _activitySource.StartActivity(nameof(GetPackagesAsync), ActivityKind.Client); + using var activity = _activitySource.StartActivity(); logger.LogDebug("Getting integrations from NuGet"); @@ -31,7 +31,7 @@ public async Task> GetPackagesAsync(FileInfo projectFi { // This search should pick up Aspire.Hosting.* and CommunityToolkit.Aspire.Hosting.* var result = await cliRunner.SearchPackagesAsync( - projectFile, + workingDirectory, "Aspire.Hosting", prerelease, SearchPageSize, diff --git a/src/Aspire.Cli/Program.cs b/src/Aspire.Cli/Program.cs index 37ec3f33636..9d86c0ee70a 100644 --- a/src/Aspire.Cli/Program.cs +++ b/src/Aspire.Cli/Program.cs @@ -10,13 +10,15 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using OpenTelemetry; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; using RootCommand = Aspire.Cli.Commands.RootCommand; namespace Aspire.Cli; public class Program { - private static readonly ActivitySource s_activitySource = new ActivitySource(nameof(Aspire.Cli.Program)); + private static readonly ActivitySource s_activitySource = new ActivitySource(nameof(Program)); private static IHost BuildApplication(string[] args) { @@ -30,14 +32,22 @@ private static IHost BuildApplication(string[] args) logging.IncludeScopes = true; }); - var otelBuilder = builder.Services.AddOpenTelemetry() - .WithTracing(tracing => { - tracing.AddSource( - nameof(Aspire.Cli.NuGetPackageCache), - nameof(Aspire.Cli.Backchannel.AppHostBackchannel), - nameof(Aspire.Cli.DotNetCliRunner), - nameof(Aspire.Cli.Program)); - }); + var otelBuilder = builder.Services + .AddOpenTelemetry() + .WithTracing(tracing => { + tracing.AddSource( + nameof(NuGetPackageCache), + nameof(AppHostBackchannel), + nameof(DotNetCliRunner), + nameof(Program), + nameof(NewCommand), + nameof(RunCommand), + nameof(AddCommand), + nameof(PublishCommand) + ); + + tracing.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("aspire-cli")); + }); if (builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"] is {}) { @@ -84,7 +94,7 @@ public static async Task Main(string[] args) var config = new CommandLineConfiguration(rootCommand); config.EnableDefaultExceptionHandler = true; - using var activity = s_activitySource.StartActivity(nameof(Main), ActivityKind.Internal); + using var activity = s_activitySource.StartActivity(); var exitCode = await config.InvokeAsync(args); await app.StopAsync().ConfigureAwait(false); diff --git a/src/Aspire.Cli/Utils/PromptUtils.cs b/src/Aspire.Cli/Utils/PromptUtils.cs new file mode 100644 index 00000000000..4b00b885e1d --- /dev/null +++ b/src/Aspire.Cli/Utils/PromptUtils.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Spectre.Console; + +namespace Aspire.Cli.Utils; + +internal static class PromptUtils +{ + public static async Task PromptForStringAsync(string promptText, string? defaultValue = null, Func? validator = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(promptText, nameof(promptText)); + var prompt = new TextPrompt(promptText); + + if (defaultValue is not null) + { + prompt.DefaultValue(defaultValue); + prompt.ShowDefaultValue(); + } + + if (validator is not null) + { + prompt.Validate(validator); + } + + return await AnsiConsole.PromptAsync(prompt, cancellationToken); + } + + public static async Task PromptForSelectionAsync(string promptText, IEnumerable choices, Func choiceFormatter, CancellationToken cancellationToken = default) where T: notnull + { + ArgumentNullException.ThrowIfNull(promptText, nameof(promptText)); + ArgumentNullException.ThrowIfNull(choices, nameof(choices)); + ArgumentNullException.ThrowIfNull(choiceFormatter, nameof(choiceFormatter)); + + var prompt = new SelectionPrompt() + .Title(promptText) + .UseConverter(choiceFormatter) + .AddChoices(choices) + .PageSize(10) + .EnableSearch() + .HighlightStyle(Style.Parse("darkmagenta")); + + return await AnsiConsole.PromptAsync(prompt, cancellationToken); + } +} \ No newline at end of file