commit 08514a27e1ede4ec6d16eaa0e85efb12a101341d Author: Tai Groot Date: Wed Feb 25 20:01:51 2026 +0000 feat: initial project scaffold - Common Manager interface, Package type, functional options - Sentinel errors for common package manager failures - Sub-package stubs for: pacman, aur, apk, apt, dpkg, dnf, rpm, flatpak, snap, pkg (FreeBSD), ports (OpenBSD) - detect/ package for auto-detection of system package manager - 0BSD license diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bef9257 --- /dev/null +++ b/LICENSE @@ -0,0 +1,12 @@ +Copyright (C) 2026 by the gogrlx contributors + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE +FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN +AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1907734 --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# snack 🍿 + +Idiomatic Go wrappers for system package managers. + +[![License 0BSD](https://img.shields.io/badge/License-0BSD-pink.svg)](https://opensource.org/licenses/0BSD) +[![GoDoc](https://img.shields.io/badge/GoDoc-reference-007d9c)](https://pkg.go.dev/github.com/gogrlx/snack) + +**snack** provides thin, context-aware Go bindings for system package managers. Think [`taigrr/systemctl`](https://github.com/taigrr/systemctl) but for package management. + +Part of the [grlx](https://github.com/gogrlx/grlx) ecosystem. + +## Supported Package Managers + +| Package | Manager | Platform | Status | +|---------|---------|----------|--------| +| `pacman` | pacman | Arch Linux | 🚧 | +| `aur` | AUR (makepkg) | Arch Linux | 🚧 | +| `apk` | apk-tools | Alpine Linux | 🚧 | +| `apt` | APT (apt-get/apt-cache) | Debian/Ubuntu | 🚧 | +| `dpkg` | dpkg | Debian/Ubuntu | 🚧 | +| `dnf` | DNF | Fedora/RHEL | 🚧 | +| `rpm` | RPM | Fedora/RHEL | 🚧 | +| `flatpak` | Flatpak | Cross-distro | 🚧 | +| `snap` | snapd | Cross-distro | 🚧 | +| `pkg` | pkg(8) | FreeBSD | 🚧 | +| `ports` | ports/packages | OpenBSD | 🚧 | +| `detect` | Auto-detection | All | 🚧 | + +## Install + +```bash +go get github.com/gogrlx/snack +``` + +## Usage + +```go +package main + +import ( + "context" + "fmt" + "log" + + "github.com/gogrlx/snack" + "github.com/gogrlx/snack/apt" +) + +func main() { + ctx := context.Background() + mgr := apt.New() + + // Install a package + err := mgr.Install(ctx, []string{"nginx"}, snack.WithSudo(), snack.WithAssumeYes()) + if err != nil { + log.Fatal(err) + } + + // Check if installed + installed, err := mgr.IsInstalled(ctx, "nginx") + if err != nil { + log.Fatal(err) + } + fmt.Println("nginx installed:", installed) +} +``` + +### Auto-detection + +```go +import "github.com/gogrlx/snack/detect" + +mgr, err := detect.Default() +if err != nil { + log.Fatal(err) +} +fmt.Println("Detected:", mgr.Name()) +``` + +## Design + +- **Thin CLI wrappers** — each sub-package wraps a package manager's CLI tools. No FFI, no library bindings. +- **Common interface** — all managers implement `snack.Manager`, making them interchangeable. +- **Context-aware** — all operations accept `context.Context` for cancellation and timeouts. +- **Platform-safe** — build tags ensure packages compile everywhere but only run where appropriate. +- **No root assumption** — use `snack.WithSudo()` when elevated privileges are needed. + +## Implementation Priority + +1. pacman + AUR (Arch Linux) +2. apk (Alpine Linux) +3. apt + dpkg (Debian/Ubuntu) +4. dnf + rpm (Fedora/RHEL) +5. flatpak + snap (cross-distro) +6. pkg + ports (BSD) + +## CLI + +A companion CLI tool is planned for direct terminal usage: + +```bash +snack install nginx +snack remove nginx +snack search redis +snack list +snack upgrade +``` + +## License + +0BSD — see [LICENSE](LICENSE). diff --git a/apk/apk.go b/apk/apk.go new file mode 100644 index 0000000..640de8e --- /dev/null +++ b/apk/apk.go @@ -0,0 +1,2 @@ +// Package apk provides Go bindings for apk-tools (Alpine Linux package manager). +package apk diff --git a/apt/apt.go b/apt/apt.go new file mode 100644 index 0000000..02a6446 --- /dev/null +++ b/apt/apt.go @@ -0,0 +1,2 @@ +// Package apt provides Go bindings for APT (Advanced Packaging Tool) on Debian/Ubuntu. +package apt diff --git a/aur/aur.go b/aur/aur.go new file mode 100644 index 0000000..e733664 --- /dev/null +++ b/aur/aur.go @@ -0,0 +1,2 @@ +// Package aur provides Go bindings for AUR (Arch User Repository) package building. +package aur diff --git a/detect/detect.go b/detect/detect.go new file mode 100644 index 0000000..3862d57 --- /dev/null +++ b/detect/detect.go @@ -0,0 +1,43 @@ +// Package detect provides auto-detection of the system's available package manager. +package detect + +import ( + "os/exec" + + "github.com/gogrlx/snack" +) + +// probeOrder defines the order in which package managers are probed. +// The first available manager wins. +var probeOrder = []struct { + name string + bin string +}{ + {"pacman", "pacman"}, + {"apk", "apk"}, + {"apt", "apt-get"}, + {"dnf", "dnf"}, + {"rpm", "rpm"}, + {"flatpak", "flatpak"}, + {"snap", "snap"}, + {"pkg", "pkg"}, +} + +// Default returns the first available package manager on the system. +// Returns ErrManagerNotFound if no supported manager is detected. +func Default() (snack.Manager, error) { + // TODO: implement — probe for each manager in order, return first match + return nil, snack.ErrManagerNotFound +} + +// All returns all available package managers on the system. +func All() []snack.Manager { + // TODO: implement + return nil +} + +// HasBinary reports whether a binary is available in PATH. +func HasBinary(name string) bool { + _, err := exec.LookPath(name) + return err == nil +} diff --git a/dnf/dnf.go b/dnf/dnf.go new file mode 100644 index 0000000..59871e6 --- /dev/null +++ b/dnf/dnf.go @@ -0,0 +1,2 @@ +// Package dnf provides Go bindings for DNF (Fedora/RHEL package manager). +package dnf diff --git a/dpkg/dpkg.go b/dpkg/dpkg.go new file mode 100644 index 0000000..3802ba3 --- /dev/null +++ b/dpkg/dpkg.go @@ -0,0 +1,2 @@ +// Package dpkg provides Go bindings for dpkg (low-level Debian package tool). +package dpkg diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..d7bcc02 --- /dev/null +++ b/errors.go @@ -0,0 +1,31 @@ +package snack + +import "errors" + +var ( + // ErrNotInstalled is returned when a queried package is not installed. + ErrNotInstalled = errors.New("package is not installed") + + // ErrNotFound is returned when a package cannot be found in any repository. + ErrNotFound = errors.New("package not found") + + // ErrUnsupportedPlatform is returned when a package manager is not + // available on the current platform. + ErrUnsupportedPlatform = errors.New("package manager not available on this platform") + + // ErrPermissionDenied is returned when an operation requires elevated + // privileges that were not provided. + ErrPermissionDenied = errors.New("permission denied; try WithSudo()") + + // ErrAlreadyInstalled is returned when attempting to install a package + // that is already present. + ErrAlreadyInstalled = errors.New("package is already installed") + + // ErrDependencyConflict is returned when a package has unresolvable + // dependency conflicts. + ErrDependencyConflict = errors.New("dependency conflict") + + // ErrManagerNotFound is returned by detect when no supported package + // manager can be found on the system. + ErrManagerNotFound = errors.New("no supported package manager found") +) diff --git a/flatpak/flatpak.go b/flatpak/flatpak.go new file mode 100644 index 0000000..b854723 --- /dev/null +++ b/flatpak/flatpak.go @@ -0,0 +1,2 @@ +// Package flatpak provides Go bindings for Flatpak (cross-distribution application packaging). +package flatpak diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b3aea07 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/gogrlx/snack + +go 1.26.0 diff --git a/pacman/pacman.go b/pacman/pacman.go new file mode 100644 index 0000000..50f2156 --- /dev/null +++ b/pacman/pacman.go @@ -0,0 +1,2 @@ +// Package pacman provides Go bindings for the pacman package manager (Arch Linux). +package pacman diff --git a/pkg/pkg.go b/pkg/pkg.go new file mode 100644 index 0000000..6039075 --- /dev/null +++ b/pkg/pkg.go @@ -0,0 +1,2 @@ +// Package pkg provides Go bindings for pkg(8) (FreeBSD package manager). +package pkg diff --git a/ports/ports.go b/ports/ports.go new file mode 100644 index 0000000..465b7b9 --- /dev/null +++ b/ports/ports.go @@ -0,0 +1,2 @@ +// Package ports provides Go bindings for OpenBSD ports/packages. +package ports diff --git a/rpm/rpm.go b/rpm/rpm.go new file mode 100644 index 0000000..f412890 --- /dev/null +++ b/rpm/rpm.go @@ -0,0 +1,2 @@ +// Package rpm provides Go bindings for RPM (low-level Red Hat package tool). +package rpm diff --git a/snack.go b/snack.go new file mode 100644 index 0000000..63e5f18 --- /dev/null +++ b/snack.go @@ -0,0 +1,47 @@ +// Package snack provides idiomatic Go wrappers for system package managers. +// +// Each sub-package wraps a specific package manager's CLI, while the root +// package defines the common [Manager] interface that all providers implement. +// Use [detect.Default] to auto-detect the system's package manager. +package snack + +import "context" + +// Manager is the common interface implemented by all package manager wrappers. +type Manager interface { + // Install one or more packages. + Install(ctx context.Context, pkgs []string, opts ...Option) error + + // Remove one or more packages. + Remove(ctx context.Context, pkgs []string, opts ...Option) error + + // Purge one or more packages (remove including config files). + Purge(ctx context.Context, pkgs []string, opts ...Option) error + + // Upgrade all installed packages to their latest versions. + Upgrade(ctx context.Context, opts ...Option) error + + // Update refreshes the package index/database. + Update(ctx context.Context) error + + // List returns all installed packages. + List(ctx context.Context) ([]Package, error) + + // Search queries the package index for packages matching the query. + Search(ctx context.Context, query string) ([]Package, error) + + // Info returns details about a specific package. + Info(ctx context.Context, pkg string) (*Package, error) + + // IsInstalled reports whether a package is currently installed. + IsInstalled(ctx context.Context, pkg string) (bool, error) + + // Version returns the installed version of a package. + Version(ctx context.Context, pkg string) (string, error) + + // Available reports whether this package manager is present on the system. + Available() bool + + // Name returns the package manager's identifier (e.g. "apt", "pacman"). + Name() string +} diff --git a/snap/snap.go b/snap/snap.go new file mode 100644 index 0000000..5280099 --- /dev/null +++ b/snap/snap.go @@ -0,0 +1,2 @@ +// Package snap provides Go bindings for snapd (Canonical's cross-distribution package manager). +package snap diff --git a/types.go b/types.go new file mode 100644 index 0000000..45f4787 --- /dev/null +++ b/types.go @@ -0,0 +1,57 @@ +package snack + +// Package represents a system package. +type Package struct { + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description,omitempty"` + Arch string `json:"arch,omitempty"` + Repository string `json:"repository,omitempty"` + Installed bool `json:"installed"` +} + +// Options holds configuration for package manager operations. +type Options struct { + Sudo bool + AssumeYes bool + DryRun bool + Root string // alternate root filesystem + Verbose bool +} + +// Option is a functional option for package manager operations. +type Option func(*Options) + +// WithSudo runs the command with sudo. +func WithSudo() Option { + return func(o *Options) { o.Sudo = true } +} + +// WithAssumeYes automatically confirms prompts. +func WithAssumeYes() Option { + return func(o *Options) { o.AssumeYes = true } +} + +// WithDryRun simulates the operation without making changes. +func WithDryRun() Option { + return func(o *Options) { o.DryRun = true } +} + +// WithRoot sets an alternate root filesystem path. +func WithRoot(root string) Option { + return func(o *Options) { o.Root = root } +} + +// WithVerbose enables verbose output. +func WithVerbose() Option { + return func(o *Options) { o.Verbose = true } +} + +// ApplyOptions processes functional options into an Options struct. +func ApplyOptions(opts ...Option) Options { + var o Options + for _, opt := range opts { + opt(&o) + } + return o +}