├── .gitignore ├── internal ├── errorhandler │ ├── error.go │ └── messages.go ├── utils │ ├── message.go │ ├── file.go │ ├── find.go │ ├── flag.go │ ├── stack.go │ └── utils.go └── constants │ └── constants.go ├── main.go ├── .pre-commit-config.yaml ├── pickyhelpers ├── delete_git.go ├── progress_bar.go ├── convert_dbconfig.go ├── commands.go ├── update_package_json.go ├── clone_repo.go ├── sources │ ├── create_ci_source.go │ └── create_cd_source.go ├── create_ci.go ├── update_dockercompose.go ├── create_cd.go ├── convert_template_database.go ├── update_db_config.go ├── get_stack_info.go ├── create_docker_files.go ├── convert_dbtests.go ├── create_dockercompose.go ├── create_infra.go ├── update_env_files.go └── convert_queries.go ├── .golangci.yml ├── cmd ├── service.go ├── test.go ├── stacks.go ├── root.go ├── infra.go ├── init.go └── create.go ├── prompt ├── database.go ├── home.go ├── git.go ├── service.go ├── cicd.go ├── init.go ├── promptutils.go ├── dockercompose.go ├── infra.go ├── deploy.go └── prompt.go ├── LICENSE ├── go.mod ├── flagcmd ├── init.go └── stacks.go ├── .github └── workflows │ ├── ci.yml │ └── product-release.yml ├── hbs ├── hbs.go └── hbsregister.go ├── README.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | coverage.out 3 | -------------------------------------------------------------------------------- /internal/errorhandler/error.go: -------------------------------------------------------------------------------- 1 | package errorhandler 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func CheckNilErr(err error) { 9 | if err != nil { 10 | // Check if the user clicks the Control + C button for exiting. 11 | if err.Error() == ErrInterrupt.Error() { 12 | err = ExitMessage 13 | } 14 | fmt.Print(err) 15 | os.Exit(1) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/wednesday-solutions/picky/cmd" 5 | "github.com/wednesday-solutions/picky/internal/errorhandler" 6 | ) 7 | 8 | func main() { 9 | err := cmd.Execute() 10 | if err != nil { 11 | if err.Error() == errorhandler.ErrInterrupt.Error() { 12 | err = errorhandler.ExitMessage 13 | } 14 | errorhandler.CheckNilErr(err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: true 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.4.0 5 | hooks: 6 | - id: trailing-whitespace 7 | - id: end-of-file-fixer 8 | - id: check-yaml 9 | - id: check-added-large-files 10 | - repo: https://github.com/dnephin/pre-commit-golang 11 | rev: v0.5.1 12 | hooks: 13 | - id: go-fmt 14 | - id: go-imports 15 | - id: no-go-testing 16 | - id: golangci-lint 17 | - id: go-unit-tests 18 | -------------------------------------------------------------------------------- /internal/errorhandler/messages.go: -------------------------------------------------------------------------------- 1 | package errorhandler 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/enescakir/emoji" 8 | ) 9 | 10 | var ( 11 | ErrInterrupt = errors.New("^C") 12 | ErrExist = errors.New("already exist") 13 | ) 14 | 15 | var ( 16 | ExitMessage = fmt.Errorf("\n%s%s\n", "Program Exited", emoji.Parse(":exclamation:")) 17 | DoneMessage = fmt.Errorf("\n%s%s\n\n", "Done", emoji.Parse(":sparkles:")) 18 | CompleteMessage = fmt.Errorf("%s%s\n\n", "Completed", emoji.Parse(":sparkles:")) 19 | WaveMessage = emoji.WavingHand 20 | Exclamation = emoji.ExclamationMark 21 | ) 22 | -------------------------------------------------------------------------------- /pickyhelpers/delete_git.go: -------------------------------------------------------------------------------- 1 | package pickyhelpers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/internal/constants" 7 | "github.com/wednesday-solutions/picky/internal/errorhandler" 8 | "github.com/wednesday-solutions/picky/internal/utils" 9 | ) 10 | 11 | // DeleteDotGitFolder deletes .git folder from stack folder. 12 | func (s StackDetails) DeleteDotGitFolder() error { 13 | 14 | path := fmt.Sprintf("%s/%s/%s", utils.CurrentDirectory(), 15 | s.DirName, 16 | constants.DotGitFolder, 17 | ) 18 | status, _ := utils.IsExists(path) 19 | if status { 20 | err := utils.RemoveAll(path) 21 | errorhandler.CheckNilErr(err) 22 | } 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /pickyhelpers/progress_bar.go: -------------------------------------------------------------------------------- 1 | package pickyhelpers 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/schollz/progressbar/v3" 8 | "github.com/wednesday-solutions/picky/internal/errorhandler" 9 | ) 10 | 11 | func ProgressBar(max int, description string, done chan bool) { 12 | 13 | bar := progressbar.NewOptions(max, 14 | progressbar.OptionEnableColorCodes(true), 15 | progressbar.OptionShowBytes(false), 16 | progressbar.OptionSetWidth(75), 17 | progressbar.OptionSetDescription(fmt.Sprintf("[cyan][1/1][reset] %s...", description)), 18 | progressbar.OptionSetTheme(progressbar.Theme{ 19 | Saucer: "[green]=[reset]", 20 | SaucerHead: "[green]>[reset]", 21 | SaucerPadding: " ", 22 | BarStart: "[", 23 | BarEnd: "]", 24 | }), 25 | ) 26 | 27 | for i := 0; i < max; i++ { 28 | err := bar.Add(1) 29 | errorhandler.CheckNilErr(err) 30 | 31 | time.Sleep(200 * time.Millisecond) 32 | } 33 | done <- true 34 | } 35 | -------------------------------------------------------------------------------- /pickyhelpers/convert_dbconfig.go: -------------------------------------------------------------------------------- 1 | package pickyhelpers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/internal/constants" 7 | "github.com/wednesday-solutions/picky/internal/utils" 8 | ) 9 | 10 | func ConvertDBConfig(stack, dirName string) error { 11 | 12 | if stack == constants.NodeExpressGraphqlTemplate { 13 | 14 | file := fmt.Sprintf("%s/%s/%s/%s/%s/%s", 15 | utils.CurrentDirectory(), 16 | dirName, 17 | "server", 18 | "utils", 19 | "testUtils", 20 | "dbConfig.js", 21 | ) 22 | dbUri := "mysql://user:password@host:3306/table" 23 | source := fmt.Sprintf(`export const DB_ENV = { 24 | DB_URI: '%s', 25 | %s: 'host', 26 | %s: 'user', 27 | %s: 'password', 28 | %s: 'table' 29 | }; 30 | `, 31 | dbUri, 32 | constants.MysqlHost, 33 | constants.MysqlUser, 34 | "MYSQL_PASSWORD", 35 | constants.MysqlDatabase, 36 | ) 37 | err := utils.WriteToFile(file, source) 38 | return err 39 | } 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | errcheck: 3 | check-type-assertions: true 4 | goconst: 5 | min-len: 2 6 | min-occurrences: 3 7 | gocritic: 8 | enabled-tags: 9 | - diagnostic 10 | - experimental 11 | - opinionated 12 | - performance 13 | - style 14 | govet: 15 | check-shadowing: true 16 | enable: 17 | - fieldalignment 18 | nolintlint: 19 | require-explanation: true 20 | require-specific: true 21 | 22 | linters: 23 | disable-all: true 24 | enable: 25 | - bodyclose 26 | # - depguard 27 | # - dogsled 28 | - dupl 29 | - errcheck 30 | - exportloopref 31 | - exhaustive 32 | - goconst 33 | - gofmt 34 | - goimports 35 | - gocyclo 36 | - gosimple 37 | - ineffassign 38 | - misspell 39 | - nolintlint 40 | - nakedret 41 | - prealloc 42 | - predeclared 43 | - staticcheck 44 | - thelper 45 | - tparallel 46 | - unconvert 47 | - unparam 48 | 49 | run: 50 | issues-exit-code: 1 51 | -------------------------------------------------------------------------------- /cmd/service.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/wednesday-solutions/picky/internal/constants" 6 | "github.com/wednesday-solutions/picky/prompt" 7 | ) 8 | 9 | var ServiceCmd = ServiceCmdFn() 10 | 11 | func RunService(*cobra.Command, []string) error { 12 | prompt.PromptHome() 13 | return nil 14 | } 15 | 16 | // ServiceCmdFn represents the ServiceCmd command 17 | func ServiceCmdFn() *cobra.Command { 18 | 19 | var ServiceCmd = &cobra.Command{ 20 | Use: constants.Service, 21 | Short: "Pick a Service", 22 | // Long: `Pick a service for your: 23 | 24 | // 1. Web 25 | // 2. Mobile 26 | // 3. Backend 27 | 28 | // from the list of @wednesday-solutions's open source projects. 29 | // `, 30 | Long: `Pick a service for your: 31 | 32 | 1. Web 33 | 2. Backend 34 | 35 | from the list of @wednesday-solutions's open source projects. 36 | `, 37 | RunE: RunService, 38 | } 39 | return ServiceCmd 40 | } 41 | 42 | func init() { 43 | 44 | RootCmd.AddCommand(ServiceCmd) 45 | 46 | ServiceCmd.Flags().BoolP("help", "h", false, "Help for service selection") 47 | } 48 | -------------------------------------------------------------------------------- /prompt/database.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/internal/constants" 7 | "github.com/wednesday-solutions/picky/internal/errorhandler" 8 | ) 9 | 10 | func (i *InitInfo) PromptSelectStackDatabase() { 11 | i.SelectDatabase() 12 | } 13 | 14 | func (i *InitInfo) SelectDatabase() { 15 | var p PromptInput 16 | p.Label = "Choose a database" 17 | p.GoBack = PromptSelectService 18 | switch i.Stack { 19 | case constants.NodeHapiTemplate, constants.NodeExpressGraphqlTemplate, constants.GolangEchoTemplate: 20 | p.Items = []string{constants.PostgreSQL, constants.MySQL} 21 | case constants.NodeExpressTemplate: 22 | p.Items = []string{constants.MongoDB} 23 | default: 24 | errorhandler.CheckNilErr(fmt.Errorf("\nSelected stack is invalid%s\n", errorhandler.Exclamation)) 25 | } 26 | i.Database, _ = p.PromptSelect() 27 | } 28 | 29 | func PromptAllDatabases() string { 30 | var p PromptInput 31 | p.Label = "Choose a database" 32 | p.GoBack = PromptSelectService 33 | p.Items = []string{constants.PostgreSQL, constants.MySQL, constants.MongoDB} 34 | db, _ := p.PromptSelect() 35 | return db 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-Present Wednesday Solutions 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pickyhelpers/commands.go: -------------------------------------------------------------------------------- 1 | package pickyhelpers 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/wednesday-solutions/picky/internal/constants" 8 | "github.com/wednesday-solutions/picky/internal/utils" 9 | ) 10 | 11 | func InstallDependencies(pkgManager string, path ...string) error { 12 | filePath := filepath.Join(path...) 13 | err := utils.RunCommandWithLogs(filePath, pkgManager, "install") 14 | return err 15 | } 16 | 17 | func BuildSST(pkgManager string) error { 18 | err := utils.RunCommandWithLogs("", "yarn", "build") 19 | return err 20 | } 21 | 22 | func DeploySST(pkgManager, environment string) error { 23 | environment = utils.GetShortEnvName(environment) 24 | arg := fmt.Sprintf("deploy:%s", environment) 25 | err := utils.RunCommandWithLogs("", pkgManager, arg) 26 | return err 27 | } 28 | 29 | func RemoveDeploy(pkgManager, environment string) error { 30 | environment = utils.GetShortEnvName(environment) 31 | arg := fmt.Sprintf("remove:%s", environment) 32 | err := utils.RunCommandWithLogs("", pkgManager, "run", arg) 33 | return err 34 | } 35 | 36 | func ParseDeployOutputs() error { 37 | err := utils.RunCommandWithoutLogs("", "node", constants.ParseSstOutputs) 38 | return err 39 | } 40 | -------------------------------------------------------------------------------- /pickyhelpers/update_package_json.go: -------------------------------------------------------------------------------- 1 | package pickyhelpers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/internal/constants" 7 | "github.com/wednesday-solutions/picky/internal/errorhandler" 8 | "github.com/wednesday-solutions/picky/internal/utils" 9 | ) 10 | 11 | func UpdatePackageDotJson(stack, dirName string) error { 12 | var pkgManager string 13 | var dependencies []string 14 | var updateCommands []string 15 | 16 | switch stack { 17 | case constants.NodeHapiTemplate: 18 | // postgres database support for mysql templates 19 | dependencies = []string{constants.Pg, constants.PgNative} 20 | 21 | case constants.NodeExpressGraphqlTemplate: 22 | // mysql database support for postgres templates 23 | dependencies = []string{constants.Mysql2} 24 | } 25 | pkgManager = utils.GetPackageManagerOfUser() 26 | if pkgManager == constants.Yarn { 27 | updateCommands = []string{"add"} 28 | updateCommands = append(updateCommands, dependencies...) 29 | } else if pkgManager == constants.Npm { 30 | updateCommands = []string{"install", "--legacy-peer-deps", "--save"} 31 | updateCommands = append(updateCommands, dependencies...) 32 | } 33 | path := fmt.Sprintf("%s/%s", utils.CurrentDirectory(), dirName) 34 | err := utils.RunCommandWithoutLogs(path, pkgManager, updateCommands...) 35 | errorhandler.CheckNilErr(err) 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wednesday-solutions/picky 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/aymerick/raymond v2.0.2+incompatible 7 | github.com/enescakir/emoji v1.0.0 8 | github.com/fatih/color v1.15.0 9 | github.com/iancoleman/strcase v0.3.0 10 | github.com/rivo/tview v0.0.0-20230814110005-ccc2c8119703 11 | github.com/schollz/progressbar/v3 v3.13.1 12 | github.com/spaceweasel/promptui v0.8.1 13 | github.com/spf13/cobra v1.7.0 14 | ) 15 | 16 | require ( 17 | github.com/chzyer/readline v1.5.1 // indirect 18 | github.com/gdamore/encoding v1.0.0 // indirect 19 | github.com/gdamore/tcell/v2 v2.6.0 // indirect 20 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 21 | github.com/juju/ansiterm v1.0.0 // indirect 22 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 23 | github.com/lunixbochs/vtclean v1.0.0 // indirect 24 | github.com/mattn/go-colorable v0.1.13 // indirect 25 | github.com/mattn/go-isatty v0.0.19 // indirect 26 | github.com/mattn/go-runewidth v0.0.15 // indirect 27 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect 28 | github.com/rivo/uniseg v0.4.4 // indirect 29 | github.com/spf13/pflag v1.0.5 // indirect 30 | github.com/stretchr/testify v1.8.4 // indirect 31 | golang.org/x/sys v0.11.0 // indirect 32 | golang.org/x/term v0.11.0 // indirect 33 | golang.org/x/text v0.12.0 // indirect 34 | gopkg.in/yaml.v2 v2.4.0 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /cmd/test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rivo/tview" 7 | "github.com/spf13/cobra" 8 | "github.com/wednesday-solutions/picky/internal/errorhandler" 9 | ) 10 | 11 | var TestCommand = TestCommandFn() 12 | 13 | func RunTest(*cobra.Command, []string) error { 14 | 15 | var firstName string 16 | var region string 17 | 18 | app := tview.NewApplication() 19 | form := tview.NewForm() 20 | form.AddInputField("Project Infra Name", "", 20, nil, func(text string) { 21 | firstName = text 22 | }) 23 | form.AddDropDown("Select Infra Region", []string{"india", "usa"}, 0, func(option string, optionIndex int) { 24 | region = option 25 | }) 26 | 27 | form.AddButton("Save", func() { 28 | app.Stop() 29 | fmt.Println("First Name: ", firstName, "\nInfra Region: ", region) 30 | }) 31 | form.AddButton("Quit", func() { 32 | app.Stop() 33 | }) 34 | 35 | form.SetBorder(true).SetTitle("Enter infra details").SetTitleAlign(tview.AlignLeft) 36 | if err := app.SetRoot(form, true).EnableMouse(true).Run(); err != nil { 37 | errorhandler.CheckNilErr(err) 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func TestCommandFn() *cobra.Command { 44 | testCommand := &cobra.Command{ 45 | Use: "test", 46 | Short: "This command is for testing", 47 | Long: "This command is for testing", 48 | RunE: RunTest, 49 | } 50 | return testCommand 51 | } 52 | 53 | func init() { 54 | RootCmd.AddCommand(TestCommand) 55 | } 56 | -------------------------------------------------------------------------------- /prompt/home.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "github.com/wednesday-solutions/picky/internal/constants" 5 | "github.com/wednesday-solutions/picky/internal/utils" 6 | ) 7 | 8 | func PromptHome() { 9 | var p PromptInput 10 | p.Label = "Pick an option" 11 | p.GoBack = PromptAlertMessage 12 | var initService bool 13 | stacks, _, _ := utils.GetExistingStacksDatabasesAndDirectories() 14 | if len(stacks) > 0 { 15 | p.Items = []string{constants.InitService} 16 | if ShowPromptGitInit() { 17 | p.Items = append(p.Items, constants.GitInit) 18 | } 19 | p.Items = append(p.Items, 20 | constants.CICD, 21 | constants.DockerCompose, 22 | constants.SetupInfra, 23 | constants.Deploy, 24 | ) 25 | if ShowRemoveDeploy() { 26 | p.Items = append(p.Items, constants.RemoveDeploy) 27 | } 28 | p.Items = append(p.Items, constants.Exit) 29 | response, _ := p.PromptSelect() 30 | switch response { 31 | case constants.InitService: 32 | initService = true 33 | case constants.DockerCompose: 34 | PromptDockerCompose() 35 | case constants.CICD: 36 | PromptCICD() 37 | case constants.SetupInfra: 38 | PromptSetupInfra() 39 | case constants.Deploy: 40 | PromptDeploy() 41 | case constants.RemoveDeploy: 42 | PromptRemoveDeploy() 43 | case constants.GitInit: 44 | PromptGitInit() 45 | case constants.Exit: 46 | PromptExit() 47 | } 48 | } 49 | if len(stacks) == 0 || initService { 50 | PromptSelectService() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pickyhelpers/clone_repo.go: -------------------------------------------------------------------------------- 1 | package pickyhelpers 2 | 3 | import ( 4 | "github.com/wednesday-solutions/picky/internal/constants" 5 | "github.com/wednesday-solutions/picky/internal/errorhandler" 6 | "github.com/wednesday-solutions/picky/internal/utils" 7 | ) 8 | 9 | // StackDetails is the collection of all stack details. 10 | type StackDetails struct { 11 | // Service refers to the service type of the stack. 12 | Service string 13 | 14 | // Stacks refers to Wednesday Solutions open source templates. 15 | Stack string 16 | 17 | // DirName refers to the name of directory of stack. 18 | DirName string 19 | 20 | // CurrentDir refers to the root directory. 21 | CurrentDir string 22 | 23 | // Database refers to the database of selected stack. 24 | Database string 25 | 26 | // Environment refers to the environment 27 | Environment string 28 | 29 | // StackInfo consist of all the details about stacks. 30 | StackInfo map[string]interface{} 31 | } 32 | 33 | func (s StackDetails) CloneRepo() error { 34 | 35 | // Download the selected stack. 36 | err := utils.RunCommandWithoutLogs("", "git", "clone", constants.Repos()[s.Stack], s.DirName) 37 | errorhandler.CheckNilErr(err) 38 | 39 | // Delete cd.yml file from the cloned repo. 40 | cdFilePatch := s.CurrentDir + "/" + s.DirName + constants.CDFilePathURL 41 | status, _ := utils.IsExists(cdFilePatch) 42 | if status { 43 | err = utils.RemoveFile(cdFilePatch) 44 | errorhandler.CheckNilErr(err) 45 | } 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /pickyhelpers/sources/create_ci_source.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/internal/utils" 7 | ) 8 | 9 | func CISource(stack, stackDir, environment string) string { 10 | envName := utils.GetShortEnvName(environment) 11 | source := fmt.Sprintf(`name: CI %s 12 | on: 13 | push: 14 | branches: 15 | - dev 16 | - qa 17 | - master 18 | paths: "%s/**" 19 | 20 | pull_request: 21 | paths: "%s/**" 22 | 23 | # Allows to run this workflow manually from the Actions tab 24 | workflow_dispatch: 25 | 26 | jobs: 27 | build-and-test: 28 | name: Build & Test 29 | runs-on: ubuntu-latest 30 | defaults: 31 | run: 32 | working-directory: ./%s 33 | strategy: 34 | matrix: 35 | node-version: [16.14.x] 36 | 37 | steps: 38 | - uses: actions/checkout@v3 39 | - name: Use Node.js ${{ matrix.node-version }} 40 | uses: actions/setup-node@v2 41 | with: 42 | node-version: ${{ matrix.node-version }} 43 | cache: "yarn" 44 | cache-dependency-path: ./%s/package.json 45 | 46 | - name: Install dependencies 47 | run: yarn 48 | 49 | - name: Lint 50 | run: yarn lint 51 | 52 | - name: Build 53 | run: yarn build:%s 54 | 55 | - name: Test 56 | run: yarn run test 57 | `, 58 | stackDir, stackDir, stackDir, stackDir, 59 | stackDir, envName, 60 | ) 61 | return source 62 | } 63 | -------------------------------------------------------------------------------- /flagcmd/init.go: -------------------------------------------------------------------------------- 1 | package flagcmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/wednesday-solutions/picky/internal/constants" 8 | "github.com/wednesday-solutions/picky/internal/errorhandler" 9 | "github.com/wednesday-solutions/picky/internal/utils" 10 | "github.com/wednesday-solutions/picky/prompt" 11 | ) 12 | 13 | type InitInfo struct { 14 | Service string 15 | Stack string 16 | Database string 17 | Directory string 18 | } 19 | 20 | func (i *InitInfo) FlagInit() error { 21 | 22 | i.Database = utils.GetDatabase(i.Database) 23 | 24 | i.Stack = utils.GetStackByFlags(i.Stack) 25 | if i.Stack == "" { 26 | return fmt.Errorf("Entered stack is invalid") 27 | } 28 | if i.Stack == constants.GolangEchoTemplate { 29 | i.Stack = fmt.Sprintf("%s-%s", strings.Split(i.Stack, " ")[0], i.Database) 30 | } 31 | i.Directory = utils.CreateStackDirectory(i.Directory, i.Stack, i.Database) 32 | 33 | status, _ := utils.IsExists(fmt.Sprintf("%s/%s", utils.CurrentDirectory(), i.Directory)) 34 | if status { 35 | return fmt.Errorf("Entered directory %s already exists\n", i.Directory) 36 | } 37 | err := utils.MakeDirectory(utils.CurrentDirectory(), i.Directory) 38 | errorhandler.CheckNilErr(err) 39 | 40 | fmt.Printf("\nService: %s\nStack: %s\nDatabase: %s\nDirectory: %s\n\n", 41 | i.Service, i.Stack, i.Database, i.Directory, 42 | ) 43 | 44 | var ii prompt.InitInfo 45 | ii.Service = i.Service 46 | ii.Stack = i.Stack 47 | ii.Database = i.Database 48 | ii.DirName = i.Directory 49 | err = ii.StackInitialize() 50 | return err 51 | } 52 | -------------------------------------------------------------------------------- /pickyhelpers/create_ci.go: -------------------------------------------------------------------------------- 1 | package pickyhelpers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/internal/constants" 7 | "github.com/wednesday-solutions/picky/internal/errorhandler" 8 | "github.com/wednesday-solutions/picky/internal/utils" 9 | "github.com/wednesday-solutions/picky/pickyhelpers/sources" 10 | ) 11 | 12 | func CreateCI(stackDirs []string) error { 13 | workflowsPath := fmt.Sprintf("%s/%s", utils.CurrentDirectory(), 14 | constants.GithubWorkflowsDir) 15 | 16 | utils.CreateGithubWorkflowDir() 17 | var stackCIPath, stack string 18 | var status bool 19 | for _, dir := range stackDirs { 20 | stackCIPath = fmt.Sprintf("%s/ci-%s.yml", workflowsPath, dir) 21 | status, _ = utils.IsExists(stackCIPath) 22 | if !status { 23 | stack, _ = utils.FindStackAndDatabase(dir) 24 | err := CreateStackCI(stackCIPath, dir, stack) 25 | errorhandler.CheckNilErr(err) 26 | } 27 | } 28 | return nil 29 | } 30 | 31 | // CreateStackCI creates and writes CI for the given stack. 32 | func CreateStackCI(path, stackDir, stack string) error { 33 | var environment, source string 34 | if stack == constants.NodeExpressGraphqlTemplate { 35 | environment = constants.Development 36 | } else { 37 | environment = constants.Dev 38 | } 39 | if stack != constants.GolangEchoTemplate { 40 | source = sources.CISource(stack, stackDir, environment) 41 | 42 | err := utils.WriteToFile(path, source) 43 | errorhandler.CheckNilErr(err) 44 | } else { 45 | err := utils.PrintWarningMessage(fmt.Sprintf( 46 | "CI of '%s' is in work in progress..!", stack, 47 | )) 48 | errorhandler.CheckNilErr(err) 49 | } 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: service-picker 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | - develop 8 | 9 | jobs: 10 | lint-test-build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - uses: actions/setup-python@v4 16 | with: 17 | python-version: "3.x" 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: "1.21" 23 | cache: false 24 | 25 | - name: Install pre-commit dependencies 26 | run: | 27 | go install github.com/fzipp/gocyclo/cmd/gocyclo@latest 28 | go install golang.org/x/tools/cmd/goimports@latest 29 | go install github.com/go-critic/go-critic/cmd/gocritic@latest 30 | go install golang.org/x/lint/golint@latest 31 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 32 | 33 | - name: Run pre-commit 34 | uses: pre-commit/action@v3.0.0 35 | 36 | - name: Build 37 | run: go build ./... 38 | 39 | - name: Test 40 | run: go test ./... -gcflags=all=-l -coverprofile=coverage.out 41 | 42 | golangci-lint: 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v3 46 | 47 | - name: Install Go 48 | uses: actions/setup-go@v4 49 | with: 50 | go-version: "1.20" 51 | cache: false 52 | 53 | - name: golangci-lint 54 | uses: golangci/golangci-lint-action@v3 55 | with: 56 | version: v1.54 57 | args: --timeout=30m --config=./.golangci.yml --issues-exit-code=0 58 | -------------------------------------------------------------------------------- /cmd/stacks.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/wednesday-solutions/picky/flagcmd" 8 | "github.com/wednesday-solutions/picky/internal/constants" 9 | "github.com/wednesday-solutions/picky/internal/errorhandler" 10 | ) 11 | 12 | var StacksCmd = StacksCmdFn() 13 | 14 | func RunStacks(cmd *cobra.Command, args []string) error { 15 | 16 | var err error 17 | var flag flagcmd.StackFlag 18 | flag.A, err = cmd.Flags().GetBool("allstacks") 19 | errorhandler.CheckNilErr(err) 20 | 21 | flag.E, err = cmd.Flags().GetBool("existingstacks") 22 | errorhandler.CheckNilErr(err) 23 | 24 | flag.W, err = cmd.Flags().GetBool("webstacks") 25 | errorhandler.CheckNilErr(err) 26 | 27 | flag.M, err = cmd.Flags().GetBool("mobilestacks") 28 | errorhandler.CheckNilErr(err) 29 | 30 | flag.B, err = cmd.Flags().GetBool("backendstacks") 31 | errorhandler.CheckNilErr(err) 32 | 33 | userOutput := flag.FlagStacks() 34 | fmt.Println(userOutput) 35 | 36 | return nil 37 | } 38 | 39 | func StacksCmdFn() *cobra.Command { 40 | stacksCmd := &cobra.Command{ 41 | Use: constants.Stacks, 42 | Short: "See stacks", 43 | Long: "See stacks", 44 | Args: cobra.NoArgs, 45 | RunE: RunStacks, 46 | } 47 | return stacksCmd 48 | } 49 | 50 | func init() { 51 | RootCmd.AddCommand(StacksCmd) 52 | // declaring flags 53 | StacksCmd.Flags().BoolP("allstacks", "a", false, "all available stacks") 54 | StacksCmd.Flags().BoolP("existingstacks", "e", true, "all existing stacks") 55 | StacksCmd.Flags().BoolP("webstacks", "w", false, "web stacks") 56 | StacksCmd.Flags().BoolP("mobilestacks", "m", false, "mobile stacks") 57 | StacksCmd.Flags().BoolP("backendstacks", "b", false, "backend stacks") 58 | } 59 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/wednesday-solutions/picky/internal/constants" 8 | "github.com/wednesday-solutions/picky/internal/errorhandler" 9 | ) 10 | 11 | // RootCmd is the command variable of root command picky. 12 | var RootCmd = RootCmdFn() 13 | var version = "0.0.9" 14 | 15 | // RootCmd represents the base command when called without any subcommands 16 | func RootCmdFn() *cobra.Command { 17 | 18 | var cmd = &cobra.Command{ 19 | Use: constants.Picky, 20 | Version: version, 21 | Short: "Service Picker", 22 | Long: fmt.Sprintf(` 23 | Hello%s 24 | Welcome to Service Picker. 25 | 26 | It contains a number of @wednesday-solutions's open source projects, connected and working together. Pick whatever you need and build your own ecosystem. 27 | 28 | This repo will have support for production applications using the following tech stacks 29 | - Frontend 30 | - react 31 | - next 32 | - Backend 33 | - Node (Hapi - REST API) 34 | - Node (Express - GraphQL API) 35 | - Databases 36 | - MySQL 37 | - PostgreSQL 38 | - Cache 39 | - Redis 40 | - Infrastructure Provider 41 | - AWS 42 | 43 | Wednesday Solutions`, errorhandler.WaveMessage), 44 | } 45 | return cmd 46 | } 47 | 48 | // Execute adds all child commands to the root command and sets flags appropriately. 49 | // This is called by main.main(). It only needs to happen once to the rootCmd. 50 | func Execute() error { 51 | RootCmd.CompletionOptions.DisableDefaultCmd = true 52 | 53 | err := RootCmd.Execute() 54 | if err != nil { 55 | if err.Error() == errorhandler.ErrInterrupt.Error() { 56 | err = errorhandler.ExitMessage 57 | } 58 | errorhandler.CheckNilErr(err) 59 | } 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /pickyhelpers/update_dockercompose.go: -------------------------------------------------------------------------------- 1 | package pickyhelpers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/iancoleman/strcase" 7 | "github.com/wednesday-solutions/picky/hbs" 8 | "github.com/wednesday-solutions/picky/internal/constants" 9 | "github.com/wednesday-solutions/picky/internal/errorhandler" 10 | "github.com/wednesday-solutions/picky/internal/utils" 11 | ) 12 | 13 | func UpdateDockerCompose(stack, dirName string, stackInfo map[string]interface{}) error { 14 | var updateDockerCompose bool 15 | snakeCaseDirName := strcase.ToSnake(dirName) 16 | source := fmt.Sprintf(`version: '3' 17 | services: 18 | %s_db: 19 | image: {{dbVersion database}} 20 | ports: 21 | - {{portConnection database}} 22 | restart: always 23 | env_file: 24 | - .env.docker 25 | 26 | redis: 27 | image: 'redis:6-alpine' 28 | ports: 29 | - {{portConnection redis}} 30 | command: ['redis-server', '--bind', 'redis', '--port', '6379'] 31 | 32 | app: 33 | build: 34 | context: . 35 | args: 36 | ENVIRONMENT_NAME: docker 37 | depends_on: 38 | - redis 39 | - %s_db 40 | restart: always 41 | ports: 42 | - {{portConnection backend}} 43 | env_file: 44 | - .env.docker 45 | `, snakeCaseDirName, snakeCaseDirName) 46 | switch stack { 47 | case constants.NodeExpressGraphqlTemplate, constants.NodeHapiTemplate: 48 | updateDockerCompose = true 49 | default: 50 | updateDockerCompose = false 51 | } 52 | if updateDockerCompose { 53 | path := fmt.Sprintf("%s/%s/%s", utils.CurrentDirectory(), 54 | dirName, 55 | constants.DockerComposeFile, 56 | ) 57 | err := hbs.ParseAndWriteToFile(source, path, stackInfo) 58 | errorhandler.CheckNilErr(err) 59 | } 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /prompt/git.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/internal/constants" 7 | "github.com/wednesday-solutions/picky/internal/errorhandler" 8 | "github.com/wednesday-solutions/picky/internal/utils" 9 | ) 10 | 11 | func ShowPromptGitInit() bool { 12 | status, _ := utils.IsExists(fmt.Sprintf("%s/%s", 13 | utils.CurrentDirectory(), 14 | constants.DotGitFolder), 15 | ) 16 | if status { 17 | return false 18 | } else { 19 | return true 20 | } 21 | } 22 | 23 | func PromptGitInit() { 24 | var p PromptInput 25 | p.Label = "Do you want to initialize git" 26 | p.GoBack = PromptHome 27 | response := p.PromptYesOrNoSelect() 28 | if response { 29 | err := GitInit() 30 | errorhandler.CheckNilErr(err) 31 | } 32 | PromptHome() 33 | } 34 | 35 | func GitInit() error { 36 | err := utils.RunCommandWithLogs("", constants.Git, constants.Init) 37 | if err != nil { 38 | return err 39 | } 40 | // create .gitignore file 41 | file := fmt.Sprintf("%s/%s", 42 | utils.CurrentDirectory(), 43 | constants.DotGitIgnore, 44 | ) 45 | err = utils.CreateFile(file) 46 | errorhandler.CheckNilErr(err) 47 | 48 | err = WriteDotGitignoreFile(file) 49 | return err 50 | } 51 | 52 | func WriteDotGitignoreFile(file string) error { 53 | source := constants.NodeModules 54 | _, _, directories := utils.GetExistingStacksDatabasesAndDirectories() 55 | for _, dir := range directories { 56 | source = fmt.Sprintf("%s\n%s/%s", source, dir, constants.NodeModules) 57 | } 58 | source = fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n", 59 | source, 60 | constants.DotSst, 61 | constants.OutputsJson, 62 | "cdk.context.json", 63 | "build", 64 | "out", 65 | ) 66 | err := utils.WriteToFile(file, source) 67 | return err 68 | } 69 | -------------------------------------------------------------------------------- /pickyhelpers/create_cd.go: -------------------------------------------------------------------------------- 1 | package pickyhelpers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/internal/constants" 7 | "github.com/wednesday-solutions/picky/internal/errorhandler" 8 | "github.com/wednesday-solutions/picky/internal/utils" 9 | "github.com/wednesday-solutions/picky/pickyhelpers/sources" 10 | ) 11 | 12 | func (s StackDetails) CreateCDFile() error { 13 | 14 | utils.CreateGithubWorkflowDir() 15 | cdDestination := fmt.Sprintf("%s/%s/cd-%s.yml", 16 | utils.CurrentDirectory(), 17 | constants.GithubWorkflowsDir, 18 | s.DirName, 19 | ) 20 | status, _ := utils.IsExists(cdDestination) 21 | if !status { 22 | 23 | done := make(chan bool) 24 | go ProgressBar(20, "Generating", done) 25 | 26 | // Create CD File 27 | err := utils.CreateFile(cdDestination) 28 | errorhandler.CheckNilErr(err) 29 | 30 | var cdSource string 31 | if s.Service == constants.Backend { 32 | cdSource = sources.CDBackendSource(s.Stack, s.DirName, s.Environment) 33 | } else if s.Service == constants.Web { 34 | cdSource = sources.CDWebSource(s.Stack, s.DirName) 35 | } 36 | 37 | // Write CDFileData to CD File 38 | err = utils.WriteToFile(cdDestination, cdSource) 39 | errorhandler.CheckNilErr(err) 40 | 41 | <-done 42 | fmt.Printf("\n%s %s", "Generating", errorhandler.CompleteMessage) 43 | 44 | } else { 45 | fmt.Println("The", s.Service, s.Stack, "CD you are looking to create already exists") 46 | return errorhandler.ErrExist 47 | } 48 | return nil 49 | } 50 | 51 | func CreateTaskDefinition(stackDir, environment string) error { 52 | environment = utils.GetShortEnvName(environment) 53 | 54 | file := fmt.Sprintf("%s/%s/%s-%s.json", 55 | utils.CurrentDirectory(), 56 | stackDir, 57 | "task-definition", 58 | environment, 59 | ) 60 | source := sources.TaskDefinitionSource(environment, stackDir) 61 | var err error 62 | if source != "" { 63 | err = utils.WriteToFile(file, source) 64 | } 65 | return err 66 | } 67 | -------------------------------------------------------------------------------- /prompt/service.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "github.com/wednesday-solutions/picky/internal/constants" 5 | "github.com/wednesday-solutions/picky/internal/errorhandler" 6 | "github.com/wednesday-solutions/picky/internal/utils" 7 | ) 8 | 9 | var services = []string{ 10 | constants.Web, 11 | // constants.Mobile, 12 | constants.Backend, 13 | } 14 | 15 | func PromptSelectService() { 16 | var i InitInfo 17 | var p PromptInput 18 | p.Label = "Pick a service" 19 | p.Items = services 20 | p.GoBack = PromptAlertMessage 21 | 22 | var index int 23 | i.Service, index = p.PromptSelect() 24 | services = ResetItems(p.Items, &index) 25 | i.PromptSelectStack() 26 | } 27 | 28 | func (i *InitInfo) PromptSelectStack() { 29 | 30 | i.Stack = i.PromptStack() 31 | _ = DisplayMultipleStackWarningMessage(i.Service) 32 | if i.Service == constants.Backend { 33 | i.PromptSelectStackDatabase() 34 | } 35 | if DisplayMultipleStackWarningMessage(i.Service) { 36 | // It will redirect to home if the selected service already exists. 37 | PromptHome() 38 | } 39 | i.DirName = i.PromptGetDirectoryName() 40 | i.PromptSelectInit() 41 | } 42 | 43 | // DisplayMultipleStackWarningMessage prints message if the selected service is already exists. 44 | func DisplayMultipleStackWarningMessage(service string) bool { 45 | var serviceExist bool 46 | backendExist, webExist, _ := utils.IsBackendWebAndMobileExist() 47 | var err error 48 | if service == constants.Web { 49 | if webExist { 50 | err = utils.PrintWarningMessage("We are working on supporting multiple frontend stacks in the upcoming releases!") 51 | errorhandler.CheckNilErr(err) 52 | serviceExist = true 53 | } 54 | } else if service == constants.Backend { 55 | if backendExist { 56 | err = utils.PrintWarningMessage("We are working on supporting multiple backend stacks in the upcoming releases!") 57 | errorhandler.CheckNilErr(err) 58 | serviceExist = true 59 | } 60 | } 61 | return serviceExist 62 | } 63 | -------------------------------------------------------------------------------- /pickyhelpers/convert_template_database.go: -------------------------------------------------------------------------------- 1 | package pickyhelpers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/internal/constants" 7 | "github.com/wednesday-solutions/picky/internal/errorhandler" 8 | "github.com/wednesday-solutions/picky/internal/utils" 9 | ) 10 | 11 | func (s StackDetails) ConvertTemplateDatabase() error { 12 | 13 | isDatabaseSupported := true 14 | 15 | switch s.Stack { 16 | case constants.NodeHapiTemplate: 17 | if s.Database == constants.PostgreSQL { 18 | isDatabaseSupported = false 19 | } 20 | 21 | case constants.NodeExpressGraphqlTemplate: 22 | if s.Database == constants.MySQL { 23 | isDatabaseSupported = false 24 | } 25 | default: 26 | return nil 27 | } 28 | 29 | if !isDatabaseSupported { 30 | // Add new dependencies to package.json 31 | err := UpdatePackageDotJson(s.Stack, s.DirName) 32 | errorhandler.CheckNilErr(err) 33 | 34 | // Update env files with respect to new database 35 | err = UpdateEnvFiles(s.Stack, s.DirName) 36 | errorhandler.CheckNilErr(err) 37 | 38 | // Convert DB Connection into MySQL. 39 | dbConfigFile := fmt.Sprintf("%s/%s/%s", utils.CurrentDirectory(), s.DirName, "config/db.js") 40 | err = UpdateDBConfig(s.Stack, dbConfigFile, s.StackInfo) 41 | errorhandler.CheckNilErr(err) 42 | 43 | // Convert queries 44 | err = ConvertQueries(s.Stack, s.DirName) 45 | errorhandler.CheckNilErr(err) 46 | 47 | // Convert testDBConfig 48 | err = ConvertDBConfig(s.Stack, s.DirName) 49 | errorhandler.CheckNilErr(err) 50 | 51 | // Convert dbTests 52 | err = ConvertDBTests(s.Stack, s.DirName) 53 | errorhandler.CheckNilErr(err) 54 | 55 | // Update docker-compose file 56 | err = UpdateDockerCompose(s.Stack, s.DirName, s.StackInfo) 57 | errorhandler.CheckNilErr(err) 58 | } else { 59 | // Update DB_HOST in .env.docker file 60 | err := UpdateEnvDockerFileForDefaultDBInTemplate(s.Stack, s.DirName) 61 | errorhandler.CheckNilErr(err) 62 | } 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /cmd/infra.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/wednesday-solutions/picky/internal/constants" 8 | "github.com/wednesday-solutions/picky/internal/errorhandler" 9 | "github.com/wednesday-solutions/picky/internal/utils" 10 | "github.com/wednesday-solutions/picky/prompt" 11 | ) 12 | 13 | var InfraCmd = InfraCmdFn() 14 | var ( 15 | stacks []string 16 | cloudProvider string 17 | environment string 18 | ) 19 | 20 | func InfraSetup(cmd *cobra.Command, args []string) error { 21 | var ( 22 | err error 23 | stacks []string 24 | cloudProvider string 25 | environment string 26 | ) 27 | stacks, err = cmd.Flags().GetStringSlice(constants.Stacks) 28 | errorhandler.CheckNilErr(err) 29 | 30 | err = utils.CheckStacksExist(stacks) 31 | if err != nil { 32 | return err 33 | } 34 | cloudProvider, err = cmd.Flags().GetString(constants.CloudProvider) 35 | errorhandler.CheckNilErr(err) 36 | environment, err = cmd.Flags().GetString(constants.Environment) 37 | errorhandler.CheckNilErr(err) 38 | 39 | cloudProvider = utils.GetCloudProvider(cloudProvider) 40 | environment = utils.GetEnvironmentValue(environment) 41 | 42 | userOutput := "\nSelected stacks:" 43 | for idx, stack := range stacks { 44 | userOutput = fmt.Sprintf("%s\n %d. %s", userOutput, idx+1, stack) 45 | } 46 | fmt.Printf("%s\nCloud Provider: %s\nEnvironment: %s\n\n", 47 | userOutput, cloudProvider, environment, 48 | ) 49 | err = prompt.CreateInfra(stacks, cloudProvider, environment) 50 | errorhandler.CheckNilErr(err) 51 | return err 52 | } 53 | 54 | func InfraCmdFn() *cobra.Command { 55 | var InfraCmd = &cobra.Command{ 56 | Use: constants.Infra, 57 | RunE: InfraSetup, 58 | } 59 | return InfraCmd 60 | } 61 | 62 | func init() { 63 | ServiceCmd.AddCommand(InfraCmd) 64 | InfraCmd.Flags().StringSliceVarP( 65 | &stacks, constants.Stacks, "t", utils.GetExistingStacks(), utils.UseInfraStacks(), 66 | ) 67 | InfraCmd.Flags().StringVarP( 68 | &cloudProvider, constants.CloudProvider, "p", constants.AWS, utils.UseCloudProvider(), 69 | ) 70 | InfraCmd.Flags().StringVarP( 71 | &environment, constants.Environment, "e", constants.Development, utils.UseEnvironment(), 72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /pickyhelpers/update_db_config.go: -------------------------------------------------------------------------------- 1 | package pickyhelpers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/hbs" 7 | "github.com/wednesday-solutions/picky/internal/constants" 8 | "github.com/wednesday-solutions/picky/internal/errorhandler" 9 | ) 10 | 11 | func UpdateDBConfig(stack, dbFile string, stackInfo map[string]interface{}) error { 12 | 13 | var dbConfigSource string 14 | 15 | switch stack { 16 | case constants.NodeHapiTemplate: 17 | dbConfigSource = fmt.Sprintf(`const pg = require('pg'); 18 | 19 | module.exports = { 20 | url: 21 | process.env.DB_URI || 22 | %spostgres://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASSWORD}@${process.env.POSTGRES_HOST}/${ 23 | process.env.POSTGRES_DB 24 | }%s, 25 | host: process.env.POSTGRES_HOST, 26 | dialectModule: pg, 27 | dialect: 'postgres', 28 | pool: { 29 | min: 0, 30 | max: 10, 31 | idle: 10000, 32 | }, 33 | define: { 34 | underscored: true, 35 | timestamps: false, 36 | }, 37 | }; 38 | `, "`", "`") 39 | 40 | case constants.NodeExpressGraphqlTemplate: 41 | dbConfigSource = fmt.Sprintf(`const Sequelize = require('sequelize'); 42 | const mysql2 = require('mysql2'); 43 | const dotenv = require('dotenv'); 44 | 45 | dotenv.config({ path: {{envEnvironmentName}} }); 46 | 47 | module.exports = { 48 | url: 49 | process.env.DB_URI || 50 | %smysql://${process.env.MYSQL_USER}:${process.env.MYSQL_PASSWORD}@${process.env.MYSQL_HOST}/${ 51 | process.env.MYSQL_DATABASE 52 | }%s, 53 | host: process.env.MYSQL_HOST, 54 | dialectModule: mysql2, 55 | dialect: 'mysql', 56 | pool: { 57 | min: 0, 58 | max: 10, 59 | idle: 10000 60 | }, 61 | define: { 62 | underscored: true, 63 | timestamps: false 64 | }, 65 | retry: { 66 | match: [ 67 | 'unknown timed out', 68 | Sequelize.TimeoutError, 69 | 'timed', 70 | 'timeout', 71 | 'TimeoutError', 72 | 'Operation timeout', 73 | 'refuse', 74 | 'SQLITE_BUSY' 75 | ], 76 | max: 10 // maximum amount of tries. 77 | } 78 | }; 79 | `, "`", "`") 80 | 81 | default: 82 | return fmt.Errorf("Selected stack is invalid") 83 | } 84 | 85 | err := hbs.ParseAndWriteToFile(dbConfigSource, dbFile, stackInfo) 86 | errorhandler.CheckNilErr(err) 87 | 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /pickyhelpers/get_stack_info.go: -------------------------------------------------------------------------------- 1 | package pickyhelpers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/iancoleman/strcase" 7 | "github.com/wednesday-solutions/picky/internal/constants" 8 | "github.com/wednesday-solutions/picky/internal/utils" 9 | ) 10 | 11 | func (s StackDetails) GetStackInfo() map[string]interface{} { 12 | 13 | var webDir, mobileDir, backendDir string 14 | var webDirectories, backendPgDirectories, backendMysqlDirectories []string 15 | currentDir := utils.CurrentDirectory() 16 | projectName := strcase.ToSnake(utils.GetProjectName()) 17 | 18 | _, databases, directories := utils.GetExistingStacksDatabasesAndDirectories() 19 | for i, dirName := range directories { 20 | s.Service = utils.FindService(dirName) 21 | switch s.Service { 22 | case constants.Web: 23 | webDir = dirName 24 | webDirectories = append(webDirectories, dirName) 25 | case constants.Mobile: 26 | mobileDir = dirName 27 | case constants.Backend: 28 | backendDir = dirName 29 | if databases[i] == constants.PostgreSQL { 30 | backendPgDirectories = append(backendPgDirectories, dirName) 31 | } else if databases[i] == constants.MySQL { 32 | backendMysqlDirectories = append(backendMysqlDirectories, dirName) 33 | } 34 | } 35 | } 36 | stackDestination := map[string]string{ 37 | constants.WebStatus: currentDir + "/" + webDir, 38 | constants.MobileStatus: currentDir + "/" + mobileDir, 39 | constants.BackendStatus: currentDir + "/" + backendDir, 40 | } 41 | stackInfo := make(map[string]interface{}) 42 | 43 | for status, destination := range stackDestination { 44 | if destination != fmt.Sprintf("%s/", currentDir) { 45 | stackInfo[status], _ = utils.IsExists(destination) 46 | } else { 47 | stackInfo[status] = false 48 | } 49 | } 50 | stackInfo[constants.Stack] = s.Stack 51 | stackInfo[constants.Database] = s.Database 52 | stackInfo[constants.ProjectName] = projectName 53 | stackInfo[constants.WebDirName] = webDir 54 | stackInfo[constants.MobileDirName] = mobileDir 55 | stackInfo[constants.BackendDirName] = backendDir 56 | stackInfo[constants.ExistingDirectories] = directories 57 | stackInfo[constants.Environment] = s.Environment 58 | stackInfo[constants.WebDirectories] = webDirectories 59 | stackInfo[constants.BackendPgDirectories] = backendPgDirectories 60 | stackInfo[constants.BackendMysqlDirectories] = backendMysqlDirectories 61 | 62 | return stackInfo 63 | } 64 | -------------------------------------------------------------------------------- /cmd/init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/wednesday-solutions/picky/flagcmd" 8 | "github.com/wednesday-solutions/picky/internal/constants" 9 | "github.com/wednesday-solutions/picky/internal/errorhandler" 10 | "github.com/wednesday-solutions/picky/internal/utils" 11 | ) 12 | 13 | func InitStack(cmd *cobra.Command, args []string) error { 14 | 15 | var ( 16 | i flagcmd.InitInfo 17 | err error 18 | errorMessage string 19 | ) 20 | 21 | i.Service, err = cmd.Flags().GetString(constants.Service) 22 | errorhandler.CheckNilErr(err) 23 | i.Stack, err = cmd.Flags().GetString(constants.Stack) 24 | errorhandler.CheckNilErr(err) 25 | i.Database, err = cmd.Flags().GetString(constants.Database) 26 | errorhandler.CheckNilErr(err) 27 | i.Directory, err = cmd.Flags().GetString(constants.Directory) 28 | errorhandler.CheckNilErr(err) 29 | 30 | allFlagsExist := true 31 | if i.Service == "" { 32 | allFlagsExist = false 33 | errorMessage = fmt.Sprintf("%s%s\n", errorMessage, 34 | "add service with the flag of '--service'", 35 | ) 36 | } 37 | if i.Stack == "" { 38 | allFlagsExist = false 39 | errorMessage = fmt.Sprintf("%s%s\n", errorMessage, 40 | "add stack with the flag of '--stack'", 41 | ) 42 | } 43 | if i.Database == "" && service == constants.Backend { 44 | allFlagsExist = false 45 | errorMessage = fmt.Sprintf("%s%s\n", errorMessage, 46 | "add database with the flag of '--database'", 47 | ) 48 | } 49 | if i.Directory == "" { 50 | allFlagsExist = false 51 | errorMessage = fmt.Sprintf("%s%s\n", errorMessage, 52 | "add directory with the flag of '--directory'", 53 | ) 54 | } 55 | if !allFlagsExist { 56 | return fmt.Errorf("%s", errorMessage) 57 | } 58 | err = i.FlagInit() 59 | return err 60 | } 61 | 62 | func InitCmdFn() *cobra.Command { 63 | var InitCommand = &cobra.Command{ 64 | Use: constants.Init, 65 | RunE: InitStack, 66 | } 67 | return InitCommand 68 | } 69 | 70 | var InitCmd = InitCmdFn() 71 | var ( 72 | service string 73 | stack string 74 | database string 75 | directory string 76 | ) 77 | 78 | func init() { 79 | ServiceCmd.AddCommand(InitCmd) 80 | InitCmd.Flags().StringVarP(&service, constants.Service, "s", "", utils.UseService()) 81 | InitCmd.Flags().StringVarP(&stack, constants.Stack, "t", "", utils.UseStack()) 82 | InitCmd.Flags().StringVarP(&database, constants.Database, "d", "", utils.UseDatabase()) 83 | InitCmd.Flags().StringVarP(&directory, constants.Directory, "f", "", utils.UseDirectory()) 84 | } 85 | -------------------------------------------------------------------------------- /internal/utils/message.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "text/template" 7 | 8 | "github.com/fatih/color" 9 | "github.com/wednesday-solutions/picky/internal/constants" 10 | "github.com/wednesday-solutions/picky/internal/errorhandler" 11 | ) 12 | 13 | // CreateMessageTemplate creates new text template for printing colorful logs. 14 | func CreateMessageTemplate(name, text string) *template.Template { 15 | tpl, err := template.New(name).Parse(text) 16 | errorhandler.CheckNilErr(err) 17 | tpl = template.Must(tpl, err) 18 | return tpl 19 | } 20 | 21 | // PrintMultiSelectMessage prints multi selected options. 22 | func PrintMultiSelectMessage(messages []string) error { 23 | var message, coloredMessage string 24 | var tpl *template.Template 25 | if len(messages) > constants.Zero { 26 | var templateText string 27 | if len(messages) == constants.One { 28 | templateText = fmt.Sprintf("%s %d option selected: {{ . }}\n", 29 | constants.IconSelect, 30 | len(messages)) 31 | } else { 32 | templateText = fmt.Sprintf("%s %d options selected: {{ . }}\n", 33 | constants.IconSelect, 34 | len(messages)) 35 | } 36 | for _, option := range messages { 37 | message = fmt.Sprintf("%s%s ", message, option) 38 | } 39 | coloredMessage = color.GreenString("%s", message) 40 | tpl = CreateMessageTemplate("message", templateText) 41 | } else { 42 | message = "No options selected, please select atleast one." 43 | coloredMessage = color.YellowString("%s", message) 44 | tpl = CreateMessageTemplate("responseMessage", fmt.Sprintf("%s {{ . }}\n", constants.IconWarn)) 45 | } 46 | err := tpl.Execute(os.Stdout, coloredMessage) 47 | return err 48 | } 49 | 50 | // PrintWarningMessage prints given message in yellow color as warning message in terminal. 51 | func PrintWarningMessage(message string) error { 52 | tpl := CreateMessageTemplate("warningMessage", fmt.Sprintf("\n%s {{ . }}\n", constants.IconWarn)) 53 | message = color.YellowString("%s", message) 54 | err := tpl.Execute(os.Stdout, message) 55 | return err 56 | } 57 | 58 | // PrintInfoMessage prints given message in cyan color as info message in terminal. 59 | func PrintInfoMessage(message string) error { 60 | tpl := CreateMessageTemplate("InfoMessage", fmt.Sprintf("\n%s {{ . }}\n", constants.IconChoose)) 61 | message = color.CyanString("%s", message) 62 | err := tpl.Execute(os.Stdout, message) 63 | return err 64 | } 65 | 66 | // PrintErrorMessage prints the given message in red color as error message 67 | func PrintErrorMessage(message string) error { 68 | tpl := CreateMessageTemplate("errorMessage", fmt.Sprintf("\n{{ . }}%s\n", errorhandler.Exclamation)) 69 | message = color.RedString("%s", message) 70 | err := tpl.Execute(os.Stdout, message) 71 | return err 72 | } 73 | -------------------------------------------------------------------------------- /flagcmd/stacks.go: -------------------------------------------------------------------------------- 1 | package flagcmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/internal/utils" 7 | ) 8 | 9 | type StackFlag struct { 10 | // all available stacks status 11 | A bool 12 | // all existing stacks status 13 | E bool 14 | // web stacks status 15 | W bool 16 | // mobile stacks status 17 | M bool 18 | // backend stacks status 19 | B bool 20 | } 21 | 22 | func (f StackFlag) FlagStacks() string { 23 | 24 | var stacks []string 25 | var userOutput string 26 | if f.A { 27 | f.E = false 28 | userOutput = utils.AllStacksString() 29 | userOutput = fmt.Sprintf("\nAll stacks: %s", userOutput) 30 | if f.B || f.M || f.W { 31 | userOutput = "" 32 | } 33 | if f.B { 34 | userOutput = fmt.Sprintf("%s%s", utils.AllBackendStacksString(), userOutput) 35 | } 36 | if f.M { 37 | userOutput = fmt.Sprintf("%s%s", utils.AllMobileStacksString(), userOutput) 38 | } 39 | if f.W { 40 | userOutput = fmt.Sprintf("%s%s", utils.AllWebStacksString(), userOutput) 41 | } 42 | } 43 | if f.E { 44 | stacks = utils.GetExistingStacks() 45 | userOutput = utils.ConvertStacksIntoString(stacks) 46 | userOutput = fmt.Sprintf("\nAll existing stacks: %s", userOutput) 47 | if f.B || f.M || f.W { 48 | userOutput = "" 49 | } 50 | if f.B { 51 | var backendStacks []string 52 | backendStack, status := "", false 53 | for _, stack := range stacks { 54 | backendStack, status = utils.IsBackendStack(stack) 55 | if status { 56 | backendStacks = append(backendStacks, backendStack) 57 | } 58 | } 59 | if len(backendStacks) == 0 { 60 | userOutput = fmt.Sprintf("\n\tNo backend stacks exist.%s", userOutput) 61 | } else { 62 | userOutput = fmt.Sprintf("%s%s", utils.ConvertStacksIntoString(backendStacks), userOutput) 63 | } 64 | userOutput = fmt.Sprintf("\nAll existing backend stacks: %s", userOutput) 65 | } 66 | if f.M { 67 | var mobileStacks []string 68 | mobileStack, status := "", false 69 | for _, stack := range stacks { 70 | mobileStack, status = utils.IsMobileStack(stack) 71 | if status { 72 | mobileStacks = append(mobileStacks, mobileStack) 73 | } 74 | } 75 | if len(mobileStacks) == 0 { 76 | userOutput = fmt.Sprintf("\n\tNo mobile stacks exist.%s", userOutput) 77 | } else { 78 | userOutput = fmt.Sprintf("%s%s", utils.ConvertStacksIntoString(mobileStacks), userOutput) 79 | } 80 | userOutput = fmt.Sprintf("\nAll existing mobile stacks: %s", userOutput) 81 | } 82 | if f.W { 83 | var webStacks []string 84 | webStack, status := "", false 85 | for _, stack := range stacks { 86 | webStack, status = utils.IsWebStack(stack) 87 | if status { 88 | webStacks = append(webStacks, webStack) 89 | } 90 | } 91 | if len(webStacks) == 0 { 92 | userOutput = fmt.Sprintf("\n\tNo web stacks exist.%s", userOutput) 93 | } else { 94 | userOutput = fmt.Sprintf("%s%s", utils.ConvertStacksIntoString(webStacks), userOutput) 95 | } 96 | userOutput = fmt.Sprintf("\nAll existing web stacks: %s", userOutput) 97 | } 98 | } 99 | return userOutput 100 | } 101 | -------------------------------------------------------------------------------- /cmd/create.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/spf13/cobra" 8 | "github.com/wednesday-solutions/picky/internal/constants" 9 | "github.com/wednesday-solutions/picky/internal/errorhandler" 10 | "github.com/wednesday-solutions/picky/internal/utils" 11 | "github.com/wednesday-solutions/picky/pickyhelpers" 12 | "github.com/wednesday-solutions/picky/prompt" 13 | ) 14 | 15 | func CreateService(cmd *cobra.Command, args []string) error { 16 | 17 | var ( 18 | flagDockercompose bool 19 | flagCI bool 20 | flagCD bool 21 | flagPlatform string 22 | flagStacks []string 23 | flagEnv string 24 | err error 25 | ) 26 | 27 | flagDockercompose, err = cmd.Flags().GetBool(constants.DockerComposeFlag) 28 | errorhandler.CheckNilErr(err) 29 | 30 | flagCI, err = cmd.Flags().GetBool(constants.CIFlag) 31 | errorhandler.CheckNilErr(err) 32 | 33 | flagCD, err = cmd.Flags().GetBool(constants.CDFlag) 34 | errorhandler.CheckNilErr(err) 35 | 36 | flagPlatform, err = cmd.Flags().GetString(constants.Platform) 37 | errorhandler.CheckNilErr(err) 38 | 39 | flagStacks, err = cmd.Flags().GetStringSlice(constants.Stacks) 40 | errorhandler.CheckNilErr(err) 41 | 42 | flagEnv, err = cmd.Flags().GetString(constants.Environment) 43 | errorhandler.CheckNilErr(err) 44 | 45 | err = utils.CheckStacksExist(flagStacks) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | fmt.Printf("\nDocker Compose: %v\nCI: %v\nCD: %v\nPlatform: %s\nStacks: %v\n", 51 | flagDockercompose, flagCI, flagCD, flagPlatform, flagStacks, 52 | ) 53 | 54 | if !strings.EqualFold(flagPlatform, constants.GitHub) { 55 | return fmt.Errorf("Only GitHub is available now.\n") 56 | } 57 | if flagDockercompose { 58 | err = prompt.GenerateDockerCompose() 59 | errorhandler.CheckNilErr(err) 60 | } 61 | if flagCI { 62 | err = pickyhelpers.CreateCI(flagStacks) 63 | errorhandler.CheckNilErr(err) 64 | } 65 | if flagCD { 66 | err = prompt.CreateCD(flagStacks, flagEnv) 67 | errorhandler.CheckNilErr(err) 68 | } 69 | return err 70 | } 71 | 72 | func CreateCmdFn() *cobra.Command { 73 | var CreateCommand = &cobra.Command{ 74 | Use: constants.Create, 75 | RunE: CreateService, 76 | } 77 | return CreateCommand 78 | } 79 | 80 | var CreateCmd = CreateCmdFn() 81 | var ( 82 | ci bool 83 | cd bool 84 | dockerCompose bool 85 | platform string 86 | flagEnvironment string 87 | ) 88 | 89 | func init() { 90 | ServiceCmd.AddCommand(CreateCmd) 91 | CreateCmd.Flags().BoolVarP(&ci, constants.CIFlag, "i", false, utils.UseCI()) 92 | CreateCmd.Flags().BoolVarP(&cd, constants.CDFlag, "d", false, utils.UseCD()) 93 | CreateCmd.Flags().BoolVarP( 94 | &dockerCompose, constants.DockerComposeFlag, "c", false, utils.UseDockerCompose(), 95 | ) 96 | CreateCmd.Flags().StringVarP( 97 | &platform, constants.Platform, "p", constants.Github, utils.UsePlatform(), 98 | ) 99 | CreateCmd.Flags().StringSliceVarP( 100 | &stacks, constants.Stacks, "t", utils.GetExistingStacks(), utils.UseInfraStacks(), 101 | ) 102 | CreateCmd.Flags().StringVarP( 103 | &flagEnvironment, constants.Environment, "e", constants.Development, utils.UseEnvironment(), 104 | ) 105 | } 106 | -------------------------------------------------------------------------------- /hbs/hbs.go: -------------------------------------------------------------------------------- 1 | package hbs 2 | 3 | import ( 4 | "github.com/aymerick/raymond" 5 | "github.com/wednesday-solutions/picky/internal/constants" 6 | "github.com/wednesday-solutions/picky/internal/errorhandler" 7 | "github.com/wednesday-solutions/picky/internal/utils" 8 | ) 9 | 10 | func init() { 11 | raymond.RegisterHelper("databaseVolumeConnection", DatabaseVolumeConnection) 12 | raymond.RegisterHelper("dbVersion", DBVersion) 13 | raymond.RegisterHelper("portConnection", PortConnection) 14 | raymond.RegisterHelper("globalAddDependencies", GlobalAddDependencies) 15 | raymond.RegisterHelper("addDependencies", AddDependencies) 16 | raymond.RegisterHelper("runBuildEnvironment", RunBuildEnvironment) 17 | raymond.RegisterHelper("waitForDBService", WaitForDBService) 18 | raymond.RegisterHelper("dependsOnFieldOfGo", DependsOnFieldOfGo) 19 | raymond.RegisterHelper("cmdDockerfile", CmdDockerfile) 20 | raymond.RegisterHelper("envEnvironmentName", EnvEnvironmentName) 21 | raymond.RegisterHelper("deployStacks", DeployStacks) 22 | raymond.RegisterHelper("sstImportStacks", SstImportStacks) 23 | } 24 | 25 | func ParseAndWriteToFile(source, filePath string, stackInfo map[string]interface{}) error { 26 | 27 | ctx := map[string]interface{}{ 28 | constants.Frontend: constants.Frontend, 29 | constants.Web: constants.Web, 30 | constants.Mobile: constants.Mobile, 31 | constants.Backend: constants.Backend, 32 | constants.Redis: constants.Redis, 33 | constants.Postgres: constants.Postgres, 34 | constants.PostgreSQL: constants.PostgreSQL, 35 | constants.Mysql: constants.Mysql, 36 | constants.MySQL: constants.MySQL, 37 | constants.GolangMySQLTemplate: constants.GolangMySQLTemplate, 38 | constants.GolangPostgreSQLTemplate: constants.GolangPostgreSQLTemplate, 39 | constants.Stack: stackInfo[constants.Stack].(string), 40 | constants.Database: stackInfo[constants.Database].(string), 41 | constants.ProjectName: stackInfo[constants.ProjectName].(string), 42 | constants.WebStatus: stackInfo[constants.WebStatus].(bool), 43 | constants.MobileStatus: stackInfo[constants.MobileStatus].(bool), 44 | constants.BackendStatus: stackInfo[constants.BackendStatus].(bool), 45 | constants.WebDirName: stackInfo[constants.WebDirName].(string), 46 | constants.MobileDirName: stackInfo[constants.MobileDirName].(string), 47 | constants.BackendDirName: stackInfo[constants.BackendDirName].(string), 48 | constants.ExistingDirectories: stackInfo[constants.ExistingDirectories].([]string), 49 | constants.WebDirectories: stackInfo[constants.WebDirectories].([]string), 50 | constants.BackendPgDirectories: stackInfo[constants.BackendPgDirectories].([]string), 51 | constants.BackendMysqlDirectories: stackInfo[constants.BackendMysqlDirectories].([]string), 52 | } 53 | // Parse the source string into template 54 | tpl, err := raymond.Parse(source) 55 | errorhandler.CheckNilErr(err) 56 | 57 | // Execute the template into string 58 | executedTemplate, err := tpl.Exec(ctx) 59 | errorhandler.CheckNilErr(err) 60 | 61 | err = utils.WriteToFile(filePath, executedTemplate) 62 | errorhandler.CheckNilErr(err) 63 | 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /pickyhelpers/create_docker_files.go: -------------------------------------------------------------------------------- 1 | package pickyhelpers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/hbs" 7 | "github.com/wednesday-solutions/picky/internal/constants" 8 | "github.com/wednesday-solutions/picky/internal/errorhandler" 9 | "github.com/wednesday-solutions/picky/internal/utils" 10 | ) 11 | 12 | func (s StackDetails) CreateDockerFiles() error { 13 | 14 | var ( 15 | path string 16 | source string 17 | fileFound bool 18 | err error 19 | ) 20 | 21 | if s.StackInfo[constants.WebStatus].(bool) { 22 | 23 | path = fmt.Sprintf("%s/%s/%s", utils.CurrentDirectory(), 24 | s.DirName, 25 | constants.DockerFile, 26 | ) 27 | fileFound, _ = utils.IsExists(path) 28 | if !fileFound { 29 | source = `FROM node:14-alpine as baseimage 30 | RUN mkdir app/ 31 | ADD . app/ 32 | WORKDIR /app 33 | 34 | RUN npm install 35 | 36 | FROM baseimage 37 | CMD {{{cmdDockerfile stack}}} 38 | EXPOSE 3000` 39 | 40 | err = hbs.ParseAndWriteToFile(source, path, s.StackInfo) 41 | errorhandler.CheckNilErr(err) 42 | } 43 | path = fmt.Sprintf("%s/%s/%s", utils.CurrentDirectory(), 44 | s.DirName, 45 | constants.DockerEnvFile, 46 | ) 47 | fileFound, _ = utils.IsExists(path) 48 | if !fileFound { 49 | source = `GITHUB_URL=https://api.github.com/` 50 | 51 | err = utils.WriteToFile(path, source) 52 | errorhandler.CheckNilErr(err) 53 | } 54 | path = fmt.Sprintf("%s/%s/%s", utils.CurrentDirectory(), 55 | s.DirName, 56 | constants.DockerIgnoreFile, 57 | ) 58 | fileFound, _ = utils.IsExists(path) 59 | if !fileFound { 60 | source = "node_modules\n.git\nbadges" 61 | err = utils.WriteToFile(path, source) 62 | errorhandler.CheckNilErr(err) 63 | } 64 | } 65 | 66 | // Add mobile related files. 67 | // if stackInfo[constants.MobileStatus].(bool) {} 68 | 69 | if s.StackInfo[constants.BackendStatus].(bool) { 70 | 71 | switch s.StackInfo[constants.Stack] { 72 | case constants.NodeExpressGraphqlTemplate, constants.NodeHapiTemplate: 73 | 74 | path = fmt.Sprintf("%s/%s/%s", utils.CurrentDirectory(), 75 | s.DirName, 76 | constants.DockerIgnoreFile, 77 | ) 78 | fileFound, _ = utils.IsExists(path) 79 | if !fileFound { 80 | source = "node_modules\n.git\nbadges" 81 | err = utils.WriteToFile(path, source) 82 | errorhandler.CheckNilErr(err) 83 | } 84 | 85 | path = fmt.Sprintf("%s/%s/%s", utils.CurrentDirectory(), 86 | s.DirName, 87 | constants.DockerFile, 88 | ) 89 | fileFound, _ = utils.IsExists(path) 90 | backendPortNumber := utils.FetchExistingPortNumber(s.DirName, constants.BackendPort) 91 | if fileFound { 92 | source = fmt.Sprintf(`FROM node:16 93 | ARG ENVIRONMENT_NAME 94 | ARG BUILD_NAME 95 | RUN mkdir -p /app-build 96 | ADD . /app-build 97 | WORKDIR /app-build 98 | RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn --frozen-lockfile 99 | RUN yarn 100 | RUN yarn {{runBuildEnvironment stack}} 101 | 102 | FROM node:16-alpine 103 | ARG ENVIRONMENT_NAME 104 | ARG BUILD_NAME 105 | RUN mkdir -p /dist 106 | RUN apk add yarn 107 | RUN yarn global add {{globalAddDependencies database}} 108 | RUN yarn add {{addDependencies database}} 109 | ADD scripts/migrate-and-run.sh / 110 | ADD package.json / 111 | ADD . / 112 | COPY --from=0 /app-build/dist ./dist 113 | 114 | CMD ["sh", "./migrate-and-run.sh"] 115 | EXPOSE %s`, backendPortNumber) 116 | 117 | err = hbs.ParseAndWriteToFile(source, path, s.StackInfo) 118 | errorhandler.CheckNilErr(err) 119 | } 120 | } 121 | } 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /prompt/cicd.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/internal/constants" 7 | "github.com/wednesday-solutions/picky/internal/errorhandler" 8 | "github.com/wednesday-solutions/picky/internal/utils" 9 | "github.com/wednesday-solutions/picky/pickyhelpers" 10 | ) 11 | 12 | func PromptCICD() { 13 | var p PromptInput 14 | platform := p.PromptPlatform() 15 | p.Label = "Select option" 16 | p.Items = []string{constants.CreateCI, constants.CreateCD} 17 | p.GoBack = PromptHome 18 | selectedOptions, _ := p.PromptMultiSelect() 19 | for len(selectedOptions) == 0 { 20 | selectedOptions, _ = p.PromptMultiSelect() 21 | } 22 | // Uncomment the below line if we environment prompt. 23 | // environment := PromptEnvironment() 24 | environment := constants.Development 25 | stacks := PromptSelectExistingStacks() 26 | 27 | if platform == constants.GitHub { 28 | isCreateCD := false 29 | for _, option := range selectedOptions { 30 | if option == constants.CreateCI { 31 | 32 | err := pickyhelpers.CreateCI(stacks) 33 | errorhandler.CheckNilErr(err) 34 | 35 | } else if option == constants.CreateCD { 36 | 37 | isCreateCD = true 38 | err := CreateCD(stacks, environment) 39 | errorhandler.CheckNilErr(err) 40 | } 41 | } 42 | if isCreateCD { 43 | var backendExist, webExist bool 44 | for _, stackDir := range stacks { 45 | service := utils.FindService(stackDir) 46 | if service == constants.Backend { 47 | // Create task-definition.json if the stack is backend. 48 | err := pickyhelpers.CreateTaskDefinition(stackDir, environment) 49 | errorhandler.CheckNilErr(err) 50 | backendExist = true 51 | 52 | // Update existing env file of selected environment. 53 | err = pickyhelpers.UpdateEnvByEnvironment(stackDir, environment) 54 | errorhandler.CheckNilErr(err) 55 | 56 | } else if service == constants.Web { 57 | webExist = true 58 | } 59 | } 60 | fmt.Printf("%s", errorhandler.DoneMessage) 61 | PrintGitHubSecretsInfo(backendExist, webExist) 62 | } 63 | } 64 | PromptHome() 65 | } 66 | 67 | func (p PromptInput) PromptPlatform() string { 68 | p.Label = "Choose a platform" 69 | p.Items = []string{constants.GitHub} 70 | p.GoBack = PromptHome 71 | platform, _ := p.PromptSelect() 72 | return platform 73 | } 74 | 75 | func PrintGitHubSecretsInfo(backendExist, webExist bool) { 76 | err := utils.PrintInfoMessage("Save the following config data in GitHub secrets after the deployment.") 77 | errorhandler.CheckNilErr(err) 78 | secrets, count := "\n", 1 79 | 80 | secretKeys := []string{ 81 | constants.AwsRegion, 82 | constants.AwsAccessKeyId, 83 | constants.AwsSecretAccessKey, 84 | constants.AwsEcrRepository, 85 | } 86 | if backendExist { 87 | for _, key := range secretKeys { 88 | secrets = fmt.Sprintf("%s %d. %s\n", secrets, count, key) 89 | count++ 90 | } 91 | } 92 | if webExist { 93 | secrets = fmt.Sprintf("%s %d. %s\n", secrets, count, constants.DistributionId) 94 | } 95 | fmt.Printf("%s\n", secrets) 96 | } 97 | 98 | func CreateCD(directories []string, environment string) error { 99 | for _, dirName := range directories { 100 | var s pickyhelpers.StackDetails 101 | s.DirName = dirName 102 | s.Service = utils.FindService(dirName) 103 | s.Environment = environment 104 | s.Stack, s.Database = utils.FindStackAndDatabase(dirName) 105 | err := s.CreateCDFile() 106 | if err != nil { 107 | if err.Error() != errorhandler.ErrExist.Error() { 108 | errorhandler.CheckNilErr(err) 109 | } 110 | } 111 | } 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /prompt/init.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/wednesday-solutions/picky/internal/constants" 9 | "github.com/wednesday-solutions/picky/internal/errorhandler" 10 | "github.com/wednesday-solutions/picky/internal/utils" 11 | "github.com/wednesday-solutions/picky/pickyhelpers" 12 | ) 13 | 14 | type InitInfo struct { 15 | Service string 16 | Stack string 17 | Database string 18 | DirName string 19 | } 20 | 21 | func (i *InitInfo) PromptSelectInit() { 22 | var p PromptInput 23 | p.GoBack = PromptSelectService 24 | response := true 25 | for response { 26 | p.Label = fmt.Sprintf("Do you want to initialize '%s' as '%s'", i.Stack, i.DirName) 27 | response = p.PromptYesOrNoSelect() 28 | if response { 29 | i.Init() 30 | } else { 31 | response = PromptConfirm() 32 | if response { 33 | PromptHome() 34 | } else { 35 | response = true 36 | } 37 | } 38 | } 39 | } 40 | 41 | func (i *InitInfo) Init() { 42 | var p PromptInput 43 | p.GoBack = PromptSelectService 44 | currentDir := utils.CurrentDirectory() 45 | if i.Stack == constants.GolangEchoTemplate { 46 | i.Stack = fmt.Sprintf("%s-%s", strings.Split(i.Stack, " ")[0], i.Database) 47 | } 48 | destination := filepath.Join(currentDir, i.DirName) 49 | status, _ := utils.IsExists(destination) 50 | var response bool 51 | for status { 52 | p.Label = fmt.Sprintf("The '%s' already exists, do you want to update it", i.DirName) 53 | response = p.PromptYesOrNoSelect() 54 | if response { 55 | // Delete all contents of existing directory. 56 | err := utils.RemoveAllContents(destination) 57 | errorhandler.CheckNilErr(err) 58 | break 59 | } else { 60 | PromptHome() 61 | } 62 | } 63 | if !status { 64 | // Create directory with directory name we got. 65 | err := utils.MakeDirectory(currentDir, i.DirName) 66 | errorhandler.CheckNilErr(err) 67 | response = true 68 | } 69 | if response { 70 | err := i.StackInitialize() 71 | errorhandler.CheckNilErr(err) 72 | } 73 | p.Label = "Do you want to initialize another service" 74 | response = p.PromptYesOrNoSelect() 75 | if response { 76 | PromptSelectService() 77 | } else { 78 | PromptHome() 79 | } 80 | } 81 | 82 | func (i InitInfo) StackInitialize() error { 83 | 84 | done := make(chan bool) 85 | go pickyhelpers.ProgressBar(100, "Downloading", done) 86 | 87 | // Clone the selected repo into service directory. 88 | var s pickyhelpers.StackDetails 89 | s.Service = i.Service 90 | s.Stack = i.Stack 91 | s.DirName = i.DirName 92 | s.CurrentDir = utils.CurrentDirectory() 93 | s.Database = i.Database 94 | err := s.CloneRepo() 95 | errorhandler.CheckNilErr(err) 96 | 97 | // Delete .git folder inside the cloned repo. 98 | err = s.DeleteDotGitFolder() 99 | errorhandler.CheckNilErr(err) 100 | 101 | // stackInfo gives the information about the stacks which is present in the root. 102 | s.Environment = constants.Environment 103 | s.StackInfo = s.GetStackInfo() 104 | // Database conversion 105 | if i.Service == constants.Backend { 106 | err = s.ConvertTemplateDatabase() 107 | errorhandler.CheckNilErr(err) 108 | } 109 | // create and update docker files 110 | err = s.CreateDockerFiles() 111 | errorhandler.CheckNilErr(err) 112 | 113 | if !ShowPromptGitInit() { 114 | file := fmt.Sprintf("%s/%s", 115 | utils.CurrentDirectory(), 116 | constants.DotGitIgnore, 117 | ) 118 | err = WriteDotGitignoreFile(file) 119 | errorhandler.CheckNilErr(err) 120 | } 121 | 122 | <-done 123 | fmt.Printf("\nDownloading %s", errorhandler.CompleteMessage) 124 | 125 | return err 126 | } 127 | -------------------------------------------------------------------------------- /pickyhelpers/convert_dbtests.go: -------------------------------------------------------------------------------- 1 | package pickyhelpers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/internal/constants" 7 | "github.com/wednesday-solutions/picky/internal/utils" 8 | ) 9 | 10 | func ConvertDBTests(stack, dirName string) error { 11 | 12 | if stack == constants.NodeExpressGraphqlTemplate { 13 | 14 | file := fmt.Sprintf("%s/%s/%s/%s/%s/%s", 15 | utils.CurrentDirectory(), 16 | dirName, 17 | "server", 18 | constants.Database, 19 | "tests", 20 | "index.test.js", 21 | ) 22 | source := `import SequelizeMock from 'sequelize-mock'; 23 | import mysql2 from 'mysql2'; 24 | import { resetAndMockDB } from '@utils/testUtils'; 25 | import { DB_ENV } from '@server/utils/testUtils/dbConfig'; 26 | 27 | const mocks = {}; 28 | describe('getClient', () => { 29 | afterAll(() => { 30 | resetAndMockDB(); 31 | }); 32 | it('successfully get DB Client', async () => { 33 | jest.unmock('@database'); 34 | mocks.sequelize = SequelizeMock; 35 | jest.doMock('sequelize', () => mocks.sequelize); 36 | jest.spyOn(mocks, 'sequelize'); 37 | const { getClient } = require('../../database'); 38 | const client = await getClient(); 39 | await expect(client).toBeInstanceOf(mocks.sequelize); 40 | 41 | expect(mocks.sequelize.mock.calls.length).toEqual(1); 42 | expect(mocks.sequelize.mock.calls[0][0]).toEqual(DB_ENV.DB_URI); 43 | expect(mocks.sequelize.mock.calls[0][1]).toEqual({ 44 | url: DB_ENV.DB_URI, 45 | host: DB_ENV.MYSQL_HOST, 46 | dialectModule: mysql2, 47 | dialect: 'mysql', 48 | logging: false, 49 | pool: { 50 | min: 0, 51 | max: 10, 52 | idle: 10000 53 | }, 54 | define: { 55 | underscored: true, 56 | timestamps: false 57 | }, 58 | retry: { 59 | match: [ 60 | 'unknown timed out', 61 | mocks.sequelize.TimeoutError, 62 | 'timed', 63 | 'timeout', 64 | 'TimeoutError', 65 | 'Operation timeout', 66 | 'refuse', 67 | 'SQLITE_BUSY' 68 | ], 69 | max: 10 70 | } 71 | }); 72 | }); 73 | it('throw error on failure', async () => { 74 | jest.unmock('@database'); 75 | mocks.sequelize = SequelizeMock; 76 | jest.doMock('sequelize', () => new Error()); 77 | jest.spyOn(mocks, 'sequelize'); 78 | 79 | const { getClient } = require('../../database'); 80 | await expect(getClient).toThrow(expect.any(Error)); 81 | }); 82 | }); 83 | 84 | describe('connect', () => { 85 | it('successfully connect to the database', async () => { 86 | jest.unmock('@database'); 87 | mocks.sequelize = SequelizeMock; 88 | jest.doMock('sequelize', () => mocks.sequelize); 89 | 90 | const { getClient, connect } = require('../../database'); 91 | const client = await getClient(); 92 | jest.spyOn(client, 'authenticate'); 93 | jest.spyOn(console, 'log'); 94 | await connect(); 95 | expect(client.authenticate.mock.calls.length).toBe(1); 96 | expect(console.log.mock.calls.length).toBe(1); 97 | expect(console.log.mock.calls.length).toBe(1); 98 | expect(console.log.mock.calls[0][0]).toBe('Connection has been established successfully.\n'); 99 | expect(console.log.mock.calls[0][1]).toEqual({ 100 | db_uri: process.env.DB_URI 101 | }); 102 | }); 103 | 104 | it('should throw an error if connection fails', async () => { 105 | jest.unmock('@database'); 106 | mocks.sequelize = SequelizeMock; 107 | jest.doMock('sequelize', () => mocks.sequelize); 108 | 109 | const { getClient, connect } = require('../../database'); 110 | const client = await getClient(); 111 | const error = new Error('failed'); 112 | client.authenticate = async () => { 113 | await expect(connect()).rejects.toEqual(error); 114 | jest.spyOn(client, 'authenticate'); 115 | jest.spyOn(console, 'log'); 116 | throw error; 117 | }; 118 | }); 119 | }); 120 | ` 121 | err := utils.WriteToFile(file, source) 122 | return err 123 | } 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /internal/utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/wednesday-solutions/picky/internal/errorhandler" 9 | ) 10 | 11 | // IsExists will check if the path exists or no. 12 | func IsExists(path string) (bool, error) { 13 | _, err := os.Stat(path) 14 | if err == nil { 15 | return true, nil 16 | } else if os.IsNotExist(err) { 17 | return false, nil 18 | } else { 19 | return false, err 20 | } 21 | } 22 | 23 | // CurrentDirectory will give the root directory. 24 | func CurrentDirectory() string { 25 | path, err := filepath.Abs(".") 26 | if err != nil { 27 | return "" 28 | } else { 29 | return path 30 | } 31 | } 32 | 33 | // MakeDirectory will make directory according to input. 34 | func MakeDirectory(path string, dirName string) error { 35 | err := os.Mkdir(path+"/"+dirName, 0755) 36 | if err != nil { 37 | return err 38 | } else { 39 | return nil 40 | } 41 | } 42 | 43 | // CreateDirectory creates directory according to the input. 44 | func CreateDirectory(filePath string) error { 45 | err := os.Mkdir(filePath, 0755) 46 | return err 47 | } 48 | 49 | // MakeFile will create new file according to input path and file name. 50 | func MakeFile(path, fileName string) error { 51 | _, err := os.Create(path + "/" + fileName) 52 | if err != nil { 53 | return err 54 | } 55 | return nil 56 | } 57 | 58 | func CreateFile(file string) error { 59 | _, err := os.Create(file) 60 | errorhandler.CheckNilErr(err) 61 | 62 | return nil 63 | } 64 | 65 | // Remove a single file 66 | func RemoveFile(path string) error { 67 | return os.Remove(path) 68 | } 69 | 70 | // Remove all the files existing in the path directory and the path. 71 | func RemoveAll(path string) error { 72 | return os.RemoveAll(path) 73 | } 74 | 75 | // Remove all existing files in the given path. 76 | func RemoveAllContents(path string) error { 77 | dirNames, err := ReadAllContents(path) 78 | errorhandler.CheckNilErr(err) 79 | for _, dir := range dirNames { 80 | err = RemoveAll(filepath.Join(path, dir)) 81 | } 82 | return err 83 | } 84 | 85 | // Read all existing in the given path. 86 | func ReadAllContents(path string) ([]string, error) { 87 | file, err := os.Open(path) 88 | errorhandler.CheckNilErr(err) 89 | return file.Readdirnames(-1) 90 | } 91 | 92 | // TruncateAndWriteToFile will delete all the existing data and write input data into the file. 93 | func TruncateAndWriteToFile(path, file, data string) error { 94 | // Opens file with read and write permissions. 95 | openFile, err := os.OpenFile(fmt.Sprintf("%s/%s", path, file), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) 96 | if err != nil { 97 | return err 98 | } 99 | defer openFile.Close() 100 | 101 | _, err = openFile.WriteString(data) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | err = openFile.Sync() 107 | if err != nil { 108 | return err 109 | } 110 | return nil 111 | } 112 | 113 | // WriteToFile will delete all the existing data and write input data into the file. 114 | func WriteToFile(file, data string) error { 115 | // Opens file with read and write permissions. 116 | openFile, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) 117 | if err != nil { 118 | return err 119 | } 120 | defer openFile.Close() 121 | 122 | _, err = openFile.WriteString(data) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | err = openFile.Sync() 128 | if err != nil { 129 | return err 130 | } 131 | return nil 132 | } 133 | 134 | // AppendToFile will append given string to existing file. 135 | func AppendToFile(path, data string) error { 136 | openFile, err := os.OpenFile(path, os.O_APPEND|os.O_RDWR, 0644) 137 | errorhandler.CheckNilErr(err) 138 | 139 | defer openFile.Close() 140 | 141 | _, err = openFile.WriteString(data) 142 | errorhandler.CheckNilErr(err) 143 | 144 | err = openFile.Sync() 145 | errorhandler.CheckNilErr(err) 146 | 147 | return nil 148 | } 149 | -------------------------------------------------------------------------------- /prompt/promptutils.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/wednesday-solutions/picky/internal/constants" 8 | "github.com/wednesday-solutions/picky/internal/errorhandler" 9 | "github.com/wednesday-solutions/picky/internal/utils" 10 | ) 11 | 12 | // ResetItems reset the values of items. index means last selected item's index. 13 | func ResetItems(items []string, index *int) []string { 14 | if index != nil { 15 | var updatedItems []string 16 | var selectedItem string 17 | for idx, item := range items { 18 | if idx != *index { 19 | updatedItems = append(updatedItems, item) 20 | } else { 21 | selectedItem = item 22 | } 23 | } 24 | updatedItems = append(updatedItems, selectedItem) 25 | items = updatedItems 26 | } 27 | return items 28 | } 29 | 30 | func (i *InitInfo) PromptGetDirectoryName() string { 31 | var p PromptInput 32 | suffix := utils.GetSuffixOfStack(i.Stack, i.Database) 33 | exampleLabel := fmt.Sprintf("('-%s' suffix will be added). Eg: test-%s ", suffix, suffix) 34 | p.Label = fmt.Sprintf("Please enter a name for the '%s' stack %s", i.Stack, exampleLabel) 35 | p.GoBack = PromptSelectService 36 | i.DirName = p.PromptGetInput() 37 | i.DirName = utils.CreateStackDirectory(i.DirName, i.Stack, i.Database) 38 | status := true 39 | var err error 40 | for status { 41 | status, err = utils.IsExists(filepath.Join(utils.CurrentDirectory(), i.DirName)) 42 | errorhandler.CheckNilErr(err) 43 | if status { 44 | p.Label = "Entered name already exists. Please enter another name" 45 | i.DirName = p.PromptGetInput() 46 | i.DirName = utils.CreateStackDirectory(i.DirName, i.Stack, i.Database) 47 | } 48 | } 49 | return i.DirName 50 | } 51 | 52 | func PromptExit() { 53 | response := PromptConfirm() 54 | if response { 55 | Exit() 56 | } else { 57 | PromptHome() 58 | } 59 | } 60 | 61 | func PromptConfirm() bool { 62 | var p PromptInput 63 | p.Label = "Are you sure" 64 | p.GoBack = PromptHome 65 | return p.PromptYesOrNoSelect() 66 | } 67 | 68 | func Exit() { 69 | errorhandler.CheckNilErr(errorhandler.ErrInterrupt) 70 | } 71 | 72 | // PromptSelectExistingStacks is a prompt function will ask for selecting available stacks. 73 | func PromptSelectExistingStacks() []string { 74 | var p PromptInput 75 | p.Label = "Select available stacks" 76 | p.GoBack = PromptHome 77 | _, _, directories := utils.GetExistingStacksDatabasesAndDirectories() 78 | p.Items = directories 79 | if len(directories) > 1 { 80 | p.Items = append(p.Items, "All") 81 | } 82 | var results []string 83 | var responses []int 84 | count := 0 85 | for { 86 | if len(responses) == 0 { 87 | results, responses = p.PromptMultiSelect() 88 | } else { 89 | break 90 | } 91 | count++ 92 | if count > 2 { 93 | PromptHome() 94 | } 95 | } 96 | for _, respIdx := range responses { 97 | if respIdx == len(p.Items)-1 { 98 | return directories 99 | } 100 | } 101 | return results 102 | } 103 | 104 | // GetDetailsTemplatesOfStacks return the details template for the given service. 105 | func (i *InitInfo) GetDetailsTemplatesOfStacks() string { 106 | details := fmt.Sprintf(` 107 | -------- %s -------- 108 | {{ "Name:" | faint }} {{ .Name }} 109 | {{ "Language:" | faint }} {{ .Language }} 110 | {{ "Framework:" | faint }} {{ .Framework }} 111 | {{ "Type:" | faint }} {{ .Type }}`, i.Service) 112 | 113 | if i.Service == constants.Backend { 114 | details = fmt.Sprintf(`%s 115 | {{ "Databases:" | faint }} {{ .Databases }} 116 | `, details) 117 | } 118 | return details 119 | } 120 | 121 | func PromptAlreadyExist(existingFile string) bool { 122 | var p PromptInput 123 | p.Label = fmt.Sprintf("'%s' already exists, do you want to rewrite it", existingFile) 124 | p.GoBack = PromptHome 125 | return p.PromptYesOrNoConfirm() 126 | } 127 | 128 | func PromptAlertMessage() { 129 | err := utils.PrintWarningMessage("Click Ctrl+C to exit.") 130 | errorhandler.CheckNilErr(err) 131 | PromptHome() 132 | } 133 | -------------------------------------------------------------------------------- /prompt/dockercompose.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/wednesday-solutions/picky/internal/constants" 8 | "github.com/wednesday-solutions/picky/internal/errorhandler" 9 | "github.com/wednesday-solutions/picky/internal/utils" 10 | "github.com/wednesday-solutions/picky/pickyhelpers" 11 | ) 12 | 13 | func PromptDockerCompose() { 14 | var p PromptInput 15 | p.Label = "Choose an option" 16 | p.GoBack = PromptHome 17 | p.Items = []string{constants.CreateDockerCompose, constants.RunDockerCompose} 18 | response, _ := p.PromptSelect() 19 | if response == constants.CreateDockerCompose { 20 | err := GenerateDockerCompose() 21 | errorhandler.CheckNilErr(err) 22 | PromptRunDockerCompose() 23 | } else if response == constants.RunDockerCompose { 24 | err := RunDockerCompose() 25 | errorhandler.CheckNilErr(err) 26 | } 27 | PromptHome() 28 | } 29 | 30 | // PromptCreateDockerCompose is a prompt function for create docker-compose. 31 | func PromptCreateDockerCompose() { 32 | var p PromptInput 33 | p.Label = fmt.Sprintf("Do you want to create '%s' file for this project", constants.DockerComposeFile) 34 | p.GoBack = PromptHome 35 | response := p.PromptYesOrNoSelect() 36 | if response { 37 | err := GenerateDockerCompose() 38 | errorhandler.CheckNilErr(err) 39 | } else { 40 | PromptDockerCompose() 41 | } 42 | PromptRunDockerCompose() 43 | } 44 | 45 | // PromptRunDockerCompose is a prompt function for run docker-compose. 46 | func PromptRunDockerCompose() { 47 | var p PromptInput 48 | p.Label = fmt.Sprintf("Do you want to run '%s' ", constants.DockerComposeFile) 49 | p.GoBack = PromptDockerCompose 50 | response := p.PromptYesOrNoSelect() 51 | if response { 52 | err := RunDockerCompose() 53 | errorhandler.CheckNilErr(err) 54 | } 55 | PromptHome() 56 | } 57 | 58 | // GenerateDockerCompose generates docker-compose file for all the existing 59 | // stacks as a monorepo in the root directory. 60 | func GenerateDockerCompose() error { 61 | var p PromptInput 62 | var s pickyhelpers.StackDetails 63 | 64 | p.GoBack = PromptDockerCompose 65 | response := true 66 | status, _ := utils.IsExists(filepath.Join(utils.CurrentDirectory(), constants.DockerComposeFile)) 67 | if status { 68 | p.Label = fmt.Sprintf("'%s' already exist, do you want to update it", constants.DockerComposeFile) 69 | response = p.PromptYesOrNoConfirm() 70 | } 71 | if response { 72 | stacks, databases, _ := utils.GetExistingStacksDatabasesAndDirectories() 73 | for i, db := range databases { 74 | if db != "" { 75 | s.Database = db 76 | s.Stack = stacks[i] 77 | break 78 | } 79 | } 80 | s.Environment = constants.Environment 81 | s.StackInfo = s.GetStackInfo() 82 | err := pickyhelpers.CreateDockerComposeFile(s.StackInfo) 83 | errorhandler.CheckNilErr(err) 84 | fmt.Printf("%s", errorhandler.DoneMessage) 85 | } 86 | return nil 87 | } 88 | 89 | // RunDockerCompose runs 'docker-compose up' from the root directory. 90 | func RunDockerCompose() error { 91 | status, _ := utils.IsExists(filepath.Join(utils.CurrentDirectory(), constants.DockerComposeFile)) 92 | if status { 93 | err := utils.PrintInfoMessage("Running docker-compose..") 94 | errorhandler.CheckNilErr(err) 95 | err = utils.RunCommandWithLogs("", "docker", "compose", "up") 96 | errorhandler.CheckNilErr(err) 97 | } else { 98 | err := utils.PrintWarningMessage(fmt.Sprintf("%s file is not exist in the root directory.", constants.DockerComposeFile)) 99 | errorhandler.CheckNilErr(err) 100 | PromptCreateDockerCompose() 101 | } 102 | return nil 103 | } 104 | 105 | // ShowCreateDockerCompose returns true if a backend service exists. 106 | // func ShowCreateDockerCompose(databases []string) bool { 107 | // var backendStatus, frontendStatus bool 108 | // for _, db := range databases { 109 | // if db == "" { 110 | // frontendStatus = true 111 | // } else { 112 | // backendStatus = true 113 | // } 114 | // if backendStatus && frontendStatus { 115 | // return true 116 | // } 117 | // } 118 | // return false 119 | // } 120 | -------------------------------------------------------------------------------- /.github/workflows/product-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish package in go-pkg 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | Release-go-pkg: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | 14 | - name: Install Go 15 | uses: actions/setup-go@v3 16 | with: 17 | go-version: 1.21 18 | 19 | - name: Actions Ecosystem Action Get Merged Pull Request 20 | uses: actions-ecosystem/action-get-merged-pull-request@v1.0.1 21 | id: getMergedPR 22 | with: 23 | github_token: ${{ secrets.GITHUB_TOKEN }} 24 | 25 | - name: Fetching all tags 26 | run: | 27 | git fetch --prune --unshallow --tags 28 | 29 | - name: Get Commit Message 30 | run: | 31 | declare -A category=( [fix]="" [chore]="" [revert]="" [build]="" [docs]="" [feat]="" [perf]="" [refactor]="" [style]="" [temp]="" [test]="" [ci]="" [others]="") 32 | declare -A categoryTitle=( [fix]="
Bug Fixes
" [chore]="
Changes to build process or aux tools
" [revert]="
Revert Commits
" [build]="
Build
" [docs]="
Documentation
" [feat]="
New Features
" [perf]="
Performace Enhancements
" [refactor]="
Refactored
" [style]="
Changed Style
" [temp]="
Temporary Commit
" [test]="
Added Tests
" [ci]="
Changes to CI config
" [others]="
Others
") 33 | msg="#${{ steps.getMergedPR.outputs.number }} ${{ steps.getMergedPR.outputs.title }}" 34 | for i in $(git log --format=%h $(git merge-base HEAD^1 HEAD^2)..HEAD^2) 35 | do 36 | IFS=":" read -r type cmmsg <<< $(git log --format=%B -n 1 $i) 37 | type="${type}" | xargs 38 | text_msg="    • $i - ${cmmsg}
" 39 | flag=1 40 | for i in "${!category[@]}" 41 | do 42 | if [ "${type}" == "$i" ] 43 | then 44 | category[$i]+="${text_msg}" 45 | flag=0 46 | break 47 | fi 48 | done 49 | if [ $flag -eq 1 ] 50 | then 51 | category[others]+="${text_msg}" 52 | fi 53 | done 54 | for i in "${!category[@]}" 55 | do 56 | if [ ! -z "${category[$i]}" ] && [ "others" != "$i" ] 57 | then 58 | msg+="${categoryTitle[$i]}${category[$i]}" 59 | fi 60 | done 61 | if [ ! -z "${category[others]}" ] 62 | then 63 | msg+="${categoryTitle[others]}${category[others]}" 64 | fi 65 | echo "New Release Note: $msg" 66 | echo "COMMIT_MESSAGE=${msg}" >> $GITHUB_ENV 67 | 68 | - name: Bump Version 69 | # DEFAULT_BUMP values: major, minor, patch 70 | uses: anothrNick/github-tag-action@v1 71 | id: bump 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | DEFAULT_BUMP: patch 75 | DRY_RUN: true 76 | WITH_V: true 77 | VERBOSE: true 78 | 79 | - name: Push tag to GitHub 80 | id: new-tag 81 | run: | 82 | git config --global user.email "$GITHUB_ACTOR@users.noreply.github.com" 83 | git config --global user.name "GITHUB_ACTOR" 84 | git config --global push.followTags true 85 | NEW_TAG=${{ steps.bump.outputs.new_tag }} 86 | echo "New Tag: $NEW_TAG" 87 | git tag $NEW_TAG 88 | git push origin $NEW_TAG 89 | echo "$NEW_TAG Tag pushed successfully." 90 | echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT 91 | 92 | - name: Publish go-pkg 93 | run: | 94 | VERSION=${{ steps.new-tag.outputs.new_tag }} 95 | echo "New version: $VERSION" 96 | PKG="github.com/wednesday-solutions/picky@$VERSION" 97 | echo "New package version: $PKG" 98 | GOPROXY=proxy.golang.org go list -m $PKG 99 | 100 | - name: Create Product Release 101 | uses: actions/create-release@latest 102 | env: 103 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 104 | with: 105 | tag_name: ${{ steps.bump.outputs.new_tag }} 106 | RELEASE_NAME: ${{ steps.bump.outputs.new_tag }} 107 | body: | 108 | ${{ env.COMMIT_MESSAGE }} 109 | @alichherawalla 110 | @praveenkumar1798 111 | @ijas-ws 112 | draft: false 113 | prerelease: false 114 | -------------------------------------------------------------------------------- /hbs/hbsregister.go: -------------------------------------------------------------------------------- 1 | package hbs 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/internal/constants" 7 | "github.com/wednesday-solutions/picky/internal/utils" 8 | ) 9 | 10 | func DatabaseVolumeConnection(db string) string { 11 | if db == constants.PostgreSQL { 12 | return fmt.Sprintf("-db-volume:/var/lib/%s", constants.PostgresqlData) 13 | } else if db == constants.MySQL { 14 | return fmt.Sprintf("-db-volume:/var/lib/%s", constants.Mysql) 15 | } else { 16 | return db 17 | } 18 | } 19 | 20 | func DBVersion(db string) string { 21 | if db == constants.PostgreSQL { 22 | return "postgres:15" 23 | } else if db == constants.MySQL { 24 | return "mysql:5.7" 25 | } else { 26 | return "" 27 | } 28 | } 29 | 30 | func PortConnection(stack string) string { 31 | var portConnectionStr string 32 | backendPortNumber := utils.GetPortNumber(constants.BackendPortNumber) 33 | webPortNumber := utils.GetPortNumber(constants.WebPortNumber) 34 | postgresPortNumber := utils.GetDatabasePortNumber(constants.PostgreSQL) 35 | mysqlPortNumber := utils.GetDatabasePortNumber(constants.MySQL) 36 | redisPortNumber := constants.RedisPortNumber 37 | switch stack { 38 | case constants.PostgreSQL: 39 | portConnectionStr = fmt.Sprintf("%d:%d", postgresPortNumber, constants.PostgresPortNumber) 40 | return portConnectionStr 41 | case constants.MySQL: 42 | portConnectionStr = fmt.Sprintf("%d:%d", mysqlPortNumber, constants.MysqlPortNumber) 43 | return portConnectionStr 44 | case constants.MongoDB: 45 | return "27017:27017" 46 | case constants.Web, constants.Mobile: 47 | portConnectionStr = fmt.Sprintf("%d:%d", webPortNumber, constants.WebPortNumber) 48 | return portConnectionStr 49 | case constants.Backend: 50 | portConnectionStr = fmt.Sprintf("%d:%d", backendPortNumber, constants.BackendPortNumber) 51 | return portConnectionStr 52 | case constants.Redis: 53 | portConnectionStr = fmt.Sprintf("%d:%d", redisPortNumber, constants.RedisPortNumber) 54 | return portConnectionStr 55 | default: 56 | return portConnectionStr 57 | } 58 | } 59 | 60 | func GlobalAddDependencies(database string) string { 61 | switch database { 62 | case constants.PostgreSQL, constants.MySQL: 63 | return "sequelize-cli@6.2.0" 64 | default: 65 | return "" 66 | } 67 | } 68 | 69 | func AddDependencies(database string) string { 70 | switch database { 71 | case constants.PostgreSQL: 72 | return "shelljs bull dotenv pg sequelize@6.6.5" 73 | case constants.MySQL: 74 | return "shelljs bull dotenv mysql2 sequelize@6.6.5" 75 | default: 76 | return "" 77 | } 78 | } 79 | 80 | func RunBuildEnvironment(stack string) string { 81 | switch stack { 82 | case constants.NodeExpressGraphqlTemplate: 83 | return "build:$BUILD_NAME" 84 | case constants.NodeHapiTemplate: 85 | return "build:$BUILD_NAME" 86 | default: 87 | return "" 88 | } 89 | } 90 | 91 | func WaitForDBService(database string) string { 92 | var portNumber string 93 | if database == constants.PostgreSQL { 94 | portNumber = "5432" 95 | } else if database == constants.MySQL { 96 | portNumber = "3306" 97 | } 98 | return fmt.Sprintf(` wait-for-db: 99 | image: atkrad/wait4x 100 | depends_on: 101 | - db 102 | command: tcp db:%s -t 30s -i 250ms`, portNumber) 103 | } 104 | 105 | func DependsOnFieldOfGo(stack string) string { 106 | output := ` depends_on: 107 | wait-for-db: 108 | condition: service_completed_successfully 109 | ` 110 | if stack == constants.GolangPostgreSQLTemplate || stack == constants.GolangMySQLTemplate { 111 | return output 112 | } else { 113 | return "" 114 | } 115 | } 116 | 117 | func CmdDockerfile(stack string) string { 118 | switch stack { 119 | case constants.ReactJS: 120 | return `["yarn", "start"]` 121 | case constants.NextJS: 122 | return `["yarn", "start:dev"]` 123 | default: 124 | return "" 125 | } 126 | } 127 | 128 | func EnvEnvironmentName() string { 129 | return "`.env.${process.env.ENVIRONMENT_NAME}`" 130 | } 131 | 132 | func DeployStacks(stackFiles []string) string { 133 | var deployStackSource string 134 | for _, stackFile := range stackFiles { 135 | // will append all the selected stack files to deploy in sst.config.js 136 | deployStackSource = fmt.Sprintf("%s.stack(%s)", deployStackSource, stackFile) 137 | } 138 | deployStackSource = fmt.Sprintf("app%s;", deployStackSource) 139 | return deployStackSource 140 | } 141 | 142 | func SstImportStacks(stackFiles []string) string { 143 | var importStackSource string 144 | // import all existing stacks in sst.config.js 145 | for _, stackFile := range stackFiles { 146 | importStackSource = fmt.Sprintf("%simport { %s } from %s./stacks/%s%s;\n", 147 | importStackSource, stackFile, `"`, stackFile, `"`) 148 | } 149 | return importStackSource 150 | } 151 | -------------------------------------------------------------------------------- /pickyhelpers/create_dockercompose.go: -------------------------------------------------------------------------------- 1 | package pickyhelpers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/iancoleman/strcase" 7 | "github.com/wednesday-solutions/picky/hbs" 8 | "github.com/wednesday-solutions/picky/internal/constants" 9 | "github.com/wednesday-solutions/picky/internal/errorhandler" 10 | "github.com/wednesday-solutions/picky/internal/utils" 11 | ) 12 | 13 | func CreateDockerComposeFile(stackInfo map[string]interface{}) error { 14 | 15 | filePath := fmt.Sprintf("%s/%s", utils.CurrentDirectory(), 16 | constants.DockerComposeFile, 17 | ) 18 | var ( 19 | backendMysqlDirectories []string 20 | backendPgDirectories []string 21 | backendMysqlSnakeCased []string 22 | backendPgSnakeCased []string 23 | ) 24 | var snakeCaseDirName string 25 | _, databases, directories := utils.GetExistingStacksDatabasesAndDirectories() 26 | for index, directory := range directories { 27 | service := utils.FindService(directory) 28 | if service == constants.Backend { 29 | if databases[index] == constants.MySQL { 30 | backendMysqlDirectories = append(backendMysqlDirectories, directory) 31 | snakeCaseDirName = strcase.ToSnake(directory) 32 | backendMysqlSnakeCased = append(backendMysqlSnakeCased, snakeCaseDirName) 33 | } else if databases[index] == constants.PostgreSQL { 34 | backendPgDirectories = append(backendPgDirectories, directory) 35 | snakeCaseDirName = strcase.ToSnake(directory) 36 | backendPgSnakeCased = append(backendPgSnakeCased, snakeCaseDirName) 37 | } 38 | } 39 | } 40 | 41 | var backendPortNumber string 42 | var dbPortNumber string 43 | // Don't make any changes in the below source string. 44 | source := `version: '3' 45 | services:` 46 | 47 | for i, d := range backendPgDirectories { 48 | backendPortNumber = utils.FetchExistingPortNumber(d, constants.BackendPort) 49 | dbPortNumber = utils.FetchExistingPortNumber(d, constants.PostgresPort) 50 | source = fmt.Sprintf(`%s 51 | # Setup {{PostgreSQL}} 52 | %s_db: 53 | image: '{{dbVersion PostgreSQL}}' 54 | ports: 55 | - %s:%d 56 | restart: always # This will make sure that the container comes up post unexpected shutdowns 57 | env_file: 58 | - ./%s/.env.docker 59 | volumes: 60 | - %s{{databaseVolumeConnection PostgreSQL}} 61 | {{#equal stack GolangPostgreSQL}} 62 | environment: 63 | POSTGRES_USER: ${PSQL_USER} 64 | POSTGRES_PASSWORD: ${PSQL_PASS} 65 | POSTGRES_DB: ${PSQL_DBNAME} 66 | POSTGRES_PORT: ${PSQL_PORT} 67 | {{/equal}} 68 | 69 | {{#equal stack GolangPostgreSQL}} 70 | {{{waitForDBService PostgreSQL}}} 71 | 72 | {{/equal}} 73 | # Setup %s api 74 | %s: 75 | build: 76 | context: './%s' 77 | args: 78 | ENVIRONMENT_NAME: docker 79 | ports: 80 | - %s:%d 81 | env_file: 82 | - ./%s/.env.docker 83 | environment: 84 | ENVIRONMENT_NAME: docker 85 | {{dependsOnFieldOfGo stack}} 86 | `, 87 | source, backendPgSnakeCased[i], dbPortNumber, 88 | constants.PostgresPortNumber, d, d, d, d, d, 89 | backendPortNumber, constants.BackendPortNumber, d, 90 | ) 91 | } 92 | 93 | for i, d := range backendMysqlDirectories { 94 | backendPortNumber = utils.FetchExistingPortNumber(d, constants.BackendPort) 95 | dbPortNumber = utils.FetchExistingPortNumber(d, constants.MysqlPort) 96 | source = fmt.Sprintf(`%s 97 | # Setup {{MySQL}} 98 | %s_db: 99 | image: '{{dbVersion MySQL}}' 100 | ports: 101 | - %s:%d 102 | restart: always # This will make sure that the container comes up post unexpected shutdowns 103 | env_file: 104 | - ./%s/.env.docker 105 | volumes: 106 | - %s{{databaseVolumeConnection MySQL}} 107 | {{#equal stack GolangMySQL}} 108 | environment: 109 | MYSQL_DATABASE: ${MYSQL_DBNAME} 110 | MYSQL_PASSWORD: ${MYSQL_PASS} 111 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} 112 | {{/equal}} 113 | 114 | {{#equal stack GolangMySQL}} 115 | {{{waitForDBService MySQL}}} 116 | 117 | {{/equal}} 118 | # Setup %s api 119 | %s: 120 | build: 121 | context: './%s' 122 | args: 123 | ENVIRONMENT_NAME: docker 124 | ports: 125 | - %s:%d 126 | env_file: 127 | - ./%s/.env.docker 128 | environment: 129 | ENVIRONMENT_NAME: docker 130 | {{dependsOnFieldOfGo stack}} 131 | `, 132 | source, backendMysqlSnakeCased[i], dbPortNumber, 133 | constants.MysqlPortNumber, d, d, d, d, d, 134 | backendPortNumber, constants.BackendPortNumber, d, 135 | ) 136 | } 137 | 138 | source = fmt.Sprintf(`%s 139 | {{#if backendStatus}} 140 | # Setup Redis 141 | redis: 142 | image: 'redis:6-alpine' 143 | ports: 144 | - {{portConnection redis}} 145 | # Default command that redis will execute at start 146 | command: ['redis-server'] 147 | 148 | {{/if}} 149 | {{#each webDirectories}} 150 | # Setup {{this}} web 151 | {{this}}: 152 | build: 153 | context: './{{this}}' 154 | ports: 155 | - {{portConnection web}} 156 | env_file: 157 | - ./{{this}}/.env.docker 158 | 159 | {{else}} 160 | # No web stacks present 161 | 162 | {{/each}}{{#if backendStatus}} 163 | # Setup Volumes 164 | volumes: 165 | {{#each backendPgDirectories}} 166 | {{this}}-db-volume: 167 | {{/each}} 168 | {{#each backendMysqlDirectories}} 169 | {{this}}-db-volume: 170 | {{/each}}{{/if}} 171 | `, source) 172 | 173 | err := hbs.ParseAndWriteToFile(source, filePath, stackInfo) 174 | errorhandler.CheckNilErr(err) 175 | 176 | return nil 177 | } 178 | -------------------------------------------------------------------------------- /pickyhelpers/create_infra.go: -------------------------------------------------------------------------------- 1 | package pickyhelpers 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/iancoleman/strcase" 8 | "github.com/wednesday-solutions/picky/hbs" 9 | "github.com/wednesday-solutions/picky/internal/constants" 10 | "github.com/wednesday-solutions/picky/internal/errorhandler" 11 | "github.com/wednesday-solutions/picky/internal/utils" 12 | "github.com/wednesday-solutions/picky/pickyhelpers/sources" 13 | ) 14 | 15 | // IsInfraFilesExist checks the infra related files are exist of not. 16 | func IsInfraFilesExist() bool { 17 | path := utils.CurrentDirectory() 18 | files := []string{ 19 | constants.PackageDotJsonFile, 20 | constants.EnvFile, 21 | constants.SstConfigFile, 22 | constants.Stacks, 23 | constants.ParseSstOutputs, 24 | } 25 | for _, file := range files { 26 | status, _ := utils.IsExists(filepath.Join(path, file)) 27 | if !status { 28 | return false 29 | } 30 | } 31 | return true 32 | } 33 | 34 | // CreateInfraSetup creates package.json and .env files for infra setup. 35 | func CreateInfraSetup() error { 36 | 37 | infraFiles := make(map[string]string) 38 | // package.json file 39 | infraFiles[constants.PackageDotJsonFile] = sources.PackageDotJsonSource() 40 | // .env file 41 | infraFiles[constants.EnvFile] = sources.EnvFileSource() 42 | 43 | var err error 44 | var path string 45 | for file, source := range infraFiles { 46 | path = fmt.Sprintf("%s/%s", utils.CurrentDirectory(), file) 47 | err = utils.WriteToFile(path, source) 48 | errorhandler.CheckNilErr(err) 49 | } 50 | return nil 51 | } 52 | 53 | type Infra struct { 54 | Service string 55 | Stack string 56 | Database string 57 | DirName string 58 | CamelCaseDirName string 59 | Environment string 60 | ForceCreate bool 61 | } 62 | 63 | // CreateInfraStack creates the infra stack file of existing stack in the stacks directory. 64 | func (i Infra) CreateInfraStack() error { 65 | var err error 66 | var stackFileName string 67 | path := fmt.Sprintf("%s/%s", utils.CurrentDirectory(), constants.Stacks) 68 | folderExist, _ := utils.IsExists(path) 69 | if !folderExist { 70 | err = utils.MakeDirectory(utils.CurrentDirectory(), constants.Stacks) 71 | errorhandler.CheckNilErr(err) 72 | } 73 | stackFileName = fmt.Sprintf("%s%s", i.CamelCaseDirName, ".js") 74 | path = fmt.Sprintf("%s/%s/%s", utils.CurrentDirectory(), constants.Stacks, stackFileName) 75 | response := true 76 | if !i.ForceCreate { 77 | stackFileExist, _ := utils.IsExists(path) 78 | if stackFileExist { 79 | return errorhandler.ErrExist 80 | } 81 | } 82 | var source string 83 | if response { 84 | switch i.Service { 85 | case constants.Web: 86 | source = sources.WebStackSource(i.DirName, i.CamelCaseDirName, i.Environment) 87 | case constants.Backend: 88 | source = sources.BackendStackSource(i.Database, i.DirName, i.Environment) 89 | default: 90 | err := utils.PrintErrorMessage("Selected stack is invalid") 91 | return err 92 | } 93 | err = utils.WriteToFile(path, source) 94 | } 95 | return err 96 | } 97 | 98 | // CreateSstConfigFile creates sst.config.js file. 99 | func CreateSstConfigFile(stackInfo map[string]interface{}, directories []string, 100 | ) error { 101 | sstConfigSource := sources.SstConfigSource() 102 | path := fmt.Sprintf("%s/%s", utils.CurrentDirectory(), constants.SstConfigFile) 103 | 104 | // SST config file for all selected existing stacks. 105 | camelCaseDirectories := utils.ConvertToCamelCase(directories) 106 | stackInfo[constants.ExistingDirectories] = camelCaseDirectories 107 | 108 | err := hbs.ParseAndWriteToFile(sstConfigSource, path, stackInfo) 109 | errorhandler.CheckNilErr(err) 110 | 111 | // create parseSstOutputs.js file 112 | parseSstOutputsSource := sources.ParseSstOutputsSource() 113 | path = fmt.Sprintf("%s/%s", utils.CurrentDirectory(), constants.ParseSstOutputs) 114 | 115 | err = utils.WriteToFile(path, parseSstOutputsSource) 116 | errorhandler.CheckNilErr(err) 117 | return err 118 | } 119 | 120 | // UpdateEnvByEnvironment updates the env file with respect to environment. 121 | func UpdateEnvByEnvironment(dirName, environment string) error { 122 | path := fmt.Sprintf("%s/%s/%s", utils.CurrentDirectory(), 123 | dirName, 124 | fmt.Sprintf("%s.%s", constants.EnvFile, environment), 125 | ) 126 | _, database := utils.FindStackAndDatabase(dirName) 127 | 128 | taskDefinition := utils.GetOutputsBackendObject(environment, dirName) 129 | 130 | envSource := sources.EnvSource(dirName, environment, database, taskDefinition.BackendObj) 131 | err := utils.WriteToFile(path, envSource) 132 | errorhandler.CheckNilErr(err) 133 | 134 | return nil 135 | } 136 | 137 | // IsInfraStacksExist will return non existing stacks in the stacks directory. 138 | // It will check the stack function is exists in the stacks directory. 139 | func GetNonExistingInfraStacks(stacksDirectories []string) []string { 140 | var status bool 141 | var nonExistingStacks []string 142 | for _, stack := range stacksDirectories { 143 | status = IsInfraStackExist(stack) 144 | if !status { 145 | nonExistingStacks = append(nonExistingStacks, stack) 146 | } 147 | } 148 | return nonExistingStacks 149 | } 150 | 151 | func IsInfraStackExist(stackDirName string) bool { 152 | camelCaseStack := fmt.Sprintf("%s%s", strcase.ToCamel(stackDirName), ".js") 153 | path := fmt.Sprintf("%s/%s/%s", utils.CurrentDirectory(), constants.Stacks, camelCaseStack) 154 | status, _ := utils.IsExists(path) 155 | return status 156 | } 157 | -------------------------------------------------------------------------------- /internal/utils/find.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/internal/constants" 7 | "github.com/wednesday-solutions/picky/internal/errorhandler" 8 | ) 9 | 10 | // FindStackAndDatabase return stack and database of given directory name. 11 | func FindStackAndDatabase(dirName string) (string, string) { 12 | var stack, database string 13 | _, langSuffix, stackSuffix, lastSuffix := SplitStackDirectoryName(dirName) 14 | 15 | switch lastSuffix { 16 | case constants.Pg: 17 | database = constants.PostgreSQL 18 | if stackSuffix == "hapi" { 19 | stack = constants.NodeHapiTemplate 20 | } else if stackSuffix == constants.Graphql { 21 | if langSuffix == "node" { 22 | stack = constants.NodeExpressGraphqlTemplate 23 | } else if langSuffix == constants.Golang { 24 | stack = constants.GolangEchoTemplate 25 | } 26 | } 27 | case constants.Mysql: 28 | database = constants.MySQL 29 | if stackSuffix == "hapi" { 30 | stack = constants.NodeHapiTemplate 31 | } else if stackSuffix == constants.Graphql { 32 | if langSuffix == "node" { 33 | stack = constants.NodeExpressGraphqlTemplate 34 | } else if langSuffix == constants.Golang { 35 | stack = constants.GolangEchoTemplate 36 | } 37 | } 38 | case constants.Web: 39 | if stackSuffix == "js" { 40 | if langSuffix == "react" { 41 | stack = constants.ReactJS 42 | } else if langSuffix == "next" { 43 | stack = constants.NextJS 44 | } 45 | } else if stackSuffix == constants.Graphql { 46 | if langSuffix == "react" { 47 | stack = constants.ReactGraphqlTS 48 | } 49 | } 50 | case constants.Mobile: 51 | if stackSuffix == "reactnative" { 52 | stack = constants.ReactNative 53 | } else if stackSuffix == "android" { 54 | stack = constants.Android 55 | } else if stackSuffix == "ios" { 56 | stack = constants.IOS 57 | } else if stackSuffix == "flutter" { 58 | stack = constants.Flutter 59 | } 60 | } 61 | return stack, database 62 | } 63 | 64 | // GetExistingStacksDatabasesAndDirectories return existing stacks details. 65 | // Stack details contain stack name, database, and the directory name. 66 | func GetExistingStacksDatabasesAndDirectories() ([]string, []string, []string) { 67 | var stacks, databases, dirNames []string 68 | var stack, database string 69 | directories, err := ReadAllContents(CurrentDirectory()) 70 | errorhandler.CheckNilErr(err) 71 | 72 | for _, dirName := range directories { 73 | stack, database = FindStackAndDatabase(dirName) 74 | if stack != "" { 75 | stacks = append(stacks, stack) 76 | databases = append(databases, database) 77 | dirNames = append(dirNames, dirName) 78 | } 79 | } 80 | return stacks, databases, dirNames 81 | } 82 | 83 | // FindUserInputStackName return user-input of the given stack directory name. 84 | func FindUserInputStackName(dirName string) string { 85 | userInput, _, _, _ := SplitStackDirectoryName(dirName) 86 | return userInput 87 | } 88 | 89 | // ExistingStackAndDatabase return stack and database of the given stack directory name. 90 | func ExistingStackAndDatabase(dirName string) (string, string) { 91 | stack, database := FindStackAndDatabase(dirName) 92 | return stack, database 93 | } 94 | 95 | // FindService return service of the given stack directory name. 96 | func FindService(dirName string) string { 97 | _, _, _, lastSuffix := SplitStackDirectoryName(dirName) 98 | switch lastSuffix { 99 | case constants.Pg, constants.Mysql, constants.Mongo: 100 | return constants.Backend 101 | default: 102 | return lastSuffix 103 | } 104 | } 105 | 106 | // FindStackDirectoriesByConfigStacks will return an array of stack directories present 107 | // in the root directory. Eg: [api-node-hapi-mysql, fe-react-web] 108 | func FindStackDirectoriesByConfigStacks(configStacks []string) []string { 109 | var stacks []string 110 | 111 | _, _, directories := GetExistingStacksDatabasesAndDirectories() 112 | camelCaseDirectories := ConvertToCamelCase(directories) 113 | 114 | for _, configStack := range configStacks { 115 | for idx, camelCaseDirName := range camelCaseDirectories { 116 | camelCaseDirName = fmt.Sprintf("%s%s", camelCaseDirName, ".js") 117 | if configStack == camelCaseDirName { 118 | stacks = append(stacks, directories[idx]) 119 | } 120 | } 121 | } 122 | return stacks 123 | } 124 | 125 | // IsBackendWebAndMobileExist checks backend, web and mobile stacks is exist or not. 126 | func IsBackendWebAndMobileExist() (bool, bool, bool) { 127 | var service string 128 | backendExist, webExist, mobileExist := false, false, false 129 | _, _, stackDirectories := GetExistingStacksDatabasesAndDirectories() 130 | for _, stackDir := range stackDirectories { 131 | service = FindService(stackDir) 132 | if service == constants.Web { 133 | webExist = true 134 | } else if service == constants.Backend { 135 | backendExist = true 136 | } else if service == constants.Mobile { 137 | mobileExist = true 138 | } 139 | } 140 | return backendExist, webExist, mobileExist 141 | } 142 | 143 | func FindExistingGraphqlBackendAndWebStacks() ([]string, []string) { 144 | var backendGraphqlStacks, webGraphqlStacks []string 145 | var stack, service string 146 | _, _, stackDirectories := GetExistingStacksDatabasesAndDirectories() 147 | for _, stackDir := range stackDirectories { 148 | service = FindService(stackDir) 149 | if service == constants.Backend { 150 | stack, _ = FindStackAndDatabase(stackDir) 151 | if stack == constants.NodeExpressGraphqlTemplate || 152 | stack == constants.GolangEchoTemplate { 153 | backendGraphqlStacks = append(backendGraphqlStacks, stackDir) 154 | } 155 | } else if service == constants.Web { 156 | stack, _ = FindStackAndDatabase(stackDir) 157 | if stack == constants.ReactGraphqlTS { 158 | webGraphqlStacks = append(webGraphqlStacks, stackDir) 159 | } 160 | } 161 | } 162 | return backendGraphqlStacks, webGraphqlStacks 163 | } 164 | -------------------------------------------------------------------------------- /prompt/infra.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/iancoleman/strcase" 7 | "github.com/wednesday-solutions/picky/internal/constants" 8 | "github.com/wednesday-solutions/picky/internal/errorhandler" 9 | "github.com/wednesday-solutions/picky/internal/utils" 10 | "github.com/wednesday-solutions/picky/pickyhelpers" 11 | ) 12 | 13 | // PromptSetupInfra is the prompt for the setup infra option of Home prompt. 14 | func PromptSetupInfra() { 15 | var p PromptInput 16 | p.Label = "Do you want to setup infrastructure for your project" 17 | p.GoBack = PromptHome 18 | response := p.PromptYesOrNoSelect() 19 | if response { 20 | cloudProvider := PromptCloudProvider() 21 | environment := PromptEnvironment() 22 | response := true 23 | // stack will get infra stack files 24 | stacks := utils.GetExistingInfraStacks() 25 | if len(stacks) > 0 { 26 | // change infra stack files into stack directory files. 27 | stacks = utils.FindStackDirectoriesByConfigStacks(stacks) 28 | message := "Infra setup is already exist for the following stacks,\n\n" 29 | for i, stack := range stacks { 30 | message = fmt.Sprintf("%s %d. %s\n", message, i+1, stack) 31 | } 32 | fmt.Printf("%s\n", message) 33 | p.Label = "Do you want to change the existing stacks" 34 | p.GoBack = PromptSetupInfra 35 | response = p.PromptYesOrNoSelect() 36 | } 37 | if response { 38 | stacks = PromptSelectExistingStacks() 39 | err := CreateInfra(stacks, cloudProvider, environment) 40 | errorhandler.CheckNilErr(err) 41 | } 42 | if ShowPromptGitInit() { 43 | p.Label = "Do you want to initialize git" 44 | response = p.PromptYesOrNoSelect() 45 | if response { 46 | err := GitInit() 47 | errorhandler.CheckNilErr(err) 48 | } 49 | } 50 | err := PromptDeployAfterInfra(stacks, environment) 51 | errorhandler.CheckNilErr(err) 52 | } 53 | PromptHome() 54 | } 55 | 56 | // PromptCloudProvider is a prompt for selecting a cloud provider. 57 | func PromptCloudProvider() string { 58 | var p PromptInput 59 | p.Label = "Choose a cloud provider" 60 | p.Items = []string{constants.AWS} 61 | p.GoBack = PromptHome 62 | cp, _ := p.PromptSelect() 63 | return cp 64 | } 65 | 66 | // PromptEnvironment is a prompt for selecting an environment. 67 | func PromptEnvironment() string { 68 | var p PromptInput 69 | p.Label = "Choose an environment" 70 | p.Items = []string{constants.Development, constants.Production} 71 | p.GoBack = PromptHome 72 | env, _ := p.PromptSelect() 73 | return env 74 | } 75 | 76 | // CreateInfra execute all the functionalities of infra setup. 77 | func CreateInfra(directories []string, cloudProvider string, environment string) error { 78 | switch cloudProvider { 79 | case constants.AWS: 80 | status := pickyhelpers.IsInfraFilesExist() 81 | 82 | var err error 83 | if !status { 84 | err = pickyhelpers.CreateInfraSetup() 85 | errorhandler.CheckNilErr(err) 86 | } 87 | var response bool 88 | var s pickyhelpers.StackDetails 89 | for _, dirName := range directories { 90 | var infra pickyhelpers.Infra 91 | infra.Service = utils.FindService(dirName) 92 | infra.Stack, infra.Database = utils.FindStackAndDatabase(dirName) 93 | infra.DirName = dirName 94 | infra.CamelCaseDirName = strcase.ToCamel(dirName) 95 | infra.Environment = environment 96 | infra.ForceCreate = false 97 | 98 | s.Service = infra.Service 99 | s.Stack = infra.Stack 100 | s.Database = infra.Database 101 | s.Environment = environment 102 | s.StackInfo = s.GetStackInfo() 103 | 104 | err = infra.CreateInfraStack() 105 | if err != nil { 106 | if err.Error() == errorhandler.ErrExist.Error() { 107 | response = PromptAlreadyExist(dirName) 108 | if response { 109 | infra.ForceCreate = true 110 | err = infra.CreateInfraStack() 111 | errorhandler.CheckNilErr(err) 112 | } 113 | } else { 114 | errorhandler.CheckNilErr(err) 115 | } 116 | } 117 | if infra.Service == constants.Backend { 118 | err = pickyhelpers.UpdateEnvByEnvironment(dirName, environment) 119 | errorhandler.CheckNilErr(err) 120 | } 121 | } 122 | err = pickyhelpers.CreateSstConfigFile(s.StackInfo, directories) 123 | errorhandler.CheckNilErr(err) 124 | 125 | fmt.Printf("\n%s %s", "Generating", errorhandler.CompleteMessage) 126 | default: 127 | fmt.Printf("\nSelected stack is invalid.\n") 128 | } 129 | return nil 130 | } 131 | 132 | // PromptCreateInfraStacksWhenDeploy will setup the infra of stacks which are not already set up. 133 | func PromptCreateInfraStacksWhenDeploy(directories []string, environment string) error { 134 | count := 0 135 | for { 136 | var p PromptInput 137 | p.Label = "Do you want to setup infra for newly selected stacks" 138 | p.GoBack = PromptDeploy 139 | response := p.PromptYesOrNoSelect() 140 | if response { 141 | var err error 142 | status := pickyhelpers.IsInfraFilesExist() 143 | if !status { 144 | err = pickyhelpers.CreateInfraSetup() 145 | errorhandler.CheckNilErr(err) 146 | } 147 | var infra pickyhelpers.Infra 148 | for _, dirName := range directories { 149 | infra.DirName = dirName 150 | infra.CamelCaseDirName = strcase.ToCamel(dirName) 151 | infra.Service = utils.FindService(dirName) 152 | infra.Stack, infra.Database = utils.FindStackAndDatabase(dirName) 153 | infra.Environment = environment 154 | infra.ForceCreate = false 155 | err = infra.CreateInfraStack() 156 | if err != nil { 157 | if err.Error() == errorhandler.ErrExist.Error() { 158 | response := PromptAlreadyExist(infra.DirName) 159 | if response { 160 | infra.ForceCreate = true 161 | err = infra.CreateInfraStack() 162 | errorhandler.CheckNilErr(err) 163 | } 164 | } 165 | errorhandler.CheckNilErr(err) 166 | } 167 | } 168 | fmt.Println(errorhandler.DoneMessage) 169 | break 170 | } 171 | count++ 172 | if count > 1 { 173 | break 174 | } 175 | } 176 | return nil 177 | } 178 | -------------------------------------------------------------------------------- /prompt/deploy.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/internal/constants" 7 | "github.com/wednesday-solutions/picky/internal/errorhandler" 8 | "github.com/wednesday-solutions/picky/internal/utils" 9 | "github.com/wednesday-solutions/picky/pickyhelpers" 10 | ) 11 | 12 | // PromptDeploy is the prompt for the deploy option of Home prompt. 13 | func PromptDeploy() { 14 | var p PromptInput 15 | p.Label = "Do you want to deploy your project" 16 | p.GoBack = PromptHome 17 | response := p.PromptYesOrNoSelect() 18 | if response { 19 | err := DeployStacks([]string{}, "") 20 | errorhandler.CheckNilErr(err) 21 | } 22 | PromptHome() 23 | } 24 | 25 | // DeployStacks will deploy the infrastructure. 26 | func DeployStacks(stacks []string, environment string) error { 27 | if environment == "" { 28 | environment = PromptEnvironment() 29 | } 30 | afterInfra := false 31 | if len(stacks) == 0 { 32 | stacks = utils.GetExistingInfraStacks() 33 | } else { 34 | // afterInfra will become true if the DeployStacks function is called from DeployAfterInfra 35 | afterInfra = true 36 | } 37 | var selectedOption string 38 | if len(stacks) > 0 { 39 | if !afterInfra { 40 | stacks = utils.FindStackDirectoriesByConfigStacks(stacks) 41 | } 42 | // Prints the existing infra stacks. 43 | message := "Existing stacks are,\n\n" 44 | for i, stack := range stacks { 45 | message = fmt.Sprintf("%s %d. %s\n", message, i+1, stack) 46 | } 47 | fmt.Printf("%s\n", message) 48 | 49 | selectedOption, _ = PromptDeployNow() 50 | if selectedOption == constants.GoBack { 51 | PromptDeploy() 52 | } 53 | } 54 | if selectedOption == constants.ChangeStacks || len(stacks) == 0 { 55 | 56 | stacks = PromptSelectExistingStacks() 57 | nonExistingStacks := pickyhelpers.GetNonExistingInfraStacks(stacks) 58 | if len(nonExistingStacks) > 0 { 59 | message := "Didn't setup Infrastructure for the following stacks,\n\n" 60 | for i, stack := range nonExistingStacks { 61 | message = fmt.Sprintf("%s %d. %s\n", message, i+1, stack) 62 | } 63 | fmt.Printf("%s\n", message) 64 | // create infra stacks for non existing stacks. 65 | err := PromptCreateInfraStacksWhenDeploy(nonExistingStacks, environment) 66 | errorhandler.CheckNilErr(err) 67 | } 68 | PromptDeploy() 69 | } 70 | var s pickyhelpers.StackDetails 71 | s.Environment = environment 72 | s.StackInfo = s.GetStackInfo() 73 | err := pickyhelpers.CreateSstConfigFile(s.StackInfo, stacks) 74 | errorhandler.CheckNilErr(err) 75 | 76 | // Deploy infrastructure 77 | err = InstallDependenciesAndDeploy(stacks, environment) 78 | return err 79 | } 80 | 81 | func PromptDeployNow() (string, int) { 82 | var p PromptInput 83 | p.Items = []string{ 84 | constants.DeployNow, 85 | constants.ChangeStacks, 86 | constants.GoBack, 87 | } 88 | p.Label = "Pick an option" 89 | p.GoBack = PromptDeploy 90 | return p.PromptSelect() 91 | } 92 | 93 | // PromptDeployAfterInfra will come up after setting up the infrastructure. 94 | func PromptDeployAfterInfra(configStacks []string, environment string) error { 95 | var p PromptInput 96 | p.Label = "Do you want to deploy your project" 97 | p.GoBack = PromptHome 98 | response := p.PromptYesOrNoSelect() 99 | if response { 100 | err := DeployStacks(configStacks, environment) 101 | return err 102 | } 103 | return nil 104 | } 105 | 106 | // PromptBuildSST runs 'yarn build' 107 | func PromptBuildSST(pkgManager string) error { 108 | var p PromptInput 109 | p.Label = "Do you want to build" 110 | p.GoBack = PromptDeploy 111 | response := p.PromptYesOrNoSelect() 112 | if response { 113 | err := pickyhelpers.BuildSST(pkgManager) 114 | return err 115 | } else { 116 | PromptDeploy() 117 | } 118 | return nil 119 | } 120 | 121 | // InstallDependenciesAndDeploy install dependencies of each file, then deploy. 122 | func InstallDependenciesAndDeploy(configStacks []string, environment string) error { 123 | pkgManager := utils.GetPackageManagerOfUser() 124 | 125 | // install sst dependencies(root directory) 126 | err := utils.PrintInfoMessage("Installing sst dependencies") 127 | errorhandler.CheckNilErr(err) 128 | err = pickyhelpers.InstallDependencies(pkgManager) 129 | errorhandler.CheckNilErr(err) 130 | 131 | // install selected stacks dependencies(respected stack directory) 132 | for _, configStackDir := range configStacks { 133 | err := utils.PrintInfoMessage(fmt.Sprintf("Installing %s dependencies", configStackDir)) 134 | errorhandler.CheckNilErr(err) 135 | err = pickyhelpers.InstallDependencies( 136 | pkgManager, 137 | utils.CurrentDirectory(), 138 | configStackDir, 139 | ) 140 | errorhandler.CheckNilErr(err) 141 | } 142 | err = utils.PrintInfoMessage("Deploying...") 143 | errorhandler.CheckNilErr(err) 144 | err = pickyhelpers.DeploySST(pkgManager, environment) 145 | errorhandler.CheckNilErr(err) 146 | 147 | err = pickyhelpers.ParseDeployOutputs() 148 | errorhandler.CheckNilErr(err) 149 | 150 | err = utils.CreateInfraOutputsJson(environment) 151 | return err 152 | } 153 | 154 | func ShowRemoveDeploy() bool { 155 | path := fmt.Sprintf("%s/%s", utils.CurrentDirectory(), constants.DotSstDirectory) 156 | status, _ := utils.IsExists(path) 157 | return status 158 | } 159 | 160 | // PromptRemoveDeploy is the prompt for the remove deploy option of Home prompt. 161 | func PromptRemoveDeploy() { 162 | var p PromptInput 163 | p.Label = "Do you want to remove the deployed infrastructure" 164 | p.GoBack = PromptHome 165 | response := p.PromptYesOrNoSelect() 166 | if response { 167 | environment := PromptEnvironment() 168 | err := RemoveDeploy(environment) 169 | errorhandler.CheckNilErr(err) 170 | } 171 | PromptHome() 172 | } 173 | 174 | func RemoveDeploy(environment string) error { 175 | pkgManager := utils.GetPackageManagerOfUser() 176 | environment = utils.GetShortEnvName(environment) 177 | 178 | err := utils.PrintInfoMessage("Removing deployed infrastructure..") 179 | errorhandler.CheckNilErr(err) 180 | arg := fmt.Sprintf("%s:%s", "remove", environment) 181 | err = utils.RunCommandWithLogs("", pkgManager, "run", arg) 182 | return err 183 | } 184 | -------------------------------------------------------------------------------- /prompt/prompt.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/spaceweasel/promptui" 8 | "github.com/wednesday-solutions/picky/internal/constants" 9 | "github.com/wednesday-solutions/picky/internal/errorhandler" 10 | "github.com/wednesday-solutions/picky/internal/utils" 11 | ) 12 | 13 | type PromptInput struct { 14 | Label string 15 | Items []string 16 | GoBack func() 17 | } 18 | 19 | func (p PromptInput) PromptSelect() (string, int) { 20 | templates := &promptui.SelectTemplates{ 21 | Active: fmt.Sprintf("%s {{ . | magenta | underline }}", constants.IconChoose), 22 | Selected: fmt.Sprintf("%s {{ . | cyan }}", constants.IconSelect), 23 | } 24 | prompt := promptui.Select{ 25 | Label: p.Label, 26 | Items: p.Items, 27 | Templates: templates, 28 | IsVimMode: false, 29 | Pointer: promptui.DefaultCursor, 30 | Size: constants.SizeOfPromptSelect, 31 | } 32 | index, result, err := prompt.Run() 33 | if err != nil { 34 | if err.Error() == errorhandler.ErrInterrupt.Error() { 35 | err = errorhandler.ExitMessage 36 | } else if err == promptui.ErrEOF { 37 | p.GoBack() 38 | fmt.Printf("\nSomething error happened in GoBack.\n") 39 | } 40 | errorhandler.CheckNilErr(err) 41 | } 42 | return result, index 43 | } 44 | 45 | type Label struct { 46 | InvalidPrefix string 47 | ValidPrefix string 48 | Question string 49 | Suffix string 50 | } 51 | 52 | func (p PromptInput) PromptYesOrNoConfirm() bool { 53 | var l Label 54 | l.Question = p.Label 55 | l.InvalidPrefix = constants.IconQuestion 56 | l.ValidPrefix = constants.IconSelect 57 | l.Suffix = "[y/N]" 58 | 59 | validateFn := func(input string) error { 60 | if len(input) < 1 { 61 | return fmt.Errorf("Please enter 'y' or 'n'") 62 | } 63 | return nil 64 | } 65 | templates := &promptui.PromptTemplates{ 66 | Valid: "{{ .ValidPrefix | bold }} {{ .Question | bold }} {{ .Suffix | faint }} ", 67 | Invalid: "{{ .InvalidPrefix | bold }} {{ .Question | bold }} {{ .Suffix | faint }} ", 68 | Success: "{{ .Question | faint }} {{ .Suffix | faint }} ", 69 | } 70 | // Refer official doc of promptui for prompt label templates. 71 | prompt := promptui.Prompt{ 72 | Label: l, 73 | Templates: templates, 74 | Validate: validateFn, 75 | } 76 | for { 77 | result, err := prompt.Run() 78 | if err != nil { 79 | if err.Error() == errorhandler.ErrInterrupt.Error() { 80 | err = errorhandler.ExitMessage 81 | } else if err == promptui.ErrEOF { 82 | p.GoBack() 83 | fmt.Printf("\nSomething error happened in GoBack.\n") 84 | } 85 | errorhandler.CheckNilErr(err) 86 | } 87 | result = strings.ToLower(result) 88 | if result == "y" || result == "yes" { 89 | return true 90 | } else if result == "n" || result == "no" { 91 | return false 92 | } else { 93 | err := utils.PrintWarningMessage("Please enter 'y' or 'n'.") 94 | errorhandler.CheckNilErr(err) 95 | } 96 | } 97 | } 98 | 99 | func (p PromptInput) PromptGetInput() string { 100 | 101 | validate := func(input string) error { 102 | if len(input) <= 1 { 103 | return fmt.Errorf("Length should be greater than 1%s\n", errorhandler.Exclamation) 104 | } 105 | return nil 106 | } 107 | templates := &promptui.PromptTemplates{ 108 | Prompt: "{{ . }}", 109 | Valid: fmt.Sprintf("%s {{ . | green }}", constants.IconSelect), 110 | Invalid: fmt.Sprintf("%s {{ . | red }}", constants.IconWrong), 111 | } 112 | prompt := promptui.Prompt{ 113 | Label: p.Label, 114 | Validate: validate, 115 | Templates: templates, 116 | IsVimMode: true, 117 | } 118 | result, err := prompt.Run() 119 | if err != nil { 120 | if err.Error() == errorhandler.ErrInterrupt.Error() { 121 | err = errorhandler.ExitMessage 122 | } else if err == promptui.ErrEOF { 123 | p.GoBack() 124 | fmt.Printf("\nSomething error happened in GoBack.\n") 125 | } 126 | errorhandler.CheckNilErr(err) 127 | } 128 | return result 129 | } 130 | 131 | func (p PromptInput) PromptYesOrNoSelect() bool { 132 | p.Items = []string{constants.Yes, constants.No} 133 | 134 | response, _ := p.PromptSelect() 135 | if response == constants.Yes { 136 | return true 137 | } else { 138 | return false 139 | } 140 | } 141 | 142 | func (p PromptInput) PromptMultiSelect() ([]string, []int) { 143 | templates := &promptui.MultiSelectTemplates{ 144 | Selected: fmt.Sprintf("%s {{ . | green }}", constants.IconSelect), 145 | } 146 | prompt := promptui.MultiSelect{ 147 | Label: p.Label, 148 | Items: p.Items, 149 | Templates: templates, 150 | Size: constants.SizeOfPromptSelect, 151 | } 152 | results, err := prompt.Run() 153 | if err != nil { 154 | if err.Error() == errorhandler.ErrInterrupt.Error() { 155 | err = errorhandler.ExitMessage 156 | } else if err == promptui.ErrEOF { 157 | p.GoBack() 158 | fmt.Printf("\nSomething error happened in GoBack.\n") 159 | } 160 | errorhandler.CheckNilErr(err) 161 | return nil, nil 162 | } 163 | selected := []string{} 164 | for _, result := range results { 165 | selected = append(selected, p.Items[result]) 166 | } 167 | err = utils.PrintMultiSelectMessage(selected) 168 | errorhandler.CheckNilErr(err) 169 | return selected, results 170 | } 171 | 172 | // PromptStack is prompt for selecting stack. It will come up after user selecting the service. 173 | func (i *InitInfo) PromptStack() string { 174 | stacksWithDetails := utils.GetStackDetails(i.Service) 175 | templates := &promptui.SelectTemplates{ 176 | Active: fmt.Sprintf("%s {{ .Name | magenta | underline }}", constants.IconChoose), 177 | Inactive: "{{ .Name }}", 178 | Selected: fmt.Sprintf("%s {{ .Name | cyan }}", constants.IconSelect), 179 | Details: i.GetDetailsTemplatesOfStacks(), 180 | } 181 | 182 | prompt := promptui.Select{ 183 | Label: "Pick a stack", 184 | Items: stacksWithDetails, 185 | Templates: templates, 186 | Size: constants.SizeOfPromptSelect, 187 | } 188 | idx, _, err := prompt.Run() 189 | if err != nil { 190 | if err.Error() == errorhandler.ErrInterrupt.Error() { 191 | err = errorhandler.ExitMessage 192 | } else if err == promptui.ErrEOF { 193 | PromptSelectService() 194 | fmt.Printf("\nSomething error happened in GoBack.\n") 195 | } 196 | errorhandler.CheckNilErr(err) 197 | } 198 | return stacksWithDetails[idx].Name 199 | } 200 | -------------------------------------------------------------------------------- /internal/utils/flag.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/wednesday-solutions/picky/internal/constants" 8 | ) 9 | 10 | func UseService() string { 11 | usageString := fmt.Sprintf(`choose a service 12 | %d. %s 13 | %d. %s 14 | %d. %s 15 | `, 16 | 1, constants.Web, 17 | 2, constants.Mobile, 18 | 3, constants.Backend) 19 | return usageString 20 | } 21 | 22 | func AllWebStacksString() string { 23 | usage := fmt.Sprintf(` 24 | Web stacks: 25 | %d. %s -> %s 26 | %d. %s -> %s 27 | %d. %s -> %s 28 | `, 29 | 1, constants.ReactJS, constants.ReactjsLower, 30 | 2, constants.NextJS, constants.NextjsLower, 31 | 3, constants.ReactGraphqlTS, constants.ReactGraphqlLower, 32 | ) 33 | return usage 34 | } 35 | 36 | func AllMobileStacksString() string { 37 | usage := fmt.Sprintf(` 38 | Mobile stacks: 39 | %d. %s -> %s 40 | %d. %s -> %s 41 | %d. %s -> %s 42 | %d. %s -> %s 43 | `, 44 | 1, constants.ReactNative, constants.ReactNativeLower, 45 | 2, constants.Android, constants.AndroidLower, 46 | 3, constants.IOS, constants.IOSLower, 47 | 4, constants.Flutter, constants.FlutterLower, 48 | ) 49 | return usage 50 | } 51 | func AllBackendStacksString() string { 52 | usage := fmt.Sprintf(` 53 | Backend stacks: 54 | %d. %s -> %s 55 | %d. %s -> %s 56 | %d. %s -> %s 57 | %d. %s -> %s 58 | `, 59 | 1, constants.NodeHapiTemplate, constants.NodeHapi, 60 | 2, constants.NodeExpressGraphqlTemplate, constants.NodeGraphql, 61 | 3, constants.NodeExpressTemplate, constants.NodeExpress, 62 | 4, constants.GolangEchoTemplate, constants.Golang, 63 | ) 64 | return usage 65 | } 66 | 67 | func AllStacksString() string { 68 | usage := fmt.Sprintf(`%s%s%s`, 69 | AllWebStacksString(), 70 | AllMobileStacksString(), 71 | AllBackendStacksString(), 72 | ) 73 | return usage 74 | } 75 | 76 | func UseStack() string { 77 | usageString := fmt.Sprintf("choose a stack (select the second name)\n%s", AllStacksString()) 78 | return usageString 79 | } 80 | 81 | func UseDatabase() string { 82 | usageString := fmt.Sprintf(`Choose a database 83 | %d. %s 84 | %d. %s 85 | %d. %s 86 | `, 87 | 1, constants.Postgresql, 88 | 2, constants.Mysql, 89 | 3, constants.Mongodb, 90 | ) 91 | return usageString 92 | } 93 | 94 | func UseDirectory() string { 95 | return `provide a directory prefix name (suffix will be added) 96 | Eg: example-react-js-web | example-node-hapi-pg` 97 | } 98 | 99 | func GetStackByFlags(stack string) string { 100 | switch stack { 101 | case constants.ReactjsLower, constants.ReactJS: 102 | return constants.ReactJS 103 | case constants.NextjsLower, constants.NextJS: 104 | return constants.NextJS 105 | case constants.ReactGraphqlLower, constants.ReactGraphqlTS: 106 | return constants.ReactGraphqlTS 107 | 108 | case constants.ReactNativeLower, constants.ReactNative: 109 | return constants.ReactNative 110 | case constants.AndroidLower, constants.Android: 111 | return constants.Android 112 | case constants.IOSLower, constants.IOS: 113 | return constants.IOS 114 | case constants.Flutter, constants.FlutterLower: 115 | return constants.FlutterLower 116 | 117 | case constants.NodeHapi, constants.NodeHapiTemplate: 118 | return constants.NodeHapiTemplate 119 | case constants.NodeGraphql, constants.NodeExpressGraphqlTemplate: 120 | return constants.NodeExpressGraphqlTemplate 121 | case constants.NodeExpress, constants.NodeExpressTemplate: 122 | return constants.NodeExpressTemplate 123 | case constants.Golang, constants.GolangEchoTemplate: 124 | return constants.GolangEchoTemplate 125 | 126 | default: 127 | return "" 128 | } 129 | } 130 | 131 | func GetDatabase(db string) string { 132 | db = strings.ToLower(db) 133 | if db == constants.Postgresql || db == constants.Postgres { 134 | return constants.PostgreSQL 135 | } else if db == constants.Mysql { 136 | return constants.MySQL 137 | } else if db == constants.Mongodb { 138 | return constants.MongoDB 139 | } else { 140 | return db 141 | } 142 | } 143 | 144 | func UseInfraStacks() string { 145 | _, _, directories := GetExistingStacksDatabasesAndDirectories() 146 | var usageString string 147 | if len(directories) == 0 { 148 | usageString = "Stacks not exist. Existing stacks see here.\n" 149 | return usageString 150 | } 151 | usageString = "existing stacks are\n" 152 | for idx, dir := range directories { 153 | usageString = fmt.Sprintf("%s %d. %s\n", usageString, idx+1, dir) 154 | } 155 | return usageString 156 | } 157 | 158 | func GetExistingStacks() []string { 159 | _, _, directories := GetExistingStacksDatabasesAndDirectories() 160 | return directories 161 | } 162 | 163 | func UseCloudProvider() string { 164 | usageString := fmt.Sprintf(`choose a cloud provider 165 | %d. %s 166 | `, 1, constants.AWS) 167 | return usageString 168 | } 169 | 170 | func UseEnvironment() string { 171 | usageString := fmt.Sprintf(`choose an environment 172 | %d. %s 173 | %d. %s 174 | %d. %s 175 | `, 1, constants.Development, 176 | 2, constants.QA, 177 | 3, constants.Production) 178 | return usageString 179 | } 180 | 181 | func GetCloudProvider(cp string) string { 182 | cp = strings.ToLower(cp) 183 | if cp == "aws" { 184 | return constants.AWS 185 | } 186 | return cp 187 | } 188 | 189 | func GetEnvironmentValue(env string) string { 190 | env = strings.ToLower(env) 191 | if env == constants.Development || env == constants.Develop || env == constants.Dev { 192 | return constants.Development 193 | } 194 | return env 195 | } 196 | 197 | func UseDockerCompose() string { 198 | return "create Docker Compose file for the stacks which are present in the root directory." 199 | } 200 | 201 | func UseCI() string { 202 | return "create CI file for the stacks which are present in the root directory." 203 | } 204 | 205 | func UseCD() string { 206 | return "create CD file for the stacks which are present in the root directory." 207 | } 208 | 209 | func UsePlatform() string { 210 | usage := fmt.Sprintf(`choose a platform 211 | %d. %s 212 | `, 213 | 1, constants.Github) 214 | return usage 215 | } 216 | 217 | func ConvertStacksIntoString(stacks []string) string { 218 | var response string 219 | for idx, stack := range stacks { 220 | response = fmt.Sprintf("%s\n %d. %s", response, idx+1, stack) 221 | } 222 | return response 223 | } 224 | -------------------------------------------------------------------------------- /pickyhelpers/update_env_files.go: -------------------------------------------------------------------------------- 1 | package pickyhelpers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/iancoleman/strcase" 7 | "github.com/wednesday-solutions/picky/internal/constants" 8 | "github.com/wednesday-solutions/picky/internal/errorhandler" 9 | "github.com/wednesday-solutions/picky/internal/utils" 10 | ) 11 | 12 | func UpdateEnvFiles(stack, dirName string) error { 13 | 14 | var envFileSources []string 15 | snakeCaseDirName := strcase.ToSnake(dirName) 16 | envFiles := []string{".env.local", ".env.development", constants.DockerEnvFile} 17 | for idx, file := range envFiles { 18 | envFiles[idx] = fmt.Sprintf("%s/%s", dirName, file) 19 | } 20 | var envLocalSource, envDevSource, envDockerSource string 21 | backendPortNumber := utils.GetPortNumber(constants.BackendPortNumber) 22 | redisPortNumber := utils.GetPortNumber(constants.RedisPortNumber) 23 | switch stack { 24 | case constants.NodeHapiTemplate: 25 | 26 | postgresPortNumber := utils.GetDatabasePortNumber(constants.PostgreSQL) 27 | envLocalSource = fmt.Sprintf(`NAME=Node Template 28 | NODE_ENV=development 29 | ENVIRONMENT_NAME=local 30 | PORT=%d 31 | DB_URI=postgres://root:password@localhost:5432/temp_dev 32 | POSTGRES_HOST=0.0.0.0 33 | POSTGRES_PORT=%d 34 | POSTGRES_DB=temp_dev 35 | POSTGRES_USER=root 36 | POSTGRES_PASSWORD=password 37 | REDIS_HOST=localhost 38 | REDIS_PORT=%d`, 39 | backendPortNumber, 40 | postgresPortNumber, 41 | redisPortNumber, 42 | ) 43 | 44 | envDevSource = fmt.Sprintf(`NAME=Node Template (DEV) 45 | NODE_ENV=development 46 | ENVIRONMENT_NAME=development 47 | PORT=%d 48 | DB_URI=postgres://root:password@db_postgres:5432/temp_dev 49 | POSTGRES_HOST=db_postgres 50 | POSTGRES_PORT=%d 51 | POSTGRES_DB=temp_dev 52 | POSTGRES_USER=root 53 | POSTGRES_PASSWORD=password 54 | REDIS_HOST=redis 55 | REDIS_PORT=%d`, 56 | backendPortNumber, 57 | postgresPortNumber, 58 | redisPortNumber, 59 | ) 60 | 61 | envDockerSource = fmt.Sprintf(`NAME=Node Template 62 | NODE_ENV=production 63 | ENVIRONMENT_NAME=docker 64 | PORT=%d 65 | DB_URI=postgres://root:password@%s_db:5432/temp_dev 66 | POSTGRES_HOST=%s_db 67 | POSTGRES_DB=temp_dev 68 | POSTGRES_USER=root 69 | POSTGRES_PASSWORD=password 70 | POSTGRES_PORT=%d 71 | REDIS_HOST=redis 72 | REDIS_PORT=%d`, 73 | backendPortNumber, 74 | snakeCaseDirName, 75 | snakeCaseDirName, 76 | postgresPortNumber, 77 | redisPortNumber, 78 | ) 79 | 80 | envFileSources = []string{envLocalSource, envDevSource, envDockerSource} 81 | 82 | case constants.NodeExpressGraphqlTemplate: 83 | 84 | mysqlPortNumber := utils.GetDatabasePortNumber(constants.MySQL) 85 | envLocalSource = fmt.Sprintf(`DB_URI=mysql://root:password@localhost:3306/reporting_dashboard_dev 86 | MYSQL_HOST=0.0.0.0 87 | MYSQL_PORT=%d 88 | MYSQL_DATABASE=reporting_dashboard_dev 89 | MYSQL_USER=root 90 | MYSQL_PASSWORD=password 91 | PORT=%d 92 | NODE_ENV=local 93 | ACCESS_TOKEN_SECRET=4cd7234152590dcfe77e1b6fc52e84f4d30c06fddadd0dd2fb42cbc51fa14b1bb195bbe9d72c9599ba0c6b556f9bd1607a8478be87e5a91b697c74032e0ae7af 94 | REDIS_HOST=localhost 95 | REDIS_PORT=%d`, 96 | mysqlPortNumber, 97 | backendPortNumber, 98 | redisPortNumber, 99 | ) 100 | 101 | envDevSource = fmt.Sprintf(`DB_URI=mysql://root:password@db_mysql:3306/reporting_dashboard_dev 102 | MYSQL_HOST=db_mysql 103 | MYSQL_PORT=%d 104 | MYSQL_DATABASE=reporting_dashboard_dev 105 | MYSQL_USER=root 106 | MYSQL_PASSWORD=password 107 | PORT=%d 108 | ACCESS_TOKEN_SECRET=4cd7234152590dcfe77e1b6fc52e84f4d30c06fddadd0dd2fb42cbc51fa14b1bb195bbe9d72c9599ba0c6b556f9bd1607a8478be87e5a91b697c74032e0ae7af`, 109 | mysqlPortNumber, 110 | backendPortNumber, 111 | ) 112 | 113 | envDockerSource = fmt.Sprintf(`DB_URI=mysql://root:password@%s_db:3306/reporting_dashboard_dev 114 | MYSQL_HOST=%s_db 115 | MYSQL_DATABASE=reporting_dashboard_dev 116 | MYSQL_USER=reporting_dashboard_role 117 | MYSQL_PASSWORD=password 118 | MYSQL_ROOT_PASSWORD=password 119 | MYSQL_PORT=%d 120 | PORT=%d 121 | NODE_ENV=local 122 | ENVIRONMENT_NAME=docker 123 | ACCESS_TOKEN_SECRET=4cd7234152590dcfe77e1b6fc52e84f4d30c06fddadd0dd2fb42cbc51fa14b1bb195bbe9d72c9599ba0c6b556f9bd1607a8478be87e5a91b697c74032e0ae7af 124 | REDIS_HOST=redis 125 | REDIS_PORT=%d 126 | APP_NAME=app`, 127 | snakeCaseDirName, 128 | snakeCaseDirName, 129 | mysqlPortNumber, 130 | backendPortNumber, 131 | redisPortNumber, 132 | ) 133 | 134 | envFileSources = []string{envLocalSource, envDevSource, envDockerSource} 135 | 136 | default: 137 | return fmt.Errorf("Selected stack is invalid") 138 | } 139 | 140 | for idx, envFile := range envFiles { 141 | envFile = fmt.Sprintf("%s/%s", utils.CurrentDirectory(), envFile) 142 | err := utils.WriteToFile(envFile, envFileSources[idx]) 143 | errorhandler.CheckNilErr(err) 144 | } 145 | 146 | return nil 147 | } 148 | 149 | func UpdateEnvDockerFileForDefaultDBInTemplate(stack, dirName string) error { 150 | snakeCaseDirName := strcase.ToSnake(dirName) 151 | var envDockerSource string 152 | backendPortNumber := utils.GetPortNumber(constants.BackendPortNumber) 153 | redisPortNumber := utils.GetPortNumber(constants.RedisPortNumber) 154 | switch stack { 155 | case constants.NodeExpressGraphqlTemplate: 156 | postgresPortNumber := utils.GetDatabasePortNumber(constants.PostgreSQL) 157 | envDockerSource = fmt.Sprintf(`DB_URI=postgres://reporting_dashboard_role:reportingdashboard123@%s_db:5432/reporting_dashboard_dev 158 | POSTGRES_HOST=%s_db 159 | POSTGRES_DB=reporting_dashboard_dev 160 | POSTGRES_USER=reporting_dashboard_role 161 | POSTGRES_PASSWORD=reportingdashboard123 162 | POSTGRES_PORT=%d 163 | ACCESS_TOKEN_SECRET=4cd7234152590dcfe77e1b6fc52e84f4d30c06fddadd0dd2fb42cbc51fa14b1bb195bbe9d72c9599ba0c6b556f9bd1607a8478be87e5a91b697c74032e0ae7af 164 | PORT=%d 165 | NODE_ENV=production 166 | ENVIRONMENT_NAME=docker 167 | REDIS_DOMAIN=redis 168 | REDIS_PORT=%d`, 169 | snakeCaseDirName, 170 | snakeCaseDirName, 171 | postgresPortNumber, 172 | backendPortNumber, 173 | redisPortNumber, 174 | ) 175 | 176 | case constants.NodeHapiTemplate: 177 | mysqlPortNumber := utils.GetDatabasePortNumber(constants.MySQL) 178 | envDockerSource = fmt.Sprintf(`NAME=Node Template (DEV) 179 | NODE_ENV=production 180 | ENVIRONMENT_NAME=docker 181 | PORT=%d 182 | DB_URI=mysql://root:password@%s_db:3306/temp_dev 183 | MYSQL_HOST=%s_db 184 | MYSQL_DATABASE=temp_dev 185 | MYSQL_USER=def_user 186 | MYSQL_PASSWORD=password 187 | MYSQL_ROOT_PASSWORD=password 188 | MYSQL_PORT=%d 189 | REDIS_HOST=redis 190 | REDIS_PORT=%d`, 191 | backendPortNumber, 192 | snakeCaseDirName, 193 | snakeCaseDirName, 194 | mysqlPortNumber, 195 | redisPortNumber, 196 | ) 197 | default: 198 | return fmt.Errorf("Selected stack is invalid.") 199 | } 200 | file := fmt.Sprintf("%s/%s/%s", utils.CurrentDirectory(), 201 | dirName, constants.DockerEnvFile, 202 | ) 203 | err := utils.WriteToFile(file, envDockerSource) 204 | errorhandler.CheckNilErr(err) 205 | 206 | return nil 207 | } 208 | -------------------------------------------------------------------------------- /internal/utils/stack.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/internal/constants" 7 | "github.com/wednesday-solutions/picky/internal/errorhandler" 8 | ) 9 | 10 | // CreateStackDirectory will create a a directory based on the user input, stack and the database selected. 11 | func CreateStackDirectory(dirName, stack, database string) string { 12 | switch stack { 13 | case constants.NodeHapiTemplate: 14 | if database == constants.PostgreSQL { 15 | dirName = fmt.Sprintf("%s-%s", dirName, constants.NodeHapiPgTemplate) 16 | } else if database == constants.MySQL { 17 | dirName = fmt.Sprintf("%s-%s", dirName, constants.NodeHapiMySqlTemplate) 18 | } 19 | case constants.NodeExpressGraphqlTemplate: 20 | if database == constants.PostgreSQL { 21 | dirName = fmt.Sprintf("%s-%s", dirName, constants.NodeGraphqlPgTemplate) 22 | } else if database == constants.MySQL { 23 | dirName = fmt.Sprintf("%s-%s", dirName, constants.NodeGraphqlMySqlTemplate) 24 | } 25 | case constants.NodeExpressTemplate: 26 | if database == constants.MongoDB { 27 | dirName = fmt.Sprintf("%s-%s", dirName, constants.NodeExpressMongoTemplate) 28 | } 29 | case constants.GolangEchoTemplate: 30 | if database == constants.PostgreSQL { 31 | dirName = fmt.Sprintf("%s-%s", dirName, constants.GolangPgTemplate) 32 | } else if database == constants.MySQL { 33 | dirName = fmt.Sprintf("%s-%s", dirName, constants.GolangMySqlTemplate) 34 | } 35 | case constants.ReactJS: 36 | dirName = fmt.Sprintf("%s-%s", dirName, constants.ReactTemplate) 37 | case constants.NextJS: 38 | dirName = fmt.Sprintf("%s-%s", dirName, constants.NextTemplate) 39 | case constants.ReactGraphqlTS: 40 | dirName = fmt.Sprintf("%s-%s", dirName, constants.ReactGraphqlTemplate) 41 | case constants.ReactNative: 42 | dirName = fmt.Sprintf("%s-%s", dirName, constants.ReactNativeTemplate) 43 | case constants.Android: 44 | dirName = fmt.Sprintf("%s-%s", dirName, constants.AndroidTemplate) 45 | case constants.IOS: 46 | dirName = fmt.Sprintf("%s-%s", dirName, constants.IOSTemplate) 47 | case constants.Flutter: 48 | dirName = fmt.Sprintf("%s-%s", dirName, constants.FlutterTemplate) 49 | } 50 | return dirName 51 | } 52 | 53 | type stackDetails struct { 54 | Name string 55 | Language string 56 | Framework string 57 | Type string 58 | Databases string 59 | } 60 | 61 | // GetStackDetails returns an array of StackDetails for showing details 62 | // when user selects stacks prompt. 63 | func GetStackDetails(service string) []stackDetails { 64 | var stacksDetails []stackDetails 65 | switch service { 66 | case constants.Backend: 67 | nodeHapi := stackDetails{ 68 | Name: constants.NodeHapiTemplate, 69 | Language: "JavaScript", 70 | Framework: "Node JS & Hapi", 71 | Type: "REST API", 72 | Databases: fmt.Sprintf("%s & %s", constants.PostgreSQL, constants.MySQL), 73 | } 74 | nodeGraphql := stackDetails{ 75 | Name: constants.NodeExpressGraphqlTemplate, 76 | Language: "JavaScript", 77 | Framework: "Node JS & Express", 78 | Type: "GraphQL API", 79 | Databases: fmt.Sprintf("%s & %s", constants.PostgreSQL, constants.MySQL), 80 | } 81 | // nodeExpress := stackDetails{ 82 | // Name: constants.NodeExpressTemplate, 83 | // Language: "JavaScript", 84 | // Framework: "Node JS & Express", 85 | // Type: "REST API", 86 | // Databases: constants.MongoDB, 87 | // } 88 | // golangGraphql := stackDetails{ 89 | // Name: constants.GolangEchoTemplate, 90 | // Language: "Golang", 91 | // Framework: "Echo", 92 | // Type: "GraphQL API", 93 | // Databases: fmt.Sprintf("%s & %s", constants.PostgreSQL, constants.MySQL), 94 | // } 95 | _, _, mobileStackExist := IsBackendWebAndMobileExist() 96 | if mobileStackExist { 97 | stacksDetails = []stackDetails{ 98 | nodeHapi, 99 | // nodeExpress, 100 | } 101 | } else { 102 | stacksDetails = []stackDetails{ 103 | nodeHapi, 104 | nodeGraphql, 105 | // nodeExpress, 106 | // golangGraphql, 107 | } 108 | } 109 | case constants.Web: 110 | reactGraphqlTs := stackDetails{ 111 | Name: constants.ReactGraphqlTS, 112 | Language: "TypeScript", 113 | Framework: "React", 114 | Type: "GraphQL API", 115 | } 116 | reactJs := stackDetails{ 117 | Name: constants.ReactJS, 118 | Language: "JavaScript", 119 | Framework: "React", 120 | Type: "REST API", 121 | } 122 | nextJs := stackDetails{ 123 | Name: constants.NextJS, 124 | Language: "JavaScript", 125 | Framework: "Next", 126 | Type: "REST API", 127 | } 128 | stacksDetails = []stackDetails{reactJs, nextJs, reactGraphqlTs} 129 | 130 | case constants.Mobile: 131 | stacksDetails = []stackDetails{ 132 | { 133 | Name: constants.ReactNative, 134 | Language: "JavaScript", 135 | Framework: "React Native", 136 | Type: "REST API", 137 | }, 138 | { 139 | Name: constants.Android, 140 | Language: "Kotlin", 141 | Framework: "-", 142 | Type: "REST API", 143 | }, 144 | { 145 | Name: constants.IOS, 146 | Language: "Swift", 147 | Framework: "-", 148 | Type: "REST API", 149 | }, 150 | { 151 | Name: constants.Flutter, 152 | Language: "Dart", 153 | Framework: "Flutter", 154 | Type: "REST API", 155 | }, 156 | } 157 | } 158 | return stacksDetails 159 | } 160 | 161 | // GetExistingInfraStacks fetch stack files inside the stacks directory. 162 | func GetExistingInfraStacks() []string { 163 | path := fmt.Sprintf("%s/%s", CurrentDirectory(), constants.Stacks) 164 | status, _ := IsExists(path) 165 | if !status { 166 | return []string{} 167 | } 168 | files, err := ReadAllContents(path) 169 | errorhandler.CheckNilErr(err) 170 | return files 171 | } 172 | 173 | // GetSuffixOfStack returns suffix name for the given stack and database. 174 | func GetSuffixOfStack(stack, database string) string { 175 | var suffix string 176 | switch stack { 177 | case constants.ReactJS: 178 | suffix = constants.ReactTemplate 179 | case constants.NextJS: 180 | suffix = constants.NextTemplate 181 | case constants.ReactGraphqlTS: 182 | suffix = constants.ReactGraphqlTemplate 183 | case constants.NodeHapiTemplate: 184 | if database == constants.PostgreSQL { 185 | suffix = constants.NodeHapiPgTemplate 186 | } else if database == constants.MySQL { 187 | suffix = constants.NodeHapiMySqlTemplate 188 | } 189 | case constants.NodeExpressGraphqlTemplate: 190 | if database == constants.PostgreSQL { 191 | suffix = constants.NodeGraphqlPgTemplate 192 | } else if database == constants.MySQL { 193 | suffix = constants.NodeGraphqlMySqlTemplate 194 | } 195 | case constants.NodeExpressTemplate: 196 | if database == constants.MongoDB { 197 | suffix = constants.NodeExpressMongoTemplate 198 | } 199 | case constants.GolangEchoTemplate: 200 | if database == constants.PostgreSQL { 201 | suffix = constants.GolangPgTemplate 202 | } else if database == constants.MySQL { 203 | suffix = constants.GolangMySqlTemplate 204 | } 205 | case constants.ReactNative: 206 | suffix = constants.ReactNativeTemplate 207 | case constants.Android: 208 | suffix = constants.AndroidTemplate 209 | case constants.IOS: 210 | suffix = constants.IOSTemplate 211 | case constants.Flutter: 212 | suffix = constants.FlutterTemplate 213 | } 214 | return suffix 215 | } 216 | 217 | func CheckStacksExist(stacks []string) error { 218 | var stackExist bool 219 | if len(stacks) == 0 { 220 | return fmt.Errorf("No stacks exist.\n") 221 | } 222 | _, _, directories := GetExistingStacksDatabasesAndDirectories() 223 | for _, stack := range stacks { 224 | for _, dir := range directories { 225 | if stack == dir { 226 | stackExist = true 227 | } 228 | } 229 | if !stackExist { 230 | return fmt.Errorf("Entered stack '%s' not exists.\n", stack) 231 | } 232 | } 233 | return nil 234 | } 235 | 236 | func IsWebStack(stack string) (string, bool) { 237 | _, _, _, lastSuffix := SplitStackDirectoryName(stack) 238 | if lastSuffix == constants.Web { 239 | return stack, true 240 | } 241 | return "", false 242 | } 243 | 244 | func IsMobileStack(stack string) (string, bool) { 245 | _, _, _, lastSuffix := SplitStackDirectoryName(stack) 246 | if lastSuffix == constants.Mobile { 247 | return stack, true 248 | } 249 | return "", false 250 | } 251 | 252 | func IsBackendStack(stack string) (string, bool) { 253 | _, _, _, lastSuffix := SplitStackDirectoryName(stack) 254 | if lastSuffix == constants.Pg || lastSuffix == constants.Mysql { 255 | return stack, true 256 | } 257 | return "", false 258 | } 259 | -------------------------------------------------------------------------------- /internal/constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | import "github.com/spaceweasel/promptui" 4 | 5 | var Repos = func() map[string]string { 6 | return map[string]string{ 7 | ReactJS: "https://github.com/wednesday-solutions/react-template", 8 | NextJS: "https://github.com/wednesday-solutions/nextjs-template", 9 | ReactGraphqlTS: "https://github.com/wednesday-solutions/react-graphql-ts-template", 10 | NodeHapiTemplate: "https://github.com/wednesday-solutions/nodejs-hapi-template", 11 | NodeExpressGraphqlTemplate: "https://github.com/wednesday-solutions/node-express-graphql-template", 12 | NodeExpressTemplate: "https://github.com/wednesday-solutions/node-mongo-express", 13 | GolangPostgreSQLTemplate: "https://github.com/wednesday-solutions/go-template", 14 | GolangMySQLTemplate: "https://github.com/wednesday-solutions/go-template-mysql", 15 | ReactNative: "https://github.com/wednesday-solutions/react-native-template", 16 | Android: "https://github.com/wednesday-solutions/android-template", 17 | IOS: "https://github.com/wednesday-solutions/ios-template", 18 | Flutter: "https://github.com/wednesday-solutions/flutter_template", 19 | } 20 | } 21 | 22 | // CLI options 23 | const ( 24 | Picky = "picky" 25 | Service = "service" 26 | Test = "test" 27 | Init = "init" 28 | Create = "create" 29 | Infra = "infra" 30 | ) 31 | 32 | // Home options 33 | const ( 34 | InitService = "Init Service" 35 | DockerCompose = "Docker Compose" 36 | CICD = "CI/CD" 37 | SetupInfra = "Setup Infra" 38 | Deploy = "Deploy" 39 | RemoveDeploy = "Remove Deploy" 40 | GitInit = "Git Init" 41 | Exit = "Exit" 42 | ) 43 | 44 | // Services 45 | const ( 46 | Web = "web" 47 | Mobile = "mobile" 48 | Backend = "backend" 49 | ) 50 | 51 | // Frontend stacks 52 | const ( 53 | ReactJS = "React JS" 54 | NextJS = "Next JS" 55 | ReactGraphqlTS = "React GraphQL TS" 56 | ) 57 | 58 | // Backend stacks 59 | const ( 60 | NodeHapiTemplate = "Node (Hapi- REST API)" 61 | NodeExpressGraphqlTemplate = "Node (Express- GraphQL API)" 62 | NodeExpressTemplate = "Node (Express- REST API)" 63 | GolangEchoTemplate = "Golang (Echo- GraphQL API)" 64 | ) 65 | 66 | // Mobile stacks 67 | const ( 68 | ReactNative = "React Native" 69 | Android = "Android" 70 | IOS = "IOS" 71 | Flutter = "Flutter" 72 | ) 73 | 74 | // Databases 75 | const ( 76 | PostgreSQL = "PostgreSQL" 77 | MySQL = "MySQL" 78 | MongoDB = "MongoDB" 79 | ) 80 | 81 | // Docker compose actions 82 | const ( 83 | CreateDockerCompose = "Create Docker Compose" 84 | RunDockerCompose = "Run Docker Compose" 85 | ) 86 | 87 | // CICD actions 88 | const ( 89 | CreateCI = "Create CI" 90 | CreateCD = "Create CD" 91 | ) 92 | 93 | // Infra Files 94 | const ( 95 | PackageDotJsonFile = "package.json" 96 | SstConfigFile = "sst.config.js" 97 | EnvFile = ".env" 98 | EnvDevFile = ".env.development" 99 | ParseSstOutputs = "parseSstOutputs.js" 100 | ) 101 | 102 | // Cloud Providers 103 | const ( 104 | AWS = "AWS" 105 | ) 106 | 107 | // helpers 108 | const ( 109 | Yes = "Yes" 110 | No = "No" 111 | Stack = "stack" 112 | Stacks = "stacks" 113 | DB = "db" 114 | Pg = "pg" 115 | Mysql = "mysql" 116 | Postgresql = "postgresql" 117 | Mongodb = "mongodb" 118 | Mongo = "mongo" 119 | Redis = "redis" 120 | Postgres = "postgres" 121 | Frontend = "frontend" 122 | Database = "database" 123 | ProjectName = "projectName" 124 | PgNative = "pg-native" 125 | Mysql2 = "mysql2" 126 | WebStatus = "webStatus" 127 | MobileStatus = "mobileStatus" 128 | BackendStatus = "backendStatus" 129 | GolangPostgreSQLTemplate = "Golang-PostgreSQL" 130 | GolangMySQLTemplate = "Golang-MySQL" 131 | WebDirName = "webDirName" 132 | MobileDirName = "mobileDirName" 133 | BackendDirName = "backendDirName" 134 | SizeOfPromptSelect = 8 135 | All = "All" 136 | SstConfigStack = "sstConfigStack" 137 | ExistingDirectories = "existingDirectories" 138 | Yarn = "yarn" 139 | Npm = "npm" 140 | WebDirectories = "webDirectories" 141 | BackendPgDirectories = "backendPgDirectories" 142 | BackendMysqlDirectories = "backendMysqlDirectories" 143 | Zero = 0 144 | One = 1 145 | Two = 2 146 | Three = 3 147 | BackendSuffixSize = 3 148 | WebSuffixSize = 3 149 | MobileSuffixSize = 2 150 | DotSstDirectory = ".sst" 151 | PostgresqlData = "postgresql/data" 152 | GithubWorkflowsDir = ".github/workflows" 153 | DotGitFolder = ".git" 154 | CDFilePathURL = "/.github/workflows/cd.yml" 155 | GitHub = "GitHub" 156 | Graphql = "graphql" 157 | Git = "git" 158 | DotGitIgnore = ".gitignore" 159 | NodeModules = "node_modules" 160 | CloudProvider = "cloudprovider" 161 | Directory = "directory" 162 | DockerComposeFlag = "dockercompose" 163 | CIFlag = "ci" 164 | CDFlag = "cd" 165 | Platform = "platform" 166 | Github = "github" 167 | DotSst = ".sst" 168 | OutputsJson = "outputs.json" 169 | DBUsername = "username" 170 | DeployNow = "Deploy Now" 171 | ChangeStacks = "Change Stacks" 172 | GoBack = "Go Back" 173 | ) 174 | 175 | // Docker related files 176 | const ( 177 | DockerComposeFile = "docker-compose.yml" 178 | DockerFile = "Dockerfile" 179 | DockerEnvFile = ".env.docker" 180 | DockerIgnoreFile = ".dockerignore" 181 | ) 182 | 183 | // Template directory name 184 | const ( 185 | NodeHapiPgTemplate = "node-hapi-pg" 186 | NodeHapiMySqlTemplate = "node-hapi-mysql" 187 | NodeGraphqlPgTemplate = "node-graphql-pg" 188 | NodeGraphqlMySqlTemplate = "node-graphql-mysql" 189 | NodeExpressMongoTemplate = "node-express-mongo" 190 | GolangPgTemplate = "golang-graphql-pg" 191 | GolangMySqlTemplate = "golang-graphql-mysql" 192 | ReactTemplate = "react-js-web" 193 | NextTemplate = "next-js-web" 194 | ReactGraphqlTemplate = "react-graphql-web" 195 | ReactNativeTemplate = "reactnative-mobile" 196 | AndroidTemplate = "android-mobile" 197 | IOSTemplate = "ios-mobile" 198 | FlutterTemplate = "flutter-mobile" 199 | ) 200 | 201 | // Environments 202 | const ( 203 | Environment = "environment" 204 | Dev = "dev" 205 | QA = "qa" 206 | Prod = "prod" 207 | Development = "development" 208 | Production = "production" 209 | Develop = "develop" 210 | ) 211 | 212 | // UI icons 213 | var ( 214 | IconChoose = promptui.Styler(promptui.FGBold)("▸") 215 | IconSelect = promptui.Styler(promptui.FGGreen)("✔") 216 | IconWarn = promptui.Styler(promptui.FGYellow)("⚠") 217 | IconWrong = promptui.Styler(promptui.FGRed)("✗") 218 | IconQuestion = promptui.Styler(promptui.FGMagenta)("?") 219 | ) 220 | 221 | // Stack short name for flags 222 | const ( 223 | ReactjsLower = "reactjs" 224 | NextjsLower = "nextjs" 225 | ReactGraphqlLower = "reactgraphql" 226 | ReactNativeLower = "reactnative" 227 | AndroidLower = "android" 228 | IOSLower = "ios" 229 | FlutterLower = "flutter" 230 | NodeHapi = "nodehapi" 231 | NodeGraphql = "nodegraphql" 232 | NodeExpress = "nodeexpress" 233 | Golang = "golang" 234 | ) 235 | 236 | // GitHub secrets 237 | const ( 238 | DistributionId = "DISTRIBUTION_ID" 239 | AwsRegion = "AWS_REGION" 240 | AwsAccessKeyId = "AWS_ACCESS_KEY_ID" 241 | AwsSecretAccessKey = "AWS_SECRET_ACCESS_KEY" 242 | AwsEcrRepository = "AWS_ECR_REPOSITORY" 243 | ) 244 | 245 | // Port numbers for infra setup 246 | const ( 247 | WebPortNumber = 3000 248 | BackendPortNumber = 9000 249 | PostgresPortNumber = 5432 250 | MysqlPortNumber = 3306 251 | RedisPortNumber = 6379 252 | ) 253 | 254 | // Env values 255 | const ( 256 | PostgresUser = "POSTGRES_USER" 257 | PostgresHost = "POSTGRES_HOST" 258 | PostgresDB = "POSTGRES_DB" 259 | MysqlUser = "MYSQL_USER" 260 | MysqlHost = "MYSQL_HOST" 261 | MysqlDatabase = "MYSQL_DATABASE" 262 | RedisHost = "REDIS_HOST" 263 | RedisPort = "REDIS_PORT" 264 | ) 265 | 266 | // Port number names 267 | const ( 268 | BackendPort = "PORT" 269 | PostgresPort = "POSTGRES_PORT" 270 | MysqlPort = "MYSQL_PORT" 271 | ) 272 | -------------------------------------------------------------------------------- /pickyhelpers/convert_queries.go: -------------------------------------------------------------------------------- 1 | package pickyhelpers 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/internal/constants" 7 | "github.com/wednesday-solutions/picky/internal/errorhandler" 8 | "github.com/wednesday-solutions/picky/internal/utils" 9 | ) 10 | 11 | func ConvertQueries(stack, dirName string) error { 12 | 13 | var queries []string 14 | var files []string 15 | 16 | switch stack { 17 | case constants.NodeHapiTemplate: 18 | 19 | oauthClients := `create sequence oauth_clients_seq; 20 | 21 | create type grant_type_enum as ENUM('CLIENT_CREDENTIALS'); 22 | 23 | create table oauth_clients ( 24 | id INT NOT NULL PRIMARY KEY DEFAULT NEXTVAL ('oauth_clients_seq'), 25 | client_id VARCHAR(320) NOT NULL, 26 | client_secret VARCHAR(36) NOT NULL, 27 | grant_type grant_type_enum, 28 | created_at TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP NOT NULL, 29 | updated_at TIMESTAMP(0) NULL, 30 | CONSTRAINT oauth_clients_client_id UNIQUE (client_id) 31 | ); 32 | 33 | CREATE INDEX oauth_clients_client_id_idx ON oauth_clients(client_id); 34 | CREATE INDEX oauth_clients_client_secret_idx ON oauth_clients(client_secret);` 35 | 36 | users := `CREATE SEQUENCE users_seq; 37 | 38 | CREATE TABLE users 39 | ( 40 | id INT NOT NULL DEFAULT NEXTVAL ('users_seq') PRIMARY KEY, 41 | oauth_client_id INT NOT NULL, 42 | first_name VARCHAR (32) NOT NULL, 43 | last_name VARCHAR(32) NOT NULL, 44 | email VARCHAR(32) NOT NULL, 45 | created_at TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP NOT NULL, 46 | CONSTRAINT users_oauth_clients_id_fk FOREIGN KEY (oauth_client_id) 47 | REFERENCES oauth_clients (id) ON UPDATE CASCADE 48 | );` 49 | 50 | oauthAccessTokens := `create sequence oauth_access_tokens_seq; 51 | 52 | CREATE OR REPLACE FUNCTION public.is_json_valid(json_data json) 53 | RETURNS boolean 54 | LANGUAGE plpgsql 55 | AS $function$ 56 | DECLARE 57 | json_type TEXT; 58 | BEGIN 59 | json_type := json_typeof(json_data); 60 | IF json_type = 'array' AND json_array_length(json_data) > 0 THEN 61 | RETURN TRUE; 62 | ELSIF json_type = 'object' AND json_data::text <> '{}'::TEXT THEN 63 | RETURN TRUE; 64 | END IF; 65 | RETURN FALSE; 66 | END; 67 | $function$; 68 | 69 | create table oauth_access_tokens ( 70 | id INT NOT NULL PRIMARY KEY DEFAULT NEXTVAL ('oauth_access_tokens_seq'), 71 | oauth_client_id INT NOT NULL, 72 | access_token VARCHAR(64) NOT NULL, 73 | expires_in INTEGER CHECK (expires_in > 0) NOT NULL, 74 | expires_on TIMESTAMP(0) NOT NULL, 75 | metadata JSON NOT NULL, 76 | created_at TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP NOT NULL, 77 | updated_at TIMESTAMP(0) NULL, 78 | CONSTRAINT oauth_access_tokens_access_token_uindex UNIQUE (access_token), 79 | CONSTRAINT oauth_access_tokens_oauth_clients_id_fk FOREIGN KEY (oauth_client_id) REFERENCES oauth_clients (id) ON UPDATE CASCADE, 80 | CONSTRAINT oauth_access_tokens_check_metadata CHECK(is_json_valid(metadata)) 81 | );` 82 | 83 | oauthClientResources := `create sequence oauth_client_resources_seq; 84 | 85 | create table oauth_client_resources ( 86 | id INT NOT NULL PRIMARY KEY DEFAULT NEXTVAL ('oauth_client_resources_seq'), 87 | oauth_client_id INT NOT NULL, 88 | resource_type VARCHAR(36) NOT NULL, 89 | resource_id VARCHAR(36) NOT NULL, 90 | created_at TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP NOT NULL, 91 | updated_at TIMESTAMP(0) NULL, 92 | CONSTRAINT oauth_client_resources_oauth_client_id_resource_uindex UNIQUE ( 93 | oauth_client_id, resource_type, resource_id 94 | ), 95 | CONSTRAINT oauth_client_resources_oauth_clients_id_fk FOREIGN KEY (oauth_client_id) REFERENCES oauth_clients (id) ON UPDATE CASCADE 96 | ); 97 | 98 | CREATE INDEX oauth_client_resources_resource_type ON oauth_client_resources(resource_type); 99 | CREATE INDEX oauth_client_resources_resource_id ON oauth_client_resources(resource_id);` 100 | 101 | oauthClientScopes := `create sequence oauth_client_scopes_seq; 102 | 103 | create table oauth_client_scopes ( 104 | id INT NOT NULL DEFAULT NEXTVAL ('oauth_client_scopes_seq') PRIMARY KEY, 105 | oauth_client_id INT NOT NULL, 106 | scope VARCHAR (36) NOT NULL, 107 | created_at TIMESTAMP(0) DEFAULT CURRENT_TIMESTAMP NOT NULL, 108 | updated_at TIMESTAMP(0) NULL, 109 | constraint oauth_client_scopes_uindex UNIQUE (oauth_client_id), 110 | constraint oauth_client_scopes_oauth_clients_id_fk FOREIGN KEY (oauth_client_id) REFERENCES oauth_clients (id) ON UPDATE CASCADE 111 | );` 112 | 113 | queries = []string{oauthClients, 114 | users, 115 | oauthAccessTokens, 116 | oauthClientResources, 117 | oauthClientScopes, 118 | } 119 | 120 | files = []string{"01_oauth_clients.sql", 121 | "02_users.sql", 122 | "03_oauth_access_tokens.sql", 123 | "04_oauth_client_resources.sql", 124 | "05_oauth_client_scopes.sql", 125 | } 126 | 127 | case constants.NodeExpressGraphqlTemplate: 128 | 129 | products := `CREATE TABLE products ( 130 | id INT AUTO_INCREMENT NOT NULL PRIMARY KEY, 131 | name VARCHAR(255) NOT NULL, 132 | category VARCHAR(255) NOT NULL, 133 | amount BIGINT NOT NULL, 134 | created_at DATETIME DEFAULT NOW(), 135 | updated_at DATETIME NULL on UPDATE NOW(), 136 | deleted_at DATETIME, 137 | INDEX(name), 138 | INDEX(category) 139 | );` 140 | 141 | addresses := `CREATE TABLE addresses ( 142 | id INT AUTO_INCREMENT PRIMARY KEY, 143 | address_1 VARCHAR(255) NOT NULL, 144 | address_2 VARCHAR(255) NOT NULL, 145 | city VARCHAR(255) NOT NULL, 146 | country VARCHAR(255) NOT NULL, 147 | latitude FLOAT NOT NULL, 148 | longitude FLOAT NOT NULL, 149 | created_at DATETIME DEFAULT NOW(), 150 | updated_at DATETIME NULL on UPDATE NOW(), 151 | deleted_at DATETIME, 152 | INDEX(latitude), 153 | INDEX(longitude) 154 | );` 155 | 156 | stores := `CREATE TABLE stores ( 157 | id INT AUTO_INCREMENT PRIMARY KEY, 158 | name VARCHAR(255) NOT NULL, 159 | address_id INT NOT NULL, 160 | created_at DATETIME DEFAULT NOW(), 161 | updated_at DATETIME NULL on UPDATE NOW(), 162 | deleted_at DATETIME, 163 | CONSTRAINT stores_address_id FOREIGN KEY (address_id) REFERENCES addresses (id), 164 | INDEX(name) 165 | );` 166 | 167 | suppliers := `CREATE TABLE suppliers ( 168 | id INT AUTO_INCREMENT PRIMARY KEY, 169 | name VARCHAR(255) NOT NULL, 170 | address_id INT NOT NULL, 171 | created_at DATETIME DEFAULT NOW(), 172 | updated_at DATETIME NULL on UPDATE NOW(), 173 | deleted_at DATETIME, 174 | CONSTRAINT suppliers_address_id FOREIGN KEY (address_id) REFERENCES addresses (id), 175 | INDEX(name) 176 | );` 177 | 178 | supplierProducts := `CREATE TABLE supplier_products ( 179 | id INT AUTO_INCREMENT PRIMARY KEY, 180 | product_id INT NOT NULL, 181 | supplier_id INT NOT NULL, 182 | created_at DATETIME DEFAULT NOW(), 183 | updated_at DATETIME NULL on UPDATE NOW(), 184 | deleted_at DATETIME, 185 | CONSTRAINT suppliers_product_products_id FOREIGN KEY (product_id) REFERENCES products (id), 186 | CONSTRAINT suppliers_product_supplier_id FOREIGN KEY (supplier_id) REFERENCES suppliers (id) 187 | );` 188 | 189 | storeProducts := `CREATE TABLE store_products ( 190 | id INT AUTO_INCREMENT PRIMARY KEY, 191 | product_id INT NOT NULL, 192 | store_id INT NOT NULL, 193 | created_at DATETIME DEFAULT NOW(), 194 | updated_at DATETIME NULL on UPDATE NOW(), 195 | deleted_at DATETIME, 196 | CONSTRAINT store_products_product_id FOREIGN KEY (product_id) REFERENCES products (id), 197 | CONSTRAINT store_products_store_id FOREIGN KEY (store_id) REFERENCES stores (id) 198 | );` 199 | 200 | purchasedProducts := `CREATE TABLE purchased_products ( 201 | id INT AUTO_INCREMENT PRIMARY KEY, 202 | product_id INT NOT NULL, 203 | price INT NOT NULL, 204 | discount INT NOT NULL, 205 | store_id INT NOT NULL, 206 | delivery_date DATETIME NOT NULL, 207 | created_at DATETIME DEFAULT NOW(), 208 | updated_at DATETIME NULL on UPDATE NOW(), 209 | deleted_at DATETIME, 210 | CONSTRAINT purchased_products_product_id FOREIGN KEY (product_id) REFERENCES products (id), 211 | CONSTRAINT purchased_products_store_id FOREIGN KEY (store_id) REFERENCES stores (id), 212 | INDEX(delivery_date), 213 | INDEX(store_id) 214 | );` 215 | 216 | users := `CREATE TABLE users ( 217 | id INT AUTO_INCREMENT PRIMARY KEY, 218 | first_name VARCHAR(255) NOT NULL, 219 | last_name VARCHAR(255) NOT NULL, 220 | email VARCHAR(255) NOT NULL UNIQUE, 221 | password VARCHAR(255) NOT NULL, 222 | created_at DATETIME DEFAULT NOW(), 223 | updated_at DATETIME NULL on UPDATE NOW(), 224 | deleted_at DATETIME, 225 | INDEX(email) 226 | );` 227 | 228 | queries = []string{products, 229 | addresses, 230 | stores, 231 | suppliers, 232 | supplierProducts, 233 | storeProducts, 234 | purchasedProducts, 235 | users, 236 | } 237 | 238 | files = []string{"01_products.sql", 239 | "02_addresses.sql", 240 | "03_stores.sql", 241 | "04_supplier.sql", 242 | "05_supplier_products.sql", 243 | "06_store_products.sql", 244 | "07_purchased_products.sql", 245 | "08_users.sql", 246 | } 247 | 248 | default: 249 | return fmt.Errorf("Selected stack is invalid") 250 | } 251 | 252 | for idx, file := range files { 253 | err := utils.WriteToFile( 254 | fmt.Sprintf("%s/%s/%s/%s", utils.CurrentDirectory(), 255 | dirName, "resources/v1", file), 256 | queries[idx], 257 | ) 258 | errorhandler.CheckNilErr(err) 259 | } 260 | 261 | return nil 262 | } 263 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |

8 |

Service Picker 9 |

10 |

11 | 12 |

13 | A CLI tool that helps to setup full-stack javascript applications without having to touch any code. You'll be able to pick templates and databases of your choice, integrate it, set up automation pipelines and create infrastructure in AWS with ease. 14 | It contains a number of Wednesday Solutions's open source projects, connected and working together. Pick whatever you need and build your own ecosystem. 15 |

16 | 17 | --- 18 | 19 |

20 |

21 | Expert teams of digital product strategists, developers, and designers. 22 |

23 |

24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 | --- 35 | 36 | We're always looking for people who value their work, so come and join us. We are hiring! 37 | 38 |
39 | 40 |
41 | 42 | ## Table of contents 43 | 44 | - [Table of contents](#table-of-contents) 45 | - [Overview](#overview) 46 | - [Tech Stacks](#tech-stacks) 47 | - [Setup and Configuration.](#setup-and-configuration) 48 | - [Pre-requisites](#pre-requisites) 49 | - [Installation](#installation) 50 | - [Creating a Project](#creating-a-project) 51 | - [User Guide](#user-guide) 52 | - [Project Structure](#project-structure) 53 | - [Feedback](#feedback) 54 | - [License](#license) 55 | - [Future Plans](#future-plans) 56 | 57 | ## Overview 58 | 59 | Once business gives the sign-off and it's time for execution, the question thats most frequently asked is "What's the tech stack that we should use for this?" 60 | 61 | Fast-forward past the long debates where engineers are pitching their favourite languages and frameworks, we kick "git init" a bunch of repos for the frontend, infrastructure & backend. We then spend some time creating some boilerplate, or use some templates, setting up the CI/CD pipelines, IaC etc. The next thing you know, it's 6 weeks later and you're still configuring the connection between your database and your ec2 instance. The amount of business logic that you've written is minimal cause all of your team's time was spent configuring repos, environments, security groups, and other nitty-grittys. 62 | 63 | Thats where the service-picker comes in. We're working on building a cli tool that allows you to scaffold batteries included code, IaC & CI/CD pipelines, across environments and stacks, that is completely integrated with each other and ready to go. 64 | 65 | This means that setting up the infra and codebase for your next project which needs a React web app, with a node.js backend and a postgreSQL db is as simple as a hitting a few arrow buttons, and enter a couple of times. 66 | 67 | Service picker works on macOS, Windows and Linux.
68 | If something doesn't work, please file an [issue](https://github.com/wednesday-solutions/service-picker/issues).
69 | If you have questions, suggestions or need help, please ask in [GitHub Discussions](https://github.com/wednesday-solutions/service-picker/discussions) 70 | 71 | ## Tech Stacks 72 | 73 | This tool will have support for production applications using the following tech stacks. 74 | 75 | **Web:** 76 | 77 | - [React JS](https://github.com/wednesday-solutions/react-template) 78 | - [Next JS](https://github.com/wednesday-solutions/nextjs-template) 79 | 80 | **Backend:** 81 | 82 | - [Node (Hapi - REST API)](https://github.com/wednesday-solutions/nodejs-hapi-template) 83 | - [Node (Express - GraphQL API)](https://github.com/wednesday-solutions/node-express-graphql-template) 84 | 85 | **Databases:** 86 | 87 | - MySQL 88 | - PostgreSQL 89 | 90 | **Cache:** 91 | 92 | - Redis 93 | 94 | **Infrastructure Provider:** 95 | 96 | - [AWS](https://aws.amazon.com/) 97 | 98 | ## Setup and Configuration. 99 | 100 | ### Pre-requisites 101 | 102 | - [Golang](https://go.dev/doc/install) 103 | - [Node JS](https://nodejs.org/en/download) 104 | - Package Manager([npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) or [yarn](https://classic.yarnpkg.com/lang/en/docs/install/#mac-stable)) 105 | - [Docker](https://docs.docker.com/engine/install/) - Install and have it running in your local to docker compose applications and setup infrastructures in AWS. 106 | - [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) - Configure to your AWS account. 107 | ```bash 108 | $ aws configure 109 | AWS Access Key ID: MYACCESSKEYID 110 | AWS Secret Access Key: MYSECRETKEY 111 | Default region name [us-west-2]: MYAWSREGION 112 | Default output format [None]: 113 | ``` 114 | - Create a repository in your [AWS ECR](https://aws.amazon.com/ecr/). 115 | 116 | ```bash 117 | $ aws ecr create-repository --repository-name cdk-hnb659fds-container-assets-MYAWSACCOUNTID-MYAWSREGION 118 | ``` 119 | 120 | 121 | ### Installation 122 | 123 | Using Picky is easy. First use `go install` to install the latest version of the library (`go` should be installed in your system). 124 | 125 | ```bash 126 | go install github.com/wednesday-solutions/picky@latest 127 | ``` 128 | 129 | Please make sure the installation is successful by running the following command. 130 | 131 | ```bash 132 | picky -v 133 | ``` 134 | 135 | ## Creating a Project 136 | 137 | To create a new project, you need to pick stacks which are mentioned in [tech stacks](#tech-stacks)
138 | To start using `picky` 139 | 140 | ```bash 141 | mkdir my-project 142 | cd my-project 143 | ``` 144 | 145 | ```bash 146 | picky service 147 | ``` 148 | 149 | ![pick_a_service](https://github.com/wednesday-solutions/service-picker/assets/114065489/a3440e2a-9486-4419-85a6-eae2029ba45a) 150 | 151 | Use the arrow keys to navigate and pick a service you want.
152 | 153 | The complete stack initialization tutorial is given below. 154 | 155 | ![stack_initialisation_demo](https://github.com/wednesday-solutions/service-picker/assets/114065489/0c2f0e0b-bc05-4a3d-9d27-a69c6654419b) 156 | 157 | You can see `picky`'s home page if you initialized atleast one stack. You can choose any option in the following. 158 | 159 | Picky Home Preview Image 160 | 161 | ***Tips:*** 162 | - If you want to go back from the prompt, click `Ctrl + D` 163 | - If you want to exit from the prompt, click `Ctrl + C` 164 | 165 | ## User Guide 166 | 167 | | Option | Use | 168 | | ---------------- | ----------------------------------------------------------------------------------------- | 169 | | `Init Service` | Initialize a stack. | 170 | | `CI/CD` | Create CI/CD Pipeline in GitHub. | 171 | | `Docker Compose` | Create Docker Compose file for the mono-repo. It consist of all the selected stacks. | 172 | | `Setup Infra` | Setup infrastructure for initialized stacks. | 173 | | `Deploy` | Deploy the infrastructure in AWS. It can deploy Frontend, Backend or Full stack projects. | 174 | | `Remove Deploy` | Remove the deployed infrastructure. | 175 | | `Git Init` | Initialize empty git repository in the current directory. | 176 | | `Exit` | Exit from the tool. | 177 | 178 | ## Project Structure 179 | 180 | It will be like the following in the current directory. 181 | 182 | ``` 183 | my-project 184 | ├── .github 185 | │ └── workflows 186 | │ ├── cd-backend-node-hapi-pg.yml 187 | │ ├── cd-frontend-next-js-web.yml 188 | │ ├── ci-backend-node-hapi-pg.yml 189 | │ └── ci-frontend-next-js-web.yml 190 | ├── .sst 191 | │ ├── artifacts 192 | │ ├── dist 193 | │ ├── types 194 | │ ├── debug.log 195 | │ └── outputs.json 196 | ├── node_modules 197 | ├── stacks 198 | │ ├── BackendNodeHapiPg.js 199 | │ └── FrontendNextJsWeb.js 200 | ├── backend-node-hapi-pg 201 | │ └── ... 202 | ├── frontend-next-js-web 203 | │ └── ... 204 | ├── .env 205 | ├── .git 206 | ├── .gitignore 207 | ├── cdk.context.json 208 | ├── docker-compose.yml 209 | ├── backend-node-hapi-outputs.json 210 | ├── frontend-next-js-web-outputs.json 211 | ├── package.json 212 | ├── parseSstOutputs.js 213 | ├── sst.config.js 214 | └── yarn.lock 215 | ``` 216 | 217 | ## Feedback 218 | 219 | If you have any feedback, please reach out to us at [GitHub Discussions](https://github.com/wednesday-solutions/service-picker/discussions) 220 | 221 | ## License 222 | 223 | This project is under the [MIT License](https://github.com/wednesday-solutions/service-picker/blob/main/LICENSE). 224 | 225 | ## Future Plans 226 | 227 | Currently the service-picker is capable of setting up full-stack javascript applications. In it's end state the service picker will allow you to choose right from your cloud infra structure provider (GCP, AWS, AZURE) to different backends and databases that you'd like to use, to your caching strategy, message broker, mobile app release tooling and any other tooling choice that you must make along your product development journey. 228 | -------------------------------------------------------------------------------- /pickyhelpers/sources/create_cd_source.go: -------------------------------------------------------------------------------- 1 | package sources 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/wednesday-solutions/picky/internal/constants" 7 | "github.com/wednesday-solutions/picky/internal/utils" 8 | ) 9 | 10 | func CDBackendSource(stack, stackDir, environment string) string { 11 | 12 | userInput := utils.FindUserInputStackName(stackDir) 13 | source := fmt.Sprintf(`# CD pipeline for %s for %s branch 14 | 15 | name: CD %s - %s 16 | 17 | on: 18 | push: 19 | branches: 20 | - dev 21 | - qa 22 | - master 23 | # paths: "%s/**" 24 | workflow_dispatch: 25 | 26 | jobs: 27 | docker-build-and-push: 28 | name: Docker build image and push 29 | runs-on: ubuntu-latest 30 | defaults: 31 | run: 32 | working-directory: ./%s 33 | strategy: 34 | matrix: 35 | node-version: [16.14.x] 36 | 37 | steps: 38 | # Checkout 39 | - name: Checkout to branch 40 | uses: actions/checkout@v2 41 | 42 | - name: Use Node.js ${{ matrix.node-version }} 43 | uses: actions/setup-node@v2 44 | with: 45 | node-version: ${{ matrix.node-version }} 46 | 47 | - name: Get branch name 48 | id: vars 49 | run: echo ::set-output name=short_ref::${GITHUB_REF_NAME} 50 | 51 | - name: Set env.ENV_NAME and env.BUILD_NAME 52 | run: | 53 | if [[ ${{ steps.vars.outputs.short_ref }} == master ]]; then 54 | echo "BUILD_NAME=prod" >> "$GITHUB_ENV" 55 | else 56 | echo "ENV_NAME=.development" >> "$GITHUB_ENV" 57 | echo "BUILD_NAME=dev" >> "$GITHUB_ENV" 58 | fi 59 | 60 | # Configure AWS with credentials 61 | - name: Configure AWS Credentials 62 | uses: aws-actions/configure-aws-credentials@v1 63 | with: 64 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 65 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 66 | aws-region: ${{ secrets.AWS_REGION }} 67 | 68 | # Login to Amazon ECR 69 | - name: Login to Amazon ECR 70 | id: login-ecr 71 | uses: aws-actions/amazon-ecr-login@v1 72 | 73 | # Build, tag, and push image to Amazon ECR 74 | - name: Build, tag, and push image to ECR 75 | env: 76 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 77 | ECR_REPOSITORY: ${{ secrets.AWS_ECR_REPOSITORY }} 78 | AWS_REGION: ${{ secrets.AWS_REGION }} 79 | IMAGE_TAG: ${{ github.sha }} 80 | DOCKER_BUILDKIT: 1 81 | run: | 82 | docker build --no-cache -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . --build-arg BUILD_NAME=${{ env.BUILD_NAME }} --build-arg ENVIRONMENT_NAME=${{ env.ENV_NAME }} 83 | docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG 84 | 85 | # Create and configure Amazon ECS task definition 86 | - name: Render Amazon ECS task definition 87 | id: %s-container 88 | uses: aws-actions/amazon-ecs-render-task-definition@v1 89 | with: 90 | task-definition: %s/task-definition-${{ env.BUILD_NAME }}.json 91 | container-name: %s-container-${{ env.BUILD_NAME }} 92 | image: ${{ steps.login-ecr.outputs.registry }}/${{ secrets.AWS_ECR_REPOSITORY }}:${{ github.sha }} 93 | 94 | # Deploy to Amazon ECS 95 | - name: Deploy to Amazon ECS 96 | uses: aws-actions/amazon-ecs-deploy-task-definition@v1 97 | with: 98 | task-definition: ${{ steps.%s-container.outputs.task-definition }} 99 | service: %s-service-${{ env.BUILD_NAME }} 100 | cluster: %s-cluster-${{ env.BUILD_NAME }} 101 | 102 | # Logout of Amazon 103 | - name: Logout of Amazon ECR 104 | if: always() 105 | run: docker logout ${{ steps.login-ecr.outputs.registry }} 106 | `, 107 | stackDir, environment, stackDir, environment, stackDir, stackDir, 108 | userInput, stackDir, userInput, userInput, userInput, userInput, 109 | ) 110 | return source 111 | } 112 | 113 | func TaskDefinitionSource(environment, stackDir string) string { 114 | 115 | taskDefinition := utils.GetOutputsBackendObject(environment, stackDir) 116 | 117 | taskDefinitionSource := fmt.Sprintf(`{ 118 | "taskRoleArn": "%s", 119 | "executionRoleArn": "%s", 120 | "taskDefinitionArn": "%s", 121 | "family": "%s", 122 | "containerDefinitions": [ 123 | { 124 | "name": "%s", 125 | "image": "%s", 126 | "logConfiguration": { 127 | "logDriver": "%s", 128 | "secretOptions": null, 129 | "options": { 130 | "awslogs-group": "%s", 131 | "awslogs-stream-prefix": "%s", 132 | "awslogs-region": "%s" 133 | } 134 | }, 135 | "portMappings": [ 136 | { 137 | "hostPort": "9000", 138 | "protocol": "tcp", 139 | "containerPort": "%s" 140 | } 141 | ], 142 | "environment": [ 143 | { 144 | "name": "BUILD_NAME", 145 | "value": "%s" 146 | }, 147 | { 148 | "name": "ENVIRONMENT_NAME", 149 | "value": "%s" 150 | } 151 | ], 152 | "secrets": [ 153 | { 154 | "name": "%s", 155 | "valueFrom": "%s:password::" 156 | } 157 | ], 158 | "cpu": 0, 159 | "memory": null, 160 | "command": null, 161 | "entryPoint": null, 162 | "dnsSearchDomains": null, 163 | "linuxParameters": null, 164 | "resourceRequirements": null, 165 | "ulimits": null, 166 | "dnsServers": null, 167 | "mountPoints": [], 168 | "workingDirectory": null, 169 | "dockerSecurityOptions": null, 170 | "memoryReservation": null, 171 | "volumesFrom": [], 172 | "stopTimeout": null, 173 | "startTimeout": null, 174 | "firelensConfiguration": null, 175 | "dependsOn": null, 176 | "disableNetworking": null, 177 | "interactive": null, 178 | "healthCheck": null, 179 | "essential": true, 180 | "links": null, 181 | "hostname": null, 182 | "extraHosts": null, 183 | "pseudoTerminal": null, 184 | "user": null, 185 | "readonlyRootFilesystem": null, 186 | "dockerLabels": null, 187 | "systemControls": null, 188 | "privileged": null 189 | } 190 | ], 191 | "placementConstraints": [], 192 | "memory": "2048", 193 | "compatibilities": ["EC2", "FARGATE"], 194 | "requiresAttributes": [ 195 | { 196 | "name": "com.amazonaws.ecs.capability.logging-driver.awslogs" 197 | }, 198 | { 199 | "name": "ecs.capability.execution-role-awslogs" 200 | }, 201 | { 202 | "name": "com.amazonaws.ecs.capability.ecr-auth" 203 | }, 204 | { 205 | "name": "com.amazonaws.ecs.capability.docker-remote-api.1.19" 206 | }, 207 | { 208 | "name": "ecs.capability.execution-role-ecr-pull" 209 | }, 210 | { 211 | "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18" 212 | }, 213 | { 214 | "name": "ecs.capability.task-eni", 215 | "targetId": null, 216 | "targetType": null, 217 | "value": null 218 | } 219 | ], 220 | "ipcMode": null, 221 | "pidMode": null, 222 | "requiresCompatibilities": ["FARGATE"], 223 | "networkMode": "awsvpc", 224 | "cpu": "1024", 225 | "revision": 17, 226 | "status": "ACTIVE", 227 | "inferenceAccelerators": null, 228 | "proxyConfiguration": null, 229 | "volumes": [], 230 | "tags": [ 231 | { 232 | "key": "sst:app", 233 | "value": "web-app" 234 | }, 235 | { 236 | "key": "sst:stage", 237 | "value": "dev" 238 | } 239 | ] 240 | } 241 | `, 242 | taskDefinition.BackendObj.TaskRole, 243 | taskDefinition.BackendObj.ExecutionRole, 244 | taskDefinition.BackendObj.TaskDefinition, 245 | taskDefinition.BackendObj.Family, 246 | taskDefinition.BackendObj.ContainerName, 247 | taskDefinition.BackendObj.Image, 248 | taskDefinition.BackendObj.LogDriver, 249 | taskDefinition.BackendObj.LogDriverOptions.AwsLogsGroup, 250 | taskDefinition.BackendObj.LogDriverOptions.AwsLogsStreamPrefix, 251 | taskDefinition.BackendObj.LogDriverOptions.AwsLogsRegion, 252 | taskDefinition.BackendObj.ContainerPort, 253 | taskDefinition.Environment, 254 | taskDefinition.EnvName, 255 | taskDefinition.SecretName, 256 | taskDefinition.BackendObj.SecretArn, 257 | ) 258 | return taskDefinitionSource 259 | } 260 | 261 | func CDWebSource(stack, dirName string) string { 262 | var sourceDir string 263 | if stack == constants.ReactJS { 264 | sourceDir = "build" 265 | } else if stack == constants.NextJS { 266 | sourceDir = "out" 267 | } 268 | source := fmt.Sprintf(`name: CD %s 269 | on: 270 | push: 271 | branches: 272 | - dev 273 | - qa 274 | - master 275 | # paths: "%s/**" 276 | workflow_dispatch: 277 | 278 | jobs: 279 | deploy: 280 | name: Deploy 281 | runs-on: ubuntu-latest 282 | if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/qa' 283 | defaults: 284 | run: 285 | working-directory: ./%s 286 | strategy: 287 | matrix: 288 | node-version: [16.13.0] 289 | env: 290 | SOURCE_DIR: "./%s/%s/" 291 | AWS_REGION: ${{ secrets.AWS_REGION }} 292 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 293 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 294 | PATHS: "/*" 295 | 296 | steps: 297 | - name: Checkout to branch 298 | uses: actions/checkout@v2 299 | 300 | - name: Use Node.js ${{ matrix.node-version }} 301 | uses: actions/setup-node@v2 302 | with: 303 | node-version: ${{ matrix.node-version }} 304 | 305 | - name: Get branch name 306 | id: vars 307 | run: echo ::set-output name=short_ref::${GITHUB_REF_NAME} 308 | 309 | - name: Set short environment name 310 | id: environment 311 | run: | 312 | if [[ ${{ steps.vars.outputs.short_ref }} == master ]]; then 313 | echo ::set-output name=short_env::prod 314 | elif [[ ${{ steps.vars.outputs.short_ref }} == qa ]]; then 315 | echo ::set-output name=short_env::qa 316 | else 317 | echo ::set-output name=short_env::dev 318 | fi 319 | 320 | - name: Install dependencies 321 | run: yarn 322 | 323 | - name: Build 324 | run: yarn build:${{ steps.environment.outputs.short_env }} 325 | 326 | - name: AWS Deploy to S3 327 | uses: jakejarvis/s3-sync-action@v0.5.1 328 | with: 329 | args: --follow-symlinks --delete 330 | env: 331 | AWS_S3_BUCKET: %s-${{ steps.environment.outputs.short_env }} 332 | 333 | - name: Invalidate CloudFront 334 | uses: chetan/invalidate-cloudfront-action@v2.4 335 | env: 336 | DISTRIBUTION: ${{ secrets.DISTRIBUTION_ID }} 337 | `, 338 | dirName, dirName, dirName, dirName, sourceDir, dirName) 339 | return source 340 | } 341 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0= 2 | github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= 3 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 4 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 5 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 6 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 7 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 8 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 9 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 10 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 11 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 12 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/enescakir/emoji v1.0.0 h1:W+HsNql8swfCQFtioDGDHCHri8nudlK1n5p2rHCJoog= 17 | github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkKp+WKFD0= 18 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 19 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 20 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 21 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 22 | github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg= 23 | github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y= 24 | github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= 25 | github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= 26 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 27 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 28 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= 29 | github.com/juju/ansiterm v1.0.0 h1:gmMvnZRq7JZJx6jkfSq9/+2LMrVEwGwt7UR6G+lmDEg= 30 | github.com/juju/ansiterm v1.0.0/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384= 31 | github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= 32 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 33 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 34 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 35 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 36 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 37 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 38 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 39 | github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= 40 | github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= 41 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 42 | github.com/mattn/go-colorable v0.1.10/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 43 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 44 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 45 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 46 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 47 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 48 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 49 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 50 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 51 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 52 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 53 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 54 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= 55 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= 56 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 57 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 58 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 59 | github.com/rivo/tview v0.0.0-20230814110005-ccc2c8119703 h1:ZyM/+FYnpbZsFWuCohniM56kRoHRB4r5EuIzXEYkpxo= 60 | github.com/rivo/tview v0.0.0-20230814110005-ccc2c8119703/go.mod h1:nVwGv4MP47T0jvlk7KuTTjjuSmrGO4JF0iaiNt4bufE= 61 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 62 | github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 63 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 64 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 65 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 66 | github.com/schollz/progressbar/v3 v3.13.1 h1:o8rySDYiQ59Mwzy2FELeHY5ZARXZTVJC7iHD6PEFUiE= 67 | github.com/schollz/progressbar/v3 v3.13.1/go.mod h1:xvrbki8kfT1fzWzBT/UZd9L6GA+jdL7HAgq2RFnO6fQ= 68 | github.com/spaceweasel/promptui v0.8.1 h1:DVxYGbaUcDRzZwpp+qifsTBKvGLupSBl3RUvxYw+laY= 69 | github.com/spaceweasel/promptui v0.8.1/go.mod h1:HOb/JMVBMo99PCMihjeHmfpIPQnwfOebaR1bFGfry8s= 70 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 71 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 72 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 73 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 74 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 75 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 76 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 77 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 78 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 79 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 80 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 81 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 82 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 83 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 84 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 85 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 86 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 87 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 88 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 89 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 90 | golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 91 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 93 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 94 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 95 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 96 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 97 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 98 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 99 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 100 | golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= 101 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 102 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 103 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 104 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 105 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 106 | golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= 107 | golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= 108 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 109 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 110 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 111 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 112 | golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= 113 | golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 114 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 115 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 116 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 117 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 118 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 119 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 120 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 121 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 122 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 123 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 124 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 125 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 126 | -------------------------------------------------------------------------------- /internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/iancoleman/strcase" 13 | "github.com/wednesday-solutions/picky/internal/constants" 14 | "github.com/wednesday-solutions/picky/internal/errorhandler" 15 | ) 16 | 17 | // SplitStackDirectoryName returns user-input, stack-suffix and last-suffix of the given stack directory name. 18 | func SplitStackDirectoryName(dirName string) (string, string, string, string) { 19 | var userInput, langSuffix, stackSuffix, lastSuffix string 20 | var splitDirName []string 21 | var isBackendStack, isWebStack, isMobileStack bool 22 | splitDirName = strings.Split(dirName, "-") 23 | if len(splitDirName) > constants.Two { 24 | lastSuffix = splitDirName[len(splitDirName)-constants.One] 25 | stackSuffix = splitDirName[len(splitDirName)-constants.Two] 26 | langSuffix = splitDirName[len(splitDirName)-constants.Three] 27 | if lastSuffix == constants.Pg || lastSuffix == constants.Mysql || lastSuffix == constants.Mongo { 28 | isBackendStack = true 29 | } else if lastSuffix == constants.Web { 30 | isWebStack = true 31 | } else if lastSuffix == constants.Mobile { 32 | isMobileStack = true 33 | } 34 | var suffixSize int 35 | if isBackendStack { 36 | suffixSize = constants.BackendSuffixSize 37 | } else if isWebStack { 38 | suffixSize = constants.WebSuffixSize 39 | } else if isMobileStack { 40 | suffixSize = constants.MobileSuffixSize 41 | } 42 | userInput = splitDirName[constants.Zero] 43 | for _, split := range splitDirName[constants.One : len(splitDirName)-suffixSize] { 44 | userInput = fmt.Sprintf("%s-%s", userInput, split) 45 | } 46 | } 47 | return userInput, langSuffix, stackSuffix, lastSuffix 48 | } 49 | 50 | // CovertToCamelCase return camel cased array of string of the given array of string. 51 | func ConvertToCamelCase(slice []string) []string { 52 | camelSlice := []string{} 53 | for _, str := range slice { 54 | camelSlice = append(camelSlice, strcase.ToCamel(str)) 55 | } 56 | return camelSlice 57 | } 58 | 59 | // RunCommandWithLogs runs the given command with logs. 60 | func RunCommandWithLogs(path string, name string, args ...string) error { 61 | cmd := exec.Command(name, args...) 62 | cmd.Stdout = os.Stdout 63 | cmd.Stderr = os.Stderr 64 | if path != "" { 65 | cmd.Dir = path 66 | } 67 | err := cmd.Run() 68 | fmt.Printf("\n") 69 | return err 70 | } 71 | 72 | // RunCommandWithLogs runs the given command without logs. 73 | func RunCommandWithoutLogs(path string, name string, args ...string) error { 74 | cmd := exec.Command(name, args...) 75 | if path != "" { 76 | cmd.Dir = path 77 | } 78 | err := cmd.Run() 79 | return err 80 | } 81 | 82 | // GetPackageManagerOfUser checks whether yarn or npm is installed in the user's machine. 83 | // If both are not installed, then the system will throw error. 84 | func GetPackageManagerOfUser() string { 85 | var pkgManager string 86 | err := RunCommandWithoutLogs("", constants.Yarn, "-v") 87 | if err != nil { 88 | err = RunCommandWithoutLogs("", constants.Npm, "-v") 89 | if err != nil { 90 | // Throw error either yarn or npm not installed 91 | errorhandler.CheckNilErr(fmt.Errorf("Please install 'yarn' or 'npm' in your machine.\n")) 92 | } else { 93 | pkgManager = constants.Npm 94 | } 95 | } else { 96 | pkgManager = constants.Yarn 97 | } 98 | return pkgManager 99 | } 100 | 101 | // GetProjectName returns projectName 102 | func GetProjectName() string { 103 | currentDir := CurrentDirectory() 104 | splitDirs := strings.Split(currentDir, "/") 105 | projectName := splitDirs[len(splitDirs)-1] 106 | return projectName 107 | } 108 | 109 | func CreateGithubWorkflowDir() { 110 | currentDir := CurrentDirectory() 111 | workflowsPath := fmt.Sprintf("%s/%s", currentDir, 112 | constants.GithubWorkflowsDir, 113 | ) 114 | workflowStatus, _ := IsExists(workflowsPath) 115 | if !workflowStatus { 116 | githubFolderPath := fmt.Sprintf("%s/%s", currentDir, ".github") 117 | githubStatus, _ := IsExists(githubFolderPath) 118 | if !githubStatus { 119 | err := CreateDirectory(githubFolderPath) 120 | errorhandler.CheckNilErr(err) 121 | } 122 | err := CreateDirectory(workflowsPath) 123 | errorhandler.CheckNilErr(err) 124 | } 125 | } 126 | 127 | func EndsWith(inputString, endString string) bool { 128 | if len(inputString) < len(endString) { 129 | return false 130 | } else if len(inputString) == len(endString) { 131 | return inputString == endString 132 | } else { 133 | for i, j := len(endString)-1, len(inputString)-1; i >= 0; i, j = i-1, j-1 { 134 | if endString[i] != inputString[j] { 135 | return false 136 | } 137 | } 138 | return true 139 | } 140 | } 141 | 142 | func StartsWith(inputString, startString string) bool { 143 | if len(inputString) < len(startString) { 144 | return false 145 | } else if len(inputString) == len(startString) { 146 | return inputString == startString 147 | } else { 148 | for i, j := 0, 0; i < len(startString); i, j = i+1, j+1 { 149 | if startString[i] != inputString[j] { 150 | return false 151 | } 152 | } 153 | return true 154 | } 155 | } 156 | 157 | type LogDriverOptionsKeys struct { 158 | AwsLogsGroup string `json:"awsLogsGroup"` 159 | AwsLogsStreamPrefix string `json:"awsLogsStreamPrefix"` 160 | AwsLogsRegion string `json:"awsLogsRegion"` 161 | } 162 | 163 | type WebOutputKeys struct { 164 | DistributionId string `json:"distributionId"` 165 | BucketName string `json:"bucketName"` 166 | SiteUrl string `json:"siteUrl"` 167 | } 168 | 169 | type BackendOutputKeys struct { 170 | TaskRole string `json:"taskRole"` 171 | Image string `json:"image"` 172 | ContainerName string `json:"containerName"` 173 | ContainerPort string `json:"containerPort"` 174 | ExecutionRole string `json:"executionRole"` 175 | TaskDefinition string `json:"taskDefinition"` 176 | LogDriver string `json:"logDriver"` 177 | LogDriverOptions LogDriverOptionsKeys 178 | Family string `json:"family"` 179 | AwsRegion string `json:"awsRegion"` 180 | RedisHost string `json:"redisHost"` 181 | SecretName string `json:"secretName"` 182 | DatabaseHost string `json:"databaseHost"` 183 | DatabaseName string `json:"databaseName"` 184 | SecretArn string `json:"secretArn"` 185 | LoadBalancerDns string `json:"loadBalancerDns"` 186 | ServiceName string `json:"serviceName"` 187 | ClusterName string `json:"clusterName"` 188 | ElasticContainerRegistryRepo string `json:"elasticContainerRegistryRepo"` 189 | } 190 | 191 | type TaskDefinitionDetails struct { 192 | BackendObj BackendOutputKeys 193 | Environment string 194 | EnvName string 195 | SecretName string 196 | } 197 | 198 | func ReadJsonDataInSstOutputs() map[string]interface{} { 199 | file := fmt.Sprintf( 200 | "%s/%s/%s", CurrentDirectory(), constants.DotSst, constants.OutputsJson, 201 | ) 202 | status, _ := IsExists(file) 203 | if !status { 204 | return nil 205 | } 206 | sstOutputFile, err := os.Open(file) 207 | errorhandler.CheckNilErr(err) 208 | 209 | fileContent, err := io.ReadAll(sstOutputFile) 210 | errorhandler.CheckNilErr(err) 211 | 212 | var data interface{} 213 | err = json.Unmarshal(fileContent, &data) 214 | errorhandler.CheckNilErr(err) 215 | 216 | if jsonData, ok := data.(map[string]interface{}); ok { 217 | return jsonData 218 | } 219 | return nil 220 | } 221 | 222 | func GetOutputsBackendObject(environment, stackDir string) TaskDefinitionDetails { 223 | camelCaseDir := strcase.ToCamel(stackDir) 224 | jsonData := ReadJsonDataInSstOutputs() 225 | // if jsonData == nil { 226 | // errorhandler.CheckNilErr(fmt.Errorf("outputs.json is not valid.")) 227 | // } 228 | var td TaskDefinitionDetails 229 | td.Environment = environment 230 | td.EnvName = GetShortEnvName(environment) 231 | key := fmt.Sprintf("%s-web-app-%s", td.EnvName, camelCaseDir) 232 | 233 | if value, ok := jsonData[key]; ok { 234 | 235 | jsonOutput, err := json.Marshal(value) 236 | errorhandler.CheckNilErr(err) 237 | 238 | var backendObj BackendOutputKeys 239 | err = json.Unmarshal(jsonOutput, &backendObj) 240 | errorhandler.CheckNilErr(err) 241 | 242 | td.BackendObj = backendObj 243 | return td 244 | } 245 | return TaskDefinitionDetails{} 246 | } 247 | 248 | func CreateInfraOutputsJson(environment string) error { 249 | 250 | jsonData := ReadJsonDataInSstOutputs() 251 | if jsonData == nil { 252 | errorhandler.CheckNilErr(fmt.Errorf("outputs.json is not valid")) 253 | } 254 | envName := GetShortEnvName(environment) 255 | _, _, directories := GetExistingStacksDatabasesAndDirectories() 256 | for _, stackDir := range directories { 257 | camelCaseDir := strcase.ToCamel(stackDir) 258 | 259 | key := fmt.Sprintf("%s-web-app-%s", envName, camelCaseDir) 260 | 261 | if value, ok := jsonData[key]; ok { 262 | outputFile := fmt.Sprintf("%s/%s-%s", CurrentDirectory(), stackDir, constants.OutputsJson) 263 | 264 | status, _ := IsExists(outputFile) 265 | if !status { 266 | err := CreateFile(outputFile) 267 | errorhandler.CheckNilErr(err) 268 | } 269 | 270 | jsonOutput, err := json.MarshalIndent(value, "", "\t") 271 | errorhandler.CheckNilErr(err) 272 | 273 | err = WriteToFile(outputFile, string(jsonOutput)) 274 | errorhandler.CheckNilErr(err) 275 | } 276 | } 277 | return nil 278 | } 279 | 280 | // GetShortEnvName return short environment name for the given environment. 281 | func GetShortEnvName(environment string) string { 282 | var shortEnv string 283 | if environment == constants.Dev || environment == constants.Develop || environment == constants.Development { 284 | shortEnv = constants.Dev 285 | } else if environment == constants.QA { 286 | shortEnv = constants.QA 287 | } else if environment == constants.Prod || environment == constants.Production { 288 | shortEnv = constants.Prod 289 | } 290 | return shortEnv 291 | } 292 | 293 | func GetPortNumber(defaultPN int) int { 294 | _, _, stackDirs := GetExistingStacksDatabasesAndDirectories() 295 | var backendServices []string 296 | for _, dir := range stackDirs { 297 | service := FindService(dir) 298 | if service == constants.Backend { 299 | backendServices = append(backendServices, service) 300 | } 301 | } 302 | currentPN := defaultPN + len(backendServices) - 1 303 | return currentPN 304 | } 305 | 306 | func GetDatabasePortNumber(driver string) int { 307 | _, _, stackDirs := GetExistingStacksDatabasesAndDirectories() 308 | var postgresStacks, mysqlStacks []string 309 | for _, dir := range stackDirs { 310 | service := FindService(dir) 311 | if service == constants.Backend { 312 | _, database := FindStackAndDatabase(dir) 313 | if database == constants.PostgreSQL { 314 | postgresStacks = append(postgresStacks, dir) 315 | } else if database == constants.MySQL { 316 | mysqlStacks = append(mysqlStacks, dir) 317 | } 318 | } 319 | } 320 | var currentPortNumber int 321 | if driver == constants.PostgreSQL { 322 | currentPortNumber = constants.PostgresPortNumber + len(postgresStacks) - 1 323 | } else if driver == constants.MySQL { 324 | currentPortNumber = constants.MysqlPortNumber + len(mysqlStacks) - 1 325 | } 326 | return currentPortNumber 327 | } 328 | 329 | func FetchExistingPortNumber(stackDir, portName string) string { 330 | envFile := fmt.Sprintf("%s/%s/%s", 331 | CurrentDirectory(), 332 | stackDir, 333 | constants.DockerEnvFile, 334 | ) 335 | var portNumber string 336 | content, err := os.ReadFile(envFile) 337 | errorhandler.CheckNilErr(err) 338 | lines := strings.Split(string(content), "\n") 339 | for _, line := range lines { 340 | if StartsWith(line, portName) { 341 | portLine := strings.Split(line, "=") 342 | portNumber = portLine[len(portLine)-1] 343 | return portNumber 344 | } 345 | } 346 | if portName == constants.BackendPort { 347 | portNumber = strconv.Itoa(constants.BackendPortNumber) 348 | } else if portName == constants.PostgresPort { 349 | portNumber = strconv.Itoa(constants.PostgresPortNumber) 350 | } else if portName == constants.MysqlPort { 351 | portNumber = strconv.Itoa(constants.MysqlPortNumber) 352 | } else if portName == constants.RedisPort { 353 | portNumber = strconv.Itoa(constants.RedisPortNumber) 354 | } 355 | return portNumber 356 | } 357 | --------------------------------------------------------------------------------