Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions internal/pkg/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)
Expand Down
40 changes: 24 additions & 16 deletions internal/pkg/create/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: "!!!",
Expand All @@ -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 {
Expand Down Expand Up @@ -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": {
Expand All @@ -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": {
Expand All @@ -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": {
Expand Down
Loading