Files
mesh-catalog/modules/mesh-cli/cmd/mesh-cli/commands.go
T
jschoubben 63de1f7e0d
mesh/delivery merged: source.main moved to novox/mesh-catalog@32c4c52261c4
mesh/check checking 63de1f7e0dd7 on 32c4c52261c4
nox cleanup delete confirms with the holder's name, as the vault asks
The vault deletes a retired holder only when confirm repeats its name, so
sending confirm "true" could never delete anything. The stand-in console now
also says which machine answered as a block of its own, as node-tools does.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 10:47:56 +01:00

730 lines
33 KiB
Go

package main
// Every command line nox takes, and the seat verb or module tool that owns its job now that the controller is
// unassigned (novox/hq issue 1126; the map is hq's .work/1126/nox-map.md). A line no seat or module owns says so and
// names what to use instead: it never fails silently, and it never pretends to have run.
import (
"encoding/json"
"fmt"
"io"
"sort"
"strconv"
"strings"
"time"
)
// line is a command line's words after the command's own: positionals and --flags, in any order.
type line struct {
pos []string
flags map[string]string
}
// boolFlags are the flags that take no value.
var boolFlags = map[string]bool{"json": true, "confirm": true, "collected": true, "brief": true, "all": true,
"reasoning": true, "clear": true}
func parseLine(args []string) (line, error) {
l := line{flags: map[string]string{}}
for i := 0; i < len(args); i++ {
a := args[i]
if !strings.HasPrefix(a, "--") || a == "--" {
l.pos = append(l.pos, a)
continue
}
name, value, has := strings.Cut(strings.TrimPrefix(a, "--"), "=")
if boolFlags[name] {
if has {
return l, usageErr("--%s takes no value", name)
}
l.flags[name] = "true"
continue
}
if !has {
if i+1 >= len(args) {
return l, usageErr("--%s needs a value", name)
}
i++
value = args[i]
}
l.flags[name] = value
}
return l, nil
}
func (l line) has(flag string) bool { return l.flags[flag] != "" }
// usageError is a line nox cannot read: nothing was called.
type usageError struct{ msg string }
func (u usageError) Error() string { return u.msg }
func usageErr(format string, a ...any) error { return usageError{fmt.Sprintf(format, a...)} }
// call is what one line asks: an address and its arguments.
type call struct {
Address string
Args map[string]any
}
// command is one of nox's lines.
type command struct {
words []string
usage string
does string
// build reads the line and answers the call; nil for a line no seat owns.
build func(l line) (call, error)
// allowed are the flags the line reads; any other is refused before anything is called.
allowed []string
// instead says, for a line with no owner, what to use; empty for one that is owned.
instead string
// render shapes a successful answer for reading; nil prints it as the console gave it.
render func(text string) string
}
// gone is the reason every unowned line gives.
const gone = "this was the controller's, and the controller is unassigned (novox/hq issue 1126). Nothing was called"
// commands is every line, longest words first within a word.
var commands = []command{
// What runs where.
{words: []string{"status"}, does: "what is wrong, what is quiet, what is moving (the delivery view)",
build: fixed("mesh-delivery-view.view_status")},
{words: []string{"nodes"}, does: "the nodes placement holds", build: fixed("mesh-placement.placement"),
render: renderNodes},
{words: []string{"node", "list"}, does: "the nodes placement holds", build: fixed("mesh-placement.placement"),
render: renderNodes},
{words: []string{"node", "show"}, usage: "<node>", does: "one node's placement: what it runs and its decided fields",
build: nodeArg("mesh-placement.placement")},
{words: []string{"node", "public-domain"}, usage: "<node> [<domain> | --clear] [--why <text>]",
does: "the domain a node composes its routed names under; with a domain or --clear, proposed to placement",
allowed: []string{"clear", "why"}, build: publicDomain},
{words: []string{"node", "add"}, instead: "a new machine joins through the seat mesh-enrolment: nox enrol issue <node> --why <text>"},
{words: []string{"node", "networks"}, instead: "no seat keeps a node's routed networks yet; mesh-placement.placement shows what a node decides"},
{words: []string{"plan"}, usage: "<node>", does: "what the assigner wants the node to run", build: optNode("mesh-delivery-view.assigner_wants")},
{words: []string{"haves"}, usage: "[<node>]", does: "what each node runs, as its node-engine said", build: optNode("mesh-delivery-view.node_haves")},
{words: []string{"switch"}, does: "what each node runs, as its node-engine said", build: fixed("mesh-delivery-view.node_haves")},
{words: []string{"composition"}, usage: "<node> <module>", does: "one module's composition on a node beside what runs",
build: func(l line) (call, error) {
if len(l.pos) != 2 {
return call{}, usageErr("give the node and the module")
}
return call{l.pos[0] + "/node-engine.composition", map[string]any{"module": l.pos[1]}}, nil
}},
{words: []string{"seats"}, does: "who holds each seat, and where", build: func(line) (call, error) {
return call{"mesh-placement.placement", map[string]any{"seats": "true"}}, nil
}},
{words: []string{"seat"}, instead: "a seat is held by placing its module: nox assign <node> <module> --why <text> (mesh-placement.propose)"},
{words: []string{"assign"}, usage: "<node> <module>... --why <text>", does: "propose placing modules on a node",
allowed: []string{"why"}, build: placeModules("place")},
{words: []string{"unassign"}, usage: "<node> <module>... --why <text>", does: "propose taking modules off a node",
allowed: []string{"why"}, build: placeModules("take-off")},
{words: []string{"pin"}, usage: "<node> <provision> <from-node> <module> --why <text>", does: "propose which provider a node gets a provision from",
allowed: []string{"why"}, build: func(l line) (call, error) {
if len(l.pos) != 4 {
return call{}, usageErr("give the node, the provision, the provider's node and its module")
}
return propose(l, map[string]any{"op": "pin", "node": l.pos[0], "provision": l.pos[1],
"provider": map[string]any{"node": l.pos[2], "module": l.pos[3]}})
}},
{words: []string{"unpin"}, usage: "<node> <provision> --why <text>", does: "propose putting that question back",
allowed: []string{"why"}, build: func(l line) (call, error) {
if len(l.pos) != 2 {
return call{}, usageErr("give the node and the provision")
}
return propose(l, map[string]any{"op": "unpin", "node": l.pos[0], "provision": l.pos[1]})
}},
{words: []string{"proposals"}, usage: "[<id>]", does: "the open proposals of placement", build: optArg("mesh-placement.proposals", "id")},
{words: []string{"overlay", "show"}, does: "every node's placement, with its tunnel", build: fixed("mesh-placement.placement")},
{words: []string{"overlay"}, instead: "a node's tunnel is a decided field of its placement: mesh-placement.propose with op decide"},
// Deliveries and builds.
{words: []string{"deliveries"}, usage: "[--state s] [--repository owner/repo] [--group g] [--all]",
does: "every delivery not final, and those that ended in the last day",
allowed: []string{"state", "repository", "group", "all"}, build: func(l line) (call, error) {
return call{"mesh-delivery.deliveries", pick(l, "state", "repository", "group", "all")}, nil
}},
{words: []string{"delivery"}, usage: "<id>", does: "one delivery or group whole", build: needArg("mesh-delivery.show", "id")},
{words: []string{"groups"}, usage: "[--all]", does: "every delivery group", allowed: []string{"all"},
build: func(l line) (call, error) { return call{"mesh-delivery.groups", pick(l, "all")}, nil }},
{words: []string{"stalled"}, does: "every delivery held past its state's bound", build: fixed("mesh-delivery.stalled")},
{words: []string{"checks"}, usage: "<owner/repo> [--number n | --commit c]", does: "what the mesh's checks said of a head",
allowed: []string{"number", "commit"}, build: func(l line) (call, error) {
if len(l.pos) != 1 {
return call{}, usageErr("give the repository, owner/repo")
}
a := pick(l, "number", "commit")
a["repository"] = l.pos[0]
return call{"mesh-delivery.checks", a}, nil
}},
{words: []string{"try"}, usage: "<owner/repo> <head> [--level l] [--trunk t] [--tests t]",
does: "try a pushed head before its pull request is opened", allowed: []string{"level", "trunk", "tests"},
build: func(l line) (call, error) {
if len(l.pos) != 2 {
return call{}, usageErr("give the repository and the head")
}
a := pick(l, "level", "trunk", "tests")
a["repository"], a["head"] = l.pos[0], l.pos[1]
return call{"mesh-delivery-checker.try", a}, nil
}},
{words: []string{"builds"}, usage: "[--since <RFC 3339 time>]", does: "every build that ended since (the last day by default)",
allowed: []string{"since"}, build: func(l line) (call, error) {
since := l.flags["since"]
if since == "" {
since = now().Add(-24 * time.Hour).UTC().Format(time.RFC3339)
}
return call{"mesh-delivery-builder.builder_builds_since", map[string]any{"since": since}}, nil
}},
{words: []string{"upgrade"}, usage: "[<module>] [--policy roll|together|record|default --why <text>]",
does: "whether each module's new builds wait for a person; with --policy, a person's choice",
allowed: []string{"policy", "why"}, build: func(l line) (call, error) {
if len(l.pos) > 1 {
return call{}, usageErr("give at most one module; rolling out is the delivery's: nox deliveries")
}
a := pick(l, "policy", "why")
if len(l.pos) == 1 {
a["module"] = l.pos[0]
}
if a["policy"] != nil && (a["module"] == nil || a["why"] == nil) {
return call{}, usageErr("a policy needs the module and --why")
}
return call{"mesh-wants.upgrade", a}, nil
}},
{words: []string{"bus"}, does: "what a bus upgrade would do", build: fixed("mesh-wants.bus")},
// Conditions, the self-check, the healers and hand acts.
{words: []string{"conditions", "show"}, usage: "<key>", does: "one condition whole", build: needArg("mesh-conditions.conditions", "key")},
{words: []string{"conditions", "silence"}, usage: "<key> --for <30m|4h|2d> --why <text> [--cause <word>]",
does: "send no message for it a while; a hand act", allowed: []string{"for", "why", "cause"},
build: func(l line) (call, error) {
if len(l.pos) != 1 || !l.has("for") || !l.has("why") {
return call{}, usageErr("give the key, --for and --why")
}
a := pick(l, "for", "why", "cause")
a["key"] = l.pos[0]
return call{"mesh-conditions.silence", a}, nil
}},
{words: []string{"conditions", "history"}, usage: "[--days N] [--key K] [--limit N]", does: "every raising, change and clearing lately",
allowed: []string{"days", "key", "limit"}, build: func(l line) (call, error) {
if l.has("key") {
return call{"mesh-conditions.conditions_history", pick(l, "key", "limit")}, nil
}
a := pick(l, "days")
a["history"] = "true"
return call{"mesh-conditions.conditions", a}, nil
}},
{words: []string{"conditions"}, usage: "[--scope S] [--severity S] [--machine M]", does: "what is wrong now, urgent first",
allowed: []string{"scope", "severity", "machine"}, build: func(l line) (call, error) {
if len(l.pos) > 0 {
return call{}, usageErr("%q is not a conditions line", l.pos[0])
}
return call{"mesh-conditions.conditions", pick(l, "scope", "severity", "machine")}, nil
}},
{words: []string{"doctor", "probes"}, does: "the self-check's probes", build: fixed("mesh-self-check.probes")},
{words: []string{"doctor", "run"}, does: "run every probe now", build: func(line) (call, error) {
return call{"mesh-self-check.doctor", map[string]any{"run": true}}, nil
}},
{words: []string{"doctor", "probe"}, usage: "<id>", does: "one probe's verdict whole", build: needArg("mesh-self-check.doctor", "probe")},
{words: []string{"doctor", "signals"}, does: "the watchman's signals and their ages", build: fixed("mesh-watchdogs.signals")},
{words: []string{"doctor"}, does: "the self-check's last verdict", build: fixed("mesh-self-check.doctor")},
{words: []string{"healers"}, usage: "[--days N]", does: "the healers, what they did lately, and their brake",
allowed: []string{"days"}, build: func(l line) (call, error) {
a, err := numbers(pick(l, "days"), "days")
return call{"mesh-healers.healers", a}, err
}},
{words: []string{"hand-act", "record"}, usage: "<what> --why <text> [--cause <word>] [--condition <key>]",
does: "record an act done by hand", allowed: []string{"why", "cause", "condition"},
build: func(l line) (call, error) {
if len(l.pos) == 0 || !l.has("why") {
return call{}, usageErr("give what was done and --why")
}
a := pick(l, "why", "cause", "condition")
a["what"] = strings.Join(l.pos, " ")
return call{"mesh-hand-acts.hand-act", a}, nil
}},
{words: []string{"hand-acts"}, usage: "[--days N] [--cause c] [--who w]", does: "what was done by hand lately",
allowed: []string{"days", "cause", "who"}, build: func(l line) (call, error) {
a, err := numbers(pick(l, "days", "cause", "who"), "days")
return call{"mesh-hand-acts.hand-acts", a}, err
}},
{words: []string{"stuck"}, does: "what stopped moving, as the watchman holds it", build: fixed("mesh-watchdogs.stuck")},
{words: []string{"dead-letters"}, usage: "[--consumer c]", does: "what a consumer gave up on", allowed: []string{"consumer"},
build: func(l line) (call, error) { return call{"mesh-watchdogs.dead-letters", pick(l, "consumer")}, nil }},
// Config, secrets, the vault, artifacts, licences, modules.
{words: []string{"settings", "show"}, usage: "<module> [--node n]", does: "a module's config values",
allowed: []string{"node"}, build: func(l line) (call, error) {
if len(l.pos) != 1 {
return call{}, usageErr("give the module")
}
a := pick(l, "node")
a["action"], a["module"] = "list", l.pos[0]
return call{"mesh-config.config", a}, nil
}},
{words: []string{"settings", "set"}, usage: "<module> <key> <value> --node n --why <text>",
does: "set one of a module's config values on one node", allowed: []string{"node", "why"},
build: func(l line) (call, error) { return configChange(l, "set") }},
{words: []string{"settings", "clear"}, usage: "<module> <key> --node n --why <text>",
does: "clear one of a module's config values on one node", allowed: []string{"node", "why"},
build: func(l line) (call, error) { return configChange(l, "clear") }},
{words: []string{"config"}, usage: "[list|set|clear|history|propose|proposals] [--module m] [--node n] [--key k] [--value v] [--why w]",
does: "the mesh's config values, at any level", allowed: []string{"module", "node", "key", "value", "why", "limit", "proposal", "pull", "clear"},
build: func(l line) (call, error) {
if len(l.pos) > 1 {
return call{}, usageErr("give at most one action")
}
a := pick(l, "module", "node", "key", "why", "proposal", "pull", "clear", "limit")
if len(l.pos) == 1 {
a["action"] = l.pos[0]
}
if v, ok := l.flags["value"]; ok {
a["value"] = jsonOrText(v)
}
if c, ok := a["clear"]; ok {
a["clear"] = c == "true"
}
a, err := numbers(a, "limit")
return call{"mesh-config.config", a}, err
}},
{words: []string{"secret", "export"}, does: "every secret the vault holds, as its copy sealed to the operator key",
build: fixed("mesh-vault.secret_vault_export")},
{words: []string{"secret", "accept"}, instead: "mesh-vault.accept takes a value from the controller alone, and no seat takes one from a person yet; a module's own secret is asked at a desk (mesh-vault.secret-ask)"},
{words: []string{"secret", "recover"}, instead: "opening a sealed copy locally is not built yet (beside nox enrol open); nox secret export gives the sealed copies"},
{words: []string{"secret"}, instead: "nox secret export; mesh-vault.rotate makes one anew"},
{words: []string{"operator", "key", "show"}, does: "what needs a person's eye in the vault, the operator key among it", build: fixed("mesh-vault.vault_status")},
{words: []string{"operator"}, instead: "no seat sets or makes the operator key yet; nox operator key show reads it (mesh-vault.vault_status)"},
{words: []string{"rotate"}, usage: "[--node n --module m --name s | --provider n --provision p] --why <text>",
does: "make a secret anew", allowed: []string{"node", "module", "name", "provider", "provision", "consumer", "consumer_module", "why"},
build: func(l line) (call, error) {
if len(l.pos) == 1 {
l.flags["provision"] = l.pos[0]
} else if len(l.pos) > 1 {
return call{}, usageErr("give the secret by its flags")
}
if !l.has("why") {
return call{}, usageErr("give --why")
}
return call{"mesh-vault.rotate", pick(l, "node", "module", "name", "provider", "provision", "consumer", "consumer_module", "why")}, nil
}},
{words: []string{"identity", "show"}, does: "the vault's signing key and authority", build: fixed("mesh-vault.vault_signers")},
{words: []string{"retire", "approve"}, usage: "<consumer>... --why <text>", does: "retire the set waiting for a person",
allowed: []string{"why"}, build: retirement("mesh-vault.provisioner_retire_approve")},
{words: []string{"retire", "reject"}, usage: "<consumer>... --why <text>", does: "keep the set waiting for a person active",
allowed: []string{"why"}, build: retirement("mesh-vault.provisioner_retire_reject")},
{words: []string{"retire"}, does: "every retirement waiting for a person", build: fixed("mesh-vault.provisioner_retirement")},
{words: []string{"cleanup", "delete"}, usage: "<consumer> --why <text> --confirm", does: "delete one retired holder from the ledger",
allowed: []string{"why", "confirm"}, build: func(l line) (call, error) {
if len(l.pos) != 1 || !l.has("why") {
return call{}, usageErr("give the consumer and --why")
}
a := pick(l, "why")
a["consumer"] = l.pos[0]
// The vault deletes only when confirm repeats the holder's name (mesh-vault retirement.go); without
// --confirm it answers what it would delete.
if l.has("confirm") {
a["confirm"] = l.pos[0]
}
return call{"mesh-vault.provisioner_delete", a}, nil
}},
{words: []string{"cleanup"}, does: "every retired holder per provider", build: fixed("mesh-vault.provisioner_retirement")},
{words: []string{"artifacts"}, usage: "[--collected]", does: "every artifact the mesh made: kept and why, or eligible",
allowed: []string{"collected"}, build: func(l line) (call, error) {
return call{"mesh-artifact-sweep.artifacts", pick(l, "collected")}, nil
}},
{words: []string{"collection"}, does: "every artifact the mesh made: kept and why, or eligible", build: fixed("mesh-artifact-sweep.artifacts")},
{words: []string{"collect"}, usage: "[--confirm --why <text>] [--most n]", does: "the sweep: a dry run, or with --confirm the sweep now",
allowed: []string{"confirm", "why", "most"}, build: func(l line) (call, error) {
if l.has("confirm") && !l.has("why") {
return call{}, usageErr("--confirm needs --why")
}
return call{"mesh-artifact-sweep.collect", pick(l, "confirm", "why", "most")}, nil
}},
{words: []string{"images"}, usage: "<node>", does: "every container image a machine's wants and haves name", build: needArg("mesh-artifact-sweep.images", "node")},
{words: []string{"mirrors"}, does: "the bases the mesh copied into its store", build: fixed("mesh-artifact-sweep.mirrors")},
{words: []string{"licence", "list"}, does: "every licence the manager holds", build: fixed("anthropic-licence-manager.licences")},
{words: []string{"licence", "use"}, usage: "<consumer> <licence>", does: "move a node's agent to another licence",
build: func(l line) (call, error) {
if len(l.pos) != 2 {
return call{}, usageErr("give the consumer (a node) and the licence")
}
return call{"anthropic-licence-manager.switch", map[string]any{"consumer": l.pos[0], "licence": l.pos[1]}}, nil
}},
{words: []string{"licence", "refresh"}, usage: "[<licence>]", does: "refresh a licence now, or every due one", build: optArg("anthropic-licence-manager.refresh", "licence")},
{words: []string{"licence", "key"}, does: "the manager's public key", build: fixed("anthropic-licence-manager.public-key")},
{words: []string{"licence"}, instead: "anthropic-licence-manager.adopt takes an API key from a file on the manager's node; nox licence list|use|refresh|key"},
{words: []string{"module", "list"}, does: "every module the indexer holds", build: fixed("mesh-delivery-view.indexer_modules")},
{words: []string{"module", "show"}, usage: "<module>", does: "what the indexer holds of one module", build: needArg("mesh-delivery-view.indexer_module", "module")},
{words: []string{"module", "moved"}, usage: "<module> --why <text>", does: "take a registered module in now", allowed: []string{"why"},
build: func(l line) (call, error) {
if len(l.pos) < 1 {
return call{}, usageErr("give the module")
}
return call{"mesh-delivery-indexer.indexer_take_in", map[string]any{"module": l.pos[0], "why": l.flags["why"]}}, nil
}},
{words: []string{"module"}, instead: "the indexer takes modules in from their repositories; nox module list|show|moved, and nothing forgets one by hand yet"},
{words: []string{"register"}, instead: "no seat registers a module from another repository yet (novox/hq ADR 0501 §4); nox module moved <module> takes a registered one in"},
{words: []string{"account", "issue"}, usage: "<user>", does: "mint one bus account the grants part wants, now", build: needArg("mesh-vault.issue-account", "user")},
// Issues and records.
{words: []string{"issues"}, usage: "[<words>] [--status s] [--limit n] [--owner o]", does: "the issues, newest first, or those that match",
allowed: []string{"status", "limit", "owner", "severity", "needs"}, build: func(l line) (call, error) {
a := pick(l, "status", "owner", "severity", "needs", "limit")
if len(l.pos) > 0 {
a["search"] = strings.Join(l.pos, " ")
}
a, err := numbers(a, "limit")
return call{"issue-tracker.list", a}, err
}},
{words: []string{"issue"}, usage: "<number> [--brief]", does: "one issue whole", allowed: []string{"brief"},
build: func(l line) (call, error) {
if len(l.pos) != 1 {
return call{}, usageErr("give the issue's number")
}
n, err := strconv.Atoi(l.pos[0])
if err != nil || n < 1 {
return call{}, usageErr("%q is not an issue's number", l.pos[0])
}
a := map[string]any{"number": n}
if l.has("brief") {
a["brief"] = true
}
return call{"issue-tracker.get", a}, nil
}},
{words: []string{"kb", "ask"}, usage: "<question>", does: "what holds about something, and why", build: func(l line) (call, error) {
if len(l.pos) == 0 {
return call{}, usageErr("give the question")
}
return call{"knowledge-base.ask", map[string]any{"question": strings.Join(l.pos, " ")}}, nil
}},
{words: []string{"kb", "get"}, usage: "<number> [--kind research] [--reasoning]", does: "one record by its number",
allowed: []string{"kind", "reasoning"}, build: func(l line) (call, error) {
if len(l.pos) != 1 {
return call{}, usageErr("give the record's number")
}
n, err := strconv.Atoi(l.pos[0])
if err != nil {
return call{}, usageErr("%q is not a record's number", l.pos[0])
}
a := pick(l, "kind")
a["number"] = n
if l.has("reasoning") {
a["reasoning"] = true
}
return call{"knowledge-base.get", a}, nil
}},
{words: []string{"progress"}, usage: "[<design>|mesh] [--detail minimal|detailed]", does: "how far the designs have come",
allowed: []string{"detail", "since"}, build: func(l line) (call, error) {
a := pick(l, "detail", "since")
a["of"] = "mesh"
if len(l.pos) == 1 {
a["of"] = l.pos[0]
if _, err := strconv.Atoi(l.pos[0]); err == nil {
a["of"] = "design " + l.pos[0]
}
}
return call{"knowledge-base.progress", a}, nil
}},
// Enrolment (the seat mesh-enrolment; `enrol open` is local, in main.go).
{words: []string{"enrol", "issue"}, usage: "<node> --why <text> [--expires 1h]", does: "a one-time token for a new machine, on the operator's Approve",
allowed: []string{"why", "expires"}, build: enrolIssue},
{words: []string{"token", "issue"}, usage: "--new <node> --why <text>", does: "the same as nox enrol issue",
allowed: []string{"new", "node", "why", "expires", "adopted"}, build: func(l line) (call, error) {
node := l.flags["new"]
if node == "" {
node = l.flags["node"]
}
if node != "" {
l.pos = append([]string{node}, l.pos...)
}
return enrolIssue(l)
}},
{words: []string{"enrol", "tokens"}, usage: "[<node>]", does: "every open enrolment, or one", build: optArg("mesh-enrolment.tokens", "node")},
{words: []string{"enrol", "revoke"}, usage: "<node> --why <text>", does: "end an enrolment at any stage", allowed: []string{"why"},
build: func(l line) (call, error) {
if len(l.pos) != 1 || !l.has("why") {
return call{}, usageErr("give the node and --why")
}
return call{"mesh-enrolment.revoke", map[string]any{"node": l.pos[0], "why": l.flags["why"]}}, nil
}},
{words: []string{"enrol", "again"}, usage: "<node>", does: "raise an enrolment's ask again", build: needArg("mesh-enrolment.again", "node")},
// Any address.
{words: []string{"call"}, usage: "<address> [<json arguments>]", does: "call any seat verb or module tool, e.g. ace/node-service-manager.status",
build: func(l line) (call, error) {
if len(l.pos) < 1 || len(l.pos) > 2 {
return call{}, usageErr("give the address and at most one JSON object of arguments")
}
return addressCall(l.pos[0], l.pos[1:])
}},
{words: []string{"ask"}, usage: "<module> <tool> [<json arguments>]", does: "call one of a module's tools",
build: func(l line) (call, error) {
if len(l.pos) < 2 || len(l.pos) > 3 {
return call{}, usageErr("give the module, the tool and at most one JSON object of arguments")
}
return addressCall(l.pos[0]+"."+l.pos[1], l.pos[2:])
}},
}
// unowned are the controller's own lines that no seat or module took over, and what to use where there is anything.
var unowned = map[string]string{
"migrate": "", "prepare": "", "serve": "", "declare": "", "broker": "", "board": "", "api": "", "facts": "",
"rehearse": "", "take": "", "converge": "", "adopt": "", "move-placement": "", "move-config": "", "durations": "",
"data": "each machine's backups: nox call <node>/node-backup.backed-up",
"forget-checks": "nox checks <owner/repo>", "queue": "nox builds; nox stuck", "cancel": "nox deliveries",
"clear": "nox deliveries", "kill": "nox deliveries", "replay": "nox dead-letters",
"pause": "nox call <node>/node-build-go.pause (or node-build-node, node-build-image)",
"resume": "nox call <node>/node-build-go.resume (or node-build-node, node-build-image)",
"push": "a merge is delivered by mesh-delivery: nox deliveries; one version on one machine is mesh-delivery-assigner.assigner_put",
"deliver": "nox deliveries", "plans": "nox deliveries", "delivery-stop": "nox stalled",
"delivery-backups-again": "nox deliveries", "delivery-walks": "nox deliveries", "delivery-order": "nox deliveries",
"rollout": "nox upgrade <module>; delivery is mesh-delivery's: nox deliveries",
}
// find answers the command whose words begin the line, the longest first, and what follows them.
func find(args []string) (*command, []string) {
var best *command
for i := range commands {
c := &commands[i]
if len(c.words) > len(args) || (best != nil && len(c.words) <= len(best.words)) {
continue
}
match := true
for j, w := range c.words {
if args[j] != w {
match = false
break
}
}
if match {
best = c
}
}
if best == nil {
return nil, nil
}
return best, args[len(best.words):]
}
// read builds the call of a line for a command that has an owner.
func (c *command) read(args []string) (call, error) {
l, err := parseLine(args)
if err != nil {
return call{}, err
}
allowed := map[string]bool{"json": true}
for _, f := range c.allowed {
allowed[f] = true
}
for f := range l.flags {
if !allowed[f] {
return call{}, usageErr("--%s is not a flag of this line", f)
}
}
return c.build(l)
}
// now is the clock; a variable so a test can say.
var now = time.Now
func fixed(address string) func(line) (call, error) {
return func(l line) (call, error) {
if len(l.pos) > 0 {
return call{}, usageErr("this line takes no %q", l.pos[0])
}
return call{address, map[string]any{}}, nil
}
}
func needArg(address, name string) func(line) (call, error) {
return func(l line) (call, error) {
if len(l.pos) != 1 {
return call{}, usageErr("give the %s", name)
}
return call{address, map[string]any{name: l.pos[0]}}, nil
}
}
func optArg(address, name string) func(line) (call, error) {
return func(l line) (call, error) {
switch len(l.pos) {
case 0:
return call{address, map[string]any{}}, nil
case 1:
return call{address, map[string]any{name: l.pos[0]}}, nil
}
return call{}, usageErr("give at most one %s", name)
}
}
func nodeArg(address string) func(line) (call, error) { return needArg(address, "node") }
func optNode(address string) func(line) (call, error) { return optArg(address, "node") }
// pick is the line's flags named, as string arguments.
func pick(l line, names ...string) map[string]any {
a := map[string]any{}
for _, n := range names {
if v, ok := l.flags[n]; ok {
a[n] = v
}
}
return a
}
// numbers turns the named arguments into numbers; the call's address is left to the caller.
func numbers(a map[string]any, names ...string) (map[string]any, error) {
for _, n := range names {
v, ok := a[n].(string)
if !ok {
continue
}
i, err := strconv.Atoi(v)
if err != nil {
return a, usageErr("--%s takes a number, not %q", n, v)
}
a[n] = i
}
return a, nil
}
// propose is a change of placement with its why.
func propose(l line, changes ...map[string]any) (call, error) {
if !l.has("why") {
return call{}, usageErr("a change of placement needs --why; the operator is asked to approve it")
}
list := make([]any, len(changes))
for i, c := range changes {
list[i] = c
}
return call{"mesh-placement.propose", map[string]any{"why": l.flags["why"], "changes": list}}, nil
}
func placeModules(op string) func(line) (call, error) {
return func(l line) (call, error) {
if len(l.pos) < 2 {
return call{}, usageErr("give the node and at least one module")
}
var changes []map[string]any
for _, m := range l.pos[1:] {
changes = append(changes, map[string]any{"op": op, "node": l.pos[0], "module": m})
}
return propose(l, changes...)
}
}
func publicDomain(l line) (call, error) {
switch {
case len(l.pos) == 1 && !l.has("clear"):
return call{"mesh-placement.placement", map[string]any{"node": l.pos[0]}}, nil
case len(l.pos) == 1:
return propose(l, map[string]any{"op": "decide", "node": l.pos[0], "field": "public-domain", "value": nil})
case len(l.pos) == 2 && !l.has("clear"):
return propose(l, map[string]any{"op": "decide", "node": l.pos[0], "field": "public-domain", "value": l.pos[1]})
}
return call{}, usageErr("give the node, and a domain or --clear")
}
func configChange(l line, action string) (call, error) {
want := 3
if action == "clear" {
want = 2
}
if len(l.pos) == 2 && action == "set" && strings.HasPrefix(strings.TrimSpace(l.pos[1]), "{") {
return call{}, usageErr("a whole settings file is not set at once any more: set each key, " +
"nox settings set <module> <key> <value> --node <node> --why <text> (mesh-config.config)")
}
if len(l.pos) != want || !l.has("node") || !l.has("why") {
return call{}, usageErr("give the module, the key%s, --node and --why", map[bool]string{true: " and the value"}[action == "set"])
}
a := map[string]any{"action": action, "module": l.pos[0], "key": l.pos[1], "node": l.flags["node"], "why": l.flags["why"]}
if action == "set" {
a["value"] = jsonOrText(l.pos[2])
}
return call{"mesh-config.config", a}, nil
}
// jsonOrText is a value as JSON when it reads as JSON (a number, true, an object), else the text itself.
func jsonOrText(v string) any {
var out any
if err := json.Unmarshal([]byte(v), &out); err == nil {
return out
}
return v
}
func retirement(address string) func(line) (call, error) {
return func(l line) (call, error) {
if len(l.pos) == 0 || !l.has("why") {
return call{}, usageErr("give the consumers waiting and --why")
}
consumers := make([]any, len(l.pos))
for i, c := range l.pos {
consumers[i] = c
}
return call{address, map[string]any{"consumers": consumers, "why": l.flags["why"]}}, nil
}
}
func enrolIssue(l line) (call, error) {
if len(l.pos) != 1 || !l.has("why") {
return call{}, usageErr("give the new machine's name and --why")
}
a := pick(l, "why", "expires")
a["node"] = l.pos[0]
return call{"mesh-enrolment.issue", a}, nil
}
func addressCall(address string, rest []string) (call, error) {
a := map[string]any{}
if len(rest) == 1 {
if err := json.Unmarshal([]byte(rest[0]), &a); err != nil {
return call{}, usageErr("the arguments are not a JSON object: %v", err)
}
}
return call{address, a}, nil
}
// renderNodes shapes placement's keys as one line per node: its name, address and site.
func renderNodes(text string) string {
var p struct {
Keys map[string]struct {
Placement struct {
Decided map[string]any `json:"decided"`
Modules []any `json:"modules"`
} `json:"placement"`
} `json:"keys"`
}
// The console says after the answer which machine answered it ("answered by ace"): kept, below the lines.
dec := json.NewDecoder(strings.NewReader(text))
if err := dec.Decode(&p); err != nil || len(p.Keys) == 0 {
return text
}
after := ""
if rest, err := io.ReadAll(dec.Buffered()); err == nil {
after = strings.TrimSpace(string(rest))
}
names := make([]string, 0, len(p.Keys))
for n := range p.Keys {
names = append(names, n)
}
sort.Strings(names)
var b strings.Builder
for _, n := range names {
k := p.Keys[n]
fmt.Fprintf(&b, "%-10s %-15v %-8v %d modules\n", n, orDash(k.Placement.Decided["address"]),
orDash(k.Placement.Decided["site"]), len(k.Placement.Modules))
}
if after != "" {
b.WriteString(after + "\n")
}
return b.String()
}
func orDash(v any) any {
if v == nil || v == "" {
return "-"
}
return v
}