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;
    }
}