diff --git a/README.md b/README.md index 2986859..c09000f 100644 --- a/README.md +++ b/README.md @@ -12,27 +12,27 @@ Start a new PocketBase project (optionally provide a service name via `pb init < Run the local dev server. +### `deploy` + +Syncs `pb_public`, `pb_migrations`, and `pb_hooks`, then restarts the remote PocketBase service. The command will automatically run `setup` if the PocketBase binary isn’t present on the remote. + ### `setup` Provision a remote PocketBase server. This will: 1. Setup up a firewall. 2. Install and setup Caddy. -3. Download pocketbase and configure the `.env`. -4. Configure the systemd service for the remote instance. - -### `deploy` - -Syncs `pb_public`, `pb_migrations`, and `pb_hooks`, then restarts the remote PocketBase service. The command will automatically run `setup` if the PocketBase binary isn’t present on the remote. +3. Download pocketbase and configure remote secrets. +4. Configure systemd. ### `logs` Connects to the configured server and streams `/root/pb/{service}/{service}.log` via `tail -n 100 -F`. -### secrets +### `secrets` -Manage the remote `/root/pb/{service}/.env` file. +Manage the remote secrets in `/root/pb/{service}/.env`. -- `pb secrets list` prints every variable name defined in the remote `.env` (comments and empty lines are ignored). +- `pb secrets list` prints every variable name (comments and empty lines are ignored). - `pb secrets set KEY=VALUE [...]` adds or updates one or more key/value pairs while leaving the other file entries untouched. -- `pb secrets delete KEY [...]` removes the named entries from the remote `.env`. +- `pb secrets delete KEY [...]` removes the named entries. diff --git a/main.go b/main.go index 8d5bfc7..40203b4 100644 --- a/main.go +++ b/main.go @@ -25,7 +25,9 @@ import ( "unicode/utf8" tea "github.com/charmbracelet/bubbletea" + "github.com/mattn/go-runewidth" "github.com/pelletier/go-toml/v2" + "golang.org/x/term" ) var ( @@ -314,9 +316,8 @@ func generateServiceName() string { } func promptServiceName(defaultName string) (string, error) { - reader := bufio.NewReader(os.Stdin) - fmt.Printf("%sService name%s [%s]: ", headerColor, remoteColorReset, defaultName) - input, err := reader.ReadString('\n') + prompt := fmt.Sprintf("%sService name%s [%s]: ", headerColor, remoteColorReset, defaultName) + input, err := readEditableLine(prompt) if err != nil { return "", err } @@ -368,13 +369,12 @@ func confirmServerConfig(pbPath string) error { } fmt.Printf("Confirm remote server settings (press Enter to keep the current value):\n") - reader := bufio.NewReader(os.Stdin) ipDefault := cfg.Server.IP if ipDefault == "" { ipDefault = "127.0.0.1" } - ip, err := promptWithDefault(reader, "Server IP", ipDefault) + ip, err := promptWithDefault("Server IP", ipDefault) if err != nil { return err } @@ -383,7 +383,7 @@ func confirmServerConfig(pbPath string) error { if portDefault <= 0 { portDefault = 8090 } - port, err := promptPort(reader, "Server port", portDefault) + port, err := promptPort("Server port", portDefault) if err != nil { return err } @@ -392,7 +392,7 @@ func confirmServerConfig(pbPath string) error { if domainDefault == "" { domainDefault = "example.com" } - domain, err := promptWithDefault(reader, "Server domain", domainDefault) + domain, err := promptWithDefault("Server domain", domainDefault) if err != nil { return err } @@ -407,15 +407,15 @@ func confirmServerConfig(pbPath string) error { return nil } -func promptWithDefault(reader *bufio.Reader, label, defaultValue string) (string, error) { - fmt.Printf("%s%s%s", headerColor, label, remoteColorReset) +func promptWithDefault(label, defaultValue string) (string, error) { + var suffix string if defaultValue != "" { - fmt.Printf(" [%s]: ", defaultValue) + suffix = fmt.Sprintf(" [%s]: ", defaultValue) } else { - fmt.Print(": ") + suffix = ": " } - - input, err := reader.ReadString('\n') + prompt := fmt.Sprintf("%s%s%s%s", headerColor, label, remoteColorReset, suffix) + input, err := readEditableLine(prompt) if err != nil { return "", err } @@ -427,14 +427,14 @@ func promptWithDefault(reader *bufio.Reader, label, defaultValue string) (string return input, nil } -func promptPort(reader *bufio.Reader, label string, defaultValue int) (int, error) { +func promptPort(label string, defaultValue int) (int, error) { if defaultValue <= 0 { defaultValue = 8090 } for { - fmt.Printf("%s%s%s [%d]: ", headerColor, label, remoteColorReset, defaultValue) - input, err := reader.ReadString('\n') + prompt := fmt.Sprintf("%s%s%s [%d]: ", headerColor, label, remoteColorReset, defaultValue) + input, err := readEditableLine(prompt) if err != nil { return 0, err } @@ -463,31 +463,20 @@ func normalizePromptInput(raw string) string { break } if r == 0x1b { - nextIndex := i + size - if nextIndex >= len(raw) { - break + nextIndex, final := skipEscapeSequence(raw, i) + if nextIndex <= i { + i++ + continue } - next, nextSize := utf8.DecodeRuneInString(raw[nextIndex:]) - if next == 0x7f || next == 0x08 { + i = nextIndex + if final == 0x7f || final == 0x08 { buf = deletePreviousWord(buf) - i = nextIndex + nextSize - continue } - if next == '[' || next == 'O' { - i = nextIndex + nextSize - for i < len(raw) { - code, codeSize := utf8.DecodeRuneInString(raw[i:]) - if codeSize == 0 { - break - } - i += codeSize - if ('A' <= code && code <= 'Z') || ('a' <= code && code <= 'z') { - break - } - } - continue - } - i = nextIndex + nextSize + continue + } + if r == 0x17 { + buf = deletePreviousWord(buf) + i += size continue } if r == 0x7f || r == 0x08 { @@ -503,6 +492,132 @@ func normalizePromptInput(raw string) string { return string(buf) } +func readEditableLine(prompt string) (string, error) { + fd := int(os.Stdin.Fd()) + reader := bufio.NewReader(os.Stdin) + if !term.IsTerminal(fd) { + line, err := reader.ReadString('\n') + if err != nil { + return "", err + } + line = strings.TrimRight(line, "\r\n") + return line, nil + } + + oldState, err := term.MakeRaw(fd) + if err != nil { + return "", err + } + defer term.Restore(fd, oldState) + + var buf []rune + var lastWidth int + fmt.Print(prompt) + + for { + r, _, err := reader.ReadRune() + if err != nil { + return "", err + } + + switch { + case r == '\r' || r == '\n': + fmt.Println() + return string(buf), nil + case r == 0x03: + fmt.Println() + return "", errors.New("input interrupted") + case r == 0x04: + fmt.Println() + return "", io.EOF + case r == 0x7f || r == 0x08: + if len(buf) > 0 { + buf = buf[:len(buf)-1] + redrawEditableLine(prompt, buf, &lastWidth) + } + case r == 0x17: + buf = deletePreviousWord(buf) + redrawEditableLine(prompt, buf, &lastWidth) + case r == 0x1b: + final, err := readEscapeFinal(reader) + if err != nil { + return "", err + } + if final == 0x7f || final == 0x08 { + buf = deletePreviousWord(buf) + redrawEditableLine(prompt, buf, &lastWidth) + } + default: + if r < 0x20 { + continue + } + buf = append(buf, r) + redrawEditableLine(prompt, buf, &lastWidth) + } + } +} + +func readEscapeFinal(reader *bufio.Reader) (rune, error) { + b, err := reader.ReadByte() + if err != nil { + return 0, err + } + if b == '[' || b == 'O' { + for { + c, err := reader.ReadByte() + if err != nil { + return 0, err + } + if isCSIFinalByte(rune(c)) { + return rune(c), nil + } + } + } + return rune(b), nil +} + +func redrawEditableLine(prompt string, buf []rune, lastWidth *int) { + fmt.Print("\r") + fmt.Print(prompt) + text := string(buf) + fmt.Print(text) + width := runewidth.StringWidth(text) + if *lastWidth > width { + fmt.Print(strings.Repeat(" ", *lastWidth-width)) + fmt.Print("\r") + fmt.Print(prompt) + fmt.Print(text) + } + *lastWidth = width +} + +func skipEscapeSequence(raw string, escIndex int) (int, rune) { + i := escIndex + 1 + if i >= len(raw) { + return i, 0 + } + next, size := utf8.DecodeRuneInString(raw[i:]) + i += size + if next == '[' || next == 'O' { + for i < len(raw) { + code, codeSize := utf8.DecodeRuneInString(raw[i:]) + if codeSize == 0 { + break + } + i += codeSize + if isCSIFinalByte(code) { + return i, code + } + } + return i, 0 + } + return i, next +} + +func isCSIFinalByte(r rune) bool { + return 0x40 <= r && r <= 0x7e +} + func deletePreviousWord(buf []rune) []rune { i := len(buf) for i > 0 && unicode.IsSpace(buf[i-1]) {