Skip to content
Merged
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
32 changes: 27 additions & 5 deletions cmd/project/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@ func NewCreateCommand(clients *shared.ClientFactory) *cobra.Command {
Long: `Create a new Slack project on your local machine from an optional template`,
Example: style.ExampleCommandsf([]style.ExampleCommand{
{Command: "create my-project", Meaning: "Create a new project from a template"},
{Command: "create agent my-agent-app", Meaning: "Create a new AI Agent app"},
{Command: "create my-project -t slack-samples/deno-hello-world", Meaning: "Start a new project from a specific template"},
}),
Args: cobra.MaximumNArgs(1),
Args: cobra.MaximumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
clients.Config.SetFlags(cmd)
return runCreateCommand(clients, cmd, args)
Expand All @@ -80,14 +81,35 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []
// Set up event logger
log := newCreateLogger(clients, cmd)

// Get optional app name passed as an arg
// Get optional app name passed as an arg and check for category shortcuts
appNameArg := ""
if len(args) > 0 && args[0] != "samples" && args[0] != "create" {
appNameArg = args[0]
categoryShortcut := ""
templateFlagProvided := cmd.Flags().Changed("template")

if len(args) > 0 {
switch args[0] {
case "samples", "create":
// These are special commands, not app names
case "agent":
// Only treat as shortcut if --template flag is not provided
if !templateFlagProvided {
// Shortcut to AI apps category
categoryShortcut = "agent"
// Check if a second argument was provided as the app name
if len(args) > 1 {
appNameArg = args[1]
}
} else {
// When --template is provided, "agent" is the app name
appNameArg = args[0]
Comment on lines +95 to +104

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.

note: This is a clever way to handle the edge-case. Personally, I start to get confused when slack create agent will name my app "agent" vs show me a prompt.

I wonder if a more obvious approach (for the simple minded folks like me) would be anytime slack create agent is used, it's focused on the agent prompt.

The drawback to my suggestion is that we'd eventually need to introduce a --name flag to allow people to customize their app, such as slack create --name agent --template <url>.

I'm okay moving forward with the current approach if it makes sense to @srtaalej and @zimeg. It comes down to personal choice.

}
default:
appNameArg = args[0]
}
}

// Collect the template URL or select a starting template
template, err := promptTemplateSelection(cmd, clients)
template, err := promptTemplateSelection(cmd, clients, categoryShortcut)
if err != nil {
return err
}
Expand Down
65 changes: 35 additions & 30 deletions cmd/project/create_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,43 +97,48 @@ func getSelectionOptionsForCategory(clients *shared.ClientFactory) []promptObjec
}

// promptTemplateSelection prompts the user to select a project template
func promptTemplateSelection(cmd *cobra.Command, clients *shared.ClientFactory) (create.Template, error) {
func promptTemplateSelection(cmd *cobra.Command, clients *shared.ClientFactory, categoryShortcut string) (create.Template, error) {
ctx := cmd.Context()
var categoryID string
var selectedTemplate string

// Prompt for the category
promptForCategory := "Select an app:"
optionsForCategory := getSelectionOptionsForCategory(clients)
titlesForCategory := make([]string, len(optionsForCategory))
for i, m := range optionsForCategory {
titlesForCategory[i] = m.Title
}
templateForCategory := getSelectionTemplate(clients)
// Check if a category shortcut was provided
if categoryShortcut == "agent" {
categoryID = "slack-cli#ai-apps"
} else {
// Prompt for the category
promptForCategory := "Select an app:"
optionsForCategory := getSelectionOptionsForCategory(clients)
titlesForCategory := make([]string, len(optionsForCategory))
for i, m := range optionsForCategory {
titlesForCategory[i] = m.Title
}
templateForCategory := getSelectionTemplate(clients)

// Print a trace with info about the category title options provided by CLI
clients.IO.PrintTrace(ctx, slacktrace.CreateCategoryOptions, strings.Join(titlesForCategory, ", "))
// Print a trace with info about the category title options provided by CLI
clients.IO.PrintTrace(ctx, slacktrace.CreateCategoryOptions, strings.Join(titlesForCategory, ", "))

// Prompt to choose a category
selection, err := clients.IO.SelectPrompt(ctx, promptForCategory, titlesForCategory, iostreams.SelectPromptConfig{
Description: func(value string, index int) string {
return optionsForCategory[index].Description
},
Flag: clients.Config.Flags.Lookup("template"),
Required: true,
Template: templateForCategory,
})
if err != nil {
return create.Template{}, slackerror.ToSlackError(err)
} else if selection.Flag {
selectedTemplate = selection.Option
} else if selection.Prompt {
categoryID = optionsForCategory[selection.Index].Repository
}
// Prompt to choose a category
selection, err := clients.IO.SelectPrompt(ctx, promptForCategory, titlesForCategory, iostreams.SelectPromptConfig{
Description: func(value string, index int) string {
return optionsForCategory[index].Description
},
Flag: clients.Config.Flags.Lookup("template"),
Required: true,
Template: templateForCategory,
})
if err != nil {
return create.Template{}, slackerror.ToSlackError(err)
} else if selection.Flag {
selectedTemplate = selection.Option
} else if selection.Prompt {
categoryID = optionsForCategory[selection.Index].Repository
}

// Set template to view more samples, so the sample prompt is triggered
if categoryID == viewMoreSamples {
selectedTemplate = viewMoreSamples
// Set template to view more samples, so the sample prompt is triggered
if categoryID == viewMoreSamples {
selectedTemplate = viewMoreSamples
}
}

// Prompt for the template
Expand Down
88 changes: 88 additions & 0 deletions cmd/project/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,94 @@ func TestCreateCommand(t *testing.T) {
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything, expected)
},
},
"creates an agent app using agent argument shortcut": {
CmdArgs: []string{"agent"},
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
// Should skip category prompt and go directly to language selection
cm.IO.On("SelectPrompt", mock.Anything, "Select a language:", mock.Anything, mock.Anything).
Return(
iostreams.SelectPromptResponse{
Prompt: true,
Index: 0, // Select Node.js template
},
nil,
)
createClientMock = new(CreateClientMock)
createClientMock.On("Create", mock.Anything, 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-assistant-template")
require.NoError(t, err)
expected := create.CreateArgs{
Template: template,
}
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything, expected)
// Verify that category prompt was NOT called
cm.IO.AssertNotCalled(t, "SelectPrompt", mock.Anything, "Select an app:", mock.Anything, mock.Anything)
},
},
"creates an agent app with app name using agent argument": {
CmdArgs: []string{"agent", "my-agent-app"},
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
// Should skip category prompt and go directly to language selection
cm.IO.On("SelectPrompt", mock.Anything, "Select a language:", mock.Anything, mock.Anything).
Return(
iostreams.SelectPromptResponse{
Prompt: true,
Index: 1, // Select Python template
},
nil,
)
createClientMock = new(CreateClientMock)
createClientMock.On("Create", mock.Anything, 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-python-assistant-template")
require.NoError(t, err)
expected := create.CreateArgs{
AppName: "my-agent-app",
Template: template,
}
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything, expected)
// Verify that category prompt was NOT called
cm.IO.AssertNotCalled(t, "SelectPrompt", mock.Anything, "Select an app:", mock.Anything, mock.Anything)
},
},
"creates an app named agent when template flag is provided": {
CmdArgs: []string{"agent", "--template", "slack-samples/bolt-js-starter-template"},
Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {
cm.IO.On("SelectPrompt", mock.Anything, "Select an app:", mock.Anything, mock.Anything).
Return(
iostreams.SelectPromptResponse{
Flag: true,
Option: "slack-samples/bolt-js-starter-template",
},
nil,
)
cm.IO.On("SelectPrompt", mock.Anything, "Select a language:", mock.Anything, mock.Anything).
Return(
iostreams.SelectPromptResponse{
Flag: true,
Option: "slack-samples/bolt-js-starter-template",
},
nil,
)
createClientMock = new(CreateClientMock)
createClientMock.On("Create", mock.Anything, 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-template")
require.NoError(t, err)
expected := create.CreateArgs{
AppName: "agent",
Template: template,
}
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything, expected)
},
},

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.

suggestion: Thoughts on adding a test for the use-case slack create agent some-name --template <url>. Seems like you've done the hard work so we could bump the test coverage a little more.

}, func(cf *shared.ClientFactory) *cobra.Command {
return NewCreateCommand(cf)
})
Expand Down
Loading