|
|
|
@@ -65,6 +65,10 @@ type RemoveInput struct {
|
|
|
|
|
Name string `path:"name" example:"htop" doc:"Package to remove"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type UpgradeOneInput struct {
|
|
|
|
|
Name string `path:"name" example:"htop" doc:"Package to upgrade"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SSE event types for streaming package operations.
|
|
|
|
|
type PkgOutputEvent struct {
|
|
|
|
|
Line string `json:"line" doc:"One line of the package manager's terminal output"`
|
|
|
|
@@ -162,6 +166,25 @@ func registerPackages(api huma.API, pm manager) {
|
|
|
|
|
bin, args := pm.upgradeArgs()
|
|
|
|
|
streamOp(ctx, send, bin, args)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
sse.Register(api, huma.Operation{
|
|
|
|
|
OperationID: "packages-upgrade-one",
|
|
|
|
|
Method: "POST",
|
|
|
|
|
Path: "/api/packages/upgrade/{name}",
|
|
|
|
|
Summary: "Upgrade a single package (streamed)",
|
|
|
|
|
Description: "Upgrades the named package to its latest version, streaming the " +
|
|
|
|
|
"package manager's output live. apt uses `install --only-upgrade` so the " +
|
|
|
|
|
"package must already be installed; dnf/pacman handle this natively.",
|
|
|
|
|
Tags: []string{tagPackages},
|
|
|
|
|
Metadata: op("write"),
|
|
|
|
|
}, pkgEvents, func(ctx context.Context, in *UpgradeOneInput, send sse.Sender) {
|
|
|
|
|
if validateName(in.Name) != nil {
|
|
|
|
|
send.Data(PkgErrorEvent{Message: "invalid package name: " + in.Name})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
bin, args := pm.upgradeOneArgs(in.Name)
|
|
|
|
|
streamOp(ctx, send, bin, args)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// streamOp runs a package write and streams its combined output to the client.
|
|
|
|
@@ -260,11 +283,11 @@ func result(pm manager, pkgs []Package) *ListOutput {
|
|
|
|
|
func (m manager) installArgs(name string) (string, []string) {
|
|
|
|
|
switch m.name {
|
|
|
|
|
case "dnf":
|
|
|
|
|
return "dnf", []string{"install", "-y", "--", name}
|
|
|
|
|
return "dnf", []string{"install", "-y", name}
|
|
|
|
|
case "apt":
|
|
|
|
|
return "apt-get", []string{"install", "-y", "--", name}
|
|
|
|
|
return "apt-get", []string{"install", "-y", name}
|
|
|
|
|
case "pacman":
|
|
|
|
|
return "pacman", []string{"-S", "--noconfirm", "--", name}
|
|
|
|
|
return "pacman", []string{"-S", "--noconfirm", name}
|
|
|
|
|
}
|
|
|
|
|
return "", nil
|
|
|
|
|
}
|
|
|
|
@@ -272,11 +295,11 @@ func (m manager) installArgs(name string) (string, []string) {
|
|
|
|
|
func (m manager) removeArgs(name string) (string, []string) {
|
|
|
|
|
switch m.name {
|
|
|
|
|
case "dnf":
|
|
|
|
|
return "dnf", []string{"remove", "-y", "--", name}
|
|
|
|
|
return "dnf", []string{"remove", "-y", name}
|
|
|
|
|
case "apt":
|
|
|
|
|
return "apt-get", []string{"remove", "-y", "--", name}
|
|
|
|
|
return "apt-get", []string{"remove", "-y", name}
|
|
|
|
|
case "pacman":
|
|
|
|
|
return "pacman", []string{"-R", "--noconfirm", "--", name}
|
|
|
|
|
return "pacman", []string{"-R", "--noconfirm", name}
|
|
|
|
|
}
|
|
|
|
|
return "", nil
|
|
|
|
|
}
|
|
|
|
@@ -293,6 +316,21 @@ func (m manager) upgradeArgs() (string, []string) {
|
|
|
|
|
return "", nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// upgradeOneArgs upgrades a single package to its latest version. apt's
|
|
|
|
|
// `install --only-upgrade` is the safe variant (won't install if absent);
|
|
|
|
|
// pacman -S re-syncs to latest; dnf upgrade is naturally scoped by name.
|
|
|
|
|
func (m manager) upgradeOneArgs(name string) (string, []string) {
|
|
|
|
|
switch m.name {
|
|
|
|
|
case "dnf":
|
|
|
|
|
return "dnf", []string{"upgrade", "-y", name}
|
|
|
|
|
case "apt":
|
|
|
|
|
return "apt-get", []string{"install", "--only-upgrade", "-y", name}
|
|
|
|
|
case "pacman":
|
|
|
|
|
return "pacman", []string{"-S", "--noconfirm", name}
|
|
|
|
|
}
|
|
|
|
|
return "", nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- parsers (pure, tested) --------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// parseTabbed reads "name\tversion" lines (dpkg-query / rpm output).
|
|
|
|
|