diff --git a/src/Aspire.Cli/Acquisition/InstallSource.cs b/src/Aspire.Cli/Acquisition/InstallSource.cs
index f6f10830b53..9adb15f51a6 100644
--- a/src/Aspire.Cli/Acquisition/InstallSource.cs
+++ b/src/Aspire.Cli/Acquisition/InstallSource.cs
@@ -13,7 +13,8 @@ internal enum InstallSource
{
///
/// No sidecar was found, or the sidecar contained a value that does not
- /// match any known route. Treated as legacy / pre-sidecar by callers.
+ /// match any known route. Callers fail closed unless an explicit override
+ /// applies.
///
Unknown = 0,
@@ -55,8 +56,7 @@ internal static class InstallSourceExtensions
///
/// Parses a sidecar source string into the strongly-typed enum.
/// Returns for null, empty, or
- /// unrecognized values so callers can treat unknown sources as a
- /// legacy / pre-sidecar install.
+ /// unrecognized values so callers can apply their unknown-route policy.
///
public static InstallSource ParseInstallSource(string? raw)
{
diff --git a/src/Aspire.Cli/Acquisition/SelfUpdateRouter.cs b/src/Aspire.Cli/Acquisition/SelfUpdateRouter.cs
new file mode 100644
index 00000000000..145e3fef6e3
--- /dev/null
+++ b/src/Aspire.Cli/Acquisition/SelfUpdateRouter.cs
@@ -0,0 +1,55 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Cli.Acquisition;
+
+///
+/// Decides how aspire update --self should behave for an
+/// . The CLI can update itself in-process only
+/// for installs it owns end-to-end (script). Every other route is owned by
+/// a package manager or by a separate install path and would be corrupted
+/// by an in-process binary swap, so we delegate by printing an
+/// installer-appropriate command.
+///
+internal enum SelfUpdateAction
+{
+ ///
+ /// Perform the existing in-process self-update flow
+ /// (CliDownloader-driven download + binary swap).
+ ///
+ InProcess,
+
+ ///
+ /// Refuse to update in-process and instead print a route-appropriate
+ /// command via . Returns
+ /// exit code 0 to match the existing dotnet-tool refusal contract;
+ /// callers that need to detect whether an update actually happened
+ /// should compare the binary version before and after the run rather
+ /// than relying on the exit code.
+ ///
+ Delegate,
+}
+
+///
+/// Pure policy lookup that maps to the action
+/// aspire update --self must take.
+///
+internal static class SelfUpdateRouter
+{
+ ///
+ /// Returns the action aspire update --self should perform for
+ /// the supplied .
+ ///
+ ///
+ /// stays in-process — it's the route
+ /// the CLI owns end-to-end. Unknown routes are refused with a hint to
+ /// investigate the install or pass --force. The pre-PR-#16817
+ /// legacy script-install case is now covered by the --force
+ /// escape hatch.
+ ///
+ public static SelfUpdateAction GetAction(InstallSource source) => source switch
+ {
+ InstallSource.Script => SelfUpdateAction.InProcess,
+ _ => SelfUpdateAction.Delegate,
+ };
+}
diff --git a/src/Aspire.Cli/Acquisition/UpgradeInstructionProvider.cs b/src/Aspire.Cli/Acquisition/UpgradeInstructionProvider.cs
new file mode 100644
index 00000000000..8ccd5521da2
--- /dev/null
+++ b/src/Aspire.Cli/Acquisition/UpgradeInstructionProvider.cs
@@ -0,0 +1,107 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Cli.Resources;
+using Aspire.Cli.Utils;
+
+namespace Aspire.Cli.Acquisition;
+
+///
+/// Returns the installer-appropriate command a user should run to update
+/// the Aspire CLI, given the install route that produced their binary.
+/// Consumed by aspire update --self's refusal path and the
+/// "update available" notifier so users always see the right command
+/// for their install.
+///
+internal interface IUpgradeInstructionProvider
+{
+ ///
+ /// Returns the command string a user should run to update an
+ /// installation produced by .
+ ///
+ /// Install route the running binary was placed by.
+ /// Absolute path of the running binary. Used
+ /// only for : a global-tool
+ /// install (under ~/.dotnet/tools/.store/) gets
+ /// dotnet tool update -g Aspire.Cli, a --tool-path
+ /// install gets the path-aware variant.
+ /// The CLI's identity channel
+ /// (CliExecutionContext.IdentityChannel). Used only for
+ /// to substitute the PR number into the
+ /// get-aspire-cli-pr command.
+ ///
+ /// The command to print verbatim, or for
+ /// (which stays in-process and has
+ /// no separate update command to display).
+ ///
+ string? GetUpdateCommand(InstallSource source, string? processPath, string identityChannel);
+}
+
+///
+/// Default . The mapping is a
+/// pure function of (source, processPath, identityChannel); no
+/// I/O beyond the path-shape parsing already performed by
+/// for the
+/// route.
+///
+internal sealed class UpgradeInstructionProvider : IUpgradeInstructionProvider
+{
+ ///
+ public string? GetUpdateCommand(InstallSource source, string? processPath, string identityChannel)
+ {
+ return source switch
+ {
+ // Script is the in-process update path; no separate command to display.
+ InstallSource.Script => null,
+
+ InstallSource.Pr => GetPrUpdateCommand(identityChannel),
+ InstallSource.Winget => "winget upgrade Microsoft.Aspire",
+ InstallSource.Brew => "brew upgrade --cask aspire",
+
+ // Prefer the supplied process path so tests and callers can
+ // classify synthesized paths without depending on Environment.ProcessPath.
+ // When no path is supplied, the no-arg overload preserves the
+ // existing production behavior and AsyncLocal test override.
+ InstallSource.DotnetTool => GetDotNetToolUpdateCommand(processPath),
+
+ // LocalHive installs are produced by re-running the dev script
+ // in the user's own checkout. There is no canonical update
+ // command — the user must rebuild from source.
+ InstallSource.LocalHive => "Run ./localhive.sh (Linux/macOS) or .\\localhive.ps1 (Windows) in the local hive directory.",
+ InstallSource.Unknown => UpdateCommandStrings.UnknownRouteRefusalHint,
+
+ _ => null,
+ };
+ }
+
+ private const string PrChannelPrefix = "pr-";
+
+ private static string GetDotNetToolUpdateCommand(string? processPath)
+ {
+ return (processPath is not null
+ ? DotNetToolDetection.GetDotNetToolUpdateCommand(processPath)
+ : DotNetToolDetection.GetDotNetToolUpdateCommand())
+ ?? "dotnet tool update -g Aspire.Cli";
+ }
+
+ private static string GetPrUpdateCommand(string identityChannel)
+ {
+ // The PR channel form is `pr-` (parsed and validated by
+ // IdentityChannelReader); extract the digits if present so the
+ // hint shows the actual PR number.
+ if (identityChannel.StartsWith(PrChannelPrefix, StringComparison.Ordinal) &&
+ identityChannel.Length > PrChannelPrefix.Length)
+ {
+ var prNumber = identityChannel[PrChannelPrefix.Length..];
+ // Print both POSIX and Windows install lines so the docs are
+ // discoverable regardless of which shell the user is on.
+ return $"get-aspire-cli-pr.sh {prNumber} # or: get-aspire-cli-pr.ps1 -PRNumber {prNumber}";
+ }
+
+ // Defensive: if a PR-route sidecar coexists with a non-PR identity
+ // channel (theoretically impossible because PR archives bake the
+ // matching channel), emit the parameterised form so the user knows
+ // they need to supply the number.
+ return "get-aspire-cli-pr.sh # or: get-aspire-cli-pr.ps1 -PRNumber ";
+ }
+}
diff --git a/src/Aspire.Cli/Commands/UpdateCommand.cs b/src/Aspire.Cli/Commands/UpdateCommand.cs
index 6290f31f994..50b2334ada0 100644
--- a/src/Aspire.Cli/Commands/UpdateCommand.cs
+++ b/src/Aspire.Cli/Commands/UpdateCommand.cs
@@ -5,6 +5,7 @@
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
+using Aspire.Cli.Acquisition;
using Aspire.Cli.Configuration;
using Aspire.Cli.Exceptions;
using Aspire.Cli.Interaction;
@@ -32,12 +33,20 @@ internal sealed class UpdateCommand : BaseCommand
private readonly IFeatures _features;
private readonly IConfigurationService _configurationService;
private readonly IConfiguration _configuration;
+ private readonly IInstallationDiscovery _installationDiscovery;
+ private readonly IUpgradeInstructionProvider _upgradeInstructionProvider;
+ private readonly ICliHostEnvironment _hostEnvironment;
+ private readonly WingetFirstRunProbe _wingetFirstRunProbe;
private static readonly OptionWithLegacy s_appHostOption = new("--apphost", "--project", UpdateCommandStrings.ProjectArgumentDescription);
private static readonly Option s_selfOption = new("--self")
{
Description = UpdateCommandStrings.SelfOptionDescription
};
+ private static readonly Option s_forceOption = new("--force")
+ {
+ Description = UpdateCommandStrings.ForceOptionDescription
+ };
private static readonly Option s_yesOption = new("--yes")
{
Description = UpdateCommandStrings.YesOptionDescription,
@@ -62,7 +71,11 @@ public UpdateCommand(
CliExecutionContext executionContext,
IConfigurationService configurationService,
AspireCliTelemetry telemetry,
- IConfiguration configuration)
+ IConfiguration configuration,
+ IInstallationDiscovery installationDiscovery,
+ IUpgradeInstructionProvider upgradeInstructionProvider,
+ ICliHostEnvironment hostEnvironment,
+ WingetFirstRunProbe wingetFirstRunProbe)
: base("update", UpdateCommandStrings.Description, features, updateNotifier, executionContext, interactionService, telemetry)
{
_projectLocator = projectLocator;
@@ -74,9 +87,14 @@ public UpdateCommand(
_features = features;
_configurationService = configurationService;
_configuration = configuration;
+ _installationDiscovery = installationDiscovery;
+ _upgradeInstructionProvider = upgradeInstructionProvider;
+ _hostEnvironment = hostEnvironment;
+ _wingetFirstRunProbe = wingetFirstRunProbe;
Options.Add(s_appHostOption);
Options.Add(s_selfOption);
+ Options.Add(s_forceOption);
Options.Add(s_yesOption);
Options.Add(s_nugetConfigDirOption);
@@ -106,11 +124,6 @@ public UpdateCommand(
protected override bool UpdateNotificationsEnabled => false;
- private static string? GetDotNetToolUpdateCommand()
- {
- return DotNetToolDetection.GetDotNetToolUpdateCommand();
- }
-
protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
var isSelfUpdate = parseResult.GetValue(s_selfOption);
@@ -118,28 +131,7 @@ protected override async Task ExecuteAsync(ParseResult parseResul
// If --self is specified, handle CLI self-update
if (isSelfUpdate)
{
- // When running as a dotnet tool, print the update command instead of executing
- var dotNetToolUpdateCommand = GetDotNetToolUpdateCommand();
- if (dotNetToolUpdateCommand is not null)
- {
- InteractionService.DisplayMessage(KnownEmojis.Information, UpdateCommandStrings.DotNetToolSelfUpdateMessage);
- InteractionService.DisplayPlainText($" {dotNetToolUpdateCommand}");
- return CommandResult.FromExitCode(0);
- }
-
- if (_cliDownloader is null)
- {
- return CommandResult.Failure(ExitCodeConstants.InvalidCommand, "CLI self-update is not available in this environment.");
- }
-
- try
- {
- return await ExecuteSelfUpdateAsync(parseResult, cancellationToken);
- }
- catch (OperationCanceledException)
- {
- return CommandResult.Cancelled();
- }
+ return await HandleSelfUpdateAsync(parseResult, cancellationToken);
}
// Otherwise, handle project update
@@ -206,11 +198,14 @@ protected override async Task ExecuteAsync(ParseResult parseResul
}
else
{
- // If there are hives (PR build directories), prompt for channel selection.
- // Otherwise, use the implicit/default channel automatically.
+ // If there are hives (PR build directories) AND interactive
+ // input is available, prompt the user to pick a channel. In
+ // non-interactive mode the prompt would crash (#15600), so
+ // fall through to the implicit/default channel — same
+ // behavior as the no-hives branch.
var hasHives = ExecutionContext.GetHiveCount() > 0;
- if (hasHives)
+ if (hasHives && _hostEnvironment.SupportsInteractiveInput)
{
// Prompt for channel selection
var channelBinding = PromptBinding.Create(parseResult, _channelOption);
@@ -257,12 +252,10 @@ protected override async Task ExecuteAsync(ParseResult parseResul
if (shouldUpdateCli)
{
- var dotNetToolUpdateCommand = GetDotNetToolUpdateCommand();
- if (dotNetToolUpdateCommand is not null)
+ var (source, canonicalPath) = ResolveRunningInstall();
+ if (SelfUpdateRouter.GetAction(source) == SelfUpdateAction.Delegate)
{
- InteractionService.DisplayMessage(KnownEmojis.Information, UpdateCommandStrings.DotNetToolSelfUpdateMessage);
- InteractionService.DisplayPlainText($" {dotNetToolUpdateCommand}");
- return CommandResult.Success();
+ return DisplaySelfUpdateRefusal(source, canonicalPath);
}
// Use the same channel that was selected for the project update
@@ -288,7 +281,7 @@ protected override async Task ExecuteAsync(ParseResult parseResul
if (string.Equals(ex.Message, ErrorStrings.NoProjectFileFound, StringComparisons.CliInputOrOutput))
{
// Only prompt for self-update if not running as dotnet tool and downloader is available
- if (GetDotNetToolUpdateCommand() is null && _cliDownloader is not null)
+ if (!DotNetToolDetection.IsRunningAsDotNetTool() && _cliDownloader is not null)
{
var shouldUpdateCli = await InteractionService.PromptConfirmAsync(
UpdateCommandStrings.NoAppHostFoundUpdateCliPrompt,
@@ -297,6 +290,12 @@ protected override async Task ExecuteAsync(ParseResult parseResul
if (shouldUpdateCli)
{
+ var (source, canonicalPath) = ResolveRunningInstall();
+ if (SelfUpdateRouter.GetAction(source) == SelfUpdateAction.Delegate)
+ {
+ return DisplaySelfUpdateRefusal(source, canonicalPath);
+ }
+
return await ExecuteSelfUpdateAsync(parseResult, cancellationToken);
}
}
@@ -312,6 +311,133 @@ protected override async Task ExecuteAsync(ParseResult parseResul
return CommandResult.FromExitCode(0);
}
+ ///
+ /// Routes aspire update --self based on the running CLI's install source.
+ /// Script installs perform the existing in-process binary swap; every other
+ /// route is refused with an installer-appropriate command from
+ /// so we don't corrupt the
+ /// package-manager-owned binary or silently demote a PR-pinned install to
+ /// stable. Unknown / missing sidecars are refused defensively.
+ ///
+ private async Task HandleSelfUpdateAsync(ParseResult parseResult, CancellationToken cancellationToken)
+ {
+ var (source, canonicalPath) = ResolveRunningInstall();
+ var force = parseResult.GetValue(s_forceOption);
+ var action = force ? SelfUpdateAction.InProcess : SelfUpdateRouter.GetAction(source);
+
+ if (force)
+ {
+ _logger.LogDebug("Forcing in-process self-update for detected install source '{InstallSource}' at '{CanonicalPath}'.", source, canonicalPath);
+ }
+
+ if (action == SelfUpdateAction.Delegate)
+ {
+ return DisplaySelfUpdateRefusal(source, canonicalPath);
+ }
+
+ // Script route, or explicit --force: existing in-process flow.
+ if (_cliDownloader is null)
+ {
+ return CommandResult.Failure(ExitCodeConstants.InvalidCommand, "CLI self-update is not available in this environment.");
+ }
+
+ try
+ {
+ return await ExecuteSelfUpdateAsync(parseResult, cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ return CommandResult.Cancelled();
+ }
+ }
+
+ ///
+ /// Resolves the install source and canonical binary path for the running CLI
+ /// via (the single source of
+ /// truth for "what is the running binary?"). Falls back to
+ /// path-shape detection when no sidecar is
+ /// present, so legacy dotnet-tool installs created before the sidecar
+ /// contract shipped still get classified as
+ /// rather than .
+ ///
+ ///
+ /// When the initial discovery reports no route (i.e., no sidecar on disk
+ /// yet), runs on the binary directory
+ /// derived from the discovery's canonical path, then re-describes so the
+ /// freshly-stamped sidecar is picked up. Without this, a fresh WinGet
+ /// install whose very first invocation is aspire update --self
+ /// would classify as and be refused
+ /// until the user investigates or explicitly passes --force.
+ /// The probe is idempotent and self-gates via
+ /// , so the call is a cheap no-op
+ /// on non-Windows / non-WinGet installs.
+ ///
+ private (InstallSource Source, string? CanonicalPath) ResolveRunningInstall()
+ {
+ var info = _installationDiscovery.DescribeSelf();
+ var canonicalPath = info.CanonicalPath ?? info.Path;
+
+ if (string.IsNullOrEmpty(info.Route) && !string.IsNullOrEmpty(canonicalPath))
+ {
+ var binaryDir = Path.GetDirectoryName(canonicalPath);
+ if (!string.IsNullOrEmpty(binaryDir))
+ {
+ _wingetFirstRunProbe.Run(binaryDir);
+ info = _installationDiscovery.DescribeSelf();
+ canonicalPath = info.CanonicalPath ?? info.Path;
+ }
+ }
+
+ var source = InstallSourceExtensions.ParseInstallSource(info.Route);
+
+ // No-arg DotNetToolDetection.IsRunningAsDotNetTool honors the
+ // s_processPathOverride AsyncLocal used by tests, so this fallback
+ // recognizes legacy dotnet-tool installs (no sidecar baked) AND
+ // remains testable via UseProcessPathForTesting.
+ if (source == InstallSource.Unknown && DotNetToolDetection.IsRunningAsDotNetTool())
+ {
+ source = InstallSource.DotnetTool;
+ }
+
+ return (source, canonicalPath);
+ }
+
+ ///
+ /// Prints the installer-appropriate update command. Returns exit code 0
+ /// matching the existing dotnet-tool refusal contract — the CLI performed
+ /// its responsibility by telling the user what to run. Users who rely on
+ /// exit-code-based detection of "did this update?" should switch to checking
+ /// the version after the run, since the command may also succeed without
+ /// changing the binary if the user is already on the latest version.
+ ///
+ private CommandResult DisplaySelfUpdateRefusal(InstallSource source, string? canonicalPath)
+ {
+ var command = _upgradeInstructionProvider.GetUpdateCommand(
+ source,
+ canonicalPath,
+ ExecutionContext.IdentityChannel);
+
+ if (command is not null)
+ {
+ InteractionService.DisplayMessage(KnownEmojis.Information, GetSelfUpdateRefusalMessage(source));
+ InteractionService.DisplayPlainText($" {command}");
+ return CommandResult.FromExitCode(0);
+ }
+
+ InteractionService.DisplayMessage(KnownEmojis.Warning, UpdateCommandStrings.SelfUpdateUnknownSourceMessage);
+ return CommandResult.Failure(ExitCodeConstants.InvalidCommand, "Cannot determine install source for self-update.");
+ }
+
+ private static string GetSelfUpdateRefusalMessage(InstallSource source) => source switch
+ {
+ InstallSource.DotnetTool => UpdateCommandStrings.DotNetToolSelfUpdateMessage,
+ InstallSource.Winget => UpdateCommandStrings.WingetSelfUpdateMessage,
+ InstallSource.Brew => UpdateCommandStrings.BrewSelfUpdateMessage,
+ InstallSource.Pr => UpdateCommandStrings.PrSelfUpdateMessage,
+ InstallSource.LocalHive => UpdateCommandStrings.LocalHiveSelfUpdateMessage,
+ _ => UpdateCommandStrings.SelfUpdateUnknownSourceMessage,
+ };
+
private async Task ExecuteSelfUpdateAsync(ParseResult parseResult, CancellationToken cancellationToken, string? selectedChannel = null)
{
var channel = selectedChannel ?? parseResult.GetValue(_channelOption) ?? parseResult.GetValue(_qualityOption);
diff --git a/src/Aspire.Cli/Configuration/AspireConfigFile.cs b/src/Aspire.Cli/Configuration/AspireConfigFile.cs
index 0a79e064e87..78c26e5f3fb 100644
--- a/src/Aspire.Cli/Configuration/AspireConfigFile.cs
+++ b/src/Aspire.Cli/Configuration/AspireConfigFile.cs
@@ -91,7 +91,7 @@ public string? SdkVersion
/// Aspire channel for package resolution.
///
[JsonPropertyName("channel")]
- [Description("The Aspire channel to use for package resolution (e.g., \"stable\", \"preview\", \"staging\", \"daily\"). Used by aspire add to determine which NuGet feed to use.")]
+ [Description("The Aspire channel to use for package resolution (e.g., \"stable\", \"staging\", \"daily\", or a per-PR \"pr-\" label). Used by aspire add to determine which NuGet feed to use.")]
public string? Channel { get; set; }
///
diff --git a/src/Aspire.Cli/Configuration/AspireJsonConfiguration.cs b/src/Aspire.Cli/Configuration/AspireJsonConfiguration.cs
index feef9e57b28..3d314b65556 100644
--- a/src/Aspire.Cli/Configuration/AspireJsonConfiguration.cs
+++ b/src/Aspire.Cli/Configuration/AspireJsonConfiguration.cs
@@ -43,11 +43,11 @@ internal sealed class AspireJsonConfiguration
public string? Language { get; set; }
///
- /// The Aspire channel to use for package resolution (e.g., "stable", "preview", "staging").
+ /// The Aspire channel to use for package resolution (e.g., "stable", "staging", "daily", or "pr-<N>").
/// Used by aspire add to determine which NuGet feed to use.
///
[JsonPropertyName("channel")]
- [Description("The Aspire channel to use for package resolution (e.g., \"stable\", \"preview\", \"staging\"). Used by aspire add to determine which NuGet feed to use.")]
+ [Description("The Aspire channel to use for package resolution (e.g., \"stable\", \"staging\", \"daily\", or a per-PR \"pr-\" label). Used by aspire add to determine which NuGet feed to use.")]
public string? Channel { get; set; }
///
diff --git a/src/Aspire.Cli/Program.cs b/src/Aspire.Cli/Program.cs
index 5dd0478b8ce..091d20b2545 100644
--- a/src/Aspire.Cli/Program.cs
+++ b/src/Aspire.Cli/Program.cs
@@ -408,6 +408,7 @@ internal static async Task BuildApplicationAsync(string[] args, CliStartu
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
diff --git a/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs
index 4d11b077e99..c6e1c224c7d 100644
--- a/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs
+++ b/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs
@@ -107,6 +107,12 @@ internal static string ProjectArgumentDescription {
internal static string QualityOptionDescription => ResourceManager.GetString("QualityOptionDescription", resourceCulture);
internal static string QualityOptionDescriptionWithStaging => ResourceManager.GetString("QualityOptionDescriptionWithStaging", resourceCulture);
internal static string DotNetToolSelfUpdateMessage => ResourceManager.GetString("DotNetToolSelfUpdateMessage", resourceCulture);
+ internal static string WingetSelfUpdateMessage => ResourceManager.GetString("WingetSelfUpdateMessage", resourceCulture);
+ internal static string BrewSelfUpdateMessage => ResourceManager.GetString("BrewSelfUpdateMessage", resourceCulture);
+ internal static string PrSelfUpdateMessage => ResourceManager.GetString("PrSelfUpdateMessage", resourceCulture);
+ internal static string LocalHiveSelfUpdateMessage => ResourceManager.GetString("LocalHiveSelfUpdateMessage", resourceCulture);
+ internal static string SelfUpdateUnknownSourceMessage => ResourceManager.GetString("SelfUpdateUnknownSourceMessage", resourceCulture);
+ internal static string UnknownRouteRefusalHint => ResourceManager.GetString("UnknownRouteRefusalHint", resourceCulture);
internal static string MigratedToNewSdkFormat => ResourceManager.GetString("MigratedToNewSdkFormat", resourceCulture);
internal static string RemovedObsoleteAppHostPackage => ResourceManager.GetString("RemovedObsoleteAppHostPackage", resourceCulture);
internal static string NoWritePermissionToInstallDirectory => ResourceManager.GetString("NoWritePermissionToInstallDirectory", resourceCulture);
@@ -117,6 +123,7 @@ internal static string ProjectArgumentDescription {
internal static string RegeneratingSdkCode => ResourceManager.GetString("RegeneratingSdkCode", resourceCulture);
internal static string RegeneratedSdkCode => ResourceManager.GetString("RegeneratedSdkCode", resourceCulture);
internal static string SelfOptionDescription => ResourceManager.GetString("SelfOptionDescription", resourceCulture);
+ internal static string ForceOptionDescription => ResourceManager.GetString("ForceOptionDescription", resourceCulture);
internal static string YesOptionDescription => ResourceManager.GetString("YesOptionDescription", resourceCulture);
internal static string NuGetConfigDirOptionDescription => ResourceManager.GetString("NuGetConfigDirOptionDescription", resourceCulture);
}
diff --git a/src/Aspire.Cli/Resources/UpdateCommandStrings.resx b/src/Aspire.Cli/Resources/UpdateCommandStrings.resx
index de575d67b7a..93bccf1d325 100644
--- a/src/Aspire.Cli/Resources/UpdateCommandStrings.resx
+++ b/src/Aspire.Cli/Resources/UpdateCommandStrings.resx
@@ -136,7 +136,25 @@
Quality level to update to (stable, staging, daily)
- To update the Aspire CLI when installed as a .NET tool, run:
+ To update the Aspire CLI when installed as a .NET tool, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+
+
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+
+
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.Migrated to new project format: <Project Sdk="Aspire.AppHost.Sdk/{0}">
@@ -168,6 +186,9 @@
Update the Aspire CLI itself to the latest version
+
+ Attempt an in-process self-update even when the install route is normally refused
+
Automatically confirm all update prompts without asking
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf
index aa20cc66077..a209fe2a2bf 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf
@@ -37,6 +37,11 @@
Používají se aktualizace…
+
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Central package management is currently not supported by 'aspire update'.Centrální správa balíčků není v současné době přes aspire update podporována.
@@ -78,8 +83,8 @@
- To update the Aspire CLI when installed as a .NET tool, run:
- Chcete-li aktualizovat rozhraní příkazového řádku Aspire, pokud je nainstalované jako nástroj .NET, spusťte:
+ To update the Aspire CLI when installed as a .NET tool, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ Chcete-li aktualizovat rozhraní příkazového řádku Aspire, pokud je nainstalované jako nástroj .NET, spusťte:
@@ -117,6 +122,16 @@
Poznámka: Plán aktualizace byl vygenerován pomocí náhradní analýzy kvůli nevyřešitelné sadě AppHost SDK. Analýza závislostí může mít sníženou přesnost.
+
+ Attempt an in-process self-update even when the install route is normally refused
+ Attempt an in-process self-update even when the install route is normally refused
+
+
+
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Mapping: {0} (added) Mapování: {0} (přidáno)
@@ -182,6 +197,11 @@
Provést aktualizace?
+
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ The path to the Aspire AppHost project file or a directory to searchCesta k souboru projektu Aspire AppHost.
@@ -242,6 +262,16 @@
Update the Aspire CLI itself to the latest version
+
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+
+
+
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+
+ Unexpected code path.Neočekávaná cesta kódu.
@@ -267,6 +297,11 @@
Který adresář pro soubor NuGet.config?
+
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Automatically confirm all update prompts without askingAutomatically confirm all update prompts without asking
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf
index 1034647812b..a45046bed3c 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf
@@ -37,6 +37,11 @@
Updates werden angewendet...
+
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Central package management is currently not supported by 'aspire update'.Die zentrale Paketverwaltung wird von „aspire update“ zurzeit nicht unterstützt.
@@ -78,8 +83,8 @@
- To update the Aspire CLI when installed as a .NET tool, run:
- Um die Aspire-CLI bei der Installation als .NET-Tool zu aktualisieren, führen Sie folgende Schritte aus:
+ To update the Aspire CLI when installed as a .NET tool, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ Um die Aspire-CLI bei der Installation als .NET-Tool zu aktualisieren, führen Sie folgende Schritte aus:
@@ -117,6 +122,16 @@
Hinweis: Aktualisieren Sie den Plan, der mithilfe der Fallbackanalyse generiert wurde, da das AppHost-SDK nicht auflösbar ist. Die Abhängigkeitsanalyse weist möglicherweise eine geringere Genauigkeit auf.
+
+ Attempt an in-process self-update even when the install route is normally refused
+ Attempt an in-process self-update even when the install route is normally refused
+
+
+
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Mapping: {0} (added) Zuordnung: {0} (hinzugefügt)
@@ -182,6 +197,11 @@
Update ausführen?
+
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ The path to the Aspire AppHost project file or a directory to searchDer Pfad zur Aspire AppHost-Projektdatei.
@@ -242,6 +262,16 @@
Update the Aspire CLI itself to the latest version
+
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+
+
+
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+
+ Unexpected code path.Unerwarteter Codepfad
@@ -267,6 +297,11 @@
Welches Verzeichnis für die NuGet.config-Datei?
+
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Automatically confirm all update prompts without askingAutomatically confirm all update prompts without asking
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf
index 817354bde05..514e70805ad 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf
@@ -37,6 +37,11 @@
Aplicando actualizaciones...
+
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Central package management is currently not supported by 'aspire update'.La administración central de paquetes no es compatible actualmente con "aspire update".
@@ -78,8 +83,8 @@
- To update the Aspire CLI when installed as a .NET tool, run:
- Para actualizar la CLI de Aspire cuando está instalada como herramienta de .NET, ejecute:
+ To update the Aspire CLI when installed as a .NET tool, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ Para actualizar la CLI de Aspire cuando está instalada como herramienta de .NET, ejecute:
@@ -117,6 +122,16 @@
Nota: El plan de actualización se generó usando un análisis alternativo debido a un SDK de AppHost no resoluble. El análisis de dependencias puede ser menos preciso.
+
+ Attempt an in-process self-update even when the install route is normally refused
+ Attempt an in-process self-update even when the install route is normally refused
+
+
+
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Mapping: {0} (added) Asignación: {0} (agregado)
@@ -182,6 +197,11 @@
¿Desea realizar actualizaciones?
+
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ The path to the Aspire AppHost project file or a directory to searchLa ruta de acceso al archivo del proyecto host de la AppHost Aspire.
@@ -242,6 +262,16 @@
Update the Aspire CLI itself to the latest version
+
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+
+
+
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+
+ Unexpected code path.Ruta de acceso al código inesperada.
@@ -267,6 +297,11 @@
¿Qué directorio del archivo NuGet.config?
+
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Automatically confirm all update prompts without askingAutomatically confirm all update prompts without asking
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf
index bb81d4b058b..d21820133b0 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf
@@ -37,6 +37,11 @@
Application en cours des mises à jour...
+
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Central package management is currently not supported by 'aspire update'.Actuellement, la gestion centralisée des packages n’est pas prise en charge par « aspire update ».
@@ -78,8 +83,8 @@
- To update the Aspire CLI when installed as a .NET tool, run:
- Pour mettre à jour l’interface CLI Aspire lorsqu’elle est installée en tant qu’outil .NET, exécutez :
+ To update the Aspire CLI when installed as a .NET tool, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ Pour mettre à jour l’interface CLI Aspire lorsqu’elle est installée en tant qu’outil .NET, exécutez :
@@ -117,6 +122,16 @@
Remarque : plan de mise à jour généré à l’aide de l’analyse de secours en raison d’un Kit de développement logiciel (SDK) AppHost non résolu. L’analyse des dépendances peut avoir une précision réduite.
+
+ Attempt an in-process self-update even when the install route is normally refused
+ Attempt an in-process self-update even when the install route is normally refused
+
+
+
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Mapping: {0} (added) Mappage : {0} (ajouté)
@@ -182,6 +197,11 @@
Voulez-vous exécuter la mise à jour maintenant ?
+
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ The path to the Aspire AppHost project file or a directory to searchChemin d’accès au fichier projet AppHost Aspire.
@@ -242,6 +262,16 @@
Update the Aspire CLI itself to the latest version
+
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+
+
+
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+
+ Unexpected code path.Chemin de code inattendu.
@@ -267,6 +297,11 @@
Quel répertoire pour le fichier NuGet.config ?
+
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Automatically confirm all update prompts without askingAutomatically confirm all update prompts without asking
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf
index 2e39559bdbc..4c87b320292 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf
@@ -37,6 +37,11 @@
Applicazione degli aggiornamenti in corso...
+
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Central package management is currently not supported by 'aspire update'.La gestione centralizzata dei pacchetti non è attualmente supportata da "aspire update".
@@ -78,8 +83,8 @@
- To update the Aspire CLI when installed as a .NET tool, run:
- Per aggiornare l'interfaccia della riga di comando di Aspire quando è installata come strumento .NET, eseguire:
+ To update the Aspire CLI when installed as a .NET tool, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ Per aggiornare l'interfaccia della riga di comando di Aspire quando è installata come strumento .NET, eseguire:
@@ -117,6 +122,16 @@
Nota: il piano di aggiornamento è stato generato usando l'analisi di fallback perché AppHost SDK non è risolvibile. L'analisi delle dipendenze potrebbe risultare meno accurata.
+
+ Attempt an in-process self-update even when the install route is normally refused
+ Attempt an in-process self-update even when the install route is normally refused
+
+
+
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Mapping: {0} (added) Mapping: {0} (aggiunto)
@@ -182,6 +197,11 @@
Eseguire gli aggiornamenti?
+
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ The path to the Aspire AppHost project file or a directory to searchPercorso del file di un progetto AppHost di Aspire.
@@ -242,6 +262,16 @@
Update the Aspire CLI itself to the latest version
+
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+
+
+
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+
+ Unexpected code path.Percorso del codice imprevisto.
@@ -267,6 +297,11 @@
Quale directory per il file NuGet.config?
+
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Automatically confirm all update prompts without askingAutomatically confirm all update prompts without asking
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf
index 58f0d042195..71801d5117e 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf
@@ -37,6 +37,11 @@
更新プログラムを適用しています...
+
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Central package management is currently not supported by 'aspire update'.現在、中央パッケージ管理では、'aspire update' によるサポートがありません。
@@ -78,8 +83,8 @@
- To update the Aspire CLI when installed as a .NET tool, run:
- .NET ツールとしてインストールされたときに Aspire CLI を更新するには、次を実行します。
+ To update the Aspire CLI when installed as a .NET tool, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ .NET ツールとしてインストールされたときに Aspire CLI を更新するには、次を実行します。
@@ -117,6 +122,16 @@
注: 解決できない AppHost SDK が原因でフォールバック解析を使用して生成されたプランを更新します。依存関係分析の精度が低下する可能性があります。
+
+ Attempt an in-process self-update even when the install route is normally refused
+ Attempt an in-process self-update even when the install route is normally refused
+
+
+
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Mapping: {0} (added) マッピング: {0} (追加済み)
@@ -182,6 +197,11 @@
更新を実行しますか?
+
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ The path to the Aspire AppHost project file or a directory to searchAspire アプリ ホスティング プロセス プロジェクト ファイルへのパス。
@@ -242,6 +262,16 @@
Update the Aspire CLI itself to the latest version
+
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+
+
+
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+
+ Unexpected code path.予期しないコード パスです。
@@ -267,6 +297,11 @@
NuGet.config ファイル用の、どのディレクトリですか?
+
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Automatically confirm all update prompts without askingAutomatically confirm all update prompts without asking
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf
index bdf0ba6229a..25079b7d614 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf
@@ -37,6 +37,11 @@
업데이트 적용 중...
+
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Central package management is currently not supported by 'aspire update'.중앙 패키지 관리는 현재 'aspire update'에서 지원되지 않습니다.
@@ -78,8 +83,8 @@
- To update the Aspire CLI when installed as a .NET tool, run:
- .NET 도구로 설치된 Aspire CLI를 업데이트하려면 다음 명령을 실행합니다.
+ To update the Aspire CLI when installed as a .NET tool, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ .NET 도구로 설치된 Aspire CLI를 업데이트하려면 다음 명령을 실행합니다.
@@ -117,6 +122,16 @@
참고: 해결할 수 없는 AppHost SDK 때문에 대체 구문 분석을 사용하여 업데이트 계획이 생성되었습니다. 종속성 분석의 정확도가 낮아질 수 있습니다.
+
+ Attempt an in-process self-update even when the install route is normally refused
+ Attempt an in-process self-update even when the install route is normally refused
+
+
+
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Mapping: {0} (added) 매핑: {0}(추가됨)
@@ -182,6 +197,11 @@
업데이트를 수행하시겠습니까?
+
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ The path to the Aspire AppHost project file or a directory to searchAspire AppHost 프로젝트 파일의 경로입니다.
@@ -242,6 +262,16 @@
Update the Aspire CLI itself to the latest version
+
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+
+
+
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+
+ Unexpected code path.예기치 않은 코드 경로입니다.
@@ -267,6 +297,11 @@
NuGet.config 파일의 디렉터리는 무엇인가요?
+
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Automatically confirm all update prompts without askingAutomatically confirm all update prompts without asking
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf
index 249238b4f53..f045fe7729b 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf
@@ -37,6 +37,11 @@
Trwa stosowanie aktualizacji...
+
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Central package management is currently not supported by 'aspire update'.Centralne zarządzanie pakietami nie jest obecnie obsługiwane przez „aktualizację Aspire”.
@@ -78,8 +83,8 @@
- To update the Aspire CLI when installed as a .NET tool, run:
- Aby zaktualizować interfejs wiersza polecenia Aspire po zainstalowaniu jako narzędzie platformy .NET, uruchom polecenie:
+ To update the Aspire CLI when installed as a .NET tool, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ Aby zaktualizować interfejs wiersza polecenia Aspire po zainstalowaniu jako narzędzie platformy .NET, uruchom polecenie:
@@ -117,6 +122,16 @@
Uwaga: plan aktualizacji został wygenerowany przy użyciu analizy zapasowej z powodu nierozpoznanego zestawu AppHost SDK. Analiza zależności może być mniej dokładna.
+
+ Attempt an in-process self-update even when the install route is normally refused
+ Attempt an in-process self-update even when the install route is normally refused
+
+
+
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Mapping: {0} (added) Mapowanie: {0} (dodano)
@@ -182,6 +197,11 @@
Wykonać aktualizacje?
+
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ The path to the Aspire AppHost project file or a directory to searchŚcieżka do pliku projektu hosta AppHost platformy Aspire.
@@ -242,6 +262,16 @@
Update the Aspire CLI itself to the latest version
+
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+
+
+
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+
+ Unexpected code path.Nieoczekiwana ścieżka w kodzie.
@@ -267,6 +297,11 @@
Który katalog dla pliku NuGet.config?
+
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Automatically confirm all update prompts without askingAutomatically confirm all update prompts without asking
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf
index 950b43eb1d8..3473c9d9fb1 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf
@@ -37,6 +37,11 @@
Aplicando atualizações...
+
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Central package management is currently not supported by 'aspire update'.No momento, o gerenciamento central de pacotes por “atualização de atualização” não ´possui suporte.
@@ -78,8 +83,8 @@
- To update the Aspire CLI when installed as a .NET tool, run:
- Para atualizar a CLI do Aspire quando instalada como uma ferramenta do .NET, execute:
+ To update the Aspire CLI when installed as a .NET tool, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ Para atualizar a CLI do Aspire quando instalada como uma ferramenta do .NET, execute:
@@ -117,6 +122,16 @@
Observação: o plano de atualização gerado usando a análise de fallback devido ao SDK do AppHost não resolvido. A análise de dependência pode ter uma precisão reduzida.
+
+ Attempt an in-process self-update even when the install route is normally refused
+ Attempt an in-process self-update even when the install route is normally refused
+
+
+
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Mapping: {0} (added) Mapeamento: {0} (adicionado)
@@ -182,6 +197,11 @@
Executar atualizações?
+
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ The path to the Aspire AppHost project file or a directory to searchO caminho para o arquivo de projeto do Aspire AppHost.
@@ -242,6 +262,16 @@
Update the Aspire CLI itself to the latest version
+
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+
+
+
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+
+ Unexpected code path.Caminho de código inesperado.
@@ -267,6 +297,11 @@
Qual diretório para o arquivo NuGet.config?
+
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Automatically confirm all update prompts without askingAutomatically confirm all update prompts without asking
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf
index 43bd862ec89..00208c0af32 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf
@@ -37,6 +37,11 @@
Применение обновлений...
+
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Central package management is currently not supported by 'aspire update'.Централизованное управление пакетами в настоящее время не поддерживается функцией "обновление Aspire".
@@ -78,8 +83,8 @@
- To update the Aspire CLI when installed as a .NET tool, run:
- Чтобы обновить Aspire CLI при установке в качестве инструмента .NET, запустите:
+ To update the Aspire CLI when installed as a .NET tool, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ Чтобы обновить Aspire CLI при установке в качестве инструмента .NET, запустите:
@@ -117,6 +122,16 @@
Примечание. План обновления создан с использованием резервного анализа из-за невозможности разрешить SDK AppHost. Точность анализа зависимостей может быть снижена.
+
+ Attempt an in-process self-update even when the install route is normally refused
+ Attempt an in-process self-update even when the install route is normally refused
+
+
+
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Mapping: {0} (added) Сопоставление: {0} (добавлено)
@@ -182,6 +197,11 @@
Выполнить обновления?
+
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ The path to the Aspire AppHost project file or a directory to searchПуть к файлу проекта Aspire AppHost.
@@ -242,6 +262,16 @@
Update the Aspire CLI itself to the latest version
+
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+
+
+
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+
+ Unexpected code path.Непредвиденный путь к коду.
@@ -267,6 +297,11 @@
Какой каталог для файла NuGet.config?
+
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Automatically confirm all update prompts without askingAutomatically confirm all update prompts without asking
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf
index 91964f63232..90e2fb367c1 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf
@@ -37,6 +37,11 @@
Güncelleştirmeler uygulanıyor...
+
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Central package management is currently not supported by 'aspire update'.Merkezi paket yönetimi şu anda 'aspire update' tarafından desteklenmiyor.
@@ -78,8 +83,8 @@
- To update the Aspire CLI when installed as a .NET tool, run:
- .NET aracı olarak yüklenen Aspire CLI’yı güncelleştirmek için şu komutu çalıştırın:
+ To update the Aspire CLI when installed as a .NET tool, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ .NET aracı olarak yüklenen Aspire CLI’yı güncelleştirmek için şu komutu çalıştırın:
@@ -117,6 +122,16 @@
Not: Çözülemeyen AppHost SDK nedeniyle geri dönüş ayrıştırması kullanılarak oluşturulan güncelleştirme planı. Bağımlılık analizi doğruluğu azaltmış olabilir.
+
+ Attempt an in-process self-update even when the install route is normally refused
+ Attempt an in-process self-update even when the install route is normally refused
+
+
+
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Mapping: {0} (added) Eşleme: {0} (eklendi)
@@ -182,6 +197,11 @@
Güncelleştirmeler uygulansın mı?
+
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ The path to the Aspire AppHost project file or a directory to searchAspire AppHost proje dosyasının yolu.
@@ -242,6 +262,16 @@
Update the Aspire CLI itself to the latest version
+
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+
+
+
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+
+ Unexpected code path.Beklenmeyen kod yolu.
@@ -267,6 +297,11 @@
NuGet.config dosyası için hangi dizin kullanılsın?
+
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Automatically confirm all update prompts without askingAutomatically confirm all update prompts without asking
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf
index 408aede0fe6..72068250632 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf
@@ -37,6 +37,11 @@
正在应用更新...
+
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Central package management is currently not supported by 'aspire update'."aspire update" 当前不支持中央包管理。
@@ -78,8 +83,8 @@
- To update the Aspire CLI when installed as a .NET tool, run:
- 若要在 Aspire CLI 作为 .NET 工具安装时对其进行更新,请运行:
+ To update the Aspire CLI when installed as a .NET tool, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ 若要在 Aspire CLI 作为 .NET 工具安装时对其进行更新,请运行:
@@ -117,6 +122,16 @@
注意: 由于 AppHost SDK 无法解析,已采用回退解析生成更新计划。依赖项分析的准确性可能有所降低。
+
+ Attempt an in-process self-update even when the install route is normally refused
+ Attempt an in-process self-update even when the install route is normally refused
+
+
+
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Mapping: {0} (added) 映射: {0} (已添加)
@@ -182,6 +197,11 @@
是否执行更新?
+
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ The path to the Aspire AppHost project file or a directory to searchAspire AppHost 项目文件的路径。
@@ -242,6 +262,16 @@
Update the Aspire CLI itself to the latest version
+
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+
+
+
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+
+ Unexpected code path.意外的代码路径。
@@ -267,6 +297,11 @@
NuGet.config 文件位于哪个目录?
+
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Automatically confirm all update prompts without askingAutomatically confirm all update prompts without asking
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf
index 2c09fa9134d..c22d1d9a75c 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf
@@ -37,6 +37,11 @@
正在套用更新...
+
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via Homebrew. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Central package management is currently not supported by 'aspire update'.中央套件管理目前不受 'aspire update' 支援。
@@ -78,8 +83,8 @@
- To update the Aspire CLI when installed as a .NET tool, run:
- 若要以 .NET 工具形式安裝 Aspire CLI,請執行:
+ To update the Aspire CLI when installed as a .NET tool, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ 若要以 .NET 工具形式安裝 Aspire CLI,請執行:
@@ -117,6 +122,16 @@
注意: 由於無法解析的 AppHost SDK,已使用後援剖析產生更新計劃。相依性分析的正確性可能因此降低。
+
+ Attempt an in-process self-update even when the install route is normally refused
+ Attempt an in-process self-update even when the install route is normally refused
+
+
+
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Mapping: {0} (added) 對應: {0} (已新增)
@@ -182,6 +197,11 @@
執行更新?
+
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI is a PR / dogfood build. It is pinned to a specific pull request; `--self` cannot update it. To re-install or update to a different PR, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ The path to the Aspire AppHost project file or a directory to searchAspire AppHost 專案檔案的路徑。
@@ -242,6 +262,16 @@
Update the Aspire CLI itself to the latest version
+
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+ Cannot determine how this Aspire CLI was installed. Refusing to perform an in-process self-update to avoid corrupting the binary.
+
+
+
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+ Aspire couldn't determine how this CLI was installed (`.aspire-install.json` is missing, unreadable, or has an unrecognized route). Investigate the install, or pass `--force` to attempt an in-process update anyway.
+
+ Unexpected code path.未預期的程式碼路徑。
@@ -267,6 +297,11 @@
哪個目錄用於 NuGet.config 檔案?
+
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+ This Aspire CLI was installed via WinGet. To update it, run (or pass `--force` as a last-resort override to attempt an in-process update):
+
+ Automatically confirm all update prompts without askingAutomatically confirm all update prompts without asking
diff --git a/src/Aspire.Cli/Utils/CliUpdateNotifier.cs b/src/Aspire.Cli/Utils/CliUpdateNotifier.cs
index 3a5c0be2c5c..a194e053e00 100644
--- a/src/Aspire.Cli/Utils/CliUpdateNotifier.cs
+++ b/src/Aspire.Cli/Utils/CliUpdateNotifier.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Aspire.Cli.Acquisition;
using Aspire.Cli.Interaction;
using Aspire.Cli.NuGet;
using Aspire.Shared;
@@ -43,7 +44,11 @@ internal static class PackageUpdateRecommendationChannels
internal class CliUpdateNotifier(
ILogger logger,
INuGetPackageCache nuGetPackageCache,
- IInteractionService interactionService) : ICliUpdateNotifier
+ IInteractionService interactionService,
+ IInstallationDiscovery installationDiscovery,
+ IUpgradeInstructionProvider upgradeInstructionProvider,
+ CliExecutionContext executionContext,
+ WingetFirstRunProbe wingetFirstRunProbe) : ICliUpdateNotifier
{
private IEnumerable? _availablePackages;
@@ -116,7 +121,7 @@ private CliVersionStatus GetCachedVersionStatus(string? updateCheckError = null)
}
var newerVersion = PackageUpdateHelpers.GetNewerVersion(logger, currentVersion, _availablePackages);
- var updateCommand = newerVersion is null ? null : DotNetToolDetection.GetDotNetToolUpdateCommand() ?? "aspire update";
+ var updateCommand = newerVersion is null ? null : GetRouteAwareUpdateCommand();
// Derive the lane the recommendation comes from so doctor can show
// 'Latest version is X (channel: stable)' vs '(channel: prerelease)'.
// GetNewerVersion picks between newestStable and newestPrerelease
@@ -129,6 +134,53 @@ private CliVersionStatus GetCachedVersionStatus(string? updateCheckError = null)
return new CliVersionStatus(currentVersionString, newerVersion?.ToString(), updateCommand, UpdateCheckError: null, LatestVersionChannel: latestChannel);
}
+ ///
+ /// Returns the route-appropriate command to recommend in the
+ /// "version X available" notification. For script-route installs we
+ /// suggest aspire update --self. For every other route, including
+ /// Unknown, we defer to so
+ /// users see the command or refusal hint that matches how they installed
+ /// the CLI (winget upgrade, brew upgrade --cask, dotnet tool update,
+ /// get-aspire-cli-pr, etc.).
+ ///
+ ///
+ /// When the initial discovery reports no route (i.e., no sidecar on disk
+ /// yet), runs on the binary directory
+ /// derived from the discovery's canonical path, then re-describes so the
+ /// freshly-stamped sidecar is picked up. The probe self-gates via
+ /// , so the call is a cheap no-op
+ /// on non-Windows / non-WinGet installs.
+ ///
+ private string GetRouteAwareUpdateCommand()
+ {
+ var info = installationDiscovery.DescribeSelf();
+ var canonicalPath = info.CanonicalPath ?? info.Path;
+
+ if (string.IsNullOrEmpty(info.Route) && !string.IsNullOrEmpty(canonicalPath))
+ {
+ var binaryDir = Path.GetDirectoryName(canonicalPath);
+ if (!string.IsNullOrEmpty(binaryDir))
+ {
+ wingetFirstRunProbe.Run(binaryDir);
+ info = installationDiscovery.DescribeSelf();
+ canonicalPath = info.CanonicalPath ?? info.Path;
+ }
+ }
+
+ var source = InstallSourceExtensions.ParseInstallSource(info.Route);
+
+ // Legacy fallback for pre-sidecar dotnet-tool installs (mirrors the
+ // UpdateCommand --self resolution rule). Uses the no-arg overload so
+ // the AsyncLocal test override is honored.
+ if (source == InstallSource.Unknown && DotNetToolDetection.IsRunningAsDotNetTool())
+ {
+ source = InstallSource.DotnetTool;
+ }
+
+ return upgradeInstructionProvider.GetUpdateCommand(source, canonicalPath, executionContext.IdentityChannel)
+ ?? "aspire update --self";
+ }
+
private async Task> GetCliPackagesAsync(DirectoryInfo workingDirectory, CancellationToken cancellationToken)
{
return await nuGetPackageCache.GetCliPackagesAsync(
diff --git a/tests/Aspire.Cli.Tests/Acquisition/SelfUpdateRouterTests.cs b/tests/Aspire.Cli.Tests/Acquisition/SelfUpdateRouterTests.cs
new file mode 100644
index 00000000000..4288e85bd0e
--- /dev/null
+++ b/tests/Aspire.Cli.Tests/Acquisition/SelfUpdateRouterTests.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Cli.Acquisition;
+
+namespace Aspire.Cli.Tests.Acquisition;
+
+public class SelfUpdateRouterTests
+{
+ [Theory]
+ // Script-route installs stay in-process — the CLI owns the binary swap.
+ [InlineData("Script", "InProcess")]
+ // Unknown sources fail closed unless the caller explicitly opts into
+ // the UpdateCommand-layer --force override.
+ [InlineData("Unknown", "Delegate")]
+ // Every other route delegates — they're either pinned (PR), package-
+ // manager-owned (winget / brew / dotnet-tool), or rebuilt-from-source
+ // (localhive). An in-process binary swap would corrupt or demote them.
+ [InlineData("Pr", "Delegate")]
+ [InlineData("Winget", "Delegate")]
+ [InlineData("Brew", "Delegate")]
+ [InlineData("DotnetTool", "Delegate")]
+ [InlineData("LocalHive", "Delegate")]
+ public void GetAction_ReturnsExpectedAction(string sourceName, string expectedActionName)
+ {
+ var source = Enum.Parse(sourceName);
+ var expected = Enum.Parse(expectedActionName);
+
+ Assert.Equal(expected, SelfUpdateRouter.GetAction(source));
+ }
+}
diff --git a/tests/Aspire.Cli.Tests/Acquisition/UpdateCommandRouteRegressionTests.cs b/tests/Aspire.Cli.Tests/Acquisition/UpdateCommandRouteRegressionTests.cs
new file mode 100644
index 00000000000..55027f064cd
--- /dev/null
+++ b/tests/Aspire.Cli.Tests/Acquisition/UpdateCommandRouteRegressionTests.cs
@@ -0,0 +1,168 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Cli.Acquisition;
+using Aspire.Cli.Commands;
+using Aspire.Cli.Tests.TestServices;
+using Aspire.Cli.Tests.Utils;
+using Aspire.Cli.Utils;
+using Microsoft.AspNetCore.InternalTesting;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Aspire.Cli.Tests.Acquisition;
+
+///
+/// End-to-end regression guard for the silent route-demotion and
+/// package-manager binary-clobber bugs on aspire update --self.
+///
+public class UpdateCommandRouteRegressionTests(ITestOutputHelper outputHelper)
+{
+ [Theory]
+ [InlineData("script", "stable", false, true, null)]
+ [InlineData("script", "stable", true, true, null)]
+ [InlineData("brew", "stable", false, false, "brew upgrade --cask aspire")]
+ [InlineData("brew", "stable", true, true, null)]
+ [InlineData("winget", "stable", false, false, "winget upgrade Microsoft.Aspire")]
+ [InlineData("winget", "stable", true, true, null)]
+ [InlineData("dotnet-tool", "stable", false, false, "dotnet tool update -g Aspire.Cli")]
+ [InlineData("dotnet-tool", "stable", true, true, null)]
+ [InlineData("pr", "pr-16817", false, false, "get-aspire-cli-pr.sh 16817 # or: get-aspire-cli-pr.ps1 -PRNumber 16817")]
+ [InlineData("pr", "pr-16817", true, true, null)]
+ [InlineData("localhive", "local", false, false, "localhive.sh")]
+ [InlineData("localhive", "local", true, true, null)]
+ [InlineData(null, "stable", false, false, "Aspire couldn't determine how this CLI was installed")]
+ [InlineData(null, "stable", true, true, null)]
+ public async Task SelfUpdate_RouteGate_RefusesUnlessScriptOrForced(
+ string? sidecarSource,
+ string identityChannel,
+ bool force,
+ bool expectInProcess,
+ string? expectedOutput)
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ using var processPathScope = DotNetToolDetection.UseProcessPathForTesting(
+ Path.Combine(workspace.WorkspaceRoot.FullName, "bin", "aspire"));
+
+ var selfInfo = new InstallationInfo
+ {
+ Path = Path.Combine(workspace.WorkspaceRoot.FullName, "bin", "aspire"),
+ CanonicalPath = Path.Combine(workspace.WorkspaceRoot.FullName, "bin", "aspire"),
+ Route = sidecarSource,
+ Channel = identityChannel,
+ Status = sidecarSource is null ? InstallationInfoStatus.NotProbed : InstallationInfoStatus.Ok,
+ };
+
+ var downloadAttempted = false;
+ TestInteractionService? interactionService = null;
+ var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
+ {
+ options.InteractionServiceFactory = _ =>
+ {
+ interactionService = new TestInteractionService();
+ return interactionService;
+ };
+
+ options.CliExecutionContextFactory = _ =>
+ {
+ var root = workspace.WorkspaceRoot;
+ var hivesDirectory = new DirectoryInfo(Path.Combine(root.FullName, ".aspire", "hives"));
+ var cacheDirectory = new DirectoryInfo(Path.Combine(root.FullName, ".aspire", "cache"));
+ var logsDirectory = new DirectoryInfo(Path.Combine(root.FullName, ".aspire", "logs"));
+ var logFilePath = Path.Combine(logsDirectory.FullName, "test.log");
+ return new CliExecutionContext(
+ root,
+ hivesDirectory,
+ cacheDirectory,
+ new DirectoryInfo(Path.Combine(root.FullName, ".aspire", "sdks")),
+ logsDirectory,
+ logFilePath,
+ identityChannel: identityChannel);
+ };
+
+ options.CliDownloaderFactory = sp =>
+ {
+ var executionContext = sp.GetRequiredService();
+ return new TestCliDownloader(new DirectoryInfo(Path.Combine(executionContext.WorkingDirectory.FullName, "tmp")))
+ {
+ DownloadLatestCliAsyncCallback = (_, _) =>
+ {
+ downloadAttempted = true;
+ throw new InvalidOperationException("download attempted");
+ }
+ };
+ };
+ });
+
+ services.AddSingleton(_ => new FakeInstallationDiscovery(selfInfo));
+
+ using var provider = services.BuildServiceProvider();
+ var command = provider.GetRequiredService();
+ var args = force ? "update --self --force --yes --channel stable" : "update --self --yes --channel stable";
+ var exitCode = await command.Parse(args).InvokeAsync().DefaultTimeout();
+
+ Assert.NotNull(interactionService);
+ Assert.Equal(expectInProcess, downloadAttempted);
+
+ var allOutput = string.Join("\n", interactionService!.DisplayedPlainText.Concat(interactionService.DisplayedMessages.Select(m => m.Message)));
+ if (expectedOutput is not null)
+ {
+ Assert.Equal(0, exitCode);
+ Assert.Contains(expectedOutput, allOutput, StringComparison.Ordinal);
+ }
+ else
+ {
+ Assert.NotEqual(0, exitCode);
+ Assert.DoesNotContain("winget upgrade", allOutput, StringComparison.Ordinal);
+ Assert.DoesNotContain("brew upgrade", allOutput, StringComparison.Ordinal);
+ Assert.DoesNotContain("dotnet tool update", allOutput, StringComparison.Ordinal);
+ Assert.DoesNotContain("get-aspire-cli-pr", allOutput, StringComparison.Ordinal);
+ Assert.DoesNotContain("localhive.sh", allOutput, StringComparison.Ordinal);
+ Assert.DoesNotContain("couldn't determine how this CLI was installed", allOutput, StringComparison.Ordinal);
+ }
+ }
+
+ ///
+ /// Legacy compatibility check: when the running binary has no sidecar
+ /// (e.g., a dotnet-tool install that predates the sidecar contract)
+ /// BUT path-shape inspection identifies the binary as a dotnet tool,
+ /// the route is fixed up to and
+ /// refused with the dotnet-tool command rather than falling through to
+ /// the in-process update flow.
+ ///
+ [Fact]
+ public async Task SelfUpdate_NoSidecar_LegacyDotnetToolPathShape_RefusesWithDotnetToolHint()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ using var processPathScope = DotNetToolDetection.UseProcessPathForTesting(
+ "/home/test/.dotnet/tools/.store/aspire.cli/9.4.0/aspire.cli.linux-x64/9.4.0/tools/net10.0/linux-x64/aspire");
+
+ var selfInfo = new InstallationInfo
+ {
+ Path = "/home/test/.dotnet/tools/.store/aspire.cli/9.4.0/aspire.cli.linux-x64/9.4.0/tools/net10.0/linux-x64/aspire",
+ CanonicalPath = "/home/test/.dotnet/tools/.store/aspire.cli/9.4.0/aspire.cli.linux-x64/9.4.0/tools/net10.0/linux-x64/aspire",
+ Route = null,
+ Status = InstallationInfoStatus.Ok,
+ };
+
+ TestInteractionService? interactionService = null;
+ var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
+ {
+ options.InteractionServiceFactory = _ =>
+ {
+ interactionService = new TestInteractionService();
+ return interactionService;
+ };
+ });
+ services.AddSingleton(_ => new FakeInstallationDiscovery(selfInfo));
+
+ using var provider = services.BuildServiceProvider();
+ var command = provider.GetRequiredService();
+ var exitCode = await command.Parse("update --self").InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(0, exitCode);
+ Assert.NotNull(interactionService);
+ Assert.Contains(
+ interactionService!.DisplayedPlainText,
+ line => line.Contains("dotnet tool update -g Aspire.Cli", StringComparison.Ordinal));
+ }
+}
diff --git a/tests/Aspire.Cli.Tests/Acquisition/UpdateNotificationRouteTests.cs b/tests/Aspire.Cli.Tests/Acquisition/UpdateNotificationRouteTests.cs
new file mode 100644
index 00000000000..2d0b9a4d119
--- /dev/null
+++ b/tests/Aspire.Cli.Tests/Acquisition/UpdateNotificationRouteTests.cs
@@ -0,0 +1,107 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Cli.Acquisition;
+using Aspire.Cli.Interaction;
+using Aspire.Cli.NuGet;
+using Aspire.Cli.Tests.TestServices;
+using Aspire.Cli.Tests.Utils;
+using Aspire.Cli.Utils;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.AspNetCore.InternalTesting;
+using NuGetPackage = Aspire.Shared.NuGetPackageCli;
+
+namespace Aspire.Cli.Tests.Acquisition;
+
+///
+/// Locks in route-aware behavior of the CLI's "version X is available"
+/// notification. Each install route must surface the command that actually
+/// updates the binary the user is running — running the script-route
+/// command from a Homebrew / WinGet / dotnet-tool / PR install is the bug
+/// pattern this test class guards against.
+///
+public class UpdateNotificationRouteTests(ITestOutputHelper outputHelper)
+{
+ // Per-source expected notification command. We do not enumerate the
+ // dotnet-tool / Pr cases here because their commands depend on the
+ // running binary's path / identity channel; those are exercised
+ // separately in UpgradeInstructionProviderTests. The rows here lock in
+ // that the notifier wires its output through the same provider, not the
+ // legacy hardcoded "aspire update" / dotnet-tool special case.
+ [Theory]
+ [InlineData("winget", "winget upgrade Microsoft.Aspire")]
+ [InlineData("brew", "brew upgrade --cask aspire")]
+ [InlineData("localhive", "Run ./localhive.sh (Linux/macOS) or .\\localhive.ps1 (Windows) in the local hive directory.")]
+ [InlineData("script", "aspire update --self")]
+ [InlineData(null, "Aspire couldn't determine how this CLI was installed")]
+ // Unrecognized sidecar source value (a route added by a future build
+ // that this CLI doesn't know about yet). The reader returns
+ // InstallSource.Unknown with RawSource preserved; the notifier must
+ // still surface an actionable hint rather than printing the raw
+ // unrecognized name.
+ [InlineData("future-route-name", "Aspire couldn't determine how this CLI was installed")]
+ public async Task NotifyIfUpdateAvailable_RouteAwareCommand_MatchesUpgradeInstructionProvider(string? sidecarSource, string expectedCommand)
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+
+ var selfInfo = new InstallationInfo
+ {
+ Path = "/test/aspire",
+ CanonicalPath = "/test/aspire",
+ Route = sidecarSource,
+ Status = sidecarSource is null
+ ? InstallationInfoStatus.NotProbed
+ : InstallationInfoStatus.Ok,
+ };
+
+ TestInteractionService? interactionService = null;
+ var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, configure =>
+ {
+ configure.NuGetPackageCacheFactory = _ => new FakeNuGetPackageCache
+ {
+ GetCliPackagesAsyncCallback = (_, _, _, _) => Task.FromResult>(
+ [
+ new NuGetPackage { Id = "Aspire.Cli", Version = "9.5.0", Source = "nuget.org" }
+ ])
+ };
+
+ configure.InteractionServiceFactory = _ =>
+ {
+ interactionService = new TestInteractionService();
+ return interactionService;
+ };
+
+ configure.CliUpdateNotifierFactory = sp =>
+ {
+ var logger = sp.GetRequiredService>();
+ var nuGetPackageCache = sp.GetRequiredService();
+ var service = sp.GetRequiredService();
+ // Pin the current version so the fake "9.5.0" available
+ // package always reads as newer regardless of the test
+ // runner's actual version.
+ return new CliUpdateNotifierWithPackageVersionOverride(
+ "9.4.0", logger, nuGetPackageCache, service,
+ sp.GetRequiredService(),
+ sp.GetRequiredService(),
+ sp.GetRequiredService(),
+ sp.GetRequiredService());
+ };
+ });
+
+ // Replace the real InstallationDiscovery (which reads
+ // Environment.ProcessPath, not test overrides) with a fake that
+ // surfaces the route under test. Done after CreateServiceCollection
+ // so the last registration wins.
+ services.AddSingleton(_ => new FakeInstallationDiscovery(selfInfo));
+
+ using var provider = services.BuildServiceProvider();
+ var notifier = provider.GetRequiredService();
+
+ await notifier.CheckForCliUpdatesAsync(workspace.WorkspaceRoot, CancellationToken.None).DefaultTimeout();
+ notifier.NotifyIfUpdateAvailable();
+
+ Assert.NotNull(interactionService);
+ Assert.Contains(expectedCommand, interactionService.LastVersionUpdateCommand, StringComparison.Ordinal);
+ }
+}
diff --git a/tests/Aspire.Cli.Tests/Acquisition/UpgradeInstructionProviderTests.cs b/tests/Aspire.Cli.Tests/Acquisition/UpgradeInstructionProviderTests.cs
new file mode 100644
index 00000000000..a18d7c8e26b
--- /dev/null
+++ b/tests/Aspire.Cli.Tests/Acquisition/UpgradeInstructionProviderTests.cs
@@ -0,0 +1,156 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Cli.Acquisition;
+using Aspire.Cli.Utils;
+
+namespace Aspire.Cli.Tests.Acquisition;
+
+public class UpgradeInstructionProviderTests
+{
+ private static readonly UpgradeInstructionProvider s_provider = new();
+
+ // Routes that have a single canonical update command and don't depend on
+ // processPath or identityChannel.
+ [Theory]
+ [InlineData("Winget", "winget upgrade Microsoft.Aspire")]
+ [InlineData("Brew", "brew upgrade --cask aspire")]
+ [InlineData("LocalHive", "Run ./localhive.sh (Linux/macOS) or .\\localhive.ps1 (Windows) in the local hive directory.")]
+ public void GetUpdateCommand_StaticHintRoutes_ReturnExpectedCommand(string sourceName, string expected)
+ {
+ var source = Enum.Parse(sourceName);
+ var command = s_provider.GetUpdateCommand(source, processPath: null, identityChannel: "local");
+ Assert.Equal(expected, command);
+ }
+
+ // Routes where there is intentionally no separate update command — script
+ // gets the in-process flow.
+ [Theory]
+ [InlineData("Script")]
+ public void GetUpdateCommand_NoHintRoutes_ReturnNull(string sourceName)
+ {
+ var source = Enum.Parse(sourceName);
+ Assert.Null(s_provider.GetUpdateCommand(source, processPath: null, identityChannel: "local"));
+ }
+
+ // PR-route hints substitute the PR number parsed from the CLI's identity
+ // channel (CliExecutionContext.IdentityChannel == "pr-" for PR builds).
+ [Theory]
+ [InlineData("pr-16817", "get-aspire-cli-pr.sh 16817 # or: get-aspire-cli-pr.ps1 -PRNumber 16817")]
+ [InlineData("pr-1", "get-aspire-cli-pr.sh 1 # or: get-aspire-cli-pr.ps1 -PRNumber 1")]
+ public void GetUpdateCommand_Pr_SubstitutesPrNumberFromIdentityChannel(string identityChannel, string expected)
+ {
+ Assert.Equal(expected, s_provider.GetUpdateCommand(InstallSource.Pr, processPath: null, identityChannel));
+ }
+
+ // Defensive: when a PR sidecar is read on a binary whose identity channel
+ // is NOT a pr- form (shouldn't happen in practice but locks in deterministic
+ // output), emit the parameterised form so the user knows they must supply N.
+ [Theory]
+ [InlineData("stable")]
+ [InlineData("daily")]
+ [InlineData("local")]
+ [InlineData("pr-")] // pr- with no number
+ [InlineData("")]
+ public void GetUpdateCommand_Pr_WithNonPrIdentityChannel_FallsBackToParameterisedForm(string identityChannel)
+ {
+ var command = s_provider.GetUpdateCommand(InstallSource.Pr, processPath: null, identityChannel);
+ Assert.Equal("get-aspire-cli-pr.sh # or: get-aspire-cli-pr.ps1 -PRNumber ", command);
+ }
+
+ [Fact]
+ public void GetUpdateCommand_DotnetTool_Global_ReturnsGlobalUpdateCommand()
+ {
+ using var processPathScope = DotNetToolDetection.UseProcessPathForTesting(
+ "/home/test/.dotnet/tools/.store/aspire.cli/9.4.0/aspire.cli.linux-x64/9.4.0/tools/net10.0/linux-x64/aspire");
+
+ var command = s_provider.GetUpdateCommand(InstallSource.DotnetTool, processPath: null, identityChannel: "stable");
+
+ Assert.Equal("dotnet tool update -g Aspire.Cli", command);
+ }
+
+ [Fact]
+ public void GetUpdateCommand_DotnetTool_ToolPath_ReturnsPathAwareUpdateCommand()
+ {
+ // Standard --tool-path install layout: the binary lives under
+ // ///tools///aspire and its sibling .store
+ // directory makes the path-shape detector recognize it. The tool path
+ // is emitted unquoted when it contains no whitespace (see
+ // DotNetToolDetection.QuoteCommandArgument). Build the path using
+ // Path.DirectorySeparatorChar so the test passes on both Unix (where
+ // it's `/`) and Windows (where DotNetToolDetection.NormalizeDirectorySeparators
+ // produces `\` separators in the extracted toolPath).
+ var s = Path.DirectorySeparatorChar;
+ var toolPath = $"{s}opt{s}my-aspire";
+ using var processPathScope = DotNetToolDetection.UseProcessPathForTesting(
+ $"{toolPath}{s}.store{s}aspire.cli{s}9.4.0{s}aspire.cli.linux-x64{s}9.4.0{s}tools{s}net10.0{s}linux-x64{s}aspire");
+
+ var command = s_provider.GetUpdateCommand(InstallSource.DotnetTool, processPath: null, identityChannel: "stable");
+
+ Assert.Equal($"dotnet tool update --tool-path {toolPath} Aspire.Cli", command);
+ }
+
+ [Fact]
+ public void GetUpdateCommand_DotnetTool_ToolPathWithSpaces_QuotesPath()
+ {
+ // Paths with whitespace get quoted by DotNetToolDetection.QuoteCommandArgument
+ // so the resulting command remains a single argv element when copy-pasted
+ // into a shell. Platform-native separators (see ToolPath test for context).
+ var s = Path.DirectorySeparatorChar;
+ var toolPath = $"{s}opt{s}My Aspire";
+ using var processPathScope = DotNetToolDetection.UseProcessPathForTesting(
+ $"{toolPath}{s}.store{s}aspire.cli{s}9.4.0{s}aspire.cli.linux-x64{s}9.4.0{s}tools{s}net10.0{s}linux-x64{s}aspire");
+
+ var command = s_provider.GetUpdateCommand(InstallSource.DotnetTool, processPath: null, identityChannel: "stable");
+
+ Assert.Equal($"dotnet tool update --tool-path \"{toolPath}\" Aspire.Cli", command);
+ }
+
+ [Fact]
+ public void GetUpdateCommand_DotnetTool_PathShapeUnrecognized_FallsBackToGlobal()
+ {
+ // When the running process path doesn't match any known dotnet-tool
+ // store layout (e.g., legacy installs without the canonical store
+ // shape, or the test runner itself), the provider falls back to the
+ // global form so the message is at least actionable.
+ using var processPathScope = DotNetToolDetection.UseProcessPathForTesting("/random/path/aspire");
+
+ var command = s_provider.GetUpdateCommand(InstallSource.DotnetTool, processPath: null, identityChannel: "stable");
+
+ Assert.Equal("dotnet tool update -g Aspire.Cli", command);
+ }
+
+ [Fact]
+ public void GetUpdateCommand_DotnetTool_UsesSuppliedProcessPath()
+ {
+ var s = Path.DirectorySeparatorChar;
+ var toolPath = $"{s}opt{s}path-from-parameter";
+ using var processPathScope = DotNetToolDetection.UseProcessPathForTesting("/random/path/aspire");
+
+ var command = s_provider.GetUpdateCommand(
+ InstallSource.DotnetTool,
+ $"{toolPath}{s}.store{s}aspire.cli{s}9.4.0{s}aspire.cli.linux-x64{s}9.4.0{s}tools{s}net10.0{s}linux-x64{s}aspire",
+ identityChannel: "stable");
+
+ Assert.Equal($"dotnet tool update --tool-path {toolPath} Aspire.Cli", command);
+ }
+
+ [Fact]
+ public void GetUpdateCommand_Unknown_ReturnsForceHint()
+ {
+ var command = s_provider.GetUpdateCommand(InstallSource.Unknown, processPath: null, identityChannel: "stable");
+
+ Assert.NotNull(command);
+ Assert.Contains("--force", command, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void GetUpdateCommand_LocalHive_ReturnsBothScriptNames()
+ {
+ var command = s_provider.GetUpdateCommand(InstallSource.LocalHive, processPath: null, identityChannel: "local");
+
+ Assert.NotNull(command);
+ Assert.Contains("localhive.sh", command, StringComparison.Ordinal);
+ Assert.Contains("localhive.ps1", command, StringComparison.Ordinal);
+ }
+}
diff --git a/tests/Aspire.Cli.Tests/Acquisition/WingetFirstRunSelfUpdateGuardTests.cs b/tests/Aspire.Cli.Tests/Acquisition/WingetFirstRunSelfUpdateGuardTests.cs
new file mode 100644
index 00000000000..179193e851a
--- /dev/null
+++ b/tests/Aspire.Cli.Tests/Acquisition/WingetFirstRunSelfUpdateGuardTests.cs
@@ -0,0 +1,190 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Cli.Acquisition;
+using Aspire.Cli.Commands;
+using Aspire.Cli.Tests.TestServices;
+using Aspire.Cli.Tests.Utils;
+using Microsoft.AspNetCore.InternalTesting;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Aspire.Cli.Tests.Acquisition;
+
+///
+/// Regression guard for the WinGet first-run sidecar bypass. WinGet sidecars
+/// are stamped lazily by from
+/// 's extract path. If a fresh
+/// WinGet install's very first command is aspire update --self, the
+/// bundle path never runs, no sidecar exists, and a naive resolver would
+/// classify the route as — which routes
+/// to in-process update and silently overwrites the WinGet-owned binary.
+/// PR α adds a call inside
+/// UpdateCommand.ResolveRunningInstall (and
+/// CliUpdateNotifier.GetRouteAwareUpdateCommand) so the sidecar is
+/// stamped before the route decision is read.
+///
+public class WingetFirstRunSelfUpdateGuardTests(ITestOutputHelper outputHelper)
+{
+ [Fact]
+ public void Probe_WingetClassifiedBinary_StampsSidecarAtomically()
+ {
+ using var tempDir = new TempDirectory();
+ var binaryName = OperatingSystem.IsWindows() ? "aspire.exe" : "aspire";
+ File.WriteAllText(Path.Combine(tempDir.Path, binaryName), string.Empty);
+
+ var fakeRegistry = new FakeWingetRegistryReader { ShouldClassifyAsWingetPortable = true };
+ var probe = new WingetFirstRunProbe(fakeRegistry, NullLogger.Instance);
+
+ Assert.False(File.Exists(Path.Combine(tempDir.Path, ".aspire-install.json")));
+
+ probe.Run(tempDir.Path);
+
+ var info = new InstallSidecarReader().TryRead(tempDir.Path);
+ Assert.NotNull(info);
+ Assert.Equal(InstallSource.Winget, info!.Source);
+ }
+
+ [Fact]
+ public void Probe_NotAWingetInstall_DoesNotWriteSidecar()
+ {
+ using var tempDir = new TempDirectory();
+ File.WriteAllText(Path.Combine(tempDir.Path, "aspire"), string.Empty);
+
+ var fakeRegistry = new FakeWingetRegistryReader { ShouldClassifyAsWingetPortable = false };
+ var probe = new WingetFirstRunProbe(fakeRegistry, NullLogger.Instance);
+
+ probe.Run(tempDir.Path);
+
+ Assert.False(
+ File.Exists(Path.Combine(tempDir.Path, ".aspire-install.json")),
+ "Probe must NOT stamp a sidecar when the registry says this is not a WinGet install.");
+ }
+
+ ///
+ /// End-to-end integration: drives aspire update --self against a
+ /// controllable temp binary directory. Initially the sidecar is absent
+ /// (mirrors a fresh WinGet install state); the fake registry classifies
+ /// the running binary as a WinGet portable install. After the SUT runs:
+ /// (a) the sidecar must exist on disk (proves the probe was invoked
+ /// before the route decision), and (b) the printed refusal must contain
+ /// the WinGet update command (proves the route was re-resolved post-probe
+ /// and the gating took the WinGet branch instead of falling through to
+ /// in-process update).
+ ///
+ [Fact]
+ public async Task SelfUpdate_FirstRunWingetInstall_StampsSidecarAndRefusesWithWingetCommand()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+
+ // Controllable binary location — discovery surfaces this as
+ // CanonicalPath, the production code derives binaryDir from it,
+ // and the probe writes the sidecar here.
+ using var binaryHome = new TempDirectory();
+ var binaryName = OperatingSystem.IsWindows() ? "aspire.exe" : "aspire";
+ var binaryPath = Path.Combine(binaryHome.Path, binaryName);
+ File.WriteAllText(binaryPath, string.Empty);
+ var sidecarPath = Path.Combine(binaryHome.Path, ".aspire-install.json");
+ Assert.False(File.Exists(sidecarPath), "Pre-condition: no sidecar yet (first-run state).");
+
+ TestInteractionService? interactionService = null;
+ var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
+ {
+ options.InteractionServiceFactory = _ =>
+ {
+ interactionService = new TestInteractionService();
+ return interactionService;
+ };
+ });
+
+ services.AddSingleton(new FakeWingetRegistryReader { ShouldClassifyAsWingetPortable = true });
+ services.AddSingleton(sp => new WingetFirstRunProbe(
+ sp.GetRequiredService(),
+ NullLogger.Instance));
+ // SidecarBackedDiscovery re-reads the sidecar on every DescribeSelf
+ // call — so once the probe stamps it, the second describe sees Winget.
+ services.AddSingleton(_ => new SidecarBackedDiscovery(binaryPath));
+
+ using var provider = services.BuildServiceProvider();
+ var command = provider.GetRequiredService();
+ var exitCode = await command.Parse("update --self").InvokeAsync().DefaultTimeout();
+
+ Assert.True(File.Exists(sidecarPath),
+ "WingetFirstRunProbe must have stamped the sidecar during ResolveRunningInstall.");
+ var stamped = new InstallSidecarReader().TryRead(binaryHome.Path);
+ Assert.NotNull(stamped);
+ Assert.Equal(InstallSource.Winget, stamped!.Source);
+
+ Assert.Equal(0, exitCode);
+ Assert.NotNull(interactionService);
+ Assert.Contains(
+ interactionService!.DisplayedPlainText,
+ line => line.Contains("winget upgrade Microsoft.Aspire", StringComparison.Ordinal));
+ }
+
+ private sealed class FakeWingetRegistryReader : IWindowsRegistryReader
+ {
+ public bool ShouldClassifyAsWingetPortable { get; set; }
+
+ public bool HasWingetAspireUninstallEntry(string processPath) => ShouldClassifyAsWingetPortable;
+ }
+
+ private sealed class SpyWingetRegistryReader : IWindowsRegistryReader
+ {
+ public int QueryCount { get; private set; }
+
+ public bool HasWingetAspireUninstallEntry(string processPath)
+ {
+ QueryCount++;
+ return false;
+ }
+ }
+
+ private sealed class SidecarBackedDiscovery : IInstallationDiscovery
+ {
+ private readonly string _binaryPath;
+ private readonly InstallSidecarReader _reader = new();
+
+ public SidecarBackedDiscovery(string binaryPath)
+ {
+ _binaryPath = binaryPath;
+ }
+
+ public InstallationInfo DescribeSelf()
+ {
+ var binaryDir = Path.GetDirectoryName(_binaryPath)!;
+ var sidecar = _reader.TryRead(binaryDir);
+ return new InstallationInfo
+ {
+ Path = _binaryPath,
+ CanonicalPath = _binaryPath,
+ Route = sidecar?.Source.ToWireString() ?? sidecar?.RawSource,
+ Status = InstallationInfoStatus.Ok,
+ };
+ }
+
+ public Task> DiscoverAllAsync(CancellationToken cancellationToken)
+ => Task.FromResult>([DescribeSelf()]);
+ }
+
+ private sealed class TempDirectory : IDisposable
+ {
+ public string Path { get; }
+
+ public TempDirectory()
+ {
+ Path = Directory.CreateTempSubdirectory("aspire-winget-firstrun-").FullName;
+ }
+
+ public void Dispose()
+ {
+ try
+ {
+ Directory.Delete(Path, recursive: true);
+ }
+ catch (IOException) { }
+ catch (UnauthorizedAccessException) { }
+ }
+ }
+}
+
diff --git a/tests/Aspire.Cli.Tests/Commands/UpdateCommandNonInteractiveTests.cs b/tests/Aspire.Cli.Tests/Commands/UpdateCommandNonInteractiveTests.cs
new file mode 100644
index 00000000000..68e90d5afe6
--- /dev/null
+++ b/tests/Aspire.Cli.Tests/Commands/UpdateCommandNonInteractiveTests.cs
@@ -0,0 +1,130 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Cli.Commands;
+using Aspire.Cli.Packaging;
+using Aspire.Cli.Projects;
+using Aspire.Cli.Tests.TestServices;
+using Aspire.Cli.Tests.Utils;
+using Microsoft.AspNetCore.InternalTesting;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Aspire.Cli.Tests.Commands;
+
+///
+/// Regression coverage for aspire update --non-interactive code paths
+/// that historically reached an interactive prompt and crashed with
+/// ("Interactive input is not
+/// supported in this environment").
+///
+public class UpdateCommandNonInteractiveTests(ITestOutputHelper outputHelper)
+{
+ ///
+ /// Regression for #15600. Pre-fix: when hive directories existed under
+ /// ~/.aspire/hives/, the update flow called
+ /// PromptForSelectionAsync for channel selection regardless of
+ /// the host environment's interactive support. Post-fix: in a non-
+ /// interactive host the implicit channel is selected silently.
+ ///
+ /// Repro mirrors the issue's steps: hive directory exists, no
+ /// --channel argument, --non-interactive set. The fix is
+ /// orthogonal to whether --yes is present (the channel prompt
+ /// has no --yes binding), so we cover both forms.
+ ///
+ [Theory]
+ [InlineData("update --non-interactive --yes")]
+ [InlineData("update --channel stable --non-interactive --yes")]
+ public async Task NonInteractiveUpdate_WithHives_DoesNotCrashAtChannelPrompt(string commandLine)
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+
+ // Mirror #15600 setup: leftover PR-dogfood hive on disk.
+ var hivesDir = workspace.CreateDirectory(".aspire").CreateSubdirectory("hives");
+ hivesDir.CreateSubdirectory("pr-99999");
+
+ var promptInvoked = false;
+ var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
+ {
+ options.ProjectLocatorFactory = _ => new TestProjectLocator()
+ {
+ UseOrFindAppHostProjectFileAsyncCallback = (_, _, _) =>
+ Task.FromResult(new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj")))
+ };
+
+ options.DotNetCliRunnerFactory = _ => new TestDotNetCliRunner();
+
+ options.PackagingServiceFactory = _ => new TestPackagingService()
+ {
+ GetChannelsAsyncCallback = (_) =>
+ {
+ var fakeCache = new FakeNuGetPackageCache();
+ var implicitChannel = PackageChannel.CreateImplicitChannel(fakeCache);
+ var stableChannel = PackageChannel.CreateExplicitChannel("stable", PackageChannelQuality.Stable, mappings: null, fakeCache);
+ return Task.FromResult>(new[] { implicitChannel, stableChannel });
+ }
+ };
+
+ options.ProjectUpdaterFactory = _ => new TestProjectUpdater()
+ {
+ UpdateProjectAsyncCallback = (_, _) => Task.FromResult(new ProjectUpdateResult { UpdatedApplied = true })
+ };
+
+ // Fail the test loudly if anything still tries to prompt. The
+ // SUT must reach the project-update path without touching this.
+ options.InteractionServiceFactory = _ => new TestInteractionService
+ {
+ PromptForSelectionCallback = (_, _, _, _) =>
+ {
+ promptInvoked = true;
+ throw new InvalidOperationException("Interactive input is not supported in this environment.");
+ },
+ };
+ });
+
+ using var provider = services.BuildServiceProvider();
+ var command = provider.GetRequiredService();
+ var parsed = command.Parse(commandLine);
+ var exitCode = await parsed.InvokeAsync().DefaultTimeout();
+
+ Assert.False(promptInvoked, "Channel prompt must not be invoked in non-interactive mode (#15600).");
+ Assert.Equal(ExitCodeConstants.Success, exitCode);
+ }
+
+ ///
+ /// Regression for #15601. The repro from the issue
+ /// (aspire update --non-interactive without --yes) now
+ /// fails command-line validation early via
+ /// ,
+ /// surfacing a clear error message instead of crashing at the
+ /// downgrade-confirmation prompt. This locks that contract in.
+ ///
+ [Fact]
+ public async Task NonInteractiveUpdate_WithoutYes_FailsValidationEarlyInsteadOfCrashingAtConfirmPrompt()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+
+ var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
+ {
+ options.ProjectLocatorFactory = _ => new TestProjectLocator()
+ {
+ UseOrFindAppHostProjectFileAsyncCallback = (_, _, _) =>
+ Task.FromResult(new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "AppHost.csproj")))
+ };
+
+ options.InteractionServiceFactory = _ => new TestInteractionService
+ {
+ ConfirmCallback = (_, _) =>
+ throw new InvalidOperationException("Interactive input is not supported in this environment."),
+ };
+ });
+
+ using var provider = services.BuildServiceProvider();
+ var command = provider.GetRequiredService();
+ var parsed = command.Parse("update --non-interactive");
+ var exitCode = await parsed.InvokeAsync().DefaultTimeout();
+
+ // Validator should fail this with InvalidCommand (non-zero) rather
+ // than letting the command reach ProjectUpdater's ConfirmAsync.
+ Assert.NotEqual(ExitCodeConstants.Success, exitCode);
+ }
+}
diff --git a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs
index 931b409b417..003ce4f5e9a 100644
--- a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs
+++ b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs
@@ -13,6 +13,7 @@
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using Aspire.Cli.Utils;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Spectre.Console;
using Spectre.Console.Rendering;
@@ -1085,6 +1086,12 @@ public async Task UpdateCommand_ProjectUpdate_WhenCancelled_DisplaysCancellation
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
+ // The channel prompt only fires when the host environment is
+ // interactive (regression guard for #15600). Default test setup
+ // uses non-interactive, so opt in here so we can exercise the
+ // cancellation path through the prompt.
+ options.CliHostEnvironmentFactory = sp => new CliHostEnvironment(sp.GetRequiredService(), nonInteractive: false);
+
options.ProjectLocatorFactory = _ => new TestProjectLocator()
{
UseOrFindAppHostProjectFileAsyncCallback = (projectFile, _, _) =>
@@ -1481,6 +1488,12 @@ public async Task UpdateCommand_WithHives_PromptOffersChannelsInPackagingService
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
+ // The channel prompt only fires when the host environment is
+ // interactive (regression guard for #15600). Default test setup
+ // uses non-interactive, so opt in here to exercise the prompt's
+ // ordering / formatting contract.
+ options.CliHostEnvironmentFactory = sp => new CliHostEnvironment(sp.GetRequiredService(), nonInteractive: false);
+
options.ProjectLocatorFactory = _ => new TestProjectLocator()
{
UseOrFindAppHostProjectFileAsyncCallback = (projectFile, _, _) =>
diff --git a/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs
index 866d4b4d8be..614f7e8f227 100644
--- a/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs
+++ b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs
@@ -159,6 +159,7 @@ public static IServiceCollection CreateServiceCollection(TemporaryWorkspace work
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
if (OperatingSystem.IsWindows())
{
@@ -362,7 +363,11 @@ private static IAnsiConsole CreateAnsiConsole(TextWriter textWriter, bool ansi =
var logger = NullLoggerFactory.Instance.CreateLogger();
var nuGetPackageCache = serviceProvider.GetRequiredService();
var interactionService = serviceProvider.GetRequiredService();
- return new CliUpdateNotifier(logger, nuGetPackageCache, interactionService);
+ var installationDiscovery = serviceProvider.GetRequiredService();
+ var upgradeInstructionProvider = serviceProvider.GetRequiredService();
+ var executionContext = serviceProvider.GetRequiredService();
+ var wingetFirstRunProbe = serviceProvider.GetRequiredService();
+ return new CliUpdateNotifier(logger, nuGetPackageCache, interactionService, installationDiscovery, upgradeInstructionProvider, executionContext, wingetFirstRunProbe);
};
public Func AddCommandPrompterFactory { get; set; } = (IServiceProvider serviceProvider) =>
diff --git a/tests/Aspire.Cli.Tests/Utils/CliUpdateNotificationServiceTests.cs b/tests/Aspire.Cli.Tests/Utils/CliUpdateNotificationServiceTests.cs
index 10cbbd35aeb..5a1aeb6d256 100644
--- a/tests/Aspire.Cli.Tests/Utils/CliUpdateNotificationServiceTests.cs
+++ b/tests/Aspire.Cli.Tests/Utils/CliUpdateNotificationServiceTests.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Aspire.Cli.Acquisition;
using Aspire.Cli.Interaction;
using Aspire.Cli.NuGet;
using Aspire.Cli.Tests.TestServices;
@@ -56,7 +57,7 @@ public async Task PrereleaseWillRecommendUpgradeToPrereleaseOnSameVersionFamily(
var interactionService = sp.GetRequiredService();
// Use a custom notifier that overrides the current version
- return new CliUpdateNotifierWithPackageVersionOverride("9.4.0-dev", logger, nuGetPackageCache, interactionService);
+ return new CliUpdateNotifierWithPackageVersionOverride("9.4.0-dev", logger, nuGetPackageCache, interactionService, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService());
};
});
@@ -108,7 +109,7 @@ public async Task PrereleaseWillRecommendUpgradeToStableInCurrentVersionFamily()
var interactionService = sp.GetRequiredService();
// Use a custom notifier that overrides the current version
- return new CliUpdateNotifierWithPackageVersionOverride("9.4.0-dev", logger, nuGetPackageCache, interactionService);
+ return new CliUpdateNotifierWithPackageVersionOverride("9.4.0-dev", logger, nuGetPackageCache, interactionService, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService());
};
});
@@ -160,7 +161,7 @@ public async Task StableWillOnlyRecommendGoingToNewerStable()
var interactionService = sp.GetRequiredService();
// Use a custom notifier that overrides the current version
- return new CliUpdateNotifierWithPackageVersionOverride("9.4.0", logger, nuGetPackageCache, interactionService);
+ return new CliUpdateNotifierWithPackageVersionOverride("9.4.0", logger, nuGetPackageCache, interactionService, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService());
};
});
@@ -196,7 +197,7 @@ public void NotifyIfUpdateAvailable_WithoutCachedPackages_DoesNotNotify()
var logger = sp.GetRequiredService>();
var nuGetPackageCache = sp.GetRequiredService();
var interactionService = sp.GetRequiredService();
- return new CliUpdateNotifierWithPackageVersionOverride("9.4.0", logger, nuGetPackageCache, interactionService);
+ return new CliUpdateNotifierWithPackageVersionOverride("9.4.0", logger, nuGetPackageCache, interactionService, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService());
};
});
@@ -233,7 +234,7 @@ public async Task NotifyIfUpdateAvailable_UsesDotnetToolCommandForNativeAotToolS
var logger = sp.GetRequiredService>();
var nuGetPackageCache = sp.GetRequiredService();
var service = sp.GetRequiredService();
- return new CliUpdateNotifierWithPackageVersionOverride("9.4.0", logger, nuGetPackageCache, service);
+ return new CliUpdateNotifierWithPackageVersionOverride("9.4.0", logger, nuGetPackageCache, service, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService());
};
});
@@ -276,7 +277,7 @@ public async Task NotifyIfUpdateAvailable_UsesToolPathCommandForCustomToolPath()
var logger = sp.GetRequiredService>();
var nuGetPackageCache = sp.GetRequiredService();
var service = sp.GetRequiredService();
- return new CliUpdateNotifierWithPackageVersionOverride("9.4.0", logger, nuGetPackageCache, service);
+ return new CliUpdateNotifierWithPackageVersionOverride("9.4.0", logger, nuGetPackageCache, service, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService());
};
});
@@ -291,7 +292,7 @@ public async Task NotifyIfUpdateAvailable_UsesToolPathCommandForCustomToolPath()
}
[Fact]
- public async Task NotifyIfUpdateAvailable_UsesAspireUpdateCommandForStandaloneArchivePath()
+ public async Task NotifyIfUpdateAvailable_UsesAspireUpdateSelfCommandForStandaloneArchivePath()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
using var processPathScope = DotNetToolDetection.UseProcessPathForTesting("/home/test/.aspire/bin/aspire");
@@ -317,7 +318,7 @@ public async Task NotifyIfUpdateAvailable_UsesAspireUpdateCommandForStandaloneAr
var logger = sp.GetRequiredService>();
var nuGetPackageCache = sp.GetRequiredService();
var service = sp.GetRequiredService();
- return new CliUpdateNotifierWithPackageVersionOverride("9.4.0", logger, nuGetPackageCache, service);
+ return new CliUpdateNotifierWithPackageVersionOverride("9.4.0", logger, nuGetPackageCache, service, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService());
};
});
@@ -328,7 +329,12 @@ public async Task NotifyIfUpdateAvailable_UsesAspireUpdateCommandForStandaloneAr
notifier.NotifyIfUpdateAvailable();
Assert.NotNull(interactionService);
- Assert.Equal("aspire update", interactionService.LastVersionUpdateCommand);
+ // Script-route (and Unknown / legacy archive installs lacking a sidecar)
+ // recommend `aspire update --self` — the route-correct command that
+ // actually performs the in-process binary swap. The previous
+ // recommendation `aspire update` runs the project-update flow, not the
+ // CLI self-update, and was misleading for users hitting this notification.
+ Assert.Equal("aspire update --self", interactionService.LastVersionUpdateCommand);
}
[Fact]
@@ -365,7 +371,7 @@ public async Task StableWillNotRecommendUpdatingToPreview()
var interactionService = sp.GetRequiredService();
// Use a custom notifier that overrides the current version
- return new CliUpdateNotifierWithPackageVersionOverride("9.4.0", logger, nuGetPackageCache, interactionService);
+ return new CliUpdateNotifierWithPackageVersionOverride("9.4.0", logger, nuGetPackageCache, interactionService, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService());
};
});
@@ -449,7 +455,7 @@ private static string GetAspireExecutableName()
}
}
-internal sealed class CliUpdateNotifierWithPackageVersionOverride(string currentVersion, ILogger logger, INuGetPackageCache nuGetPackageCache, IInteractionService interactionService) : CliUpdateNotifier(logger, nuGetPackageCache, interactionService)
+internal sealed class CliUpdateNotifierWithPackageVersionOverride(string currentVersion, ILogger logger, INuGetPackageCache nuGetPackageCache, IInteractionService interactionService, IInstallationDiscovery installationDiscovery, IUpgradeInstructionProvider upgradeInstructionProvider, CliExecutionContext executionContext, WingetFirstRunProbe wingetFirstRunProbe) : CliUpdateNotifier(logger, nuGetPackageCache, interactionService, installationDiscovery, upgradeInstructionProvider, executionContext, wingetFirstRunProbe)
{
protected override SemVersion? GetCurrentVersion()
{