Skip to content

Commit

Permalink
feat(cli): project template scaffold cli command (#533)
Browse files Browse the repository at this point in the history
  • Loading branch information
muhammad-asghar-ali authored Feb 14, 2025
1 parent 005d888 commit c4cc1ff
Show file tree
Hide file tree
Showing 5 changed files with 376 additions and 0 deletions.
82 changes: 82 additions & 0 deletions cmd/project/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package project

import (
"errors"
"fmt"
"os"

"github.com/spf13/cobra"
)

func CreateProjectCmd() *cobra.Command {
var (
name string
template string
)

exampleCMD := `
resonate project create --name my-app --template py
resonate project create -n my-app -t py
`

cmd := &cobra.Command{
Use: "create",
Short: "Create a new resonate application node project",
Example: exampleCMD,
RunE: func(cmd *cobra.Command, args []string) error {
if err := validate(template, name); err != nil {
return err
}

if err := scaffold(template, name); err != nil {
return err
}

fmt.Printf("\nproject successfully created in folder %s\n", name)
return nil
},
}

cmd.Flags().StringVarP(&name, "name", "n", "", "name of the project")
cmd.Flags().StringVarP(&template, "template", "t", "", "name of the template, run 'resonate project list' to view available templates")

_ = cmd.MarkFlagRequired("name")
_ = cmd.MarkFlagRequired("template")

return cmd
}

func validate(project, name string) error {
if name == "" {
return errors.New("a folder name is required")
}

if project == "" {
return errors.New("project name is required")
}

err := checkFolderExists(name)
if err != nil {
return err
}

return nil
}

func checkFolderExists(name string) error {
info, err := os.Stat(name)

if err != nil {
if os.IsNotExist(err) {
return nil
}

return err
}

if info.IsDir() {
return fmt.Errorf("a folder named '%s' already exists", name)
}

return nil
}
32 changes: 32 additions & 0 deletions cmd/project/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package project

import (
"fmt"

"github.com/spf13/cobra"
)

func ListProjectCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List the available application node projects",
Example: "resonate project list",
RunE: func(cmd *cobra.Command, args []string) error {
templates, err := GetProjects()
if err != nil {
return err
}

display(templates)
return nil
},
}

return cmd
}

func display(templates Projects) {
for name, t := range templates {
fmt.Printf("\n%s\n\t%s\n", name, t.Desc)
}
}
80 changes: 80 additions & 0 deletions cmd/project/project.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package project

import (
"encoding/json"
"io"
"net/http"

"github.com/spf13/cobra"
)

type (
Project struct {
Href string `json:"href"`
Desc string `json:"desc"`
}

Projects map[string]Project
)

func NewCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "project",
Aliases: []string{"project"},
Short: "Resonate application node projects",
Run: func(cmd *cobra.Command, args []string) {
_ = cmd.Help()
},
}

// Add subcommands
cmd.AddCommand(ListProjectCmd()) // list available projects
cmd.AddCommand(CreateProjectCmd()) // create a project

return cmd
}

func GetProjects() (Projects, error) {
const url = "https://raw.githubusercontent.com/resonatehq/templates/refs/heads/main/templates.json"

res, err := http.Get(url)
if err != nil {
return nil, err
}
defer res.Body.Close()

if err := checkstatus(res); err != nil {
return nil, err
}

body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}

projects, err := parse(body)
if err != nil {
return nil, err
}

return projects, nil
}

func parse(body []byte) (Projects, error) {
projects := Projects{}
if err := json.Unmarshal(body, &projects); err != nil {
return nil, err
}

return projects, nil
}

func GetProjectKeys(projects Projects) []string {
keys := make([]string, 0)

for name := range projects {
keys = append(keys, name)
}

return keys
}
180 changes: 180 additions & 0 deletions cmd/project/scaffold.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package project

import (
"archive/zip"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
)

// scaffold orchestrates the setup of the project from source to destination.
func scaffold(tmpl, name string) error {
projects, err := GetProjects()
if err != nil {
return err
}

// find the project based on project (key)
project, exists := projects[tmpl]
if !exists {
return fmt.Errorf("unknown project '%s', available projects are: %v", tmpl, GetProjectKeys(projects))
}

if err := setup(project.Href, name); err != nil {
return err
}

return nil
}

// setup downloads and unzips the project to the destination folder.
func setup(url, dest string) error {
tmp := dest + ".zip"
if err := download(url, tmp); err != nil {
return err
}
defer os.Remove(tmp)

if err := unzip(tmp, dest); err != nil {
return err
}

return nil
}

// download fetches a file from the URL and stores it locally.
func download(url, file string) error {
res, err := http.Get(url)
if err != nil {
return err
}
defer res.Body.Close()

if err := checkstatus(res); err != nil {
return err
}

out, err := os.Create(file)
if err != nil {
return err
}
defer out.Close()

_, err = io.Copy(out, res.Body)
return err
}

// checkstatus verifies the HTTP response for a successful status.
func checkstatus(res *http.Response) error {
if res.StatusCode != http.StatusOK {
return fmt.Errorf("failed to fetch project: %s", res.Status)
}

return nil
}

// unzip extracts the contents of a zip file to the destination folder.
func unzip(src, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()

root, err := extract(r, dest)
if err != nil {
return err
}

if root != "" {
path := filepath.Join(dest, root)
return restructure(path, dest)
}

return nil
}

// extract unzips the contents and returns the root folder name.
func extract(r *zip.ReadCloser, dest string) (string, error) {
var root string
for _, f := range r.File {
rel := strings.TrimPrefix(f.Name, root)
file := filepath.Join(dest, rel)

if root == "" {
root = base(f.Name)
}

if f.FileInfo().IsDir() {
if err := os.MkdirAll(file, os.ModePerm); err != nil {
return "", err
}
continue
}

if err := os.MkdirAll(filepath.Dir(file), os.ModePerm); err != nil {
return "", err
}

if err := write(f, file); err != nil {
return "", err
}
}

return root, nil
}

// base returns the root directory name from a path.
func base(name string) string {
parts := strings.Split(name, "/")
if len(parts) > 0 {
return parts[0]
}

return ""
}

// write writes a file from a zip entry to the destination path.
func write(f *zip.File, path string) error {
out, err := os.Create(path)
if err != nil {
return err
}
defer out.Close()

rc, err := f.Open()
if err != nil {
return err
}
defer rc.Close()

_, err = io.Copy(out, rc)
return err
}

// restructure moves extracted contents from a root directory to destination.
func restructure(src, dest string) error {
entries, err := os.ReadDir(src)
if err != nil {
return err
}

for _, entry := range entries {
if err := move(src, dest, entry); err != nil {
return err
}
}

return os.Remove(src)
}

// move moves a file or directory from the source to the destination
func move(src, dest string, entry os.DirEntry) error {
old := filepath.Join(src, entry.Name())
new := filepath.Join(dest, entry.Name())

return os.Rename(old, new)
}
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/resonatehq/resonate/cmd/callbacks"
"github.com/resonatehq/resonate/cmd/dst"
"github.com/resonatehq/resonate/cmd/project"
"github.com/resonatehq/resonate/cmd/promises"
"github.com/resonatehq/resonate/cmd/quickstart"
"github.com/resonatehq/resonate/cmd/schedules"
Expand Down Expand Up @@ -42,6 +43,7 @@ func init() {
rootCmd.AddCommand(quickstart.NewCmd())
rootCmd.AddCommand(tasks.NewCmd())
rootCmd.AddCommand(callbacks.NewCmd())
rootCmd.AddCommand(project.NewCmd())
rootCmd.AddCommand(subscriptions.NewCmd())

// Set default output
Expand Down

0 comments on commit c4cc1ff

Please sign in to comment.