Discovery from what answers: grants, the controller announces its seat, JSON lists (hq ADR 0195, 0197) #241

Merged
mesh-admin merged 2 commits from feat/node-and-module-lists-as-json into main 2026-10-03 20:11:29 +00:00
4 changed files with 73 additions and 2 deletions
Showing only changes of commit 85873b19e1 - Show all commits
+30
View File
@@ -147,6 +147,36 @@ func moduleCommand(ctx context.Context, args []string) error {
if err != nil { if err != nil {
return err return err
} }
// **The same list, for something other than a person** (novox/hq ADR 0195): what each module
// is, where it runs, whether it is current, and what it says of itself.
if len(args) > 1 && args[1] == "--json" {
type listed struct {
Module string `json:"module"`
Version string `json:"version"`
Built string `json:"built,omitempty"`
Head string `json:"head,omitempty"`
Current bool `json:"current"`
Provided bool `json:"provided,omitempty"`
On []string `json:"on"`
Provides []string `json:"provides,omitempty"`
Requires []string `json:"requires,omitempty"`
Claims []string `json:"claims,omitempty"`
Capabilities []string `json:"capabilities,omitempty"`
}
out := make([]listed, 0, len(entries))
for _, e := range entries {
m := e.Manifest
l := listed{Module: m.Module, Version: m.Version, Built: e.Source.BuiltFrom, Head: e.Source.Head,
Current: e.Provided || e.Source.Repository == "" || e.Source.Current(), Provided: e.Provided,
On: append([]string{}, e.On...), Provides: m.Offers(), Requires: m.Requires,
Capabilities: m.Capabilities}
for _, c := range m.Claims {
l.Claims = append(l.Claims, c.At()+"/"+c.Name)
}
out = append(out, l)
}
return printJSON(out)
}
if len(entries) == 0 { if len(entries) == 0 {
fmt.Println("this mesh knows about no modules yet") fmt.Println("this mesh knows about no modules yet")
return nil return nil
+26
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
@@ -44,6 +45,21 @@ func nodeCommand(ctx context.Context, args []string) error {
if err != nil { if err != nil {
return err return err
} }
// **The same list, for something other than a person** — the console's discovery reads it
// (novox/hq ADR 0195), and a reader that parses a printed column breaks when it is reworded.
if len(args) > 1 && args[1] == "--json" {
type listed struct {
Name string `json:"name"`
Heard string `json:"heard"`
Mode string `json:"mode"`
ID string `json:"id"`
}
out := make([]listed, 0, len(nodes))
for _, n := range nodes {
out = append(out, listed{Name: n.Name, Heard: heardFrom(n), Mode: modeOf(n), ID: n.ID})
}
return printJSON(out)
}
if len(nodes) == 0 { if len(nodes) == 0 {
// Said rather than printed as nothing: an empty list and a failed read must never // Said rather than printed as nothing: an empty list and a failed read must never
// look the same, and this command answering "none" is only honest because getting // look the same, and this command answering "none" is only honest because getting
@@ -500,3 +516,13 @@ func orNotReported(s string) string {
} }
return s return s
} }
// printJSON prints a value as indented JSON, the shape every `--json` answers in.
func printJSON(v any) error {
body, err := json.MarshalIndent(v, "", " ")
if err != nil {
return err
}
fmt.Println(string(body))
return nil
}
+2 -2
View File
@@ -66,14 +66,14 @@ func argvFor(verb string, args map[string]any) ([]string, error) {
case "status": case "status":
return []string{"status", "--json"}, nil return []string{"status", "--json"}, nil
case "nodes": case "nodes":
return []string{"node", "list"}, nil return []string{"node", "list", "--json"}, nil
case "node": case "node":
if err := need("node"); err != nil { if err := need("node"); err != nil {
return nil, err return nil, err
} }
return []string{"node", "show", str("node")}, nil return []string{"node", "show", str("node")}, nil
case "modules": case "modules":
return []string{"module", "list"}, nil return []string{"module", "list", "--json"}, nil
case "seats": case "seats":
return []string{"seats", "--json"}, nil return []string{"seats", "--json"}, nil
case "builds": case "builds":
+15
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"fmt"
"strings" "strings"
"testing" "testing"
@@ -249,3 +250,17 @@ func TestARowAheadOfThisBuildIsServedAnyway(t *testing.T) {
} }
} }
} }
// novox/hq ADR 0195: the console's discovery reads the machines and the modules; they answer as JSON,
// as status and seats do, so nothing parses a printed column.
func TestTheNodesAndModulesVerbsAnswerAsJSON(t *testing.T) {
for verb, want := range map[string]string{"nodes": "[node list --json]", "modules": "[module list --json]"} {
argv, err := argvFor(verb, map[string]any{})
if err != nil {
t.Fatal(err)
}
if fmt.Sprint(argv) != want {
t.Errorf("%s runs %v, want %s", verb, argv, want)
}
}
}