File: Agents\CommonAgentApplicators.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.Tool.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 Aspire.Cli.Agents.Playwright;
 
namespace Aspire.Cli.Agents;
 
/// <summary>
/// Provides factory methods for creating common agent applicators that are shared across different agent environments.
/// </summary>
internal static class CommonAgentApplicators
{
    /// <summary>
    /// The name of the Aspire skill.
    /// </summary>
    internal const string AspireSkillName = "aspire";
 
    /// <summary>
    /// Tries to add an applicator for creating/updating the Aspire skill file at the specified path.
    /// </summary>
    /// <param name="context">The scan context.</param>
    /// <param name="workspaceRoot">The workspace root directory.</param>
    /// <param name="skillRelativePath">The relative path to the skill file from workspace root (e.g., ".github/skills/aspire/SKILL.md").</param>
    /// <param name="description">The description to show in the applicator prompt.</param>
    /// <returns>True if the applicator was added, false if it was already added.</returns>
    public static bool TryAddSkillFileApplicator(
        AgentEnvironmentScanContext context,
        DirectoryInfo workspaceRoot,
        string skillRelativePath,
        string description)
    {
        // Check if we've already added an applicator for this specific skill path
        if (context.HasSkillFileApplicator(skillRelativePath))
        {
            return false;
        }
 
        var skillFilePath = Path.Combine(workspaceRoot.FullName, skillRelativePath);
 
        // Mark this skill path as having an applicator (whether file exists or not)
        context.MarkSkillFileApplicatorAdded(skillRelativePath);
 
        // Check if the skill file already exists
        if (File.Exists(skillFilePath))
        {
            // Read existing content and check if it differs from current content
            // Normalize line endings for comparison to handle cross-platform differences
            var existingContent = File.ReadAllText(skillFilePath);
            var normalizedExisting = NormalizeLineEndings(existingContent);
            var normalizedExpected = NormalizeLineEndings(SkillFileContent);
 
            if (!string.Equals(normalizedExisting, normalizedExpected, StringComparison.Ordinal))
            {
                // Content differs, offer to update
                context.AddApplicator(new AgentEnvironmentApplicator(
                    $"{description} (update - content has changed)",
                    ct => UpdateSkillFileAsync(skillFilePath, ct),
                    promptGroup: McpInitPromptGroup.SkillFiles,
                    priority: 0));
                return true;
            }
 
            // File exists and content is the same, nothing to do
            return false;
        }
 
        // Skill file doesn't exist, add applicator to create it
        context.AddApplicator(new AgentEnvironmentApplicator(
            description,
            ct => CreateSkillFileAsync(skillFilePath, ct),
            promptGroup: McpInitPromptGroup.SkillFiles,
            priority: 0));
 
        return true;
    }
 
    /// <summary>
    /// Adds a single Playwright CLI installation applicator if not already added.
    /// Called by scanners that detect an environment supporting Playwright.
    /// The applicator uses <see cref="PlaywrightCliInstaller"/> to securely install the CLI and generate skill files.
    /// </summary>
    /// <param name="context">The scan context.</param>
    /// <param name="installer">The Playwright CLI installer that handles secure installation.</param>
    /// <param name="skillBaseDirectory">The relative path to the skill base directory for this agent environment (e.g., ".claude/skills", ".github/skills").</param>
    public static void AddPlaywrightCliApplicator(
        AgentEnvironmentScanContext context,
        PlaywrightCliInstaller installer,
        string skillBaseDirectory)
    {
        // Register the skill base directory so skill files can be mirrored to all environments
        context.AddSkillBaseDirectory(skillBaseDirectory);
 
        // Only add the Playwright applicator prompt once across all environments
        if (context.PlaywrightApplicatorAdded)
        {
            return;
        }
 
        context.PlaywrightApplicatorAdded = true;
        context.AddApplicator(new AgentEnvironmentApplicator(
            "Install Playwright CLI (Recommended for browser automation)",
            ct => installer.InstallAsync(context, ct),
            promptGroup: McpInitPromptGroup.Tools,
            priority: 1));
    }
 
    /// <summary>
    /// Creates a skill file at the specified path.
    /// </summary>
    private static async Task CreateSkillFileAsync(string skillFilePath, CancellationToken cancellationToken)
    {
        // Ensure the directory exists
        var directory = Path.GetDirectoryName(skillFilePath);
        if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
        {
            Directory.CreateDirectory(directory);
        }
 
        // Only create the file if it doesn't already exist
        if (!File.Exists(skillFilePath))
        {
            await File.WriteAllTextAsync(skillFilePath, SkillFileContent, cancellationToken);
        }
    }
 
    /// <summary>
    /// Updates an existing skill file at the specified path with the latest content.
    /// </summary>
    private static async Task UpdateSkillFileAsync(string skillFilePath, CancellationToken cancellationToken)
    {
        await File.WriteAllTextAsync(skillFilePath, SkillFileContent, cancellationToken);
    }
 
    /// <summary>
    /// Normalizes line endings to LF for consistent comparison across platforms.
    /// </summary>
    private static string NormalizeLineEndings(string content)
    {
        return content.ReplaceLineEndings("\n");
    }
 
    /// <summary>
    /// Gets the content for the Aspire skill file.
    /// </summary>
    internal const string SkillFileContent =
        """
        ---
        name: aspire
        description: "Orchestrates Aspire distributed applications using the Aspire CLI for running, debugging, and managing distributed apps. USE FOR: aspire start, aspire stop, start aspire app, aspire describe, list aspire integrations, debug aspire issues, view aspire logs, add aspire resource, aspire dashboard, update aspire apphost. DO NOT USE FOR: non-Aspire .NET apps (use dotnet CLI), container-only deployments (use docker/podman), Azure deployment after local testing (use azure-deploy skill). INVOKES: Aspire CLI commands (aspire start, aspire describe, aspire otel logs, aspire docs search, aspire add), bash. FOR SINGLE OPERATIONS: Use Aspire CLI commands directly for quick resource status or doc lookups."
        ---
 
        # Aspire Skill
 
        This repository uses Aspire to orchestrate its distributed application. Resources are defined in the AppHost project (`apphost.cs` or `apphost.ts`).
 
        ## CLI command reference
 
        | Task | Command |
        |---|---|
        | Start the app | `aspire start` |
        | Start isolated (worktrees) | `aspire start --isolated` |
        | Restart the app | `aspire start` (stops previous automatically) |
        | Wait for resource healthy | `aspire wait <resource>` |
        | Stop the app | `aspire stop` |
        | List resources | `aspire describe` or `aspire resources` |
        | Run resource command | `aspire resource <resource> <command>` |
        | Start/stop/restart resource | `aspire resource <resource> start|stop|restart` |
        | View console logs | `aspire logs [resource]` |
        | View structured logs | `aspire otel logs [resource]` |
        | View traces | `aspire otel traces [resource]` |
        | Logs for a trace | `aspire otel logs --trace-id <id>` |
        | Add an integration | `aspire add` |
        | List running AppHosts | `aspire ps` |
        | Update AppHost packages | `aspire update` |
        | Search docs | `aspire docs search <query>` |
        | Get doc page | `aspire docs get <slug>` |
        | List doc pages | `aspire docs list` |
        | Environment diagnostics | `aspire doctor` |
        | List resource MCP tools | `aspire mcp tools` |
        | Call resource MCP tool | `aspire mcp call <resource> <tool> --input <json>` |
 
        Most commands support `--format Json` for machine-readable output. Use `--apphost <path>` to target a specific AppHost.
 
        ## Key workflows
 
        ### Running in agent environments
 
        Use `aspire start` to run the AppHost in the background. When working in a git worktree, use `--isolated` to avoid port conflicts and to prevent sharing user secrets or other local state with other running instances:
 
        ```bash
        aspire start --isolated
        ```
 
        Use `aspire wait <resource>` to block until a resource is healthy before interacting with it:
 
        ```bash
        aspire start --isolated
        aspire wait myapi
        ```
 
        Relaunching is safe — `aspire start` automatically stops any previous instance. Re-run `aspire start` whenever changes are made to the AppHost project.
 
        ### Debugging issues
 
        Before making code changes, inspect the app state:
 
        1. `aspire describe` — check resource status
        2. `aspire otel logs <resource>` — view structured logs
        3. `aspire logs <resource>` — view console output
        4. `aspire otel traces <resource>` — view distributed traces
 
        ### Adding integrations
 
        Use `aspire docs search` to find integration documentation, then `aspire docs get` to read the full guide. Use `aspire add` to add the integration package to the AppHost.
 
        After adding an integration, restart the app with `aspire start` for the new resource to take effect.
 
        ### Using resource MCP tools
 
        Some resources expose MCP tools (e.g. `WithPostgresMcp()` adds SQL query tools). Discover and call them via CLI:
 
        ```bash
        aspire mcp tools                                              # list available tools
        aspire mcp tools --format Json                                # includes input schemas
        aspire mcp call <resource> <tool> --input '{"key":"value"}'   # invoke a tool
        ```
 
        ## Important rules
 
        - **Always start the app first** (`aspire start`) before making changes to verify the starting state.
        - **To restart, just run `aspire start` again** — it automatically stops the previous instance. NEVER use `aspire stop` then `aspire run`. NEVER use `aspire run` at all.
        - Use `--isolated` when working in a worktree.
        - **Avoid persistent containers** early in development to prevent state management issues.
        - **Never install the Aspire workload** — it is obsolete.
        - Prefer `aspire.dev` and `learn.microsoft.com/microsoft/aspire` for official documentation.
 
        ## Playwright CLI
 
        If configured, use Playwright CLI for functional testing of resources. Get endpoints via `aspire describe`. Run `playwright-cli --help` for available commands.
        """;
}