Files
mesh-catalog/modules/pacman/cmd/pacman-tools/main.go
T
jochen d9336d11d0 pacman: the package manager's configuration, mirrors and cache as a module
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).
2026-10-04 12:50:20 +02:00

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() },
},
}
}