Retention read no package-lock.json on the forge: in lockfile v2 and v3 a packages entry's dependencies is a map of name to range, which the reader decoded as nested entries, so all 32 files failed whole and the versions they pin were kept by nothing. Entries are now read one by one, tolerating unknown fields and skipping workspace links; an entry of an unknown shape is said, not the whole file. A dry run measured only the versions it would delete, so every kept version answered 0 bytes. Every version is now measured, the registry's total is answered, and a size that cannot be read is said (hq ADR 0251 §4).
132 lines
5.0 KiB
Go
132 lines
5.0 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)))
|
|
s.Said = append(s.Said, sizesSaid(s)...)
|
|
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
|
|
}
|