diff --git a/cmd/generate.go b/cmd/generate.go index e40196415..3ec02d8e6 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -6,6 +6,7 @@ import ( "os" "strings" + "github.com/manifoldco/promptui" "github.com/spf13/cobra" "github.com/teamkeel/keel/colors" "github.com/teamkeel/keel/node" @@ -18,8 +19,17 @@ var generateCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { logPrefix := colors.Green("|").String() - err := node.Bootstrap( + packageManager, err := resolvePackageManager(flagProjectDir, false) + if err == promptui.ErrAbort { + return nil + } + if err != nil { + panic(err) + } + + err = node.Bootstrap( flagProjectDir, + node.WithPackageManager(packageManager), node.WithPackagesPath(flagNodePackagesPath), node.WithLogger(func(s string) { fmt.Println(logPrefix, s) diff --git a/cmd/init.go b/cmd/init.go index db5a2f618..fbbd133a5 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -1,49 +1,443 @@ package cmd import ( + "archive/zip" + "bytes" + "context" + "encoding/json" + "errors" "fmt" + "io" + "io/fs" + "net/http" "os" + "os/exec" + "path/filepath" + "runtime/debug" + "strings" - tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/manifoldco/promptui" + "github.com/samber/lo" "github.com/spf13/cobra" - "github.com/teamkeel/keel/cmd/program" + "github.com/teamkeel/keel/codegen" + "github.com/teamkeel/keel/colors" + "github.com/teamkeel/keel/node" + "github.com/teamkeel/keel/schema" ) var initCmd = &cobra.Command{ Use: "init", - Args: cobra.RangeArgs(0, 1), Short: "Initializes a new Keel project", - PreRunE: func(cmd *cobra.Command, args []string) error { - if len(args) > 0 { - return fmt.Errorf("unexpected arguments: %v", args) + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + defer panicHandler() + + state := &InitState{} + steps := []func(state *InitState) error{ + initState, + initStepDir, + initStepTemplate, + initStepPackageManager, + initStepGit, + initStepCreateProject, } - return nil + + printLogo() + fmt.Println(" Welcome to Keel!") + fmt.Println(colors.Gray(" Let's build something great")) + fmt.Println("") + + for _, step := range steps { + err := step(state) + if err != nil { + if err == promptui.ErrInterrupt { + fmt.Println("| Aborting...") + fmt.Println("") + return + } + + panic(err) + } + + fmt.Println("") + } + + fmt.Println("Your new Keel project is ready to roll ✨") + fmt.Println("") }, - Run: func(cmd *cobra.Command, args []string) { - cwd, err := os.Getwd() +} + +func panicHandler() { + if r := recover(); r != nil { + errStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("15")). + Background(lipgloss.Color("1")) + + fmt.Println("") + fmt.Println(errStyle.Render("======= Oh no ==========")) + fmt.Println("Something seems to have gone wrong.") + fmt.Println("This is likely a bug with Keel - please let us know via:") + fmt.Println(" - Discord (https://discord.gg/HV8g38nBnm)") + fmt.Println(" - GitHub Issue (https://github.com/teamkeel/keel/issues/new)") + fmt.Println("") + fmt.Println("Please include the following stack trace in your report:") + fmt.Println(colors.Gray(string(debug.Stack()))) + fmt.Println(errStyle.Render("========================")) + fmt.Println("") + } +} + +type InitState struct { + cwd string + gitRoot string + targetDir string + initGitRepo bool + files codegen.GeneratedFiles + packageManager string +} + +func initState(state *InitState) error { + wd, err := os.Getwd() + if err != nil { + panic(err) + } + + state.cwd = wd + + c := exec.Command("git", "rev-parse", "--show-toplevel") + out, err := c.Output() + if err == nil { + state.gitRoot = strings.TrimSpace(string(out)) + } + + return nil +} + +func initStepDir(state *InitState) error { + initSectionHeading("Directory") + + entries, err := os.ReadDir(state.cwd) + if err != nil { + panic(err) + } + + defaultDir := "" + + switch { + case len(entries) == 0: + defaultDir = "." + case lo.ContainsBy(entries, func(e fs.DirEntry) bool { return e.Name() == "package.json" }): + defaultDir = "./keel" + default: + defaultDir = "./my-keel-app" + } + + prompt := promptui.Prompt{ + Label: "Where should we create your project?", + Default: defaultDir, + AllowEdit: false, + Validate: func(v string) error { + e, _ := os.ReadDir(v) + if len(e) > 0 { + return errors.New("directory is not empty") + } + return nil + }, + Pointer: promptui.PipeCursor, + } + + dir, err := prompt.Run() + if err != nil { + panic(err) + } + + state.targetDir = dir + return nil +} + +func initStepTemplate(state *InitState) error { + initSectionHeading("Template") + + optionBlank := "Blank project" + optionStarter := "Starter template" + + template := promptui.Select{ + Label: "How would you like to start your new project?", + Items: []string{optionStarter, optionBlank}, + Templates: &promptui.SelectTemplates{ + Label: "{{ . }}", + Selected: fmt.Sprintf("%s {{ . | bold }} ", promptui.IconGood), + Active: "{{ . | cyan }}", + Inactive: "{{ . }}", + }, + } + + _, result, err := template.Run() + if err != nil { + panic(err) + } + + if result == optionBlank { + state.files = append(state.files, &codegen.GeneratedFile{ + Path: ".gitignore", + Contents: `node_modules/ +.DS_Store +*.local + +# Keel +.build/ + `, + }) + + state.files = append(state.files, &codegen.GeneratedFile{ + Path: "schema.keel", + Contents: "// Visit https://docs.keel.so/ for documentation on how to get started", + }) + + state.files = append(state.files, &codegen.GeneratedFile{ + Path: "keelconfig.yaml", + Contents: `# Visit https://docs.keel.so/envvars for more information about environment variables +environment: + +# Visit https://docs.keel.so/secrets for more information about secrets +secrets: +`, + }) + return nil + } + + res, err := http.Get("https://api.github.com/repos/teamkeel/starter-templates/zipball/main") + if err != nil { + panic(err) + } + defer res.Body.Close() + + zipBytes, err := io.ReadAll(res.Body) + if err != nil { + panic(err) + } + + type StarterTemplate struct { + Name string `json:"name"` + Path string `json:"path"` + } + + starterFiles := map[string][]byte{} + templates := []*StarterTemplate{} + + zipReader, err := zip.NewReader(bytes.NewReader(zipBytes), int64(len(zipBytes))) + if err != nil { + panic(err) + } + + for _, f := range zipReader.File { + if f.FileInfo().IsDir() { + continue + } + + r, err := f.Open() + if err != nil { + panic(err) + } + + b, err := io.ReadAll(r) if err != nil { panic(err) } - dir := cwd + // The zip archive contains a directory which contains the repo contents. We don't + // care about the top-level directory so we drop it from the name + name := filepath.Join(strings.Split(f.Name, "/")[1:]...) + + starterFiles[name] = b - if len(args) > 0 { - dir = args[0] + if name == "templates.json" { + err = json.Unmarshal(b, &templates) + if err != nil { + panic(err) + } } + } - model := &program.InitModel{ - ProjectDir: dir, + templateNames := lo.Map(templates, func(v *StarterTemplate, _ int) string { + return v.Name + }) + + starter := promptui.Select{ + Label: "Which template would you like to use?", + Items: templateNames, + Templates: &promptui.SelectTemplates{ + Label: "{{ . }}", + Selected: fmt.Sprintf("%s {{ . | bold }} ", promptui.IconGood), + Active: "{{ . | cyan }}", + Inactive: "{{ . }}", + }, + } + + idx, _, err := starter.Run() + if err != nil { + panic(err) + } + + for k, v := range starterFiles { + if strings.HasPrefix(k, templates[idx].Path) { + path := strings.TrimPrefix(k, templates[idx].Path+"/") + state.files = append(state.files, &codegen.GeneratedFile{ + Path: path, + Contents: string(v), + }) } + } + + return nil +} + +func initStepPackageManager(state *InitState) error { + initSectionHeading("Package Manager") + + rootDir := state.cwd + if state.gitRoot != "" { + rootDir = state.gitRoot + } + + packageManager, err := resolvePackageManager(rootDir, true) + if err != nil { + return err + } + + state.packageManager = packageManager + return nil +} + +func initStepGit(state *InitState) error { + initSectionHeading("Version Control") + + if state.gitRoot != "" { + printSuccess(fmt.Sprintf("Git repo detected: %s", colors.Gray(state.gitRoot).String())) + return nil + } + + starter := promptui.Prompt{ + Label: "Should we initialise a Git repo in your new project?", + IsConfirm: true, + } + + _, err := starter.Run() + if err == promptui.ErrAbort { + return nil + } + if err != nil { + panic(err) + } + + state.initGitRepo = true + return nil +} + +func initStepCreateProject(state *InitState) error { + initSectionHeading("Generating Project") + + err := state.files.Write(state.targetDir) + if err != nil { + panic(err) + } + + err = node.Bootstrap( + state.targetDir, + node.WithPackageManager(state.packageManager), + node.WithLogger(func(s string) { + fmt.Println("|", colors.Gray(s)) + }), + node.WithOutputWriter(os.Stdout), + ) + if err != nil { + panic(err) + } + + b := schema.Builder{} + schema, err := b.MakeFromDirectory(state.targetDir) + if err != nil { + panic(err) + } + + files, err := node.Generate(context.Background(), schema, node.WithDevelopmentServer(true)) + if err != nil { + panic(err) + } - _, err = tea.NewProgram(model).Run() + err = files.Write(state.targetDir) + if err != nil { + panic(err) + } + + fmt.Println("| Generated @teamkeel/sdk") + fmt.Println("| Generated @teamkeel/testing") + + if state.initGitRepo { + fmt.Println("| Initialising git repo") + c := exec.Command("git", "init") + c.Dir = state.targetDir + err := c.Run() if err != nil { panic(err) } + } - if model.Err != nil { - os.Exit(1) - } - }, + return nil +} + +func initSectionHeading(v string) { + style := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("15")). + Background(lipgloss.Color("#7D56F4")). + PaddingLeft(1). + PaddingRight(1) + + fmt.Println(style.Render(v)) +} + +func printSuccess(v string) { + fmt.Println(colors.Green("✔"), v) +} + +func printLogo() { + logo := ` + bbbb + bbbbbbbbb + bbbbbbbbbbbbbb + bbbbbbbbbbbbbb + y bbbbbbbbbbbbbb + yyyyyyy bbbbbbbbbbbbbb + yyyyyyyyyyy bbbbbbbbbbbbbb + ooooooooooo ppppppppppppppp + ooooooo ppppppppppppppp + o ppppppppppppppp + ppppppppppppppp + pppppppppppppp + ppppppppp + pppp + ` + + blue := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#0094FF")).Bold(true) + + purple := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#AB84FF")).Bold(true) + + yellow := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FFBA17")).Bold(true) + + orange := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FF866F")).Bold(true) + + logo = strings.ReplaceAll(logo, "b", blue.Render("●")) + logo = strings.ReplaceAll(logo, "p", purple.Render("●")) + logo = strings.ReplaceAll(logo, "y", yellow.Render("●")) + logo = strings.ReplaceAll(logo, "o", orange.Render("●")) + + fmt.Println(logo) } func init() { diff --git a/cmd/program/commands.go b/cmd/program/commands.go index 46e897c67..f318241fe 100644 --- a/cmd/program/commands.go +++ b/cmd/program/commands.go @@ -233,9 +233,13 @@ type SetupFunctionsMsg struct { Err error } -func SetupFunctions(dir string, nodePackagesPath string) tea.Cmd { +func SetupFunctions(dir string, nodePackagesPath string, packageManager string) tea.Cmd { return func() tea.Msg { - err := node.Bootstrap(dir, node.WithPackagesPath(nodePackagesPath)) + err := node.Bootstrap( + dir, + node.WithPackageManager(packageManager), + node.WithPackagesPath(nodePackagesPath), + ) if err != nil { return SetupFunctionsMsg{ Err: err, diff --git a/cmd/program/model.go b/cmd/program/model.go index f491dd567..d2fb70f89 100644 --- a/cmd/program/model.go +++ b/cmd/program/model.go @@ -114,6 +114,9 @@ type Model struct { // from this path, rather than NPM. NodePackagesPath string + // Either 'npm' or 'pnpm' + PackageManager string + // If set then runtime will be configured with private key // located at this path in pem format. PrivateKeyPath string @@ -236,7 +239,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.PrivateKey = msg.PrivateKey m.Status = StatusSetupFunctions - return m, SetupFunctions(m.ProjectDir, m.NodePackagesPath) + return m, SetupFunctions(m.ProjectDir, m.NodePackagesPath, m.PackageManager) case SetupFunctionsMsg: m.Err = msg.Err diff --git a/cmd/root.go b/cmd/root.go index 596402dc8..97fa8f270 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,8 +3,12 @@ package cmd import ( "fmt" "os" + "os/exec" + "path/filepath" + "github.com/manifoldco/promptui" "github.com/spf13/cobra" + "github.com/teamkeel/keel/colors" "github.com/teamkeel/keel/runtime" ) @@ -49,3 +53,64 @@ func init() { rootCmd.PersistentFlags().StringVarP(&flagProjectDir, "dir", "d", workingDir, "directory containing a Keel project") rootCmd.PersistentFlags().BoolVarP(&flagVersion, "version", "v", false, "Print the Keel CLI version") } + +func resolvePackageManager(dir string, isInit bool) (string, error) { + dir = filepath.Clean(dir) + + for { + packageLockPath := filepath.Join(dir, "package-lock.json") + pnpmLockPath := filepath.Join(dir, "pnpm-lock.yaml") + + _, err := os.Stat(packageLockPath) + if err == nil { + if isInit { + fmt.Println("|", colors.Gray("package-lock.json found at"), packageLockPath) + } + return "npm", nil + } + + _, err = os.Stat(pnpmLockPath) + if err == nil { + if isInit { + fmt.Println("|", colors.Gray("pnpm-lock.yaml found at"), packageLockPath) + } + return "pnpm", nil + } + + parent := filepath.Dir(dir) + if parent == dir { + break + } + + dir = parent + } + + _, err := exec.LookPath("pnpm") + if err != nil { + if isInit { + fmt.Println("|", colors.Gray("pnpm not detected")) + } + return "npm", nil + } + + if !isInit { + fmt.Println( + colors.Yellow("| We're not sure which package manager you'd like to use as we can't find any lockfiles"), + ) + fmt.Println("") + } + + s := promptui.Select{ + Label: "Which Node.js package manager would you like to use?", + Items: []string{"pnpm", "npm"}, + Templates: &promptui.SelectTemplates{ + Label: "{{ . }}", + Selected: fmt.Sprintf("%s {{ . | bold }} ", promptui.IconGood), + Active: "{{ . | cyan }}", + Inactive: "{{ . }}", + }, + } + + _, v, err := s.Run() + return v, err +} diff --git a/cmd/run.go b/cmd/run.go index df9fc2021..d6635e806 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" + "github.com/manifoldco/promptui" "github.com/spf13/cobra" "github.com/teamkeel/keel/cmd/program" ) @@ -17,6 +18,14 @@ var runCmd = &cobra.Command{ return nil }, Run: func(cmd *cobra.Command, args []string) { + packageManager, err := resolvePackageManager(flagProjectDir, false) + if err == promptui.ErrAbort { + return + } + if err != nil { + panic(err) + } + program.Run(&program.Model{ Mode: program.ModeRun, ProjectDir: flagProjectDir, @@ -24,6 +33,7 @@ var runCmd = &cobra.Command{ Port: flagPort, TracingEnabled: flagTracing, NodePackagesPath: flagNodePackagesPath, + PackageManager: packageManager, PrivateKeyPath: flagPrivateKeyPath, }) }, diff --git a/cmd/test.go b/cmd/test.go index 9fcdb7aad..e54513fc6 100644 --- a/cmd/test.go +++ b/cmd/test.go @@ -8,6 +8,7 @@ import ( "os/exec" "path/filepath" + "github.com/manifoldco/promptui" "github.com/spf13/cobra" "github.com/teamkeel/keel/cmd/database" "github.com/teamkeel/keel/cmd/program" @@ -32,9 +33,18 @@ var testCmd = &cobra.Command{ // Only do bootstrap if no node_modules directory present _, err := os.Stat(filepath.Join(flagProjectDir, "node_modules")) if os.IsNotExist(err) { + packageManager, err := resolvePackageManager(flagProjectDir, false) + if err == promptui.ErrAbort { + return nil + } + if err != nil { + panic(err) + } + logPrefix := colors.Green("|").String() - err := node.Bootstrap( + err = node.Bootstrap( flagProjectDir, + node.WithPackageManager(packageManager), node.WithPackagesPath(flagNodePackagesPath), node.WithLogger(func(s string) { fmt.Println(logPrefix, s) diff --git a/go.mod b/go.mod index 14877e589..323ecff57 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/karlseguin/typed v1.1.8 github.com/lestrrat-go/jwx/v2 v2.0.11 github.com/lib/pq v1.10.9 + github.com/manifoldco/promptui v0.9.0 github.com/nleeper/goment v1.4.4 github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 github.com/otiai10/copy v1.7.0 @@ -60,6 +61,7 @@ require ( github.com/PaesslerAG/gval v1.0.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/fatih/color v1.13.0 // indirect @@ -86,7 +88,7 @@ require ( github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.1 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect github.com/rogpeppe/go-internal v1.8.0 // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/spf13/afero v1.9.3 // indirect @@ -101,7 +103,7 @@ require ( go.opentelemetry.io/otel/metric v1.16.0 // indirect go.opentelemetry.io/proto/otlp v0.19.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/term v0.13.0 // indirect + golang.org/x/term v0.14.0 // indirect golang.org/x/text v0.13.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect @@ -122,7 +124,7 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect @@ -133,6 +135,6 @@ require ( github.com/spf13/pflag v1.0.5 // indirect golang.org/x/crypto v0.14.0 golang.org/x/net v0.16.0 // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/sys v0.14.0 // indirect gotest.tools/v3 v3.3.0 // indirect ) diff --git a/go.sum b/go.sum index a916ea412..a24a62788 100644 --- a/go.sum +++ b/go.sum @@ -82,8 +82,11 @@ github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06 github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -289,6 +292,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/matryer/moq v0.2.7/go.mod h1:kITsx543GOENm48TUAQyJ9+SAvFSr7iGQXPoth/VUBk= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -299,8 +304,8 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= @@ -356,8 +361,9 @@ github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm github.com/relvacode/iso8601 v1.3.0 h1:HguUjsGpIMh/zsTczGN3DVJFxTU/GX+MMmzcKoMO7ko= github.com/relvacode/iso8601 v1.3.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -571,6 +577,7 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -625,14 +632,14 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/node/bootstrap.go b/node/bootstrap.go index b752643a9..63ca288ef 100644 --- a/node/bootstrap.go +++ b/node/bootstrap.go @@ -49,9 +49,10 @@ func GetDependencies(options *bootstrapOptions) (map[string]string, map[string]s } type bootstrapOptions struct { - packagesPath string - logger func(string) - output io.Writer + packagesPath string + packageManager string + logger func(string) + output io.Writer } // WithPackagesPath causes any @teamkeel packages to be installed @@ -75,6 +76,12 @@ func WithOutputWriter(w io.Writer) BootstrapOption { } } +func WithPackageManager(p string) BootstrapOption { + return func(o *bootstrapOptions) { + o.packageManager = p + } +} + type BootstrapOption func(o *bootstrapOptions) type PackageJson struct { @@ -87,6 +94,7 @@ type PackageJson struct { // file has been generated func Bootstrap(dir string, opts ...BootstrapOption) error { options := &bootstrapOptions{ + packageManager: "npm", logger: func(s string) { fmt.Println(s) }, @@ -96,6 +104,10 @@ func Bootstrap(dir string, opts ...BootstrapOption) error { o(options) } + if !lo.Contains([]string{"npm", "pnpm"}, options.packageManager) { + return fmt.Errorf("unsupported package manager: %s", options.packageManager) + } + packageJsonPath := filepath.Join(dir, "package.json") tsConfigPath := filepath.Join(dir, "tsconfig.json") @@ -132,7 +144,7 @@ func Bootstrap(dir string, opts ...BootstrapOption) error { } if len(toInstall) > 0 { options.logger("Installing dependencies...") - err = installDeps(dir, toInstall, false, options.output) + err = installDeps(dir, toInstall, false, options) if err != nil { return err } @@ -144,7 +156,7 @@ func Bootstrap(dir string, opts ...BootstrapOption) error { } if len(toInstall) > 0 { options.logger("Installing dev dependencies...") - err = installDeps(dir, toInstall, true, options.output) + err = installDeps(dir, toInstall, true, options) if err != nil { return err } @@ -220,14 +232,22 @@ func getDepsToInstall(required map[string]string, existing map[string]string) ([ return toInstall, nil } -func installDeps(dir string, deps []string, dev bool, out io.Writer) error { - args := []string{"install"} +func installDeps(dir string, deps []string, dev bool, options *bootstrapOptions) error { + args := []string{} + + switch options.packageManager { + case "npm": + args = append(args, "install") + case "pnpm": + args = append(args, "add") + } + args = append(args, deps...) - args = append(args, lo.Ternary(dev, "--save-dev", "--save")) + args = append(args, lo.Ternary(dev, "--save-dev", "--save-prod")) - cmd := exec.Command("npm", args...) - cmd.Stdout = out - cmd.Stderr = out + cmd := exec.Command(options.packageManager, args...) + cmd.Stdout = options.output + cmd.Stderr = options.output cmd.Dir = dir return cmd.Run()