Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 68 additions & 13 deletions src/Aspire.Cli/Commands/AgentInitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ private async Task<AgentInitExecutionResult> ExecuteAgentInitAsync(DirectoryInfo
}
}

var installedSkills = new List<InstalledSkillSummaryItem>();

foreach (var location in selectedLocations)
{
context.AddSkillBaseDirectory(location.RelativeSkillDirectory);
Expand All @@ -328,27 +330,39 @@ private async Task<AgentInitExecutionResult> ExecuteAgentInitAsync(DirectoryInfo
continue;
}

hasErrors |= !await InstallSkillAsync(
var installResult = await InstallSkillAsync(
workspaceRoot,
location.RelativeSkillDirectory,
skill,
aspireSkillsBundle,
isUserLevel: false,
cancellationToken);
hasErrors |= !installResult.Succeeded;
if (installResult.UpdatedSkill is not null)
{
installedSkills.Add(installResult.UpdatedSkill);
}

if (location.IncludeUserLevel)
{
hasErrors |= !await InstallSkillAsync(
installResult = await InstallSkillAsync(
ExecutionContext.HomeDirectory,
location.RelativeSkillDirectory,
skill,
aspireSkillsBundle,
isUserLevel: true,
cancellationToken);
hasErrors |= !installResult.Succeeded;
if (installResult.UpdatedSkill is not null)
{
installedSkills.Add(installResult.UpdatedSkill);
}
}
}
}

DisplayInstalledSkillsSummary(installedSkills);

// --- Phase 4: Handle Playwright CLI (installs binary + mirrors skill files to registered directories) ---
var selectedSkillDirs = selectedLocations.Select(l => l.RelativeSkillDirectory).ToHashSet(StringComparer.OrdinalIgnoreCase);
if (selectedSkills.Contains(SkillDefinition.PlaywrightCli) && selectedLocations.Count > 0)
Expand Down Expand Up @@ -424,8 +438,8 @@ private async Task<AgentInitExecutionResult> ExecuteAgentInitAsync(DirectoryInfo
/// <summary>
/// Installs the files for a skill at the specified location, creating or updating them as needed.
/// </summary>
/// <returns><c>true</c> if successful, <c>false</c> if an error occurred.</returns>
private async Task<bool> InstallSkillAsync(
/// <returns>The install result, including the skill/location pair when files were updated.</returns>
private async Task<SkillInstallResult> InstallSkillAsync(
DirectoryInfo rootDirectory,
string relativeSkillDirectory,
SkillDefinition skill,
Expand Down Expand Up @@ -465,23 +479,60 @@ private async Task<bool> InstallSkillAsync(

if (!anyFileUpdated)
{
return true;
return new(Succeeded: true, UpdatedSkill: null);
}

var displayRelativeSkillPath = relativeSkillPath
.Replace(Path.DirectorySeparatorChar, '/')
.Replace(Path.AltDirectorySeparatorChar, '/');
var displayPath = isUserLevel ? $"~/{displayRelativeSkillPath}" : displayRelativeSkillPath;
_interactionService.DisplayMessage(KnownEmojis.Robot,
string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkill, skill.Name, displayPath));
return true;
var displayLocation = GetDisplaySkillDirectory(relativeSkillDirectory, isUserLevel);
return new(Succeeded: true, new InstalledSkillSummaryItem(skill.Name, displayLocation));
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidOperationException)
{
_interactionService.DisplayError(
string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_FailedToInstallSkill, skill.Name, fullSkillDirectoryPath, ex.Message));
return false;
return new(Succeeded: false, UpdatedSkill: null);
}
}

private void DisplayInstalledSkillsSummary(IReadOnlyList<InstalledSkillSummaryItem> installedSkills)
{
if (installedSkills.Count == 0)
{
return;
}

var skillNames = string.Join(", ", GetUniqueValues(installedSkills.Select(static installedSkill => installedSkill.SkillName)));
var locations = string.Join(", ", GetUniqueValues(installedSkills.Select(static installedSkill => installedSkill.DisplayLocation)));
var message = string.Join(Environment.NewLine,
AgentCommandStrings.InitCommand_InstalledSkillsSummary,
$" {string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkillsSummarySkills, skillNames)}",
$" {string.Format(CultureInfo.CurrentCulture, AgentCommandStrings.InitCommand_InstalledSkillsSummaryLocations, locations)}");

_interactionService.DisplayMessage(KnownEmojis.Robot, message);
}

private static IReadOnlyList<string> GetUniqueValues(IEnumerable<string> values)
{
var uniqueValues = new List<string>();
var seenValues = new HashSet<string>(StringComparer.Ordinal);

foreach (var value in values)
{
if (seenValues.Add(value))
{
uniqueValues.Add(value);
}
}

return uniqueValues;
}

private static string GetDisplaySkillDirectory(string relativeSkillDirectory, bool isUserLevel)
{
var displayRelativeSkillDirectory = relativeSkillDirectory
.Replace(Path.DirectorySeparatorChar, '/')
.Replace(Path.AltDirectorySeparatorChar, '/');

return isUserLevel ? $"~/{displayRelativeSkillDirectory}" : displayRelativeSkillDirectory;
}

private static async Task<IReadOnlyList<SkillAssetFile>> GetSkillFilesAsync(SkillDefinition skill, AspireSkillsBundle? aspireSkillsBundle, CancellationToken cancellationToken)
Expand Down Expand Up @@ -509,6 +560,10 @@ private enum AgentInitErrorMode
Strict,
BestEffort
}

private sealed record InstalledSkillSummaryItem(string SkillName, string DisplayLocation);

private readonly record struct SkillInstallResult(bool Succeeded, InstalledSkillSummaryItem? UpdatedSkill);
}

internal readonly record struct AgentInitExecutionResult(
Expand Down
24 changes: 21 additions & 3 deletions src/Aspire.Cli/Resources/AgentCommandStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions src/Aspire.Cli/Resources/AgentCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,14 @@
<data name="InitCommand_PlaywrightCliSkipped" xml:space="preserve">
<value>Playwright CLI requires npm, which was not found on PATH. Skipping installation.</value>
</data>
<data name="InitCommand_InstalledSkill" xml:space="preserve">
<value>Installed {0} skill ({1}).</value>
<data name="InitCommand_InstalledSkillsSummary" xml:space="preserve">
<value>Installed Aspire agent skills:</value>
</data>
<data name="InitCommand_InstalledSkillsSummarySkills" xml:space="preserve">
<value>Skills: {0}</value>
</data>
<data name="InitCommand_InstalledSkillsSummaryLocations" xml:space="preserve">
<value>Locations: {0}</value>
</data>
<data name="InitCommand_FailedToInstallSkill" xml:space="preserve">
<value>Failed to install {0} skill at {1}: {2}</value>
Expand Down
16 changes: 13 additions & 3 deletions src/Aspire.Cli/Resources/xlf/AgentCommandStrings.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions src/Aspire.Cli/Resources/xlf/AgentCommandStrings.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions src/Aspire.Cli/Resources/xlf/AgentCommandStrings.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions src/Aspire.Cli/Resources/xlf/AgentCommandStrings.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions src/Aspire.Cli/Resources/xlf/AgentCommandStrings.it.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ja.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions src/Aspire.Cli/Resources/xlf/AgentCommandStrings.ko.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pl.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions src/Aspire.Cli/Resources/xlf/AgentCommandStrings.pt-BR.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading