├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── lint.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── Dockerfile ├── LICENSE ├── README.md ├── asciiArt.txt ├── cmd ├── launch.go └── root.go ├── formula └── bootstrap-cli.rb ├── go.mod ├── go.sum ├── internal ├── constants │ ├── backend.go │ ├── common.go │ ├── docker.go │ ├── frontend.go │ ├── kubernetes.go │ └── navigation.go ├── templates │ ├── backend.go │ ├── docker.go │ ├── frontend.go │ └── item.go └── ui │ ├── init.go │ ├── inputs │ ├── app.go │ ├── model.go │ ├── style.go │ ├── update.go │ └── view.go │ ├── item.go │ ├── keys.go │ ├── list │ ├── item.go │ ├── keys.go │ ├── model.go │ ├── style.go │ ├── update.go │ └── view.go │ ├── model.go │ ├── root.go │ ├── styles.go │ ├── update.go │ └── view.go └── main.go /.dockerignore: -------------------------------------------------------------------------------- 1 | # The .dockerignore file excludes files from the container build process. 2 | # https://docs.docker.com/engine/reference/builder/#dockerignore-file 3 | 4 | # Exclude locally vendored dependencies. 5 | vendor/ 6 | 7 | # Exclude "build-time" ignore files. 8 | .dockerignore 9 | 10 | # Exclude git history and configuration. 11 | .gitignore -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEAT]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: [push, pull_request] 4 | jobs: 5 | golangci: 6 | name: lint 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Set up Go 1.19 10 | uses: actions/setup-go@v3 11 | with: 12 | go-version: 1.19 13 | id: go 14 | - uses: actions/checkout@v3 15 | - name: golangci-lint 16 | uses: golangci/golangci-lint-action@v3.2.0 17 | with: 18 | args: --issues-exit-code=0 -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: write 8 | 9 | jobs: 10 | release: 11 | name: GoReleaser build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Check out source code 15 | uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | - name: Set up Go 1.19 19 | uses: actions/setup-go@v3 20 | with: 21 | go-version: 1.19 22 | id: go 23 | - name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@v3 25 | with: 26 | version: latest 27 | args: release --rm-dist 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | - name: Upload assets 31 | uses: actions/upload-artifact@v3 32 | with: 33 | name: bootstrap-cli 34 | path: dist/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | demo* 2 | bootstrap-cli -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: bootstrap-cli 2 | 3 | before: 4 | hooks: 5 | - go mod tidy 6 | - go mod download 7 | 8 | builds: 9 | - main: ./main.go 10 | goos: 11 | - linux 12 | - darwin 13 | - windows 14 | goarch: 15 | - amd64 16 | - arm64 17 | targets: 18 | - linux_amd64 19 | - darwin_amd64 20 | - darwin_arm64 21 | - windows_amd64 22 | env: 23 | - CGO_ENABLED=0 24 | ldflags: 25 | - -s -w 26 | hooks: 27 | post: 28 | - upx --brute "{{ .Path }}" 29 | 30 | brews: 31 | - tap: 32 | owner: wingkwong 33 | name: bootstrap-cli 34 | url_template: "https://github.com/wingkwong/bootstrap-cli/releases/download/{{ .Tag }}/{{ .ArtifactName }}" 35 | commit_author: 36 | name: wingkwong 37 | email: wingkwong.code@gmail.com 38 | folder: formula 39 | caveats: "A minimalistic CLI to bootstrap projects with different frameworks." 40 | homepage: "https://github.com/wingkwong/bootstrap-cli" 41 | description: "A minimalistic CLI to bootstrap projects with different frameworks." 42 | license: "MIT" 43 | dependencies: 44 | - name: npm 45 | - name: go 46 | 47 | archives: 48 | - replacements: 49 | darwin: macOS 50 | linux: Linux 51 | windows: Windows 52 | amd64: x86_64 53 | format_overrides: 54 | - goos: windows 55 | format: zip 56 | files: 57 | - LICENSE 58 | - README.md 59 | 60 | snapshot: 61 | name_template: "{{ .Tag }}" 62 | 63 | changelog: 64 | sort: asc 65 | filters: 66 | exclude: 67 | - "^*.md:" -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Go image as the base image 2 | FROM golang:1.19 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Copy the source code to the container 8 | COPY . . 9 | 10 | # Build the Go application 11 | RUN go build -o bootstrap-cli . 12 | 13 | # Specify the command to run the application when the container starts 14 | CMD ["./bootstrap-cli"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 աɨռɢӄաօռɢ 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | bootstrap-cli-logo 4 |
5 | Bootstrap CLI 6 |

7 |

A minimalistic CLI to bootstrap projects with different frameworks.

8 | 9 | 10 |

11 | go version 12 |   13 | 14 | go report 15 | 16 |   17 | 18 | license 19 | 20 |

21 | 22 | 23 | ![demo](https://user-images.githubusercontent.com/35857179/219651301-cfa1d385-b215-43c8-be65-68b88c1139d0.gif) 24 | 25 | ## 🛠 Prerequisites 26 | 27 | - Go 1.19 28 | 29 | ## 💻 Quick Start 30 | 31 | ```go 32 | go install github.com/wingkwong/bootstrap-cli@latest 33 | ``` 34 | 35 | ## 📚 Available Templates 36 | 37 |
38 | 📘 Frontend 39 | 40 | - vue 41 | - vue-ts 42 | - react 43 | - react-ts 44 | - next 45 | - next-ts 46 | - vanilla 47 | - vanilla-ts 48 | - gatsby 49 | - gatsby-ts 50 |
51 | 52 |
53 | 📙 Backend 54 | 55 | - express 56 | - koa 57 |
58 | 59 | 66 | 67 | ## 🗣️ Join Community 68 | 69 | [Join the Bootstrap CLI Discord Server](https://discord.gg/hGKVsGxMY3) 70 | 71 | ## 🔱 Contributing 72 | 73 | Contributions are welcome. However, please discuss the details in Discord first. 74 | 75 | ## 🎴 License 76 | 77 | This project is licensed under the [MIT License](https://raw.githubusercontent.com/wingkwong/bootstrap-cli/develop/LICENSE). -------------------------------------------------------------------------------- /asciiArt.txt: -------------------------------------------------------------------------------- 1 | ____ __ __ ____ ____ ____ ____ __ ____ ___ __ __ 2 | ( _ \ / \ / \(_ _)/ ___)(_ _)( _ \ / _\ ( _ \ ___ / __)( ) ( ) 3 | ) _ (( O )( O ) )( \___ \ )( ) // \ ) __/(___)( (__ / (_/\ )( 4 | (____/ \__/ \__/ (__) (____/ (__) (__\_)\_/\_/(__) \___)\____/(__) 5 | -------------------------------------------------------------------------------- /cmd/launch.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "github.com/wingkwong/bootstrap-cli/internal/ui" 6 | ) 7 | 8 | // launchCmd represents the launch command 9 | var launchCmd = &cobra.Command{ 10 | Use: "launch", 11 | Short: "launches text user interface view", 12 | Long: "launches the TUI (text user interface) view for the application", 13 | Aliases: []string{"tui"}, 14 | Run: func(cmd *cobra.Command, args []string) { 15 | 16 | ui.Execute() 17 | }, 18 | } 19 | 20 | func init() { 21 | rootCmd.AddCommand(launchCmd) 22 | } 23 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // rootCmd represents the base command when called without any subcommands 12 | var rootCmd = &cobra.Command{ 13 | Use: "bootstrap-cli", 14 | Short: "A minimalistic CLI to bootstrap projects with different frameworks", 15 | Long: GetAsciiArt() + "\nA minimalistic CLI to bootstrap projects with different frameworks.", 16 | } 17 | 18 | func Execute() { 19 | err := rootCmd.Execute() 20 | if err != nil { 21 | os.Exit(1) 22 | } 23 | } 24 | 25 | func init() { 26 | rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 27 | } 28 | 29 | func GetAsciiArt() string { 30 | 31 | var ( 32 | colorArr = [5]string{"\u001b[33m", "\u001b[33m", "\u001b[33m", "\u001b[31m", "\u001b[35m"} 33 | turnoff = "\u001b[0m\n" 34 | buf strings.Builder 35 | bytes []byte 36 | err error 37 | ) 38 | 39 | if bytes, err = os.ReadFile("asciiArt.txt"); err != nil { 40 | return "no file" 41 | } 42 | strSlice := strings.Split(string(bytes), "\n") 43 | for i, color := range colorArr { 44 | buf.WriteString(fmt.Sprintf("%s %s %s", color, strSlice[i], turnoff)) 45 | } 46 | return buf.String() 47 | 48 | } 49 | -------------------------------------------------------------------------------- /formula/bootstrap-cli.rb: -------------------------------------------------------------------------------- 1 | # typed: false 2 | # frozen_string_literal: true 3 | 4 | # This file was generated by GoReleaser. DO NOT EDIT. 5 | class BootstrapCli < Formula 6 | desc "A minimalistic CLI to bootstrap projects with different frameworks." 7 | homepage "https://github.com/wingkwong/bootstrap-cli" 8 | version "0.1.0" 9 | license "MIT" 10 | 11 | depends_on "npm" 12 | depends_on "go" 13 | 14 | on_macos do 15 | if Hardware::CPU.arm? 16 | url "https://github.com/wingkwong/bootstrap-cli/releases/download/v0.1.0/bootstrap-cli_0.1.0_macOS_arm64.tar.gz" 17 | sha256 "a789b449c77404e0d4c0ed1da8003a52af6f5be7425008c649b9f2ecc6b7b7de" 18 | 19 | def install 20 | bin.install "bootstrap-cli" 21 | end 22 | end 23 | if Hardware::CPU.intel? 24 | url "https://github.com/wingkwong/bootstrap-cli/releases/download/v0.1.0/bootstrap-cli_0.1.0_macOS_x86_64.tar.gz" 25 | sha256 "c2371cad7a0f867b1d5be39f836a88b654fae72721c324e384a9ccaa878d4497" 26 | 27 | def install 28 | bin.install "bootstrap-cli" 29 | end 30 | end 31 | end 32 | 33 | on_linux do 34 | if Hardware::CPU.intel? 35 | url "https://github.com/wingkwong/bootstrap-cli/releases/download/v0.1.0/bootstrap-cli_0.1.0_Linux_x86_64.tar.gz" 36 | sha256 "f9eeb8a4f7c2ae5e6df77eae45b717c1dfea75356f04383891508b65ca572f3d" 37 | 38 | def install 39 | bin.install "bootstrap-cli" 40 | end 41 | end 42 | end 43 | 44 | def caveats 45 | <<~EOS 46 | A minimalistic CLI to bootstrap projects with different frameworks. 47 | EOS 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wingkwong/bootstrap-cli 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/charmbracelet/bubbles v0.15.0 7 | github.com/charmbracelet/bubbletea v0.23.2 8 | github.com/charmbracelet/lipgloss v0.6.0 9 | ) 10 | 11 | require ( 12 | github.com/atotto/clipboard v0.1.4 // indirect 13 | github.com/aymanbagabas/go-osc52 v1.2.1 // indirect 14 | github.com/containerd/console v1.0.3 // indirect 15 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 16 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 17 | github.com/mattn/go-isatty v0.0.17 // indirect 18 | github.com/mattn/go-localereader v0.0.1 // indirect 19 | github.com/mattn/go-runewidth v0.0.14 // indirect 20 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect 21 | github.com/muesli/cancelreader v0.2.2 // indirect 22 | github.com/muesli/reflow v0.3.0 // indirect 23 | github.com/muesli/termenv v0.14.0 // indirect 24 | github.com/rivo/uniseg v0.2.0 // indirect 25 | github.com/sahilm/fuzzy v0.1.0 // indirect 26 | github.com/spf13/cobra v1.8.0 // indirect 27 | github.com/spf13/pflag v1.0.5 // indirect 28 | golang.org/x/sync v0.1.0 // indirect 29 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect 30 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 31 | golang.org/x/text v0.3.7 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 2 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 3 | github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= 4 | github.com/aymanbagabas/go-osc52 v1.2.1 h1:q2sWUyDcozPLcLabEMd+a+7Ea2DitxZVN9hTxab9L4E= 5 | github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= 6 | github.com/charmbracelet/bubbles v0.15.0 h1:c5vZ3woHV5W2b8YZI1q7v4ZNQaPetfHuoHzx+56Z6TI= 7 | github.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74= 8 | github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU= 9 | github.com/charmbracelet/bubbletea v0.23.2 h1:vuUJ9HJ7b/COy4I30e8xDVQ+VRDUEFykIjryPfgsdps= 10 | github.com/charmbracelet/bubbletea v0.23.2/go.mod h1:FaP3WUivcTM0xOKNmhciz60M6I+weYLF76mr1JyI7sM= 11 | github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= 12 | github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY= 13 | github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk= 14 | github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= 15 | github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= 16 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 17 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 18 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 19 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 20 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 21 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 22 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 23 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 24 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 25 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 26 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 27 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 28 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 29 | github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 30 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 31 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 32 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= 33 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 34 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= 35 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= 36 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 37 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 38 | github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= 39 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 40 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 41 | github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= 42 | github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= 43 | github.com/muesli/termenv v0.14.0 h1:8x9NFfOe8lmIWK4pgy3IfVEy47f+ppe3tUqdPZG2Uy0= 44 | github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8= 45 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 46 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 47 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 48 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 49 | github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= 50 | github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= 51 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 52 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 53 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 54 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 55 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 56 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 57 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 59 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 60 | golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= 62 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= 64 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 65 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 66 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 67 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 68 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 69 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 70 | -------------------------------------------------------------------------------- /internal/constants/backend.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | BACKEND_FRAMEWORKS = "📙 Backend Frameworks" 5 | BACKEND_FRAMEWORKS_DESC = "Explore Backend Framework Templates" 6 | BACKEND_TEMPLATE_LIST = "backend-template-list" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/constants/common.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | APP_NAME = "Bootstrap CLI" 5 | APP_DESC = "A minimalistic CLI to bootstrap projects with different frameworks" 6 | APP_REPO_URL = "https://discord.gg/hGKVsGxMY3" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/constants/docker.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | DOCKER_FRAMEWORKS = "📒 Docker" 5 | DOCKER_FRAMEWORKS_DESC = "Explore Docker Commands" 6 | DOCKER_TEMPLATE_LIST = "docker-template-list" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/constants/frontend.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | FRONTEND_FRAMEWORKS = "📘 Frontend Frameworks" 5 | FRONTEND_FRAMEWORKS_DESC = "Explore Frontend Framework Templates" 6 | FRONTEND_TEMPLATE_LIST = "frontned-template-list" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/constants/kubernetes.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | KUBERNETES_FRAMEWORKS = "📕 Kubernetes" 5 | KUBERNETES_FRAMEWORKS_DESC = "Explore Kubernetes Templates" 6 | ) 7 | -------------------------------------------------------------------------------- /internal/constants/navigation.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | NAVIGATION_TEMPLATE_LIST = "navigation-template-list" 5 | ) 6 | -------------------------------------------------------------------------------- /internal/templates/backend.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | var BACKEND_TEMPLATES = []Item{ 4 | {Id: 0, Title: "express", Desc: "Generate Express.js App Template", Command: "npx", CommandArgs: "--yes express-generator my-express-app"}, 5 | {Id: 1, Title: "koa", Desc: "Generate Koa.js App Template", Command: "npx", CommandArgs: "--yes create-koa-application my-koa-app"}, 6 | } 7 | -------------------------------------------------------------------------------- /internal/templates/docker.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | var DOCKER_TEMPLATES = []Item{ 4 | {Id: 0, Title: "mssql", Desc: "Install the SQL Server container image", Command: "docker", CommandArgs: " run -e 'ACCEPT_EULA=1' -e 'MSSQL_SA_PASSWORD=p@ssw0rd' -e 'MSSQL_PID=Developer' -e 'MSSQL_USER=SA' -p 1433:1433 -d --name=sql mcr.microsoft.com/azure-sql-edge"}, 5 | } 6 | -------------------------------------------------------------------------------- /internal/templates/frontend.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | var FRONTEND_TEMPLATES = []Item{ 4 | {Id: 0, Title: "vue", Desc: "Generate Vue.js App Template", Command: "npm", CommandArgs: "init --y vite@latest my-vue-app -- --template vue"}, 5 | {Id: 1, Title: "vue-ts", Desc: "Generate Vue.js App Template in TypeScript", Command: "npm", CommandArgs: "init --y vite@latest my-vue-typescript-app -- --template vue-ts"}, 6 | {Id: 2, Title: "react", Desc: "Generate React.js App Template", Command: "npm", CommandArgs: "init --y vite@latest my-react-app -- --template react"}, 7 | {Id: 3, Title: "react-ts", Desc: "Generate React.js App Template in TypeScript", Command: "npm", CommandArgs: "init --y vite@latest my-react-typescript-app -- --template react-ts"}, 8 | // TODO: move to prompt 9 | {Id: 4, Title: "next", Desc: "Generate Next.js App Template", Command: "npx", CommandArgs: "--yes create-next-app my-next-app --eslint --src-dir --experimental-app false --use-npm --import-alias '@/*' --js"}, 10 | {Id: 5, Title: "next-ts", Desc: "Generate Next.js App Template in TypeScript", Command: "npx", CommandArgs: "--y create-next-app my-next-typescript-app --eslint --src-dir --experimental-app false --use-npm --import-alias '@/*' --ts"}, 11 | {Id: 6, Title: "vanilla", Desc: "Generate Vanilla.js App Template", Command: "npm", CommandArgs: "init --y vite@latest my-vanilla-app -- --template vanilla"}, 12 | {Id: 7, Title: "vanilla-ts", Desc: "Generate Vanilla.js App Template in TypeScript", Command: "npm", CommandArgs: "init --y vite@latest my-vanilla-typescript-app -- --template vanilla-ts"}, 13 | {Id: 8, Title: "gatsby", Desc: "Generate Gatsby App Template", Command: "npm", CommandArgs: "init --y vite@latest my-gatsby-app -- --template gatsby"}, 14 | {Id: 9, Title: "gatsby-ts", Desc: "Generate Gatsby App Template in TypeScript", Command: "npm", CommandArgs: "init --y vite@latest my-gatsby-typescript-app -- --template gatsby-ts"}, 15 | } 16 | -------------------------------------------------------------------------------- /internal/templates/item.go: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | type Item struct { 4 | Id int 5 | Title, Desc, Command, CommandArgs string 6 | } 7 | -------------------------------------------------------------------------------- /internal/ui/init.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | tea "github.com/charmbracelet/bubbletea" 5 | ) 6 | 7 | func (b Bubble) Init() tea.Cmd { 8 | return b.spinner.Tick 9 | } 10 | -------------------------------------------------------------------------------- /internal/ui/inputs/app.go: -------------------------------------------------------------------------------- 1 | package inputs 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/charmbracelet/bubbles/textinput" 7 | ) 8 | 9 | type inputBubble struct { 10 | placeholder string 11 | echoMode textinput.EchoMode 12 | } 13 | 14 | func NewInputModel(data []inputBubble) Bubble { 15 | n := len(data) 16 | b := Bubble{ 17 | Inputs: make([]textinput.Model, n), 18 | } 19 | var t textinput.Model 20 | for i := range b.Inputs { 21 | t = textinput.New() 22 | t.CursorStyle = cursorStyle 23 | t.CharLimit = 200 24 | if i == 0 { 25 | t.Focus() 26 | t.PromptStyle = focusedStyle 27 | t.TextStyle = focusedStyle 28 | } 29 | t.Placeholder = data[i].placeholder 30 | t.EchoMode = data[i].echoMode 31 | if data[i].echoMode == 2 { 32 | t.EchoCharacter = '•' 33 | } 34 | b.Inputs[i] = t 35 | } 36 | return b 37 | } 38 | 39 | func NewViteInputModel(title string) Bubble { 40 | return NewInputModel([]inputBubble{ 41 | {placeholder: fmt.Sprintf("Enter App Name. (Default: my-%s-app)", title), echoMode: textinput.EchoNormal}, 42 | {placeholder: "Enter the directory. (Default: current directory)", echoMode: textinput.EchoNormal}, 43 | }) 44 | } 45 | 46 | func NewMSSQLInputModel() Bubble { 47 | return NewInputModel([]inputBubble{ 48 | {placeholder: "MSSQL_USER", echoMode: textinput.EchoNormal}, 49 | {placeholder: "MSSQL_SA_PASSWORD", echoMode: textinput.EchoNormal}, 50 | {placeholder: "MSSQL_PID", echoMode: textinput.EchoPassword}, 51 | {placeholder: "name", echoMode: textinput.EchoNormal}, 52 | {placeholder: "port", echoMode: textinput.EchoNormal}, 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /internal/ui/inputs/model.go: -------------------------------------------------------------------------------- 1 | package inputs 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/textinput" 5 | tea "github.com/charmbracelet/bubbletea" 6 | ) 7 | 8 | type Bubble struct { 9 | FocusIndex int 10 | Inputs []textinput.Model 11 | Finished bool 12 | Active bool 13 | } 14 | 15 | func (b Bubble) Init() tea.Cmd { 16 | return textinput.Blink 17 | } 18 | 19 | func (b Bubble) IsFinished() bool { return b.Finished } 20 | 21 | func (b Bubble) GetInputs() []textinput.Model { return b.Inputs } 22 | 23 | func (b *Bubble) SetActive(v bool) { 24 | b.Active = v 25 | } 26 | -------------------------------------------------------------------------------- /internal/ui/inputs/style.go: -------------------------------------------------------------------------------- 1 | package inputs 2 | 3 | import ( 4 | "github.com/charmbracelet/lipgloss" 5 | ) 6 | 7 | var ( 8 | focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("167")) 9 | blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) 10 | cursorStyle = focusedStyle.Copy() 11 | noStyle = lipgloss.NewStyle() 12 | helpStyle = blurredStyle.Copy() 13 | cursorModeHelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244")) 14 | 15 | submitButton = lipgloss.NewStyle().Width(10).Padding(1, 0).Align(lipgloss.Center) 16 | focusedButton = submitButton.Foreground(lipgloss.Color("255")).Background(lipgloss.Color("167")).Render("Submit") 17 | blurredButton = submitButton.Foreground(lipgloss.Color("167")).Background(lipgloss.Color("255")).Render("Submit") 18 | ) 19 | -------------------------------------------------------------------------------- /internal/ui/inputs/update.go: -------------------------------------------------------------------------------- 1 | package inputs 2 | 3 | import ( 4 | tea "github.com/charmbracelet/bubbletea" 5 | ) 6 | 7 | func (b Bubble) Update(msg tea.Msg) (Bubble, tea.Cmd) { 8 | if !b.Active { 9 | return b, nil 10 | } 11 | switch msg := msg.(type) { 12 | case tea.KeyMsg: 13 | switch msg.String() { 14 | // Set focus to next input 15 | case "tab", "shift+tab", "enter", "up", "down": 16 | s := msg.String() 17 | 18 | // Did the user press enter while the submit button was focused? 19 | // If so, exit. 20 | if s == "enter" && b.FocusIndex == len(b.Inputs) { 21 | // return b, tea.Quit 22 | b.Finished = true 23 | return b, nil 24 | } 25 | 26 | // Cycle indexes 27 | if s == "up" || s == "shift+tab" { 28 | b.FocusIndex-- 29 | } else { 30 | b.FocusIndex++ 31 | } 32 | 33 | if b.FocusIndex > len(b.Inputs) { 34 | b.FocusIndex = 0 35 | } else if b.FocusIndex < 0 { 36 | b.FocusIndex = len(b.Inputs) 37 | } 38 | 39 | cmds := make([]tea.Cmd, len(b.Inputs)) 40 | for i := 0; i <= len(b.Inputs)-1; i++ { 41 | if i == b.FocusIndex { 42 | // Set focused state 43 | cmds[i] = b.Inputs[i].Focus() 44 | b.Inputs[i].PromptStyle = focusedStyle 45 | b.Inputs[i].TextStyle = focusedStyle 46 | continue 47 | } 48 | // Remove focused state 49 | b.Inputs[i].Blur() 50 | b.Inputs[i].PromptStyle = noStyle 51 | b.Inputs[i].TextStyle = noStyle 52 | } 53 | 54 | return b, tea.Batch(cmds...) 55 | } 56 | } 57 | 58 | // Handle character input and blinking 59 | return b, b.UpdateInputs(msg) 60 | } 61 | 62 | func (b *Bubble) UpdateInputs(msg tea.Msg) tea.Cmd { 63 | cmds := make([]tea.Cmd, len(b.Inputs)) 64 | 65 | // Only text inputs with Focus() set will respond, so it's safe to simply 66 | // update all of them here without any further logic. 67 | for i := range b.Inputs { 68 | b.Inputs[i], cmds[i] = b.Inputs[i].Update(msg) 69 | } 70 | 71 | return tea.Batch(cmds...) 72 | } 73 | -------------------------------------------------------------------------------- /internal/ui/inputs/view.go: -------------------------------------------------------------------------------- 1 | package inputs 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/charmbracelet/lipgloss" 8 | ) 9 | 10 | func (b Bubble) View() string { 11 | var wrapper = lipgloss.NewStyle().Padding(0, 1) 12 | var sb strings.Builder 13 | 14 | for i := range b.Inputs { 15 | sb.WriteString(b.Inputs[i].View()) 16 | if i < len(b.Inputs)-1 { 17 | sb.WriteRune('\n') 18 | } 19 | } 20 | 21 | button := &blurredButton 22 | if b.FocusIndex == len(b.Inputs) { 23 | button = &focusedButton 24 | } 25 | fmt.Fprintf(&sb, "\n\n%s\n\n", *button) 26 | 27 | return wrapper.Render(sb.String()) 28 | } 29 | -------------------------------------------------------------------------------- /internal/ui/item.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | type Item struct { 4 | id int 5 | title, name, desc, command, commandArgs string 6 | } 7 | 8 | func (i Item) Id() int { return i.id } 9 | 10 | func (i Item) Title() string { return i.title } 11 | 12 | func (i Item) Description() string { return i.desc } 13 | 14 | func (i Item) Name() string { return i.name } 15 | 16 | func (i Item) Command() string { return i.command } 17 | 18 | func (i Item) CommandArgs() string { return i.commandArgs } 19 | 20 | func (i Item) FilterValue() string { return i.title } 21 | -------------------------------------------------------------------------------- /internal/ui/keys.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import "github.com/charmbracelet/bubbles/key" 4 | 5 | type KeyMap struct { 6 | Quit key.Binding 7 | Exit key.Binding 8 | SelectListItemKey key.Binding 9 | } 10 | 11 | func DefaultKeyMap() KeyMap { 12 | return KeyMap{ 13 | Quit: key.NewBinding( 14 | key.WithKeys("ctrl+c"), 15 | ), 16 | Exit: key.NewBinding( 17 | key.WithKeys("q"), 18 | ), 19 | SelectListItemKey: key.NewBinding( 20 | key.WithKeys("enter"), 21 | ), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/ui/list/item.go: -------------------------------------------------------------------------------- 1 | package list 2 | -------------------------------------------------------------------------------- /internal/ui/list/keys.go: -------------------------------------------------------------------------------- 1 | package list 2 | -------------------------------------------------------------------------------- /internal/ui/list/model.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | _list "github.com/charmbracelet/bubbles/list" 5 | ) 6 | 7 | type Bubble struct { 8 | List _list.Model 9 | Active bool 10 | } 11 | 12 | func New( 13 | list _list.Model, 14 | active bool, 15 | ) Bubble { 16 | 17 | list.SetShowStatusBar(false) 18 | list.SetFilteringEnabled(true) 19 | list.Styles.PaginationStyle = paginationStyle 20 | list.Styles.HelpStyle = helpStyle 21 | 22 | return Bubble{ 23 | List: list, 24 | Active: active, 25 | } 26 | } 27 | 28 | func (b Bubble) IsFiltering() bool { 29 | return b.List.FilterState() == _list.Filtering 30 | } 31 | 32 | func (b *Bubble) SetActive(v bool) { 33 | b.Active = v 34 | } 35 | 36 | func (b *Bubble) SetSize(width, height int) { 37 | b.List.SetSize( 38 | width, 39 | height, 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /internal/ui/list/style.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | ) 6 | 7 | var ( 8 | paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) 9 | helpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) 10 | ) 11 | -------------------------------------------------------------------------------- /internal/ui/list/update.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | tea "github.com/charmbracelet/bubbletea" 5 | ) 6 | 7 | func (b Bubble) Update(msg tea.Msg) (Bubble, tea.Cmd) { 8 | var ( 9 | cmd tea.Cmd 10 | cmds []tea.Cmd 11 | ) 12 | 13 | if b.Active { 14 | b.List, cmd = b.List.Update(msg) 15 | cmds = append(cmds, cmd) 16 | } 17 | 18 | return b, tea.Batch(cmds...) 19 | } 20 | -------------------------------------------------------------------------------- /internal/ui/list/view.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | func (b Bubble) View() string { 4 | return b.List.View() 5 | } 6 | -------------------------------------------------------------------------------- /internal/ui/model.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | _constants "github.com/wingkwong/bootstrap-cli/internal/constants" 7 | _templates "github.com/wingkwong/bootstrap-cli/internal/templates" 8 | _inputs "github.com/wingkwong/bootstrap-cli/internal/ui/inputs" 9 | _list "github.com/wingkwong/bootstrap-cli/internal/ui/list" 10 | ) 11 | 12 | var ( 13 | vh int 14 | vw int 15 | ) 16 | 17 | type sessionState int 18 | 19 | const ( 20 | navigationState sessionState = iota 21 | templateState 22 | installState 23 | inputState 24 | ) 25 | 26 | type Bubble struct { 27 | navigationList _list.Bubble 28 | frontendTemplateList _list.Bubble 29 | backendTemplateList _list.Bubble 30 | dockerTemplateList _list.Bubble 31 | frontendTemplateInputs []_inputs.Bubble 32 | backendTemplateInputs []_inputs.Bubble 33 | dockerTemplateInputs []_inputs.Bubble 34 | selectedInputs _inputs.Bubble 35 | frameworkType string 36 | framework string 37 | installOutput []byte 38 | installError error 39 | isInstalling bool 40 | isInputting bool 41 | spinner spinner.Model 42 | state sessionState 43 | keys KeyMap 44 | width int 45 | } 46 | 47 | func (b Bubble) GetFrameworkType() string { 48 | return b.frameworkType 49 | } 50 | 51 | func (b Bubble) GetFramework() string { 52 | return b.framework 53 | } 54 | 55 | func (b Bubble) GetSelecteNavigationItem() Item { 56 | item, ok := b.navigationList.List.SelectedItem().(Item) 57 | if ok { 58 | return item 59 | } 60 | return Item{} 61 | } 62 | 63 | func (b *Bubble) deactivateAllBubbles() { 64 | b.navigationList.SetActive(false) 65 | b.frontendTemplateList.SetActive(false) 66 | b.backendTemplateList.SetActive(false) 67 | b.dockerTemplateList.SetActive(false) 68 | for i := range b.frontendTemplateInputs { 69 | b.frontendTemplateInputs[i].SetActive(false) 70 | } 71 | 72 | for i := range b.backendTemplateInputs { 73 | b.backendTemplateInputs[i].SetActive(false) 74 | } 75 | 76 | for i := range b.dockerTemplateInputs { 77 | b.dockerTemplateInputs[i].SetActive(false) 78 | } 79 | } 80 | 81 | func (b *Bubble) resizeAllBubbles(vw int, vh int) { 82 | b.frontendTemplateList.SetSize(vw, vh) 83 | b.backendTemplateList.SetSize(vw, vh) 84 | b.dockerTemplateList.SetSize(vw, vh) 85 | } 86 | 87 | func New() Bubble { 88 | const defaultWidth = 40 89 | var navigationList list.Model 90 | var frontendTemplateList list.Model 91 | var backendTemplateList list.Model 92 | var dockerTemplateList list.Model 93 | var frontendTemplateInputs []_inputs.Bubble 94 | var backendTemplateInputs []_inputs.Bubble 95 | var dockerTemplateInputs []_inputs.Bubble 96 | var items []list.Item 97 | 98 | // navigation 99 | items = []list.Item{ 100 | Item{title: _constants.FRONTEND_FRAMEWORKS, desc: _constants.FRONTEND_FRAMEWORKS_DESC}, 101 | Item{title: _constants.BACKEND_FRAMEWORKS, desc: _constants.BACKEND_FRAMEWORKS_DESC}, 102 | // TODO: hide at this moment 103 | // Item{title: _constants.KUBERNETES_FRAMEWORKS, desc: _constants.KUBERNETES_FRAMEWORKS_DESC}, 104 | // Item{title: _constants.DOCKER_FRAMEWORKS, desc: _constants.DOCKER_FRAMEWORKS_DESC}, 105 | } 106 | listDelegate := list.NewDefaultDelegate() 107 | listDelegate.Styles.SelectedTitle = delegateStyle 108 | listDelegate.Styles.SelectedDesc = listDelegate.Styles.SelectedTitle.Copy().Bold(false) 109 | 110 | navigationList = list.New(items, listDelegate, defaultWidth, listHeight) 111 | navigationList.SetShowTitle(false) 112 | 113 | // frontend 114 | items = []list.Item{} 115 | for _, v := range _templates.FRONTEND_TEMPLATES { 116 | items = append(items, Item{ 117 | id: v.Id, 118 | title: "🔵 " + v.Title, 119 | name: v.Title, 120 | desc: v.Desc, 121 | command: v.Command, 122 | commandArgs: v.CommandArgs, 123 | }) 124 | frontendTemplateInputs = append(frontendTemplateInputs, _inputs.NewViteInputModel(v.Title)) 125 | } 126 | 127 | listDelegate.Styles.SelectedTitle = frontendDelegateStyle 128 | listDelegate.Styles.SelectedDesc = listDelegate.Styles.SelectedTitle.Copy().Bold(false) 129 | frontendTemplateList = list.New(items, listDelegate, defaultWidth, listHeight) 130 | frontendTemplateList.SetShowTitle(false) 131 | 132 | // backend 133 | items = []list.Item{} 134 | for _, v := range _templates.BACKEND_TEMPLATES { 135 | items = append(items, Item{ 136 | id: v.Id, 137 | title: "🟠 " + v.Title, 138 | name: v.Title, 139 | desc: v.Desc, 140 | command: v.Command, 141 | commandArgs: v.CommandArgs, 142 | }) 143 | backendTemplateInputs = append(backendTemplateInputs, _inputs.NewViteInputModel(v.Title)) 144 | } 145 | listDelegate.Styles.SelectedTitle = backendDelegateStyle 146 | listDelegate.Styles.SelectedDesc = listDelegate.Styles.SelectedTitle.Copy().Bold(false) 147 | backendTemplateList = list.New(items, listDelegate, defaultWidth, listHeight) 148 | backendTemplateList.SetShowTitle(false) 149 | 150 | // docker (TODO) 151 | items = []list.Item{} 152 | for _, v := range _templates.DOCKER_TEMPLATES { 153 | items = append(items, Item{ 154 | id: v.Id, 155 | title: "🟡 " + v.Title, 156 | name: v.Title, 157 | desc: v.Desc, 158 | command: v.Command, 159 | commandArgs: v.CommandArgs, 160 | }) 161 | } 162 | listDelegate.Styles.SelectedTitle = dockerDelegateStyle 163 | listDelegate.Styles.SelectedDesc = listDelegate.Styles.SelectedTitle.Copy().Bold(false) 164 | dockerTemplateList = list.New(items, listDelegate, defaultWidth, listHeight) 165 | dockerTemplateList.SetShowTitle(false) 166 | 167 | s := spinner.New() 168 | s.Spinner = spinner.Dot 169 | s.Style = spinnerStyle 170 | 171 | return Bubble{ 172 | navigationList: _list.New(navigationList, false), 173 | frontendTemplateList: _list.New(frontendTemplateList, false), 174 | backendTemplateList: _list.New(backendTemplateList, false), 175 | dockerTemplateList: _list.New(dockerTemplateList, false), 176 | frontendTemplateInputs: frontendTemplateInputs, 177 | backendTemplateInputs: backendTemplateInputs, 178 | dockerTemplateInputs: dockerTemplateInputs, 179 | spinner: s, 180 | isInstalling: false, 181 | keys: DefaultKeyMap(), 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /internal/ui/root.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | tea "github.com/charmbracelet/bubbletea" 8 | ) 9 | 10 | func Execute() { 11 | p := tea.NewProgram(New()) 12 | 13 | if _, err := p.Run(); err != nil { 14 | fmt.Println("Error running bootstrap-cli:", err) 15 | os.Exit(1) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/ui/styles.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/lipgloss" 6 | ) 7 | 8 | var ( 9 | style = lipgloss.NewStyle() 10 | headerAppNameStyle = style.Background(lipgloss.Color("167")).Bold(true).Margin(0, 0).Padding(1, 1) 11 | headerUrlStyle = style.MarginLeft(1).Foreground(lipgloss.Color("167")).Background(lipgloss.Color("#FFFFFF")).Margin(0, 0).Padding(1, 1) 12 | headerAppDescStyle = style.Foreground(lipgloss.Color("243")).Padding(1, 1, 0, 1) 13 | bubbleStyle = style.Margin(1, 1) 14 | titleStyle = style.Foreground(lipgloss.Color("#FFFDF5")).Padding(0, 1).Align(lipgloss.Center) 15 | frontendTitleStyle = style.Foreground(lipgloss.Color("#FFFDF5")).Padding(0, 1) 16 | backendTitleStyle = style.Foreground(lipgloss.Color("#FFFDF5")).Padding(0, 1) 17 | dockerTitleStyle = style.Foreground(lipgloss.Color("#FFFDF5")).Padding(0, 1) 18 | delegateStyle = list.NewDefaultDelegate().Styles.SelectedTitle.Foreground(lipgloss.Color("#ADD8E6")).BorderLeftForeground(lipgloss.Color("#FFFFE0")).Bold(true) 19 | frontendDelegateStyle = list.NewDefaultDelegate().Styles.SelectedTitle.Foreground(lipgloss.Color("#ADD8E6")).BorderLeftForeground(lipgloss.Color("#FFFFE0")).Bold(true) 20 | backendDelegateStyle = list.NewDefaultDelegate().Styles.SelectedTitle.Foreground(lipgloss.Color("#FFA500")).BorderLeftForeground(lipgloss.Color("#FFA500")).Bold(true) 21 | dockerDelegateStyle = list.NewDefaultDelegate().Styles.SelectedTitle.Foreground(lipgloss.Color("#FDFD96")).BorderLeftForeground(lipgloss.Color("#FDFD96")).Bold(true) 22 | quitTextStyle = style.Margin(1, 0, 2, 4) 23 | spinnerStyle = style.Foreground(lipgloss.Color("205")) 24 | listHeight = 20 25 | ) 26 | -------------------------------------------------------------------------------- /internal/ui/update.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os/exec" 7 | "strings" 8 | 9 | "github.com/charmbracelet/bubbles/key" 10 | tea "github.com/charmbracelet/bubbletea" 11 | "github.com/charmbracelet/lipgloss" 12 | _constants "github.com/wingkwong/bootstrap-cli/internal/constants" 13 | _inputs "github.com/wingkwong/bootstrap-cli/internal/ui/inputs" 14 | _list "github.com/wingkwong/bootstrap-cli/internal/ui/list" 15 | ) 16 | 17 | type installFinishedMsg struct { 18 | err error 19 | out bytes.Buffer 20 | } 21 | 22 | func (b *Bubble) getTemplateList() _list.Bubble { 23 | if b.frameworkType == _constants.FRONTEND_FRAMEWORKS { 24 | return b.frontendTemplateList 25 | } else if b.frameworkType == _constants.BACKEND_FRAMEWORKS { 26 | return b.backendTemplateList 27 | } else if b.frameworkType == _constants.DOCKER_FRAMEWORKS { 28 | return b.dockerTemplateList 29 | } 30 | return b.navigationList 31 | } 32 | 33 | func (b Bubble) getTemplateInputs(id int) _inputs.Bubble { 34 | if b.frameworkType == _constants.FRONTEND_FRAMEWORKS { 35 | return b.frontendTemplateInputs[id] 36 | } else if b.frameworkType == _constants.BACKEND_FRAMEWORKS { 37 | return b.backendTemplateInputs[id] 38 | } else if b.frameworkType == _constants.DOCKER_FRAMEWORKS { 39 | return b.dockerTemplateInputs[id] 40 | } 41 | return _inputs.Bubble{} 42 | } 43 | 44 | func (b Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 45 | var cmd tea.Cmd 46 | var cmds []tea.Cmd 47 | 48 | switch msg := msg.(type) { 49 | case installFinishedMsg: 50 | b.installOutput = msg.out.Bytes() 51 | b.isInstalling = false 52 | if msg.err != nil { 53 | b.installError = msg.err 54 | return b, tea.Quit 55 | } 56 | case tea.WindowSizeMsg: 57 | h, w := lipgloss.NewStyle().GetFrameSize() 58 | vh = msg.Height - h - 20 59 | vw = msg.Width - w 60 | b.resizeAllBubbles(vw, vh) 61 | b.width = vw 62 | case tea.KeyMsg: 63 | templateList := b.getTemplateList() 64 | switch { 65 | case key.Matches(msg, b.keys.Quit): 66 | return b, tea.Quit 67 | case key.Matches(msg, b.keys.Exit): 68 | if !templateList.IsFiltering() && !b.isInputting { 69 | return b, tea.Quit 70 | } 71 | case key.Matches(msg, b.keys.SelectListItemKey): 72 | if templateList.IsFiltering() { 73 | return b, nil 74 | } 75 | var item Item 76 | var ok bool 77 | if b.state == navigationState { 78 | item, ok := b.navigationList.List.SelectedItem().(Item) 79 | if ok { 80 | b.frameworkType = item.title 81 | b.state = templateState 82 | } 83 | } else if b.state == templateState { 84 | item, ok = templateList.List.SelectedItem().(Item) 85 | if ok { 86 | b.isInputting = true 87 | b.selectedInputs = b.getTemplateInputs(item.id) 88 | b.framework = item.name 89 | b.state = inputState 90 | b.deactivateAllBubbles() 91 | b.selectedInputs.SetActive(true) 92 | return b, cmd 93 | } 94 | } else if b.state == inputState { 95 | if b.selectedInputs.IsFinished() { 96 | b.isInputting = false 97 | item, ok = templateList.List.SelectedItem().(Item) 98 | if ok { 99 | var cmdArgs = item.commandArgs 100 | // set app name 101 | if b.selectedInputs.Inputs[0].Value() != "" { 102 | cmdArgs = strings.Replace( 103 | item.commandArgs, 104 | fmt.Sprintf("my-%s-app", item.name), 105 | b.selectedInputs.Inputs[0].Value(), 106 | 1) 107 | } 108 | 109 | b.state = installState 110 | b.isInstalling = true 111 | var args = strings.Split(cmdArgs, " ") 112 | c := exec.Command(item.command, args...) 113 | 114 | // set directory 115 | if b.selectedInputs.Inputs[1].Value() != "" { 116 | c.Dir = b.selectedInputs.Inputs[1].Value() 117 | } 118 | 119 | var out bytes.Buffer 120 | c.Stdout = &out 121 | return b, tea.ExecProcess(c, func(err error) tea.Msg { 122 | return installFinishedMsg{err, out} 123 | }) 124 | } 125 | } 126 | } 127 | } 128 | default: 129 | var cmd tea.Cmd 130 | b.spinner, cmd = b.spinner.Update(msg) 131 | return b, cmd 132 | } 133 | 134 | if b.state == navigationState { 135 | b.deactivateAllBubbles() 136 | b.navigationList.SetActive(true) 137 | b.navigationList.List, cmd = b.navigationList.List.Update(msg) 138 | cmds = append(cmds, cmd) 139 | } else if b.state == templateState { 140 | b.deactivateAllBubbles() 141 | if b.frameworkType == _constants.FRONTEND_FRAMEWORKS { 142 | b.frontendTemplateList.List, cmd = b.frontendTemplateList.List.Update(msg) 143 | cmds = append(cmds, cmd) 144 | } else if b.frameworkType == _constants.BACKEND_FRAMEWORKS { 145 | b.backendTemplateList.List, cmd = b.backendTemplateList.List.Update(msg) 146 | cmds = append(cmds, cmd) 147 | } else if b.frameworkType == _constants.DOCKER_FRAMEWORKS { 148 | b.dockerTemplateList.List, cmd = b.dockerTemplateList.List.Update(msg) 149 | cmds = append(cmds, cmd) 150 | } 151 | } 152 | 153 | b.navigationList, cmd = b.navigationList.Update(msg) 154 | cmds = append(cmds, cmd) 155 | 156 | b.frontendTemplateList, cmd = b.frontendTemplateList.Update(msg) 157 | cmds = append(cmds, cmd) 158 | 159 | b.backendTemplateList, cmd = b.backendTemplateList.Update(msg) 160 | cmds = append(cmds, cmd) 161 | 162 | b.dockerTemplateList, cmd = b.dockerTemplateList.Update(msg) 163 | cmds = append(cmds, cmd) 164 | 165 | b.selectedInputs, cmd = b.selectedInputs.Update(msg) 166 | cmds = append(cmds, cmd) 167 | 168 | return b, tea.Batch(cmds...) 169 | } 170 | -------------------------------------------------------------------------------- /internal/ui/view.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/charmbracelet/lipgloss" 7 | _constants "github.com/wingkwong/bootstrap-cli/internal/constants" 8 | ) 9 | 10 | func (b Bubble) View() string { 11 | // header 12 | headerAppName := headerAppNameStyle.Render(_constants.APP_NAME) 13 | headerDesc := headerAppDescStyle.Render(_constants.APP_DESC) 14 | mxWidth := lipgloss.Width(headerDesc) 15 | if b.width > mxWidth { 16 | mxWidth = b.width 17 | } 18 | wrapper := lipgloss.NewStyle().Width(mxWidth) 19 | headerUrl := headerUrlStyle.Copy().Width(b.width - lipgloss.Width(headerAppName)).Align(lipgloss.Right).Render(_constants.APP_REPO_URL) 20 | var view = style.Render(lipgloss.JoinVertical(lipgloss.Top, 21 | wrapper.Render(style.Render( 22 | lipgloss.JoinHorizontal( 23 | lipgloss.Left, 24 | headerAppName, 25 | headerUrl))), 26 | wrapper.Render(headerDesc))) 27 | 28 | view += "\n\n" 29 | // content 30 | if b.state == navigationState { 31 | view += b.navigationList.View() 32 | } else if b.state == templateState { 33 | if b.frameworkType == _constants.FRONTEND_FRAMEWORKS { 34 | view += b.frontendTemplateList.View() 35 | } else if b.frameworkType == _constants.BACKEND_FRAMEWORKS { 36 | view += b.backendTemplateList.View() 37 | } else if b.frameworkType == _constants.DOCKER_FRAMEWORKS { 38 | view += b.dockerTemplateList.View() 39 | } 40 | } else if b.state == installState { 41 | if b.installError != nil { 42 | view += "Error: " + b.installError.Error() + "\n" 43 | } else if b.isInstalling { 44 | view += fmt.Sprintf("%s Installing ... ", b.spinner.View()) 45 | } else if b.installOutput != nil { 46 | view += fmt.Sprintf("%s \n 🚀 %s %s", b.installOutput, b.framework, "has been installed. Press `Enter` to quit. ") 47 | } 48 | } else if b.state == inputState { 49 | view += b.selectedInputs.View() 50 | } 51 | return wrapper.Render(view) 52 | } 53 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/wingkwong/bootstrap-cli/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | --------------------------------------------------------------------------------