Skip to content

Commit b6131c9

Browse files
committed
chore: merge w main
2 parents 9305f85 + 02805c7 commit b6131c9

11 files changed

Lines changed: 304 additions & 214 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![tests](https://github.com/slackapi/slack-cli/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/slackapi/slack-cli/actions/workflows/tests.yml)
44
[![codecov](https://codecov.io/gh/slackapi/slack-cli/branch/main/graph/badge.svg?token=G5TU59IV9I)](https://codecov.io/gh/slackapi/slack-cli)
5-
[![circleci](https://dl.circleci.com/status-badge/img/gh/slackapi/slack-cli/tree/main.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/slackapi/slack-cli/tree/main)
5+
[![CircleCI](https://dl.circleci.com/status-badge/img/gh/slackapi/slack-cli/tree/main.svg?style=shield&circle-token=CCIPRJ_K7pGxh2PtW5AurUdJTKWpb_2845116e5a6f641726a73654f618e450b0c4e95b)](https://dl.circleci.com/status-badge/redirect/gh/slackapi/slack-cli/tree/main)
66

77
> Command-line interface for building apps on the Slack Platform.
88

cmd/project/create.go

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []
8989
ctx := cmd.Context()
9090

9191
// Get optional app name passed as an arg and check for category shortcuts
92-
appNameArg := ""
92+
appPathArg := ""
9393
categoryShortcut := ""
9494
templateFlagProvided := cmd.Flags().Changed("template")
9595
nameFlagProvided := cmd.Flags().Changed("name")
@@ -105,23 +105,17 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []
105105
categoryShortcut = "agent"
106106
// Check if a second argument was provided as the app name
107107
if len(args) > 1 {
108-
appNameArg = args[1]
108+
appPathArg = args[1]
109109
}
110110
} else {
111111
// When --template is provided, "agent" is the app name
112-
appNameArg = args[0]
112+
appPathArg = args[0]
113113
}
114114
default:
115-
appNameArg = args[0]
115+
appPathArg = args[0]
116116
}
117117
}
118118

119-
// --name flag overrides any positional app name argument
120-
// This allows users to name their app "agent" without triggering the AI Agent shortcut
121-
if nameFlagProvided {
122-
appNameArg = createAppNameFlag
123-
}
124-
125119
// List templates and exit early if the --list flag is set
126120
if createListFlag {
127121
return listTemplates(ctx, clients, categoryShortcut)
@@ -139,8 +133,20 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []
139133
return err
140134
}
141135

136+
// --name flag overrides the manifest display name but preserves any path
137+
// from the positional argument. When no positional arg is given (e.g.
138+
// "slack create --name APPPP"), the name flag also becomes the directory
139+
// path since there's nothing else to derive it from.
140+
displayName := ""
141+
if nameFlagProvided {
142+
displayName = createAppNameFlag
143+
if appPathArg == "" {
144+
appPathArg = createAppNameFlag
145+
}
146+
}
147+
142148
// Prompt for app name if not provided via flag or argument
143-
if appNameArg == "" {
149+
if appPathArg == "" {
144150
if clients.IO.IsTTY() {
145151
defaultName := generateRandomAppName()
146152
name, err := clients.IO.InputPrompt(ctx, "Name your app:", iostreams.InputPromptConfig{
@@ -150,12 +156,12 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []
150156
return err
151157
}
152158
if name != "" {
153-
appNameArg = name
159+
appPathArg = name
154160
} else {
155-
appNameArg = defaultName
161+
appPathArg = defaultName
156162
}
157163
} else {
158-
appNameArg = generateRandomAppName()
164+
appPathArg = generateRandomAppName()
159165
}
160166
}
161167

@@ -164,10 +170,11 @@ func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []
164170
subdir = template.GetSubdir()
165171
}
166172
createArgs := create.CreateArgs{
167-
AppName: appNameArg,
168-
Template: template,
169-
GitBranch: createGitBranchFlag,
170-
Subdir: subdir,
173+
AppPath: appPathArg,
174+
DisplayName: displayName,
175+
Template: template,
176+
GitBranch: createGitBranchFlag,
177+
Subdir: subdir,
171178
}
172179
clients.EventTracker.SetAppTemplate(template.GetTemplatePath())
173180

cmd/project/create_test.go

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func TestCreateCommand(t *testing.T) {
7272
template, err := create.ResolveTemplateURL("slack-samples/bolt-js-starter-template")
7373
require.NoError(t, err)
7474
expected := create.CreateArgs{
75-
AppName: "my-app",
75+
AppPath: "my-app",
7676
Template: template,
7777
}
7878
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, expected)
@@ -109,7 +109,7 @@ func TestCreateCommand(t *testing.T) {
109109
template, err := create.ResolveTemplateURL("slack-samples/deno-starter-template")
110110
require.NoError(t, err)
111111
expected := create.CreateArgs{
112-
AppName: "my-deno-app",
112+
AppPath: "my-deno-app",
113113
Template: template,
114114
}
115115
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, expected)
@@ -156,7 +156,7 @@ func TestCreateCommand(t *testing.T) {
156156
require.NoError(t, err)
157157
template.SetSubdir("claude-agent-sdk")
158158
expected := create.CreateArgs{
159-
AppName: "my-agent",
159+
AppPath: "my-agent",
160160
Template: template,
161161
Subdir: "claude-agent-sdk",
162162
}
@@ -203,7 +203,7 @@ func TestCreateCommand(t *testing.T) {
203203
require.NoError(t, err)
204204
template.SetSubdir("claude-agent-sdk")
205205
expected := create.CreateArgs{
206-
AppName: "my-agent-app",
206+
AppPath: "my-agent-app",
207207
Template: template,
208208
Subdir: "claude-agent-sdk",
209209
}
@@ -234,7 +234,7 @@ func TestCreateCommand(t *testing.T) {
234234
require.NoError(t, err)
235235
template.SetSubdir("pydantic-ai")
236236
expected := create.CreateArgs{
237-
AppName: "my-pydantic-app",
237+
AppPath: "my-pydantic-app",
238238
Template: template,
239239
Subdir: "pydantic-ai",
240240
}
@@ -268,7 +268,7 @@ func TestCreateCommand(t *testing.T) {
268268
template, err := create.ResolveTemplateURL("slack-samples/bolt-js-starter-template")
269269
require.NoError(t, err)
270270
expected := create.CreateArgs{
271-
AppName: "agent",
271+
AppPath: "agent",
272272
Template: template,
273273
}
274274
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, expected)
@@ -304,8 +304,9 @@ func TestCreateCommand(t *testing.T) {
304304
template, err := create.ResolveTemplateURL("slack-samples/bolt-js-starter-template")
305305
require.NoError(t, err)
306306
expected := create.CreateArgs{
307-
AppName: "agent",
308-
Template: template,
307+
AppPath: "agent",
308+
DisplayName: "agent",
309+
Template: template,
309310
}
310311
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, expected)
311312
// Verify that category prompt WAS called (shortcut was not triggered)
@@ -351,9 +352,10 @@ func TestCreateCommand(t *testing.T) {
351352
require.NoError(t, err)
352353
template.SetSubdir("claude-agent-sdk")
353354
expected := create.CreateArgs{
354-
AppName: "my-custom-name", // --name flag overrides
355-
Template: template,
356-
Subdir: "claude-agent-sdk",
355+
AppPath: "my-custom-name", // --name flag used as path when no positional arg
356+
DisplayName: "my-custom-name",
357+
Template: template,
358+
Subdir: "claude-agent-sdk",
357359
}
358360
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, expected)
359361
// Verify that category prompt was NOT called (shortcut was triggered)
@@ -387,8 +389,9 @@ func TestCreateCommand(t *testing.T) {
387389
template, err := create.ResolveTemplateURL("slack-samples/bolt-js-starter-template")
388390
require.NoError(t, err)
389391
expected := create.CreateArgs{
390-
AppName: "my-name", // --name flag overrides "my-project" positional arg
391-
Template: template,
392+
AppPath: "my-project", // positional arg preserved as path
393+
DisplayName: "my-name", // --name flag sets manifest display name
394+
Template: template,
392395
}
393396
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, expected)
394397
// Verify that name prompt was NOT called since --name flag was provided
@@ -432,9 +435,10 @@ func TestCreateCommand(t *testing.T) {
432435
require.NoError(t, err)
433436
template.SetSubdir("claude-agent-sdk")
434437
expected := create.CreateArgs{
435-
AppName: "my-name", // --name flag overrides "my-project" positional arg
436-
Template: template,
437-
Subdir: "claude-agent-sdk",
438+
AppPath: "my-project", // positional arg preserved as path
439+
DisplayName: "my-name", // --name flag sets manifest display name
440+
Template: template,
441+
Subdir: "claude-agent-sdk",
438442
}
439443
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, expected)
440444
// Verify that category prompt was NOT called (agent shortcut was triggered)
@@ -505,7 +509,7 @@ func TestCreateCommand(t *testing.T) {
505509
cm.IO.AssertCalled(t, "InputPrompt", mock.Anything, "Name your app:", mock.Anything)
506510
// When the user accepts the default (empty return), the generated name is used
507511
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.MatchedBy(func(args create.CreateArgs) bool {
508-
return args.AppName != ""
512+
return args.AppPath != ""
509513
}))
510514
},
511515
},
@@ -538,7 +542,7 @@ func TestCreateCommand(t *testing.T) {
538542
cm.IO.AssertNotCalled(t, "InputPrompt", mock.Anything, "Name your app:", mock.Anything)
539543
// Should still call Create with a non-empty generated name
540544
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.MatchedBy(func(args create.CreateArgs) bool {
541-
return args.AppName != ""
545+
return args.AppPath != ""
542546
}))
543547
},
544548
},
@@ -569,7 +573,7 @@ func TestCreateCommand(t *testing.T) {
569573
template, err := create.ResolveTemplateURL("slack-samples/bolt-js-starter-template")
570574
require.NoError(t, err)
571575
expected := create.CreateArgs{
572-
AppName: "my-project",
576+
AppPath: "my-project",
573577
Template: template,
574578
}
575579
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, expected)
@@ -615,7 +619,7 @@ func TestCreateCommand(t *testing.T) {
615619
template, err := create.ResolveTemplateURL("slack-samples/bolt-js-starter-template")
616620
require.NoError(t, err)
617621
createClientMock.AssertCalled(t, "Create", mock.Anything, mock.Anything, mock.MatchedBy(func(args create.CreateArgs) bool {
618-
return args.AppName != "" && args.Template == template && args.Subdir == "apps/my-app"
622+
return args.AppPath != "" && args.Template == template && args.Subdir == "apps/my-app"
619623
}))
620624
},
621625
},

cmd/project/samples_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,14 @@ func TestSamplesCommand(t *testing.T) {
7373
)
7474
CreateFunc = func(ctx context.Context, clients *shared.ClientFactory, createArgs createPkg.CreateArgs) (appDirPath string, err error) {
7575
capturedArgs = createArgs
76-
return createArgs.AppName, nil
76+
return createArgs.AppPath, nil
7777
}
7878
},
7979
ExpectedOutputs: []string{
8080
"cd my-sample-app/",
8181
},
8282
ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) {
83-
assert.Equal(t, "my-sample-app", capturedArgs.AppName)
83+
assert.Equal(t, "my-sample-app", capturedArgs.AppPath)
8484
assert.Equal(t, "slack-samples/deno-starter-template", capturedArgs.Template.GetTemplatePath())
8585
},
8686
},

internal/icon/icon.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2022-2026 Salesforce, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package icon
16+
17+
import (
18+
"path/filepath"
19+
20+
"github.com/spf13/afero"
21+
)
22+
23+
func ResolveIconPath(fs afero.Fs, manifestIcon string) string {
24+
if manifestIcon != "" {
25+
return manifestIcon
26+
}
27+
supportedExtensions := []string{".png", ".jpg", ".jpeg", ".gif"}
28+
for _, dir := range []string{"assets", "."} {
29+
for _, ext := range supportedExtensions {
30+
candidate := filepath.Join(dir, "icon"+ext)
31+
if _, err := fs.Stat(candidate); err == nil {
32+
return candidate
33+
}
34+
}
35+
}
36+
return ""
37+
}

internal/icon/icon_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright 2022-2026 Salesforce, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package icon
16+
17+
import (
18+
"testing"
19+
20+
"github.com/spf13/afero"
21+
"github.com/stretchr/testify/assert"
22+
"github.com/stretchr/testify/require"
23+
)
24+
25+
func Test_ResolveIconPath(t *testing.T) {
26+
tests := map[string]struct {
27+
manifestIcon string
28+
files []string
29+
expected string
30+
}{
31+
"manifest icon set returns it directly": {
32+
manifestIcon: "custom/my-icon.png",
33+
expected: "custom/my-icon.png",
34+
},
35+
"manifest icon preferred over magic icon files": {
36+
manifestIcon: "custom/my-icon.png",
37+
files: []string{"assets/icon.png", "icon.png"},
38+
expected: "custom/my-icon.png",
39+
},
40+
"assets/icon.png found": {
41+
files: []string{"assets/icon.png"},
42+
expected: "assets/icon.png",
43+
},
44+
"assets/icon.jpg found": {
45+
files: []string{"assets/icon.jpg"},
46+
expected: "assets/icon.jpg",
47+
},
48+
"assets/icon.jpeg found": {
49+
files: []string{"assets/icon.jpeg"},
50+
expected: "assets/icon.jpeg",
51+
},
52+
"assets/icon.gif found": {
53+
files: []string{"assets/icon.gif"},
54+
expected: "assets/icon.gif",
55+
},
56+
"png wins over other extensions": {
57+
files: []string{"assets/icon.jpg", "assets/icon.jpeg", "assets/icon.gif", "assets/icon.png"},
58+
expected: "assets/icon.png",
59+
},
60+
"jpg wins over gif in assets": {
61+
files: []string{"assets/icon.jpg", "assets/icon.gif"},
62+
expected: "assets/icon.jpg",
63+
},
64+
"root icon.png found when no assets": {
65+
files: []string{"icon.png"},
66+
expected: "icon.png",
67+
},
68+
"root icon.jpg found when no assets": {
69+
files: []string{"icon.jpg"},
70+
expected: "icon.jpg",
71+
},
72+
"assets takes priority over root": {
73+
files: []string{"assets/icon.gif", "icon.png"},
74+
expected: "assets/icon.gif",
75+
},
76+
"no icon files returns empty": {
77+
files: []string{},
78+
expected: "",
79+
},
80+
}
81+
for name, tc := range tests {
82+
t.Run(name, func(t *testing.T) {
83+
fs := afero.NewMemMapFs()
84+
for _, f := range tc.files {
85+
require.NoError(t, afero.WriteFile(fs, f, []byte("img"), 0o644))
86+
}
87+
result := ResolveIconPath(fs, tc.manifestIcon)
88+
assert.Equal(t, tc.expected, result)
89+
})
90+
}
91+
}

0 commit comments

Comments
 (0)