From 233039acb0b19a86da0cb60f6a77df8d16ced676 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 14 May 2026 19:31:02 -0400 Subject: [PATCH 1/8] feat(cli): aspire update --self refuses non-script install routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #15600 partial scope (PR γ stack — does not close issue): - Adds SelfUpdateRouter (pure policy lookup mapping InstallSource to in-process vs delegate action). Script + Unknown stay in-process for legacy compatibility; Pr/Winget/Brew/DotnetTool/LocalHive delegate. - Adds IUpgradeInstructionProvider + UpgradeInstructionProvider returning the route-appropriate update command. Reuses DotNetToolDetection.GetDotNetToolUpdateCommand for the dotnet-tool case (correctly handles -g vs --tool-path installs). PR-route command substitutes the PR number from CliExecutionContext.IdentityChannel. - UpdateCommand --self resolves the running install via IInstallationDiscovery.DescribeSelf (single source of truth — no new symlink-resolution duplicated). Falls back to no-arg DotNetToolDetection.IsRunningAsDotNetTool so the AsyncLocal test override continues to work and legacy dotnet-tool installs without sidecars are still recognized. - Adds 5 RESX strings for per-route refusal messages; xlf for 14 locales. Closes the silent-PR-demotion and package-manager binary-clobber regressions on `aspire update --self` (the silent overwrite path now gates on InstallSource and only the script route runs in-process). Tests: 23 new (Theory-flattened where applicable). All 48 existing UpdateCommand tests and 177 acquisition-area tests still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Acquisition/SelfUpdateRouter.cs | 57 +++++++ .../Acquisition/UpgradeInstructionProvider.cs | 100 ++++++++++++ src/Aspire.Cli/Commands/UpdateCommand.cs | 150 ++++++++++++++---- src/Aspire.Cli/Program.cs | 1 + .../UpdateCommandStrings.Designer.cs | 5 + .../Resources/UpdateCommandStrings.resx | 15 ++ .../Resources/xlf/UpdateCommandStrings.cs.xlf | 25 +++ .../Resources/xlf/UpdateCommandStrings.de.xlf | 25 +++ .../Resources/xlf/UpdateCommandStrings.es.xlf | 25 +++ .../Resources/xlf/UpdateCommandStrings.fr.xlf | 25 +++ .../Resources/xlf/UpdateCommandStrings.it.xlf | 25 +++ .../Resources/xlf/UpdateCommandStrings.ja.xlf | 25 +++ .../Resources/xlf/UpdateCommandStrings.ko.xlf | 25 +++ .../Resources/xlf/UpdateCommandStrings.pl.xlf | 25 +++ .../xlf/UpdateCommandStrings.pt-BR.xlf | 25 +++ .../Resources/xlf/UpdateCommandStrings.ru.xlf | 25 +++ .../Resources/xlf/UpdateCommandStrings.tr.xlf | 25 +++ .../xlf/UpdateCommandStrings.zh-Hans.xlf | 25 +++ .../xlf/UpdateCommandStrings.zh-Hant.xlf | 25 +++ .../Acquisition/SelfUpdateRouterTests.cs | 31 ++++ .../UpgradeInstructionProviderTests.cs | 118 ++++++++++++++ tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs | 1 + 22 files changed, 769 insertions(+), 34 deletions(-) create mode 100644 src/Aspire.Cli/Acquisition/SelfUpdateRouter.cs create mode 100644 src/Aspire.Cli/Acquisition/UpgradeInstructionProvider.cs create mode 100644 tests/Aspire.Cli.Tests/Acquisition/SelfUpdateRouterTests.cs create mode 100644 tests/Aspire.Cli.Tests/Acquisition/UpgradeInstructionProviderTests.cs diff --git a/src/Aspire.Cli/Acquisition/SelfUpdateRouter.cs b/src/Aspire.Cli/Acquisition/SelfUpdateRouter.cs new file mode 100644 index 00000000000..9cef35e7481 --- /dev/null +++ b/src/Aspire.Cli/Acquisition/SelfUpdateRouter.cs @@ -0,0 +1,57 @@ +// 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 . The exit + /// code is non-zero so scripts notice the no-op. + /// + 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. also stays + /// in-process for legacy compatibility: pre-PR-#16817 installs (script + /// route, before the sidecar contract shipped) have no sidecar, and + /// preserving in-process update for them avoids breaking working setups. + /// The PR-#16817-and-later install paths for the gated routes (PR / winget + /// / brew / dotnet-tool / localhive) all write sidecars at install time, + /// so a post-PR-#16817 binary appearing as + /// is almost certainly a legacy script install rather than one of those + /// routes. + /// + public static SelfUpdateAction GetAction(InstallSource source) => source switch + { + InstallSource.Script or InstallSource.Unknown => 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..368deaa5eed --- /dev/null +++ b/src/Aspire.Cli/Acquisition/UpgradeInstructionProvider.cs @@ -0,0 +1,100 @@ +// 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.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", + + // DotNetToolDetection.GetDotNetToolUpdateCommand (no-arg) honors + // the s_processPathOverride AsyncLocal used by tests AND inspects + // Environment.ProcessPath in production, returning the right + // command for `-g` (global) vs `--tool-path` installs. Falling + // back to the global form if path detection fails preserves the + // most common case. + InstallSource.DotnetTool => DotNetToolDetection.GetDotNetToolUpdateCommand() + ?? "dotnet tool update -g Aspire.Cli", + + // 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 => "./localhive.sh # re-run from your Aspire checkout", + + _ => null, + }; + } + + private const string PrChannelPrefix = "pr-"; + + 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..4e064861890 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,6 +33,8 @@ 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 static readonly OptionWithLegacy s_appHostOption = new("--apphost", "--project", UpdateCommandStrings.ProjectArgumentDescription); private static readonly Option s_selfOption = new("--self") @@ -62,7 +65,9 @@ public UpdateCommand( CliExecutionContext executionContext, IConfigurationService configurationService, AspireCliTelemetry telemetry, - IConfiguration configuration) + IConfiguration configuration, + IInstallationDiscovery installationDiscovery, + IUpgradeInstructionProvider upgradeInstructionProvider) : base("update", UpdateCommandStrings.Description, features, updateNotifier, executionContext, interactionService, telemetry) { _projectLocator = projectLocator; @@ -74,6 +79,8 @@ public UpdateCommand( _features = features; _configurationService = configurationService; _configuration = configuration; + _installationDiscovery = installationDiscovery; + _upgradeInstructionProvider = upgradeInstructionProvider; Options.Add(s_appHostOption); Options.Add(s_selfOption); @@ -106,11 +113,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 +120,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 @@ -257,12 +238,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 +267,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 +276,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 +297,103 @@ 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 action = SelfUpdateRouter.GetAction(source); + + if (action == SelfUpdateAction.Delegate) + { + return DisplaySelfUpdateRefusal(source, canonicalPath); + } + + // Script route: 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 . + /// + private (InstallSource Source, string? CanonicalPath) ResolveRunningInstall() + { + var info = _installationDiscovery.DescribeSelf(); + var source = InstallSourceExtensions.ParseInstallSource(info.Route); + var canonicalPath = info.CanonicalPath ?? info.Path; + + // 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/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..8b87c374182 100644 --- a/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs +++ b/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs @@ -107,6 +107,11 @@ 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 MigratedToNewSdkFormat => ResourceManager.GetString("MigratedToNewSdkFormat", resourceCulture); internal static string RemovedObsoleteAppHostPackage => ResourceManager.GetString("RemovedObsoleteAppHostPackage", resourceCulture); internal static string NoWritePermissionToInstallDirectory => ResourceManager.GetString("NoWritePermissionToInstallDirectory", resourceCulture); diff --git a/src/Aspire.Cli/Resources/UpdateCommandStrings.resx b/src/Aspire.Cli/Resources/UpdateCommandStrings.resx index de575d67b7a..bc6da1bc4ec 100644 --- a/src/Aspire.Cli/Resources/UpdateCommandStrings.resx +++ b/src/Aspire.Cli/Resources/UpdateCommandStrings.resx @@ -138,6 +138,21 @@ To update the Aspire CLI when installed as a .NET tool, run: + + This Aspire CLI was installed via WinGet. To update it, run: + + + This Aspire CLI was installed via Homebrew. To update it, run: + + + 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: + + + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + + + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + Migrated to new project format: <Project Sdk="Aspire.AppHost.Sdk/{0}"> diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf index aa20cc66077..0bb056677ff 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: + This Aspire CLI was installed via Homebrew. To update it, run: + + 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. @@ -117,6 +122,11 @@ 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. + + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + + Mapping: {0} (added) Mapování: {0} (přidáno) @@ -182,6 +192,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: + 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: + + The path to the Aspire AppHost project file or a directory to search Cesta k souboru projektu Aspire AppHost. @@ -242,6 +257,11 @@ Update the Aspire CLI itself to the latest version + + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + + Unexpected code path. Neočekávaná cesta kódu. @@ -267,6 +287,11 @@ Který adresář pro soubor NuGet.config? + + This Aspire CLI was installed via WinGet. To update it, run: + This Aspire CLI was installed via WinGet. To update it, run: + + 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..1fc90e80b73 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: + This Aspire CLI was installed via Homebrew. To update it, run: + + Central package management is currently not supported by 'aspire update'. Die zentrale Paketverwaltung wird von „aspire update“ zurzeit nicht unterstützt. @@ -117,6 +122,11 @@ 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. + + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + + Mapping: {0} (added) Zuordnung: {0} (hinzugefügt) @@ -182,6 +192,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: + 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: + + The path to the Aspire AppHost project file or a directory to search Der Pfad zur Aspire AppHost-Projektdatei. @@ -242,6 +257,11 @@ Update the Aspire CLI itself to the latest version + + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + + Unexpected code path. Unerwarteter Codepfad @@ -267,6 +287,11 @@ Welches Verzeichnis für die NuGet.config-Datei? + + This Aspire CLI was installed via WinGet. To update it, run: + This Aspire CLI was installed via WinGet. To update it, run: + + 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..e281207f047 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: + This Aspire CLI was installed via Homebrew. To update it, run: + + Central package management is currently not supported by 'aspire update'. La administración central de paquetes no es compatible actualmente con "aspire update". @@ -117,6 +122,11 @@ 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. + + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + + Mapping: {0} (added) Asignación: {0} (agregado) @@ -182,6 +192,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: + 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: + + 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 +257,11 @@ Update the Aspire CLI itself to the latest version + + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + + Unexpected code path. Ruta de acceso al código inesperada. @@ -267,6 +287,11 @@ ¿Qué directorio del archivo NuGet.config? + + This Aspire CLI was installed via WinGet. To update it, run: + This Aspire CLI was installed via WinGet. To update it, run: + + 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..d7474026e13 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: + This Aspire CLI was installed via Homebrew. To update it, run: + + 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 ». @@ -117,6 +122,11 @@ 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. + + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + + Mapping: {0} (added) Mappage : {0} (ajouté) @@ -182,6 +192,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: + 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: + + The path to the Aspire AppHost project file or a directory to search Chemin d’accès au fichier projet AppHost Aspire. @@ -242,6 +257,11 @@ Update the Aspire CLI itself to the latest version + + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + + Unexpected code path. Chemin de code inattendu. @@ -267,6 +287,11 @@ Quel répertoire pour le fichier NuGet.config ? + + This Aspire CLI was installed via WinGet. To update it, run: + This Aspire CLI was installed via WinGet. To update it, run: + + 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..abb795ed7cf 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: + This Aspire CLI was installed via Homebrew. To update it, run: + + Central package management is currently not supported by 'aspire update'. La gestione centralizzata dei pacchetti non è attualmente supportata da "aspire update". @@ -117,6 +122,11 @@ 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. + + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + + Mapping: {0} (added) Mapping: {0} (aggiunto) @@ -182,6 +192,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: + 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: + + The path to the Aspire AppHost project file or a directory to search Percorso del file di un progetto AppHost di Aspire. @@ -242,6 +257,11 @@ Update the Aspire CLI itself to the latest version + + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + + Unexpected code path. Percorso del codice imprevisto. @@ -267,6 +287,11 @@ Quale directory per il file NuGet.config? + + This Aspire CLI was installed via WinGet. To update it, run: + This Aspire CLI was installed via WinGet. To update it, run: + + 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..eb9c0b12b4c 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: + This Aspire CLI was installed via Homebrew. To update it, run: + + Central package management is currently not supported by 'aspire update'. 現在、中央パッケージ管理では、'aspire update' によるサポートがありません。 @@ -117,6 +122,11 @@ 注: 解決できない AppHost SDK が原因でフォールバック解析を使用して生成されたプランを更新します。依存関係分析の精度が低下する可能性があります。 + + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + + Mapping: {0} (added) マッピング: {0} (追加済み) @@ -182,6 +192,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: + 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: + + The path to the Aspire AppHost project file or a directory to search Aspire アプリ ホスティング プロセス プロジェクト ファイルへのパス。 @@ -242,6 +257,11 @@ Update the Aspire CLI itself to the latest version + + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + + Unexpected code path. 予期しないコード パスです。 @@ -267,6 +287,11 @@ NuGet.config ファイル用の、どのディレクトリですか? + + This Aspire CLI was installed via WinGet. To update it, run: + This Aspire CLI was installed via WinGet. To update it, run: + + 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..cd60c6d7f12 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: + This Aspire CLI was installed via Homebrew. To update it, run: + + Central package management is currently not supported by 'aspire update'. 중앙 패키지 관리는 현재 'aspire update'에서 지원되지 않습니다. @@ -117,6 +122,11 @@ 참고: 해결할 수 없는 AppHost SDK 때문에 대체 구문 분석을 사용하여 업데이트 계획이 생성되었습니다. 종속성 분석의 정확도가 낮아질 수 있습니다. + + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + + Mapping: {0} (added) 매핑: {0}(추가됨) @@ -182,6 +192,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: + 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: + + The path to the Aspire AppHost project file or a directory to search Aspire AppHost 프로젝트 파일의 경로입니다. @@ -242,6 +257,11 @@ Update the Aspire CLI itself to the latest version + + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + + Unexpected code path. 예기치 않은 코드 경로입니다. @@ -267,6 +287,11 @@ NuGet.config 파일의 디렉터리는 무엇인가요? + + This Aspire CLI was installed via WinGet. To update it, run: + This Aspire CLI was installed via WinGet. To update it, run: + + 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..30cfd2a8690 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: + This Aspire CLI was installed via Homebrew. To update it, run: + + Central package management is currently not supported by 'aspire update'. Centralne zarządzanie pakietami nie jest obecnie obsługiwane przez „aktualizację Aspire”. @@ -117,6 +122,11 @@ 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. + + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + + Mapping: {0} (added) Mapowanie: {0} (dodano) @@ -182,6 +192,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: + 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: + + The path to the Aspire AppHost project file or a directory to search Ścieżka do pliku projektu hosta AppHost platformy Aspire. @@ -242,6 +257,11 @@ Update the Aspire CLI itself to the latest version + + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + + Unexpected code path. Nieoczekiwana ścieżka w kodzie. @@ -267,6 +287,11 @@ Który katalog dla pliku NuGet.config? + + This Aspire CLI was installed via WinGet. To update it, run: + This Aspire CLI was installed via WinGet. To update it, run: + + 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..5b46382692c 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: + This Aspire CLI was installed via Homebrew. To update it, run: + + 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. @@ -117,6 +122,11 @@ 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. + + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + + Mapping: {0} (added) Mapeamento: {0} (adicionado) @@ -182,6 +192,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: + 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: + + 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 +257,11 @@ Update the Aspire CLI itself to the latest version + + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + + Unexpected code path. Caminho de código inesperado. @@ -267,6 +287,11 @@ Qual diretório para o arquivo NuGet.config? + + This Aspire CLI was installed via WinGet. To update it, run: + This Aspire CLI was installed via WinGet. To update it, run: + + 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..803c87a9589 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: + This Aspire CLI was installed via Homebrew. To update it, run: + + Central package management is currently not supported by 'aspire update'. Централизованное управление пакетами в настоящее время не поддерживается функцией "обновление Aspire". @@ -117,6 +122,11 @@ Примечание. План обновления создан с использованием резервного анализа из-за невозможности разрешить SDK AppHost. Точность анализа зависимостей может быть снижена. + + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + + Mapping: {0} (added) Сопоставление: {0} (добавлено) @@ -182,6 +192,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: + 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: + + The path to the Aspire AppHost project file or a directory to search Путь к файлу проекта Aspire AppHost. @@ -242,6 +257,11 @@ Update the Aspire CLI itself to the latest version + + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + + Unexpected code path. Непредвиденный путь к коду. @@ -267,6 +287,11 @@ Какой каталог для файла NuGet.config? + + This Aspire CLI was installed via WinGet. To update it, run: + This Aspire CLI was installed via WinGet. To update it, run: + + 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..686d81fab73 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: + This Aspire CLI was installed via Homebrew. To update it, run: + + Central package management is currently not supported by 'aspire update'. Merkezi paket yönetimi şu anda 'aspire update' tarafından desteklenmiyor. @@ -117,6 +122,11 @@ 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. + + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + + Mapping: {0} (added) Eşleme: {0} (eklendi) @@ -182,6 +192,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: + 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: + + The path to the Aspire AppHost project file or a directory to search Aspire AppHost proje dosyasının yolu. @@ -242,6 +257,11 @@ Update the Aspire CLI itself to the latest version + + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + + Unexpected code path. Beklenmeyen kod yolu. @@ -267,6 +287,11 @@ NuGet.config dosyası için hangi dizin kullanılsın? + + This Aspire CLI was installed via WinGet. To update it, run: + This Aspire CLI was installed via WinGet. To update it, run: + + 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..990441161fe 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: + This Aspire CLI was installed via Homebrew. To update it, run: + + Central package management is currently not supported by 'aspire update'. "aspire update" 当前不支持中央包管理。 @@ -117,6 +122,11 @@ 注意: 由于 AppHost SDK 无法解析,已采用回退解析生成更新计划。依赖项分析的准确性可能有所降低。 + + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + + Mapping: {0} (added) 映射: {0} (已添加) @@ -182,6 +192,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: + 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: + + The path to the Aspire AppHost project file or a directory to search Aspire AppHost 项目文件的路径。 @@ -242,6 +257,11 @@ Update the Aspire CLI itself to the latest version + + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + + Unexpected code path. 意外的代码路径。 @@ -267,6 +287,11 @@ NuGet.config 文件位于哪个目录? + + This Aspire CLI was installed via WinGet. To update it, run: + This Aspire CLI was installed via WinGet. To update it, run: + + 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..4ec86f5fa7f 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: + This Aspire CLI was installed via Homebrew. To update it, run: + + Central package management is currently not supported by 'aspire update'. 中央套件管理目前不受 'aspire update' 支援。 @@ -117,6 +122,11 @@ 注意: 由於無法解析的 AppHost SDK,已使用後援剖析產生更新計劃。相依性分析的正確性可能因此降低。 + + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + + Mapping: {0} (added) 對應: {0} (已新增) @@ -182,6 +192,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: + 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: + + The path to the Aspire AppHost project file or a directory to search Aspire AppHost 專案檔案的路徑。 @@ -242,6 +257,11 @@ Update the Aspire CLI itself to the latest version + + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + + Unexpected code path. 未預期的程式碼路徑。 @@ -267,6 +287,11 @@ 哪個目錄用於 NuGet.config 檔案? + + This Aspire CLI was installed via WinGet. To update it, run: + This Aspire CLI was installed via WinGet. To update it, run: + + Automatically confirm all update prompts without asking Automatically confirm all update prompts without asking diff --git a/tests/Aspire.Cli.Tests/Acquisition/SelfUpdateRouterTests.cs b/tests/Aspire.Cli.Tests/Acquisition/SelfUpdateRouterTests.cs new file mode 100644 index 00000000000..62e01c58056 --- /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 stay in-process for legacy pre-sidecar script installs + // (see SelfUpdateRouter.GetAction docs). + [InlineData("Unknown", "InProcess")] + // 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/UpgradeInstructionProviderTests.cs b/tests/Aspire.Cli.Tests/Acquisition/UpgradeInstructionProviderTests.cs new file mode 100644 index 00000000000..ead25060e38 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Acquisition/UpgradeInstructionProviderTests.cs @@ -0,0 +1,118 @@ +// 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", "./localhive.sh # re-run from your Aspire checkout")] + 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; Unknown has no actionable hint. + [Theory] + [InlineData("Script")] + [InlineData("Unknown")] + 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). + var toolPath = "/opt/my-aspire"; + using var processPathScope = DotNetToolDetection.UseProcessPathForTesting( + $"{toolPath}/.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 --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. + var toolPath = "/opt/My Aspire"; + using var processPathScope = DotNetToolDetection.UseProcessPathForTesting( + $"{toolPath}/.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 --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("/tmp/random/path/aspire"); + + var command = s_provider.GetUpdateCommand(InstallSource.DotnetTool, processPath: null, identityChannel: "stable"); + + Assert.Equal("dotnet tool update -g Aspire.Cli", command); + } +} diff --git a/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs index 866d4b4d8be..b2638c35497 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()) { From 90c9ad3004fb253f72c1650749d276621e35208e Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 14 May 2026 19:49:22 -0400 Subject: [PATCH 2/8] feat(cli): route-aware 'update available' notification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'version X is available' notification now surfaces the command that actually updates the binary the user is running. Before: hardcoded 'aspire update' fallback (which only runs the project-update flow, not the CLI self-update) or 'dotnet tool update' special-case. After: the notifier resolves install source via IInstallationDiscovery.DescribeSelf and delegates to IUpgradeInstructionProvider, falling back to 'aspire update --self' for Script and Unknown (legacy compat). Closes the part of S9.1–S9.4 (route-aware notifications) addressable from the notifier path. Pkg-mgr users see 'brew upgrade --cask aspire' / 'winget upgrade Microsoft.Aspire'; PR-route users see the 'get-aspire-cli-pr.sh ' hint with their PR number substituted; dotnet-tool users get the path-aware -g vs --tool-path command. Tests: new UpdateNotificationRouteTests Theory-flattens the 5 routes. One pre-existing test renamed (was asserting the literal 'aspire update' recommendation for archive-path installs; updated to 'aspire update --self' which is the route-correct command for the script route). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Cli/Utils/CliUpdateNotifier.cs | 35 +++++- .../UpdateNotificationRouteTests.cs | 100 ++++++++++++++++++ tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs | 5 +- .../CliUpdateNotificationServiceTests.cs | 28 +++-- 4 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 tests/Aspire.Cli.Tests/Acquisition/UpdateNotificationRouteTests.cs diff --git a/src/Aspire.Cli/Utils/CliUpdateNotifier.cs b/src/Aspire.Cli/Utils/CliUpdateNotifier.cs index 3a5c0be2c5c..f1f5cdc7b21 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,10 @@ internal static class PackageUpdateRecommendationChannels internal class CliUpdateNotifier( ILogger logger, INuGetPackageCache nuGetPackageCache, - IInteractionService interactionService) : ICliUpdateNotifier + IInteractionService interactionService, + IInstallationDiscovery installationDiscovery, + IUpgradeInstructionProvider upgradeInstructionProvider, + CliExecutionContext executionContext) : ICliUpdateNotifier { private IEnumerable? _availablePackages; @@ -116,7 +120,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 +133,33 @@ 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 and Unknown + /// installs we suggest aspire update --self (which actually + /// performs the in-process update for script). For every other route we + /// defer to so users see the + /// command that matches how they installed the CLI (winget upgrade, + /// brew upgrade --cask, dotnet tool update, get-aspire-cli-pr, etc.). + /// + private string GetRouteAwareUpdateCommand() + { + var info = installationDiscovery.DescribeSelf(); + var source = InstallSourceExtensions.ParseInstallSource(info.Route); + var canonicalPath = info.CanonicalPath ?? info.Path; + + // 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/UpdateNotificationRouteTests.cs b/tests/Aspire.Cli.Tests/Acquisition/UpdateNotificationRouteTests.cs new file mode 100644 index 00000000000..e2e8fe2f539 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Acquisition/UpdateNotificationRouteTests.cs @@ -0,0 +1,100 @@ +// 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", "./localhive.sh # re-run from your Aspire checkout")] + [InlineData("script", "aspire update --self")] + [InlineData(null, "aspire update --self")] + 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()); + }; + }); + + // 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.Equal(expectedCommand, interactionService.LastVersionUpdateCommand); + } +} diff --git a/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs index b2638c35497..3617207289d 100644 --- a/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs +++ b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs @@ -363,7 +363,10 @@ 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(); + return new CliUpdateNotifier(logger, nuGetPackageCache, interactionService, installationDiscovery, upgradeInstructionProvider, executionContext); }; 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..d3c57ae99d7 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()); }; }); @@ -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()); }; }); @@ -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()); }; }); @@ -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()); }; }); @@ -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()); }; }); @@ -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()); }; }); @@ -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()); }; }); @@ -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()); }; }); @@ -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) : CliUpdateNotifier(logger, nuGetPackageCache, interactionService, installationDiscovery, upgradeInstructionProvider, executionContext) { protected override SemVersion? GetCurrentVersion() { From 3f0a0e05950453e8878b29536b9b75d48a508a75 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 14 May 2026 20:03:54 -0400 Subject: [PATCH 3/8] fix(cli): aspire update --non-interactive does not crash at channel prompt (#15600) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-fix: when hive directories exist under `~/.aspire/hives/`, `aspire update --non-interactive` reached `PromptForSelectionAsync` to ask the user to pick a channel — and crashed with "Interactive input is not supported in this environment". Fix: gate the channel-selection prompt on `ICliHostEnvironment.SupportsInteractiveInput`. In non-interactive hosts (explicit `--non-interactive`, CI environment variables, missing console handles) the implicit/default channel is picked silently — matching the branch already taken when no hives exist. Also adds a regression test for #15601 that verifies the pre-existing `AddNonInteractiveRequiresYesValidator` short-circuits `--non-interactive` without `--yes` before any `ProjectUpdater.PromptConfirmAsync` site is reached. Also: tidy stale 'preview' channel reference in the Channel property description on `AspireConfigFile` / `AspireJsonConfiguration` (the real non-stable channels are stable / staging / daily / pr- / local). Tests: 3 new (Theory-flattened), 2 pre-existing tests updated to opt in to interactive host environment so the channel-prompt content contract is still exercised. All 89 acquisition+update tests pass. Closes #15600. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Cli/Commands/UpdateCommand.cs | 14 +- .../Configuration/AspireConfigFile.cs | 2 +- .../Configuration/AspireJsonConfiguration.cs | 4 +- .../UpdateCommandNonInteractiveTests.cs | 130 ++++++++++++++++++ .../Commands/UpdateCommandTests.cs | 13 ++ 5 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 tests/Aspire.Cli.Tests/Commands/UpdateCommandNonInteractiveTests.cs diff --git a/src/Aspire.Cli/Commands/UpdateCommand.cs b/src/Aspire.Cli/Commands/UpdateCommand.cs index 4e064861890..9c747f0c596 100644 --- a/src/Aspire.Cli/Commands/UpdateCommand.cs +++ b/src/Aspire.Cli/Commands/UpdateCommand.cs @@ -35,6 +35,7 @@ internal sealed class UpdateCommand : BaseCommand private readonly IConfiguration _configuration; private readonly IInstallationDiscovery _installationDiscovery; private readonly IUpgradeInstructionProvider _upgradeInstructionProvider; + private readonly ICliHostEnvironment _hostEnvironment; private static readonly OptionWithLegacy s_appHostOption = new("--apphost", "--project", UpdateCommandStrings.ProjectArgumentDescription); private static readonly Option s_selfOption = new("--self") @@ -67,7 +68,8 @@ public UpdateCommand( AspireCliTelemetry telemetry, IConfiguration configuration, IInstallationDiscovery installationDiscovery, - IUpgradeInstructionProvider upgradeInstructionProvider) + IUpgradeInstructionProvider upgradeInstructionProvider, + ICliHostEnvironment hostEnvironment) : base("update", UpdateCommandStrings.Description, features, updateNotifier, executionContext, interactionService, telemetry) { _projectLocator = projectLocator; @@ -81,6 +83,7 @@ public UpdateCommand( _configuration = configuration; _installationDiscovery = installationDiscovery; _upgradeInstructionProvider = upgradeInstructionProvider; + _hostEnvironment = hostEnvironment; Options.Add(s_appHostOption); Options.Add(s_selfOption); @@ -187,11 +190,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); 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/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, _, _) => From bcfd2153f548a1a23ac77653c056227b25e39be1 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 14 May 2026 20:12:28 -0400 Subject: [PATCH 4/8] test(cli): end-to-end regression net for aspire update --self route gating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds UpdateCommandRouteRegressionTests — a Theory-flattened end-to-end guard that drives 'aspire update --self' for each newly-gated route (pr / winget / brew / localhive) via RootCommand + Parse + InvokeAsync, and asserts: (a) the binary is NOT touched (the in-process update flow is not reached) (b) the route-appropriate refusal command is printed verbatim to stdout (c) exit code is 0 (matches the existing dotnet-tool refusal contract) This complements the unit-level SelfUpdateRouterTests + UpgradeInstructionProviderTests by exercising the full UpdateCommand flow with a FakeInstallationDiscovery surfacing each route, locking in the regression net for silent-PR-demotion + pkg-mgr binary-clobber bugs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../UpdateCommandRouteRegressionTests.cs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 tests/Aspire.Cli.Tests/Acquisition/UpdateCommandRouteRegressionTests.cs diff --git a/tests/Aspire.Cli.Tests/Acquisition/UpdateCommandRouteRegressionTests.cs b/tests/Aspire.Cli.Tests/Acquisition/UpdateCommandRouteRegressionTests.cs new file mode 100644 index 00000000000..1a00db27143 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Acquisition/UpdateCommandRouteRegressionTests.cs @@ -0,0 +1,98 @@ +// 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; + +namespace Aspire.Cli.Tests.Acquisition; + +/// +/// End-to-end regression guard for the silent-PR-demotion and +/// package-manager binary-clobber bugs on aspire update --self. +/// Pre-fix: an in-process binary swap ran unconditionally for every +/// non-dotnet-tool route, overwriting WinGet / Homebrew / PR-pinned +/// binaries with the latest stable archive. Post-fix: each non-script +/// route gets refused with the installer-appropriate command and the +/// binary is left untouched. +/// +public class UpdateCommandRouteRegressionTests(ITestOutputHelper outputHelper) +{ + // Each row encodes (sidecar source, identityChannel for PR substitution, + // expected refusal command). Script and Unknown stay in-process by design, + // so they're excluded from this regression net. + [Theory] + [InlineData("pr", "pr-16817", "get-aspire-cli-pr.sh 16817 # or: get-aspire-cli-pr.ps1 -PRNumber 16817")] + [InlineData("winget", "stable", "winget upgrade Microsoft.Aspire")] + [InlineData("brew", "stable", "brew upgrade --cask aspire")] + [InlineData("localhive", "local", "./localhive.sh # re-run from your Aspire checkout")] + public async Task SelfUpdate_OnGatedRoute_RefusesWithRouteAppropriateCommand( + string sidecarSource, + string identityChannel, + string expectedCommand) + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var selfInfo = new InstallationInfo + { + Path = "/test/aspire", + CanonicalPath = "/test/aspire", + Route = sidecarSource, + Channel = identityChannel, + Status = InstallationInfoStatus.Ok, + }; + + TestInteractionService? interactionService = null; + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.InteractionServiceFactory = _ => + { + interactionService = new TestInteractionService(); + return interactionService; + }; + + // Force the running CLI's identity channel so the PR-route + // substitution exercises the parsed PR number path. + 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(Path.GetTempPath(), "aspire-test-sdks")), + logsDirectory, + logFilePath, + identityChannel: identityChannel); + }; + }); + + // Replace the real InstallationDiscovery with a fake surfacing the + // route under test. Last registration wins. + services.AddSingleton(_ => new FakeInstallationDiscovery(selfInfo)); + + using var provider = services.BuildServiceProvider(); + var command = provider.GetRequiredService(); + var parsed = command.Parse("update --self"); + var exitCode = await parsed.InvokeAsync().DefaultTimeout(); + + Assert.NotNull(interactionService); + // Exit 0 by design — the CLI succeeded in telling the user what to + // do (matches the existing dotnet-tool refusal contract). + Assert.Equal(0, exitCode); + + // The expected command must appear verbatim in the displayed plain + // text — this is the signal a user / CI script would actually + // observe in stdout. + Assert.Contains( + interactionService!.DisplayedPlainText, + line => line.Contains(expectedCommand, StringComparison.Ordinal)); + } +} From d686df2bcd3029854fb9589a32e96b3bc0b873a9 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 14 May 2026 21:05:46 -0400 Subject: [PATCH 5/8] test(cli): edge-case coverage for ResolveRunningInstall fallbacks + unknown sidecar source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three new non-happy-path test rows close the remaining coverage gaps for PR α's route gating: 1. SelfUpdate_NoSidecar_LegacyDotnetToolPathShape_RefusesWithDotnetToolHint — when discovery returns no route AND path-shape inspection identifies the binary as a dotnet-tool, the fallback in ResolveRunningInstall fixes up Unknown → DotnetTool and refuses with 'dotnet tool update -g Aspire.Cli'. Locks in the legacy pre-sidecar dotnet-tool compat path. 2. SelfUpdate_NoSidecar_NotDotnetTool_FallsThroughToInProcessFlow — when discovery returns no route AND path is unrecognized, ResolveRunningInstall yields Unknown which SelfUpdateRouter maps to InProcess (legacy script-route compat). Asserts none of the route-specific refusal strings appear, verifying the gated branch was not taken. 3. UpdateNotificationRouteTests Theory gains a 'future-route-name' row — when the sidecar source is a string this CLI doesn't recognize yet (a new route added by a future build), ParseInstallSource yields Unknown and the notification falls back to 'aspire update --self' rather than printing the raw unknown name. Future-tolerance contract. 194 / 194 acquisition+update tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../UpdateCommandRouteRegressionTests.cs | 118 ++++++++++++++++++ .../UpdateNotificationRouteTests.cs | 7 ++ 2 files changed, 125 insertions(+) diff --git a/tests/Aspire.Cli.Tests/Acquisition/UpdateCommandRouteRegressionTests.cs b/tests/Aspire.Cli.Tests/Acquisition/UpdateCommandRouteRegressionTests.cs index 1a00db27143..25743f1e698 100644 --- a/tests/Aspire.Cli.Tests/Acquisition/UpdateCommandRouteRegressionTests.cs +++ b/tests/Aspire.Cli.Tests/Acquisition/UpdateCommandRouteRegressionTests.cs @@ -5,6 +5,7 @@ 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; @@ -95,4 +96,121 @@ public async Task SelfUpdate_OnGatedRoute_RefusesWithRouteAppropriateCommand( interactionService!.DisplayedPlainText, line => line.Contains(expectedCommand, 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 (which would corrupt the + /// package-manager-owned binary). + /// + [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"); + + // Discovery returns no route (no sidecar on disk). The fallback in + // ResolveRunningInstall must then consult DotNetToolDetection via + // the no-arg overload, which honors UseProcessPathForTesting. + var selfInfo = new InstallationInfo + { + Path = "/test/aspire", + CanonicalPath = "/test/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); + // The refusal must surface the global dotnet-tool update command — + // the binary IS under ~/.dotnet/tools/.store/ so DotNetToolDetection + // classifies it as a global-tool install. + Assert.Contains( + interactionService!.DisplayedPlainText, + line => line.Contains("dotnet tool update -g Aspire.Cli", StringComparison.Ordinal)); + } + + /// + /// Pre-sidecar script install compat: when the running binary has no + /// sidecar AND the process path doesn't match any dotnet-tool layout, + /// the resolver yields and + /// routes Unknown to + /// . --self must then + /// reach the in-process flow rather than printing a refusal. We assert + /// the SUT does NOT print any of the refusal-message prefixes + /// (verifying it didn't take the gated branch) instead of trying to + /// drive a full network download. + /// + [Fact] + public async Task SelfUpdate_NoSidecar_NotDotnetTool_FallsThroughToInProcessFlow() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + using var processPathScope = DotNetToolDetection.UseProcessPathForTesting("/tmp/random/aspire"); + + var selfInfo = new InstallationInfo + { + Path = "/tmp/random/aspire", + CanonicalPath = "/tmp/random/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(); + // Invoke --self; we don't expect this to complete the actual update + // (no real network in the test process), but we do expect it to NOT + // emit any of the route-refusal messages. Any non-refusal outcome + // (timeout / failure / success / cancellation) is acceptable here; + // what matters is that the gated branch was not taken. + try + { + await command.Parse("update --self --yes --channel stable").InvokeAsync().DefaultTimeout(); + } + catch + { + // The in-process flow may throw for any number of network / + // download / signature reasons; the test doesn't depend on + // those succeeding. + } + + Assert.NotNull(interactionService); + // None of the route-specific refusal messages must appear: those + // are the signal that the gated branch was taken. + var allOutput = string.Join("\n", interactionService!.DisplayedPlainText); + Assert.DoesNotContain("get-aspire-cli-pr.sh", allOutput, StringComparison.Ordinal); + Assert.DoesNotContain("winget upgrade", allOutput, StringComparison.Ordinal); + Assert.DoesNotContain("brew upgrade", allOutput, StringComparison.Ordinal); + Assert.DoesNotContain("./localhive.sh", allOutput, StringComparison.Ordinal); + Assert.DoesNotContain("dotnet tool update", allOutput, StringComparison.Ordinal); + } } diff --git a/tests/Aspire.Cli.Tests/Acquisition/UpdateNotificationRouteTests.cs b/tests/Aspire.Cli.Tests/Acquisition/UpdateNotificationRouteTests.cs index e2e8fe2f539..4ccedb99b2a 100644 --- a/tests/Aspire.Cli.Tests/Acquisition/UpdateNotificationRouteTests.cs +++ b/tests/Aspire.Cli.Tests/Acquisition/UpdateNotificationRouteTests.cs @@ -35,6 +35,13 @@ public class UpdateNotificationRouteTests(ITestOutputHelper outputHelper) [InlineData("localhive", "./localhive.sh # re-run from your Aspire checkout")] [InlineData("script", "aspire update --self")] [InlineData(null, "aspire update --self")] + // 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. Falls back to "aspire update --self" via + // SelfUpdateRouter's Unknown → in-process classification. + [InlineData("future-route-name", "aspire update --self")] public async Task NotifyIfUpdateAvailable_RouteAwareCommand_MatchesUpgradeInstructionProvider(string? sidecarSource, string expectedCommand) { using var workspace = TemporaryWorkspace.Create(outputHelper); From b30699e21cd6806ec69618d7b093313678308151 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 14 May 2026 21:48:09 -0400 Subject: [PATCH 6/8] fix(cli): aspire update --self runs WinGet first-run probe before route resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this fix, a fresh WinGet install whose very first command was `aspire update --self` would have no install-route sidecar yet (the sidecar is stamped lazily by WingetFirstRunProbe from BundleService.EnsureExtractedAsync). The resolver classified the binary as Unknown, the router mapped Unknown to InProcess for legacy script-route compat, and the in-process updater would silently overwrite the WinGet-owned binary — the exact regression PR α was meant to prevent. Caught by GPT-5.5 code review. Fix: ResolveRunningInstall (UpdateCommand) and GetRouteAwareUpdateCommand (CliUpdateNotifier) now run WingetFirstRunProbe.Run before reading the sidecar, on any platform when the initial DescribeSelf reports no route. The probe self-gates via IWindowsRegistryReader (NullWindowsRegistryReader on non-Windows makes it a cheap no-op there), so the call is safe in every environment. After the probe runs, DescribeSelf is called again so the freshly-stamped sidecar is picked up. The probe's binaryDir is now derived from IInstallationDiscovery.DescribeSelf().CanonicalPath rather than Environment.ProcessPath. This both removes the OS-conditional guard that lived at the call site (the probe self-gates) and makes the integration testable end-to-end via FakeInstallationDiscovery. Also fixes the SelfUpdateAction.Delegate XML doc that still claimed non-zero exit (caught by Opus code review). The implementation deliberately returns exit 0 to match the existing dotnet-tool refusal contract; the doc is now correct. Tests: - WingetFirstRunSelfUpdateGuardTests covers (a) probe stamps sidecar when registry confirms WinGet portable, (b) probe is a no-op when registry says no, and (c) the end-to-end aspire update --self path with a fake registry returning true → sidecar stamped + refusal prints 'winget upgrade Microsoft.Aspire'. The integration test is platform-agnostic via SidecarBackedDiscovery + a controllable temp binary directory. - 197 / 197 acquisition+update tests pass. Two CliUpdateNotifier constructor-surface ripples: the test override class CliUpdateNotifierWithPackageVersionOverride gained a 4th forwarded dep, and 8 call sites in CliUpdateNotificationServiceTests / 1 in CliTestHelper bulk-updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Acquisition/SelfUpdateRouter.cs | 7 +- src/Aspire.Cli/Commands/UpdateCommand.cs | 40 +++- src/Aspire.Cli/Utils/CliUpdateNotifier.cs | 25 ++- .../UpdateNotificationRouteTests.cs | 3 +- .../WingetFirstRunSelfUpdateGuardTests.cs | 190 ++++++++++++++++++ tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs | 3 +- .../CliUpdateNotificationServiceTests.cs | 18 +- 7 files changed, 269 insertions(+), 17 deletions(-) create mode 100644 tests/Aspire.Cli.Tests/Acquisition/WingetFirstRunSelfUpdateGuardTests.cs diff --git a/src/Aspire.Cli/Acquisition/SelfUpdateRouter.cs b/src/Aspire.Cli/Acquisition/SelfUpdateRouter.cs index 9cef35e7481..567388557b9 100644 --- a/src/Aspire.Cli/Acquisition/SelfUpdateRouter.cs +++ b/src/Aspire.Cli/Acquisition/SelfUpdateRouter.cs @@ -21,8 +21,11 @@ internal enum SelfUpdateAction /// /// Refuse to update in-process and instead print a route-appropriate - /// command via . The exit - /// code is non-zero so scripts notice the no-op. + /// 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, } diff --git a/src/Aspire.Cli/Commands/UpdateCommand.cs b/src/Aspire.Cli/Commands/UpdateCommand.cs index 9c747f0c596..6fc66e91c79 100644 --- a/src/Aspire.Cli/Commands/UpdateCommand.cs +++ b/src/Aspire.Cli/Commands/UpdateCommand.cs @@ -36,6 +36,7 @@ internal sealed class UpdateCommand : BaseCommand 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") @@ -69,7 +70,8 @@ public UpdateCommand( IConfiguration configuration, IInstallationDiscovery installationDiscovery, IUpgradeInstructionProvider upgradeInstructionProvider, - ICliHostEnvironment hostEnvironment) + ICliHostEnvironment hostEnvironment, + WingetFirstRunProbe wingetFirstRunProbe) : base("update", UpdateCommandStrings.Description, features, updateNotifier, executionContext, interactionService, telemetry) { _projectLocator = projectLocator; @@ -84,6 +86,7 @@ public UpdateCommand( _installationDiscovery = installationDiscovery; _upgradeInstructionProvider = upgradeInstructionProvider; _hostEnvironment = hostEnvironment; + _wingetFirstRunProbe = wingetFirstRunProbe; Options.Add(s_appHostOption); Options.Add(s_selfOption); @@ -346,12 +349,45 @@ private async Task HandleSelfUpdateAsync(ParseResult parseResult, /// contract shipped still get classified as /// rather than . /// + /// + /// 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 , route to + /// in-process update, and silently overwrite the WinGet-owned binary. + /// 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 source = InstallSourceExtensions.ParseInstallSource(info.Route); 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 diff --git a/src/Aspire.Cli/Utils/CliUpdateNotifier.cs b/src/Aspire.Cli/Utils/CliUpdateNotifier.cs index f1f5cdc7b21..3d9ee1e450e 100644 --- a/src/Aspire.Cli/Utils/CliUpdateNotifier.cs +++ b/src/Aspire.Cli/Utils/CliUpdateNotifier.cs @@ -47,7 +47,8 @@ internal class CliUpdateNotifier( IInteractionService interactionService, IInstallationDiscovery installationDiscovery, IUpgradeInstructionProvider upgradeInstructionProvider, - CliExecutionContext executionContext) : ICliUpdateNotifier + CliExecutionContext executionContext, + WingetFirstRunProbe wingetFirstRunProbe) : ICliUpdateNotifier { private IEnumerable? _availablePackages; @@ -142,12 +143,32 @@ private CliVersionStatus GetCachedVersionStatus(string? updateCheckError = null) /// command 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 source = InstallSourceExtensions.ParseInstallSource(info.Route); 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. diff --git a/tests/Aspire.Cli.Tests/Acquisition/UpdateNotificationRouteTests.cs b/tests/Aspire.Cli.Tests/Acquisition/UpdateNotificationRouteTests.cs index 4ccedb99b2a..1d02147709f 100644 --- a/tests/Aspire.Cli.Tests/Acquisition/UpdateNotificationRouteTests.cs +++ b/tests/Aspire.Cli.Tests/Acquisition/UpdateNotificationRouteTests.cs @@ -85,7 +85,8 @@ public async Task NotifyIfUpdateAvailable_RouteAwareCommand_MatchesUpgradeInstru "9.4.0", logger, nuGetPackageCache, service, sp.GetRequiredService(), sp.GetRequiredService(), - sp.GetRequiredService()); + sp.GetRequiredService(), + sp.GetRequiredService()); }; }); 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/Utils/CliTestHelper.cs b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs index 3617207289d..614f7e8f227 100644 --- a/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs +++ b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs @@ -366,7 +366,8 @@ private static IAnsiConsole CreateAnsiConsole(TextWriter textWriter, bool ansi = var installationDiscovery = serviceProvider.GetRequiredService(); var upgradeInstructionProvider = serviceProvider.GetRequiredService(); var executionContext = serviceProvider.GetRequiredService(); - return new CliUpdateNotifier(logger, nuGetPackageCache, interactionService, installationDiscovery, upgradeInstructionProvider, executionContext); + 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 d3c57ae99d7..5a1aeb6d256 100644 --- a/tests/Aspire.Cli.Tests/Utils/CliUpdateNotificationServiceTests.cs +++ b/tests/Aspire.Cli.Tests/Utils/CliUpdateNotificationServiceTests.cs @@ -57,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, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService()); + return new CliUpdateNotifierWithPackageVersionOverride("9.4.0-dev", logger, nuGetPackageCache, interactionService, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService()); }; }); @@ -109,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, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService()); + return new CliUpdateNotifierWithPackageVersionOverride("9.4.0-dev", logger, nuGetPackageCache, interactionService, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService()); }; }); @@ -161,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, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService()); + return new CliUpdateNotifierWithPackageVersionOverride("9.4.0", logger, nuGetPackageCache, interactionService, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService()); }; }); @@ -197,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, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService()); + return new CliUpdateNotifierWithPackageVersionOverride("9.4.0", logger, nuGetPackageCache, interactionService, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService()); }; }); @@ -234,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, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService()); + return new CliUpdateNotifierWithPackageVersionOverride("9.4.0", logger, nuGetPackageCache, service, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService()); }; }); @@ -277,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, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService()); + return new CliUpdateNotifierWithPackageVersionOverride("9.4.0", logger, nuGetPackageCache, service, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService()); }; }); @@ -318,7 +318,7 @@ public async Task NotifyIfUpdateAvailable_UsesAspireUpdateSelfCommandForStandalo var logger = sp.GetRequiredService>(); var nuGetPackageCache = sp.GetRequiredService(); var service = sp.GetRequiredService(); - return new CliUpdateNotifierWithPackageVersionOverride("9.4.0", logger, nuGetPackageCache, service, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService()); + return new CliUpdateNotifierWithPackageVersionOverride("9.4.0", logger, nuGetPackageCache, service, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService()); }; }); @@ -371,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, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService()); + return new CliUpdateNotifierWithPackageVersionOverride("9.4.0", logger, nuGetPackageCache, interactionService, sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService()); }; }); @@ -455,7 +455,7 @@ private static string GetAspireExecutableName() } } -internal sealed class CliUpdateNotifierWithPackageVersionOverride(string currentVersion, ILogger logger, INuGetPackageCache nuGetPackageCache, IInteractionService interactionService, IInstallationDiscovery installationDiscovery, IUpgradeInstructionProvider upgradeInstructionProvider, CliExecutionContext executionContext) : CliUpdateNotifier(logger, nuGetPackageCache, interactionService, installationDiscovery, upgradeInstructionProvider, executionContext) +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() { From e9f6189863db9a1cd997b5abe2c9b2231d0329b0 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 14 May 2026 22:48:05 -0400 Subject: [PATCH 7/8] test(cli): make UpgradeInstructionProvider tool-path tests Windows-safe CI on Windows (run 25895814470) failed UpgradeInstructionProviderTests.GetUpdateCommand_DotnetTool_ToolPath_* because the tests built the input path with forward slashes but DotNetToolDetection.NormalizeDirectorySeparators converts every separator to Path.DirectorySeparatorChar before walking up to the .store directory. On Windows the extracted toolPath therefore comes back with backslashes (\opt\my-aspire) while the assertion expected forward slashes (/opt/my-aspire). Fix: construct both the input processPath and the expected toolPath via Path.DirectorySeparatorChar so the test passes on both Unix (`/`) and Windows (`\`). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../UpgradeInstructionProviderTests.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/Aspire.Cli.Tests/Acquisition/UpgradeInstructionProviderTests.cs b/tests/Aspire.Cli.Tests/Acquisition/UpgradeInstructionProviderTests.cs index ead25060e38..3d31a7443d3 100644 --- a/tests/Aspire.Cli.Tests/Acquisition/UpgradeInstructionProviderTests.cs +++ b/tests/Aspire.Cli.Tests/Acquisition/UpgradeInstructionProviderTests.cs @@ -77,10 +77,14 @@ public void GetUpdateCommand_DotnetTool_ToolPath_ReturnsPathAwareUpdateCommand() // ///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). - var toolPath = "/opt/my-aspire"; + // 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}/.store/aspire.cli/9.4.0/aspire.cli.linux-x64/9.4.0/tools/net10.0/linux-x64/aspire"); + $"{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"); @@ -92,10 +96,11 @@ 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. - var toolPath = "/opt/My Aspire"; + // 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}/.store/aspire.cli/9.4.0/aspire.cli.linux-x64/9.4.0/tools/net10.0/linux-x64/aspire"); + $"{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"); From 0c0d1e322c62fd9e5865176be54609eb0a71e14b Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Fri, 15 May 2026 00:51:57 -0400 Subject: [PATCH 8/8] fix(cli): refuse update --self for unknown routes; add --force escape hatch Unknown install routes now fail closed instead of entering the in-process binary swap. The new --force option preserves an explicit escape hatch for users who have investigated their install and want to attempt the in-process update anyway, while logging the detected route for follow-up diagnostics. Route refusal hints now include the force override, the unknown-route hint explains missing or unreadable sidecars, dotnet-tool hints honor the supplied process path, and local hive guidance includes both shell and PowerShell scripts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Cli/Acquisition/InstallSource.cs | 6 +- .../Acquisition/SelfUpdateRouter.cs | 15 +- .../Acquisition/UpgradeInstructionProvider.cs | 25 ++- src/Aspire.Cli/Commands/UpdateCommand.cs | 28 +-- .../UpdateCommandStrings.Designer.cs | 2 + .../Resources/UpdateCommandStrings.resx | 18 +- .../Resources/xlf/UpdateCommandStrings.cs.xlf | 34 ++-- .../Resources/xlf/UpdateCommandStrings.de.xlf | 34 ++-- .../Resources/xlf/UpdateCommandStrings.es.xlf | 34 ++-- .../Resources/xlf/UpdateCommandStrings.fr.xlf | 34 ++-- .../Resources/xlf/UpdateCommandStrings.it.xlf | 34 ++-- .../Resources/xlf/UpdateCommandStrings.ja.xlf | 34 ++-- .../Resources/xlf/UpdateCommandStrings.ko.xlf | 34 ++-- .../Resources/xlf/UpdateCommandStrings.pl.xlf | 34 ++-- .../xlf/UpdateCommandStrings.pt-BR.xlf | 34 ++-- .../Resources/xlf/UpdateCommandStrings.ru.xlf | 34 ++-- .../Resources/xlf/UpdateCommandStrings.tr.xlf | 34 ++-- .../xlf/UpdateCommandStrings.zh-Hans.xlf | 34 ++-- .../xlf/UpdateCommandStrings.zh-Hant.xlf | 34 ++-- src/Aspire.Cli/Utils/CliUpdateNotifier.cs | 12 +- .../Acquisition/SelfUpdateRouterTests.cs | 6 +- .../UpdateCommandRouteRegressionTests.cs | 172 +++++++----------- .../UpdateNotificationRouteTests.cs | 11 +- .../UpgradeInstructionProviderTests.cs | 41 ++++- 24 files changed, 452 insertions(+), 326 deletions(-) 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 index 567388557b9..145e3fef6e3 100644 --- a/src/Aspire.Cli/Acquisition/SelfUpdateRouter.cs +++ b/src/Aspire.Cli/Acquisition/SelfUpdateRouter.cs @@ -42,19 +42,14 @@ internal static class SelfUpdateRouter /// /// /// stays in-process — it's the route - /// the CLI owns end-to-end. also stays - /// in-process for legacy compatibility: pre-PR-#16817 installs (script - /// route, before the sidecar contract shipped) have no sidecar, and - /// preserving in-process update for them avoids breaking working setups. - /// The PR-#16817-and-later install paths for the gated routes (PR / winget - /// / brew / dotnet-tool / localhive) all write sidecars at install time, - /// so a post-PR-#16817 binary appearing as - /// is almost certainly a legacy script install rather than one of those - /// routes. + /// 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 or InstallSource.Unknown => SelfUpdateAction.InProcess, + InstallSource.Script => SelfUpdateAction.InProcess, _ => SelfUpdateAction.Delegate, }; } diff --git a/src/Aspire.Cli/Acquisition/UpgradeInstructionProvider.cs b/src/Aspire.Cli/Acquisition/UpgradeInstructionProvider.cs index 368deaa5eed..8ccd5521da2 100644 --- a/src/Aspire.Cli/Acquisition/UpgradeInstructionProvider.cs +++ b/src/Aspire.Cli/Acquisition/UpgradeInstructionProvider.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.Resources; using Aspire.Cli.Utils; namespace Aspire.Cli.Acquisition; @@ -57,19 +58,17 @@ internal sealed class UpgradeInstructionProvider : IUpgradeInstructionProvider InstallSource.Winget => "winget upgrade Microsoft.Aspire", InstallSource.Brew => "brew upgrade --cask aspire", - // DotNetToolDetection.GetDotNetToolUpdateCommand (no-arg) honors - // the s_processPathOverride AsyncLocal used by tests AND inspects - // Environment.ProcessPath in production, returning the right - // command for `-g` (global) vs `--tool-path` installs. Falling - // back to the global form if path detection fails preserves the - // most common case. - InstallSource.DotnetTool => DotNetToolDetection.GetDotNetToolUpdateCommand() - ?? "dotnet tool update -g Aspire.Cli", + // 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 => "./localhive.sh # re-run from your Aspire checkout", + InstallSource.LocalHive => "Run ./localhive.sh (Linux/macOS) or .\\localhive.ps1 (Windows) in the local hive directory.", + InstallSource.Unknown => UpdateCommandStrings.UnknownRouteRefusalHint, _ => null, }; @@ -77,6 +76,14 @@ internal sealed class UpgradeInstructionProvider : IUpgradeInstructionProvider 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 diff --git a/src/Aspire.Cli/Commands/UpdateCommand.cs b/src/Aspire.Cli/Commands/UpdateCommand.cs index 6fc66e91c79..50b2334ada0 100644 --- a/src/Aspire.Cli/Commands/UpdateCommand.cs +++ b/src/Aspire.Cli/Commands/UpdateCommand.cs @@ -43,6 +43,10 @@ internal sealed class UpdateCommand : BaseCommand { 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, @@ -90,6 +94,7 @@ public UpdateCommand( Options.Add(s_appHostOption); Options.Add(s_selfOption); + Options.Add(s_forceOption); Options.Add(s_yesOption); Options.Add(s_nugetConfigDirOption); @@ -317,14 +322,20 @@ protected override async Task ExecuteAsync(ParseResult parseResul private async Task HandleSelfUpdateAsync(ParseResult parseResult, CancellationToken cancellationToken) { var (source, canonicalPath) = ResolveRunningInstall(); - var action = SelfUpdateRouter.GetAction(source); + 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: existing in-process flow. + // 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."); @@ -340,15 +351,6 @@ private async Task HandleSelfUpdateAsync(ParseResult parseResult, } } - /// - /// 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 . - /// /// /// Resolves the install source and canonical binary path for the running CLI /// via (the single source of @@ -364,8 +366,8 @@ private async Task HandleSelfUpdateAsync(ParseResult parseResult, /// 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 , route to - /// in-process update, and silently overwrite the WinGet-owned binary. + /// 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. diff --git a/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs index 8b87c374182..c6e1c224c7d 100644 --- a/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs +++ b/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs @@ -112,6 +112,7 @@ internal static string ProjectArgumentDescription { 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); @@ -122,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 bc6da1bc4ec..93bccf1d325 100644 --- a/src/Aspire.Cli/Resources/UpdateCommandStrings.resx +++ b/src/Aspire.Cli/Resources/UpdateCommandStrings.resx @@ -136,22 +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: + 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: + 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: + 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: + 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 (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to 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}"> @@ -183,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 0bb056677ff..a209fe2a2bf 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf @@ -38,8 +38,8 @@ - This Aspire CLI was installed via Homebrew. To update it, run: - This Aspire CLI was installed via Homebrew. To update it, run: + 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): @@ -83,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: @@ -122,9 +122,14 @@ 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: - This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + 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): @@ -193,8 +198,8 @@ - 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: - 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: + 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): @@ -258,8 +263,13 @@ - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + 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. @@ -288,8 +298,8 @@ - This Aspire CLI was installed via WinGet. To update it, run: - This Aspire CLI was installed via WinGet. To update it, run: + 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): diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf index 1fc90e80b73..a45046bed3c 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf @@ -38,8 +38,8 @@ - This Aspire CLI was installed via Homebrew. To update it, run: - This Aspire CLI was installed via Homebrew. To update it, run: + 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): @@ -83,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: @@ -122,9 +122,14 @@ 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: - This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + 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): @@ -193,8 +198,8 @@ - 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: - 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: + 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): @@ -258,8 +263,13 @@ - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + 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. @@ -288,8 +298,8 @@ - This Aspire CLI was installed via WinGet. To update it, run: - This Aspire CLI was installed via WinGet. To update it, run: + 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): diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf index e281207f047..514e70805ad 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf @@ -38,8 +38,8 @@ - This Aspire CLI was installed via Homebrew. To update it, run: - This Aspire CLI was installed via Homebrew. To update it, run: + 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): @@ -83,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: @@ -122,9 +122,14 @@ 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: - This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + 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): @@ -193,8 +198,8 @@ - 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: - 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: + 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): @@ -258,8 +263,13 @@ - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + 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. @@ -288,8 +298,8 @@ - This Aspire CLI was installed via WinGet. To update it, run: - This Aspire CLI was installed via WinGet. To update it, run: + 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): diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf index d7474026e13..d21820133b0 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf @@ -38,8 +38,8 @@ - This Aspire CLI was installed via Homebrew. To update it, run: - This Aspire CLI was installed via Homebrew. To update it, run: + 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): @@ -83,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 : @@ -122,9 +122,14 @@ 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: - This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + 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): @@ -193,8 +198,8 @@ - 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: - 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: + 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): @@ -258,8 +263,13 @@ - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + 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. @@ -288,8 +298,8 @@ - This Aspire CLI was installed via WinGet. To update it, run: - This Aspire CLI was installed via WinGet. To update it, run: + 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): diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf index abb795ed7cf..4c87b320292 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf @@ -38,8 +38,8 @@ - This Aspire CLI was installed via Homebrew. To update it, run: - This Aspire CLI was installed via Homebrew. To update it, run: + 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): @@ -83,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: @@ -122,9 +122,14 @@ 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: - This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + 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): @@ -193,8 +198,8 @@ - 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: - 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: + 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): @@ -258,8 +263,13 @@ - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + 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. @@ -288,8 +298,8 @@ - This Aspire CLI was installed via WinGet. To update it, run: - This Aspire CLI was installed via WinGet. To update it, run: + 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): diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf index eb9c0b12b4c..71801d5117e 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf @@ -38,8 +38,8 @@ - This Aspire CLI was installed via Homebrew. To update it, run: - This Aspire CLI was installed via Homebrew. To update it, run: + 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): @@ -83,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 を更新するには、次を実行します。 @@ -122,9 +122,14 @@ 注: 解決できない 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: - This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + 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): @@ -193,8 +198,8 @@ - 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: - 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: + 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): @@ -258,8 +263,13 @@ - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + 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. @@ -288,8 +298,8 @@ - This Aspire CLI was installed via WinGet. To update it, run: - This Aspire CLI was installed via WinGet. To update it, run: + 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): diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf index cd60c6d7f12..25079b7d614 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf @@ -38,8 +38,8 @@ - This Aspire CLI was installed via Homebrew. To update it, run: - This Aspire CLI was installed via Homebrew. To update it, run: + 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): @@ -83,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를 업데이트하려면 다음 명령을 실행합니다. @@ -122,9 +122,14 @@ 참고: 해결할 수 없는 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: - This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + 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): @@ -193,8 +198,8 @@ - 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: - 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: + 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): @@ -258,8 +263,13 @@ - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + 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. @@ -288,8 +298,8 @@ - This Aspire CLI was installed via WinGet. To update it, run: - This Aspire CLI was installed via WinGet. To update it, run: + 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): diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf index 30cfd2a8690..f045fe7729b 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf @@ -38,8 +38,8 @@ - This Aspire CLI was installed via Homebrew. To update it, run: - This Aspire CLI was installed via Homebrew. To update it, run: + 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): @@ -83,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: @@ -122,9 +122,14 @@ 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: - This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + 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): @@ -193,8 +198,8 @@ - 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: - 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: + 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): @@ -258,8 +263,13 @@ - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + 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. @@ -288,8 +298,8 @@ - This Aspire CLI was installed via WinGet. To update it, run: - This Aspire CLI was installed via WinGet. To update it, run: + 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): diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf index 5b46382692c..3473c9d9fb1 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf @@ -38,8 +38,8 @@ - This Aspire CLI was installed via Homebrew. To update it, run: - This Aspire CLI was installed via Homebrew. To update it, run: + 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): @@ -83,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: @@ -122,9 +122,14 @@ 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: - This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + 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): @@ -193,8 +198,8 @@ - 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: - 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: + 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): @@ -258,8 +263,13 @@ - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + 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. @@ -288,8 +298,8 @@ - This Aspire CLI was installed via WinGet. To update it, run: - This Aspire CLI was installed via WinGet. To update it, run: + 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): diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf index 803c87a9589..00208c0af32 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf @@ -38,8 +38,8 @@ - This Aspire CLI was installed via Homebrew. To update it, run: - This Aspire CLI was installed via Homebrew. To update it, run: + 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): @@ -83,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, запустите: @@ -122,9 +122,14 @@ Примечание. План обновления создан с использованием резервного анализа из-за невозможности разрешить 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: - This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + 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): @@ -193,8 +198,8 @@ - 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: - 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: + 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): @@ -258,8 +263,13 @@ - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + 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. @@ -288,8 +298,8 @@ - This Aspire CLI was installed via WinGet. To update it, run: - This Aspire CLI was installed via WinGet. To update it, run: + 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): diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf index 686d81fab73..90e2fb367c1 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf @@ -38,8 +38,8 @@ - This Aspire CLI was installed via Homebrew. To update it, run: - This Aspire CLI was installed via Homebrew. To update it, run: + 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): @@ -83,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: @@ -122,9 +122,14 @@ 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: - This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + 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): @@ -193,8 +198,8 @@ - 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: - 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: + 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): @@ -258,8 +263,13 @@ - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + 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. @@ -288,8 +298,8 @@ - This Aspire CLI was installed via WinGet. To update it, run: - This Aspire CLI was installed via WinGet. To update it, run: + 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): diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf index 990441161fe..72068250632 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf @@ -38,8 +38,8 @@ - This Aspire CLI was installed via Homebrew. To update it, run: - This Aspire CLI was installed via Homebrew. To update it, run: + 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): @@ -83,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 工具安装时对其进行更新,请运行: @@ -122,9 +122,14 @@ 注意: 由于 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: - This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + 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): @@ -193,8 +198,8 @@ - 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: - 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: + 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): @@ -258,8 +263,13 @@ - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + 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. @@ -288,8 +298,8 @@ - This Aspire CLI was installed via WinGet. To update it, run: - This Aspire CLI was installed via WinGet. To update it, run: + 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): diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf index 4ec86f5fa7f..c22d1d9a75c 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf @@ -38,8 +38,8 @@ - This Aspire CLI was installed via Homebrew. To update it, run: - This Aspire CLI was installed via Homebrew. To update it, run: + 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): @@ -83,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,請執行: @@ -122,9 +122,14 @@ 注意: 由於無法解析的 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: - This Aspire CLI is a local development build. To update it, rebuild from your Aspire checkout: + 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): @@ -193,8 +198,8 @@ - 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: - 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: + 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): @@ -258,8 +263,13 @@ - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. - Cannot determine how this Aspire CLI was installed (no install-route sidecar found). Refusing to perform an in-process self-update to avoid corrupting the binary. Re-run your original install command to update. + 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. @@ -288,8 +298,8 @@ - This Aspire CLI was installed via WinGet. To update it, run: - This Aspire CLI was installed via WinGet. To update it, run: + 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): diff --git a/src/Aspire.Cli/Utils/CliUpdateNotifier.cs b/src/Aspire.Cli/Utils/CliUpdateNotifier.cs index 3d9ee1e450e..a194e053e00 100644 --- a/src/Aspire.Cli/Utils/CliUpdateNotifier.cs +++ b/src/Aspire.Cli/Utils/CliUpdateNotifier.cs @@ -136,12 +136,12 @@ private CliVersionStatus GetCachedVersionStatus(string? updateCheckError = null) /// /// Returns the route-appropriate command to recommend in the - /// "version X available" notification. For script-route and Unknown - /// installs we suggest aspire update --self (which actually - /// performs the in-process update for script). For every other route we - /// defer to so users see the - /// command that matches how they installed the CLI (winget upgrade, - /// brew upgrade --cask, dotnet tool update, get-aspire-cli-pr, etc.). + /// "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 diff --git a/tests/Aspire.Cli.Tests/Acquisition/SelfUpdateRouterTests.cs b/tests/Aspire.Cli.Tests/Acquisition/SelfUpdateRouterTests.cs index 62e01c58056..4288e85bd0e 100644 --- a/tests/Aspire.Cli.Tests/Acquisition/SelfUpdateRouterTests.cs +++ b/tests/Aspire.Cli.Tests/Acquisition/SelfUpdateRouterTests.cs @@ -10,9 +10,9 @@ public class SelfUpdateRouterTests [Theory] // Script-route installs stay in-process — the CLI owns the binary swap. [InlineData("Script", "InProcess")] - // Unknown sources stay in-process for legacy pre-sidecar script installs - // (see SelfUpdateRouter.GetAction docs). - [InlineData("Unknown", "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. diff --git a/tests/Aspire.Cli.Tests/Acquisition/UpdateCommandRouteRegressionTests.cs b/tests/Aspire.Cli.Tests/Acquisition/UpdateCommandRouteRegressionTests.cs index 25743f1e698..55027f064cd 100644 --- a/tests/Aspire.Cli.Tests/Acquisition/UpdateCommandRouteRegressionTests.cs +++ b/tests/Aspire.Cli.Tests/Acquisition/UpdateCommandRouteRegressionTests.cs @@ -12,40 +12,47 @@ namespace Aspire.Cli.Tests.Acquisition; /// -/// End-to-end regression guard for the silent-PR-demotion and +/// End-to-end regression guard for the silent route-demotion and /// package-manager binary-clobber bugs on aspire update --self. -/// Pre-fix: an in-process binary swap ran unconditionally for every -/// non-dotnet-tool route, overwriting WinGet / Homebrew / PR-pinned -/// binaries with the latest stable archive. Post-fix: each non-script -/// route gets refused with the installer-appropriate command and the -/// binary is left untouched. /// public class UpdateCommandRouteRegressionTests(ITestOutputHelper outputHelper) { - // Each row encodes (sidecar source, identityChannel for PR substitution, - // expected refusal command). Script and Unknown stay in-process by design, - // so they're excluded from this regression net. [Theory] - [InlineData("pr", "pr-16817", "get-aspire-cli-pr.sh 16817 # or: get-aspire-cli-pr.ps1 -PRNumber 16817")] - [InlineData("winget", "stable", "winget upgrade Microsoft.Aspire")] - [InlineData("brew", "stable", "brew upgrade --cask aspire")] - [InlineData("localhive", "local", "./localhive.sh # re-run from your Aspire checkout")] - public async Task SelfUpdate_OnGatedRoute_RefusesWithRouteAppropriateCommand( - string sidecarSource, + [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, - string expectedCommand) + 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 = "/test/aspire", - CanonicalPath = "/test/aspire", + Path = Path.Combine(workspace.WorkspaceRoot.FullName, "bin", "aspire"), + CanonicalPath = Path.Combine(workspace.WorkspaceRoot.FullName, "bin", "aspire"), Route = sidecarSource, Channel = identityChannel, - Status = InstallationInfoStatus.Ok, + Status = sidecarSource is null ? InstallationInfoStatus.NotProbed : InstallationInfoStatus.Ok, }; + var downloadAttempted = false; TestInteractionService? interactionService = null; var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => { @@ -55,8 +62,6 @@ public async Task SelfUpdate_OnGatedRoute_RefusesWithRouteAppropriateCommand( return interactionService; }; - // Force the running CLI's identity channel so the PR-route - // substitution exercises the parsed PR number path. options.CliExecutionContextFactory = _ => { var root = workspace.WorkspaceRoot; @@ -68,33 +73,52 @@ public async Task SelfUpdate_OnGatedRoute_RefusesWithRouteAppropriateCommand( root, hivesDirectory, cacheDirectory, - new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-sdks")), + 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"); + } + }; + }; }); - // Replace the real InstallationDiscovery with a fake surfacing the - // route under test. Last registration wins. services.AddSingleton(_ => new FakeInstallationDiscovery(selfInfo)); using var provider = services.BuildServiceProvider(); var command = provider.GetRequiredService(); - var parsed = command.Parse("update --self"); - var exitCode = await parsed.InvokeAsync().DefaultTimeout(); + 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); - // Exit 0 by design — the CLI succeeded in telling the user what to - // do (matches the existing dotnet-tool refusal contract). - Assert.Equal(0, exitCode); + Assert.Equal(expectInProcess, downloadAttempted); - // The expected command must appear verbatim in the displayed plain - // text — this is the signal a user / CI script would actually - // observe in stdout. - Assert.Contains( - interactionService!.DisplayedPlainText, - line => line.Contains(expectedCommand, StringComparison.Ordinal)); + 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); + } } /// @@ -103,8 +127,7 @@ public async Task SelfUpdate_OnGatedRoute_RefusesWithRouteAppropriateCommand( /// 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 (which would corrupt the - /// package-manager-owned binary). + /// the in-process update flow. /// [Fact] public async Task SelfUpdate_NoSidecar_LegacyDotnetToolPathShape_RefusesWithDotnetToolHint() @@ -113,13 +136,10 @@ public async Task SelfUpdate_NoSidecar_LegacyDotnetToolPathShape_RefusesWithDotn 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"); - // Discovery returns no route (no sidecar on disk). The fallback in - // ResolveRunningInstall must then consult DotNetToolDetection via - // the no-arg overload, which honors UseProcessPathForTesting. var selfInfo = new InstallationInfo { - Path = "/test/aspire", - CanonicalPath = "/test/aspire", + 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, }; @@ -141,76 +161,8 @@ public async Task SelfUpdate_NoSidecar_LegacyDotnetToolPathShape_RefusesWithDotn Assert.Equal(0, exitCode); Assert.NotNull(interactionService); - // The refusal must surface the global dotnet-tool update command — - // the binary IS under ~/.dotnet/tools/.store/ so DotNetToolDetection - // classifies it as a global-tool install. Assert.Contains( interactionService!.DisplayedPlainText, line => line.Contains("dotnet tool update -g Aspire.Cli", StringComparison.Ordinal)); } - - /// - /// Pre-sidecar script install compat: when the running binary has no - /// sidecar AND the process path doesn't match any dotnet-tool layout, - /// the resolver yields and - /// routes Unknown to - /// . --self must then - /// reach the in-process flow rather than printing a refusal. We assert - /// the SUT does NOT print any of the refusal-message prefixes - /// (verifying it didn't take the gated branch) instead of trying to - /// drive a full network download. - /// - [Fact] - public async Task SelfUpdate_NoSidecar_NotDotnetTool_FallsThroughToInProcessFlow() - { - using var workspace = TemporaryWorkspace.Create(outputHelper); - using var processPathScope = DotNetToolDetection.UseProcessPathForTesting("/tmp/random/aspire"); - - var selfInfo = new InstallationInfo - { - Path = "/tmp/random/aspire", - CanonicalPath = "/tmp/random/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(); - // Invoke --self; we don't expect this to complete the actual update - // (no real network in the test process), but we do expect it to NOT - // emit any of the route-refusal messages. Any non-refusal outcome - // (timeout / failure / success / cancellation) is acceptable here; - // what matters is that the gated branch was not taken. - try - { - await command.Parse("update --self --yes --channel stable").InvokeAsync().DefaultTimeout(); - } - catch - { - // The in-process flow may throw for any number of network / - // download / signature reasons; the test doesn't depend on - // those succeeding. - } - - Assert.NotNull(interactionService); - // None of the route-specific refusal messages must appear: those - // are the signal that the gated branch was taken. - var allOutput = string.Join("\n", interactionService!.DisplayedPlainText); - Assert.DoesNotContain("get-aspire-cli-pr.sh", allOutput, StringComparison.Ordinal); - Assert.DoesNotContain("winget upgrade", allOutput, StringComparison.Ordinal); - Assert.DoesNotContain("brew upgrade", allOutput, StringComparison.Ordinal); - Assert.DoesNotContain("./localhive.sh", allOutput, StringComparison.Ordinal); - Assert.DoesNotContain("dotnet tool update", allOutput, StringComparison.Ordinal); - } } diff --git a/tests/Aspire.Cli.Tests/Acquisition/UpdateNotificationRouteTests.cs b/tests/Aspire.Cli.Tests/Acquisition/UpdateNotificationRouteTests.cs index 1d02147709f..2d0b9a4d119 100644 --- a/tests/Aspire.Cli.Tests/Acquisition/UpdateNotificationRouteTests.cs +++ b/tests/Aspire.Cli.Tests/Acquisition/UpdateNotificationRouteTests.cs @@ -32,16 +32,15 @@ public class UpdateNotificationRouteTests(ITestOutputHelper outputHelper) [Theory] [InlineData("winget", "winget upgrade Microsoft.Aspire")] [InlineData("brew", "brew upgrade --cask aspire")] - [InlineData("localhive", "./localhive.sh # re-run from your Aspire checkout")] + [InlineData("localhive", "Run ./localhive.sh (Linux/macOS) or .\\localhive.ps1 (Windows) in the local hive directory.")] [InlineData("script", "aspire update --self")] - [InlineData(null, "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. Falls back to "aspire update --self" via - // SelfUpdateRouter's Unknown → in-process classification. - [InlineData("future-route-name", "aspire update --self")] + // 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); @@ -103,6 +102,6 @@ public async Task NotifyIfUpdateAvailable_RouteAwareCommand_MatchesUpgradeInstru notifier.NotifyIfUpdateAvailable(); Assert.NotNull(interactionService); - Assert.Equal(expectedCommand, interactionService.LastVersionUpdateCommand); + 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 index 3d31a7443d3..a18d7c8e26b 100644 --- a/tests/Aspire.Cli.Tests/Acquisition/UpgradeInstructionProviderTests.cs +++ b/tests/Aspire.Cli.Tests/Acquisition/UpgradeInstructionProviderTests.cs @@ -15,7 +15,7 @@ public class UpgradeInstructionProviderTests [Theory] [InlineData("Winget", "winget upgrade Microsoft.Aspire")] [InlineData("Brew", "brew upgrade --cask aspire")] - [InlineData("LocalHive", "./localhive.sh # re-run from your Aspire checkout")] + [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); @@ -24,10 +24,9 @@ public void GetUpdateCommand_StaticHintRoutes_ReturnExpectedCommand(string sourc } // Routes where there is intentionally no separate update command — script - // gets the in-process flow; Unknown has no actionable hint. + // gets the in-process flow. [Theory] [InlineData("Script")] - [InlineData("Unknown")] public void GetUpdateCommand_NoHintRoutes_ReturnNull(string sourceName) { var source = Enum.Parse(sourceName); @@ -114,10 +113,44 @@ public void GetUpdateCommand_DotnetTool_PathShapeUnrecognized_FallsBackToGlobal( // 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("/tmp/random/path/aspire"); + 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); + } }