diff options
Diffstat (limited to 'cmd/nebbet/main.go')
| -rw-r--r-- | cmd/nebbet/main.go | 229 |
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`) +} |
