Skip to content

Commit 2bafbd9

Browse files
authored
Merge pull request #2033 from onflow/cf/project-dir-creation
Allow use of `flow init` for existing directories
2 parents dbfdac5 + 6dc532a commit 2bafbd9

3 files changed

Lines changed: 254 additions & 64 deletions

File tree

cmd/flow/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func main() {
6767
test.TestCommand.AddToParent(cmd)
6868

6969
// super commands
70-
super.SetupCommand.AddToParent(cmd)
70+
super.InitCommand.AddToParent(cmd)
7171
super.DevCommand.AddToParent(cmd)
7272

7373
// structured commands
Lines changed: 117 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ package super
2121
import (
2222
"bytes"
2323
"fmt"
24-
"io"
2524
"os"
2625
"path/filepath"
26+
"strings"
2727

2828
"github.com/onflow/flowkit/v2"
2929

@@ -42,22 +42,32 @@ import (
4242
"github.com/onflow/flow-cli/internal/util"
4343
)
4444

45-
type flagsSetup struct {
45+
type flagsInit struct {
4646
ConfigOnly bool `default:"false" flag:"config-only" info:"Only create a flow.json default config"`
4747
}
4848

49-
var setupFlags = flagsSetup{}
49+
var initFlags = flagsInit{}
50+
51+
const (
52+
// File permissions for created directories
53+
defaultDirPerm = 0755
54+
// Core Flow project files that indicate an existing Flow project
55+
flowConfigFile = "flow.json"
56+
// README files
57+
defaultReadmeFile = "README.md"
58+
flowReadmeFile = "README_flow.md"
59+
)
5060

5161
// TODO: Add --config-only flag
52-
var SetupCommand = &command.Command{
62+
var InitCommand = &command.Command{
5363
Cmd: &cobra.Command{
5464
Use: "init <project name>",
5565
Short: "Start a new Flow project",
5666
Example: "flow init my-project",
5767
Args: cobra.MaximumNArgs(1),
5868
GroupID: "super",
5969
},
60-
Flags: &setupFlags,
70+
Flags: &initFlags,
6171
Run: create,
6272
}
6373

@@ -71,7 +81,7 @@ func create(
7181
var targetDir string
7282
var err error
7383

74-
if setupFlags.ConfigOnly {
84+
if initFlags.ConfigOnly {
7585
if len(args) > 0 {
7686
return nil, fmt.Errorf("project name not required when using --config-only flag")
7787
}
@@ -85,13 +95,61 @@ func create(
8595

8696
return nil, nil
8797
} else {
88-
targetDir, err = startInteractiveSetup(args, logger)
98+
targetDir, err = startInteractiveInit(args, logger)
8999
if err != nil {
90100
return nil, err
91101
}
92102
}
93103

94-
return &setupResult{targetDir: targetDir}, nil
104+
return &initResult{targetDir: targetDir}, nil
105+
}
106+
107+
func validateCurrentDirectoryForInit() error {
108+
pwd, err := os.Getwd()
109+
if err != nil {
110+
return err
111+
}
112+
113+
// Only check for core Flow project files that would cause real conflicts
114+
coreFlowPaths := []string{
115+
flowConfigFile,
116+
cadenceDir,
117+
}
118+
119+
var conflicts []string
120+
for _, path := range coreFlowPaths {
121+
fullPath := filepath.Join(pwd, path)
122+
if _, err := os.Stat(fullPath); err == nil {
123+
conflicts = append(conflicts, path)
124+
}
125+
}
126+
127+
if len(conflicts) > 0 {
128+
return fmt.Errorf("Flow project files already exist: %s. Cannot initialize Flow project in directory with existing Flow files", strings.Join(conflicts, ", "))
129+
}
130+
131+
return nil
132+
}
133+
134+
// resolveTargetDirectory determines the target directory for the Flow project
135+
// based on user input. Empty input means current directory.
136+
func resolveTargetDirectory(userInput string) (string, error) {
137+
if strings.TrimSpace(userInput) == "" {
138+
// Validate current directory for Flow project conflicts
139+
if err := validateCurrentDirectoryForInit(); err != nil {
140+
return "", err
141+
}
142+
143+
// Use current directory
144+
pwd, err := os.Getwd()
145+
if err != nil {
146+
return "", fmt.Errorf("failed to get current working directory: %w", err)
147+
}
148+
return pwd, nil
149+
}
150+
151+
// Use provided name to create new directory
152+
return getTargetDirectory(userInput)
95153
}
96154

97155
func updateGitignore(targetDir string, readerWriter flowkit.ReaderWriter) error {
@@ -133,7 +191,7 @@ func createConfigOnly(targetDir string, readerWriter flowkit.ReaderWriter) error
133191
return nil
134192
}
135193

136-
func startInteractiveSetup(
194+
func startInteractiveInit(
137195
args []string,
138196
logger output.Logger,
139197
) (string, error) {
@@ -155,22 +213,20 @@ func startInteractiveSetup(
155213
Fs: afero.NewOsFs(),
156214
}
157215

158-
// Ask for project name if not given
216+
// Resolve target directory from arguments or user input
217+
var userInput string
159218
if len(args) < 1 {
160-
userInput, err := prompt.RunTextInput("Enter the name of your project", "Type your project name here...")
219+
userInput, err = prompt.RunTextInput("Enter the name of your project (leave blank to use current directory)", "Type your project name here or press Enter for current directory...")
161220
if err != nil {
162221
return "", fmt.Errorf("error running project name: %v", err)
163222
}
164-
165-
targetDir, err = getTargetDirectory(userInput)
166-
if err != nil {
167-
return "", err
168-
}
169223
} else {
170-
targetDir, err = getTargetDirectory(args[0])
171-
if err != nil {
172-
return "", err
173-
}
224+
userInput = args[0]
225+
}
226+
227+
targetDir, err = resolveTargetDirectory(userInput)
228+
if err != nil {
229+
return "", err
174230
}
175231

176232
// Create a temp directory which will later be moved to the target directory if successful
@@ -212,6 +268,9 @@ func startInteractiveSetup(
212268
// cadence/tests/DefaultContract_test.cdc
213269
// README.md
214270

271+
// Determine README filename - avoid conflicts with existing README.md
272+
readmeFileName := getReadmeFileName(targetDir)
273+
215274
templates := []generator.TemplateItem{
216275
generator.ContractTemplate{
217276
Name: "Counter",
@@ -229,7 +288,7 @@ func startInteractiveSetup(
229288
},
230289
generator.FileTemplate{
231290
TemplatePath: "README.md.tmpl",
232-
TargetPath: "README.md",
291+
TargetPath: readmeFileName,
233292
Data: map[string]interface{}{
234293
"Dependencies": (func() []map[string]interface{} {
235294
contracts := []map[string]interface{}{}
@@ -277,69 +336,64 @@ func startInteractiveSetup(
277336
return "", err
278337
}
279338

280-
// Move the temp directory to the target directory
281-
err = os.Rename(tempDir, targetDir)
282-
if err != nil {
283-
return "", fmt.Errorf("failed to move temp directory to target directory: %w", err)
284-
}
285-
286-
return targetDir, nil
287-
}
288-
289-
// getTargetDirectory checks if the specified directory path is suitable for use.
290-
// It verifies that the path points to an existing, empty directory.
291-
// If the directory does not exist, the function returns the path without error,
292-
// indicating that the path is available for use (assuming creation is handled elsewhere).
293-
func getTargetDirectory(directory string) (string, error) {
294-
pwd, err := os.Getwd()
295-
if err != nil {
296-
return "", err
297-
}
298-
299-
target := filepath.Join(pwd, directory)
300-
info, err := os.Stat(target)
301-
if !os.IsNotExist(err) {
302-
if !info.IsDir() {
303-
return "", fmt.Errorf("%s is a file", target)
304-
}
305-
306-
file, err := os.Open(target)
339+
// Move or copy the temp directory contents to the target directory
340+
pwd, _ := os.Getwd()
341+
if targetDir == pwd {
342+
// For current directory, copy contents instead of moving the directory
343+
err = copyDirContents(tempDir, targetDir)
307344
if err != nil {
308-
return "", err
345+
return "", fmt.Errorf("failed to copy temp directory contents to current directory: %w", err)
309346
}
310-
defer file.Close()
311-
312-
_, err = file.Readdirnames(1)
313-
if err != io.EOF {
314-
return "", fmt.Errorf("directory is not empty: %s", target)
347+
} else {
348+
// For new directory, move the entire temp directory
349+
err = os.Rename(tempDir, targetDir)
350+
if err != nil {
351+
return "", fmt.Errorf("failed to move temp directory to target directory: %w", err)
315352
}
316353
}
317-
return target, nil
354+
355+
return targetDir, nil
318356
}
319357

320-
type setupResult struct {
358+
type initResult struct {
321359
targetDir string
322360
}
323361

324-
func (s *setupResult) String() string {
362+
func (s *initResult) String() string {
325363
wd, _ := os.Getwd()
326364
relDir, _ := filepath.Rel(wd, s.targetDir)
327365
out := bytes.Buffer{}
328366

329367
out.WriteString(fmt.Sprintf("%s Congrats! your project was created.\n\n", output.SuccessEmoji()))
368+
369+
// Check if we created README_flow.md instead of README.md
370+
readmeFile := defaultReadmeFile
371+
if _, err := os.Stat(filepath.Join(s.targetDir, flowReadmeFile)); err == nil {
372+
readmeFile = flowReadmeFile
373+
out.WriteString("📝 Note: Created README_flow.md since README.md already exists.\n\n")
374+
}
375+
330376
out.WriteString("Start development by following these steps:\n")
331-
out.WriteString(fmt.Sprintf("1. '%s' to change to your new project,\n", output.Bold(fmt.Sprintf("cd %s", relDir))))
332-
out.WriteString(fmt.Sprintf("2. '%s' or run Flowser to start the emulator,\n", output.Bold("flow emulator")))
333-
out.WriteString(fmt.Sprintf("3. '%s' to test your project.\n\n", output.Bold("flow test")))
334-
out.WriteString(fmt.Sprintf("You should also read README.md to learn more about the development process!\n"))
377+
378+
// Only show cd command if not current directory
379+
if s.targetDir != wd {
380+
out.WriteString(fmt.Sprintf("1. '%s' to change to your new project,\n", output.Bold(fmt.Sprintf("cd %s", relDir))))
381+
out.WriteString(fmt.Sprintf("2. '%s' to start the emulator,\n", output.Bold("flow emulator")))
382+
out.WriteString(fmt.Sprintf("3. '%s' to test your project.\n\n", output.Bold("flow test")))
383+
} else {
384+
out.WriteString(fmt.Sprintf("1. '%s' to start the emulator,\n", output.Bold("flow emulator")))
385+
out.WriteString(fmt.Sprintf("2. '%s' to test your project.\n\n", output.Bold("flow test")))
386+
}
387+
388+
out.WriteString(fmt.Sprintf("You should also read %s to learn more about the development process!\n", readmeFile))
335389

336390
return out.String()
337391
}
338392

339-
func (s *setupResult) Oneliner() string {
393+
func (s *initResult) Oneliner() string {
340394
return fmt.Sprintf("Project created inside %s", s.targetDir)
341395
}
342396

343-
func (s *setupResult) JSON() any {
397+
func (s *initResult) JSON() any {
344398
return nil
345399
}

0 commit comments

Comments
 (0)