Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions src/Aspire.Cli/Commands/UpdateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ protected override async Task<int> 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(
Expand Down Expand Up @@ -274,15 +274,23 @@ private async Task<int> 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;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@davidfowl does this make sense? Maybe we should force the user to specify the channel, and fail the command?

}
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
Expand Down
5 changes: 4 additions & 1 deletion src/Aspire.Cli/Interaction/ConsoleInteractionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -424,7 +426,8 @@ public async Task<bool> 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);
Expand Down
2 changes: 2 additions & 0 deletions src/Aspire.Cli/Interaction/ExtensionInteractionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
8 changes: 8 additions & 0 deletions src/Aspire.Cli/Interaction/IInteractionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ internal interface IInteractionService
void DisplayCancellationMessage();
void DisplayEmptyLine();

/// <summary>
/// Gets whether the interaction service supports interactive input (e.g., prompts).
/// When <c>false</c>, calling interactive methods like <see cref="PromptForStringAsync"/> or
/// <see cref="PromptForSelectionAsync{T}"/> will throw <see cref="InvalidOperationException"/>.
/// <see cref="ConfirmAsync"/> returns the default value instead of prompting.
/// </summary>
bool SupportsInteractiveInput { get; }

/// <summary>
/// Gets or sets the default console output stream for human-readable messages.
/// When set to <see cref="ConsoleOutput.Error"/>, display methods route output to stderr
Expand Down
2 changes: 2 additions & 0 deletions tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1600,6 +1600,8 @@ internal sealed class OrderTrackingInteractionService(List<string> operationOrde
{
public ConsoleOutput Console { get; set; }

public bool SupportsInteractiveInput => true;

public Task<T> ShowStatusAsync<T>(string statusText, Func<Task<T>> action, KnownEmoji? emoji = null, bool allowMarkup = false)
{
return action();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,7 @@ internal sealed class TestConsoleInteractionServiceWithPromptTracking : IInterac
private bool _shouldCancel;

public ConsoleOutput Console { get; set; }
public bool SupportsInteractiveInput => true;
public List<StringPromptCall> StringPromptCalls { get; } = [];
public List<object> SelectionPromptCalls { get; } = []; // Using object to handle generic types
public List<BooleanPromptCall> BooleanPromptCalls { get; } = [];
Expand Down
2 changes: 2 additions & 0 deletions tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<InvalidOperationException>(() =>
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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ public Task LaunchAppHostAsync(string projectFile, List<string> 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<T> ShowStatusAsync<T>(string statusText, Func<Task<T>> 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<string> PromptForStringAsync(string promptText, string? defaultValue = null, Func<string, Spectre.Console.ValidationResult>? validator = null, bool isSecret = false, bool required = false, CancellationToken cancellationToken = default) => throw new NotImplementedException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,8 @@ private sealed class TestInteractionService : IInteractionService
{
public ConsoleOutput Console { get; set; }

public bool SupportsInteractiveInput => true;

public Task<T> PromptForSelectionAsync<T>(string prompt, IEnumerable<T> choices, Func<T, string> displaySelector, CancellationToken cancellationToken) where T : notnull
=> throw new NotImplementedException();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>? DisplayErrorCallback { get; set; }
public Action<string>? DisplaySubtleMessageCallback { get; set; }
public Action<string>? DisplayConsoleWriteLineMessage { get; set; }
Expand Down
2 changes: 2 additions & 0 deletions tests/Aspire.Cli.Tests/TestServices/TestInteractionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ internal sealed class TestInteractionService : IInteractionService

public ConsoleOutput Console { get; set; }

public bool SupportsInteractiveInput { get; set; } = true;

// Callback hooks
public Action<string>? DisplaySubtleMessageCallback { get; set; }
public Action<string>? DisplayConsoleWriteLineMessage { get; set; }
Expand Down
Loading