Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 49 additions & 32 deletions src/Aspire.Cli/Commands/AddCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,22 @@ internal sealed class AddCommand : BaseCommand
private readonly INuGetPackageCache _nuGetPackageCache;
private readonly IInteractionService _interactionService;
private readonly IProjectLocator _projectLocator;
private readonly IAddCommandPrompter _prompter;

public AddCommand(IDotNetCliRunner runner, INuGetPackageCache nuGetPackageCache, IInteractionService interactionService, IProjectLocator projectLocator)
public AddCommand(IDotNetCliRunner runner, INuGetPackageCache nuGetPackageCache, IInteractionService interactionService, IProjectLocator projectLocator, IAddCommandPrompter prompter)
: base("add", "Add an integration to the Aspire project.")
{
ArgumentNullException.ThrowIfNull(runner);
ArgumentNullException.ThrowIfNull(nuGetPackageCache);
ArgumentNullException.ThrowIfNull(interactionService);
ArgumentNullException.ThrowIfNull(projectLocator);

ArgumentNullException.ThrowIfNull(prompter);

_runner = runner;
_nuGetPackageCache = nuGetPackageCache;
_interactionService = interactionService;
_projectLocator = projectLocator;
_prompter = prompter;

var integrationArgument = new Argument<string>("integration");
integrationArgument.Description = "The name of the integration to add (e.g. redis, postgres).";
Expand Down Expand Up @@ -170,23 +173,11 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
{
var distinctPackages = possiblePackages.DistinctBy(p => p.Package.Id);

var packagePrompt = new SelectionPrompt<(string FriendlyName, NuGetPackage Package)>()
.Title("Select an integration to add:")
.UseConverter(PackageNameWithFriendlyNameIfAvailable)
.PageSize(10)
.EnableSearch()
.HighlightStyle(Style.Parse("darkmagenta"))
.AddChoices(distinctPackages);

// If there is only one package, we can skip the prompt and just use it.
var selectedPackage = distinctPackages.Count() switch
{
1 => distinctPackages.First(),
> 1 => await _interactionService.PromptForSelectionAsync(
"Select an integration to add:",
distinctPackages,
PackageNameWithFriendlyNameIfAvailable,
cancellationToken),
> 1 => await _prompter.PromptForIntegrationAsync(distinctPackages, cancellationToken),
_ => throw new InvalidOperationException("Unexpected number of packages found.")
};

Expand All @@ -201,25 +192,9 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
}

// ... otherwise we had better prompt.
var version = await _interactionService.PromptForSelectionAsync(
$"Select a version of the {selectedPackage.Package.Id}:",
packageVersions,
p => p.Package.Version,
cancellationToken);
var version = await _prompter.PromptForIntegrationVersionAsync(packageVersions, cancellationToken);

return version;

static string PackageNameWithFriendlyNameIfAvailable((string FriendlyName, NuGetPackage Package) packageWithFriendlyName)
{
if (packageWithFriendlyName.FriendlyName is { } friendlyName)
{
return $"[bold]{friendlyName}[/] ({packageWithFriendlyName.Package.Id})";
}
else
{
return packageWithFriendlyName.Package.Id;
}
}
}

private static (string FriendlyName, NuGetPackage Package) GenerateFriendlyName(NuGetPackage package)
Expand All @@ -244,3 +219,45 @@ private static (string FriendlyName, NuGetPackage Package) GenerateFriendlyName(
return (shortNameBuilder.ToString(), package);
}
}

internal interface IAddCommandPrompter
{
Task<(string FriendlyName, NuGetPackage Package)> PromptForIntegrationAsync(IEnumerable<(string FriendlyName, NuGetPackage Package)> packages, CancellationToken cancellationToken);
Task<(string FriendlyName, NuGetPackage Package)> PromptForIntegrationVersionAsync(IEnumerable<(string FriendlyName, NuGetPackage Package)> packages, CancellationToken cancellationToken);
}

internal class AddCommandPrompter(IInteractionService interactionService) : IAddCommandPrompter
{
public virtual async Task<(string FriendlyName, NuGetPackage Package)> PromptForIntegrationVersionAsync(IEnumerable<(string FriendlyName, NuGetPackage Package)> packages, CancellationToken cancellationToken)
{
var selectedPackage = packages.First();
var version = await interactionService.PromptForSelectionAsync(
$"Select a version of the {selectedPackage.Package.Id}:",
packages,
p => p.Package.Version,
cancellationToken);
return version;
}

public virtual async Task<(string FriendlyName, NuGetPackage Package)> PromptForIntegrationAsync(IEnumerable<(string FriendlyName, NuGetPackage Package)> packages, CancellationToken cancellationToken)
{
var selectedIntegration = await interactionService.PromptForSelectionAsync(
"Select an integration to add:",
packages,
PackageNameWithFriendlyNameIfAvailable,
cancellationToken);
return selectedIntegration;
}

private static string PackageNameWithFriendlyNameIfAvailable((string FriendlyName, NuGetPackage Package) packageWithFriendlyName)
{
if (packageWithFriendlyName.FriendlyName is { } friendlyName)
{
return $"[bold]{friendlyName}[/] ({packageWithFriendlyName.Package.Id})";
}
else
{
return packageWithFriendlyName.Package.Id;
}
}
}
42 changes: 27 additions & 15 deletions src/Aspire.Cli/Interaction/InteractionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,29 @@ namespace Aspire.Cli.Interaction;

internal class InteractionService : IInteractionService
{
private readonly IAnsiConsole _ansiConsole;

public InteractionService() : this(AnsiConsole.Console)
{
}

public InteractionService(IAnsiConsole ansiConsole)
{
ArgumentNullException.ThrowIfNull(ansiConsole);
_ansiConsole = ansiConsole;
}

public async Task<T> ShowStatusAsync<T>(string statusText, Func<Task<T>> action)
{
return await AnsiConsole.Status()
return await _ansiConsole.Status()
.Spinner(Spinner.Known.Dots3)
.SpinnerStyle(Style.Parse("purple"))
.StartAsync(statusText, (context) => action());
}

public void ShowStatus(string statusText, Action action)
{
AnsiConsole.Status()
_ansiConsole.Status()
.Spinner(Spinner.Known.Dots3)
.SpinnerStyle(Style.Parse("purple"))
.Start(statusText, (context) => action());
Expand All @@ -40,8 +52,8 @@ public async Task<string> PromptForStringAsync(string promptText, string? defaul
{
prompt.Validate(validator);
}

return await AnsiConsole.PromptAsync(prompt, cancellationToken);
return await _ansiConsole.PromptAsync(prompt, cancellationToken);
}

public async Task<T> PromptForSelectionAsync<T>(string promptText, IEnumerable<T> choices, Func<T, string> choiceFormatter, CancellationToken cancellationToken = default) where T : notnull
Expand All @@ -58,7 +70,7 @@ public async Task<T> PromptForSelectionAsync<T>(string promptText, IEnumerable<T
.EnableSearch()
.HighlightStyle(Style.Parse("darkmagenta"));

return await AnsiConsole.PromptAsync(prompt, cancellationToken);
return await _ansiConsole.PromptAsync(prompt, cancellationToken);
}

public int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, string appHostHostingSdkVersion)
Expand All @@ -67,9 +79,9 @@ public int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, stri

DisplayError("The app host is not compatible. Consider upgrading the app host or Aspire CLI.");
Console.WriteLine();
AnsiConsole.MarkupLine($"\t[bold]Aspire Hosting SDK Version[/]: {appHostHostingSdkVersion}");
AnsiConsole.MarkupLine($"\t[bold]Aspire CLI Version[/]: {cliInformationalVersion}");
AnsiConsole.MarkupLine($"\t[bold]Required Capability[/]: {ex.RequiredCapability}");
_ansiConsole.MarkupLine($"\t[bold]Aspire Hosting SDK Version[/]: {appHostHostingSdkVersion}");
_ansiConsole.MarkupLine($"\t[bold]Aspire CLI Version[/]: {cliInformationalVersion}");
_ansiConsole.MarkupLine($"\t[bold]Required Capability[/]: {ex.RequiredCapability}");
Console.WriteLine();
return ExitCodeConstants.AppHostIncompatible;
}
Expand All @@ -81,7 +93,7 @@ public void DisplayError(string errorMessage)

public void DisplayMessage(string emoji, string message)
{
AnsiConsole.MarkupLine($":{emoji}: {message}");
_ansiConsole.MarkupLine($":{emoji}: {message}");
}

public void DisplaySuccess(string message)
Expand All @@ -91,17 +103,17 @@ public void DisplaySuccess(string message)

public void DisplayDashboardUrls((string BaseUrlWithLoginToken, string? CodespacesUrlWithLoginToken) dashboardUrls)
{
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine($"[green bold]Dashboard[/]:");
_ansiConsole.WriteLine();
_ansiConsole.MarkupLine($"[green bold]Dashboard[/]:");
if (dashboardUrls.CodespacesUrlWithLoginToken is not null)
{
AnsiConsole.MarkupLine($":chart_increasing: Direct: [link={dashboardUrls.BaseUrlWithLoginToken}]{dashboardUrls.BaseUrlWithLoginToken}[/]");
AnsiConsole.MarkupLine($":chart_increasing: Codespaces: [link={dashboardUrls.CodespacesUrlWithLoginToken}]{dashboardUrls.CodespacesUrlWithLoginToken}[/]");
_ansiConsole.MarkupLine($":chart_increasing: Direct: [link={dashboardUrls.BaseUrlWithLoginToken}]{dashboardUrls.BaseUrlWithLoginToken}[/]");
_ansiConsole.MarkupLine($":chart_increasing: Codespaces: [link={dashboardUrls.CodespacesUrlWithLoginToken}]{dashboardUrls.CodespacesUrlWithLoginToken}[/]");
}
else
{
AnsiConsole.MarkupLine($":chart_increasing: [link={dashboardUrls.BaseUrlWithLoginToken}]{dashboardUrls.BaseUrlWithLoginToken}[/]");
_ansiConsole.MarkupLine($":chart_increasing: [link={dashboardUrls.BaseUrlWithLoginToken}]{dashboardUrls.BaseUrlWithLoginToken}[/]");
}
AnsiConsole.WriteLine();
_ansiConsole.WriteLine();
}
}
1 change: 1 addition & 0 deletions src/Aspire.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ private static IHost BuildApplication(string[] args)
// Shared services.
builder.Services.AddSingleton(BuildProjectLocator);
builder.Services.AddSingleton<INewCommandPrompter, NewCommandPrompter>();
builder.Services.AddSingleton<IAddCommandPrompter, AddCommandPrompter>();
builder.Services.AddSingleton<IInteractionService, InteractionService>();
builder.Services.AddSingleton<ICertificateService, CertificateService>();
builder.Services.AddTransient<IDotNetCliRunner, DotNetCliRunner>();
Expand Down
Loading