Artifact 34e0fd7c5a53954a9bd3e9f1fb1d4074b66779005d12ca6a9f643f8cf096f0fd:
- File
cmd/quarry/main.go
— part of check-in
[fb2f393a1e]
at
2026-08-15 23:02:23
on branch trunk
— docs: API reference generated from the registry; staleness gate
quarry docs + make docs; TestDocsCurrent fails when docs/api.md drifts (0003: documentation is a phase exit criterion). (user: dmestas, size: 9490) [annotate] [blame] [check-ins using]
// quarry is a multi-repository forge for zit. See docs/rfcs/. package main import ( "encoding/json" "flag" "fmt" "log" "net/http" "os" "path/filepath" "regexp" "strings" "time" "craftdesign.group/quarry/internal/api" "craftdesign.group/quarry/internal/events" "craftdesign.group/quarry/internal/index" "craftdesign.group/quarry/internal/ingest" "craftdesign.group/quarry/internal/oltp" "craftdesign.group/quarry/internal/ops" "craftdesign.group/quarry/internal/web" "craftdesign.group/quarry/internal/xfer" "craftdesign.group/quarry/internal/zitcli" ) // version is stamped at build time; every page footer and the API report it // (0001 Principle 1). var version = "0.0.0-dev" func main() { if len(os.Args) > 1 { switch os.Args[1] { case "version": fmt.Println("quarry", version) return case "spec": if err := printSpec(os.Stdout); err != nil { fmt.Fprintln(os.Stderr, "quarry:", err) os.Exit(1) } return case "serve": if err := serveCmd(os.Args[2:]); err != nil { fmt.Fprintln(os.Stderr, "quarry:", err) os.Exit(1) } return case "reindex": if err := reindexCmd(os.Args[2:]); err != nil { fmt.Fprintln(os.Stderr, "quarry:", err) os.Exit(1) } return case "docs": r := ops.NewRegistry() if err := api.RegisterAll(r, api.Deps{Version: version}); err != nil { fmt.Fprintln(os.Stderr, "quarry:", err) os.Exit(1) } os.Stdout.Write(ops.Docs(r, "quarry")) return case "user": if err := userCmd(os.Args[2:]); err != nil { fmt.Fprintln(os.Stderr, "quarry:", err) os.Exit(1) } return } } fmt.Fprintln(os.Stderr, "usage: quarry version|spec|serve|reindex|user|docs") os.Exit(2) } // userCmd manages sync users (0001 §7 v1 auth): `quarry user add` // derives the RFC §6.3 shared secret from the store's project code and // stores it in forge OLTP — plaintext is never persisted. A repo with // at least one sync user requires authenticated pushes. func userCmd(args []string) error { if len(args) < 1 || args[0] != "add" { return fmt.Errorf("usage: quarry user add --repo name=path --user U [--password P] [--data DIR]") } fs := flag.NewFlagSet("user add", flag.ContinueOnError) repos := repoFlags{} fs.Var(repos, "repo", "repository as name=path") user := fs.String("user", "", "sync login name") password := fs.String("password", "", "password (omit to read from QUARRY_PASSWORD)") data := fs.String("data", "", "derived-data directory (default: .quarry beside the repo)") if err := fs.Parse(args[1:]); err != nil { return err } if len(repos) != 1 || *user == "" { return fmt.Errorf("user add wants exactly one --repo and a --user") } pw := *password if pw == "" { pw = os.Getenv("QUARRY_PASSWORD") } if pw == "" { return fmt.Errorf("no password: use --password or QUARRY_PASSWORD") } var name, path string for n, p := range repos { name, path = n, p } dir := *data if dir == "" { dir = filepath.Join(path, ".quarry") } if err := os.MkdirAll(dir, 0o755); err != nil { return err } code, err := xfer.ProjectCode(path) if err != nil { return fmt.Errorf("project code of %s: %w", name, err) } db, err := oltp.Open(filepath.Join(dir, "oltp.db")) if err != nil { return err } defer db.Close() if err := db.SetSyncUser(name, *user, xfer.SharedSecret(code, *user, pw)); err != nil { return err } fmt.Printf("sync user %q added to %s; pushes now require authentication\n", *user, name) return nil } // reindexCmd is 0001 §5's full rebuild as a verb: for each --repo, drop // the derived tables and cursor, then replay the store. Run it with the // same --repo/--data flags as serve, while serve is stopped (the // embedded NATS store directory admits one process). func reindexCmd(args []string) error { fs := flag.NewFlagSet("reindex", flag.ContinueOnError) repos := repoFlags{} fs.Var(repos, "repo", "repository as name=path (repeatable; bare path uses its basename)") data := fs.String("data", "", "derived-data directory (default: .quarry beside the first repo)") if err := fs.Parse(args); err != nil { return err } if len(repos) == 0 { return fmt.Errorf("reindex wants at least one --repo") } dir := *data if dir == "" { for _, p := range repos { dir = filepath.Join(p, ".quarry") break } } ev, err := events.Start(filepath.Join(dir, "nats")) if err != nil { return fmt.Errorf("%w (is serve still running against this data dir?)", err) } defer ev.Close() for name, path := range repos { if err := os.MkdirAll(filepath.Join(dir, name), 0o755); err != nil { return err } ix, err := index.Open(filepath.Join(dir, name, "index.db")) if err != nil { return err } n, err := ingest.NewPipeline(zitcli.New(path), ev, ix, name).Reindex() ix.Close() if err != nil { return fmt.Errorf("reindex %s: %w", name, err) } fmt.Printf("reindexed %s: %d artifacts replayed\n", name, n) } return nil } // repoFlags collects repeatable --repo name=path values (bare paths take // their directory basename as the repository name). type repoFlags map[string]string func (r repoFlags) String() string { return fmt.Sprint(map[string]string(r)) } func (r repoFlags) Set(v string) error { name, path, found := strings.Cut(v, "=") if !found { path = v name = filepath.Base(strings.TrimSuffix(v, "/")) } if name == "" || path == "" { return fmt.Errorf("--repo wants name=path or path, got %q", v) } if !repoName.MatchString(name) { return fmt.Errorf("repository name %q: use letters, digits, dot, dash, underscore", name) } if _, dup := r[name]; dup { return fmt.Errorf("repository %q registered twice", name) } r[name] = path return nil } var repoName = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$`) // serveCmd runs the HTTP surface: the op-generated API plus the HTML // pages, both consumers of one registry. Each --repo gets its own index // and watcher pipeline; one embedded NATS carries every store's events // under zit.evt.<name>.* (0001 §4: a watcher per registered store). func serveCmd(args []string) error { fs := flag.NewFlagSet("serve", flag.ContinueOnError) addr := fs.String("addr", "127.0.0.1:8080", "listen address") repos := repoFlags{} fs.Var(repos, "repo", "repository to serve as name=path (repeatable; bare path uses its basename)") data := fs.String("data", "", "derived-data directory (default: .quarry beside the first repo)") interval := fs.Duration("sync-interval", 2*time.Second, "store poll interval") if err := fs.Parse(args); err != nil { return err } deps := api.Deps{Version: version, Repos: map[string]*api.Repo{}} var notifier web.Notifier var fronter *xfer.Fronter if len(repos) > 0 { fronter = xfer.New() defer fronter.Close() for name, path := range repos { fronter.Register(name, path) } } if len(repos) > 0 { dir := *data if dir == "" { for _, p := range repos { dir = filepath.Join(p, ".quarry") break } } if err := os.MkdirAll(dir, 0o755); err != nil { return err } odb, err := oltp.Open(filepath.Join(dir, "oltp.db")) if err != nil { return err } defer odb.Close() fronter.SetAuth(odb) ev, err := events.Start(filepath.Join(dir, "nats")) if err != nil { return err } defer ev.Close() notifier = ev for name, path := range repos { if err := os.MkdirAll(filepath.Join(dir, name), 0o755); err != nil { return err } ix, err := index.Open(filepath.Join(dir, name, "index.db")) if err != nil { return err } defer ix.Close() cli := zitcli.New(path) deps.Repos[name] = &api.Repo{Index: ix, Zit: cli} pipe := ingest.NewPipeline(cli, ev, ix, name) if n, err := pipe.Sync(); err != nil { return fmt.Errorf("initial ingest of %s: %w", name, err) } else { fmt.Printf("ingested %d artifacts from %s (%s)\n", n, name, path) } go func(name string, pipe *ingest.Pipeline) { for range time.Tick(*interval) { if _, err := pipe.Sync(); err != nil { log.Printf("ingest %s: %v", name, err) } } }(name, pipe) } } mux, err := buildHandler(deps, notifier) if err != nil { return err } if fronter != nil { mount := http.NewServeMux() // Sync fronting (0001 §7): zit clients POST the repo URL // verbatim; /xfer is the alias zit serve itself answers on. mount.Handle("POST /r/{repo}", fronter) mount.Handle("POST /r/{repo}/xfer", fronter) mount.Handle("/", mux) mux = mount } fmt.Println("quarry", version, "listening on", *addr) return http.ListenAndServe(*addr, mux) } // buildHandler wires the two registry consumers onto one mux. notifier // may be nil (no live pages). func buildHandler(deps api.Deps, notifier web.Notifier) (http.Handler, error) { r := ops.NewRegistry() if err := api.RegisterAll(r, deps); err != nil { return nil, err } apiRouter, err := api.Router(r, deps.Version) if err != nil { return nil, err } mux := http.NewServeMux() mux.Handle("/api/", apiRouter) mux.Handle("/", web.Handler(r, notifier)) return mux, nil } // printSpec emits the OpenAPI document generated from the full registry — // the same registry serve will run, so the spec cannot drift (0001 §9). func printSpec(w *os.File) error { r := ops.NewRegistry() if err := api.RegisterAll(r, api.Deps{Version: version}); err != nil { return err } spec, err := ops.Spec(r, "quarry", version) if err != nil { return err } enc := json.NewEncoder(w) enc.SetIndent("", " ") return enc.Encode(spec) }