diff --git a/README.md b/README.md index 1af5b2d..46acee7 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ A simple, rsync-based PocketBase deployment tool. - start a new PocketBase project +## dev + +- run a local dev server + ## deploy ## logs diff --git a/go.mod b/go.mod index f4accab..0b53995 100644 --- a/go.mod +++ b/go.mod @@ -2,10 +2,22 @@ module pb go 1.23.1 -require github.com/urfave/cli/v2 v2.27.7 +require github.com/charmbracelet/bubbletea v0.26.1 require ( - github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.4.6 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/term v0.19.0 // indirect + golang.org/x/text v0.3.8 // indirect ) diff --git a/go.sum b/go.sum index f749703..76b7b79 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,37 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= -github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= -github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbletea v0.26.1 h1:xujcQeF73rh4jwu3+zhfQsvV18x+7zIjlw7/CYbzGJ0= +github.com/charmbracelet/bubbletea v0.26.1/go.mod h1:FzKr7sKoO8iFVcdIBM9J0sJOcQv5nDQaYwsee3kpbgo= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg= +github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= diff --git a/main.go b/main.go index 6c82bdb..c6a7bfb 100644 --- a/main.go +++ b/main.go @@ -16,120 +16,222 @@ import ( "strings" "time" - "github.com/urfave/cli/v2" + tea "github.com/charmbracelet/bubbletea" ) func main() { - app := &cli.App{ - Name: "pb", - Usage: "PocketBase deployment helper", - Commands: []*cli.Command{ - { - Name: "init", - Usage: "start a new PocketBase project", - Action: initAction(), - }, - { - Name: "dev", - Usage: "run the PocketBase binary locally", - Action: devAction(), - }, - { - Name: "deploy", - Usage: "deploy the PocketBase project", - Action: placeholderAction("deploy"), - }, - { - Name: "logs", - Usage: "show PocketBase logs", - Action: placeholderAction("logs"), - }, - { - Name: "secrets", - Usage: "manage deployment secrets", - Action: placeholderAction("secrets"), - }, - }, + commands := defaultCommands() + args := os.Args[1:] + if len(args) > 0 { + if args[0] == "--" { + args = args[1:] + } + if len(args) > 0 { + if err := runCommandByName(commands, args[0]); err != nil { + log.Fatal(err) + } + return + } } - if err := app.Run(os.Args); err != nil { + if err := tea.NewProgram(newModel()).Start(); err != nil { log.Fatal(err) } } -func placeholderAction(name string) func(*cli.Context) error { - return func(c *cli.Context) error { +func defaultCommands() []command { + return []command{ + {name: "init", description: "start a new PocketBase project", action: runInit}, + {name: "dev", description: "run the PocketBase binary locally", action: runDev}, + {name: "deploy", description: "deploy the PocketBase project", action: placeholderAction("deploy")}, + {name: "logs", description: "show PocketBase logs", action: placeholderAction("logs")}, + {name: "secrets", description: "manage deployment secrets", action: placeholderAction("secrets")}, + } +} + +type commandAction func() error + +type command struct { + name string + description string + action commandAction +} + +func runCommandByName(commands []command, name string) error { + for _, cmd := range commands { + if cmd.name == name { + return cmd.action() + } + } + return fmt.Errorf("unknown command %q", name) +} + +func newModel() model { + return model{commands: defaultCommands()} +} + +type model struct { + commands []command + cursor int + status string + running bool + lastCommand string +} + +type commandResultMsg struct { + name string + err error +} + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q": + return m, tea.Quit + case "up", "k": + if m.running { + return m, nil + } + if m.cursor > 0 { + m.cursor-- + } else { + m.cursor = len(m.commands) - 1 + } + return m, nil + case "down", "j": + if m.running { + return m, nil + } + if m.cursor < len(m.commands)-1 { + m.cursor++ + } else { + m.cursor = 0 + } + return m, nil + case "enter": + if m.running { + return m, nil + } + selected := m.commands[m.cursor] + m.running = true + m.lastCommand = selected.name + m.status = fmt.Sprintf("Running %s...", selected.name) + return m, runCommand(selected) + } + case commandResultMsg: + m.running = false + if msg.err != nil { + m.status = fmt.Sprintf("%s failed: %v", msg.name, msg.err) + } else { + m.status = fmt.Sprintf("%s finished successfully", msg.name) + } + return m, nil + } + return m, nil +} + +func (m model) View() string { + var b strings.Builder + b.WriteString("PocketBase deployment helper\n\n") + for idx, cmd := range m.commands { + cursor := " " + if m.cursor == idx { + cursor = ">" + } + b.WriteString(fmt.Sprintf("%s %s\n %s\n", cursor, cmd.name, cmd.description)) + } + b.WriteString("\n") + if m.status != "" { + b.WriteString(fmt.Sprintf("%s\n\n", m.status)) + } + if m.running { + b.WriteString("(running... press ctrl+c to abort)\n") + } + b.WriteString("↑/↓ to navigate, enter to run, q to quit\n") + return b.String() +} + +func runCommand(cmd command) tea.Cmd { + return func() tea.Msg { + return commandResultMsg{name: cmd.name, err: cmd.action()} + } +} + +func placeholderAction(name string) commandAction { + return func() error { fmt.Printf("TODO: implement %s command\n", name) return nil } } -func devAction() func(*cli.Context) error { - return func(c *cli.Context) error { - cwd, err := os.Getwd() - if err != nil { - return err - } - - binaryPath := filepath.Join(cwd, "bin", pocketbaseBinaryName(runtime.GOOS)) - if _, err := os.Stat(binaryPath); err != nil { - return fmt.Errorf("cannot find PocketBase binary at %s: %w", binaryPath, err) - } - - overrides, err := loadEnv(filepath.Join(cwd, ".env")) - if err != nil { - return err - } - - cmd := exec.Command(binaryPath, "serve") - cmd.Dir = cwd - cmd.Env = mergeEnv(overrides) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - - fmt.Println("starting PocketBase development server") - return cmd.Run() +func runInit() error { + cwd, err := os.Getwd() + if err != nil { + return err } + + pbPath := filepath.Join(cwd, "pb.toml") + if _, err := os.Stat(pbPath); err == nil { + return fmt.Errorf("pb.toml already exists, aborting") + } else if !errors.Is(err, os.ErrNotExist) { + return err + } + + serviceName := generateServiceName() + + if err := writePBConfig(pbPath, serviceName); err != nil { + return err + } + + targetDir := filepath.Join(cwd, "bin") + if err := os.MkdirAll(targetDir, 0o755); err != nil { + return err + } + + binaryPath := filepath.Join(targetDir, pocketbaseBinaryName(runtime.GOOS)) + if err := downloadPocketbase(defaultPocketbaseVersion, runtime.GOOS, runtime.GOARCH, binaryPath); err != nil { + return err + } + + if err := ensureGitignoreEntries(filepath.Join(cwd, ".gitignore"), []string{"bin/pocketbase", "pb_data"}); err != nil { + return err + } + + fmt.Printf("Initialized PocketBase project %q\n", serviceName) + return nil } -func initAction() func(*cli.Context) error { - return func(c *cli.Context) error { - cwd, err := os.Getwd() - if err != nil { - return err - } - - pbPath := filepath.Join(cwd, "pb.toml") - if _, err := os.Stat(pbPath); err == nil { - return fmt.Errorf("pb.toml already exists, aborting") - } else if !errors.Is(err, os.ErrNotExist) { - return err - } - - serviceName := generateServiceName() - - if err := writePBConfig(pbPath, serviceName); err != nil { - return err - } - - targetDir := filepath.Join(cwd, "bin") - if err := os.MkdirAll(targetDir, 0o755); err != nil { - return err - } - - binaryPath := filepath.Join(targetDir, pocketbaseBinaryName(runtime.GOOS)) - if err := downloadPocketbase(defaultPocketbaseVersion, runtime.GOOS, runtime.GOARCH, binaryPath); err != nil { - return err - } - - if err := ensureGitignoreEntries(filepath.Join(cwd, ".gitignore"), []string{"bin/", "pb.toml"}); err != nil { - return err - } - - fmt.Printf("Initialized PocketBase project %q\n", serviceName) - return nil +func runDev() error { + cwd, err := os.Getwd() + if err != nil { + return err } + + binaryPath := filepath.Join(cwd, "bin", pocketbaseBinaryName(runtime.GOOS)) + if _, err := os.Stat(binaryPath); err != nil { + return fmt.Errorf("cannot find PocketBase binary at %s: %w", binaryPath, err) + } + + overrides, err := loadEnv(filepath.Join(cwd, ".env")) + if err != nil { + return err + } + + cmd := exec.Command(binaryPath, "serve") + cmd.Args = append(cmd.Args, "--dir", filepath.Join(cwd, "pb_data")) + cmd.Dir = cwd + cmd.Env = mergeEnv(overrides) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + + fmt.Println("starting PocketBase development server") + return cmd.Run() } const defaultPocketbaseVersion = "0.35.1"