@wirecat/cli-core 0.344.0 → 0.345.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/cli/bot/command.js +3 -1
  3. package/dist/cli/command-contract.d.ts +13 -0
  4. package/dist/cli/command-contract.js +1 -1
  5. package/dist/cli/context.d.ts +2 -0
  6. package/dist/cli/guide.d.ts +52 -0
  7. package/dist/cli/guide.js +307 -0
  8. package/dist/cli/help/command-help.d.ts +30 -0
  9. package/dist/cli/help/command-help.js +218 -0
  10. package/dist/cli/help/commands-text.d.ts +2 -0
  11. package/dist/cli/help/commands-text.js +1 -1
  12. package/dist/cli/help/first-screen.d.ts +4 -3
  13. package/dist/cli/help/first-screen.js +6 -3
  14. package/dist/cli/help/sections.js +1 -0
  15. package/dist/cli/index.d.ts +2 -0
  16. package/dist/cli/index.js +2 -0
  17. package/dist/cli/messenger/conversations-command.js +4 -1
  18. package/dist/cli/messenger/inbox.js +1 -1
  19. package/dist/cli/messenger/observed.js +15 -9
  20. package/dist/cli/messenger/private-people-command.js +10 -1
  21. package/dist/cli/messenger/review.js +1 -1
  22. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  23. package/dist/cli/messenger/store-maintenance-command.js +8 -5
  24. package/dist/cli/profile.js +1 -1
  25. package/dist/cli/program.js +2 -2
  26. package/dist/meetings/mcp/index.d.ts +15 -15
  27. package/dist/services/index.d.ts +3 -0
  28. package/dist/services/index.js +1 -0
  29. package/dist/services/project-gathering-request.d.ts +120 -0
  30. package/dist/services/project-gathering-request.js +217 -0
  31. package/dist/services/project-gathering.d.ts +89 -0
  32. package/dist/services/project-gathering.js +202 -0
  33. package/package.json +5 -4
  34. package/parity.json +2 -2
package/CHANGELOG.md CHANGED
@@ -7,6 +7,50 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.345.0 — 18.10.2026
11
+
12
+ ### Added
13
+
14
+ - **`gatherProjectEvidence` collects cited evidence for one project** from explicitly selected stored messages,
15
+ mail and imported documents: the project name, aliases, keywords and people's names are alternative search
16
+ lanes, the window defaults to 60 days and is read in 90-day chunks, every invocation has its own read, byte,
17
+ time and item budgets, and an opaque reference-only checkpoint resumes the run with its window frozen. An
18
+ optional host fetch port runs first; a failed fetch or source is reported as coverage, never as absence. It
19
+ writes nothing. See `docs/dev/PROJECT-GATHERING.md`.
20
+ - **`<tool> guide [name]`: scenarios, the commands for a common task in order** (`guideCommand(app, { directory,
21
+ worksWith?, profile? })` from `@wirecat/cli-core/cli`). The list names each scenario and, under "Works with",
22
+ the ones another tool holds (`memo guide person-brief`); one scenario prints its goal, each step as a command and
23
+ why, and what to look at; `--json` gives the same as data. The source is one Markdown file per scenario with
24
+ front matter (`title`, `goal`, `steps: [{ run, why }]`, `look`) in a directory the tool ships and names.
25
+ `guide` takes no profile and is not an MCP tool. `await checkGuide(() => program, { packageRoot })` is for the
26
+ tool's CI: each step's command path exists and its options and arguments parse, with no account and no network,
27
+ and the directory is in the package's `files`. The first screen's "Next" names `guide` once a tool mounts it. The
28
+ peer range takes cli-base 0.46 too (`>=0.45.0 <0.47.0`): `cli-dev lint` knows `guide` from 0.46.0 on.
29
+ - **The run record of a one-shot command's passive listen counts what it received**: the `adapter.listen`
30
+ response carries `counts` by event (`message`, `edit`, …), as `messages.watch` does, so a run shows how many
31
+ messages a store-history messenger handed on and saved during the command.
32
+ - **Every leaf command's `--help` ends with Returns, Next usually and Example**, worked out from its definition when
33
+ the help is shown. Returns names the declared output entity and its first fields, or repeats the description
34
+ when there is none. Next usually lists up to three commands whose arguments take what this one returns (a
35
+ message, a chat, a person, a task…), reads first; `nextUsually(command, { pin, drop })` overrides them. Example
36
+ is the definition's own `examples`, or one built from the usage, a sample value per argument (`Team`, `1234`,
37
+ `"invoice"`) and the mandatory options. Examples leave out the profile, as the first screen does; the Usage line
38
+ shows `<profile>`.
39
+ `commandHelpMarkdown(root, path)` gives the same three lines for `docs/commands.md`, and
40
+ `exampleReport(program)` lists the commands still on a generated example.
41
+ - **`contacts alias set|rm`, `contacts notes list|show|add|edit|remove`, `conversations consents list|revoke` and
42
+ `bot sends list` have descriptions**, in `--help`, `commands` and the commands document.
43
+
44
+ ### Fixed
45
+
46
+ - **`inbox` and `review` declare their output**: they named the view `message.summary`, so the entity looked
47
+ up was `message.message.summary`, which does not exist. `commands schema inbox` now gives the message summary's
48
+ fields instead of open items, and `--help` ends with "Returns: message summary — …". `--json=<fields>` is now
49
+ checked against those fields, so a name they do not have is refused with the list instead of giving empty items.
50
+ The JSON they print is unchanged.
51
+ - **`--brief` and `--full` no longer mention mail in a messenger's help.** They said "found messages and mail"; tg,
52
+ max and wa have no mail. Consumers' help snapshots change on bump.
53
+
10
54
  ## 0.344.0 — 18.10.2026
11
55
 
12
56
  ### Changed — may break callers
@@ -159,7 +159,9 @@ const recipientsCommand = (bot) => {
159
159
  };
160
160
  const sendsCommand = (bot) => new Command("sends")
161
161
  .description("what this bot sent, edited and deleted from this machine — ids and outcomes, never text")
162
- .addCommand(annotate(new Command("list"), { effects: LOCAL_READ }).action(async function () {
162
+ .addCommand(annotate(new Command("list"), { effects: LOCAL_READ })
163
+ .description("every send, edit and delete this bot made from this machine, with its outcome")
164
+ .action(async function () {
163
165
  const context = botContext(this, bot);
164
166
  await context.run(async () => renderAnswer(this, context, wholeList("bot sends list", context.journal().entries().map(snakeKeys))));
165
167
  }));
@@ -1,6 +1,19 @@
1
1
  import type { Command } from "commander";
2
2
  export type JsonSchema = Record<string, unknown>;
3
3
  export declare const SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
4
+ /**
5
+ * What `--json` prints, from the entity the command declares (`annotate(…, { output })`): a list or one thing of it.
6
+ * A command that declares none answers the same envelopes with items of any shape; `--jsonl` streams the items.
7
+ */
8
+ export declare const outputOf: (command: Command) => {
9
+ schema: {
10
+ [x: string]: unknown;
11
+ };
12
+ item: {
13
+ [x: string]: unknown;
14
+ };
15
+ coverage: string;
16
+ };
4
17
  export declare const commandPathOf: (command: Command) => string[];
5
18
  export declare const findCommand: (root: Command, path: readonly string[]) => Command;
6
19
  export declare const commandContract: (command: Command) => {
@@ -9,7 +9,7 @@ export const SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
9
9
  * What `--json` prints, from the entity the command declares (`annotate(…, { output })`): a list or one thing of it.
10
10
  * A command that declares none answers the same envelopes with items of any shape; `--jsonl` streams the items.
11
11
  */
12
- const outputOf = (command) => {
12
+ export const outputOf = (command) => {
13
13
  const output = metaOf(command).output;
14
14
  const entity = output
15
15
  ? ENTITIES[(output.view ? `${output.entity}.${output.view}` : output.entity)]
@@ -33,6 +33,8 @@ export interface BaseEnvironment {
33
33
  platform?: NodeJS.Platform;
34
34
  /** The local speech recognizer in place of the downloaded model; tests hand one in. */
35
35
  recognizer?: OpenRecognizer;
36
+ /** How long `store restore` and `store reset` wait on another connection's write, 1 s unless a test hands one in. */
37
+ storeLockWaitMs?: number;
36
38
  /** Where raw bytes go when stdout is not this process's own — an MCP session relayed by serve. */
37
39
  stdout?: NodeJS.WritableStream;
38
40
  /** Where a prompt and what is typed at it go, in place of the process's stderr; tests hand one in. */
@@ -0,0 +1,52 @@
1
+ import { Command } from "commander";
2
+ import type { AppIdentity } from "../ports/app.js";
3
+ export interface GuideStep {
4
+ /** The command as a person types it: the tool's name first, no profile — `tg review --since 1d`. */
5
+ run: string;
6
+ why: string;
7
+ }
8
+ export interface Scenario {
9
+ name: string;
10
+ title: string;
11
+ goal: string;
12
+ steps: GuideStep[];
13
+ look: string[];
14
+ }
15
+ /** A scenario another tool holds — memo's cross-tool ones — listed by reference under "Works with". */
16
+ export interface GuideReference {
17
+ tool: string;
18
+ name: string;
19
+ title: string;
20
+ }
21
+ export interface GuideOptions {
22
+ /** Absolute path of the tool's scenario files: `fileURLToPath(new URL("../scenarios", import.meta.url))`. */
23
+ directory: string;
24
+ worksWith?: readonly GuideReference[];
25
+ /** `false` for a tool whose commands take no leading profile. */
26
+ profile?: boolean;
27
+ }
28
+ /** One scenario file, or what is wrong with it. The Markdown body is for the site's page; the CLI reads none of it. */
29
+ export declare const parseScenario: (file: string, content: string) => Promise<{
30
+ scenario?: Scenario;
31
+ problems: string[];
32
+ }>;
33
+ export declare const loadScenarios: (directory: string) => Promise<{
34
+ scenarios: Scenario[];
35
+ problems: string[];
36
+ }>;
37
+ /**
38
+ * `<tool> guide` lists the tool's scenarios and the ones it works with; `<tool> guide <name>` prints one: the goal,
39
+ * each step as a command and why, and what to look at. The files are read when the command runs, never when the
40
+ * program is built. `checkGuide` validates them in the tool's CI.
41
+ */
42
+ export declare const guideCommand: (app: Pick<AppIdentity, "command">, options: GuideOptions) => Command;
43
+ /** What is wrong with one step on this program: the path, each option and its value, the arguments. Never runs it. */
44
+ export declare const checkStep: (program: Command, run: string) => string[];
45
+ /**
46
+ * For a tool's CI: every scenario file parses, and each step's command exists and its options and arguments parse
47
+ * on a program built fresh per step — no account, no network, no action runs. With `packageRoot`, the scenario
48
+ * directory must also be in the package's `files`, so it ships. Returns the problems; empty means sound.
49
+ */
50
+ export declare const checkGuide: (build: () => Command, { packageRoot }?: {
51
+ packageRoot?: string;
52
+ }) => Promise<string[]>;
@@ -0,0 +1,307 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { basename, join, relative, resolve, sep } from "node:path";
3
+ import { CliError } from "@wirecat/cli-base";
4
+ import { annotate } from "@wirecat/cli-base/commands";
5
+ import { Command } from "commander";
6
+ import { environmentOf, outputFor } from "./context.js";
7
+ import { NO_EFFECTS } from "./effects.js";
8
+ import { itemOf, listOf, renderAnswer } from "./envelope.js";
9
+ import { PROFILE_FREE } from "./profile.js";
10
+ const FRONT_MATTER = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
11
+ const FIELDS = new Set(["title", "goal", "steps", "look"]);
12
+ const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
13
+ const text = (value) => typeof value === "string" && value.trim().length > 0;
14
+ /** One scenario file, or what is wrong with it. The Markdown body is for the site's page; the CLI reads none of it. */
15
+ export const parseScenario = async (file, content) => {
16
+ const name = basename(file, ".md");
17
+ const at = (problem) => `${basename(file)}: ${problem}`;
18
+ if (!NAME.test(name))
19
+ return { problems: [at("the file name is the scenario's name: lower case words and dashes")] };
20
+ const match = FRONT_MATTER.exec(content);
21
+ if (!match)
22
+ return { problems: [at("needs front matter between --- lines")] };
23
+ // Loaded here, not at the top: every tool start loads this module, and only `guide` reads YAML.
24
+ const { parseDocument } = await import("yaml");
25
+ const yaml = parseDocument(match[1] ?? "", { uniqueKeys: true });
26
+ if (yaml.errors.length > 0)
27
+ return { problems: [at("front matter is not valid YAML")] };
28
+ const data = yaml.toJS({ maxAliasCount: 0 });
29
+ if (!data || typeof data !== "object" || Array.isArray(data))
30
+ return { problems: [at("front matter is not a map")] };
31
+ const fields = data;
32
+ const problems = Object.keys(fields)
33
+ .filter((key) => !FIELDS.has(key))
34
+ .map((key) => at(`unknown field ${key}; the fields are ${[...FIELDS].join(", ")}`));
35
+ if (!text(fields.title))
36
+ problems.push(at("title is a line of text"));
37
+ if (!text(fields.goal))
38
+ problems.push(at("goal is a line of text"));
39
+ const steps = Array.isArray(fields.steps) ? fields.steps : [];
40
+ if (steps.length === 0)
41
+ problems.push(at("steps is a list of { run, why }"));
42
+ steps.forEach((step, index) => {
43
+ const one = (step ?? {});
44
+ if (!text(one.run) || !text(one.why))
45
+ problems.push(at(`step ${index + 1} needs run and why`));
46
+ });
47
+ const look = typeof fields.look === "string" ? [fields.look] : fields.look;
48
+ if (!Array.isArray(look) || look.length === 0 || !look.every(text))
49
+ problems.push(at("look is what to look at in the output: a line or a list of lines"));
50
+ if (problems.length > 0)
51
+ return { problems };
52
+ return {
53
+ scenario: {
54
+ name,
55
+ title: String(fields.title).trim(),
56
+ goal: String(fields.goal).trim(),
57
+ steps: steps.map(({ run, why }) => ({ run: run.trim(), why: why.trim() })),
58
+ look: look.map((line) => line.trim()),
59
+ },
60
+ problems: [],
61
+ };
62
+ };
63
+ export const loadScenarios = async (directory) => {
64
+ let files;
65
+ try {
66
+ files = readdirSync(directory).filter((file) => file.endsWith(".md") && file.toLowerCase() !== "readme.md");
67
+ }
68
+ catch {
69
+ return { scenarios: [], problems: [`${directory}: no such directory`] };
70
+ }
71
+ const parsed = await Promise.all(files.sort().map((file) => parseScenario(file, readFileSync(join(directory, file), "utf8"))));
72
+ return {
73
+ scenarios: parsed.flatMap(({ scenario }) => (scenario ? [scenario] : [])),
74
+ problems: parsed.flatMap(({ problems }) => problems),
75
+ };
76
+ };
77
+ const guides = new WeakMap();
78
+ const rows = (pairs) => {
79
+ const width = pairs.reduce((most, [left]) => Math.max(most, left.length), 0) + 2;
80
+ return pairs.map(([left, right]) => ` ${left.padEnd(width)}${right}`);
81
+ };
82
+ const listText = (cli, scenarios, worksWith) => [
83
+ scenarios.length > 0 ? `Scenarios — ${cli} guide <name> for the steps:` : `${cli} has no scenarios yet.`,
84
+ ...rows(scenarios.map((one) => [one.name, one.title])),
85
+ ...(worksWith.length > 0
86
+ ? ["", "Works with:", ...rows(worksWith.map((one) => [`${one.tool} guide ${one.name}`, one.title]))]
87
+ : []),
88
+ ].join("\n");
89
+ const needsProfile = (cli, run) => {
90
+ const [tool, first] = words(run);
91
+ return tool === cli && first !== undefined && !first.startsWith("-") && !PROFILE_FREE.has(first);
92
+ };
93
+ const scenarioText = (cli, scenario, profile) => [
94
+ scenario.title,
95
+ "",
96
+ `Goal: ${scenario.goal}`,
97
+ "",
98
+ profile && scenario.steps.some((step) => needsProfile(cli, step.run))
99
+ ? `Steps — account commands start with your profile: ${cli} <profile> …`
100
+ : "Steps:",
101
+ ...scenario.steps.flatMap((step, index) => [` ${index + 1}. ${step.run}`, ` ${step.why}`]),
102
+ "",
103
+ "Look at:",
104
+ ...scenario.look.map((line) => ` - ${line}`),
105
+ ].join("\n");
106
+ /**
107
+ * `<tool> guide` lists the tool's scenarios and the ones it works with; `<tool> guide <name>` prints one: the goal,
108
+ * each step as a command and why, and what to look at. The files are read when the command runs, never when the
109
+ * program is built. `checkGuide` validates them in the tool's CI.
110
+ */
111
+ export const guideCommand = (app, options) => {
112
+ const command = annotate(new Command("guide"), {
113
+ effects: NO_EFFECTS,
114
+ mcp: false,
115
+ examples: [`${app.command} guide`, `${app.command} guide <name> --json`],
116
+ })
117
+ .description("scenarios: the commands for a common task, in order, and why each")
118
+ .argument("[name]", "one scenario; without it, the list")
119
+ .action(async function (name) {
120
+ const cli = app.command;
121
+ const { scenarios, problems } = await loadScenarios(options.directory);
122
+ if (problems.length > 0)
123
+ throw new CliError("configuration_error", `broken scenario files: ${problems.join("; ")}`);
124
+ const worksWith = options.worksWith ?? [];
125
+ const output = outputFor(this);
126
+ const { json, jsonl } = this.optsWithGlobals();
127
+ const asText = json === undefined && !jsonl && !environmentOf(this).agent;
128
+ if (name === undefined) {
129
+ const items = [
130
+ ...scenarios.map(({ name, title, goal }) => ({
131
+ tool: cli,
132
+ name,
133
+ title,
134
+ goal,
135
+ command: `${cli} guide ${name}`,
136
+ })),
137
+ ...worksWith.map(({ tool, name, title }) => ({ tool, name, title, command: `${tool} guide ${name}` })),
138
+ ];
139
+ if (asText)
140
+ output.streams.data(listText(cli, scenarios, worksWith));
141
+ else
142
+ renderAnswer(this, output, listOf("guide", { items, limit: Math.max(1, items.length), has_more: false }));
143
+ return;
144
+ }
145
+ const scenario = scenarios.find((one) => one.name === name);
146
+ if (!scenario) {
147
+ const elsewhere = worksWith.find((one) => one.name === name);
148
+ throw new CliError("not_found", elsewhere
149
+ ? `"${name}" is ${elsewhere.tool}'s scenario: ${elsewhere.tool} guide ${name}`
150
+ : `no scenario "${name}"; ${cli} guide lists them`, { name, available: scenarios.map((one) => one.name) });
151
+ }
152
+ if (asText)
153
+ output.streams.data(scenarioText(cli, scenario, options.profile !== false));
154
+ else
155
+ renderAnswer(this, output, itemOf(scenario));
156
+ });
157
+ guides.set(command, options);
158
+ return command;
159
+ };
160
+ /** A step's words, split as a shell would on spaces, with "…" and '…' kept together. */
161
+ const words = (line) => {
162
+ const found = [];
163
+ let word;
164
+ let quote;
165
+ for (const char of line) {
166
+ if (quote) {
167
+ if (char === quote)
168
+ quote = undefined;
169
+ else
170
+ word = (word ?? "") + char;
171
+ }
172
+ else if (char === '"' || char === "'") {
173
+ quote = char;
174
+ word ??= "";
175
+ }
176
+ else if (/\s/.test(char)) {
177
+ if (word !== undefined)
178
+ found.push(word);
179
+ word = undefined;
180
+ }
181
+ else
182
+ word = (word ?? "") + char;
183
+ }
184
+ if (word !== undefined)
185
+ found.push(word);
186
+ return found;
187
+ };
188
+ const silent = (command) => command.exitOverride().configureOutput({ writeOut: () => { }, writeErr: () => { }, outputError: () => { } });
189
+ const ownerOf = (chain, flag) => {
190
+ for (const owner of chain) {
191
+ const option = owner.options.find((one) => one.long === flag || one.short === flag);
192
+ if (option)
193
+ return { owner, option };
194
+ }
195
+ return undefined;
196
+ };
197
+ /** What is wrong with one step on this program: the path, each option and its value, the arguments. Never runs it. */
198
+ export const checkStep = (program, run) => {
199
+ const [tool, ...rest] = words(run);
200
+ if (tool !== program.name())
201
+ return [`starts with "${tool ?? ""}", not ${program.name()}`];
202
+ const chain = [program];
203
+ let at = program;
204
+ let index = 0;
205
+ for (; index < rest.length; index++) {
206
+ const next = at.commands.find((one) => one.name() === rest[index] || one.aliases().includes(rest[index] ?? ""));
207
+ if (!next)
208
+ break;
209
+ chain.unshift(next);
210
+ at = next;
211
+ }
212
+ if (at === program)
213
+ return [`no command "${rest[0] ?? ""}" — steps leave out the profile`];
214
+ if (at.commands.length > 0)
215
+ return [`needs one of: ${at.commands.map((one) => one.name()).join(", ")}`];
216
+ for (const command of chain)
217
+ silent(command);
218
+ const problems = [];
219
+ const operands = [];
220
+ const left = rest.slice(index);
221
+ for (let word = 0; word < left.length; word++) {
222
+ const token = left[word];
223
+ if (token === "--") {
224
+ operands.push(...left.slice(word + 1));
225
+ break;
226
+ }
227
+ if (!token.startsWith("-") || token === "-" || /^-\d/.test(token)) {
228
+ operands.push(token);
229
+ continue;
230
+ }
231
+ const flag = token.split("=")[0];
232
+ const found = ownerOf(chain, flag);
233
+ if (!found) {
234
+ problems.push(`no option ${flag}`);
235
+ continue;
236
+ }
237
+ const tokens = [token];
238
+ const takes = found.option.required || found.option.optional;
239
+ if (takes && !token.includes("=")) {
240
+ while (left[word + 1] !== undefined && !left[word + 1].startsWith("-")) {
241
+ tokens.push(left[++word]);
242
+ if (!found.option.variadic)
243
+ break;
244
+ }
245
+ if (found.option.required && tokens.length === 1) {
246
+ problems.push(`${flag} needs a value`);
247
+ continue;
248
+ }
249
+ }
250
+ try {
251
+ found.owner.parseOptions(tokens);
252
+ }
253
+ catch (error) {
254
+ problems.push(`${flag}: ${error instanceof Error ? error.message.replace(/^error: /, "") : String(error)}`);
255
+ }
256
+ }
257
+ for (const option of at.options)
258
+ if (option.mandatory && at.getOptionValue(option.attributeName()) === undefined)
259
+ problems.push(`${option.long ?? option.flags} is required`);
260
+ const declared = at.registeredArguments;
261
+ const required = declared.filter((one) => one.required).length;
262
+ const unbounded = declared.at(-1)?.variadic === true;
263
+ if (operands.length < required)
264
+ problems.push(`needs ${declared
265
+ .filter((one) => one.required)
266
+ .map((one) => `<${one.name()}>`)
267
+ .join(" ")}`);
268
+ if (!unbounded && operands.length > declared.length)
269
+ problems.push(`takes ${declared.length} argument(s), got ${operands.length}: ${operands.join(" ")}`);
270
+ declared.forEach((argument, position) => {
271
+ const value = operands[position];
272
+ if (value !== undefined && argument.argChoices && !argument.argChoices.includes(value))
273
+ problems.push(`<${argument.name()}> is one of ${argument.argChoices.join(", ")}, not ${value}`);
274
+ });
275
+ return problems;
276
+ };
277
+ const covered = (files, path) => files.some((entry) => {
278
+ const plain = entry.replace(/(\/\*\*?)?(\/\*)?\/?$/, "").replace(/^\.\//, "");
279
+ return path === plain || path.startsWith(`${plain}/`);
280
+ });
281
+ /**
282
+ * For a tool's CI: every scenario file parses, and each step's command exists and its options and arguments parse
283
+ * on a program built fresh per step — no account, no network, no action runs. With `packageRoot`, the scenario
284
+ * directory must also be in the package's `files`, so it ships. Returns the problems; empty means sound.
285
+ */
286
+ export const checkGuide = async (build, { packageRoot } = {}) => {
287
+ const guide = build().commands.find((one) => one.name() === "guide");
288
+ const options = guide ? guides.get(guide) : undefined;
289
+ if (!options)
290
+ return ["the program has no guideCommand"];
291
+ const { scenarios, problems } = await loadScenarios(options.directory);
292
+ for (const scenario of scenarios)
293
+ scenario.steps.forEach((step, index) => {
294
+ for (const problem of checkStep(build(), step.run))
295
+ problems.push(`${scenario.name}.md step ${index + 1} (${step.run}): ${problem}`);
296
+ });
297
+ for (const reference of options.worksWith ?? [])
298
+ if (!NAME.test(reference.name) || !text(reference.tool) || !text(reference.title))
299
+ problems.push(`works with ${reference.tool} ${reference.name}: needs tool, a lower-case name and a title`);
300
+ if (packageRoot !== undefined) {
301
+ const path = relative(resolve(packageRoot), resolve(options.directory)).split(sep).join("/");
302
+ const files = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")).files;
303
+ if (files !== undefined && !covered(files, path))
304
+ problems.push(`package.json files leaves out ${path}`);
305
+ }
306
+ return problems;
307
+ };
@@ -0,0 +1,30 @@
1
+ import type { Command } from "commander";
2
+ export interface NextChoice {
3
+ /** Command paths shown first, in this order: `messages context`. */
4
+ pin?: readonly string[];
5
+ /** Command paths never shown, though their arguments take what this one returns. */
6
+ drop?: readonly string[];
7
+ }
8
+ /** Overrides the "Next usually" a command's help derives from types. Kept off `annotate`, like its section. */
9
+ export declare const nextUsually: <T extends Command>(command: T, choice: NextChoice) => T;
10
+ export interface CommandHelp {
11
+ returns: string;
12
+ next: readonly string[];
13
+ examples: readonly string[];
14
+ /** `generated` until the definition has an example of its own; the examples report lists those. */
15
+ origin: "written" | "generated";
16
+ }
17
+ /** Returns, Next usually and Example for one command, from its definition alone. */
18
+ export declare const commandHelp: (command: Command) => CommandHelp;
19
+ /** The lines `--help` prints after a command's options. */
20
+ export declare const commandHelpText: (command: Command) => string;
21
+ /** The same, as Markdown lines for `docs/commands.md`: hand it to cli-base's `commandsPage` per command. */
22
+ export declare const commandHelpMarkdown: (root: Command, path: readonly string[]) => string[];
23
+ /** Puts Returns, Next usually and Example on every leaf command's `--help`, worked out when the help is shown. */
24
+ export declare const installCommandHelp: (program: Command) => void;
25
+ /** Which leaf commands still show a generated example, so the important ones get a written one. Report only. */
26
+ export declare const exampleReport: (program: Command) => {
27
+ written: string[];
28
+ generated: string[];
29
+ text: string;
30
+ };