// 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 – add a user to the passwords file // nebbet user list – list users // nebbet user delete – remove a user // nebbet user passwd – 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 [username]") os.Exit(1) } a := auth.New(passwordFile) switch args[0] { case "add": requireArg(args, 1, "nebbet user add ") 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 ") 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 ") 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 add user to password file user list list users user delete remove user user passwd change user password`) }