// npm-registry: the forge module's Go bundle for its package registry (novox/hq ADR 0251 §4, to-be 51). // A process the node's runtime launches and speaks MCP over stdio to, beside the module's TypeScript // bundle. It says which versions of each npm package the registry holds, what keeps each one, and — // asked with a why — deletes the versions nothing keeps. stdout is the protocol; it says what it says on // stderr, and never the forge's credential. package main import ( "context" "fmt" "math" "os" "strings" stdio "git.novox.be/novox/mesh-sdk/go" ) // ToolNames are the tools this bundle serves. The module's manifest lists no `tools`: its TypeScript // bundle's tools are served without one, and a claim without `serves` would offer a `tools` list to both // of the module's seats as their verbs. The README names these two, and a test holds the two together. var ToolNames = []string{"npm_packages", "npm_retention"} func main() { if err := stdio.Serve("", Tools(func() (*Forge, error) { return ForgeFromEnv() })); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } // Tools are the bundle's tools over the forge the function gives; asked per call, so a manifest that // does not set the environment is a refusal naming what is missing, not a bundle that will not start. func Tools(forge func() (*Forge, error)) []stdio.Tool { pkg := map[string]any{"type": "string", "description": "one package, by its name (default every package)"} keep := map[string]any{"type": "integer", "description": fmt.Sprintf("how many of each package's newest versions are kept whatever names them (default %d, at least 1)", DefaultKeep)} return []stdio.Tool{ { Name: "npm_packages", Description: "Every npm package in the forge's package registry, each version newest first with when it was published, " + "its size and what keeps it: a lockfile on a repository's default branch that names it, the highest version " + "satisfying a range a package.json there names, a dist-tag, or being among the newest `keep` of its package " + "(novox/hq ADR 0251). Repositories and files that could not be read are named. Changes nothing. Replaces " + "npm view and npm dist-tag ls. (r)", Input: map[string]any{"package": pkg, "keep": keep}, Run: func(args map[string]any) (any, error) { f, err := forge() if err != nil { return nil, err } k, err := count(args, "keep", DefaultKeep, 1) if err != nil { return nil, err } s, err := survey(context.Background(), f, text(args, "package"), k, func(Item) bool { return true }) if err != nil { return nil, err } kept, total := 0, 0 for _, p := range s.Packages { for _, v := range p.Versions { total++ if v.Kept { kept++ } } } s.Said = append(s.Said, fmt.Sprintf("%d packages, %d versions, %d kept; %d repositories read, %d unread", len(s.Packages), total, kept, s.Repositories, len(s.Unread))) return s, nil }, }, { Name: "npm_retention", Description: "What retention would delete from the forge's package registry: every version nothing keeps — no lockfile " + "on a repository's default branch names it, it is not the highest version satisfying any range a package.json " + "there names, no dist-tag names it, and it is not among the newest `keep` of its package — with the bytes. " + "A dry run unless dry_run is false; a real run needs why, and deletes nothing at all when any repository or " + "file could not be read (novox/hq ADR 0251). Replaces npm unpublish. (a)", Input: map[string]any{ "package": pkg, "keep": keep, "dry_run": map[string]any{"type": "boolean", "description": "false to delete; default true"}, "why": map[string]any{"type": "string", "description": "why the versions are deleted; required for a real run"}, }, Run: func(args map[string]any) (any, error) { f, err := forge() if err != nil { return nil, err } k, err := count(args, "keep", DefaultKeep, 1) if err != nil { return nil, err } dry := true if v, given := args["dry_run"]; given && v != nil { b, ok := v.(bool) if !ok { if s, isText := v.(string); isText && (s == "true" || s == "false") { b, ok = s == "true", true } } if !ok { return nil, fmt.Errorf("dry_run is true or false, not %v", v) } dry = b } return retention(context.Background(), f, text(args, "package"), k, dry, text(args, "why")) }, }, } } func text(args map[string]any, key string) string { s, _ := args[key].(string) return strings.TrimSpace(s) } func count(args map[string]any, key string, fallback, least int) (int, error) { v, given := args[key] if !given || v == nil { return fallback, nil } x, ok := v.(float64) if !ok || x != math.Trunc(x) { return 0, fmt.Errorf("%s must be a whole number", key) } if int(x) < least { return 0, fmt.Errorf("%s must be at least %d", key, least) } return int(x), nil }