fix: improve feature completeness and correctness

Pass 1 (Feature & Completeness):
- Replace apt CLI with apt-get for listUpgrades (apt CLI is unstable for scripting)
- Verify snapd daemon is running in snap Available() check
- Add ErrDaemonNotRunning sentinel error for daemon-dependent managers
- Fix staticcheck S1011: replace loop with append(keys, matches...)
- Fix staticcheck SA1012: use context.TODO() instead of nil in dpkg tests
This commit is contained in:
2026-02-26 01:26:51 +00:00
parent d4c7a058fb
commit 6edb79df3f
6 changed files with 45 additions and 32 deletions

View File

@@ -37,37 +37,43 @@ func latestVersion(ctx context.Context, pkg string) (string, error) {
}
func listUpgrades(ctx context.Context) ([]snack.Package, error) {
cmd := exec.CommandContext(ctx, "apt", "list", "--upgradable")
cmd.Env = append(os.Environ(), "LANG=C")
// Use apt-get --just-print upgrade instead of `apt list --upgradable`
// because `apt` has unstable CLI output not intended for scripting.
cmd := exec.CommandContext(ctx, "apt-get", "--just-print", "upgrade")
cmd.Env = append(os.Environ(), "LANG=C", "DEBIAN_FRONTEND=noninteractive")
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("apt list --upgradable: %w", err)
return nil, fmt.Errorf("apt-get --just-print upgrade: %w", err)
}
var pkgs []snack.Package
for _, line := range strings.Split(string(out), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "Listing...") {
// Lines starting with "Inst " indicate upgradable packages.
// Format: "Inst pkg [old-ver] (new-ver repo [arch])"
if !strings.HasPrefix(line, "Inst ") {
continue
}
// Format: "pkg/source version arch [upgradable from: old-version]"
slashIdx := strings.Index(line, "/")
if slashIdx < 0 {
continue
}
name := line[:slashIdx]
rest := line[slashIdx+1:]
fields := strings.Fields(rest)
line = strings.TrimPrefix(line, "Inst ")
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
name := fields[0]
// Find the new version in parentheses
parenStart := strings.Index(line, "(")
parenEnd := strings.Index(line, ")")
if parenStart < 0 || parenEnd < 0 {
continue
}
verFields := strings.Fields(line[parenStart+1 : parenEnd])
if len(verFields) < 1 {
continue
}
p := snack.Package{
Name: name,
Version: fields[1],
Version: verFields[0],
Installed: true,
}
if len(fields) > 2 {
p.Arch = fields[2]
}
pkgs = append(pkgs, p)
}
return pkgs, nil
@@ -361,9 +367,7 @@ func listKeys(ctx context.Context) ([]string, error) {
// List keyring files
matches, _ := filepath.Glob("/etc/apt/keyrings/*.gpg")
for _, m := range matches {
keys = append(keys, m)
}
keys = append(keys, matches...)
ascMatches, _ := filepath.Glob("/etc/apt/keyrings/*.asc")
keys = append(keys, ascMatches...)

View File

@@ -4,14 +4,14 @@ package snack
// 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
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.

View File

@@ -1,6 +1,7 @@
package dpkg
import (
"context"
"testing"
"github.com/gogrlx/snack"
@@ -73,14 +74,14 @@ func TestNew(t *testing.T) {
func TestUpgradeUnsupported(t *testing.T) {
d := New()
if err := d.Upgrade(nil); err != snack.ErrUnsupportedPlatform {
if err := d.Upgrade(context.TODO()); err != snack.ErrUnsupportedPlatform {
t.Errorf("expected ErrUnsupportedPlatform, got %v", err)
}
}
func TestUpdateUnsupported(t *testing.T) {
d := New()
if err := d.Update(nil); err != snack.ErrUnsupportedPlatform {
if err := d.Update(context.TODO()); err != snack.ErrUnsupportedPlatform {
t.Errorf("expected ErrUnsupportedPlatform, got %v", err)
}
}

View File

@@ -28,4 +28,8 @@ var (
// ErrManagerNotFound is returned by detect when no supported package
// manager can be found on the system.
ErrManagerNotFound = errors.New("no supported package manager found")
// ErrDaemonNotRunning is returned when a package manager's required
// daemon (e.g. snapd) is not running.
ErrDaemonNotRunning = errors.New("package manager daemon is not running")
)

View File

@@ -13,8 +13,12 @@ import (
)
func available() bool {
_, err := exec.LookPath("snap")
return err == nil
if _, err := exec.LookPath("snap"); err != nil {
return false
}
// Verify snapd is running by checking snap version (requires daemon).
cmd := exec.Command("snap", "version")
return cmd.Run() == nil
}
func run(ctx context.Context, args []string) (string, error) {

View File

@@ -69,7 +69,7 @@ func WithReinstall() Option {
// Repository represents a configured package repository.
type Repository struct {
ID string `json:"id"` // unique identifier
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