diff --git a/cmd/project/create_template.go b/cmd/project/create_template.go index 98d9f9d0..e4e935dc 100644 --- a/cmd/project/create_template.go +++ b/cmd/project/create_template.go @@ -71,38 +71,96 @@ func getSelectionOptions(categoryID string) []promptObject { return templatePromptObjects[categoryID] } -// getFrameworkOptions returns the framework choices for a given template. +// getFrameworkOptions returns the framework choices for a given AI app template. func getFrameworkOptions(template string) []promptObject { frameworkPromptObjects := map[string][]promptObject{ "slack-cli#ai-apps/support-agent": { { - Title: fmt.Sprintf("Claude Agent SDK %s", style.Secondary("Bolt for Python")), + Title: fmt.Sprintf("Bolt for JavaScript %s", style.Secondary("Node.js")), + Repository: "slack-cli#ai-apps/support-agent/bolt-js", + }, + { + Title: fmt.Sprintf("Bolt for Python %s", style.Secondary("Python")), + Repository: "slack-cli#ai-apps/support-agent/bolt-python", + }, + }, + "slack-cli#ai-apps/starter-agent": { + { + Title: fmt.Sprintf("Bolt for JavaScript %s", style.Secondary("Node.js")), + Repository: "slack-cli#ai-apps/starter-agent/bolt-js", + }, + { + Title: fmt.Sprintf("Bolt for Python %s", style.Secondary("Python")), + Repository: "slack-cli#ai-apps/starter-agent/bolt-python", + }, + }, + } + return frameworkPromptObjects[template] +} + +// getAdapterOptions returns the AI adapter choices for a given template and framework. +func getAdapterOptions(framework string) []promptObject { + adapterPromptObjects := map[string][]promptObject{ + "slack-cli#ai-apps/support-agent/bolt-js": { + { + Title: "Claude Agent SDK", + Repository: "slack-samples/bolt-js-support-agent", + Subdir: "claude-agent-sdk", + }, + { + Title: "OpenAI Agents SDK", + Repository: "slack-samples/bolt-js-support-agent", + Subdir: "openai-agents-sdk", + }, + }, + "slack-cli#ai-apps/support-agent/bolt-python": { + { + Title: "Claude Agent SDK", Repository: "slack-samples/bolt-python-support-agent", Subdir: "claude-agent-sdk", }, { - Title: fmt.Sprintf("OpenAI Agents SDK %s", style.Secondary("Bolt for Python")), + Title: "OpenAI Agents SDK", Repository: "slack-samples/bolt-python-support-agent", Subdir: "openai-agents-sdk", }, { - Title: fmt.Sprintf("Pydantic AI %s", style.Secondary("Bolt for Python")), + Title: "Pydantic AI", Repository: "slack-samples/bolt-python-support-agent", Subdir: "pydantic-ai", }, }, - "slack-cli#ai-apps/starter-agent": { + "slack-cli#ai-apps/starter-agent/bolt-js": { { - Title: fmt.Sprintf("Bolt for JavaScript %s", style.Secondary("Node.js")), + Title: "Claude Agent SDK", Repository: "slack-samples/bolt-js-starter-agent", + Subdir: "claude-agent-sdk", }, { - Title: fmt.Sprintf("Bolt for Python %s", style.Secondary("Python")), + Title: "OpenAI Agents SDK", + Repository: "slack-samples/bolt-js-starter-agent", + Subdir: "openai-agents-sdk", + }, + }, + "slack-cli#ai-apps/starter-agent/bolt-python": { + { + Title: "Claude Agent SDK", + Repository: "slack-samples/bolt-python-starter-agent", + Subdir: "claude-agent-sdk", + }, + { + Title: "OpenAI Agents SDK", + Repository: "slack-samples/bolt-python-starter-agent", + Subdir: "openai-agents-sdk", + }, + { + Title: "Pydantic AI", Repository: "slack-samples/bolt-python-starter-agent", + Subdir: "pydantic-ai", }, }, } - return frameworkPromptObjects[template] + return adapterPromptObjects[framework] } // getSelectionOptionsForCategory returns the top-level category options for @@ -223,31 +281,63 @@ func promptTemplateSelection(cmd *cobra.Command, clients *shared.ClientFactory, } template := options[selection.Index].Repository - // Prompt for the example framework - examples := getFrameworkOptions(template) - choices := make([]string, len(examples)) - for i, opt := range examples { - choices[i] = opt.Title + // Prompt for the framework + frameworks := getFrameworkOptions(template) + frameworkChoices := make([]string, len(frameworks)) + for i, opt := range frameworks { + frameworkChoices[i] = opt.Title } - choice, err := clients.IO.SelectPrompt(ctx, "Select a framework:", choices, iostreams.SelectPromptConfig{ + frameworkSelection, err := clients.IO.SelectPrompt(ctx, "Select a framework:", frameworkChoices, iostreams.SelectPromptConfig{ Description: func(value string, index int) string { - return examples[index].Description + return frameworks[index].Description }, Required: true, Template: getSelectionTemplate(clients), }) if err != nil { return create.Template{}, err - } else if choice.Flag { + } else if frameworkSelection.Flag { return create.Template{}, slackerror.New(slackerror.ErrPrompt) } - example := examples[choice.Index] - resolved, err := create.ResolveTemplateURL(example.Repository) + framework := frameworks[frameworkSelection.Index] + + // Check if there are adapter options for this framework + adapters := getAdapterOptions(framework.Repository) + if len(adapters) > 0 { + adapterChoices := make([]string, len(adapters)) + for i, opt := range adapters { + adapterChoices[i] = opt.Title + } + adapterSelection, err := clients.IO.SelectPrompt(ctx, "Select an adapter:", adapterChoices, iostreams.SelectPromptConfig{ + Description: func(value string, index int) string { + return adapters[index].Description + }, + Required: true, + Template: getSelectionTemplate(clients), + }) + if err != nil { + return create.Template{}, err + } else if adapterSelection.Flag { + return create.Template{}, slackerror.New(slackerror.ErrPrompt) + } + adapter := adapters[adapterSelection.Index] + resolved, err := create.ResolveTemplateURL(adapter.Repository) + if err != nil { + return create.Template{}, err + } + if adapter.Subdir != "" { + resolved.SetSubdir(adapter.Subdir) + } + return resolved, nil + } + + // No adapter options - resolve the framework directly + resolved, err := create.ResolveTemplateURL(framework.Repository) if err != nil { return create.Template{}, err } - if example.Subdir != "" { - resolved.SetSubdir(example.Subdir) + if framework.Subdir != "" { + resolved.SetSubdir(framework.Subdir) } return resolved, nil } @@ -315,12 +405,18 @@ func listTemplates(ctx context.Context, clients *shared.ClientFactory, categoryS for _, category := range categories { var secondary []string if frameworks := getFrameworkOptions(category.id); len(frameworks) > 0 { - for _, tmpl := range frameworks { - repo := tmpl.Repository - if tmpl.Subdir != "" { - repo = fmt.Sprintf("%s --subdir %s", repo, tmpl.Subdir) + for _, fw := range frameworks { + if adapters := getAdapterOptions(fw.Repository); len(adapters) > 0 { + for _, adapter := range adapters { + repo := adapter.Repository + if adapter.Subdir != "" { + repo = fmt.Sprintf("%s --subdir %s", repo, adapter.Subdir) + } + secondary = append(secondary, repo) + } + } else { + secondary = append(secondary, fw.Repository) } - secondary = append(secondary, repo) } } else { for _, tmpl := range getSelectionOptions(category.id) { diff --git a/cmd/project/create_test.go b/cmd/project/create_test.go index dce88f48..5a331b35 100644 --- a/cmd/project/create_test.go +++ b/cmd/project/create_test.go @@ -133,7 +133,15 @@ func TestCreateCommand(t *testing.T) { Return( iostreams.SelectPromptResponse{ Prompt: true, - Index: 0, // Select Node.js template + Index: 0, // Select Node.js + }, + nil, + ) + cm.IO.On("SelectPrompt", mock.Anything, "Select an adapter:", mock.Anything, mock.Anything). + Return( + iostreams.SelectPromptResponse{ + Prompt: true, + Index: 0, // Select Claude Agent SDK }, nil, ) @@ -146,9 +154,11 @@ func TestCreateCommand(t *testing.T) { ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { template, err := create.ResolveTemplateURL("slack-samples/bolt-js-starter-agent") require.NoError(t, err) + template.SetSubdir("claude-agent-sdk") expected := create.CreateArgs{ AppName: "my-agent", Template: template, + Subdir: "claude-agent-sdk", } createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, expected) // Verify that category prompt was NOT called @@ -172,7 +182,15 @@ func TestCreateCommand(t *testing.T) { Return( iostreams.SelectPromptResponse{ Prompt: true, - Index: 1, // Select Python template + Index: 1, // Select Python + }, + nil, + ) + cm.IO.On("SelectPrompt", mock.Anything, "Select an adapter:", mock.Anything, mock.Anything). + Return( + iostreams.SelectPromptResponse{ + Prompt: true, + Index: 0, // Select Claude Agent SDK }, nil, ) @@ -183,9 +201,11 @@ func TestCreateCommand(t *testing.T) { ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { template, err := create.ResolveTemplateURL("slack-samples/bolt-python-starter-agent") require.NoError(t, err) + template.SetSubdir("claude-agent-sdk") expected := create.CreateArgs{ AppName: "my-agent-app", Template: template, + Subdir: "claude-agent-sdk", } createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, expected) // Verify that category prompt was NOT called @@ -202,7 +222,9 @@ func TestCreateCommand(t *testing.T) { cm.IO.On("SelectPrompt", mock.Anything, "Select a template:", mock.Anything, mock.Anything). Return(iostreams.SelectPromptResponse{Prompt: true, Index: 0}, nil) cm.IO.On("SelectPrompt", mock.Anything, "Select a framework:", mock.Anything, mock.Anything). - Return(iostreams.SelectPromptResponse{Prompt: true, Index: 2}, nil) + Return(iostreams.SelectPromptResponse{Prompt: true, Index: 1}, nil) // Select Bolt for Python + cm.IO.On("SelectPrompt", mock.Anything, "Select an adapter:", mock.Anything, mock.Anything). + Return(iostreams.SelectPromptResponse{Prompt: true, Index: 2}, nil) // Select Pydantic AI createClientMock = new(CreateClientMock) createClientMock.On("Create", mock.Anything, mock.Anything, mock.Anything).Return("", nil) CreateFunc = createClientMock.Create @@ -312,6 +334,14 @@ func TestCreateCommand(t *testing.T) { }, nil, ) + cm.IO.On("SelectPrompt", mock.Anything, "Select an adapter:", mock.Anything, mock.Anything). + Return( + iostreams.SelectPromptResponse{ + Prompt: true, + Index: 0, // Select Claude Agent SDK + }, + nil, + ) createClientMock = new(CreateClientMock) createClientMock.On("Create", mock.Anything, mock.Anything, mock.Anything).Return("", nil) CreateFunc = createClientMock.Create @@ -319,9 +349,11 @@ func TestCreateCommand(t *testing.T) { ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { template, err := create.ResolveTemplateURL("slack-samples/bolt-js-starter-agent") require.NoError(t, err) + template.SetSubdir("claude-agent-sdk") expected := create.CreateArgs{ AppName: "my-custom-name", // --name flag overrides Template: template, + Subdir: "claude-agent-sdk", } createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, expected) // Verify that category prompt was NOT called (shortcut was triggered) @@ -383,6 +415,14 @@ func TestCreateCommand(t *testing.T) { }, nil, ) + cm.IO.On("SelectPrompt", mock.Anything, "Select an adapter:", mock.Anything, mock.Anything). + Return( + iostreams.SelectPromptResponse{ + Prompt: true, + Index: 0, // Select Claude Agent SDK + }, + nil, + ) createClientMock = new(CreateClientMock) createClientMock.On("Create", mock.Anything, mock.Anything, mock.Anything).Return("", nil) CreateFunc = createClientMock.Create @@ -390,9 +430,11 @@ func TestCreateCommand(t *testing.T) { ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { template, err := create.ResolveTemplateURL("slack-samples/bolt-js-starter-agent") require.NoError(t, err) + template.SetSubdir("claude-agent-sdk") expected := create.CreateArgs{ AppName: "my-name", // --name flag overrides "my-project" positional arg Template: template, + Subdir: "claude-agent-sdk", } createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, expected) // Verify that category prompt was NOT called (agent shortcut was triggered) @@ -601,12 +643,17 @@ func TestCreateCommand(t *testing.T) { "slack-samples/bolt-js-starter-template", "slack-samples/bolt-python-starter-template", "Support agent", + "slack-samples/bolt-js-support-agent --subdir claude-agent-sdk", + "slack-samples/bolt-js-support-agent --subdir openai-agents-sdk", "slack-samples/bolt-python-support-agent --subdir claude-agent-sdk", "slack-samples/bolt-python-support-agent --subdir openai-agents-sdk", "slack-samples/bolt-python-support-agent --subdir pydantic-ai", "Starter agent", - "slack-samples/bolt-js-starter-agent", - "slack-samples/bolt-python-starter-agent", + "slack-samples/bolt-js-starter-agent --subdir claude-agent-sdk", + "slack-samples/bolt-js-starter-agent --subdir openai-agents-sdk", + "slack-samples/bolt-python-starter-agent --subdir claude-agent-sdk", + "slack-samples/bolt-python-starter-agent --subdir openai-agents-sdk", + "slack-samples/bolt-python-starter-agent --subdir pydantic-ai", "Automation apps", "slack-samples/bolt-js-custom-function-template", "slack-samples/bolt-python-custom-function-template", @@ -624,12 +671,17 @@ func TestCreateCommand(t *testing.T) { }, ExpectedOutputs: []string{ "Support agent", + "slack-samples/bolt-js-support-agent --subdir claude-agent-sdk", + "slack-samples/bolt-js-support-agent --subdir openai-agents-sdk", "slack-samples/bolt-python-support-agent --subdir claude-agent-sdk", "slack-samples/bolt-python-support-agent --subdir openai-agents-sdk", "slack-samples/bolt-python-support-agent --subdir pydantic-ai", "Starter agent", - "slack-samples/bolt-js-starter-agent", - "slack-samples/bolt-python-starter-agent", + "slack-samples/bolt-js-starter-agent --subdir claude-agent-sdk", + "slack-samples/bolt-js-starter-agent --subdir openai-agents-sdk", + "slack-samples/bolt-python-starter-agent --subdir claude-agent-sdk", + "slack-samples/bolt-python-starter-agent --subdir openai-agents-sdk", + "slack-samples/bolt-python-starter-agent --subdir pydantic-ai", }, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { createClientMock.AssertNotCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything)