diff --git a/README.md b/README.md index 1907734..b8a24d5 100644 --- a/README.md +++ b/README.md @@ -77,10 +77,40 @@ if err != nil { fmt.Println("Detected:", mgr.Name()) ``` +## Interfaces + +snack uses a layered interface design. Every provider implements `Manager` (the base). Extended capabilities are optional — use type assertions to check support: + +```go +// Base — every provider +snack.Manager // Install, Remove, Purge, Upgrade, Update, List, Search, Info, IsInstalled, Version + +// Optional capabilities — type-assert to check +snack.VersionQuerier // LatestVersion, ListUpgrades, UpgradeAvailable, VersionCmp +snack.Holder // Hold, Unhold, ListHeld (version pinning) +snack.Cleaner // Autoremove, Clean (orphan/cache cleanup) +snack.FileOwner // FileList, Owner (file-to-package queries) +snack.RepoManager // ListRepos, AddRepo, RemoveRepo +snack.KeyManager // AddKey, RemoveKey, ListKeys (GPG keys) +snack.Grouper // GroupList, GroupInfo, GroupInstall +snack.NameNormalizer // NormalizeName, ParseArch +``` + +Check capabilities at runtime: + +```go +caps := snack.GetCapabilities(mgr) +if caps.Hold { + mgr.(snack.Holder).Hold(ctx, []string{"nginx"}) +} +``` + ## 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. +- **Capability interfaces** — extended features via type assertion, so providers aren't forced to stub unsupported operations. +- **Per-provider mutex** — each provider serializes mutating operations independently; apt + snap can run in parallel. - **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. diff --git a/capabilities.go b/capabilities.go new file mode 100644 index 0000000..240bfa1 --- /dev/null +++ b/capabilities.go @@ -0,0 +1,37 @@ +package snack + +// Capabilities reports which optional interfaces a Manager implements. +// Useful for grlx to determine what operations are available before +// attempting them. +type Capabilities struct { + VersionQuery bool + Hold bool + Clean bool + FileOwnership bool + RepoManagement bool + KeyManagement bool + Groups bool + NameNormalize bool +} + +// GetCapabilities probes a Manager for all optional interface support. +func GetCapabilities(m Manager) Capabilities { + _, vq := m.(VersionQuerier) + _, h := m.(Holder) + _, c := m.(Cleaner) + _, fo := m.(FileOwner) + _, rm := m.(RepoManager) + _, km := m.(KeyManager) + _, g := m.(Grouper) + _, nn := m.(NameNormalizer) + return Capabilities{ + VersionQuery: vq, + Hold: h, + Clean: c, + FileOwnership: fo, + RepoManagement: rm, + KeyManagement: km, + Groups: g, + NameNormalize: nn, + } +} diff --git a/snack.go b/snack.go index 37db2ad..9dae405 100644 --- a/snack.go +++ b/snack.go @@ -46,7 +46,17 @@ func Targets(names ...string) []Target { return targets } -// Manager is the common interface implemented by all package manager wrappers. +// Manager is the base interface implemented by all package manager wrappers. +// It covers the core operations every package manager supports. +// +// For extended capabilities, use type assertions against the optional +// interfaces below. Example: +// +// if holder, ok := mgr.(snack.Holder); ok { +// holder.Hold(ctx, "nginx") +// } else { +// log.Warn("hold not supported by", mgr.Name()) +// } type Manager interface { // Install one or more packages. Install(ctx context.Context, pkgs []Target, opts ...Option) error @@ -84,3 +94,108 @@ type Manager interface { // Name returns the package manager's identifier (e.g. "apt", "pacman"). Name() string } + +// VersionQuerier provides version comparison and upgrade availability checks. +// Supported by: apt, pacman, apk, dnf. +type VersionQuerier interface { + // LatestVersion returns the latest available version of a package + // from configured repositories. + LatestVersion(ctx context.Context, pkg string) (string, error) + + // ListUpgrades returns packages that have newer versions available. + ListUpgrades(ctx context.Context) ([]Package, error) + + // UpgradeAvailable reports whether a newer version of a package is + // available in the repositories. + UpgradeAvailable(ctx context.Context, pkg string) (bool, error) + + // VersionCmp compares two version strings using the package manager's + // native version comparison logic. Returns -1, 0, or 1. + VersionCmp(ctx context.Context, ver1, ver2 string) (int, error) +} + +// Holder provides package version pinning (hold/unhold). +// Supported by: apt, pacman (with pacman-contrib), dnf. +type Holder interface { + // Hold pins packages at their current version, preventing upgrades. + Hold(ctx context.Context, pkgs []string) error + + // Unhold removes version pins, allowing packages to be upgraded. + Unhold(ctx context.Context, pkgs []string) error + + // ListHeld returns all currently held/pinned packages. + ListHeld(ctx context.Context) ([]Package, error) +} + +// Cleaner provides orphan/cache cleanup operations. +// Supported by: apt, pacman, apk, dnf. +type Cleaner interface { + // Autoremove removes packages that were installed as dependencies + // but are no longer required by any installed package. + Autoremove(ctx context.Context, opts ...Option) error + + // Clean removes cached package files from the local cache. + Clean(ctx context.Context) error +} + +// FileOwner provides file-to-package ownership queries. +// Supported by: apt/dpkg, pacman, rpm/dnf, apk. +type FileOwner interface { + // FileList returns all files installed by a package. + FileList(ctx context.Context, pkg string) ([]string, error) + + // Owner returns the package that owns a given file path. + Owner(ctx context.Context, path string) (string, error) +} + +// RepoManager provides repository configuration operations. +// Supported by: apt, dnf, pacman (partially). +type RepoManager interface { + // ListRepos returns all configured package repositories. + ListRepos(ctx context.Context) ([]Repository, error) + + // AddRepo adds a new package repository. + AddRepo(ctx context.Context, repo Repository) error + + // RemoveRepo removes a configured repository. + RemoveRepo(ctx context.Context, id string) error +} + +// KeyManager provides GPG/signing key management for repositories. +// Supported by: apt, rpm/dnf. +type KeyManager interface { + // AddKey imports a GPG key for package verification. + // The key can be a URL, file path, or key ID. + AddKey(ctx context.Context, key string) error + + // RemoveKey removes a GPG key. + RemoveKey(ctx context.Context, keyID string) error + + // ListKeys returns all trusted package signing keys. + ListKeys(ctx context.Context) ([]string, error) +} + +// Grouper provides package group operations. +// Supported by: pacman, dnf/yum. +type Grouper interface { + // GroupList returns all available package groups. + GroupList(ctx context.Context) ([]string, error) + + // GroupInfo returns the packages in a group. + GroupInfo(ctx context.Context, group string) ([]Package, error) + + // GroupInstall installs all packages in a group. + GroupInstall(ctx context.Context, group string, opts ...Option) error +} + +// NormalizeName provides package name normalization. +// Supported by: apt (strips :arch suffixes), rpm. +type NameNormalizer interface { + // NormalizeName returns the canonical form of a package name, + // stripping architecture suffixes, epoch prefixes, etc. + NormalizeName(name string) string + + // ParseArch extracts the architecture from a package name if present. + // Returns the name without arch and the arch string. + ParseArch(name string) (string, string) +} diff --git a/types.go b/types.go index bf1a9c4..82ebadc 100644 --- a/types.go +++ b/types.go @@ -67,6 +67,18 @@ func WithReinstall() Option { return func(o *Options) { o.Reinstall = true } } +// Repository represents a configured package repository. +type Repository struct { + ID string `json:"id"` // unique identifier + Name string `json:"name,omitempty"` // human-readable name + URL string `json:"url"` // repository URL + Enabled bool `json:"enabled"` // whether the repo is active + GPGCheck bool `json:"gpg_check,omitempty"` // whether GPG verification is enabled + GPGKey string `json:"gpg_key,omitempty"` // GPG key URL or ID + Type string `json:"type,omitempty"` // e.g. "deb", "rpm-md", "pkg" + Arch string `json:"arch,omitempty"` // architecture filter +} + // ApplyOptions processes functional options into an Options struct. func ApplyOptions(opts ...Option) Options { var o Options