-
Notifications
You must be signed in to change notification settings - Fork 39
Expand file tree
/
Copy pathcreate.go
More file actions
330 lines (295 loc) · 11.6 KB
/
Copy pathcreate.go
File metadata and controls
330 lines (295 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
// Copyright 2022-2026 Salesforce, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package project
import (
"context"
"fmt"
"math/rand"
"os"
"path/filepath"
"strings"
"time"
"github.com/slackapi/slack-cli/cmd/app"
"github.com/slackapi/slack-cli/internal/iostreams"
"github.com/slackapi/slack-cli/internal/manifest"
"github.com/slackapi/slack-cli/internal/pkg/create"
"github.com/slackapi/slack-cli/internal/shared"
"github.com/slackapi/slack-cli/internal/shared/types"
"github.com/slackapi/slack-cli/internal/slackerror"
"github.com/slackapi/slack-cli/internal/slacktrace"
"github.com/slackapi/slack-cli/internal/style"
"github.com/spf13/cobra"
)
// Flags
var createAppNameFlag string
var createEnvironmentFlag string
var createGitBranchFlag string
var createListFlag bool
var createSubdirFlag string
var createTemplateURLFlag string
// Handle to client's create function used for testing
// TODO - Find best practice, such as using an Interface and Struct to create a client
var CreateFunc = create.Create
// promptObject describes the Github app template
type promptObject struct {
Title string // "Reverse string"
Repository string // "slack-samples/reverse-string"
Description string // "A function that reverses a given string"
Subdir string // "agents/hello-world" - subdirectory within the repository
}
const viewMoreSamples = "slack-cli#view-more-samples"
func NewCreateCommand(clients *shared.ClientFactory) *cobra.Command {
cmd := &cobra.Command{
SuggestFor: []string{"new"},
Use: "create [name | agent <name>] [flags]",
Short: "Create a new Slack project",
Long: `Create a new Slack project on your local machine from an optional template.
The 'agent' argument is a shortcut to create an AI Agent app. If you want to
name your app 'agent' (not create an AI Agent), use the --name flag instead.`,
Example: style.ExampleCommandsf([]style.ExampleCommand{
{Command: "create my-project", Meaning: "Create a new project from a template"},
{Command: "create agent my-agent-app", Meaning: "Create a new AI Agent app"},
{Command: "create my-project -t slack-samples/deno-hello-world", Meaning: "Start a new project from a specific template"},
{Command: "create --name my-project", Meaning: "Create a project named 'my-project'"},
{Command: "create my-project -t org/monorepo --subdir apps/my-app", Meaning: "Create from a subdirectory of a template"},
{Command: "create my-project -t slack-samples/bolt-js-starter-template --app A0123456789 --environment local", Meaning: "Create from template and link to an existing app"},
}),
Args: cobra.MaximumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
clients.Config.SetFlags(cmd)
return runCreateCommand(clients, cmd, args)
},
}
// Add flags
cmd.Flags().StringVarP(&createTemplateURLFlag, "template", "t", "", "template URL for your app")
cmd.Flags().StringVarP(&createGitBranchFlag, "branch", "b", "", "name of git branch to checkout")
cmd.Flags().StringVarP(&createAppNameFlag, "name", "n", "", "name for your app (overrides the name argument)")
cmd.Flags().BoolVar(&createListFlag, "list", false, "list available app templates")
cmd.Flags().StringVar(&createSubdirFlag, "subdir", "", "subdirectory in the template to use as project")
cmd.Flags().StringVarP(&createEnvironmentFlag, "environment", "E", "", "environment to save existing app (local, deployed)")
return cmd
}
func runCreateCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// Get optional app name passed as an arg and check for category shortcuts
appPathArg := ""
categoryShortcut := ""
templateFlagProvided := cmd.Flags().Changed("template")
nameFlagProvided := cmd.Flags().Changed("name")
if len(args) > 0 {
switch args[0] {
case "samples", "create":
// These are special commands, not app names
case "agent":
// Only treat as shortcut if --template flag is not provided
if !templateFlagProvided {
// Shortcut to AI apps category
categoryShortcut = "agent"
// Check if a second argument was provided as the app name
if len(args) > 1 {
appPathArg = args[1]
}
} else {
// When --template is provided, "agent" is the app name
appPathArg = args[0]
}
default:
appPathArg = args[0]
}
}
// List templates and exit early if the --list flag is set
if createListFlag {
return listTemplates(ctx, clients, categoryShortcut)
}
// --subdir requires --template
if cmd.Flags().Changed("subdir") && !templateFlagProvided {
return slackerror.New(slackerror.ErrMismatchedFlags).
WithMessage("The --subdir flag requires the --template flag")
}
// --app must be an app ID when used with create
appFlagProvided := clients.Config.AppFlag != ""
if appFlagProvided && !types.IsAppID(clients.Config.AppFlag) {
return slackerror.New(slackerror.ErrInvalidAppFlag).
WithMessage("The --app flag requires an app ID when used with create")
}
// --app requires --template
if appFlagProvided && !templateFlagProvided {
return slackerror.New(slackerror.ErrMismatchedFlags).
WithMessage("The --app flag requires the --template flag when used with create")
}
// --environment requires --app and must be "local" or "deployed"
if cmd.Flags().Changed("environment") {
if !appFlagProvided {
return slackerror.New(slackerror.ErrMismatchedFlags).
WithMessage("The --environment flag requires the --app flag when used with create")
}
if !types.IsAppFlagEnvironment(createEnvironmentFlag) {
return slackerror.New(slackerror.ErrMismatchedFlags).
WithMessage("The --environment flag must be either 'local' or 'deployed'")
}
}
// Collect the template URL or select a starting template
template, err := promptTemplateSelection(cmd, clients, categoryShortcut)
if err != nil {
return err
}
// --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.
displayName := ""
if nameFlagProvided {
displayName = createAppNameFlag
if appPathArg == "" {
appPathArg = createAppNameFlag
}
}
// Prompt for app name if not provided via flag or argument
if appPathArg == "" {
if clients.IO.IsTTY() {
defaultName := generateRandomAppName()
name, err := clients.IO.InputPrompt(ctx, "Name your app:", iostreams.InputPromptConfig{
Placeholder: defaultName,
})
if err != nil {
return err
}
if name != "" {
appPathArg = name
} else {
appPathArg = defaultName
}
} else {
appPathArg = generateRandomAppName()
}
}
subdir := createSubdirFlag
if subdir == "" {
subdir = template.GetSubdir()
}
createArgs := create.CreateArgs{
AppPath: appPathArg,
DisplayName: displayName,
Template: template,
GitBranch: createGitBranchFlag,
Subdir: subdir,
}
clients.EventTracker.SetAppTemplate(template.GetTemplatePath())
appDirPath, err := CreateFunc(ctx, clients, createArgs)
if err != nil {
return err
}
if appFlagProvided {
absProjectPath, err := filepath.Abs(appDirPath)
if err != nil {
return slackerror.Wrap(err, slackerror.ErrAppDirectoryAccess)
}
originalDir, err := clients.Os.Getwd()
if err != nil {
return slackerror.Wrap(err, slackerror.ErrAppDirectoryAccess)
}
if err := os.Chdir(absProjectPath); err != nil {
return slackerror.Wrap(err, slackerror.ErrAppDirectoryAccess)
}
defer func() {
_ = os.Chdir(originalDir)
}()
linkedApp := &types.App{}
if err := app.LinkExistingApp(ctx, clients, linkedApp); err != nil {
return err
}
if linkedApp.AppID != "" {
auth, err := clients.Auth().AuthWithTeamID(ctx, linkedApp.TeamID)
if err != nil {
return err
}
fetchErr := manifest.SetManifestLocal(ctx, clients, auth.Token, linkedApp.AppID, absProjectPath)
if fetchErr != nil {
clients.IO.PrintWarning(ctx, "%s", style.Sectionf(style.TextSection{
Text: "Could not fetch the remote app manifest",
Secondary: []string{
fetchErr.Error(),
"The template manifest was kept unchanged",
},
}))
} else {
clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{
Emoji: "pencil2",
Text: "Manifest",
Secondary: []string{"Written to manifest.json from remote app settings"},
}))
}
}
clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{
Emoji: "house",
Text: "App",
Secondary: app.FormatListSuccess([]types.App{*linkedApp}),
}))
}
printCreateSuccess(ctx, clients, appDirPath)
return nil
}
// printCreateSuccess outputs an informative message after creating a new app
func printCreateSuccess(ctx context.Context, clients *shared.ClientFactory, appPath string) {
// Check if this is a Deno project to conditionally enable some features
var isDenoProject = false
if clients.Runtime != nil {
isDenoProject = strings.Contains(strings.ToLower(clients.Runtime.Name()), "deno")
}
// Include documentation and information about ROSI for deno apps
if isDenoProject {
clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{
Emoji: "compass",
Text: "Explore the documentation to learn more",
Secondary: []string{
"Read the README.md or peruse the docs over at " + style.Highlight("https://docs.slack.dev/tools/deno-slack-sdk"),
"Find available commands and usage info with " + style.Commandf("help", false),
},
}))
clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{
Emoji: "clipboard",
Text: "Follow the steps below to begin development",
Secondary: []string{
"Change into your project directory with " + style.CommandText(fmt.Sprintf("cd %s%s", appPath, string(filepath.Separator))),
"Develop locally and see changes in real-time with " + style.Commandf("run", true),
"When you're ready to deploy for production with " + style.Commandf("deploy", true),
},
}))
} else {
var secondaryOutput []string
// Output about the README.md
if _, err := clients.Fs.Stat(filepath.Join(appPath, "README.md")); !clients.Os.IsNotExist(err) {
secondaryOutput = append(secondaryOutput, "Learn more about the project in the "+style.Highlight("README.md"))
}
// Output about general usage
secondaryOutput = append(secondaryOutput,
"Change into your project with "+style.CommandText(fmt.Sprintf("cd %s%s", appPath, string(filepath.Separator))),
"Start developing and see changes in real-time with "+style.Commandf("run", true),
)
clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{
Emoji: "clipboard",
Text: "Next Steps",
Secondary: secondaryOutput,
}))
}
clients.IO.PrintTrace(ctx, slacktrace.CreateSuccess)
}
// generateRandomAppName will create a random app name based on two words and a number
func generateRandomAppName() string {
rand.New(rand.NewSource(time.Now().UnixNano()))
var firstRandomNum = rand.Intn(len(create.Adjectives))
var secondRandomNum = rand.Intn(len(create.Animals))
var randomName = fmt.Sprintf("%s-%s-%d", create.Adjectives[firstRandomNum], create.Animals[secondRandomNum], rand.Intn(1000))
return randomName
}