Files
mesh-catalog/modules/gitea/cmd/npm-registry/main.go
T
jochen a378abc758 gitea: say what keeps each npm version and delete only what nothing names (hq ADR 0251)
Every publish added a version and nothing removed one. A Go bundle beside the
TypeScript one keeps what a lockfile or a range on the forge names, what a
dist-tag names and the newest five; a dry run unless asked with a why, and
nothing deleted while any repository is unread.
2026-10-08 12:00:48 +02:00

131 lines
4.9 KiB
Go

// 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
}