diff --git a/cli/azd/pkg/templates/awesome_source.go b/cli/azd/pkg/templates/awesome_source.go index 70b9251d9b6..594542fa467 100644 --- a/cli/azd/pkg/templates/awesome_source.go +++ b/cli/azd/pkg/templates/awesome_source.go @@ -22,7 +22,11 @@ type awesomeAzdTemplate struct { Source string `json:"source"` Tags []string `json:"tags"` AzureServiceTags []string `json:"azureServices"` - LanguageTags []string `json:"languages"` + + // A list of languages supported by the template + // + // As of November 2025, known values include: bicep, php, javascript, dotnetCsharp, typescript, python, nodejs, java + LanguageTags []string `json:"languages"` } // newAwesomeAzdTemplateSource creates a new template source from the awesome-azd templates json file. diff --git a/cli/azd/pkg/templates/template.go b/cli/azd/pkg/templates/template.go index 655e6889feb..255a5555797 100644 --- a/cli/azd/pkg/templates/template.go +++ b/cli/azd/pkg/templates/template.go @@ -5,6 +5,7 @@ package templates import ( "io" + "slices" "strings" "text/tabwriter" @@ -73,3 +74,42 @@ func (t *Template) Display(writer io.Writer) error { return tabs.Flush() } + +// DisplayLanguages returns a list of languages suitable for display from the associated template Tags. +func (t *Template) DisplayLanguages() []string { + languages := make([]string, 0, len(t.Tags)) + for _, lang := range t.Tags { + switch lang { + case "dotnetCsharp": + languages = append(languages, "csharp") + case "nodejs": + languages = append(languages, "nodejs") + case "javascript": + if !slices.Contains(t.Tags, "nodejs") && !slices.Contains(t.Tags, "ts") { + languages = append(languages, "js") + } + case "typescript": + if !slices.Contains(t.Tags, "nodejs") { + languages = append(languages, "ts") + } + case "python", "java": + languages = append(languages, lang) + } + } + + return languages +} + +// CanonicalPath returns a canonicalized path for the template repository +func (t *Template) CanonicalPath() string { + path := t.RepositoryPath + if after, ok := strings.CutPrefix(path, "https://github.com/"); ok { + path = after + } + + if after, ok := strings.CutPrefix(strings.ToLower(path), "azure-samples/"); ok { + path = after + } + + return path +} diff --git a/cli/azd/pkg/templates/template_manager.go b/cli/azd/pkg/templates/template_manager.go index d7596b232cd..be21d870ec2 100644 --- a/cli/azd/pkg/templates/template_manager.go +++ b/cli/azd/pkg/templates/template_manager.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/azure/azure-dev/cli/azd/pkg/input" + "github.com/azure/azure-dev/cli/azd/pkg/output" ) var ( @@ -196,47 +197,41 @@ func PromptTemplate( return Template{}, fmt.Errorf("prompting for template: %w", err) } - templateChoices := []*Template{} - duplicateNames := []string{} - - // Check for duplicate template names - for _, template := range templates { - hasDuplicateName := slices.ContainsFunc(templateChoices, func(t *Template) bool { - return t.Name == template.Name - }) + slices.SortFunc(templates, func(a, b *Template) int { + return strings.Compare(a.Name, b.Name) + }) - if hasDuplicateName { - duplicateNames = append(duplicateNames, template.Name) + choices := make([]string, 0, len(templates)+1) + for i, template := range templates { + choice := template.Name + if len(choice) > 70 { + choice = choice[0:67] + "..." } - templateChoices = append(templateChoices, template) - } - - templateNames := make([]string, 0, len(templates)+1) - templateDetails := make([]string, 0, len(templates)+1) - - for _, template := range templates { - templateChoice := template.Name - - // Disambiguate duplicate template names with source identifier - if slices.Contains(duplicateNames, template.Name) { - templateChoice += fmt.Sprintf(" (%s)", template.Source) + for j, otherTemplate := range templates { + if i != j && strings.EqualFold(template.Name, otherTemplate.Name) { + // Disambiguate duplicate template names with source identifier + choice += fmt.Sprintf(" (%s)", template.Source) + } } - templateDetails = append(templateDetails, template.RepositoryPath) + languages := template.DisplayLanguages() + path := template.CanonicalPath() - if slices.Contains(templateNames, templateChoice) { - duplicateNames = append(duplicateNames, templateChoice) - } + choices = append(choices, fmt.Sprintf("%s\t%s\t[%s]", + choice, + path, + strings.Join(languages, ","))) + } - templateNames = append(templateNames, templateChoice) + choices, err = output.TabAlign(choices, 3) + if err != nil { + return Template{}, fmt.Errorf("aligning choices: %w", err) } selected, err := console.Select(ctx, input.ConsoleOptions{ - Message: message, - Options: templateNames, - OptionDetails: templateDetails, - DefaultValue: templateNames[0], + Message: message, + Options: choices, }) // separate this prompt from the next log