Skip to content

Add skill file update detection to aspire update command#14348

Open
mitchdenny wants to merge 8 commits intomainfrom
feature/update-skill-files
Open

Add skill file update detection to aspire update command#14348
mitchdenny wants to merge 8 commits intomainfrom
feature/update-skill-files

Conversation

@mitchdenny
Copy link
Member

Summary

This PR improves the aspire update command to detect the presence of SKILL.md files dropped by aspire agent init and prompt the user to update them if they are out of date.

Changes

Core Feature

  • Modified UpdateCommand.cs to detect outdated SKILL.md files after updating project packages
  • Added IGitRepository dependency to find the git repository root
  • Added CheckAndUpdateSkillFilesAsync method that:
    • Finds the git repository root
    • Checks if a skill file exists at .github/skills/aspire/SKILL.md
    • Compares content with the current expected content (using normalized line endings)
    • Prompts user to update if content differs
    • Updates the file when user confirms

Resource Strings

  • Added SkillFileOutdatedPrompt - "The Aspire skill file ({0}) is out of date. Would you like to update it?"
  • Added SkillFileUpdatedMessage - "Aspire skill file updated successfully."

Test Infrastructure

  • Added TestGitRepository.cs - Mock implementation of IGitRepository for unit tests
  • Added DisplaySuccessCallback to TestConsoleInteractionService

Unit Tests (5 new tests in UpdateCommandSkillFileTests)

  1. UpdateCommand_DetectsOutdatedSkillFile_PromptsForUpdate - Verifies prompt appears for outdated skill file
  2. UpdateCommand_WithOutdatedSkillFile_UpdatesWhenConfirmed - Verifies file is updated when user confirms
  3. UpdateCommand_WithUpToDateSkillFile_DoesNotPrompt - Verifies no prompt when file is current
  4. UpdateCommand_WithNoSkillFile_DoesNotPrompt - Verifies no prompt when no skill file exists
  5. UpdateCommand_WhenNotInGitRepo_DoesNotCheckSkillFile - Verifies no check when not in git repo

E2E Test

  • Added UpdateSkillFileTests.cs with test UpdateCommand_DetectsOutdatedSkillFile_PromptsForUpdate
    • Creates a placeholder SKILL.md file with outdated content
    • Runs aspire update
    • Verifies the skill file update prompt appears
    • Confirms the update and verifies file content is updated

Testing

  • All 20 UpdateCommand unit tests pass (including 5 new tests)
  • E2E test compiles successfully
  • Build succeeds with no warnings or errors

Copilot AI review requested due to automatic review settings February 5, 2026 01:38
@github-actions
Copy link
Contributor

github-actions bot commented Feb 5, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 14348

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 14348"

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the aspire update command to detect and prompt users to update outdated SKILL.md files that were previously created by the aspire agent init command. The feature checks for skill files after updating project packages and offers to update them if they differ from the current expected content.

Changes:

  • Added skill file detection logic to UpdateCommand that checks for outdated SKILL.md files after package updates
  • Added new resource strings for user prompts and success messages with localization support across 13 languages
  • Created comprehensive unit and E2E test coverage with 5 new unit tests and 1 E2E test
  • Enhanced test infrastructure with TestGitRepository mock and DisplaySuccessCallback in TestConsoleInteractionService

Reviewed changes

Copilot reviewed 19 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Aspire.Cli/Commands/UpdateCommand.cs Added CheckAndUpdateSkillFilesAsync method, IGitRepository dependency, and skill file path constant to detect and update outdated skill files
src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs Added properties for new resource strings (SkillFileOutdatedPrompt, SkillFileUpdatedMessage)
src/Aspire.Cli/Resources/UpdateCommandStrings.resx Defined base English resource strings for skill file update prompts and messages
src/Aspire.Cli/Resources/xlf/*.xlf Added localization entries for 13 languages (marked as "new" for translation)
tests/Aspire.Cli.Tests/TestServices/TestGitRepository.cs Created mock implementation of IGitRepository for unit testing
tests/Aspire.Cli.Tests/TestServices/TestConsoleInteractionService.cs Added DisplaySuccessCallback property to test service for success message verification
tests/Aspire.Cli.Tests/Commands/UpdateCommandTests.cs Added UpdateCommandSkillFileTests class with 5 comprehensive unit tests covering various scenarios
tests/Aspire.Cli.EndToEnd.Tests/UpdateSkillFileTests.cs Added end-to-end test validating skill file detection, update prompt, and file content update
Files not reviewed (1)
  • src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs: Language not supported

Comment on lines 582 to 625
// Try to discover the git repository root
var gitRoot = await _gitRepository.GetRootAsync(cancellationToken);
if (gitRoot is null)
{
_logger.LogDebug("Not in a git repository, skipping skill file update check");
return;
}

var skillFilePath = Path.Combine(gitRoot.FullName, s_skillFileRelativePath);

if (!File.Exists(skillFilePath))
{
_logger.LogDebug("No skill file found at {SkillFilePath}, skipping update check", skillFilePath);
return;
}

// Read existing content and compare with current expected content
var existingContent = await File.ReadAllTextAsync(skillFilePath, cancellationToken);
var normalizedExisting = NormalizeLineEndings(existingContent);
var normalizedExpected = NormalizeLineEndings(CommonAgentApplicators.SkillFileContent);

if (string.Equals(normalizedExisting, normalizedExpected, StringComparison.Ordinal))
{
_logger.LogDebug("Skill file at {SkillFilePath} is up to date", skillFilePath);
return;
}

// Skill file is outdated, prompt for update
var promptMessage = string.Format(
CultureInfo.CurrentCulture,
UpdateCommandStrings.SkillFileOutdatedPrompt,
s_skillFileRelativePath);

var shouldUpdate = await InteractionService.ConfirmAsync(
promptMessage,
defaultValue: true,
cancellationToken);

if (shouldUpdate)
{
await File.WriteAllTextAsync(skillFilePath, CommonAgentApplicators.SkillFileContent, cancellationToken);
InteractionService.DisplaySuccess(UpdateCommandStrings.SkillFileUpdatedMessage);
_logger.LogInformation("Updated skill file at {SkillFilePath}", skillFilePath);
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CheckAndUpdateSkillFilesAsync method lacks error handling. If an exception occurs during file I/O operations (reading at line 599 or writing at line 622), it will propagate to the outer try-catch block and cause the entire update command to fail. Since skill file checking is an optional enhancement feature, consider wrapping the method body in a try-catch block that logs errors and continues gracefully. This ensures that failures in skill file detection don't prevent the main update operation from completing successfully.

Suggested change
// Try to discover the git repository root
var gitRoot = await _gitRepository.GetRootAsync(cancellationToken);
if (gitRoot is null)
{
_logger.LogDebug("Not in a git repository, skipping skill file update check");
return;
}
var skillFilePath = Path.Combine(gitRoot.FullName, s_skillFileRelativePath);
if (!File.Exists(skillFilePath))
{
_logger.LogDebug("No skill file found at {SkillFilePath}, skipping update check", skillFilePath);
return;
}
// Read existing content and compare with current expected content
var existingContent = await File.ReadAllTextAsync(skillFilePath, cancellationToken);
var normalizedExisting = NormalizeLineEndings(existingContent);
var normalizedExpected = NormalizeLineEndings(CommonAgentApplicators.SkillFileContent);
if (string.Equals(normalizedExisting, normalizedExpected, StringComparison.Ordinal))
{
_logger.LogDebug("Skill file at {SkillFilePath} is up to date", skillFilePath);
return;
}
// Skill file is outdated, prompt for update
var promptMessage = string.Format(
CultureInfo.CurrentCulture,
UpdateCommandStrings.SkillFileOutdatedPrompt,
s_skillFileRelativePath);
var shouldUpdate = await InteractionService.ConfirmAsync(
promptMessage,
defaultValue: true,
cancellationToken);
if (shouldUpdate)
{
await File.WriteAllTextAsync(skillFilePath, CommonAgentApplicators.SkillFileContent, cancellationToken);
InteractionService.DisplaySuccess(UpdateCommandStrings.SkillFileUpdatedMessage);
_logger.LogInformation("Updated skill file at {SkillFilePath}", skillFilePath);
}
try
{
// Try to discover the git repository root
var gitRoot = await _gitRepository.GetRootAsync(cancellationToken);
if (gitRoot is null)
{
_logger.LogDebug("Not in a git repository, skipping skill file update check");
return;
}
var skillFilePath = Path.Combine(gitRoot.FullName, s_skillFileRelativePath);
if (!File.Exists(skillFilePath))
{
_logger.LogDebug("No skill file found at {SkillFilePath}, skipping update check", skillFilePath);
return;
}
// Read existing content and compare with current expected content
var existingContent = await File.ReadAllTextAsync(skillFilePath, cancellationToken);
var normalizedExisting = NormalizeLineEndings(existingContent);
var normalizedExpected = NormalizeLineEndings(CommonAgentApplicators.SkillFileContent);
if (string.Equals(normalizedExisting, normalizedExpected, StringComparison.Ordinal))
{
_logger.LogDebug("Skill file at {SkillFilePath} is up to date", skillFilePath);
return;
}
// Skill file is outdated, prompt for update
var promptMessage = string.Format(
CultureInfo.CurrentCulture,
UpdateCommandStrings.SkillFileOutdatedPrompt,
s_skillFileRelativePath);
var shouldUpdate = await InteractionService.ConfirmAsync(
promptMessage,
defaultValue: true,
cancellationToken);
if (shouldUpdate)
{
await File.WriteAllTextAsync(skillFilePath, CommonAgentApplicators.SkillFileContent, cancellationToken);
InteractionService.DisplaySuccess(UpdateCommandStrings.SkillFileUpdatedMessage);
_logger.LogInformation("Updated skill file at {SkillFilePath}", skillFilePath);
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to check or update Aspire skill files. Continuing without updating skill files.");
}

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Contributor

github-actions bot commented Feb 5, 2026

🎬 CLI E2E Test Recordings

The following terminal recordings are available for commit bfb4279:

Test Recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View Recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View Recording
AgentInitCommand_OffersUniversalSkillFile ▶️ View Recording
Banner_DisplayedOnFirstRun ▶️ View Recording
Banner_DisplayedWithExplicitFlag ▶️ View Recording
CreateAndDeployToDockerCompose ▶️ View Recording
CreateAndDeployToDockerComposeInteractive ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateEmptyAppHostProject ▶️ View Recording
CreateStartAndStopAspireProject ▶️ View Recording
CreateTypeScriptAppHostWithViteApp ▶️ View Recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View Recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View Recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View Recording
LogsCommandShowsResourceLogs ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
ResourcesCommandShowsRunningResources ▶️ View Recording
UpdateCommand_DetectsOutdatedSkillFiles_AllLocations ▶️ View Recording

📹 Recordings uploaded automatically from CI run #21893791897

@davidfowl
Copy link
Member

Super slick.

@mitchdenny mitchdenny force-pushed the feature/update-skill-files branch 3 times, most recently from af7982f to 5fabcee Compare February 5, 2026 05:43
Mitch Denny added 8 commits February 11, 2026 10:39
- Modified UpdateCommand to detect outdated SKILL.md files dropped by 'aspire agent init'
- After updating project packages, checks if a skill file exists in the repository
- Compares existing skill file content with the current expected content
- Prompts user to update the skill file if it's out of date
- Added IGitRepository dependency to find git repository root

Tests:
- Added 5 unit tests for skill file detection and update functionality
- Added E2E CLI test that creates an outdated SKILL.md and verifies prompt appears

Test infrastructure:
- Added TestGitRepository for mocking git repository operations in tests
- Added DisplaySuccessCallback to TestConsoleInteractionService
- Check all three skill file locations: .github, .opencode, .claude
- Prompt for each outdated file separately
- Add unit tests for each location and multi-file scenario
- Update E2E test to verify all locations are detected
Add support for the agent skills convention (.agents/skills/) which is
a universal location recognized by multiple coding agents.

Changes:
- Add UniversalSkillFileScanner that always offers to create the skill file
  at .agents/skills/aspire/SKILL.md regardless of detected agents
- Update UpdateCommand to check the new skill file path
- Add unit tests for the new scanner
- Add E2E test to verify agent init offers the universal skill file
- Add VerifyFileExists helper method for E2E tests
Add .NotRequired() to MultiSelectionPrompt to allow users to skip
agent environment selection and proceed to skill file options.
This enables pressing Enter without selecting any items.
@mitchdenny mitchdenny force-pushed the feature/update-skill-files branch from 00de1af to bfb4279 Compare February 10, 2026 23:40
@davidfowl davidfowl closed this Feb 11, 2026
@davidfowl davidfowl reopened this Feb 11, 2026
@dotnet-policy-service dotnet-policy-service bot added this to the 13.2 milestone Feb 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants