diff --git a/src/Aspire.Cli/Commands/UpdateCommand.cs b/src/Aspire.Cli/Commands/UpdateCommand.cs index cb80b265f04..2df5af52ac1 100644 --- a/src/Aspire.Cli/Commands/UpdateCommand.cs +++ b/src/Aspire.Cli/Commands/UpdateCommand.cs @@ -177,7 +177,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell // Otherwise, use the implicit/default channel automatically. var hasHives = ExecutionContext.GetPrHiveCount() > 0; - if (hasHives) + if (hasHives && InteractionService.SupportsInteractiveInput) { // Prompt for channel selection channel = await InteractionService.PromptForSelectionAsync( @@ -274,15 +274,23 @@ private async Task ExecuteSelfUpdateAsync(ParseResult parseResult, Cancella // for future 'aspire new' and 'aspire init' commands. if (string.IsNullOrEmpty(channel)) { - var isStagingEnabled = KnownFeatures.IsStagingChannelEnabled(_features, _configuration); - var channels = isStagingEnabled - ? new[] { PackageChannelNames.Stable, PackageChannelNames.Staging, PackageChannelNames.Daily } - : new[] { PackageChannelNames.Stable, PackageChannelNames.Daily }; - channel = await InteractionService.PromptForSelectionAsync( - "Select the channel to update to:", - channels, - q => q, - cancellationToken); + if (!InteractionService.SupportsInteractiveInput) + { + // In non-interactive mode, default to the stable channel + channel = PackageChannelNames.Stable; + } + else + { + var isStagingEnabled = KnownFeatures.IsStagingChannelEnabled(_features, _configuration); + var channels = isStagingEnabled + ? new[] { PackageChannelNames.Stable, PackageChannelNames.Staging, PackageChannelNames.Daily } + : new[] { PackageChannelNames.Stable, PackageChannelNames.Daily }; + channel = await InteractionService.PromptForSelectionAsync( + "Select the channel to update to:", + channels, + q => q, + cancellationToken); + } } try diff --git a/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs b/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs index 87deb6385d5..f668d710130 100644 --- a/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs +++ b/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs @@ -37,6 +37,8 @@ internal class ConsoleInteractionService : IInteractionService public ConsoleOutput Console { get; set; } + public bool SupportsInteractiveInput => _hostEnvironment.SupportsInteractiveInput; + public ConsoleInteractionService(ConsoleEnvironment consoleEnvironment, CliExecutionContext executionContext, ICliHostEnvironment hostEnvironment, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(consoleEnvironment); @@ -424,7 +426,8 @@ public async Task ConfirmAsync(string promptText, bool defaultValue = true { if (!_hostEnvironment.SupportsInteractiveInput) { - throw new InvalidOperationException(InteractionServiceStrings.InteractiveInputNotSupported); + MessageLogger.LogInformation("Confirm (non-interactive, using default): {PromptText} = {DefaultValue}", promptText, defaultValue); + return defaultValue; } MessageLogger.LogInformation("Confirm: {PromptText} (default: {DefaultValue})", promptText, defaultValue); diff --git a/src/Aspire.Cli/Interaction/ExtensionInteractionService.cs b/src/Aspire.Cli/Interaction/ExtensionInteractionService.cs index c6060ec1496..b3d191aaa2a 100644 --- a/src/Aspire.Cli/Interaction/ExtensionInteractionService.cs +++ b/src/Aspire.Cli/Interaction/ExtensionInteractionService.cs @@ -384,6 +384,8 @@ public ConsoleOutput Console set => _consoleInteractionService.Console = value; } + public bool SupportsInteractiveInput => _extensionPromptEnabled || _consoleInteractionService.SupportsInteractiveInput; + public void DisplayRawText(string text, ConsoleOutput? consoleOverride = null) { var result = _extensionTaskChannel.Writer.TryWrite(() => Backchannel.DisplayPlainTextAsync(text, _cancellationToken)); diff --git a/src/Aspire.Cli/Interaction/IInteractionService.cs b/src/Aspire.Cli/Interaction/IInteractionService.cs index 0b2711e4adb..dbd3198369e 100644 --- a/src/Aspire.Cli/Interaction/IInteractionService.cs +++ b/src/Aspire.Cli/Interaction/IInteractionService.cs @@ -32,6 +32,14 @@ internal interface IInteractionService void DisplayCancellationMessage(); void DisplayEmptyLine(); + /// + /// Gets whether the interaction service supports interactive input (e.g., prompts). + /// When false, calling interactive methods like or + /// will throw . + /// returns the default value instead of prompting. + /// + bool SupportsInteractiveInput { get; } + /// /// Gets or sets the default console output stream for human-readable messages. /// When set to , display methods route output to stderr diff --git a/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs index 4638378df66..07fada4e332 100644 --- a/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs @@ -1600,6 +1600,8 @@ internal sealed class OrderTrackingInteractionService(List operationOrde { public ConsoleOutput Console { get; set; } + public bool SupportsInteractiveInput => true; + public Task ShowStatusAsync(string statusText, Func> action, KnownEmoji? emoji = null, bool allowMarkup = false) { return action(); diff --git a/tests/Aspire.Cli.Tests/Commands/PublishCommandPromptingIntegrationTests.cs b/tests/Aspire.Cli.Tests/Commands/PublishCommandPromptingIntegrationTests.cs index a3e046d8e77..5bee13335c6 100644 --- a/tests/Aspire.Cli.Tests/Commands/PublishCommandPromptingIntegrationTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/PublishCommandPromptingIntegrationTests.cs @@ -852,6 +852,7 @@ internal sealed class TestConsoleInteractionServiceWithPromptTracking : IInterac private bool _shouldCancel; public ConsoleOutput Console { get; set; } + public bool SupportsInteractiveInput => true; public List StringPromptCalls { get; } = []; public List SelectionPromptCalls { get; } = []; // Using object to handle generic types public List BooleanPromptCalls { get; } = []; diff --git a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs index 6c8d9d1acc5..88451ade15b 100644 --- a/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs @@ -1034,6 +1034,8 @@ public ConsoleOutput Console set => _innerService.Console = value; } + public bool SupportsInteractiveInput => _innerService.SupportsInteractiveInput; + public Action? OnCancellationMessageDisplayed { get; set; } public CancellationTrackingInteractionService(IInteractionService innerService) diff --git a/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs b/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs index 23fa754443b..8d6fceb99ed 100644 --- a/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs +++ b/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs @@ -279,17 +279,19 @@ public async Task PromptForSelectionsAsync_WhenInteractiveInputNotSupported_Thro } [Fact] - public async Task ConfirmAsync_WhenInteractiveInputNotSupported_ThrowsInvalidOperationException() + public async Task ConfirmAsync_WhenInteractiveInputNotSupported_ReturnsDefaultValue() { // Arrange var executionContext = new CliExecutionContext(new DirectoryInfo("."), new DirectoryInfo("."), new DirectoryInfo("."), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-runtimes")), new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), "test.log"); var hostEnvironment = TestHelpers.CreateNonInteractiveHostEnvironment(); var interactionService = CreateInteractionService(AnsiConsole.Console, executionContext, hostEnvironment); - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - interactionService.ConfirmAsync("Confirm?", true, CancellationToken.None)); - Assert.Contains(InteractionServiceStrings.InteractiveInputNotSupported, exception.Message); + // Act & Assert - returns the default value instead of throwing + var resultTrue = await interactionService.ConfirmAsync("Confirm?", defaultValue: true, CancellationToken.None); + Assert.True(resultTrue); + + var resultFalse = await interactionService.ConfirmAsync("Confirm?", defaultValue: false, CancellationToken.None); + Assert.False(resultFalse); } [Fact] diff --git a/tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs b/tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs index 52670fb8571..8a4d3438224 100644 --- a/tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/ExtensionGuestLauncherTests.cs @@ -156,6 +156,7 @@ public Task LaunchAppHostAsync(string projectFile, List arguments, List< public void ConsoleDisplaySubtleMessage(string message, bool allowMarkup = false) => throw new NotImplementedException(); public void WriteConsoleLog(string message, int? lineNumber = null, string? type = null, bool isErrorMessage = false) => throw new NotImplementedException(); public ConsoleOutput Console { get; set; } + public bool SupportsInteractiveInput => true; public Task ShowStatusAsync(string statusText, Func> action, KnownEmoji? emoji = null, bool allowMarkup = false) => throw new NotImplementedException(); public void ShowStatus(string statusText, Action action, KnownEmoji? emoji = null, bool allowMarkup = false) => throw new NotImplementedException(); public Task PromptForStringAsync(string promptText, string? defaultValue = null, Func? validator = null, bool isSecret = false, bool required = false, CancellationToken cancellationToken = default) => throw new NotImplementedException(); diff --git a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs index 3bf653368cc..08a4643b96e 100644 --- a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs +++ b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs @@ -454,6 +454,8 @@ private sealed class TestInteractionService : IInteractionService { public ConsoleOutput Console { get; set; } + public bool SupportsInteractiveInput => true; + public Task PromptForSelectionAsync(string prompt, IEnumerable choices, Func displaySelector, CancellationToken cancellationToken) where T : notnull => throw new NotImplementedException(); diff --git a/tests/Aspire.Cli.Tests/TestServices/TestExtensionInteractionService.cs b/tests/Aspire.Cli.Tests/TestServices/TestExtensionInteractionService.cs index a27e9ae7c6c..9b2deb91506 100644 --- a/tests/Aspire.Cli.Tests/TestServices/TestExtensionInteractionService.cs +++ b/tests/Aspire.Cli.Tests/TestServices/TestExtensionInteractionService.cs @@ -14,6 +14,7 @@ namespace Aspire.Cli.Tests.TestServices; internal sealed class TestExtensionInteractionService(IServiceProvider serviceProvider) : IExtensionInteractionService { public ConsoleOutput Console { get; set; } + public bool SupportsInteractiveInput => true; public Action? DisplayErrorCallback { get; set; } public Action? DisplaySubtleMessageCallback { get; set; } public Action? DisplayConsoleWriteLineMessage { get; set; } diff --git a/tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs b/tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs index befce116ddf..f3f49e42be8 100644 --- a/tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs +++ b/tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs @@ -17,6 +17,8 @@ internal sealed class TestInteractionService : IInteractionService public ConsoleOutput Console { get; set; } + public bool SupportsInteractiveInput { get; set; } = true; + // Callback hooks public Action? DisplaySubtleMessageCallback { get; set; } public Action? DisplayConsoleWriteLineMessage { get; set; }