From 7c99f80c7f44e8c25490bc6e5475d9595528decf Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Thu, 12 Feb 2026 12:40:45 -0500 Subject: [PATCH] fix: show generated app name in hint line instead of input default --- cmd/project/create.go | 16 +++++++ cmd/project/create_test.go | 90 +++++++++++++++++++++++++++++++++++ internal/iostreams/survey.go | 4 +- internal/pkg/create/create.go | 6 +-- 4 files changed, 112 insertions(+), 4 deletions(-) diff --git a/cmd/project/create.go b/cmd/project/create.go index 9a489120..5a163a48 100644 --- a/cmd/project/create.go +++ b/cmd/project/create.go @@ -20,6 +20,7 @@ import ( "path/filepath" "strings" + "github.com/slackapi/slack-cli/internal/iostreams" "github.com/slackapi/slack-cli/internal/logger" "github.com/slackapi/slack-cli/internal/pkg/create" "github.com/slackapi/slack-cli/internal/shared" @@ -127,6 +128,21 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args [] return err } + // Prompt for app name if not provided via flag or argument + if appNameArg == "" { + defaultName := create.GenerateRandomAppName() + cmd.Print(style.Secondary(fmt.Sprintf(" Press Enter to use the generated name: %s", defaultName)), "\n") + name, err := clients.IO.InputPrompt(ctx, "Name your app:", iostreams.InputPromptConfig{}) + if err != nil { + return err + } + if name != "" { + appNameArg = name + } else { + appNameArg = defaultName + } + } + // Set up spinners appCreateSpinner = style.NewSpinner(cmd.OutOrStdout()) diff --git a/cmd/project/create_test.go b/cmd/project/create_test.go index 7007f988..e094dd51 100644 --- a/cmd/project/create_test.go +++ b/cmd/project/create_test.go @@ -62,6 +62,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,9 +72,11 @@ 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": { @@ -94,6 +98,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,9 +108,11 @@ 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": { @@ -119,6 +127,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 +137,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 +172,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 +207,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 +245,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 +308,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 +338,76 @@ 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("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 != "" + })) + }, + }, + "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) }, }, }, func(cf *shared.ClientFactory) *cobra.Command { diff --git a/internal/iostreams/survey.go b/internal/iostreams/survey.go index 1f26f79e..f804633c 100644 --- a/internal/iostreams/survey.go +++ b/internal/iostreams/survey.go @@ -175,7 +175,8 @@ var InputQuestionTemplate = fmt.Sprintf(` // InputPromptConfig holds additional config for an Input prompt type InputPromptConfig struct { - Required bool // Whether the input must be non-empty + Required bool // Whether the input must be non-empty + Default string // Default value pre-filled in the prompt } // GetFlags returns all flags for the Input prompt @@ -200,6 +201,7 @@ func (io *IOStreams) InputPrompt(ctx context.Context, message string, cfg InputP var input string err := survey.AskOne(&survey.Input{ Message: message, + Default: cfg.Default, }, &input, SurveyOptions(cfg)...) if err != nil { diff --git a/internal/pkg/create/create.go b/internal/pkg/create/create.go index 6e37f41b..335a4ced 100644 --- a/internal/pkg/create/create.go +++ b/internal/pkg/create/create.go @@ -150,8 +150,8 @@ 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 { +// 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)) @@ -162,7 +162,7 @@ func generateRandomAppName() string { // getAppDirName will validate and return the app's directory name func getAppDirName(appName string) (string, error) { if len(appName) <= 0 { - return generateRandomAppName(), nil + return GenerateRandomAppName(), nil } // trim whitespace