diff --git a/src/Aspire.Cli/Aspire.Cli.csproj b/src/Aspire.Cli/Aspire.Cli.csproj
index 109d9009369..26a7cfa4974 100644
--- a/src/Aspire.Cli/Aspire.Cli.csproj
+++ b/src/Aspire.Cli/Aspire.Cli.csproj
@@ -76,6 +76,7 @@
+
@@ -97,6 +98,7 @@
+
diff --git a/src/Aspire.Cli/Commands/DashboardCommand.cs b/src/Aspire.Cli/Commands/DashboardCommand.cs
new file mode 100644
index 00000000000..3cd8bd3b32c
--- /dev/null
+++ b/src/Aspire.Cli/Commands/DashboardCommand.cs
@@ -0,0 +1,438 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.CommandLine;
+using System.Globalization;
+using Aspire.Cli.Configuration;
+using Aspire.Cli.Diagnostics;
+using Aspire.Cli.DotNet;
+using Aspire.Cli.Interaction;
+using Aspire.Cli.Layout;
+using Aspire.Cli.Resources;
+using Aspire.Cli.Telemetry;
+using Aspire.Cli.Utils;
+using Aspire.Hosting;
+using Microsoft.Extensions.Logging;
+using Spectre.Console;
+
+namespace Aspire.Cli.Commands;
+
+///
+/// Command that starts a standalone Aspire Dashboard instance.
+///
+internal sealed class DashboardCommand : BaseCommand
+{
+ internal override HelpGroup HelpGroup => HelpGroup.Monitoring;
+
+ private readonly IInteractionService _interactionService;
+ private readonly ILayoutDiscovery _layoutDiscovery;
+ private readonly LayoutProcessRunner _layoutProcessRunner;
+ private readonly FileLoggerProvider _fileLoggerProvider;
+ private readonly ILogger _logger;
+
+ private static readonly Option s_frontendUrlOption = new("--frontend-url")
+ {
+ Description = DashboardCommandStrings.FrontendUrlOptionDescription
+ };
+
+ private static readonly Option s_otlpGrpcUrlOption = new("--otlp-grpc-url")
+ {
+ Description = DashboardCommandStrings.OtlpGrpcUrlOptionDescription
+ };
+
+ private static readonly Option s_otlpHttpUrlOption = new("--otlp-http-url")
+ {
+ Description = DashboardCommandStrings.OtlpHttpUrlOptionDescription
+ };
+
+ private static readonly Option s_allowAnonymousOption = new("--allow-anonymous")
+ {
+ Description = DashboardCommandStrings.AllowAnonymousOptionDescription
+ };
+
+ private static readonly Option s_configFilePathOption = new("--config-file-path")
+ {
+ Description = DashboardCommandStrings.ConfigFilePathOptionDescription
+ };
+
+ public DashboardCommand(
+ IInteractionService interactionService,
+ ILayoutDiscovery layoutDiscovery,
+ LayoutProcessRunner layoutProcessRunner,
+ FileLoggerProvider fileLoggerProvider,
+ IFeatures features,
+ ICliUpdateNotifier updateNotifier,
+ CliExecutionContext executionContext,
+ ILogger logger,
+ AspireCliTelemetry telemetry)
+ : base("dashboard", DashboardCommandStrings.Description, features, updateNotifier, executionContext, interactionService, telemetry)
+ {
+ _interactionService = interactionService;
+ _layoutDiscovery = layoutDiscovery;
+ _layoutProcessRunner = layoutProcessRunner;
+ _fileLoggerProvider = fileLoggerProvider;
+ _logger = logger;
+
+ Options.Add(s_frontendUrlOption);
+ Options.Add(s_otlpGrpcUrlOption);
+ Options.Add(s_otlpHttpUrlOption);
+ Options.Add(s_allowAnonymousOption);
+ Options.Add(s_configFilePathOption);
+ TreatUnmatchedTokensAsErrors = false;
+ }
+
+ protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
+ {
+ var layout = _layoutDiscovery.DiscoverLayout();
+ if (layout is null)
+ {
+ _interactionService.DisplayError(DashboardCommandStrings.BundleLayoutNotFound);
+ return ExitCodeConstants.DashboardFailure;
+ }
+
+ var managedPath = layout.GetManagedPath();
+ if (managedPath is null || !File.Exists(managedPath))
+ {
+ _interactionService.DisplayError(DashboardCommandStrings.ManagedBinaryNotFound);
+ return ExitCodeConstants.DashboardFailure;
+ }
+
+ var dashboardArgs = new List { "dashboard" };
+
+ // Build args from typed options. These are added before unmatched tokens
+ // so that raw pass-through arguments (unmatched tokens) take precedence.
+ var unmatchedTokens = parseResult.UnmatchedTokens;
+ var allowAnonymous = parseResult.GetValue(s_allowAnonymousOption);
+ AddOptionArgs(parseResult, dashboardArgs, unmatchedTokens, ExecutionContext);
+
+ // Set a browser token for frontend auth unless anonymous access is enabled.
+ // The token is passed via environment variable (not command-line arg) to
+ // avoid exposing it in process listings (e.g. ps, Task Manager).
+ string? browserToken = null;
+ Dictionary? environmentVariables = null;
+ if (!allowAnonymous && !ConfigSettingHasValue(unmatchedTokens, ExecutionContext, "ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS"))
+ {
+ if (!ConfigSettingHasValue(unmatchedTokens, ExecutionContext, "DASHBOARD__FRONTEND__BROWSERTOKEN"))
+ {
+ browserToken = TokenGenerator.GenerateToken();
+ environmentVariables = new Dictionary
+ {
+ ["DASHBOARD__FRONTEND__BROWSERTOKEN"] = browserToken
+ };
+ }
+ }
+
+ dashboardArgs.AddRange(unmatchedTokens);
+
+ // Resolve URLs for the summary display.
+ var dashboardInfo = ResolveDashboardInfo(dashboardArgs, unmatchedTokens, ExecutionContext, browserToken);
+
+ return await ExecuteForegroundAsync(managedPath, dashboardArgs, dashboardInfo, environmentVariables, cancellationToken).ConfigureAwait(false);
+ }
+
+ private static void AddOptionArgs(ParseResult parseResult, List args, IReadOnlyList unmatchedTokens, CliExecutionContext executionContext)
+ {
+ AddStringOptionArg(parseResult, args, unmatchedTokens, executionContext, s_frontendUrlOption, "ASPNETCORE_URLS", defaultValue: "http://localhost:18888");
+ AddStringOptionArg(parseResult, args, unmatchedTokens, executionContext, s_otlpGrpcUrlOption, "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL", defaultValue: "http://localhost:4317");
+ AddStringOptionArg(parseResult, args, unmatchedTokens, executionContext, s_otlpHttpUrlOption, "ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL", defaultValue: "http://localhost:4318");
+ AddBoolOptionArg(parseResult, args, unmatchedTokens, executionContext, s_allowAnonymousOption, "ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS");
+ AddStringOptionArg(parseResult, args, unmatchedTokens, executionContext, s_configFilePathOption, "ASPIRE_DASHBOARD_CONFIG_FILE_PATH", defaultValue: null);
+ }
+
+ private static void AddStringOptionArg(ParseResult parseResult, List args, IReadOnlyList unmatchedTokens,
+ CliExecutionContext executionContext, Option option, string envVarName, string? defaultValue)
+ {
+ if (ConfigSettingHasValue(unmatchedTokens, executionContext, envVarName))
+ {
+ return;
+ }
+
+ var value = parseResult.GetResult(option) is not null
+ ? parseResult.GetValue(option)
+ : defaultValue;
+
+ if (value is not null)
+ {
+ args.Add($"--{envVarName}={value}");
+ }
+ }
+
+ private static void AddBoolOptionArg(ParseResult parseResult, List args, IReadOnlyList unmatchedTokens,
+ CliExecutionContext executionContext, Option option, string envVarName)
+ {
+ if (ConfigSettingHasValue(unmatchedTokens, executionContext, envVarName))
+ {
+ return;
+ }
+
+ var result = parseResult.GetResult(option);
+
+ // Skip when the result comes from the option's default value rather than
+ // explicit user input. Without the Implicit check, Option defaults
+ // (e.g. false) would always emit "--ALLOW_ANONYMOUS=false" even when the
+ // user never specified --allow-anonymous.
+ if (result is not null && !result.Implicit)
+ {
+ var value = parseResult.GetValue(option);
+ args.Add($"--{envVarName}={value.ToString().ToLowerInvariant()}");
+ }
+ }
+
+ internal static bool ConfigSettingHasValue(IReadOnlyList unmatchedTokens, CliExecutionContext executionContext, string envVarName)
+ {
+ // Check if already provided via unmatched tokens.
+ var prefix = $"--{envVarName}=";
+ for (var i = 0; i < unmatchedTokens.Count; i++)
+ {
+ if (unmatchedTokens[i].StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ // Also handle bare "--KEY" (boolean flag) or "--KEY value" (space-separated) forms.
+ // A bare key is treated as present because it will be forwarded to the
+ // child process via AddRange(unmatchedTokens), which uses last-wins
+ // semantics. For booleans (e.g. "--ALLOW_ANONYMOUS") it means true;
+ // for strings the bare key overrides our default regardless.
+ if (unmatchedTokens[i].Equals($"--{envVarName}", StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ // Check if already set as an environment variable.
+ if (executionContext.GetEnvironmentVariable(envVarName) is not null)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ internal static DashboardInfo ResolveDashboardInfo(List dashboardArgs, IReadOnlyList unmatchedTokens, CliExecutionContext executionContext, string? browserToken)
+ {
+ var frontendUrl = ResolveSettingValue(dashboardArgs, unmatchedTokens, executionContext, "ASPNETCORE_URLS") ?? "http://localhost:18888";
+ var otlpGrpcUrl = ResolveSettingValue(dashboardArgs, unmatchedTokens, executionContext, "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL") ?? "http://localhost:4317";
+ var otlpHttpUrl = ResolveSettingValue(dashboardArgs, unmatchedTokens, executionContext, "ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL") ?? "http://localhost:4318";
+
+ // Take the first URL if multiple are specified (semicolon-separated).
+ var parts = frontendUrl.Split(';', StringSplitOptions.RemoveEmptyEntries);
+ var firstUrl = parts.Length > 0 ? parts[0].TrimEnd('/') : "http://localhost:18888";
+
+ var dashboardUrl = browserToken is not null
+ ? $"{firstUrl}/login?t={browserToken}"
+ : firstUrl;
+
+ return new DashboardInfo(dashboardUrl, otlpGrpcUrl, otlpHttpUrl);
+ }
+
+ ///
+ /// Resolves a setting value by checking, in order: args (--KEY=value), unmatched tokens
+ /// (--KEY value with space separator), and environment variables.
+ ///
+ internal static string? ResolveSettingValue(List args, IReadOnlyList unmatchedTokens, CliExecutionContext executionContext, string key)
+ {
+ // First check --KEY=value in args (last-wins).
+ var result = ResolveArgValue(args, key);
+ if (result is not null)
+ {
+ return result;
+ }
+
+ // Check unmatched tokens for space-separated form: --KEY value
+ var bareKey = $"--{key}";
+ for (var i = 0; i < unmatchedTokens.Count; i++)
+ {
+ if (unmatchedTokens[i].Equals(bareKey, StringComparison.OrdinalIgnoreCase) && i + 1 < unmatchedTokens.Count)
+ {
+ return unmatchedTokens[i + 1];
+ }
+ }
+
+ // Fall back to environment variable.
+ return executionContext.GetEnvironmentVariable(key);
+ }
+
+ internal static string? ResolveArgValue(List args, string key)
+ {
+ // Scan for --KEY=value (last-wins).
+ string? result = null;
+ var prefix = $"--{key}=";
+ foreach (var arg in args)
+ {
+ if (arg.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ result = arg.Substring(prefix.Length);
+ }
+ }
+
+ return result;
+ }
+
+ internal sealed record DashboardInfo(string DashboardUrl, string OtlpGrpcUrl, string OtlpHttpUrl);
+
+ private static string GetExitCodeMessage(int exitCode)
+ {
+ return exitCode switch
+ {
+ DashboardExitCodes.UnexpectedError => DashboardCommandStrings.DashboardExitedUnexpectedError,
+ DashboardExitCodes.ValidationFailure => DashboardCommandStrings.DashboardExitedValidationFailure,
+ DashboardExitCodes.AddressInUse => DashboardCommandStrings.DashboardExitedAddressInUse,
+ _ => string.Format(CultureInfo.CurrentCulture, DashboardCommandStrings.DashboardExitedWithError, exitCode),
+ };
+ }
+
+ private void RenderDashboardSummary(DashboardInfo info, string logFilePath)
+ {
+ _interactionService.DisplayEmptyLine();
+ var grid = new Grid();
+ grid.AddColumn();
+ grid.AddColumn();
+
+ var dashboardLabel = DashboardCommandStrings.DashboardLabel;
+ var otlpGrpcLabel = DashboardCommandStrings.OtlpGrpcLabel;
+ var otlpHttpLabel = DashboardCommandStrings.OtlpHttpLabel;
+ var logsLabel = DashboardCommandStrings.LogsLabel;
+
+ var labels = new List { dashboardLabel, otlpGrpcLabel, otlpHttpLabel, logsLabel };
+
+ var longestLabelLength = labels.Max(s => s.Length) + 1; // +1 for colon
+ grid.Columns[0].Width = longestLabelLength;
+
+ // Dashboard row
+ var escapedDashboardUrl = Markup.Escape(info.DashboardUrl);
+ grid.AddRow(
+ new Align(new Markup($"[bold green]{dashboardLabel}[/]:"), HorizontalAlignment.Right),
+ new Markup($"[link={escapedDashboardUrl}]{escapedDashboardUrl}[/]"));
+ grid.AddRow(Text.Empty, Text.Empty);
+
+ // OTLP gRPC row
+ grid.AddRow(
+ new Align(new Markup($"[bold green]{otlpGrpcLabel}[/]:"), HorizontalAlignment.Right),
+ new Text(info.OtlpGrpcUrl));
+ grid.AddRow(Text.Empty, Text.Empty);
+
+ // OTLP HTTP row
+ grid.AddRow(
+ new Align(new Markup($"[bold green]{otlpHttpLabel}[/]:"), HorizontalAlignment.Right),
+ new Text(info.OtlpHttpUrl));
+ grid.AddRow(Text.Empty, Text.Empty);
+
+ // Logs row
+ grid.AddRow(
+ new Align(new Markup($"[bold green]{logsLabel}[/]:"), HorizontalAlignment.Right),
+ new Text(logFilePath));
+
+ var padder = new Padder(grid, new Padding(3, 0));
+ _interactionService.DisplayRenderable(padder);
+ }
+
+ private async Task ExecuteForegroundAsync(string managedPath, List dashboardArgs, DashboardInfo dashboardInfo, IDictionary? environmentVariables, CancellationToken cancellationToken)
+ {
+ _logger.LogDebug("Starting dashboard in foreground: {ManagedPath}", managedPath);
+
+ var outputCollector = new OutputCollector(_fileLoggerProvider, "Dashboard");
+ var readyTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ var options = new ProcessInvocationOptions
+ {
+ StandardOutputCallback = line =>
+ {
+ outputCollector.AppendOutput(line);
+
+ // The dashboard writes "Now listening on: {urls}" when it's ready to accept requests.
+ // Wait for that message before showing the dashboard URL to the user.
+ // This message isn't localized, so we can reliably look for it in the output regardless of the user's language/locale.
+ if (line.Contains("Now listening on:", StringComparison.OrdinalIgnoreCase))
+ {
+ readyTcs.TrySetResult();
+ }
+ },
+ StandardErrorCallback = line =>
+ {
+ outputCollector.AppendError(line);
+ },
+ };
+
+ using var process = _layoutProcessRunner.Start(managedPath, dashboardArgs, environmentVariables: environmentVariables, options: options);
+
+ // Wait for the dashboard to become ready, the process to exit, or a timeout.
+ var processExitTask = process.WaitForExitAsync(cancellationToken);
+ var readyOrFailed = Task.WhenAny(readyTcs.Task, processExitTask);
+
+ var completedTask = await _interactionService.ShowStatusAsync(
+ DashboardCommandStrings.StartingDashboard,
+ async () =>
+ {
+ using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+ using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
+
+ try
+ {
+ return await readyOrFailed.WaitAsync(linkedCts.Token).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
+ {
+ // Timeout — return the processExitTask so the caller detects it wasn't the ready signal.
+ return processExitTask;
+ }
+ });
+
+ if (cancellationToken.IsCancellationRequested)
+ {
+ _interactionService.DisplayMessage(KnownEmojis.StopSign, $"[teal bold]{DashboardCommandStrings.StoppingDashboard}[/]", allowMarkup: true);
+
+ if (!process.HasExited)
+ {
+ process.Kill(entireProcessTree: true);
+ }
+
+ return ExitCodeConstants.Success;
+ }
+
+ if (completedTask != readyTcs.Task)
+ {
+ // Dashboard didn't become ready — either it exited or timed out.
+ var exitMessage = process.HasExited
+ ? GetExitCodeMessage(process.ExitCode)
+ : DashboardCommandStrings.DashboardStartTimedOut;
+
+ _interactionService.DisplayError(exitMessage);
+ _interactionService.DisplayMessage(KnownEmojis.PageFacingUp, string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.SeeLogsAt, ExecutionContext.LogFilePath));
+
+ if (!process.HasExited)
+ {
+ process.Kill(entireProcessTree: true);
+ }
+
+ return ExitCodeConstants.DashboardFailure;
+ }
+
+ // Dashboard is ready.
+ RenderDashboardSummary(dashboardInfo, ExecutionContext.LogFilePath);
+ _interactionService.DisplayEmptyLine();
+
+ try
+ {
+ await processExitTask.ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ _interactionService.DisplayMessage(KnownEmojis.StopSign, $"[teal bold]{DashboardCommandStrings.StoppingDashboard}[/]", allowMarkup: true);
+
+ if (!process.HasExited)
+ {
+ process.Kill(entireProcessTree: true);
+ }
+
+ return ExitCodeConstants.Success;
+ }
+
+ if (process.ExitCode != 0)
+ {
+ _interactionService.DisplayError(GetExitCodeMessage(process.ExitCode));
+ }
+
+ return process.ExitCode == 0 ? ExitCodeConstants.Success : ExitCodeConstants.DashboardFailure;
+ }
+}
diff --git a/src/Aspire.Cli/Commands/ExecCommand.cs b/src/Aspire.Cli/Commands/ExecCommand.cs
index ba87ea2e045..a22e19eadb5 100644
--- a/src/Aspire.Cli/Commands/ExecCommand.cs
+++ b/src/Aspire.Cli/Commands/ExecCommand.cs
@@ -147,7 +147,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell
return ExitCodeConstants.FailedToDotnetRunAppHost;
}
- var runOptions = new DotNetCliRunnerInvocationOptions
+ var runOptions = new ProcessInvocationOptions
{
StandardOutputCallback = runOutputCollector.AppendOutput,
StandardErrorCallback = runOutputCollector.AppendError,
diff --git a/src/Aspire.Cli/Commands/InitCommand.cs b/src/Aspire.Cli/Commands/InitCommand.cs
index b83f3814977..62c36a2e069 100644
--- a/src/Aspire.Cli/Commands/InitCommand.cs
+++ b/src/Aspire.Cli/Commands/InitCommand.cs
@@ -212,7 +212,7 @@ private async Task InitializeExistingSolutionAsync(InitContext initContext,
initContext.GetSolutionProjectsOutputCollector = new OutputCollector();
var (getSolutionExitCode, solutionProjects) = await InteractionService.ShowStatusAsync("Reading solution...", async () =>
{
- var options = new DotNetCliRunnerInvocationOptions
+ var options = new ProcessInvocationOptions
{
StandardOutputCallback = initContext.GetSolutionProjectsOutputCollector.AppendOutput,
StandardErrorCallback = initContext.GetSolutionProjectsOutputCollector.AppendError
@@ -356,7 +356,7 @@ await nugetConfigPrompter.PromptToCreateOrUpdateAsync(
"Getting templates...",
async () =>
{
- var options = new DotNetCliRunnerInvocationOptions
+ var options = new ProcessInvocationOptions
{
StandardOutputCallback = initContext.InstallTemplateOutputCollector.AppendOutput,
StandardErrorCallback = initContext.InstallTemplateOutputCollector.AppendError
@@ -384,7 +384,7 @@ await nugetConfigPrompter.PromptToCreateOrUpdateAsync(
"Creating Aspire projects from template...",
async () =>
{
- var options = new DotNetCliRunnerInvocationOptions
+ var options = new ProcessInvocationOptions
{
StandardOutputCallback = initContext.NewProjectOutputCollector.AppendOutput,
StandardErrorCallback = initContext.NewProjectOutputCollector.AppendError
@@ -439,7 +439,7 @@ await nugetConfigPrompter.PromptToCreateOrUpdateAsync(
InitCommandStrings.AddingAppHostProjectToSolution,
async () =>
{
- var options = new DotNetCliRunnerInvocationOptions
+ var options = new ProcessInvocationOptions
{
StandardOutputCallback = initContext.AddAppHostToSolutionOutputCollector.AppendOutput,
StandardErrorCallback = initContext.AddAppHostToSolutionOutputCollector.AppendError
@@ -465,7 +465,7 @@ await nugetConfigPrompter.PromptToCreateOrUpdateAsync(
InitCommandStrings.AddingServiceDefaultsProjectToSolution,
async () =>
{
- var options = new DotNetCliRunnerInvocationOptions
+ var options = new ProcessInvocationOptions
{
StandardOutputCallback = initContext.AddServiceDefaultsToSolutionOutputCollector.AppendOutput,
StandardErrorCallback = initContext.AddServiceDefaultsToSolutionOutputCollector.AppendError
@@ -497,7 +497,7 @@ await nugetConfigPrompter.PromptToCreateOrUpdateAsync(
var addRefResult = await InteractionService.ShowStatusAsync(
$"Adding {project.ProjectFile.Name} to AppHost...", async () =>
{
- var options = new DotNetCliRunnerInvocationOptions
+ var options = new ProcessInvocationOptions
{
StandardOutputCallback = outputCollector.AppendOutput,
StandardErrorCallback = outputCollector.AppendError
@@ -531,7 +531,7 @@ await nugetConfigPrompter.PromptToCreateOrUpdateAsync(
var addRefResult = await InteractionService.ShowStatusAsync(
$"Adding ServiceDefaults reference to {project.ProjectFile.Name}...", async () =>
{
- var options = new DotNetCliRunnerInvocationOptions
+ var options = new ProcessInvocationOptions
{
StandardOutputCallback = outputCollector.AppendOutput,
StandardErrorCallback = outputCollector.AppendError
@@ -638,7 +638,7 @@ private async Task EvaluateSolutionProjectsAsync(InitContext initContext, Cancel
foreach (var project in initContext.SolutionProjects)
{
- var options = new DotNetCliRunnerInvocationOptions
+ var options = new ProcessInvocationOptions
{
StandardOutputCallback = initContext.EvaluateSolutionProjectsOutputCollector.AppendOutput,
StandardErrorCallback = initContext.EvaluateSolutionProjectsOutputCollector.AppendError
diff --git a/src/Aspire.Cli/Commands/RestoreCommand.cs b/src/Aspire.Cli/Commands/RestoreCommand.cs
index 6f780ac53fe..41240817f59 100644
--- a/src/Aspire.Cli/Commands/RestoreCommand.cs
+++ b/src/Aspire.Cli/Commands/RestoreCommand.cs
@@ -96,7 +96,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell
var restoreExitCode = await _interactionService.ShowStatusAsync(
RestoreCommandStrings.RestoringSdkCode,
- async () => await _runner.RestoreAsync(effectiveAppHostFile, new DotNetCliRunnerInvocationOptions(), cancellationToken),
+ async () => await _runner.RestoreAsync(effectiveAppHostFile, new ProcessInvocationOptions(), cancellationToken),
emoji: KnownEmojis.Gear);
if (restoreExitCode == 0)
diff --git a/src/Aspire.Cli/Commands/RootCommand.cs b/src/Aspire.Cli/Commands/RootCommand.cs
index 38b24cf00df..7a0c06743f8 100644
--- a/src/Aspire.Cli/Commands/RootCommand.cs
+++ b/src/Aspire.Cli/Commands/RootCommand.cs
@@ -131,6 +131,7 @@ public RootCommand(
AgentCommand agentCommand,
TelemetryCommand telemetryCommand,
ExportCommand exportCommand,
+ DashboardCommand dashboardCommand,
DocsCommand docsCommand,
SecretCommand secretCommand,
SdkCommand sdkCommand,
@@ -222,6 +223,7 @@ public RootCommand(
Subcommands.Add(telemetryCommand);
Subcommands.Add(exportCommand);
Subcommands.Add(docsCommand);
+ Subcommands.Add(dashboardCommand);
Subcommands.Add(secretCommand);
#if DEBUG
diff --git a/src/Aspire.Cli/DotNet/DotNetCliExecutionFactory.cs b/src/Aspire.Cli/DotNet/DotNetCliExecutionFactory.cs
deleted file mode 100644
index fbd85db507e..00000000000
--- a/src/Aspire.Cli/DotNet/DotNetCliExecutionFactory.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Diagnostics;
-using System.Globalization;
-using System.Runtime.InteropServices;
-using Aspire.Hosting;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-
-namespace Aspire.Cli.DotNet;
-
-internal sealed class DotNetCliExecutionFactory(
- ILogger logger,
- IConfiguration configuration,
- CliExecutionContext executionContext) : IDotNetCliExecutionFactory
-{
- internal static int GetCurrentProcessId() => Environment.ProcessId;
-
- internal static long GetCurrentProcessStartTimeUnixSeconds()
- {
- var startTime = Process.GetCurrentProcess().StartTime;
- return ((DateTimeOffset)startTime).ToUnixTimeSeconds();
- }
-
- public IDotNetCliExecution CreateExecution(string[] args, IDictionary? env, DirectoryInfo workingDirectory, DotNetCliRunnerInvocationOptions options)
- {
- var suppressLogging = options.SuppressLogging;
-
- if (!suppressLogging)
- {
- logger.LogDebug("Running {FullName} with args: {Args}", workingDirectory.FullName, string.Join(" ", args));
-
- if (env is not null)
- {
- foreach (var envKvp in env)
- {
- logger.LogDebug("Running {FullName} with env: {EnvKey}={EnvValue}", workingDirectory.FullName, envKvp.Key, envKvp.Value);
- }
- }
- }
-
- var startInfo = new ProcessStartInfo("dotnet")
- {
- WorkingDirectory = workingDirectory.FullName,
- UseShellExecute = false,
- CreateNoWindow = true,
- RedirectStandardInput = true,
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- };
-
- if (env is not null)
- {
- foreach (var envKvp in env)
- {
- startInfo.EnvironmentVariables[envKvp.Key] = envKvp.Value;
- }
- }
-
- foreach (var a in args)
- {
- startInfo.ArgumentList.Add(a);
- }
-
- // The AppHost uses this environment variable to signal to the CliOrphanDetector which process
- // it should monitor in order to know when to stop the CLI. As long as the process still exists
- // the orphan detector will allow the CLI to keep running. If the environment variable does
- // not exist the orphan detector will exit.
- startInfo.EnvironmentVariables[KnownConfigNames.CliProcessId] = GetCurrentProcessId().ToString(CultureInfo.InvariantCulture);
-
- // Set the CLI process start time for robust orphan detection to prevent PID reuse issues.
- // The AppHost will verify both PID and start time to ensure it's monitoring the correct process.
- startInfo.EnvironmentVariables[KnownConfigNames.CliProcessStarted] = GetCurrentProcessStartTimeUnixSeconds().ToString(CultureInfo.InvariantCulture);
-
- // Always set MSBUILDTERMINALLOGGER=false for all dotnet command executions to ensure consistent terminal logger behavior
- startInfo.EnvironmentVariables[KnownConfigNames.MsBuildTerminalLogger] = "false";
-
- // Suppress the .NET welcome message that appears on first run
- startInfo.EnvironmentVariables["DOTNET_NOLOGO"] = "1";
-
- // Configure DOTNET_ROOT to point to the private SDK installation if it exists
- ConfigurePrivateSdkEnvironment(startInfo);
-
- // Set debug session info if available
- var debugSessionInfo = configuration[KnownConfigNames.DebugSessionInfo];
- if (!string.IsNullOrEmpty(debugSessionInfo))
- {
- startInfo.EnvironmentVariables[KnownConfigNames.DebugSessionInfo] = debugSessionInfo;
- }
-
- var process = new Process { StartInfo = startInfo };
- return new DotNetCliExecution(process, logger, options);
- }
-
- ///
- /// Configures environment variables to use the private SDK installation if it exists.
- ///
- /// The process start info to configure.
- private void ConfigurePrivateSdkEnvironment(ProcessStartInfo startInfo)
- {
- // Get the effective minimum SDK version to determine which private SDK to use
- var sdkVersion = DotNetSdkInstaller.GetEffectiveMinimumSdkVersion(configuration);
- var sdksDirectory = executionContext.SdksDirectory.FullName;
- var sdkInstallPath = Path.Combine(sdksDirectory, "dotnet", sdkVersion);
- var dotnetExecutablePath = Path.Combine(
- sdkInstallPath,
- RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"
- );
-
- // Check if the private SDK exists
- if (Directory.Exists(sdkInstallPath))
- {
- // Set the executable path to be the private SDK.
- startInfo.FileName = dotnetExecutablePath;
-
- // Set DOTNET_ROOT to point to the private SDK installation
- startInfo.EnvironmentVariables["DOTNET_ROOT"] = sdkInstallPath;
-
- // Also set DOTNET_MULTILEVEL_LOOKUP to 0 to prevent fallback to system SDKs
- startInfo.EnvironmentVariables["DOTNET_MULTILEVEL_LOOKUP"] = "0";
-
- // Prepend the private SDK path to PATH so the dotnet executable from the private installation is found first
- var currentPath = startInfo.EnvironmentVariables["PATH"] ?? Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
- startInfo.EnvironmentVariables["PATH"] = $"{sdkInstallPath}{Path.PathSeparator}{currentPath}";
-
- logger.LogDebug("Using private SDK installation at {SdkPath}", sdkInstallPath);
- }
- }
-}
diff --git a/src/Aspire.Cli/DotNet/DotNetCliRunner.cs b/src/Aspire.Cli/DotNet/DotNetCliRunner.cs
index 2f88d686563..acf4de7f891 100644
--- a/src/Aspire.Cli/DotNet/DotNetCliRunner.cs
+++ b/src/Aspire.Cli/DotNet/DotNetCliRunner.cs
@@ -5,6 +5,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net.Sockets;
+using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
@@ -26,23 +27,23 @@ namespace Aspire.Cli.DotNet;
internal interface IDotNetCliRunner
{
- Task<(int ExitCode, bool IsAspireHost, string? AspireHostingVersion)> GetAppHostInformationAsync(FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
- Task<(int ExitCode, JsonDocument? Output)> GetProjectItemsAndPropertiesAsync(FileInfo projectFile, string[] items, string[] properties, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
- Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, bool noRestore, string[] args, IDictionary? env, TaskCompletionSource? backchannelCompletionSource, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
- Task<(int ExitCode, string? TemplateVersion)> InstallTemplateAsync(string packageName, string version, FileInfo? nugetConfigFile, string? nugetSource, bool force, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
- Task NewProjectAsync(string templateName, string name, string outputPath, string[] extraArgs, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
- Task RestoreAsync(FileInfo projectFilePath, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
- Task BuildAsync(FileInfo projectFilePath, bool noRestore, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
- Task AddPackageAsync(FileInfo projectFilePath, string packageName, string packageVersion, string? nugetSource, bool noRestore, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
- Task AddProjectToSolutionAsync(FileInfo solutionFile, FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
- Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
- Task<(int ExitCode, string[] ConfigPaths)> GetNuGetConfigPathsAsync(DirectoryInfo workingDirectory, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
- Task<(int ExitCode, IReadOnlyList Projects)> GetSolutionProjectsAsync(FileInfo solutionFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
- Task AddProjectReferenceAsync(FileInfo projectFile, FileInfo referencedProject, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
- Task InitUserSecretsAsync(FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
+ Task<(int ExitCode, bool IsAspireHost, string? AspireHostingVersion)> GetAppHostInformationAsync(FileInfo projectFile, ProcessInvocationOptions options, CancellationToken cancellationToken);
+ Task<(int ExitCode, JsonDocument? Output)> GetProjectItemsAndPropertiesAsync(FileInfo projectFile, string[] items, string[] properties, ProcessInvocationOptions options, CancellationToken cancellationToken);
+ Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, bool noRestore, string[] args, IDictionary? env, TaskCompletionSource? backchannelCompletionSource, ProcessInvocationOptions options, CancellationToken cancellationToken);
+ Task<(int ExitCode, string? TemplateVersion)> InstallTemplateAsync(string packageName, string version, FileInfo? nugetConfigFile, string? nugetSource, bool force, ProcessInvocationOptions options, CancellationToken cancellationToken);
+ Task NewProjectAsync(string templateName, string name, string outputPath, string[] extraArgs, ProcessInvocationOptions options, CancellationToken cancellationToken);
+ Task RestoreAsync(FileInfo projectFilePath, ProcessInvocationOptions options, CancellationToken cancellationToken);
+ Task BuildAsync(FileInfo projectFilePath, bool noRestore, ProcessInvocationOptions options, CancellationToken cancellationToken);
+ Task AddPackageAsync(FileInfo projectFilePath, string packageName, string packageVersion, string? nugetSource, bool noRestore, ProcessInvocationOptions options, CancellationToken cancellationToken);
+ Task AddProjectToSolutionAsync(FileInfo solutionFile, FileInfo projectFile, ProcessInvocationOptions options, CancellationToken cancellationToken);
+ Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, ProcessInvocationOptions options, CancellationToken cancellationToken);
+ Task<(int ExitCode, string[] ConfigPaths)> GetNuGetConfigPathsAsync(DirectoryInfo workingDirectory, ProcessInvocationOptions options, CancellationToken cancellationToken);
+ Task<(int ExitCode, IReadOnlyList Projects)> GetSolutionProjectsAsync(FileInfo solutionFile, ProcessInvocationOptions options, CancellationToken cancellationToken);
+ Task AddProjectReferenceAsync(FileInfo projectFile, FileInfo referencedProject, ProcessInvocationOptions options, CancellationToken cancellationToken);
+ Task InitUserSecretsAsync(FileInfo projectFile, ProcessInvocationOptions options, CancellationToken cancellationToken);
}
-internal sealed class DotNetCliRunnerInvocationOptions
+internal sealed class ProcessInvocationOptions
{
public Action? StandardOutputCallback { get; set; }
public Action? StandardErrorCallback { get; set; }
@@ -68,7 +69,7 @@ internal sealed class DotNetCliRunner(
IFeatures features,
IInteractionService interactionService,
CliExecutionContext executionContext,
- IDotNetCliExecutionFactory executionFactory) : IDotNetCliRunner
+ IProcessExecutionFactory executionFactory) : IDotNetCliRunner
{
private readonly IDiskCache _diskCache = diskCache;
@@ -102,10 +103,17 @@ private async Task ExecuteAsync(
FileInfo? projectFile,
DirectoryInfo workingDirectory,
TaskCompletionSource? backchannelCompletionSource,
- DotNetCliRunnerInvocationOptions options,
+ ProcessInvocationOptions options,
CancellationToken cancellationToken)
{
- var execution = executionFactory.CreateExecution(args, env, workingDirectory, options);
+ // Build the final environment variables by merging caller-provided env with dotnet-specific settings.
+ var finalEnv = env?.ToDictionary() ?? new Dictionary();
+ ConfigureDotNetEnvironment(finalEnv);
+
+ // Resolve the dotnet executable path, preferring the private SDK installation if available.
+ var dotnetPath = ResolveDotNetPath(finalEnv);
+
+ using var execution = executionFactory.CreateExecution(dotnetPath, args, finalEnv, workingDirectory, options);
// Get socket path from env if present
string? socketPath = null;
@@ -146,7 +154,78 @@ await extensionInteractionService.LaunchAppHostAsync(
return await execution.WaitForExitAsync(cancellationToken);
}
- private async Task StartBackchannelAsync(IDotNetCliExecution? execution, string socketPath, TaskCompletionSource backchannelCompletionSource, CancellationToken cancellationToken)
+ internal static int GetCurrentProcessId() => Environment.ProcessId;
+
+ internal static long GetCurrentProcessStartTimeUnixSeconds()
+ {
+ var startTime = Process.GetCurrentProcess().StartTime;
+ return ((DateTimeOffset)startTime).ToUnixTimeSeconds();
+ }
+
+ ///
+ /// Configures dotnet-specific environment variables for CLI process executions.
+ ///
+ private void ConfigureDotNetEnvironment(IDictionary env)
+ {
+ // The AppHost uses this environment variable to signal to the CliOrphanDetector which process
+ // it should monitor in order to know when to stop the CLI. As long as the process still exists
+ // the orphan detector will allow the CLI to keep running. If the environment variable does
+ // not exist the orphan detector will exit.
+ env[KnownConfigNames.CliProcessId] = GetCurrentProcessId().ToString(CultureInfo.InvariantCulture);
+
+ // Set the CLI process start time for robust orphan detection to prevent PID reuse issues.
+ // The AppHost will verify both PID and start time to ensure it's monitoring the correct process.
+ env[KnownConfigNames.CliProcessStarted] = GetCurrentProcessStartTimeUnixSeconds().ToString(CultureInfo.InvariantCulture);
+
+ // Always set MSBUILDTERMINALLOGGER=false for all dotnet command executions to ensure consistent terminal logger behavior
+ env[KnownConfigNames.MsBuildTerminalLogger] = "false";
+
+ // Suppress the .NET welcome message that appears on first run
+ env["DOTNET_NOLOGO"] = "1";
+
+ // Set debug session info if available
+ var debugSessionInfo = configuration[KnownConfigNames.DebugSessionInfo];
+ if (!string.IsNullOrEmpty(debugSessionInfo))
+ {
+ env[KnownConfigNames.DebugSessionInfo] = debugSessionInfo;
+ }
+ }
+
+ ///
+ /// Resolves the dotnet executable path, preferring a private SDK installation if available.
+ /// When a private SDK is found, the appropriate environment variables (DOTNET_ROOT, PATH, etc.)
+ /// are set on the provided dictionary.
+ ///
+ /// The path to the dotnet executable.
+ private string ResolveDotNetPath(IDictionary env)
+ {
+ var sdkVersion = DotNetSdkInstaller.GetEffectiveMinimumSdkVersion(configuration);
+ var sdksDirectory = executionContext.SdksDirectory.FullName;
+ var sdkInstallPath = Path.Combine(sdksDirectory, "dotnet", sdkVersion);
+ var dotnetExecutablePath = Path.Combine(
+ sdkInstallPath,
+ RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"
+ );
+
+ if (Directory.Exists(sdkInstallPath))
+ {
+ env["DOTNET_ROOT"] = sdkInstallPath;
+ env["DOTNET_MULTILEVEL_LOOKUP"] = "0";
+
+ // Prepend the private SDK path to PATH. Check if the caller already provided a PATH override.
+ var currentPath = env.TryGetValue("PATH", out var userPath)
+ ? userPath
+ : Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
+ env["PATH"] = $"{sdkInstallPath}{Path.PathSeparator}{currentPath}";
+
+ logger.LogDebug("Using private SDK installation at {SdkPath}", sdkInstallPath);
+ return dotnetExecutablePath;
+ }
+
+ return "dotnet";
+ }
+
+ private async Task StartBackchannelAsync(IProcessExecution? execution, string socketPath, TaskCompletionSource backchannelCompletionSource, CancellationToken cancellationToken)
{
using var activity = telemetry.StartDiagnosticActivity();
@@ -228,7 +307,7 @@ private async Task StartBackchannelAsync(IDotNetCliExecution? execution, string
// Cache expiry/max age handled inside DiskCache implementation.
- public async Task<(int ExitCode, bool IsAspireHost, string? AspireHostingVersion)> GetAppHostInformationAsync(FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public async Task<(int ExitCode, bool IsAspireHost, string? AspireHostingVersion)> GetAppHostInformationAsync(FileInfo projectFile, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
using var activity = telemetry.StartDiagnosticActivity();
@@ -311,7 +390,7 @@ private async Task StartBackchannelAsync(IDotNetCliExecution? execution, string
}
}
- public async Task<(int ExitCode, JsonDocument? Output)> GetProjectItemsAndPropertiesAsync(FileInfo projectFile, string[] items, string[] properties, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public async Task<(int ExitCode, JsonDocument? Output)> GetProjectItemsAndPropertiesAsync(FileInfo projectFile, string[] items, string[] properties, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
using var activity = telemetry.StartDiagnosticActivity();
@@ -412,7 +491,7 @@ private async Task StartBackchannelAsync(IDotNetCliExecution? execution, string
return (1, null);
}
- public async Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, bool noRestore, string[] args, IDictionary? env, TaskCompletionSource? backchannelCompletionSource, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public async Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, bool noRestore, string[] args, IDictionary? env, TaskCompletionSource? backchannelCompletionSource, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
using var activity = telemetry.StartDiagnosticActivity();
@@ -487,7 +566,7 @@ public async Task RunAsync(FileInfo projectFile, bool watch, bool noBuild,
cancellationToken: cancellationToken);
}
- public async Task<(int ExitCode, string? TemplateVersion)> InstallTemplateAsync(string packageName, string version, FileInfo? nugetConfigFile, string? nugetSource, bool force, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public async Task<(int ExitCode, string? TemplateVersion)> InstallTemplateAsync(string packageName, string version, FileInfo? nugetConfigFile, string? nugetSource, bool force, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
using var activity = telemetry.StartDiagnosticActivity(kind: ActivityKind.Client);
@@ -616,7 +695,7 @@ internal static bool TryParsePackageVersionFromStdout(string stdout, [NotNullWhe
}
}
- public async Task NewProjectAsync(string templateName, string name, string outputPath, string[] extraArgs, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public async Task NewProjectAsync(string templateName, string name, string outputPath, string[] extraArgs, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
using var activity = telemetry.StartDiagnosticActivity();
@@ -631,7 +710,7 @@ public async Task NewProjectAsync(string templateName, string name, string
cancellationToken: cancellationToken);
}
- public async Task RestoreAsync(FileInfo projectFilePath, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public async Task RestoreAsync(FileInfo projectFilePath, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
using var activity = telemetry.StartDiagnosticActivity();
@@ -647,7 +726,7 @@ public async Task RestoreAsync(FileInfo projectFilePath, DotNetCliRunnerInv
cancellationToken: cancellationToken);
}
- public async Task BuildAsync(FileInfo projectFilePath, bool noRestore, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public async Task BuildAsync(FileInfo projectFilePath, bool noRestore, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
using var activity = telemetry.StartDiagnosticActivity();
@@ -670,7 +749,7 @@ public async Task BuildAsync(FileInfo projectFilePath, bool noRestore, DotN
options: options,
cancellationToken: cancellationToken);
}
- public async Task AddPackageAsync(FileInfo projectFilePath, string packageName, string packageVersion, string? nugetSource, bool noRestore, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public async Task AddPackageAsync(FileInfo projectFilePath, string packageName, string packageVersion, string? nugetSource, bool noRestore, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
using var activity = telemetry.StartDiagnosticActivity();
@@ -734,7 +813,7 @@ public async Task AddPackageAsync(FileInfo projectFilePath, string packageN
return result;
}
- public async Task AddProjectToSolutionAsync(FileInfo solutionFile, FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public async Task AddProjectToSolutionAsync(FileInfo solutionFile, FileInfo projectFile, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
using var activity = telemetry.StartDiagnosticActivity();
@@ -763,7 +842,7 @@ public async Task AddProjectToSolutionAsync(FileInfo solutionFile, FileInfo
return result;
}
- public async Task ComputeNuGetConfigHierarchySha256Async(DirectoryInfo workingDirectory, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public async Task ComputeNuGetConfigHierarchySha256Async(DirectoryInfo workingDirectory, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
// The purpose of this method is to compute a hash that can be used as a substitute for an explicitly passed
// in NuGet.config file hash. This is useful for when `aspire add` is invoked and we present options from the
@@ -826,7 +905,7 @@ public async Task ComputeNuGetConfigHierarchySha256Async(DirectoryInfo w
return result;
}
- public async Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public async Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
using var activity = telemetry.StartDiagnosticActivity();
@@ -995,7 +1074,7 @@ public async Task ComputeNuGetConfigHierarchySha256Async(DirectoryInfo w
}
}
- public async Task<(int ExitCode, string[] ConfigPaths)> GetNuGetConfigPathsAsync(DirectoryInfo workingDirectory, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public async Task<(int ExitCode, string[] ConfigPaths)> GetNuGetConfigPathsAsync(DirectoryInfo workingDirectory, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
using var activity = telemetry.StartDiagnosticActivity();
@@ -1035,7 +1114,7 @@ public async Task ComputeNuGetConfigHierarchySha256Async(DirectoryInfo w
}
}
- public async Task<(int ExitCode, IReadOnlyList Projects)> GetSolutionProjectsAsync(FileInfo solutionFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public async Task<(int ExitCode, IReadOnlyList Projects)> GetSolutionProjectsAsync(FileInfo solutionFile, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
using var activity = telemetry.StartDiagnosticActivity();
@@ -1101,7 +1180,7 @@ public async Task ComputeNuGetConfigHierarchySha256Async(DirectoryInfo w
return (exitCode, projects);
}
- public async Task AddProjectReferenceAsync(FileInfo projectFile, FileInfo referencedProject, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public async Task AddProjectReferenceAsync(FileInfo projectFile, FileInfo referencedProject, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
using var activity = telemetry.StartDiagnosticActivity();
@@ -1130,7 +1209,7 @@ public async Task AddProjectReferenceAsync(FileInfo projectFile, FileInfo r
return result;
}
- public Task InitUserSecretsAsync(FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task InitUserSecretsAsync(FileInfo projectFile, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
return ExecuteAsync(["user-secrets", "init", "--project", projectFile.FullName], env: null, projectFile: null, projectFile.Directory!, backchannelCompletionSource: null, options, cancellationToken);
}
diff --git a/src/Aspire.Cli/DotNet/IDotNetCliExecution.cs b/src/Aspire.Cli/DotNet/IProcessExecution.cs
similarity index 79%
rename from src/Aspire.Cli/DotNet/IDotNetCliExecution.cs
rename to src/Aspire.Cli/DotNet/IProcessExecution.cs
index a8d596b3faf..8dafaecbaf3 100644
--- a/src/Aspire.Cli/DotNet/IDotNetCliExecution.cs
+++ b/src/Aspire.Cli/DotNet/IProcessExecution.cs
@@ -4,9 +4,9 @@
namespace Aspire.Cli.DotNet;
///
-/// Represents a configured dotnet CLI execution that can be started and awaited.
+/// Represents a configured process execution that can be started and awaited.
///
-internal interface IDotNetCliExecution
+internal interface IProcessExecution : IDisposable
{
///
/// Gets the file name of the executable to run.
@@ -45,4 +45,10 @@ internal interface IDotNetCliExecution
/// Gets the exit code of the process. Only valid after returns true.
///
int ExitCode { get; }
+
+ ///
+ /// Kills the process.
+ ///
+ /// When true, kills the entire process tree; otherwise kills only the root process.
+ void Kill(bool entireProcessTree);
}
diff --git a/src/Aspire.Cli/DotNet/IDotNetCliExecutionFactory.cs b/src/Aspire.Cli/DotNet/IProcessExecutionFactory.cs
similarity index 54%
rename from src/Aspire.Cli/DotNet/IDotNetCliExecutionFactory.cs
rename to src/Aspire.Cli/DotNet/IProcessExecutionFactory.cs
index 5f5b192791e..bd3e68c12e8 100644
--- a/src/Aspire.Cli/DotNet/IDotNetCliExecutionFactory.cs
+++ b/src/Aspire.Cli/DotNet/IProcessExecutionFactory.cs
@@ -4,22 +4,23 @@
namespace Aspire.Cli.DotNet;
///
-/// Creates configured dotnet CLI executions.
+/// Creates configured process executions.
///
-internal interface IDotNetCliExecutionFactory
+internal interface IProcessExecutionFactory
{
///
- /// Creates a configured dotnet CLI execution ready to be started.
+ /// Creates a configured process execution ready to be started.
///
- /// The command-line arguments to pass to dotnet.
- /// Optional environment variables to set for the process. If backchannel communication
- /// is needed, the socket path should be set via ASPIRE__BACKCHANNEL__UNIXSOCKETPATH.
+ /// The executable path to run.
+ /// The command-line arguments to pass to the process.
+ /// Optional environment variables to set for the process.
/// The working directory for the process.
/// Invocation options for the command.
- /// A configured ready to be started.
- IDotNetCliExecution CreateExecution(
+ /// A configured ready to be started.
+ IProcessExecution CreateExecution(
+ string fileName,
string[] args,
IDictionary? env,
DirectoryInfo workingDirectory,
- DotNetCliRunnerInvocationOptions options);
+ ProcessInvocationOptions options);
}
diff --git a/src/Aspire.Cli/DotNet/DotNetCliExecution.cs b/src/Aspire.Cli/DotNet/ProcessExecution.cs
similarity index 81%
rename from src/Aspire.Cli/DotNet/DotNetCliExecution.cs
rename to src/Aspire.Cli/DotNet/ProcessExecution.cs
index bf7d715aa23..5b1bcb9be6a 100644
--- a/src/Aspire.Cli/DotNet/DotNetCliExecution.cs
+++ b/src/Aspire.Cli/DotNet/ProcessExecution.cs
@@ -7,17 +7,17 @@
namespace Aspire.Cli.DotNet;
///
-/// Represents a configured dotnet CLI execution backed by a real process.
+/// Represents a configured process execution backed by a real OS process.
///
-internal sealed class DotNetCliExecution : IDotNetCliExecution
+internal sealed class ProcessExecution : IProcessExecution
{
private readonly Process _process;
private readonly ILogger _logger;
- private readonly DotNetCliRunnerInvocationOptions _options;
+ private readonly ProcessInvocationOptions _options;
private Task? _stdoutForwarder;
private Task? _stderrForwarder;
- internal DotNetCliExecution(Process process, ILogger logger, DotNetCliRunnerInvocationOptions options)
+ internal ProcessExecution(Process process, ILogger logger, ProcessInvocationOptions options)
{
_process = process;
_logger = logger;
@@ -51,14 +51,14 @@ public bool Start()
{
if (!suppressLogging)
{
- _logger.LogDebug("Failed to start dotnet process with args: {Args}", string.Join(" ", Arguments));
+ _logger.LogDebug("Failed to start process {FileName} with args: {Args}", FileName, string.Join(" ", Arguments));
}
return false;
}
if (!suppressLogging)
{
- _logger.LogDebug("Started dotnet with PID: {ProcessId}", _process.Id);
+ _logger.LogDebug("Started {FileName} with PID: {ProcessId}", FileName, _process.Id);
}
// Start stream forwarders
@@ -90,7 +90,7 @@ public async Task WaitForExitAsync(CancellationToken cancellationToken)
if (!suppressLogging)
{
- _logger.LogDebug("Waiting for dotnet process to exit with PID: {ProcessId}", _process.Id);
+ _logger.LogDebug("Waiting for process to exit with PID: {ProcessId}", _process.Id);
}
await _process.WaitForExitAsync(cancellationToken);
@@ -99,7 +99,7 @@ public async Task WaitForExitAsync(CancellationToken cancellationToken)
{
if (!suppressLogging)
{
- _logger.LogDebug("dotnet process with PID: {ProcessId} has not exited, killing it.", _process.Id);
+ _logger.LogDebug("Process with PID: {ProcessId} has not exited, killing it.", _process.Id);
}
_process.Kill(false);
}
@@ -107,7 +107,7 @@ public async Task WaitForExitAsync(CancellationToken cancellationToken)
{
if (!suppressLogging)
{
- _logger.LogDebug("dotnet process with PID: {ProcessId} has exited with code: {ExitCode}", _process.Id, _process.ExitCode);
+ _logger.LogDebug("Process with PID: {ProcessId} has exited with code: {ExitCode}", _process.Id, _process.ExitCode);
}
}
@@ -141,6 +141,18 @@ public async Task WaitForExitAsync(CancellationToken cancellationToken)
return _process.ExitCode;
}
+ ///
+ public void Kill(bool entireProcessTree)
+ {
+ _process.Kill(entireProcessTree);
+ }
+
+ ///
+ public void Dispose()
+ {
+ _process.Dispose();
+ }
+
private async Task ForwardStreamToLoggerAsync(StreamReader reader, string identifier, Action? lineCallback, bool suppressLogging)
{
if (!suppressLogging)
@@ -160,7 +172,8 @@ private async Task ForwardStreamToLoggerAsync(StreamReader reader, string identi
if (!suppressLogging)
{
_logger.LogDebug(
- "dotnet({ProcessId}) {Identifier}: {Line}",
+ "{FileName}({ProcessId}) {Identifier}: {Line}",
+ FileName,
_process.Id,
identifier,
line
diff --git a/src/Aspire.Cli/DotNet/ProcessExecutionFactory.cs b/src/Aspire.Cli/DotNet/ProcessExecutionFactory.cs
new file mode 100644
index 00000000000..3d7d549e158
--- /dev/null
+++ b/src/Aspire.Cli/DotNet/ProcessExecutionFactory.cs
@@ -0,0 +1,58 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using Microsoft.Extensions.Logging;
+
+namespace Aspire.Cli.DotNet;
+
+///
+/// Creates process executions backed by real OS processes.
+///
+internal sealed class ProcessExecutionFactory(
+ ILogger logger) : IProcessExecutionFactory
+{
+ public IProcessExecution CreateExecution(string fileName, string[] args, IDictionary? env, DirectoryInfo workingDirectory, ProcessInvocationOptions options)
+ {
+ var suppressLogging = options.SuppressLogging;
+
+ if (!suppressLogging)
+ {
+ logger.LogDebug("Running {FullName} with args: {Args}", workingDirectory.FullName, string.Join(" ", args));
+
+ if (env is not null)
+ {
+ foreach (var envKvp in env)
+ {
+ logger.LogDebug("Running {FullName} with env: {EnvKey}={EnvValue}", workingDirectory.FullName, envKvp.Key, envKvp.Value);
+ }
+ }
+ }
+
+ var startInfo = new ProcessStartInfo(fileName)
+ {
+ WorkingDirectory = workingDirectory.FullName,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ RedirectStandardInput = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ };
+
+ if (env is not null)
+ {
+ foreach (var envKvp in env)
+ {
+ startInfo.EnvironmentVariables[envKvp.Key] = envKvp.Value;
+ }
+ }
+
+ foreach (var a in args)
+ {
+ startInfo.ArgumentList.Add(a);
+ }
+
+ var process = new Process { StartInfo = startInfo };
+ return new ProcessExecution(process, logger, options);
+ }
+}
diff --git a/src/Aspire.Cli/Layout/LayoutProcessRunner.cs b/src/Aspire.Cli/Layout/LayoutProcessRunner.cs
index 19d8e1716b8..3b59daf80f6 100644
--- a/src/Aspire.Cli/Layout/LayoutProcessRunner.cs
+++ b/src/Aspire.Cli/Layout/LayoutProcessRunner.cs
@@ -1,7 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Diagnostics;
+using System.Text;
+using Aspire.Cli.DotNet;
using Aspire.Shared;
namespace Aspire.Cli.Layout;
@@ -24,92 +25,62 @@ internal static class RuntimeIdentifierHelper
}
///
-/// Utilities for running processes using layout tools.
-/// All layout tools are self-contained executables — no muxer needed.
+/// Runs processes using layout tools via an .
///
-internal static class LayoutProcessRunner
+internal sealed class LayoutProcessRunner(IProcessExecutionFactory executionFactory)
{
- ///
- /// Runs a tool and captures output. The tool is always run directly as a native executable.
- ///
- public static async Task<(int ExitCode, string Output, string Error)> RunAsync(
+ ///
+ public async Task<(int ExitCode, string Output, string Error)> RunAsync(
string toolPath,
IEnumerable arguments,
string? workingDirectory = null,
IDictionary? environmentVariables = null,
CancellationToken ct = default)
{
- using var process = CreateProcess(toolPath, arguments, workingDirectory, environmentVariables, redirectOutput: true);
+ var outputBuilder = new StringBuilder();
+ var errorBuilder = new StringBuilder();
+
+ var options = new ProcessInvocationOptions
+ {
+ SuppressLogging = true,
+ StandardOutputCallback = line => outputBuilder.AppendLine(line),
+ StandardErrorCallback = line => errorBuilder.AppendLine(line),
+ };
- process.Start();
+ var args = arguments.ToArray();
+ var workDir = new DirectoryInfo(workingDirectory ?? Directory.GetCurrentDirectory());
- var outputTask = process.StandardOutput.ReadToEndAsync(ct);
- var errorTask = process.StandardError.ReadToEndAsync(ct);
+ using var execution = executionFactory.CreateExecution(toolPath, args, environmentVariables, workDir, options);
+
+ if (!execution.Start())
+ {
+ throw new InvalidOperationException($"Failed to start process: {toolPath}");
+ }
- await process.WaitForExitAsync(ct);
+ var exitCode = await execution.WaitForExitAsync(ct).ConfigureAwait(false);
- return (process.ExitCode, await outputTask, await errorTask);
+ return (exitCode, outputBuilder.ToString(), errorBuilder.ToString());
}
- ///
- /// Starts a process without waiting for it to exit.
- /// Returns the Process object for the caller to manage.
- ///
- public static Process Start(
+ ///
+ public IProcessExecution Start(
string toolPath,
IEnumerable arguments,
string? workingDirectory = null,
IDictionary? environmentVariables = null,
- bool redirectOutput = false)
+ ProcessInvocationOptions? options = null)
{
- var process = CreateProcess(toolPath, arguments, workingDirectory, environmentVariables, redirectOutput);
- process.Start();
- return process;
- }
+ var args = arguments.ToArray();
+ var workDir = new DirectoryInfo(workingDirectory ?? Directory.GetCurrentDirectory());
- ///
- /// Creates a configured Process for running a bundle tool.
- /// Tools are always self-contained executables — run directly.
- ///
- private static Process CreateProcess(
- string toolPath,
- IEnumerable arguments,
- string? workingDirectory,
- IDictionary? environmentVariables,
- bool redirectOutput)
- {
- var process = new Process();
-
- process.StartInfo.UseShellExecute = false;
- process.StartInfo.CreateNoWindow = true;
- process.StartInfo.FileName = toolPath;
-
- if (redirectOutput)
- {
- process.StartInfo.RedirectStandardOutput = true;
- process.StartInfo.RedirectStandardError = true;
- }
-
- // Add custom environment variables
- if (environmentVariables is not null)
- {
- foreach (var (key, value) in environmentVariables)
- {
- process.StartInfo.Environment[key] = value;
- }
- }
-
- if (workingDirectory is not null)
- {
- process.StartInfo.WorkingDirectory = workingDirectory;
- }
+ var execution = executionFactory.CreateExecution(toolPath, args, environmentVariables, workDir, options ?? new ProcessInvocationOptions());
- // Add arguments
- foreach (var arg in arguments)
+ if (!execution.Start())
{
- process.StartInfo.ArgumentList.Add(arg);
+ execution.Dispose();
+ throw new InvalidOperationException($"Failed to start process: {toolPath}");
}
- return process;
+ return execution;
}
}
diff --git a/src/Aspire.Cli/NuGet/BundleNuGetPackageCache.cs b/src/Aspire.Cli/NuGet/BundleNuGetPackageCache.cs
index 6821eecfb93..6eac3b30246 100644
--- a/src/Aspire.Cli/NuGet/BundleNuGetPackageCache.cs
+++ b/src/Aspire.Cli/NuGet/BundleNuGetPackageCache.cs
@@ -18,15 +18,18 @@ namespace Aspire.Cli.NuGet;
internal sealed class BundleNuGetPackageCache : INuGetPackageCache
{
private readonly IBundleService _bundleService;
+ private readonly LayoutProcessRunner _layoutProcessRunner;
private readonly ILogger _logger;
private readonly IFeatures _features;
public BundleNuGetPackageCache(
IBundleService bundleService,
+ LayoutProcessRunner layoutProcessRunner,
ILogger logger,
IFeatures features)
{
_bundleService = bundleService;
+ _layoutProcessRunner = layoutProcessRunner;
_logger = logger;
_features = features;
}
@@ -155,7 +158,7 @@ private async Task> SearchPackagesInternalAsync(
_logger.LogDebug("NuGet search args: {Args}", string.Join(" ", args));
_logger.LogDebug("Working directory: {WorkingDir}", workingDirectory.FullName);
- var (exitCode, output, error) = await LayoutProcessRunner.RunAsync(
+ var (exitCode, output, error) = await _layoutProcessRunner.RunAsync(
managedPath,
args,
workingDirectory: workingDirectory.FullName,
diff --git a/src/Aspire.Cli/NuGet/BundleNuGetService.cs b/src/Aspire.Cli/NuGet/BundleNuGetService.cs
index 4a1193fe051..03b3c5edc6a 100644
--- a/src/Aspire.Cli/NuGet/BundleNuGetService.cs
+++ b/src/Aspire.Cli/NuGet/BundleNuGetService.cs
@@ -32,17 +32,20 @@ Task RestorePackagesAsync(
///
/// NuGet service implementation that uses the bundle's NuGetHelper tool.
///
-public sealed class BundleNuGetService : INuGetService
+internal sealed class BundleNuGetService : INuGetService
{
private readonly ILayoutDiscovery _layoutDiscovery;
+ private readonly LayoutProcessRunner _layoutProcessRunner;
private readonly ILogger _logger;
private readonly string _cacheDirectory;
public BundleNuGetService(
ILayoutDiscovery layoutDiscovery,
+ LayoutProcessRunner layoutProcessRunner,
ILogger logger)
{
_layoutDiscovery = layoutDiscovery;
+ _layoutProcessRunner = layoutProcessRunner;
_logger = logger;
_cacheDirectory = GetCacheDirectory();
}
@@ -130,7 +133,7 @@ public async Task RestorePackagesAsync(
_logger.LogDebug("aspire-managed path: {ManagedPath}", managedPath);
_logger.LogDebug("NuGet restore args: {Args}", string.Join(" ", restoreArgs));
- var (exitCode, output, error) = await LayoutProcessRunner.RunAsync(
+ var (exitCode, output, error) = await _layoutProcessRunner.RunAsync(
managedPath,
restoreArgs,
ct: ct);
@@ -169,7 +172,7 @@ public async Task RestorePackagesAsync(
_logger.LogDebug("Creating layout from {AssetsPath}", assetsPath);
_logger.LogDebug("NuGet layout args: {Args}", string.Join(" ", layoutArgs));
- (exitCode, output, error) = await LayoutProcessRunner.RunAsync(
+ (exitCode, output, error) = await _layoutProcessRunner.RunAsync(
managedPath,
layoutArgs,
ct: ct);
diff --git a/src/Aspire.Cli/NuGet/NuGetPackageCache.cs b/src/Aspire.Cli/NuGet/NuGetPackageCache.cs
index c6b8ff47ddf..ab25548a5d9 100644
--- a/src/Aspire.Cli/NuGet/NuGetPackageCache.cs
+++ b/src/Aspire.Cli/NuGet/NuGetPackageCache.cs
@@ -101,7 +101,7 @@ public async Task> GetPackagesAsync(DirectoryInfo work
skip,
nugetConfigFile,
useCache, // Pass through the useCache parameter
- new DotNetCliRunnerInvocationOptions { SuppressLogging = true },
+ new ProcessInvocationOptions { SuppressLogging = true },
cancellationToken
);
diff --git a/src/Aspire.Cli/Program.cs b/src/Aspire.Cli/Program.cs
index 55881912e73..08b70592d62 100644
--- a/src/Aspire.Cli/Program.cs
+++ b/src/Aspire.Cli/Program.cs
@@ -323,7 +323,8 @@ internal static async Task BuildApplicationAsync(string[] args, CliStartu
builder.Services.AddSingleton(BuildConfigurationService);
builder.Services.AddSingleton();
builder.Services.AddTelemetryServices();
- builder.Services.AddTransient();
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
// Register certificate tool runner - uses native CertificateManager directly (no subprocess needed)
builder.Services.AddSingleton(sp => CertificateManager.Create(sp.GetRequiredService>()));
@@ -454,6 +455,7 @@ internal static async Task BuildApplicationAsync(string[] args, CliStartu
builder.Services.AddTransient();
builder.Services.AddTransient();
builder.Services.AddTransient();
+ builder.Services.AddTransient();
builder.Services.AddTransient();
builder.Services.AddTransient();
builder.Services.AddTransient();
diff --git a/src/Aspire.Cli/Projects/DotNetAppHostProject.cs b/src/Aspire.Cli/Projects/DotNetAppHostProject.cs
index 0ac674d1eca..013402f21f0 100644
--- a/src/Aspire.Cli/Projects/DotNetAppHostProject.cs
+++ b/src/Aspire.Cli/Projects/DotNetAppHostProject.cs
@@ -172,7 +172,7 @@ public async Task ValidateAppHostAsync(FileInfo appHost
}
// For project files, check if it's a valid Aspire AppHost using GetAppHostInformationAsync
- var information = await _runner.GetAppHostInformationAsync(appHostFile, new DotNetCliRunnerInvocationOptions(), cancellationToken);
+ var information = await _runner.GetAppHostInformationAsync(appHostFile, new ProcessInvocationOptions(), cancellationToken);
if (information.ExitCode == 0 && information.IsAspireHost)
{
@@ -261,7 +261,7 @@ public async Task RunAsync(AppHostProjectContext context, CancellationToken
var shouldBuildInCli = !isExtensionHost || extensionHasBuildCapability;
if (shouldBuildInCli)
{
- var buildOptions = new DotNetCliRunnerInvocationOptions
+ var buildOptions = new ProcessInvocationOptions
{
StandardOutputCallback = buildOutputCollector.AppendOutput,
StandardErrorCallback = buildOutputCollector.AppendError,
@@ -309,7 +309,7 @@ public async Task RunAsync(AppHostProjectContext context, CancellationToken
// Signal that build/preparation is complete
context.BuildCompletionSource?.TrySetResult(true);
- var runOptions = new DotNetCliRunnerInvocationOptions
+ var runOptions = new ProcessInvocationOptions
{
StandardOutputCallback = runOutputCollector.AppendOutput,
StandardErrorCallback = runOutputCollector.AppendError,
@@ -408,7 +408,7 @@ public async Task PublishAsync(PublishContext context, CancellationToken ca
if (!context.NoBuild)
{
var buildOutputCollector = new OutputCollector(_fileLoggerProvider, "Build");
- var buildOptions = new DotNetCliRunnerInvocationOptions
+ var buildOptions = new ProcessInvocationOptions
{
StandardOutputCallback = buildOutputCollector.AppendOutput,
StandardErrorCallback = buildOutputCollector.AppendError,
@@ -439,7 +439,7 @@ public async Task PublishAsync(PublishContext context, CancellationToken ca
var runOutputCollector = new OutputCollector(_fileLoggerProvider, "AppHost");
context.OutputCollector = runOutputCollector;
- var runOptions = new DotNetCliRunnerInvocationOptions
+ var runOptions = new ProcessInvocationOptions
{
StandardOutputCallback = runOutputCollector.AppendOutput,
StandardErrorCallback = runOutputCollector.AppendError,
@@ -473,7 +473,7 @@ public async Task AddPackageAsync(AddPackageContext context, CancellationT
var outputCollector = new OutputCollector(_fileLoggerProvider, "Package");
context.OutputCollector = outputCollector;
- var options = new DotNetCliRunnerInvocationOptions
+ var options = new ProcessInvocationOptions
{
StandardOutputCallback = outputCollector.AppendOutput,
StandardErrorCallback = outputCollector.AppendError,
@@ -539,7 +539,7 @@ public async Task FindAndStopRunningInstanceAsync(FileInf
await _runner.InitUserSecretsAsync(
projectFile,
- new DotNetCliRunnerInvocationOptions(),
+ new ProcessInvocationOptions(),
cancellationToken);
// Re-query
@@ -554,7 +554,7 @@ await _runner.InitUserSecretsAsync(
projectFile,
items: [],
properties: ["UserSecretsId"],
- new DotNetCliRunnerInvocationOptions(),
+ new ProcessInvocationOptions(),
cancellationToken);
if (exitCode != 0 || jsonDocument is null)
diff --git a/src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs b/src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs
index eb58875f4a7..937e5d45957 100644
--- a/src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs
+++ b/src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs
@@ -424,7 +424,7 @@ private XDocument CreateProjectFile(IEnumerable integratio
var outputCollector = new OutputCollector();
var projectFile = new FileInfo(Path.Combine(_projectModelPath, ProjectFileName));
- var options = new DotNetCliRunnerInvocationOptions
+ var options = new ProcessInvocationOptions
{
StandardOutputCallback = outputCollector.AppendOutput,
StandardErrorCallback = outputCollector.AppendError
diff --git a/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs b/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs
index 7bfdb9e3100..cadb8d3c844 100644
--- a/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs
+++ b/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs
@@ -243,7 +243,7 @@ await File.WriteAllTextAsync(
var exitCode = await _dotNetCliRunner.BuildAsync(
new FileInfo(projectFilePath),
noRestore: false,
- new DotNetCliRunnerInvocationOptions
+ new ProcessInvocationOptions
{
StandardOutputCallback = buildOutput.AppendOutput,
StandardErrorCallback = buildOutput.AppendError
diff --git a/src/Aspire.Cli/Resources/DashboardCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/DashboardCommandStrings.Designer.cs
new file mode 100644
index 00000000000..fecfd7d3df8
--- /dev/null
+++ b/src/Aspire.Cli/Resources/DashboardCommandStrings.Designer.cs
@@ -0,0 +1,175 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Aspire.Cli.Resources {
+ using System;
+
+
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ public class DashboardCommandStrings {
+
+ private static System.Resources.ResourceManager resourceMan;
+
+ private static System.Globalization.CultureInfo resourceCulture;
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal DashboardCommandStrings() {
+ }
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.Equals(null, resourceMan)) {
+ System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Aspire.Cli.Resources.DashboardCommandStrings", typeof(DashboardCommandStrings).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ public static string Description {
+ get {
+ return ResourceManager.GetString("Description", resourceCulture);
+ }
+ }
+
+ public static string BundleNotAvailable {
+ get {
+ return ResourceManager.GetString("BundleNotAvailable", resourceCulture);
+ }
+ }
+
+ public static string BundleLayoutNotFound {
+ get {
+ return ResourceManager.GetString("BundleLayoutNotFound", resourceCulture);
+ }
+ }
+
+ public static string ManagedBinaryNotFound {
+ get {
+ return ResourceManager.GetString("ManagedBinaryNotFound", resourceCulture);
+ }
+ }
+
+ public static string DashboardStarted {
+ get {
+ return ResourceManager.GetString("DashboardStarted", resourceCulture);
+ }
+ }
+
+ public static string DashboardExitedWithError {
+ get {
+ return ResourceManager.GetString("DashboardExitedWithError", resourceCulture);
+ }
+ }
+
+ public static string StartingDashboard {
+ get {
+ return ResourceManager.GetString("StartingDashboard", resourceCulture);
+ }
+ }
+
+ public static string DashboardStartTimedOut {
+ get {
+ return ResourceManager.GetString("DashboardStartTimedOut", resourceCulture);
+ }
+ }
+
+ public static string FrontendUrlOptionDescription {
+ get {
+ return ResourceManager.GetString("FrontendUrlOptionDescription", resourceCulture);
+ }
+ }
+
+ public static string OtlpGrpcUrlOptionDescription {
+ get {
+ return ResourceManager.GetString("OtlpGrpcUrlOptionDescription", resourceCulture);
+ }
+ }
+
+ public static string OtlpHttpUrlOptionDescription {
+ get {
+ return ResourceManager.GetString("OtlpHttpUrlOptionDescription", resourceCulture);
+ }
+ }
+
+ public static string AllowAnonymousOptionDescription {
+ get {
+ return ResourceManager.GetString("AllowAnonymousOptionDescription", resourceCulture);
+ }
+ }
+
+ public static string ConfigFilePathOptionDescription {
+ get {
+ return ResourceManager.GetString("ConfigFilePathOptionDescription", resourceCulture);
+ }
+ }
+
+ public static string DashboardLabel {
+ get {
+ return ResourceManager.GetString("DashboardLabel", resourceCulture);
+ }
+ }
+
+ public static string OtlpGrpcLabel {
+ get {
+ return ResourceManager.GetString("OtlpGrpcLabel", resourceCulture);
+ }
+ }
+
+ public static string OtlpHttpLabel {
+ get {
+ return ResourceManager.GetString("OtlpHttpLabel", resourceCulture);
+ }
+ }
+
+ public static string LogsLabel {
+ get {
+ return ResourceManager.GetString("LogsLabel", resourceCulture);
+ }
+ }
+
+ public static string DashboardExitedUnexpectedError {
+ get {
+ return ResourceManager.GetString("DashboardExitedUnexpectedError", resourceCulture);
+ }
+ }
+
+ public static string DashboardExitedValidationFailure {
+ get {
+ return ResourceManager.GetString("DashboardExitedValidationFailure", resourceCulture);
+ }
+ }
+
+ public static string DashboardExitedAddressInUse {
+ get {
+ return ResourceManager.GetString("DashboardExitedAddressInUse", resourceCulture);
+ }
+ }
+
+ public static string StoppingDashboard {
+ get {
+ return ResourceManager.GetString("StoppingDashboard", resourceCulture);
+ }
+ }
+
+ }
+}
diff --git a/src/Aspire.Cli/Resources/DashboardCommandStrings.resx b/src/Aspire.Cli/Resources/DashboardCommandStrings.resx
new file mode 100644
index 00000000000..3a8bf704302
--- /dev/null
+++ b/src/Aspire.Cli/Resources/DashboardCommandStrings.resx
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Start the Aspire dashboard
+
+
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+
+
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+
+
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+
+
+ Dashboard started (PID {0}).
+
+
+ Dashboard exited with exit code {0}.
+
+
+ Starting dashboard...
+
+
+ Dashboard did not become ready within the expected time.
+
+
+ One or more HTTP endpoints through which the dashboard frontend is served
+
+
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+
+
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+
+
+ Allow anonymous access to the dashboard
+
+
+ The path for a JSON configuration file
+
+
+ Dashboard
+
+
+ OTLP/gRPC
+
+
+ OTLP/HTTP
+
+
+ Logs
+
+
+ Dashboard exited unexpectedly due to an error. See logs for details.
+
+
+ Dashboard configuration is invalid. Check the provided options and try again.
+
+
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+
+
+ Stopping dashboard
+
+
diff --git a/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.cs.xlf
new file mode 100644
index 00000000000..8c0af45e796
--- /dev/null
+++ b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.cs.xlf
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Allow anonymous access to the dashboard
+ Allow anonymous access to the dashboard
+
+
+
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+
+
+
+ The path for a JSON configuration file
+ The path for a JSON configuration file
+
+
+
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+
+
+
+ Dashboard exited unexpectedly due to an error. See logs for details.
+ Dashboard exited unexpectedly due to an error. See logs for details.
+
+
+
+ Dashboard configuration is invalid. Check the provided options and try again.
+ Dashboard configuration is invalid. Check the provided options and try again.
+
+
+
+ Dashboard exited with exit code {0}.
+ Dashboard exited with exit code {0}.
+
+
+
+ Dashboard
+ Dashboard
+
+
+
+ Dashboard did not become ready within the expected time.
+ Dashboard did not become ready within the expected time.
+
+
+
+ Dashboard started (PID {0}).
+ Dashboard started (PID {0}).
+
+
+
+ Start the Aspire dashboard
+ Start the Aspire dashboard
+
+
+
+ One or more HTTP endpoints through which the dashboard frontend is served
+ One or more HTTP endpoints through which the dashboard frontend is served
+
+
+
+ Logs
+ Logs
+
+
+
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ OTLP/gRPC
+ OTLP/gRPC
+
+
+
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+
+
+
+ OTLP/HTTP
+ OTLP/HTTP
+
+
+
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+
+
+
+ Starting dashboard...
+ Starting dashboard...
+
+
+
+ Stopping dashboard
+ Stopping dashboard
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.de.xlf
new file mode 100644
index 00000000000..f330a85a859
--- /dev/null
+++ b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.de.xlf
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Allow anonymous access to the dashboard
+ Allow anonymous access to the dashboard
+
+
+
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+
+
+
+ The path for a JSON configuration file
+ The path for a JSON configuration file
+
+
+
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+
+
+
+ Dashboard exited unexpectedly due to an error. See logs for details.
+ Dashboard exited unexpectedly due to an error. See logs for details.
+
+
+
+ Dashboard configuration is invalid. Check the provided options and try again.
+ Dashboard configuration is invalid. Check the provided options and try again.
+
+
+
+ Dashboard exited with exit code {0}.
+ Dashboard exited with exit code {0}.
+
+
+
+ Dashboard
+ Dashboard
+
+
+
+ Dashboard did not become ready within the expected time.
+ Dashboard did not become ready within the expected time.
+
+
+
+ Dashboard started (PID {0}).
+ Dashboard started (PID {0}).
+
+
+
+ Start the Aspire dashboard
+ Start the Aspire dashboard
+
+
+
+ One or more HTTP endpoints through which the dashboard frontend is served
+ One or more HTTP endpoints through which the dashboard frontend is served
+
+
+
+ Logs
+ Logs
+
+
+
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ OTLP/gRPC
+ OTLP/gRPC
+
+
+
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+
+
+
+ OTLP/HTTP
+ OTLP/HTTP
+
+
+
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+
+
+
+ Starting dashboard...
+ Starting dashboard...
+
+
+
+ Stopping dashboard
+ Stopping dashboard
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.es.xlf
new file mode 100644
index 00000000000..cc9ea1d7feb
--- /dev/null
+++ b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.es.xlf
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Allow anonymous access to the dashboard
+ Allow anonymous access to the dashboard
+
+
+
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+
+
+
+ The path for a JSON configuration file
+ The path for a JSON configuration file
+
+
+
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+
+
+
+ Dashboard exited unexpectedly due to an error. See logs for details.
+ Dashboard exited unexpectedly due to an error. See logs for details.
+
+
+
+ Dashboard configuration is invalid. Check the provided options and try again.
+ Dashboard configuration is invalid. Check the provided options and try again.
+
+
+
+ Dashboard exited with exit code {0}.
+ Dashboard exited with exit code {0}.
+
+
+
+ Dashboard
+ Dashboard
+
+
+
+ Dashboard did not become ready within the expected time.
+ Dashboard did not become ready within the expected time.
+
+
+
+ Dashboard started (PID {0}).
+ Dashboard started (PID {0}).
+
+
+
+ Start the Aspire dashboard
+ Start the Aspire dashboard
+
+
+
+ One or more HTTP endpoints through which the dashboard frontend is served
+ One or more HTTP endpoints through which the dashboard frontend is served
+
+
+
+ Logs
+ Logs
+
+
+
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ OTLP/gRPC
+ OTLP/gRPC
+
+
+
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+
+
+
+ OTLP/HTTP
+ OTLP/HTTP
+
+
+
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+
+
+
+ Starting dashboard...
+ Starting dashboard...
+
+
+
+ Stopping dashboard
+ Stopping dashboard
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.fr.xlf
new file mode 100644
index 00000000000..8b3e5feafd7
--- /dev/null
+++ b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.fr.xlf
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Allow anonymous access to the dashboard
+ Allow anonymous access to the dashboard
+
+
+
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+
+
+
+ The path for a JSON configuration file
+ The path for a JSON configuration file
+
+
+
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+
+
+
+ Dashboard exited unexpectedly due to an error. See logs for details.
+ Dashboard exited unexpectedly due to an error. See logs for details.
+
+
+
+ Dashboard configuration is invalid. Check the provided options and try again.
+ Dashboard configuration is invalid. Check the provided options and try again.
+
+
+
+ Dashboard exited with exit code {0}.
+ Dashboard exited with exit code {0}.
+
+
+
+ Dashboard
+ Dashboard
+
+
+
+ Dashboard did not become ready within the expected time.
+ Dashboard did not become ready within the expected time.
+
+
+
+ Dashboard started (PID {0}).
+ Dashboard started (PID {0}).
+
+
+
+ Start the Aspire dashboard
+ Start the Aspire dashboard
+
+
+
+ One or more HTTP endpoints through which the dashboard frontend is served
+ One or more HTTP endpoints through which the dashboard frontend is served
+
+
+
+ Logs
+ Logs
+
+
+
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ OTLP/gRPC
+ OTLP/gRPC
+
+
+
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+
+
+
+ OTLP/HTTP
+ OTLP/HTTP
+
+
+
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+
+
+
+ Starting dashboard...
+ Starting dashboard...
+
+
+
+ Stopping dashboard
+ Stopping dashboard
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.it.xlf
new file mode 100644
index 00000000000..df07598d875
--- /dev/null
+++ b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.it.xlf
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Allow anonymous access to the dashboard
+ Allow anonymous access to the dashboard
+
+
+
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+
+
+
+ The path for a JSON configuration file
+ The path for a JSON configuration file
+
+
+
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+
+
+
+ Dashboard exited unexpectedly due to an error. See logs for details.
+ Dashboard exited unexpectedly due to an error. See logs for details.
+
+
+
+ Dashboard configuration is invalid. Check the provided options and try again.
+ Dashboard configuration is invalid. Check the provided options and try again.
+
+
+
+ Dashboard exited with exit code {0}.
+ Dashboard exited with exit code {0}.
+
+
+
+ Dashboard
+ Dashboard
+
+
+
+ Dashboard did not become ready within the expected time.
+ Dashboard did not become ready within the expected time.
+
+
+
+ Dashboard started (PID {0}).
+ Dashboard started (PID {0}).
+
+
+
+ Start the Aspire dashboard
+ Start the Aspire dashboard
+
+
+
+ One or more HTTP endpoints through which the dashboard frontend is served
+ One or more HTTP endpoints through which the dashboard frontend is served
+
+
+
+ Logs
+ Logs
+
+
+
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ OTLP/gRPC
+ OTLP/gRPC
+
+
+
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+
+
+
+ OTLP/HTTP
+ OTLP/HTTP
+
+
+
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+
+
+
+ Starting dashboard...
+ Starting dashboard...
+
+
+
+ Stopping dashboard
+ Stopping dashboard
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.ja.xlf
new file mode 100644
index 00000000000..16373b44275
--- /dev/null
+++ b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.ja.xlf
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Allow anonymous access to the dashboard
+ Allow anonymous access to the dashboard
+
+
+
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+
+
+
+ The path for a JSON configuration file
+ The path for a JSON configuration file
+
+
+
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+
+
+
+ Dashboard exited unexpectedly due to an error. See logs for details.
+ Dashboard exited unexpectedly due to an error. See logs for details.
+
+
+
+ Dashboard configuration is invalid. Check the provided options and try again.
+ Dashboard configuration is invalid. Check the provided options and try again.
+
+
+
+ Dashboard exited with exit code {0}.
+ Dashboard exited with exit code {0}.
+
+
+
+ Dashboard
+ Dashboard
+
+
+
+ Dashboard did not become ready within the expected time.
+ Dashboard did not become ready within the expected time.
+
+
+
+ Dashboard started (PID {0}).
+ Dashboard started (PID {0}).
+
+
+
+ Start the Aspire dashboard
+ Start the Aspire dashboard
+
+
+
+ One or more HTTP endpoints through which the dashboard frontend is served
+ One or more HTTP endpoints through which the dashboard frontend is served
+
+
+
+ Logs
+ Logs
+
+
+
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ OTLP/gRPC
+ OTLP/gRPC
+
+
+
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+
+
+
+ OTLP/HTTP
+ OTLP/HTTP
+
+
+
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+
+
+
+ Starting dashboard...
+ Starting dashboard...
+
+
+
+ Stopping dashboard
+ Stopping dashboard
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.ko.xlf
new file mode 100644
index 00000000000..0bef1377ffb
--- /dev/null
+++ b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.ko.xlf
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Allow anonymous access to the dashboard
+ Allow anonymous access to the dashboard
+
+
+
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+
+
+
+ The path for a JSON configuration file
+ The path for a JSON configuration file
+
+
+
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+
+
+
+ Dashboard exited unexpectedly due to an error. See logs for details.
+ Dashboard exited unexpectedly due to an error. See logs for details.
+
+
+
+ Dashboard configuration is invalid. Check the provided options and try again.
+ Dashboard configuration is invalid. Check the provided options and try again.
+
+
+
+ Dashboard exited with exit code {0}.
+ Dashboard exited with exit code {0}.
+
+
+
+ Dashboard
+ Dashboard
+
+
+
+ Dashboard did not become ready within the expected time.
+ Dashboard did not become ready within the expected time.
+
+
+
+ Dashboard started (PID {0}).
+ Dashboard started (PID {0}).
+
+
+
+ Start the Aspire dashboard
+ Start the Aspire dashboard
+
+
+
+ One or more HTTP endpoints through which the dashboard frontend is served
+ One or more HTTP endpoints through which the dashboard frontend is served
+
+
+
+ Logs
+ Logs
+
+
+
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ OTLP/gRPC
+ OTLP/gRPC
+
+
+
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+
+
+
+ OTLP/HTTP
+ OTLP/HTTP
+
+
+
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+
+
+
+ Starting dashboard...
+ Starting dashboard...
+
+
+
+ Stopping dashboard
+ Stopping dashboard
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.pl.xlf
new file mode 100644
index 00000000000..b817ee48693
--- /dev/null
+++ b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.pl.xlf
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Allow anonymous access to the dashboard
+ Allow anonymous access to the dashboard
+
+
+
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+
+
+
+ The path for a JSON configuration file
+ The path for a JSON configuration file
+
+
+
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+
+
+
+ Dashboard exited unexpectedly due to an error. See logs for details.
+ Dashboard exited unexpectedly due to an error. See logs for details.
+
+
+
+ Dashboard configuration is invalid. Check the provided options and try again.
+ Dashboard configuration is invalid. Check the provided options and try again.
+
+
+
+ Dashboard exited with exit code {0}.
+ Dashboard exited with exit code {0}.
+
+
+
+ Dashboard
+ Dashboard
+
+
+
+ Dashboard did not become ready within the expected time.
+ Dashboard did not become ready within the expected time.
+
+
+
+ Dashboard started (PID {0}).
+ Dashboard started (PID {0}).
+
+
+
+ Start the Aspire dashboard
+ Start the Aspire dashboard
+
+
+
+ One or more HTTP endpoints through which the dashboard frontend is served
+ One or more HTTP endpoints through which the dashboard frontend is served
+
+
+
+ Logs
+ Logs
+
+
+
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ OTLP/gRPC
+ OTLP/gRPC
+
+
+
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+
+
+
+ OTLP/HTTP
+ OTLP/HTTP
+
+
+
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+
+
+
+ Starting dashboard...
+ Starting dashboard...
+
+
+
+ Stopping dashboard
+ Stopping dashboard
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.pt-BR.xlf
new file mode 100644
index 00000000000..5a44d14dca7
--- /dev/null
+++ b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.pt-BR.xlf
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Allow anonymous access to the dashboard
+ Allow anonymous access to the dashboard
+
+
+
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+
+
+
+ The path for a JSON configuration file
+ The path for a JSON configuration file
+
+
+
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+
+
+
+ Dashboard exited unexpectedly due to an error. See logs for details.
+ Dashboard exited unexpectedly due to an error. See logs for details.
+
+
+
+ Dashboard configuration is invalid. Check the provided options and try again.
+ Dashboard configuration is invalid. Check the provided options and try again.
+
+
+
+ Dashboard exited with exit code {0}.
+ Dashboard exited with exit code {0}.
+
+
+
+ Dashboard
+ Dashboard
+
+
+
+ Dashboard did not become ready within the expected time.
+ Dashboard did not become ready within the expected time.
+
+
+
+ Dashboard started (PID {0}).
+ Dashboard started (PID {0}).
+
+
+
+ Start the Aspire dashboard
+ Start the Aspire dashboard
+
+
+
+ One or more HTTP endpoints through which the dashboard frontend is served
+ One or more HTTP endpoints through which the dashboard frontend is served
+
+
+
+ Logs
+ Logs
+
+
+
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ OTLP/gRPC
+ OTLP/gRPC
+
+
+
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+
+
+
+ OTLP/HTTP
+ OTLP/HTTP
+
+
+
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+
+
+
+ Starting dashboard...
+ Starting dashboard...
+
+
+
+ Stopping dashboard
+ Stopping dashboard
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.ru.xlf
new file mode 100644
index 00000000000..b9e70ef110a
--- /dev/null
+++ b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.ru.xlf
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Allow anonymous access to the dashboard
+ Allow anonymous access to the dashboard
+
+
+
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+
+
+
+ The path for a JSON configuration file
+ The path for a JSON configuration file
+
+
+
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+
+
+
+ Dashboard exited unexpectedly due to an error. See logs for details.
+ Dashboard exited unexpectedly due to an error. See logs for details.
+
+
+
+ Dashboard configuration is invalid. Check the provided options and try again.
+ Dashboard configuration is invalid. Check the provided options and try again.
+
+
+
+ Dashboard exited with exit code {0}.
+ Dashboard exited with exit code {0}.
+
+
+
+ Dashboard
+ Dashboard
+
+
+
+ Dashboard did not become ready within the expected time.
+ Dashboard did not become ready within the expected time.
+
+
+
+ Dashboard started (PID {0}).
+ Dashboard started (PID {0}).
+
+
+
+ Start the Aspire dashboard
+ Start the Aspire dashboard
+
+
+
+ One or more HTTP endpoints through which the dashboard frontend is served
+ One or more HTTP endpoints through which the dashboard frontend is served
+
+
+
+ Logs
+ Logs
+
+
+
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ OTLP/gRPC
+ OTLP/gRPC
+
+
+
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+
+
+
+ OTLP/HTTP
+ OTLP/HTTP
+
+
+
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+
+
+
+ Starting dashboard...
+ Starting dashboard...
+
+
+
+ Stopping dashboard
+ Stopping dashboard
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.tr.xlf
new file mode 100644
index 00000000000..4db5a2ecb43
--- /dev/null
+++ b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.tr.xlf
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Allow anonymous access to the dashboard
+ Allow anonymous access to the dashboard
+
+
+
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+
+
+
+ The path for a JSON configuration file
+ The path for a JSON configuration file
+
+
+
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+
+
+
+ Dashboard exited unexpectedly due to an error. See logs for details.
+ Dashboard exited unexpectedly due to an error. See logs for details.
+
+
+
+ Dashboard configuration is invalid. Check the provided options and try again.
+ Dashboard configuration is invalid. Check the provided options and try again.
+
+
+
+ Dashboard exited with exit code {0}.
+ Dashboard exited with exit code {0}.
+
+
+
+ Dashboard
+ Dashboard
+
+
+
+ Dashboard did not become ready within the expected time.
+ Dashboard did not become ready within the expected time.
+
+
+
+ Dashboard started (PID {0}).
+ Dashboard started (PID {0}).
+
+
+
+ Start the Aspire dashboard
+ Start the Aspire dashboard
+
+
+
+ One or more HTTP endpoints through which the dashboard frontend is served
+ One or more HTTP endpoints through which the dashboard frontend is served
+
+
+
+ Logs
+ Logs
+
+
+
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ OTLP/gRPC
+ OTLP/gRPC
+
+
+
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+
+
+
+ OTLP/HTTP
+ OTLP/HTTP
+
+
+
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+
+
+
+ Starting dashboard...
+ Starting dashboard...
+
+
+
+ Stopping dashboard
+ Stopping dashboard
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.zh-Hans.xlf
new file mode 100644
index 00000000000..27a4192f4be
--- /dev/null
+++ b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.zh-Hans.xlf
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Allow anonymous access to the dashboard
+ Allow anonymous access to the dashboard
+
+
+
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+
+
+
+ The path for a JSON configuration file
+ The path for a JSON configuration file
+
+
+
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+
+
+
+ Dashboard exited unexpectedly due to an error. See logs for details.
+ Dashboard exited unexpectedly due to an error. See logs for details.
+
+
+
+ Dashboard configuration is invalid. Check the provided options and try again.
+ Dashboard configuration is invalid. Check the provided options and try again.
+
+
+
+ Dashboard exited with exit code {0}.
+ Dashboard exited with exit code {0}.
+
+
+
+ Dashboard
+ Dashboard
+
+
+
+ Dashboard did not become ready within the expected time.
+ Dashboard did not become ready within the expected time.
+
+
+
+ Dashboard started (PID {0}).
+ Dashboard started (PID {0}).
+
+
+
+ Start the Aspire dashboard
+ Start the Aspire dashboard
+
+
+
+ One or more HTTP endpoints through which the dashboard frontend is served
+ One or more HTTP endpoints through which the dashboard frontend is served
+
+
+
+ Logs
+ Logs
+
+
+
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ OTLP/gRPC
+ OTLP/gRPC
+
+
+
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+
+
+
+ OTLP/HTTP
+ OTLP/HTTP
+
+
+
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+
+
+
+ Starting dashboard...
+ Starting dashboard...
+
+
+
+ Stopping dashboard
+ Stopping dashboard
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.zh-Hant.xlf
new file mode 100644
index 00000000000..21ef6a847c4
--- /dev/null
+++ b/src/Aspire.Cli/Resources/xlf/DashboardCommandStrings.zh-Hant.xlf
@@ -0,0 +1,112 @@
+
+
+
+
+
+ Allow anonymous access to the dashboard
+ Allow anonymous access to the dashboard
+
+
+
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+ The 'aspire dashboard' command requires the CLI bundle, but no bundle layout was found. The bundle provides the dashboard binary and supporting files. Run 'aspire setup --force' to extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+ The Aspire dashboard requires the CLI bundle. The bundle was not found.
+
+
+
+ The path for a JSON configuration file
+ The path for a JSON configuration file
+
+
+
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+ Dashboard could not start because a configured port is already in use. Use a different port or stop the conflicting process.
+
+
+
+ Dashboard exited unexpectedly due to an error. See logs for details.
+ Dashboard exited unexpectedly due to an error. See logs for details.
+
+
+
+ Dashboard configuration is invalid. Check the provided options and try again.
+ Dashboard configuration is invalid. Check the provided options and try again.
+
+
+
+ Dashboard exited with exit code {0}.
+ Dashboard exited with exit code {0}.
+
+
+
+ Dashboard
+ Dashboard
+
+
+
+ Dashboard did not become ready within the expected time.
+ Dashboard did not become ready within the expected time.
+
+
+
+ Dashboard started (PID {0}).
+ Dashboard started (PID {0}).
+
+
+
+ Start the Aspire dashboard
+ Start the Aspire dashboard
+
+
+
+ One or more HTTP endpoints through which the dashboard frontend is served
+ One or more HTTP endpoints through which the dashboard frontend is served
+
+
+
+ Logs
+ Logs
+
+
+
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+ The CLI bundle layout was found, but the dashboard binary (aspire-managed) is missing. The bundle may be corrupted or incomplete. Run 'aspire setup --force' to re-extract the bundle, or reinstall the Aspire CLI.
+
+
+
+ OTLP/gRPC
+ OTLP/gRPC
+
+
+
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+ The OTLP/gRPC endpoint. This endpoint hosts an OTLP service and receives telemetry using gRPC
+
+
+
+ OTLP/HTTP
+ OTLP/HTTP
+
+
+
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+ The OTLP/HTTP endpoint. This endpoint hosts an OTLP service and receives telemetry using Protobuf over HTTP
+
+
+
+ Starting dashboard...
+ Starting dashboard...
+
+
+
+ Stopping dashboard
+ Stopping dashboard
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs b/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs
index a4788202094..30380beed72 100644
--- a/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs
+++ b/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs
@@ -496,7 +496,7 @@ private async Task ApplyTemplateAsync(CallbackTemplate template,
TemplatingStrings.GettingTemplates,
async () =>
{
- var options = new DotNetCliRunnerInvocationOptions()
+ var options = new ProcessInvocationOptions()
{
StandardOutputCallback = templateInstallCollector.AppendOutput,
StandardErrorCallback = templateInstallCollector.AppendOutput,
@@ -533,7 +533,7 @@ private async Task ApplyTemplateAsync(CallbackTemplate template,
TemplatingStrings.CreatingNewProject,
async () =>
{
- var options = new DotNetCliRunnerInvocationOptions()
+ var options = new ProcessInvocationOptions()
{
StandardOutputCallback = newProjectCollector.AppendOutput,
StandardErrorCallback = newProjectCollector.AppendOutput,
diff --git a/src/Aspire.Cli/Utils/AppHostHelper.cs b/src/Aspire.Cli/Utils/AppHostHelper.cs
index f080a53e909..5ecf5b881c1 100644
--- a/src/Aspire.Cli/Utils/AppHostHelper.cs
+++ b/src/Aspire.Cli/Utils/AppHostHelper.cs
@@ -59,14 +59,14 @@ internal static class AppHostHelper
$"{InteractionServiceStrings.CheckingProjectType}: {relativePath}",
() => runner.GetAppHostInformationAsync(
projectFile,
- new DotNetCliRunnerInvocationOptions(),
+ new ProcessInvocationOptions(),
cancellationToken),
emoji: KnownEmojis.Microscope);
return appHostInformationResult;
}
- internal static async Task BuildAppHostAsync(IDotNetCliRunner runner, IInteractionService interactionService, FileInfo projectFile, bool noRestore, DotNetCliRunnerInvocationOptions options, DirectoryInfo workingDirectory, CancellationToken cancellationToken)
+ internal static async Task BuildAppHostAsync(IDotNetCliRunner runner, IInteractionService interactionService, FileInfo projectFile, bool noRestore, ProcessInvocationOptions options, DirectoryInfo workingDirectory, CancellationToken cancellationToken)
{
var relativePath = Path.GetRelativePath(workingDirectory.FullName, projectFile.FullName);
return await interactionService.ShowStatusAsync(
diff --git a/src/Aspire.Dashboard/Aspire.Dashboard.csproj b/src/Aspire.Dashboard/Aspire.Dashboard.csproj
index 33fb1008644..64031db7068 100644
--- a/src/Aspire.Dashboard/Aspire.Dashboard.csproj
+++ b/src/Aspire.Dashboard/Aspire.Dashboard.csproj
@@ -298,6 +298,7 @@
+
diff --git a/src/Aspire.Dashboard/DashboardWebApplication.cs b/src/Aspire.Dashboard/DashboardWebApplication.cs
index e7dbbd35382..c2f0539fe85 100644
--- a/src/Aspire.Dashboard/DashboardWebApplication.cs
+++ b/src/Aspire.Dashboard/DashboardWebApplication.cs
@@ -3,6 +3,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
+using System.Net.Sockets;
using System.Reflection;
using System.Security.Claims;
using System.Security.Cryptography;
@@ -46,6 +47,21 @@ namespace Aspire.Dashboard;
public sealed class DashboardWebApplication : IAsyncDisposable
{
+ ///
+ /// Exit code returned for unexpected startup errors.
+ ///
+ public const int ExitCodeUnexpectedError = DashboardExitCodes.UnexpectedError;
+
+ ///
+ /// Exit code returned when dashboard configuration is invalid.
+ ///
+ public const int ExitCodeValidationFailure = DashboardExitCodes.ValidationFailure;
+
+ ///
+ /// Exit code returned when the configured address is already in use.
+ ///
+ public const int ExitCodeAddressInUse = DashboardExitCodes.AddressInUse;
+
private const string DashboardAuthCookieName = ".Aspire.Dashboard.Auth";
private const string DashboardAntiForgeryCookieName = ".Aspire.Dashboard.Antiforgery";
private readonly WebApplication _app;
@@ -896,11 +912,37 @@ public int Run()
{
if (_validationFailures.Count > 0)
{
- return -1;
+ return ExitCodeValidationFailure;
+ }
+
+ try
+ {
+ _app.Run();
+ return 0;
+ }
+ catch (IOException ex) when (ContainsAddressInUse(ex))
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return ExitCodeAddressInUse;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return ExitCodeUnexpectedError;
+ }
+ }
+
+ private static bool ContainsAddressInUse(Exception ex)
+ {
+ for (var current = ex.InnerException; current is not null; current = current.InnerException)
+ {
+ if (current is SocketException { SocketErrorCode: SocketError.AddressAlreadyInUse })
+ {
+ return true;
+ }
}
- _app.Run();
- return 0;
+ return false;
}
public Task StartAsync(CancellationToken cancellationToken = default)
diff --git a/src/Shared/DashboardExitCodes.cs b/src/Shared/DashboardExitCodes.cs
new file mode 100644
index 00000000000..a42bf0a27a1
--- /dev/null
+++ b/src/Shared/DashboardExitCodes.cs
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting;
+
+internal static class DashboardExitCodes
+{
+ public const int UnexpectedError = 1;
+ public const int ValidationFailure = 2;
+ public const int AddressInUse = 3;
+}
diff --git a/tests/Aspire.Cli.Tests/Commands/DashboardCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/DashboardCommandTests.cs
new file mode 100644
index 00000000000..b64bcffdb1e
--- /dev/null
+++ b/tests/Aspire.Cli.Tests/Commands/DashboardCommandTests.cs
@@ -0,0 +1,440 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Cli.Commands;
+using Aspire.Cli.Layout;
+using Aspire.Cli.Resources;
+using Aspire.Cli.Tests.TestServices;
+using Aspire.Cli.Tests.Utils;
+using Aspire.Shared;
+using Microsoft.AspNetCore.InternalTesting;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Aspire.Cli.Tests.Commands;
+
+public class DashboardCommandTests(ITestOutputHelper outputHelper)
+{
+ [Fact]
+ public async Task DashboardCommand_BundleNotAvailable_DisplaysError()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+
+ var testInteractionService = new TestInteractionService();
+
+ var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
+ {
+ options.InteractionServiceFactory = _ => testInteractionService;
+ });
+
+ var provider = services.BuildServiceProvider();
+ var command = provider.GetRequiredService();
+ var result = command.Parse("dashboard");
+
+ var exitCode = await result.InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(ExitCodeConstants.DashboardFailure, exitCode);
+ var errorMessage = Assert.Single(testInteractionService.DisplayedErrors);
+ Assert.Equal(DashboardCommandStrings.BundleLayoutNotFound, errorMessage);
+ }
+
+ [Fact]
+ public async Task DashboardCommand_Help_ReturnsSuccess()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
+ var provider = services.BuildServiceProvider();
+
+ var command = provider.GetRequiredService();
+ var result = command.Parse("dashboard --help");
+
+ var exitCode = await result.InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(ExitCodeConstants.Success, exitCode);
+ }
+
+ [Theory]
+ [InlineData("--frontend-url http://localhost:5000")]
+ [InlineData("--otlp-grpc-url http://localhost:4317")]
+ [InlineData("--otlp-http-url http://localhost:4318")]
+ [InlineData("--allow-anonymous")]
+ [InlineData("--config-file-path /path/to/config.json")]
+ public void DashboardCommand_ParsesOptionsWithoutErrors(string args)
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
+ var provider = services.BuildServiceProvider();
+
+ var command = provider.GetRequiredService();
+ var result = command.Parse($"dashboard {args}");
+
+ Assert.Empty(result.Errors);
+ }
+
+ [Fact]
+ public void DashboardCommand_ForwardsUnmatchedTokens()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
+ var provider = services.BuildServiceProvider();
+
+ var command = provider.GetRequiredService();
+ var result = command.Parse("dashboard --ASPNETCORE_URLS=http://localhost:9999");
+
+ Assert.Empty(result.Errors);
+ Assert.Equal("--ASPNETCORE_URLS=http://localhost:9999", Assert.Single(result.UnmatchedTokens));
+ }
+
+ [Fact]
+ public void DashboardCommand_SkipsDefaultWhenEnvVarIsSet()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+
+ var envVars = new Dictionary
+ {
+ ["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = "http://custom:9999"
+ };
+ var executionContext = CreateExecutionContext(workspace, envVars);
+
+ var unmatchedTokens = Array.Empty();
+
+ Assert.True(DashboardCommand.ConfigSettingHasValue(unmatchedTokens, executionContext, "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"));
+ Assert.False(DashboardCommand.ConfigSettingHasValue(unmatchedTokens, executionContext, "ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL"));
+ }
+
+ [Fact]
+ public void DashboardCommand_SkipsDefaultWhenUnmatchedTokenHasValue()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+
+ var executionContext = CreateExecutionContext(workspace, new Dictionary());
+
+ var unmatchedTokens = new[] { "--ASPNETCORE_URLS=http://localhost:9999" };
+
+ Assert.True(DashboardCommand.ConfigSettingHasValue(unmatchedTokens, executionContext, "ASPNETCORE_URLS"));
+ Assert.False(DashboardCommand.ConfigSettingHasValue(unmatchedTokens, executionContext, "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"));
+ }
+
+ [Fact]
+ public void DashboardCommand_SkipsDefaultWhenUnmatchedTokenHasSpaceSeparatedValue()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+
+ var executionContext = CreateExecutionContext(workspace, new Dictionary());
+
+ var unmatchedTokens = new[] { "--ASPNETCORE_URLS", "http://localhost:9999" };
+
+ Assert.True(DashboardCommand.ConfigSettingHasValue(unmatchedTokens, executionContext, "ASPNETCORE_URLS"));
+ }
+
+ [Fact]
+ public async Task DashboardCommand_DefaultOptions_DoesNotEmitAllowAnonymous()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+
+ string[]? capturedArgs = null;
+ var (services, _, executionFactory) = CreateServicesWithLayout(workspace);
+ executionFactory.AssertionCallback = (args, _, _, _) => { capturedArgs = args; };
+
+ var provider = services.BuildServiceProvider();
+ var command = provider.GetRequiredService();
+ var result = command.Parse("dashboard");
+
+ var exitCode = await result.InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(ExitCodeConstants.Success, exitCode);
+ Assert.NotNull(capturedArgs);
+ Assert.DoesNotContain(capturedArgs, arg => arg.Contains("ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS"));
+ }
+
+ [Fact]
+ public async Task DashboardCommand_DefaultOptions_PassesDefaultArgsToProcess()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+
+ string[]? capturedArgs = null;
+ var (services, _, executionFactory) = CreateServicesWithLayout(workspace);
+ executionFactory.AssertionCallback = (args, _, _, _) => { capturedArgs = args; };
+
+ var provider = services.BuildServiceProvider();
+ var command = provider.GetRequiredService();
+ var result = command.Parse("dashboard");
+
+ var exitCode = await result.InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(ExitCodeConstants.Success, exitCode);
+ Assert.NotNull(capturedArgs);
+ Assert.Collection(capturedArgs,
+ arg => Assert.Equal("dashboard", arg),
+ arg => Assert.Equal("--ASPNETCORE_URLS=http://localhost:18888", arg),
+ arg => Assert.Equal("--ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL=http://localhost:4317", arg),
+ arg => Assert.Equal("--ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL=http://localhost:4318", arg));
+ }
+
+ [Theory]
+ [InlineData("--frontend-url http://localhost:5000", "--ASPNETCORE_URLS=http://localhost:5000")]
+ [InlineData("--otlp-grpc-url http://localhost:9317", "--ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL=http://localhost:9317")]
+ [InlineData("--otlp-http-url http://localhost:9318", "--ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL=http://localhost:9318")]
+ [InlineData("--allow-anonymous", "--ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true")]
+ [InlineData("--config-file-path /path/to/config.json", "--ASPIRE_DASHBOARD_CONFIG_FILE_PATH=/path/to/config.json")]
+ public async Task DashboardCommand_IndividualOption_PassesCorrectArgToProcess(string cliArgs, string expectedArg)
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+
+ string[]? capturedArgs = null;
+ var (services, _, executionFactory) = CreateServicesWithLayout(workspace);
+ executionFactory.AssertionCallback = (args, _, _, _) => { capturedArgs = args; };
+
+ var provider = services.BuildServiceProvider();
+ var command = provider.GetRequiredService();
+ var result = command.Parse($"dashboard {cliArgs}");
+
+ var exitCode = await result.InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(ExitCodeConstants.Success, exitCode);
+ Assert.NotNull(capturedArgs);
+ Assert.Contains(expectedArg, capturedArgs);
+ }
+
+ [Fact]
+ public async Task DashboardCommand_WithoutAllowAnonymous_SetsBrowserTokenEnvVar()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+
+ IDictionary? capturedEnv = null;
+ var (services, _, executionFactory) = CreateServicesWithLayout(workspace);
+ executionFactory.AssertionCallback = (_, env, _, _) => { capturedEnv = env; };
+
+ var provider = services.BuildServiceProvider();
+ var command = provider.GetRequiredService();
+ var result = command.Parse("dashboard");
+
+ var exitCode = await result.InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(ExitCodeConstants.Success, exitCode);
+ Assert.NotNull(capturedEnv);
+ Assert.True(capturedEnv.ContainsKey("DASHBOARD__FRONTEND__BROWSERTOKEN"));
+ Assert.False(string.IsNullOrEmpty(capturedEnv["DASHBOARD__FRONTEND__BROWSERTOKEN"]));
+ }
+
+ [Fact]
+ public async Task DashboardCommand_UnmatchedTokens_ForwardedToProcess()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+
+ string[]? capturedArgs = null;
+ var (services, _, executionFactory) = CreateServicesWithLayout(workspace);
+ executionFactory.AssertionCallback = (args, _, _, _) => { capturedArgs = args; };
+
+ var provider = services.BuildServiceProvider();
+ var command = provider.GetRequiredService();
+ var result = command.Parse("dashboard --CUSTOM_SETTING=myvalue");
+
+ var exitCode = await result.InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(ExitCodeConstants.Success, exitCode);
+ Assert.NotNull(capturedArgs);
+ Assert.Collection(capturedArgs,
+ arg => Assert.Equal("dashboard", arg),
+ arg => Assert.Equal("--ASPNETCORE_URLS=http://localhost:18888", arg),
+ arg => Assert.Equal("--ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL=http://localhost:4317", arg),
+ arg => Assert.Equal("--ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL=http://localhost:4318", arg),
+ arg => Assert.Equal("--CUSTOM_SETTING=myvalue", arg));
+ }
+
+ [Fact]
+ public async Task DashboardCommand_CombinedOptions_PassesAllArgsToProcess()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+
+ string[]? capturedArgs = null;
+ var (services, _, executionFactory) = CreateServicesWithLayout(workspace);
+ executionFactory.AssertionCallback = (args, _, _, _) => { capturedArgs = args; };
+
+ var provider = services.BuildServiceProvider();
+ var command = provider.GetRequiredService();
+ var result = command.Parse("dashboard --frontend-url http://localhost:5000 --otlp-grpc-url http://localhost:9317 --otlp-http-url http://localhost:9318 --allow-anonymous --config-file-path /my/config.json");
+
+ var exitCode = await result.InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(ExitCodeConstants.Success, exitCode);
+ Assert.NotNull(capturedArgs);
+ Assert.Collection(capturedArgs,
+ arg => Assert.Equal("dashboard", arg),
+ arg => Assert.Equal("--ASPNETCORE_URLS=http://localhost:5000", arg),
+ arg => Assert.Equal("--ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL=http://localhost:9317", arg),
+ arg => Assert.Equal("--ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL=http://localhost:9318", arg),
+ arg => Assert.Equal("--ASPIRE_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true", arg),
+ arg => Assert.Equal("--ASPIRE_DASHBOARD_CONFIG_FILE_PATH=/my/config.json", arg));
+ }
+
+ [Fact]
+ public async Task DashboardCommand_ProcessExitsWithError_ReturnsFailure()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+
+ var testInteractionService = new TestInteractionService();
+ var (services, _, executionFactory) = CreateServicesWithLayout(workspace, interactionService: testInteractionService);
+ executionFactory.AttemptCallback = (_, _) => (1, null);
+
+ var provider = services.BuildServiceProvider();
+ var command = provider.GetRequiredService();
+ var result = command.Parse("dashboard");
+
+ var exitCode = await result.InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(ExitCodeConstants.DashboardFailure, exitCode);
+ }
+
+ [Theory]
+ [InlineData("", "http://localhost:18888")]
+ [InlineData(";;;", "http://localhost:18888")]
+ [InlineData("http://first:5000;http://second:5001", "http://first:5000")]
+ [InlineData("http://custom:9000", "http://custom:9000")]
+ [InlineData("http://trailing:8080/", "http://trailing:8080")]
+ public void ResolveDashboardInfo_FrontendUrlVariants_ResolvesExpectedUrl(string urlValue, string expectedDashboardUrl)
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var executionContext = CreateExecutionContext(workspace, new Dictionary());
+
+ var args = new List { "dashboard", $"--ASPNETCORE_URLS={urlValue}" };
+ var unmatchedTokens = Array.Empty();
+
+ var info = DashboardCommand.ResolveDashboardInfo(args, unmatchedTokens, executionContext, browserToken: null);
+
+ Assert.Equal(expectedDashboardUrl, info.DashboardUrl);
+ }
+
+ [Fact]
+ public void ResolveDashboardInfo_EnvVarFrontendUrl_UsedInSummary()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var executionContext = CreateExecutionContext(workspace, new Dictionary
+ {
+ ["ASPNETCORE_URLS"] = "http://envhost:9999"
+ });
+
+ // No arg in the list — should fall back to the environment variable.
+ var args = new List { "dashboard" };
+ var unmatchedTokens = Array.Empty();
+
+ var info = DashboardCommand.ResolveDashboardInfo(args, unmatchedTokens, executionContext, browserToken: null);
+
+ Assert.Equal("http://envhost:9999", info.DashboardUrl);
+ }
+
+ [Fact]
+ public void ResolveDashboardInfo_SpaceSeparatedUnmatchedToken_UsedInSummary()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var executionContext = CreateExecutionContext(workspace, new Dictionary());
+
+ // No --ASPNETCORE_URLS= in the args list; the value comes through space-separated unmatched tokens.
+ var args = new List { "dashboard" };
+ var unmatchedTokens = new[] { "--ASPNETCORE_URLS", "http://space:7777" };
+
+ var info = DashboardCommand.ResolveDashboardInfo(args, unmatchedTokens, executionContext, browserToken: null);
+
+ Assert.Equal("http://space:7777", info.DashboardUrl);
+ }
+
+ [Fact]
+ public void ResolveDashboardInfo_EnvVarOtlpUrls_UsedInSummary()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var executionContext = CreateExecutionContext(workspace, new Dictionary
+ {
+ ["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = "http://grpc:1111",
+ ["ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL"] = "http://http:2222"
+ });
+
+ var args = new List { "dashboard" };
+ var unmatchedTokens = Array.Empty();
+
+ var info = DashboardCommand.ResolveDashboardInfo(args, unmatchedTokens, executionContext, browserToken: null);
+
+ Assert.Equal("http://grpc:1111", info.OtlpGrpcUrl);
+ Assert.Equal("http://http:2222", info.OtlpHttpUrl);
+ }
+
+ [Fact]
+ public void ResolveDashboardInfo_ArgTakesPrecedenceOverEnvVar()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var executionContext = CreateExecutionContext(workspace, new Dictionary
+ {
+ ["ASPNETCORE_URLS"] = "http://envhost:9999"
+ });
+
+ var args = new List { "dashboard", "--ASPNETCORE_URLS=http://arghost:5555" };
+ var unmatchedTokens = Array.Empty();
+
+ var info = DashboardCommand.ResolveDashboardInfo(args, unmatchedTokens, executionContext, browserToken: null);
+
+ Assert.Equal("http://arghost:5555", info.DashboardUrl);
+ }
+
+ [Fact]
+ public void ResolveDashboardInfo_WithBrowserToken_AppendsLoginPath()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var executionContext = CreateExecutionContext(workspace, new Dictionary());
+
+ var args = new List { "dashboard", "--ASPNETCORE_URLS=http://localhost:18888" };
+ var unmatchedTokens = Array.Empty();
+
+ var info = DashboardCommand.ResolveDashboardInfo(args, unmatchedTokens, executionContext, browserToken: "abc123");
+
+ Assert.Equal("http://localhost:18888/login?t=abc123", info.DashboardUrl);
+ }
+
+ private (IServiceCollection Services, string ManagedPath, TestProcessExecutionFactory ExecutionFactory) CreateServicesWithLayout(
+ TemporaryWorkspace workspace,
+ TestInteractionService? interactionService = null)
+ {
+ var layoutDir = Path.Combine(workspace.WorkspaceRoot.FullName, "layout");
+ var managedDir = Path.Combine(layoutDir, "managed");
+ Directory.CreateDirectory(managedDir);
+ var managedPath = Path.Combine(managedDir, BundleDiscovery.GetExecutableFileName("aspire-managed"));
+ File.WriteAllText(managedPath, "fake");
+
+ var layout = new LayoutConfiguration
+ {
+ LayoutPath = layoutDir,
+ Components = new LayoutComponents { Managed = "managed" }
+ };
+
+ var executionFactory = new TestProcessExecutionFactory
+ {
+ AttemptCallback = (_, _) => (0, "Now listening on: http://localhost:18888")
+ };
+
+ var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
+ {
+ options.LayoutDiscoveryFactory = _ => new FakeLayoutDiscovery(layout);
+ options.DotNetCliExecutionFactoryFactory = _ => executionFactory;
+ if (interactionService is not null)
+ {
+ options.InteractionServiceFactory = _ => interactionService;
+ }
+ });
+
+ return (services, managedPath, executionFactory);
+ }
+
+ private static CliExecutionContext CreateExecutionContext(TemporaryWorkspace workspace, Dictionary envVars)
+ {
+ var dir = workspace.WorkspaceRoot;
+ var hivesDir = new DirectoryInfo(Path.Combine(dir.FullName, ".aspire", "hives"));
+ var cacheDir = new DirectoryInfo(Path.Combine(dir.FullName, ".aspire", "cache"));
+ var logsDir = new DirectoryInfo(Path.Combine(dir.FullName, ".aspire", "logs"));
+ var logFile = Path.Combine(logsDir.FullName, "test.log");
+ return new CliExecutionContext(dir, hivesDir, cacheDir, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-sdks")), logsDir, logFile, environmentVariables: envVars);
+ }
+
+ private sealed class FakeLayoutDiscovery(LayoutConfiguration layout) : ILayoutDiscovery
+ {
+ public LayoutConfiguration? DiscoverLayout(string? projectDirectory = null) => layout;
+ public string? GetComponentPath(LayoutComponent component, string? projectDirectory = null) => layout.GetComponentPath(component);
+ public bool IsBundleModeAvailable(string? projectDirectory = null) => true;
+ }
+}
diff --git a/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs
index d8b0a243b4a..27efdc26b39 100644
--- a/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs
+++ b/tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs
@@ -478,7 +478,7 @@ public async Task AppHostHelper_BuildAppHostAsync_IncludesRelativePathInStatusMe
var appHostProjectFile = new FileInfo(appHostProjectPath);
File.WriteAllText(appHostProjectFile.FullName, "");
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
await AppHostHelper.BuildAppHostAsync(testRunner, testInteractionService, appHostProjectFile, noRestore: false, options, workspace.WorkspaceRoot, CancellationToken.None).DefaultTimeout();
}
@@ -941,7 +941,7 @@ public async Task DotNetCliRunner_RunAsync_WhenWatchIsTrue_IncludesNonInteractiv
var provider = services.BuildServiceProvider();
var logger = provider.GetRequiredService>();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = new CliExecutionContext(
workingDirectory: workspace.WorkspaceRoot, hivesDirectory: workspace.WorkspaceRoot.CreateSubdirectory(".aspire").CreateSubdirectory("hives"), cacheDirectory: workspace.WorkspaceRoot.CreateSubdirectory(".aspire").CreateSubdirectory("cache"), sdksDirectory: new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-sdks")), logsDirectory: new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), logFilePath: "test.log"
@@ -991,7 +991,7 @@ public async Task DotNetCliRunner_RunAsync_WhenWatchIsFalse_DoesNotIncludeNonInt
var provider = services.BuildServiceProvider();
var logger = provider.GetRequiredService>();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = new CliExecutionContext(
workingDirectory: workspace.WorkspaceRoot, hivesDirectory: workspace.WorkspaceRoot.CreateSubdirectory(".aspire").CreateSubdirectory("hives"), cacheDirectory: workspace.WorkspaceRoot.CreateSubdirectory(".aspire").CreateSubdirectory("cache"), sdksDirectory: new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-sdks")), logsDirectory: new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), logFilePath: "test.log"
@@ -1037,7 +1037,7 @@ public async Task DotNetCliRunner_RunAsync_WhenWatchIsTrueAndDebugIsTrue_Include
var provider = services.BuildServiceProvider();
var logger = provider.GetRequiredService>();
- var options = new DotNetCliRunnerInvocationOptions { Debug = true };
+ var options = new ProcessInvocationOptions { Debug = true };
var executionContext = new CliExecutionContext(
workingDirectory: workspace.WorkspaceRoot, hivesDirectory: workspace.WorkspaceRoot.CreateSubdirectory(".aspire").CreateSubdirectory("hives"), cacheDirectory: workspace.WorkspaceRoot.CreateSubdirectory(".aspire").CreateSubdirectory("cache"), sdksDirectory: new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-sdks")), logsDirectory: new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), logFilePath: "test.log"
@@ -1087,7 +1087,7 @@ public async Task DotNetCliRunner_RunAsync_WhenWatchIsTrueAndDebugIsFalse_DoesNo
var provider = services.BuildServiceProvider();
var logger = provider.GetRequiredService>();
- var options = new DotNetCliRunnerInvocationOptions { Debug = false };
+ var options = new ProcessInvocationOptions { Debug = false };
var executionContext = new CliExecutionContext(
workingDirectory: workspace.WorkspaceRoot, hivesDirectory: workspace.WorkspaceRoot.CreateSubdirectory(".aspire").CreateSubdirectory("hives"), cacheDirectory: workspace.WorkspaceRoot.CreateSubdirectory(".aspire").CreateSubdirectory("cache"), sdksDirectory: new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-sdks")), logsDirectory: new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), logFilePath: "test.log"
@@ -1132,7 +1132,7 @@ public async Task DotNetCliRunner_RunAsync_WhenWatchIsFalseAndDebugIsTrue_DoesNo
var provider = services.BuildServiceProvider();
var logger = provider.GetRequiredService>();
- var options = new DotNetCliRunnerInvocationOptions { Debug = true };
+ var options = new ProcessInvocationOptions { Debug = true };
var executionContext = new CliExecutionContext(
workingDirectory: workspace.WorkspaceRoot, hivesDirectory: workspace.WorkspaceRoot.CreateSubdirectory(".aspire").CreateSubdirectory("hives"), cacheDirectory: workspace.WorkspaceRoot.CreateSubdirectory(".aspire").CreateSubdirectory("cache"), sdksDirectory: new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-sdks")), logsDirectory: new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), logFilePath: "test.log"
@@ -1178,7 +1178,7 @@ public async Task DotNetCliRunner_RunAsync_WhenWatchIsTrue_SetsSuppressLaunchBro
var provider = services.BuildServiceProvider();
var logger = provider.GetRequiredService>();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = new CliExecutionContext(
workingDirectory: workspace.WorkspaceRoot, hivesDirectory: workspace.WorkspaceRoot.CreateSubdirectory(".aspire").CreateSubdirectory("hives"), cacheDirectory: workspace.WorkspaceRoot.CreateSubdirectory(".aspire").CreateSubdirectory("cache"), sdksDirectory: new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-sdks")), logsDirectory: new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), logFilePath: "test.log"
@@ -1224,7 +1224,7 @@ public async Task DotNetCliRunner_RunAsync_WhenWatchIsFalse_DoesNotSetSuppressLa
var provider = services.BuildServiceProvider();
var logger = provider.GetRequiredService>();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = new CliExecutionContext(
workingDirectory: workspace.WorkspaceRoot, hivesDirectory: workspace.WorkspaceRoot.CreateSubdirectory(".aspire").CreateSubdirectory("hives"), cacheDirectory: workspace.WorkspaceRoot.CreateSubdirectory(".aspire").CreateSubdirectory("cache"), sdksDirectory: new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-sdks")), logsDirectory: new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-logs")), logFilePath: "test.log"
diff --git a/tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs b/tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs
index a7974739d47..8049ecb658f 100644
--- a/tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs
+++ b/tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs
@@ -35,7 +35,7 @@ public async Task DotNetCliCorrectlyAppliesNoLaunchProfileArgumentWhenSpecifiedI
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions()
+ var options = new ProcessInvocationOptions()
{
NoLaunchProfile = true
};
@@ -75,7 +75,7 @@ public async Task BuildAsyncAlwaysInjectsDotnetCliUseMsBuildServerEnvironmentVar
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
@@ -104,7 +104,7 @@ public async Task RestoreAsyncRunsDotnetRestoreCommand()
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
@@ -142,7 +142,7 @@ public async Task BuildAsyncUsesConfigurationValueForDotnetCliUseMsBuildServer()
});
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
@@ -171,7 +171,7 @@ public async Task BuildAsyncIncludesNoRestoreFlagWhenNoRestoreIsTrue()
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
@@ -200,7 +200,7 @@ public async Task BuildAsyncDoesNotIncludeNoRestoreFlagWhenNoRestoreIsFalse()
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
@@ -229,7 +229,7 @@ public async Task RunAsyncInjectsDotnetCliUseMsBuildServerWhenNoBuildIsFalse()
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
@@ -267,7 +267,7 @@ public async Task RunAsyncDoesNotInjectDotnetCliUseMsBuildServerWhenNoBuildIsTru
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
@@ -308,7 +308,7 @@ public async Task RunAsyncPreservesExistingEnvironmentVariables()
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
@@ -351,7 +351,7 @@ public async Task NewProjectAsyncReturnsExitCode73WhenProjectAlreadyExists()
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
@@ -389,7 +389,7 @@ public async Task RunAsyncSetsVersionCheckDisabledWhenUpdateNotificationsFeature
});
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var runner = DotNetCliRunnerTestHelper.Create(
provider,
@@ -429,7 +429,7 @@ public async Task RunAsyncDoesNotSetVersionCheckDisabledWhenUpdateNotificationsF
});
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var runner = DotNetCliRunnerTestHelper.Create(
provider,
@@ -471,7 +471,7 @@ public async Task RunAsyncDoesNotOverrideUserProvidedVersionCheckDisabledValue()
});
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var runner = DotNetCliRunnerTestHelper.Create(
provider,
@@ -546,7 +546,7 @@ public async Task RunAsyncLaunchesAppHostInExtensionHostIfConnected()
args: [],
env: null,
backchannelCompletionSource: new TaskCompletionSource(),
- options: new DotNetCliRunnerInvocationOptions(),
+ options: new ProcessInvocationOptions(),
cancellationToken: CancellationToken.None).DefaultTimeout();
await launchAppHostCalledTcs.Task.DefaultTimeout();
@@ -563,7 +563,7 @@ public async Task AddPackageAsyncUseFilesSwitchForSingleFileAppHost()
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
@@ -616,7 +616,7 @@ public async Task AddPackageAsyncUsesPositionalArgumentForCsprojFile()
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
@@ -683,7 +683,7 @@ public async Task AddPackageAsyncUsesPositionalArgumentForCsprojFileWithNoRestor
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
@@ -749,7 +749,7 @@ public async Task AddPackageAsyncWithSourceAndNoRestoreHasArgumentSourceAndNoRes
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
@@ -831,7 +831,7 @@ public async Task GetSolutionProjectsAsync_ParsesOutputCorrectly()
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions
+ var options = new ProcessInvocationOptions
{
StandardOutputCallback = (line) => outputHelper.WriteLine($"stdout: {line}")
};
@@ -872,7 +872,7 @@ public async Task AddProjectReferenceAsync_ExecutesCorrectCommand()
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
@@ -902,7 +902,7 @@ public async Task RunAsyncAppliesNoLaunchProfileForSingleFileAppHost()
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions()
+ var options = new ProcessInvocationOptions()
{
NoLaunchProfile = true
};
@@ -948,7 +948,7 @@ public async Task RunAsyncDoesNotIncludeNoLaunchProfileForSingleFileAppHostWhenN
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions()
+ var options = new ProcessInvocationOptions()
{
NoLaunchProfile = false
};
@@ -994,7 +994,7 @@ public async Task RunAsyncFiltersOutEmptyAndWhitespaceArguments()
var provider = services.BuildServiceProvider();
// Use watch=true and NoLaunchProfile=false to ensure some empty strings are generated
- var options = new DotNetCliRunnerInvocationOptions()
+ var options = new ProcessInvocationOptions()
{
NoLaunchProfile = false,
Debug = false
@@ -1038,7 +1038,7 @@ public async Task RunAsyncFiltersOutEmptyArgumentsForSingleFileAppHost()
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions()
+ var options = new ProcessInvocationOptions()
{
NoLaunchProfile = false // This will generate an empty string for noProfileSwitch
};
@@ -1089,7 +1089,7 @@ public async Task RunAsyncIncludesAllNonEmptyFlagsWhenEnabled()
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions()
+ var options = new ProcessInvocationOptions()
{
NoLaunchProfile = true,
Debug = true
@@ -1138,7 +1138,7 @@ public async Task RunAsyncCorrectlyHandlesWatchWithoutDebug()
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions()
+ var options = new ProcessInvocationOptions()
{
NoLaunchProfile = true,
Debug = false // No debug, so no --verbose
@@ -1186,7 +1186,7 @@ public async Task GetProjectItemsAndPropertiesAsync_UsesBuild_ForSingleFileAppHo
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
@@ -1222,7 +1222,7 @@ public async Task GetProjectItemsAndPropertiesAsync_UsesMsBuild_ForCsProjFile()
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
@@ -1275,7 +1275,7 @@ public async Task SearchPackagesAsyncRetriesOnFailureAndSucceedsOnSecondAttempt(
logger: logger
);
- var options = new DotNetCliRunnerInvocationOptions { SuppressLogging = true };
+ var options = new ProcessInvocationOptions { SuppressLogging = true };
var result = await runner.SearchPackagesAsync(
workspace.WorkspaceRoot,
@@ -1316,7 +1316,7 @@ public async Task SearchPackagesAsyncRetriesMaxTimesAndReturnsFailure()
logger: logger
);
- var options = new DotNetCliRunnerInvocationOptions { SuppressLogging = true };
+ var options = new ProcessInvocationOptions { SuppressLogging = true };
var result = await runner.SearchPackagesAsync(
workspace.WorkspaceRoot,
@@ -1357,7 +1357,7 @@ public async Task SearchPackagesAsyncSucceedsOnFirstAttemptWithoutRetry()
logger: logger
);
- var options = new DotNetCliRunnerInvocationOptions { SuppressLogging = true };
+ var options = new ProcessInvocationOptions { SuppressLogging = true };
var result = await runner.SearchPackagesAsync(
workspace.WorkspaceRoot,
@@ -1401,7 +1401,7 @@ public async Task RunAsyncIncludesNoBuildFlagWhenNoBuildIsTrue()
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
@@ -1439,7 +1439,7 @@ public async Task RunAsyncDoesNotIncludeNoBuildFlagWhenNoBuildIsFalse()
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
@@ -1477,7 +1477,7 @@ public async Task RunAsyncIncludesNoRestoreFlagWhenNoRestoreIsTrueAndNoBuildIsFa
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
@@ -1517,7 +1517,7 @@ public async Task RunAsyncDoesNotIncludeNoRestoreFlagWhenNoBuildIsTrue()
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
- var options = new DotNetCliRunnerInvocationOptions();
+ var options = new ProcessInvocationOptions();
var executionContext = CreateExecutionContext(workspace.WorkspaceRoot);
var runner = DotNetCliRunnerTestHelper.Create(
diff --git a/tests/Aspire.Cli.Tests/Projects/ProjectUpdaterTests.cs b/tests/Aspire.Cli.Tests/Projects/ProjectUpdaterTests.cs
index 4aef9e1b59e..d0d4d5b1358 100644
--- a/tests/Aspire.Cli.Tests/Projects/ProjectUpdaterTests.cs
+++ b/tests/Aspire.Cli.Tests/Projects/ProjectUpdaterTests.cs
@@ -206,7 +206,7 @@ await File.WriteAllTextAsync(
var document = JsonDocument.Parse(json);
return (0, document);
},
- // FileInfo, string, string, string?, DotNetCliRunnerInvocationOptions, CancellationToken, int
+ // FileInfo, string, string, string?, ProcessInvocationOptions, CancellationToken, int
AddPackageAsyncCallback = (projectFile, packageId, packageVersion, source, noRestore, _, _) =>
{
packagesAddsExecuted.Add((projectFile, packageId, packageVersion, source!, noRestore));
@@ -345,7 +345,7 @@ await File.WriteAllTextAsync(
var document = JsonDocument.Parse(json);
return (0, document);
},
- // FileInfo, string, string, string?, DotNetCliRunnerInvocationOptions, CancellationToken, int
+ // FileInfo, string, string, string?, ProcessInvocationOptions, CancellationToken, int
AddPackageAsyncCallback = (projectFile, packageId, packageVersion, source, noRestore, _, _) =>
{
packagesAddsExecuted.Add((projectFile, packageId, packageVersion, source!, noRestore));
diff --git a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs
index b3b801f5668..45e050dafb2 100644
--- a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs
+++ b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs
@@ -498,46 +498,46 @@ public void DisplayRenderable(IRenderable renderable) { }
private sealed class TestDotNetCliRunner : IDotNetCliRunner
{
- public Task<(int ExitCode, string? TemplateVersion)> InstallTemplateAsync(string packageName, string version, FileInfo? nugetConfigFile, string? nugetSource, bool force, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task<(int ExitCode, string? TemplateVersion)> InstallTemplateAsync(string packageName, string version, FileInfo? nugetConfigFile, string? nugetSource, bool force, ProcessInvocationOptions options, CancellationToken cancellationToken)
=> throw new NotImplementedException();
- public Task NewProjectAsync(string templateName, string projectName, string outputPath, string[] extraArgs, DotNetCliRunnerInvocationOptions? options, CancellationToken cancellationToken)
+ public Task NewProjectAsync(string templateName, string projectName, string outputPath, string[] extraArgs, ProcessInvocationOptions? options, CancellationToken cancellationToken)
=> throw new NotImplementedException();
- public Task RestoreAsync(FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task RestoreAsync(FileInfo projectFile, ProcessInvocationOptions options, CancellationToken cancellationToken)
=> throw new NotImplementedException();
- public Task BuildAsync(FileInfo projectFile, bool noRestore, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task BuildAsync(FileInfo projectFile, bool noRestore, ProcessInvocationOptions options, CancellationToken cancellationToken)
=> throw new NotImplementedException();
- public Task AddPackageAsync(FileInfo projectFile, string packageName, string version, string? packageSourceUrl, bool noRestore, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task AddPackageAsync(FileInfo projectFile, string packageName, string version, string? packageSourceUrl, bool noRestore, ProcessInvocationOptions options, CancellationToken cancellationToken)
=> throw new NotImplementedException();
- public Task AddProjectToSolutionAsync(FileInfo solutionFile, FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task AddProjectToSolutionAsync(FileInfo solutionFile, FileInfo projectFile, ProcessInvocationOptions options, CancellationToken cancellationToken)
=> throw new NotImplementedException();
- public Task<(int ExitCode, IReadOnlyList Projects)> GetSolutionProjectsAsync(FileInfo solutionFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task<(int ExitCode, IReadOnlyList Projects)> GetSolutionProjectsAsync(FileInfo solutionFile, ProcessInvocationOptions options, CancellationToken cancellationToken)
=> throw new NotImplementedException();
- public Task AddProjectReferenceAsync(FileInfo projectFile, FileInfo referencedProjectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task AddProjectReferenceAsync(FileInfo projectFile, FileInfo referencedProjectFile, ProcessInvocationOptions options, CancellationToken cancellationToken)
=> throw new NotImplementedException();
- public Task<(int ExitCode, NuGetPackageCli[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task<(int ExitCode, NuGetPackageCli[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, ProcessInvocationOptions options, CancellationToken cancellationToken)
=> throw new NotImplementedException();
- public Task<(int ExitCode, bool IsAspireHost, string? AspireHostingVersion)> GetAppHostInformationAsync(FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task<(int ExitCode, bool IsAspireHost, string? AspireHostingVersion)> GetAppHostInformationAsync(FileInfo projectFile, ProcessInvocationOptions options, CancellationToken cancellationToken)
=> throw new NotImplementedException();
- public Task<(int ExitCode, JsonDocument? Output)> GetProjectItemsAndPropertiesAsync(FileInfo projectFile, string[] items, string[] properties, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task<(int ExitCode, JsonDocument? Output)> GetProjectItemsAndPropertiesAsync(FileInfo projectFile, string[] items, string[] properties, ProcessInvocationOptions options, CancellationToken cancellationToken)
=> throw new NotImplementedException();
- public Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, bool noRestore, string[] args, IDictionary? env, TaskCompletionSource? backchannelCompletionSource, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task RunAsync(FileInfo projectFile, bool watch, bool noBuild, bool noRestore, string[] args, IDictionary? env, TaskCompletionSource? backchannelCompletionSource, ProcessInvocationOptions options, CancellationToken cancellationToken)
=> throw new NotImplementedException();
- public Task<(int ExitCode, string[] ConfigPaths)> GetNuGetConfigPathsAsync(DirectoryInfo workingDirectory, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task<(int ExitCode, string[] ConfigPaths)> GetNuGetConfigPathsAsync(DirectoryInfo workingDirectory, ProcessInvocationOptions options, CancellationToken cancellationToken)
=> throw new NotImplementedException();
- public Task InitUserSecretsAsync(FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task InitUserSecretsAsync(FileInfo projectFile, ProcessInvocationOptions options, CancellationToken cancellationToken)
=> Task.FromResult(0);
}
diff --git a/tests/Aspire.Cli.Tests/TestServices/TestDotNetCliRunner.cs b/tests/Aspire.Cli.Tests/TestServices/TestDotNetCliRunner.cs
index 32101c9ae3a..3bbf76173e0 100644
--- a/tests/Aspire.Cli.Tests/TestServices/TestDotNetCliRunner.cs
+++ b/tests/Aspire.Cli.Tests/TestServices/TestDotNetCliRunner.cs
@@ -11,49 +11,49 @@ namespace Aspire.Cli.Tests.TestServices;
internal sealed class TestDotNetCliRunner : IDotNetCliRunner
{
- public Func? AddPackageAsyncCallback { get; set; }
- public Func? AddProjectToSolutionAsyncCallback { get; set; }
- public Func? BuildAsyncCallback { get; set; }
- public Func? RestoreAsyncCallback { get; set; }
- public Func? GetAppHostInformationAsyncCallback { get; set; }
- public Func? GetNuGetConfigPathsAsyncCallback { get; set; }
- public Func? GetProjectItemsAndPropertiesAsyncCallback { get; set; }
- public Func? InstallTemplateAsyncCallback { get; set; }
- public Func? NewProjectAsyncCallback { get; set; }
- public Func?, TaskCompletionSource?, DotNetCliRunnerInvocationOptions, CancellationToken, Task>? RunAsyncCallback { get; set; }
- public Func? SearchPackagesAsyncCallback { get; set; }
- public Func Projects)>? GetSolutionProjectsAsyncCallback { get; set; }
- public Func? AddProjectReferenceAsyncCallback { get; set; }
-
- public Task AddPackageAsync(FileInfo projectFilePath, string packageName, string packageVersion, string? nugetSource, bool noRestore, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Func? AddPackageAsyncCallback { get; set; }
+ public Func? AddProjectToSolutionAsyncCallback { get; set; }
+ public Func? BuildAsyncCallback { get; set; }
+ public Func? RestoreAsyncCallback { get; set; }
+ public Func? GetAppHostInformationAsyncCallback { get; set; }
+ public Func? GetNuGetConfigPathsAsyncCallback { get; set; }
+ public Func? GetProjectItemsAndPropertiesAsyncCallback { get; set; }
+ public Func? InstallTemplateAsyncCallback { get; set; }
+ public Func? NewProjectAsyncCallback { get; set; }
+ public Func?, TaskCompletionSource?, ProcessInvocationOptions, CancellationToken, Task>? RunAsyncCallback { get; set; }
+ public Func? SearchPackagesAsyncCallback { get; set; }
+ public Func Projects)>? GetSolutionProjectsAsyncCallback { get; set; }
+ public Func? AddProjectReferenceAsyncCallback { get; set; }
+
+ public Task AddPackageAsync(FileInfo projectFilePath, string packageName, string packageVersion, string? nugetSource, bool noRestore, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
return AddPackageAsyncCallback != null
? Task.FromResult(AddPackageAsyncCallback(projectFilePath, packageName, packageVersion, nugetSource, noRestore, options, cancellationToken))
: throw new NotImplementedException();
}
- public Task AddProjectToSolutionAsync(FileInfo solutionFile, FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task AddProjectToSolutionAsync(FileInfo solutionFile, FileInfo projectFile, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
return AddProjectToSolutionAsyncCallback != null
? Task.FromResult(AddProjectToSolutionAsyncCallback(solutionFile, projectFile, options, cancellationToken))
: Task.FromResult(0); // If not overridden, just return success.
}
- public Task BuildAsync(FileInfo projectFilePath, bool noRestore, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task BuildAsync(FileInfo projectFilePath, bool noRestore, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
return BuildAsyncCallback != null
? Task.FromResult(BuildAsyncCallback(projectFilePath, noRestore, options, cancellationToken))
: throw new NotImplementedException();
}
- public Task RestoreAsync(FileInfo projectFilePath, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task RestoreAsync(FileInfo projectFilePath, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
return RestoreAsyncCallback != null
? Task.FromResult(RestoreAsyncCallback(projectFilePath, options, cancellationToken))
: throw new NotImplementedException();
}
- public Task<(int ExitCode, bool IsAspireHost, string? AspireHostingVersion)> GetAppHostInformationAsync(FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task<(int ExitCode, bool IsAspireHost, string? AspireHostingVersion)> GetAppHostInformationAsync(FileInfo projectFile, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
var informationalVersion = VersionHelper.GetDefaultTemplateVersion();
@@ -62,7 +62,7 @@ public Task RestoreAsync(FileInfo projectFilePath, DotNetCliRunnerInvocatio
: Task.FromResult<(int, bool, string?)>((0, true, informationalVersion));
}
- public Task<(int ExitCode, string[] ConfigPaths)> GetNuGetConfigPathsAsync(DirectoryInfo workingDirectory, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task<(int ExitCode, string[] ConfigPaths)> GetNuGetConfigPathsAsync(DirectoryInfo workingDirectory, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
return GetNuGetConfigPathsAsyncCallback != null
? Task.FromResult(GetNuGetConfigPathsAsyncCallback(workingDirectory, options, cancellationToken))
@@ -78,55 +78,55 @@ private static string[] GetGlobalNuGetPaths()
};
}
- public Task<(int ExitCode, JsonDocument? Output)> GetProjectItemsAndPropertiesAsync(FileInfo projectFile, string[] items, string[] properties, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task<(int ExitCode, JsonDocument? Output)> GetProjectItemsAndPropertiesAsync(FileInfo projectFile, string[] items, string[] properties, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
return GetProjectItemsAndPropertiesAsyncCallback != null
? Task.FromResult(GetProjectItemsAndPropertiesAsyncCallback(projectFile, items, properties, options, cancellationToken))
: throw new NotImplementedException();
}
- public Task<(int ExitCode, string? TemplateVersion)> InstallTemplateAsync(string packageName, string version, FileInfo? nugetConfigFile, string? nugetSource, bool force, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task<(int ExitCode, string? TemplateVersion)> InstallTemplateAsync(string packageName, string version, FileInfo? nugetConfigFile, string? nugetSource, bool force, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
return InstallTemplateAsyncCallback != null
? Task.FromResult(InstallTemplateAsyncCallback(packageName, version, nugetSource, force, options, cancellationToken))
: Task.FromResult<(int, string?)>((0, version)); // If not overridden, just return success for the version specified.
}
- public Task NewProjectAsync(string templateName, string name, string outputPath, string[] extraArgs, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
+ public Task