From 913105077617340958c52ebcb3f954bfaba4c721 Mon Sep 17 00:00:00 2001 From: Michael Brooks Date: Mon, 9 Mar 2026 09:48:07 -0700 Subject: [PATCH 1/2] feat: normalize project directory names to kebab-case The create command now converts app names to kebab-case (lowercase, dash-delimited, no special characters) when creating project directories. For example, "My App" becomes "my-app" instead of "My-App". --- internal/pkg/create/create.go | 20 ++++++- internal/pkg/create/create_test.go | 96 +++++++++++++++++++++--------- 2 files changed, 86 insertions(+), 30 deletions(-) diff --git a/internal/pkg/create/create.go b/internal/pkg/create/create.go index ee079fb3..a57bd32f 100644 --- a/internal/pkg/create/create.go +++ b/internal/pkg/create/create.go @@ -22,6 +22,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "strings" "github.com/go-git/go-git/v5" @@ -166,15 +167,28 @@ func Create(ctx context.Context, clients *shared.ClientFactory, log *logger.Logg return appDirPath, nil } -// getAppDirName will validate and return the app's directory name +// nonAlphanumericRe matches any character that is not a lowercase letter, digit, or dash. +var nonAlphanumericRe = regexp.MustCompile(`[^a-z0-9-]+`) + +// multiDashRe matches consecutive dashes. +var multiDashRe = regexp.MustCompile(`-{2,}`) + +// getAppDirName will validate and return the app's directory name in kebab-case func getAppDirName(appName string) (string, error) { if len(appName) <= 0 { return "", fmt.Errorf("app name is required") } - // trim whitespace + // Normalize to kebab-case: lowercase, replace non-alphanumeric with dashes, collapse, and trim appName = strings.TrimSpace(appName) - appName = strings.ReplaceAll(appName, " ", "-") + appName = strings.ToLower(appName) + appName = nonAlphanumericRe.ReplaceAllString(appName, "-") + appName = multiDashRe.ReplaceAllString(appName, "-") + appName = strings.Trim(appName, "-") + + if len(appName) == 0 { + return "", fmt.Errorf("app name must contain at least one alphanumeric character") + } // name cannot be a reserved word if goutils.Contains(reserved, appName, false) { diff --git a/internal/pkg/create/create_test.go b/internal/pkg/create/create_test.go index baaa5583..dc80185b 100644 --- a/internal/pkg/create/create_test.go +++ b/internal/pkg/create/create_test.go @@ -38,33 +38,75 @@ func TestCreate(t *testing.T) { } func TestGetProjectDirectoryName(t *testing.T) { - var appName string - var err error - - // Test with empty name returns an error - appName, err = getAppDirName("") - assert.Error(t, err, "should return an error for empty name") - assert.Equal(t, "", appName) - - // Test with app name - appName, err = getAppDirName("my-app") - assert.NoError(t, err, "should not return an error") - assert.Equal(t, "my-app", appName, "should return 'my-app'") - - // Test with a dot in the app name - appName, err = getAppDirName(".my-app") - assert.NoError(t, err, "should not return an error") - assert.Equal(t, ".my-app", appName, "should return '.my-app'") - - // Spaces replaced with hyphens - appName, err = getAppDirName("my cool app") - assert.NoError(t, err) - assert.Equal(t, "my-cool-app", appName) - - // Leading/trailing spaces trimmed - appName, err = getAppDirName(" my-app ") - assert.NoError(t, err) - assert.Equal(t, "my-app", appName) + tests := map[string]struct { + input string + expected string + hasError bool + }{ + "empty name returns error": { + input: "", + hasError: true, + }, + "simple kebab-case name": { + input: "my-app", + expected: "my-app", + }, + "spaces replaced with hyphens": { + input: "my cool app", + expected: "my-cool-app", + }, + "leading and trailing spaces trimmed": { + input: " my-app ", + expected: "my-app", + }, + "uppercase converted to lowercase": { + input: "My Slack App", + expected: "my-slack-app", + }, + "mixed case normalized": { + input: "My-Slack-App", + expected: "my-slack-app", + }, + "special characters replaced with dashes": { + input: "my_app!@#test", + expected: "my-app-test", + }, + "consecutive special characters collapsed to single dash": { + input: "my---app", + expected: "my-app", + }, + "leading and trailing special characters trimmed": { + input: "---my-app---", + expected: "my-app", + }, + "dots converted to dashes": { + input: ".my-app", + expected: "my-app", + }, + "only special characters returns error": { + input: "!!!", + hasError: true, + }, + "numbers preserved": { + input: "app123", + expected: "app123", + }, + "complex mixed input": { + input: " My Cool App! (v2) ", + expected: "my-cool-app-v2", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result, err := getAppDirName(tc.input) + if tc.hasError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expected, result) + } + }) + } } func TestGetAvailableDirectory(t *testing.T) { From 963580354fef486c091a953bed07c16e0f817179 Mon Sep 17 00:00:00 2001 From: Michael Brooks Date: Mon, 9 Mar 2026 09:54:14 -0700 Subject: [PATCH 2/2] chore: alphabetize regex variable declarations --- internal/pkg/create/create.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/pkg/create/create.go b/internal/pkg/create/create.go index a57bd32f..e050dcb7 100644 --- a/internal/pkg/create/create.go +++ b/internal/pkg/create/create.go @@ -167,12 +167,12 @@ func Create(ctx context.Context, clients *shared.ClientFactory, log *logger.Logg return appDirPath, nil } -// nonAlphanumericRe matches any character that is not a lowercase letter, digit, or dash. -var nonAlphanumericRe = regexp.MustCompile(`[^a-z0-9-]+`) - // multiDashRe matches consecutive dashes. var multiDashRe = regexp.MustCompile(`-{2,}`) +// nonAlphanumericRe matches any character that is not a lowercase letter, digit, or dash. +var nonAlphanumericRe = regexp.MustCompile(`[^a-z0-9-]+`) + // getAppDirName will validate and return the app's directory name in kebab-case func getAppDirName(appName string) (string, error) { if len(appName) <= 0 {