mirror of
https://github.com/gogrlx/snack.git
synced 2026-04-02 05:08:42 -07:00
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
This commit is contained in:
12
LICENSE
Normal file
12
LICENSE
Normal file
@@ -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.
|
||||
111
README.md
Normal file
111
README.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# snack 🍿
|
||||
|
||||
Idiomatic Go wrappers for system package managers.
|
||||
|
||||
[](https://opensource.org/licenses/0BSD)
|
||||
[](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).
|
||||
2
apk/apk.go
Normal file
2
apk/apk.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package apk provides Go bindings for apk-tools (Alpine Linux package manager).
|
||||
package apk
|
||||
2
apt/apt.go
Normal file
2
apt/apt.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package apt provides Go bindings for APT (Advanced Packaging Tool) on Debian/Ubuntu.
|
||||
package apt
|
||||
2
aur/aur.go
Normal file
2
aur/aur.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package aur provides Go bindings for AUR (Arch User Repository) package building.
|
||||
package aur
|
||||
43
detect/detect.go
Normal file
43
detect/detect.go
Normal file
@@ -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
|
||||
}
|
||||
2
dnf/dnf.go
Normal file
2
dnf/dnf.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package dnf provides Go bindings for DNF (Fedora/RHEL package manager).
|
||||
package dnf
|
||||
2
dpkg/dpkg.go
Normal file
2
dpkg/dpkg.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package dpkg provides Go bindings for dpkg (low-level Debian package tool).
|
||||
package dpkg
|
||||
31
errors.go
Normal file
31
errors.go
Normal file
@@ -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")
|
||||
)
|
||||
2
flatpak/flatpak.go
Normal file
2
flatpak/flatpak.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package flatpak provides Go bindings for Flatpak (cross-distribution application packaging).
|
||||
package flatpak
|
||||
2
pacman/pacman.go
Normal file
2
pacman/pacman.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package pacman provides Go bindings for the pacman package manager (Arch Linux).
|
||||
package pacman
|
||||
2
pkg/pkg.go
Normal file
2
pkg/pkg.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package pkg provides Go bindings for pkg(8) (FreeBSD package manager).
|
||||
package pkg
|
||||
2
ports/ports.go
Normal file
2
ports/ports.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package ports provides Go bindings for OpenBSD ports/packages.
|
||||
package ports
|
||||
2
rpm/rpm.go
Normal file
2
rpm/rpm.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package rpm provides Go bindings for RPM (low-level Red Hat package tool).
|
||||
package rpm
|
||||
47
snack.go
Normal file
47
snack.go
Normal file
@@ -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
|
||||
}
|
||||
2
snap/snap.go
Normal file
2
snap/snap.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package snap provides Go bindings for snapd (Canonical's cross-distribution package manager).
|
||||
package snap
|
||||
57
types.go
Normal file
57
types.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user