Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 121 additions & 25 deletions cmd/project/create_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: We originally brainstormed having the Secondary display the breadcrumb of the previous selection. I'm just curious why you decided to not include that?

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",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Why don't these have any Secondary text? To me, it feels a little off that these offer no hints or additional descriptions.

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
Expand Down Expand Up @@ -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{
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Why did you choose the term "adapter"? My concern is that "receiver" and "adapter" in Bolt Slack App development and these are not receivers or adapters.

I'd prefer a term such as "Select an agent framework:" or if we must keep it general then "Select a third-party framework:"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm i think adapter does make sense here but I also prefer agent framework or perhaps llm provider! 🤔

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
}
Expand Down Expand Up @@ -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) {
Expand Down
66 changes: 59 additions & 7 deletions cmd/project/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand All @@ -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
Expand All @@ -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,
)
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -312,16 +334,26 @@ 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
},
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)
Expand Down Expand Up @@ -383,16 +415,26 @@ 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
},
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)
Expand Down Expand Up @@ -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",
Expand All @@ -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)
Expand Down
Loading