From 6541acb3920dfd98e60d0516b8b97a768ca2749c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 08:24:17 +0000 Subject: [PATCH 1/5] Initial plan From 335b328734c916fa2d74262ac569387be4140d3f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 08:33:24 +0000 Subject: [PATCH 2/5] Add NuGet.config creation to InitializeExistingSolutionAsync method Co-authored-by: mitchdenny <513398+mitchdenny@users.noreply.github.com> --- src/Aspire.Cli/Commands/InitCommand.cs | 53 ++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/Aspire.Cli/Commands/InitCommand.cs b/src/Aspire.Cli/Commands/InitCommand.cs index ec46dc15f2e..f67ac2cd750 100644 --- a/src/Aspire.Cli/Commands/InitCommand.cs +++ b/src/Aspire.Cli/Commands/InitCommand.cs @@ -461,6 +461,9 @@ ServiceDefaults project contains helper code to make it easier await _certificateService.EnsureCertificatesTrustedAsync(_runner, cancellationToken); + // Create or update NuGet.config for explicit channels + await CreateOrUpdateNuGetConfigAsync(initContext.SolutionDirectory, selectedTemplateDetails.Channel, cancellationToken); + InteractionService.DisplaySuccess(InitCommandStrings.AspireInitializationComplete); return ExitCodeConstants.Success; } @@ -597,6 +600,56 @@ private static bool IsSupportedTfm(string tfm) }; } + /// + /// Creates or updates a NuGet.config file in the solution directory for explicit channels. + /// + private async Task CreateOrUpdateNuGetConfigAsync(DirectoryInfo solutionDirectory, PackageChannel channel, CancellationToken cancellationToken) + { + if (channel.Type is not PackageChannelType.Explicit) + { + return; + } + + var mappings = channel.Mappings; + if (mappings is null || mappings.Length == 0) + { + return; + } + + var hasConfigInSolutionDir = NuGetConfigMerger.TryFindNuGetConfigInDirectory(solutionDirectory, out var nugetConfigFile); + var hasMissingSources = hasConfigInSolutionDir && NuGetConfigMerger.HasMissingSources(solutionDirectory, channel); + + if (!hasConfigInSolutionDir) + { + // Ask for confirmation before creating the file + var choice = await InteractionService.PromptForSelectionAsync( + TemplatingStrings.CreateNugetConfigConfirmation, + [TemplatingStrings.Yes, TemplatingStrings.No], + c => c, + cancellationToken); + + if (string.Equals(choice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput)) + { + await NuGetConfigMerger.CreateOrUpdateAsync(solutionDirectory, channel, cancellationToken: cancellationToken); + InteractionService.DisplayMessage("package", TemplatingStrings.NuGetConfigCreatedConfirmationMessage); + } + } + else if (hasMissingSources) + { + var updateChoice = await InteractionService.PromptForSelectionAsync( + "Update NuGet.config to add missing package sources for the selected channel?", + [TemplatingStrings.Yes, TemplatingStrings.No], + c => c, + cancellationToken); + + if (string.Equals(updateChoice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput)) + { + await NuGetConfigMerger.CreateOrUpdateAsync(solutionDirectory, channel, cancellationToken: cancellationToken); + InteractionService.DisplayMessage("package", "Updated NuGet.config with required package sources."); + } + } + } + private async Task<(NuGetPackage Package, PackageChannel Channel)> GetProjectTemplatesVersionAsync(ParseResult parseResult, CancellationToken cancellationToken) { var channels = await _packagingService.GetChannelsAsync(cancellationToken); From 872cc3017068df6883ce07bb93bc941960905619 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 12:47:42 +0000 Subject: [PATCH 3/5] Refactor NuGet.config prompting logic into shared NuGetConfigPrompter class Co-authored-by: mitchdenny <513398+mitchdenny@users.noreply.github.com> --- src/Aspire.Cli/Commands/InitCommand.cs | 53 +-------- .../Packaging/NuGetConfigPrompter.cs | 105 ++++++++++++++++++ .../Templating/DotNetTemplateFactory.cs | 51 +-------- 3 files changed, 112 insertions(+), 97 deletions(-) create mode 100644 src/Aspire.Cli/Packaging/NuGetConfigPrompter.cs diff --git a/src/Aspire.Cli/Commands/InitCommand.cs b/src/Aspire.Cli/Commands/InitCommand.cs index f67ac2cd750..700611b3cac 100644 --- a/src/Aspire.Cli/Commands/InitCommand.cs +++ b/src/Aspire.Cli/Commands/InitCommand.cs @@ -462,7 +462,8 @@ ServiceDefaults project contains helper code to make it easier await _certificateService.EnsureCertificatesTrustedAsync(_runner, cancellationToken); // Create or update NuGet.config for explicit channels - await CreateOrUpdateNuGetConfigAsync(initContext.SolutionDirectory, selectedTemplateDetails.Channel, cancellationToken); + var nugetConfigPrompter = new NuGetConfigPrompter(InteractionService); + await nugetConfigPrompter.PromptToCreateOrUpdateAsync(initContext.SolutionDirectory, selectedTemplateDetails.Channel, cancellationToken); InteractionService.DisplaySuccess(InitCommandStrings.AspireInitializationComplete); return ExitCodeConstants.Success; @@ -600,56 +601,6 @@ private static bool IsSupportedTfm(string tfm) }; } - /// - /// Creates or updates a NuGet.config file in the solution directory for explicit channels. - /// - private async Task CreateOrUpdateNuGetConfigAsync(DirectoryInfo solutionDirectory, PackageChannel channel, CancellationToken cancellationToken) - { - if (channel.Type is not PackageChannelType.Explicit) - { - return; - } - - var mappings = channel.Mappings; - if (mappings is null || mappings.Length == 0) - { - return; - } - - var hasConfigInSolutionDir = NuGetConfigMerger.TryFindNuGetConfigInDirectory(solutionDirectory, out var nugetConfigFile); - var hasMissingSources = hasConfigInSolutionDir && NuGetConfigMerger.HasMissingSources(solutionDirectory, channel); - - if (!hasConfigInSolutionDir) - { - // Ask for confirmation before creating the file - var choice = await InteractionService.PromptForSelectionAsync( - TemplatingStrings.CreateNugetConfigConfirmation, - [TemplatingStrings.Yes, TemplatingStrings.No], - c => c, - cancellationToken); - - if (string.Equals(choice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput)) - { - await NuGetConfigMerger.CreateOrUpdateAsync(solutionDirectory, channel, cancellationToken: cancellationToken); - InteractionService.DisplayMessage("package", TemplatingStrings.NuGetConfigCreatedConfirmationMessage); - } - } - else if (hasMissingSources) - { - var updateChoice = await InteractionService.PromptForSelectionAsync( - "Update NuGet.config to add missing package sources for the selected channel?", - [TemplatingStrings.Yes, TemplatingStrings.No], - c => c, - cancellationToken); - - if (string.Equals(updateChoice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput)) - { - await NuGetConfigMerger.CreateOrUpdateAsync(solutionDirectory, channel, cancellationToken: cancellationToken); - InteractionService.DisplayMessage("package", "Updated NuGet.config with required package sources."); - } - } - } - private async Task<(NuGetPackage Package, PackageChannel Channel)> GetProjectTemplatesVersionAsync(ParseResult parseResult, CancellationToken cancellationToken) { var channels = await _packagingService.GetChannelsAsync(cancellationToken); diff --git a/src/Aspire.Cli/Packaging/NuGetConfigPrompter.cs b/src/Aspire.Cli/Packaging/NuGetConfigPrompter.cs new file mode 100644 index 00000000000..5e0ce63a038 --- /dev/null +++ b/src/Aspire.Cli/Packaging/NuGetConfigPrompter.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Cli.Interaction; +using Aspire.Cli.Resources; + +namespace Aspire.Cli.Packaging; + +/// +/// Handles prompting users to create or update NuGet.config files for explicit package channels. +/// +internal class NuGetConfigPrompter +{ + private readonly IInteractionService _interactionService; + + public NuGetConfigPrompter(IInteractionService interactionService) + { + ArgumentNullException.ThrowIfNull(interactionService); + _interactionService = interactionService; + } + + /// + /// Prompts to create or update a NuGet.config for explicit channels. + /// Always prompts the user before creating or updating the file. + /// + /// The directory where the NuGet.config should be created or updated. + /// The package channel providing mapping information. + /// A cancellation token to observe while waiting for the task to complete. + public async Task PromptToCreateOrUpdateAsync(DirectoryInfo targetDirectory, PackageChannel channel, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(targetDirectory); + ArgumentNullException.ThrowIfNull(channel); + + if (channel.Type is not PackageChannelType.Explicit) + { + return; + } + + var mappings = channel.Mappings; + if (mappings is null || mappings.Length == 0) + { + return; + } + + var hasConfigInTargetDir = NuGetConfigMerger.TryFindNuGetConfigInDirectory(targetDirectory, out var nugetConfigFile); + var hasMissingSources = hasConfigInTargetDir && NuGetConfigMerger.HasMissingSources(targetDirectory, channel); + + if (!hasConfigInTargetDir) + { + // Ask for confirmation before creating the file + var choice = await _interactionService.PromptForSelectionAsync( + TemplatingStrings.CreateNugetConfigConfirmation, + [TemplatingStrings.Yes, TemplatingStrings.No], + c => c, + cancellationToken); + + if (string.Equals(choice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput)) + { + await NuGetConfigMerger.CreateOrUpdateAsync(targetDirectory, channel, cancellationToken: cancellationToken); + _interactionService.DisplayMessage("package", TemplatingStrings.NuGetConfigCreatedConfirmationMessage); + } + } + else if (hasMissingSources) + { + var updateChoice = await _interactionService.PromptForSelectionAsync( + "Update NuGet.config to add missing package sources for the selected channel?", + [TemplatingStrings.Yes, TemplatingStrings.No], + c => c, + cancellationToken); + + if (string.Equals(updateChoice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput)) + { + await NuGetConfigMerger.CreateOrUpdateAsync(targetDirectory, channel, cancellationToken: cancellationToken); + _interactionService.DisplayMessage("package", "Updated NuGet.config with required package sources."); + } + } + } + + /// + /// Creates or updates a NuGet.config for explicit channels without prompting. + /// This is used when creating projects in subdirectories where the behavior is expected. + /// + /// The directory where the NuGet.config should be created or updated. + /// The package channel providing mapping information. + /// A cancellation token to observe while waiting for the task to complete. + public async Task CreateOrUpdateWithoutPromptAsync(DirectoryInfo targetDirectory, PackageChannel channel, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(targetDirectory); + ArgumentNullException.ThrowIfNull(channel); + + if (channel.Type is not PackageChannelType.Explicit) + { + return; + } + + var mappings = channel.Mappings; + if (mappings is null || mappings.Length == 0) + { + return; + } + + await NuGetConfigMerger.CreateOrUpdateAsync(targetDirectory, channel, cancellationToken: cancellationToken); + _interactionService.DisplayMessage("package", "Created or updated NuGet.config in the project directory with required package sources."); + } +} diff --git a/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs b/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs index a73df537306..40e31325f4a 100644 --- a/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs +++ b/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs @@ -480,60 +480,19 @@ private async Task PromptToCreateOrUpdateNuGetConfigAsync(PackageChannel channel var normalizedWorkingPath = workingDir.FullName; var isInPlaceCreation = string.Equals(normalizedOutputPath, normalizedWorkingPath, StringComparison.OrdinalIgnoreCase); + var nugetConfigPrompter = new NuGetConfigPrompter(interactionService); + if (!isInPlaceCreation) { // For subdirectory creation, always create/update NuGet.config in the output directory only // and ignore any existing NuGet.config in the working directory - await NuGetConfigMerger.CreateOrUpdateAsync(outputDir, channel, cancellationToken: cancellationToken); - interactionService.DisplayMessage("package", "Created or updated NuGet.config in the project directory with required package sources."); + await nugetConfigPrompter.CreateOrUpdateWithoutPromptAsync(outputDir, channel, cancellationToken); return; } // In-place creation: preserve existing behavior - // Check if we need to create or update a NuGet.config in the working directory - var hasConfigInWorkingDir = TryFindNuGetConfigInDirectory(workingDir, out var nugetConfigFile); - var hasMissingSources = hasConfigInWorkingDir && NuGetConfigMerger.HasMissingSources(workingDir, channel); - - if (!hasConfigInWorkingDir) - { - // Ask for confirmation before creating the file - var choice = await interactionService.PromptForSelectionAsync( - TemplatingStrings.CreateNugetConfigConfirmation, - [TemplatingStrings.Yes, TemplatingStrings.No], - c => c, - cancellationToken); - - if (string.Equals(choice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput)) - { - await NuGetConfigMerger.CreateOrUpdateAsync(outputDir, channel, cancellationToken: cancellationToken); - interactionService.DisplayMessage("package", TemplatingStrings.NuGetConfigCreatedConfirmationMessage); - } - } - else if (hasMissingSources) - { - var updateChoice = await interactionService.PromptForSelectionAsync( - "Update NuGet.config to add missing package sources for the selected channel?", - [TemplatingStrings.Yes, TemplatingStrings.No], - c => c, - cancellationToken); - - if (string.Equals(updateChoice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput)) - { - await NuGetConfigMerger.CreateOrUpdateAsync(workingDir, channel, cancellationToken: cancellationToken); - interactionService.DisplayMessage("package", "Updated NuGet.config with required package sources."); - } - } - } - - private static bool TryFindNuGetConfigInDirectory(DirectoryInfo directory, out FileInfo? nugetConfigFile) - { - ArgumentNullException.ThrowIfNull(directory); - - // Search only the specified directory for a file named "nuget.config", ignoring case - nugetConfigFile = directory - .EnumerateFiles("*", SearchOption.TopDirectoryOnly) - .FirstOrDefault(f => string.Equals(f.Name, "nuget.config", StringComparison.OrdinalIgnoreCase)); - return nugetConfigFile is not null; + // Prompt user before creating or updating NuGet.config + await nugetConfigPrompter.PromptToCreateOrUpdateAsync(workingDir, channel, cancellationToken); } } From 8fc619b8f28d6383abe4f9e94991af17cf5aaede Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Mon, 13 Oct 2025 14:50:59 +1100 Subject: [PATCH 4/5] Move nuget prompting earlier to make sure it is available when apphost is added to solution. --- src/Aspire.Cli/Commands/InitCommand.cs | 36 +++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/Aspire.Cli/Commands/InitCommand.cs b/src/Aspire.Cli/Commands/InitCommand.cs index 700611b3cac..a4900551bee 100644 --- a/src/Aspire.Cli/Commands/InitCommand.cs +++ b/src/Aspire.Cli/Commands/InitCommand.cs @@ -249,6 +249,10 @@ ServiceDefaults project contains helper code to make it easier // Get template version/channel selection using the same logic as NewCommand var selectedTemplateDetails = await GetProjectTemplatesVersionAsync(parseResult, cancellationToken); + + // Create or update NuGet.config for explicit channels in the solution directory + // This matches the behavior of 'aspire new' when creating in-place + await PromptToCreateOrUpdateNuGetConfigAsync(selectedTemplateDetails.Channel, ExecutionContext.WorkingDirectory, cancellationToken); // Create a temporary directory for the template output var tempProjectDir = Path.Combine(Path.GetTempPath(), $"aspire-init-{Guid.NewGuid()}"); @@ -461,10 +465,6 @@ ServiceDefaults project contains helper code to make it easier await _certificateService.EnsureCertificatesTrustedAsync(_runner, cancellationToken); - // Create or update NuGet.config for explicit channels - var nugetConfigPrompter = new NuGetConfigPrompter(InteractionService); - await nugetConfigPrompter.PromptToCreateOrUpdateAsync(initContext.SolutionDirectory, selectedTemplateDetails.Channel, cancellationToken); - InteractionService.DisplaySuccess(InitCommandStrings.AspireInitializationComplete); return ExitCodeConstants.Success; } @@ -601,6 +601,34 @@ private static bool IsSupportedTfm(string tfm) }; } + /// + /// Prompts to create or update a NuGet.config for explicit channels in the target directory. + /// This ensures consistent behavior with the DotNetTemplateFactory approach. + /// + /// The package channel. + /// The target directory where NuGet.config should be created or updated. + /// A cancellation token. + private async Task PromptToCreateOrUpdateNuGetConfigAsync(PackageChannel channel, DirectoryInfo targetDirectory, CancellationToken cancellationToken) + { + if (channel.Type is not PackageChannelType.Explicit) + { + return; + } + + var mappings = channel.Mappings; + if (mappings is null || mappings.Length == 0) + { + return; + } + + var nugetConfigPrompter = new NuGetConfigPrompter(InteractionService); + + // For solution-based init, we're creating projects in the solution directory + // This is equivalent to "in-place creation" in DotNetTemplateFactory terms + // We should prompt the user before creating/updating the NuGet.config + await nugetConfigPrompter.PromptToCreateOrUpdateAsync(targetDirectory, channel, cancellationToken); + } + private async Task<(NuGetPackage Package, PackageChannel Channel)> GetProjectTemplatesVersionAsync(ParseResult parseResult, CancellationToken cancellationToken) { var channels = await _packagingService.GetChannelsAsync(cancellationToken); From e55b10f1f4e30f32945329a6f2a9dc565a37a22c Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Mon, 13 Oct 2025 17:56:02 +1100 Subject: [PATCH 5/5] Reduce duplication. --- src/Aspire.Cli/Commands/InitCommand.cs | 34 ++++---------------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/src/Aspire.Cli/Commands/InitCommand.cs b/src/Aspire.Cli/Commands/InitCommand.cs index a4900551bee..cf21112410d 100644 --- a/src/Aspire.Cli/Commands/InitCommand.cs +++ b/src/Aspire.Cli/Commands/InitCommand.cs @@ -252,7 +252,11 @@ ServiceDefaults project contains helper code to make it easier // Create or update NuGet.config for explicit channels in the solution directory // This matches the behavior of 'aspire new' when creating in-place - await PromptToCreateOrUpdateNuGetConfigAsync(selectedTemplateDetails.Channel, ExecutionContext.WorkingDirectory, cancellationToken); + var nugetConfigPrompter = new NuGetConfigPrompter(InteractionService); + await nugetConfigPrompter.PromptToCreateOrUpdateAsync( + ExecutionContext.WorkingDirectory, + selectedTemplateDetails.Channel, + cancellationToken); // Create a temporary directory for the template output var tempProjectDir = Path.Combine(Path.GetTempPath(), $"aspire-init-{Guid.NewGuid()}"); @@ -601,34 +605,6 @@ private static bool IsSupportedTfm(string tfm) }; } - /// - /// Prompts to create or update a NuGet.config for explicit channels in the target directory. - /// This ensures consistent behavior with the DotNetTemplateFactory approach. - /// - /// The package channel. - /// The target directory where NuGet.config should be created or updated. - /// A cancellation token. - private async Task PromptToCreateOrUpdateNuGetConfigAsync(PackageChannel channel, DirectoryInfo targetDirectory, CancellationToken cancellationToken) - { - if (channel.Type is not PackageChannelType.Explicit) - { - return; - } - - var mappings = channel.Mappings; - if (mappings is null || mappings.Length == 0) - { - return; - } - - var nugetConfigPrompter = new NuGetConfigPrompter(InteractionService); - - // For solution-based init, we're creating projects in the solution directory - // This is equivalent to "in-place creation" in DotNetTemplateFactory terms - // We should prompt the user before creating/updating the NuGet.config - await nugetConfigPrompter.PromptToCreateOrUpdateAsync(targetDirectory, channel, cancellationToken); - } - private async Task<(NuGetPackage Package, PackageChannel Channel)> GetProjectTemplatesVersionAsync(ParseResult parseResult, CancellationToken cancellationToken) { var channels = await _packagingService.GetChannelsAsync(cancellationToken);