// 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: "Ask for a full system upgrade of a node (this one unless named): the ask is written to the bus, where that node's keeper " + "takes it and runs it as a run: the package record, the kernel check, the transaction as a unit of its own, and the after-upgrade " + "check (ADR 0508 §8). Answers at once with the ask and the distribution's news since the last upgrade — read the news first; " + "pacman_upgrades with the node shows the run that names the ask. Given a transaction unit, answers whether it is running, " + "finished and succeeded, with the tail of its log.", Input: schema(map[string]any{ "node": map[string]any{"type": "string", "description": "the node to upgrade (default: this one)"}, "unit": map[string]any{"type": "string", "description": "a transaction unit, 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 } if unit != "" { return m.UpgradeStatus(unit, n) } node, err := text(args, "node", false) if err != nil { return nil, err } if node == "" { node = thisNode() } a, err := AskUpgrade(upgradeState(StateAsks), node, m.Now()) if err != nil { return nil, err } return map[string]any{"asked": a, "follow": "pacman_upgrades with node " + node + " lists the run that names this ask", "news": m.News("")}, nil }, }, { 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() }, }, } }