// `mosaic hosts` (row 234, #1740, r6 E4): the broker's hosts.status, hosts.check and hosts.remove. The // human runs all three. With --tester the tester's session runs them. check logs in as mosaic-installer // and runs `list` on the host, then marks the host ready. remove drops a host no grant uses. Nothing here // reads a key: the answers carry fingerprints only. Row 235 (#1741, r6 section 6, X3) adds revoke, which // takes an agent's grant on a host away at once, and sweep, which removes the lines a failure left. import { CliError } from "./errors.mjs"; import { orgSelector } from "./org-selector.mjs"; export const HOSTS_USAGE = "mosaic hosts [status] [--json] [--tester] [--business ID] | check|remove|sweep HOST [--tester] [--business ID] | revoke AGENT HOST --reason R [--tester] [--business ID]"; function parse(args) { const out = { positional: [], flags: new Set(), values: {} }; // --org and --business are one selector in this parser's scope (rename design r2). const selector = orgSelector(() => new CliError(HOSTS_USAGE, 4)); for (let i = 0; i < args.length; i++) { const arg = args[i]; if (selector.owns(arg)) { selector.take(arg, args[i + 1]); i++; } else if (arg === "--json" || arg === "--tester") out.flags.add(arg); else if (arg === "--reason") { if (!args[i + 1] || Object.hasOwn(out.values, arg)) throw new CliError(HOSTS_USAGE, 4); out.values[arg] = args[++i]; } else if (arg.startsWith("--")) throw new CliError(HOSTS_USAGE, 4); else out.positional.push(arg); } if (selector.given) out.values["--business"] = selector.value; return out; } const line = (h) => [h.host, h.state, `${h.user}@${h.address}:${h.port}`, h.sudo ? "sudo" : "no-sudo", h.checkedAt ?? "unchecked", ...(h.hostLineLeft.length ? [`line left: ${h.hostLineLeft.join(" ")}`] : []), ...(h.revoking?.length ? [`revoking: ${h.revoking.join(" ")}`] : [])].join("\t"); export async function hostsCommand(args, io, callFor) { const parsed = parse(args); const [verb = "status", first, second, ...extra] = parsed.positional; const one = first !== undefined && second === undefined; const shape = { status: first === undefined, check: one, remove: one, sweep: one, revoke: second !== undefined }; // N10: own keys only, so `constructor` or `toString` is a usage error, not a verb. if (extra.length || !Object.hasOwn(shape, verb) || !shape[verb] || (parsed.flags.has("--json") && verb !== "status") || (verb === "revoke") !== Object.hasOwn(parsed.values, "--reason")) throw new CliError(HOSTS_USAGE, 4); try { const call = callFor(parsed); if (verb === "status") { const rows = await call("hosts.status", {}); io.stdout.write(parsed.flags.has("--json") ? JSON.stringify(rows, null, 2) + "\n" : rows.map(line).join("\n") + (rows.length ? "\n" : "")); return; } const args = verb === "revoke" ? { agent: first, host: second, reason: parsed.values["--reason"] } : { host: first }; io.stdout.write(JSON.stringify(await call(`hosts.${verb}`, args), null, 2) + "\n"); } catch (error) { if (error instanceof CliError) throw error; throw new CliError(error.message, 2); } }