Skip to content

Commit 924faa3

Browse files
Give every advertised simbroker group a working --help page.
Why: Global help told newcomers to run `simbroker <group> --help` for events, pin, simulators, service, and doctor, but those names had no help handler and failed with a JSON unknown-command error. Changed: Add help payloads and execute cases for events, pin, simulators, service, and doctor. Treat doctor as a top-level command in global help. Route `--help` before service start/stop/status. Skip the service probe for help so `--help` stays local. Cover every advertised `--help` page plus alias-unhealthy doctor prose. Specs now record the help/doctor human default and `--json` machine payload. Verification: npm run test:client npm run agent:verify -- --profile spec-only --paths client/command-dispatch.mjs,client/bin/simbroker.mjs,client/test/simbroker.test.mjs,spec/global-simulator-broker.md,spec/architecture.md --session-dir task-sessions/20260818-help-groups npm run agent:verify -- --profile implementation --paths client/command-dispatch.mjs,client/bin/simbroker.mjs,client/test/simbroker.test.mjs,spec/global-simulator-broker.md,spec/architecture.md --session-dir task-sessions/20260818-help-groups Affected: client/command-dispatch.mjs client/bin/simbroker.mjs client/test/simbroker.test.mjs spec/global-simulator-broker.md spec/architecture.md Refs: #3 Session: task-sessions/20260818-help-groups
1 parent c1d3142 commit 924faa3

5 files changed

Lines changed: 124 additions & 8 deletions

File tree

client/bin/simbroker.mjs

Lines changed: 6 additions & 1 deletion
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;
@@ -572,6 +572,11 @@ async function main() {
572572
const [group, command] = positionals;
573573
const paths = buildPaths(flags);
574574

575+
if (flags.has("help") || group === "help" || command === "help") {
576+
const request = createCommandRequest(paths, group, command, flags);
577+
return runServiceAwareRequest(paths, request);
578+
}
579+
575580
switch (`${group ?? ""}:${command ?? ""}`) {
576581
case "service:start":
577582
rejectUnknownServiceControlFlags(flags);

client/command-dispatch.mjs

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,14 +325,23 @@ function formatHelpText(payload) {
325325
];
326326

327327
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));
328331
lines.push("Command groups:");
329-
for (const command of payload.commands) {
332+
for (const command of groups) {
330333
lines.push(` ${command}`);
331334
}
335+
if (topLevelCommands.length > 0) {
336+
lines.push("", "Top-level commands:");
337+
for (const command of topLevelCommands) {
338+
lines.push(` ${command}`);
339+
}
340+
}
332341
lines.push(
333342
"",
334343
"Use `simbroker <group> --help` for the commands in a group.",
335-
"Use `simbroker doctor` to check this Mac.",
344+
"Use `simbroker doctor --help` for doctor usage.",
336345
);
337346
} else {
338347
lines.push("Commands:");
@@ -625,6 +634,49 @@ function helpPayload(group) {
625634
group: "lease",
626635
usage: "simbroker lease <command>",
627636
},
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+
},
628680
};
629681
return {
630682
ok: true,
@@ -1129,6 +1181,11 @@ export function executeBrokerCommand(paths, request) {
11291181
case "help:lease":
11301182
case "help:capacity":
11311183
case "help:idle":
1184+
case "help:events":
1185+
case "help:pin":
1186+
case "help:simulators":
1187+
case "help:service":
1188+
case "help:doctor":
11321189
return helpPayload(request.command);
11331190
case "doctor:status":
11341191
payload = doctorBroker(paths, options);

client/test/simbroker.test.mjs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import path from "node:path";
77
import { spawn, spawnSync } from "node:child_process";
88
import { EventEmitter } from "node:events";
99

10-
import { createCommandRequest, executeBrokerCommand, parseArgs, streamEventsLocal } from "../command-dispatch.mjs";
10+
import { createCommandRequest, executeBrokerCommand, format, parseArgs, streamEventsLocal } from "../command-dispatch.mjs";
1111
import {
1212
executeServiceCommand,
1313
probeService,
@@ -535,7 +535,10 @@ test("bare help and doctor print human text by default and JSON with --json", ()
535535
assert.notEqual(result.stdout.trimStart()[0], "{");
536536
assert.match(result.stdout, /Usage:/);
537537
assert.match(result.stdout, /host/);
538+
assert.match(result.stdout, /Top-level commands:/);
538539
assert.match(result.stdout, /doctor/);
540+
assert.match(result.stdout, /simbroker <group> --help/);
541+
assert.match(result.stdout, /simbroker doctor --help/);
539542
assert.match(result.stdout, /--json/);
540543
}
541544

@@ -573,6 +576,54 @@ test("bare help and doctor print human text by default and JSON with --json", ()
573576
assert.equal(typeof doctorJson.json.stateRoot, "string");
574577
});
575578

579+
test("every advertised group and doctor have a working --help page", () => {
580+
const fixture = makeFixture();
581+
const advertised = [
582+
["host", "host status"],
583+
["project", "project forget"],
584+
["lease", "lease contain"],
585+
["capacity", "capacity check"],
586+
["idle", "idle status"],
587+
["events", "events watch"],
588+
["pin", "pin create"],
589+
["simulators", "simulators repair"],
590+
["service", "service start"],
591+
["doctor", "doctor --json"],
592+
];
593+
594+
for (const [group, expectedCommand] of advertised) {
595+
const human = spawnCli(fixture, group, "--help");
596+
assert.equal(human.status, 0, human.stderr || human.stdout);
597+
assert.notEqual(human.stdout.trimStart()[0], "{");
598+
assert.match(human.stdout, /Usage:/);
599+
assert.match(human.stdout, new RegExp(expectedCommand.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
600+
601+
const machine = runCli(fixture, group, "--help", "--json");
602+
assert.equal(machine.status, 0, machine.stderr);
603+
assert.equal(machine.json.ok, true);
604+
assert.equal(machine.json.group, group);
605+
assert.ok(machine.json.commands.some((command) => command.includes(expectedCommand)));
606+
}
607+
});
608+
609+
test("doctor human formatter names unhealthy aliases and next commands", () => {
610+
const text = format({
611+
hostConfigPath: "/tmp/host-config.json",
612+
issues: [{
613+
alias: "ui-1",
614+
health: "repair-needed",
615+
reasonCode: "alias-unhealthy",
616+
}],
617+
ok: false,
618+
stateRoot: "/tmp/state",
619+
});
620+
621+
assert.notEqual(text.trimStart()[0], "{");
622+
assert.match(text, /Status: needs attention/);
623+
assert.match(text, /Alias ui-1: repair-needed/);
624+
assert.match(text, /simbroker simulators repair --alias ui-1/);
625+
});
626+
576627
test("doctor human output reports missing registry and next commands", () => {
577628
const fixture = makeFixture();
578629

spec/architecture.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ Does not own:
8484
Owns:
8585

8686
- CLI surface for humans, agents, and shell scripts
87-
- stable JSON output
87+
- stable JSON output for mutating and status commands; `help` and `doctor`
88+
default to human-readable text and keep the same payload behind `--json`
8889
- repo project discovery and validation
8990
- repo project scaffolding through `project init`
9091
- explicit removal of inactive local project registrations through `project forget`

spec/global-simulator-broker.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
Related: `spec/README.md`, `spec/architecture.md`, `spec/implementation-plan.md`, `spec/build-and-test.md`, `spec/project-structure.md`, `spec/tasks/public-safe-on-demand-simulator-lifecycle.md`, `references/README.md`
33

44
> **Document ID:** `GSB-001`
5-
> **Version:** `0.14.0`
6-
> **Last Updated:** `2026-08-10`
5+
> **Version:** `0.14.1`
6+
> **Last Updated:** `2026-08-18`
77
> **Status:** `Draft`
88
> **Owner:** `spec-steward`
99
> **Implementation owners:** `spec-steward`, `ios-dev`
@@ -175,7 +175,8 @@ Current implementation slice:
175175
Must own:
176176

177177
- stable human and agent CLI
178-
- stable JSON output
178+
- stable JSON output for mutating and status commands; `help` and `doctor`
179+
default to human-readable text and keep the same payload behind `--json`
179180
- repo project discovery and validation
180181
- structured explain and denial reporting
181182
- compatibility shims for repo-owned wrappers
@@ -536,6 +537,7 @@ This repo is ready for public-source collaboration only if:
536537

537538
| Version | Date | Summary |
538539
| --- | --- | --- |
540+
| 0.14.1 | 2026-08-18 | Help and doctor default to human-readable text; `--json` keeps the stable machine payload. |
539541
| 0.14.0 | 2026-08-10 | Replaced lease rotation with deterministic warm reuse, required boot-on-acquire, and added opt-in public-safe idle lifecycle, scheduler, cleanup, app, and verification contracts. |
540542
| 0.13.13 | 2026-08-10 | Added locked, explicit, idempotent cleanup for inactive local project registrations with lease/pin conflict protection and snapshot refresh. |
541543
| 0.13.12 | 2026-08-04 | Clarified startup-lock sampler timeout coverage, per-state-load stale containment budget retries, deterministic snapshot host-config fixtures, and non-remediable erase conflicts. |

0 commit comments

Comments
 (0)