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 10 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
93 changes: 93 additions & 0 deletions cmd/create/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package create

import (
"errors"
"fmt"
"os"
"strings"

"github.com/spf13/cobra"
)

func NewCmd() *cobra.Command {
var (
// cmd
// TODO - may add the 3rd input as --template or short -t
name string // name of the project
sdk string // type of the project
)

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

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

return nil
},
}

cmd.Flags().StringVarP(&name, "name", "n", "", "Name of the project (required)")
cmd.Flags().StringVarP(&sdk, "sdk", "s", "python", "SDK to use (e.g., python, typescript)")

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

return cmd
}

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

if sdk == "" {
return errors.New("sdk type is required")
}

if !isSupported(sdk) {
return fmt.Errorf("unsupported sdk type. supported sdks are: %s", strings.Join(SDKs, ", "))
}

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

return nil
}

func isSupported(sdk string) bool {
for _, supported := range SDKs {
if sdk == supported {
return true
}
}

return false
}

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)
}

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

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

var (
exampleCMD = `
# Create Resonate project
resonate create --name my-python-app --sdk python

OR

# Create Resonate project
resonate create -n my-python-app -s python
`

// valiate the user input sdk with the supported list
// TODO - may convert to map
// TODO - we cal also use the struct which combine the sdk and repo urls
/*
type SdkRepo struct {
Name string
RepoURL string
}
*/
SDKs = []string{"python", "ts"}
muhammad-asghar-ali marked this conversation as resolved.
Show resolved Hide resolved

// Repos
Repos = map[string]string{
muhammad-asghar-ali marked this conversation as resolved.
Show resolved Hide resolved
"python": "https://github.com/resonatehq/scaffold-py/archive/refs/heads/main.zip",
}
)

// scaffold orchestrates the setup of the SDK from source to destination.
func scaffold(sdk, name string) error {
url, err := source(sdk)
if err != nil {
return err
}

if err := setup(url, name); err != nil {
return err
}

return nil
}

// source retrieves the URL for the given SDK.
func source(sdk string) (string, error) {
url, ok := Repos[sdk]
if !ok {
return "", fmt.Errorf("unsupported sdk: %s", sdk)
}

return url, nil
}

// setup downloads and unzips the SDK 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 := check(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
}

// check verifies the HTTP response for a successful status.
func check(res *http.Response) error {
if res.StatusCode != http.StatusOK {
return fmt.Errorf("failed to download file: %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 @@ -6,6 +6,7 @@ import (
"strings"

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

// Set default output
Expand Down