Skip to content
6 changes: 6 additions & 0 deletions eng/Signing.props
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@
<FileSignInfo Include="MessagePack.dll" CertificateName="3PartySHA2" />
<FileSignInfo Include="MessagePack.Annotations.dll" CertificateName="3PartySHA2" />
<FileSignInfo Include="Spectre.Console.dll" CertificateName="3PartySHA2" />
<FileSignInfo Include="OpenTelemetry.dll" CertificateName="3PartySHA2" />
<FileSignInfo Include="OpenTelemetry.Api.dll" CertificateName="3PartySHA2" />
<FileSignInfo Include="OpenTelemetry.Api.ProviderBuilderExtensions.dll" CertificateName="3PartySHA2" />
<FileSignInfo Include="OpenTelemetry.Exporter.OpenTelemetryProtocol.dll" CertificateName="3PartySHA2" />
<FileSignInfo Include="OpenTelemetry.Extensions.Hosting.dll" CertificateName="3PartySHA2" />
<FileSignInfo Include="Semver.dll" CertificateName="3PartySHA2" />
</ItemGroup>

<ItemGroup>
Expand Down
16 changes: 8 additions & 8 deletions src/Aspire.Cli/Backchannel/AppHostBackchannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ namespace Aspire.Cli.Backchannel;

internal sealed class AppHostBackchannel(ILogger<AppHostBackchannel> logger, CliRpcTarget target)
{
private readonly ActivitySource _activitySource = new(nameof(Aspire.Cli.Backchannel.AppHostBackchannel), "1.0.0");
private readonly ActivitySource _activitySource = new(nameof(AppHostBackchannel));
private readonly TaskCompletionSource<JsonRpc> _rpcTaskCompletionSource = new();
private Process? _process;

public async Task<long> PingAsync(long timestamp, CancellationToken cancellationToken)
{
using var activity = _activitySource.StartActivity(nameof(PingAsync), ActivityKind.Client);
using var activity = _activitySource.StartActivity();

var rpc = await _rpcTaskCompletionSource.Task;

Expand All @@ -37,7 +37,7 @@ public async Task RequestStopAsync(CancellationToken cancellationToken)
// of the AppHost process. The AppHost process will then trigger the shutdown
// which will allow the CLI to await the pending run.

using var activity = _activitySource.StartActivity(nameof(RequestStopAsync), ActivityKind.Client);
using var activity = _activitySource.StartActivity();

var rpc = await _rpcTaskCompletionSource.Task;

Expand All @@ -51,7 +51,7 @@ await rpc.InvokeWithCancellationAsync(

public async Task<(string BaseUrlWithLoginToken, string? CodespacesUrlWithLoginToken)> GetDashboardUrlsAsync(CancellationToken cancellationToken)
{
using var activity = _activitySource.StartActivity(nameof(GetDashboardUrlsAsync), ActivityKind.Client);
using var activity = _activitySource.StartActivity();

var rpc = await _rpcTaskCompletionSource.Task;

Expand All @@ -67,7 +67,7 @@ await rpc.InvokeWithCancellationAsync(

public async IAsyncEnumerable<(string Resource, string Type, string State, string[] Endpoints)> GetResourceStatesAsync([EnumeratorCancellation]CancellationToken cancellationToken)
{
using var activity = _activitySource.StartActivity(nameof(GetResourceStatesAsync), ActivityKind.Client);
using var activity = _activitySource.StartActivity();

var rpc = await _rpcTaskCompletionSource.Task;

Expand All @@ -88,7 +88,7 @@ await rpc.InvokeWithCancellationAsync(

public async Task ConnectAsync(Process process, string socketPath, CancellationToken cancellationToken)
{
using var activity = _activitySource.StartActivity(nameof(ConnectAsync), ActivityKind.Client);
using var activity = _activitySource.StartActivity();

_process = process;

Expand All @@ -111,7 +111,7 @@ public async Task ConnectAsync(Process process, string socketPath, CancellationT

public async Task<string[]> GetPublishersAsync(CancellationToken cancellationToken)
{
using var activity = _activitySource.StartActivity(nameof(GetPublishersAsync), ActivityKind.Client);
using var activity = _activitySource.StartActivity();

var rpc = await _rpcTaskCompletionSource.Task.ConfigureAwait(false);

Expand All @@ -127,7 +127,7 @@ public async Task<string[]> GetPublishersAsync(CancellationToken cancellationTok

public async IAsyncEnumerable<(string Id, string StatusText, bool IsComplete, bool IsError)> GetPublishingActivitiesAsync([EnumeratorCancellation]CancellationToken cancellationToken)
{
using var activity = _activitySource.StartActivity(nameof(GetPublishingActivitiesAsync), ActivityKind.Client);
using var activity = _activitySource.StartActivity();

var rpc = await _rpcTaskCompletionSource.Task;

Expand Down
22 changes: 14 additions & 8 deletions src/Aspire.Cli/Commands/AddCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,42 +11,48 @@ namespace Aspire.Cli.Commands;

internal sealed class AddCommand : BaseCommand
{
private readonly ActivitySource _activitySource = new ActivitySource("Aspire.Cli");
private readonly ActivitySource _activitySource = new ActivitySource(nameof(AddCommand));
private readonly DotNetCliRunner _runner;
private readonly INuGetPackageCache _nuGetPackageCache;

public AddCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache) : base("add", "Add an integration or other resource to the Aspire project.")
public AddCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache)
: base("add", "Add an integration to the Aspire project.")
{
ArgumentNullException.ThrowIfNull(runner, nameof(runner));
ArgumentNullException.ThrowIfNull(nuGetPackageCache, nameof(nuGetPackageCache));
_runner = runner;
_nuGetPackageCache = nuGetPackageCache;

var resourceArgument = new Argument<string>("resource");
resourceArgument.Arity = ArgumentArity.ZeroOrOne;
Arguments.Add(resourceArgument);
var integrationArgument = new Argument<string>("integration");
integrationArgument.Description = "The name of the integration to add (e.g. redis, postgres).";
integrationArgument.Arity = ArgumentArity.ZeroOrOne;
Arguments.Add(integrationArgument);

var projectOption = new Option<FileInfo?>("--project");
projectOption.Description = "The path to the project file to add the integration to.";
projectOption.Validators.Add(ProjectFileHelper.ValidateProjectOption);
Options.Add(projectOption);

var versionOption = new Option<string>("--version", "-v");
versionOption.Description = "The version of the integration to add.";
Options.Add(versionOption);

var prereleaseOption = new Option<bool>("--prerelease");
prereleaseOption.Description = "Include pre-release versions of the integration when searching.";
Options.Add(prereleaseOption);

var sourceOption = new Option<string?>("--source", "-s");
sourceOption.Description = "The NuGet source to use for the integration.";
Options.Add(sourceOption);
}

protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
using var activity = _activitySource.StartActivity($"{nameof(ExecuteAsync)}", ActivityKind.Internal);
using var activity = _activitySource.StartActivity();

try
{
var integrationName = parseResult.GetValue<string>("resource");
var integrationName = parseResult.GetValue<string>("integration");

var passedAppHostProjectFile = parseResult.GetValue<FileInfo?>("--project");
var effectiveAppHostProjectFile = ProjectFileHelper.UseOrFindAppHostProjectFile(passedAppHostProjectFile);
Expand All @@ -62,7 +68,7 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell

var packages = await AnsiConsole.Status().StartAsync(
"Searching for Aspire packages...",
context => _nuGetPackageCache.GetPackagesAsync(effectiveAppHostProjectFile, prerelease, source, cancellationToken)
context => _nuGetPackageCache.GetPackagesAsync(effectiveAppHostProjectFile.Directory!, prerelease, source, cancellationToken)
);

var version = parseResult.GetValue<string?>("--version");
Expand Down
148 changes: 86 additions & 62 deletions src/Aspire.Cli/Commands/NewCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,50 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.CommandLine;
using System.CommandLine.Parsing;
using System.Diagnostics;
using Aspire.Cli.Utils;
using Semver;
using Spectre.Console;

namespace Aspire.Cli.Commands;

internal sealed class NewCommand : BaseCommand
{
private readonly ActivitySource _activitySource = new ActivitySource("Aspire.Cli");
private readonly ActivitySource _activitySource = new ActivitySource(nameof(NewCommand));
private readonly DotNetCliRunner _runner;
private readonly INuGetPackageCache _nuGetPackageCache;

public NewCommand(DotNetCliRunner runner) : base("new", "Create a new Aspire sample project.")
public NewCommand(DotNetCliRunner runner, INuGetPackageCache nuGetPackageCache)
: base("new", "Create a new Aspire sample project.")
{
ArgumentNullException.ThrowIfNull(runner, nameof(runner));
ArgumentNullException.ThrowIfNull(nuGetPackageCache, nameof(nuGetPackageCache));
_runner = runner;
_nuGetPackageCache = nuGetPackageCache;

var templateArgument = new Argument<string>("template");
templateArgument.Validators.Add(ValidateProjectTemplate);
templateArgument.Description = "The name of the project template to use (e.g. aspire-starter, aspire).";
templateArgument.Arity = ArgumentArity.ZeroOrOne;
Arguments.Add(templateArgument);

var nameOption = new Option<string>("--name", "-n");
nameOption.Description = "The name of the project to create.";
Options.Add(nameOption);

var outputOption = new Option<string?>("--output", "-o");
outputOption.Description = "The output path for the project.";
Options.Add(outputOption);

var prereleaseOption = new Option<bool>("--prerelease");
Options.Add(prereleaseOption);

var sourceOption = new Option<string?>("--source", "-s");
sourceOption.Description = "The NuGet source to use for the project templates.";
Options.Add(sourceOption);

var templateVersionOption = new Option<string?>("--version", "-v");
templateVersionOption.Description = "The version of the project templates to use.";
Options.Add(templateVersionOption);
}

private static void ValidateProjectTemplate(ArgumentResult result)
private static async Task<(string TemplateName, string TemplateDescription, string? PathAppendage)> GetProjectTemplateAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
// TODO: We need to integrate with the template engine to interrogate
// the list of available templates. For now we will just hard-code
Expand All @@ -49,55 +54,91 @@ private static void ValidateProjectTemplate(ArgumentResult result)
// Once we integrate with template engine we will also be able to
// interrogate the various options and add them. For now we will
// keep it simple.
string[] validTemplates = [
"aspire-starter",
"aspire",
"aspire-apphost",
"aspire-servicedefaults",
"aspire-mstest",
"aspire-nunit",
"aspire-xunit"
(string TemplateName, string TemplateDescription, string? PathAppendage)[] validTemplates = [
("aspire-starter", "Aspire Starter App", "src") ,
("aspire", "Aspire Empty App", "src"),
("aspire-apphost", "Aspire App Host", null),
("aspire-servicedefaults", "Aspire Service Defaults", null),
("aspire-mstest", "Aspire Test Project (MSTest)", null),
("aspire-nunit", "Aspire Test Project (NUnit)", null),
("aspire-xunit", "Aspire Test Project (xUnit)", null)
];

var value = result.GetValueOrDefault<string>();

if (value is null)
if (parseResult.GetValue<string?>("template") is { } templateName && validTemplates.SingleOrDefault(t => t.TemplateName == templateName) is { } template)
{
// This is OK, for now we will use the default
// template of aspire-starter, but we might
// be able to do more intelligent selection in the
// future based on what is already in the working directory.
return;
return template;
}

if (value is { } templateName && !validTemplates.Contains(templateName))
else
{
result.AddError($"The specified template '{templateName}' is not valid. Valid templates are [{string.Join(", ", validTemplates)}].");
return;
return await PromptUtils.PromptForSelectionAsync(
"Select a project template:",
validTemplates,
t => $"{t.TemplateName} ({t.TemplateDescription})",
cancellationToken
);
}
}

protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
private static async Task<string> GetProjectNameAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
using var activity = _activitySource.StartActivity($"{nameof(ExecuteAsync)}", ActivityKind.Internal);
if (parseResult.GetValue<string>("--name") is not { } name)
{
var defaultName = new DirectoryInfo(Environment.CurrentDirectory).Name;
name = await PromptUtils.PromptForStringAsync("Enter the project name:",
defaultValue: defaultName,
cancellationToken: cancellationToken);
}

var templateVersion = parseResult.GetValue<string>("--version");
var prerelease = parseResult.GetValue<bool>("--prerelease");
return name;
}

if (templateVersion is not null && prerelease)
private static async Task<string> GetOutputPathAsync(ParseResult parseResult, string? pathAppendage, CancellationToken cancellationToken)
{
if (parseResult.GetValue<string>("--output") is not { } outputPath)
{
AnsiConsole.MarkupLine("[red bold]:thumbs_down: The --version and --prerelease options are mutually exclusive.[/]");
return ExitCodeConstants.FailedToCreateNewProject;
outputPath = await PromptUtils.PromptForStringAsync(
"Enter the output path:",
defaultValue: Path.Combine(Environment.CurrentDirectory, pathAppendage ?? string.Empty),
cancellationToken: cancellationToken
);
}
else if (prerelease)

return Path.GetFullPath(outputPath);
}

private static async Task<string> GetProjectTemplatesVersionAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
if (parseResult.GetValue<string>("--version") is { } version)
{
templateVersion = "*-*";
return version;
}
else if (templateVersion is null)
else
{
templateVersion = VersionHelper.GetDefaultTemplateVersion();
version = await PromptUtils.PromptForStringAsync(
"Project templates version:",
defaultValue: VersionHelper.GetDefaultTemplateVersion(),
validator: (string value) => {
if (SemVersion.TryParse(value, out var parsedVersion))
{
return ValidationResult.Success();
}

return ValidationResult.Error("Invalid version format. Please enter a valid version.");
},
cancellationToken);

return version;
}
}

protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
using var activity = _activitySource.StartActivity();

var template = await GetProjectTemplateAsync(parseResult, cancellationToken);
var name = await GetProjectNameAsync(parseResult, cancellationToken);
var outputPath = await GetOutputPathAsync(parseResult, template.PathAppendage, cancellationToken);
var version = await GetProjectTemplatesVersionAsync(parseResult, cancellationToken);
var source = parseResult.GetValue<string?>("--source");

var templateInstallResult = await AnsiConsole.Status()
Expand All @@ -106,7 +147,7 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
.StartAsync(
":ice: Getting latest templates...",
async context => {
return await _runner.InstallTemplateAsync("Aspire.ProjectTemplates", templateVersion!, source, true, cancellationToken);
return await _runner.InstallTemplateAsync("Aspire.ProjectTemplates", version, source, true, cancellationToken);
});

if (templateInstallResult.ExitCode != 0)
Expand All @@ -117,35 +158,18 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell

AnsiConsole.MarkupLine($":package: Using project templates version: {templateInstallResult.TemplateVersion}");

var templateName = parseResult.GetValue<string>("template") ?? "aspire-starter";

if (parseResult.GetValue<string>("--output") is not { } outputPath)
{
outputPath = Environment.CurrentDirectory;
}
else
{
outputPath = Path.GetFullPath(outputPath);
}

if (parseResult.GetValue<string>("--name") is not { } name)
{
var outputPathDirectoryInfo = new DirectoryInfo(outputPath);
name = outputPathDirectoryInfo.Name;
}

int newProjectExitCode = await AnsiConsole.Status()
.Spinner(Spinner.Known.Dots3)
.SpinnerStyle(Style.Parse("purple"))
.StartAsync(
":rocket: Creating new Aspire project...",
async context => {
return await _runner.NewProjectAsync(
templateName,
name,
outputPath,
cancellationToken);
});
template.TemplateName,
name,
outputPath,
cancellationToken);
});

if (newProjectExitCode != 0)
{
Expand Down
Loading