feat: implement detect package and snack CLI

- detect: auto-detection with Default(), All(), ByName()
  - Platform-specific probing via build tags (linux, freebsd, openbsd)
  - Linux probe order: apt, dnf, pacman, apk, flatpak, snap
- cmd/snack: full CLI with cobra + charmbracelet/fang
  - Subcommands: install, remove, purge, upgrade, update, list, search,
    info, which, hold, unhold, clean, detect, version
  - Global flags: --manager, --sudo, --yes, --dry-run
  - Capability type-assertions for FileOwner, Holder, Cleaner
- detect: basic compilation tests
This commit is contained in:
2026-02-26 01:08:27 +00:00
parent 10c8bd6f4d
commit ca2fdd49ac
15 changed files with 1243 additions and 28 deletions

View File

@@ -7,35 +7,45 @@ import (
"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
for _, fn := range candidates() {
m := fn()
if m.Available() {
return m, nil
}
}
return nil, snack.ErrManagerNotFound
}
// All returns all available package managers on the system.
func All() []snack.Manager {
// TODO: implement
return nil
var out []snack.Manager
for _, fn := range candidates() {
m := fn()
if m.Available() {
out = append(out, m)
}
}
return out
}
// ByName returns a specific manager by name, regardless of availability.
// Returns ErrManagerNotFound if the name is not recognized.
func ByName(name string) (snack.Manager, error) {
for _, fn := range allManagers() {
m := fn()
if m.Name() == name {
return m, nil
}
}
return nil, snack.ErrManagerNotFound
}
// managerFactory is a function that returns a new Manager instance.
type managerFactory func() snack.Manager
// HasBinary reports whether a binary is available in PATH.
func HasBinary(name string) bool {
_, err := exec.LookPath(name)

18
detect/detect_freebsd.go Normal file
View File

@@ -0,0 +1,18 @@
//go:build freebsd
package detect
import (
"github.com/gogrlx/snack"
"github.com/gogrlx/snack/pkg"
)
func candidates() []managerFactory {
return []managerFactory{
func() snack.Manager { return pkg.New() },
}
}
func allManagers() []managerFactory {
return candidates()
}

31
detect/detect_linux.go Normal file
View File

@@ -0,0 +1,31 @@
//go:build linux
package detect
import (
"github.com/gogrlx/snack"
"github.com/gogrlx/snack/apk"
"github.com/gogrlx/snack/apt"
"github.com/gogrlx/snack/dnf"
"github.com/gogrlx/snack/flatpak"
"github.com/gogrlx/snack/pacman"
"github.com/gogrlx/snack/snap"
)
// candidates returns manager factories in probe order for Linux.
// The first available manager wins for Default().
func candidates() []managerFactory {
return []managerFactory{
func() snack.Manager { return apt.New() },
func() snack.Manager { return dnf.New() },
func() snack.Manager { return pacman.New() },
func() snack.Manager { return apk.New() },
func() snack.Manager { return flatpak.New() },
func() snack.Manager { return snap.New() },
}
}
// allManagers returns all known manager factories (for ByName).
func allManagers() []managerFactory {
return candidates()
}

13
detect/detect_openbsd.go Normal file
View File

@@ -0,0 +1,13 @@
//go:build openbsd
package detect
// candidates returns manager factories in probe order for OpenBSD.
// TODO: wire up ports.New() once the ports package is implemented.
func candidates() []managerFactory {
return nil
}
func allManagers() []managerFactory {
return nil
}

22
detect/detect_test.go Normal file
View File

@@ -0,0 +1,22 @@
package detect
import (
"testing"
)
func TestByNameUnknown(t *testing.T) {
_, err := ByName("nonexistent")
if err == nil {
t.Fatal("expected error for unknown manager")
}
}
func TestAllReturnsSlice(t *testing.T) {
// Just verify it doesn't panic; actual availability depends on system.
_ = All()
}
func TestDefaultDoesNotPanic(t *testing.T) {
// May return error if no managers available; that's fine.
_, _ = Default()
}