@@ -21,6 +21,7 @@ import (
2121 "time"
2222
2323 "github.com/slackapi/slack-cli/internal/api"
24+ "github.com/slackapi/slack-cli/internal/experiment"
2425 "github.com/slackapi/slack-cli/internal/iostreams"
2526 "github.com/slackapi/slack-cli/internal/pkg/create"
2627 "github.com/slackapi/slack-cli/internal/shared"
@@ -30,12 +31,52 @@ import (
3031 "github.com/spf13/cobra"
3132)
3233
34+ // getSelectionOptions returns the app template options for a given category.
3335func getSelectionOptions (clients * shared.ClientFactory , categoryID string ) []promptObject {
36+ if clients .Config .WithExperimentOn (experiment .Templates ) {
37+ templatePromptObjects := map [string ]([]promptObject ){
38+ "slack-cli#getting-started" : {
39+ {
40+ Title : fmt .Sprintf ("Bolt for JavaScript %s" , style .Secondary ("Node.js" )),
41+ Repository : "slack-samples/bolt-js-starter-template" ,
42+ },
43+ {
44+ Title : fmt .Sprintf ("Bolt for Python %s" , style .Secondary ("Python" )),
45+ Repository : "slack-samples/bolt-python-starter-template" ,
46+ },
47+ },
48+ "slack-cli#ai-apps" : {
49+ {
50+ Title : fmt .Sprintf ("Support Agent %s" , style .Secondary ("Resolve IT support cases" )),
51+ Repository : "slack-cli#ai-apps/support-agent" ,
52+ },
53+ {
54+ Title : fmt .Sprintf ("Custom Agent %s" , style .Secondary ("Start from scratch" )),
55+ Repository : "slack-cli#ai-apps/custom-agent" ,
56+ },
57+ },
58+ "slack-cli#automation-apps" : {
59+ {
60+ Title : fmt .Sprintf ("Bolt for JavaScript %s" , style .Secondary ("Node.js" )),
61+ Repository : "slack-samples/bolt-js-custom-function-template" ,
62+ },
63+ {
64+ Title : fmt .Sprintf ("Bolt for Python %s" , style .Secondary ("Python" )),
65+ Repository : "slack-samples/bolt-python-custom-function-template" ,
66+ },
67+ {
68+ Title : fmt .Sprintf ("Deno Slack SDK %s" , style .Secondary ("Deno" )),
69+ Repository : "slack-samples/deno-starter-template" ,
70+ },
71+ },
72+ }
73+ return templatePromptObjects [categoryID ]
74+ }
75+
3476 if strings .TrimSpace (categoryID ) == "" {
3577 categoryID = "slack-cli#getting-started"
3678 }
3779
38- // App categories and templates
3980 templatePromptObjects := map [string ]([]promptObject ){
4081 "slack-cli#getting-started" : []promptObject {
4182 {
@@ -76,6 +117,42 @@ func getSelectionOptions(clients *shared.ClientFactory, categoryID string) []pro
76117 return templatePromptObjects [categoryID ]
77118}
78119
120+ // getFrameworkOptions returns the framework choices for a given template.
121+ func getFrameworkOptions (template string ) []promptObject {
122+ frameworkPromptObjects := map [string ][]promptObject {
123+ "slack-cli#ai-apps/support-agent" : {
124+ {
125+ Title : fmt .Sprintf ("Claude Agent SDK %s" , style .Secondary ("Bolt for Python" )),
126+ Repository : "slack-samples/bolt-python-support-agent" ,
127+ Subdir : "claude-agent-sdk" ,
128+ },
129+ {
130+ Title : fmt .Sprintf ("OpenAI Agents SDK %s" , style .Secondary ("Bolt for Python" )),
131+ Repository : "slack-samples/bolt-python-support-agent" ,
132+ Subdir : "openai-agents-sdk" ,
133+ },
134+ {
135+ Title : fmt .Sprintf ("Pydantic AI %s" , style .Secondary ("Bolt for Python" )),
136+ Repository : "slack-samples/bolt-python-support-agent" ,
137+ Subdir : "pydantic-ai" ,
138+ },
139+ },
140+ "slack-cli#ai-apps/custom-agent" : {
141+ {
142+ Title : fmt .Sprintf ("Bolt for JavaScript %s" , style .Secondary ("Node.js" )),
143+ Repository : "slack-samples/bolt-js-assistant-template" ,
144+ },
145+ {
146+ Title : fmt .Sprintf ("Bolt for Python %s" , style .Secondary ("Python" )),
147+ Repository : "slack-samples/bolt-python-assistant-template" ,
148+ },
149+ },
150+ }
151+ return frameworkPromptObjects [template ]
152+ }
153+
154+ // getSelectionOptionsForCategory returns the top-level category options for
155+ // the create command template selection.
79156func getSelectionOptionsForCategory (clients * shared.ClientFactory ) []promptObject {
80157 return []promptObject {
81158 {
@@ -101,11 +178,16 @@ func getSelectionOptionsForCategory(clients *shared.ClientFactory) []promptObjec
101178func promptTemplateSelection (cmd * cobra.Command , clients * shared.ClientFactory , categoryShortcut string ) (create.Template , error ) {
102179 ctx := cmd .Context ()
103180 var categoryID string
104- var selectedTemplate string
105181
106182 // Check if a category shortcut was provided
107- if categoryShortcut == "agent" {
108- categoryID = "slack-cli#ai-apps"
183+ if categoryShortcut != "" {
184+ switch categoryShortcut {
185+ case "agent" :
186+ categoryID = "slack-cli#ai-apps"
187+ default :
188+ return create.Template {}, slackerror .New (slackerror .ErrInvalidArgs ).
189+ WithMessage ("The %s category was not found" , categoryShortcut )
190+ }
109191 } else {
110192 // Prompt for the category
111193 promptForCategory := "Select an app:"
@@ -128,73 +210,96 @@ func promptTemplateSelection(cmd *cobra.Command, clients *shared.ClientFactory,
128210 if err != nil {
129211 return create.Template {}, slackerror .ToSlackError (err )
130212 } else if selection .Flag {
131- selectedTemplate = selection .Option
213+ template , err := create .ResolveTemplateURL (selection .Option )
214+ if err != nil {
215+ return create.Template {}, err
216+ }
217+ confirm , err := confirmExternalTemplateSelection (cmd , clients , template )
218+ if err != nil {
219+ return create.Template {}, slackerror .ToSlackError (err )
220+ } else if ! confirm {
221+ return create.Template {}, slackerror .New (slackerror .ErrUntrustedSource )
222+ }
223+ return template , nil
132224 } else if selection .Prompt {
133225 categoryID = optionsForCategory [selection .Index ].Repository
134226 }
135227
136- // Set template to view more samples, so the sample prompt is triggered
137228 if categoryID == viewMoreSamples {
138- selectedTemplate = viewMoreSamples
229+ sampler := api .NewHTTPClient (api.HTTPClientOptions {
230+ TotalTimeOut : 60 * time .Second ,
231+ })
232+ samples , err := create .GetSampleRepos (sampler )
233+ if err != nil {
234+ return create.Template {}, err
235+ }
236+ selectedSample , err := promptSampleSelection (ctx , clients , samples )
237+ if err != nil {
238+ return create.Template {}, err
239+ }
240+ return create .ResolveTemplateURL (selectedSample )
139241 }
140242 }
141243
142- // Prompt for the template
143- if selectedTemplate == "" {
144- prompt := "Select a language:"
145- options := getSelectionOptions ( clients , categoryID )
146- titles := make ([] string , len ( options ))
147- for i , m := range options {
148- titles [ i ] = m . Title
244+ // Prompt for the example template
245+ prompt := "Select a language:"
246+ if clients . Config . WithExperimentOn ( experiment . Templates ) {
247+ if categoryID == "slack-cli#ai-apps" {
248+ prompt = "Select a template:"
249+ } else {
250+ prompt = "Select a language:"
149251 }
150- template := getSelectionTemplate (clients )
151-
152- // Print a trace with info about the template title options provided by CLI
153- clients .IO .PrintTrace (ctx , slacktrace .CreateTemplateOptions , strings .Join (titles , ", " ))
252+ }
253+ options := getSelectionOptions (clients , categoryID )
254+ titles := make ([]string , len (options ))
255+ for i , m := range options {
256+ titles [i ] = m .Title
257+ }
258+ clients .IO .PrintTrace (ctx , slacktrace .CreateTemplateOptions , strings .Join (titles , ", " ))
154259
155- // Prompt to choose a template
156- selection , err := clients . IO . SelectPrompt ( ctx , prompt , titles , iostreams. SelectPromptConfig {
157- Flag : clients . Config . Flags . Lookup ( "template" ),
158- Required : true ,
159- Template : template ,
160- })
161- if err != nil {
162- return create. Template {}, slackerror . ToSlackError ( err )
163- } else if selection . Flag {
164- selectedTemplate = selection .Option
165- } else if selection . Prompt {
166- selectedTemplate = options [selection .Index ].Repository
167- }
260+ selection , err := clients . IO . SelectPrompt ( ctx , prompt , titles , iostreams. SelectPromptConfig {
261+ Description : func ( value string , index int ) string {
262+ return options [ index ]. Description
263+ } ,
264+ Required : true ,
265+ Template : getSelectionTemplate ( clients ),
266+ })
267+ if err != nil {
268+ return create. Template {}, err
269+ } else if selection .Flag {
270+ return create. Template {}, slackerror . New ( slackerror . ErrPrompt )
271+ } else if selection . Prompt && ! strings . HasPrefix ( options [selection .Index ].Repository , "slack-cli#" ) {
272+ return create . ResolveTemplateURL ( options [ selection . Index ]. Repository )
168273 }
274+ template := options [selection .Index ].Repository
169275
170- // Ensure user is okay to proceed if template source is from a non-trusted source
171- switch selectedTemplate {
172- case viewMoreSamples :
173- sampler := api .NewHTTPClient (api.HTTPClientOptions {
174- TotalTimeOut : 60 * time .Second ,
175- })
176- samples , err := create .GetSampleRepos (sampler )
177- if err != nil {
178- return create.Template {}, err
179- }
180- selectedSample , err := promptSampleSelection (ctx , clients , samples )
181- if err != nil {
182- return create.Template {}, err
183- }
184- return create .ResolveTemplateURL (selectedSample )
185- default :
186- template , err := create .ResolveTemplateURL (selectedTemplate )
187- if err != nil {
188- return create.Template {}, err
189- }
190- confirm , err := confirmExternalTemplateSelection (cmd , clients , template )
191- if err != nil {
192- return create.Template {}, slackerror .ToSlackError (err )
193- } else if ! confirm {
194- return create.Template {}, slackerror .New (slackerror .ErrUntrustedSource )
195- }
196- return template , nil
276+ // Prompt for the example framework
277+ examples := getFrameworkOptions (template )
278+ choices := make ([]string , len (examples ))
279+ for i , opt := range examples {
280+ choices [i ] = opt .Title
281+ }
282+ choice , err := clients .IO .SelectPrompt (ctx , "Select a language:" , choices , iostreams.SelectPromptConfig {
283+ Description : func (value string , index int ) string {
284+ return examples [index ].Description
285+ },
286+ Required : true ,
287+ Template : getSelectionTemplate (clients ),
288+ })
289+ if err != nil {
290+ return create.Template {}, err
291+ } else if choice .Flag {
292+ return create.Template {}, slackerror .New (slackerror .ErrPrompt )
293+ }
294+ example := examples [choice .Index ]
295+ resolved , err := create .ResolveTemplateURL (example .Repository )
296+ if err != nil {
297+ return create.Template {}, err
298+ }
299+ if example .Subdir != "" {
300+ resolved .SetSubdir (example .Subdir )
197301 }
302+ return resolved , nil
198303}
199304
200305// confirmExternalTemplateSelection prompts the user to confirm that they want to create an app from
@@ -243,10 +348,22 @@ func listTemplates(ctx context.Context, clients *shared.ClientFactory, categoryS
243348 }
244349
245350 var categories []categoryInfo
246- if categoryShortcut == "agent" {
351+ if categoryShortcut == "agent" && clients .Config .WithExperimentOn (experiment .Templates ) {
352+ categories = []categoryInfo {
353+ {id : "slack-cli#ai-apps/support-agent" , name : "Support agent" },
354+ {id : "slack-cli#ai-apps/custom-agent" , name : "Custom agent" },
355+ }
356+ } else if categoryShortcut == "agent" {
247357 categories = []categoryInfo {
248358 {id : "slack-cli#ai-apps" , name : "AI Agent apps" },
249359 }
360+ } else if clients .Config .WithExperimentOn (experiment .Templates ) {
361+ categories = []categoryInfo {
362+ {id : "slack-cli#getting-started" , name : "Getting started" },
363+ {id : "slack-cli#ai-apps/support-agent" , name : "Support agent" },
364+ {id : "slack-cli#ai-apps/custom-agent" , name : "Custom agent" },
365+ {id : "slack-cli#automation-apps" , name : "Automation apps" },
366+ }
250367 } else {
251368 categories = []categoryInfo {
252369 {id : "slack-cli#getting-started" , name : "Getting started" },
@@ -256,10 +373,19 @@ func listTemplates(ctx context.Context, clients *shared.ClientFactory, categoryS
256373 }
257374
258375 for _ , category := range categories {
259- templates := getSelectionOptions (clients , category .id )
260- secondary := make ([]string , len (templates ))
261- for i , tmpl := range templates {
262- secondary [i ] = tmpl .Repository
376+ var secondary []string
377+ if frameworks := getFrameworkOptions (category .id ); len (frameworks ) > 0 {
378+ for _ , tmpl := range frameworks {
379+ repo := tmpl .Repository
380+ if tmpl .Subdir != "" {
381+ repo = fmt .Sprintf ("%s --subdir %s" , repo , tmpl .Subdir )
382+ }
383+ secondary = append (secondary , repo )
384+ }
385+ } else {
386+ for _ , tmpl := range getSelectionOptions (clients , category .id ) {
387+ secondary = append (secondary , tmpl .Repository )
388+ }
263389 }
264390 clients .IO .PrintInfo (ctx , false , "%s" , style .Sectionf (style.TextSection {
265391 Emoji : "house_buildings" ,
0 commit comments