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
10 changes: 6 additions & 4 deletions src/Aspire.Cli/Commands/AddCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,12 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) =>
// which prevents 'dotnet add package' from modifying the project.
if (_features.IsFeatureEnabled(KnownFeatures.RunningInstanceDetectionEnabled, defaultValue: true))
{
var runningInstanceResult = await project.FindAndStopRunningInstanceAsync(
effectiveAppHostProjectFile,
ExecutionContext.HomeDirectory,
cancellationToken);
var runningInstanceResult = await InteractionService.ShowStatusAsync(
AddCommandStrings.CheckingForRunningInstances,
async () => await project.FindAndStopRunningInstanceAsync(
effectiveAppHostProjectFile,
ExecutionContext.HomeDirectory,
cancellationToken));

if (runningInstanceResult == RunningInstanceResult.InstanceStopped)
{
Expand Down
4 changes: 3 additions & 1 deletion src/Aspire.Cli/Commands/InitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,9 @@ private static bool IsSupportedTfm(string tfm)

private async Task<(NuGetPackage Package, PackageChannel Channel)> GetProjectTemplatesVersionAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
var allChannels = await _packagingService.GetChannelsAsync(cancellationToken);
var allChannels = await InteractionService.ShowStatusAsync(
InitCommandStrings.ResolvingTemplateVersion,
async () => await _packagingService.GetChannelsAsync(cancellationToken));

// Check if --channel option was provided (highest priority)
var channelName = parseResult.GetValue(_channelOption);
Expand Down
100 changes: 61 additions & 39 deletions src/Aspire.Cli/Commands/NewCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.CommandLine;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using Aspire.Cli.Configuration;
using Aspire.Cli.Interaction;
Expand Down Expand Up @@ -232,50 +233,68 @@ private ITemplate[] GetTemplatesForPrompt(ParseResult parseResult)
return null;
}

return await _prompter.PromptForTemplateAsync(templatesForPrompt, cancellationToken);
}

private async Task<string?> ResolveCliTemplateVersionAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
var channels = await _packagingService.GetChannelsAsync(cancellationToken);
var result = await _prompter.PromptForTemplateAsync(templatesForPrompt, cancellationToken);

var configuredChannelName = parseResult.GetValue(_channelOption);
if (string.IsNullOrWhiteSpace(configuredChannelName))
// The prompt is cleared after selection.
// Write out the selected template again for context before proceeding.
if (result != null)
{
configuredChannelName = await _configurationService.GetConfigurationAsync("channel", cancellationToken);
InteractionService.DisplayPlainText($"{NewCommandStrings.SelectAProjectTemplate} {result.Description}");
}
return result;
}

var selectedChannel = string.IsNullOrWhiteSpace(configuredChannelName)
? channels.FirstOrDefault(c => c.Type is PackageChannelType.Implicit) ?? channels.FirstOrDefault()
: channels.FirstOrDefault(c => string.Equals(c.Name, configuredChannelName, StringComparison.OrdinalIgnoreCase));

if (selectedChannel is null)
{
if (string.IsNullOrWhiteSpace(configuredChannelName))
{
InteractionService.DisplayError("No package channels are available.");
}
else
{
InteractionService.DisplayError($"No channel found matching '{configuredChannelName}'. Valid options are: {string.Join(", ", channels.Select(c => c.Name))}");
}

return null;
}
private sealed class ResolveTemplateVersionResult
{
public string? Version { get; init; }

var packages = await selectedChannel.GetTemplatePackagesAsync(ExecutionContext.WorkingDirectory, cancellationToken);
var package = packages
.Where(p => Semver.SemVersion.TryParse(p.Version, Semver.SemVersionStyles.Strict, out _))
.OrderByDescending(p => Semver.SemVersion.Parse(p.Version, Semver.SemVersionStyles.Strict), Semver.SemVersion.PrecedenceComparer)
.FirstOrDefault();
[MemberNotNullWhen(true, nameof(Version))]
[MemberNotNullWhen(false, nameof(ErrorMessage))]
public bool Success => Version is not null;

if (package is null)
{
InteractionService.DisplayError($"No template versions found in channel '{selectedChannel.Name}'.");
return null;
}
public string? ErrorMessage { get; init; }
}

return package.Version;
private async Task<ResolveTemplateVersionResult> ResolveCliTemplateVersionAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
return await InteractionService.ShowStatusAsync(
NewCommandStrings.ResolvingTemplateVersion,
async () =>
{
var channels = await _packagingService.GetChannelsAsync(cancellationToken);

var configuredChannelName = parseResult.GetValue(_channelOption);
if (string.IsNullOrWhiteSpace(configuredChannelName))
{
configuredChannelName = await _configurationService.GetConfigurationAsync("channel", cancellationToken);
}

var selectedChannel = string.IsNullOrWhiteSpace(configuredChannelName)
? channels.FirstOrDefault(c => c.Type is PackageChannelType.Implicit) ?? channels.FirstOrDefault()
: channels.FirstOrDefault(c => string.Equals(c.Name, configuredChannelName, StringComparison.OrdinalIgnoreCase));

if (selectedChannel is null)
{
var errorMessage = string.IsNullOrWhiteSpace(configuredChannelName)
? "No package channels are available."
: $"No channel found matching '{configuredChannelName}'. Valid options are: {string.Join(", ", channels.Select(c => c.Name))}";

return new ResolveTemplateVersionResult { ErrorMessage = errorMessage };
}

var packages = await selectedChannel.GetTemplatePackagesAsync(ExecutionContext.WorkingDirectory, cancellationToken);
var package = packages
.Where(p => Semver.SemVersion.TryParse(p.Version, Semver.SemVersionStyles.Strict, out _))
.OrderByDescending(p => Semver.SemVersion.Parse(p.Version, Semver.SemVersionStyles.Strict), Semver.SemVersion.PrecedenceComparer)
.FirstOrDefault();

if (package is null)
{
return new ResolveTemplateVersionResult { ErrorMessage = $"No template versions found in channel '{selectedChannel.Name}'." };
}

return new ResolveTemplateVersionResult { Version = package.Version };
});
}

protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
Expand All @@ -298,11 +317,14 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
if (ShouldResolveCliTemplateVersion(template) &&
string.IsNullOrWhiteSpace(version))
{
version = await ResolveCliTemplateVersionAsync(parseResult, cancellationToken);
if (string.IsNullOrWhiteSpace(version))
var resolveResult = await ResolveCliTemplateVersionAsync(parseResult, cancellationToken);
if (!resolveResult.Success)
{
InteractionService.DisplayError(resolveResult.ErrorMessage);
return ExitCodeConstants.InvalidCommand;
}

version = resolveResult.Version;
}

var inputs = new TemplateInputs
Expand Down
4 changes: 3 additions & 1 deletion src/Aspire.Cli/Commands/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,9 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
// Even if we fail to stop we won't block the apphost starting
// to make sure we don't ever break flow. It should mostly stop
// just fine though.
var runningInstanceResult = await project.FindAndStopRunningInstanceAsync(effectiveAppHostFile, ExecutionContext.HomeDirectory, cancellationToken);
var runningInstanceResult = await InteractionService.ShowStatusAsync(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This flickers because its fast. I wonder if we should avoid the status if the underlying task finishes quicky.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely. Some of these were added by AI.

I'll remove it.

RunCommandStrings.CheckingForRunningInstances,
async () => await project.FindAndStopRunningInstanceAsync(effectiveAppHostFile, ExecutionContext.HomeDirectory, cancellationToken));

// If in isolated mode and a running instance was stopped, warn the user
if (isolated && runningInstanceResult == RunningInstanceResult.InstanceStopped)
Expand Down
16 changes: 13 additions & 3 deletions src/Aspire.Cli/Commands/UpdateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,9 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
var channelName = parseResult.GetValue(_channelOption) ?? parseResult.GetValue(_qualityOption);
PackageChannel channel;

var allChannels = await _packagingService.GetChannelsAsync(cancellationToken);
var allChannels = await InteractionService.ShowStatusAsync(
UpdateCommandStrings.CheckingForUpdates,
async () => await _packagingService.GetChannelsAsync(cancellationToken));

if (!string.IsNullOrEmpty(channelName))
{
Expand Down Expand Up @@ -347,8 +349,16 @@ private async Task ExtractAndUpdateAsync(string archivePath, CancellationToken c
try
{
// Extract archive
InteractionService.DisplayMessage(KnownEmojis.Package, "Extracting new CLI...");
await ArchiveHelper.ExtractAsync(archivePath, tempExtractDir, cancellationToken);
await InteractionService.ShowStatusAsync(
UpdateCommandStrings.ExtractingNewCli,
async () =>
{
await ArchiveHelper.ExtractAsync(archivePath, tempExtractDir, cancellationToken);
return 0;
},
KnownEmojis.Package);

InteractionService.DisplayMessage(KnownEmojis.Package, UpdateCommandStrings.ExtractedNewCli);

// Find the aspire executable in the extracted files
var newExePath = Path.Combine(tempExtractDir, exeName);
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Cli/Interaction/KnownEmojis.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ internal static class KnownEmojis
public static readonly KnownEmoji FloppyDisk = new("floppy_disk");
public static readonly KnownEmoji Gear = new("gear");
public static readonly KnownEmoji Hammer = new("hammer");
public static readonly KnownEmoji Ice = new("ice");
public static readonly KnownEmoji HammerAndWrench = new("hammer_and_wrench");
public static readonly KnownEmoji Information = new("information");
public static readonly KnownEmoji Key = new("key");
Expand Down
11 changes: 9 additions & 2 deletions src/Aspire.Cli/Projects/GuestAppHostProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -964,8 +964,15 @@ public async Task<UpdatePackagesResult> UpdatePackagesAsync(UpdatePackagesContex

// Rebuild and regenerate SDK code with updated packages
_interactionService.DisplayEmptyLine();
_interactionService.DisplaySubtleMessage("Regenerating SDK code with updated packages...");
await BuildAndGenerateSdkAsync(directory, cancellationToken);
await _interactionService.ShowStatusAsync(
UpdateCommandStrings.RegeneratingSdkCode,
async () =>
{
await BuildAndGenerateSdkAsync(directory, cancellationToken);
return 0;
});

_interactionService.DisplayMessage(KnownEmojis.Package, UpdateCommandStrings.RegeneratedSdkCode);

_interactionService.DisplayEmptyLine();
_interactionService.DisplaySuccess(UpdateCommandStrings.UpdateSuccessfulMessage);
Expand Down
17 changes: 12 additions & 5 deletions src/Aspire.Cli/Projects/ProjectUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,18 @@ public async Task<ProjectUpdateResult> UpdateProjectAsync(FileInfo projectFile,

interactionService.DisplayEmptyLine();

foreach (var updateStep in updateSteps)
{
interactionService.DisplaySubtleMessage(string.Format(CultureInfo.InvariantCulture, UpdateCommandStrings.ExecutingUpdateStepFormat, updateStep.Description));
await updateStep.Callback();
}
await interactionService.ShowStatusAsync(
UpdateCommandStrings.ApplyingUpdates,
async () =>
{
foreach (var updateStep in updateSteps)
{
interactionService.DisplaySubtleMessage(string.Format(CultureInfo.InvariantCulture, UpdateCommandStrings.ExecutingUpdateStepFormat, updateStep.Description));
await updateStep.Callback();
}

return 0;
});

interactionService.DisplayEmptyLine();

Expand Down
8 changes: 8 additions & 0 deletions src/Aspire.Cli/Resources/AddCommandStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Aspire.Cli/Resources/AddCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,7 @@
<data name="UnableToStopRunningInstances" xml:space="preserve">
<value>Unable to stop one or more running Aspire AppHost instances. Please stop the application and try again.</value>
</data>
<data name="CheckingForRunningInstances" xml:space="preserve">
<value>Checking for running instances...</value>
</data>
</root>
6 changes: 6 additions & 0 deletions src/Aspire.Cli/Resources/InitCommandStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Aspire.Cli/Resources/InitCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,7 @@
<data name="AddingServiceDefaultsProjectToSolution" xml:space="preserve">
<value>Adding ServiceDefaults project to solution...</value>
</data>
<data name="ResolvingTemplateVersion" xml:space="preserve">
<value>Resolving template version...</value>
</data>
</root>
6 changes: 6 additions & 0 deletions src/Aspire.Cli/Resources/NewCommandStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Aspire.Cli/Resources/NewCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,7 @@
<data name="LanguageOptionDescription" xml:space="preserve">
<value>The programming language for the AppHost.</value>
</data>
<data name="ResolvingTemplateVersion" xml:space="preserve">
<value>Resolving template version...</value>
</data>
</root>
6 changes: 6 additions & 0 deletions src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions src/Aspire.Cli/Resources/UpdateCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,22 @@
<data name="NoWritePermissionToInstallDirectory" xml:space="preserve">
<value>Cannot write to installation directory '{0}'. Please run the update with elevated permissions (e.g., using sudo on Linux/macOS).</value>
</data>
<data name="CheckingForUpdates" xml:space="preserve">
<value>Checking for updates...</value>
</data>
<data name="ApplyingUpdates" xml:space="preserve">
<value>Applying updates...</value>
</data>
<data name="ExtractingNewCli" xml:space="preserve">
<value>Extracting new CLI...</value>
</data>
<data name="ExtractedNewCli" xml:space="preserve">
<value>Extracted new CLI</value>
</data>
<data name="RegeneratingSdkCode" xml:space="preserve">
<value>Regenerating SDK code with updated packages...</value>
</data>
<data name="RegeneratedSdkCode" xml:space="preserve">
<value>Regenerated SDK code with updated packages.</value>
</data>
</root>
5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/AddCommandStrings.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/AddCommandStrings.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/AddCommandStrings.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/AddCommandStrings.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading