mesh/delivery merged: source.main moved to novox/mesh-catalog@32c4c52261c4
mesh/check checking 63de1f7e0dd7 on 32c4c52261c4
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
730 lines
33 KiB
Go
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
|
|
}
|