Skip to content

Commit 09ec703

Browse files
Merge pull request #3 from fiveonecode/cli/human-help-json
Make simbroker help and doctor human-readable by default
2 parents 62bb6f9 + 924faa3 commit 09ec703

8 files changed

Lines changed: 350 additions & 21 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ There is no Homebrew formula, npm package, or GitHub Release yet. Building the
3636
app from this checkout is the current install path. `~/.local/bin` must be on
3737
`PATH`, or the `source .../env.sh` step is required in each new shell.
3838

39+
`simbroker` help and `simbroker doctor` print human-readable text by default.
40+
Pass `--json` for machine-readable payloads.
41+
3942
If the app shows **Set Up This Mac**, click **Complete first-time setup**.
4043
That creates a starter simulator pool. The CLI equivalent is
4144
`simbroker host init --bootstrap-config`, which also creates real simulator

client/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
CLI client and compatibility wrappers live here.
44

5+
Help and `doctor` print human-readable text by default. Pass `--json` for
6+
machine-readable payloads. Other commands still emit JSON by default so
7+
existing wrappers keep working.
8+
59
Current entrypoint:
610

711
- `node client/bin/simbroker.mjs`

client/bin/simbroker.mjs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ async function stopService(paths) {
446446
}
447447

448448
async function runServiceAwareRequest(paths, request) {
449-
const canUseService = !localOnlyMode(process.env);
449+
const canUseService = !localOnlyMode(process.env) && request.group !== "help";
450450
let service = canUseService
451451
? await probeService(paths, { timeoutMs: serviceCommandTimeoutMs(request) })
452452
: null;
@@ -560,12 +560,23 @@ async function runServiceAwareRequest(paths, request) {
560560
return payload;
561561
}
562562

563+
const invocation = parseArgs(process.argv.slice(2));
564+
565+
function wantsJson() {
566+
return invocation.flags.has("json");
567+
}
568+
563569
async function main() {
564-
const { flags, positionals } = parseArgs(process.argv.slice(2));
570+
const { flags, positionals } = invocation;
565571
rejectExtraPositionals(positionals);
566572
const [group, command] = positionals;
567573
const paths = buildPaths(flags);
568574

575+
if (flags.has("help") || group === "help" || command === "help") {
576+
const request = createCommandRequest(paths, group, command, flags);
577+
return runServiceAwareRequest(paths, request);
578+
}
579+
569580
switch (`${group ?? ""}:${command ?? ""}`) {
570581
case "service:start":
571582
rejectUnknownServiceControlFlags(flags);
@@ -591,11 +602,11 @@ main()
591602
process.stdout.write(format({
592603
ok: true,
593604
...payload,
594-
}));
605+
}, { json: wantsJson() }));
595606
})
596607
.catch((error) => {
597608
if (error instanceof BrokerError) {
598-
process.stdout.write(format(error.payload));
609+
process.stdout.write(format(error.payload, { json: true }));
599610
process.exit(error.exitCode);
600611
return;
601612
}
@@ -606,7 +617,7 @@ main()
606617
exitCode,
607618
ok: false,
608619
reasonCode: error.reasonCode,
609-
}));
620+
}, { json: true }));
610621
process.exit(exitCode);
611622
return;
612623
}
@@ -617,6 +628,6 @@ main()
617628
ok: false,
618629
reasonCode: INTERNAL_ERROR_REASON_CODE,
619630
stack: error?.stack ?? null,
620-
}));
631+
}, { json: true }));
621632
process.exit(BROKER_EXIT_CODES.internal);
622633
});

client/command-dispatch.mjs

Lines changed: 185 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,142 @@ function hostInitOptions(flags) {
287287
};
288288
}
289289

290-
export function format(payload) {
291-
return `${JSON.stringify(payload, null, 2)}\n`;
290+
export function format(payload, options = {}) {
291+
if (options.json === true || !shouldFormatAsText(payload)) {
292+
return `${JSON.stringify(payload, null, 2)}\n`;
293+
}
294+
if (isHelpPayload(payload)) {
295+
return formatHelpText(payload);
296+
}
297+
return formatDoctorText(payload);
298+
}
299+
300+
function isHelpPayload(payload) {
301+
return payload != null
302+
&& typeof payload.usage === "string"
303+
&& Array.isArray(payload.commands)
304+
&& typeof payload.group === "string";
305+
}
306+
307+
function isDoctorPayload(payload) {
308+
return payload != null
309+
&& typeof payload.ok === "boolean"
310+
&& Array.isArray(payload.issues)
311+
&& typeof payload.hostConfigPath === "string"
312+
&& typeof payload.stateRoot === "string";
313+
}
314+
315+
function shouldFormatAsText(payload) {
316+
return isHelpPayload(payload) || isDoctorPayload(payload);
317+
}
318+
319+
function formatHelpText(payload) {
320+
const lines = [
321+
"Simulator Broker",
322+
"",
323+
`Usage: ${payload.usage}`,
324+
"",
325+
];
326+
327+
if (payload.group === "global") {
328+
const topLevel = new Set(["doctor"]);
329+
const groups = payload.commands.filter((command) => !topLevel.has(command));
330+
const topLevelCommands = payload.commands.filter((command) => topLevel.has(command));
331+
lines.push("Command groups:");
332+
for (const command of groups) {
333+
lines.push(` ${command}`);
334+
}
335+
if (topLevelCommands.length > 0) {
336+
lines.push("", "Top-level commands:");
337+
for (const command of topLevelCommands) {
338+
lines.push(` ${command}`);
339+
}
340+
}
341+
lines.push(
342+
"",
343+
"Use `simbroker <group> --help` for the commands in a group.",
344+
"Use `simbroker doctor --help` for doctor usage.",
345+
);
346+
} else {
347+
lines.push("Commands:");
348+
for (const command of payload.commands) {
349+
lines.push(` ${command}`);
350+
}
351+
}
352+
353+
lines.push(
354+
"",
355+
"Pass --json for machine-readable output.",
356+
"",
357+
);
358+
return `${lines.join("\n")}`;
359+
}
360+
361+
function formatDoctorIssue(issue) {
362+
if (issue == null || typeof issue !== "object") {
363+
return "- Unexpected doctor issue. Re-run with --json for details.";
364+
}
365+
366+
if (issue.reasonCode === "missing-registry") {
367+
return [
368+
"- Registry: missing.",
369+
" Next: run `simbroker host init --bootstrap-config` if this Mac is not set up yet.",
370+
].join("\n");
371+
}
372+
373+
if (issue.reasonCode === "alias-unhealthy") {
374+
const alias = typeof issue.alias === "string" ? issue.alias : "unknown";
375+
const health = typeof issue.health === "string" ? issue.health : "unhealthy";
376+
return [
377+
`- Alias ${alias}: ${health}.`,
378+
` Next: inspect with \`simbroker host status\`, then repair with \`simbroker simulators repair --alias ${alias}\` if needed.`,
379+
].join("\n");
380+
}
381+
382+
if (typeof issue.error === "string" && issue.error.trim() !== "") {
383+
const reason = typeof issue.reasonCode === "string" ? ` (${issue.reasonCode})` : "";
384+
return `- ${issue.error}${reason}`;
385+
}
386+
387+
if (typeof issue.reasonCode === "string") {
388+
return `- ${issue.reasonCode}. Re-run \`simbroker doctor --json\` for details.`;
389+
}
390+
391+
return "- Unexpected doctor issue. Re-run with --json for details.";
392+
}
393+
394+
function formatDoctorText(payload) {
395+
const lines = [
396+
"Simulator Broker doctor",
397+
"",
398+
`Status: ${payload.ok ? "healthy" : "needs attention"}`,
399+
`Host config: ${payload.hostConfigPath}`,
400+
`State root: ${payload.stateRoot}`,
401+
"",
402+
"Checklist:",
403+
];
404+
405+
if (payload.issues.length === 0) {
406+
lines.push(
407+
"- Host config: ok",
408+
"- Registry: ok",
409+
"- Alias health: ok",
410+
"",
411+
"No issues found.",
412+
"Next: run `simbroker host status` or `simbroker project init` in a repo.",
413+
);
414+
} else {
415+
for (const issue of payload.issues) {
416+
lines.push(formatDoctorIssue(issue));
417+
}
418+
lines.push(
419+
"",
420+
"Pass --json for the machine-readable issue list.",
421+
);
422+
}
423+
424+
lines.push("");
425+
return `${lines.join("\n")}`;
292426
}
293427

294428
export function eventFilters(flags) {
@@ -500,6 +634,49 @@ function helpPayload(group) {
500634
group: "lease",
501635
usage: "simbroker lease <command>",
502636
},
637+
events: {
638+
commands: [
639+
"events watch [--follow] [--json-lines] [--limit <n>] [--after-event-id <id>]",
640+
],
641+
group: "events",
642+
usage: "simbroker events <command>",
643+
},
644+
pin: {
645+
commands: [
646+
"pin create --purpose <purpose> --alias <alias> [--repo-root <repo>] [--note <note>]",
647+
"pin clear --alias <alias>",
648+
],
649+
group: "pin",
650+
usage: "simbroker pin <command>",
651+
},
652+
simulators: {
653+
commands: [
654+
"simulators list",
655+
"simulators boot --alias <alias>",
656+
"simulators shutdown --alias <alias>",
657+
"simulators erase --alias <alias>",
658+
"simulators repair --alias <alias>",
659+
],
660+
group: "simulators",
661+
usage: "simbroker simulators <command>",
662+
},
663+
service: {
664+
commands: [
665+
"service start",
666+
"service status",
667+
"service stop",
668+
],
669+
group: "service",
670+
usage: "simbroker service <command>",
671+
},
672+
doctor: {
673+
commands: [
674+
"doctor",
675+
"doctor --json",
676+
],
677+
group: "doctor",
678+
usage: "simbroker doctor [--json]",
679+
},
503680
};
504681
return {
505682
ok: true,
@@ -514,6 +691,7 @@ function helpPayload(group) {
514691
"pin",
515692
"simulators",
516693
"service",
694+
"doctor",
517695
],
518696
group: "global",
519697
usage: "simbroker <group> <command> [flags]",
@@ -1003,6 +1181,11 @@ export function executeBrokerCommand(paths, request) {
10031181
case "help:lease":
10041182
case "help:capacity":
10051183
case "help:idle":
1184+
case "help:events":
1185+
case "help:pin":
1186+
case "help:simulators":
1187+
case "help:service":
1188+
case "help:doctor":
10061189
return helpPayload(request.command);
10071190
case "doctor:status":
10081191
payload = doctorBroker(paths, options);

0 commit comments

Comments
 (0)