// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using Aspire.Cli.Projects; using Aspire.Cli.Resources; namespace Aspire.Cli.Agents; /// /// Represents a skill that can be installed into a skill location. /// [DebuggerDisplay("Name = {Name}, Description = {Description}, IsDefault = {IsDefault}")] internal sealed class SkillDefinition { /// /// The Aspire skill for CLI commands and workflows. /// public static readonly SkillDefinition Aspire = new( CommonAgentApplicators.AspireSkillName, AgentCommandStrings.SkillDescription_Aspire, skillContent: null, embeddedResourceRoot: CommonAgentApplicators.AspireSkillResourceRoot, installExcludedRelativePaths: [Path.Combine("evals")], isDefault: true); /// /// The Playwright CLI skill for browser automation. /// public static readonly SkillDefinition PlaywrightCli = new( "playwright-cli", AgentCommandStrings.SkillDescription_PlaywrightCli, skillContent: null, embeddedResourceRoot: null, // Playwright is installed via PlaywrightCliInstaller, not a static file installExcludedRelativePaths: [], isDefault: false); /// /// The dotnet-inspect skill for querying .NET API surfaces. /// Only offered when the workspace contains a .NET AppHost. /// public static readonly SkillDefinition DotnetInspect = new( CommonAgentApplicators.DotnetInspectSkillName, AgentCommandStrings.SkillDescription_DotnetInspect, CommonAgentApplicators.DotnetInspectSkillFileContent, embeddedResourceRoot: null, installExcludedRelativePaths: [], isDefault: false, applicableLanguages: [KnownLanguageId.CSharp]); /// /// One-time skill for completing Aspire initialization. /// Installed by aspire init to scan the repo, wire up the AppHost, and configure dependencies. /// public static readonly SkillDefinition Aspireify = new( CommonAgentApplicators.AspireifySkillName, AgentCommandStrings.SkillDescription_Aspireify, skillContent: null, embeddedResourceRoot: CommonAgentApplicators.AspireifySkillResourceRoot, installExcludedRelativePaths: [], isDefault: true); private SkillDefinition(string name, string description, string? skillContent, string? embeddedResourceRoot, IReadOnlyList installExcludedRelativePaths, bool isDefault, IReadOnlyList? applicableLanguages = null) { Name = name; Description = description; SkillContent = skillContent; EmbeddedResourceRoot = embeddedResourceRoot; InstallExcludedRelativePaths = installExcludedRelativePaths; IsDefault = isDefault; ApplicableLanguages = applicableLanguages ?? []; } /// /// Gets the skill name (used as the folder name under skill locations). /// public string Name { get; } /// /// Gets the description shown in the selection prompt. /// public string Description { get; } /// /// Gets the content for the top-level SKILL.md file when the skill is defined as a single-file bundle, /// or null when installable files come from or another installer. /// public string? SkillContent { get; } /// /// Gets the embedded resource root for bundled skill files, or null if the skill is not installed from an embedded file tree. /// public string? EmbeddedResourceRoot { get; } /// /// Gets relative paths that should be excluded when the skill is installed into a workspace. /// public IReadOnlyList InstallExcludedRelativePaths { get; } /// /// Gets whether a bundled file should be installed into a workspace. /// public bool ShouldInstallFile(string relativePath) { foreach (var excludedPath in InstallExcludedRelativePaths) { if (PathMatchesOrIsUnder(relativePath, excludedPath)) { return false; } } return true; } /// /// Gets whether this skill should be selected by default. /// public bool IsDefault { get; } /// /// Gets the set of language identifiers (from ) this skill applies to. /// An empty list means the skill is language-agnostic and always offered. /// When non-empty, the skill is only offered when the detected language matches one of the entries. /// public IReadOnlyList ApplicableLanguages { get; } /// /// Returns whether this skill is applicable for the given detected language. /// A skill with no restrictions is always applicable. /// A skill with restrictions is only applicable when the detected language matches one of the entries. /// When no language is detected ( is null), language-restricted skills are excluded. /// public bool IsApplicableToLanguage(LanguageId? detectedLanguage) { if (ApplicableLanguages.Count == 0) { return true; } if (detectedLanguage is null) { return false; } return ApplicableLanguages.Any(l => string.Equals(l, detectedLanguage.Value.Value, StringComparison.OrdinalIgnoreCase)); } private static bool PathMatchesOrIsUnder(string relativePath, string excludedPath) { if (string.Equals(relativePath, excludedPath, StringComparison.Ordinal)) { return true; } if (!relativePath.StartsWith(excludedPath, StringComparison.Ordinal)) { return false; } return relativePath.Length > excludedPath.Length && relativePath[excludedPath.Length] == Path.DirectorySeparatorChar; } /// /// Gets all available skill definitions. /// public static IReadOnlyList All { get; } = [Aspire, Aspireify, PlaywrightCli, DotnetInspect]; /// public override string ToString() => Name; }