summaryrefslogtreecommitdiffstats
path: root/cmd/nebbet
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/nebbet')
-rw-r--r--cmd/nebbet/main.go229
1 files changed, 229 insertions, 0 deletions
diff --git a/cmd/nebbet/main.go b/cmd/nebbet/main.go
new file mode 100644
index 0000000..1d8b3c0
--- /dev/null
+++ b/cmd/nebbet/main.go
@@ -0,0 +1,229 @@
+// Command nebbet is the CLI for the nebbet.no static site generator.
+//
+// Usage:
+//
+// nebbet build – build all pages once
+// nebbet watch – watch for changes and rebuild
+// nebbet serve [--port N] – serve with live admin UI + watch mode
+// nebbet user add <name> – add a user to the passwords file
+// nebbet user list – list users
+// nebbet user delete <name> – remove a user
+// nebbet user passwd <name> – change a user's password
+package main
+
+import (
+ "flag"
+ "fmt"
+ "net/http"
+ "os"
+
+ "nebbet.no/internal/admin"
+ "nebbet.no/internal/auth"
+ "nebbet.no/internal/builder"
+ "nebbet.no/internal/db"
+)
+
+const (
+ contentDir = "content"
+ outputDir = "public"
+ postsDir = "content/posts"
+ dataDir = "data"
+ metaDBPath = "data/meta.db"
+ searchDBPath = "data/search.db"
+ passwordFile = ".passwords"
+)
+
+func main() {
+ if len(os.Args) < 2 {
+ usage()
+ os.Exit(1)
+ }
+ switch os.Args[1] {
+ case "build":
+ cmdBuild(os.Args[2:])
+ case "watch":
+ cmdWatch()
+ case "serve":
+ cmdServe(os.Args[2:])
+ case "user":
+ cmdUser(os.Args[2:])
+ default:
+ fmt.Fprintf(os.Stderr, "unknown command %q\n", os.Args[1])
+ usage()
+ os.Exit(1)
+ }
+}
+
+// ── build ─────────────────────────────────────────────────────────────────────
+
+func cmdBuild(args []string) {
+ fs := flag.NewFlagSet("build", flag.ExitOnError)
+ watch := fs.Bool("watch", false, "watch for changes and rebuild")
+ _ = fs.Parse(args)
+
+ b := mustBuilder()
+ defer b.MetaDB.Close()
+ defer b.SearchDB.Close()
+
+ if err := b.BuildAll(); err != nil {
+ fmt.Fprintln(os.Stderr, "build error:", err)
+ os.Exit(1)
+ }
+ if *watch {
+ if err := b.Watch(); err != nil {
+ fmt.Fprintln(os.Stderr, "watch error:", err)
+ os.Exit(1)
+ }
+ }
+}
+
+// ── watch ─────────────────────────────────────────────────────────────────────
+
+func cmdWatch() {
+ b := mustBuilder()
+ defer b.MetaDB.Close()
+ defer b.SearchDB.Close()
+ if err := b.BuildAll(); err != nil {
+ fmt.Fprintln(os.Stderr, "build error:", err)
+ os.Exit(1)
+ }
+ if err := b.Watch(); err != nil {
+ fmt.Fprintln(os.Stderr, "watch error:", err)
+ os.Exit(1)
+ }
+}
+
+// ── serve ─────────────────────────────────────────────────────────────────────
+
+func cmdServe(args []string) {
+ fs := flag.NewFlagSet("serve", flag.ExitOnError)
+ port := fs.String("port", "8080", "port to listen on")
+ _ = fs.Parse(args)
+
+ b := mustBuilder()
+ defer b.MetaDB.Close()
+ defer b.SearchDB.Close()
+
+ // Initial build.
+ if err := b.BuildAll(); err != nil {
+ fmt.Fprintln(os.Stderr, "build error:", err)
+ os.Exit(1)
+ }
+
+ // Watch in background.
+ go func() {
+ if err := b.Watch(); err != nil {
+ fmt.Fprintln(os.Stderr, "watch error:", err)
+ }
+ }()
+
+ adminSrv := &admin.Server{
+ PostsDir: postsDir,
+ AuthFile: passwordFile,
+ Builder: b,
+ }
+
+ mux := http.NewServeMux()
+ // Admin routes — handled dynamically, never from the static output dir.
+ mux.Handle("/admin/", http.StripPrefix("/admin", adminSrv))
+ // Everything else — serve the pre-built static files.
+ mux.Handle("/", http.FileServer(http.Dir(outputDir)))
+
+ addr := ":" + *port
+ fmt.Printf("listening on http://localhost%s\n", addr)
+ fmt.Printf(" public site: http://localhost%s/\n", addr)
+ fmt.Printf(" admin UI: http://localhost%s/admin/\n", addr)
+ if err := http.ListenAndServe(addr, mux); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
+
+// ── user ─────────────────────────────────────────────────────────────────────
+
+func cmdUser(args []string) {
+ if len(args) == 0 {
+ fmt.Fprintln(os.Stderr, "usage: nebbet user <add|list|delete|passwd> [username]")
+ os.Exit(1)
+ }
+ a := auth.New(passwordFile)
+ switch args[0] {
+ case "add":
+ requireArg(args, 1, "nebbet user add <username>")
+ if err := a.AddUser(args[1]); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ fmt.Println("user added:", args[1])
+ case "list":
+ users, err := a.ListUsers()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ if len(users) == 0 {
+ fmt.Println("(no users)")
+ return
+ }
+ for _, u := range users {
+ fmt.Println(u)
+ }
+ case "delete":
+ requireArg(args, 1, "nebbet user delete <username>")
+ if err := a.DeleteUser(args[1]); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ fmt.Println("user deleted:", args[1])
+ case "passwd":
+ requireArg(args, 1, "nebbet user passwd <username>")
+ if err := a.ChangePassword(args[1]); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ fmt.Println("password updated:", args[1])
+ default:
+ fmt.Fprintf(os.Stderr, "unknown user subcommand %q\n", args[0])
+ os.Exit(1)
+ }
+}
+
+// ── helpers ───────────────────────────────────────────────────────────────────
+
+func mustBuilder() *builder.Builder {
+ if err := os.MkdirAll(dataDir, 0755); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+ meta, err := db.OpenMeta(metaDBPath)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "meta db:", err)
+ os.Exit(1)
+ }
+ search, err := db.OpenSearch(searchDBPath)
+ if err != nil {
+ fmt.Fprintln(os.Stderr, "search db:", err)
+ os.Exit(1)
+ }
+ return builder.New(contentDir, outputDir, meta, search)
+}
+
+func requireArg(args []string, n int, usage string) {
+ if len(args) <= n {
+ fmt.Fprintln(os.Stderr, "usage:", usage)
+ os.Exit(1)
+ }
+}
+
+func usage() {
+ fmt.Fprintln(os.Stderr, `nebbet — static site generator with admin
+
+Commands:
+ build [--watch] build all pages (optionally watch for changes)
+ watch watch for changes and rebuild
+ serve [--port N] serve site + admin UI (default port 8080)
+ user add <name> add user to password file
+ user list list users
+ user delete <name> remove user
+ user passwd <name> change user password`)
+}