22// The .NET Foundation licenses this file to you under the MIT license.
33
44using System . CommandLine ;
5- using System . CommandLine . Parsing ;
65using System . Diagnostics ;
76using Aspire . Cli . Utils ;
7+ using Semver ;
88using Spectre . Console ;
99
1010namespace Aspire . Cli . Commands ;
1111
1212internal sealed class NewCommand : BaseCommand
1313{
14- private readonly ActivitySource _activitySource = new ActivitySource ( "Aspire.Cli" ) ;
14+ private readonly ActivitySource _activitySource = new ActivitySource ( nameof ( NewCommand ) ) ;
1515 private readonly DotNetCliRunner _runner ;
16+ private readonly INuGetPackageCache _nuGetPackageCache ;
1617
17- public NewCommand ( DotNetCliRunner runner ) : base ( "new" , "Create a new Aspire sample project." )
18+ public NewCommand ( DotNetCliRunner runner , INuGetPackageCache nuGetPackageCache ) : base ( "new" , "Create a new Aspire sample project." )
1819 {
1920 ArgumentNullException . ThrowIfNull ( runner , nameof ( runner ) ) ;
21+ ArgumentNullException . ThrowIfNull ( nuGetPackageCache , nameof ( nuGetPackageCache ) ) ;
2022 _runner = runner ;
23+ _nuGetPackageCache = nuGetPackageCache ;
2124
2225 var templateArgument = new Argument < string > ( "template" ) ;
23- templateArgument . Validators . Add ( ValidateProjectTemplate ) ;
2426 templateArgument . Arity = ArgumentArity . ZeroOrOne ;
2527 Arguments . Add ( templateArgument ) ;
2628
@@ -29,9 +31,6 @@ internal sealed class NewCommand : BaseCommand
2931
3032 var outputOption = new Option < string ? > ( "--output" , "-o" ) ;
3133 Options . Add ( outputOption ) ;
32-
33- var prereleaseOption = new Option < bool > ( "--prerelease" ) ;
34- Options . Add ( prereleaseOption ) ;
3534
3635 var sourceOption = new Option < string ? > ( "--source" , "-s" ) ;
3736 Options . Add ( sourceOption ) ;
@@ -40,7 +39,7 @@ internal sealed class NewCommand : BaseCommand
4039 Options . Add ( templateVersionOption ) ;
4140 }
4241
43- private static void ValidateProjectTemplate ( ArgumentResult result )
42+ private static async Task < ( string TemplateName , string TemplateDescription , string ? PathAppendage ) > GetProjectTemplateAsync ( ParseResult parseResult , CancellationToken cancellationToken )
4443 {
4544 // TODO: We need to integrate with the template engine to interrogate
4645 // the list of available templates. For now we will just hard-code
@@ -49,55 +48,91 @@ private static void ValidateProjectTemplate(ArgumentResult result)
4948 // Once we integrate with template engine we will also be able to
5049 // interrogate the various options and add them. For now we will
5150 // keep it simple.
52- string [ ] validTemplates = [
53- "aspire-starter" ,
54- "aspire" ,
55- "aspire-apphost" ,
56- "aspire-servicedefaults" ,
57- "aspire-mstest" ,
58- "aspire-nunit" ,
59- "aspire-xunit"
51+ ( string TemplateName , string TemplateDescription , string ? PathAppendage ) [ ] validTemplates = [
52+ ( "aspire-starter" , "Aspire Starter App" , "src" ) ,
53+ ( "aspire" , "Aspire Empty App" , "src" ) ,
54+ ( "aspire-apphost" , "Aspire App Host" , null ) ,
55+ ( "aspire-servicedefaults" , "Aspire Service Defaults" , null ) ,
56+ ( "aspire-mstest" , "Aspire Test Project (MSTest)" , null ) ,
57+ ( "aspire-nunit" , "Aspire Test Project (NUnit)" , null ) ,
58+ ( "aspire-xunit" , "Aspire Test Project (xUnit)" , null )
6059 ] ;
6160
62- var value = result . GetValueOrDefault < string > ( ) ;
63-
64- if ( value is null )
61+ if ( parseResult . GetValue < string ? > ( "template" ) is { } templateName && validTemplates . SingleOrDefault ( t => t . TemplateName == templateName ) is { } template )
6562 {
66- // This is OK, for now we will use the default
67- // template of aspire-starter, but we might
68- // be able to do more intelligent selection in the
69- // future based on what is already in the working directory.
70- return ;
63+ return template ;
7164 }
72-
73- if ( value is { } templateName && ! validTemplates . Contains ( templateName ) )
65+ else
7466 {
75- result . AddError ( $ "The specified template '{ templateName } ' is not valid. Valid templates are [{ string . Join ( ", " , validTemplates ) } ].") ;
76- return ;
67+ return await PromptUtils . PromptForSelectionAsync (
68+ "Select a project template:" ,
69+ validTemplates ,
70+ t => $ "{ t . TemplateName } ({ t . TemplateDescription } )",
71+ cancellationToken
72+ ) ;
7773 }
7874 }
7975
80- protected override async Task < int > ExecuteAsync ( ParseResult parseResult , CancellationToken cancellationToken )
76+ private static async Task < string > GetProjectNameAsync ( ParseResult parseResult , CancellationToken cancellationToken )
8177 {
82- using var activity = _activitySource . StartActivity ( $ "{ nameof ( ExecuteAsync ) } ", ActivityKind . Internal ) ;
78+ if ( parseResult . GetValue < string > ( "--name" ) is not { } name )
79+ {
80+ var defaultName = new DirectoryInfo ( Environment . CurrentDirectory ) . Name ;
81+ name = await PromptUtils . PromptForStringAsync ( "Enter the project name:" ,
82+ defaultValue : defaultName ,
83+ cancellationToken : cancellationToken ) ;
84+ }
8385
84- var templateVersion = parseResult . GetValue < string > ( "--version" ) ;
85- var prerelease = parseResult . GetValue < bool > ( "--prerelease" ) ;
86+ return name ;
87+ }
8688
87- if ( templateVersion is not null && prerelease )
89+ private static async Task < string > GetOutputPathAsync ( ParseResult parseResult , string ? pathAppendage , CancellationToken cancellationToken )
90+ {
91+ if ( parseResult . GetValue < string > ( "--output" ) is not { } outputPath )
8892 {
89- AnsiConsole . MarkupLine ( "[red bold]:thumbs_down: The --version and --prerelease options are mutually exclusive.[/]" ) ;
90- return ExitCodeConstants . FailedToCreateNewProject ;
93+ outputPath = await PromptUtils . PromptForStringAsync (
94+ "Enter the output path:" ,
95+ defaultValue : Path . Combine ( Environment . CurrentDirectory , pathAppendage ?? string . Empty ) ,
96+ cancellationToken : cancellationToken
97+ ) ;
9198 }
92- else if ( prerelease )
99+
100+ return Path . GetFullPath ( outputPath ) ;
101+ }
102+
103+ private static async Task < string > GetProjectTemplatesVersionAsync ( ParseResult parseResult , CancellationToken cancellationToken )
104+ {
105+ if ( parseResult . GetValue < string > ( "--version" ) is { } version )
93106 {
94- templateVersion = "*-*" ;
107+ return version ;
95108 }
96- else if ( templateVersion is null )
109+ else
97110 {
98- templateVersion = VersionHelper . GetDefaultTemplateVersion ( ) ;
111+ version = await PromptUtils . PromptForStringAsync (
112+ "Project templates version:" ,
113+ defaultValue : VersionHelper . GetDefaultTemplateVersion ( ) ,
114+ validator : ( string value ) => {
115+ if ( SemVersion . TryParse ( value , out var parsedVersion ) )
116+ {
117+ return ValidationResult . Success ( ) ;
118+ }
119+
120+ return ValidationResult . Error ( "Invalid version format. Please enter a valid version." ) ;
121+ } ,
122+ cancellationToken ) ;
123+
124+ return version ;
99125 }
126+ }
100127
128+ protected override async Task < int > ExecuteAsync ( ParseResult parseResult , CancellationToken cancellationToken )
129+ {
130+ using var activity = _activitySource . StartActivity ( ) ;
131+
132+ var template = await GetProjectTemplateAsync ( parseResult , cancellationToken ) ;
133+ var name = await GetProjectNameAsync ( parseResult , cancellationToken ) ;
134+ var outputPath = await GetOutputPathAsync ( parseResult , template . PathAppendage , cancellationToken ) ;
135+ var version = await GetProjectTemplatesVersionAsync ( parseResult , cancellationToken ) ;
101136 var source = parseResult . GetValue < string ? > ( "--source" ) ;
102137
103138 var templateInstallResult = await AnsiConsole . Status ( )
@@ -106,7 +141,7 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
106141 . StartAsync (
107142 ":ice: Getting latest templates..." ,
108143 async context => {
109- return await _runner . InstallTemplateAsync ( "Aspire.ProjectTemplates" , templateVersion ! , source , true , cancellationToken ) ;
144+ return await _runner . InstallTemplateAsync ( "Aspire.ProjectTemplates" , version , source , true , cancellationToken ) ;
110145 } ) ;
111146
112147 if ( templateInstallResult . ExitCode != 0 )
@@ -117,35 +152,18 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
117152
118153 AnsiConsole . MarkupLine ( $ ":package: Using project templates version: { templateInstallResult . TemplateVersion } ") ;
119154
120- var templateName = parseResult . GetValue < string > ( "template" ) ?? "aspire-starter" ;
121-
122- if ( parseResult . GetValue < string > ( "--output" ) is not { } outputPath )
123- {
124- outputPath = Environment . CurrentDirectory ;
125- }
126- else
127- {
128- outputPath = Path . GetFullPath ( outputPath ) ;
129- }
130-
131- if ( parseResult . GetValue < string > ( "--name" ) is not { } name )
132- {
133- var outputPathDirectoryInfo = new DirectoryInfo ( outputPath ) ;
134- name = outputPathDirectoryInfo . Name ;
135- }
136-
137155 int newProjectExitCode = await AnsiConsole . Status ( )
138156 . Spinner ( Spinner . Known . Dots3 )
139157 . SpinnerStyle ( Style . Parse ( "purple" ) )
140158 . StartAsync (
141159 ":rocket: Creating new Aspire project..." ,
142160 async context => {
143161 return await _runner . NewProjectAsync (
144- templateName ,
145- name ,
146- outputPath ,
147- cancellationToken ) ;
148- } ) ;
162+ template . TemplateName ,
163+ name ,
164+ outputPath ,
165+ cancellationToken ) ;
166+ } ) ;
149167
150168 if ( newProjectExitCode != 0 )
151169 {
0 commit comments