-
Notifications
You must be signed in to change notification settings - Fork 28
feat(create): replace the generated default app name with an optional input prompt #340
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7c99f80
af97eca
5ed7e88
7b610e5
a120396
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,6 +46,7 @@ func TestCreateCommand(t *testing.T) { | |
| testutil.TableTestCommand(t, testutil.CommandTests{ | ||
| "creates a bolt application from prompts": { | ||
| Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { | ||
| cm.IO.On("IsTTY").Return(true) | ||
|
Comment on lines
47
to
+49
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧪 suggestion: We might want to include a test case alongside af97eca changes - do we have a unit test that's checking for |
||
| cm.IO.On("SelectPrompt", mock.Anything, "Select an app:", mock.Anything, mock.Anything). | ||
| Return( | ||
| iostreams.SelectPromptResponse{ | ||
|
|
@@ -62,6 +63,8 @@ func TestCreateCommand(t *testing.T) { | |
| }, | ||
| nil, | ||
| ) | ||
| cm.IO.On("InputPrompt", mock.Anything, "Name your app:", mock.Anything). | ||
| Return("my-app", nil) | ||
| createClientMock = new(CreateClientMock) | ||
| createClientMock.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("", nil) | ||
| CreateFunc = createClientMock.Create | ||
|
|
@@ -70,14 +73,17 @@ func TestCreateCommand(t *testing.T) { | |
| template, err := create.ResolveTemplateURL("slack-samples/bolt-js-starter-template") | ||
| require.NoError(t, err) | ||
| expected := create.CreateArgs{ | ||
| AppName: "my-app", | ||
| Template: template, | ||
| } | ||
| createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything, expected) | ||
| cm.IO.AssertCalled(t, "InputPrompt", mock.Anything, "Name your app:", mock.Anything) | ||
| }, | ||
| }, | ||
| "creates a deno application from flags": { | ||
| CmdArgs: []string{"--template", "slack-samples/deno-starter-template"}, | ||
| Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { | ||
| cm.IO.On("IsTTY").Return(true) | ||
| cm.IO.On("SelectPrompt", mock.Anything, "Select an app:", mock.Anything, mock.Anything). | ||
| Return( | ||
| iostreams.SelectPromptResponse{ | ||
|
|
@@ -94,6 +100,8 @@ func TestCreateCommand(t *testing.T) { | |
| }, | ||
| nil, | ||
| ) | ||
| cm.IO.On("InputPrompt", mock.Anything, "Name your app:", mock.Anything). | ||
| Return("my-deno-app", nil) | ||
| createClientMock = new(CreateClientMock) | ||
| createClientMock.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("", nil) | ||
| CreateFunc = createClientMock.Create | ||
|
|
@@ -102,14 +110,17 @@ func TestCreateCommand(t *testing.T) { | |
| template, err := create.ResolveTemplateURL("slack-samples/deno-starter-template") | ||
| require.NoError(t, err) | ||
| expected := create.CreateArgs{ | ||
| AppName: "my-deno-app", | ||
| Template: template, | ||
| } | ||
| createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything, expected) | ||
| cm.IO.AssertCalled(t, "InputPrompt", mock.Anything, "Name your app:", mock.Anything) | ||
| }, | ||
| }, | ||
| "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) { | ||
| cm.IO.On("IsTTY").Return(true) | ||
| // Should skip category prompt and go directly to language selection | ||
| cm.IO.On("SelectPrompt", mock.Anything, "Select a language:", mock.Anything, mock.Anything). | ||
| Return( | ||
|
|
@@ -119,6 +130,8 @@ func TestCreateCommand(t *testing.T) { | |
| }, | ||
| nil, | ||
| ) | ||
| cm.IO.On("InputPrompt", mock.Anything, "Name your app:", mock.Anything). | ||
| Return("my-agent", nil) | ||
| createClientMock = new(CreateClientMock) | ||
| createClientMock.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("", nil) | ||
| CreateFunc = createClientMock.Create | ||
|
|
@@ -127,11 +140,13 @@ func TestCreateCommand(t *testing.T) { | |
| template, err := create.ResolveTemplateURL("slack-samples/bolt-js-assistant-template") | ||
| require.NoError(t, err) | ||
| expected := create.CreateArgs{ | ||
| AppName: "my-agent", | ||
| 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) | ||
| cm.IO.AssertCalled(t, "InputPrompt", mock.Anything, "Name your app:", mock.Anything) | ||
| }, | ||
| }, | ||
| "creates an agent app with app name using agent argument": { | ||
|
|
@@ -160,6 +175,8 @@ func TestCreateCommand(t *testing.T) { | |
| 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) | ||
| // Verify that name prompt was NOT called since name was provided as arg | ||
| cm.IO.AssertNotCalled(t, "InputPrompt", mock.Anything, "Name your app:", mock.Anything) | ||
| }, | ||
| }, | ||
| "creates an app named agent when template flag is provided": { | ||
|
|
@@ -193,6 +210,8 @@ func TestCreateCommand(t *testing.T) { | |
| Template: template, | ||
| } | ||
| createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything, expected) | ||
| // Verify that name prompt was NOT called since name was provided as arg | ||
| cm.IO.AssertNotCalled(t, "InputPrompt", mock.Anything, "Name your app:", mock.Anything) | ||
| }, | ||
| }, | ||
| "creates an app named agent using name flag without triggering shortcut": { | ||
|
|
@@ -229,6 +248,8 @@ func TestCreateCommand(t *testing.T) { | |
| createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything, expected) | ||
| // Verify that category prompt WAS called (shortcut was not triggered) | ||
| cm.IO.AssertCalled(t, "SelectPrompt", mock.Anything, "Select an app:", mock.Anything, mock.Anything) | ||
| // Verify that name prompt was NOT called since --name flag was provided | ||
| cm.IO.AssertNotCalled(t, "InputPrompt", mock.Anything, "Name your app:", mock.Anything) | ||
| }, | ||
| }, | ||
| "creates an agent app with name flag overriding positional arg": { | ||
|
|
@@ -290,6 +311,8 @@ func TestCreateCommand(t *testing.T) { | |
| Template: template, | ||
| } | ||
| createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything, expected) | ||
| // Verify that name prompt was NOT called since --name flag was provided | ||
| cm.IO.AssertNotCalled(t, "InputPrompt", mock.Anything, "Name your app:", mock.Anything) | ||
| }, | ||
| }, | ||
| "name flag overrides positional app name argument with agent shortcut": { | ||
|
|
@@ -318,6 +341,110 @@ func TestCreateCommand(t *testing.T) { | |
| createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything, expected) | ||
| // Verify that category prompt was NOT called (agent shortcut was triggered) | ||
| cm.IO.AssertNotCalled(t, "SelectPrompt", mock.Anything, "Select an app:", mock.Anything, mock.Anything) | ||
| // Verify that name prompt was NOT called since --name flag was provided | ||
| cm.IO.AssertNotCalled(t, "InputPrompt", mock.Anything, "Name your app:", mock.Anything) | ||
| }, | ||
| }, | ||
| "user accepts default name from prompt": { | ||
| Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { | ||
| cm.IO.On("IsTTY").Return(true) | ||
| cm.IO.On("SelectPrompt", mock.Anything, "Select an app:", mock.Anything, mock.Anything). | ||
| Return( | ||
| iostreams.SelectPromptResponse{ | ||
| Prompt: true, | ||
| Index: 0, | ||
| }, | ||
| nil, | ||
| ) | ||
| cm.IO.On("SelectPrompt", mock.Anything, "Select a language:", mock.Anything, mock.Anything). | ||
| Return( | ||
| iostreams.SelectPromptResponse{ | ||
| Prompt: true, | ||
| Index: 0, | ||
| }, | ||
| nil, | ||
| ) | ||
| // Return empty string to simulate pressing Enter (accepting default) | ||
| cm.IO.On("InputPrompt", mock.Anything, "Name your app:", mock.Anything). | ||
| Return("", 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) { | ||
| cm.IO.AssertCalled(t, "InputPrompt", mock.Anything, "Name your app:", mock.Anything) | ||
| // When the user accepts the default (empty return), the generated name is used | ||
| createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(args create.CreateArgs) bool { | ||
| return args.AppName != "" | ||
| })) | ||
| }, | ||
| }, | ||
| "non-TTY without name falls back to generated name": { | ||
| CmdArgs: []string{"--template", "slack-samples/bolt-js-starter-template"}, | ||
| Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { | ||
| // IsTTY defaults to false via AddDefaultMocks, simulating piped output | ||
| 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) { | ||
| // Should NOT prompt for name since not a TTY | ||
| cm.IO.AssertNotCalled(t, "InputPrompt", mock.Anything, "Name your app:", mock.Anything) | ||
| // Should still call Create with a non-empty generated name | ||
| createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything, mock.MatchedBy(func(args create.CreateArgs) bool { | ||
| return args.AppName != "" | ||
| })) | ||
| }, | ||
| }, | ||
| "positional arg skips name prompt": { | ||
| CmdArgs: []string{"my-project"}, | ||
| 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{ | ||
| Prompt: true, | ||
| Index: 0, | ||
| }, | ||
| nil, | ||
| ) | ||
| cm.IO.On("SelectPrompt", mock.Anything, "Select a language:", mock.Anything, mock.Anything). | ||
| Return( | ||
| iostreams.SelectPromptResponse{ | ||
| Prompt: true, | ||
| Index: 0, | ||
| }, | ||
| 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: "my-project", | ||
| Template: template, | ||
| } | ||
| createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.Anything, expected) | ||
| // Verify that name prompt was NOT called since name was provided as positional arg | ||
| cm.IO.AssertNotCalled(t, "InputPrompt", mock.Anything, "Name your app:", mock.Anything) | ||
| }, | ||
| }, | ||
| "lists all templates with --list flag": { | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📠 nit(non-blocking): If these aren't used elsewhere, can we move these constants to |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,13 +18,11 @@ import ( | |
| "context" | ||
| "fmt" | ||
| "io" | ||
| "math/rand" | ||
| "net/http" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/go-git/go-git/v5" | ||
| "github.com/go-git/go-git/v5/plumbing" | ||
|
|
@@ -150,23 +148,15 @@ func Create(ctx context.Context, clients *shared.ClientFactory, log *logger.Logg | |
| return appDirPath, nil | ||
| } | ||
|
|
||
| // generateRandomAppName will create a random app name based on two words and a number | ||
| func generateRandomAppName() string { | ||
| rand.New(rand.NewSource(time.Now().UnixNano())) | ||
| var firstRandomNum = rand.Intn(len(adjectives)) | ||
| var secondRandomNum = rand.Intn(len(animals)) | ||
| var randomName = fmt.Sprintf("%s-%s-%d", adjectives[firstRandomNum], animals[secondRandomNum], rand.Intn(1000)) | ||
| return randomName | ||
| } | ||
|
|
||
| // getAppDirName will validate and return the app's directory name | ||
| func getAppDirName(appName string) (string, error) { | ||
| if len(appName) <= 0 { | ||
| return generateRandomAppName(), nil | ||
| return "", fmt.Errorf("app name is required") | ||
| } | ||
|
|
||
| // trim whitespace | ||
| appName = strings.ReplaceAll(appName, " ", "") | ||
| appName = strings.TrimSpace(appName) | ||
| appName = strings.ReplaceAll(appName, " ", "-") | ||
|
Comment on lines
168
to
+159
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📣 note: Let's call this out in the changelog as a fix as well! I'll make a few changes to this since we'll also want to keep it focused on just developer-facing features- |
||
|
|
||
| // name cannot be a reserved word | ||
| if goutils.Contains(reserved, appName, false) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😸 praise: This is a fun prompt!