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