Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
16 changes: 16 additions & 0 deletions cli/cmd/nitric.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ func NewTemplatesCmd(injector do.Injector) *cobra.Command {
}
}

// NewInitCmd creates the init command
func NewInitCmd(injector do.Injector) *cobra.Command {
return &cobra.Command{
Use: "init",
Short: "Setup a new Nitric project",
Long: `Setup a new Nitric project, including within existing applications`,
Run: func(cmd *cobra.Command, args []string) {
app, err := do.Invoke[*app.NitricApp](injector)
if err != nil {
cobra.CheckErr(err)
}
cobra.CheckErr(app.Init())
},
}
}

// NewNewCmd creates the new command
func NewNewCmd(injector do.Injector) *cobra.Command {
var force bool
Expand Down
1 change: 1 addition & 0 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ test, and deploy your Nitric applications.`,
rootCmd.AddCommand(NewAccessTokenCmd(injector))
rootCmd.AddCommand(NewVersionCmd(injector))
rootCmd.AddCommand(NewTemplatesCmd(injector))
rootCmd.AddCommand(NewInitCmd(injector))
rootCmd.AddCommand(NewNewCmd(injector))
rootCmd.AddCommand(NewBuildCmd(injector))
rootCmd.AddCommand(NewGenerateCmd(injector))
Expand Down
117 changes: 98 additions & 19 deletions cli/pkg/app/nitric.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/nitrictech/nitric/cli/internal/plugins"
"github.com/nitrictech/nitric/cli/internal/simulation"
"github.com/nitrictech/nitric/cli/internal/style/colors"
"github.com/nitrictech/nitric/cli/internal/style/icons"
"github.com/nitrictech/nitric/cli/pkg/client"
"github.com/nitrictech/nitric/cli/pkg/files"
"github.com/nitrictech/nitric/cli/pkg/schema"
Expand All @@ -34,13 +35,18 @@ import (
type NitricApp struct {
config *config.Config
apiClient *api.NitricApiClient
fs afero.Fs
}

func NewNitricApp(injector do.Injector) (*NitricApp, error) {
config := do.MustInvoke[*config.Config](injector)
apiClient := do.MustInvoke[*api.NitricApiClient](injector)
fs, err := do.Invoke[afero.Fs](injector)
if err != nil {
fs = afero.NewOsFs()
}

return &NitricApp{config: config, apiClient: apiClient}, nil
return &NitricApp{config: config, apiClient: apiClient, fs: fs}, nil
}

// Templates handles the templates command logic
Expand Down Expand Up @@ -70,6 +76,86 @@ func (c *NitricApp) Templates() error {
return nil
}

// Init initializes nitric for an existing project, creating a nitric.yaml file if it doesn't exist
func (c *NitricApp) Init() error {
emphasize := lipgloss.NewStyle().Foreground(colors.Teal).Bold(true)

nitricYamlPath := filepath.Join(".", "nitric.yaml")
exists, _ := afero.Exists(c.fs, nitricYamlPath)

// Read the nitric.yaml file
_, err := schema.LoadFromFile(c.fs, nitricYamlPath, true)
if err == nil {
fmt.Printf("Project already initialized, run %s to edit the project\n", emphasize.Render("nitric edit"))
return nil
} else if exists {
fmt.Printf("Project already initialized, but an error occurred loading %s\n", emphasize.Render("nitric.yaml"))
return err
}

fmt.Printf("Welcome to %s, this command will walk you through creating a nitric.yaml file.\n", emphasize.Render("Nitric"))
fmt.Printf("This file is used to define your app's infrastructure, resources and deployment targets.\n")
fmt.Println()
fmt.Printf("Here we'll only cover the basics, use %s to continue editing the project.\n", emphasize.Render("nitric edit"))
fmt.Println()

fmt.Println("Press esc or ctrl+c to quit")

// Project Name Prompt
name, err := tui.RunTextInput("Project name:", func(input string) error {
if input == "" {
return errors.New("project name is required")
}

// Must be kebab-case
if !regexp.MustCompile(`^[a-z][a-z0-9-]*$`).MatchString(input) {
return errors.New("project name must start with a letter and be lower kebab-case")
}

return nil
})
if err != nil || name == "" {
return nil
}

// Project Description Prompt
description, err := tui.RunTextInput("Project description:", func(input string) error {
return nil
})
if err != nil {
return nil
}

newProject := &schema.Application{
Name: name,
Description: description,
}

err = schema.SaveToYaml(c.fs, nitricYamlPath, newProject)
if err != nil {
fmt.Println("Failed to save nitric.yaml file")
return err
}

successStyle := lipgloss.NewStyle().Foreground(colors.Teal).Bold(true)
faint := lipgloss.NewStyle().Faint(true)

fmt.Println(successStyle.Render("\n " + icons.Check + " Project initialized!"))
fmt.Println(faint.Render("nitric project written to " + nitricYamlPath))

fmt.Println()
fmt.Println("Next steps:")
fmt.Println("1. Run", emphasize.Render("nitric edit"), "to start the nitric editor")
fmt.Println("2. Design your app's resources and deployment targets")
fmt.Println("3. Optionally, use", emphasize.Render("nitric generate"), "to generate the client libraries for your app")
fmt.Println("4. Run", emphasize.Render("nitric dev"), "to start the development server")
fmt.Println("5. Run", emphasize.Render("nitric build"), "to build the project for a specific platform")
fmt.Println()
fmt.Println("For more information, see the", emphasize.Render("nitric docs"), "at", emphasize.Render("https://nitric.io/docs"))

return nil
}

// New handles the new project creation command logic
func (c *NitricApp) New(projectName string, force bool) error {
templates, err := c.apiClient.GetTemplates()
Expand All @@ -83,8 +169,6 @@ func (c *NitricApp) New(projectName string, force bool) error {
return nil
}

fs := afero.NewOsFs()

if projectName == "" {
fmt.Println()
var err error
Expand All @@ -102,14 +186,13 @@ func (c *NitricApp) New(projectName string, force bool) error {
})
if err != nil || projectName == "" {
fmt.Println(err)
fmt.Println("+" + projectName + "+")
return nil
}
}

projectDir := filepath.Join(".", projectName)
if !force {
projectExists, err := projectExists(fs, projectDir)
projectExists, err := projectExists(c.fs, projectDir)
if err != nil {
fmt.Println(err.Error())
return nil
Expand Down Expand Up @@ -151,7 +234,7 @@ func (c *NitricApp) New(projectName string, force bool) error {

templateDir := filepath.Join(home, ".nitric", "templates", template.TeamSlug, template.TemplateSlug, template.Version)

templateCached, err := afero.Exists(fs, filepath.Join(templateDir, "nitric.yaml"))
templateCached, err := afero.Exists(c.fs, filepath.Join(templateDir, "nitric.yaml"))
if err != nil {
fmt.Printf("Failed read template cache directory: %v\n", err)
return err
Expand Down Expand Up @@ -180,22 +263,22 @@ func (c *NitricApp) New(projectName string, force bool) error {
return err
}

err = files.CopyDir(fs, templateDir, projectDir)
err = files.CopyDir(c.fs, templateDir, projectDir)
if err != nil {
fmt.Printf("Failed to copy template directory: %v\n", err)
return err
}

nitricYamlPath := filepath.Join(projectDir, "nitric.yaml")

appSpec, err := schema.LoadFromFile(fs, nitricYamlPath, false)
appSpec, err := schema.LoadFromFile(c.fs, nitricYamlPath, false)
if err != nil {
return err
}

appSpec.Name = projectName

err = schema.SaveToYaml(fs, nitricYamlPath, appSpec)
err = schema.SaveToYaml(c.fs, nitricYamlPath, appSpec)
if err != nil {
return err
}
Expand All @@ -220,9 +303,7 @@ func (c *NitricApp) New(projectName string, force bool) error {
// Build handles the build command logic
func (c *NitricApp) Build() error {
// Read the nitric.yaml file
fs := afero.NewOsFs()

appSpec, err := schema.LoadFromFile(fs, "nitric.yaml", true)
appSpec, err := schema.LoadFromFile(c.fs, "nitric.yaml", true)
if err != nil {
return err
}
Expand All @@ -232,7 +313,7 @@ func (c *NitricApp) Build() error {
// TODO:prompt for platform selection if multiple targets are specified
targetPlatform := appSpec.Targets[0]

platform, err := terraform.PlatformFromId(fs, targetPlatform, platformRepository)
platform, err := terraform.PlatformFromId(c.fs, targetPlatform, platformRepository)
if err != nil {
return err
}
Expand Down Expand Up @@ -261,9 +342,7 @@ func (c *NitricApp) Generate(goFlag, pythonFlag, javascriptFlag, typescriptFlag
return fmt.Errorf("at least one language flag must be specified")
}

fs := afero.NewOsFs()

appSpec, err := schema.LoadFromFile(fs, "nitric.yaml", true)
appSpec, err := schema.LoadFromFile(c.fs, "nitric.yaml", true)
if err != nil {
return err
}
Expand All @@ -277,23 +356,23 @@ func (c *NitricApp) Generate(goFlag, pythonFlag, javascriptFlag, typescriptFlag
if goFlag {
fmt.Println("Generating Go client...")
// TODO: add flags for output directory and package name
err = client.GenerateGo(fs, *appSpec, goOutputDir, goPackageName)
err = client.GenerateGo(c.fs, *appSpec, goOutputDir, goPackageName)
if err != nil {
return err
}
}

if pythonFlag {
fmt.Println("Generating Python client...")
err = client.GeneratePython(fs, *appSpec, pythonOutputDir)
err = client.GeneratePython(c.fs, *appSpec, pythonOutputDir)
if err != nil {
return err
}
}

if typescriptFlag {
fmt.Println("Generating NodeJS client...")
err = client.GenerateTypeScript(fs, *appSpec, typescriptOutputDir)
err = client.GenerateTypeScript(c.fs, *appSpec, typescriptOutputDir)
if err != nil {
return err
}
Expand Down
5 changes: 5 additions & 0 deletions cli/pkg/tui/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package tui

import "errors"

var ErrUserAborted = errors.New("quit")
6 changes: 6 additions & 0 deletions cli/pkg/tui/select.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Select struct {
style SelectStyle
maxDisplayed int
viewCursor int
quit bool
}

const TitleAndHelpHeight = 3
Expand Down Expand Up @@ -103,6 +104,7 @@ func (s *Select) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
s.selected = s.cursor
return s, tea.Quit
case "q", "ctrl+c", "ctrl+\\", "esc":
s.quit = true
return s, tea.Quit
}
case tea.WindowSizeMsg:
Expand Down Expand Up @@ -224,6 +226,10 @@ func RunSelect(items []string, title string) (string, int, error) {
return "", -1, err
}

if s.quit {
return "", -1, ErrUserAborted
}

selected, index := s.GetSelected()
return selected, index, nil
}
10 changes: 8 additions & 2 deletions cli/pkg/tui/textinput.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package tui

import (
"fmt"
"strings"

"github.com/charmbracelet/bubbles/textinput"
Expand All @@ -16,6 +15,8 @@ type textInputModel struct {
style textInputStyle
showError bool

quit bool

value string
}

Expand All @@ -39,8 +40,10 @@ func (m textInputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "esc", "ctrl+\\":
m.quit = true
return m, tea.Quit
case "enter":
m.textinput, _ = m.textinput.Update(msg)
if m.textinput.Err != nil {
m.showError = true
return m, nil
Expand Down Expand Up @@ -107,9 +110,12 @@ func RunTextInput(title string, validate func(string) error) (string, error) {
p := tea.NewProgram(model)
m, err := p.Run()
if err != nil {
fmt.Println("Error: " + err.Error())
return "", err
}

if m.(textInputModel).quit {
return "", ErrUserAborted
}

return m.(textInputModel).value, nil
}
Loading
Loading