diff --git a/install.go b/install.go index abdfdc4..0aeb876 100644 --- a/install.go +++ b/install.go @@ -21,6 +21,8 @@ package main import ( "fmt" + "log/slog" + "strings" "github.com/leonelquinteros/gotext" "github.com/urfave/cli/v2" @@ -110,25 +112,52 @@ func InstallCmd() *cli.Command { return nil }), BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error { - ctx := c.Context deps, err := appbuilder. New(ctx). WithConfig(). WithDB(). + WithManager(). Build() if err != nil { return err } defer deps.Defer() + seen := make(map[string]struct{}) + + var prefix string + if c.Args().Len() > 0 { + prefix = c.Args().Get(c.Args().Len() - 1) + if strings.HasPrefix(prefix, "-") { + prefix = "" + } + } + result, err := deps.DB.GetPkgs(c.Context, "true") if err != nil { return cliutils.FormatCliExit(gotext.Get("Error getting packages"), err) } for _, pkg := range result { - fmt.Println(pkg.Name) + if prefix == "" || strings.HasPrefix(pkg.Name, prefix) { + if _, ok := seen[pkg.Name]; !ok { + seen[pkg.Name] = struct{}{} + fmt.Println(pkg.Name) + } + } + } + + sysPkgs, err := deps.Manager.ListAvailable(prefix) + if err != nil { + slog.Debug("failed to list system packages", "err", err) + } else { + for _, name := range sysPkgs { + if _, ok := seen[name]; !ok { + seen[name] = struct{}{} + fmt.Println(name) + } + } } return nil diff --git a/internal/manager/apk.go b/internal/manager/apk.go index c809156..c8ee149 100644 --- a/internal/manager/apk.go +++ b/internal/manager/apk.go @@ -116,6 +116,48 @@ func (a *APK) UpgradeAll(opts *Opts) error { return a.Upgrade(opts) } +func (a *APK) ListAvailable(prefix string) ([]string, error) { + cmd := exec.Command("apk", "search", "-q", prefix) + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("apk: listavailable: %w", err) + } + + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("apk: listavailable: %w", err) + } + + seen := make(map[string]struct{}) + var pkgs []string + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + line := scanner.Text() + if line == "" { + continue + } + // apk search -q returns "name-version", extract name + lastDash := strings.LastIndex(line, "-") + name := line + if lastDash > 0 { + name = line[:lastDash] + } + if _, ok := seen[name]; !ok { + seen[name] = struct{}{} + pkgs = append(pkgs, name) + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("apk: listavailable: %w", err) + } + + if err := cmd.Wait(); err != nil { + return nil, fmt.Errorf("apk: listavailable: %w", err) + } + + return pkgs, nil +} + func (a *APK) ListInstalled(opts *Opts) (map[string]string, error) { out := map[string]string{} cmd := exec.Command("apk", "list", "-I") diff --git a/internal/manager/apt.go b/internal/manager/apt.go index 72b1709..14d2db8 100644 --- a/internal/manager/apt.go +++ b/internal/manager/apt.go @@ -196,6 +196,10 @@ func (a *APT) IsInstalled(pkg string) (bool, error) { return strings.Contains(status, "install ok installed"), nil } +func (a *APT) ListAvailable(prefix string) ([]string, error) { + return aptCacheListAvailable(prefix) +} + func (a *APT) GetInstalledVersion(pkg string) (string, error) { resolved := a.resolvePackageName(pkg) cmd := exec.Command("dpkg-query", "-f", "${Version}", "-W", resolved) diff --git a/internal/manager/apt_common.go b/internal/manager/apt_common.go new file mode 100644 index 0000000..f23be8e --- /dev/null +++ b/internal/manager/apt_common.go @@ -0,0 +1,51 @@ +// ALR - Any Linux Repository +// Copyright (C) 2025 The ALR Authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package manager + +import ( + "bufio" + "fmt" + "os/exec" +) + +func aptCacheListAvailable(prefix string) ([]string, error) { + cmd := exec.Command("apt-cache", "pkgnames", prefix) + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("apt-cache: listavailable: %w", err) + } + + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("apt-cache: listavailable: %w", err) + } + + var pkgs []string + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + pkgs = append(pkgs, scanner.Text()) + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("apt-cache: listavailable: %w", err) + } + + if err := cmd.Wait(); err != nil { + return nil, fmt.Errorf("apt-cache: listavailable: %w", err) + } + + return pkgs, nil +} diff --git a/internal/manager/apt_rpm.go b/internal/manager/apt_rpm.go index c3144df..ed39807 100644 --- a/internal/manager/apt_rpm.go +++ b/internal/manager/apt_rpm.go @@ -100,6 +100,10 @@ func (a *APTRpm) Upgrade(opts *Opts, pkgs ...string) error { return a.Install(opts, pkgs...) } +func (a *APTRpm) ListAvailable(prefix string) ([]string, error) { + return aptCacheListAvailable(prefix) +} + func (a *APTRpm) UpgradeAll(opts *Opts) error { opts = ensureOpts(opts) cmd := a.getCmd(opts, "apt-get", "dist-upgrade") diff --git a/internal/manager/dnf.go b/internal/manager/dnf.go index 05e9b34..3fe64f9 100644 --- a/internal/manager/dnf.go +++ b/internal/manager/dnf.go @@ -20,6 +20,7 @@ package manager import ( + "bufio" "fmt" "os/exec" ) @@ -107,6 +108,42 @@ func (d *DNF) Upgrade(opts *Opts, pkgs ...string) error { return nil } +func (d *DNF) ListAvailable(prefix string) ([]string, error) { + cmd := exec.Command("dnf", "repoquery", "--qf", "%{name}\n", "--quiet", prefix+"*") + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("dnf: listavailable: %w", err) + } + + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("dnf: listavailable: %w", err) + } + + seen := make(map[string]struct{}) + var pkgs []string + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + name := scanner.Text() + if name == "" { + continue + } + if _, ok := seen[name]; !ok { + seen[name] = struct{}{} + pkgs = append(pkgs, name) + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("dnf: listavailable: %w", err) + } + + if err := cmd.Wait(); err != nil { + return nil, fmt.Errorf("dnf: listavailable: %w", err) + } + + return pkgs, nil +} + // UpgradeAll обновляет все установленные пакеты func (d *DNF) UpgradeAll(opts *Opts) error { opts = ensureOpts(opts) diff --git a/internal/manager/managers.go b/internal/manager/managers.go index 648bae8..dd5e7c2 100644 --- a/internal/manager/managers.go +++ b/internal/manager/managers.go @@ -79,6 +79,9 @@ type Manager interface { // GetInstalledVersion returns the version of an installed package. // Returns empty string and no error if package is not installed. GetInstalledVersion(string) (string, error) + // ListAvailable returns names of available packages matching the given prefix. + // The prefix is used for filtering to avoid returning all packages. + ListAvailable(prefix string) ([]string, error) } // Detect returns the package manager detected on the system diff --git a/internal/manager/pacman.go b/internal/manager/pacman.go index daaef00..f79391b 100644 --- a/internal/manager/pacman.go +++ b/internal/manager/pacman.go @@ -115,6 +115,42 @@ func (p *Pacman) UpgradeAll(opts *Opts) error { return nil } +func (p *Pacman) ListAvailable(prefix string) ([]string, error) { + cmd := exec.Command("pacman", "-Ssq", "^"+prefix) + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("pacman: listavailable: %w", err) + } + + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("pacman: listavailable: %w", err) + } + + seen := make(map[string]struct{}) + var pkgs []string + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + name := scanner.Text() + if _, ok := seen[name]; !ok { + seen[name] = struct{}{} + pkgs = append(pkgs, name) + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("pacman: listavailable: %w", err) + } + + if err := cmd.Wait(); err != nil { + if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { + return nil, nil + } + return nil, fmt.Errorf("pacman: listavailable: %w", err) + } + + return pkgs, nil +} + func (p *Pacman) ListInstalled(opts *Opts) (map[string]string, error) { out := map[string]string{} cmd := exec.Command("pacman", "-Q") diff --git a/internal/manager/yum.go b/internal/manager/yum.go index a3867dd..4d37778 100644 --- a/internal/manager/yum.go +++ b/internal/manager/yum.go @@ -20,6 +20,7 @@ package manager import ( + "bufio" "fmt" "os/exec" ) @@ -103,6 +104,42 @@ func (y *YUM) Upgrade(opts *Opts, pkgs ...string) error { return nil } +func (y *YUM) ListAvailable(prefix string) ([]string, error) { + cmd := exec.Command("yum", "repoquery", "--qf", "%{name}\n", "--quiet", prefix+"*") + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("yum: listavailable: %w", err) + } + + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("yum: listavailable: %w", err) + } + + seen := make(map[string]struct{}) + var pkgs []string + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + name := scanner.Text() + if name == "" { + continue + } + if _, ok := seen[name]; !ok { + seen[name] = struct{}{} + pkgs = append(pkgs, name) + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("yum: listavailable: %w", err) + } + + if err := cmd.Wait(); err != nil { + return nil, fmt.Errorf("yum: listavailable: %w", err) + } + + return pkgs, nil +} + func (y *YUM) UpgradeAll(opts *Opts) error { opts = ensureOpts(opts) cmd := y.getCmd(opts, "yum", "upgrade") diff --git a/internal/manager/zypper.go b/internal/manager/zypper.go index 51de062..9524354 100644 --- a/internal/manager/zypper.go +++ b/internal/manager/zypper.go @@ -20,8 +20,10 @@ package manager import ( + "bufio" "fmt" "os/exec" + "strings" ) // Zypper represents the Zypper package manager @@ -103,6 +105,55 @@ func (z *Zypper) Upgrade(opts *Opts, pkgs ...string) error { return nil } +func (z *Zypper) ListAvailable(prefix string) ([]string, error) { + cmd := exec.Command("zypper", "--quiet", "search", "--type", "package", prefix+"*") + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("zypper: listavailable: %w", err) + } + + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("zypper: listavailable: %w", err) + } + + seen := make(map[string]struct{}) + var pkgs []string + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + line := scanner.Text() + // zypper table format: "S | Name | Summary | Type" + // Skip separator lines and headers + if !strings.Contains(line, "|") { + continue + } + fields := strings.Split(line, "|") + if len(fields) < 2 { + continue + } + name := strings.TrimSpace(fields[1]) + if name == "" || name == "Name" { + continue + } + if _, ok := seen[name]; !ok { + seen[name] = struct{}{} + pkgs = append(pkgs, name) + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("zypper: listavailable: %w", err) + } + + if err := cmd.Wait(); err != nil { + if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 104 { + return nil, nil + } + return nil, fmt.Errorf("zypper: listavailable: %w", err) + } + + return pkgs, nil +} + func (z *Zypper) UpgradeAll(opts *Opts) error { opts = ensureOpts(opts) cmd := z.getCmd(opts, "zypper", "update", "-y")