Mirrors were generated once and never again and caches never cleaned. The module holds node-package-manager (hq ADR 0207), declares pacman itself, owns /etc/pacman.conf whole — [options] cannot take an appended block — with the union of the enabled repositories and improved options, proven by pacman-conf in its test, and enables reflector.timer (its config owned) and paccache.timer. Fifteen tools from a Go bundle; transactions run as transient units so a call's timeout never kills pacman mid-transaction (to-be 42).
279 lines
11 KiB
Go
279 lines
11 KiB
Go
// pacman's tools bundle (novox/hq to-be 42 Phase 1, research 026/05): a process the node's runtime
|
|
// launches and speaks MCP over stdio to, through the Go SDK (ADR 0188, ADR 0193). It reads the
|
|
// package manager — search, info, what is installed and why, owners, files, updates, orphans,
|
|
// foreign packages, history, mirrors, the configuration in force, the distribution's news — and acts
|
|
// on it: a full upgrade, removing orphans, cleaning the cache, refreshing the mirrors. Acts go through
|
|
// sudo -n, and a transaction runs as a unit of its own (acts.go says why).
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// binaryName is what the build names this bundle's executable: the manifest's `binary`.
|
|
const binaryName = "pacman-tools"
|
|
|
|
func bg() context.Context { return context.Background() }
|
|
|
|
func main() {
|
|
// An empty name serves as the module the runtime names (MESH_SERVED_MODULE): pacman.
|
|
if err := stdio.Serve("", tools(ThisMachine())); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
var (
|
|
pkgArg = map[string]any{"type": "string", "description": "a package's name"}
|
|
limitArg = func(def, most int) map[string]any {
|
|
return map[string]any{"type": "integer", "description": fmt.Sprintf("at most this many (default %d, at most %d)", def, most)}
|
|
}
|
|
sinceArg = map[string]any{"type": "string", "description": "a day, YYYY-MM-DD"}
|
|
)
|
|
|
|
func tools(m *Machine) []stdio.Tool {
|
|
return []stdio.Tool{
|
|
{
|
|
Name: "pacman_search",
|
|
Description: "Search the repositories (pacman -Ss): each package's repository, name, version, groups, whether it is installed and at which version, and its description.",
|
|
Input: schema(map[string]any{"query": map[string]any{"type": "string", "description": "words, each a regular expression; all must match"}, "limit": limitArg(50, 500)}, "query"),
|
|
Run: func(args map[string]any) (any, error) {
|
|
q, err := text(args, "query", true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
n, err := whole(args, "limit", 50, 1, 500)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.Search(q, n)
|
|
},
|
|
},
|
|
{
|
|
Name: "pacman_info",
|
|
Description: "One package's details (pacman -Qi, or -Si when it is not installed): version, description, dependencies, what requires it, sizes, dates, install reason; lists as lists.",
|
|
Input: schema(map[string]any{"package": pkgArg}, "package"),
|
|
Run: func(args map[string]any) (any, error) {
|
|
p, err := text(args, "package", true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.Info(p)
|
|
},
|
|
},
|
|
{
|
|
Name: "pacman_installed",
|
|
Description: "Installed packages with version, why each is installed (explicit or dependency) and whether it is foreign (in no repository); narrowed by name, reason or foreign; with totals.",
|
|
Input: schema(map[string]any{
|
|
"match": map[string]any{"type": "string", "description": "only names holding this"},
|
|
"reason": map[string]any{"type": "string", "enum": []string{"explicit", "dependency"}},
|
|
"foreign": map[string]any{"type": "boolean", "description": "only foreign packages"},
|
|
"limit": limitArg(5000, 20000),
|
|
}),
|
|
Run: func(args map[string]any) (any, error) {
|
|
match, err := text(args, "match", false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reason, err := text(args, "reason", false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if reason != "" && reason != "explicit" && reason != "dependency" {
|
|
return nil, fmt.Errorf("reason is explicit or dependency")
|
|
}
|
|
foreign, err := flag(args, "foreign")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
n, err := whole(args, "limit", 5000, 1, 20000)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.Installed(match, reason, foreign, n)
|
|
},
|
|
},
|
|
{
|
|
Name: "pacman_owns",
|
|
Description: "Which installed package owns a path (pacman -Qo); owned false when none does.",
|
|
Input: schema(map[string]any{"path": map[string]any{"type": "string", "description": "an absolute path"}}, "path"),
|
|
Run: func(args map[string]any) (any, error) {
|
|
p, err := text(args, "path", true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.Owns(p)
|
|
},
|
|
},
|
|
{
|
|
Name: "pacman_files",
|
|
Description: "The paths an installed package placed (pacman -Ql), bounded.",
|
|
Input: schema(map[string]any{"package": pkgArg, "limit": limitArg(2000, 20000)}, "package"),
|
|
Run: func(args map[string]any) (any, error) {
|
|
p, err := text(args, "package", true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
n, err := whole(args, "limit", 2000, 1, 20000)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.Files(p, n)
|
|
},
|
|
},
|
|
{
|
|
Name: "pacman_updates",
|
|
Description: "What a full upgrade would change, each package from and to (checkupdates: the repositories are asked into a copy of their databases, so asking never sets up a partial upgrade). Needs the network.",
|
|
Input: schema(map[string]any{}),
|
|
Run: func(map[string]any) (any, error) { return m.Updates() },
|
|
},
|
|
{
|
|
Name: "pacman_upgrade",
|
|
Description: "Start a full system upgrade (pacman -Syu --noconfirm, through sudo -n) as a transient unit of its own that outlives the call, " +
|
|
"answering at once with the unit and the distribution's news since the last upgrade — read the news first. Given that unit, " +
|
|
"answer whether it is running, finished and succeeded, with the tail of its log.",
|
|
Input: schema(map[string]any{
|
|
"unit": map[string]any{"type": "string", "description": "a unit this tool started, to read how it goes (optional)"},
|
|
"lines": limitArg(60, 400),
|
|
}),
|
|
Run: func(args map[string]any) (any, error) {
|
|
unit, err := text(args, "unit", false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
n, err := whole(args, "lines", 60, 1, 400)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.Upgrade(unit, n)
|
|
},
|
|
},
|
|
{
|
|
Name: "pacman_orphans",
|
|
Description: "Packages installed as dependencies that nothing requires any more (pacman -Qdt), with versions.",
|
|
Input: schema(map[string]any{}),
|
|
Run: func(map[string]any) (any, error) { return m.Orphans() },
|
|
},
|
|
{
|
|
Name: "pacman_remove_orphans",
|
|
Description: "Remove orphans (pacman -Rs, through sudo -n, as a unit of its own): the names given, each of which must be an orphan, " +
|
|
"or every orphan with all: true. Changed configuration files are kept as .pacsave. Answers what was removed and the log.",
|
|
Input: schema(map[string]any{
|
|
"names": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "orphans to remove"},
|
|
"all": map[string]any{"type": "boolean", "description": "remove every orphan"},
|
|
}),
|
|
Run: func(args map[string]any) (any, error) {
|
|
all, err := flag(args, "all")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var names []string
|
|
if raw, ok := args["names"].([]any); ok {
|
|
for i := range raw {
|
|
n, err := text(map[string]any{"name": raw[i]}, "name", true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
names = append(names, n)
|
|
}
|
|
}
|
|
return m.RemoveOrphans(names, all)
|
|
},
|
|
},
|
|
{
|
|
Name: "pacman_cache",
|
|
Description: "The package cache: files, bytes, interrupted downloads left behind, what paccache would remove keeping the last " +
|
|
"keep versions of each package (or only packages no longer installed), and the paccache timer. With clean: true it removes them (sudo -n).",
|
|
Input: schema(map[string]any{
|
|
"keep": map[string]any{"type": "integer", "description": "versions of each package to keep (default 3)"},
|
|
"uninstalled": map[string]any{"type": "boolean", "description": "only packages no longer installed"},
|
|
"clean": map[string]any{"type": "boolean", "description": "remove them, rather than say what would go"},
|
|
}),
|
|
Run: func(args map[string]any) (any, error) {
|
|
keep, err := whole(args, "keep", 3, 0, 100)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
un, err := flag(args, "uninstalled")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
clean, err := flag(args, "clean")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.Cache(keep, un, clean)
|
|
},
|
|
},
|
|
{
|
|
Name: "pacman_history",
|
|
Description: "What the package manager did, from /var/log/pacman.log: each install, upgrade, downgrade, reinstall and removal since a day (default thirty days back), narrowed to an action or a name, with counts and the last full upgrade.",
|
|
Input: schema(map[string]any{
|
|
"since": sinceArg,
|
|
"action": map[string]any{"type": "string", "enum": Actions},
|
|
"match": map[string]any{"type": "string", "description": "only packages whose name holds this"},
|
|
"limit": limitArg(500, 5000),
|
|
}),
|
|
Run: func(args map[string]any) (any, error) {
|
|
since, err := text(args, "since", false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
action, err := text(args, "action", false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
match, err := text(args, "match", false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
n, err := whole(args, "limit", 500, 1, 5000)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.History(since, action, match, n)
|
|
},
|
|
},
|
|
{
|
|
Name: "pacman_mirrors",
|
|
Description: "The mirror list in force (servers, commented ones, who generated it and when), reflector's options, its timer and its last run. With refresh: true, start reflector now (sudo -n), without waiting.",
|
|
Input: schema(map[string]any{"refresh": map[string]any{"type": "boolean", "description": "rank and rewrite the list now"}}),
|
|
Run: func(args map[string]any) (any, error) {
|
|
refresh, err := flag(args, "refresh")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.MirrorList(refresh)
|
|
},
|
|
},
|
|
{
|
|
Name: "pacman_foreign",
|
|
Description: "Installed packages that no repository this machine syncs carries (pacman -Qm): built from the AUR or by hand, which the mesh cannot install.",
|
|
Input: schema(map[string]any{}),
|
|
Run: func(map[string]any) (any, error) { return m.Foreign() },
|
|
},
|
|
{
|
|
Name: "pacman_news",
|
|
Description: "The distribution's news posts since a day, or since the last full upgrade by default — what an upgrade may need a person to do. Fetched over https; no network is answered as reachable: false.",
|
|
Input: schema(map[string]any{"since": sinceArg}),
|
|
Run: func(args map[string]any) (any, error) {
|
|
since, err := text(args, "since", false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.News(since), nil
|
|
},
|
|
},
|
|
{
|
|
Name: "pacman_config",
|
|
Description: "The configuration pacman runs with, as pacman-conf resolves it: every option, each repository with its signature level and how many servers, and whether /etc/pacman.conf is the module's.",
|
|
Input: schema(map[string]any{}),
|
|
Run: func(map[string]any) (any, error) { return m.Conf() },
|
|
},
|
|
}
|
|
}
|