Repository: onflow/flow-cli (go)

Issue:
Command to create scripts/tx boilerplate
When developing you're often having to create a new file manually or with `touch` and then add the basic boilerplate for each type:

```
// Script Boilerplate

pub fun main() {}
```

```
// Tx Boilerplate

transaction() {
    prepare() {}

    execute {}
}
```

It would be nice if you could run a command like the following:

```
flow create tx CreateNFT
flow create script ReadNFT
```

This would then ouput the following with the boilerplate:

```
cadence/transactions/CreateNFT.cdc
cadence/scripts/ReadNFT.cdc
```

Pull request description:
Add generate templates command
Closes #1233 

______

For contributor use:

- [x] Targeted PR against `master` branch
- [x] Linked to Github issue with discussion and accepted design OR link to spec that describes this work
- [x] Code follows the [standards mentioned here](https://github.com/onflow/flow-cli/blob/master/CONTRIBUTING.md#styleguides)
- [ ] Updated relevant documentation
- [x] Re-reviewed `Files changed` in the Github PR explorer
- [x] Added appropriate labels


Patch:
diff --git a/README.md b/README.md
index bfe0ac86..7ad66645 100644
--- a/README.md
+++ b/README.md
@@ -36,23 +36,42 @@ Read about supported commands in the [CLI documentation website](https://develop
 Usage:
   flow [command]
 
-Available Commands:
-  accounts     Utilities to manage accounts
-  blocks       Utilities to read blocks
+👋 Welcome Flow developer!
+   If you are starting a new flow project try running 'flow setup <project_name>'. 
+
+🔥 Super Commands
+  dev          Build your Flow project
+  flix         Execute FLIX template with a given id, name, or local filename
+  generate     Generate new boilerplate files
+  setup        Start a new Flow project
+
+📦 Flow Entities
+  accounts     Create and retrieve accounts and deploy contracts
+  blocks       Retrieve blocks
+  collections  Retrieve collections
+  events       Retrieve events
+
+💬 Flow Interactions
+  scripts      Execute Cadence scripts
+  transactions Build, sign, send and retrieve transactions
+
+🔨 Flow Tools
   cadence      Execute Cadence code
-  collections  Utilities to read collections
-  config       Utilities to manage configuration
-  emulator     Starts the Flow emulator server
+  dev-wallet   Run a development wallet
+  emulator     Run Flow network for development
+  flowser      Run Flowser project explorer
   test         Run Cadence tests
-  events       Utilities to read events
-  help         Help about any command
+
+🏄 Flow Project
+  deploy       Deploy all project contracts
   init         Initialize a new configuration
-  keys         Utilities to manage keys
   project      Manage your Cadence project
-  scripts      Utilities to execute scripts
-  status       Display the status of the Flow network
-  transactions Utilities to send transactions
-  version      View version and commit information
+  run          Start emulator and deploy all project contracts
+
+🔒 Flow Security
+  keys         Generate and decode Flow keys
+  signatures   Signature verification and creation
+
 ```
 
 The Flow CLI includes several commands to interact with Flow networks, such as querying account information, or sending transactions. It also includes the [Flow Emulator](https://developers.flow.com/tools/emulator).
diff --git a/cmd/flow/main.go b/cmd/flow/main.go
index ce35fd9a..1b77738b 100644
--- a/cmd/flow/main.go
+++ b/cmd/flow/main.go
@@ -86,6 +86,7 @@ func main() {
 	cmd.AddCommand(signatures.Cmd)
 	cmd.AddCommand(snapshot.Cmd)
 	cmd.AddCommand(super.FlixCmd)
+	cmd.AddCommand(super.GenerateCommand)
 
 	command.InitFlags(cmd)
 	cmd.AddGroup(&cobra.Group{
diff --git a/flowkit/state.go b/flowkit/state.go
index 57967348..4832ab87 100644
--- a/flowkit/state.go
+++ b/flowkit/state.go
@@ -37,6 +37,7 @@ import (
 type ReaderWriter interface {
 	ReadFile(source string) ([]byte, error)
 	WriteFile(filename string, data []byte, perm os.FileMode) error
+	MkdirAll(path string, perm os.FileMode) error
 }
 
 // State manages the state for a Flow project.
diff --git a/internal/super/generate.go b/internal/super/generate.go
new file mode 100644
index 00000000..e6d51401
--- /dev/null
+++ b/internal/super/generate.go
@@ -0,0 +1,204 @@
+/*
+ * Flow CLI
+ *
+ * Copyright 2019 Dapper Labs, 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 super
+
+import (
+	"fmt"
+	"path/filepath"
+	"strings"
+
+	"github.com/onflow/flow-cli/flowkit/config"
+
+	"github.com/onflow/flow-cli/flowkit"
+
+	"github.com/onflow/flow-cli/flowkit/output"
+	"github.com/onflow/flow-cli/internal/command"
+
+	"github.com/spf13/cobra"
+)
+
+type generateFlagsDef struct {
+	Directory string `default:"" flag:"dir" info:"Directory to generate files in"`
+}
+
+var generateFlags = generateFlagsDef{}
+
+var GenerateCommand = &cobra.Command{
+	Use:     "generate",
+	Short:   "Generate new boilerplate files",
+	GroupID: "super",
+	Aliases: []string{"g"},
+}
+
+var GenerateContractCommand = &command.Command{
+	Cmd: &cobra.Command{
+		Use:     "contract <name>",
+		Short:   "Generate a new contract",
+		Example: "flow generate contract HelloWorld",
+		Args:    cobra.ExactArgs(1),
+	},
+	Flags: &generateFlags,
+	RunS:  generateContract,
+}
+
+var GenerateTransactionCommand = &command.Command{
+	Cmd: &cobra.Command{
+		Use:     "transaction <name>",
+		Short:   "Generate a new transaction",
+		Example: "flow generate transaction SomeTransaction",
+		Args:    cobra.ExactArgs(1),
+	},
+	Flags: &generateFlags,
+	RunS:  generateTransaction,
+}
+
+var GenerateScriptCommand = &command.Command{
+	Cmd: &cobra.Command{
+		Use:     "script <name>",
+		Short:   "Generate a new script",
+		Example: "flow generate script SomeScript",
+		Args:    cobra.ExactArgs(1),
+	},
+	Flags: &generateFlags,
+	RunS:  generateScript,
+}
+
+func init() {
+	GenerateContractCommand.AddToParent(GenerateCommand)
+	GenerateTransactionCommand.AddToParent(GenerateCommand)
+	GenerateScriptCommand.AddToParent(GenerateCommand)
+}
+
+func generateContract(
+	args []string,
+	_ command.GlobalFlags,
+	logger output.Logger,
+	flow flowkit.Services,
+	state *flowkit.State,
+) (result command.Result, err error) {
+	return generateNew(args, "contract", logger, state)
+}
+
+func generateTransaction(
+	args []string,
+	_ command.GlobalFlags,
+	logger output.Logger,
+	flow flowkit.Services,
+	state *flowkit.State,
+) (result command.Result, err error) {
+	return generateNew(args, "transaction", logger, state)
+}
+
+func generateScript(
+	args []string,
+	_ command.GlobalFlags,
+	logger output.Logger,
+	flow flowkit.Services,
+	state *flowkit.State,
+) (result command.Result, err error) {
+	return generateNew(args, "script", logger, state)
+}
+
+func generateNew(
+	args []string,
+	templateType string,
+	logger output.Logger,
+	state *flowkit.State,
+) (result command.Result, err error) {
+	if len(args) < 1 {
+		return nil, fmt.Errorf("invalid number of arguments")
+	}
+
+	name := args[0]
+	var filename string
+
+	// Don't add .cdc extension if it's already there
+	if strings.HasSuffix(name, ".cdc") {
+		filename = name
+	} else {
+		filename = fmt.Sprintf("%s.cdc", name)
+	}
+
+	var fileToWrite string
+	var basePath string
+
+	if generateFlags.Directory != "" {
+		basePath = generateFlags.Directory
+	} else {
+		switch templateType {
+		case "contract":
+			basePath = "cadence/contracts"
+		case "script":
+			basePath = "cadence/scripts"
+		case "transaction":
+			basePath = "cadence/transactions"
+		default:
+			return nil, fmt.Errorf("invalid template type: %s", templateType)
+		}
+	}
+
+	switch templateType {
+	case "contract":
+		fileToWrite = fmt.Sprintf(`
+access(all) contract %s {
+    init() {}
+}`, name)
+	case "script":
+		fileToWrite = `access(all) fun main() {
+    // Script details here
+}`
+	case "transaction":
+		fileToWrite = `transaction() {
+    prepare(account:AuthAccount) {}
+
+    execute {}
+}`
+	default:
+		return nil, fmt.Errorf("invalid template type: %s", templateType)
+	}
+
+	filenameWithBasePath := filepath.Join(basePath, filename)
+
+	// Check file existence
+	if _, err := state.ReaderWriter().ReadFile(filenameWithBasePath); err == nil {
+		return nil, fmt.Errorf("file already exists: %s", filenameWithBasePath)
+	}
+
+	// Ensure the directory exists
+	if err := state.ReaderWriter().MkdirAll(basePath, 0755); err != nil {
+		return nil, fmt.Errorf("error creating directories: %w", err)
+	}
+
+	err = state.ReaderWriter().WriteFile(filenameWithBasePath, []byte(fileToWrite), 0644)
+	if err != nil {
+		return nil, fmt.Errorf("error writing file: %w", err)
+	}
+
+	logger.Info(fmt.Sprintf("Generated new %s: %s at %s", templateType, name, filenameWithBasePath))
+
+	if templateType == "contract" {
+		state.Contracts().AddOrUpdate(config.Contract{Name: name, Location: filenameWithBasePath})
+		err = state.SaveDefault()
+		if err != nil {
+			return nil, fmt.Errorf("error saving to flow.json: %w", err)
+		}
+	}
+
+	return nil, err
+}


Test patch:
diff --git a/internal/super/generate_test.go b/internal/super/generate_test.go
new file mode 100644
index 00000000..84d56a29
--- /dev/null
+++ b/internal/super/generate_test.go
@@ -0,0 +1,192 @@
+/*
+ * Flow CLI
+ *
+ * Copyright 2019 Dapper Labs, 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 super
+
+import (
+	"os"
+	"testing"
+
+	"github.com/onflow/flow-cli/internal/util"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/onflow/flow-cli/flowkit/output"
+)
+
+func TestGenerateNewContract(t *testing.T) {
+	dir, err := os.MkdirTemp("", "test")
+	assert.NoError(t, err, "Failed to create temp dir")
+	defer os.RemoveAll(dir)
+
+	if err := os.Chdir(dir); err != nil {
+		t.Fatalf("Failed to change directory: %v", err)
+	}
+
+	logger := output.NewStdoutLogger(output.NoneLog)
+	_, state, _ := util.TestMocks(t)
+
+	// Test contract generation
+	_, err = generateNew([]string{"TestContract"}, "contract", logger, state)
+	assert.NoError(t, err, "Failed to generate contract")
+
+	fileContent, err := state.ReaderWriter().ReadFile("cadence/contracts/TestContract.cdc")
+	assert.NoError(t, err, "Failed to read generated file")
+	assert.NotNil(t, fileContent)
+
+	// Check content is correct
+	expectedContent := `
+access(all) contract TestContract {
+    init() {}
+}`
+	assert.Equal(t, expectedContent, string(fileContent))
+
+	// Test file already exists scenario
+	_, err = generateNew([]string{"TestContract"}, "contract", logger, state)
+	assert.Error(t, err)
+	assert.Equal(t, "file already exists: cadence/contracts/TestContract.cdc", err.Error())
+}
+
+func TestGenerateNewContractFileAlreadyExists(t *testing.T) {
+	dir, err := os.MkdirTemp("", "test")
+	assert.NoError(t, err, "Failed to create temp dir")
+	defer os.RemoveAll(dir)
+
+	if err := os.Chdir(dir); err != nil {
+		t.Fatalf("Failed to change directory: %v", err)
+	}
+
+	logger := output.NewStdoutLogger(output.NoneLog)
+	_, state, _ := util.TestMocks(t)
+
+	// Test contract generation
+	_, err = generateNew([]string{"TestContract"}, "contract", logger, state)
+	assert.NoError(t, err, "Failed to generate contract")
+
+	//// Check if the file exists in the correct directory
+	content, err := state.ReaderWriter().ReadFile("cadence/contracts/TestContract.cdc")
+	assert.NoError(t, err, "Failed to read generated file")
+	assert.NotNil(t, content)
+
+	// Test file already exists scenario
+	_, err = generateNew([]string{"TestContract"}, "contract", logger, state)
+	assert.Error(t, err)
+	assert.Equal(t, "file already exists: cadence/contracts/TestContract.cdc", err.Error())
+}
+
+func TestGenerateNewContractWithFileExtension(t *testing.T) {
+	dir, err := os.MkdirTemp("", "test")
+	assert.NoError(t, err, "Failed to create temp dir")
+	defer os.RemoveAll(dir)
+
+	if err := os.Chdir(dir); err != nil {
+		t.Fatalf("Failed to change directory: %v", err)
+	}
+
+	logger := output.NewStdoutLogger(output.NoneLog)
+	_, state, _ := util.TestMocks(t)
+
+	_, err = generateNew([]string{"TestContract.cdc"}, "contract", logger, state)
+	assert.NoError(t, err, "Failed to generate contract")
+
+	// Check file exists
+	content, err := state.ReaderWriter().ReadFile("cadence/contracts/TestContract.cdc")
+	assert.NoError(t, err, "Failed to read generated file")
+	assert.NotNil(t, content)
+}
+
+func TestGenerateNewScript(t *testing.T) {
+	dir, err := os.MkdirTemp("", "test")
+	assert.NoError(t, err, "Failed to create temp dir")
+	defer os.RemoveAll(dir)
+
+	if err := os.Chdir(dir); err != nil {
+		t.Fatalf("Failed to change directory: %v", err)
+	}
+
+	logger := output.NewStdoutLogger(output.NoneLog)
+	_, state, _ := util.TestMocks(t)
+
+	_, err = generateNew([]string{"TestScript"}, "script", logger, state)
+	assert.NoError(t, err, "Failed to generate contract")
+
+	content, err := state.ReaderWriter().ReadFile("cadence/scripts/TestScript.cdc")
+	assert.NoError(t, err, "Failed to read generated file")
+	assert.NotNil(t, content)
+
+	expectedContent := `access(all) fun main() {
+    // Script details here
+}`
+	assert.Equal(t, expectedContent, string(content))
+}
+
+func TestGenerateNewTransaction(t *testing.T) {
+	dir, err := os.MkdirTemp("", "test")
+	assert.NoError(t, err, "Failed to create temp dir")
+	defer os.RemoveAll(dir)
+
+	if err := os.Chdir(dir); err != nil {
+		t.Fatalf("Failed to change directory: %v", err)
+	}
+
+	logger := output.NewStdoutLogger(output.NoneLog)
+	_, state, _ := util.TestMocks(t)
+
+	_, err = generateNew([]string{"TestTransaction"}, "transaction", logger, state)
+	assert.NoError(t, err, "Failed to generate contract")
+
+	content, err := state.ReaderWriter().ReadFile("cadence/transactions/TestTransaction.cdc")
+	assert.NoError(t, err, "Failed to read generated file")
+	assert.NotNil(t, content)
+
+	expectedContent := `transaction() {
+    prepare(account:AuthAccount) {}
+
+    execute {}
+}`
+	assert.Equal(t, expectedContent, string(content))
+}
+
+func TestGenerateNewWithDirFlag(t *testing.T) {
+	dir, err := os.MkdirTemp("", "test")
+	assert.NoError(t, err, "Failed to create temp dir")
+	defer os.RemoveAll(dir)
+
+	if err := os.Chdir(dir); err != nil {
+		t.Fatalf("Failed to change directory: %v", err)
+	}
+
+	logger := output.NewStdoutLogger(output.NoneLog)
+	_, state, _ := util.TestMocks(t)
+
+	// Set a custom directory
+	generateFlags.Directory = "customDir"
+
+	_, err = generateNew([]string{"TestContract"}, "contract", logger, state)
+	assert.NoError(t, err, "Failed to generate contract")
+
+	content, err := state.ReaderWriter().ReadFile("customDir/TestContract.cdc")
+	assert.NoError(t, err, "Failed to read generated file")
+	assert.NotNil(t, content)
+
+	expectedContent := `
+access(all) contract TestContract {
+    init() {}
+}`
+	assert.Equal(t, expectedContent, string(content))
+}