Skip to content
Merged
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: 14 additions & 14 deletions eng/Version.Details.xml
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<Dependencies>
<ProductDependencies>
<Dependency Name="Microsoft.DeveloperControlPlane.darwin-amd64" Version="0.22.4">
<Dependency Name="Microsoft.DeveloperControlPlane.darwin-amd64" Version="0.22.6">
<Uri>https://github.com/microsoft/dcp</Uri>
<Sha>f1dbae0486549c8e25572c82524e533ce40e3bc1</Sha>
<Sha>9585d3bbfad8a356770096fcda944349da4145f1</Sha>
</Dependency>
<Dependency Name="Microsoft.DeveloperControlPlane.darwin-arm64" Version="0.22.4">
<Dependency Name="Microsoft.DeveloperControlPlane.darwin-arm64" Version="0.22.6">
<Uri>https://github.com/microsoft/dcp</Uri>
<Sha>f1dbae0486549c8e25572c82524e533ce40e3bc1</Sha>
<Sha>9585d3bbfad8a356770096fcda944349da4145f1</Sha>
</Dependency>
<Dependency Name="Microsoft.DeveloperControlPlane.linux-amd64" Version="0.22.4">
<Dependency Name="Microsoft.DeveloperControlPlane.linux-amd64" Version="0.22.6">
<Uri>https://github.com/microsoft/dcp</Uri>
<Sha>f1dbae0486549c8e25572c82524e533ce40e3bc1</Sha>
<Sha>9585d3bbfad8a356770096fcda944349da4145f1</Sha>
</Dependency>
<Dependency Name="Microsoft.DeveloperControlPlane.linux-arm64" Version="0.22.4">
<Dependency Name="Microsoft.DeveloperControlPlane.linux-arm64" Version="0.22.6">
<Uri>https://github.com/microsoft/dcp</Uri>
<Sha>f1dbae0486549c8e25572c82524e533ce40e3bc1</Sha>
<Sha>9585d3bbfad8a356770096fcda944349da4145f1</Sha>
</Dependency>
<Dependency Name="Microsoft.DeveloperControlPlane.linux-musl-amd64" Version="0.22.4">
<Dependency Name="Microsoft.DeveloperControlPlane.linux-musl-amd64" Version="0.22.6">
<Uri>https://github.com/microsoft/dcp</Uri>
<Sha>f1dbae0486549c8e25572c82524e533ce40e3bc1</Sha>
<Sha>9585d3bbfad8a356770096fcda944349da4145f1</Sha>
</Dependency>
<Dependency Name="Microsoft.DeveloperControlPlane.windows-amd64" Version="0.22.4">
<Dependency Name="Microsoft.DeveloperControlPlane.windows-amd64" Version="0.22.6">
<Uri>https://github.com/microsoft/dcp</Uri>
<Sha>f1dbae0486549c8e25572c82524e533ce40e3bc1</Sha>
<Sha>9585d3bbfad8a356770096fcda944349da4145f1</Sha>
</Dependency>
<Dependency Name="Microsoft.DeveloperControlPlane.windows-arm64" Version="0.22.4">
<Dependency Name="Microsoft.DeveloperControlPlane.windows-arm64" Version="0.22.6">
<Uri>https://github.com/microsoft/dcp</Uri>
<Sha>f1dbae0486549c8e25572c82524e533ce40e3bc1</Sha>
<Sha>9585d3bbfad8a356770096fcda944349da4145f1</Sha>
</Dependency>
<Dependency Name="Microsoft.Extensions.DependencyInjection.AutoActivation" Version="10.2.0">
<Uri>https://dev.azure.com/dnceng/internal/_git/dotnet-extensions</Uri>
Expand Down
14 changes: 7 additions & 7 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@
<!-- Package versions defined directly in <reporoot>/Directory.Packages.props -->
<MicrosoftDotnetSdkInternalVersion>8.0.100-rtm.23512.16</MicrosoftDotnetSdkInternalVersion>
<!-- DCP -->
<MicrosoftDeveloperControlPlanedarwinamd64Version>0.22.4</MicrosoftDeveloperControlPlanedarwinamd64Version>
<MicrosoftDeveloperControlPlanedarwinarm64Version>0.22.4</MicrosoftDeveloperControlPlanedarwinarm64Version>
<MicrosoftDeveloperControlPlanelinuxamd64Version>0.22.4</MicrosoftDeveloperControlPlanelinuxamd64Version>
<MicrosoftDeveloperControlPlanelinuxarm64Version>0.22.4</MicrosoftDeveloperControlPlanelinuxarm64Version>
<MicrosoftDeveloperControlPlanelinuxmuslamd64Version>0.22.4</MicrosoftDeveloperControlPlanelinuxmuslamd64Version>
<MicrosoftDeveloperControlPlanewindowsamd64Version>0.22.4</MicrosoftDeveloperControlPlanewindowsamd64Version>
<MicrosoftDeveloperControlPlanewindowsarm64Version>0.22.4</MicrosoftDeveloperControlPlanewindowsarm64Version>
<MicrosoftDeveloperControlPlanedarwinamd64Version>0.22.6</MicrosoftDeveloperControlPlanedarwinamd64Version>
<MicrosoftDeveloperControlPlanedarwinarm64Version>0.22.6</MicrosoftDeveloperControlPlanedarwinarm64Version>
<MicrosoftDeveloperControlPlanelinuxamd64Version>0.22.6</MicrosoftDeveloperControlPlanelinuxamd64Version>
<MicrosoftDeveloperControlPlanelinuxarm64Version>0.22.6</MicrosoftDeveloperControlPlanelinuxarm64Version>
<MicrosoftDeveloperControlPlanelinuxmuslamd64Version>0.22.6</MicrosoftDeveloperControlPlanelinuxmuslamd64Version>
<MicrosoftDeveloperControlPlanewindowsamd64Version>0.22.6</MicrosoftDeveloperControlPlanewindowsamd64Version>
<MicrosoftDeveloperControlPlanewindowsarm64Version>0.22.6</MicrosoftDeveloperControlPlanewindowsarm64Version>
<!-- Other -->
<MicrosoftDotNetRemoteExecutorVersion>10.0.0-beta.26110.1</MicrosoftDotNetRemoteExecutorVersion>
<MicrosoftDotNetXUnitV3ExtensionsVersion>10.0.0-beta.26110.1</MicrosoftDotNetXUnitV3ExtensionsVersion>
Expand Down
5 changes: 3 additions & 2 deletions src/Aspire.Cli/Backchannel/AppHostConnectionResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Aspire.Cli.Interaction;
using Aspire.Cli.Utils;
using Microsoft.Extensions.Logging;
using Spectre.Console;

namespace Aspire.Cli.Backchannel;

Expand Down Expand Up @@ -125,7 +126,7 @@ public async Task<AppHostConnectionResult> ResolveConnectionAsync(
var selectedDisplay = await interactionService.PromptForSelectionAsync(
selectPrompt,
choices.Select(c => c.Display).ToArray(),
c => c,
c => c.EscapeMarkup(),
cancellationToken);

selectedConnection = choices.FirstOrDefault(c => c.Display == selectedDisplay).Connection;
Expand All @@ -148,7 +149,7 @@ public async Task<AppHostConnectionResult> ResolveConnectionAsync(
var selectedDisplay = await interactionService.PromptForSelectionAsync(
selectPrompt,
choices.Select(c => c.Display).ToArray(),
c => c,
c => c.EscapeMarkup(),
cancellationToken);

selectedConnection = choices.FirstOrDefault(c => c.Display == selectedDisplay).Connection;
Expand Down
9 changes: 5 additions & 4 deletions src/Aspire.Cli/Commands/AddCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Aspire.Cli.Telemetry;
using Aspire.Cli.Utils;
using Semver;
using Spectre.Console;
using NuGetPackage = Aspire.Shared.NuGetPackageCli;

namespace Aspire.Cli.Commands;
Expand Down Expand Up @@ -325,7 +326,7 @@ internal class AddCommandPrompter(IInteractionService interactionService) : IAdd
// Helper to keep labels consistently formatted: "Version (source)"
static string FormatVersionLabel((string FriendlyName, NuGetPackage Package, PackageChannel Channel) item)
{
return $"{item.Package.Version} ({item.Channel.SourceDetails})";
return $"{item.Package.Version.EscapeMarkup()} ({item.Channel.SourceDetails.EscapeMarkup()})";
}

async Task<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> PromptForChannelPackagesAsync(
Expand Down Expand Up @@ -395,7 +396,7 @@ static string FormatVersionLabel((string FriendlyName, NuGetPackage Package, Pac
var item = channelGroup.HighestVersion;

rootChoices.Add((
Label: channel.Name,
Label: channel.Name.EscapeMarkup(),
// For explicit channels, we still show submenu but with only the highest version
Action: ct => PromptForChannelPackagesAsync(channel, new[] { item }, ct)
));
Expand Down Expand Up @@ -442,11 +443,11 @@ private static string PackageNameWithFriendlyNameIfAvailable((string FriendlyNam
{
if (packageWithFriendlyName.FriendlyName is { } friendlyName)
{
return $"[bold]{friendlyName}[/] ({packageWithFriendlyName.Package.Id})";
return $"[bold]{friendlyName.EscapeMarkup()}[/] ({packageWithFriendlyName.Package.Id.EscapeMarkup()})";
}
else
{
return packageWithFriendlyName.Package.Id;
return packageWithFriendlyName.Package.Id.EscapeMarkup();
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Cli/Commands/InitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ later as needed.
var selectedProjects = await InteractionService.PromptForSelectionsAsync(
"Select projects to add to the AppHost:",
initContext.ExecutableProjects,
project => Path.GetFileNameWithoutExtension(project.ProjectFile.Name),
project => Path.GetFileNameWithoutExtension(project.ProjectFile.Name).EscapeMarkup(),
cancellationToken);

initContext.ExecutableProjectsToAddToAppHost = selectedProjects;
Expand Down Expand Up @@ -298,7 +298,7 @@ ServiceDefaults project contains helper code to make it easier
initContext.ProjectsToAddServiceDefaultsTo = await InteractionService.PromptForSelectionsAsync(
"Select projects to add ServiceDefaults reference to:",
initContext.ExecutableProjectsToAddToAppHost,
project => Path.GetFileNameWithoutExtension(project.ProjectFile.Name),
project => Path.GetFileNameWithoutExtension(project.ProjectFile.Name).EscapeMarkup(),
cancellationToken);
break;
case "none":
Expand Down
6 changes: 3 additions & 3 deletions src/Aspire.Cli/Commands/NewCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ internal class NewCommandPrompter(IInteractionService interactionService) : INew
static string FormatPackageLabel((NuGetPackage Package, PackageChannel Channel) item)
{
// Keep it concise: "Version (source)"
return $"{item.Package.Version} ({item.Channel.SourceDetails})";
return $"{item.Package.Version.EscapeMarkup()} ({item.Channel.SourceDetails.EscapeMarkup()})";
}

async Task<(NuGetPackage Package, PackageChannel Channel)> PromptForChannelPackagesAsync(
Expand Down Expand Up @@ -330,7 +330,7 @@ static string FormatPackageLabel((NuGetPackage Package, PackageChannel Channel)
var items = channelGroup.ToArray();

rootChoices.Add((
Label: channel.Name,
Label: channel.Name.EscapeMarkup(),
Action: ct => PromptForChannelPackagesAsync(channel, items, ct)
));
}
Expand Down Expand Up @@ -380,7 +380,7 @@ public virtual async Task<ITemplate> PromptForTemplateAsync(ITemplate[] validTem
return await interactionService.PromptForSelectionAsync(
NewCommandStrings.SelectAProjectTemplate,
validTemplates,
t => t.Description,
t => t.Description.EscapeMarkup(),
cancellationToken
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Commands/PipelineCommandBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@ private async Task HandlePromptActivityAsync(PublishingActivity activity, IAppHo
var (value, displayText) = await InteractionService.PromptForSelectionAsync(
promptText,
options,
choice => choice.Value,
choice => choice.Value.EscapeMarkup(),
cancellationToken);

if (value == CustomChoiceValue)
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Commands/PublishCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public virtual async Task<string> PromptForPublisherAsync(IEnumerable<string> pu
return await interactionService.PromptForSelectionAsync(
PublishCommandStrings.SelectAPublisher,
publishers,
p => p,
p => p.EscapeMarkup(),
cancellationToken
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Commands/UpdateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
channel = await InteractionService.PromptForSelectionAsync(
UpdateCommandStrings.SelectChannelPrompt,
allChannels,
(c) => $"{c.Name} ({c.SourceDetails})",
(c) => $"{c.Name.EscapeMarkup()} ({c.SourceDetails.EscapeMarkup()})",
cancellationToken);
}
else
Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Cli/Interaction/ConsoleInteractionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public async Task<T> PromptForSelectionAsync<T>(string promptText, IEnumerable<T

var prompt = new SelectionPrompt<T>()
.Title(promptText)
.UseConverter(item => choiceFormatter(item).EscapeMarkup())
.UseConverter(choiceFormatter)
.AddChoices(choices)
.PageSize(10)
.EnableSearch();
Expand Down Expand Up @@ -174,7 +174,7 @@ public async Task<IReadOnlyList<T>> PromptForSelectionsAsync<T>(string promptTex

var prompt = new MultiSelectionPrompt<T>()
.Title(promptText)
.UseConverter(item => choiceFormatter(item).EscapeMarkup())
.UseConverter(choiceFormatter)
.AddChoices(choices)
.PageSize(10);

Expand Down
5 changes: 3 additions & 2 deletions src/Aspire.Cli/Projects/ProjectLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Aspire.Cli.Utils;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.Logging;
using Spectre.Console;

namespace Aspire.Cli.Projects;

Expand Down Expand Up @@ -199,7 +200,7 @@ public async Task<AppHostProjectSearchResult> UseOrFindAppHostProjectFileAsync(F
projectFile = await interactionService.PromptForSelectionAsync(
InteractionServiceStrings.SelectAppHostToUse,
appHostProjects,
file => $"{file.Name} ({Path.GetRelativePath(executionContext.WorkingDirectory.FullName, file.FullName)})",
file => $"{file.Name.EscapeMarkup()} ({Path.GetRelativePath(executionContext.WorkingDirectory.FullName, file.FullName).EscapeMarkup()})",
cancellationToken
);
}
Expand Down Expand Up @@ -277,7 +278,7 @@ public async Task<AppHostProjectSearchResult> UseOrFindAppHostProjectFileAsync(F
selectedAppHost = multipleAppHostProjectsFoundBehavior switch
{
MultipleAppHostProjectsFoundBehavior.Throw => throw new ProjectLocatorException(ErrorStrings.MultipleProjectFilesFound),
MultipleAppHostProjectsFoundBehavior.Prompt => await interactionService.PromptForSelectionAsync(InteractionServiceStrings.SelectAppHostToUse, results.BuildableAppHost, projectFile => $"{projectFile.Name} ({Path.GetRelativePath(executionContext.WorkingDirectory.FullName, projectFile.FullName)})", cancellationToken),
MultipleAppHostProjectsFoundBehavior.Prompt => await interactionService.PromptForSelectionAsync(InteractionServiceStrings.SelectAppHostToUse, results.BuildableAppHost, projectFile => $"{projectFile.Name.EscapeMarkup()} ({Path.GetRelativePath(executionContext.WorkingDirectory.FullName, projectFile.FullName).EscapeMarkup()})", cancellationToken),
MultipleAppHostProjectsFoundBehavior.None => null,
_ => selectedAppHost
};
Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Cli/Projects/SolutionLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Aspire.Cli.Interaction;
using Aspire.Cli.Resources;
using Microsoft.Extensions.Logging;
using Spectre.Console;

namespace Aspire.Cli.Projects;

Expand Down Expand Up @@ -39,7 +40,7 @@ internal sealed class SolutionLocator(ILogger<SolutionLocator> logger, IInteract
var selectedSolution = await interactionService.PromptForSelectionAsync(
InitCommandStrings.MultipleSolutionsFound,
solutionFiles,
solutionFile => $"{solutionFile.Name} ({Path.GetRelativePath(startDirectory.FullName, solutionFile.FullName)})",
solutionFile => $"{solutionFile.Name.EscapeMarkup()} ({Path.GetRelativePath(startDirectory.FullName, solutionFile.FullName).EscapeMarkup()})",
cancellationToken);

return selectedSolution;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -547,4 +547,70 @@ public void DisplaySubtleMessage_WithMarkupCharacters_EscapesByDefault()
var outputString = output.ToString();
Assert.Contains("[Module]", outputString);
}

[Fact]
public void SelectionPrompt_ConverterPreservesIntentionalMarkup()
{
// Arrange - verifies that PromptForSelectionAsync does NOT escape the formatter output,
// allowing callers to include intentional Spectre markup (e.g., [bold]...[/]).
// This is a regression test for https://github.com/dotnet/aspire/pull/14422 where
// blanket EscapeMarkup() in the converter broke [bold] rendering in 'aspire add'.
var output = new StringBuilder();
var console = AnsiConsole.Create(new AnsiConsoleSettings
{
Ansi = AnsiSupport.Yes,
ColorSystem = ColorSystemSupport.Standard,
Out = new AnsiConsoleOutput(new StringWriter(output))
});

// Build a SelectionPrompt the same way ConsoleInteractionService does,
// using a formatter that returns intentional markup (like AddCommand does).
Func<string, string> choiceFormatter = item => $"[bold]{item}[/] (Aspire.Hosting.{item})";

var prompt = new SelectionPrompt<string>()
.Title("Select an integration:")
.UseConverter(choiceFormatter)
.AddChoices(["PostgreSQL", "Redis"]);

// Act - verify the converter output preserves the [bold] markup
// by checking that the converter is the formatter itself (not wrapped with EscapeMarkup)
var converterOutput = choiceFormatter("PostgreSQL");

// Assert - the formatter should produce raw markup, not escaped markup
Assert.Equal("[bold]PostgreSQL[/] (Aspire.Hosting.PostgreSQL)", converterOutput);
Assert.DoesNotContain("[[bold]]", converterOutput); // Must NOT be escaped
}

[Fact]
public void SelectionPrompt_ConverterWithBracketsInData_MustBeEscapedByCaller()
{
// Arrange - verifies that callers are responsible for escaping dynamic data
// that may contain bracket characters, while preserving intentional markup.
// This tests the pattern used by AddCommand.PackageNameWithFriendlyNameIfAvailable.
var output = new StringBuilder();
var console = AnsiConsole.Create(new AnsiConsoleSettings
{
Ansi = AnsiSupport.No,
ColorSystem = ColorSystemSupport.NoColors,
Out = new AnsiConsoleOutput(new StringWriter(output))
});

// Simulate a package name that contains brackets (e.g., from an external source)
var friendlyName = "Azure Storage [Preview]";
var packageId = "Aspire.Hosting.Azure.Storage";

// The formatter should escape dynamic values but preserve intentional markup
var formattedOutput = $"[bold]{friendlyName.EscapeMarkup()}[/] ({packageId.EscapeMarkup()})";

// Assert - intentional markup preserved, dynamic brackets escaped
Assert.Equal("[bold]Azure Storage [[Preview]][/] (Aspire.Hosting.Azure.Storage)", formattedOutput);

// Verify Spectre can render this without throwing
var exception = Record.Exception(() => console.MarkupLine(formattedOutput));
Assert.Null(exception);

var outputString = output.ToString();
Assert.Contains("Azure Storage [Preview]", outputString);
Assert.Contains("Aspire.Hosting.Azure.Storage", outputString);
}
}
Loading