From 7e47feec9b0d3e9f1e328e356475b32e46ca5d84 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Mon, 4 May 2026 16:04:13 -0400 Subject: [PATCH 1/9] fix: use title case for manifest display names during app creation The display_information.name and bot_user.display_name fields in manifest files were set to the kebab-case directory name (e.g. "my-app") instead of a human-readable title case name (e.g. "My App"). --- internal/app/app.go | 22 +++++++++---- internal/app/app_test.go | 44 ++++++++++++++++++-------- test/testdata/manifest-app-name.js | 4 +-- test/testdata/manifest-app-name.json | 4 +-- test/testdata/manifest-app-name.ts | 4 +-- test/testdata/manifest-sdk-app-name.ts | 2 +- 6 files changed, 53 insertions(+), 27 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index da097231..2113d635 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -19,12 +19,15 @@ import ( "fmt" "path/filepath" "regexp" + "strings" "text/template" "github.com/slackapi/slack-cli/internal/api" "github.com/slackapi/slack-cli/internal/config" "github.com/slackapi/slack-cli/internal/shared/types" "github.com/spf13/afero" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) // Client to access the app/project @@ -46,18 +49,25 @@ func NewClient( } } +func kebabToTitleCase(s string) string { + return cases.Title(language.English).String(strings.ReplaceAll(s, "-", " ")) +} + // UpdateDefaultProjectFiles should update any project specific files if any func UpdateDefaultProjectFiles(fs afero.Fs, dirPath string, appDirName string) error { + displayName := kebabToTitleCase(appDirName) + // Files and their corresponding app name replacement functions projectFiles := []struct { filename string replacer func([]byte, string) []byte + name string }{ - {"manifest.json", regexReplaceAppNameInManifest}, - {"manifest.js", regexReplaceAppNameInManifest}, - {"manifest.ts", regexReplaceAppNameInManifest}, - {"package.json", regexReplaceAppNameInPackageJSON}, - {"pyproject.toml", regexReplaceAppNameInPyprojectToml}, + {"manifest.json", regexReplaceAppNameInManifest, displayName}, + {"manifest.js", regexReplaceAppNameInManifest, displayName}, + {"manifest.ts", regexReplaceAppNameInManifest, displayName}, + {"package.json", regexReplaceAppNameInPackageJSON, appDirName}, + {"pyproject.toml", regexReplaceAppNameInPyprojectToml, appDirName}, } for _, pf := range projectFiles { @@ -67,7 +77,7 @@ func UpdateDefaultProjectFiles(fs afero.Fs, dirPath string, appDirName string) e continue } - fileData = pf.replacer(fileData, appDirName) + fileData = pf.replacer(fileData, pf.name) if err := afero.WriteFile(fs, filePath, fileData, 0644); err != nil { return err } diff --git a/internal/app/app_test.go b/internal/app/app_test.go index c4720846..573ff7e7 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -114,16 +114,6 @@ func Test_App_UpdateDefaultProjectFiles(t *testing.T) { expectedFiles: map[string]string{}, expectedErrorType: nil, }, - "WriteFile error": { - appDirName: "vibrant-butterfly-1234", - existingFiles: map[string]string{ - "manifest.json": string(testdata.ManifestJSON), - }, - expectedFiles: map[string]string{ - "manifest.json": string(testdata.ManifestJSONAppName), - }, - expectedErrorType: nil, - }, } for name, tc := range tests { @@ -161,6 +151,32 @@ func Test_App_UpdateDefaultProjectFiles(t *testing.T) { } } +func Test_kebabToTitleCase(t *testing.T) { + tests := map[string]struct { + input string + expected string + }{ + "multiple words": { + input: "my-app", + expected: "My App", + }, + "multiple words with numbers": { + input: "vibrant-butterfly-1234", + expected: "Vibrant Butterfly 1234", + }, + "single word": { + input: "hello", + expected: "Hello", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result := kebabToTitleCase(tc.input) + require.Equal(t, tc.expected, result) + }) + } +} + func Test_RegexReplaceAppNameInManifest(t *testing.T) { tests := map[string]struct { src []byte @@ -169,22 +185,22 @@ func Test_RegexReplaceAppNameInManifest(t *testing.T) { }{ "manifest.json is validate": { src: testdata.ManifestJSON, - appName: "vibrant-butterfly-1234", + appName: "Vibrant Butterfly 1234", expectedSrc: testdata.ManifestJSONAppName, }, "manifest.js is validate": { src: testdata.ManifestJS, - appName: "vibrant-butterfly-1234", + appName: "Vibrant Butterfly 1234", expectedSrc: testdata.ManifestJSAppName, }, "manifest.ts is validate": { src: testdata.ManifestTS, - appName: "vibrant-butterfly-1234", + appName: "Vibrant Butterfly 1234", expectedSrc: testdata.ManifestTSAppName, }, "manifest.ts with sdk is validate": { src: testdata.ManifestSDKTS, - appName: "vibrant-butterfly-1234", + appName: "Vibrant Butterfly 1234", expectedSrc: testdata.ManifestSDKTSAppName, }, } diff --git a/test/testdata/manifest-app-name.js b/test/testdata/manifest-app-name.js index 6cd7e6c6..43e73aad 100644 --- a/test/testdata/manifest-app-name.js +++ b/test/testdata/manifest-app-name.js @@ -3,7 +3,7 @@ export default { major_version: 2 }, display_information: { - name: 'vibrant-butterfly-1234' + name: 'Vibrant Butterfly 1234' }, // This is a comment features: { @@ -13,7 +13,7 @@ export default { messages_tab_read_only_enabled: false }, bot_user: { - display_name: 'vibrant-butterfly-1234' + display_name: 'Vibrant Butterfly 1234' } }, functions: { diff --git a/test/testdata/manifest-app-name.json b/test/testdata/manifest-app-name.json index f9b9e097..0984465d 100644 --- a/test/testdata/manifest-app-name.json +++ b/test/testdata/manifest-app-name.json @@ -3,7 +3,7 @@ "major_version": 2 }, "display_information": { - "name": "vibrant-butterfly-1234" + "name": "Vibrant Butterfly 1234" }, "features": { "app_home": { @@ -12,7 +12,7 @@ "messages_tab_read_only_enabled": false }, "bot_user": { - "display_name": "vibrant-butterfly-1234" + "display_name": "Vibrant Butterfly 1234" } }, "functions": { diff --git a/test/testdata/manifest-app-name.ts b/test/testdata/manifest-app-name.ts index ca28ee87..c2879228 100644 --- a/test/testdata/manifest-app-name.ts +++ b/test/testdata/manifest-app-name.ts @@ -12,7 +12,7 @@ export default { "major_version": 2 }, "display_information": { - "name": "vibrant-butterfly-1234" + "name": "Vibrant Butterfly 1234" }, // This is a comment "features": { @@ -22,7 +22,7 @@ export default { "messages_tab_read_only_enabled": false }, "bot_user": { - "display_name": "vibrant-butterfly-1234" + "display_name": "Vibrant Butterfly 1234" } }, "functions": { diff --git a/test/testdata/manifest-sdk-app-name.ts b/test/testdata/manifest-sdk-app-name.ts index 1818b2ab..385c5a19 100644 --- a/test/testdata/manifest-sdk-app-name.ts +++ b/test/testdata/manifest-sdk-app-name.ts @@ -11,7 +11,7 @@ const obj = { }; export default Manifest({ - "name": "vibrant-butterfly-1234", + "name": "Vibrant Butterfly 1234", "description": "Reverse a string", // "runtime_environment": "slack", "runtime": "deno1.x", From af8e3f4b92355dc36eedaec4d9d77cef8e25e825 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Mon, 4 May 2026 16:53:48 -0400 Subject: [PATCH 2/9] fix: pass original app name as display name instead of title-casing kebab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback — instead of converting the kebab-case directory name to title case (which mangles names like "TIM" or "Translator - Translate Languages"), pass the original user-provided app name through to manifest display fields. This preserves the user's exact casing and formatting. --- internal/app/app.go | 11 +------- internal/app/app_test.go | 51 ++++++++++++----------------------- internal/pkg/create/create.go | 2 +- 3 files changed, 19 insertions(+), 45 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index 2113d635..2f5836a6 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -19,15 +19,12 @@ import ( "fmt" "path/filepath" "regexp" - "strings" "text/template" "github.com/slackapi/slack-cli/internal/api" "github.com/slackapi/slack-cli/internal/config" "github.com/slackapi/slack-cli/internal/shared/types" "github.com/spf13/afero" - "golang.org/x/text/cases" - "golang.org/x/text/language" ) // Client to access the app/project @@ -49,14 +46,8 @@ func NewClient( } } -func kebabToTitleCase(s string) string { - return cases.Title(language.English).String(strings.ReplaceAll(s, "-", " ")) -} - // UpdateDefaultProjectFiles should update any project specific files if any -func UpdateDefaultProjectFiles(fs afero.Fs, dirPath string, appDirName string) error { - displayName := kebabToTitleCase(appDirName) - +func UpdateDefaultProjectFiles(fs afero.Fs, dirPath string, appDirName string, displayName string) error { // Files and their corresponding app name replacement functions projectFiles := []struct { filename string diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 573ff7e7..17be7ec4 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -28,12 +28,14 @@ import ( func Test_App_UpdateDefaultProjectFiles(t *testing.T) { tests := map[string]struct { appDirName string + displayName string existingFiles map[string]string expectedFiles map[string]string expectedErrorType error }{ "manifest.json file exists": { - appDirName: "vibrant-butterfly-1234", + appDirName: "vibrant-butterfly-1234", + displayName: "Vibrant Butterfly 1234", existingFiles: map[string]string{ "manifest.json": string(testdata.ManifestJSON), }, @@ -43,7 +45,8 @@ func Test_App_UpdateDefaultProjectFiles(t *testing.T) { expectedErrorType: nil, }, "manifest.js file exists": { - appDirName: "vibrant-butterfly-1234", + appDirName: "vibrant-butterfly-1234", + displayName: "Vibrant Butterfly 1234", existingFiles: map[string]string{ "manifest.js": string(testdata.ManifestJS), }, @@ -53,7 +56,8 @@ func Test_App_UpdateDefaultProjectFiles(t *testing.T) { expectedErrorType: nil, }, "manifest.ts file exists": { - appDirName: "vibrant-butterfly-1234", + appDirName: "vibrant-butterfly-1234", + displayName: "Vibrant Butterfly 1234", existingFiles: map[string]string{ "manifest.ts": string(testdata.ManifestTS), }, @@ -63,7 +67,8 @@ func Test_App_UpdateDefaultProjectFiles(t *testing.T) { expectedErrorType: nil, }, "Multiple manifest files exist": { - appDirName: "vibrant-butterfly-1234", + appDirName: "vibrant-butterfly-1234", + displayName: "Vibrant Butterfly 1234", existingFiles: map[string]string{ "manifest.json": string(testdata.ManifestJSON), "manifest.ts": string(testdata.ManifestTS), @@ -75,7 +80,8 @@ func Test_App_UpdateDefaultProjectFiles(t *testing.T) { expectedErrorType: nil, }, "package.json file exists": { - appDirName: "vibrant-butterfly-1234", + appDirName: "vibrant-butterfly-1234", + displayName: "Vibrant Butterfly 1234", existingFiles: map[string]string{ "package.json": string(testdata.PackageJSON), }, @@ -85,7 +91,8 @@ func Test_App_UpdateDefaultProjectFiles(t *testing.T) { expectedErrorType: nil, }, "pyproject.toml file exists": { - appDirName: "vibrant-butterfly-1234", + appDirName: "vibrant-butterfly-1234", + displayName: "Vibrant Butterfly 1234", existingFiles: map[string]string{ "pyproject.toml": string(testdata.PyprojectTOML), }, @@ -95,7 +102,8 @@ func Test_App_UpdateDefaultProjectFiles(t *testing.T) { expectedErrorType: nil, }, "Multiple project files exist": { - appDirName: "vibrant-butterfly-1234", + appDirName: "vibrant-butterfly-1234", + displayName: "Vibrant Butterfly 1234", existingFiles: map[string]string{ "manifest.json": string(testdata.ManifestJSON), "package.json": string(testdata.PackageJSON), @@ -110,6 +118,7 @@ func Test_App_UpdateDefaultProjectFiles(t *testing.T) { }, "No manifest files exist": { appDirName: "vibrant-butterfly-1234", + displayName: "Vibrant Butterfly 1234", existingFiles: map[string]string{}, expectedFiles: map[string]string{}, expectedErrorType: nil, @@ -136,7 +145,7 @@ func Test_App_UpdateDefaultProjectFiles(t *testing.T) { } // Run the tests - err := UpdateDefaultProjectFiles(fs, projectDirPath, tc.appDirName) + err := UpdateDefaultProjectFiles(fs, projectDirPath, tc.appDirName, tc.displayName) // Assertions require.IsType(t, err, tc.expectedErrorType) @@ -151,32 +160,6 @@ func Test_App_UpdateDefaultProjectFiles(t *testing.T) { } } -func Test_kebabToTitleCase(t *testing.T) { - tests := map[string]struct { - input string - expected string - }{ - "multiple words": { - input: "my-app", - expected: "My App", - }, - "multiple words with numbers": { - input: "vibrant-butterfly-1234", - expected: "Vibrant Butterfly 1234", - }, - "single word": { - input: "hello", - expected: "Hello", - }, - } - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - result := kebabToTitleCase(tc.input) - require.Equal(t, tc.expected, result) - }) - } -} - func Test_RegexReplaceAppNameInManifest(t *testing.T) { tests := map[string]struct { src []byte diff --git a/internal/pkg/create/create.go b/internal/pkg/create/create.go index f10e07ec..357785c3 100644 --- a/internal/pkg/create/create.go +++ b/internal/pkg/create/create.go @@ -150,7 +150,7 @@ func Create(ctx context.Context, clients *shared.ClientFactory, createArgs Creat }() // Update default project files' app name, bot name, etc - if err := app.UpdateDefaultProjectFiles(clients.Fs, projectDirPath, appDirName); err != nil { + if err := app.UpdateDefaultProjectFiles(clients.Fs, projectDirPath, appDirName, createArgs.AppName); err != nil { return "", slackerror.Wrap(err, slackerror.ErrProjectFileUpdate) } From e0c3bb05cd6cb5ed7cf8b3ee98e14712d3e149c8 Mon Sep 17 00:00:00 2001 From: Ale Mercado <104795114+srtaalej@users.noreply.github.com> Date: Thu, 7 May 2026 15:20:53 -0400 Subject: [PATCH 3/9] Update test/testdata/manifest-app-name.ts Co-authored-by: Eden Zimbelman --- test/testdata/manifest-app-name.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/testdata/manifest-app-name.ts b/test/testdata/manifest-app-name.ts index c2879228..a158c19b 100644 --- a/test/testdata/manifest-app-name.ts +++ b/test/testdata/manifest-app-name.ts @@ -12,7 +12,7 @@ export default { "major_version": 2 }, "display_information": { - "name": "Vibrant Butterfly 1234" + "name": "Vibrant butterfly - 1234" }, // This is a comment "features": { From 39cdaad26501dc2109b9a70294b589ed2e4324a2 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Thu, 7 May 2026 16:11:39 -0400 Subject: [PATCH 4/9] liinter errors --- internal/app/app_test.go | 22 +++++++++++----------- test/testdata/manifest-app-name.js | 4 ++-- test/testdata/manifest-app-name.json | 4 ++-- test/testdata/manifest-app-name.ts | 2 +- test/testdata/manifest-sdk-app-name.ts | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 17be7ec4..a429b665 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -35,7 +35,7 @@ func Test_App_UpdateDefaultProjectFiles(t *testing.T) { }{ "manifest.json file exists": { appDirName: "vibrant-butterfly-1234", - displayName: "Vibrant Butterfly 1234", + displayName: "Vibrant butterfly - 1234", existingFiles: map[string]string{ "manifest.json": string(testdata.ManifestJSON), }, @@ -46,7 +46,7 @@ func Test_App_UpdateDefaultProjectFiles(t *testing.T) { }, "manifest.js file exists": { appDirName: "vibrant-butterfly-1234", - displayName: "Vibrant Butterfly 1234", + displayName: "Vibrant butterfly - 1234", existingFiles: map[string]string{ "manifest.js": string(testdata.ManifestJS), }, @@ -57,7 +57,7 @@ func Test_App_UpdateDefaultProjectFiles(t *testing.T) { }, "manifest.ts file exists": { appDirName: "vibrant-butterfly-1234", - displayName: "Vibrant Butterfly 1234", + displayName: "Vibrant butterfly - 1234", existingFiles: map[string]string{ "manifest.ts": string(testdata.ManifestTS), }, @@ -68,7 +68,7 @@ func Test_App_UpdateDefaultProjectFiles(t *testing.T) { }, "Multiple manifest files exist": { appDirName: "vibrant-butterfly-1234", - displayName: "Vibrant Butterfly 1234", + displayName: "Vibrant butterfly - 1234", existingFiles: map[string]string{ "manifest.json": string(testdata.ManifestJSON), "manifest.ts": string(testdata.ManifestTS), @@ -81,7 +81,7 @@ func Test_App_UpdateDefaultProjectFiles(t *testing.T) { }, "package.json file exists": { appDirName: "vibrant-butterfly-1234", - displayName: "Vibrant Butterfly 1234", + displayName: "Vibrant butterfly - 1234", existingFiles: map[string]string{ "package.json": string(testdata.PackageJSON), }, @@ -92,7 +92,7 @@ func Test_App_UpdateDefaultProjectFiles(t *testing.T) { }, "pyproject.toml file exists": { appDirName: "vibrant-butterfly-1234", - displayName: "Vibrant Butterfly 1234", + displayName: "Vibrant butterfly - 1234", existingFiles: map[string]string{ "pyproject.toml": string(testdata.PyprojectTOML), }, @@ -103,7 +103,7 @@ func Test_App_UpdateDefaultProjectFiles(t *testing.T) { }, "Multiple project files exist": { appDirName: "vibrant-butterfly-1234", - displayName: "Vibrant Butterfly 1234", + displayName: "Vibrant butterfly - 1234", existingFiles: map[string]string{ "manifest.json": string(testdata.ManifestJSON), "package.json": string(testdata.PackageJSON), @@ -168,22 +168,22 @@ func Test_RegexReplaceAppNameInManifest(t *testing.T) { }{ "manifest.json is validate": { src: testdata.ManifestJSON, - appName: "Vibrant Butterfly 1234", + appName: "Vibrant butterfly - 1234", expectedSrc: testdata.ManifestJSONAppName, }, "manifest.js is validate": { src: testdata.ManifestJS, - appName: "Vibrant Butterfly 1234", + appName: "Vibrant butterfly - 1234", expectedSrc: testdata.ManifestJSAppName, }, "manifest.ts is validate": { src: testdata.ManifestTS, - appName: "Vibrant Butterfly 1234", + appName: "Vibrant butterfly - 1234", expectedSrc: testdata.ManifestTSAppName, }, "manifest.ts with sdk is validate": { src: testdata.ManifestSDKTS, - appName: "Vibrant Butterfly 1234", + appName: "Vibrant butterfly - 1234", expectedSrc: testdata.ManifestSDKTSAppName, }, } diff --git a/test/testdata/manifest-app-name.js b/test/testdata/manifest-app-name.js index 43e73aad..5f3914e3 100644 --- a/test/testdata/manifest-app-name.js +++ b/test/testdata/manifest-app-name.js @@ -3,7 +3,7 @@ export default { major_version: 2 }, display_information: { - name: 'Vibrant Butterfly 1234' + name: 'Vibrant butterfly - 1234' }, // This is a comment features: { @@ -13,7 +13,7 @@ export default { messages_tab_read_only_enabled: false }, bot_user: { - display_name: 'Vibrant Butterfly 1234' + display_name: 'Vibrant butterfly - 1234' } }, functions: { diff --git a/test/testdata/manifest-app-name.json b/test/testdata/manifest-app-name.json index 0984465d..fbc49b43 100644 --- a/test/testdata/manifest-app-name.json +++ b/test/testdata/manifest-app-name.json @@ -3,7 +3,7 @@ "major_version": 2 }, "display_information": { - "name": "Vibrant Butterfly 1234" + "name": "Vibrant butterfly - 1234" }, "features": { "app_home": { @@ -12,7 +12,7 @@ "messages_tab_read_only_enabled": false }, "bot_user": { - "display_name": "Vibrant Butterfly 1234" + "display_name": "Vibrant butterfly - 1234" } }, "functions": { diff --git a/test/testdata/manifest-app-name.ts b/test/testdata/manifest-app-name.ts index a158c19b..429ea553 100644 --- a/test/testdata/manifest-app-name.ts +++ b/test/testdata/manifest-app-name.ts @@ -22,7 +22,7 @@ export default { "messages_tab_read_only_enabled": false }, "bot_user": { - "display_name": "Vibrant Butterfly 1234" + "display_name": "Vibrant butterfly - 1234" } }, "functions": { diff --git a/test/testdata/manifest-sdk-app-name.ts b/test/testdata/manifest-sdk-app-name.ts index 385c5a19..a8d9584a 100644 --- a/test/testdata/manifest-sdk-app-name.ts +++ b/test/testdata/manifest-sdk-app-name.ts @@ -11,7 +11,7 @@ const obj = { }; export default Manifest({ - "name": "Vibrant Butterfly 1234", + "name": "Vibrant butterfly - 1234", "description": "Reverse a string", // "runtime_environment": "slack", "runtime": "deno1.x", From ac8fcea047b2b7097509520e75af0301f8046e52 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Thu, 7 May 2026 17:16:13 -0400 Subject: [PATCH 5/9] fix: preserve path separators in create command argument The getAppDirName function was normalizing the entire input including path separators, so `slack create path/to/my-app` produced a flat directory `path-to-my-app` instead of preserving the path structure. Adds parseAppPath which splits user input into path prefix and basename, only kebab-casing the basename. The display name for manifests is derived from the raw basename (preserving original casing). Co-Authored-By: Claude --- internal/pkg/create/create.go | 37 +++++++++++--- internal/pkg/create/create_test.go | 79 ++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 7 deletions(-) diff --git a/internal/pkg/create/create.go b/internal/pkg/create/create.go index 357785c3..5c8ebd69 100644 --- a/internal/pkg/create/create.go +++ b/internal/pkg/create/create.go @@ -67,16 +67,16 @@ func Create(ctx context.Context, clients *shared.ClientFactory, createArgs Creat return "", slackerror.Wrap(err, slackerror.ErrAppDirectoryAccess) } - // Get the app selection and accompanying app directory name (this may change when we find the unique directory name) - appDirName, err := getAppDirName(createArgs.AppName) + // Parse the app name input into a directory path and display name + appPath, displayName, err := parseAppPath(createArgs.AppName) if err != nil { return "", slackerror.Wrap(err, slackerror.ErrAppDirectoryAccess) } // Get the project's full directory path projectDirPath := "" - if filepath.IsLocal(appDirName) { - projectDirPath = filepath.Join(workingDirPath, appDirName) + if filepath.IsLocal(appPath) { + projectDirPath = filepath.Join(workingDirPath, appPath) projectDirPath, err = getAvailableDir(ctx, projectDirPath) if err != nil { return "", slackerror.Wrap(err, slackerror.ErrAppDirectoryAccess) @@ -86,7 +86,7 @@ func Create(ctx context.Context, clients *shared.ClientFactory, createArgs Creat return "", slackerror.Wrap(err, slackerror.ErrAppDirectoryAccess) } } else { - projectDirPath = filepath.Join(appDirName) + projectDirPath = filepath.Join(appPath) projectDirPath, err = getAvailableDir(ctx, projectDirPath) if err != nil { return "", slackerror.Wrap(err, slackerror.ErrAppDirectoryAccess) @@ -98,7 +98,7 @@ func Create(ctx context.Context, clients *shared.ClientFactory, createArgs Creat } // Update the app's directory name now that the unique directory is created - appDirName = filepath.Base(projectDirPath) + appDirName := filepath.Base(projectDirPath) // Print a bunch of information about the progress of the command to traces // and debugs and the standard output here @@ -150,7 +150,7 @@ func Create(ctx context.Context, clients *shared.ClientFactory, createArgs Creat }() // Update default project files' app name, bot name, etc - if err := app.UpdateDefaultProjectFiles(clients.Fs, projectDirPath, appDirName, createArgs.AppName); err != nil { + if err := app.UpdateDefaultProjectFiles(clients.Fs, projectDirPath, appDirName, displayName); err != nil { return "", slackerror.Wrap(err, slackerror.ErrProjectFileUpdate) } @@ -192,6 +192,29 @@ func getAppDirName(appName string) (string, error) { return appName, nil } +// parseAppPath splits user input into a directory path (with kebab-cased basename) +// and a display name (the raw basename preserving original casing/spacing). +func parseAppPath(input string) (appPath string, displayName string, err error) { + input = strings.TrimSpace(input) + if input == "" { + return "", "", fmt.Errorf("app name is required") + } + + input = filepath.Clean(input) + displayName = filepath.Base(input) + pathPrefix := filepath.Dir(input) + + dirName, err := getAppDirName(displayName) + if err != nil { + return "", "", err + } + + if pathPrefix == "." { + return dirName, displayName, nil + } + return filepath.Join(pathPrefix, dirName), displayName, nil +} + // getAvailableDir will return a unique directory path. // If dirPath already exists, then a unique numbered path will be appended to the path. func getAvailableDir(ctx context.Context, dirPath string) (string, error) { diff --git a/internal/pkg/create/create_test.go b/internal/pkg/create/create_test.go index 1362d30e..cb3cb9f3 100644 --- a/internal/pkg/create/create_test.go +++ b/internal/pkg/create/create_test.go @@ -108,6 +108,85 @@ func TestGetProjectDirectoryName(t *testing.T) { } } +func TestParseAppPath(t *testing.T) { + tests := map[string]struct { + input string + expectedPath string + expectedDisplay string + hasError bool + }{ + "simple kebab-case name": { + input: "my-app", + expectedPath: "my-app", + expectedDisplay: "my-app", + }, + "name with spaces": { + input: "My Cool App", + expectedPath: "my-cool-app", + expectedDisplay: "My Cool App", + }, + "relative path with simple name": { + input: "path/to/my-app", + expectedPath: filepath.Join("path", "to", "my-app"), + expectedDisplay: "my-app", + }, + "relative path with spaced name": { + input: "path/to/My App", + expectedPath: filepath.Join("path", "to", "my-app"), + expectedDisplay: "My App", + }, + "dot-prefixed path": { + input: "./my-app", + expectedPath: "my-app", + expectedDisplay: "my-app", + }, + "absolute path": { + input: "/abs/path/app", + expectedPath: filepath.Join("/abs", "path", "app"), + expectedDisplay: "app", + }, + "single directory depth": { + input: "projects/my-app", + expectedPath: filepath.Join("projects", "my-app"), + expectedDisplay: "my-app", + }, + "uppercase in nested path": { + input: "projects/My Slack App", + expectedPath: filepath.Join("projects", "my-slack-app"), + expectedDisplay: "My Slack App", + }, + "trailing slash is trimmed": { + input: "path/to/my-app/", + expectedPath: filepath.Join("path", "to", "my-app"), + expectedDisplay: "my-app", + }, + "empty string returns error": { + input: "", + hasError: true, + }, + "whitespace only returns error": { + input: " ", + hasError: true, + }, + "basename with only special chars returns error": { + input: "path/to/!!!", + hasError: true, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + appPath, displayName, err := parseAppPath(tc.input) + if tc.hasError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expectedPath, appPath) + assert.Equal(t, tc.expectedDisplay, displayName) + } + }) + } +} + func TestGetAvailableDirectory(t *testing.T) { var exists bool var err error From fca83ad3a8e7a340ec653694a9fa9a374a6e5344 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Fri, 8 May 2026 16:10:05 -0400 Subject: [PATCH 6/9] test: cover accessible mode branch in buildPasswordForm --- internal/iostreams/forms_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/internal/iostreams/forms_test.go b/internal/iostreams/forms_test.go index 721d4e6c..606f81d6 100644 --- a/internal/iostreams/forms_test.go +++ b/internal/iostreams/forms_test.go @@ -485,6 +485,25 @@ func TestFormsAccessible(t *testing.T) { assert.Equal(t, "", input) assert.Contains(t, out.String(), "Name your app (default: cool-app-123):") }) + + t.Run("password form appends colon in accessible mode", func(t *testing.T) { + var input string + f := buildPasswordForm(io, "Enter token", PasswordPromptConfig{}, &input) + f.Update(f.Init()) + + view := ansi.Strip(f.View()) + assert.Contains(t, view, "Enter token:") + }) + + t.Run("password form preserves existing colon in accessible mode", func(t *testing.T) { + var input string + f := buildPasswordForm(io, "Enter token:", PasswordPromptConfig{}, &input) + f.Update(f.Init()) + + view := ansi.Strip(f.View()) + assert.Contains(t, view, "Enter token:") + assert.NotContains(t, view, "Enter token::") + }) } func TestFormsNoColor(t *testing.T) { From 8c6c53b62d97f0a80a5fd3c27974a0fcd04c6075 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Mon, 11 May 2026 09:32:51 -0400 Subject: [PATCH 7/9] fix: remove unrelated password form tests from this branch --- internal/iostreams/forms_test.go | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/internal/iostreams/forms_test.go b/internal/iostreams/forms_test.go index 606f81d6..f5c94538 100644 --- a/internal/iostreams/forms_test.go +++ b/internal/iostreams/forms_test.go @@ -486,24 +486,6 @@ func TestFormsAccessible(t *testing.T) { assert.Contains(t, out.String(), "Name your app (default: cool-app-123):") }) - t.Run("password form appends colon in accessible mode", func(t *testing.T) { - var input string - f := buildPasswordForm(io, "Enter token", PasswordPromptConfig{}, &input) - f.Update(f.Init()) - - view := ansi.Strip(f.View()) - assert.Contains(t, view, "Enter token:") - }) - - t.Run("password form preserves existing colon in accessible mode", func(t *testing.T) { - var input string - f := buildPasswordForm(io, "Enter token:", PasswordPromptConfig{}, &input) - f.Update(f.Init()) - - view := ansi.Strip(f.View()) - assert.Contains(t, view, "Enter token:") - assert.NotContains(t, view, "Enter token::") - }) } func TestFormsNoColor(t *testing.T) { From bdeadda38e758d206084ba0a4128dc1d7e5ab2d6 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Mon, 11 May 2026 09:47:57 -0700 Subject: [PATCH 8/9] style: remove blank line --- internal/iostreams/forms_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/iostreams/forms_test.go b/internal/iostreams/forms_test.go index f5c94538..721d4e6c 100644 --- a/internal/iostreams/forms_test.go +++ b/internal/iostreams/forms_test.go @@ -485,7 +485,6 @@ func TestFormsAccessible(t *testing.T) { assert.Equal(t, "", input) assert.Contains(t, out.String(), "Name your app (default: cool-app-123):") }) - } func TestFormsNoColor(t *testing.T) { From cf6a674875dad2a92e41c5d39fbdd004ede78581 Mon Sep 17 00:00:00 2001 From: Ale Mercado Date: Mon, 11 May 2026 15:50:22 -0400 Subject: [PATCH 9/9] fix: --name flag overrides display name without replacing path argument The --name flag now only sets the manifest display name, preserving any path provided via the positional argument for directory placement. --- cmd/project/create.go | 21 ++++++++++++++------- cmd/project/create_test.go | 24 ++++++++++++++---------- internal/pkg/create/create.go | 14 ++++++++++---- 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/cmd/project/create.go b/cmd/project/create.go index 8a14edbb..ffd8881a 100644 --- a/cmd/project/create.go +++ b/cmd/project/create.go @@ -116,10 +116,16 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args [] } } - // --name flag overrides any positional app name argument - // This allows users to name their app "agent" without triggering the AI Agent shortcut + // --name flag overrides the manifest display name but preserves any path + // from the positional argument. When no positional arg is given (e.g. + // "slack create --name APPPP"), the name flag also becomes the directory + // path since there's nothing else to derive it from. + displayNameOverride := "" if nameFlagProvided { - appNameArg = createAppNameFlag + displayNameOverride = createAppNameFlag + if appNameArg == "" { + appNameArg = createAppNameFlag + } } // List templates and exit early if the --list flag is set @@ -164,10 +170,11 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args [] subdir = template.GetSubdir() } createArgs := create.CreateArgs{ - AppName: appNameArg, - Template: template, - GitBranch: createGitBranchFlag, - Subdir: subdir, + AppName: appNameArg, + DisplayName: displayNameOverride, + Template: template, + GitBranch: createGitBranchFlag, + Subdir: subdir, } clients.EventTracker.SetAppTemplate(template.GetTemplatePath()) diff --git a/cmd/project/create_test.go b/cmd/project/create_test.go index c64266c2..4b02440f 100644 --- a/cmd/project/create_test.go +++ b/cmd/project/create_test.go @@ -304,8 +304,9 @@ func TestCreateCommand(t *testing.T) { template, err := create.ResolveTemplateURL("slack-samples/bolt-js-starter-template") require.NoError(t, err) expected := create.CreateArgs{ - AppName: "agent", - Template: template, + AppName: "agent", + DisplayName: "agent", + Template: template, } createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, expected) // Verify that category prompt WAS called (shortcut was not triggered) @@ -351,9 +352,10 @@ func TestCreateCommand(t *testing.T) { 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", + AppName: "my-custom-name", // --name flag used as path when no positional arg + DisplayName: "my-custom-name", + 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) @@ -387,8 +389,9 @@ func TestCreateCommand(t *testing.T) { template, err := create.ResolveTemplateURL("slack-samples/bolt-js-starter-template") require.NoError(t, err) expected := create.CreateArgs{ - AppName: "my-name", // --name flag overrides "my-project" positional arg - Template: template, + AppName: "my-project", // positional arg preserved as path + DisplayName: "my-name", // --name flag sets manifest display name + Template: template, } createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, expected) // Verify that name prompt was NOT called since --name flag was provided @@ -432,9 +435,10 @@ func TestCreateCommand(t *testing.T) { 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", + AppName: "my-project", // positional arg preserved as path + DisplayName: "my-name", // --name flag sets manifest display name + 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) diff --git a/internal/pkg/create/create.go b/internal/pkg/create/create.go index 5c8ebd69..19933a64 100644 --- a/internal/pkg/create/create.go +++ b/internal/pkg/create/create.go @@ -50,10 +50,11 @@ var copyIgnoreFiles = []string{".DS_Store"} // CreateArgs are the arguments passed into the Create function type CreateArgs struct { - AppName string - Template Template - GitBranch string - Subdir string + AppName string + DisplayName string + Template Template + GitBranch string + Subdir string } // Create will create a new Slack app on the file system and app manifest on the Slack API. @@ -73,6 +74,11 @@ func Create(ctx context.Context, clients *shared.ClientFactory, createArgs Creat return "", slackerror.Wrap(err, slackerror.ErrAppDirectoryAccess) } + // --name flag overrides only the display name, preserving the path from the argument + if createArgs.DisplayName != "" { + displayName = createArgs.DisplayName + } + // Get the project's full directory path projectDirPath := "" if filepath.IsLocal(appPath) {