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.
|
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`
|
### `setup`
|
||||||
|
|
||||||
Provision a remote PocketBase server. This will:
|
Provision a remote PocketBase server. This will:
|
||||||
|
|
||||||
1. Setup up a firewall.
|
1. Setup up a firewall.
|
||||||
2. Install and setup Caddy.
|
2. Install and setup Caddy.
|
||||||
3. Download pocketbase and configure the `.env`.
|
3. Download pocketbase and configure remote secrets.
|
||||||
4. Configure the systemd service for the remote instance.
|
4. Configure systemd.
|
||||||
|
|
||||||
### `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.
|
|
||||||
|
|
||||||
### `logs`
|
### `logs`
|
||||||
|
|
||||||
Connects to the configured server and streams `/root/pb/{service}/{service}.log` via `tail -n 100 -F`.
|
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 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.
|
||||||
|
|||||||
187
main.go
187
main.go
@@ -25,7 +25,9 @@ import (
|
|||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/mattn/go-runewidth"
|
||||||
"github.com/pelletier/go-toml/v2"
|
"github.com/pelletier/go-toml/v2"
|
||||||
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -314,9 +316,8 @@ func generateServiceName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func promptServiceName(defaultName string) (string, error) {
|
func promptServiceName(defaultName string) (string, error) {
|
||||||
reader := bufio.NewReader(os.Stdin)
|
prompt := fmt.Sprintf("%sService name%s [%s]: ", headerColor, remoteColorReset, defaultName)
|
||||||
fmt.Printf("%sService name%s [%s]: ", headerColor, remoteColorReset, defaultName)
|
input, err := readEditableLine(prompt)
|
||||||
input, err := reader.ReadString('\n')
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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")
|
fmt.Printf("Confirm remote server settings (press Enter to keep the current value):\n")
|
||||||
reader := bufio.NewReader(os.Stdin)
|
|
||||||
|
|
||||||
ipDefault := cfg.Server.IP
|
ipDefault := cfg.Server.IP
|
||||||
if ipDefault == "" {
|
if ipDefault == "" {
|
||||||
ipDefault = "127.0.0.1"
|
ipDefault = "127.0.0.1"
|
||||||
}
|
}
|
||||||
ip, err := promptWithDefault(reader, "Server IP", ipDefault)
|
ip, err := promptWithDefault("Server IP", ipDefault)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -383,7 +383,7 @@ func confirmServerConfig(pbPath string) error {
|
|||||||
if portDefault <= 0 {
|
if portDefault <= 0 {
|
||||||
portDefault = 8090
|
portDefault = 8090
|
||||||
}
|
}
|
||||||
port, err := promptPort(reader, "Server port", portDefault)
|
port, err := promptPort("Server port", portDefault)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -392,7 +392,7 @@ func confirmServerConfig(pbPath string) error {
|
|||||||
if domainDefault == "" {
|
if domainDefault == "" {
|
||||||
domainDefault = "example.com"
|
domainDefault = "example.com"
|
||||||
}
|
}
|
||||||
domain, err := promptWithDefault(reader, "Server domain", domainDefault)
|
domain, err := promptWithDefault("Server domain", domainDefault)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -407,15 +407,15 @@ func confirmServerConfig(pbPath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func promptWithDefault(reader *bufio.Reader, label, defaultValue string) (string, error) {
|
func promptWithDefault(label, defaultValue string) (string, error) {
|
||||||
fmt.Printf("%s%s%s", headerColor, label, remoteColorReset)
|
var suffix string
|
||||||
if defaultValue != "" {
|
if defaultValue != "" {
|
||||||
fmt.Printf(" [%s]: ", defaultValue)
|
suffix = fmt.Sprintf(" [%s]: ", defaultValue)
|
||||||
} else {
|
} else {
|
||||||
fmt.Print(": ")
|
suffix = ": "
|
||||||
}
|
}
|
||||||
|
prompt := fmt.Sprintf("%s%s%s%s", headerColor, label, remoteColorReset, suffix)
|
||||||
input, err := reader.ReadString('\n')
|
input, err := readEditableLine(prompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -427,14 +427,14 @@ func promptWithDefault(reader *bufio.Reader, label, defaultValue string) (string
|
|||||||
return input, nil
|
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 {
|
if defaultValue <= 0 {
|
||||||
defaultValue = 8090
|
defaultValue = 8090
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
fmt.Printf("%s%s%s [%d]: ", headerColor, label, remoteColorReset, defaultValue)
|
prompt := fmt.Sprintf("%s%s%s [%d]: ", headerColor, label, remoteColorReset, defaultValue)
|
||||||
input, err := reader.ReadString('\n')
|
input, err := readEditableLine(prompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -463,31 +463,20 @@ func normalizePromptInput(raw string) string {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if r == 0x1b {
|
if r == 0x1b {
|
||||||
nextIndex := i + size
|
nextIndex, final := skipEscapeSequence(raw, i)
|
||||||
if nextIndex >= len(raw) {
|
if nextIndex <= i {
|
||||||
break
|
i++
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
next, nextSize := utf8.DecodeRuneInString(raw[nextIndex:])
|
i = nextIndex
|
||||||
if next == 0x7f || next == 0x08 {
|
if final == 0x7f || final == 0x08 {
|
||||||
buf = deletePreviousWord(buf)
|
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
|
continue
|
||||||
}
|
}
|
||||||
i = nextIndex + nextSize
|
if r == 0x17 {
|
||||||
|
buf = deletePreviousWord(buf)
|
||||||
|
i += size
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if r == 0x7f || r == 0x08 {
|
if r == 0x7f || r == 0x08 {
|
||||||
@@ -503,6 +492,132 @@ func normalizePromptInput(raw string) string {
|
|||||||
return string(buf)
|
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 {
|
func deletePreviousWord(buf []rune) []rune {
|
||||||
i := len(buf)
|
i := len(buf)
|
||||||
for i > 0 && unicode.IsSpace(buf[i-1]) {
|
for i > 0 && unicode.IsSpace(buf[i-1]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user