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