package updates import ( "bufio" "bytes" "context" "os/exec" "strings" "time" ) type PackageUpdate struct { Name string CurrentVersion string NewVersion string } func detectPM() string { for _, pm := range []string{"apt-get", "dnf", "yum", "pacman", "zypper", "apk"} { if _, err := exec.LookPath(pm); err == nil { if pm == "apt-get" { return "apt" } return pm } } return "" } func CheckAvailable() ([]PackageUpdate, error) { switch detectPM() { case "apt": return checkApt() case "dnf": return checkDnfYum("dnf") case "yum": return checkDnfYum("yum") case "pacman": return checkPacman() case "zypper": return checkZypper() case "apk": return checkApk() default: return nil, nil } } func ApplyAll() error { switch detectPM() { case "apt": ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() if err := exec.CommandContext(ctx, "apt-get", "update", "-qq").Run(); err != nil { return err } return exec.CommandContext(ctx, "apt-get", "upgrade", "-y").Run() case "dnf": return exec.Command("dnf", "upgrade", "-y").Run() case "yum": return exec.Command("yum", "upgrade", "-y").Run() case "pacman": return exec.Command("pacman", "-Syu", "--noconfirm").Run() case "zypper": return exec.Command("zypper", "update", "-y").Run() case "apk": return exec.Command("apk", "upgrade").Run() default: return nil } } func checkApt() ([]PackageUpdate, error) { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() exec.CommandContext(ctx, "apt-get", "update", "-qq").Run() out, err := exec.Command("apt", "list", "--upgradable").Output() if err != nil { return nil, err } var updates []PackageUpdate scanner := bufio.NewScanner(bytes.NewReader(out)) for scanner.Scan() { line := scanner.Text() if !strings.Contains(line, "[upgradable from:") { continue } parts := strings.Fields(line) if len(parts) < 2 { continue } name := strings.SplitN(parts[0], "/", 2)[0] newVer := parts[1] oldVer := "" if idx := strings.Index(line, "upgradable from: "); idx != -1 { rest := line[idx+len("upgradable from: "):] oldVer = strings.TrimSuffix(strings.TrimSpace(rest), "]") } updates = append(updates, PackageUpdate{Name: name, CurrentVersion: oldVer, NewVersion: newVer}) } return updates, nil } func checkDnfYum(pm string) ([]PackageUpdate, error) { cmd := exec.Command(pm, "check-update") out, err := cmd.Output() if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 100 { err = nil } if err != nil { return nil, err } var updates []PackageUpdate pastHeader := false scanner := bufio.NewScanner(bytes.NewReader(out)) for scanner.Scan() { line := scanner.Text() if !pastHeader { if strings.TrimSpace(line) == "" { pastHeader = true } continue } parts := strings.Fields(line) if len(parts) < 2 { continue } name := strings.SplitN(parts[0], ".", 2)[0] updates = append(updates, PackageUpdate{Name: name, NewVersion: parts[1]}) } return updates, nil } func checkPacman() ([]PackageUpdate, error) { out, _ := exec.Command("pacman", "-Qu").Output() var updates []PackageUpdate scanner := bufio.NewScanner(bytes.NewReader(out)) for scanner.Scan() { parts := strings.Fields(scanner.Text()) if len(parts) < 4 { continue } updates = append(updates, PackageUpdate{Name: parts[0], CurrentVersion: parts[1], NewVersion: parts[3]}) } return updates, nil } func checkZypper() ([]PackageUpdate, error) { out, err := exec.Command("zypper", "list-updates").Output() if err != nil { return nil, err } var updates []PackageUpdate scanner := bufio.NewScanner(bytes.NewReader(out)) for scanner.Scan() { line := scanner.Text() if !strings.HasPrefix(line, "v |") && !strings.HasPrefix(line, "i |") { continue } parts := strings.Split(line, "|") if len(parts) < 5 { continue } updates = append(updates, PackageUpdate{ Name: strings.TrimSpace(parts[2]), CurrentVersion: strings.TrimSpace(parts[3]), NewVersion: strings.TrimSpace(parts[4]), }) } return updates, nil } func checkApk() ([]PackageUpdate, error) { out, err := exec.Command("apk", "list", "--upgradable").Output() if err != nil { return nil, err } var updates []PackageUpdate scanner := bufio.NewScanner(bytes.NewReader(out)) for scanner.Scan() { line := scanner.Text() if !strings.Contains(line, "[upgradable") { continue } parts := strings.Fields(line) if len(parts) < 1 { continue } pkgVer := parts[0] name := apkName(pkgVer) newVer := apkVersion(pkgVer) oldVer := "" if idx := strings.Index(line, "upgradable from:"); idx != -1 { rest := strings.TrimSpace(line[idx+len("upgradable from:"):]) rest = strings.TrimSuffix(rest, "]") oldVer = apkVersion(strings.TrimSpace(rest)) } updates = append(updates, PackageUpdate{Name: name, CurrentVersion: oldVer, NewVersion: newVer}) } return updates, nil } func apkName(pkgVer string) string { parts := strings.Split(pkgVer, "-") var name []string for _, p := range parts { if len(p) > 0 && p[0] >= '0' && p[0] <= '9' { break } name = append(name, p) } return strings.Join(name, "-") } func apkVersion(pkgVer string) string { parts := strings.Split(pkgVer, "-") var ver []string inVer := false for _, p := range parts { if !inVer && len(p) > 0 && p[0] >= '0' && p[0] <= '9' { inVer = true } if inVer { ver = append(ver, p) } } return strings.Join(ver, "-") }