Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): project template scaffold cli command #533

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3665fd7
feat: create resonate project command
muhammad-asghar-ali Jan 16, 2025
7ea2333
chore: revert the docker compose
muhammad-asghar-ali Jan 16, 2025
727fdd6
feat: check project exist in dir and replace the repo url
muhammad-asghar-ali Jan 17, 2025
906c927
Merge branch 'main' of github.com:resonatehq/resonate into project-te…
muhammad-asghar-ali Jan 17, 2025
803435f
Merge branch 'resonatehq:main' into project-template-scaffold
muhammad-asghar-ali Jan 20, 2025
be9df8e
callbacks
muhammad-asghar-ali Jan 22, 2025
6d97ace
Merge branch 'project-template-scaffold' of github.com:muhammad-asgha…
muhammad-asghar-ali Jan 22, 2025
d47ea80
chore: remove client and pre run for command
muhammad-asghar-ali Jan 22, 2025
16fddf9
feat: shift from cloning to downlaod and unzip repo
muhammad-asghar-ali Jan 22, 2025
aa84f2b
Merge branch 'main' of github.com:resonatehq/resonate into project-te…
muhammad-asghar-ali Jan 23, 2025
c5e44f7
chore: changed variable names to lowercase
muhammad-asghar-ali Jan 24, 2025
0a48c3e
Merge branch 'main' of github.com:resonatehq/resonate into project-te…
muhammad-asghar-ali Jan 24, 2025
5a41e16
feat: refactor the create and add the templates with subcmd
muhammad-asghar-ali Jan 28, 2025
5cf51ad
feat: more improvments, make the struct and remove the vars make the …
muhammad-asghar-ali Jan 29, 2025
3c1069d
Merge branch 'main' of github.com:resonatehq/resonate into project-te…
muhammad-asghar-ali Jan 29, 2025
18c1a17
feat: shift sdk flag to template
muhammad-asghar-ali Jan 30, 2025
da6fd7f
chore(): some formatting and improve messages
muhammad-asghar-ali Jan 31, 2025
392a701
chore: from table to paragraph and change function name
muhammad-asghar-ali Jan 31, 2025
6d30ea7
chore(): from templates to project top level command
muhammad-asghar-ali Feb 6, 2025
0414e52
chore(): from templates to project top level command
muhammad-asghar-ali Feb 6, 2025
876d233
Merge branch 'project-template-scaffold' of github.com:muhammad-asgha…
muhammad-asghar-ali Feb 6, 2025
49f1a43
chore: refactor template into project
muhammad-asghar-ali Feb 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/resonatehq/resonate/cmd/serve"
"github.com/resonatehq/resonate/cmd/subscriptions"
"github.com/resonatehq/resonate/cmd/tasks"
"github.com/resonatehq/resonate/cmd/templates"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand Down Expand Up @@ -42,6 +43,7 @@ func init() {
rootCmd.AddCommand(quickstart.NewCmd())
rootCmd.AddCommand(tasks.NewCmd())
rootCmd.AddCommand(callbacks.NewCmd())
rootCmd.AddCommand(templates.NewCmd())
rootCmd.AddCommand(subscriptions.NewCmd())

// Set default output
Expand Down
86 changes: 86 additions & 0 deletions cmd/templates/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package templates

import (
"errors"
"fmt"
"os"

"github.com/spf13/cobra"
)

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

exampleCMD := `
# Create Resonate project
muhammad-asghar-ali marked this conversation as resolved.
Show resolved Hide resolved
resonate templates create --name my-app --template py

OR

# Create Resonate project
resonate templates create -n my-app -t py
`

cmd := &cobra.Command{
Use: "create",
Short: "Create a new Resonate project",
muhammad-asghar-ali marked this conversation as resolved.
Show resolved Hide resolved
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
}

muhammad-asghar-ali marked this conversation as resolved.
Show resolved Hide resolved
return nil
},
}

cmd.Flags().StringVarP(&name, "name", "n", "", "Name of the project (required).")
muhammad-asghar-ali marked this conversation as resolved.
Show resolved Hide resolved
cmd.Flags().StringVarP(&template, "template", "t", "", "Name of the template (required). Run 'resonate templates list' to view available templates.")
muhammad-asghar-ali marked this conversation as resolved.
Show resolved Hide resolved

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

return cmd
}

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

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

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

return nil
}

func checkProjectExists(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("project named '%s' already exists", name)
muhammad-asghar-ali marked this conversation as resolved.
Show resolved Hide resolved
}

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

import (
"fmt"
"os"
"text/tabwriter"

"github.com/spf13/cobra"
)

func ListTemplateCmd() *cobra.Command {
exampleCMD := `
# Create Resonate project
muhammad-asghar-ali marked this conversation as resolved.
Show resolved Hide resolved
resonate templates list
`

cmd := &cobra.Command{
Use: "list",
Short: "List the available template projects",
muhammad-asghar-ali marked this conversation as resolved.
Show resolved Hide resolved
Example: exampleCMD,
RunE: func(cmd *cobra.Command, args []string) error {
templates, err := GetTemplates()
if err != nil {
return err
}

display(templates)
return nil
},
}

return cmd
}

func display(templates Templates) {
dfarr marked this conversation as resolved.
Show resolved Hide resolved
writer := tabwriter.NewWriter(os.Stdout, 20, 0, 1, ' ', 0)

fmt.Fprintln(writer, "\n| Templates | Description |")
fmt.Fprintln(writer, "|--------------------|----------------------------|")

for name, t := range templates {
fmt.Fprintf(writer, "| %-18s | %-26s |\n", name, t.Desc)
}

writer.Flush()
}
180 changes: 180 additions & 0 deletions cmd/templates/scaffold.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package templates

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

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

// find the template based on template (key)
template, exists := templates[tmpl]
if !exists {
return fmt.Errorf("unsupported template type. supported template are: %v", GetTemplateKeys(templates))
muhammad-asghar-ali marked this conversation as resolved.
Show resolved Hide resolved
}

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

return nil
}

// setup downloads and unzips the Template 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 template: %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)
}
Loading