mirror of
https://github.com/gogrlx/snack.git
synced 2026-04-02 05:08:42 -07:00
Merge pull request #10 from gogrlx/cd/detect-cli
feat: implement detect package and snack CLI
This commit is contained in:
@@ -2,22 +2,408 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/fang"
|
||||||
|
"github.com/gogrlx/snack"
|
||||||
|
"github.com/gogrlx/snack/detect"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var (
|
||||||
Use: "snack",
|
version = "dev"
|
||||||
Short: "A unified CLI for system package managers",
|
flagMgr string
|
||||||
Long: `snack wraps system package managers (apt, pacman, apk, dnf, and more)
|
flagSudo bool
|
||||||
behind a single, consistent interface.`,
|
flagYes bool
|
||||||
}
|
flagDry bool
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := rootCmd.Execute(); err != nil {
|
rootCmd := &cobra.Command{
|
||||||
fmt.Fprintln(os.Stderr, err)
|
Use: "snack",
|
||||||
|
Short: "A unified CLI for system package managers",
|
||||||
|
Long: `snack wraps system package managers (apt, pacman, apk, dnf, and more)
|
||||||
|
behind a single, consistent interface.`,
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCmd.PersistentFlags().StringVar(&flagMgr, "manager", "", "override auto-detected package manager")
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&flagSudo, "sudo", false, "run with sudo")
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&flagYes, "yes", false, "assume yes to prompts")
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&flagDry, "dry-run", false, "simulate the operation")
|
||||||
|
|
||||||
|
rootCmd.AddCommand(
|
||||||
|
installCmd(),
|
||||||
|
removeCmd(),
|
||||||
|
purgeCmd(),
|
||||||
|
upgradeCmd(),
|
||||||
|
updateCmd(),
|
||||||
|
listCmd(),
|
||||||
|
searchCmd(),
|
||||||
|
infoCmd(),
|
||||||
|
whichCmd(),
|
||||||
|
holdCmd(),
|
||||||
|
unholdCmd(),
|
||||||
|
cleanCmd(),
|
||||||
|
detectCmd(),
|
||||||
|
versionCmd(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := fang.Execute(context.Background(), rootCmd); err != nil {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getManager() (snack.Manager, error) {
|
||||||
|
if flagMgr != "" {
|
||||||
|
m, err := detect.ByName(flagMgr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unknown manager %q", flagMgr)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return detect.Default()
|
||||||
|
}
|
||||||
|
|
||||||
|
func opts() []snack.Option {
|
||||||
|
var o []snack.Option
|
||||||
|
if flagSudo {
|
||||||
|
o = append(o, snack.WithSudo())
|
||||||
|
}
|
||||||
|
if flagYes {
|
||||||
|
o = append(o, snack.WithAssumeYes())
|
||||||
|
}
|
||||||
|
if flagDry {
|
||||||
|
o = append(o, snack.WithDryRun())
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
func targets(args []string, ver string) []snack.Target {
|
||||||
|
t := snack.Targets(args...)
|
||||||
|
if ver != "" && len(t) > 0 {
|
||||||
|
for i := range t {
|
||||||
|
t[i].Version = ver
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func installCmd() *cobra.Command {
|
||||||
|
var ver string
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "install <packages...>",
|
||||||
|
Short: "Install packages",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
m, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return m.Install(cmd.Context(), targets(args, ver), opts()...)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVar(&ver, "version", "", "pin version for all targets")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "remove <packages...>",
|
||||||
|
Short: "Remove packages",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
m, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return m.Remove(cmd.Context(), snack.Targets(args...), opts()...)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func purgeCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "purge <packages...>",
|
||||||
|
Short: "Purge packages (remove including config files)",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
m, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return m.Purge(cmd.Context(), snack.Targets(args...), opts()...)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func upgradeCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "upgrade",
|
||||||
|
Short: "Upgrade all installed packages",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
m, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return m.Upgrade(cmd.Context(), opts()...)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "update",
|
||||||
|
Short: "Refresh the package index",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
m, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return m.Update(cmd.Context())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func listCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List installed packages",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
m, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pkgs, err := m.List(cmd.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, p := range pkgs {
|
||||||
|
if p.Version != "" {
|
||||||
|
fmt.Printf("%s %s\n", p.Name, p.Version)
|
||||||
|
} else {
|
||||||
|
fmt.Println(p.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "search <query>",
|
||||||
|
Short: "Search for packages",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
m, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pkgs, err := m.Search(cmd.Context(), args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, p := range pkgs {
|
||||||
|
line := p.Name
|
||||||
|
if p.Version != "" {
|
||||||
|
line += " " + p.Version
|
||||||
|
}
|
||||||
|
if p.Description != "" {
|
||||||
|
line += " - " + p.Description
|
||||||
|
}
|
||||||
|
fmt.Println(line)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func infoCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "info <package>",
|
||||||
|
Short: "Show package information",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
m, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p, err := m.Info(cmd.Context(), args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("Name: %s\n", p.Name)
|
||||||
|
fmt.Printf("Version: %s\n", p.Version)
|
||||||
|
if p.Description != "" {
|
||||||
|
fmt.Printf("Description: %s\n", p.Description)
|
||||||
|
}
|
||||||
|
if p.Arch != "" {
|
||||||
|
fmt.Printf("Arch: %s\n", p.Arch)
|
||||||
|
}
|
||||||
|
if p.Repository != "" {
|
||||||
|
fmt.Printf("Repository: %s\n", p.Repository)
|
||||||
|
}
|
||||||
|
fmt.Printf("Installed: %v\n", p.Installed)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func whichCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "which <path>",
|
||||||
|
Short: "Find the package that owns a file",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
m, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fo, ok := m.(snack.FileOwner)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%s does not support file ownership queries", m.Name())
|
||||||
|
}
|
||||||
|
pkg, err := fo.Owner(cmd.Context(), args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(pkg)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func holdCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "hold <packages...>",
|
||||||
|
Short: "Hold packages at their current version",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
m, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h, ok := m.(snack.Holder)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%s does not support hold/unhold", m.Name())
|
||||||
|
}
|
||||||
|
return h.Hold(cmd.Context(), args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unholdCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "unhold <packages...>",
|
||||||
|
Short: "Remove version hold from packages",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
m, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h, ok := m.(snack.Holder)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%s does not support hold/unhold", m.Name())
|
||||||
|
}
|
||||||
|
return h.Unhold(cmd.Context(), args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "clean",
|
||||||
|
Short: "Autoremove unused packages and clean cache",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
m, err := getManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c, ok := m.(snack.Cleaner)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%s does not support clean operations", m.Name())
|
||||||
|
}
|
||||||
|
if err := c.Autoremove(cmd.Context(), opts()...); err != nil {
|
||||||
|
return fmt.Errorf("autoremove: %w", err)
|
||||||
|
}
|
||||||
|
if err := c.Clean(cmd.Context()); err != nil {
|
||||||
|
return fmt.Errorf("clean: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "detect",
|
||||||
|
Short: "Show detected package manager(s) and capabilities",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(_ *cobra.Command, _ []string) error {
|
||||||
|
all := detect.All()
|
||||||
|
if len(all) == 0 {
|
||||||
|
fmt.Println("No supported package managers detected.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
def, _ := detect.Default()
|
||||||
|
for _, m := range all {
|
||||||
|
marker := " "
|
||||||
|
if def != nil && m.Name() == def.Name() {
|
||||||
|
marker = "* "
|
||||||
|
}
|
||||||
|
caps := snack.GetCapabilities(m)
|
||||||
|
var capList []string
|
||||||
|
if caps.VersionQuery {
|
||||||
|
capList = append(capList, "version-query")
|
||||||
|
}
|
||||||
|
if caps.Hold {
|
||||||
|
capList = append(capList, "hold")
|
||||||
|
}
|
||||||
|
if caps.Clean {
|
||||||
|
capList = append(capList, "clean")
|
||||||
|
}
|
||||||
|
if caps.FileOwnership {
|
||||||
|
capList = append(capList, "file-owner")
|
||||||
|
}
|
||||||
|
if caps.RepoManagement {
|
||||||
|
capList = append(capList, "repo")
|
||||||
|
}
|
||||||
|
if caps.KeyManagement {
|
||||||
|
capList = append(capList, "keys")
|
||||||
|
}
|
||||||
|
if caps.Groups {
|
||||||
|
capList = append(capList, "groups")
|
||||||
|
}
|
||||||
|
if caps.NameNormalize {
|
||||||
|
capList = append(capList, "normalize")
|
||||||
|
}
|
||||||
|
capStr := ""
|
||||||
|
if len(capList) > 0 {
|
||||||
|
capStr = " [" + strings.Join(capList, ", ") + "]"
|
||||||
|
}
|
||||||
|
fmt.Printf("%s%s%s\n", marker, m.Name(), capStr)
|
||||||
|
}
|
||||||
|
fmt.Println("\n* = default")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func versionCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Short: "Show snack version",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Run: func(_ *cobra.Command, _ []string) {
|
||||||
|
fmt.Printf("snack %s\n", version)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,35 +7,45 @@ import (
|
|||||||
"github.com/gogrlx/snack"
|
"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.
|
// Default returns the first available package manager on the system.
|
||||||
// Returns ErrManagerNotFound if no supported manager is detected.
|
// Returns ErrManagerNotFound if no supported manager is detected.
|
||||||
func Default() (snack.Manager, error) {
|
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
|
return nil, snack.ErrManagerNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// All returns all available package managers on the system.
|
// All returns all available package managers on the system.
|
||||||
func All() []snack.Manager {
|
func All() []snack.Manager {
|
||||||
// TODO: implement
|
var out []snack.Manager
|
||||||
return nil
|
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.
|
// HasBinary reports whether a binary is available in PATH.
|
||||||
func HasBinary(name string) bool {
|
func HasBinary(name string) bool {
|
||||||
_, err := exec.LookPath(name)
|
_, err := exec.LookPath(name)
|
||||||
|
|||||||
18
detect/detect_freebsd.go
Normal file
18
detect/detect_freebsd.go
Normal 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
31
detect/detect_linux.go
Normal 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
13
detect/detect_openbsd.go
Normal 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
22
detect/detect_test.go
Normal 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()
|
||||||
|
}
|
||||||
28
go.mod
28
go.mod
@@ -2,9 +2,35 @@ module github.com/gogrlx/snack
|
|||||||
|
|
||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require github.com/spf13/cobra v1.10.2
|
require (
|
||||||
|
github.com/charmbracelet/fang v0.4.4
|
||||||
|
github.com/spf13/cobra v1.10.2
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.3.3 // indirect
|
||||||
|
github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.0 // indirect
|
||||||
|
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||||
|
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||||
|
github.com/clipperhouse/displaywidth v0.4.1 // indirect
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/mango v0.1.0 // indirect
|
||||||
|
github.com/muesli/mango-cobra v1.2.0 // indirect
|
||||||
|
github.com/muesli/mango-pflag v0.1.0 // indirect
|
||||||
|
github.com/muesli/roff v0.1.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/spf13/pflag v1.0.9 // indirect
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
|
golang.org/x/text v0.24.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
62
go.sum
62
go.sum
@@ -1,10 +1,72 @@
|
|||||||
|
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 h1:D9PbaszZYpB4nj+d6HTWr1onlmlyuGVNfL9gAi8iB3k=
|
||||||
|
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||||
|
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
|
||||||
|
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
|
||||||
|
github.com/charmbracelet/fang v0.4.4 h1:G4qKxF6or/eTPgmAolwPuRNyuci3hTUGGX1rj1YkHJY=
|
||||||
|
github.com/charmbracelet/fang v0.4.4/go.mod h1:P5/DNb9DddQ0Z0dbc0P3ol4/ix5Po7Ofr2KMBfAqoCo=
|
||||||
|
github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692 h1:r/3jQZ1LjWW6ybp8HHfhrKrwHIWiJhUuY7wwYIWZulQ=
|
||||||
|
github.com/charmbracelet/ultraviolet v0.0.0-20251106190538-99ea45596692/go.mod h1:Y8B4DzWeTb0ama8l3+KyopZtkE8fZjwRQ3aEAPEXHE0=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.0 h1:uuIVK7GIplwX6UBIz8S2TF8nkr7xRlygSsBRjSJqIvA=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.0/go.mod h1:uQt8bOrq/xgXjlGcFMc8U2WYbnxyjrKhnvTQluvfCaE=
|
||||||
|
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0=
|
||||||
|
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||||
|
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
||||||
|
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||||
|
github.com/clipperhouse/displaywidth v0.4.1 h1:uVw9V8UDfnggg3K2U84VWY1YLQ/x2aKSCtkRyYozfoU=
|
||||||
|
github.com/clipperhouse/displaywidth v0.4.1/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI=
|
||||||
|
github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
|
||||||
|
github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg=
|
||||||
|
github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA=
|
||||||
|
github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg=
|
||||||
|
github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0=
|
||||||
|
github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
|
||||||
|
github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
|
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||||
|
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
58
pkg/capabilities.go
Normal file
58
pkg/capabilities.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogrlx/snack"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Compile-time interface checks.
|
||||||
|
var (
|
||||||
|
_ snack.VersionQuerier = (*Pkg)(nil)
|
||||||
|
_ snack.Cleaner = (*Pkg)(nil)
|
||||||
|
_ snack.FileOwner = (*Pkg)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// LatestVersion returns the latest available version from configured repositories.
|
||||||
|
func (p *Pkg) LatestVersion(ctx context.Context, pkg string) (string, error) {
|
||||||
|
return latestVersion(ctx, pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUpgrades returns packages that have newer versions available.
|
||||||
|
func (p *Pkg) ListUpgrades(ctx context.Context) ([]snack.Package, error) {
|
||||||
|
return listUpgrades(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpgradeAvailable reports whether a newer version is available.
|
||||||
|
func (p *Pkg) UpgradeAvailable(ctx context.Context, pkg string) (bool, error) {
|
||||||
|
return upgradeAvailable(ctx, pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VersionCmp compares two version strings using pkg's version comparison.
|
||||||
|
func (p *Pkg) VersionCmp(ctx context.Context, ver1, ver2 string) (int, error) {
|
||||||
|
return versionCmp(ctx, ver1, ver2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Autoremove removes packages no longer required as dependencies.
|
||||||
|
func (p *Pkg) Autoremove(ctx context.Context, opts ...snack.Option) error {
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
return autoremove(ctx, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean removes cached package files.
|
||||||
|
func (p *Pkg) Clean(ctx context.Context) error {
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
return clean(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileList returns all files installed by a package.
|
||||||
|
func (p *Pkg) FileList(ctx context.Context, pkg string) ([]string, error) {
|
||||||
|
return fileList(ctx, pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Owner returns the package that owns a given file path.
|
||||||
|
func (p *Pkg) Owner(ctx context.Context, path string) (string, error) {
|
||||||
|
return owner(ctx, path)
|
||||||
|
}
|
||||||
102
pkg/capabilities_freebsd.go
Normal file
102
pkg/capabilities_freebsd.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
//go:build freebsd
|
||||||
|
|
||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gogrlx/snack"
|
||||||
|
)
|
||||||
|
|
||||||
|
func latestVersion(ctx context.Context, pkg string) (string, error) {
|
||||||
|
out, err := run(ctx, []string{"rquery", "%v", pkg}, snack.Options{})
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "exit status 1") || strings.Contains(err.Error(), "exit status 70") {
|
||||||
|
return "", fmt.Errorf("pkg latestVersion %s: %w", pkg, snack.ErrNotFound)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("pkg latestVersion: %w", err)
|
||||||
|
}
|
||||||
|
v := strings.TrimSpace(out)
|
||||||
|
if v == "" {
|
||||||
|
return "", fmt.Errorf("pkg latestVersion %s: %w", pkg, snack.ErrNotFound)
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func listUpgrades(ctx context.Context) ([]snack.Package, error) {
|
||||||
|
out, err := run(ctx, []string{"upgrade", "-n"}, snack.Options{})
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "exit status 1") {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("pkg listUpgrades: %w", err)
|
||||||
|
}
|
||||||
|
return parseUpgrades(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func upgradeAvailable(ctx context.Context, pkg string) (bool, error) {
|
||||||
|
upgrades, err := listUpgrades(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
for _, u := range upgrades {
|
||||||
|
if u.Name == pkg {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func versionCmp(ctx context.Context, ver1, ver2 string) (int, error) {
|
||||||
|
c := exec.CommandContext(ctx, "pkg", "version", "-t", ver1, ver2)
|
||||||
|
out, err := c.Output()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("pkg version -t: %w", err)
|
||||||
|
}
|
||||||
|
switch strings.TrimSpace(string(out)) {
|
||||||
|
case "<":
|
||||||
|
return -1, nil
|
||||||
|
case ">":
|
||||||
|
return 1, nil
|
||||||
|
case "=":
|
||||||
|
return 0, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("pkg version -t: unexpected output %q", string(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func autoremove(ctx context.Context, opts ...snack.Option) error {
|
||||||
|
o := snack.ApplyOptions(opts...)
|
||||||
|
_, err := run(ctx, []string{"autoremove", "-y"}, o)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func clean(ctx context.Context) error {
|
||||||
|
_, err := run(ctx, []string{"clean", "-y"}, snack.Options{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileList(ctx context.Context, pkg string) ([]string, error) {
|
||||||
|
out, err := run(ctx, []string{"info", "-l", pkg}, snack.Options{})
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "exit status 1") || strings.Contains(err.Error(), "exit status 70") {
|
||||||
|
return nil, fmt.Errorf("pkg fileList %s: %w", pkg, snack.ErrNotInstalled)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("pkg fileList: %w", err)
|
||||||
|
}
|
||||||
|
return parseFileList(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func owner(ctx context.Context, path string) (string, error) {
|
||||||
|
out, err := run(ctx, []string{"which", path}, snack.Options{})
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "exit status 1") || strings.Contains(err.Error(), "was not found") {
|
||||||
|
return "", fmt.Errorf("pkg owner %s: %w", path, snack.ErrNotFound)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("pkg owner: %w", err)
|
||||||
|
}
|
||||||
|
return parseOwner(out), nil
|
||||||
|
}
|
||||||
41
pkg/capabilities_other.go
Normal file
41
pkg/capabilities_other.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
//go:build !freebsd
|
||||||
|
|
||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogrlx/snack"
|
||||||
|
)
|
||||||
|
|
||||||
|
func latestVersion(_ context.Context, _ string) (string, error) {
|
||||||
|
return "", snack.ErrUnsupportedPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func listUpgrades(_ context.Context) ([]snack.Package, error) {
|
||||||
|
return nil, snack.ErrUnsupportedPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func upgradeAvailable(_ context.Context, _ string) (bool, error) {
|
||||||
|
return false, snack.ErrUnsupportedPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func versionCmp(_ context.Context, _, _ string) (int, error) {
|
||||||
|
return 0, snack.ErrUnsupportedPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func autoremove(_ context.Context, _ ...snack.Option) error {
|
||||||
|
return snack.ErrUnsupportedPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func clean(_ context.Context) error {
|
||||||
|
return snack.ErrUnsupportedPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileList(_ context.Context, _ string) ([]string, error) {
|
||||||
|
return nil, snack.ErrUnsupportedPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func owner(_ context.Context, _ string) (string, error) {
|
||||||
|
return "", snack.ErrUnsupportedPlatform
|
||||||
|
}
|
||||||
162
pkg/parse.go
Normal file
162
pkg/parse.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gogrlx/snack"
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseQuery parses the output of `pkg query '%n\t%v\t%c'`.
|
||||||
|
func parseQuery(output string) []snack.Package {
|
||||||
|
var pkgs []snack.Package
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(line, "\t", 3)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p := snack.Package{
|
||||||
|
Name: parts[0],
|
||||||
|
Version: parts[1],
|
||||||
|
Installed: true,
|
||||||
|
}
|
||||||
|
if len(parts) == 3 {
|
||||||
|
p.Description = parts[2]
|
||||||
|
}
|
||||||
|
pkgs = append(pkgs, p)
|
||||||
|
}
|
||||||
|
return pkgs
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSearch parses the output of `pkg search <query>`.
|
||||||
|
// Format: "name-version Comment text"
|
||||||
|
func parseSearch(output string) []snack.Package {
|
||||||
|
var pkgs []snack.Package
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Split on whitespace; first field is name-version
|
||||||
|
parts := strings.SplitN(line, " ", 2)
|
||||||
|
nameVer := parts[0]
|
||||||
|
name, ver := splitNameVersion(nameVer)
|
||||||
|
p := snack.Package{
|
||||||
|
Name: name,
|
||||||
|
Version: ver,
|
||||||
|
}
|
||||||
|
if len(parts) == 2 {
|
||||||
|
p.Description = strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
pkgs = append(pkgs, p)
|
||||||
|
}
|
||||||
|
return pkgs
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseInfo parses the output of `pkg info <pkg>`.
|
||||||
|
// Format is "Key: Value" lines.
|
||||||
|
func parseInfo(output string) *snack.Package {
|
||||||
|
pkg := &snack.Package{Installed: true}
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
idx := strings.Index(line, ":")
|
||||||
|
if idx < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.TrimSpace(line[:idx])
|
||||||
|
val := strings.TrimSpace(line[idx+1:])
|
||||||
|
switch key {
|
||||||
|
case "Name":
|
||||||
|
pkg.Name = val
|
||||||
|
case "Version":
|
||||||
|
pkg.Version = val
|
||||||
|
case "Comment":
|
||||||
|
pkg.Description = val
|
||||||
|
case "Arch":
|
||||||
|
pkg.Arch = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pkg.Name == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return pkg
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseUpgrades parses the output of `pkg upgrade -n`.
|
||||||
|
// Looks for lines like:
|
||||||
|
//
|
||||||
|
// Installing pkg-name: oldver -> newver
|
||||||
|
// Upgrading pkg-name: oldver -> newver
|
||||||
|
func parseUpgrades(output string) []snack.Package {
|
||||||
|
var pkgs []snack.Package
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
// Look for "Upgrading" or "Reinstalling" lines with ->
|
||||||
|
if !strings.Contains(line, "->") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var nameVer string
|
||||||
|
if strings.HasPrefix(line, "Upgrading ") {
|
||||||
|
nameVer = strings.TrimPrefix(line, "Upgrading ")
|
||||||
|
} else if strings.HasPrefix(line, "Installing ") {
|
||||||
|
nameVer = strings.TrimPrefix(line, "Installing ")
|
||||||
|
} else if strings.HasPrefix(line, "Reinstalling ") {
|
||||||
|
nameVer = strings.TrimPrefix(line, "Reinstalling ")
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// "name: oldver -> newver"
|
||||||
|
colonIdx := strings.Index(nameVer, ":")
|
||||||
|
if colonIdx < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(nameVer[:colonIdx])
|
||||||
|
rest := strings.TrimSpace(nameVer[colonIdx+1:])
|
||||||
|
parts := strings.Fields(rest)
|
||||||
|
if len(parts) >= 3 && parts[1] == "->" {
|
||||||
|
pkgs = append(pkgs, snack.Package{
|
||||||
|
Name: name,
|
||||||
|
Version: parts[2],
|
||||||
|
Installed: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pkgs
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFileList parses `pkg info -l <pkg>` output.
|
||||||
|
// Lines starting with "/" after the header are file paths.
|
||||||
|
func parseFileList(output string) []string {
|
||||||
|
var files []string
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(line, "/") {
|
||||||
|
files = append(files, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOwner parses `pkg which <path>` output.
|
||||||
|
// Format: "/path was installed by package name-version"
|
||||||
|
func parseOwner(output string) string {
|
||||||
|
output = strings.TrimSpace(output)
|
||||||
|
if idx := strings.Index(output, "was installed by package "); idx != -1 {
|
||||||
|
remainder := output[idx+len("was installed by package "):]
|
||||||
|
name, _ := splitNameVersion(strings.TrimSpace(remainder))
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitNameVersion splits "name-version" into name and version.
|
||||||
|
// FreeBSD pkg uses the last hyphen as separator (name can contain hyphens).
|
||||||
|
func splitNameVersion(s string) (string, string) {
|
||||||
|
idx := strings.LastIndex(s, "-")
|
||||||
|
if idx <= 0 {
|
||||||
|
return s, ""
|
||||||
|
}
|
||||||
|
return s[:idx], s[idx+1:]
|
||||||
|
}
|
||||||
85
pkg/pkg.go
85
pkg/pkg.go
@@ -1,2 +1,87 @@
|
|||||||
// Package pkg provides Go bindings for pkg(8) (FreeBSD package manager).
|
// Package pkg provides Go bindings for pkg(8) (FreeBSD package manager).
|
||||||
package pkg
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogrlx/snack"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pkg wraps the FreeBSD pkg(8) package manager CLI.
|
||||||
|
type Pkg struct {
|
||||||
|
snack.Locker
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new Pkg manager.
|
||||||
|
func New() *Pkg {
|
||||||
|
return &Pkg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns "pkg".
|
||||||
|
func (p *Pkg) Name() string { return "pkg" }
|
||||||
|
|
||||||
|
// Available reports whether pkg is present on the system.
|
||||||
|
func (p *Pkg) Available() bool { return available() }
|
||||||
|
|
||||||
|
// Install one or more packages.
|
||||||
|
func (p *Pkg) Install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
return install(ctx, pkgs, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove one or more packages.
|
||||||
|
func (p *Pkg) Remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
return remove(ctx, pkgs, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Purge removes packages with force (removing dependent packages).
|
||||||
|
func (p *Pkg) Purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
return purge(ctx, pkgs, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upgrade all installed packages to their latest versions.
|
||||||
|
func (p *Pkg) Upgrade(ctx context.Context, opts ...snack.Option) error {
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
return upgrade(ctx, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update refreshes the package database.
|
||||||
|
func (p *Pkg) Update(ctx context.Context) error {
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
return update(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns all installed packages.
|
||||||
|
func (p *Pkg) List(ctx context.Context) ([]snack.Package, error) {
|
||||||
|
return list(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search queries the repositories for packages matching the query.
|
||||||
|
func (p *Pkg) Search(ctx context.Context, query string) ([]snack.Package, error) {
|
||||||
|
return search(ctx, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info returns details about a specific package.
|
||||||
|
func (p *Pkg) Info(ctx context.Context, pkg string) (*snack.Package, error) {
|
||||||
|
return info(ctx, pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsInstalled reports whether a package is currently installed.
|
||||||
|
func (p *Pkg) IsInstalled(ctx context.Context, pkg string) (bool, error) {
|
||||||
|
return isInstalled(ctx, pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version returns the installed version of a package.
|
||||||
|
func (p *Pkg) Version(ctx context.Context, pkg string) (string, error) {
|
||||||
|
return version(ctx, pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify interface compliance at compile time.
|
||||||
|
var _ snack.Manager = (*Pkg)(nil)
|
||||||
|
|||||||
148
pkg/pkg_freebsd.go
Normal file
148
pkg/pkg_freebsd.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
//go:build freebsd
|
||||||
|
|
||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gogrlx/snack"
|
||||||
|
)
|
||||||
|
|
||||||
|
func available() bool {
|
||||||
|
_, err := exec.LookPath("pkg")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(ctx context.Context, args []string, opts snack.Options) (string, error) {
|
||||||
|
cmdName := "pkg"
|
||||||
|
cmdArgs := make([]string, 0, len(args)+2)
|
||||||
|
cmdArgs = append(cmdArgs, args...)
|
||||||
|
|
||||||
|
if opts.Sudo {
|
||||||
|
cmdArgs = append([]string{cmdName}, cmdArgs...)
|
||||||
|
cmdName = "sudo"
|
||||||
|
}
|
||||||
|
|
||||||
|
c := exec.CommandContext(ctx, cmdName, cmdArgs...)
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
c.Stdout = &stdout
|
||||||
|
c.Stderr = &stderr
|
||||||
|
err := c.Run()
|
||||||
|
if err != nil {
|
||||||
|
se := stderr.String()
|
||||||
|
if strings.Contains(se, "permission denied") || strings.Contains(se, "Insufficient privileges") {
|
||||||
|
return "", fmt.Errorf("pkg: %w", snack.ErrPermissionDenied)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("pkg: %s: %w", strings.TrimSpace(se), err)
|
||||||
|
}
|
||||||
|
return stdout.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatTargets(targets []snack.Target) []string {
|
||||||
|
args := make([]string, 0, len(targets))
|
||||||
|
for _, t := range targets {
|
||||||
|
if t.Version != "" {
|
||||||
|
args = append(args, t.Name+"-"+t.Version)
|
||||||
|
} else {
|
||||||
|
args = append(args, t.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func install(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||||
|
o := snack.ApplyOptions(opts...)
|
||||||
|
args := append([]string{"install", "-y"}, formatTargets(pkgs)...)
|
||||||
|
_, err := run(ctx, args, o)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||||
|
o := snack.ApplyOptions(opts...)
|
||||||
|
args := append([]string{"delete", "-y"}, snack.TargetNames(pkgs)...)
|
||||||
|
_, err := run(ctx, args, o)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func purge(ctx context.Context, pkgs []snack.Target, opts ...snack.Option) error {
|
||||||
|
o := snack.ApplyOptions(opts...)
|
||||||
|
args := append([]string{"delete", "-y", "-f"}, snack.TargetNames(pkgs)...)
|
||||||
|
_, err := run(ctx, args, o)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func upgrade(ctx context.Context, opts ...snack.Option) error {
|
||||||
|
o := snack.ApplyOptions(opts...)
|
||||||
|
_, err := run(ctx, []string{"upgrade", "-y"}, o)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(ctx context.Context) error {
|
||||||
|
_, err := run(ctx, []string{"update"}, snack.Options{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func list(ctx context.Context) ([]snack.Package, error) {
|
||||||
|
out, err := run(ctx, []string{"query", "%n\t%v\t%c"}, snack.Options{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pkg list: %w", err)
|
||||||
|
}
|
||||||
|
return parseQuery(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func search(ctx context.Context, query string) ([]snack.Package, error) {
|
||||||
|
out, err := run(ctx, []string{"search", query}, snack.Options{})
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "exit status 1") {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("pkg search: %w", err)
|
||||||
|
}
|
||||||
|
return parseSearch(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func info(ctx context.Context, pkg string) (*snack.Package, error) {
|
||||||
|
out, err := run(ctx, []string{"info", pkg}, snack.Options{})
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "exit status 1") || strings.Contains(err.Error(), "exit status 70") {
|
||||||
|
return nil, fmt.Errorf("pkg info %s: %w", pkg, snack.ErrNotFound)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("pkg info: %w", err)
|
||||||
|
}
|
||||||
|
p := parseInfo(out)
|
||||||
|
if p == nil {
|
||||||
|
return nil, fmt.Errorf("pkg info %s: %w", pkg, snack.ErrNotFound)
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isInstalled(ctx context.Context, pkg string) (bool, error) {
|
||||||
|
c := exec.CommandContext(ctx, "pkg", "info", pkg)
|
||||||
|
err := c.Run()
|
||||||
|
if err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() != 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("pkg isInstalled: %w", err)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func version(ctx context.Context, pkg string) (string, error) {
|
||||||
|
out, err := run(ctx, []string{"query", "%v", pkg}, snack.Options{})
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "exit status 1") || strings.Contains(err.Error(), "exit status 70") {
|
||||||
|
return "", fmt.Errorf("pkg version %s: %w", pkg, snack.ErrNotInstalled)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("pkg version: %w", err)
|
||||||
|
}
|
||||||
|
v := strings.TrimSpace(out)
|
||||||
|
if v == "" {
|
||||||
|
return "", fmt.Errorf("pkg version %s: %w", pkg, snack.ErrNotInstalled)
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
51
pkg/pkg_other.go
Normal file
51
pkg/pkg_other.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
//go:build !freebsd
|
||||||
|
|
||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gogrlx/snack"
|
||||||
|
)
|
||||||
|
|
||||||
|
func available() bool { return false }
|
||||||
|
|
||||||
|
func install(_ context.Context, _ []snack.Target, _ ...snack.Option) error {
|
||||||
|
return snack.ErrUnsupportedPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(_ context.Context, _ []snack.Target, _ ...snack.Option) error {
|
||||||
|
return snack.ErrUnsupportedPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func purge(_ context.Context, _ []snack.Target, _ ...snack.Option) error {
|
||||||
|
return snack.ErrUnsupportedPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func upgrade(_ context.Context, _ ...snack.Option) error {
|
||||||
|
return snack.ErrUnsupportedPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(_ context.Context) error {
|
||||||
|
return snack.ErrUnsupportedPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func list(_ context.Context) ([]snack.Package, error) {
|
||||||
|
return nil, snack.ErrUnsupportedPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func search(_ context.Context, _ string) ([]snack.Package, error) {
|
||||||
|
return nil, snack.ErrUnsupportedPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func info(_ context.Context, _ string) (*snack.Package, error) {
|
||||||
|
return nil, snack.ErrUnsupportedPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func isInstalled(_ context.Context, _ string) (bool, error) {
|
||||||
|
return false, snack.ErrUnsupportedPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func version(_ context.Context, _ string) (string, error) {
|
||||||
|
return "", snack.ErrUnsupportedPlatform
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user