mirror of
https://github.com/gogrlx/snack.git
synced 2026-04-01 20:58: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