Skip to content
Merged
40 changes: 38 additions & 2 deletions src/Aspire.Cli/Commands/UpdateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ internal sealed class UpdateCommand : BaseCommand
private readonly IProjectUpdater _projectUpdater;
private readonly ILogger<UpdateCommand> _logger;
private readonly ICliDownloader? _cliDownloader;
private readonly ICliUpdateNotifier _updateNotifier;

public UpdateCommand(
IProjectLocator projectLocator,
Expand All @@ -41,12 +42,14 @@ public UpdateCommand(
ArgumentNullException.ThrowIfNull(packagingService);
ArgumentNullException.ThrowIfNull(projectUpdater);
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(updateNotifier);

_projectLocator = projectLocator;
_packagingService = packagingService;
_projectUpdater = projectUpdater;
_logger = logger;
_cliDownloader = cliDownloader;
_updateNotifier = updateNotifier;

var projectOption = new Option<FileInfo?>("--project");
projectOption.Description = UpdateCommandStrings.ProjectArgumentDescription;
Expand Down Expand Up @@ -116,6 +119,21 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
cancellationToken);

await _projectUpdater.UpdateProjectAsync(projectFile!, channel, cancellationToken);

// After successful project update, check if CLI update is available and prompt
if (_cliDownloader is not null && _updateNotifier.IsUpdateAvailable())
{
var shouldUpdateCli = await InteractionService.ConfirmAsync(
UpdateCommandStrings.UpdateCliAfterProjectUpdatePrompt,
defaultValue: true,
cancellationToken);

if (shouldUpdateCli)
{
// Use the same channel that was selected for the project update
return await ExecuteSelfUpdateAsync(parseResult, cancellationToken, channel.Name);
}
}
}
catch (ProjectUpdaterException ex)
{
Expand All @@ -125,15 +143,33 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
}
catch (ProjectLocatorException ex)
{
// Check if this is a "no project found" error and prompt for self-update
if (string.Equals(ex.Message, ErrorStrings.NoProjectFileFound, StringComparisons.CliInputOrOutput))
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

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

The comparison uses StringComparisons.CliInputOrOutput which is likely incorrect. Based on the codebase pattern of comparing error messages, this should use StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase depending on whether case-sensitive comparison is needed for error messages.

Suggested change
if (string.Equals(ex.Message, ErrorStrings.NoProjectFileFound, StringComparisons.CliInputOrOutput))
if (string.Equals(ex.Message, ErrorStrings.NoProjectFileFound, StringComparison.Ordinal))

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The current code using StringComparisons.CliInputOrOutput is correct. This is a standard pattern defined in src/Shared/StringComparers.cs (line 62) where CliInputOrOutput maps to StringComparison.Ordinal, which is the appropriate comparison for CLI error messages. This same pattern is used consistently throughout BaseCommand.cs for all error message comparisons (lines 61, 66, 71, 76, 81), ensuring consistency across the codebase.

{
// Only prompt for self-update if not running as dotnet tool and downloader is available
if (_cliDownloader is not null)
{
var shouldUpdateCli = await InteractionService.ConfirmAsync(
UpdateCommandStrings.NoAppHostFoundUpdateCliPrompt,
defaultValue: true,
cancellationToken);

if (shouldUpdateCli)
{
return await ExecuteSelfUpdateAsync(parseResult, cancellationToken);
}
}
}

return HandleProjectLocatorException(ex, InteractionService);
}

return 0;
}

private async Task<int> ExecuteSelfUpdateAsync(ParseResult parseResult, CancellationToken cancellationToken)
private async Task<int> ExecuteSelfUpdateAsync(ParseResult parseResult, CancellationToken cancellationToken, string? selectedQuality = null)
{
var quality = parseResult.GetValue<string?>("--quality");
var quality = selectedQuality ?? parseResult.GetValue<string?>("--quality");

// If quality is not specified, prompt the user
if (string.IsNullOrEmpty(quality))
Expand Down
8 changes: 7 additions & 1 deletion src/Aspire.Cli/Interaction/ConsoleInteractionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,16 @@ public void DisplayEmptyLine()

private const string UpdateUrl = "https://aka.ms/aspire/update";

public void DisplayVersionUpdateNotification(string newerVersion)
public void DisplayVersionUpdateNotification(string newerVersion, string? updateCommand = null)
{
_ansiConsole.WriteLine();
_ansiConsole.MarkupLine(string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.NewCliVersionAvailable, newerVersion));

if (!string.IsNullOrEmpty(updateCommand))
{
_ansiConsole.MarkupLine(string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.ToUpdateRunCommand, updateCommand));
}

_ansiConsole.MarkupLine(string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.MoreInfoNewCliVersion, UpdateUrl));
}
}
4 changes: 2 additions & 2 deletions src/Aspire.Cli/Interaction/ExtensionInteractionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,9 +308,9 @@ public void DisplayMarkdown(string markdown)
_consoleInteractionService.DisplayMarkdown(markdown);
}

public void DisplayVersionUpdateNotification(string newerVersion)
public void DisplayVersionUpdateNotification(string newerVersion, string? updateCommand = null)
{
_consoleInteractionService.DisplayVersionUpdateNotification(newerVersion);
_consoleInteractionService.DisplayVersionUpdateNotification(newerVersion, updateCommand);
}

public void LogMessage(LogLevel logLevel, string message)
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Interaction/IInteractionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ internal interface IInteractionService
void DisplayCancellationMessage();
void DisplayEmptyLine();

void DisplayVersionUpdateNotification(string newerVersion);
void DisplayVersionUpdateNotification(string newerVersion, string? updateCommand = null);
void WriteConsoleLog(string message, int? lineNumber = null, string? type = null, bool isErrorMessage = false);
}

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

4 changes: 4 additions & 0 deletions src/Aspire.Cli/Resources/InteractionServiceStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@
<value>[dim]For more information, see: [link]{0}[/][/]</value>
<comment>Do not translate [dim] and [link]. Also leave [/] as-is. {0} is a URL</comment>
</data>
<data name="ToUpdateRunCommand" xml:space="preserve">
<value>[dim]To update, run: {0}[/]</value>
<comment>Do not translate [dim]. Also leave [/] as-is. {0} is the command to run</comment>
</data>
<data name="UnbuildableAppHostsDetected" xml:space="preserve">
<value>No app hosts were found (there may be app hosts project files with syntax errors/invalid SDK versions).</value>
</data>
Expand Down
2 changes: 2 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.

6 changes: 6 additions & 0 deletions src/Aspire.Cli/Resources/UpdateCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,10 @@
<data name="FallbackParsingWarning" xml:space="preserve">
<value>Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.</value>
</data>
<data name="NoAppHostFoundUpdateCliPrompt" xml:space="preserve">
<value>No Aspire AppHost project found. Would you like to update the Aspire CLI instead?</value>
</data>
<data name="UpdateCliAfterProjectUpdatePrompt" xml:space="preserve">
<value>An update is available for the Aspire CLI. Would you like to update it now?</value>
</data>
</root>

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

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

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

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

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

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

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

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

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

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

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

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

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

10 changes: 10 additions & 0 deletions src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf

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

10 changes: 10 additions & 0 deletions src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf

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

10 changes: 10 additions & 0 deletions src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf

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

Loading
Loading