| File: Agents\SkillDefinition.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.Diagnostics; using Aspire.Cli.Projects; using Aspire.Cli.Resources; namespace Aspire.Cli.Agents; /// <summary> /// Represents a skill that can be installed into a skill location. /// </summary> [DebuggerDisplay("Name = {Name}, Description = {Description}, IsDefault = {IsDefault}")] internal sealed class SkillDefinition { /// <summary> /// The Playwright CLI skill for browser automation. /// </summary> public static readonly SkillDefinition PlaywrightCli = new( "playwright-cli", AgentCommandStrings.SkillDescription_PlaywrightCli, skillContent: null, sourceKind: SkillSourceKind.ExternalInstaller, // Playwright is installed via PlaywrightCliInstaller, not a static file installExcludedRelativePaths: [], isDefault: false); /// <summary> /// The dotnet-inspect skill for querying .NET API surfaces. /// Only offered when the workspace contains a .NET AppHost. /// </summary> public static readonly SkillDefinition DotnetInspect = new( CommonAgentApplicators.DotnetInspectSkillName, AgentCommandStrings.SkillDescription_DotnetInspect, CommonAgentApplicators.DotnetInspectSkillFileContent, sourceKind: SkillSourceKind.Static, installExcludedRelativePaths: [], isDefault: false, applicableLanguages: [KnownLanguageId.CSharp]); /// <summary> /// Creates a skill definition sourced from the Aspire skills bundle. All bundle-sourced /// skills are pre-selected by default in the install prompt; callers like <c>aspire new</c> /// and standalone <c>aspire agent init</c> can still narrow that set with a predicate /// (see <c>AgentInitCommand.ExcludeOneTimeSetupSkillsFromDefaults</c>). /// </summary> internal static SkillDefinition CreateAspireSkillsBundle( string name, string description, IReadOnlyList<string>? installExcludedRelativePaths = null, IReadOnlyList<string>? applicableLanguages = null) { ArgumentException.ThrowIfNullOrWhiteSpace(name); ArgumentException.ThrowIfNullOrWhiteSpace(description); return new( name, description, skillContent: null, sourceKind: SkillSourceKind.AspireSkillsBundle, installExcludedRelativePaths: installExcludedRelativePaths ?? [], isDefault: true, applicableLanguages); } private SkillDefinition(string name, string description, string? skillContent, SkillSourceKind sourceKind, IReadOnlyList<string> installExcludedRelativePaths, bool isDefault, IReadOnlyList<string>? applicableLanguages = null) { Name = name; Description = description; SkillContent = skillContent; SourceKind = sourceKind; InstallExcludedRelativePaths = installExcludedRelativePaths; IsDefault = isDefault; ApplicableLanguages = applicableLanguages ?? []; } /// <summary> /// Gets the skill name (used as the folder name under skill locations). /// </summary> public string Name { get; } /// <summary> /// Gets the description shown in the selection prompt. /// </summary> public string Description { get; } /// <summary> /// Gets the content for the top-level SKILL.md file when the skill is defined as a single-file bundle. /// </summary> public string? SkillContent { get; } /// <summary> /// Gets where the installable files for this skill come from. /// </summary> public SkillSourceKind SourceKind { get; } /// <summary> /// Gets whether this skill has files that <c>aspire agent init</c> installs directly. /// </summary> public bool HasInstallableFiles => SkillContent is not null || SourceKind is SkillSourceKind.AspireSkillsBundle; /// <summary> /// Gets relative paths that should be excluded when the skill is installed into a workspace. /// </summary> public IReadOnlyList<string> InstallExcludedRelativePaths { get; } /// <summary> /// Gets whether a bundled file should be installed into a workspace. /// </summary> public bool ShouldInstallFile(string relativePath) { foreach (var excludedPath in InstallExcludedRelativePaths) { if (PathMatchesOrIsUnder(relativePath, excludedPath)) { return false; } } return true; } /// <summary> /// Gets whether this skill should be selected by default. /// </summary> public bool IsDefault { get; } /// <summary> /// Gets the set of language identifiers (from <see cref="KnownLanguageId"/>) 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. /// </summary> public IReadOnlyList<string> ApplicableLanguages { get; } /// <summary> /// Returns whether this skill is applicable for the given detected language. /// A skill with no <see cref="ApplicableLanguages"/> 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 (<paramref name="detectedLanguage"/> is <c>null</c>), language-restricted skills are excluded. /// </summary> 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)); } /// <summary> /// Returns whether this skill has the specified name. /// </summary> public bool HasName(string name, StringComparison comparison = StringComparison.Ordinal) => string.Equals(Name, name, comparison); 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; } /// <summary> /// Gets CLI-defined skills that are not sourced from the Aspire skills bundle. /// </summary> public static IReadOnlyList<SkillDefinition> CliDefined { get; } = [PlaywrightCli, DotnetInspect]; /// <inheritdoc /> public override string ToString() => Name; } /// <summary> /// Identifies where skill files are sourced from. /// </summary> internal enum SkillSourceKind { /// <summary> /// The skill is represented by static content compiled into the CLI. /// </summary> Static, /// <summary> /// The skill is installed from the external Aspire skills bundle. /// </summary> AspireSkillsBundle, /// <summary> /// The skill is managed by a dedicated external installer. /// </summary> ExternalInstaller }