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.
131 lines
4.9 KiB
Go
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
|
|
}
|