diff --git a/internal/pkg/create/create.go b/internal/pkg/create/create.go index a7e4ae47..fe8d5dd8 100644 --- a/internal/pkg/create/create.go +++ b/internal/pkg/create/create.go @@ -171,21 +171,20 @@ func Create(ctx context.Context, clients *shared.ClientFactory, createArgs Creat // 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-]+`) +// nonPathSafeRe matches characters that are not safe for file paths (not alphanumeric, dash, underscore, or dot). +var nonPathSafeRe = regexp.MustCompile(`[^a-zA-Z0-9._-]+`) -// getAppDirName will validate and return the app's directory name in kebab-case +// getAppDirName will validate and return the app's directory name safe for file paths func getAppDirName(appName string) (string, error) { if len(appName) <= 0 { return "", fmt.Errorf("app name is required") } - // Normalize to a variation of kebab-case: replace non-alphanumeric with dashes, collapse, and trim + // Normalize: trim whitespace, replace spaces with dashes, remove unsafe characters appName = strings.TrimSpace(appName) - appName = strings.ToLower(appName) - appName = nonAlphanumericRe.ReplaceAllString(appName, "-") + appName = strings.ReplaceAll(appName, " ", "-") + appName = nonPathSafeRe.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") @@ -198,7 +197,7 @@ func getAppDirName(appName string) (string, error) { return appName, nil } -// parseAppPath splits user input into a directory path (with kebab-cased basename) +// parseAppPath splits user input into a directory path (with path-safe 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) diff --git a/internal/pkg/create/create_test.go b/internal/pkg/create/create_test.go index cb3cb9f3..2be54346 100644 --- a/internal/pkg/create/create_test.go +++ b/internal/pkg/create/create_test.go @@ -58,29 +58,37 @@ func TestGetProjectDirectoryName(t *testing.T) { input: " my-app ", expected: "my-app", }, - "uppercase converted to lowercase": { + "uppercase preserved with spaces replaced": { input: "My Slack App", - expected: "my-slack-app", + expected: "My-Slack-App", }, - "mixed case normalized": { + "mixed case preserved": { input: "My-Slack-App", - expected: "my-slack-app", + expected: "My-Slack-App", }, - "special characters replaced with dashes": { - input: "my_app!@#test", - expected: "my-app-test", + "unsafe characters removed": { + input: "my_App!@#Test", + expected: "my_AppTest", }, - "consecutive special characters collapsed to single dash": { + "consecutive dashes collapsed to single dash": { input: "my---app", expected: "my-app", }, - "leading and trailing special characters trimmed": { + "leading and trailing dashes preserved": { input: "---my-app---", - expected: "my-app", + expected: "-my-app-", + }, + "underscores preserved": { + input: "my_app", + expected: "my_app", + }, + "dots preserved": { + input: "my.app", + expected: "my.app", }, - "dots converted to dashes": { + "leading dots preserved": { input: ".my-app", - expected: "my-app", + expected: ".my-app", }, "only special characters returns error": { input: "!!!", @@ -92,7 +100,7 @@ func TestGetProjectDirectoryName(t *testing.T) { }, "complex mixed input": { input: " My Cool App! (v2) ", - expected: "my-cool-app-v2", + expected: "My-Cool-App-v2", }, } for name, tc := range tests { @@ -122,7 +130,7 @@ func TestParseAppPath(t *testing.T) { }, "name with spaces": { input: "My Cool App", - expectedPath: "my-cool-app", + expectedPath: "My-Cool-App", expectedDisplay: "My Cool App", }, "relative path with simple name": { @@ -132,7 +140,7 @@ func TestParseAppPath(t *testing.T) { }, "relative path with spaced name": { input: "path/to/My App", - expectedPath: filepath.Join("path", "to", "my-app"), + expectedPath: filepath.Join("path", "to", "My-App"), expectedDisplay: "My App", }, "dot-prefixed path": { @@ -152,7 +160,7 @@ func TestParseAppPath(t *testing.T) { }, "uppercase in nested path": { input: "projects/My Slack App", - expectedPath: filepath.Join("projects", "my-slack-app"), + expectedPath: filepath.Join("projects", "My-Slack-App"), expectedDisplay: "My Slack App", }, "trailing slash is trimmed": {