| File: Commands\AgentInitCommand.cs | Web Access |
| Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire) |
// 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 System.Text.Json; using Aspire.Cli.Agents; 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; /// <summary> /// Command that initializes agent environment configuration for detected agents. /// This is the new command under 'aspire agent init'. /// </summary> internal sealed class AgentInitCommand : BaseCommand, IPackageMetaPrefetchingCommand { private readonly IInteractionService _interactionService; private readonly IAgentEnvironmentDetector _agentEnvironmentDetector; private readonly IGitRepository _gitRepository; /// <summary> /// AgentInitCommand does not need template package metadata prefetching. /// </summary> public bool PrefetchesTemplatePackageMetadata => false; /// <summary> /// AgentInitCommand does not need CLI package metadata prefetching. /// </summary> public bool PrefetchesCliPackageMetadata => false; public AgentInitCommand( IInteractionService interactionService, IFeatures features, ICliUpdateNotifier updateNotifier, CliExecutionContext executionContext, IAgentEnvironmentDetector agentEnvironmentDetector, IGitRepository gitRepository, AspireCliTelemetry telemetry) : base("init", AgentCommandStrings.InitCommand_Description, features, updateNotifier, executionContext, interactionService, telemetry) { _interactionService = interactionService; _agentEnvironmentDetector = agentEnvironmentDetector; _gitRepository = gitRepository; } protected override bool UpdateNotificationsEnabled => false; /// <summary> /// Public entry point for executing the init command. /// This allows McpInitCommand to delegate to this implementation. /// </summary> internal Task<int> ExecuteCommandAsync(ParseResult parseResult, CancellationToken cancellationToken) { 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) { 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); 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<int> 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)); if (applicators.Length == 0) { _interactionService.DisplaySubtleMessage(McpCommandStrings.InitCommand_NoAgentEnvironmentsDetected); return ExitCodeConstants.Success; } // 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); } } if (userChoices.Count == 0) { _interactionService.DisplaySuccess(McpCommandStrings.InitCommand_ConfigurationComplete); return ExitCodeConstants.Success; } // Categorize by prompt group (not string matching) var skillApplicators = userChoices .Where(a => a.PromptGroup == McpInitPromptGroup.SkillFiles) .ToList(); var mcpApplicators = userChoices .Where(a => a.PromptGroup == McpInitPromptGroup.AgentEnvironments) .ToList(); var toolApplicators = userChoices .Where(a => a.PromptGroup == McpInitPromptGroup.Tools) .ToList(); var otherApplicators = userChoices .Except(skillApplicators) .Except(mcpApplicators) .Except(toolApplicators) .ToList(); // Build a flat list: collapsed skill (pre-selected), then others (Playwright CLI, etc.) var promptChoices = new List<AgentEnvironmentApplicator>(); // Collapse all skill applicators into a single line (pre-selected) AgentEnvironmentApplicator? combinedSkillApplicator = null; if (skillApplicators.Count > 0) { combinedSkillApplicator = new AgentEnvironmentApplicator( AgentCommandStrings.InitCommand_InstallSkillFile, async ct => { foreach (var skill in skillApplicators) { await skill.ApplyAsync(ct); _interactionService.DisplayMessage(KnownEmojis.CheckMark, skill.Description); } }, promptGroup: McpInitPromptGroup.AdditionalOptions); promptChoices.Add(combinedSkillApplicator); } promptChoices.AddRange(toolApplicators); promptChoices.AddRange(otherApplicators); // Add collapsed MCP as last option for compatibility if (mcpApplicators.Count > 0) { var combinedMcpApplicator = new AgentEnvironmentApplicator( AgentCommandStrings.InitCommand_ConfigureMcpServer, async ct => { foreach (var mcp in mcpApplicators) { await mcp.ApplyAsync(ct); _interactionService.DisplayMessage(KnownEmojis.CheckMark, mcp.Description); } }, promptGroup: McpInitPromptGroup.AdditionalOptions); promptChoices.Add(combinedMcpApplicator); } // Pre-select the skill applicator var preSelected = combinedSkillApplicator is not null ? [combinedSkillApplicator] : Array.Empty<AgentEnvironmentApplicator>(); // Present a single flat prompt with skill pre-selected var selected = await _interactionService.PromptForSelectionsAsync( AgentCommandStrings.InitCommand_WhatToConfigure, promptChoices, applicator => applicator.Description, preSelected: preSelected, optional: true, cancellationToken); if (selected.Count == 0) { _interactionService.DisplaySubtleMessage(AgentCommandStrings.InitCommand_NothingSelected); return ExitCodeConstants.Success; } // Apply selected applicators var hasErrors = false; foreach (var applicator in selected) { try { await applicator.ApplyAsync(cancellationToken); } catch (InvalidOperationException ex) { _interactionService.DisplayError(ex.Message); if (ex.InnerException is JsonException) { _interactionService.DisplaySubtleMessage( string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.SkippedMalformedConfigFile, applicator.Description)); } hasErrors = true; } } if (hasErrors) { _interactionService.DisplayMessage(KnownEmojis.Warning, AgentCommandStrings.ConfigurationCompletedWithErrors); } else { _interactionService.DisplaySuccess(McpCommandStrings.InitCommand_ConfigurationComplete); } return hasErrors ? ExitCodeConstants.InvalidCommand : ExitCodeConstants.Success; } }