From 57253f0523d43383eb9f36a00136f455f9ef07e8 Mon Sep 17 00:00:00 2001 From: huntedbytheirs Date: Wed, 19 Aug 2026 16:27:51 -0400 Subject: [PATCH] feat(cli): add Go module, subcommand dispatch, frontend registry Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .gitignore | 2 + cmd/zeta-reconstruct/main.go | 109 +++++++++++++++++++++++++++++++ go.mod | 3 + internal/arch/binary/frontend.go | 23 +++++++ internal/arch/src/frontend.go | 23 +++++++ internal/registry/registry.go | 67 +++++++++++++++++++ 6 files changed, 227 insertions(+) create mode 100644 .gitignore create mode 100644 cmd/zeta-reconstruct/main.go create mode 100644 go.mod create mode 100644 internal/arch/binary/frontend.go create mode 100644 internal/arch/src/frontend.go create mode 100644 internal/registry/registry.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ae9c49 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/zr-out +*.part diff --git a/cmd/zeta-reconstruct/main.go b/cmd/zeta-reconstruct/main.go new file mode 100644 index 0000000..5c02c24 --- /dev/null +++ b/cmd/zeta-reconstruct/main.go @@ -0,0 +1,109 @@ +// Command zeta-reconstruct converts Arch Linux packages into Zeta package +// format. Subcommands are provided by frontends registered in the registry. +package main + +import ( + "flag" + "fmt" + "os" + + "git.spectoria.dev/huntedbytheirs/zeta-reconstruct/internal/registry" + + // Import frontend packages so their init() functions register them. + _ "git.spectoria.dev/huntedbytheirs/zeta-reconstruct/internal/arch/binary" + _ "git.spectoria.dev/huntedbytheirs/zeta-reconstruct/internal/arch/src" +) + +const defaultRepo = "https://raw.githubusercontent.com/gretagen/zeta-packages/refs/heads/main" + +const usageText = `usage: zeta-reconstruct [flags] + +Commands: + arch-binary convert an Arch binary package (.pkg.tar.zst) to Zeta + arch-src convert a PKGBUILD/.SRCINFO source recipe to Zeta + +Flags: + --list list registered frontends + -h, --help show this help + +arch-binary flags: + --output output directory (default ".") + --repo repository base URL (default "` + defaultRepo + `") + +arch-src flags: + --output output directory (default ".") + --arch target architecture (default "x86_64") +` + +func main() { + os.Exit(run(os.Args[1:])) +} + +func run(args []string) int { + if len(args) == 0 { + fmt.Fprintln(os.Stderr, "error: no command specified") + fmt.Fprintln(os.Stderr, "Run 'zeta-reconstruct --help' for usage.") + return 2 + } + + switch args[0] { + case "--list": + for _, name := range registry.List() { + fmt.Println(name) + } + return 0 + case "--help", "-h": + fmt.Print(usageText) + return 0 + } + + frontend, ok := registry.Get(args[0]) + if !ok { + fmt.Fprintf(os.Stderr, "error: unknown command %q\n", args[0]) + fmt.Fprintln(os.Stderr, "Run 'zeta-reconstruct --help' for usage.") + return 2 + } + + fs := flag.NewFlagSet(frontend.Name(), flag.ContinueOnError) + fs.SetOutput(os.Stderr) + opts := registry.Options{Output: "."} + switch frontend.Name() { + case "arch-binary": + opts.Repo = defaultRepo + fs.StringVar(&opts.Repo, "repo", opts.Repo, "repository base URL") + case "arch-src": + opts.Arch = "x86_64" + fs.StringVar(&opts.Arch, "arch", opts.Arch, "target architecture") + } + fs.StringVar(&opts.Output, "output", opts.Output, "output directory") + fs.Usage = func() { + fmt.Fprintf(os.Stderr, "usage: zeta-reconstruct %s [flags] \n", frontend.Name()) + fmt.Fprintln(os.Stderr, "Run 'zeta-reconstruct --help' for usage.") + } + + if err := fs.Parse(args[1:]); err != nil { + if err == flag.ErrHelp { + fs.Usage() + return 0 + } + return 2 + } + + rest := fs.Args() + if len(rest) == 0 { + fmt.Fprintf(os.Stderr, "error: %s requires an input argument\n", frontend.Name()) + fs.Usage() + return 1 + } + if len(rest) > 1 { + fmt.Fprintf(os.Stderr, "error: unexpected argument %q\n", rest[1]) + fs.Usage() + return 2 + } + + if err := frontend.Convert(rest[0], opts); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + return 1 + } + return 0 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7c7f96f --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.spectoria.dev/huntedbytheirs/zeta-reconstruct + +go 1.26.5 diff --git a/internal/arch/binary/frontend.go b/internal/arch/binary/frontend.go new file mode 100644 index 0000000..2342770 --- /dev/null +++ b/internal/arch/binary/frontend.go @@ -0,0 +1,23 @@ +// Package binary converts Arch Linux binary packages (.pkg.tar.zst) into +// Zeta packages. +package binary + +import ( + "errors" + + "git.spectoria.dev/huntedbytheirs/zeta-reconstruct/internal/registry" +) + +// frontend implements registry.Frontend for Arch binary packages. +type frontend struct{} + +func (frontend) Name() string { return "arch-binary" } + +func (frontend) Convert(input string, opts registry.Options) error { + // Implemented in a later todo (.pkg.tar.zst -> tarball + package.lua). + return errors.New("not implemented") +} + +func init() { + registry.Register(frontend{}) +} diff --git a/internal/arch/src/frontend.go b/internal/arch/src/frontend.go new file mode 100644 index 0000000..0bae5be --- /dev/null +++ b/internal/arch/src/frontend.go @@ -0,0 +1,23 @@ +// Package src converts Arch Linux source packages (PKGBUILD/.SRCINFO) into +// Zeta recipes. +package src + +import ( + "errors" + + "git.spectoria.dev/huntedbytheirs/zeta-reconstruct/internal/registry" +) + +// frontend implements registry.Frontend for Arch source packages. +type frontend struct{} + +func (frontend) Name() string { return "arch-src" } + +func (frontend) Convert(input string, opts registry.Options) error { + // Implemented in a later todo (PKGBUILD/.SRCINFO -> .recipe + build.sh). + return errors.New("not implemented") +} + +func init() { + registry.Register(frontend{}) +} diff --git a/internal/registry/registry.go b/internal/registry/registry.go new file mode 100644 index 0000000..67383d6 --- /dev/null +++ b/internal/registry/registry.go @@ -0,0 +1,67 @@ +// Package registry provides a global registry of package-format frontends. +// +// Frontends self-register via init() from their own packages, so a future +// format (e.g. Gentoo ebuild, void-src) only needs a new package that imports +// this one — nothing else changes. +package registry + +import ( + "fmt" + "sort" + "sync" +) + +// Options carries per-conversion settings shared by all frontends. +type Options struct { + Output string // directory to write converted output into + Repo string // repository base URL (binary frontend) + Arch string // target architecture (source frontend) +} + +// Frontend converts one input of a specific package format to Zeta. +type Frontend interface { + // Name returns the frontend's CLI subcommand name. + Name() string + // Convert converts input into Zeta format under opts.Output. + Convert(input string, opts Options) error +} + +var ( + mu sync.RWMutex + frontends = make(map[string]Frontend) +) + +// Register adds f to the global registry. It panics if a frontend with the +// same Name is already registered. +func Register(f Frontend) { + mu.Lock() + defer mu.Unlock() + name := f.Name() + if name == "" { + panic("registry: frontend with empty name") + } + if _, dup := frontends[name]; dup { + panic(fmt.Sprintf("registry: duplicate frontend %q", name)) + } + frontends[name] = f +} + +// List returns the names of all registered frontends, sorted. +func List() []string { + mu.RLock() + defer mu.RUnlock() + names := make([]string, 0, len(frontends)) + for name := range frontends { + names = append(names, name) + } + sort.Strings(names) + return names +} + +// Get returns the frontend registered under name. +func Get(name string) (Frontend, bool) { + mu.RLock() + defer mu.RUnlock() + f, ok := frontends[name] + return f, ok +}