55using Aspire . Cli . Backchannel ;
66using Aspire . Cli . Resources ;
77using Aspire . Cli . Utils ;
8+ using Microsoft . Extensions . Logging ;
89using Spectre . Console ;
910using Spectre . Console . Rendering ;
1011
@@ -22,24 +23,32 @@ internal class ConsoleInteractionService : IInteractionService
2223 private readonly IAnsiConsole _errorConsole ;
2324 private readonly CliExecutionContext _executionContext ;
2425 private readonly ICliHostEnvironment _hostEnvironment ;
26+ private readonly ILogger _stdoutLogger ;
27+ private readonly ILogger _stderrLogger ;
2528 private int _inStatus ;
2629
2730 /// <summary>
2831 /// Console used for human-readable messages; routes to stderr when <see cref="Console"/> is set to <see cref="ConsoleOutput.Error"/>.
2932 /// </summary>
3033 private IAnsiConsole MessageConsole => Console == ConsoleOutput . Error ? _errorConsole : _outConsole ;
3134
35+ // Limit logging to prompts and messages. Don't log raw text output since it may contain sensitive information.
36+ private ILogger MessageLogger => Console == ConsoleOutput . Error ? _stderrLogger : _stdoutLogger ;
37+
3238 public ConsoleOutput Console { get ; set ; }
3339
34- public ConsoleInteractionService ( ConsoleEnvironment consoleEnvironment , CliExecutionContext executionContext , ICliHostEnvironment hostEnvironment )
40+ public ConsoleInteractionService ( ConsoleEnvironment consoleEnvironment , CliExecutionContext executionContext , ICliHostEnvironment hostEnvironment , ILoggerFactory loggerFactory )
3541 {
3642 ArgumentNullException . ThrowIfNull ( consoleEnvironment ) ;
3743 ArgumentNullException . ThrowIfNull ( executionContext ) ;
3844 ArgumentNullException . ThrowIfNull ( hostEnvironment ) ;
45+ ArgumentNullException . ThrowIfNull ( loggerFactory ) ;
3946 _outConsole = consoleEnvironment . Out ;
4047 _errorConsole = consoleEnvironment . Error ;
4148 _executionContext = executionContext ;
4249 _hostEnvironment = hostEnvironment ;
50+ _stdoutLogger = loggerFactory . CreateLogger ( "Aspire.Cli.Console.Stdout" ) ;
51+ _stderrLogger = loggerFactory . CreateLogger ( "Aspire.Cli.Console.Stderr" ) ;
4352 }
4453
4554 public async Task < T > ShowStatusAsync < T > ( string statusText , Func < Task < T > > action , KnownEmoji ? emoji = null , bool allowMarkup = false )
@@ -69,6 +78,10 @@ public async Task<T> ShowStatusAsync<T>(string statusText, Func<Task<T>> action,
6978 // Text has already been escaped and emoji prepended, so pass as markup
7079 DisplaySubtleMessage ( statusText , allowMarkup : true ) ;
7180 }
81+ else
82+ {
83+ MessageLogger . LogInformation ( "Status: {StatusText}" , statusText ) ;
84+ }
7285 return await action ( ) ;
7386 }
7487
@@ -86,6 +99,8 @@ public async Task<T> ShowStatusAsync<T>(string statusText, Func<Task<T>> action,
8699
87100 public void ShowStatus ( string statusText , Action action , KnownEmoji ? emoji = null , bool allowMarkup = false )
88101 {
102+ MessageLogger . LogInformation ( "Status: {StatusText}" , statusText ) ;
103+
89104 if ( ! allowMarkup )
90105 {
91106 statusText = statusText . EscapeMarkup ( ) ;
@@ -135,6 +150,8 @@ public async Task<string> PromptForStringAsync(string promptText, string? defaul
135150 throw new InvalidOperationException ( InteractionServiceStrings . InteractiveInputNotSupported ) ;
136151 }
137152
153+ MessageLogger . LogInformation ( "Prompt: {PromptText} (default: {DefaultValue}, secret: {IsSecret})" , promptText , isSecret ? "****" : defaultValue ?? "(none)" , isSecret ) ;
154+
138155 var prompt = new TextPrompt < string > ( promptText )
139156 {
140157 IsSecret = isSecret ,
@@ -153,7 +170,9 @@ public async Task<string> PromptForStringAsync(string promptText, string? defaul
153170 prompt . Validate ( validator ) ;
154171 }
155172
156- return await _outConsole . PromptAsync ( prompt , cancellationToken ) ;
173+ var result = await MessageConsole . PromptAsync ( prompt , cancellationToken ) ;
174+ MessageLogger . LogInformation ( "Prompt result: {Result}" , isSecret ? "****" : result ) ;
175+ return result ;
157176 }
158177
159178 public Task < string > PromptForFilePathAsync ( string promptText , string ? defaultValue = null , Func < string , ValidationResult > ? validator = null , bool directory = false , bool required = false , CancellationToken cancellationToken = default )
@@ -185,6 +204,8 @@ public async Task<T> PromptForSelectionAsync<T>(string promptText, IEnumerable<T
185204 // the text is safe for both rendering and search highlighting.
186205 var safeFormatter = MakeSafeFormatter ( choiceFormatter ) ;
187206
207+ MessageLogger . LogInformation ( "Selection prompt: {PromptText}" , promptText ) ;
208+
188209 var prompt = new SelectionPrompt < T > ( )
189210 . Title ( promptText )
190211 . UseConverter ( safeFormatter )
@@ -194,7 +215,9 @@ public async Task<T> PromptForSelectionAsync<T>(string promptText, IEnumerable<T
194215
195216 prompt . SearchHighlightStyle = s_searchHighlightStyle ;
196217
197- return await _outConsole . PromptAsync ( prompt , cancellationToken ) ;
218+ var result = await MessageConsole . PromptAsync ( prompt , cancellationToken ) ;
219+ MessageLogger . LogInformation ( "Selection result: {Result}" , safeFormatter ( result ) ) ;
220+ return result ;
198221 }
199222
200223 public async Task < IReadOnlyList < T > > PromptForSelectionsAsync < T > ( string promptText , IEnumerable < T > choices , Func < T , string > choiceFormatter , IEnumerable < T > ? preSelected = null , bool optional = false , CancellationToken cancellationToken = default ) where T : notnull
@@ -219,6 +242,8 @@ public async Task<IReadOnlyList<T>> PromptForSelectionsAsync<T>(string promptTex
219242
220243 var safeFormatter = MakeSafeFormatter ( choiceFormatter ) ;
221244
245+ MessageLogger . LogInformation ( "Selection prompt: {PromptText}" , promptText ) ;
246+
222247 var prompt = new MultiSelectionPrompt < T > ( )
223248 . Title ( promptText )
224249 . UseConverter ( safeFormatter )
@@ -235,7 +260,8 @@ public async Task<IReadOnlyList<T>> PromptForSelectionsAsync<T>(string promptTex
235260 }
236261 }
237262
238- var result = await _outConsole . PromptAsync ( prompt , cancellationToken ) ;
263+ var result = await MessageConsole . PromptAsync ( prompt , cancellationToken ) ;
264+ MessageLogger . LogInformation ( "Selection results: {Results}" , string . Join ( ", " , result . Select ( safeFormatter ) ) ) ;
239265 return result ;
240266 }
241267
@@ -299,6 +325,14 @@ public void DisplayError(string errorMessage)
299325
300326 public void DisplayMessage ( KnownEmoji emoji , string message , bool allowMarkup = false )
301327 {
328+ if ( MessageLogger . IsEnabled ( LogLevel . Information ) )
329+ {
330+ // Only attempt to parse/remove markup when the message is expected to contain it.
331+ // Plain text messages may contain characters like '[' that would be rejected by the markup parser.
332+ var logMessage = allowMarkup ? message . RemoveMarkup ( ) : message ;
333+ MessageLogger . LogInformation ( "{Message}" , ConsoleHelpers . FormatEmojiPrefix ( emoji , MessageConsole , replaceEmoji : true ) + logMessage ) ;
334+ }
335+
302336 var displayMessage = allowMarkup ? message : message . EscapeMarkup ( ) ;
303337 MessageConsole . MarkupLine ( ConsoleHelpers . FormatEmojiPrefix ( emoji , MessageConsole ) + displayMessage ) ;
304338 }
@@ -311,9 +345,9 @@ public void DisplayPlainText(string message)
311345
312346 public void DisplayRawText ( string text , ConsoleOutput ? consoleOverride = null )
313347 {
348+ var effectiveConsole = consoleOverride ?? Console ;
314349 // Write raw text directly to avoid console wrapping.
315350 // When consoleOverride is null, respect the Console setting.
316- var effectiveConsole = consoleOverride ?? Console ;
317351 var target = effectiveConsole == ConsoleOutput . Error ? _errorConsole : _outConsole ;
318352 target . Profile . Out . Writer . WriteLine ( text ) ;
319353 }
@@ -386,18 +420,22 @@ public void DisplayCancellationMessage()
386420 DisplayMessage ( KnownEmojis . StopSign , $ "[teal bold]{ InteractionServiceStrings . StoppingAspire } [/]", allowMarkup : true ) ;
387421 }
388422
389- public Task < bool > ConfirmAsync ( string promptText , bool defaultValue = true , CancellationToken cancellationToken = default )
423+ public async Task < bool > ConfirmAsync ( string promptText , bool defaultValue = true , CancellationToken cancellationToken = default )
390424 {
391425 if ( ! _hostEnvironment . SupportsInteractiveInput )
392426 {
393427 throw new InvalidOperationException ( InteractionServiceStrings . InteractiveInputNotSupported ) ;
394428 }
395429
396- return _outConsole . ConfirmAsync ( promptText , defaultValue , cancellationToken ) ;
430+ MessageLogger . LogInformation ( "Confirm: {PromptText} (default: {DefaultValue})" , promptText , defaultValue ) ;
431+ var result = await MessageConsole . ConfirmAsync ( promptText , defaultValue , cancellationToken ) ;
432+ MessageLogger . LogInformation ( "Confirm result: {Result}" , result ) ;
433+ return result ;
397434 }
398435
399436 public void DisplaySubtleMessage ( string message , bool allowMarkup = false )
400437 {
438+ MessageLogger . LogInformation ( "{Message}" , message ) ;
401439 var displayMessage = allowMarkup ? message : message . EscapeMarkup ( ) ;
402440 MessageConsole . MarkupLine ( $ "[dim]{ displayMessage } [/]") ;
403441 }
@@ -422,5 +460,4 @@ public void DisplayVersionUpdateNotification(string newerVersion, string? update
422460
423461 _errorConsole . MarkupLine ( string . Format ( CultureInfo . CurrentCulture , InteractionServiceStrings . MoreInfoNewCliVersion , UpdateUrl ) ) ;
424462 }
425-
426463}
0 commit comments