// 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.Diagnostics;
using System.Globalization;
using System.Text.Json;
using Aspire.Cli.Agents;
using Aspire.Cli.Agents.Playwright;
using Aspire.Cli.Configuration;
using Aspire.Cli.Git;
using Aspire.Cli.Interaction;
using Aspire.Cli.NuGet;
using Aspire.Cli.Resources;
using Aspire.Cli.Telemetry;
using Aspire.Cli.Utils;
using Spectre.Console;
namespace Aspire.Cli.Commands;
///
/// Command that initializes agent environment configuration for detected agents.
/// This is the new command under 'aspire agent init'.
///
internal sealed class AgentInitCommand : BaseCommand, IPackageMetaPrefetchingCommand
{
private readonly IInteractionService _interactionService;
private readonly IAgentEnvironmentDetector _agentEnvironmentDetector;
private readonly PlaywrightCliInstaller _playwrightCliInstaller;
private readonly IGitRepository _gitRepository;
///
/// AgentInitCommand does not need template package metadata prefetching.
///
public bool PrefetchesTemplatePackageMetadata => false;
///
/// AgentInitCommand does not need CLI package metadata prefetching.
///
public bool PrefetchesCliPackageMetadata => false;
public AgentInitCommand(
IInteractionService interactionService,
IFeatures features,
ICliUpdateNotifier updateNotifier,
CliExecutionContext executionContext,
IAgentEnvironmentDetector agentEnvironmentDetector,
PlaywrightCliInstaller playwrightCliInstaller,
IGitRepository gitRepository,
AspireCliTelemetry telemetry)
: base("init", AgentCommandStrings.InitCommand_Description, features, updateNotifier, executionContext, interactionService, telemetry)
{
_interactionService = interactionService;
_agentEnvironmentDetector = agentEnvironmentDetector;
_playwrightCliInstaller = playwrightCliInstaller;
_gitRepository = gitRepository;
}
protected override bool UpdateNotificationsEnabled => false;
///
/// Public entry point for executing the init command.
/// This allows McpInitCommand to delegate to this implementation.
///
internal Task ExecuteCommandAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
return ExecuteAsync(parseResult, cancellationToken);
}
///
/// Prompts the user to run agent init after a successful command, then chains into agent init if accepted.
/// Used by commands (e.g. aspire init, aspire new) to offer agent init as a follow-up step.
///
internal async Task 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 ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
var workspaceRoot = await PromptForWorkspaceRootAsync(cancellationToken);
return await ExecuteAgentInitAsync(workspaceRoot, cancellationToken);
}
private async Task PromptForWorkspaceRootAsync(CancellationToken cancellationToken)
{
// Try to discover the git repository root to use as the default workspace root
var gitRoot = await _gitRepository.GetRootAsync(cancellationToken);
var defaultWorkspaceRoot = gitRoot ?? ExecutionContext.WorkingDirectory;
// Prompt the user for the workspace root
var workspaceRootPath = await _interactionService.PromptForFilePathAsync(
McpCommandStrings.InitCommand_WorkspaceRootPrompt,
defaultValue: defaultWorkspaceRoot.FullName,
validator: path =>
{
if (string.IsNullOrWhiteSpace(path))
{
return ValidationResult.Error(McpCommandStrings.InitCommand_WorkspaceRootRequired);
}
if (!Directory.Exists(path))
{
return ValidationResult.Error(string.Format(CultureInfo.InvariantCulture, McpCommandStrings.InitCommand_WorkspaceRootNotFound, path));
}
return ValidationResult.Success();
},
directory: true,
cancellationToken: cancellationToken);
return new DirectoryInfo(workspaceRootPath);
}
private async Task ExecuteAgentInitAsync(DirectoryInfo workspaceRoot, CancellationToken cancellationToken)
{
var context = new AgentEnvironmentScanContext
{
WorkingDirectory = ExecutionContext.WorkingDirectory,
RepositoryRoot = workspaceRoot
};
var applicators = await _interactionService.ShowStatusAsync(
McpCommandStrings.InitCommand_DetectingAgentEnvironments,
async () => await _agentEnvironmentDetector.DetectAsync(context, cancellationToken));
// Apply deprecated config migrations silently (these are fixes, not choices)
var configUpdates = applicators.Where(a => a.PromptGroup == McpInitPromptGroup.ConfigUpdates).ToList();
var userChoices = applicators.Where(a => a.PromptGroup != McpInitPromptGroup.ConfigUpdates).ToList();
foreach (var update in configUpdates)
{
try
{
await update.ApplyAsync(cancellationToken);
_interactionService.DisplayMessage(KnownEmojis.Wrench, update.Description);
}
catch (InvalidOperationException ex)
{
_interactionService.DisplayError(ex.Message);
}
}
// --- Phase 1: Skill location selection ---
var selectedLocations = await _interactionService.PromptForSelectionsAsync(
AgentCommandStrings.InitCommand_SelectSkillLocations,
SkillLocation.All,
loc => $"{loc.Name} — {loc.Description}",
preSelected: SkillLocation.All.Where(l => l.IsDefault),
optional: true,
cancellationToken);
// --- Phase 2: Skill and MCP server selection (only if locations were selected) ---
IReadOnlyList selectedSkills = [];
AgentEnvironmentApplicator? combinedMcpApplicator = null;
var mcpApplicators = userChoices.Where(a => a.PromptGroup == McpInitPromptGroup.AgentEnvironments).ToList();
if (selectedLocations.Count > 0)
{
// Build prompt items: skills first, then MCP as a separate non-default item
var skillChoices = new List