pacman_packages answers one package on every node, or each node's counts, its explicit packages no module declares and its foreign ones apart, the versions that differ and the packages on some nodes only, with a node over the bus's cap marked not current. pacman_updates, pacman_installed and pacman_foreign read the keeper's states instead of the machine; pacman_updates says when each node's list last changed.
382 lines
15 KiB
Go
382 lines
15 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"
|
|
"regexp"
|
|
|
|
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() {
|
|
// `pacman-tools keeper` is the module's process (keeper.go); without a word it serves the tools.
|
|
if len(os.Args) > 1 && os.Args[1] == "keeper" {
|
|
os.Exit(keeper())
|
|
}
|
|
// 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"}
|
|
nodeArg = map[string]any{"type": "string", "description": "a node's name"}
|
|
nodeName = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
|
)
|
|
|
|
// nodeOf is the node argument, empty when absent; a node's name is one key token.
|
|
func nodeOf(args map[string]any) (string, error) {
|
|
n, err := text(args, "node", false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if n != "" && !nodeName.MatchString(n) {
|
|
return "", fmt.Errorf("node %q is not a node's name", n)
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
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: "A node's installed packages, from the bus (ADR 0507 §2): version, why each is installed (explicit or dependency), its repository, " +
|
|
"or foreign (in no repository); narrowed by name, reason or foreign; with totals, the time of the transaction they describe and whether they are current. " +
|
|
"This node's by default. (r)",
|
|
Input: schema(map[string]any{
|
|
"node": nodeArg,
|
|
"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) {
|
|
node, err := nodeOf(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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.InstalledFrom(node, match, reason, foreign, n)
|
|
},
|
|
},
|
|
{
|
|
Name: "pacman_packages",
|
|
Description: "Every node's packages at once, from the bus (ADR 0507 §6): with package, that package on every node (version, reason, repository " +
|
|
"or foreign, the modules declaring it); without, per node its counts, the packages installed explicitly that no module assigned to it declares, " +
|
|
"and its foreign packages apart, then the packages whose versions differ between nodes and the packages on some nodes only. " +
|
|
"A node whose packages are over the bus's cap is marked not current. No machine is asked. (r)",
|
|
Input: schema(map[string]any{
|
|
"package": map[string]any{"type": "string", "description": "one package, on every node"},
|
|
"limit": limitArg(500, 20000),
|
|
}),
|
|
Run: func(args map[string]any) (any, error) {
|
|
p, err := text(args, "package", false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
n, err := whole(args, "limit", 500, 1, 20000)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.Packages(p, 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, from the bus (ADR 0507 §4), on one node or on every node: each package from and to, whether the " +
|
|
"node reached its mirrors (false: the list is the last it could confirm), and when the node's list last changed. The keeper reads it with " +
|
|
"checkupdates at its start, after every transaction, after every mirror refresh and once a day. (r)",
|
|
Input: schema(map[string]any{"node": nodeArg}),
|
|
Run: func(args map[string]any) (any, error) {
|
|
node, err := nodeOf(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.UpdatesFor(node)
|
|
},
|
|
},
|
|
{
|
|
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_upgrade_health",
|
|
Description: "Whether the weekly full upgrade (Saturday 04:00 local time, ADR 0477) last passed, as the module's health check: unhealthy " +
|
|
"with the exit and the last error of the journal when it failed, healthy while it runs, after it passed and before it ever ran. (r)",
|
|
Input: schema(map[string]any{}),
|
|
Run: func(map[string]any) (any, error) {
|
|
if h, said := m.InstalledHealth(); said {
|
|
return h, nil
|
|
}
|
|
return m.UpgradeHealth(), nil
|
|
},
|
|
},
|
|
{
|
|
Name: "pacman_upgrades",
|
|
Description: "Every full upgrade and rollback of every node, from the bus (ADR 0508 §1): each run's node, kind, what started it, " +
|
|
"its round, when it began and ended, its outcome (refused, finished, failed or running) and whether its after-upgrade check passed; " +
|
|
"narrowed to a node and to days, newest first. With a node and a run, that run whole, every step with its time.",
|
|
Input: schema(map[string]any{
|
|
"node": map[string]any{"type": "string", "description": "only this node's runs"},
|
|
"from": sinceArg,
|
|
"until": map[string]any{"type": "string", "description": "a day, YYYY-MM-DD, the last one listed"},
|
|
"run": map[string]any{"type": "string", "description": "one run, by its id, with node"},
|
|
"limit": limitArg(50, 1000),
|
|
}),
|
|
Run: func(args map[string]any) (any, error) {
|
|
var v [4]string
|
|
for i, k := range []string{"node", "from", "until", "run"} {
|
|
s, err := text(args, k, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
v[i] = s
|
|
}
|
|
n, err := whole(args, "limit", 50, 1, 1000)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ListUpgrades(upgradeState(StateRuns), v[0], v[1], v[2], v[3], 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: "A node's packages that no repository it syncs carries, from the bus: built from the AUR or by hand, which the mesh neither " +
|
|
"upgrades nor removes (ADR 0507 §7). This node's by default. (r)",
|
|
Input: schema(map[string]any{"node": nodeArg}),
|
|
Run: func(args map[string]any) (any, error) {
|
|
node, err := nodeOf(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.ForeignFrom(node)
|
|
},
|
|
},
|
|
{
|
|
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() },
|
|
},
|
|
}
|
|
}
|