diff --git a/cmd/mesh-controller/modules.go b/cmd/mesh-controller/modules.go index 2a91408..f49965c 100644 --- a/cmd/mesh-controller/modules.go +++ b/cmd/mesh-controller/modules.go @@ -147,6 +147,36 @@ func moduleCommand(ctx context.Context, args []string) error { if err != nil { 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 { fmt.Println("this mesh knows about no modules yet") return nil diff --git a/cmd/mesh-controller/nodes.go b/cmd/mesh-controller/nodes.go index fe96b48..04a38e4 100644 --- a/cmd/mesh-controller/nodes.go +++ b/cmd/mesh-controller/nodes.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "errors" "flag" "fmt" @@ -44,6 +45,21 @@ func nodeCommand(ctx context.Context, args []string) error { if err != nil { 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 { // 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 @@ -500,3 +516,13 @@ func orNotReported(s string) string { } 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 +} diff --git a/cmd/mesh-controller/seatverbs.go b/cmd/mesh-controller/seatverbs.go index 85d3fe3..b39fb61 100644 --- a/cmd/mesh-controller/seatverbs.go +++ b/cmd/mesh-controller/seatverbs.go @@ -66,14 +66,14 @@ func argvFor(verb string, args map[string]any) ([]string, error) { case "status": return []string{"status", "--json"}, nil case "nodes": - return []string{"node", "list"}, nil + return []string{"node", "list", "--json"}, nil case "node": if err := need("node"); err != nil { return nil, err } return []string{"node", "show", str("node")}, nil case "modules": - return []string{"module", "list"}, nil + return []string{"module", "list", "--json"}, nil case "seats": return []string{"seats", "--json"}, nil case "builds": diff --git a/cmd/mesh-controller/seatverbs_test.go b/cmd/mesh-controller/seatverbs_test.go index 95cb8ba..114a4f1 100644 --- a/cmd/mesh-controller/seatverbs_test.go +++ b/cmd/mesh-controller/seatverbs_test.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "strings" "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) + } + } +}