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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion src/Aspire.Cli/Commands/AgentInitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,47 @@ internal Task<int> ExecuteCommandAsync(ParseResult parseResult, CancellationToke
return ExecuteAsync(parseResult, cancellationToken);
}

/// <summary>
/// Prompts the user to run agent init after a successful command, then chains into agent init if accepted.
/// Used by commands (e.g. <c>aspire init</c>, <c>aspire new</c>) to offer agent init as a follow-up step.
/// </summary>
internal async Task<int> PromptAndChainAsync(
ICliHostEnvironment hostEnvironment,
IInteractionService interactionService,
int previousResultExitCode,
DirectoryInfo workspaceRoot,
CancellationToken cancellationToken)
{
if (previousResultExitCode != ExitCodeConstants.Success)
{
return previousResultExitCode;
}

if (!hostEnvironment.SupportsInteractiveInput)
{
return ExitCodeConstants.Success;
}

var runAgentInit = await interactionService.ConfirmAsync(
SharedCommandStrings.PromptRunAgentInit,
defaultValue: true,
cancellationToken: cancellationToken);

if (runAgentInit)
{
return await ExecuteAgentInitAsync(workspaceRoot, cancellationToken);
}

return ExitCodeConstants.Success;
}

protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since callers explicitly choose which overload to call, the "API consistency" rationale doesn't provide real value. A simpler internal method taking just (DirectoryInfo, CancellationToken) would be more honest. Minor nit though.

{
var workspaceRoot = await PromptForWorkspaceRootAsync(cancellationToken);
return await ExecuteAgentInitAsync(workspaceRoot, cancellationToken);
}

private async Task<DirectoryInfo> PromptForWorkspaceRootAsync(CancellationToken cancellationToken)
{
// Try to discover the git repository root to use as the default workspace root
var gitRoot = await _gitRepository.GetRootAsync(cancellationToken);
Expand All @@ -89,8 +129,11 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
directory: true,
cancellationToken: cancellationToken);

var workspaceRoot = new DirectoryInfo(workspaceRootPath);
return new DirectoryInfo(workspaceRootPath);
}

private async Task<int> ExecuteAgentInitAsync(DirectoryInfo workspaceRoot, CancellationToken cancellationToken)
{
var context = new AgentEnvironmentScanContext
{
WorkingDirectory = ExecutionContext.WorkingDirectory,
Expand Down
21 changes: 17 additions & 4 deletions src/Aspire.Cli/Commands/InitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ internal sealed class InitCommand : BaseCommand, IPackageMetaPrefetchingCommand
private readonly ILanguageService _languageService;
private readonly ILanguageDiscovery _languageDiscovery;
private readonly IScaffoldingService _scaffoldingService;
private readonly AgentInitCommand _agentInitCommand;
private readonly ICliHostEnvironment _hostEnvironment;

private static readonly Option<string?> s_sourceOption = new("--source", "-s")
{
Expand Down Expand Up @@ -80,7 +82,9 @@ public InitCommand(
IConfigurationService configurationService,
ILanguageService languageService,
ILanguageDiscovery languageDiscovery,
IScaffoldingService scaffoldingService)
IScaffoldingService scaffoldingService,
AgentInitCommand agentInitCommand,
ICliHostEnvironment hostEnvironment)
: base("init", InitCommandStrings.Description, features, updateNotifier, executionContext, interactionService, telemetry)
{
_runner = runner;
Expand All @@ -96,6 +100,8 @@ public InitCommand(
_languageService = languageService;
_languageDiscovery = languageDiscovery;
_scaffoldingService = scaffoldingService;
_agentInitCommand = agentInitCommand;
_hostEnvironment = hostEnvironment;

Options.Add(s_sourceOption);
Options.Add(s_versionOption);
Expand Down Expand Up @@ -140,7 +146,8 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
InteractionService.DisplayEmptyLine();
InteractionService.DisplayMessage(KnownEmojis.Information, $"Creating {languageInfo.DisplayName} AppHost...");
InteractionService.DisplayEmptyLine();
return await CreatePolyglotAppHostAsync(languageInfo, cancellationToken);
var polyglotResult = await CreatePolyglotAppHostAsync(languageInfo, cancellationToken);
return await _agentInitCommand.PromptAndChainAsync(_hostEnvironment, InteractionService, polyglotResult, _executionContext.WorkingDirectory, cancellationToken);
}

// For C#, we need the .NET SDK
Expand All @@ -155,20 +162,26 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
// Use SolutionLocator to find solution files, walking up the directory tree
initContext.SelectedSolutionFile = await _solutionLocator.FindSolutionFileAsync(_executionContext.WorkingDirectory, cancellationToken);

int initResult;
DirectoryInfo workspaceRoot;
if (initContext.SelectedSolutionFile is not null)
{
InteractionService.DisplayEmptyLine();
InteractionService.DisplayMessage(KnownEmojis.Information, string.Format(CultureInfo.CurrentCulture, InitCommandStrings.SolutionDetected, initContext.SelectedSolutionFile.Name));
InteractionService.DisplayEmptyLine();
return await InitializeExistingSolutionAsync(initContext, parseResult, cancellationToken);
initResult = await InitializeExistingSolutionAsync(initContext, parseResult, cancellationToken);
workspaceRoot = initContext.SolutionDirectory ?? _executionContext.WorkingDirectory;
}
else
{
InteractionService.DisplayEmptyLine();
InteractionService.DisplayMessage(KnownEmojis.Information, InitCommandStrings.NoSolutionFoundCreatingSingleFileAppHost);
InteractionService.DisplayEmptyLine();
return await CreateEmptyAppHostAsync(parseResult, cancellationToken);
initResult = await CreateEmptyAppHostAsync(parseResult, cancellationToken);
workspaceRoot = _executionContext.WorkingDirectory;
}

return await _agentInitCommand.PromptAndChainAsync(_hostEnvironment, InteractionService, initResult, workspaceRoot, cancellationToken);
}

private async Task<int> InitializeExistingSolutionAsync(InitContext initContext, ParseResult parseResult, CancellationToken cancellationToken)
Expand Down
11 changes: 9 additions & 2 deletions src/Aspire.Cli/Commands/NewCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ internal sealed class NewCommand : BaseCommand, IPackageMetaPrefetchingCommand
private readonly IFeatures _features;
private readonly IPackagingService _packagingService;
private readonly IConfigurationService _configurationService;
private readonly AgentInitCommand _agentInitCommand;
private readonly ICliHostEnvironment _hostEnvironment;

private static readonly Option<string> s_nameOption = new("--name", "-n")
{
Expand Down Expand Up @@ -72,14 +74,18 @@ public NewCommand(
ICliUpdateNotifier updateNotifier,
CliExecutionContext executionContext,
IPackagingService packagingService,
IConfigurationService configurationService)
IConfigurationService configurationService,
AgentInitCommand agentInitCommand,
ICliHostEnvironment hostEnvironment)
: base("new", NewCommandStrings.Description, features, updateNotifier, executionContext, interactionService, telemetry)
{
_prompter = prompter;
_templateProvider = templateProvider;
_features = features;
_packagingService = packagingService;
_configurationService = configurationService;
_agentInitCommand = agentInitCommand;
_hostEnvironment = hostEnvironment;

Options.Add(s_nameOption);
Options.Add(s_outputOption);
Expand Down Expand Up @@ -366,7 +372,8 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
extensionInteractionService.OpenEditor(templateResult.OutputPath);
}

return templateResult.ExitCode;
var workspaceRoot = new DirectoryInfo(templateResult.OutputPath ?? ExecutionContext.WorkingDirectory.FullName);
return await _agentInitCommand.PromptAndChainAsync(_hostEnvironment, InteractionService, templateResult.ExitCode, workspaceRoot, cancellationToken);
}

private static bool ShouldResolveCliTemplateVersion(ITemplate template)
Expand Down
6 changes: 6 additions & 0 deletions src/Aspire.Cli/Resources/SharedCommandStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Aspire.Cli/Resources/SharedCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,7 @@
<data name="MultipleInScopeAppHosts" xml:space="preserve">
<value>Multiple running AppHosts found in the current directory. Select from running AppHosts.</value>
</data>
<data name="PromptRunAgentInit" xml:space="preserve">
<value>Would you like to configure AI agent environments for this project?</value>
</data>
</root>
5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/SharedCommandStrings.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/SharedCommandStrings.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/SharedCommandStrings.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/SharedCommandStrings.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/SharedCommandStrings.it.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/SharedCommandStrings.ja.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/SharedCommandStrings.ko.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/SharedCommandStrings.pl.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/SharedCommandStrings.pt-BR.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/SharedCommandStrings.ru.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/SharedCommandStrings.tr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/SharedCommandStrings.zh-Hans.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Aspire.Cli/Resources/xlf/SharedCommandStrings.zh-Hant.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tests/Aspire.Cli.EndToEnd.Tests/BundleSmokeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public async Task CreateAndRunAspireStarterProjectWithBundle()
.Enter()
.WaitUntil(s => waitingForTestPrompt.Search(s).Count > 0, TimeSpan.FromSeconds(10))
.Enter()
.DeclineAgentInitPrompt()
Copy link
Copy Markdown
Member

@JamesNK JamesNK Mar 6, 2026

Choose a reason for hiding this comment

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

The new agent prompt required updating 50+ tests.

Something I've been thinking about is there should be an E2E test helper method for calling aspire new. It enscapsulates all the CLI interactions involved with running aspire new.

If we had that method, and called it from all the end to end tests, then changes to aspire new (responding to new prompts, or changes in text) would only require updating one helper method. And not every test.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I have an agent looking at this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

.WaitForSuccessPrompt(counter)
// Start AppHost in detached mode and capture JSON output
.Type("aspire run --detach --format json | tee /tmp/aspire-detach.json")
Expand Down
2 changes: 2 additions & 0 deletions tests/Aspire.Cli.EndToEnd.Tests/DescribeCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public async Task DescribeCommandShowsRunningResources()
.Enter()
.WaitUntil(s => waitingForTestPrompt.Search(s).Count > 0, TimeSpan.FromSeconds(10))
.Enter()
.DeclineAgentInitPrompt()
.WaitForSuccessPrompt(counter);

// Navigate to the AppHost directory
Expand Down Expand Up @@ -223,6 +224,7 @@ public async Task DescribeCommandResolvesReplicaNames()
.Enter()
.WaitUntil(s => waitingForTestPrompt.Search(s).Count > 0, TimeSpan.FromSeconds(10))
.Enter()
.DeclineAgentInitPrompt()
.WaitForSuccessPrompt(counter);

// Navigate to the AppHost directory
Expand Down
2 changes: 2 additions & 0 deletions tests/Aspire.Cli.EndToEnd.Tests/DockerDeploymentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public async Task CreateAndDeployToDockerCompose()
.WaitUntil(s => waitingForTestPrompt.Search(s).Count > 0, TimeSpan.FromSeconds(10))
// For test project prompt, default is "No" so just press Enter to accept it
.Enter()
.DeclineAgentInitPrompt()
.WaitForSuccessPrompt(counter);

// Step 2: Navigate into the project directory
Expand Down Expand Up @@ -253,6 +254,7 @@ public async Task CreateAndDeployToDockerComposeInteractive()
.WaitUntil(s => waitingForTestPrompt.Search(s).Count > 0, TimeSpan.FromSeconds(10))
// For test project prompt, default is "No" so just press Enter to accept it
.Enter()
.DeclineAgentInitPrompt()
.WaitForSuccessPrompt(counter);

// Step 2: Navigate into the project directory
Expand Down
Loading
Loading