Skip to content

Commit fddd559

Browse files
ralyodioclaude
andauthored
docs: generate README's command table from the schema (PRD 0006 R13) (#260)
* docs: generate README's command table from the schema (PRD 0006 R13) The README described the CLI from memory. Nothing connected its command listings to the table the dispatcher actually reads, so a renamed verb stayed documented and a new one stayed invisible — the same drift that had already happened inside `moshcode help` itself. Generated rather than checked, which R13 offers as the alternative and is the better half of it: a checker tells you the README is wrong, a generator makes it right. `moshcode help --markdown` emits the table, and it goes between markers in README.md exactly like the PRD index this repo already maintains, so the convention is one people here already know. Three tests, in both directions: - the table is regenerated and compared, so adding a verb without regenerating fails with the command that fixes it; - every command named in the table resolves in the schema; - every `moshcode <verb>` invoked in a fenced code block anywhere in the README is a real command, engine or tool. Scoped to fenced blocks because the prose also contains sentences like "moshcode warns you", and a grep that cannot tell those apart is a test nobody trusts. Verified the guard bites: adding a command to the schema without regenerating fails the suite, and the failure names `moshcode help --markdown` as the fix. 1089 tests, 877 pass, 0 fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(help): moshscript verbs, terminal manners, and completable topics (#263) The tail of PRD 0006 — R14, R15, R16. R14. `moshcode help ask` answered "no help for ask". `ask()` is as much part of the interface as `moshcode prd`, but the vocabulary lives in a registry help had never been introduced to. Verbs now carry an optional `usage` — `ai(prompt, { engine })`, `ask(...prompt)` — and help renders it with the summary and a note that this one is called from a .mosh file, not a shell. `usage` stays optional, so a verb registered by a host that has not declared one still renders as `name(…)`. cliVerb() derives its own signature rather than repeating it twenty times: every CLI verb forwards its arguments to `moshcode <name>`, so adding a line to the vocabulary documents it. R15 turned out to be already true and is now nailed down. src/ui.mjs gates colour on `NO_COLOR == null && process.stdout.isTTY`, so both the CLI and the pit already emit plain text to a pipe. Three tests keep it that way, including one for the pit — which needs spawnSync rather than the execFile helper the rest of the file uses, because the pit reads stdin and execFile has no way to supply it. Nothing paginates. R16. `moshcode help <tab>` now completes topics, in all four shells. The list is what help itself accepts — commands, engines, tools and moshscript verbs — so tab-completion and help cannot disagree about what is answerable. A test asserts exactly that: every offered topic resolves. 1099 tests, 887 pass, 0 fail. That closes PRD 0006. R1–R16 are implemented across #257, #258, #260 and this. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a3beaa1 commit fddd559

8 files changed

Lines changed: 334 additions & 5 deletions

File tree

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,44 @@ curl -fsSL https://moshcoding.com/install.sh | sh
1414
Zero-dependency ESM — all it needs is Node.js 18+. Later: `… | sh -s -- update`
1515
to upgrade, `… | sh -s -- remove` to uninstall.
1616

17+
## Commands
18+
19+
`moshcode help <command>` drills into any of these — flags, examples and all.
20+
`moshcode help --json` is the same thing for a machine.
21+
22+
This table is generated from the command table the CLI itself dispatches from
23+
(`moshcode help --markdown`), so it cannot describe a verb that does not exist
24+
or miss one that does. A test fails the build when it drifts.
25+
26+
<!-- COMMANDS:START -->
27+
| command | group | what it does |
28+
|---|---|---|
29+
| `moshcode agents` | engines | list engines or launch one autonomously |
30+
| `moshcode start` | engines | launch an engine with its native defaults |
31+
| `moshcode install` | engines | install an engine or workflow tool |
32+
| `moshcode uninstall` <br>`remove` | engines | take an engine or workflow tool off this machine |
33+
| `moshcode upgrade` <br>`update` | engines | update moshcode, engines, or tools |
34+
| `moshcode mcp` | extend | register and inspect MCP servers |
35+
| `moshcode skill` <br>`skills` | extend | install and inspect agent skills |
36+
| `moshcode prd` | script | publish or list product requirement documents |
37+
| `moshcode login` | account | authenticate with app.moshcode.sh |
38+
| `moshcode whoami` | account | show the logged-in account |
39+
| `moshcode logout` | account | clear the logged-in account |
40+
| `moshcode console` | account | serve or connect to the browser terminal |
41+
| `moshcode dns` | hosting | resolve Moshpit names on this machine |
42+
| `moshcode doh` | hosting | run the DNS-over-HTTPS resolver |
43+
| `moshcode site` <br>`serve` | hosting | install web-server config for a Moshpit name |
44+
| `moshcode template` <br>`templates` | hosting | scaffold a stack for a Moshpit-hosted service |
45+
| `moshcode pwd` <br>`where` | system | show the current directory and git context |
46+
| `moshcode engines` | engines | list engines and installation status |
47+
| `moshcode tools` | tools | list workflow tools and installation status |
48+
| `moshcode commands` | script | list built-in moshscript commands |
49+
| `moshcode completion` | extend | print a shell completion script |
50+
| `moshcode run` | script | run a moshscript |
51+
| `moshcode help` <br>`--help` `-h` | system | show command help |
52+
| `moshcode version` <br>`--version` `-v` | system | show the installed version |
53+
<!-- COMMANDS:END -->
54+
1755
## Engines
1856

1957
```sh

bin/moshcode.mjs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { completionScript } from "../src/completion.mjs";
3232
import { CORE_CLI_COMMAND_NAMES } from "../src/cli-schema.mjs";
3333
import {
3434
findCommand, findVerb, helpModel, renderAll, renderCommand, renderOverview,
35-
suggest, wantsHelp, withoutHelp,
35+
renderMarkdown, renderScriptVerb, suggest, wantsHelp, withoutHelp,
3636
} from "../src/help.mjs";
3737
import { moshcodeVersion } from "../src/ui.mjs";
3838

@@ -179,6 +179,13 @@ function handledHelp(cmd, rest) {
179179
const topLevel = ["help", "--help", "-h"].includes(cmd);
180180

181181
if (topLevel) {
182+
// `--markdown` is the generator behind README.md's command table (R13). A
183+
// build-time convenience, deliberately not a docs pipeline: it emits one
184+
// table, and the test that keeps README.md honest calls the same function.
185+
if (rest.includes("--markdown")) {
186+
console.log(renderMarkdown());
187+
return true;
188+
}
182189
const args = withoutHelp(rest).filter((a) => a !== "--json" && a !== "--all");
183190
// `moshcode help --help` asks about `help` itself, and R4 says every
184191
// command answers for itself. Only the spelled-out verb: `moshcode --help`
@@ -209,7 +216,16 @@ function handledHelp(cmd, rest) {
209216
console.log(`see also: moshcode help tools · moshcode help install`);
210217
return true;
211218
}
212-
const near = suggest(args[0], [...Object.keys(ENGINES), ...Object.keys(TOOLS)]);
219+
// A moshscript verb is a fair thing to ask about — `ask()` is as much
220+
// part of the interface as `moshcode prd` (R14).
221+
const verbHelp = renderScriptVerb(moshVocabulary().get(args[0]));
222+
if (verbHelp) {
223+
console.log(verbHelp);
224+
return true;
225+
}
226+
const near = suggest(args[0], [
227+
...Object.keys(ENGINES), ...Object.keys(TOOLS), ...moshVocabulary().names(),
228+
]);
213229
console.error(`✗ no help for ${JSON.stringify(args[0])}${near ? ` — did you mean ${near}?` : ""}`);
214230
console.error(" moshcode help list commands");
215231
process.exitCode = 1;

src/cli-schema.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ export const CORE_CLI_COMMANDS = [
309309
flags: [
310310
["--all", "render every command instead of the overview", ""],
311311
["--json", "emit the help model as JSON", ""],
312+
["--markdown", "emit the command table for README.md", ""],
312313
],
313314
examples: [
314315
["moshcode help mcp install", "a sub-verb"],

src/cli.mjs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,17 @@ export function runMoshcode(cmd, args, ctx) {
6060

6161
/** A vocabulary command mapping `name(...args)` → `moshcode name ...args`. */
6262
export function cliVerb(name, summary) {
63-
return { name, summary, run: (ctx, ...args) => runMoshcode(name, args, ctx) };
63+
// `usage` is the call signature `moshcode help <verb>` renders (PRD 0006
64+
// R14). Every CLI verb has the same one — it forwards its arguments to
65+
// `moshcode <name>` — so deriving it here means a verb added below is
66+
// documented by the act of adding it.
67+
return {
68+
name,
69+
summary,
70+
usage: `${name}(...args)`,
71+
detail: `runs \`moshcode ${name} ...args\` and returns { ok, code }`,
72+
run: (ctx, ...args) => runMoshcode(name, args, ctx),
73+
};
6474
}
6575

6676
/**
@@ -102,5 +112,7 @@ export function runAi(ctx, prompt, opts = {}) {
102112
export const aiVerb = {
103113
name: "ai",
104114
summary: "run a coding engine on a prompt and return its output (shortcut)",
115+
usage: "ai(prompt, { engine })",
116+
detail: "blocking; returns the engine's stdout as a string. engine defaults to the first installed one. needs await",
105117
run: (ctx, prompt, opts) => runAi(ctx, prompt, opts),
106118
};

src/commands.mjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ const COMMANDS = [
6767
{
6868
name: "code",
6969
summary: "compile features (no bugs)",
70+
usage: "code()",
71+
detail: "narrates a build step; takes no arguments",
7072
run(ctx, ...args) {
7173
expectNoArgs("code", args);
7274
ctx.out(" ⌨ code() → compiling features (no bugs)…");
@@ -75,6 +77,8 @@ const COMMANDS = [
7577
{
7678
name: "mosh",
7779
summary: "open the pit + blast the moshcoding playlist",
80+
usage: "mosh()",
81+
detail: "opens the pit and starts the playlist",
7882
run(ctx, ...args) {
7983
expectNoArgs("mosh", args);
8084
ctx.out(" 🤘 mosh() → opening the pit");
@@ -91,6 +95,8 @@ const COMMANDS = [
9195
{
9296
name: "notify",
9397
summary: "ping the operator via app.moshcode.sh (email/SMS/Slack/Telegram/push)",
98+
usage: "notify(...message)",
99+
detail: "returns { id, url } — fire and forget, no reply awaited",
94100
// Fire-and-forget. Posts the approval to the app, which fans it out to the
95101
// operator's channels. Returns { id, url } so a script can hand the link off.
96102
async run(ctx, ...args) {
@@ -107,6 +113,8 @@ const COMMANDS = [
107113
{
108114
name: "ask",
109115
summary: "notify + BLOCK until the human approves/instructs at app.moshcode.sh",
116+
usage: "ask(...prompt)",
117+
detail: "BLOCKS until a human answers at app.moshcode.sh; returns their reply or null. needs await",
110118
// The human-in-the-loop gate. Posts the approval to the app, then waits for
111119
// the operator to open app.moshcode.sh/approve/:id, read the context, and
112120
// submit. Resolves with their instructions (or null). Requires `await`.
@@ -130,6 +138,8 @@ const COMMANDS = [
130138
{
131139
name: "repeat",
132140
summary: "back to the top of the loop",
141+
usage: "repeat()",
142+
detail: "jumps back to the top of the loop",
133143
run(ctx, ...args) {
134144
expectNoArgs("repeat", args);
135145
ctx.out(" ↻ repeat() → back to the top");
@@ -138,13 +148,17 @@ const COMMANDS = [
138148
{
139149
name: "say",
140150
summary: "print a line",
151+
usage: "say(...parts)",
152+
detail: "prints one line",
141153
run(ctx, ...args) {
142154
ctx.out(` 💬 ${args.join(" ")}`);
143155
},
144156
},
145157
{
146158
name: "sleep",
147159
summary: "pause for N milliseconds (blocking)",
160+
usage: "sleep(ms)",
161+
detail: "blocks for ms milliseconds",
148162
// Synchronous/blocking so it pauses inline in the simple no-`await` style:
149163
// `while (alive) { work(); sleep(1000); }` actually waits each iteration.
150164
run(ctx, ...args) {
@@ -163,6 +177,8 @@ const COMMANDS = [
163177
{
164178
name: "stop",
165179
summary: "end the loop (alive = false)",
180+
usage: "stop()",
181+
detail: "ends the loop (alive = false)",
166182
run(ctx, ...args) {
167183
expectNoArgs("stop", args);
168184
ctx.stop();
@@ -173,6 +189,8 @@ const COMMANDS = [
173189
{
174190
name: "shell",
175191
summary: "run a shell command (blocking, cmd.exe on Windows or $SHELL -c elsewhere)",
192+
usage: "shell(cmd)",
193+
detail: "runs cmd in $SHELL; returns { ok, code, signal }",
176194
// The moshscript system verb for arbitrary shell commands. Blocking
177195
// (spawnSync + inherited stdio) so it runs inline in the no-`await` style,
178196
// and the child owns the terminal for interactive commands. Returns

src/completion.mjs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from "./cli-schema.mjs";
77
import { ENGINES, ENGINE_ALIASES } from "./engines.mjs";
88
import { TOOLS } from "./tools.mjs";
9+
import { moshVocabulary } from "./commands.mjs";
910

1011
export const COMPLETION_SHELLS = ["bash", "zsh", "fish", "powershell"];
1112

@@ -54,6 +55,16 @@ export function completionModel() {
5455
skills: uniqueEntries(SKILL_VERBS),
5556
skillSources: uniqueEntries(SKILL_VERBS.filter(({ acceptsSource }) => acceptsSource)),
5657
shells: COMPLETION_SHELLS.map((name) => entry(name, `generate ${name} completion`)),
58+
// `moshcode help <topic>` accepts anything help can answer for: a command,
59+
// an engine, a tool, or a moshscript verb (PRD 0006 R16). Offering the same
60+
// set here is what keeps tab-completion and help one discoverability
61+
// surface rather than two that disagree.
62+
helpTopics: uniqueEntries([
63+
...CORE_CLI_COMMANDS.filter(({ name }) => !name.startsWith("-")),
64+
...engines,
65+
...tools,
66+
...moshVocabulary().all().map((c) => entry(c.name, c.summary || "moshscript verb")),
67+
]),
5768
};
5869
}
5970

@@ -110,6 +121,7 @@ ${powershellEntries("MoshcodeCompletionMcpServerSpecs", model.mcpServerSpecs)}
110121
${powershellEntries("MoshcodeCompletionSkills", model.skills)}
111122
${powershellEntries("MoshcodeCompletionSkillSources", model.skillSources)}
112123
${powershellEntries("MoshcodeCompletionShells", model.shells)}
124+
${powershellEntries("MoshcodeCompletionHelpTopics", model.helpTopics)}
113125
${powershellEntries("MoshcodeCompletionJson", optionEntries("--json", "print JSON"))}
114126
${powershellEntries("MoshcodeCompletionLogin", optionEntries("--browser -b --device -d", "authentication mode"))}
115127
${powershellEntries("MoshcodeCompletionRun", optionEntries("--dry-run --max -n", "run option"))}
@@ -154,6 +166,9 @@ Register-ArgumentCompleter -Native -CommandName moshcode -ScriptBlock {
154166
{ $_ -in @('upgrade', 'update') } {
155167
$choices = $script:MoshcodeCompletionUpgrade
156168
}
169+
'help' {
170+
if ($argumentIndex -eq 2) { $choices = $script:MoshcodeCompletionHelpTopics }
171+
}
157172
'completion' {
158173
if ($argumentIndex -eq 2) { $choices = $script:MoshcodeCompletionShells }
159174
}
@@ -244,6 +259,9 @@ _moshcode_completion() {
244259
completion)
245260
(( COMP_CWORD == 2 )) && choices="${names(model.shells)}"
246261
;;
262+
help)
263+
(( COMP_CWORD == 2 )) && choices="${names(model.helpTopics)}"
264+
;;
247265
mcp)
248266
if (( COMP_CWORD == 2 )); then
249267
choices="${names(model.mcp)}"
@@ -350,6 +368,12 @@ _moshcode() {
350368
_describe "shell" choices
351369
fi
352370
;;
371+
help)
372+
if (( CURRENT == 3 )); then
373+
choices=(${zshValues(model.helpTopics)})
374+
_describe "help topic" choices
375+
fi
376+
;;
353377
mcp)
354378
if (( CURRENT == 3 )); then
355379
choices=(${zshValues(model.mcp)})
@@ -458,6 +482,7 @@ ${fishEntries(atSecondToken("install"), model.install)}
458482
${fishEntries(atSecondToken("uninstall remove"), model.uninstall)}
459483
${fishEntries("__moshcode_command_is upgrade update", model.upgrade)}
460484
${fishEntries(atSecondToken("completion"), model.shells)}
485+
${fishEntries(atSecondToken("help"), model.helpTopics)}
461486
${fishEntries(atSecondToken("mcp"), model.mcp)}
462487
${fishEntries(atSecondToken("skill skills"), model.skills)}
463488
complete -c moshcode -n '__moshcode_nested_is mcp list' -l json -d 'print JSON'

src/help.mjs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,72 @@ export function helpModel({ engines = [], tools = [], version = "" } = {}) {
308308
* to stderr and exit 1; `renderCommand` on stdout is the same text for the
309309
* person who asked politely.
310310
*/
311+
/* ------------------------------------------------------- moshscript verbs */
312+
313+
/**
314+
* A moshscript verb, rendered from the registry (PRD 0006 R14).
315+
*
316+
* `moshcode help ask` is a fair question — `ask()` is as much part of the
317+
* interface as `moshcode prd` — and it used to answer "no help for ask",
318+
* because the vocabulary lives in a registry that help had never been
319+
* introduced to.
320+
*
321+
* `usage` is optional on a command object, so a verb registered by a host that
322+
* has not declared one still renders: the signature falls back to `name(…)` and
323+
* the summary carries the meaning.
324+
*/
325+
export function renderScriptVerb(command) {
326+
if (!command) return null;
327+
const out = [`${command.usage || `${command.name}(…)`}${command.summary || "a moshscript verb"}`];
328+
if (command.detail) out.push("", wrap(command.detail, 0));
329+
out.push("", "a moshscript verb — call it from a .mosh file, not from the shell.");
330+
out.push("", "see also: moshcode help run · moshcode help commands");
331+
return out.join("\n");
332+
}
333+
334+
/* ---------------------------------------------------------------- markdown */
335+
336+
export const README_START = "<!-- COMMANDS:START -->";
337+
export const README_END = "<!-- COMMANDS:END -->";
338+
339+
/**
340+
* The command table as markdown, for README.md (PRD 0006 R13).
341+
*
342+
* Generated rather than checked against a hand-written list, and the difference
343+
* matters: a checker tells you the README is wrong, a generator makes it right.
344+
* Same shape as the PRD index this repo already keeps between markers, so the
345+
* convention is one people here already know.
346+
*
347+
* Aliases ride along in their target's row instead of getting rows of their
348+
* own — six extra lines saying "alias for X" is how a table stops being read.
349+
*/
350+
export function renderMarkdown() {
351+
const rows = primaryCommands().map((command) => {
352+
const aliases = aliasesFor(command.name);
353+
const name = `\`moshcode ${command.name}\`${aliases.length ? ` <br>${aliases.map((a) => `\`${a}\``).join(" ")}` : ""}`;
354+
return `| ${name} | ${command.group} | ${command.description} |`;
355+
});
356+
return [
357+
"| command | group | what it does |",
358+
"|---|---|---|",
359+
...rows,
360+
].join("\n");
361+
}
362+
363+
/**
364+
* Put the generated table between the markers in `markdown`.
365+
*
366+
* Returns the document unchanged when it carries no markers, so this can be
367+
* pointed at a file that has not opted in without mangling it.
368+
*/
369+
export function withCommandTable(markdown) {
370+
const text = String(markdown);
371+
const from = text.indexOf(README_START);
372+
const to = text.indexOf(README_END);
373+
if (from < 0 || to < 0 || to < from) return text;
374+
return `${text.slice(0, from + README_START.length)}\n${renderMarkdown()}\n${text.slice(to)}`;
375+
}
376+
311377
/* ------------------------------------------------------------------ the pit */
312378

313379
/** Resolve a pit command, following its aliases. */

0 commit comments

Comments
 (0)