diff --git a/src/Aspire.Cli/Agents/Playwright/PlaywrightCliInstaller.cs b/src/Aspire.Cli/Agents/Playwright/PlaywrightCliInstaller.cs index c519c6b210b..1aac6c747e9 100644 --- a/src/Aspire.Cli/Agents/Playwright/PlaywrightCliInstaller.cs +++ b/src/Aspire.Cli/Agents/Playwright/PlaywrightCliInstaller.cs @@ -85,7 +85,7 @@ internal sealed class PlaywrightCliInstaller( /// /// Generates skills in isolation before copying them to the selected absolute destinations. /// - public async Task<(PlaywrightInstallStatus Status, string? Message)> InstallSelectedAsync(IReadOnlySet selectedSkillDirectories, CancellationToken cancellationToken) + public async Task<(PlaywrightInstallStatus Status, string? Message, IReadOnlyList InstalledSkillDirectories)> InstallSelectedAsync(IReadOnlySet selectedSkillDirectories, CancellationToken cancellationToken) { // The upstream generator chooses its own output directories. Generate in isolation, // then copy only to destinations belonging to the selected agents and scope. @@ -125,21 +125,21 @@ internal sealed class PlaywrightCliInstaller( /// The workspace/repository root directory. /// The skill directories the user explicitly selected. /// A token to cancel the operation. - /// A tuple where Status indicates the outcome (installed, skipped, or failed) and Message contains additional details when applicable. - public async Task<(PlaywrightInstallStatus Status, string? Message)> InstallAsync(string repoRoot, IReadOnlySet selectedSkillDirectories, CancellationToken cancellationToken) + /// The installation status, optional message, and selected skill directories that were successfully written. + public async Task<(PlaywrightInstallStatus Status, string? Message, IReadOnlyList InstalledSkillDirectories)> InstallAsync(string repoRoot, IReadOnlySet selectedSkillDirectories, CancellationToken cancellationToken) { return await interactionService.ShowStatusAsync( AgentCommandStrings.PlaywrightCliInstaller_InstallingStatus, () => InstallCoreAsync(repoRoot, selectedSkillDirectories, cancellationToken)); } - private async Task<(PlaywrightInstallStatus Status, string? Message)> InstallCoreAsync(string repoRoot, IReadOnlySet selectedSkillDirectories, CancellationToken cancellationToken) + private async Task<(PlaywrightInstallStatus Status, string? Message, IReadOnlyList InstalledSkillDirectories)> InstallCoreAsync(string repoRoot, IReadOnlySet selectedSkillDirectories, CancellationToken cancellationToken) { // Early exit if npm is not available — playwright-cli requires npm. if (!npmRunner.IsAvailable) { logger.LogDebug("npm is not available on PATH, skipping Playwright CLI installation."); - return (PlaywrightInstallStatus.Skipped, null); + return (PlaywrightInstallStatus.Skipped, null, []); } // Step 1: Resolve the target version from the public npm registry. @@ -156,7 +156,7 @@ internal sealed class PlaywrightCliInstaller( // See https://semver.org/spec/v2.0.0.html for the accepted shape. if (!SemVersion.TryParse(versionOverride, SemVersionStyles.Strict, out _)) { - return (PlaywrightInstallStatus.Failed, string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.PlaywrightCliInstaller_InvalidVersionOverride, VersionOverrideKey, versionOverride)); + return (PlaywrightInstallStatus.Failed, string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.PlaywrightCliInstaller_InvalidVersionOverride, VersionOverrideKey, versionOverride), []); } effectiveRange = versionOverride; @@ -172,7 +172,7 @@ internal sealed class PlaywrightCliInstaller( if (packageInfo is null) { - return (PlaywrightInstallStatus.Failed, string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.PlaywrightCliInstaller_FailedToResolvePackage, NpmPackageInfo.FormatPackageSpecifier(PackageName, effectiveRange))); + return (PlaywrightInstallStatus.Failed, string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.PlaywrightCliInstaller_FailedToResolvePackage, NpmPackageInfo.FormatPackageSpecifier(PackageName, effectiveRange)), []); } logger.LogDebug("Resolved {PackageSpecifier}.", NpmPackageInfo.FormatPackageSpecifier(PackageName, packageInfo.Version)); @@ -221,7 +221,7 @@ internal sealed class PlaywrightCliInstaller( if (tarballPath is null) { logger.LogWarning("Failed to download {PackageSpecifier}.", NpmPackageInfo.FormatPackageSpecifier(PackageName, packageInfo.Version)); - return (PlaywrightInstallStatus.Failed, string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.PlaywrightCliInstaller_FailedToDownload, NpmPackageInfo.FormatPackageSpecifier(PackageName, packageInfo.Version))); + return (PlaywrightInstallStatus.Failed, string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.PlaywrightCliInstaller_FailedToDownload, NpmPackageInfo.FormatPackageSpecifier(PackageName, packageInfo.Version)), []); } if (!validationDisabled) @@ -250,7 +250,7 @@ internal sealed class PlaywrightCliInstaller( NpmPackageInfo.FormatPackageSpecifier(PackageName, packageInfo.Version), provenanceResult.Outcome, ExpectedSourceRepository); - return (PlaywrightInstallStatus.Failed, string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.PlaywrightCliInstaller_ProvenanceVerificationFailed, NpmPackageInfo.FormatPackageSpecifier(PackageName, packageInfo.Version), provenanceResult.Outcome)); + return (PlaywrightInstallStatus.Failed, string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.PlaywrightCliInstaller_ProvenanceVerificationFailed, NpmPackageInfo.FormatPackageSpecifier(PackageName, packageInfo.Version), provenanceResult.Outcome), []); } logger.LogDebug( @@ -266,7 +266,7 @@ internal sealed class PlaywrightCliInstaller( if (!installSuccess) { logger.LogWarning("Failed to install {PackageSpecifier} globally.", NpmPackageInfo.FormatPackageSpecifier(PackageName, packageInfo.Version)); - return (PlaywrightInstallStatus.Failed, string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.PlaywrightCliInstaller_FailedToInstallGlobally, NpmPackageInfo.FormatPackageSpecifier(PackageName, packageInfo.Version))); + return (PlaywrightInstallStatus.Failed, string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.PlaywrightCliInstaller_FailedToInstallGlobally, NpmPackageInfo.FormatPackageSpecifier(PackageName, packageInfo.Version)), []); } // Step 6: Generate skill files and mirror to selected locations. @@ -294,7 +294,7 @@ internal sealed class PlaywrightCliInstaller( /// to the user-selected locations and cleans up any unselected locations that /// playwright-cli created during this run. /// - private async Task<(PlaywrightInstallStatus Status, string? Message)> InstallAndMirrorSkillsAsync( + private async Task<(PlaywrightInstallStatus Status, string? Message, IReadOnlyList InstalledSkillDirectories)> InstallAndMirrorSkillsAsync( string repoRoot, IReadOnlySet selectedSkillDirectories, CancellationToken cancellationToken) @@ -304,7 +304,7 @@ internal sealed class PlaywrightCliInstaller( var skillsInstalled = await playwrightCliRunner.InstallSkillsAsync(repoRoot, cancellationToken); if (!skillsInstalled) { - return (PlaywrightInstallStatus.Failed, AgentCommandStrings.PlaywrightCliInstaller_FailedToGenerateSkillFiles); + return (PlaywrightInstallStatus.Failed, AgentCommandStrings.PlaywrightCliInstaller_FailedToGenerateSkillFiles, []); } return MirrorSkillFiles(repoRoot, selectedSkillDirectories, preExisting); @@ -348,7 +348,7 @@ .. clients.Select(client => Path.GetRelativePath(repoRoot, /// user-selected skill directories, then cleans up any directories that /// playwright-cli created in unselected locations during this run. /// - private (PlaywrightInstallStatus Status, string? Message) MirrorSkillFiles(string repoRoot, IReadOnlySet selectedSkillDirectories, HashSet preExistingLocations) + private (PlaywrightInstallStatus Status, string? Message, IReadOnlyList InstalledSkillDirectories) MirrorSkillFiles(string repoRoot, IReadOnlySet selectedSkillDirectories, HashSet preExistingLocations) { // Playwright emits Claude-format skills even when another client is selected. // https://github.com/microsoft/playwright-cli/issues/294 @@ -359,15 +359,16 @@ .. clients.Select(client => Path.GetRelativePath(repoRoot, if (!Directory.Exists(primarySkillDir)) { logger.LogWarning("Primary skill directory does not exist: {PrimarySkillDir}", primarySkillDir); - return (PlaywrightInstallStatus.Failed, AgentCommandStrings.PlaywrightCliInstaller_FailedToGenerateSkillFiles); + return (PlaywrightInstallStatus.Failed, AgentCommandStrings.PlaywrightCliInstaller_FailedToGenerateSkillFiles, []); } - var mirrorFailures = 0; + var installedSkillDirectories = new List(); // Mirror to each user-selected location (skip the primary — it's the source). foreach (var skillBaseDir in selectedSkillDirectories) { if (string.Equals(skillBaseDir, primarySkillBaseDirectory, StringComparison.OrdinalIgnoreCase)) { + installedSkillDirectories.Add(skillBaseDir); continue; } @@ -376,12 +377,12 @@ .. clients.Select(client => Path.GetRelativePath(repoRoot, try { SyncDirectory(primarySkillDir, targetSkillDir); + installedSkillDirectories.Add(skillBaseDir); logger.LogDebug("Mirrored playwright-cli skills to {TargetDir}.", targetSkillDir); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { logger.LogWarning(ex, "Failed to mirror playwright-cli skills to {TargetDir}.", targetSkillDir); - mirrorFailures++; } } @@ -419,14 +420,14 @@ .. clients.Select(client => Path.GetRelativePath(repoRoot, } } - if (mirrorFailures == 0) + if (installedSkillDirectories.Count == selectedSkillDirectories.Count) { - return (PlaywrightInstallStatus.Installed, null); + return (PlaywrightInstallStatus.Installed, null, installedSkillDirectories); } - return mirrorFailures == selectedSkillDirectories.Count - ? (PlaywrightInstallStatus.Failed, AgentCommandStrings.PlaywrightCliInstaller_FailedToMirrorSkillFiles) - : (PlaywrightInstallStatus.InstalledWithWarnings, AgentCommandStrings.PlaywrightCliInstaller_InstalledWithMirrorWarnings); + return installedSkillDirectories.Count == 0 + ? (PlaywrightInstallStatus.Failed, AgentCommandStrings.PlaywrightCliInstaller_FailedToMirrorSkillFiles, installedSkillDirectories) + : (PlaywrightInstallStatus.InstalledWithWarnings, AgentCommandStrings.PlaywrightCliInstaller_InstalledWithMirrorWarnings, installedSkillDirectories); } /// diff --git a/src/Aspire.Cli/Commands/AgentInitCommand.cs b/src/Aspire.Cli/Commands/AgentInitCommand.cs index 0c861b287ed..00534453395 100644 --- a/src/Aspire.Cli/Commands/AgentInitCommand.cs +++ b/src/Aspire.Cli/Commands/AgentInitCommand.cs @@ -514,10 +514,6 @@ private async Task ExecuteAgentInitAsync( { selectedSkills.Add(SkillDefinition.DotnetInspect); } - if (selectedSkills.Count > 0 || configureMcp) - { - DisplayInstallationLocations(selectedClients, scope, workspaceRoot, selectedSkills.Count > 0, configureMcp); - } // The selected clients determine destinations; defaults never fabricate detection. var selectedSkillDirs = new HashSet(OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal); @@ -532,7 +528,7 @@ private async Task ExecuteAgentInitAsync( { cancellationToken.ThrowIfCancellationRequested(); registrationResults = await AgentConfiguration.ApplyRemoteAsync(selectedClients, workspaceRoot, scope, skillSources, cancellationToken); - hasErrors |= DisplayRemoteConfigurationResults(registrationResults, workspaceRoot); + hasErrors |= DisplayRemoteConfigurationResults(registrationResults, skillSources, workspaceRoot); } // --- Phase 3: Apply copied skill files at each unique destination --- @@ -552,25 +548,25 @@ private async Task ExecuteAgentInitAsync( var installResult = await InstallSkillAsync( skillDirectory, - AgentPath.GetDisplayPath(skillDirectory, scope, workspaceRoot, ExecutionContext), skill, cancellationToken); - hasErrors |= !installResult.Succeeded; - if (installResult.UpdatedSkill is not null) + if (installResult is not null) + { + installedSkills.Add(installResult); + } + else { - installedSkills.Add(installResult.UpdatedSkill); + hasErrors = true; } } } - DisplayInstalledSkillsSummary(installedSkills); - // --- Phase 4: Handle Playwright CLI (installs binary + mirrors skill files to registered directories) --- if (selectedSkills.Contains(SkillDefinition.PlaywrightCli)) { try { - var (status, message) = await _playwrightCliInstaller.InstallSelectedAsync(selectedSkillDirs, cancellationToken); + var (status, message, installedSkillDirectories) = await _playwrightCliInstaller.InstallSelectedAsync(selectedSkillDirs, cancellationToken); switch (status) { case PlaywrightInstallStatus.Installed: @@ -590,14 +586,21 @@ private async Task ExecuteAgentInitAsync( default: throw new UnreachableException($"Unexpected PlaywrightInstallStatus: {status}"); } + + installedSkills.AddRange(installedSkillDirectories.Select(directory => + new InstalledSkillSummaryItem(SkillDefinition.PlaywrightCli.Name, directory))); } catch (InvalidOperationException ex) { InteractionService.DisplayError(ex.Message); hasErrors = true; } + + InteractionService.DisplayEmptyLine(); } + DisplayInstalledSkillsSummary(installedSkills, scope, workspaceRoot); + // --- Phase 5: Apply MCP server configuration if the user confirmed --- if (configureMcp) { @@ -805,10 +808,8 @@ internal static string SimplifyDescription(string description) /// /// Installs the files for a skill at the specified location, creating or updating them as needed. /// - /// The install result, including the skill/location pair when files were updated. - private async Task InstallSkillAsync( + private async Task InstallSkillAsync( string skillBaseDirectory, - string displayLocation, SkillDefinition skill, CancellationToken cancellationToken) { @@ -830,55 +831,58 @@ private async Task InstallSkillAsync( var existingContent = await File.ReadAllTextAsync(fullPath, cancellationToken); if (string.Equals(existingContent.ReplaceLineEndings("\n"), skillContent.ReplaceLineEndings("\n"), StringComparison.Ordinal)) { - return new(Succeeded: true, UpdatedSkill: null); + return new(skill.Name, skillBaseDirectory); } } await File.WriteAllTextAsync(fullPath, skillContent, cancellationToken); - return new(Succeeded: true, new InstalledSkillSummaryItem(skill.Name, displayLocation)); + return new(skill.Name, skillBaseDirectory); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException) { InteractionService.DisplayError( string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_FailedToInstallSkill, skill.Name, fullSkillDirectoryPath, ex.Message)); - return new(Succeeded: false, UpdatedSkill: null); + return null; } } - private void DisplayInstalledSkillsSummary(IReadOnlyList installedSkills) + private void DisplayInstalledSkillsSummary(IReadOnlyList installedSkills, AgentConfigurationScope scope, DirectoryInfo workspaceRoot) { if (installedSkills.Count == 0) { return; } - var skillNames = string.Join(", ", GetUniqueValues(installedSkills.Select(static installedSkill => installedSkill.SkillName))); - var locations = string.Join(", ", GetUniqueValues(installedSkills.Select(static installedSkill => installedSkill.DisplayLocation))); - var message = string.Join(Environment.NewLine, - AgentCommandStrings.InitCommand_InstalledSkillsSummary, - $" {string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkillsSummarySkills, skillNames)}", - $" {string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkillsSummaryLocations, locations)}"); - - InteractionService.DisplayMessage(KnownEmojis.Robot, message); - } + var lines = new List { AgentCommandStrings.InitCommand_InstalledSkillsSummary }; + lines.Add($" {string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkillsSummarySkills, + string.Join(", ", installedSkills.Select(static skill => skill.SkillName).Distinct(StringComparer.Ordinal)))}"); + var locations = new List(); + var hasSharedLocation = false; + foreach (var directory in installedSkills.Select(static skill => skill.Directory).Distinct(StringComparers.FileSystemPath)) + { + var clients = _clients.Where(client => + StringComparers.FileSystemPath.Equals(directory, client.GetSkillBaseDirectory(scope, workspaceRoot))) + .ToArray(); + hasSharedLocation |= clients.Length > 1; + locations.Add(string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_LocationWithAgents, + AgentPath.GetDisplayPath(directory, scope, workspaceRoot, ExecutionContext), + string.Join(", ", clients.Select(static client => client.DisplayName)))); + } - private static IReadOnlyList GetUniqueValues(IEnumerable values) - { - var uniqueValues = new List(); - var seenValues = new HashSet(StringComparer.Ordinal); + lines.Add($" {string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkillsSummaryLocations, + string.Join(", ", locations))}"); - foreach (var value in values) + var message = string.Join(Environment.NewLine, lines.Select(static line => line.EscapeMarkup())); + if (hasSharedLocation) { - if (seenValues.Add(value)) - { - uniqueValues.Add(value); - } + message += $"{Environment.NewLine} [dim]{AgentCommandStrings.InitCommand_SharedLocationNotice.EscapeMarkup()}[/]"; } - return uniqueValues; + InteractionService.DisplayMessage(KnownEmojis.Robot, message, allowMarkup: true); + InteractionService.DisplayEmptyLine(); } - private bool DisplayRemoteConfigurationResults(IReadOnlyList results, DirectoryInfo workspaceRoot) + private bool DisplayRemoteConfigurationResults(IReadOnlyList results, IReadOnlyList sources, DirectoryInfo workspaceRoot) { var hasErrors = false; foreach (var resultGroup in results.GroupBy(result => result.Status switch @@ -888,89 +892,53 @@ private bool DisplayRemoteConfigurationResults(IReadOnlyList _ => throw new UnreachableException() })) { - var entries = resultGroup.SelectMany(result => result.Clients, (result, client) => (Result: result, Client: client)); - var targets = resultGroup.Key - ? entries.Select(entry => - { - var target = string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_ConfigurationTarget, - entry.Client.DisplayName, AgentPath.GetDisplayPath(entry.Result.TargetPath, workspaceRoot, ExecutionContext)); - - return entry.Result.Message is null ? target : $"{target}{Environment.NewLine} {entry.Result.Message}"; - }) - : entries.GroupBy(entry => entry.Client.Id, StringComparers.AgentClientId).Select(agent => - string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_ConfigurationTarget, - agent.First().Client.DisplayName, - string.Join(", ", agent.Select(entry => entry.Result.TargetPath).Distinct(StringComparers.FileSystemPath) - .Select(path => AgentPath.GetDisplayPath(path, workspaceRoot, ExecutionContext))))); - var format = resultGroup.Key ? AgentCommandStrings.InitCommand_FailedTarget : AgentCommandStrings.InitCommand_ConfiguredTargets; - var message = string.Format(CultureInfo.CurrentCulture, format, string.Join($"{Environment.NewLine} ", targets)); if (resultGroup.Key) { - InteractionService.DisplayError(message); + var failedTargets = resultGroup.SelectMany(result => result.Clients, (result, client) => + { + var target = string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_ConfigurationTarget, + client.DisplayName, AgentPath.GetDisplayPath(result.TargetPath, workspaceRoot, ExecutionContext)); + + return result.Message is null ? target : $"{target}{Environment.NewLine} {result.Message}"; + }); + InteractionService.DisplayError(string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_FailedTarget, + string.Join($"{Environment.NewLine} ", failedTargets))); + InteractionService.DisplayEmptyLine(); hasErrors = true; + continue; } - else - { - InteractionService.DisplayMessage(KnownEmojis.Robot, message); - } - } - - return hasErrors; - } - private void DisplayInstallationLocations(IReadOnlyList clients, AgentConfigurationScope scope, DirectoryInfo workspaceRoot, bool installSkills, bool configureMcp) - { - var lines = new List { AgentCommandStrings.InitCommand_InstallationLocations }; - var pathComparer = OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; - var hasSharedLocation = false; - if (installSkills) - { - var directories = clients.Select(client => client.GetSkillBaseDirectory(scope, workspaceRoot)) - .Distinct(pathComparer); - foreach (var directory in directories) + var targets = resultGroup.Select(result => + string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_LocationWithAgents, + AgentPath.GetDisplayPath(result.TargetPath, workspaceRoot, ExecutionContext), + string.Join(", ", result.Clients.Select(static client => client.DisplayName)))); + var sourceUrls = new List(); + if (resultGroup.Any(result => result.Clients.Any(client => client.Id != OpenCodeAgentClient.ClientId))) { - AddLocation(AgentCommandStrings.InitCommand_SkillsLocationHint, directory, - [.. _clients.Where(client => pathComparer.Equals(directory, - client.GetSkillBaseDirectory(scope, workspaceRoot)))]); + sourceUrls.AddRange(sources.Select(static source => source.GetRepositoryUrl())); } - } - if (configureMcp) - { - var paths = clients.Select(client => client.GetMcpConfigPath(scope, workspaceRoot)) - .Distinct(pathComparer); - foreach (var path in paths) + foreach (var result in resultGroup.Where(result => result.Clients.Any(client => client.Id == OpenCodeAgentClient.ClientId))) { - AddLocation(AgentCommandStrings.InitCommand_McpLocationHint, path, - [.. _clients.Where(client => pathComparer.Equals(path, - client.GetMcpConfigPath(scope, workspaceRoot)))]); + var catalogs = AspireSkillsRemoteConfiguration.ReadOpenCodeSkills(result.Configuration!); + sourceUrls.AddRange((catalogs ?? []).Select(AspireSkillsRemoteConfiguration.String).OfType() + .Where(url => sources.Any(source => AspireSkillsRemoteConfiguration.IsOpenCodeCatalog(url, source)))); } - } - if (hasSharedLocation) - { - lines.Add(AgentCommandStrings.InitCommand_SharedLocationNotice); + var sourceLine = string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_ConfiguredSourceList, + string.Join(", ", sourceUrls.Distinct(StringComparer.Ordinal))); + var locationLine = string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkillsSummaryLocations, + string.Join(", ", targets)); + InteractionService.DisplayMessage(KnownEmojis.Robot, + string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_ConfiguredTargets, + $"{sourceLine}{Environment.NewLine} {locationLine}")); + InteractionService.DisplayEmptyLine(); } - InteractionService.DisplaySubtleMessage(string.Join(Environment.NewLine, lines)); - - void AddLocation(string format, string path, IReadOnlyList sharedClients) - { - var location = AgentPath.GetDisplayPath(path, scope, workspaceRoot, ExecutionContext); - if (sharedClients.Count > 1) - { - hasSharedLocation = true; - location = string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_SharedLocation, - location, string.Join(", ", sharedClients.Select(static client => client.DisplayName))); - } - - lines.Add($" {string.Format(CultureInfo.CurrentCulture, format, location)}"); - } + return hasErrors; } - private sealed record InstalledSkillSummaryItem(string SkillName, string DisplayLocation); - - private readonly record struct SkillInstallResult(bool Succeeded, InstalledSkillSummaryItem? UpdatedSkill); + private sealed record InstalledSkillSummaryItem(string SkillName, string Directory); } internal readonly record struct AgentInitExecutionResult( diff --git a/src/Aspire.Cli/Resources/AgentCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/AgentCommandStrings.Designer.cs index 701441efdef..b0097f84a33 100644 --- a/src/Aspire.Cli/Resources/AgentCommandStrings.Designer.cs +++ b/src/Aspire.Cli/Resources/AgentCommandStrings.Designer.cs @@ -241,7 +241,16 @@ internal static string InitCommand_ConfigurationTarget { } /// - /// Looks up a localized string similar to Configured Aspire plugin: + /// Looks up a localized string similar to Sources: {0}. + /// + internal static string InitCommand_ConfiguredSourceList { + get { + return ResourceManager.GetString("InitCommand_ConfiguredSourceList", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configured sources: /// {0}. /// internal static string InitCommand_ConfiguredTargets { @@ -350,7 +359,7 @@ internal static string InitCommand_InstalledPlaywrightCli { } /// - /// Looks up a localized string similar to Installed Aspire agent skills:. + /// Looks up a localized string similar to Installed skills:. /// internal static string InitCommand_InstalledSkillsSummary { get { @@ -367,6 +376,15 @@ internal static string InitCommand_InstalledSkillsSummaryLocations { } } + /// + /// Looks up a localized string similar to {0} ({1}). + /// + internal static string InitCommand_LocationWithAgents { + get { + return ResourceManager.GetString("InitCommand_LocationWithAgents", resourceCulture); + } + } + /// /// Looks up a localized string similar to Skills: {0}. /// diff --git a/src/Aspire.Cli/Resources/AgentCommandStrings.resx b/src/Aspire.Cli/Resources/AgentCommandStrings.resx index 84e31845a56..6b48e5f45f1 100644 --- a/src/Aspire.Cli/Resources/AgentCommandStrings.resx +++ b/src/Aspire.Cli/Resources/AgentCommandStrings.resx @@ -151,7 +151,7 @@ Playwright CLI requires npm, which was not found on PATH. Skipping installation. - Installed Aspire agent skills: + Installed skills: Skills: {0} @@ -159,6 +159,10 @@ Locations: {0} + + {0} ({1}) + {0} is a location, and {1} lists the agents that use it, separated by commas. + Failed to install {0} skill at {1}: {2} @@ -294,9 +298,13 @@ {0} lists the configured skill source repository and/or catalog URLs for the selected agent clients, one per line. - Configured Aspire plugin: + Configured sources: {0} - {0} lists one agent per line, with that agent's configuration paths comma-separated. + {0} contains the source URLs and configuration locations, each on its own labeled line. + + + Sources: {0} + {0} lists the configured repository and catalog URLs separated by commas. {0}: {1} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.cs.xlf index f9be37c226f..719c1dbe6ab 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.cs.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.cs.xlf @@ -107,12 +107,17 @@ Chcete nakonfigurovat server MCP Aspire pro vybraná prostředí agentů? + + Sources: {0} + Sources: {0} + {0} lists the configured repository and catalog URLs separated by commas. + - Configured Aspire plugin: + Configured sources: {0} - Configured Aspire plugin: + Configured sources: {0} - {0} lists one agent per line, with that agent's configuration paths comma-separated. + {0} contains the source URLs and configuration locations, each on its own labeled line. Initialize configuration and skills for selected agent clients @@ -162,8 +167,8 @@ - Installed Aspire agent skills: - Nainstalované dovednosti agentů Aspire: + Installed skills: + Nainstalované dovednosti agentů Aspire: @@ -198,6 +203,11 @@ {0} {0} lists the local skill entry-point paths, one per line. + + {0} ({1}) + {0} ({1}) + {0} is a location, and {1} lists the agents that use it, separated by commas. + Configure Aspire MCP for {0} at {1} Nakonfigurovat MCP Aspire pro {0} v umístění {1} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.de.xlf index ea73157596f..6cc2273eac5 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.de.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.de.xlf @@ -107,12 +107,17 @@ Den Aspire-MCP-Server für ausgewählte Agentenumgebungen konfigurieren. + + Sources: {0} + Sources: {0} + {0} lists the configured repository and catalog URLs separated by commas. + - Configured Aspire plugin: + Configured sources: {0} - Configured Aspire plugin: + Configured sources: {0} - {0} lists one agent per line, with that agent's configuration paths comma-separated. + {0} contains the source URLs and configuration locations, each on its own labeled line. Initialize configuration and skills for selected agent clients @@ -162,8 +167,8 @@ - Installed Aspire agent skills: - Installierte Aspire-Agent-Skills: + Installed skills: + Installierte Aspire-Agent-Skills: @@ -198,6 +203,11 @@ {0} {0} lists the local skill entry-point paths, one per line. + + {0} ({1}) + {0} ({1}) + {0} is a location, and {1} lists the agents that use it, separated by commas. + Configure Aspire MCP for {0} at {1} Aspire MCP für {0} unter {1} konfigurieren diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.es.xlf index 0ef2d54363d..f769e4684ad 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.es.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.es.xlf @@ -107,12 +107,17 @@ ¿Configurar el servidor MCP de Aspire para los entornos de agente seleccionados? + + Sources: {0} + Sources: {0} + {0} lists the configured repository and catalog URLs separated by commas. + - Configured Aspire plugin: + Configured sources: {0} - Configured Aspire plugin: + Configured sources: {0} - {0} lists one agent per line, with that agent's configuration paths comma-separated. + {0} contains the source URLs and configuration locations, each on its own labeled line. Initialize configuration and skills for selected agent clients @@ -162,8 +167,8 @@ - Installed Aspire agent skills: - Aptitudes del agente de Aspire instaladas: + Installed skills: + Aptitudes del agente de Aspire instaladas: @@ -198,6 +203,11 @@ {0} {0} lists the local skill entry-point paths, one per line. + + {0} ({1}) + {0} ({1}) + {0} is a location, and {1} lists the agents that use it, separated by commas. + Configure Aspire MCP for {0} at {1} Configurar MCP de Aspire para {0} en {1} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.fr.xlf index f1abf18afe3..c89383a6fa5 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.fr.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.fr.xlf @@ -107,12 +107,17 @@ Voulez-vous configurer le serveur Aspire MCP pour les environnements d’agents détectés ? + + Sources: {0} + Sources: {0} + {0} lists the configured repository and catalog URLs separated by commas. + - Configured Aspire plugin: + Configured sources: {0} - Configured Aspire plugin: + Configured sources: {0} - {0} lists one agent per line, with that agent's configuration paths comma-separated. + {0} contains the source URLs and configuration locations, each on its own labeled line. Initialize configuration and skills for selected agent clients @@ -162,8 +167,8 @@ - Installed Aspire agent skills: - Compétences d’assistant Aspire installées : + Installed skills: + Compétences d’assistant Aspire installées : @@ -198,6 +203,11 @@ {0} {0} lists the local skill entry-point paths, one per line. + + {0} ({1}) + {0} ({1}) + {0} is a location, and {1} lists the agents that use it, separated by commas. + Configure Aspire MCP for {0} at {1} Configurer Aspire MCP pour {0} à {1} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.it.xlf index e16d9aaa738..bf941e5fbf9 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.it.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.it.xlf @@ -107,12 +107,17 @@ Configurare il server MCP di Aspire per gli ambienti degli agenti selezionati? + + Sources: {0} + Sources: {0} + {0} lists the configured repository and catalog URLs separated by commas. + - Configured Aspire plugin: + Configured sources: {0} - Configured Aspire plugin: + Configured sources: {0} - {0} lists one agent per line, with that agent's configuration paths comma-separated. + {0} contains the source URLs and configuration locations, each on its own labeled line. Initialize configuration and skills for selected agent clients @@ -162,8 +167,8 @@ - Installed Aspire agent skills: - Competenze dell'agente Aspire installate: + Installed skills: + Competenze dell'agente Aspire installate: @@ -198,6 +203,11 @@ {0} {0} lists the local skill entry-point paths, one per line. + + {0} ({1}) + {0} ({1}) + {0} is a location, and {1} lists the agents that use it, separated by commas. + Configure Aspire MCP for {0} at {1} Configura MCP di Aspire per {0} in {1} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ja.xlf index 777474cb633..74f442cf263 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ja.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ja.xlf @@ -107,12 +107,17 @@ 選択したエージェント環境用に Aspire MCP サーバーを構成しますか? + + Sources: {0} + Sources: {0} + {0} lists the configured repository and catalog URLs separated by commas. + - Configured Aspire plugin: + Configured sources: {0} - Configured Aspire plugin: + Configured sources: {0} - {0} lists one agent per line, with that agent's configuration paths comma-separated. + {0} contains the source URLs and configuration locations, each on its own labeled line. Initialize configuration and skills for selected agent clients @@ -162,8 +167,8 @@ - Installed Aspire agent skills: - インストールされている Aspire エージェントのスキル: + Installed skills: + インストールされている Aspire エージェントのスキル: @@ -198,6 +203,11 @@ {0} {0} lists the local skill entry-point paths, one per line. + + {0} ({1}) + {0} ({1}) + {0} is a location, and {1} lists the agents that use it, separated by commas. + Configure Aspire MCP for {0} at {1} Aspire MCP を {0} で {1} 用に構成する diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ko.xlf index 4dea3e10e99..524dbecd8dc 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ko.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ko.xlf @@ -107,12 +107,17 @@ 선택한 에이전트 환경에 대해 Aspire MCP 서버를 구성하시겠습니까? + + Sources: {0} + Sources: {0} + {0} lists the configured repository and catalog URLs separated by commas. + - Configured Aspire plugin: + Configured sources: {0} - Configured Aspire plugin: + Configured sources: {0} - {0} lists one agent per line, with that agent's configuration paths comma-separated. + {0} contains the source URLs and configuration locations, each on its own labeled line. Initialize configuration and skills for selected agent clients @@ -162,8 +167,8 @@ - Installed Aspire agent skills: - 설치된 Aspire 에이전트 기술: + Installed skills: + 설치된 Aspire 에이전트 기술: @@ -198,6 +203,11 @@ {0} {0} lists the local skill entry-point paths, one per line. + + {0} ({1}) + {0} ({1}) + {0} is a location, and {1} lists the agents that use it, separated by commas. + Configure Aspire MCP for {0} at {1} {1}에서 {0}용 Aspire MCP 구성 diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pl.xlf index 8f2d47d674b..360606df05c 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pl.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pl.xlf @@ -107,12 +107,17 @@ Skonfigurować serwer MCP Aspire dla wykrytych środowisk agentów? + + Sources: {0} + Sources: {0} + {0} lists the configured repository and catalog URLs separated by commas. + - Configured Aspire plugin: + Configured sources: {0} - Configured Aspire plugin: + Configured sources: {0} - {0} lists one agent per line, with that agent's configuration paths comma-separated. + {0} contains the source URLs and configuration locations, each on its own labeled line. Initialize configuration and skills for selected agent clients @@ -162,8 +167,8 @@ - Installed Aspire agent skills: - Zainstalowane umiejętności agenta Aspire: + Installed skills: + Zainstalowane umiejętności agenta Aspire: @@ -198,6 +203,11 @@ {0} {0} lists the local skill entry-point paths, one per line. + + {0} ({1}) + {0} ({1}) + {0} is a location, and {1} lists the agents that use it, separated by commas. + Configure Aspire MCP for {0} at {1} Skonfiguruj Aspire MCP dla {0} w {1} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pt-BR.xlf index d98dda79e65..29493db6611 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pt-BR.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pt-BR.xlf @@ -107,12 +107,17 @@ Configurar o servidor MCP do Aspire para os ambientes de agente selecionados? + + Sources: {0} + Sources: {0} + {0} lists the configured repository and catalog URLs separated by commas. + - Configured Aspire plugin: + Configured sources: {0} - Configured Aspire plugin: + Configured sources: {0} - {0} lists one agent per line, with that agent's configuration paths comma-separated. + {0} contains the source URLs and configuration locations, each on its own labeled line. Initialize configuration and skills for selected agent clients @@ -162,8 +167,8 @@ - Installed Aspire agent skills: - Habilidades de agente do Aspire instaladas: + Installed skills: + Habilidades de agente do Aspire instaladas: @@ -198,6 +203,11 @@ {0} {0} lists the local skill entry-point paths, one per line. + + {0} ({1}) + {0} ({1}) + {0} is a location, and {1} lists the agents that use it, separated by commas. + Configure Aspire MCP for {0} at {1} Configurar o MCP do Aspire para {0} em {1} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ru.xlf index 035cc7d8f5b..f6fb70263d0 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ru.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ru.xlf @@ -107,12 +107,17 @@ Настроить сервер Aspire MCP для выбранных сред агентов? + + Sources: {0} + Sources: {0} + {0} lists the configured repository and catalog URLs separated by commas. + - Configured Aspire plugin: + Configured sources: {0} - Configured Aspire plugin: + Configured sources: {0} - {0} lists one agent per line, with that agent's configuration paths comma-separated. + {0} contains the source URLs and configuration locations, each on its own labeled line. Initialize configuration and skills for selected agent clients @@ -162,8 +167,8 @@ - Installed Aspire agent skills: - Установленные навыки агента Aspire: + Installed skills: + Установленные навыки агента Aspire: @@ -198,6 +203,11 @@ {0} {0} lists the local skill entry-point paths, one per line. + + {0} ({1}) + {0} ({1}) + {0} is a location, and {1} lists the agents that use it, separated by commas. + Configure Aspire MCP for {0} at {1} Настройте MCP Aspire для {0} в {1} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.tr.xlf index 8998561a7e0..f30c8e697fc 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.tr.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.tr.xlf @@ -107,12 +107,17 @@ Seçili aracı ortamları için Aspire MCP sunucusunu yapılandırılsın mı? + + Sources: {0} + Sources: {0} + {0} lists the configured repository and catalog URLs separated by commas. + - Configured Aspire plugin: + Configured sources: {0} - Configured Aspire plugin: + Configured sources: {0} - {0} lists one agent per line, with that agent's configuration paths comma-separated. + {0} contains the source URLs and configuration locations, each on its own labeled line. Initialize configuration and skills for selected agent clients @@ -162,8 +167,8 @@ - Installed Aspire agent skills: - Yüklenen Aspire aracı becerileri: + Installed skills: + Yüklenen Aspire aracı becerileri: @@ -198,6 +203,11 @@ {0} {0} lists the local skill entry-point paths, one per line. + + {0} ({1}) + {0} ({1}) + {0} is a location, and {1} lists the agents that use it, separated by commas. + Configure Aspire MCP for {0} at {1} {0} için Aspire MCP'yi yapılandırın ({1}) diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hans.xlf index e5122d522c7..6a7614a62a4 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hans.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hans.xlf @@ -107,12 +107,17 @@ 为所选智能体环境配置 Aspire MCP 服务器? + + Sources: {0} + Sources: {0} + {0} lists the configured repository and catalog URLs separated by commas. + - Configured Aspire plugin: + Configured sources: {0} - Configured Aspire plugin: + Configured sources: {0} - {0} lists one agent per line, with that agent's configuration paths comma-separated. + {0} contains the source URLs and configuration locations, each on its own labeled line. Initialize configuration and skills for selected agent clients @@ -162,8 +167,8 @@ - Installed Aspire agent skills: - 已安装 Aspire 智能体技能: + Installed skills: + 已安装 Aspire 智能体技能: @@ -198,6 +203,11 @@ {0} {0} lists the local skill entry-point paths, one per line. + + {0} ({1}) + {0} ({1}) + {0} is a location, and {1} lists the agents that use it, separated by commas. + Configure Aspire MCP for {0} at {1} 为 {0} 在 {1} 配置 Aspire MCP diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hant.xlf index 90a7499fe52..345e92f80ad 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hant.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hant.xlf @@ -107,12 +107,17 @@ 是否針對選取的代理程式環境設定 Aspire MCP 伺服器? + + Sources: {0} + Sources: {0} + {0} lists the configured repository and catalog URLs separated by commas. + - Configured Aspire plugin: + Configured sources: {0} - Configured Aspire plugin: + Configured sources: {0} - {0} lists one agent per line, with that agent's configuration paths comma-separated. + {0} contains the source URLs and configuration locations, each on its own labeled line. Initialize configuration and skills for selected agent clients @@ -162,8 +167,8 @@ - Installed Aspire agent skills: - 已安裝的 Aspire 代理程式技能: + Installed skills: + 已安裝的 Aspire 代理程式技能: @@ -198,6 +203,11 @@ {0} {0} lists the local skill entry-point paths, one per line. + + {0} ({1}) + {0} ({1}) + {0} is a location, and {1} lists the agents that use it, separated by commas. + Configure Aspire MCP for {0} at {1} 為 {0} 在 {1} 設定 Aspire MCP diff --git a/tests/Aspire.Cli.Tests/Agents/PlaywrightCliInstallerTests.cs b/tests/Aspire.Cli.Tests/Agents/PlaywrightCliInstallerTests.cs index 66972f4dbce..9ded7208957 100644 --- a/tests/Aspire.Cli.Tests/Agents/PlaywrightCliInstallerTests.cs +++ b/tests/Aspire.Cli.Tests/Agents/PlaywrightCliInstallerTests.cs @@ -72,10 +72,11 @@ public async Task InstallSelectedAsync_GeneratesOnceAndPreservesUnselectedScope( Path.Combine(root.FullName, scope, ".claude", "skills") }; - var (status, message) = await installer.InstallSelectedAsync(destinations, CancellationToken.None); + var (status, message, installedSkillDirectories) = await installer.InstallSelectedAsync(destinations, CancellationToken.None); Assert.Equal(PlaywrightInstallStatus.Installed, status); Assert.Null(message); + Assert.Equal(destinations.Order(StringComparer.Ordinal), installedSkillDirectories.Order(StringComparer.Ordinal)); Assert.Equal(1, npmRunner.PackCallCount); Assert.Equal(1, playwrightRunner.InstallSkillsCallCount); Assert.True(npmRunner.InstallGlobalCalled); @@ -106,11 +107,12 @@ public async Task InstallSelectedAsync_GenerationFailure_RemovesStagingWithoutWr var installer = new PlaywrightCliInstaller(_context.Clients, npmRunner, new TestNpmProvenanceChecker(), playwrightRunner, new TestInteractionService(), new ConfigurationBuilder().Build(), NullLogger.Instance); - var (status, message) = await installer.InstallSelectedAsync( + var (status, message, installedSkillDirectories) = await installer.InstallSelectedAsync( new HashSet { Path.Combine(root.FullName, ".agents", "skills") }, CancellationToken.None); Assert.Equal(PlaywrightInstallStatus.Failed, status); Assert.NotNull(message); + Assert.Empty(installedSkillDirectories); Assert.NotNull(playwrightRunner.InstallSkillsWorkingDirectory); Assert.False(Directory.Exists(playwrightRunner.InstallSkillsWorkingDirectory)); Assert.Empty(root.GetFileSystemInfos()); @@ -143,7 +145,7 @@ public async Task InstallSelectedAsync_MirrorFailure_ReportsFailureOrPartialSucc var installer = new PlaywrightCliInstaller(_context.Clients, npmRunner, new TestNpmProvenanceChecker(), playwrightRunner, new TestInteractionService(), new ConfigurationBuilder().Build(), NullLogger.Instance); - var (status, message) = await installer.InstallSelectedAsync(selectedDirectories, CancellationToken.None); + var (status, message, installedSkillDirectories) = await installer.InstallSelectedAsync(selectedDirectories, CancellationToken.None); Assert.Equal(successfulDestination ? PlaywrightInstallStatus.InstalledWithWarnings : PlaywrightInstallStatus.Failed, status); Assert.Equal(successfulDestination @@ -152,8 +154,13 @@ public async Task InstallSelectedAsync_MirrorFailure_ReportsFailureOrPartialSucc Assert.Equal("existing file", await File.ReadAllTextAsync(blockedPath)); if (successfulDestination) { + Assert.Equal(destination, Assert.Single(installedSkillDirectories)); Assert.Equal("# Playwright CLI Skill", await File.ReadAllTextAsync(Path.Combine(destination, PlaywrightCliInstaller.PlaywrightCliSkillName, "SKILL.md"))); } + else + { + Assert.Empty(installedSkillDirectories); + } Assert.NotNull(playwrightRunner.InstallSkillsWorkingDirectory); Assert.False(Directory.Exists(playwrightRunner.InstallSkillsWorkingDirectory)); } @@ -180,11 +187,12 @@ public async Task InstallSelectedAsync_MissingGeneratedSkills_ReturnsFailure() var installer = new PlaywrightCliInstaller(_context.Clients, npmRunner, new TestNpmProvenanceChecker(), playwrightRunner, new TestInteractionService(), new ConfigurationBuilder().Build(), NullLogger.Instance); - var (status, message) = await installer.InstallSelectedAsync( + var (status, message, installedSkillDirectories) = await installer.InstallSelectedAsync( new HashSet { Path.Combine(root.FullName, "skills") }, CancellationToken.None); Assert.Equal(PlaywrightInstallStatus.Failed, status); Assert.Equal(AgentCommandStrings.PlaywrightCliInstaller_FailedToGenerateSkillFiles, message); + Assert.Empty(installedSkillDirectories); Assert.Empty(root.GetFileSystemInfos()); Assert.NotNull(playwrightRunner.InstallSkillsWorkingDirectory); Assert.False(Directory.Exists(playwrightRunner.InstallSkillsWorkingDirectory)); @@ -226,7 +234,7 @@ public async Task InstallSelectedAsync_ReadOnlyTemporaryFile_DoesNotOverrideInst new ConfigurationBuilder().Build(), NullLogger.Instance); var destination = Path.Combine(root.FullName, "skills"); - var (status, message) = await installer.InstallSelectedAsync(new HashSet { destination }, CancellationToken.None); + var (status, message, _) = await installer.InstallSelectedAsync(new HashSet { destination }, CancellationToken.None); Assert.Equal(PlaywrightInstallStatus.Installed, status); Assert.Null(message); @@ -263,7 +271,7 @@ public async Task InstallAsync_WhenNpmResolveReturnsNull_ReturnsErrorMessage() var playwrightRunner = new TestPlaywrightCliRunner(); var installer = new PlaywrightCliInstaller(_context.Clients, npmRunner, new TestNpmProvenanceChecker(), playwrightRunner, new TestInteractionService(), new ConfigurationBuilder().Build(), NullLogger.Instance); - var (status, message) = await installer.InstallAsync(tempDir, s_emptySkillDirs, CancellationToken.None); + var (status, message, _) = await installer.InstallAsync(tempDir, s_emptySkillDirs, CancellationToken.None); Assert.Equal(PlaywrightInstallStatus.Failed, status); Assert.NotNull(message); @@ -296,9 +304,10 @@ public async Task InstallAsync_WhenAlreadyInstalledAtSameVersion_SkipsInstallAnd }; var installer = new PlaywrightCliInstaller(_context.Clients, npmRunner, new TestNpmProvenanceChecker(), playwrightRunner, new TestInteractionService(), new ConfigurationBuilder().Build(), NullLogger.Instance); - var (status, _) = await installer.InstallAsync(tempDir, s_emptySkillDirs, CancellationToken.None); + var (status, _, installedSkillDirectories) = await installer.InstallAsync(tempDir, s_emptySkillDirs, CancellationToken.None); Assert.Equal(PlaywrightInstallStatus.Installed, status); + Assert.Empty(installedSkillDirectories); Assert.True(playwrightRunner.InstallSkillsCalled); Assert.False(npmRunner.PackCalled); Assert.False(npmRunner.InstallGlobalCalled); @@ -366,7 +375,7 @@ public async Task InstallAsync_WhenNewerVersionInstalled_SkipsInstallAndInstalls }; var installer = new PlaywrightCliInstaller(_context.Clients, npmRunner, new TestNpmProvenanceChecker(), playwrightRunner, new TestInteractionService(), new ConfigurationBuilder().Build(), NullLogger.Instance); - var (status, _) = await installer.InstallAsync(tempDir, s_emptySkillDirs, CancellationToken.None); + var (status, _, _) = await installer.InstallAsync(tempDir, s_emptySkillDirs, CancellationToken.None); Assert.Equal(PlaywrightInstallStatus.Installed, status); Assert.True(playwrightRunner.InstallSkillsCalled); @@ -397,7 +406,7 @@ public async Task InstallAsync_WhenPackFails_ReturnsErrorMessage() var playwrightRunner = new TestPlaywrightCliRunner(); var installer = new PlaywrightCliInstaller(_context.Clients, npmRunner, new TestNpmProvenanceChecker(), playwrightRunner, new TestInteractionService(), new ConfigurationBuilder().Build(), NullLogger.Instance); - var (status, message) = await installer.InstallAsync(tempDir, s_emptySkillDirs, CancellationToken.None); + var (status, message, _) = await installer.InstallAsync(tempDir, s_emptySkillDirs, CancellationToken.None); Assert.Equal(PlaywrightInstallStatus.Failed, status); Assert.NotNull(message); @@ -439,7 +448,7 @@ public async Task InstallAsync_WhenIntegrityCheckPasses_InstallsGlobally() var repoRoot = CreateTestRepoRoot(); try { - var (status, _) = await installer.InstallAsync(repoRoot, s_emptySkillDirs, CancellationToken.None); + var (status, _, _) = await installer.InstallAsync(repoRoot, s_emptySkillDirs, CancellationToken.None); Assert.Equal(PlaywrightInstallStatus.Installed, status); Assert.True(npmRunner.InstallGlobalCalled); @@ -484,7 +493,7 @@ public async Task InstallAsync_UsesDownloadedTarballDigestForProvenance() try { - var (status, _) = await installer.InstallAsync(repoRoot, s_emptySkillDirs, CancellationToken.None); + var (status, _, _) = await installer.InstallAsync(repoRoot, s_emptySkillDirs, CancellationToken.None); Assert.Equal(PlaywrightInstallStatus.Installed, status); Assert.Equal(expectedIntegrity, provenanceChecker.CapturedSriIntegrity); @@ -527,7 +536,7 @@ public async Task InstallAsync_WhenGlobalInstallFails_ReturnsErrorMessage() var repoRoot = CreateTestRepoRoot(); try { - var (status, message) = await installer.InstallAsync(repoRoot, s_emptySkillDirs, CancellationToken.None); + var (status, message, _) = await installer.InstallAsync(repoRoot, s_emptySkillDirs, CancellationToken.None); Assert.Equal(PlaywrightInstallStatus.Failed, status); Assert.NotNull(message); @@ -575,7 +584,7 @@ public async Task InstallAsync_WhenOlderVersionInstalled_PerformsUpgrade() var repoRoot = CreateTestRepoRoot(); try { - var (status, _) = await installer.InstallAsync(repoRoot, s_emptySkillDirs, CancellationToken.None); + var (status, _, _) = await installer.InstallAsync(repoRoot, s_emptySkillDirs, CancellationToken.None); Assert.Equal(PlaywrightInstallStatus.Installed, status); Assert.True(npmRunner.PackCalled); @@ -671,7 +680,7 @@ public async Task InstallAsync_WhenProvenanceCheckFails_ReturnsErrorMessage() var playwrightRunner = new TestPlaywrightCliRunner(); var installer = new PlaywrightCliInstaller(_context.Clients, npmRunner, provenanceChecker, playwrightRunner, new TestInteractionService(), new ConfigurationBuilder().Build(), NullLogger.Instance); - var (status, message) = await installer.InstallAsync(tempDir, s_emptySkillDirs, CancellationToken.None); + var (status, message, _) = await installer.InstallAsync(tempDir, s_emptySkillDirs, CancellationToken.None); Assert.Equal(PlaywrightInstallStatus.Failed, status); Assert.NotNull(message); @@ -717,7 +726,7 @@ public async Task InstallAsync_WhenValidationDisabled_SkipsAllValidationChecks() var repoRoot = CreateTestRepoRoot(); try { - var (status, _) = await installer.InstallAsync(repoRoot, s_emptySkillDirs, CancellationToken.None); + var (status, _, _) = await installer.InstallAsync(repoRoot, s_emptySkillDirs, CancellationToken.None); Assert.Equal(PlaywrightInstallStatus.Installed, status); Assert.False(provenanceChecker.ProvenanceCalled); @@ -798,7 +807,7 @@ public async Task InstallAsync_WhenVersionOverrideIsNotStrictSemVer_ReturnsFaile .Build(); var installer = new PlaywrightCliInstaller(_context.Clients, npmRunner, new TestNpmProvenanceChecker(), playwrightRunner, new TestInteractionService(), configuration, NullLogger.Instance); - var (status, message) = await installer.InstallAsync(tempDir, s_emptySkillDirs, CancellationToken.None); + var (status, message, _) = await installer.InstallAsync(tempDir, s_emptySkillDirs, CancellationToken.None); Assert.Equal(PlaywrightInstallStatus.Failed, status); Assert.NotNull(message); @@ -882,8 +891,11 @@ public async Task InstallAsync_MirrorsSkillFilesToOtherAgentEnvironments() Path.Combine(".claude", "skills"), Path.Combine(".agents", "skills") }; - await installer.InstallAsync(tempDir, selectedDirs, CancellationToken.None); + var (status, message, installedSkillDirectories) = await installer.InstallAsync(tempDir, selectedDirs, CancellationToken.None); + Assert.Equal(PlaywrightInstallStatus.Installed, status); + Assert.Null(message); + Assert.Equal(selectedDirs.Order(StringComparer.Ordinal), installedSkillDirectories.Order(StringComparer.Ordinal)); Assert.Equal( [ Path.Combine(".agents", "skills", "playwright-cli", "SKILL.md"), diff --git a/tests/Aspire.Cli.Tests/Commands/AgentInitClientSelectionTests.cs b/tests/Aspire.Cli.Tests/Commands/AgentInitClientSelectionTests.cs index 45016cb8de0..afdba878013 100644 --- a/tests/Aspire.Cli.Tests/Commands/AgentInitClientSelectionTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/AgentInitClientSelectionTests.cs @@ -8,6 +8,7 @@ using Aspire.Cli.Agents.OpenCode; using Aspire.Cli.Commands; using Aspire.Cli.Interaction; +using Aspire.Cli.Npm; using Aspire.Cli.Resources; using Aspire.Cli.Tests.Agents; using Aspire.Cli.Tests.TestServices; @@ -15,6 +16,7 @@ using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Semver; namespace Aspire.Cli.Tests.Commands; @@ -324,7 +326,7 @@ public async Task AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedCon await File.WriteAllTextAsync(Path.Combine(workspace.Path, "apphost.cs"), string.Empty); var homeDirectory = workspace.CreateDirectory("fake-home"); var labels = new List(); - var locations = new List(); + var subtleMessages = new List(); var interaction = new TestInteractionService { PromptForSelectionsCallback = (prompt, choices, formatter, _) => @@ -333,13 +335,7 @@ public async Task AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedCon labels.AddRange(choices.Cast().Select(formatter)); return choices.Cast().Where(client => string.Equals(client.Id, "copilot", StringComparisons.AgentClientId)).Cast().ToArray(); }, - DisplaySubtleMessageCallback = message => - { - if (message.StartsWith(AgentCommandStrings.InitCommand_InstallationLocations, StringComparison.Ordinal)) - { - locations.Add(message); - } - } + DisplaySubtleMessageCallback = subtleMessages.Add }; var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => { @@ -350,7 +346,103 @@ public async Task AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedCon var result = provider.GetRequiredService().Parse( $"agent init --scope {scope} --skills {(installSkills ? "dotnet-inspect" : "none")} --mcp={configureMcp}"); Assert.Equal(CliExitCodes.Success, await result.InvokeAsync().DefaultTimeout()); - await Verify(new { Agents = labels, Locations = Assert.Single(locations) }).UseParameters(scope, installSkills, configureMcp); + Assert.Empty(subtleMessages); + await Verify(new + { + Agents = labels, + Results = interaction.DisplayedMessages.Select(message => message.Message + .Replace(homeDirectory.FullName, "~", StringComparison.Ordinal) + .Replace(workspace.WorkspaceRoot.FullName, "", StringComparison.Ordinal) + .Replace('\\', '/')) + }).UseParameters(scope, installSkills, configureMcp); + } + + [Theory] + [InlineData("project", "installed")] + [InlineData("global", "installed")] + [InlineData("project", "skipped")] + [InlineData("project", "failed")] + public async Task AgentInit_SkillSummary_GroupsInstalledSkillsByLocationAndAgent(string scope, string playwrightOutcome) + { + using var workspace = TemporaryWorkspace.CreateForCli(outputHelper); + await File.WriteAllTextAsync(Path.Combine(workspace.Path, "apphost.cs"), string.Empty); + var homeDirectory = workspace.CreateDirectory("fake-home"); + var interaction = new TestInteractionService(); + var version = SemVersion.Parse("0.1.3", SemVersionStyles.Strict); + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.CliExecutionContextFactory = _ => TestExecutionContextHelper.CreateExecutionContext(workspace.WorkspaceRoot, homeDirectory: homeDirectory); + options.InteractionServiceFactory = _ => interaction; + options.NpmRunnerFactory = _ => new FakeNpmRunner + { + IsAvailable = playwrightOutcome != "skipped", + ResolvePackageAsyncCallback = (_, _, _) => Task.FromResult( + playwrightOutcome == "installed" ? new NpmPackageInfo { Version = version } : null) + }; + options.PlaywrightCliRunnerFactory = _ => new FakePlaywrightCliRunner { InstalledVersion = version }; + }); + using var provider = services.BuildServiceProvider(); + var result = provider.GetRequiredService().Parse( + $"agent init --agents copilot,claude,opencode --scope {scope} --skills dotnet-inspect,playwright-cli --mcp=false"); + + Assert.Equal(playwrightOutcome == "failed" ? CliExitCodes.InvalidCommand : CliExitCodes.Success, + await result.InvokeAsync().DefaultTimeout()); + var summary = Assert.Single(interaction.DisplayedMessages, + message => message.Message.StartsWith(AgentCommandStrings.InitCommand_InstalledSkillsSummary, StringComparison.Ordinal)); + foreach (var client in provider.GetServices()) + { + var directory = client.GetSkillBaseDirectory(Enum.Parse(scope, ignoreCase: true), workspace.WorkspaceRoot); + Assert.True(File.Exists(Path.Combine(directory, "dotnet-inspect", "SKILL.md"))); + Assert.Equal(playwrightOutcome == "installed", File.Exists(Path.Combine(directory, "playwright-cli", "SKILL.md"))); + } + + if (playwrightOutcome == "installed") + { + var previousCount = interaction.DisplayedMessages.Count; + Assert.Equal(CliExitCodes.Success, await result.InvokeAsync().DefaultTimeout()); + Assert.Equal(summary, Assert.Single(interaction.DisplayedMessages.Skip(previousCount), + message => message.Message.StartsWith(AgentCommandStrings.InitCommand_InstalledSkillsSummary, StringComparison.Ordinal))); + } + + await Verify(summary.Message) + .UseParameters(scope, playwrightOutcome == "installed" ? "installed" : "not-installed") + .DisableRequireUniquePrefix(); + } + + [Fact] + public async Task AgentInit_SkillSummary_IncludesOnlySuccessfulPlaywrightDestinations() + { + using var workspace = TemporaryWorkspace.CreateForCli(outputHelper); + await File.WriteAllTextAsync(Path.Combine(workspace.Path, "apphost.cs"), string.Empty); + var homeDirectory = workspace.CreateDirectory("fake-home"); + var blockedSkillPath = Path.Combine(workspace.WorkspaceRoot.FullName, ".claude", "skills", "playwright-cli"); + await AgentConfigurationTestContext.WriteAsync(blockedSkillPath, "blocked destination"); + var interaction = new TestInteractionService(); + var version = SemVersion.Parse("0.1.3", SemVersionStyles.Strict); + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.CliExecutionContextFactory = _ => TestExecutionContextHelper.CreateExecutionContext(workspace.WorkspaceRoot, homeDirectory: homeDirectory); + options.InteractionServiceFactory = _ => interaction; + options.NpmRunnerFactory = _ => new FakeNpmRunner + { + ResolvePackageAsyncCallback = (_, _, _) => Task.FromResult(new NpmPackageInfo { Version = version }) + }; + options.PlaywrightCliRunnerFactory = _ => new FakePlaywrightCliRunner { InstalledVersion = version }; + }); + using var provider = services.BuildServiceProvider(); + var result = provider.GetRequiredService().Parse( + "agent init --agents copilot,claude,opencode --scope project --skills playwright-cli --mcp=false"); + + Assert.Equal(CliExitCodes.Success, await result.InvokeAsync().DefaultTimeout()); + Assert.Empty(interaction.DisplayedErrors); + var warning = Assert.Single(interaction.DisplayedMessages, message => message.Emoji.Equals(KnownEmojis.PoliceCarLight)); + Assert.Equal(AgentCommandStrings.PlaywrightCliInstaller_InstalledWithMirrorWarnings, warning.Message); + Assert.Equal("# Playwright CLI Skill", await File.ReadAllTextAsync( + Path.Combine(workspace.WorkspaceRoot.FullName, ".agents", "skills", "playwright-cli", "SKILL.md"))); + Assert.Equal("blocked destination", await File.ReadAllTextAsync(blockedSkillPath)); + var summary = Assert.Single(interaction.DisplayedMessages, + message => message.Message.StartsWith(AgentCommandStrings.InitCommand_InstalledSkillsSummary, StringComparison.Ordinal)); + await Verify(summary.Message); } [Fact] diff --git a/tests/Aspire.Cli.Tests/Commands/AgentInitCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/AgentInitCommandTests.cs index ec71b49fb66..06e75ec8cb1 100644 --- a/tests/Aspire.Cli.Tests/Commands/AgentInitCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/AgentInitCommandTests.cs @@ -769,9 +769,12 @@ public async Task Remote_RegistersSelectedUndetectedClientsWithoutMcp(string cli { Assert.Equal(KnownEmojis.Robot, message.Emoji); Assert.Equal(string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_ConfiguredTargets, - string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_ConfigurationTarget, - context.Clients.Single(value => value.Id == client).DisplayName, - scope == "project" ? relativePath : $"~/{relativePath}")), message.Message); + string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_ConfiguredSourceList, + client == "opencode" ? SkillSource.DefaultV1Catalog : AspireSkillsRemoteConfiguration.RepositoryUrl) + System.Environment.NewLine + " " + + string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkillsSummaryLocations, + string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_LocationWithAgents, + scope == "project" ? relativePath : $"~/{relativePath}", + context.Clients.Single(value => value.Id == client).DisplayName))), message.Message); }); Assert.Empty(interaction.DisplayedErrors); } @@ -796,7 +799,7 @@ public async Task Remote_CombinesNewAndExistingRegistrationsWithOneRowPerAgent(s var scopeRoot = scope is AgentConfigurationScope.Project ? context.Project : context.Home; var prefix = scope is AgentConfigurationScope.Global ? "~/" : string.Empty; var targetEntries = context.Clients.Select(client => - $"{client.DisplayName}: {prefix}{Path.GetRelativePath(scopeRoot.FullName, client.GetIntegrationConfigPath(scope, context.Project)).Replace(Path.DirectorySeparatorChar, '/')}").ToArray(); + $"{prefix}{Path.GetRelativePath(scopeRoot.FullName, client.GetIntegrationConfigPath(scope, context.Project)).Replace(Path.DirectorySeparatorChar, '/')} ({client.DisplayName})").ToArray(); for (var invocation = 0; invocation < 2; invocation++) { @@ -824,12 +827,69 @@ public async Task Remote_CombinesNewAndExistingRegistrationsWithOneRowPerAgent(s { Assert.Equal(KnownEmojis.Robot, message.Emoji); Assert.Equal(string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_ConfiguredTargets, - string.Join($"{System.Environment.NewLine} ", targetEntries)), message.Message); + string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_ConfiguredSourceList, + string.Join(", ", AspireSkillsRemoteConfiguration.RepositoryUrl, SkillSource.DefaultV1Catalog)) + System.Environment.NewLine + " " + + string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkillsSummaryLocations, + string.Join(", ", targetEntries))), message.Message); }); Assert.Empty(interaction.DisplayedErrors); } } + [Fact] + public async Task Remote_SummaryListsConfiguredSourcesAndAgentLocations() + { + using var context = new AgentConfigurationTestContext(outputHelper); + await AgentConfigurationTestContext.WriteAsync(Path.Combine(context.Project.FullName, "aspire.config.json"), """ + {"skillSources":{"team":{"repository":"example/team","plugins":["deployment"]}}} + """); + var interaction = new TestInteractionService(); + using var provider = context.CreateServiceCollection(outputHelper, options => + { + options.InteractionServiceFactory = _ => interaction; + }).BuildServiceProvider(); + var result = provider.GetRequiredService().Parse( + "agent init --agents copilot,claude --scope project --skills aspire --mcp=false"); + + Assert.Equal(CliExitCodes.Success, await result.InvokeAsync().DefaultTimeout()); + var summary = Assert.Single(interaction.DisplayedMessages, message => message.Emoji.Equals(KnownEmojis.Robot)); + await Verify(summary.Message); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public async Task Remote_SummaryUsesConfiguredOpenCodeCatalog(bool array, bool pinned) + { + using var context = new AgentConfigurationTestContext(outputHelper); + var catalogs = new JsonArray("https://example.test/unrelated-catalog/"); + if (pinned) + { + catalogs.Add("https://raw.githubusercontent.com/microsoft/aspire-skills/release/opencode/v2/"); + } + var config = new JsonObject + { + ["skills"] = array ? catalogs : new JsonObject { ["urls"] = catalogs } + }; + await AgentConfigurationTestContext.WriteAsync( + context.OpenCode.GetIntegrationConfigPath(AgentConfigurationScope.Project, context.Project), config.ToJsonString()); + var interaction = new TestInteractionService(); + using var provider = context.CreateServiceCollection(outputHelper, options => + { + options.InteractionServiceFactory = _ => interaction; + }).BuildServiceProvider(); + var result = provider.GetRequiredService().Parse( + "agent init --agents copilot,claude,opencode --scope project --skills aspire --mcp=false"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + outputHelper.WriteLine(string.Join(System.Environment.NewLine, interaction.DisplayedErrors)); + Assert.Empty(interaction.DisplayedErrors); + Assert.Equal(CliExitCodes.Success, exitCode); + var summary = Assert.Single(interaction.DisplayedMessages, message => message.Emoji.Equals(KnownEmojis.Robot)); + await Verify(summary.Message).UseParameters(array, pinned); + } + [Fact] public async Task Remote_FailedRegistrationsKeepEachTargetWithItsReason() { diff --git a/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=global_installSkills=False_configureMcp=True.verified.txt b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=global_installSkills=False_configureMcp=True.verified.txt index 10f1794c123..c582c405df7 100644 --- a/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=global_installSkills=False_configureMcp=True.verified.txt +++ b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=global_installSkills=False_configureMcp=True.verified.txt @@ -4,7 +4,7 @@ Claude Code, OpenCode ], - Locations: -Installation locations: - MCP: ~/.copilot/mcp-config.json + Results: [ + Configure Aspire MCP for GitHub Copilot at ~/.copilot/mcp-config.json + ] } \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=global_installSkills=True_configureMcp=False.verified.txt b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=global_installSkills=True_configureMcp=False.verified.txt index c52226fe138..1b3be0eabbc 100644 --- a/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=global_installSkills=True_configureMcp=False.verified.txt +++ b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=global_installSkills=True_configureMcp=False.verified.txt @@ -4,8 +4,10 @@ Claude Code, OpenCode ], - Locations: -Installation locations: - Skills: ~/.agents/skills (shared: GitHub Copilot, OpenCode) -Shared files may be discovered by agents that aren't selected. + Results: [ + Installed skills: + Skills: dotnet-inspect + Locations: ~/.agents/skills (GitHub Copilot, OpenCode) + [dim]Shared files may be discovered by agents that aren't selected.[/] + ] } \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=global_installSkills=True_configureMcp=True.verified.txt b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=global_installSkills=True_configureMcp=True.verified.txt index 1ba231e8a1e..ff7d909fd8d 100644 --- a/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=global_installSkills=True_configureMcp=True.verified.txt +++ b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=global_installSkills=True_configureMcp=True.verified.txt @@ -4,9 +4,11 @@ Claude Code, OpenCode ], - Locations: -Installation locations: - Skills: ~/.agents/skills (shared: GitHub Copilot, OpenCode) - MCP: ~/.copilot/mcp-config.json -Shared files may be discovered by agents that aren't selected. + Results: [ + Installed skills: + Skills: dotnet-inspect + Locations: ~/.agents/skills (GitHub Copilot, OpenCode) + [dim]Shared files may be discovered by agents that aren't selected.[/], + Configure Aspire MCP for GitHub Copilot at ~/.copilot/mcp-config.json + ] } \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=project_installSkills=False_configureMcp=True.verified.txt b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=project_installSkills=False_configureMcp=True.verified.txt index 4bfa265f1c1..c0dea5866a6 100644 --- a/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=project_installSkills=False_configureMcp=True.verified.txt +++ b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=project_installSkills=False_configureMcp=True.verified.txt @@ -4,8 +4,7 @@ Claude Code, OpenCode ], - Locations: -Installation locations: - MCP: .mcp.json (shared: GitHub Copilot, Claude Code) -Shared files may be discovered by agents that aren't selected. + Results: [ + Configure Aspire MCP for GitHub Copilot at /.mcp.json + ] } \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=project_installSkills=True_configureMcp=False.verified.txt b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=project_installSkills=True_configureMcp=False.verified.txt index 0020099dfe1..cb9df6abe9d 100644 --- a/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=project_installSkills=True_configureMcp=False.verified.txt +++ b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=project_installSkills=True_configureMcp=False.verified.txt @@ -4,8 +4,10 @@ Claude Code, OpenCode ], - Locations: -Installation locations: - Skills: .agents/skills (shared: GitHub Copilot, OpenCode) -Shared files may be discovered by agents that aren't selected. + Results: [ + Installed skills: + Skills: dotnet-inspect + Locations: .agents/skills (GitHub Copilot, OpenCode) + [dim]Shared files may be discovered by agents that aren't selected.[/] + ] } \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=project_installSkills=True_configureMcp=True.verified.txt b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=project_installSkills=True_configureMcp=True.verified.txt index 175222d82a7..a50b5036383 100644 --- a/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=project_installSkills=True_configureMcp=True.verified.txt +++ b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_InstallationLocations_ShowSelectedAssetsAndSharedConsumers_scope=project_installSkills=True_configureMcp=True.verified.txt @@ -4,9 +4,11 @@ Claude Code, OpenCode ], - Locations: -Installation locations: - Skills: .agents/skills (shared: GitHub Copilot, OpenCode) - MCP: .mcp.json (shared: GitHub Copilot, Claude Code) -Shared files may be discovered by agents that aren't selected. + Results: [ + Installed skills: + Skills: dotnet-inspect + Locations: .agents/skills (GitHub Copilot, OpenCode) + [dim]Shared files may be discovered by agents that aren't selected.[/], + Configure Aspire MCP for GitHub Copilot at /.mcp.json + ] } \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_SkillSummary_GroupsInstalledSkillsByLocationAndAgent_scope=global_playwrightOutcome=installed.verified.txt b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_SkillSummary_GroupsInstalledSkillsByLocationAndAgent_scope=global_playwrightOutcome=installed.verified.txt new file mode 100644 index 00000000000..5ab2337be6f --- /dev/null +++ b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_SkillSummary_GroupsInstalledSkillsByLocationAndAgent_scope=global_playwrightOutcome=installed.verified.txt @@ -0,0 +1,4 @@ +Installed skills: + Skills: dotnet-inspect, playwright-cli + Locations: ~/.agents/skills (GitHub Copilot, OpenCode), ~/.claude/skills (Claude Code) + [dim]Shared files may be discovered by agents that aren't selected.[/] \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_SkillSummary_GroupsInstalledSkillsByLocationAndAgent_scope=project_playwrightOutcome=installed.verified.txt b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_SkillSummary_GroupsInstalledSkillsByLocationAndAgent_scope=project_playwrightOutcome=installed.verified.txt new file mode 100644 index 00000000000..0d5e66dd168 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_SkillSummary_GroupsInstalledSkillsByLocationAndAgent_scope=project_playwrightOutcome=installed.verified.txt @@ -0,0 +1,4 @@ +Installed skills: + Skills: dotnet-inspect, playwright-cli + Locations: .agents/skills (GitHub Copilot, OpenCode), .claude/skills (Claude Code) + [dim]Shared files may be discovered by agents that aren't selected.[/] \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_SkillSummary_GroupsInstalledSkillsByLocationAndAgent_scope=project_playwrightOutcome=not-installed.verified.txt b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_SkillSummary_GroupsInstalledSkillsByLocationAndAgent_scope=project_playwrightOutcome=not-installed.verified.txt new file mode 100644 index 00000000000..4642dd3f738 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_SkillSummary_GroupsInstalledSkillsByLocationAndAgent_scope=project_playwrightOutcome=not-installed.verified.txt @@ -0,0 +1,4 @@ +Installed skills: + Skills: dotnet-inspect + Locations: .agents/skills (GitHub Copilot, OpenCode), .claude/skills (Claude Code) + [dim]Shared files may be discovered by agents that aren't selected.[/] \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_SkillSummary_IncludesOnlySuccessfulPlaywrightDestinations.verified.txt b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_SkillSummary_IncludesOnlySuccessfulPlaywrightDestinations.verified.txt new file mode 100644 index 00000000000..9e9075e3c4e --- /dev/null +++ b/tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_SkillSummary_IncludesOnlySuccessfulPlaywrightDestinations.verified.txt @@ -0,0 +1,4 @@ +Installed skills: + Skills: playwright-cli + Locations: .agents/skills (GitHub Copilot, OpenCode) + [dim]Shared files may be discovered by agents that aren't selected.[/] \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Snapshots/AgentInitCommandTests.Remote_SummaryListsConfiguredSourcesAndAgentLocations.verified.txt b/tests/Aspire.Cli.Tests/Snapshots/AgentInitCommandTests.Remote_SummaryListsConfiguredSourcesAndAgentLocations.verified.txt new file mode 100644 index 00000000000..65d39eb6dcc --- /dev/null +++ b/tests/Aspire.Cli.Tests/Snapshots/AgentInitCommandTests.Remote_SummaryListsConfiguredSourcesAndAgentLocations.verified.txt @@ -0,0 +1,3 @@ +Configured sources: + Sources: https://github.com/microsoft/aspire-skills, https://github.com/example/team + Locations: .github/copilot/settings.json (GitHub Copilot), .claude/settings.json (Claude Code) \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Snapshots/AgentInitCommandTests.Remote_SummaryUsesConfiguredOpenCodeCatalog_array=False_pinned=False.verified.txt b/tests/Aspire.Cli.Tests/Snapshots/AgentInitCommandTests.Remote_SummaryUsesConfiguredOpenCodeCatalog_array=False_pinned=False.verified.txt new file mode 100644 index 00000000000..05186debe32 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Snapshots/AgentInitCommandTests.Remote_SummaryUsesConfiguredOpenCodeCatalog_array=False_pinned=False.verified.txt @@ -0,0 +1,3 @@ +Configured sources: + Sources: https://github.com/microsoft/aspire-skills, https://raw.githubusercontent.com/microsoft/aspire-skills/main/opencode/v1/ + Locations: .github/copilot/settings.json (GitHub Copilot), .claude/settings.json (Claude Code), opencode.jsonc (OpenCode) \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Snapshots/AgentInitCommandTests.Remote_SummaryUsesConfiguredOpenCodeCatalog_array=True_pinned=False.verified.txt b/tests/Aspire.Cli.Tests/Snapshots/AgentInitCommandTests.Remote_SummaryUsesConfiguredOpenCodeCatalog_array=True_pinned=False.verified.txt new file mode 100644 index 00000000000..064ccbbee48 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Snapshots/AgentInitCommandTests.Remote_SummaryUsesConfiguredOpenCodeCatalog_array=True_pinned=False.verified.txt @@ -0,0 +1,3 @@ +Configured sources: + Sources: https://github.com/microsoft/aspire-skills, https://raw.githubusercontent.com/microsoft/aspire-skills/main/opencode/v2/ + Locations: .github/copilot/settings.json (GitHub Copilot), .claude/settings.json (Claude Code), opencode.jsonc (OpenCode) \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Snapshots/AgentInitCommandTests.Remote_SummaryUsesConfiguredOpenCodeCatalog_array=True_pinned=True.verified.txt b/tests/Aspire.Cli.Tests/Snapshots/AgentInitCommandTests.Remote_SummaryUsesConfiguredOpenCodeCatalog_array=True_pinned=True.verified.txt new file mode 100644 index 00000000000..6612f4634b1 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Snapshots/AgentInitCommandTests.Remote_SummaryUsesConfiguredOpenCodeCatalog_array=True_pinned=True.verified.txt @@ -0,0 +1,3 @@ +Configured sources: + Sources: https://github.com/microsoft/aspire-skills, https://raw.githubusercontent.com/microsoft/aspire-skills/release/opencode/v2/ + Locations: .github/copilot/settings.json (GitHub Copilot), .claude/settings.json (Claude Code), opencode.jsonc (OpenCode) \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/TestServices/FakePlaywrightServices.cs b/tests/Aspire.Cli.Tests/TestServices/FakePlaywrightServices.cs index 1ffb8d791a7..aa3b8e9d794 100644 --- a/tests/Aspire.Cli.Tests/TestServices/FakePlaywrightServices.cs +++ b/tests/Aspire.Cli.Tests/TestServices/FakePlaywrightServices.cs @@ -46,8 +46,10 @@ internal sealed class FakePlaywrightCliRunner : IPlaywrightCliRunner { internal static string SkillBaseDirectory { get; } = Path.Combine(".claude", "skills"); + public SemVersion? InstalledVersion { get; set; } + public Task GetVersionAsync(CancellationToken cancellationToken) - => Task.FromResult(null); + => Task.FromResult(InstalledVersion); public Task InstallSkillsAsync(string workingDirectory, CancellationToken cancellationToken) {