feat(apt,dpkg): implement apt and dpkg package manager wrappers

Implement the snack.Manager interface for both apt (Debian/Ubuntu) and
dpkg (low-level Debian package tool).

apt wraps apt-get, apt-cache, and dpkg-query for full package management.
dpkg wraps dpkg and dpkg-query for low-level .deb operations.
Upgrade and Update return ErrUnsupportedPlatform for dpkg.

Both packages include:
- Linux implementations with proper error wrapping
- Non-linux build stubs returning ErrUnsupportedPlatform
- Output parsing helpers with tests
- Functional options support (sudo, assume-yes, dry-run)
This commit is contained in:
2026-02-25 20:25:19 +00:00
parent f04365e600
commit 5163a948ea
10 changed files with 838 additions and 0 deletions

77
apt/parse.go Normal file
View File

@@ -0,0 +1,77 @@
package apt
import (
"strings"
"github.com/gogrlx/snack"
)
// parseList parses dpkg-query -W output into packages.
func parseList(output string) []snack.Package {
var pkgs []snack.Package
for _, line := range strings.Split(strings.TrimSpace(output), "\n") {
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 apt-cache search output.
func parseSearch(output string) []snack.Package {
var pkgs []snack.Package
for _, line := range strings.Split(strings.TrimSpace(output), "\n") {
if line == "" {
continue
}
// Format: "package - description"
parts := strings.SplitN(line, " - ", 2)
if len(parts) < 1 {
continue
}
p := snack.Package{Name: strings.TrimSpace(parts[0])}
if len(parts) == 2 {
p.Description = strings.TrimSpace(parts[1])
}
pkgs = append(pkgs, p)
}
return pkgs
}
// parseInfo parses apt-cache show output into a Package.
func parseInfo(output string) (*snack.Package, error) {
p := &snack.Package{}
for _, line := range strings.Split(output, "\n") {
key, val, ok := strings.Cut(line, ": ")
if !ok {
continue
}
switch key {
case "Package":
p.Name = val
case "Version":
p.Version = val
case "Description":
p.Description = val
case "Architecture":
p.Arch = val
}
}
if p.Name == "" {
return nil, snack.ErrNotFound
}
return p, nil
}