setup input
This commit is contained in:
20
README.md
20
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.
|
||||
|
||||
191
main.go
191
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]) {
|
||||
|
||||
Reference in New Issue
Block a user