diff --git a/src/Aspire.Cli/Commands/AgentInitCommand.cs b/src/Aspire.Cli/Commands/AgentInitCommand.cs index bfb45024e53..5788f880841 100644 --- a/src/Aspire.Cli/Commands/AgentInitCommand.cs +++ b/src/Aspire.Cli/Commands/AgentInitCommand.cs @@ -311,6 +311,8 @@ private async Task ExecuteAgentInitAsync(DirectoryInfo } } + var installedSkills = new List(); + foreach (var location in selectedLocations) { context.AddSkillBaseDirectory(location.RelativeSkillDirectory); @@ -328,27 +330,39 @@ private async Task ExecuteAgentInitAsync(DirectoryInfo continue; } - hasErrors |= !await InstallSkillAsync( + var installResult = await InstallSkillAsync( workspaceRoot, location.RelativeSkillDirectory, skill, aspireSkillsBundle, isUserLevel: false, cancellationToken); + hasErrors |= !installResult.Succeeded; + if (installResult.UpdatedSkill is not null) + { + installedSkills.Add(installResult.UpdatedSkill); + } if (location.IncludeUserLevel) { - hasErrors |= !await InstallSkillAsync( + installResult = await InstallSkillAsync( ExecutionContext.HomeDirectory, location.RelativeSkillDirectory, skill, aspireSkillsBundle, isUserLevel: true, cancellationToken); + hasErrors |= !installResult.Succeeded; + if (installResult.UpdatedSkill is not null) + { + installedSkills.Add(installResult.UpdatedSkill); + } } } } + DisplayInstalledSkillsSummary(installedSkills); + // --- Phase 4: Handle Playwright CLI (installs binary + mirrors skill files to registered directories) --- var selectedSkillDirs = selectedLocations.Select(l => l.RelativeSkillDirectory).ToHashSet(StringComparer.OrdinalIgnoreCase); if (selectedSkills.Contains(SkillDefinition.PlaywrightCli) && selectedLocations.Count > 0) @@ -424,8 +438,8 @@ private async Task ExecuteAgentInitAsync(DirectoryInfo /// /// Installs the files for a skill at the specified location, creating or updating them as needed. /// - /// true if successful, false if an error occurred. - private async Task InstallSkillAsync( + /// The install result, including the skill/location pair when files were updated. + private async Task InstallSkillAsync( DirectoryInfo rootDirectory, string relativeSkillDirectory, SkillDefinition skill, @@ -465,23 +479,60 @@ private async Task InstallSkillAsync( if (!anyFileUpdated) { - return true; + return new(Succeeded: true, UpdatedSkill: null); } - var displayRelativeSkillPath = relativeSkillPath - .Replace(Path.DirectorySeparatorChar, '/') - .Replace(Path.AltDirectorySeparatorChar, '/'); - var displayPath = isUserLevel ? $"~/{displayRelativeSkillPath}" : displayRelativeSkillPath; - _interactionService.DisplayMessage(KnownEmojis.Robot, - string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkill, skill.Name, displayPath)); - return true; + var displayLocation = GetDisplaySkillDirectory(relativeSkillDirectory, isUserLevel); + return new(Succeeded: true, new InstalledSkillSummaryItem(skill.Name, displayLocation)); } 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 false; + return new(Succeeded: false, UpdatedSkill: null); + } + } + + private void DisplayInstalledSkillsSummary(IReadOnlyList installedSkills) + { + 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); + } + + private static IReadOnlyList GetUniqueValues(IEnumerable values) + { + var uniqueValues = new List(); + var seenValues = new HashSet(StringComparer.Ordinal); + + foreach (var value in values) + { + if (seenValues.Add(value)) + { + uniqueValues.Add(value); + } } + + return uniqueValues; + } + + private static string GetDisplaySkillDirectory(string relativeSkillDirectory, bool isUserLevel) + { + var displayRelativeSkillDirectory = relativeSkillDirectory + .Replace(Path.DirectorySeparatorChar, '/') + .Replace(Path.AltDirectorySeparatorChar, '/'); + + return isUserLevel ? $"~/{displayRelativeSkillDirectory}" : displayRelativeSkillDirectory; } private static async Task> GetSkillFilesAsync(SkillDefinition skill, AspireSkillsBundle? aspireSkillsBundle, CancellationToken cancellationToken) @@ -509,6 +560,10 @@ private enum AgentInitErrorMode Strict, BestEffort } + + private sealed record InstalledSkillSummaryItem(string SkillName, string DisplayLocation); + + private readonly record struct SkillInstallResult(bool Succeeded, InstalledSkillSummaryItem? UpdatedSkill); } internal readonly record struct AgentInitExecutionResult( diff --git a/src/Aspire.Cli/Resources/AgentCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/AgentCommandStrings.Designer.cs index f1e80b58211..21fe07396eb 100644 --- a/src/Aspire.Cli/Resources/AgentCommandStrings.Designer.cs +++ b/src/Aspire.Cli/Resources/AgentCommandStrings.Designer.cs @@ -205,11 +205,29 @@ internal static string InitCommand_PlaywrightCliSkipped { } /// - /// Looks up a localized string similar to Installed {0} skill ({1}).. + /// Looks up a localized string similar to Installed Aspire agent skills:. /// - internal static string InitCommand_InstalledSkill { + internal static string InitCommand_InstalledSkillsSummary { get { - return ResourceManager.GetString("InitCommand_InstalledSkill", resourceCulture); + return ResourceManager.GetString("InitCommand_InstalledSkillsSummary", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Skills: {0}. + /// + internal static string InitCommand_InstalledSkillsSummarySkills { + get { + return ResourceManager.GetString("InitCommand_InstalledSkillsSummarySkills", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Locations: {0}. + /// + internal static string InitCommand_InstalledSkillsSummaryLocations { + get { + return ResourceManager.GetString("InitCommand_InstalledSkillsSummaryLocations", resourceCulture); } } diff --git a/src/Aspire.Cli/Resources/AgentCommandStrings.resx b/src/Aspire.Cli/Resources/AgentCommandStrings.resx index 275a332b141..58200adddbc 100644 --- a/src/Aspire.Cli/Resources/AgentCommandStrings.resx +++ b/src/Aspire.Cli/Resources/AgentCommandStrings.resx @@ -108,8 +108,14 @@ Playwright CLI requires npm, which was not found on PATH. Skipping installation. - - Installed {0} skill ({1}). + + Installed Aspire agent skills: + + + Skills: {0} + + + Locations: {0} Failed to install {0} skill at {1}: {2} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.cs.xlf index 4c3fa5cbf55..9d0d3f73197 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.cs.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.cs.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.de.xlf index 9083254de4e..18040330c2d 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.de.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.de.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.es.xlf index d3657b160c0..726777809ba 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.es.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.es.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.fr.xlf index 297ceb6f134..ec7eb479f9f 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.fr.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.fr.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.it.xlf index 0735f52225e..2443bf1c751 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.it.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.it.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ja.xlf index 922fd845ae4..92aa8ecd3d8 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ja.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ja.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ko.xlf index 87a7da1cb2e..d95b6611cb4 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ko.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ko.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pl.xlf index 09c9a59599b..40716c99436 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pl.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pl.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pt-BR.xlf index cddc1e8fedd..6885e7e33cf 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pt-BR.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pt-BR.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ru.xlf index f42eb379f17..8ac8e6a9d96 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ru.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ru.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.tr.xlf index 8559bf134d6..02725975345 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.tr.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.tr.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hans.xlf index 2e2531cc0ad..799665a0009 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hans.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hans.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hant.xlf index 177fbc55ac9..2b6bf955c01 100644 --- a/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hant.xlf +++ b/src/Aspire.Cli/Resources/xlf/AgentCommandStrings.zh-Hant.xlf @@ -57,9 +57,19 @@ Installed Playwright CLI. - - Installed {0} skill ({1}). - Installed {0} skill ({1}). + + Installed Aspire agent skills: + Installed Aspire agent skills: + + + + Locations: {0} + Locations: {0} + + + + Skills: {0} + Skills: {0} diff --git a/tests/Aspire.Cli.Tests/Commands/AgentInitCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/AgentInitCommandTests.cs index 6c7d792f38d..ace28bcd5c4 100644 --- a/tests/Aspire.Cli.Tests/Commands/AgentInitCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/AgentInitCommandTests.cs @@ -17,7 +17,7 @@ namespace Aspire.Cli.Tests.Commands; public class AgentInitCommandTests(ITestOutputHelper outputHelper) { [Fact] - public async Task AgentInitCommand_UsesNormalizedDisplayPath_WhenInstallingUserLevelSkill() + public async Task AgentInitCommand_SummarizesNormalizedDisplayPath_WhenInstallingUserLevelSkill() { using var workspace = TemporaryWorkspace.Create(outputHelper); var homeDirectory = workspace.CreateDirectory("fake-home"); @@ -44,13 +44,60 @@ public async Task AgentInitCommand_UsesNormalizedDisplayPath_WhenInstallingUserL var exitCode = await result.InvokeAsync().DefaultTimeout(); Assert.Equal(0, exitCode); + var expectedSummary = string.Join(Environment.NewLine, + AgentCommandStrings.InitCommand_InstalledSkillsSummary, + $" {string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkillsSummarySkills, SkillDefinition.Aspire.Name)}", + $" {string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkillsSummaryLocations, ".agents/skills, ~/.agents/skills")}"); + Assert.Contains( interactionService.DisplayedMessages, - displayedMessage => displayedMessage.Message == string.Format( + displayedMessage => displayedMessage.Emoji.Equals(KnownEmojis.Robot) && displayedMessage.Message == expectedSummary); + Assert.DoesNotContain( + interactionService.DisplayedMessages, + displayedMessage => displayedMessage.Message.Contains("Installed aspire skill", StringComparison.Ordinal)); + } + + [Fact] + public async Task AgentInitCommand_SummarizesDefaultSkillsOnce() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var homeDirectory = workspace.CreateDirectory("fake-home"); + var interactionService = new TestInteractionService(); + interactionService.SetupStringPromptResponse(workspace.WorkspaceRoot.FullName); + interactionService.PromptForSelectionsCallback = (_, choices, _, _) => choices.Cast() + .Where(choice => choice switch + { + SkillLocation location => location == SkillLocation.Standard, + SkillDefinition skill => skill.IsDefault, + _ => false + }) + .ToList(); + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.InteractionServiceFactory = _ => interactionService; + options.CliExecutionContextFactory = _ => CreateExecutionContext(workspace.WorkspaceRoot, homeDirectory); + }); + + using var provider = services.BuildServiceProvider(); + var command = provider.GetRequiredService(); + var result = command.Parse("agent init"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(0, exitCode); + + var expectedSummary = string.Join(Environment.NewLine, + AgentCommandStrings.InitCommand_InstalledSkillsSummary, + $" {string.Format( CultureInfo.CurrentCulture, - AgentCommandStrings.InitCommand_InstalledSkill, - SkillDefinition.Aspire.Name, - "~/.agents/skills/aspire")); + AgentCommandStrings.InitCommand_InstalledSkillsSummarySkills, + string.Join(", ", SkillDefinition.All.Where(static skill => skill.IsDefault).Select(static skill => skill.Name)))}", + $" {string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkillsSummaryLocations, ".agents/skills, ~/.agents/skills")}"); + var message = Assert.Single(interactionService.DisplayedMessages, displayedMessage => displayedMessage.Emoji.Equals(KnownEmojis.Robot)); + Assert.Equal(expectedSummary, message.Message); + Assert.DoesNotContain( + interactionService.DisplayedMessages, + displayedMessage => displayedMessage.Message.Contains("Installed aspire skill", StringComparison.Ordinal)); } [Fact]