commit f9046d63f7f67ec72a59ad35aca79491d08752c8 Author: Nick Goodall Date: Tue Jan 13 11:07:51 2026 +0000 init command diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2e0a30 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +## Generated binaries +pb + +## Build artifacts and caches +.cache/ +# bin/ +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out + +## Go build specific +/vendor/ + +## Editor temporary files +*.swp +*.swo +.vscode/ +.idea/ + +## macOS +.DS_Store + diff --git a/README.md b/README.md new file mode 100644 index 0000000..1af5b2d --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# pb + +A simple, rsync-based PocketBase deployment tool. + +## init + +- start a new PocketBase project + +## deploy + +## logs + +## secrets diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f4accab --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module pb + +go 1.23.1 + +require github.com/urfave/cli/v2 v2.27.7 + +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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f749703 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..534a017 --- /dev/null +++ b/main.go @@ -0,0 +1,269 @@ +package main + +import ( + "archive/zip" + "errors" + "fmt" + "io" + "log" + "math/rand" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/urfave/cli/v2" +) + +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: "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"), + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} + +func placeholderAction(name string) func(*cli.Context) error { + return func(c *cli.Context) error { + fmt.Printf("TODO: implement %s command\n", name) + 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/pocketbase", "pb.toml"}); err != nil { + return err + } + + fmt.Printf("Initialized PocketBase project %q\n", serviceName) + return nil + } +} + +const defaultPocketbaseVersion = "0.35.1" + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +func writePBConfig(path, serviceName string) error { + const tmpl = `[server] +ip = "127.0.0.1" +port = 8090 +domain = "example.com" + +[pocketbase] +version = "%s" +service_name = "%s" +` + return os.WriteFile(path, []byte(fmt.Sprintf(tmpl, defaultPocketbaseVersion, serviceName)), 0o644) +} + +func generateServiceName() string { + adjectives := []string{"sunny", "silent", "cobalt", "rusty", "plush", "brisk", "sage", "glow"} + nouns := []string{"harbor", "delta", "bloom", "atlas", "ripple", "ember", "lumen", "orbit"} + adjective := adjectives[rand.Intn(len(adjectives))] + noun := nouns[rand.Intn(len(nouns))] + return fmt.Sprintf("%s-%s", adjective, noun) +} + +func pocketbaseBinaryName(goos string) string { + if goos == "windows" { + return "pocketbase.exe" + } + return "pocketbase" +} + +func downloadPocketbase(version, goos, goarch, dest string) error { + url, binaryName, err := releaseAssetURL(version, goos, goarch) + if err != nil { + return err + } + + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("failed to download %s: %w", url, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status %s while downloading %s", resp.Status, url) + } + + tmpFile, err := os.CreateTemp("", "pocketbase-*.zip") + if err != nil { + return err + } + defer os.Remove(tmpFile.Name()) + defer tmpFile.Close() + + if _, err := io.Copy(tmpFile, resp.Body); err != nil { + return err + } + + if err := extractBinary(tmpFile.Name(), binaryName, dest); err != nil { + return err + } + + return os.Chmod(dest, 0o755) +} + +func releaseAssetURL(version, goos, goarch string) (string, string, error) { + osName := map[string]string{ + "darwin": "darwin", + "linux": "linux", + "windows": "windows", + } + archName := map[string]string{ + "amd64": "amd64", + "386": "386", + "arm64": "arm64", + "arm": "armv7", + } + + osPart, ok := osName[goos] + if !ok { + return "", "", fmt.Errorf("unsupported GOOS: %s", goos) + } + archPart, ok := archName[goarch] + if !ok { + return "", "", fmt.Errorf("unsupported GOARCH: %s", goarch) + } + + filename := fmt.Sprintf("pocketbase_%s_%s_%s.zip", version, osPart, archPart) + url := fmt.Sprintf("https://github.com/pocketbase/pocketbase/releases/download/v%s/%s", version, filename) + return url, pocketbaseBinaryName(goos), nil +} + +func extractBinary(zipPath, binaryName, dest string) error { + r, err := zip.OpenReader(zipPath) + if err != nil { + return err + } + defer r.Close() + + for _, f := range r.File { + if filepath.Base(f.Name) != binaryName { + continue + } + + if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { + return err + } + + src, err := f.Open() + if err != nil { + return err + } + defer src.Close() + + out, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755) + if err != nil { + return err + } + defer out.Close() + + if _, err := io.Copy(out, src); err != nil { + return err + } + + return nil + } + return fmt.Errorf("binary %s not found in archive", binaryName) +} + +func ensureGitignoreEntries(path string, entries []string) error { + var existing = make(map[string]struct{}) + var original []byte + if data, err := os.ReadFile(path); err == nil { + original = data + for _, line := range strings.Split(string(data), "\n") { + trimmed := strings.TrimSpace(line) + if trimmed != "" { + existing[trimmed] = struct{}{} + } + } + } else if !errors.Is(err, os.ErrNotExist) { + return err + } + + builder := &strings.Builder{} + if len(original) > 0 { + builder.Write(original) + if !strings.HasSuffix(string(original), "\n") { + builder.WriteByte('\n') + } + } + + var added bool + for _, entry := range entries { + if _, seen := existing[entry]; seen { + continue + } + builder.WriteString(entry) + builder.WriteByte('\n') + added = true + } + + if !added { + return nil + } + + return os.WriteFile(path, []byte(builder.String()), 0o644) +}