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 search Cesta 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 asking Automatically 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 search Der 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 asking Automatically 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 search La 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 asking Automatically 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 search Chemin 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 asking Automatically 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 search Percorso 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 asking Automatically 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 search 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. 予期しないコード パスです。 @@ -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 asking Automatically 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 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 asking Automatically 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 asking Automatically 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 search O 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 asking Automatically 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 asking Automatically 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 search Aspire 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 asking Automatically 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 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 asking Automatically 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 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 asking Automatically 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() {