TESTER_VERBS gains usage, names, limits, plugins, instances, project.list, thread.list, name.set and project.create. A tester reads limits, plugins and instances and sends plugins.set, instances.set and routes.set, each logged with actor tester. The project file verbs refuse a tester tester-refused. The broker and CLI drop the [mos test] prefix rule (mos-0872 Q1). The CLI takes --tester on the seven reads and the W1-W7 write forms and refuses it (exit 4) elsewhere, bus start and stop included. project init --tester checks the project area, refuses the area root itself, binds the tester with one agents read, then commits the layout as tester (mos-0872 Q2). scripts/mosaic sends plugins, name, names, follow, threads and thread to packages/cli (sage-1081). The row 164 pins are untouched. Built by Dewey on9e74925c2(three commits to989699f7c, manifest 2e30f2a1...29d5f), approved by Filbert in round 3 (#1673 comment 29279), Sage's gate green on716ddcf1eplus the patch.
122 lines
6.9 KiB
JavaScript
122 lines
6.9 KiB
JavaScript
// `mosaic business instances` and `mosaic business instance add|set` (row 165, #1662, Q171,
|
|
// REQ-ROLE-3): the human reads, adds and changes role instances through the broker's instances
|
|
// and instances.set verbs, and with --tester the tester does (row 175, #1673, Q173). The broker
|
|
// refuses an agent and a reader; the business package's instance writer refuses a stale revision
|
|
// and every document `mosaic business validate` would refuse, and logs the change with its actor.
|
|
import { readFileSync } from "node:fs";
|
|
import { CliError } from "./errors.mjs";
|
|
|
|
export const INSTANCES_USAGE = "mosaic business instances BUSINESS [--json] [--tester] | " +
|
|
"business instance add BUSINESS INSTANCE --definition DEF [--vars FILE] [--launch] --revision REV [--tester] | " +
|
|
"business instance set BUSINESS INSTANCE [--definition DEF] [--vars FILE] [--launch | --no-launch] --revision REV [--tester]";
|
|
|
|
// The bus answers a refusal with its code alone, so the reason a human reads is here.
|
|
export const REASONS = {
|
|
"organization-credentials-out-of-scope": "this command binds no credentials, so it refuses a definition that declares them " +
|
|
"and a definition change on an instance that holds them; that stays a hand edit of the business file",
|
|
"organization-instance-exists": "the business already has that instance; change it with `mosaic business instance set`",
|
|
"organization-unknown-instance": "the business has no such instance; add it with `mosaic business instance add`",
|
|
"organization-definition-refused": "this command neither adds nor changes an assistant, and moves no instance to the assistant " +
|
|
"definition; an assistant changes only through `mosaic limits set` with an assistants patch",
|
|
"organization-launch-not-configured": "the business file has no launch block, so launch membership can't change",
|
|
"organization-no-change": "the business file already says that",
|
|
"organization-project-mismatch": "a project file's id differs from its key in the business file; `mosaic business validate` shows it",
|
|
"organization-unresolvable-instance": "an instance wouldn't resolve against a project file; `mosaic business validate` shows why",
|
|
"revision-conflict": "the business file changed since that revision; read it again with `mosaic business instances`",
|
|
};
|
|
|
|
export const RESTART_NEEDED = "restart needed: the running bus keeps the roles it loaded at start; this command does not restart it";
|
|
const NO_RESTART = "no restart: the launcher reads the change at its next launch check";
|
|
|
|
function parse(args) {
|
|
const out = { positional: [], flags: new Set(), values: {} };
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
if (["--json", "--launch", "--no-launch", "--tester"].includes(arg)) {
|
|
if (out.flags.has(arg)) throw new CliError(INSTANCES_USAGE, 4);
|
|
out.flags.add(arg);
|
|
} else if (["--definition", "--vars", "--revision"].includes(arg)) {
|
|
if (!args[i + 1] || Object.hasOwn(out.values, arg)) throw new CliError(INSTANCES_USAGE, 4);
|
|
out.values[arg] = args[++i];
|
|
} else if (arg.startsWith("--")) throw new CliError(INSTANCES_USAGE, 4);
|
|
else out.positional.push(arg);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// The args instances.set takes, from `instance add|set`. A usage error is exit 4 before any call.
|
|
function setArgs(parsed) {
|
|
const [, op, , instance] = parsed.positional;
|
|
const { values, flags } = parsed;
|
|
const launch = flags.has("--launch") ? true : flags.has("--no-launch") ? false : undefined;
|
|
if (parsed.positional.length !== 4 || !["add", "set"].includes(op) || !values["--revision"] || flags.has("--json") ||
|
|
(flags.has("--launch") && flags.has("--no-launch")) || (op === "add" && (!values["--definition"] || launch === false)) ||
|
|
(op === "set" && !values["--definition"] && !values["--vars"] && launch === undefined)) {
|
|
throw new CliError(INSTANCES_USAGE, 4);
|
|
}
|
|
const args = { revision: values["--revision"], instance, op: op === "add" ? "add" : "change" };
|
|
if (values["--definition"]) args.definition = values["--definition"];
|
|
if (values["--vars"]) {
|
|
try {
|
|
args.vars = JSON.parse(readFileSync(values["--vars"], "utf8"));
|
|
} catch (error) {
|
|
throw new CliError(`--vars ${values["--vars"]}: ${error.message}`, 4);
|
|
}
|
|
}
|
|
if (launch !== undefined) args.launch = launch;
|
|
return args;
|
|
}
|
|
|
|
export function formatInstances(view) {
|
|
const lines = [`business ${view.business}, revision ${view.revision}`];
|
|
for (const [name, entry] of Object.entries(view.instances)) {
|
|
const marks = [entry.launcher && "launcher", entry.launched && "launched", entry.protected && "protected"].filter(Boolean);
|
|
const vars = Object.keys(entry.vars).length ? ` vars ${JSON.stringify(entry.vars)}` : "";
|
|
lines.push(` ${name}: ${entry.definition}${marks.length ? ` (${marks.join(", ")})` : ""}${vars}`);
|
|
}
|
|
const definitions = Object.entries(view.definitions);
|
|
const accepted = definitions.filter(([, d]) => d.accepted).map(([name]) => name);
|
|
const bound = definitions.filter(([, d]) => d.services.length).map(([name, d]) => `${name} (${d.services.join(", ")})`);
|
|
lines.push(`definitions add and set accept: ${accepted.join(", ") || "none"}`);
|
|
if (bound.length) lines.push(`definitions that declare credentials, a hand edit: ${bound.join(", ")}`);
|
|
return `${lines.join("\n")}\n`;
|
|
}
|
|
|
|
// args are the words after `mosaic business`. callFor(parsed) returns the human's call(verb, args).
|
|
export async function instancesCommand(args, io, callFor) {
|
|
const parsed = parse(args);
|
|
const sub = parsed.positional[0];
|
|
const business = parsed.positional[sub === "instances" ? 1 : 2];
|
|
if (!["instances", "instance"].includes(sub) || !business) throw new CliError(INSTANCES_USAGE, 4);
|
|
let request;
|
|
if (sub === "instances") {
|
|
if (parsed.positional.length !== 2 || Object.keys(parsed.values).length || parsed.flags.has("--launch") || parsed.flags.has("--no-launch")) {
|
|
throw new CliError(INSTANCES_USAGE, 4);
|
|
}
|
|
} else request = setArgs(parsed);
|
|
try {
|
|
const call = callFor({ positional: parsed.positional, flags: parsed.flags, values: { "--business": business } });
|
|
if (!request) {
|
|
const view = await call("instances", {});
|
|
return io.stdout.write(parsed.flags.has("--json") ? `${JSON.stringify(view, null, 2)}\n` : formatInstances(view));
|
|
}
|
|
const result = await call("instances.set", request);
|
|
const entry = result.instances[request.instance];
|
|
io.stdout.write([
|
|
`business ${result.business}: ${request.op === "add" ? "added" : "changed"} ${request.instance} (${entry.definition}${entry.launched ? ", launched" : ""})`,
|
|
`revision ${result.revision}`,
|
|
`transaction ${result.transaction}`,
|
|
result.restart ? RESTART_NEEDED : NO_RESTART,
|
|
].join("\n") + "\n");
|
|
} catch (error) {
|
|
const code = error.code;
|
|
if (Object.hasOwn(REASONS, code)) {
|
|
const refused = new CliError(`${code}: ${REASONS[code]}`, error.exitCode ?? 2);
|
|
refused.code = code;
|
|
throw refused;
|
|
}
|
|
if (error instanceof CliError) throw error;
|
|
throw new CliError(error.message, 2);
|
|
}
|
|
}
|