├── .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 |
4 |
5 | Bootstrap CLI
6 |
7 | A minimalistic CLI to bootstrap projects with different frameworks.
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 
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 |
--------------------------------------------------------------------------------