From be186b68e372bf4b63341b6f343c15521010e4aa Mon Sep 17 00:00:00 2001
From: Ella Hathaway <67609881+ellahathaway@users.noreply.github.com>
Date: Sat, 7 Nov 2026 14:57:56 -0800
Subject: [PATCH] Clarify source and skill summaries in aspire agent init
(#21883)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
`aspire agent init` shows a generic installation-location preview that
can be mistaken for plugin configuration. Its result summary omits
configured source URLs and does not show where Playwright skills are
installed.
This PR separates configured remote sources from installed local skills:
- Show repository URLs and the actual OpenCode catalogs, including
retained version pins, alongside configuration locations.
- Include successfully installed Playwright skills and already-current
static skills in the installed-skills summary.
- Group locations with their supported agents, separate result sections
with blank lines, and dim the shared-location notice.
The excerpts below show project-scoped setup with GitHub Copilot, Claude
Code, and OpenCode selected, installing Aspire configuration and
Playwright CLI.
### Before
```text
Installation locations:
Skills: .agents/skills (shared: GitHub Copilot, OpenCode)
Skills: .claude/skills
Shared files may be discovered by agents that aren't selected.
🤖 Configured Aspire plugin:
GitHub Copilot: .github/copilot/settings.json
Claude Code: .claude/settings.json
OpenCode: opencode.jsonc
✅ Installed Playwright CLI.
✅ Agent environment configuration complete.
```
### After
```text
🤖 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)
✅ Installed Playwright CLI.
🤖 Installed skills:
Skills: playwright-cli
Locations: .agents/skills (GitHub Copilot, OpenCode), .claude/skills (Claude Code)
Shared files may be discovered by agents that aren't selected.
✅ Agent environment configuration complete.
```
The shared-location notice is dim in the terminal; the text block does
not show that styling.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a47a61ad-c59b-4674-8e74-e493bff49b02
(cherry picked from commit 2fefac5abe97fbfa878abac3b9f3b5dbbd211cd7)
---
.../Playwright/PlaywrightCliInstaller.cs | 43 +++--
src/Aspire.Cli/Commands/AgentInitCommand.cs | 182 ++++++++----------
.../Resources/AgentCommandStrings.Designer.cs | 22 ++-
.../Resources/AgentCommandStrings.resx | 14 +-
.../Resources/xlf/AgentCommandStrings.cs.xlf | 20 +-
.../Resources/xlf/AgentCommandStrings.de.xlf | 20 +-
.../Resources/xlf/AgentCommandStrings.es.xlf | 20 +-
.../Resources/xlf/AgentCommandStrings.fr.xlf | 20 +-
.../Resources/xlf/AgentCommandStrings.it.xlf | 20 +-
.../Resources/xlf/AgentCommandStrings.ja.xlf | 20 +-
.../Resources/xlf/AgentCommandStrings.ko.xlf | 20 +-
.../Resources/xlf/AgentCommandStrings.pl.xlf | 20 +-
.../xlf/AgentCommandStrings.pt-BR.xlf | 20 +-
.../Resources/xlf/AgentCommandStrings.ru.xlf | 20 +-
.../Resources/xlf/AgentCommandStrings.tr.xlf | 20 +-
.../xlf/AgentCommandStrings.zh-Hans.xlf | 20 +-
.../xlf/AgentCommandStrings.zh-Hant.xlf | 20 +-
.../Agents/PlaywrightCliInstallerTests.cs | 46 +++--
.../Commands/AgentInitClientSelectionTests.cs | 110 ++++++++++-
.../Commands/AgentInitCommandTests.cs | 70 ++++++-
...kills=False_configureMcp=True.verified.txt | 6 +-
...kills=True_configureMcp=False.verified.txt | 10 +-
...Skills=True_configureMcp=True.verified.txt | 12 +-
...kills=False_configureMcp=True.verified.txt | 7 +-
...kills=True_configureMcp=False.verified.txt | 10 +-
...Skills=True_configureMcp=True.verified.txt | 12 +-
...l_playwrightOutcome=installed.verified.txt | 4 +
...t_playwrightOutcome=installed.verified.txt | 4 +
...aywrightOutcome=not-installed.verified.txt | 4 +
...cessfulPlaywrightDestinations.verified.txt | 4 +
...guredSourcesAndAgentLocations.verified.txt | 3 +
...alog_array=False_pinned=False.verified.txt | 3 +
...talog_array=True_pinned=False.verified.txt | 3 +
...atalog_array=True_pinned=True.verified.txt | 3 +
.../TestServices/FakePlaywrightServices.cs | 4 +-
35 files changed, 581 insertions(+), 255 deletions(-)
create mode 100644 tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_SkillSummary_GroupsInstalledSkillsByLocationAndAgent_scope=global_playwrightOutcome=installed.verified.txt
create mode 100644 tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_SkillSummary_GroupsInstalledSkillsByLocationAndAgent_scope=project_playwrightOutcome=installed.verified.txt
create mode 100644 tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_SkillSummary_GroupsInstalledSkillsByLocationAndAgent_scope=project_playwrightOutcome=not-installed.verified.txt
create mode 100644 tests/Aspire.Cli.Tests/Snapshots/AgentInitClientSelectionTests.AgentInit_SkillSummary_IncludesOnlySuccessfulPlaywrightDestinations.verified.txt
create mode 100644 tests/Aspire.Cli.Tests/Snapshots/AgentInitCommandTests.Remote_SummaryListsConfiguredSourcesAndAgentLocations.verified.txt
create mode 100644 tests/Aspire.Cli.Tests/Snapshots/AgentInitCommandTests.Remote_SummaryUsesConfiguredOpenCodeCatalog_array=False_pinned=False.verified.txt
create mode 100644 tests/Aspire.Cli.Tests/Snapshots/AgentInitCommandTests.Remote_SummaryUsesConfiguredOpenCodeCatalog_array=True_pinned=False.verified.txt
create mode 100644 tests/Aspire.Cli.Tests/Snapshots/AgentInitCommandTests.Remote_SummaryUsesConfiguredOpenCodeCatalog_array=True_pinned=True.verified.txt
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